@tinycloud/node-sdk 3.10.0-beta.3 → 3.11.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.
@@ -1,6 +1,6 @@
1
1
  import { NamedKeyClient } from '@tinycloud/sdk-services/encryption/named-keys';
2
2
  export { DecryptManyResult, KeyCallOptions, KeyDescriptor, KeyEncryptOptions, NamedKeyClient, NamedKeyEnvelope, NamedKeyError, NamedKeyErrorCode, PublicKeyDescriptor, buildKeyAdminFacts, buildKeyDescribeFacts } from '@tinycloud/sdk-services/encryption/named-keys';
3
- import { T as TinyCloudNode } from './TinyCloudNode-BhWBbCay.cjs';
3
+ import { T as TinyCloudNode } from './TinyCloudNode-wAQN1Ndh.cjs';
4
4
  import '@tinycloud/sdk-core';
5
5
  import '@tinycloud/sdk-services/kv/replication';
6
6
  import '@tinycloud/sdk-services';
@@ -1,6 +1,6 @@
1
1
  import { NamedKeyClient } from '@tinycloud/sdk-services/encryption/named-keys';
2
2
  export { DecryptManyResult, KeyCallOptions, KeyDescriptor, KeyEncryptOptions, NamedKeyClient, NamedKeyEnvelope, NamedKeyError, NamedKeyErrorCode, PublicKeyDescriptor, buildKeyAdminFacts, buildKeyDescribeFacts } from '@tinycloud/sdk-services/encryption/named-keys';
3
- import { T as TinyCloudNode } from './TinyCloudNode-BjJJIifr.js';
3
+ import { T as TinyCloudNode } from './TinyCloudNode-BpolXzu5.js';
4
4
  import '@tinycloud/sdk-core';
5
5
  import '@tinycloud/sdk-services/kv/replication';
6
6
  import '@tinycloud/sdk-services';
package/dist/index.cjs CHANGED
@@ -18969,6 +18969,16 @@ var MemorySessionStorage = class {
18969
18969
  };
18970
18970
 
18971
18971
  // src/authorization/NodeUserAuthorization.ts
