| … | … | @@ -0,0 +1,327 @@ |
| 1 | --- |
| 2 | name: atproto-identity |
| 3 | description: atproto identity layer — one BIP39 mnemonic -> secp256k1/did:plc + Ed25519 (keet), device-attested PLC genesis, session auth (scrypt/JWT/ES256K), com.atproto.label sign/verify, DID web sign-in verify, DID-derived P2P discovery topic. Offline except the injectable PLC submit ceremony. |
| 4 | category: library |
| 5 | priority: medium |
| 6 | location: hyper-fleeky/atproto-identity |
| 7 | updated: 2026-08-05 |
| 8 | --- |
| 9 | |
| 10 | **v0.0.3.** Audited 2026-08-05 against source: **12/12** `lib/*.js` modules now |
| 11 | covered. ⚠ The previous description's "No I/O" claim was wrong — `plc-directory.js` |
| 12 | performs the PLC **submit ceremony** (injectable `fetchImpl`; nothing runs at |
| 13 | import). Everything else is genuinely offline. |
| 14 | |
| 15 | # atproto-identity |
| 16 | |
| 17 | Read this before touching did:plc genesis construction, secp256k1/Ed25519 |
| 18 | derivation, PDS session auth, or device-attestation for the atproto stack. |
| 19 | This repo is **pure and offline**: no network, no persistent storage, no |
| 20 | daemon. Every function is a deterministic transform over bytes it's handed |
| 21 | — the caller (atproto-bridge / hyper-proto-peer) owns all I/O and all key |
| 22 | custody. `package.json:4` states the mandate exactly: "Offline did:plc |
| 23 | genesis + verification." |
| 24 | |
| 25 | Consumers (verified via `grep -rl atproto-identity package.json` across |
| 26 | siblings): `atproto-bridge/package.json`, `atproto-bridge/pds/package.json`, |
| 27 | `hyper-proto-peer/package.json`, `atproto-repo/package.json`. This repo has |
| 28 | zero outbound deps on any of them — it is a leaf. |
| 29 | |
| 30 | ## Module map (13 tests, all pass offline, ~650ms — verified 2026-07-04) |
| 31 | |
| 32 | ### `lib/did-key.js` — did:key codec |
| 33 | - `encodeDidKey(curve, compressedPubkey)` (`did-key.js:42`) — 33-byte SEC1 |
| 34 | compressed pubkey + multicodec varint prefix -> `did:key:z...` (base58btc). |
| 35 | - `decodeDidKey(didKey)` (`did-key.js:53`) — inverse; returns `{curve, pubkey}`. |
| 36 | - Multicodec prefixes (`did-key.js:17-21`): secp256k1-pub=`0xe7` -> `zQ3s…`, |
| 37 | p256-pub=`0x1200` -> `zDna…`. Matches `@atproto/crypto`'s `formatDidKey`. |
| 38 | |
| 39 | ### `lib/did-plc.js` — byte-exact did:plc derivation (THE pinned invariant) |
| 40 | - `encodeOp(op)` (`did-plc.js:26`) — canonical DAG-CBOR bytes (`@ipld/dag-cbor`, |
| 41 | sorted keys, definite lengths) — this is why key order in the JS object |
| 42 | never matters. |
| 43 | - `didForCreateOp(signedOp, truncate=24)` (`did-plc.js:31`) — `did:plc:` + |
| 44 | RFC4648 **lowercase, no-padding, no multibase-prefix** base32 of |
| 45 | `sha256(dagCbor(signedOp))`, truncated to 24 chars. Hashes the **signed** |
| 46 | op (sig included) — `did-plc.js:9`. |
| 47 | - `unsignedOp(op)` (`did-plc.js:40`) — strips `sig`; this is what gets signed |
| 48 | to produce the op that then gets hashed (with `sig` reattached) to mint the DID. |
| 49 | - **Gotcha**: `multiformats`' `base32.encode` prepends a multibase `'b'` |
| 50 | char — did:plc wants raw base32, so `rawBase32()` (`did-plc.js:20-23`) |
| 51 | strips it. Getting this wrong silently produces a plausible-looking but |
| 52 | wrong DID (no crash) — this is exactly what `golden-did-plc.test.js` guards. |
| 53 | |
| 54 | ### `lib/atproto-identity.js` — secp256k1 keys + genesis-op building |
| 55 | - `ATPROTO_SIGNING_PATH = "m/44'/0'/0'/0/0"` (`:22`) — a **project |
| 56 | convention**, not a registered BIP44 coin type. Changing it changes every |
| 57 | derived DID for every existing user. |
| 58 | - `deriveAtprotoKey(mnemonic, path=ATPROTO_SIGNING_PATH)` (`:33`) — |
| 59 | `@scure/bip39` seed -> `@scure/bip32` HDKey -> secp256k1 keypair. |
| 60 | - `keyFromPrivate(privateKey)` (`:41`) / `generateAtprotoKey()` (`:47`) — |
| 61 | raw-key and random-key entry points (server-minted accounts w/ no seed). |
| 62 | - `signOp(unsigned, privateKey)` (`:52`) — ECDSA sign `sha256(encodeOp(unsigned))`, |
| 63 | **low-S, RFC6979 deterministic** (noble's default), base64url compact sig. |
| 64 | - `verifyOp(unsigned, sigB64url, publicKey)` (`:59`). |
| 65 | - `buildGenesisOp({signingDidKey, rotationDidKeys, handle, pdsUrl})` (`:66`) |
| 66 | — builds the unsigned PLC op shape; `rotationDidKeys` defaults to |
| 67 | `[signingDidKey]` (single-key setup) if omitted. |
| 68 | - `identityFromKey` (`:80`) / `createAtprotoIdentity(mnemonic, opts)` (`:89`) |
| 69 | / `createAtprotoIdentityFromKey(privateKey, opts)` (`:94`) — full |
| 70 | mnemonic/key -> signed genesis + did:plc flow. |
| 71 | - Re-exports `unsignedOp`, `didForCreateOp` from `did-plc.js` (`:98`). |
| 72 | |
| 73 | ### `lib/binding.js` — the one-mnemonic dual-derivation |
| 74 | - `deriveIdentity(mnemonic, {handle, pdsUrl, path})` (`:17`, async) — derives |
| 75 | **both** halves from one phrase: `keet-identity-key`'s `IdentityKey.from` |
| 76 | for Ed25519 (Hypercore/P2P world), this repo's `createAtprotoIdentity` for |
| 77 | secp256k1/did:plc (bsky world). Calls `idk.clear()` (`:20`) right after |
| 78 | copying the pubkey out — does not retain the Ed25519 secret. |
| 79 | - `bindingRecord(mnemonic, opts)` (`:34`, async) — JSON-safe record (hex |
| 80 | pubkeys) for persistence by hyper-identity's `atproto` provider. |
| 81 | - **Verified prerequisite** (`binding.js:6-8`, proven by |
| 82 | `test/binding.test.js`): keet's `bip39-mnemonic` and this repo's |
| 83 | `@scure/bip39` derive the identical standard BIP39 seed from one phrase — |
| 84 | this is *why* one mnemonic can safely anchor both curves. |
| 85 | |
| 86 | ### `lib/device-attestation.js` — atproto-side device attestation |
| 87 | The identity model, stated precisely (`device-attestation.js:1-19`): |
| 88 | - `rotationKeys` = authority/recovery keys = the **ROOT** identity, kept off |
| 89 | the always-on serving box. Any rotation key can sign a PLC op to rotate |
| 90 | the signing key, change handle, or move PDS. |
| 91 | - `verificationMethods.atproto` = the repo **signing** key = a per-**DEVICE** |
| 92 | key; the box running the PDS holds only this. |
| 93 | - Genesis is signed by the ROOT, but *authorizes* the DEVICE key as signer. |
| 94 | Device loss -> root rotates to a new device; DID/handle/repos persist. |
| 95 | **The did:plc is baked from the genesis hash, so the rotation set must be |
| 96 | correct before the one irreversible PLC-directory publish** — there is no |
| 97 | "fix it later" for genesis composition. |
| 98 | - `buildAttestedIdentity({rootMnemonic|rootKey, deviceMnemonic|deviceKey|deviceDidKey, devicePath, handle, pdsUrl, extraRotationDidKeys})` |
| 99 | (`:37`) — the real production path passes only `deviceDidKey` (public) — |
| 100 | **the device private key never leaves the device box**; the root just |
| 101 | authorizes the pubkey into the genesis (`:45-53`). Returns |
| 102 | `{did, signedOp, rootDidKey, rootPublicKey, deviceDidKey, deviceSigningKey, rotationKeys}`; |
| 103 | `deviceSigningKey` is `null` in the pubkey-only flow. |
| 104 | - `verifyAttestedGenesis(signedOp, rootPublicKey)` (`:74`) — verifies the sig |
| 105 | is from a rotation key (root), not the device. |
| 106 | |
| 107 | ### `lib/device-chain.js` — keet-world (Ed25519) parallel attestation |
| 108 | Runs in parallel to the atproto PLC authority, on the same root/device pair, |
| 109 | but is the chain **our P2P world** verifies (not federation-visible): |
| 110 | - `deviceKeetKeyPair(mnemonic, seed)` (`:19`, async) — device's Ed25519 |
| 111 | keypair via `keet-identity-key`'s `IdentityKey.from`. |
| 112 | - `rootIdentityPublicKey(mnemonic, seed)` (`:25`, async). |
| 113 | - `attestDeviceChain({rootMnemonic, rootSeed, devicePublicKey})` (`:31`, |
| 114 | async) — wraps `IdentityKey.bootstrap`; root attests the device pubkey. |
| 115 | - `attestAtprotoBinding({deviceKeyPair, did, proof})` (`:37`) — wraps |
| 116 | `IdentityKey.attestData`; the device signs the **atproto DID itself** as |
| 117 | attested data, extending the proof: root -> device -> vouches-for(did). |
| 118 | - `verifyAtprotoBinding({proof, did, expectedIdentity, expectedDevice})` |
| 119 | (`:43`) — wraps `IdentityKey.verify`; returns |
| 120 | `{receipt, identityPublicKey, devicePublicKey}` or falsy on any mismatch |
| 121 | (wrong DID, wrong expected device). |
| 122 | |
| 123 | ### `lib/attested-anchor.js` — composes both worlds |
| 124 | - `buildAttestedAnchor({rootMnemonic, rootSeed, deviceMnemonic, deviceSeed, devicePath, handle, pdsUrl, extraRotationDidKeys})` |
| 125 | (`:17`, async) — one call producing both the atproto genesis |
| 126 | (`buildAttestedIdentity`) and the keet P2P proof (`device-chain.js`), |
| 127 | bound to the same root+device. Returns everything needed to (a) publish |
| 128 | the PLC op and (b) let P2P peers verify the device chain -> DID binding. |
| 129 | This helper takes **both mnemonics** — it's the dev/single-operator |
| 130 | end-to-end case, not the split-custody production path (use |
| 131 | `buildAttestedIdentity` + `deviceDidKey` directly for that). |
| 132 | |
| 133 | ### `lib/auth.js` — PDS-side HTTP session primitives (no I/O) |
| 134 | Stated purpose (`auth.js:1-8`): the *only* auth the bsky-facing HTTP |
| 135 | membrane needs. Pure JS crypto (`@noble/hashes`), bare-safe (no Node |
| 136 | `crypto` global), randomness from `hypercore-crypto` (sodium-backed, bare-native). |
| 137 | - `hashPassword(password, {salt})` / `verifyPassword(password, stored)` |
| 138 | (`:47`, `:53`) — scrypt, stored as `scrypt$N$r$p$saltHex$hashHex`; work |
| 139 | factor `N=16384, r=8, p=1, dkLen=32` (`:21`) — "interactive-login |
| 140 | appropriate" per the source comment, i.e. NOT tuned for high-value key |
| 141 | material, only PDS account passwords. |
| 142 | - `signJwt(payload, secret)` / `verifyJwt(token, secret, {nowSec})` |
| 143 | (`:69`, `:79`) — HS256 session tokens (`accessJwt`/`refreshJwt` shape); |
| 144 | verify checks HMAC then `exp`. Throws typed errors via `authError(status, error, message)` |
| 145 | (`:133`) so the caller can map straight to an XRPC error response. |
| 146 | - `signAssertion(payload, privKey)` / `verifyAssertion(token, pubKeyCompressed, {nowSec})` |
| 147 | (`:98`, `:107`) — **ES256K key-native login**: the client signs a |
| 148 | short-lived assertion with the atproto signing privkey; the PDS verifies |
| 149 | against the pubkey already published in the DID doc, so **no secret is |
| 150 | stored server-side**. This is the P2P trust root (key possession) brought |
| 151 | to the HTTP edge — same key that signs repo commits. |
| 152 | - `timingSafeEqual(a, b)` (`:38`) — constant-time compare, used by both |
| 153 | password and signature verification paths. |
| 154 | - `parseBearer(authHeader)` (`:127`), `decodeSegment(seg)` (`:123`, decodes |
| 155 | without verifying — for reading the issuer before choosing which key to |
| 156 | verify against), `b64url`/`b64urlToBytes` (`:25`, `:28`). |
| 157 | - `ACCESS_SCOPE = 'com.atproto.access'`, `REFRESH_SCOPE = 'com.atproto.refresh'` (`:140-141`). |
| 158 | |
| 159 | ### `lib/discovery.js` — DID-derived P2P rendezvous |
| 160 | - `discoveryTopic(did)` (`:15`) — `hypercore-crypto.data(NS + did)`, |
| 161 | `NS = 'hyper-proto-peer/discovery/v1:'` (`:12`) -> deterministic 32-byte |
| 162 | Hyperswarm topic. **Design intent** (`:1-8`, "the sneaky P2P binding"): |
| 163 | the DID document stays 100% vanilla atproto (only `atproto_pds` service |
| 164 | entry, pointing at the bridge) — bsky/HTTP never sees a P2P side. A |
| 165 | self-custody peer announces on `discoveryTopic(did)`; a bridge holding |
| 166 | the DID derives the identical topic and connects. The topic lookup is |
| 167 | deterministic and **unauthenticated by design** — trust is established |
| 168 | at connection time (the peer must sign its served repo against the DID's |
| 169 | key), not at the rendezvous address, so a topic squatter can't forge |
| 170 | anything by finding the topic. |
| 171 | |
| 172 | ### `lib/plc-directory.js` — ⚠ the PLC **submit** ceremony (the only network I/O) |
| 173 | |
| 174 | The SUBMIT half of did:plc; the offline half lives in `did-plc.js` / |
| 175 | `atproto-identity.js`. **This is the module that breaks the package's |
| 176 | "pure offline / no I/O" framing** — everything else here is offline. |
| 177 | |
| 178 | ⚠ **`POST /:did` is a PERMANENT, irreversible append to the global PLC op-log**, |
| 179 | nullifiable only by a higher-authority fork within a **72h** window. So the |
| 180 | module is OFFLINE-FIRST by construction: op construction, the §6 validation |
| 181 | MIRROR, and the ceremony orchestration are all pure/injectable and fully tested |
| 182 | **without the network**. The only I/O is the injectable `fetchImpl`; nothing |
| 183 | touches the live directory at import. |
| 184 | |
| 185 | The self-custody sequence: |
| 186 | ``` |
| 187 | GET /:did/log/last → live head → opCid(head) == our prev |
| 188 | → build full-state op → sign with a CURRENT rotation key |
| 189 | → LOCALLY re-validate (§6) → POST /:did → confirm via GET /:did/data |
| 190 | ``` |
| 191 | |
| 192 | Exports: `verifySig`, `verifyOpSig`, `verifyOpSignedByAny`, `checkGenesisOp`, |
| 193 | `checkUpdateOp`, `class PlcDirectory`. Supports **both** secp256k1 and p256 |
| 194 | (`@noble/curves/p256`, `CURVE_P256`). `verifySig` is also the primitive |
| 195 | `lib/labels.js` verifies label signatures with. |
| 196 | |
| 197 | ### `lib/labels.js` — `com.atproto.label` build / sign / verify |
| 198 | |
| 199 | A label is a SIGNED tag on a subject: `uri` is an AT-URI (a record) **or** an |
| 200 | account DID; `val` is the value (`spam`, `!hide`, custom); `src` is the labeler's |
| 201 | DID. **Retraction is NEGATION, not deletion** — `neg: true` with the same |
| 202 | `src`/`uri`/`val`. |
| 203 | |
| 204 | `labelSigningBytes(label)` = DAG-CBOR of the label **minus `sig`**, undefined |
| 205 | fields omitted. DAG-CBOR sorts map keys deterministically, so signer and verifier |
| 206 | agree regardless of insertion order. The signature is a raw 64-byte secp256k1 |
| 207 | **low-S** signature — byte-matching atproto/ozone, which signs with an |
| 208 | `#atproto_label` key (verified against that did:key). |
| 209 | |
| 210 | Exports: `labelSigningBytes`, `buildLabel`, `signLabel`, `verifyLabel`, |
| 211 | `encodeLabel`, `decodeLabel`. |
| 212 | |
| 213 | Lives here because the crypto (dag-cbor + secp256k1 + verifySig + did:key) |
| 214 | already does; extractable to a dedicated `atproto-label` module later. The |
| 215 | moderation **policy** (which labelers to trust, how a `val` renders) is the |
| 216 | CONSUMER's job — [[favela-web]]'s `labeler.js` (reader) and `labeler-export.js` |
| 217 | (emitter) are the consumers. |
| 218 | |
| 219 | ### `lib/web-auth.js` — DID web sign-in, verify half |
| 220 | |
| 221 | The atproto-native "pgv3 method". A browser holds a secp256k1 **SUB-KEY**; the |
| 222 | DID's `#atproto` key **DELEGATES** it via a signed assertion anchored to the |
| 223 | DID's PLC-published key. Login = the sub-key signs a fresh server challenge as an |
| 224 | ES256K assertion. The atproto analog of pgv3's root-anchored multi-identity: the |
| 225 | **DID is the root anchor**, sub-keys are delegated **devices** — revoke a device |
| 226 | by expiring/removing its delegation, and the DID is untouched. |
| 227 | |
| 228 | All secp256k1/ES256K (`signAssertion`/`verifyAssertion`) — **no keet, no anproto**. |
| 229 | |
| 230 | This module owns **only the crypto verify**. The challenge store (single-use + |
| 231 | TTL) and the session live in the consumer (favela wraps this + hyper-identity's |
| 232 | `AtprotoProvider`). `resolveVerifyKey(did)` is **injected** — a PLC-directory |
| 233 | resolver in prod, a mock in tests — so it stays network-free and unit-testable. |
| 234 | **Fails closed** on any bad/absent/expired input. |
| 235 | |
| 236 | Exports: `verifyChallengeSig`, `makeVerifyDelegation`, `makeAtprotoWebVerify`. |
| 237 | |
| 238 | ## Identity model summary (must-not-break invariants) |
| 239 | |
| 240 | 1. **One seed, two curves, two roles.** One BIP39 mnemonic -> Ed25519 (keet, |
| 241 | P2P/Hypercore-verifiable) + secp256k1 (atproto/did:plc, federation-visible). |
| 242 | Both derivations are proven to agree on one phrase's seed |
| 243 | (`test/binding.test.js`, `binding.js:6-8`). |
| 244 | 2. **rotationKeys (root/recovery) vs verificationMethod (device/signing) are |
| 245 | never the same key in the attested flow.** Root box never runs the PDS; |
| 246 | device box never holds the root seed. Enforced/proven by |
| 247 | `test/device-attestation.test.js`. |
| 248 | 3. **did:plc = hash(genesis) is baked forever.** Any change to the rotation |
| 249 | set or verification method at genesis time yields a different, permanent |
| 250 | DID (`test/device-attestation.test.js:43-48`). There is no in-place fix |
| 251 | after the PLC directory write — get the genesis right first. |
| 252 | 4. **Deterministic signing (RFC6979) + low-S**, everywhere ECDSA is used |
| 253 | (`atproto-identity.js:54`, `auth.js:102`) — no signature randomness to |
| 254 | leak or get wrong on a bare/embedded RNG. |
| 255 | 5. **DAG-CBOR canonicalization is what makes did:plc key-order-independent** |
| 256 | — proven by the golden test's "reordered" case |
| 257 | (`test/golden-did-plc.test.js:27-37`). Never hand-roll JSON stringify for |
| 258 | anything that feeds `didForCreateOp`. |
| 259 | |
| 260 | ## Gotchas |
| 261 | |
| 262 | - `did-plc.js`'s base32 handling is the single most fragile line in the |
| 263 | repo: forgetting to strip multiformats' leading `'b'` multibase char |
| 264 | produces a *plausible, valid-looking, but wrong* did:plc with no error. |
| 265 | Always validate against `test/golden-did-plc.test.js`'s real |
| 266 | millipds-minted vector (`did:plc:bh2hiuoeb34g4lif62zj2vx7`, |
| 267 | `test/vectors/bridgetest-genesis.json`) after touching this file. |
| 268 | - `ATPROTO_SIGNING_PATH` is a made-up convention (no registered BIP44 coin |
| 269 | type for atproto) — it is load-bearing for every derived identity; |
| 270 | changing the default breaks backward compatibility with already-derived |
| 271 | DIDs silently (new mnemonic -> different key -> different DID, no crash). |
| 272 | - `auth.js`'s scrypt params (`N=16384`) are for PDS *account passwords*, |
| 273 | not identity keys — do not confuse this with key-derivation security; the |
| 274 | actual identity root of trust is the BIP39 seed + HD derivation, never a |
| 275 | password. |
| 276 | - `device-attestation.js`'s `buildAttestedIdentity` silently returns |
| 277 | `deviceSigningKey: null` when called with `deviceDidKey` (pubkey-only) — |
| 278 | callers must not assume `.deviceSigningKey.privateKey` exists. |
| 279 | - Bare-runtime compatibility is patched, not native: `scripts/bare-deps.mjs` |
| 280 | (postinstall) rewrites `@noble/hashes`' `./crypto` export map to add a |
| 281 | `bare` condition pointing at the pure-JS crypto shim, because Bare can't |
| 282 | load `node:crypto` (which `cryptoNode.js` imports). This patches |
| 283 | `node_modules` post-install and must re-run after every reinstall |
| 284 | (wired via `package.json:9`'s `postinstall` script) — if a bare-runtime |
| 285 | build starts throwing on `node:crypto`, this is the first thing to check. |
| 286 | Comment at `bare-deps.mjs:5` notes it's safe because all signing here is |
| 287 | deterministic (RFC6979) — the shim degrading to `undefined` randomness is |
| 288 | never actually invoked. |
| 289 | - This repo has NO network code, NO storage, NO daemon — if you find |
| 290 | yourself wanting to add persistence or an HTTP call here, it belongs in |
| 291 | atproto-bridge or hyper-proto-peer instead; keep this repo a pure function |
| 292 | library (the "purity firewall" referenced in `attested-anchor.js:1`'s |
| 293 | comment, pointing at an `ATPROTO-P2P-BRIDGE.md` §13 that lives in a |
| 294 | sibling repo, not here). |
| 295 | |
| 296 | ## Tests (13 tests / 53 asserts, all pass offline in ~650ms — verified 2026-07-04) |
| 297 | |
| 298 | - `test/golden-did-plc.test.js` — **the byte-exact pin**: recomputes |
| 299 | `did:plc:bh2hiuoeb34g4lif62zj2vx7` from a real millipds-minted genesis op |
| 300 | (`test/vectors/bridgetest-genesis.json`) and also proves DID stability |
| 301 | under JSON key-reordering (DAG-CBOR canonicalization). If dag-cbor, |
| 302 | sha256, base32, or truncation logic drifts by a byte, this fails. |
| 303 | - `test/derivation.test.js` — did:key round-trip against golden P-256 |
| 304 | values, deterministic secp256k1 derivation (same mnemonic -> same key; |
| 305 | different path -> different key), genesis sign/verify + tamper detection, |
| 306 | and full mnemonic -> did:plc flow determinism. |
| 307 | - `test/binding.test.js` — one mnemonic -> both keet Ed25519 (checked |
| 308 | against a captured golden pubkey) and atproto secp256k1, JSON-round-trip |
| 309 | of the binding record. |
| 310 | - `test/device-attestation.test.js` — proves root=rotationKey, |
| 311 | device=verificationMethod, root-signed (never device-signed); proves a |
| 312 | changed rotation set yields a different permanent DID. |
| 313 | - `test/device-chain.test.js` — keet chain resolves to root identity, |
| 314 | rejects wrong DID / wrong expected device. |
| 315 | - `test/attested-anchor.test.js` — composed anchor: atproto genesis + keet |
| 316 | proof agree on one root/device; proves the root can build the identical |
| 317 | DID from **only** the device's public key (production split-custody path). |
| 318 | |
| 319 | Run: `npm test` (`brittle-node test/*.test.js`) — fully offline, no network, |
| 320 | no ports. Safe to run anytime. |
| 321 | |
| 322 | ## Dependencies |
| 323 | |
| 324 | `@ipld/dag-cbor`, `@noble/curves`, `@noble/hashes`, `@scure/bip32`, |
| 325 | `@scure/bip39`, `hypercore-crypto`, `keet-identity-key`, `multiformats` |
| 326 | (runtime); `brittle` (dev/test only). `type: module` (ESM), designed to run |
| 327 | under both Node and Bare (see the `bare-deps.mjs` postinstall shim above). |