@forgesworn/fold-kit 0.0.0-seed.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +180 -4
  2. package/dist/access.d.ts +34 -0
  3. package/dist/access.js +102 -0
  4. package/dist/channel.d.ts +27 -0
  5. package/dist/channel.js +38 -0
  6. package/dist/credential.d.ts +74 -0
  7. package/dist/credential.js +177 -0
  8. package/dist/display-name.d.ts +38 -0
  9. package/dist/display-name.js +78 -0
  10. package/dist/epoch.d.ts +387 -0
  11. package/dist/epoch.js +665 -0
  12. package/dist/hex.d.ts +28 -0
  13. package/dist/hex.js +32 -0
  14. package/dist/identity.d.ts +49 -0
  15. package/dist/identity.js +17 -0
  16. package/dist/index.d.ts +29 -0
  17. package/dist/index.js +19 -0
  18. package/dist/invitation.d.ts +217 -0
  19. package/dist/invitation.js +484 -0
  20. package/dist/kinds.d.ts +64 -0
  21. package/dist/kinds.js +64 -0
  22. package/dist/lane.d.ts +41 -0
  23. package/dist/lane.js +63 -0
  24. package/dist/link.d.ts +52 -0
  25. package/dist/link.js +107 -0
  26. package/dist/network-hints.d.ts +16 -0
  27. package/dist/network-hints.js +65 -0
  28. package/dist/persistent-invitation.d.ts +35 -0
  29. package/dist/persistent-invitation.js +118 -0
  30. package/dist/room.d.ts +53 -0
  31. package/dist/room.js +153 -0
  32. package/dist/scoped.d.ts +67 -0
  33. package/dist/scoped.js +83 -0
  34. package/dist/sub-cert.d.ts +117 -0
  35. package/dist/sub-cert.js +192 -0
  36. package/dist/transport.d.ts +24 -0
  37. package/dist/transport.js +1 -0
  38. package/dist/types.d.ts +62 -0
  39. package/dist/types.js +1 -0
  40. package/dist/verify.d.ts +26 -0
  41. package/dist/verify.js +46 -0
  42. package/llms.txt +106 -0
  43. package/package.json +77 -6
  44. package/vectors/circle-vectors.json +2174 -0
  45. package/vectors/kithmoot-vectors.json +1550 -0