18972
+ function assertMaxExpiresAt(value) {
18973
+ if (value === void 0 || typeof value === "function") return;
18974
+ const ms = value instanceof Date ? value.getTime() : value;
18975
+ if (typeof ms !== "number" || !Number.isFinite(new Date(ms).getTime())) {
18976
+ throw new Error("maxExpiresAt must be a finite number of epoch milliseconds, a valid Date, or a function returning one");
18977
+ }
18978
+ }
18979
+ function lastAnchoredLine(siwe, pattern) {
18980
+ return [...siwe.matchAll(pattern)].at(-1)?.[1];
18981
+ }
18972
18982
  function pkhSpaceIdentity(spaceId) {
18973
18983
  const match = /^(tinycloud:pkh:eip155:\d+:)(0x[0-9a-fA-F]{40})(:.+)$/.exec(spaceId);
18974
18984
  return match === null ? spaceId : `${match[1]}${match[2].toLowerCase()}${match[3]}`;
@@ -20261,10 +20271,8 @@ var NodeUserAuthorization = class {
20261
20271
  siwe: prepared.siwe,
20262
20272
  signature: signature2
20263
20273
  };
20264
- const expirationMatch = prepared.siwe?.match(/Expiration Time: (.+)/);
20265
- const issuedAtMatch = prepared.siwe?.match(/Issued At: (.+)/);
20266
- const expiresAt = expirationMatch?.[1] ?? new Date(Date.now() + this.sessionExpirationMs).toISOString();
20267
- const createdAt = issuedAtMatch?.[1] ?? (/* @__PURE__ */ new Date()).toISOString();
20274
+ const expiresAt = lastAnchoredLine(prepared.siwe ?? "", /^Expiration Time: (.+)$/gm) ?? new Date(Date.now() + this.sessionExpirationMs).toISOString();
20275
+ const createdAt = lastAnchoredLine(prepared.siwe ?? "", /^Issued At: (.+)$/gm) ?? (/* @__PURE__ */ new Date()).toISOString();
20268
20276
  const replicaStamp = await this.establishReplicaStamp?.(tinyCloudSession);
20269
20277
  const persistedData = this.persistedData(tinyCloudSession, expiresAt, createdAt, replicaStamp);
20270
20278
  await this.sessionStorage.save(address, persistedData);
@@ -20323,6 +20331,7 @@ var NodeUserAuthorization = class {
20323
20331
  /** Finish address, host and SIWE preparation before an approval click. */
20324
20332
  async prepareOpenKeySignIn(options) {
20325
20333
  refuseCaveated(this.getCapabilityRequest()?.manifests);
20334
+ assertMaxExpiresAt(options?.maxExpiresAt);
20326
20335
  if (options?.host !== void 0) {
20327
20336
  const overrideHost = options.host.trim();
20328
20337
  if (!overrideHost) {
@@ -20343,7 +20352,9 @@ var NodeUserAuthorization = class {
20343
20352
  verificationMethod: prepared.verificationMethod,
20344
20353
  host: hostHint,
20345
20354
  keyId: options?.openkeyKeyId,
20346
- presentation
20355
+ presentation,
20356
+ // Present only when set, so the default request shape is unchanged.
20357
+ ...options?.expiryChangeable === true ? { expiryChangeable: true } : {}
20347
20358
  };
20348
20359
  return {
20349
20360
  request: originalRequest,
@@ -20353,7 +20364,10 @@ var NodeUserAuthorization = class {
20353
20364
  address,
20354
20365
  chainId,
20355
20366
  generation: this.sessionGeneration,
20356
- createdAt: Date.now()
20367
+ createdAt: Date.now(),
20368
+ // Kept in memory with the pending request. It is not part of `request`, so it is never sent to OpenKey, and
20369
+ // nothing here is persisted.
20370
+ ...options?.maxExpiresAt === void 0 ? {} : { maxExpiresAt: options.maxExpiresAt }
20357
20371
  };
20358
20372
  }
20359
20373
  /** Complete a prepared approval. The caller starts authorization before awaiting this method. */
@@ -20371,7 +20385,11 @@ var NodeUserAuthorization = class {
20371
20385
  for (let attempt = 0; ; ) {
20372
20386
  try {
20373
20387
  result ?? (result = await authorization);
20374
- signed2 = await this.verifyOpenKeyResult(result, prepared);
20388
+ signed2 = await this.verifyOpenKeyResult(result, prepared, {
20389
+ // Relax only for a request that actually carried the opt-in.
20390
+ allowExpirationChange: originalRequest.expiryChangeable === true,
20391
+ maxExpiresAt: pending.maxExpiresAt
20392
+ });
20375
20393
  signature2 = result.signature;
20376
20394
  break;
20377
20395
  } catch (error) {
@@ -20463,13 +20481,23 @@ var NodeUserAuthorization = class {
20463
20481
  * in the original prepared SIWE — narrowing OK, broadening rejected.
20464
20482
  * 5. Every `result.selectedActionKeys` entry corresponds to a
20465
20483
  * (resource, action) pair in the returned SIWE.
20484
+ *
20485
+ * `options.expiryChangeable` is true only when the request sent to OpenKey
20486
+ * carried `expiryChangeable: true`. Then (and only then) the signed
20487
+ * `Expiration Time` may differ from the prepared one and is the expiry the
20488
+ * session is built with; the changed expiry must pass
20489
+ * `checkChangedExpiration` and every other field in (3) must still match.
20490
+ * Without it, any expiry difference is refused as in (3).
20466
20491
  */
20467
- async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
20468
- const signed2 = await this.verifyOpenKeyResult(result, prepared);
20492
+ async signInWithOpenKeyResult(result, prepared, keyId, jwk, options) {
20493
+ const signed2 = await this.verifyOpenKeyResult(result, prepared, {
20494
+ allowExpirationChange: options?.expiryChangeable === true,
20495
+ maxExpiresAt: options?.maxExpiresAt
20496
+ });
20469
20497
  return this.signInWithPreparedSession(signed2, result.signature, keyId, jwk);
20470
20498
  }
20471
20499
  /** Validate a candidate without activating a network session, so paste retries reuse one preparation. */
20472
- async verifyOpenKeyResult(result, prepared) {
20500
+ async verifyOpenKeyResult(result, prepared, options) {
20473
20501
  if (result.protocolVersion !== 1) {
20474
20502
  throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
20475
20503
  }
@@ -20512,16 +20540,39 @@ var NodeUserAuthorization = class {
20512
20540
  `OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
20513
20541
  );
20514
20542
  }
20543
+ const parsedExpiry = parsedSignedSiwe.expirationTime;
20544
+ const lineExpiry = lastAnchoredLine(result.signedMessage, /^Expiration Time: (.+)$/gm);
20545
+ if (parsedExpiry === void 0 || parsedExpiry === null || lineExpiry === void 0) {
20546
+ throw new Error("OpenKey signedMessage has no Expiration Time line of its own");
20547
+ }
20548
+ const parsedExpiryMs = Date.parse(parsedExpiry);
20549
+ if (!Number.isFinite(parsedExpiryMs) || parsedExpiryMs !== Date.parse(lineExpiry)) {
20550
+ throw new Error("OpenKey signedMessage Expiration Time line does not match the message's own expiry");
20551
+ }
20515
20552
  const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
20516
20553
  const originalFields = (0, import_sdk_core5.extractImmutableSiweFields)(prepared.siwe);
20517
20554
  const signedFields = (0, import_sdk_core5.extractImmutableSiweFields)(result.signedMessage);
20518
- const changedFields = (0, import_sdk_core5.diffImmutableSiweFields)(originalFields, signedFields, {
20519
- originalHasRecap
20520
- });
20521
- if (changedFields.length > 0) {
20522
- throw new Error(
20523
- `OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
20524
- );
20555
+ if (options?.allowExpirationChange === true) {
20556
+ const maxExpiresAt = typeof options.maxExpiresAt === "function" ? options.maxExpiresAt() : options.maxExpiresAt;
20557
+ const refusal = (0, import_sdk_core5.checkChangedExpiration)({
20558
+ preparedSiwe: prepared.siwe,
20559
+ signedSiwe: result.signedMessage,
20560
+ now: Date.now(),
20561
+ originalHasRecap,
20562
+ ...maxExpiresAt === void 0 ? {} : { maxExpiresAt }
20563
+ });
20564
+ if (refusal !== null) {
20565
+ throw new Error(`OpenKey signedMessage ${refusal}`);
20566
+ }
20567
+ } else {
20568
+ const changedFields = (0, import_sdk_core5.diffImmutableSiweFields)(originalFields, signedFields, {
20569
+ originalHasRecap
20570
+ });
20571
+ if (changedFields.length > 0) {
20572
+ throw new Error(
20573
+ `OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
20574
+ );
20575
+ }
20525
20576
  }
20526
20577
  let originalCaps;
20527
20578
  let signedCaps;
@@ -22706,14 +22757,14 @@ var _TinyCloudNode = class _TinyCloudNode {
22706
22757
  await this.signInWith(() => this.auth.signInWithOpenKey(authorizeFn, options));
22707
22758
  }
22708
22759
  /** Prepare the OpenKey request before the approval click. */
22709
- async prepareOpenKeySignIn() {
22760
+ async prepareOpenKeySignIn(options) {
22710
22761
  if (!this.signer || !this.tc || !this.auth) {
22711
22762
  throw new Error("Cannot prepare OpenKey sign-in in session-only mode. Provide a signer in config.");
22712
22763
  }
22713
22764
  await this.signOutInFlight;
22714
22765
  await this.accountRegistryTail;
22715
22766
  await this.wasmBindings.ensureInitialized?.();
22716
- return this.auth.prepareOpenKeySignIn();
22767
+ return this.auth.prepareOpenKeySignIn(options);
22717
22768
  }
22718
22769
  /** Start OpenKey authorization synchronously, then complete normal session setup. */
22719
22770
  signInWithPreparedOpenKey(pending, authorizeFn) {
@@ -25297,7 +25348,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25297
25348
  path: "",
25298
25349
  // Root access
25299
25350
  actions: [...ROOT_DELEGATION_ACTIONS],
25300
- expiry: this.getSessionExpiry(),
25351
+ expiry: this.getSignedSessionExpiry(),
25301
25352
  isRevoked: false,
25302
25353
  allowSubDelegation: true
25303
25354
  };
@@ -25310,7 +25361,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25310
25361
  spaceId,
25311
25362
  path: "",
25312
25363
  actions: [...ROOT_DELEGATION_ACTIONS],
25313
- expiry: this.getSessionExpiry(),
25364
+ expiry: this.getSignedSessionExpiry(),
25314
25365
  isRevoked: false,
25315
25366
  allowSubDelegation: true
25316
25367
  });
@@ -25402,7 +25453,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25402
25453
  assertActive: () => graph.assertActive(),
25403
25454
  keyProvider: this._keyProvider,
25404
25455
  registry: this._capabilityRegistry,
25405
- sessionExpiry: this.getSessionExpiry(),
25456
+ sessionExpiry: this.getSignedSessionExpiry(),
25406
25457
  createDelegationWasm: (params) => {
25407
25458
  graph.assertActive();
25408
25459
  return this.createDelegationWrapper(params);
@@ -25449,6 +25500,29 @@ var _TinyCloudNode = class _TinyCloudNode {
25449
25500
  const expirationMs = this.config.sessionExpirationMs ?? DEFAULT_SESSION_EXPIRATION_MS;
25450
25501
  return new Date(Date.now() + expirationMs);
25451
25502
  }
25503
+ /**
25504
+ * The expiry of the session that was actually signed, so the registry root
25505
+ * and the sharing service never outlive or undercut what the SIWE grants
25506
+ * (a user may change the expiry in the OpenKey approval).
25507
+ *
25508
+ * Any expiry the SIWE parses to is used exactly as signed, however near or
25509
+ * far. The configured lifetime is used only when the SIWE cannot be parsed
25510
+ * (the session is already established, so failing sign-in here would be
25511
+ * worse; real sign-ins verify the SIWE first) or when a valid SIWE carries
25512
+ * no `Expiration Time` line, as the other `extractSiweExpiration` callers do.
25513
+ * @internal
25514
+ */
25515
+ getSignedSessionExpiry() {
25516
+ const siwe = this.auth?.tinyCloudSession?.siwe;
25517
+ if (!siwe) return this.getSessionExpiry();
25518
+ let signed2;
25519
+ try {
25520
+ signed2 = extractSiweExpiration(siwe);
25521
+ } catch {
25522
+ return this.getSessionExpiry();
25523
+ }
25524
+ return signed2 ?? this.getSessionExpiry();
25525
+ }
25452
25526
  /**
25453
25527
  * Wrapper for the WASM createDelegation function.
25454
25528
  *
@@ -28335,7 +28409,9 @@ function wireOpenKeyAuthorize(openkey, opts) {
28335
28409
  host: input.host,
28336
28410
  // Forward the presentation envelope verbatim — the OpenKey SDK
28337
28411
  // validates and size-bounds it before use.
28338
- presentation: input.presentation
28412
+ presentation: input.presentation,
28413
+ // Sent only when true, so a request without it keeps its prior shape.
28414
+ ...input.expiryChangeable === true ? { expiryChangeable: true } : {}
28339
28415
  }, opts);
28340
28416
  if (result.protocolVersion !== 1) {
28341
28417
  throw new Error(