diff --git a/apps/web/content/blog/smirk-v0-3-0-nostr-and-self-hosting.mdx b/apps/web/content/blog/smirk-v0-3-0-nostr-and-self-hosting.mdx index ad58406..8d4b0c1 100644 --- a/apps/web/content/blog/smirk-v0-3-0-nostr-and-self-hosting.mdx +++ b/apps/web/content/blog/smirk-v0-3-0-nostr-and-self-hosting.mdx @@ -1,29 +1,27 @@ --- -title: "Smirk Wallet v0.3.0: Nostr identity, three slate transports, and a backend you can run" +title: "Smirk Wallet v0.3.0: Nostr identity, federated backends, and Grin slate transports" date: "2026-08-20" -excerpt: "v0.3.0 makes Smirk federated end to end: a NIP-07 provider with tiered signing, Grin slates that travel over Nostr instead of a shared server, and smirk-backend-core released so you can host the whole thing yourself." +excerpt: "v0.3.0 makes Smirk federated end to end: a configurable backend host, a NIP-07 provider with tiered signing, Grin slates over NIP-59 gift-wraps, and smirk-backend-core released under MIT." draft: false --- # Smirk Wallet v0.3.0 -v0.3.0 is out. It is the release that removes the last places Smirk assumed our -infrastructure: the backend host is configurable, Grin is fully non-custodial, and the -server that used to be ours is now a public repo you can run yourself. +v0.3.0 removes the remaining assumptions about our infrastructure. The backend host is +per-wallet configuration, Grin is fully non-custodial, and the server is a public repo. -The wallet ships as a Chrome MV3 and Firefox extension, plus a Tauri 2.x desktop shell for -Windows, macOS and Linux built from the same code path. Capacitor mobile is deferred to +The wallet ships as a Chrome MV3 and Firefox extension plus a Tauri 2.x desktop shell for +Windows, macOS and Linux, built from the same code path. Capacitor mobile is deferred to v0.4. ## Federation -Nothing in the wallet points at `api.smirk.cash` any more. `homeDomain` is per-wallet -configuration, and the backend it names is one instance of -[smirk-backend-core](https://github.com/Such-Software/smirk-backend-core), which is now -public under MIT. +No component points at `api.smirk.cash`. `homeDomain` is per-wallet configuration naming an +instance of [smirk-backend-core](https://github.com/Such-Software/smirk-backend-core), +released under MIT.
- + @@ -65,24 +63,43 @@ public under MIT.
-The two backends share nothing. A wallet on `a.example` can pay a wallet on `b.example` -because identity and message transport ride Nostr relays, not a shared database. +The two backends share no state. A wallet on `a.example` pays a wallet on `b.example` +because identity and message transport ride Nostr relays rather than a shared database. -## Nostr identity, and why signing is tiered +## Backend origins: onion and LAN + +A backend URL must be `https`, with three exemptions where requiring TLS would make a +legitimate instance unreachable: + +| Origin | Accepted over plain http | Reason | +|---|---|---| +| `.onion` (16 or 56 char) | yes | Onion services carry their own transport authentication and encryption, and no CA issues certificates for them | +| Private ranges, `.local` | yes | A self-hoster running a box on their own LAN | +| `localhost` | yes | Local development | + +Everything else is rejected. The onion pattern matches the label immediately before +`.onion`, so `abcdefghijklmnop.onion.example.com` is a normal public host and is refused. + +The backend's admin plane binds to loopback by default. Exposing it requires an SSH tunnel +or an onion: `TOR_ADMIN_ONION=.onion` adds that one host to the admin `Host` +allowlist, which otherwise accepts only loopback names. A request arriving with any other +`Host` gets a 403, which is the DNS-rebinding guard. A non-loopback bind additionally +requires `ADMIN_ALLOW_PUBLIC_BIND=true`. + +There is no bundled Tor daemon and no I2P support. The wallet accepts onion origins; routing +to them is the operator's Tor instance. + +## Nostr identity and tiered signing The wallet exposes a NIP-07 `window.nostr` provider, so Nostr-native sites request a public -key and get events signed without a second extension. A multi-identity vault keeps separate -keypairs apart, with an in-wallet switcher. +key and have events signed without a second extension. A multi-identity vault holds separate +keypairs with an in-wallet switcher. -The part worth explaining is what a connected site may sign *without asking again*. A Nostr -signature is a bearer credential, so that is a security decision rather than a convenience -setting. Kinds are sorted into three tiers, and the wallet handler is the enforcement point: -it computes `sessionCovered=false` for anything not session-grantable and refuses to persist -a money-tier kind into a session at all, so the rule holds even if the approval UI is -compromised. +A Nostr signature is a bearer credential, so which events a connected origin may sign +without a fresh prompt is a security decision. Kinds are classified into three tiers.
- + @@ -120,17 +137,17 @@ compromised.
-Kind 27235 is the instructive one. NIP-98 HTTP auth reads like plumbing, and it was -session-grantable in an early draft. But the backend mints a full session from a NIP-98 event -via `POST /auth/nostr`, so a grant meant for social posting would have let a site collect an -auth token for the user's own backend. It is money-tier now, with an origin guard on the -event's `u` tag. +The wallet handler is the enforcement point. It computes `sessionCovered=false` for any kind +that is not session-grantable and refuses to persist a money-tier kind into a session, so the +classification holds independently of the approval UI. -## Three transports for a Grin slate +Kind 27235 is NIP-98 HTTP auth. The backend mints a full session from a NIP-98 event via +`POST /auth/nostr`, so it is money-tier, with an origin guard on the event's `u` tag. -Grin is interactive: a payment needs slatepack legs to travel between sender and receiver. -A slate is one payload, and how it gets there is a separate choice with different trust -properties. +## Grin slate transports + +Grin is interactive: a payment requires slatepack legs to travel between sender and receiver. +The slate is one payload; the channel is a separate choice.
@@ -144,30 +161,32 @@ properties. federated default, crosses backends, relays learn neither sender, amount, nor slate npub / NIP-05 Manual clipboard - universal fallback, no network at all, age-encrypted to the recipient address + universal fallback, no network dependency, age-encrypted to the recipient address grin1… address Backend relay - same-instance shortcut only, both parties on one backend, never a requirement + same-instance only, both parties registered on one backend, never required backend user_id
-What you type in the send box picks the transport. An npub or NIP-05 handle routes over -Nostr. A bare `grin1…` address falls back to clipboard, unless a same-instance lookup can -bridge that address to its owner's npub, in which case it is upgraded to gift-wrap. The -backend relay is a convenience for two users on one host and is never on the critical path, -because there is no cross-backend relay federation and there deliberately isn't going to be. +The address in the send box selects the transport. An npub or NIP-05 handle routes over +Nostr. A bare `grin1…` address defaults to clipboard, and is upgraded to gift-wrap when a +same-instance lookup resolves that address to its owner's npub. The backend relay serves two +users on one host; there is no cross-backend relay federation. -## The backend +Counterparty keys are pinned on first use, so a later key change for the same NIP-05 handle +is surfaced rather than trusted. -Rust, Axum, PostgreSQL. It gives a wallet chain reads, fee estimation and broadcast, Nostr -identity, a price feed, a Grin slatepack mailbox and the public social-tips subsystem, -without ever holding a spend key or a seed. +## smirk-backend-core -It stores public addresses, public keys, and for Monero and Wownero the incoming view keys. -View credentials are forwarded per request to whatever scans with them and are not persisted. -Signing and broadcast happen in the wallet. +Rust, Axum, PostgreSQL. It provides chain reads, fee estimation, broadcast, Nostr identity, a +price feed, a Grin slatepack mailbox and the public social-tips subsystem, without holding a +spend key or a seed. + +Stored: public addresses, public keys, and for Monero and Wownero the incoming view keys. +View credentials are forwarded per request to the chain backends that scan with them and are +not persisted. Signing and broadcast happen in the wallet. | Chain | Source | |---|---| @@ -175,30 +194,29 @@ Signing and broadcast happen in the wallet. | Monero, Wownero | light-wallet-server, stateless view-key forwarding | | Grin | grin-lws, or grin-wallet view-only plus a node | -Chains are independently feature-flagged. One that is not configured reports -`enabled: false` from `/capabilities` instead of failing when something calls it, so a -Bitcoin-only deployment is a supported outcome rather than a half-broken one. +Chains are independently feature-flagged. An unconfigured chain reports `enabled: false` from +`/capabilities` rather than failing at call time, so a Bitcoin-only deployment is a supported +configuration. -The HTTP contract is generated from the handlers into `openapi.json`, which is also what the -wallet's client is generated from, with a CI gate that fails on drift. +The HTTP contract is generated from the handlers into `openapi.json`, which is also the +source for the wallet's generated client. A CI gate fails the build on drift. -Grin became fully non-custodial in this release. The backend no longer holds Grin balances, -outputs or transaction records; those endpoints and the server-side wallets table are gone. -Grin outputs use the standard deterministic rewind-nonce scheme, so a wallet restored from a -seed phrase rediscovers its outputs from the chain the way Bitcoin and Monero restores -already did. Outputs from earlier builds stay spendable. +Grin is non-custodial in this release. The backend no longer holds Grin balances, outputs or +transaction records; those endpoints and the server-side wallets table are removed. Grin +outputs use the standard deterministic rewind-nonce scheme, so a wallet restored from a seed +phrase rediscovers its outputs from the chain, matching how Bitcoin and Monero already +restored. Outputs created by earlier builds remain spendable. -Two supporting pieces are public for the same reason: -[grin-lws](https://github.com/Such-Software/grin-lws), a standalone Grin light wallet server, -and a small non-custodial BTC/LTC payment processor exposing a BTCPay-style invoice surface -where funds go straight to operator-controlled addresses. +Two supporting components are public: [grin-lws](https://github.com/Such-Software/grin-lws), +a standalone Grin light wallet server, and a non-custodial BTC/LTC payment processor exposing +a BTCPay-style invoice surface where funds go to operator-controlled addresses. -## Notes +## Also in this release -v0.3.0 is feature-complete and has been through a security review, but the schema and API are -young and may still move. If you self-host, run your own chain backends where you can and -read the code first. +Taproot (`bc1p…`, `ltc1p…`) recipients in Send and Swap. Cross-chain sends blocked, where a +Monero address could previously be used as a Wownero destination. Public tip keys encrypted +at rest. A custom fee rate below the relay minimum now fails rather than being silently +raised at broadcast. Connected sites visible and revocable in Settings. -Also in this release: taproot (`bc1p…`, `ltc1p…`) recipients, cross-chain sends blocked -before they can destroy funds, encrypted public tip keys at rest, a fee you sign that matches -the fee you were shown, and connected sites made visible and revocable in Settings. +v0.3.0 is feature-complete and security-reviewed. The schema and API may still change; run +your own chain backends where possible. diff --git a/apps/web/package.json b/apps/web/package.json index 742c286..02e1632 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -24,6 +24,7 @@ "react": "^19.2.4", "react-dom": "^19.2.4", "react-icons": "^5.5.0", + "remark-gfm": "^4.0.1", "resend": "^4.0.0", "zod": "^3.22.4" }, diff --git a/apps/web/src/app/blog/[slug]/page.tsx b/apps/web/src/app/blog/[slug]/page.tsx index 08559d0..88a7695 100644 --- a/apps/web/src/app/blog/[slug]/page.tsx +++ b/apps/web/src/app/blog/[slug]/page.tsx @@ -12,6 +12,7 @@ import Link from "next/link"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { MDXRemote } from "next-mdx-remote/rsc"; +import remarkGfm from "remark-gfm"; import { getAllSlugs, getPost } from "@/lib/blog"; import { JsonLd, articleLd, breadcrumbLd } from "@/components/seo/json-ld"; @@ -73,7 +74,13 @@ export default async function BlogPost({ params }: Props) {
- in literal backtick characters via + code::before / code::after. The monospace treatment already marks code as code, so the + backticks are a second, redundant marker that reads as a typo in running prose: + `homeDomain` instead of homeDomain. Turn them off across the blog. */ +.prose :where(code):not(:where([class~="not-prose"] *))::before, +.prose :where(code):not(:where([class~="not-prose"] *))::after { + content: none; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3e2d2be..899406b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,6 +78,9 @@ importers: react-icons: specifier: ^5.5.0 version: 5.5.0(react@19.2.4) + remark-gfm: + specifier: ^4.0.1 + version: 4.0.1 resend: specifier: ^4.0.0 version: 4.8.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -1165,6 +1168,10 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + eslint-config-next@15.5.9: resolution: {integrity: sha512-852JYI3NkFNzW8CqsMhI0K2CDRxTObdZ2jQJj5CtpEaOkYHn13107tHpNuD/h0WRpU4FAbCdUaxQsrfBtNK9Kw==} peerDependencies: @@ -1739,13 +1746,37 @@ packages: resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} engines: {node: '>=16'} + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + mdast-util-from-markdown@2.0.3: resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==} + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + mdast-util-mdx-expression@2.0.1: resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} @@ -1777,6 +1808,27 @@ packages: micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-extension-mdx-expression@3.0.1: resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} @@ -2180,6 +2232,9 @@ packages: rehype-recma@1.0.0: resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + remark-mdx@3.1.1: resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} @@ -2189,6 +2244,9 @@ packages: remark-rehype@11.1.2: resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + resend@4.8.0: resolution: {integrity: sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA==} engines: {node: '>=18'} @@ -3593,6 +3651,8 @@ snapshots: escape-string-regexp@4.0.0: {} + escape-string-regexp@5.0.0: {} + eslint-config-next@15.5.9(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3): dependencies: '@next/eslint-plugin-next': 15.5.9 @@ -4308,8 +4368,17 @@ snapshots: markdown-extensions@2.0.0: {} + markdown-table@3.0.4: {} + math-intrinsics@1.1.0: {} + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + mdast-util-from-markdown@2.0.3: dependencies: '@types/mdast': 4.0.4 @@ -4327,6 +4396,63 @@ snapshots: transitivePeerDependencies: - supports-color + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.3 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.3 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 @@ -4430,6 +4556,64 @@ snapshots: micromark-util-symbol: 2.0.1 micromark-util-types: 2.0.2 + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + micromark-extension-mdx-expression@3.0.1: dependencies: '@types/estree': 1.0.8 @@ -4964,6 +5148,17 @@ snapshots: transitivePeerDependencies: - supports-color + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + remark-mdx@3.1.1: dependencies: mdast-util-mdx: 3.0.0 @@ -4988,6 +5183,12 @@ snapshots: unified: 11.0.5 vfile: 6.0.3 + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + resend@4.8.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: '@react-email/render': 1.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)