@bcts/gstp 1.0.0-alpha.22 → 1.0.0-beta.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.
@@ -9,11 +9,28 @@
9
9
  * eliminating the need for local state storage and enhancing security
10
10
  * for devices with limited storage or requiring distributed state management.
11
11
  *
12
- * Ported from gstp-rust/src/continuation.rs
12
+ * Ported from gstp-rust/src/continuation.rs.
13
+ *
14
+ * Wire shape — mirrors Rust:
15
+ * ```
16
+ * {
17
+ * <state envelope>
18
+ * } [
19
+ * 'id': ARID(...)
20
+ * 'validUntil': Date(...) ← CBOR tag 1, not ISO 8601 text
21
+ * ]
22
+ * ```
23
+ *
24
+ * The `state` envelope is **wrapped** before assertions are attached,
25
+ * matching Rust `self.state.wrap().add_optional_assertion(...)`. The
26
+ * earlier port attached the assertions directly to the un-wrapped state,
27
+ * producing a different digest tree.
13
28
  */
14
29
 
15
30
  import { ARID, type Encrypter, type PrivateKeys } from "@bcts/components";
16
31
  import { Envelope, type EnvelopeEncodableValue } from "@bcts/envelope";
32
+ import { CborDate } from "@bcts/dcbor";
33
+ import type { Cbor } from "@bcts/dcbor";
17
34
  import { ID, VALID_UNTIL } from "@bcts/known-values";
18
35
  import { GstpError } from "./error";
19
36
 
