@forgesworn/fold-kit 0.9.0 → 0.12.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/CHANGELOG.md +357 -0
- package/EXTRACTION.md +549 -0
- package/README.md +198 -15
- package/dist/epoch.d.ts +28 -1
- package/dist/epoch.js +99 -60
- package/dist/index.d.ts +7 -2
- package/dist/index.js +5 -1
- package/dist/invitation-decline.d.ts +26 -0
- package/dist/invitation-decline.js +74 -0
- package/dist/invitation.d.ts +61 -5
- package/dist/invitation.js +300 -57
- package/dist/live-persistent-admission.d.ts +52 -0
- package/dist/live-persistent-admission.js +183 -0
- package/dist/mls-admission.d.ts +79 -0
- package/dist/mls-admission.js +400 -0
- package/dist/persistent-invitation-evidence.d.ts +83 -0
- package/dist/persistent-invitation-evidence.js +381 -0
- package/dist/quiet-reply.d.ts +45 -0
- package/dist/quiet-reply.js +82 -0
- package/docs/extraction-plan-excerpt.md +56 -0
- package/docs/live-persistent-admission.md +192 -0
- package/docs/member-epoch-catch-up.md +346 -0
- package/docs/mls-admission-v4.md +84 -0
- package/docs/persistent-invitation-evidence.md +135 -0
- package/docs/quiet-replies.md +81 -0
- package/docs/room-destruct.md +142 -0
- package/docs/scheduled-rekey.md +142 -0
- package/docs/seal-key.md +104 -0
- package/llms.txt +97 -9
- package/package.json +24 -4
- package/vectors/fold-vectors.json +894 -0
- package/vectors/invitation-account-vectors.json +134 -0
- package/vectors/invitation-decline-vectors.json +110 -0
- package/vectors/live-persistent-vectors.json +1240 -0
- package/vectors/mls-admission-v4.json +25 -0
package/README.md
CHANGED
|
@@ -13,10 +13,12 @@ vectors (see EXTRACTION.md). Framework-free: owns no storage, UI, relay
|
|
|
13
13
|
defaults or identity keys, and no relay I/O of its own - callers inject a
|
|
14
14
|
transport.
|
|
15
15
|
|
|
16
|
-
**Status:**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
**Status:** Version 0.12.0 on `main`. Published on npm as
|
|
17
|
+
`@forgesworn/fold-kit`, but `main` can run ahead of the npm `latest` release, so
|
|
18
|
+
compare `npm view @forgesworn/fold-kit version` with this repository's
|
|
19
|
+
`package.json` before relying on an API listed here. KithMoot has cut over to
|
|
20
|
+
this kit and pins an exact npm version; its moved files are re-export shims
|
|
21
|
+
(see EXTRACTION.md).
|
|
20
22
|
|
|
21
23
|
## Install
|
|
22
24
|
|
|
@@ -24,14 +26,12 @@ copy of these modules.
|
|
|
24
26
|
npm install @forgesworn/fold-kit
|
|
25
27
|
```
|
|
26
28
|
|
|
27
|
-
|
|
29
|
+
To use a change that has not been released, pin an immutable Git commit (a full
|
|
30
|
+
SHA, for example from `git ls-remote https://github.com/forgesworn/fold-kit main`).
|
|
31
|
+
The package's `prepare` script builds `dist/` on install:
|
|
28
32
|
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
"dependencies": {
|
|
32
|
-
"@forgesworn/fold-kit": "github:forgesworn/fold-kit#<commit>"
|
|
33
|
-
}
|
|
34
|
-
}
|
|
33
|
+
```bash
|
|
34
|
+
npm install "github:forgesworn/fold-kit#<commit>"
|
|
35
35
|
```
|
|
36
36
|
|
|
37
37
|
### Peer dependencies
|
|
@@ -46,6 +46,12 @@ Not yet published to npm. Until it is, pin an immutable Git commit:
|
|
|
46
46
|
}
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
+
Install the peers yourself, keeping `@noble/hashes` on 1.x:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm install "nostr-tools@>=2.24.2 <3" "@noble/hashes@^1.8.0" "@noble/curves@^2.0.1"
|
|
53
|
+
```
|
|
54
|
+
|
|
49
55
|
Peers, not dependencies, so a consumer such as KithMoot keeps a single copy
|
|
50
56
|
of `nostr-tools` (guarded by its own `overrides` and version-guard test) and
|
|
51
57
|
of the noble libraries, at the same versions KithMoot already pins.
|
|
@@ -57,12 +63,69 @@ satisfy this kit's imports as written.
|
|
|
57
63
|
|
|
58
64
|
ESM only (`"type": "module"`). Node.js `>=22.13`.
|
|
59
65
|
|
|
66
|
+
## Quick start
|
|
67
|
+
|
|
68
|
+
Derive a room, then publish and load a stored group invitation through a
|
|
69
|
+
`RelayTransport` you supply (this one is an in-memory stub; a real transport
|
|
70
|
+
talks to relays and applies the filters):
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import {
|
|
74
|
+
generateRoomSecret,
|
|
75
|
+
deriveRoom,
|
|
76
|
+
deriveEpoch,
|
|
77
|
+
localIdentity,
|
|
78
|
+
createRoomInvitation,
|
|
79
|
+
encodePersistentInvitation,
|
|
80
|
+
requestPersistentRoomAdmission,
|
|
81
|
+
type RelayTransport,
|
|
82
|
+
} from '@forgesworn/fold-kit'
|
|
83
|
+
import { generateSecretKey, type Event } from 'nostr-tools/pure'
|
|
84
|
+
|
|
85
|
+
// The person: a local signer. An external one (NIP-07, NIP-46) fits the same seam.
|
|
86
|
+
const me = localIdentity(generateSecretKey())
|
|
87
|
+
|
|
88
|
+
// The room: one secret opens epoch 0; later epochs come from rekeys.
|
|
89
|
+
const roomSecret = generateRoomSecret()
|
|
90
|
+
const { roomId, roomKey } = deriveRoom(roomSecret)
|
|
91
|
+
const epoch0 = deriveEpoch({ epoch: 0, secret: roomSecret }) // { epoch: 0, id: roomId, key: roomKey }
|
|
92
|
+
|
|
93
|
+
// A RelayTransport stub: stores events, replays them on subscribe, then signals EOSE.
|
|
94
|
+
const stored: Event[] = []
|
|
95
|
+
const transport: RelayTransport = {
|
|
96
|
+
async publish(event) { stored.push(event) },
|
|
97
|
+
subscribe(_filters, onEvent, onEose) {
|
|
98
|
+
queueMicrotask(() => { for (const event of stored) onEvent(event); onEose?.() })
|
|
99
|
+
return () => {}
|
|
100
|
+
},
|
|
101
|
+
close() {},
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Invitation flow (v3 stored group invitation): the inviter publishes once, and a
|
|
105
|
+
// guest holding the link loads it from the relays with no responder online.
|
|
106
|
+
const { invitation, inviterSk } = createRoomInvitation(true)
|
|
107
|
+
await transport.publish(encodePersistentInvitation({
|
|
108
|
+
invitation, inviterSk, roomSecret, now: Math.floor(Date.now() / 1000),
|
|
109
|
+
}))
|
|
110
|
+
const admission = await requestPersistentRoomAdmission({ transport, invitation })
|
|
111
|
+
// admission.secret is the room secret: deriveRoom(admission.secret).roomId === roomId
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Invitation versions: v1 (`encodeJoinUrl`, the room secret in the link) is
|
|
115
|
+
legacy, and new links go through `createRoomInvitation`. Use
|
|
116
|
+
`createRoomInvitation()` for the v2 live rendezvous, which needs a responder
|
|
117
|
+
online (`hostRoomInvitation` answers, `requestRoomAdmissionCapability` joins),
|
|
118
|
+
or `createRoomInvitation(true)` for the v3 stored group invitation above, which
|
|
119
|
+
needs none.
|
|
120
|
+
|
|
60
121
|
## Exports
|
|
61
122
|
|
|
62
|
-
|
|
123
|
+
Four entry points: `@forgesworn/fold-kit` (circle primitives),
|
|
63
124
|
`@forgesworn/fold-kit/lane` (just the lane helpers, for a consumer that only
|
|
64
125
|
wants to classify a relay as `public`/`sheltered`/`direct` without pulling in
|
|
65
|
-
the rest)
|
|
126
|
+
the rest), `@forgesworn/fold-kit/mls-admission` (opt-in v4 evidence codec), and
|
|
127
|
+
`@forgesworn/fold-kit/persistent-invitation-evidence` (bounded stored invitation
|
|
128
|
+
observation and independent revalidation).
|
|
66
129
|
|
|
67
130
|
### Identity, verification, hex
|
|
68
131
|
|
|
@@ -104,13 +167,57 @@ the rest).
|
|
|
104
167
|
|
|
105
168
|
### Links and invitations
|
|
106
169
|
|
|
170
|
+
`hostRoomInvitation` distinguishes approval from grant publication. Its
|
|
171
|
+
`onGrantPublished(request)` and legacy `onAdmitted(device)` callbacks run only
|
|
172
|
+
after the injected transport acknowledges the grant's publication.
|
|
173
|
+
`onGrantFailed(request, error)` reports a rejected or throwing publication,
|
|
174
|
+
or approval of a request which has expired;
|
|
175
|
+
neither success callback runs for that failure. Closing or retiring the host
|
|
176
|
+
suppresses callbacks from outstanding sends. Publication acknowledgement does
|
|
177
|
+
not prove that a guest received the grant or joined; the guest validates its
|
|
178
|
+
grant independently. Observer exceptions do not change the publish outcome. Fresh retries of the same
|
|
179
|
+
signed request resend the exact cached grant after the configured quiet delay;
|
|
180
|
+
overlapping approval, wait and publication work is coalesced. Approval and
|
|
181
|
+
success callbacks each occur at most once per request. Failed sends may recover
|
|
182
|
+
on a later retry. The host retains up to 256 live decisions, including declines;
|
|
183
|
+
at capacity it refuses new work until settled requests expire. Close, retirement,
|
|
184
|
+
request freshness and responder authority still gate every send.
|
|
185
|
+
|
|
186
|
+
For a device that answers automatically, `grantDelayMs: [min, max]` waits a
|
|
187
|
+
random time before each grant, and `coordinate: true` lets several delegated
|
|
188
|
+
devices share one link: each waits (default 500 to 2000 ms) and stands down,
|
|
189
|
+
at most once per request, if another responder's grant to the same
|
|
190
|
+
requester appears. Both are off by default, and the wire format is
|
|
191
|
+
unchanged. See `docs/quiet-replies.md`.
|
|
192
|
+
|
|
193
|
+
An invitation request's `participant` is an unverified account claim.
|
|
194
|
+
Automatic admission of a previously invited account must use
|
|
195
|
+
`verifiedParticipant`, which is present only after a fresh signature check of
|
|
196
|
+
an account proof bound to this invitation, ephemeral request device and request
|
|
197
|
+
timestamp. `encodeInvitationAccountProof` creates that proof through the
|
|
198
|
+
supplied matching `ParticipantIdentity`. It grants no room access itself and
|
|
199
|
+
stays inside the bearer-encrypted request, rather than being published as a
|
|
200
|
+
public account event. Invalid or missing proofs leave a request unverified;
|
|
201
|
+
hosts can still make an explicit decision for anonymous and older clients.
|
|
202
|
+
|
|
203
|
+
Pass `identity` to `requestRoomAdmissionCapability` (or `requestRoomAdmission`,
|
|
204
|
+
its secret-only wrapper) to create the proof automatically. If `participant` is also supplied, it must
|
|
205
|
+
match the signer; a signer failure rejects the request without falling back to
|
|
206
|
+
another identity. `signal` cancels signing or waiting. Signing and relay waiting
|
|
207
|
+
share one deadline, capped at the request's 90-second freshness window, and
|
|
208
|
+
late signatures or grants cannot revive a cancelled, expired or retired request.
|
|
209
|
+
Retrying publication uses the same signed request. A fresh human retry starts a
|
|
210
|
+
new exchange. The helper copies a supplied `requesterSk` before signing and
|
|
211
|
+
erases only its owned copy on settlement. Hosts recheck freshness when a human
|
|
212
|
+
decision completes, so approval cannot extend an expired request's authority.
|
|
213
|
+
|
|
107
214
|
- `parseRoomLink`, `encodeRoomLink`, `RoomLink` - the v1/v2/v3 link envelope
|
|
108
215
|
- `createRoomInvitation`, `roomInvitation`, `deriveInvitationId`,
|
|
109
|
-
`encodeInvitationRequest`, `decodeInvitationRequest`,
|
|
216
|
+
`encodeInvitationAccountProof`, `encodeInvitationRequest`, `decodeInvitationRequest`,
|
|
110
217
|
`verifyInvitationDelegation`, `encodeInvitationGrant`,
|
|
111
218
|
`decodeRoomAdmissionGrant`, `encodeInvitationRetirement`,
|
|
112
219
|
`decodeInvitationRetirementNotice`, `hostRoomInvitation`,
|
|
113
|
-
`requestRoomAdmission` - the v2 live rendezvous invitation, its delegation
|
|
220
|
+
`requestRoomAdmissionCapability`, `requestRoomAdmission` - the v2 live rendezvous invitation, its delegation
|
|
114
221
|
chain and retirement
|
|
115
222
|
- `encodePersistentInvitation`, `decodePersistentInvitation`,
|
|
116
223
|
`requestPersistentRoomAdmission` - the v3 stored group invitation (1463).
|
|
@@ -118,6 +225,23 @@ the rest).
|
|
|
118
225
|
conference room: the encrypted body carries `ends` and the event a NIP-40
|
|
119
226
|
`expiration` tag, so relays drop it when the room ends; the decoder returns
|
|
120
227
|
`endsAt` and refuses a tag that disagrees with the body
|
|
228
|
+
- From `@forgesworn/fold-kit/persistent-invitation-evidence`,
|
|
229
|
+
`requestPersistentRoomAdmissionEvidence` and
|
|
230
|
+
`revalidatePersistentInvitationEvidence` retain and independently recheck a
|
|
231
|
+
bounded set of canonical signed invitation/retirement carriers, with exact
|
|
232
|
+
invitation and contributing event IDs. The additive evidence path holds on
|
|
233
|
+
conflicting newest equal-time relay lists; the legacy request is unchanged.
|
|
234
|
+
EOSE is local observation metadata, never proof of current membership or
|
|
235
|
+
global non-retirement. Persist the evidence in protected custody before
|
|
236
|
+
using its decoded fields. See [the evidence contract](docs/persistent-invitation-evidence.md).
|
|
237
|
+
- The same optional entry exports `revalidatePersistentInvitationSource` for
|
|
238
|
+
an actual encoder event or its exact canonical retained string. It requires
|
|
239
|
+
the invitation capability and exact room-root pin, independently verifies
|
|
240
|
+
the signature and supported body/tags, and returns one canonical carrier
|
|
241
|
+
plus owned decoded fields. This single source has no EOSE, publication,
|
|
242
|
+
latest-selection or current-membership claim. Creators retain that event
|
|
243
|
+
and their publication intent before opening a pool, then retry the same
|
|
244
|
+
bytes; protected persistence and fresh effect guards belong to the client.
|
|
121
245
|
- Room relays: pass `relays` (one to eight distinct safe relay URLs in
|
|
122
246
|
canonical `normalizeURL` form, e.g. `wss://relay.example.com/`) and the
|
|
123
247
|
encrypted body carries them after `ends`. Every member's pool includes
|
|
@@ -152,6 +276,14 @@ the rest).
|
|
|
152
276
|
catch-up for a device that missed a rekey
|
|
153
277
|
- `hostRoomEpoch`, `requestRoomEpoch`, `EpochRefusedError` - the live
|
|
154
278
|
request/grant desk
|
|
279
|
+
- Quiet replies (`docs/quiet-replies.md`): `hostRoomEpoch` waits a random
|
|
280
|
+
`replyDelayMs` (default `[500, 2000]`) before any answer, sends a
|
|
281
|
+
participant a `removed` or `closed` refusal once per `refuseAgainAfter`
|
|
282
|
+
(default 3600 s) and `unknown` once per `reportUnknownEvery` (60 s), and
|
|
283
|
+
caps refusals at `maxRefusalsPerMinute` (20). Grants are never limited.
|
|
284
|
+
`RefusalLimiter`, `replyDelay` and the `DEFAULT_REPLY_DELAY_MS`,
|
|
285
|
+
`REFUSE_AGAIN_AFTER_SECONDS`, `MAX_REFUSALS_PER_MINUTE` and
|
|
286
|
+
`REFUSAL_LIMITER_CAPACITY` constants are exported
|
|
155
287
|
- Member epoch catch-up (`docs/member-epoch-catch-up.md`): any current
|
|
156
288
|
member can bring an admitted, non-removed device up to date when the
|
|
157
289
|
authority is offline, and the device checks the answer against the
|
|
@@ -263,3 +395,54 @@ There is no separate lint script.
|
|
|
263
395
|
## Licence
|
|
264
396
|
|
|
265
397
|
MIT
|
|
398
|
+
|
|
399
|
+
### Live persistent admission (opt-in codecs)
|
|
400
|
+
|
|
401
|
+
`parseLivePersistentEvent` bounds raw event JSON and refuses duplicate fields.
|
|
402
|
+
`encodeLivePersistentDescriptor` / `decodeLivePersistentDescriptor` carry
|
|
403
|
+
routing metadata alongside an unchanged persistent invitation link.
|
|
404
|
+
`encodeLivePersistentRequest` / `decodeLivePersistentRequest` and
|
|
405
|
+
`encodeLivePersistentAnswer` / `decodeLivePersistentAnswer` implement a fresh,
|
|
406
|
+
root-signed challenge/answer with no delegation. The answer retains epoch-zero
|
|
407
|
+
capability separately from `epochHint`. Constants bound request/answer lifetimes
|
|
408
|
+
to 90/30 seconds. `LIVE_PERSISTENT_ADMISSION_LABELS` freezes its distinct KDF.
|
|
409
|
+
|
|
410
|
+
These functions are codecs, not an offline join API. The host must have exclusive
|
|
411
|
+
durable lifecycle authority; the client must complete authenticated epoch
|
|
412
|
+
admission before publishing, including at epoch zero. No application enables
|
|
413
|
+
this profile automatically. See [the wire profile and integration gates](docs/live-persistent-admission.md).
|
|
414
|
+
Frozen synthetic fixtures are in `vectors/live-persistent-vectors.json`; run
|
|
415
|
+
`npm run build && npm run generate-live-persistent` to regenerate them.
|
|
416
|
+
|
|
417
|
+
## Authenticated invitation refusal (0.12.0)
|
|
418
|
+
|
|
419
|
+
`encodeInvitationDecline` and `decodeInvitationDecline` exchange a private,
|
|
420
|
+
request-bound refusal from the pinned inviter or a current delegated responder.
|
|
421
|
+
The version-3 refusal body uses reply kind 20467 and contains no room secret or
|
|
422
|
+
new delegation. Legacy version-2 grant readers ignore it.
|
|
423
|
+
`requestRoomAdmissionCapability` stops retries and rejects with
|
|
424
|
+
`InvitationDeclinedError` after verifying a refusal. Existing host `admit` hooks
|
|
425
|
+
returning false still leave requests unanswered; hosts must explicitly publish
|
|
426
|
+
a refusal using their guarded, acknowledged decision path. No UI integration or
|
|
427
|
+
physical acceptance is claimed by this protocol release.
|
|
428
|
+
|
|
429
|
+
### MLS admission v4 evidence codec
|
|
430
|
+
|
|
431
|
+
The separate opt-in v4 codec binds an encrypted account/device request to signed
|
|
432
|
+
keeper consent and an expected MLS group. It uses existing 20466/20467 carriers,
|
|
433
|
+
strict canonical arrays and a fixed payload bucket. Existing room links/codecs
|
|
434
|
+
are unchanged. Import from `@forgesworn/fold-kit/mls-admission`; see [API and boundaries](docs/mls-admission-v4.md).
|
|
435
|
+
|
|
436
|
+
Exports: `encodeMlsInvitationV4`, `readMlsInvitationV4`,
|
|
437
|
+
`mlsAdmissionProofTemplatesV4`, `encodeMlsAdmissionRequestV4`,
|
|
438
|
+
`decodeMlsAdmissionRequestV4`, `readMlsAdmissionRequestV4`,
|
|
439
|
+
`encodeMlsAdmissionAnswerV4`, `decodeMlsAdmissionAnswerV4`,
|
|
440
|
+
`readMlsAdmissionAnswerV4`, `MLS_ADMISSION_LABELS`,
|
|
441
|
+
`MLS_ADMISSION_PLAINTEXT_BYTES`, `MLS_ADMISSION_CIPHERTEXT_CHARACTERS`;
|
|
442
|
+
types `VerifiedMlsAdmissionRequest` and `VerifiedAdmissionAnswer`.
|
|
443
|
+
|
|
444
|
+
Verified answers are evidence only. Actual admission requires new purpose-bound
|
|
445
|
+
vault proof and authoritative live box/Link role-witness APIs, a conditional
|
|
446
|
+
keeper revision at the effect boundary, current custody/consent, durable counter
|
|
447
|
+
and one-shot operation storage, and VMLS/Welcome verification. No browser
|
|
448
|
+
admission, authority factory, transport, storage or automatic retry is provided.
|
package/dist/epoch.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type Event } from 'nostr-tools/pure';
|
|
|
2
2
|
import type { RelayTransport } from './transport.js';
|
|
3
3
|
import type { DeviceCredential, KindredProof, RoomPolicy } from './types.js';
|
|
4
4
|
import type { MemberEpochSource } from './member-epoch.js';
|
|
5
|
+
import { type ReplyDelayRange, type ReplySchedule } from './quiet-reply.js';
|
|
5
6
|
/**
|
|
6
7
|
* Room epochs: how a member is removed.
|
|
7
8
|
*
|
|
@@ -413,15 +414,34 @@ export interface HostRoomEpochOptions {
|
|
|
413
414
|
* is making `known` say yes; the requester's next ask is then granted. */
|
|
414
415
|
onUnknown?: (request: EpochRequest) => void;
|
|
415
416
|
/** Seconds before a participant still unknown and still asking is
|
|
416
|
-
* reported again. Default 60.
|
|
417
|
+
* reported again. Default 60. An `unknown` answer goes on the wire at
|
|
418
|
+
* the same cadence: a fresh request inside it is reported to nobody and
|
|
419
|
+
* answered with silence. */
|
|
417
420
|
reportUnknownEvery?: number;
|
|
418
421
|
policy?: RoomPolicy;
|
|
419
422
|
legacyParticipants?: ReadonlySet<string>;
|
|
420
423
|
now?: () => number;
|
|
421
424
|
onGranted?: (request: EpochRequest) => void;
|
|
425
|
+
/** Called for every refusal, including one the limits below kept off the
|
|
426
|
+
* wire. */
|
|
422
427
|
onRefused?: (request: EpochRequest, why: EpochRefusal) => void;
|
|
423
428
|
/** A conference room's end: every grant carries it as an expiration. */
|
|
424
429
|
expiresAt?: number;
|
|
430
|
+
/** Seconds before the same participant is sent a `removed` or `closed`
|
|
431
|
+
* refusal again. Default 3600 (`REFUSE_AGAIN_AFTER_SECONDS`). A request
|
|
432
|
+
* inside it is not answered, so the asker times out instead. */
|
|
433
|
+
refuseAgainAfter?: number;
|
|
434
|
+
/** Refusals of any kind published in any 60 seconds, whoever asks.
|
|
435
|
+
* Default 20 (`MAX_REFUSALS_PER_MINUTE`). Grants are never limited. */
|
|
436
|
+
maxRefusalsPerMinute?: number;
|
|
437
|
+
/** The random wait, in milliseconds, before any answer is published,
|
|
438
|
+
* grant or refusal. Default `[500, 2000]` (`DEFAULT_REPLY_DELAY_MS`);
|
|
439
|
+
* `[0, 0]` answers at once, as before. */
|
|
440
|
+
replyDelayMs?: ReplyDelayRange;
|
|
441
|
+
/** For tests: a source of numbers in [0, 1). Default `Math.random`. */
|
|
442
|
+
random?: () => number;
|
|
443
|
+
/** For tests: how the wait is timed. Default `setTimeout`. */
|
|
444
|
+
schedule?: ReplySchedule;
|
|
425
445
|
}
|
|
426
446
|
/**
|
|
427
447
|
* The credential to seal an answer to: the one the request carries, unless
|
|
@@ -448,6 +468,13 @@ export declare function sealCredential(request: {
|
|
|
448
468
|
* to participants the room knows (`known`), and answers anybody else
|
|
449
469
|
* `unknown` until a member lets them in. A room that has never removed
|
|
450
470
|
* anybody is answered exactly as before.
|
|
471
|
+
*
|
|
472
|
+
* Every answer waits a random `replyDelayMs` first, and refusals are
|
|
473
|
+
* rationed: one a participant per `refuseAgainAfter` (per
|
|
474
|
+
* `reportUnknownEvery` for `unknown`), and `maxRefusalsPerMinute` in all.
|
|
475
|
+
* A removed member who still holds the room key can otherwise ask with
|
|
476
|
+
* fresh requests for ever and time each signed reply, which tells them
|
|
477
|
+
* when the authority is online. What an answer says is unchanged.
|
|
451
478
|
*/
|
|
452
479
|
export declare function hostRoomEpoch(opts: HostRoomEpochOptions): {
|
|
453
480
|
close(): void;
|
package/dist/epoch.js
CHANGED
|
@@ -15,6 +15,7 @@ import { verifyEventUncached } from './verify.js';
|
|
|
15
15
|
import { newerCredential, openSealed, sealTo } from './seal.js';
|
|
16
16
|
import { withExpiration } from './expiration.js';
|
|
17
17
|
import { epochCommitment } from './epoch-commit.js';
|
|
18
|
+
import { DEFAULT_REPLY_DELAY_MS, REFUSE_AGAIN_AFTER_SECONDS, RefusalLimiter, defaultReplySchedule, replyDelay } from './quiet-reply.js';
|
|
18
19
|
/**
|
|
19
20
|
* Room epochs: how a member is removed.
|
|
20
21
|
*
|
|
@@ -622,6 +623,13 @@ export function sealCredential(request, presented, credentialFor, roomId, now) {
|
|
|
622
623
|
* to participants the room knows (`known`), and answers anybody else
|
|
623
624
|
* `unknown` until a member lets them in. A room that has never removed
|
|
624
625
|
* anybody is answered exactly as before.
|
|
626
|
+
*
|
|
627
|
+
* Every answer waits a random `replyDelayMs` first, and refusals are
|
|
628
|
+
* rationed: one a participant per `refuseAgainAfter` (per
|
|
629
|
+
* `reportUnknownEvery` for `unknown`), and `maxRefusalsPerMinute` in all.
|
|
630
|
+
* A removed member who still holds the room key can otherwise ask with
|
|
631
|
+
* fresh requests for ever and time each signed reply, which tells them
|
|
632
|
+
* when the authority is online. What an answer says is unchanged.
|
|
625
633
|
*/
|
|
626
634
|
export function hostRoomEpoch(opts) {
|
|
627
635
|
require32(opts.authoritySk, 'authority secret key');
|
|
@@ -640,6 +648,13 @@ export function hostRoomEpoch(opts) {
|
|
|
640
648
|
if (set.size > 256)
|
|
641
649
|
set.delete(set.values().next().value);
|
|
642
650
|
};
|
|
651
|
+
const refusals = new RefusalLimiter({ perMinute: opts.maxRefusalsPerMinute });
|
|
652
|
+
const delayRange = opts.replyDelayMs ?? DEFAULT_REPLY_DELAY_MS;
|
|
653
|
+
const random = opts.random ?? Math.random;
|
|
654
|
+
const schedule = opts.schedule ?? defaultReplySchedule;
|
|
655
|
+
/** Requests waiting out their delay, each with how to cancel it. Bounded:
|
|
656
|
+
* a request turned away while it is full is answered on its retry. */
|
|
657
|
+
const pending = new Map();
|
|
643
658
|
let closed = false;
|
|
644
659
|
const unsub = opts.transport.subscribe([{ kinds: [KINDS.EPOCH_REQUEST], '#d': [roomId], '#p': [authority] }], (event) => {
|
|
645
660
|
if (closed)
|
|
@@ -652,80 +667,104 @@ export function hostRoomEpoch(opts) {
|
|
|
652
667
|
policy: opts.policy,
|
|
653
668
|
legacyParticipants: opts.legacyParticipants,
|
|
654
669
|
});
|
|
655
|
-
if (!read || answered.has(read.request.request))
|
|
670
|
+
if (!read || answered.has(read.request.request) || pending.has(read.request.request))
|
|
656
671
|
return;
|
|
657
672
|
const request = read.request;
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
else if (opts.removed().has(request.participant))
|
|
662
|
-
refused = 'removed';
|
|
663
|
-
else if (opts.removed().size > 0 && !opts.known?.(request.participant))
|
|
664
|
-
refused = 'unknown';
|
|
665
|
-
if (refused === 'unknown') {
|
|
666
|
-
if (waiting.has(request.request))
|
|
673
|
+
const reply = () => {
|
|
674
|
+
pending.delete(request.request);
|
|
675
|
+
if (closed)
|
|
667
676
|
return;
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
roomId,
|
|
681
|
-
authoritySk: opts.authoritySk,
|
|
682
|
-
device: request.device,
|
|
683
|
-
request: request.request,
|
|
684
|
-
now: now(),
|
|
685
|
-
refused,
|
|
686
|
-
credential: sealCredential(request, read.credential, opts.credentialFor, roomId, now()),
|
|
687
|
-
expiresAt: opts.expiresAt,
|
|
688
|
-
});
|
|
677
|
+
let refused;
|
|
678
|
+
if (opts.closed?.())
|
|
679
|
+
refused = 'closed';
|
|
680
|
+
else if (opts.removed().has(request.participant))
|
|
681
|
+
refused = 'removed';
|
|
682
|
+
else if (opts.removed().size > 0 && !opts.known?.(request.participant))
|
|
683
|
+
refused = 'unknown';
|
|
684
|
+
if (refused === 'unknown') {
|
|
685
|
+
if (waiting.has(request.request))
|
|
686
|
+
return;
|
|
687
|
+
waiting.add(request.request);
|
|
688
|
+
bound(waiting);
|
|
689
689
|
}
|
|
690
690
|
else {
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
authoritySk: opts.authoritySk,
|
|
695
|
-
device: request.device,
|
|
696
|
-
request: request.request,
|
|
697
|
-
now: now(),
|
|
698
|
-
epoch,
|
|
699
|
-
removed: [...opts.removed()],
|
|
700
|
-
...(opts.members ? { members: [...opts.members()] } : {}),
|
|
701
|
-
...(opts.past ? { passed: passedFor(opts.past, epoch.epoch, now()) } : {}),
|
|
702
|
-
credential: sealCredential(request, read.credential, opts.credentialFor, roomId, now()),
|
|
703
|
-
expiresAt: opts.expiresAt,
|
|
704
|
-
});
|
|
691
|
+
waiting.delete(request.request);
|
|
692
|
+
answered.add(request.request);
|
|
693
|
+
bound(answered);
|
|
705
694
|
}
|
|
706
|
-
|
|
707
|
-
|
|
695
|
+
const quiet = refused !== undefined && !refusals.allow(request.participant, refused === 'unknown' ? (opts.reportUnknownEvery ?? REPORT_UNKNOWN_EVERY_SECONDS) : (opts.refuseAgainAfter ?? REFUSE_AGAIN_AFTER_SECONDS), now());
|
|
696
|
+
if (!quiet) {
|
|
697
|
+
let grant;
|
|
698
|
+
try {
|
|
699
|
+
if (refused) {
|
|
700
|
+
grant = encodeEpochGrant({
|
|
701
|
+
roomId,
|
|
702
|
+
authoritySk: opts.authoritySk,
|
|
703
|
+
device: request.device,
|
|
704
|
+
request: request.request,
|
|
705
|
+
now: now(),
|
|
706
|
+
refused,
|
|
707
|
+
credential: sealCredential(request, read.credential, opts.credentialFor, roomId, now()),
|
|
708
|
+
expiresAt: opts.expiresAt,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
else {
|
|
712
|
+
const epoch = opts.current();
|
|
713
|
+
grant = encodeEpochGrant({
|
|
714
|
+
roomId,
|
|
715
|
+
authoritySk: opts.authoritySk,
|
|
716
|
+
device: request.device,
|
|
717
|
+
request: request.request,
|
|
718
|
+
now: now(),
|
|
719
|
+
epoch,
|
|
720
|
+
removed: [...opts.removed()],
|
|
721
|
+
...(opts.members ? { members: [...opts.members()] } : {}),
|
|
722
|
+
...(opts.past ? { passed: passedFor(opts.past, epoch.epoch, now()) } : {}),
|
|
723
|
+
credential: sealCredential(request, read.credential, opts.credentialFor, roomId, now()),
|
|
724
|
+
expiresAt: opts.expiresAt,
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
catch {
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
opts.transport.publish(grant).catch(() => { });
|
|
732
|
+
}
|
|
733
|
+
if (refused === 'unknown' && now() - (reported.get(request.participant) ?? -Infinity) >= (opts.reportUnknownEvery ?? REPORT_UNKNOWN_EVERY_SECONDS)) {
|
|
734
|
+
reported.set(request.participant, now());
|
|
735
|
+
if (reported.size > 256)
|
|
736
|
+
reported.delete(reported.keys().next().value);
|
|
737
|
+
opts.onUnknown?.(request);
|
|
738
|
+
}
|
|
739
|
+
if (!refused)
|
|
740
|
+
reported.delete(request.participant);
|
|
741
|
+
if (!refused)
|
|
742
|
+
refusals.forget(request.participant);
|
|
743
|
+
if (refused)
|
|
744
|
+
opts.onRefused?.(request, refused);
|
|
745
|
+
else
|
|
746
|
+
opts.onGranted?.(request);
|
|
747
|
+
};
|
|
748
|
+
const delay = replyDelay(delayRange, random);
|
|
749
|
+
if (delay === 0) {
|
|
750
|
+
reply();
|
|
708
751
|
return;
|
|
709
752
|
}
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
}
|
|
717
|
-
if (!refused)
|
|
718
|
-
reported.delete(request.participant);
|
|
719
|
-
if (refused)
|
|
720
|
-
opts.onRefused?.(request, refused);
|
|
721
|
-
else
|
|
722
|
-
opts.onGranted?.(request);
|
|
753
|
+
if (pending.size >= 256)
|
|
754
|
+
return;
|
|
755
|
+
pending.set(request.request, () => { });
|
|
756
|
+
const cancel = schedule(reply, delay);
|
|
757
|
+
if (pending.has(request.request))
|
|
758
|
+
pending.set(request.request, cancel);
|
|
723
759
|
});
|
|
724
760
|
return {
|
|
725
761
|
close() {
|
|
726
762
|
if (closed)
|
|
727
763
|
return;
|
|
728
764
|
closed = true;
|
|
765
|
+
for (const cancel of pending.values())
|
|
766
|
+
cancel();
|
|
767
|
+
pending.clear();
|
|
729
768
|
unsub();
|
|
730
769
|
},
|
|
731
770
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -18,8 +18,10 @@ export { MAX_RELAY_HINTS, MAX_ICE_HINTS, MAX_NETWORK_HINT_LENGTH, isSafeRelayUrl
|
|
|
18
18
|
export { sanitiseDisplayName, MAX_DISPLAY_NAME_LENGTH } from './display-name.js';
|
|
19
19
|
export { issueKindredProof, evaluateAccess, ACCESS_LABELS, } from './access.js';
|
|
20
20
|
export type { IssueKindredProofOptions } from './access.js';
|
|
21
|
-
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';
|
|
22
|
-
export type { RoomInvitation, RoomInvitationHost, InvitationDelegation, RoomInvitationDelegate, RoomAdmission, EncodeInvitationRequestOptions, InvitationRequest, DecodeInvitationRequestOptions, EncodeInvitationGrantOptions, DecodeInvitationGrantOptions, HostRoomInvitationOptions, EncodeInvitationRetirementOptions, RequestRoomAdmissionOptions, } from './invitation.js';
|
|
21
|
+
export { INVITATION_DELEGATION_TTL_SECONDS, MAX_INVITATION_DELEGATION_DEPTH, createRoomInvitation, roomInvitation, deriveInvitationId, encodeInvitationAccountProof, encodeInvitationRequest, decodeInvitationRequest, verifyInvitationDelegation, encodeInvitationGrant, decodeRoomAdmissionGrant, decodeInvitationGrant, ROOM_ENDED_MESSAGE, encodeInvitationRetirement, decodeInvitationRetirement, decodeInvitationRetirementNotice, retirementError, hostRoomInvitation, requestRoomAdmissionCapability, requestRoomAdmission, INVITATION_LABELS, } from './invitation.js';
|
|
22
|
+
export type { RoomInvitation, RoomInvitationHost, InvitationDelegation, RoomInvitationDelegate, RoomAdmission, EncodeInvitationAccountProofOptions, EncodeInvitationRequestOptions, InvitationRequest, DecodeInvitationRequestOptions, EncodeInvitationGrantOptions, DecodeInvitationGrantOptions, HostRoomInvitationOptions, EncodeInvitationRetirementOptions, RequestRoomAdmissionOptions, } from './invitation.js';
|
|
23
|
+
export { encodeInvitationDecline, decodeInvitationDecline, InvitationDeclinedError } from './invitation-decline.js';
|
|
24
|
+
export type { EncodeInvitationDeclineOptions, InvitationDecline } from './invitation-decline.js';
|
|
23
25
|
export { encodePersistentInvitation, decodePersistentInvitation, requestPersistentRoomAdmission, PERSISTENT_INVITATION_LABELS, } from './persistent-invitation.js';
|
|
24
26
|
export type { PersistentRoomAdmission } from './persistent-invitation.js';
|
|
25
27
|
export { withExpiration, isRoomEnds, requireRoomEnds, MAX_ROOM_ENDS_SECONDS } from './expiration.js';
|
|
@@ -28,9 +30,12 @@ export { MAX_ROOM_LINK_FRAGMENT_LENGTH, parseRoomLink, encodeRoomLink, } from '.
|
|
|
28
30
|
export type { RoomLink } from './link.js';
|
|
29
31
|
export { EPOCH_ID_INFO, EPOCH_KEY_INFO, MAX_EPOCH, EPOCH_REQUEST_KEY_INFO, generateEpochSecret, deriveEpoch, encodeRekeyEvent, peekRekeyEvent, readMemberList, REPORT_UNKNOWN_EVERY_SECONDS, decodeRekeyEvent, HISTORY_WINDOW_SECONDS, MAX_HISTORY_EPOCHS, epochsInWindow, deriveEpochRequestKey, epochRequestAdmission, encodeEpochRequest, decodeEpochRequest, encodeEpochGrant, decodeEpochGrant, hostRoomEpoch, sealCredential, EpochRefusedError, requestRoomEpoch, canonicalAdmins, CHANNEL_NAME, RESERVED_CHANNELS, canonicalChannels, signChannels, verifyChannels, signAdmins, verifyAdmins, EPOCH_LABELS, } from './epoch.js';
|
|
30
32
|
export type { RoomEpoch, EpochKeys, EncodeRekeyOptions, RekeyRecipient, RekeyNotice, PeekRekeyOptions, DecodeRekeyOptions, LeftEpoch, EpochRequestAdmissionOptions, EncodeEpochRequestOptions, DecodeEpochRequestOptions, EpochRequest, EpochRefusal, EncodeEpochGrantOptions, DecodeEpochGrantOptions, EpochGrant, HostRoomEpochOptions, RequestRoomEpochOptions, SignAdminsOptions, SignChannelsOptions, VerifyChannelsOptions, VerifyAdminsOptions, } from './epoch.js';
|
|
33
|
+
export { DEFAULT_REPLY_DELAY_MS, REFUSE_AGAIN_AFTER_SECONDS, MAX_REFUSALS_PER_MINUTE, REFUSAL_LIMITER_CAPACITY, RefusalLimiter, replyDelay, } from './quiet-reply.js';
|
|
34
|
+
export type { ReplyDelayRange, ReplySchedule, RefusalLimiterOptions } from './quiet-reply.js';
|
|
31
35
|
export { EPOCH_COMMIT_PREFIX, epochCommitment, EPOCH_COMMIT_LABELS } from './epoch-commit.js';
|
|
32
36
|
export { MEMBER_EPOCH_KINDS, MEMBER_EPOCH_REQUEST_KEY_INFO, MAX_MEMBER_EPOCH_CHAIN, deriveMemberEpochRequestKey, readRekeyEvidence, encodeMemberEpochRequest, decodeMemberEpochRequest, encodeMemberEpochGrant, decodeMemberEpochGrant, hostMemberEpochDesk, memberEpochSource, requestMemberEpoch, MEMBER_EPOCH_LABELS, } from './member-epoch.js';
|
|
33
37
|
export type { RekeyEvidence, EncodeMemberEpochRequestOptions, DecodeMemberEpochRequestOptions, MemberEpochRequest, EncodeMemberEpochGrantOptions, DecodeMemberEpochGrantOptions, MemberEpochGrant, HostMemberEpochDeskOptions, MemberEpochRequestOptions, MemberEpochSource, } from './member-epoch.js';
|
|
34
38
|
export { deriveChannel, CHANNEL_ID_INFO, CHANNEL_KEY_INFO, MAX_CHANNEL_NAME_LENGTH, CHANNEL_LABELS } from './channel.js';
|
|
35
39
|
export { LANES, LANE_MEANING, LANE_LABEL, LANE_GLYPH, isLane, laneOfRelayUrl, laneOfRelays, weakestLane, isDowngrade, } from './lane.js';
|
|
36
40
|
export type { Lane } from './lane.js';
|
|
41
|
+
export * from "./live-persistent-admission.js";
|
package/dist/index.js
CHANGED
|
@@ -12,13 +12,17 @@ export { generateRoomSecret, deriveRoom, encodeJoinUrl, decodeJoinUrl, parseRoom
|
|
|
12
12
|
export { MAX_RELAY_HINTS, MAX_ICE_HINTS, MAX_NETWORK_HINT_LENGTH, isSafeRelayUrl, safeRelayUrls, isSafeIceUrl, safeIceUrls, assertNetworkHintBounds, } from './network-hints.js';
|
|
13
13
|
export { sanitiseDisplayName, MAX_DISPLAY_NAME_LENGTH } from './display-name.js';
|
|
14
14
|
export { issueKindredProof, evaluateAccess, ACCESS_LABELS, } 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';
|
|
15
|
+
export { INVITATION_DELEGATION_TTL_SECONDS, MAX_INVITATION_DELEGATION_DEPTH, createRoomInvitation, roomInvitation, deriveInvitationId, encodeInvitationAccountProof, encodeInvitationRequest, decodeInvitationRequest, verifyInvitationDelegation, encodeInvitationGrant, decodeRoomAdmissionGrant, decodeInvitationGrant, ROOM_ENDED_MESSAGE, encodeInvitationRetirement, decodeInvitationRetirement, decodeInvitationRetirementNotice, retirementError, hostRoomInvitation, requestRoomAdmissionCapability, requestRoomAdmission, INVITATION_LABELS, } from './invitation.js';
|
|
16
|
+
export { encodeInvitationDecline, decodeInvitationDecline, InvitationDeclinedError } from './invitation-decline.js';
|
|
16
17
|
export { encodePersistentInvitation, decodePersistentInvitation, requestPersistentRoomAdmission, PERSISTENT_INVITATION_LABELS, } from './persistent-invitation.js';
|
|
17
18
|
export { withExpiration, isRoomEnds, requireRoomEnds, MAX_ROOM_ENDS_SECONDS } from './expiration.js';
|
|
18
19
|
export { isInvitationRelays, requireInvitationRelays, MAX_INVITATION_RELAYS } from './invitation-relays.js';
|
|
19
20
|
export { MAX_ROOM_LINK_FRAGMENT_LENGTH, parseRoomLink, encodeRoomLink, } from './link.js';
|
|
20
21
|
export { EPOCH_ID_INFO, EPOCH_KEY_INFO, MAX_EPOCH, EPOCH_REQUEST_KEY_INFO, generateEpochSecret, deriveEpoch, encodeRekeyEvent, peekRekeyEvent, readMemberList, REPORT_UNKNOWN_EVERY_SECONDS, decodeRekeyEvent, HISTORY_WINDOW_SECONDS, MAX_HISTORY_EPOCHS, epochsInWindow, deriveEpochRequestKey, epochRequestAdmission, encodeEpochRequest, decodeEpochRequest, encodeEpochGrant, decodeEpochGrant, hostRoomEpoch, sealCredential, EpochRefusedError, requestRoomEpoch, canonicalAdmins, CHANNEL_NAME, RESERVED_CHANNELS, canonicalChannels, signChannels, verifyChannels, signAdmins, verifyAdmins, EPOCH_LABELS, } from './epoch.js';
|
|
22
|
+
export { DEFAULT_REPLY_DELAY_MS, REFUSE_AGAIN_AFTER_SECONDS, MAX_REFUSALS_PER_MINUTE, REFUSAL_LIMITER_CAPACITY, RefusalLimiter, replyDelay, } from './quiet-reply.js';
|
|
21
23
|
export { EPOCH_COMMIT_PREFIX, epochCommitment, EPOCH_COMMIT_LABELS } from './epoch-commit.js';
|
|
22
24
|
export { MEMBER_EPOCH_KINDS, MEMBER_EPOCH_REQUEST_KEY_INFO, MAX_MEMBER_EPOCH_CHAIN, deriveMemberEpochRequestKey, readRekeyEvidence, encodeMemberEpochRequest, decodeMemberEpochRequest, encodeMemberEpochGrant, decodeMemberEpochGrant, hostMemberEpochDesk, memberEpochSource, requestMemberEpoch, MEMBER_EPOCH_LABELS, } from './member-epoch.js';
|
|
23
25
|
export { deriveChannel, CHANNEL_ID_INFO, CHANNEL_KEY_INFO, MAX_CHANNEL_NAME_LENGTH, CHANNEL_LABELS } from './channel.js';
|
|
24
26
|
export { LANES, LANE_MEANING, LANE_LABEL, LANE_GLYPH, isLane, laneOfRelayUrl, laneOfRelays, weakestLane, isDowngrade, } from './lane.js';
|
|
27
|
+
// Opt-in live persistent capability exchange; host lifecycle and epoch gates remain required.
|
|
28
|
+
export * from "./live-persistent-admission.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type Event } from 'nostr-tools/pure';
|
|
2
|
+
import type { DecodeInvitationGrantOptions, InvitationDelegation, RoomInvitation } from './invitation.js';
|
|
3
|
+
export interface EncodeInvitationDeclineOptions {
|
|
4
|
+
invitation: RoomInvitation;
|
|
5
|
+
inviterSk: Uint8Array;
|
|
6
|
+
requester: string;
|
|
7
|
+
request: string;
|
|
8
|
+
now: number;
|
|
9
|
+
/** Root-to-responder chain; empty only for the pinned root inviter. */
|
|
10
|
+
delegation?: InvitationDelegation[];
|
|
11
|
+
}
|
|
12
|
+
export interface InvitationDecline {
|
|
13
|
+
request: string;
|
|
14
|
+
responder: string;
|
|
15
|
+
}
|
|
16
|
+
export declare class InvitationDeclinedError extends Error {
|
|
17
|
+
constructor();
|
|
18
|
+
}
|
|
19
|
+
/** A private refusal, carrying no room secret or new responder delegation.
|
|
20
|
+
* Version 3 distinguishes it from version-2 grants on the same reply kind.
|
|
21
|
+
* Older readers ignore it and continue their existing bounded wait. */
|
|
22
|
+
export declare function encodeInvitationDecline(opts: EncodeInvitationDeclineOptions): Event;
|
|
23
|
+
/** Only a fresh, request-bound refusal from the pinned root or a currently
|
|
24
|
+
* delegated responder can stop this guest's wait. A bearer holder has no
|
|
25
|
+
* authority to refuse someone else. No room capability is returned. */
|
|
26
|
+
export declare function decodeInvitationDecline(event: Event, opts: DecodeInvitationGrantOptions): InvitationDecline | null;
|