package/llms.txt ADDED
@@ -0,0 +1,106 @@
1
+ # @forgesworn/fold-kit
2
+
3
+ > Circle primitives for private Nostr groups nobody operates: a room key per epoch, sealed to each device that stays; link invitations as capabilities; member removal by rekey; device credentials so a person approves a device once and it signs from then on. Extracted from KithMoot, byte-identical on the wire. Framework-free: owns no storage, UI, relay defaults or identity keys, and does no relay I/O of its own.
4
+
5
+ ESM-only, Node.js 22.13+. Two entry points: `@forgesworn/fold-kit` (everything) and `@forgesworn/fold-kit/lane` (relay lane classification only).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @forgesworn/fold-kit
11
+ ```
12
+
13
+ Not yet published to npm under this API. Until it is, pin an immutable Git commit:
14
+
15
+ ```json
16
+ {
17
+ "dependencies": {
18
+ "@forgesworn/fold-kit": "github:forgesworn/fold-kit#<commit>"
19
+ }
20
+ }
21
+ ```
22
+
23
+ ### Peer dependencies
24
+
25
+ `nostr-tools` (`>=2.24.2 <3`), `@noble/hashes` (`^1.8.0`), `@noble/curves` (`^2.0.1`) - peers, not dependencies, so a consumer keeps a single copy of each. `@noble/hashes` is 1.x only: this kit's imports have no `.js` suffix and its `hkdf` calls pass a string `info`, both of which 2.x rejects.
26
+
27
+ ## Exports
28
+
29
+ ### Identity, verification, hex
30
+
31
+ - `hexEquals(a, b)` → `boolean` - constant-shape hex comparison
32
+ - `normaliseHex(hex)` → `string` - lower-cases and validates hex
33
+ - `verifyEventUncached(event)` → `boolean`
34
+ - `boundedEventVerifier(limit?, verify?)` → `(event) => boolean` - an LRU-bounded verifier
35
+ - `localIdentity(participantSk)` → `ParticipantIdentity` - a local signer, plus the `ParticipantIdentity`/`UnsignedEvent` seam every codec below takes as a dependency
36
+
37
+ ### Wire kinds and types
38
+
39
+ - `KINDS` → `{ CREDENTIAL: 20460, CHAT: 1460, INVITATION_REQUEST: 20466, INVITATION_GRANT: 20467, INVITATION_RETIREMENT: 1461, GROUP_INVITATION: 1463, ROOM_REKEY: 1462, EPOCH_REQUEST: 20468, EPOCH_GRANT: 20469 }`
40
+ - `DeviceCredential`, `AccessTier` (`'open' | 'ken' | 'kith' | 'kin'`), `AgentRule`, `RoomPolicy`, `KindredProof` - link-layer wire types
41
+ - `RelayTransport` → `{ publish(event), subscribe(filters, onEvent, onEose?), close() }` - the transport seam every relay-facing codec takes; this kit declares its own interface rather than importing a relay pool implementation
42
+
43
+ ### Rooms, credentials, access
44
+
45
+ - `generateRoomSecret()` → `Uint8Array` (32 bytes)
46
+ - `deriveRoom(secret)` → `{ roomId, roomKey }`
47
+ - `encodeJoinUrl(base, secret, relays, policy?)` / `decodeJoinUrl(url)` - legacy v1 join URL
48
+ - `parseRoomPolicy(raw)` → `RoomPolicy | undefined`
49
+ - `createDeviceCredential(opts)` → `Promise<DeviceCredential>` - room-scope or person-scope (`scope: 'person'`)
50
+ - `verifyDeviceCredential(event, opts)` → `VerifyResult` - pass `acceptPerson: true` to accept a person-scope credential where a room credential is expected
51
+ - `PERSON_CREDENTIAL_MAX_SECONDS` - 30-day cap on a person-scope credential
52
+ - `RestampedCredentialExpiryError` - thrown by `createDeviceCredential` when a restamping signer would make a person-scope credential unverifiable everywhere; has `overBySeconds`; catch and retry with a shorter `expiresAt` margin (see CHANGELOG.md)
53
+ - `issueKindredProof(opts)` → `KindredProof`
54
+ - `evaluateAccess(policy, participant, proof, now, roomId)` → `{ admitted, reason }`
55
+ - `sanitiseDisplayName(raw)` → `string | undefined`, `MAX_DISPLAY_NAME_LENGTH`
56
+ - `safeRelayUrls(raw)`, `safeIceUrls(raw)`, `assertNetworkHintBounds(relays, ice)`, `MAX_RELAY_HINTS`, `MAX_ICE_HINTS`, `MAX_NETWORK_HINT_LENGTH`
57
+
58
+ ### Links and invitations
59
+
60
+ - `parseRoomLink(url)` / `encodeRoomLink(base, link)` → `RoomLink` - the v1/v2/v3 link envelope
61
+ - `createRoomInvitation(persistent?)`, `roomInvitation(bearer, inviter, persistent?)`, `deriveInvitationId(invitation)`
62
+ - `encodeInvitationRequest(opts)` / `decodeInvitationRequest(event, opts)` - v2 live rendezvous request
63
+ - `encodeInvitationGrant(opts)` / `decodeRoomAdmissionGrant(event, opts)` - v2 grant, with a delegation chain
64
+ - `verifyInvitationDelegation(invitation, chain, now)`
65
+ - `encodeInvitationRetirement(opts)` / `decodeInvitationRetirementNotice(event, invitation)` - durable tombstone (kind 1461)
66
+ - `hostRoomInvitation(opts)` / `requestRoomAdmission(opts)` - the live desk, both sides
67
+ - `encodePersistentInvitation(opts)` / `decodePersistentInvitation(event, invitation)` - v3 stored group invitation (kind 1463)
68
+ - `requestPersistentRoomAdmission(opts)` - joins from stored admission alone, no live desk needed
69
+
70
+ ### Epochs (removal by rekey)
71
+
72
+ - `deriveEpoch({ epoch, secret })` → `{ epoch, id, key }`, `generateEpochSecret()`
73
+ - `encodeRekeyEvent(opts)` / `peekRekeyEvent(event, opts)` / `decodeRekeyEvent(event, opts)` - the durable rekey notice (kind 1462), sealed per remaining device, naming who was removed
74
+ - `encodeEpochRequest(opts)` / `decodeEpochRequest(event, opts)` - a device asking the authority for the current epoch
75
+ - `encodeEpochGrant(opts)` / `decodeEpochGrant(event, opts)` - the authority's answer (secret, epoch-0, or a refusal `'removed' | 'closed'`)
76
+ - `epochRequestAdmission(opts)`, `deriveEpochRequestKey(roomKey)`
77
+ - `hostRoomEpoch(opts)` / `requestRoomEpoch(opts)` / `EpochRefusedError` - the live request/grant desk, both sides
78
+ - `canonicalAdmins(admins)` / `signAdmins(opts)` / `verifyAdmins(opts)` - the authority's signed admin list
79
+ - `canonicalChannels(channels)` / `signChannels(opts)` / `verifyChannels(opts)` - the authority's signed channel list; `CHANNEL_NAME`, `RESERVED_CHANNELS`
80
+
81
+ ### Scoped labels and sub-key certificates
82
+
83
+ - `deriveScoped(epoch, label)` → `{ id, key }` - HKDF from the epoch KEY under an app's own labelled namespace (e.g. `myapp/v1/board/<id>/update`); refuses this kit's own protocol namespace and any label outside `SCOPED_LABEL_PATTERN`
84
+ - `createSubKeyCertificate(opts)` → `Promise<DeviceCredential>` / `verifySubKeyCertificate(event, opts)` → result - a locally-signed, never-published statement binding an app-derived key to the credentialled device that minted it; `scope: 'sub'` is a value `verifyDeviceCredential` already refuses, so the two forms can never be confused. `verifySubKeyCertificate` takes `opts.credential` (the already-verified device credential) and reads its `device`/`expiration` tags itself; verification is strict (exactly four two-element tags, empty content) and canonical (expiration compared as the exact tag string). `SUB_KEY_CERTIFICATE_SCOPE`.
85
+
86
+ ### Channel derivation
87
+
88
+ - `deriveChannel(roomId, roomKey, channel?)` → `{ id, key }` - a named channel's id/key, derived from the room key; `CHANNEL_ID_INFO`, `CHANNEL_KEY_INFO`, `MAX_CHANNEL_NAME_LENGTH`. The chat log and event codecs themselves stay in the consuming app.
89
+
90
+ ### Lane (`@forgesworn/fold-kit/lane`)
91
+
92
+ - `laneOfRelayUrl(url, circle?)` / `laneOfRelays(urls, circle?)` → `Lane | undefined` (`'public' | 'sheltered' | 'direct'`)
93
+ - `isLane(value)`, `weakestLane(lanes)`, `isDowngrade(requested, actual)`
94
+ - `LANES`, `LANE_MEANING`, `LANE_LABEL`, `LANE_GLYPH`
95
+
96
+ ## Design notes
97
+
98
+ - Every `kithmoot/v*` derivation label and message prefix is byte-identical to the source KithMoot commit this kit was extracted from (see EXTRACTION.md). These are protocol names now, not branding.
99
+ - No baked-in relay defaults, no storage, no DOM access. A caller supplies a `RelayTransport` implementation (a relay pool, or a test double).
100
+ - A room's authority - the party that may rekey and remove members - is the root inviter key pinned in the room's link, not a separate role system.
101
+
102
+ ## Docs
103
+
104
+ - README.md - full API overview, peer dependency rationale, how this kit relates to `covey-kit` and `roost-kit`
105
+ - EXTRACTION.md - the source commit, exactly which modules and functions moved, and the two splits (`access.ts`, `chat.ts`) performed on the way
106
+ - AGENTS.md - build/test commands and repository conventions for an agent working in this repo
package/package.json CHANGED
@@ -1,19 +1,90 @@
1
1
  {
2
2
  "name": "@forgesworn/fold-kit",
3
- "version": "0.0.0-seed.0",
3
+ "version": "0.2.0",
4
4
  "description": "Circle primitives for private Nostr groups nobody operates: epoch keys, link invites, removal by rekey, device credentials.",
5
5
  "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./lane": {
14
+ "types": "./dist/lane.d.ts",
15
+ "default": "./dist/lane.js"
16
+ },
17
+ "./package.json": "./package.json"
18
+ },
6
19
  "files": [
20
+ "dist",
21
+ "vectors/kithmoot-vectors.json",
22
+ "vectors/circle-vectors.json",
7
23
  "LICENSE",
8
- "README.md"
24
+ "README.md",
25
+ "llms.txt"
9
26
  ],
10
- "repository": {
11
- "type": "git",
12
- "url": "git+https://github.com/forgesworn/fold-kit.git"
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.json",
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "typecheck": "tsc -p tsconfig.typecheck.json",
32
+ "diff-source": "node scripts/diff-source.mjs",
33
+ "generate-fold": "node scripts/generate-fold.mjs",
34
+ "bundle-check": "node scripts/bundle-check.mjs",
35
+ "tarball-smoke": "node scripts/tarball-smoke.mjs",
36
+ "vectors": "vitest run vectors/",
37
+ "check": "npm run typecheck && npm test && npm run diff-source",
38
+ "prepare": "npm run build",
39
+ "prepublishOnly": "npm run check && npm run bundle-check && npm run tarball-smoke"
13
40
  },
41
+ "keywords": [
42
+ "nostr",
43
+ "e2ee",
44
+ "circle",
45
+ "group-chat",
46
+ "epoch-key",
47
+ "invite-link",
48
+ "device-credential"
49
+ ],
14
50
  "author": "forgesworn",
15
51
  "license": "MIT",
52
+ "sideEffects": false,
53
+ "engines": {
54
+ "node": ">=22.13"
55
+ },
56
+ "peerDependencies": {
57
+ "nostr-tools": ">=2.24.2 <3",
58
+ "@noble/hashes": "^1.8.0",
59
+ "@noble/curves": "^2.0.1"
60
+ },
61
+ "dependencies": {
62
+ "@scure/base": "^2.0.0"
63
+ },
64
+ "devDependencies": {
65
+ "@noble/curves": "2.0.1",
66
+ "@noble/hashes": "^1.8.0",
67
+ "@types/node": "^22",
68
+ "esbuild": "^0.25.0",
69
+ "nostr-tools": "2.25.0",
70
+ "signet-login": "0.17.1",
71
+ "typescript": "^5.6.0",
72
+ "vitest": "^4.1.11"
73
+ },
16
74
  "publishConfig": {
17
- "access": "public"
75
+ "access": "public",
76
+ "provenance": true
77
+ },
78
+ "funding": {
79
+ "type": "lightning",
80
+ "url": "lightning:profusemeat89@walletofsatoshi.com"
81
+ },
82
+ "homepage": "https://github.com/forgesworn/fold-kit#readme",
83
+ "bugs": {
84
+ "url": "https://github.com/forgesworn/fold-kit/issues"
85
+ },
86
+ "repository": {
87
+ "type": "git",
88
+ "url": "git+https://github.com/forgesworn/fold-kit.git"
18
89
  }
19
90
  }