@@ -132,8 +149,10 @@ export class Continuation {
132
149
  /**
133
150
  * Checks if the continuation is valid at the given time.
134
151
  *
135
- * If no valid_until is set, always returns true.
136
- * If no time is provided, always returns true.
152
+ * Mirrors Rust `is_valid_date(now)`: at the exact `valid_until`
153
+ * instant, the continuation is **expired** (returns `false`). The
154
+ * earlier port used `<=` here, which differed from Rust by one
155
+ * millisecond at the boundary.
137
156
  *
138
157
  * @param now - The time to check against, or undefined to skip time validation
139
158
  * @returns true if the continuation is valid at the given time
@@ -145,7 +164,8 @@ export class Continuation {
145
164
  if (now === undefined) {
146
165
  return true;
147
166
  }
148
- return now.getTime() <= this._validUntil.getTime();
167
+ // Strict `<` mirrors Rust `valid_until > now`.
168
+ return now.getTime() < this._validUntil.getTime();
149
169
  }
150
170
 
151
171
  /**
@@ -181,25 +201,33 @@ export class Continuation {
181
201
  /**
182
202
  * Converts the continuation to an envelope.
183
203
  *
184
- * If a recipient is provided, the envelope is encrypted to that recipient.
204
+ * Mirrors Rust `Continuation::to_envelope`:
205
+ *
206
+ * ```rust
207
+ * self.state.wrap()
208
+ * .add_optional_assertion(ID, self.valid_id)
209
+ * .add_optional_assertion(VALID_UNTIL, self.valid_until)
210
+ * ```
211
+ *
212
+ * The state is wrapped first; the optional assertions then live on
213
+ * the wrap node. `valid_until` is encoded as a CBOR-tagged Date
214
+ * (tag 1) — never as a plain ISO 8601 string.
185
215
  *
186
216
  * @param recipient - Optional recipient to encrypt the envelope to
187
217
  * @returns The continuation as an envelope
188
218
  */
189
219
  toEnvelope(recipient?: Encrypter): Envelope {
190
- let envelope = this._state;
220
+ let envelope = this._state.wrap();
191
221
 
192
- // Add ID assertion if set
193
222
  if (this._validId !== undefined) {
194
223
  envelope = envelope.addAssertion(ID, this._validId);
195
224
  }
196
225
 
197
- // Add valid_until assertion if set
198
226
  if (this._validUntil !== undefined) {
199
- envelope = envelope.addAssertion(VALID_UNTIL, this._validUntil.toISOString());
227
+ // Pass a tagged-CBOR Date; mirrors Rust `Date → CBOR` (tag 1).
228
+ envelope = envelope.addAssertion(VALID_UNTIL, CborDate.fromDatetime(this._validUntil));
200
229
  }
201
230
 
202
- // Encrypt to recipient if provided
203
231
  if (recipient !== undefined) {
204
232
  envelope = envelope.encryptToRecipients([recipient]);
205
233
  }
@@ -210,6 +238,14 @@ export class Continuation {
210
238
  /**
211
239
  * Parses a continuation from an envelope.
212
240
  *
241
+ * Mirrors Rust `Continuation::try_from_envelope`:
242
+ *
243
+ * ```rust
244
+ * state: envelope.try_unwrap()?, // unwrap
245
+ * valid_id: envelope.extract_optional_object_for_predicate(ID)?,
246
+ * valid_until: envelope.extract_optional_object_for_predicate(VALID_UNTIL)?,
247
+ * ```
248
+ *
213
249
  * @param encryptedEnvelope - The envelope to parse
214
250
  * @param expectedId - Optional ID to validate against
215
251
  * @param now - Optional time to validate against
@@ -223,8 +259,15 @@ export class Continuation {
223
259
  now?: Date,
224
260
  recipient?: PrivateKeys,
225
261
  ): Continuation {
226
- // Decrypt if recipient is provided
227
- let envelope = encryptedEnvelope;
262
+ type EnvelopeExt = Envelope & {
263
+ decryptToRecipient(p: PrivateKeys): Envelope;
264
+ tryUnwrap(): Envelope;
265
+ objectForPredicate(p: unknown): Envelope;
266
+ optionalObjectForPredicate(p: unknown): Envelope | undefined;
267
+ asLeaf(): Cbor | undefined;
268
+ };
269
+
270
+ let envelope = encryptedEnvelope as EnvelopeExt;
228
271
  if (recipient !== undefined) {
229
272
  try {
230
273
  envelope = encryptedEnvelope.decryptToRecipient(recipient);
@@ -233,47 +276,49 @@ export class Continuation {
233
276
  }
234
277
  }
235
278
 
236
- // Extract the state (the subject of the envelope)
237
- const state = envelope.subject();
279
+ // Mirrors Rust `envelope.try_unwrap()?` peel off the
280
+ // `state.wrap()` introduced in `to_envelope`.
281
+ let state: Envelope;
282
+ try {
283
+ state = envelope.tryUnwrap();
284
+ } catch (e) {
285
+ throw GstpError.envelope(e instanceof Error ? e : new Error(String(e)));
286
+ }
238
287
 
239
- // Extract optional ID
240
288
  let validId: ARID | undefined;
241
- try {
242
- const idObj = envelope.objectForPredicate(ID);
243
- if (idObj !== undefined) {
244
- // The ID is stored as a leaf envelope containing the ARID's tagged CBOR
245
- const leafCbor = idObj.asLeaf();
246
- if (leafCbor !== undefined) {
247
- validId = ARID.fromTaggedCborData(leafCbor.toData());
289
+ const idObj = envelope.optionalObjectForPredicate(ID) as EnvelopeExt | undefined;
290
+ if (idObj !== undefined) {
291
+ const leafCbor = idObj.asLeaf();
292
+ if (leafCbor !== undefined) {
293
+ try {
294
+ validId = ARID.fromTaggedCbor(leafCbor);
295
+ } catch (e) {
296
+ throw GstpError.envelope(e instanceof Error ? e : new Error(String(e)));
248
297
  }
249
298
  }
250
- } catch {
251
- // ID is optional
252
299
  }
253
300
 
254
- // Extract optional valid_until
255
301
  let validUntil: Date | undefined;
256
- try {
257
- const validUntilObj = envelope.objectForPredicate(VALID_UNTIL);
258
- if (validUntilObj !== undefined) {
259
- const dateStr = validUntilObj.asText();
260
- if (dateStr !== undefined) {
261
- validUntil = new Date(dateStr);
302
+ const validUntilObj = envelope.optionalObjectForPredicate(VALID_UNTIL) as
303
+ | EnvelopeExt
304
+ | undefined;
305
+ if (validUntilObj !== undefined) {
306
+ const leafCbor = validUntilObj.asLeaf();
307
+ if (leafCbor !== undefined) {
308
+ try {
309
+ validUntil = CborDate.fromTaggedCbor(leafCbor).datetime();
310
+ } catch (e) {
311
+ throw GstpError.envelope(e instanceof Error ? e : new Error(String(e)));
262
312
  }
263
313
  }
264
- } catch {
265
- // valid_until is optional
266
314
  }
267
315
 
268
- // Create the continuation
269
316
  const continuation = new Continuation(state, validId, validUntil);
270
317
 
271
- // Validate date
272
318
  if (!continuation.isValidDate(now)) {
273
319
  throw GstpError.continuationExpired();
274
320
  }
275
321
 
276
- // Validate ID
277
322
  if (!continuation.isValidId(expectedId)) {
278
323
  throw GstpError.continuationIdInvalid();
279
324
  }
@@ -290,12 +335,10 @@ export class Continuation {
290
335
  * @returns true if the continuations are equal
291
336
  */
292
337
  equals(other: Continuation): boolean {
293
- // Compare state envelopes by their digests
294
338
  if (!this._state.digest().equals(other._state.digest())) {
295
339
  return false;
296
340
  }
297
341
 
298
- // Compare IDs
299
342
  if (this._validId === undefined && other._validId === undefined) {
300
343
  // Both undefined, equal
301
344
  } else if (this._validId !== undefined && other._validId !== undefined) {
@@ -303,11 +346,9 @@ export class Continuation {
303
346
  return false;
304
347
  }
305
348
  } else {
306
- // One is undefined, one is not
307
349
  return false;
308
350
  }
309
351
 
310
- // Compare valid_until
311
352
  if (this._validUntil === undefined && other._validUntil === undefined) {
312
353
  // Both undefined, equal
313
354
  } else if (this._validUntil !== undefined && other._validUntil !== undefined) {
@@ -315,7 +356,6 @@ export class Continuation {
315
356
  return false;
316
357
  }
317
358
  } else {
318
- // One is undefined, one is not
319
359
  return false;
320
360
  }
321
361
 
@@ -287,9 +287,12 @@ export class SealedEvent<T extends EnvelopeEncodableValue> implements SealedEven
287
287
  signer?: Signer,
288
288
  recipients?: XIDDocument[],
289
289
  ): Envelope {
290
- // Get sender's encryption key (from inception key)
291
- const senderInceptionKey = this._sender.inceptionKey();
292
- const senderEncryptionKey = senderInceptionKey?.publicKeys()?.encapsulationPublicKey();
290
+ // Mirrors Rust `self.sender.encryption_key()` uses the
291
+ // inception key's encapsulation public key when available, falls
292
+ // back to the first key in the document's key set. Eagerly fetched
293
+ // (matching Rust `sealed_event.rs:247–250`) so the error is raised
294
+ // even when neither state nor validUntil is present.
295
+ const senderEncryptionKey = this._sender.encryptionKey();
293
296
  if (senderEncryptionKey === undefined) {
294
297
  throw GstpError.senderMissingEncryptionKey();
295
298
  }
@@ -381,9 +384,9 @@ export class SealedEvent<T extends EnvelopeEncodableValue> implements SealedEven
381
384
  throw GstpError.xid(e instanceof Error ? e : new Error(String(e)));
382
385
  }
383
386
 
384
- // Get sender's verification key and verify signature (from inception key)
385
- const senderInceptionKey = sender.inceptionKey();
386
- const senderVerificationKey = senderInceptionKey?.publicKeys()?.signingPublicKey();
387
+ // Mirrors Rust `sender.verification_key()` (with first-key
388
+ // fallback).
389
+ const senderVerificationKey = sender.verificationKey();
387
390
  if (senderVerificationKey === undefined) {
388
391
  throw GstpError.senderMissingVerificationKey();
389
392
  }
@@ -11,7 +11,8 @@
11
11
  * Ported from gstp-rust/src/sealed_request.rs
12
12
  */
13
13
 
14
- import type { ARID, PrivateKeys, Signer } from "@bcts/components";
14
+ import { ARID, type PrivateKeys, type Signer } from "@bcts/components";
15
+ import { CborDate, type Cbor } from "@bcts/dcbor";
15
16
  import {
16
17
  Envelope,
17
18
  Request,
@@ -25,6 +26,45 @@ import { XIDDocument } from "@bcts/xid";
25
26
  import { Continuation } from "./continuation";
26
27
  import { GstpError } from "./error";
27
28
 
29
+ /**
30
+ * Decode a CBOR value into a typed JS value.
31
+ *
32
+ * Mirrors the type-driven `T: TryFrom<CBOR>` dispatch Rust's
33
+ * `extract_object_for_parameter` relies on. TS lacks compile-time
34
+ * trait dispatch, so we hand-roll the most common cases:
35
+ * - tag 1 (`Date`) → JS `Date`,
36
+ * - tag 40012 (`ARID`) → `ARID`,
37
+ * - integer / text / bool / number / byte-string primitives,
38
+ * - everything else → the raw `Cbor` value.
39
+ *
40
+ * Callers needing other typed extraction should use
41
+ * `objectForParameter()` directly and decode the envelope themselves.
42
+ */
43
+ function extractCborAsT<T>(cbor: Cbor): T {
44
+ const tagged = cbor.asTagged();
45
+ if (tagged !== undefined) {
46
+ const [tag] = tagged;
47
+ const tagNumber = Number(tag.value);
48
+ // Tag 1 — Standard date/time (RFC 8949 §3.4.2). Rust impls
49
+ // `TryFrom<CBOR> for chrono::DateTime`. We surface the JS
50
+ // `Date` to mirror what frost-hubert's typed callers expect.
51
+ if (tagNumber === 1) {
52
+ return CborDate.fromTaggedCbor(cbor).datetime() as T;
53
+ }
54
+ // Tag 40012 — ARID (`bc-tags::TAG_ARID`). Rust impls
55
+ // `TryFrom<CBOR> for ARID`.
56
+ if (tagNumber === 40012) {
57
+ return ARID.fromTaggedCbor(cbor) as T;
58
+ }
59
+ }
60
+ if (cbor.isInteger()) return cbor.toInteger() as T;
61
+ if (cbor.isText()) return cbor.toText() as T;
62
+ if (cbor.isBool()) return cbor.toBool() as T;
63
+ if (cbor.isNumber()) return cbor.toNumber() as T;
64
+ if (cbor.isByteString()) return cbor.toByteString() as T;
65
+ return cbor as T;
66
+ }
67
+
28
68
  /**
29
69
  * Interface that defines the behavior of a sealed request.
30
70
  *
@@ -188,29 +228,35 @@ export class SealedRequest implements SealedRequestBehavior {
188
228
 
189
229
  /**
190
230
  * Returns all objects for a parameter.
231
+ *
232
+ * Mirrors Rust `SealedRequest::objects_for_parameter` which delegates
233
+ * to `Expression::objects_for_parameter`. GSTP requests can carry
234
+ * multiple parameters with the same ID — e.g. a DKG invite has
235
+ * one `participant` per group member — and a decoder must see
236
+ * every one of them.
191
237
  */
192
238
  objectsForParameter(param: ParameterID): Envelope[] {
193
- const obj = this._request.body().getParameter(param);
194
- return obj !== undefined ? [obj] : [];
239
+ return this._request.body().objectsForParameter(param);
195
240
  }
196
241
 
197
242
  /**
198
243
  * Extracts an object for a parameter as a specific type.
244
+ *
245
+ * Mirrors Rust `SealedRequest::extract_object_for_parameter` — Rust
246
+ * uses a `T: TryFrom<CBOR>` constraint and dispatches to whatever
247
+ * `From<CBOR> for T` impl is in scope (e.g. tag-1 CBOR decodes to
248
+ * `chrono::DateTime`, tag-40012 to `ARID`, etc.). TS lacks that
249
+ * trait dispatch, so we recognise the most common tagged types
250
+ * (`Date` via tag 1, `ARID` via tag 40012) plus the primitive
251
+ * fall-through. Callers needing other typed extraction should use
252
+ * `objectForParameter()` directly and decode the envelope themselves.
199
253
  */
200
254
  extractObjectForParameter<T>(param: ParameterID): T {
201
255
  const envelope = this.objectForParameter(param);
202
256
  if (envelope === undefined) {
203
257
  throw GstpError.envelope(new Error(`Parameter not found: ${param}`));
204
258
  }
205
- return envelope.extractSubject((cbor) => {
206
- // Extract primitive value from CBOR
207
- if (cbor.isInteger()) return cbor.toInteger() as T;
208
- if (cbor.isText()) return cbor.toText() as T;
209
- if (cbor.isBool()) return cbor.toBool() as T;
210
- if (cbor.isNumber()) return cbor.toNumber() as T;
211
- if (cbor.isByteString()) return cbor.toByteString() as T;
212
- return cbor as T;
213
- });
259
+ return envelope.extractSubject((cbor) => extractCborAsT<T>(cbor));
214
260
  }
215
261
 
216
262
  /**
@@ -221,15 +267,7 @@ export class SealedRequest implements SealedRequestBehavior {
221
267
  if (envelope === undefined) {
222
268
  return undefined;
223
269
  }
224
- return envelope.extractSubject((cbor) => {
225
- // Extract primitive value from CBOR
226
- if (cbor.isInteger()) return cbor.toInteger() as T;
227
- if (cbor.isText()) return cbor.toText() as T;
228
- if (cbor.isBool()) return cbor.toBool() as T;
229
- if (cbor.isNumber()) return cbor.toNumber() as T;
230
- if (cbor.isByteString()) return cbor.toByteString() as T;
231
- return cbor as T;
232
- });
270
+ return envelope.extractSubject((cbor) => extractCborAsT<T>(cbor));
233
271
  }
234
272
 
235
273
  /**
@@ -404,9 +442,12 @@ export class SealedRequest implements SealedRequestBehavior {
404
442
  const stateEnvelope = this._state ?? Envelope.new(null);
405
443
  const continuation = new Continuation(stateEnvelope, this.id(), validUntil);
406
444
 
407
- // Get sender's encryption key (from inception key)
408
- const senderInceptionKey = this._sender.inceptionKey();
409
- const senderEncryptionKey = senderInceptionKey?.publicKeys()?.encapsulationPublicKey();
445
+ // Mirrors Rust `self.sender.encryption_key()` which prefers the
446
+ // inception key's encapsulation public key but falls back to the
447
+ // first key in the document's key set. The earlier port called
448
+ // `inceptionKey()?.publicKeys()?.encapsulationPublicKey()` directly
449
+ // and skipped that fallback.
450
+ const senderEncryptionKey = this._sender.encryptionKey();
410
451
  if (senderEncryptionKey === undefined) {
411
452
  throw GstpError.senderMissingEncryptionKey();
412
453
  }
@@ -485,9 +526,10 @@ export class SealedRequest implements SealedRequestBehavior {
485
526
  throw GstpError.xid(e instanceof Error ? e : new Error(String(e)));
486
527
  }
487
528
 
488
- // Get sender's verification key and verify signature (from inception key)
489
- const senderInceptionKey = sender.inceptionKey();
490
- const senderVerificationKey = senderInceptionKey?.publicKeys()?.signingPublicKey();
529
+ // Mirrors Rust `sender.verification_key()` (with first-key
530
+ // fallback). The earlier port read the inception signing key
531
+ // directly and skipped that fallback.
532
+ const senderVerificationKey = sender.verificationKey();
491
533
  if (senderVerificationKey === undefined) {
492
534
  throw GstpError.senderMissingVerificationKey();
493
535
  }
@@ -325,9 +325,10 @@ export class SealedResponse implements SealedResponseBehavior {
325
325
  if (this._state !== undefined) {
326
326
  const continuation = new Continuation(this._state, undefined, validUntil);
327
327
 
328
- // Get sender's encryption key (from inception key)
329
- const senderInceptionKey = this._sender.inceptionKey();
330
- const senderEncryptionKey = senderInceptionKey?.publicKeys()?.encapsulationPublicKey();
328
+ // Mirrors Rust `self.sender.encryption_key()` uses the
329
+ // inception key's encapsulation public key when available, falls
330
+ // back to the first key in the document's key set.
331
+ const senderEncryptionKey = this._sender.encryptionKey();
331
332
  if (senderEncryptionKey === undefined) {
332
333
  throw GstpError.senderMissingEncryptionKey();
333
334
  }
@@ -408,9 +409,9 @@ export class SealedResponse implements SealedResponseBehavior {
408
409
  throw GstpError.xid(e instanceof Error ? e : new Error(String(e)));
409
410
  }
410
411
 
411
- // Get sender's verification key and verify signature (from inception key)
412
- const senderInceptionKey = sender.inceptionKey();
413
- const senderVerificationKey = senderInceptionKey?.publicKeys()?.signingPublicKey();
412
+ // Mirrors Rust `sender.verification_key()` (with first-key
413
+ // fallback).
414
+ const senderVerificationKey = sender.verificationKey();
414
415
  if (senderVerificationKey === undefined) {
415
416
  throw GstpError.senderMissingVerificationKey();
416
417
  }
@@ -1,18 +0,0 @@
1
- //#region \0rolldown/runtime.js
2
- var __defProp = Object.defineProperty;
3
- var __exportAll = (all, no_symbols) => {
4
- let target = {};
5
- for (var name in all) {
6
- __defProp(target, name, {
7
- get: all[name],
8
- enumerable: true
9
- });
10
- }
11
- if (!no_symbols) {
12
- __defProp(target, Symbol.toStringTag, { value: "Module" });
13
- }
14
- return target;
15
- };
16
-
17
- //#endregion
18
- export { __exportAll as t };