@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.
- package/README.md +180 -4
- package/dist/access.d.ts +34 -0
- package/dist/access.js +102 -0
- package/dist/channel.d.ts +27 -0
- package/dist/channel.js +38 -0
- package/dist/credential.d.ts +74 -0
- package/dist/credential.js +177 -0
- package/dist/display-name.d.ts +38 -0
- package/dist/display-name.js +78 -0
- package/dist/epoch.d.ts +387 -0
- package/dist/epoch.js +665 -0
- package/dist/hex.d.ts +28 -0
- package/dist/hex.js +32 -0
- package/dist/identity.d.ts +49 -0
- package/dist/identity.js +17 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.js +19 -0
- package/dist/invitation.d.ts +217 -0
- package/dist/invitation.js +484 -0
- package/dist/kinds.d.ts +64 -0
- package/dist/kinds.js +64 -0
- package/dist/lane.d.ts +41 -0
- package/dist/lane.js +63 -0
- package/dist/link.d.ts +52 -0
- package/dist/link.js +107 -0
- package/dist/network-hints.d.ts +16 -0
- package/dist/network-hints.js +65 -0
- package/dist/persistent-invitation.d.ts +35 -0
- package/dist/persistent-invitation.js +118 -0
- package/dist/room.d.ts +53 -0
- package/dist/room.js +153 -0
- package/dist/scoped.d.ts +67 -0
- package/dist/scoped.js +83 -0
- package/dist/sub-cert.d.ts +117 -0
- package/dist/sub-cert.js +192 -0
- package/dist/transport.d.ts +24 -0
- package/dist/transport.js +1 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +1 -0
- package/dist/verify.d.ts +26 -0
- package/dist/verify.js +46 -0
- package/llms.txt +106 -0
- package/package.json +77 -6
- package/vectors/circle-vectors.json +2174 -0
- 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.
|
|
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
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
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
|
}
|