@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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
This kit is pre-1.0 (see AGENTS.md "Release Notes"); a behaviour change on
|
|
4
|
+
`main` is still called out here, because `createDeviceCredential` is a
|
|
5
|
+
byte-identical copy of a KithMoot function (see EXTRACTION.md) and this is
|
|
6
|
+
the one place its behaviour has deliberately diverged.
|
|
7
|
+
|
|
8
|
+
## 0.12.0
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Live invitation hosts resend the same signed grant on a fresh duplicate
|
|
13
|
+
request. This repairs a lost ephemeral grant, including when its first
|
|
14
|
+
publication precedes the guest's subscription. Approval and successful
|
|
15
|
+
publication observers run once; concurrent retries coalesce. Retry sends
|
|
16
|
+
retain quiet delays and recheck freshness and authority. The 256-entry
|
|
17
|
+
decision cache retains pending and declined requests rather than evicting
|
|
18
|
+
live decisions; close and retirement stop outstanding work.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- `hostRoomEpoch` answers quietly by default: every grant and refusal waits
|
|
23
|
+
a random `replyDelayMs` (default `[500, 2000]` ms), a participant is sent
|
|
24
|
+
a `removed` or `closed` refusal at most once per `refuseAgainAfter`
|
|
25
|
+
(default 3600 s) and `unknown` at most once per `reportUnknownEvery`
|
|
26
|
+
(60 s), and no more than `maxRefusalsPerMinute` (20) refusals go out in
|
|
27
|
+
any minute. Grants are never limited. A removed member holding the room
|
|
28
|
+
key could otherwise ask with fresh requests and time each signed reply.
|
|
29
|
+
A requester refused inside the window now times out with the plain
|
|
30
|
+
"not answering" error instead of `EpochRefusedError`. Callers and tests
|
|
31
|
+
that need the old instant reply pass `replyDelayMs: [0, 0]`.
|
|
32
|
+
|
|
33
|
+
### Added
|
|
34
|
+
|
|
35
|
+
- `hostRoomInvitation` options `grantDelayMs` and `coordinate`, off by
|
|
36
|
+
default: a jittered wait before automatic grants, and a coordinated mode
|
|
37
|
+
in which delegated devices stand down when another responder's grant to
|
|
38
|
+
the same requester appears. `random` and `schedule` are injectable on both
|
|
39
|
+
desks.
|
|
40
|
+
- `RefusalLimiter`, `replyDelay`, `DEFAULT_REPLY_DELAY_MS`,
|
|
41
|
+
`REFUSE_AGAIN_AFTER_SECONDS`, `MAX_REFUSALS_PER_MINUTE`,
|
|
42
|
+
`REFUSAL_LIMITER_CAPACITY`, and the `ReplyDelayRange`, `ReplySchedule`
|
|
43
|
+
and `RefusalLimiterOptions` types. See `docs/quiet-replies.md`.
|
|
44
|
+
|
|
45
|
+
The retry repair and quiet-reply controls leave existing events, tags, kinds
|
|
46
|
+
and vectors unchanged. Consumers upgrading from npm 0.10.0 also receive the
|
|
47
|
+
account-signed invitation proofs and bounded request lifecycle described under
|
|
48
|
+
0.11.0 below; 0.11.0 was not published to npm.
|
|
49
|
+
|
|
50
|
+
## 0.11.0
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- Optional account-signed invitation proofs, bound to the invitation, pinned
|
|
55
|
+
inviter, request device and timestamp. `encodeInvitationAccountProof` checks
|
|
56
|
+
the matching signer's returned signature and context. The proof remains
|
|
57
|
+
inside the bearer-encrypted request; it must never be published separately.
|
|
58
|
+
- `InvitationRequest.verifiedParticipant`, set only after fresh verification.
|
|
59
|
+
`participant` remains an unverified claim. Consumers must use the verified
|
|
60
|
+
field for automatic admission based on an invited account.
|
|
61
|
+
- Matching `identity` and cancellation `signal` options for
|
|
62
|
+
`requestRoomAdmissionCapability`, with known-answer proof vectors and a
|
|
63
|
+
packaged-consumer proof round trip.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- Signing and relay waiting share one deadline capped at the 90-second request
|
|
68
|
+
freshness window. Cancellation, retirement and expiry suppress late signing
|
|
69
|
+
and retries; a host rechecks freshness after an awaited admission decision.
|
|
70
|
+
- Request helpers clone and erase their own exchange key while preserving the
|
|
71
|
+
caller's key and the admitted responder's retained delegation key.
|
|
72
|
+
- Browser size budgets cover measured growth of 2,725 minified bytes and
|
|
73
|
+
729 gzip bytes over 0.10, with no new dependency. CI checks browser bundles
|
|
74
|
+
and a real installed tarball as well as protocol/unit tests.
|
|
75
|
+
|
|
76
|
+
Without an account proof, the existing request and grant wire formats remain
|
|
77
|
+
unchanged. This additive API does not repair a consumer that still trusts an
|
|
78
|
+
unverified `participant` claim.
|
|
79
|
+
|
|
80
|
+
## 0.9.0
|
|
81
|
+
|
|
82
|
+
### Added
|
|
83
|
+
|
|
84
|
+
- Self-destructing rooms (`docs/room-destruct.md`):
|
|
85
|
+
`encodePersistentInvitation({ destruct: true })` writes `"destruct": true`
|
|
86
|
+
inside the encrypted group invitation body, after `ends`; `ends` is not
|
|
87
|
+
required. `decodePersistentInvitation` returns
|
|
88
|
+
`PersistentRoomAdmission.destruct` and refuses the envelope when the field
|
|
89
|
+
is present and not `true`. `requestPersistentRoomAdmission` keeps
|
|
90
|
+
`destruct` if any valid signed copy carries it, in either order, as it
|
|
91
|
+
keeps the earliest end.
|
|
92
|
+
- `encodeInvitationRetirement({ ended: true, destruct: true })` writes
|
|
93
|
+
`{"v":1,"ended":true,"destruct":true}` and throws on `destruct` without
|
|
94
|
+
`ended`. `decodeInvitationRetirementNotice` now returns
|
|
95
|
+
`{ ended: boolean; destruct?: true }`, believing `destruct` only beside
|
|
96
|
+
`ended`. The retirement content is plain JSON, as `ended` always was.
|
|
97
|
+
- `encodeRekeyEvent({ closed: true, destruct: true })` writes
|
|
98
|
+
`"destruct": true` inside the encrypted body, after `closed`, and throws
|
|
99
|
+
on `destruct` without `closed`. `RekeyNotice.destruct`
|
|
100
|
+
(`decodeRekeyEvent`) and `RekeyEvidence.destruct` (`readRekeyEvidence`)
|
|
101
|
+
report it only on a closing body; on an open rekey the flag is dropped
|
|
102
|
+
and the rekey still read.
|
|
103
|
+
- Vectors: `vectors/destruct-vectors.json` (`npm run generate-destruct`).
|
|
104
|
+
|
|
105
|
+
### Changed
|
|
106
|
+
|
|
107
|
+
- `decodePersistentInvitation` refuses a group invitation whose body
|
|
108
|
+
carries `destruct` as anything but `true`. No released writer produces
|
|
109
|
+
one.
|
|
110
|
+
|
|
111
|
+
Without the flag, or with it `false`, every event is byte-identical to
|
|
112
|
+
0.8.0's. A 0.8.0 reader ignores the flag: it admits to a self-destructing
|
|
113
|
+
room, reads its retirement as an ended room and its closure as a close, and
|
|
114
|
+
ends the room the old way.
|
|
115
|
+
|
|
116
|
+
## 0.8.0
|
|
117
|
+
|
|
118
|
+
### Added
|
|
119
|
+
|
|
120
|
+
- Scheduled rekeys (`docs/scheduled-rekey.md`): `encodeRekeyEvent({
|
|
121
|
+
scheduled: true })` writes `"scheduled": true` inside the encrypted rekey
|
|
122
|
+
body, so a client can move to the new epoch without announcing it. The
|
|
123
|
+
encoder throws when it is combined with a removal or a close.
|
|
124
|
+
`RekeyNotice.scheduled` (`decodeRekeyEvent`) and `RekeyEvidence.scheduled`
|
|
125
|
+
(`readRekeyEvidence`) report it only when nobody was removed and the room
|
|
126
|
+
stays open.
|
|
127
|
+
- The history window: `HISTORY_WINDOW_SECONDS` (30 days),
|
|
128
|
+
`MAX_HISTORY_EPOCHS` (16) and `epochsInWindow(left, now)`, the left epochs
|
|
129
|
+
a member goes on reading: those left within the window, newest first, at
|
|
130
|
+
most 16.
|
|
131
|
+
- The authority's grant carries the window. `encodeEpochGrant({ passed })`
|
|
132
|
+
writes the left epochs (`LeftEpoch`: `{ epoch, secret, leftAt }`) as
|
|
133
|
+
`passed: [{ epoch, secret, left }]`, oldest first, and throws on more than
|
|
134
|
+
16, epoch 0, an epoch not below the one granted, a doubled epoch, a bad
|
|
135
|
+
secret or a bad time. `hostRoomEpoch({ past })` asks for them on every
|
|
136
|
+
grant, keeps the window's and leaves out anything the encoder would refuse.
|
|
137
|
+
`decodeEpochGrant` returns them as `EpochGrant.passed`, and drops a
|
|
138
|
+
malformed list while keeping the grant.
|
|
139
|
+
- Vectors: `vectors/schedule-vectors.json` (`npm run generate-schedule`).
|
|
140
|
+
|
|
141
|
+
### Changed
|
|
142
|
+
|
|
143
|
+
- `EpochGrant.passed` and `MemberEpochGrant.passed` are now
|
|
144
|
+
`Array<RoomEpoch & { leftAt?: number }>`, and `decodeMemberEpochGrant`
|
|
145
|
+
sets `leftAt` from the `created_at` of the authority's rekey out of each
|
|
146
|
+
passed epoch.
|
|
147
|
+
|
|
148
|
+
A rekey without the marker, and a grant without `passed`, is byte-identical
|
|
149
|
+
to 0.7.0's. A 0.7.0 reader ignores both fields: it announces a scheduled
|
|
150
|
+
rekey as any other, and reads the current epoch from a grant that carries
|
|
151
|
+
the window.
|
|
152
|
+
|
|
153
|
+
## 0.7.0
|
|
154
|
+
|
|
155
|
+
### Added
|
|
156
|
+
|
|
157
|
+
- Seal keys (`docs/seal-key.md`): healing after a device key is copied.
|
|
158
|
+
`createDeviceCredential({ seal })` appends `["seal", <x-only pubkey>]` to
|
|
159
|
+
the credential, under the participant's signature. `encodeRekeyEvent`
|
|
160
|
+
takes recipients as `{ device, credential }` as well as bare devices
|
|
161
|
+
(`RekeyRecipient`), `encodeEpochGrant` and `encodeMemberEpochGrant` take
|
|
162
|
+
`credential`, and each copy is sealed to that credential's seal key when it
|
|
163
|
+
names one. `decodeRekeyEvent`, `decodeEpochGrant` and
|
|
164
|
+
`decodeMemberEpochGrant` take `sealSks`, and `requestRoomEpoch` and
|
|
165
|
+
`memberEpochSource` take a `sealSks()` getter; each tries the seal keys,
|
|
166
|
+
then the device key. `hostRoomEpoch` and `hostMemberEpochDesk` take
|
|
167
|
+
`credentialFor(device)` and seal each answer to the newer of that and the
|
|
168
|
+
credential the request carries (`sealCredential`). New in `seal.ts`:
|
|
169
|
+
`SEAL_TAG`, `isSealPubkey`, `generateSealKey`, `credentialSeal`,
|
|
170
|
+
`sealTarget`, `sealTo`, `openSealed`, `newerCredential`.
|
|
171
|
+
- Vectors: `vectors/seal-vectors.json` (`npm run generate-seal`).
|
|
172
|
+
|
|
173
|
+
### Changed
|
|
174
|
+
|
|
175
|
+
- `decodeRekeyEvent`: a copy for this device that none of its keys opens is
|
|
176
|
+
read as no copy (the notice without `secret`), rather than the whole rekey
|
|
177
|
+
reading as null. A session asked the authority either way; it now also
|
|
178
|
+
learns who was removed.
|
|
179
|
+
|
|
180
|
+
With no seal key anywhere, every event is byte-identical to 0.6.0's.
|
|
181
|
+
|
|
182
|
+
## 0.6.0
|
|
183
|
+
|
|
184
|
+
### Changed
|
|
185
|
+
|
|
186
|
+
- The known-members gate (#207). The epoch desks' admission proof is made
|
|
187
|
+
under the epoch-0 room key, which a removed member keeps, so a removed
|
|
188
|
+
person could ask again under a fresh participant key and be handed the
|
|
189
|
+
current epoch. Once a room has removed anybody, `hostRoomEpoch` and
|
|
190
|
+
`hostMemberEpochDesk` now grant only to participants for which the new
|
|
191
|
+
`known(participant)` option says yes, and report anybody else through
|
|
192
|
+
`onUnknown`: once per participant, and again every `reportUnknownEvery`
|
|
193
|
+
seconds (default 60) while they keep asking. **A desk given no `known` lets nobody through after a
|
|
194
|
+
removal**: wire `known` before taking this release. Rooms that have
|
|
195
|
+
never removed anybody behave exactly as before.
|
|
196
|
+
- New refusal `'unknown'` (`EpochRefusal`): the authority's answer to a
|
|
197
|
+
participant it does not know. Not final: `requestRoomEpoch` keeps asking,
|
|
198
|
+
calls its new `onUnknown` once, and rejects with
|
|
199
|
+
`EpochRefusedError('unknown')` only at its timeout. A decoder from 0.5.1
|
|
200
|
+
reads it as no answer and keeps asking.
|
|
201
|
+
|
|
202
|
+
### Added
|
|
203
|
+
|
|
204
|
+
- `encodeRekeyEvent({ members })`: the authority's member list in the
|
|
205
|
+
encrypted rekey body (after `commit`, before `keys`), removed
|
|
206
|
+
participants dropped. Read as `RekeyNotice.members`,
|
|
207
|
+
`RekeyEvidence.members` and `MemberEpochGrant.members`.
|
|
208
|
+
`hostRoomEpoch({ members })` and `encodeEpochGrant({ members })` carry it
|
|
209
|
+
in the authority's grant (`EpochGrant.members`). `readMemberList`.
|
|
210
|
+
Omitted, every event is byte-identical to 0.5.1's.
|
|
211
|
+
- Vector `rekey-with-members` in `vectors/member-epoch-vectors.json`.
|
|
212
|
+
|
|
213
|
+
## 0.5.1
|
|
214
|
+
|
|
215
|
+
### Added
|
|
216
|
+
|
|
217
|
+
- `MemberEpochGrant.passed`: the epochs a member grant carried between the
|
|
218
|
+
requester's and the one it hands over, oldest first, each proven by the
|
|
219
|
+
next rekey in the chain as before. `requestRoomEpoch`'s grant carries it
|
|
220
|
+
as an optional `passed` when a member answered. Wire unchanged; a
|
|
221
|
+
consumer that ignores it behaves exactly as with 0.5.0. KithMoot uses it
|
|
222
|
+
to read what was said in the epochs a returning device skipped, rather
|
|
223
|
+
than reporting them lost.
|
|
224
|
+
|
|
225
|
+
## 0.5.0
|
|
226
|
+
|
|
227
|
+
### Added
|
|
228
|
+
|
|
229
|
+
- Member epoch catch-up: any current member can bring an admitted,
|
|
230
|
+
non-removed device up to date while the authority is offline, and the
|
|
231
|
+
device verifies the answer against the authority's signatures instead of
|
|
232
|
+
trusting the member. See `docs/member-epoch-catch-up.md`.
|
|
233
|
+
- `encodeRekeyEvent` takes an optional `commit: true`, which writes
|
|
234
|
+
`commit: epochCommitment(roomId, epoch, secret)` into the encrypted
|
|
235
|
+
body (key order `v, epoch, removed, by, closed, commit, keys`; the body
|
|
236
|
+
version stays 1 and a 0.4.0 reader ignores the key). Without it the
|
|
237
|
+
event is byte-identical to 0.4.0's.
|
|
238
|
+
- New module `epoch-commit.ts`: `epochCommitment`, `EPOCH_COMMIT_PREFIX`.
|
|
239
|
+
- New module `member-epoch.ts`: kinds 20471 (member epoch request) and
|
|
240
|
+
20472 (member epoch grant) as `MEMBER_EPOCH_KINDS`;
|
|
241
|
+
`hostMemberEpochDesk`, `memberEpochSource`, `requestMemberEpoch`,
|
|
242
|
+
`encodeMemberEpochRequest`, `decodeMemberEpochRequest`,
|
|
243
|
+
`encodeMemberEpochGrant`, `decodeMemberEpochGrant`, `readRekeyEvidence`,
|
|
244
|
+
`deriveMemberEpochRequestKey`, `MAX_MEMBER_EPOCH_CHAIN`.
|
|
245
|
+
- `requestRoomEpoch` takes an optional `members` source; without it, it
|
|
246
|
+
behaves exactly as before.
|
|
247
|
+
- `memberEpochSource` watches the room's rekeys and refuses a member
|
|
248
|
+
grant that stops short of the newest authority-signed one, so a member
|
|
249
|
+
removed at epoch E cannot hold a requester at E-1 when the caller
|
|
250
|
+
passes no `expected`.
|
|
251
|
+
- Member grants (20472) are signed by a one-time key per grant, not the
|
|
252
|
+
answering member's device key, so a grant does not publicly tie that
|
|
253
|
+
device to the room. `MemberEpochGrant` has no `from`, and
|
|
254
|
+
`encodeMemberEpochGrant` takes no `deviceSk`.
|
|
255
|
+
- `memberEpochSource`'s `removed` is a function or an iterable read once,
|
|
256
|
+
so a generator is not used up by the first grant.
|
|
257
|
+
- Two new wire labels: `kithmoot/v1/epoch-commit:` and
|
|
258
|
+
`kithmoot/v1/member-epoch-request-key`.
|
|
259
|
+
- `vectors/member-epoch-vectors.json` (KithMoot vector format, shipped in
|
|
260
|
+
the package) and `scripts/generate-member-epoch.mjs`.
|
|
261
|
+
- The browser bundle budget for the main entry rises to 64 KB minified /
|
|
262
|
+
19 KB gzip (measured 51.2 / 14.9).
|
|
263
|
+
|
|
264
|
+
## 0.4.0
|
|
265
|
+
|
|
266
|
+
### Added
|
|
267
|
+
|
|
268
|
+
- Room relays: the relays a room is created on, carried in its signed group
|
|
269
|
+
invitation so every member's pool includes them and two members can never
|
|
270
|
+
end up on disjoint relays. `encodePersistentInvitation` takes an optional
|
|
271
|
+
`relays`: one to eight distinct strings, each a safe relay URL
|
|
272
|
+
(`isSafeRelayUrl`) in canonical form (`normalizeURL` from
|
|
273
|
+
`nostr-tools/utils`, e.g. `wss://relay.example.com/`) with no
|
|
274
|
+
credentials, else it throws. The encrypted v3 body carries them after
|
|
275
|
+
`ends` (key order `v, room, secret, ends, relays`; the version stays 3).
|
|
276
|
+
`decodePersistentInvitation` returns `relays` (and
|
|
277
|
+
`PersistentRoomAdmission` gains it) and returns null for the whole
|
|
278
|
+
envelope when the list is malformed - never a trimmed list.
|
|
279
|
+
`requestPersistentRoomAdmission` keeps the relays of the newest signed
|
|
280
|
+
copy (by `created_at`) that names any; a copy naming none says nothing
|
|
281
|
+
about them, and between equal timestamps the first heard stays. Contrast
|
|
282
|
+
`endsAt`, where the earliest end wins.
|
|
283
|
+
- `isInvitationRelays`, `requireInvitationRelays` and
|
|
284
|
+
`MAX_INVITATION_RELAYS` (8): the rule, for callers that build the list.
|
|
285
|
+
|
|
286
|
+
With no `relays`, the body is byte-identical to 0.3.0's, and a 0.3.0
|
|
287
|
+
reader ignores the key. `scripts/diff-source.mjs` declares each added line
|
|
288
|
+
(see EXTRACTION.md "Room relays").
|
|
289
|
+
|
|
290
|
+
## 0.3.0
|
|
291
|
+
|
|
292
|
+
### Added
|
|
293
|
+
|
|
294
|
+
- Conference rooms: a persistent group that ends on a fixed date.
|
|
295
|
+
`encodePersistentInvitation` takes an optional `endsAt` (unix seconds,
|
|
296
|
+
after `now` and no more than 30 days beyond it, else it throws). With it,
|
|
297
|
+
the encrypted v3 body carries `ends` (the version stays 3) and the kind
|
|
298
|
+
1463 event carries a NIP-40 `['expiration', String(endsAt)]` tag, so
|
|
299
|
+
relays drop the invitation when the room ends. `decodePersistentInvitation`
|
|
300
|
+
returns `endsAt` (and `PersistentRoomAdmission` gains it) when the body
|
|
301
|
+
has a positive whole-number `ends`; it returns null when an `expiration`
|
|
302
|
+
tag is present and does not equal the body's `ends`, when there are two
|
|
303
|
+
`expiration` tags, or when `ends` is malformed. `requestPersistentRoomAdmission`
|
|
304
|
+
keeps the earliest `endsAt` among the signed copies it hears.
|
|
305
|
+
- `encodeInvitationRetirement` takes `endsAt`, tagging the kind 1461
|
|
306
|
+
tombstone with the same expiration.
|
|
307
|
+
- `encodeRekeyEvent`, `encodeEpochRequest`, `encodeEpochGrant`,
|
|
308
|
+
`hostRoomEpoch` and `requestRoomEpoch` take `expiresAt`, so a conference
|
|
309
|
+
room's epoch events lapse with it.
|
|
310
|
+
- `withExpiration(tags, expiresAt)`, `isRoomEnds`, `requireRoomEnds` and
|
|
311
|
+
`MAX_ROOM_ENDS_SECONDS`: the expiration rule every event signed for a
|
|
312
|
+
conference room follows - add the end if the event has no expiration,
|
|
313
|
+
keep an earlier one, lower a later one, never two.
|
|
314
|
+
|
|
315
|
+
With no `endsAt`/`expiresAt`, every encoder produces exactly the bytes it
|
|
316
|
+
did in 0.2.0. `scripts/diff-source.mjs` declares each added line (see
|
|
317
|
+
EXTRACTION.md "Conference rooms").
|
|
318
|
+
|
|
319
|
+
## 0.2.0
|
|
320
|
+
|
|
321
|
+
### Added
|
|
322
|
+
|
|
323
|
+
- `deriveScoped(epoch, label)` for app-defined keys under an epoch, and
|
|
324
|
+
`createSubKeyCertificate` / `verifySubKeyCertificate` for never-published
|
|
325
|
+
sub-key certificates (see README and `vectors/fold-vectors.json`).
|
|
326
|
+
|
|
327
|
+
### Changed (breaking behaviour, pre-1.0)
|
|
328
|
+
|
|
329
|
+
- **`createDeviceCredential` now throws for a person-scope credential a
|
|
330
|
+
restamping signer would make unverifiable**
|
|
331
|
+
([forgesworn/kithmoot#205](https://github.com/forgesworn/kithmoot/issues/205)).
|
|
332
|
+
Previously, a remote signer that restamped `created_at` to an earlier
|
|
333
|
+
clock could make `createDeviceCredential` return a credential that every
|
|
334
|
+
`verifyDeviceCredential` call would then refuse as `"longer than 30
|
|
335
|
+
days"` - the mint-time cap was checked against the requested `now`, while
|
|
336
|
+
the verifier checks against the signed `created_at`. `createDeviceCredential`
|
|
337
|
+
now re-checks with the verifier's own measurement and throws a new typed
|
|
338
|
+
error, `RestampedCredentialExpiryError` (exported from the package root),
|
|
339
|
+
instead of returning a credential doomed to fail everywhere.
|
|
340
|
+
- **Who is affected**: only callers passing `scope: 'person'` whose
|
|
341
|
+
`identity.signEvent` may restamp `created_at` (a NIP-46 bunker, a NIP-55
|
|
342
|
+
phone signer, or any other out-of-process signer) AND whose requested
|
|
343
|
+
`expiresAt` is close enough to `now + 30 days` that the restamp pushes
|
|
344
|
+
the real duration over the cap. A local signer (`localIdentity`), or any
|
|
345
|
+
signer that does not restamp, is unaffected. Room-scope credentials
|
|
346
|
+
(`roomId` set, no `scope`) have no 30-day cap and are unaffected.
|
|
347
|
+
- **Migration**: catch `RestampedCredentialExpiryError` and retry with a
|
|
348
|
+
shorter `expiresAt` margin (this kit's own vectors use a one-hour
|
|
349
|
+
margin: `now + PERSON_CREDENTIAL_MAX_SECONDS - 3600`), as the downstream
|
|
350
|
+
sync spec that reported this issue already does.
|
|
351
|
+
- **KithMoot pairing required**: KithMoot has cut over to this kit and
|
|
352
|
+
pins `@forgesworn/fold-kit` exactly, so its `src/credential.ts` is a
|
|
353
|
+
re-export and picks this fix up on its next version bump. That bump must
|
|
354
|
+
land in the same KithMoot PR as the matching change to its
|
|
355
|
+
`vectors/verify-circle.test.ts` `refused-over-30-days` case (M15) and
|
|
356
|
+
`vectors/generate-circle.mjs`, which still expect the mint to return the
|
|
357
|
+
over-cap event rather than throw.
|