@forgesworn/fold-kit 0.0.0-seed.0 → 0.1.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/dist/hex.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Case-insensitive equality for hex identifiers.
3
+ *
4
+ * Nostr pubkeys, room ids, and every other hex field this protocol compares
5
+ * are canonically produced in lower case, but nothing on the wire enforces
6
+ * that: an allow-list entry typed or pasted by a person, in particular, may
7
+ * carry upper-case hex naming exactly the same key. Every place this
8
+ * protocol decides whether two hex identifiers name the same thing must go
9
+ * through this function rather than `===`/`!==`, so a case difference is
10
+ * never mistaken for a different identity - see `vectors/README.md`.
11
+ */
12
+ export function hexEquals(a, b) {
13
+ return a.toLowerCase() === b.toLowerCase();
14
+ }
15
+ /**
16
+ * Canonicalise a hex identifier to lower case.
17
+ *
18
+ * `hexEquals` makes an equality check safe regardless of case, but nothing
19
+ * protects a *lexicographic* comparison the same way: `Peer`'s glare
20
+ * tiebreak and `resolveSingularRoles`' device tiebreak both order hex
21
+ * strings with `<`, and two implementations that disagree on which of two
22
+ * differently-cased spellings of the same identifier sorts first can reach
23
+ * opposite answers from identical input - the exact deadlock perfect
24
+ * negotiation exists to prevent. Call this once, at the point a hex
25
+ * identifier enters the system - a decoded event, a parsed credential or
26
+ * proof, a pubkey read from storage or a URL - rather than at each
27
+ * comparison site, so every later equality or ordering check on it is
28
+ * correct by construction. See `vectors/README.md`.
29
+ */
30
+ export function normaliseHex(hex) {
31
+ return hex.toLowerCase();
32
+ }
@@ -0,0 +1,49 @@
1
+ import { type Event } from 'nostr-tools/pure';
2
+ /**
3
+ * An event with everything filled in except the three fields a signature
4
+ * produces: `pubkey`, `id`, `sig`.
5
+ *
6
+ * Deliberately the same shape an external signer expects - NIP-07's
7
+ * `window.nostr.signEvent`, NIP-46's `sign_event`, and signet-login's
8
+ * `SignetSigner.signEvent` all take exactly this - so a signer built for
9
+ * any of those satisfies the interface below without an adapter.
10
+ */
11
+ export interface UnsignedEvent {
12
+ kind: number;
13
+ created_at: number;
14
+ tags: string[][];
15
+ content: string;
16
+ }
17
+ /**
18
+ * The person, as far as this protocol is concerned.
19
+ *
20
+ * The participant key is used for exactly one thing: signing a device
21
+ * credential, one small event per room. Everything else in a room already
22
+ * runs on other keys - the device key signs the roster and the gift-wrapped
23
+ * signalling, the room key encrypts the roster and the chat. So this is the
24
+ * whole surface an identity has to cover.
25
+ *
26
+ * That narrowness is what makes an external signer practical, and the
27
+ * reason to prefer one: a participant who signs in with a browser extension
28
+ * or a remote bunker never has a participant secret in this app's storage
29
+ * at all. `localIdentity` is the other implementation - a key generated and
30
+ * held here, which is the zero-friction default and is exactly as valid.
31
+ *
32
+ * `signEvent` is async because a remote signer is a round trip: an
33
+ * extension prompt, a relay hop to a bunker, a tap on a phone. Every caller
34
+ * in this library awaits it rather than assuming an answer is available in
35
+ * the same tick.
36
+ */
37
+ export interface ParticipantIdentity {
38
+ /** Lower-case hex. See `normaliseHex`. */
39
+ readonly pubkey: string;
40
+ signEvent(unsigned: UnsignedEvent): Promise<Event>;
41
+ }
42
+ /**
43
+ * An identity backed by a secret key held in this process.
44
+ *
45
+ * The key never leaves the closure, which is the same guarantee the app
46
+ * gave before this seam existed - it is simply now one implementation of an
47
+ * interface rather than the only way to be a participant.
48
+ */
49
+ export declare function localIdentity(participantSk: Uint8Array): ParticipantIdentity;
@@ -0,0 +1,17 @@
1
+ import { finalizeEvent, getPublicKey } from 'nostr-tools/pure';
2
+ import { normaliseHex } from './hex.js';
3
+ /**
4
+ * An identity backed by a secret key held in this process.
5
+ *
6
+ * The key never leaves the closure, which is the same guarantee the app
7
+ * gave before this seam existed - it is simply now one implementation of an
8
+ * interface rather than the only way to be a participant.
9
+ */
10
+ export function localIdentity(participantSk) {
11
+ return {
12
+ pubkey: normaliseHex(getPublicKey(participantSk)),
13
+ async signEvent(unsigned) {
14
+ return finalizeEvent(unsigned, participantSk);
15
+ },
16
+ };
17
+ }
@@ -0,0 +1,25 @@
1
+ export { hexEquals, normaliseHex } from './hex.js';
2
+ export { verifyEventUncached, boundedEventVerifier } from './verify.js';
3
+ export type { UnsignedEvent, ParticipantIdentity } from './identity.js';
4
+ export { localIdentity } from './identity.js';
5
+ export { KINDS } from './kinds.js';
6
+ export type { DeviceCredential, AccessTier, AgentRule, RoomPolicy, KindredProof } from './types.js';
7
+ export type { RelayTransport } from './transport.js';
8
+ export { createDeviceCredential, verifyDeviceCredential, PERSON_CREDENTIAL_MAX_SECONDS, } from './credential.js';
9
+ export type { CreateCredentialOptions, VerifyResult } from './credential.js';
10
+ export { generateRoomSecret, deriveRoom, encodeJoinUrl, decodeJoinUrl, parseRoomPolicy, ROOM_LABELS, } from './room.js';
11
+ export { MAX_RELAY_HINTS, MAX_ICE_HINTS, MAX_NETWORK_HINT_LENGTH, isSafeRelayUrl, safeRelayUrls, isSafeIceUrl, safeIceUrls, assertNetworkHintBounds, } from './network-hints.js';
12
+ export { sanitiseDisplayName, MAX_DISPLAY_NAME_LENGTH } from './display-name.js';
13
+ export { issueKindredProof, evaluateAccess, ACCESS_LABELS, } from './access.js';
14
+ export type { IssueKindredProofOptions } from './access.js';
15
+ export { INVITATION_DELEGATION_TTL_SECONDS, MAX_INVITATION_DELEGATION_DEPTH, createRoomInvitation, roomInvitation, deriveInvitationId, encodeInvitationRequest, decodeInvitationRequest, verifyInvitationDelegation, encodeInvitationGrant, decodeRoomAdmissionGrant, decodeInvitationGrant, ROOM_ENDED_MESSAGE, encodeInvitationRetirement, decodeInvitationRetirement, decodeInvitationRetirementNotice, retirementError, hostRoomInvitation, requestRoomAdmissionCapability, requestRoomAdmission, INVITATION_LABELS, } from './invitation.js';
16
+ export type { RoomInvitation, RoomInvitationHost, InvitationDelegation, RoomInvitationDelegate, RoomAdmission, EncodeInvitationRequestOptions, InvitationRequest, DecodeInvitationRequestOptions, EncodeInvitationGrantOptions, DecodeInvitationGrantOptions, HostRoomInvitationOptions, EncodeInvitationRetirementOptions, RequestRoomAdmissionOptions, } from './invitation.js';
17
+ export { encodePersistentInvitation, decodePersistentInvitation, requestPersistentRoomAdmission, PERSISTENT_INVITATION_LABELS, } from './persistent-invitation.js';
18
+ export type { PersistentRoomAdmission } from './persistent-invitation.js';
19
+ export { MAX_ROOM_LINK_FRAGMENT_LENGTH, parseRoomLink, encodeRoomLink, } from './link.js';
20
+ export type { RoomLink } from './link.js';
21
+ export { EPOCH_ID_INFO, EPOCH_KEY_INFO, MAX_EPOCH, EPOCH_REQUEST_KEY_INFO, generateEpochSecret, deriveEpoch, encodeRekeyEvent, peekRekeyEvent, decodeRekeyEvent, deriveEpochRequestKey, epochRequestAdmission, encodeEpochRequest, decodeEpochRequest, encodeEpochGrant, decodeEpochGrant, hostRoomEpoch, EpochRefusedError, requestRoomEpoch, canonicalAdmins, CHANNEL_NAME, RESERVED_CHANNELS, canonicalChannels, signChannels, verifyChannels, signAdmins, verifyAdmins, EPOCH_LABELS, } from './epoch.js';
22
+ export type { RoomEpoch, EpochKeys, EncodeRekeyOptions, RekeyNotice, PeekRekeyOptions, DecodeRekeyOptions, EpochRequestAdmissionOptions, EncodeEpochRequestOptions, DecodeEpochRequestOptions, EpochRequest, EpochRefusal, EncodeEpochGrantOptions, DecodeEpochGrantOptions, EpochGrant, HostRoomEpochOptions, RequestRoomEpochOptions, SignAdminsOptions, SignChannelsOptions, VerifyChannelsOptions, VerifyAdminsOptions, } from './epoch.js';
23
+ export { deriveChannel, CHANNEL_ID_INFO, CHANNEL_KEY_INFO, MAX_CHANNEL_NAME_LENGTH, CHANNEL_LABELS } from './channel.js';
24
+ export { LANES, LANE_MEANING, LANE_LABEL, LANE_GLYPH, isLane, laneOfRelayUrl, laneOfRelays, weakestLane, isDowngrade, } from './lane.js';
25
+ export type { Lane } from './lane.js';
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ // Public entry point. See README.md for an API overview and EXTRACTION.md
2
+ // for where each export came from.
3
+ export { hexEquals, normaliseHex } from './hex.js';
4
+ export { verifyEventUncached, boundedEventVerifier } from './verify.js';
5
+ export { localIdentity } from './identity.js';
6
+ export { KINDS } from './kinds.js';
7
+ export { createDeviceCredential, verifyDeviceCredential, PERSON_CREDENTIAL_MAX_SECONDS, } from './credential.js';
8
+ export { generateRoomSecret, deriveRoom, encodeJoinUrl, decodeJoinUrl, parseRoomPolicy, ROOM_LABELS, } from './room.js';
9
+ export { MAX_RELAY_HINTS, MAX_ICE_HINTS, MAX_NETWORK_HINT_LENGTH, isSafeRelayUrl, safeRelayUrls, isSafeIceUrl, safeIceUrls, assertNetworkHintBounds, } from './network-hints.js';
10
+ export { sanitiseDisplayName, MAX_DISPLAY_NAME_LENGTH } from './display-name.js';
11
+ export { issueKindredProof, evaluateAccess, ACCESS_LABELS, } from './access.js';
12
+ export { INVITATION_DELEGATION_TTL_SECONDS, MAX_INVITATION_DELEGATION_DEPTH, createRoomInvitation, roomInvitation, deriveInvitationId, encodeInvitationRequest, decodeInvitationRequest, verifyInvitationDelegation, encodeInvitationGrant, decodeRoomAdmissionGrant, decodeInvitationGrant, ROOM_ENDED_MESSAGE, encodeInvitationRetirement, decodeInvitationRetirement, decodeInvitationRetirementNotice, retirementError, hostRoomInvitation, requestRoomAdmissionCapability, requestRoomAdmission, INVITATION_LABELS, } from './invitation.js';
13
+ export { encodePersistentInvitation, decodePersistentInvitation, requestPersistentRoomAdmission, PERSISTENT_INVITATION_LABELS, } from './persistent-invitation.js';
14
+ export { MAX_ROOM_LINK_FRAGMENT_LENGTH, parseRoomLink, encodeRoomLink, } from './link.js';
15
+ export { EPOCH_ID_INFO, EPOCH_KEY_INFO, MAX_EPOCH, EPOCH_REQUEST_KEY_INFO, generateEpochSecret, deriveEpoch, encodeRekeyEvent, peekRekeyEvent, decodeRekeyEvent, deriveEpochRequestKey, epochRequestAdmission, encodeEpochRequest, decodeEpochRequest, encodeEpochGrant, decodeEpochGrant, hostRoomEpoch, EpochRefusedError, requestRoomEpoch, canonicalAdmins, CHANNEL_NAME, RESERVED_CHANNELS, canonicalChannels, signChannels, verifyChannels, signAdmins, verifyAdmins, EPOCH_LABELS, } from './epoch.js';
16
+ export { deriveChannel, CHANNEL_ID_INFO, CHANNEL_KEY_INFO, MAX_CHANNEL_NAME_LENGTH, CHANNEL_LABELS } from './channel.js';
17
+ export { LANES, LANE_MEANING, LANE_LABEL, LANE_GLYPH, isLane, laneOfRelayUrl, laneOfRelays, weakestLane, isDowngrade, } from './lane.js';
@@ -0,0 +1,217 @@
1
+ import { type Event } from 'nostr-tools/pure';
2
+ import type { RelayTransport } from './transport.js';
3
+ /** A delegated responder cannot silently make its authority permanent. */
4
+ export declare const INVITATION_DELEGATION_TTL_SECONDS: number;
5
+ /** Bounds both verification work and the size of a grant from a hostile peer. */
6
+ export declare const MAX_INVITATION_DELEGATION_DEPTH = 16;
7
+ /**
8
+ * What a share link grants.
9
+ *
10
+ * `bearer` proves that somebody received the link. It is not a traffic key.
11
+ * `inviter` pins the root pubkey allowed to establish a room-bound responder
12
+ * chain, so another bearer can ask to enter but cannot nominate an authority
13
+ * or substitute a room of their own.
14
+ */
15
+ export interface RoomInvitation {
16
+ bearer: Uint8Array;
17
+ inviter: string;
18
+ /** Version 3 group link: admission is stored encrypted on the relays. */
19
+ persistent?: true;
20
+ }
21
+ /** The private half retained by the browser that created an invitation. */
22
+ export interface RoomInvitationHost {
23
+ invitation: RoomInvitation;
24
+ inviterSk: Uint8Array;
25
+ }
26
+ /**
27
+ * One hop in the authority chain rooted at the inviter pubkey in the link.
28
+ *
29
+ * Every field that gives the certificate meaning is signed. In particular,
30
+ * `invitation` binds it to one bearer-derived rendezvous and `room` to one
31
+ * traffic-secret derivation, so a member delegated for an old link cannot
32
+ * answer a replacement or substitute another room.
33
+ */
34
+ export interface InvitationDelegation {
35
+ invitation: string;
36
+ room: string;
37
+ issuer: string;
38
+ delegate: string;
39
+ expiresAt: number;
40
+ sig: string;
41
+ }
42
+ /** The capability an admitted browser retains so it can answer the next
43
+ * joiner even after the creator has left. */
44
+ export interface RoomInvitationDelegate {
45
+ delegateSk: Uint8Array;
46
+ chain: InvitationDelegation[];
47
+ }
48
+ /** The result of admission: room traffic capability plus bounded authority
49
+ * to keep this particular invitation available. */
50
+ export interface RoomAdmission {
51
+ secret: Uint8Array;
52
+ delegate: RoomInvitationDelegate;
53
+ /**
54
+ * The epoch the responder says the room is at. A hint, not a key: the
55
+ * secret above opens epoch 0, and a room that has been rekeyed is read
56
+ * only with the current epoch's secret, which the room's authority hands
57
+ * to a member on proof of who it is (see `epoch.ts`). A joiner told the
58
+ * room is ahead asks before it announces; one told nothing waits a
59
+ * moment for the rekey events themselves. Absent from a responder that
60
+ * predates epochs.
61
+ */
62
+ epoch?: number;
63
+ }
64
+ /** Create a bearer plus a fresh, unlinkable inviter key for one share URL. */
65
+ export declare function createRoomInvitation(persistent?: boolean): RoomInvitationHost;
66
+ /** Validate and canonicalise an invitation crossing a URL/storage boundary. */
67
+ export declare function roomInvitation(bearer: Uint8Array, inviter: string, persistent?: boolean): RoomInvitation;
68
+ /** Public rendezvous id. Relays see this and timing, but cannot derive the bearer. */
69
+ export declare function deriveInvitationId(invitation: RoomInvitation): string;
70
+ export interface EncodeInvitationRequestOptions {
71
+ invitation: RoomInvitation;
72
+ requesterSk: Uint8Array;
73
+ now: number;
74
+ name?: string;
75
+ participant?: string;
76
+ }
77
+ /** Who is asking, as decoded from a request. */
78
+ export interface InvitationRequest {
79
+ device: string;
80
+ request: string;
81
+ name?: string;
82
+ participant?: string;
83
+ }
84
+ /** Prove possession of the bearer without putting it, or a traffic key, on a relay. */
85
+ export declare function encodeInvitationRequest(opts: EncodeInvitationRequestOptions): Event;
86
+ export interface DecodeInvitationRequestOptions {
87
+ invitation: RoomInvitation;
88
+ now: number;
89
+ maxAgeSeconds?: number;
90
+ }
91
+ /** Returns null for all malformed, stale, wrongly addressed, or unauthorised asks. */
92
+ export declare function decodeInvitationRequest(event: Event, opts: DecodeInvitationRequestOptions): InvitationRequest | null;
93
+ /**
94
+ * Verify a whole chain and return the pubkey authorised by its final hop.
95
+ * An empty chain names the root inviter itself.
96
+ */
97
+ export declare function verifyInvitationDelegation(invitation: RoomInvitation, chain: InvitationDelegation[], now: number): string | null;
98
+ export interface EncodeInvitationGrantOptions {
99
+ invitation: RoomInvitation;
100
+ inviterSk: Uint8Array;
101
+ requester: string;
102
+ request: string;
103
+ roomSecret: Uint8Array;
104
+ now: number;
105
+ /** Empty for the creator; otherwise the root-to-signer chain received when
106
+ * this responder joined. */
107
+ delegation?: InvitationDelegation[];
108
+ delegationTtlSeconds?: number;
109
+ /** The epoch this responder is at, so the requester knows whether the
110
+ * secret it is being handed opens the live room or only its history. */
111
+ epoch?: number;
112
+ }
113
+ /** Encrypt a room secret only to the requester and authenticate its signer
114
+ * through the room-bound chain rooted at the inviter pinned in the link. */
115
+ export declare function encodeInvitationGrant(opts: EncodeInvitationGrantOptions): Event;
116
+ export interface DecodeInvitationGrantOptions {
117
+ invitation: RoomInvitation;
118
+ requesterSk: Uint8Array;
119
+ request: string;
120
+ now: number;
121
+ maxAgeSeconds?: number;
122
+ }
123
+ /**
124
+ * Accept a fresh response whose delegation chain terminates at its event
125
+ * signer, then retain the final requester hop so this member can become a
126
+ * responder in turn. The chain always roots at the pubkey pinned in the
127
+ * link; a bearer holder cannot nominate an authority of their own.
128
+ */
129
+ export declare function decodeRoomAdmissionGrant(event: Event, opts: DecodeInvitationGrantOptions): RoomAdmission | null;
130
+ /** Backward-compatible convenience for callers that only need the traffic
131
+ * secret. New interactive clients should retain `decodeRoomAdmissionGrant`'s
132
+ * delegate capability so the room does not depend on its creator staying. */
133
+ export declare function decodeInvitationGrant(event: Event, opts: DecodeInvitationGrantOptions): Uint8Array | null;
134
+ export interface HostRoomInvitationOptions {
135
+ transport: RelayTransport;
136
+ invitation: RoomInvitation;
137
+ inviterSk: Uint8Array;
138
+ roomSecret: Uint8Array;
139
+ /** Root-to-this-responder chain. Empty/absent only on the creator. */
140
+ delegation?: InvitationDelegation[];
141
+ now?: () => number;
142
+ onAdmitted?: (device: string) => void;
143
+ /** Called when the creator's durable retirement tombstone is heard. */
144
+ onRetired?: () => void;
145
+ /** The epoch this responder is at, asked on every grant because it
146
+ * moves. Omit to say nothing, which a joiner treats as unknown. */
147
+ epoch?: () => number;
148
+ /**
149
+ * Asked before every grant, when present. Return true to let the
150
+ * request in, false to leave it unanswered: there is no refusal on the
151
+ * wire, so a declined person sees the room not answer, which is the same
152
+ * as nobody being home. A request is asked about once, whatever its
153
+ * retries; a person who tries again with a fresh request is asked about
154
+ * again. Absent, every request is granted, which is what a temporary
155
+ * room's link has always meant.
156
+ */
157
+ admit?: (request: InvitationRequest) => boolean | Promise<boolean>;
158
+ }
159
+ export interface EncodeInvitationRetirementOptions {
160
+ invitation: RoomInvitation;
161
+ /** Only the root inviter may retire a link. Delegates never receive this
162
+ * key, which stops one room member disabling admission for everybody. */
163
+ inviterSk: Uint8Array;
164
+ now: number;
165
+ /** The room itself was ended, not just this link replaced. Additive: a
166
+ * reader that predates it still sees an ordinary retirement. */
167
+ ended?: boolean;
168
+ }
169
+ /** What a joiner is told when a link was retired because its room ended. */
170
+ export declare const ROOM_ENDED_MESSAGE = "this room was ended by the person who started it";
171
+ /** Make a permanent tombstone for one invitation rendezvous. */
172
+ export declare function encodeInvitationRetirement(opts: EncodeInvitationRetirementOptions): Event;
173
+ /** A valid tombstone never expires: invitation ids are random and unique,
174
+ * and a retired bearer must not become usable again after a timeout. */
175
+ export declare function decodeInvitationRetirement(event: Event, invitation: RoomInvitation): boolean;
176
+ /** A valid retirement, and whether it says the room was ended. Undefined
177
+ * for anything that is not a valid retirement of this invitation. */
178
+ export declare function decodeInvitationRetirementNotice(event: Event, invitation: RoomInvitation): {
179
+ ended: boolean;
180
+ } | undefined;
181
+ /** The error a joiner rejects with on a retirement. */
182
+ export declare function retirementError(notice: {
183
+ ended: boolean;
184
+ }): Error;
185
+ /**
186
+ * Auto-admit anybody holding this link while this admitted member is online.
187
+ *
188
+ * Closing the returned handle stops this responder. People
189
+ * already admitted necessarily retain the room secret they were given; link
190
+ * rotation is not member revocation and the UI must never claim that it is.
191
+ */
192
+ export declare function hostRoomInvitation(opts: HostRoomInvitationOptions): {
193
+ close(): void;
194
+ };
195
+ export interface RequestRoomAdmissionOptions {
196
+ transport: RelayTransport;
197
+ invitation: RoomInvitation;
198
+ requesterSk?: Uint8Array;
199
+ now?: () => number;
200
+ timeoutMs?: number;
201
+ retryMs?: number;
202
+ /** Carried in the request for a host who asks before letting people in. */
203
+ name?: string;
204
+ participant?: string;
205
+ }
206
+ /** Resolve the room and a bounded responder delegation, with no account or
207
+ * prompt. Retaining the delegation is what removes the creator as an
208
+ * availability dependency for the next arrival. */
209
+ export declare function requestRoomAdmissionCapability(opts: RequestRoomAdmissionOptions): Promise<RoomAdmission>;
210
+ /** Compatibility wrapper for non-interactive consumers. It joins correctly,
211
+ * but discards the ability to keep the link available for somebody else. */
212
+ export declare function requestRoomAdmission(opts: RequestRoomAdmissionOptions): Promise<Uint8Array>;
213
+ /** Every wire-format literal this module owns (each one a kithmoot protocol string), frozen for
214
+ * `src/labels.test.ts`, which checks each module against its own exported
215
+ * list rather than scanning file text for matching comments. Pure data -
216
+ * adding this export changes no runtime behaviour. */
217
+ export declare const INVITATION_LABELS: readonly ["kithmoot/v2/invitation-delegation:", "kithmoot/v2/invitation-id", "kithmoot/v2/invitation-request-key"];