@tinycloud/node-sdk 3.10.0 → 3.11.0-beta.1

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-CnVaWnAX.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-BlNcvo1c.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]}`;
@@ -19942,7 +19952,7 @@ var NodeUserAuthorization = class {
19942
19952
  }
19943
19953
  const jwk = JSON.parse(jwkString);
19944
19954
  const now = /* @__PURE__ */ new Date();
19945
- const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
19955
+ const expirationTime = this.sessionExpiryFrom(now);
19946
19956
  const primaryIdentity = pkhSpaceIdentity(options.spaceId);
19947
19957
  const resourcesBySpace = /* @__PURE__ */ new Map();
19948
19958
  for (const resource of options.capabilityRequest.resources) {
@@ -19997,6 +20007,17 @@ var NodeUserAuthorization = class {
19997
20007
  signature: signature2
19998
20008
  };
19999
20009
  }
20010
+ /**
20011
+ * When a session that starts at `now` ends. Refused with a dated message when
20012
+ * the configured lifetime reaches past what a sign-in message can carry,
20013
+ * instead of the bare `RangeError` or WASM parse error `toISOString()` and
20014
+ * `prepareSession` would otherwise give.
20015
+ */
20016
+ sessionExpiryFrom(now) {
20017
+ const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
20018
+ (0, import_sdk_core5.assertExpiryWithinTechnicalLimit)(expirationTime, "siwe");
20019
+ return expirationTime;
20020
+ }
20000
20021
  persistedData(session, expiresAt, createdAt, replicaStamp) {
20001
20022
  return {
20002
20023
  address: session.address,
@@ -20052,7 +20073,7 @@ var NodeUserAuthorization = class {
20052
20073
  const capabilityPlan = this.resolveSignInCapabilities(address, chainId);
20053
20074
  const spaceId = capabilityPlan.spaceId;
20054
20075
  const now = /* @__PURE__ */ new Date();
20055
- const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
20076
+ const expirationTime = this.sessionExpiryFrom(now);
20056
20077
  const prepared = this.prepareSessionWithResources({
20057
20078
  abilities: capabilityPlan.abilities,
20058
20079
  ...capabilityPlan.spaceAbilities !== void 0 ? { spaceAbilities: capabilityPlan.spaceAbilities } : {},
@@ -20198,7 +20219,7 @@ var NodeUserAuthorization = class {
20198
20219
  const capabilityPlan = this.resolveSignInCapabilities(address, chainId);
20199
20220
  const spaceId = capabilityPlan.spaceId;
20200
20221
  const now = /* @__PURE__ */ new Date();
20201
- const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
20222
+ const expirationTime = this.sessionExpiryFrom(now);
20202
20223
  const prepared = this.prepareSessionWithResources({
20203
20224
  abilities: capabilityPlan.abilities,
20204
20225
  ...capabilityPlan.spaceAbilities !== void 0 ? { spaceAbilities: capabilityPlan.spaceAbilities } : {},
@@ -20261,10 +20282,8 @@ var NodeUserAuthorization = class {
20261
20282
  siwe: prepared.siwe,
20262
20283
  signature: signature2
20263
20284
  };
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();
20285
+ const expiresAt = lastAnchoredLine(prepared.siwe ?? "", /^Expiration Time: (.+)$/gm) ?? this.sessionExpiryFrom(/* @__PURE__ */ new Date()).toISOString();
20286
+ const createdAt = lastAnchoredLine(prepared.siwe ?? "", /^Issued At: (.+)$/gm) ?? (/* @__PURE__ */ new Date()).toISOString();
20268
20287
  const replicaStamp = await this.establishReplicaStamp?.(tinyCloudSession);
20269
20288
  const persistedData = this.persistedData(tinyCloudSession, expiresAt, createdAt, replicaStamp);
20270
20289
  await this.sessionStorage.save(address, persistedData);
@@ -20323,6 +20342,7 @@ var NodeUserAuthorization = class {
20323
20342
  /** Finish address, host and SIWE preparation before an approval click. */
20324
20343
  async prepareOpenKeySignIn(options) {
20325
20344
  refuseCaveated(this.getCapabilityRequest()?.manifests);
20345
+ assertMaxExpiresAt(options?.maxExpiresAt);
20326
20346
  if (options?.host !== void 0) {
20327
20347
  const overrideHost = options.host.trim();
20328
20348
  if (!overrideHost) {
@@ -20343,7 +20363,9 @@ var NodeUserAuthorization = class {
20343
20363
  verificationMethod: prepared.verificationMethod,
20344
20364
  host: hostHint,
20345
20365
  keyId: options?.openkeyKeyId,
20346
- presentation
20366
+ presentation,
20367
+ // Present only when set, so the default request shape is unchanged.
20368
+ ...options?.expiryChangeable === true ? { expiryChangeable: true } : {}
20347
20369
  };
20348
20370
  return {
20349
20371
  request: originalRequest,
@@ -20353,7 +20375,10 @@ var NodeUserAuthorization = class {
20353
20375
  address,
20354
20376
  chainId,
20355
20377
  generation: this.sessionGeneration,
20356
- createdAt: Date.now()
20378
+ createdAt: Date.now(),
20379
+ // Kept in memory with the pending request. It is not part of `request`, so it is never sent to OpenKey, and
20380
+ // nothing here is persisted.
20381
+ ...options?.maxExpiresAt === void 0 ? {} : { maxExpiresAt: options.maxExpiresAt }
20357
20382
  };
20358
20383
  }
20359
20384
  /** Complete a prepared approval. The caller starts authorization before awaiting this method. */
@@ -20371,7 +20396,11 @@ var NodeUserAuthorization = class {
20371
20396
  for (let attempt = 0; ; ) {
20372
20397
  try {
20373
20398
  result ?? (result = await authorization);
20374
- signed2 = await this.verifyOpenKeyResult(result, prepared);
20399
+ signed2 = await this.verifyOpenKeyResult(result, prepared, {
20400
+ // Relax only for a request that actually carried the opt-in.
20401
+ allowExpirationChange: originalRequest.expiryChangeable === true,
20402
+ maxExpiresAt: pending.maxExpiresAt
20403
+ });
20375
20404
  signature2 = result.signature;
20376
20405
  break;
20377
20406
  } catch (error) {
@@ -20463,13 +20492,23 @@ var NodeUserAuthorization = class {
20463
20492
  * in the original prepared SIWE — narrowing OK, broadening rejected.
20464
20493
  * 5. Every `result.selectedActionKeys` entry corresponds to a
20465
20494
  * (resource, action) pair in the returned SIWE.
20495
+ *
20496
+ * `options.expiryChangeable` is true only when the request sent to OpenKey
20497
+ * carried `expiryChangeable: true`. Then (and only then) the signed
20498
+ * `Expiration Time` may differ from the prepared one and is the expiry the
20499
+ * session is built with; the changed expiry must pass
20500
+ * `checkChangedExpiration` and every other field in (3) must still match.
20501
+ * Without it, any expiry difference is refused as in (3).
20466
20502
  */
20467
- async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
20468
- const signed2 = await this.verifyOpenKeyResult(result, prepared);
20503
+ async signInWithOpenKeyResult(result, prepared, keyId, jwk, options) {
20504
+ const signed2 = await this.verifyOpenKeyResult(result, prepared, {
20505
+ allowExpirationChange: options?.expiryChangeable === true,
20506
+ maxExpiresAt: options?.maxExpiresAt
20507
+ });
20469
20508
  return this.signInWithPreparedSession(signed2, result.signature, keyId, jwk);
20470
20509
  }
20471
20510
  /** Validate a candidate without activating a network session, so paste retries reuse one preparation. */
20472
- async verifyOpenKeyResult(result, prepared) {
20511
+ async verifyOpenKeyResult(result, prepared, options) {
20473
20512
  if (result.protocolVersion !== 1) {
20474
20513
  throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
20475
20514
  }
@@ -20512,16 +20551,39 @@ var NodeUserAuthorization = class {
20512
20551
  `OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
20513
20552
  );
20514
20553
  }
20554
+ const parsedExpiry = parsedSignedSiwe.expirationTime;
20555
+ const lineExpiry = lastAnchoredLine(result.signedMessage, /^Expiration Time: (.+)$/gm);
20556
+ if (parsedExpiry === void 0 || parsedExpiry === null || lineExpiry === void 0) {
20557
+ throw new Error("OpenKey signedMessage has no Expiration Time line of its own");
20558
+ }
20559
+ const parsedExpiryMs = Date.parse(parsedExpiry);
20560
+ if (!Number.isFinite(parsedExpiryMs) || parsedExpiryMs !== Date.parse(lineExpiry)) {
20561
+ throw new Error("OpenKey signedMessage Expiration Time line does not match the message's own expiry");
20562
+ }
20515
20563
  const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
20516
20564
  const originalFields = (0, import_sdk_core5.extractImmutableSiweFields)(prepared.siwe);
20517
20565
  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
- );
20566
+ if (options?.allowExpirationChange === true) {
20567
+ const maxExpiresAt = typeof options.maxExpiresAt === "function" ? options.maxExpiresAt() : options.maxExpiresAt;
20568
+ const refusal = (0, import_sdk_core5.checkChangedExpiration)({
20569
+ preparedSiwe: prepared.siwe,
20570
+ signedSiwe: result.signedMessage,
20571
+ now: Date.now(),
20572
+ originalHasRecap,
20573
+ ...maxExpiresAt === void 0 ? {} : { maxExpiresAt }
20574
+ });
20575
+ if (refusal !== null) {
20576
+ throw new Error(`OpenKey signedMessage ${refusal}`);
20577
+ }
20578
+ } else {
20579
+ const changedFields = (0, import_sdk_core5.diffImmutableSiweFields)(originalFields, signedFields, {
20580
+ originalHasRecap
20581
+ });
20582
+ if (changedFields.length > 0) {
20583
+ throw new Error(
20584
+ `OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
20585
+ );
20586
+ }
20525
20587
  }
20526
20588
  let originalCaps;
20527
20589
  let signedCaps;
@@ -22706,14 +22768,14 @@ var _TinyCloudNode = class _TinyCloudNode {
22706
22768
  await this.signInWith(() => this.auth.signInWithOpenKey(authorizeFn, options));
22707
22769
  }
22708
22770
  /** Prepare the OpenKey request before the approval click. */
22709
- async prepareOpenKeySignIn() {
22771
+ async prepareOpenKeySignIn(options) {
22710
22772
  if (!this.signer || !this.tc || !this.auth) {
22711
22773
  throw new Error("Cannot prepare OpenKey sign-in in session-only mode. Provide a signer in config.");
22712
22774
  }
22713
22775
  await this.signOutInFlight;
22714
22776
  await this.accountRegistryTail;
22715
22777
  await this.wasmBindings.ensureInitialized?.();
22716
- return this.auth.prepareOpenKeySignIn();
22778
+ return this.auth.prepareOpenKeySignIn(options);
22717
22779
  }
22718
22780
  /** Start OpenKey authorization synchronously, then complete normal session setup. */
22719
22781
  signInWithPreparedOpenKey(pending, authorizeFn) {
@@ -25297,7 +25359,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25297
25359
  path: "",
25298
25360
  // Root access
25299
25361
  actions: [...ROOT_DELEGATION_ACTIONS],
25300
- expiry: this.getSessionExpiry(),
25362
+ expiry: this.getSignedSessionExpiry(),
25301
25363
  isRevoked: false,
25302
25364
  allowSubDelegation: true
25303
25365
  };
@@ -25310,7 +25372,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25310
25372
  spaceId,
25311
25373
  path: "",
25312
25374
  actions: [...ROOT_DELEGATION_ACTIONS],
25313
- expiry: this.getSessionExpiry(),
25375
+ expiry: this.getSignedSessionExpiry(),
25314
25376
  isRevoked: false,
25315
25377
  allowSubDelegation: true
25316
25378
  });
@@ -25402,7 +25464,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25402
25464
  assertActive: () => graph.assertActive(),
25403
25465
  keyProvider: this._keyProvider,
25404
25466
  registry: this._capabilityRegistry,
25405
- sessionExpiry: this.getSessionExpiry(),
25467
+ sessionExpiry: this.getSignedSessionExpiry(),
25406
25468
  createDelegationWasm: (params) => {
25407
25469
  graph.assertActive();
25408
25470
  return this.createDelegationWrapper(params);
@@ -25449,6 +25511,29 @@ var _TinyCloudNode = class _TinyCloudNode {
25449
25511
  const expirationMs = this.config.sessionExpirationMs ?? DEFAULT_SESSION_EXPIRATION_MS;
25450
25512
  return new Date(Date.now() + expirationMs);
25451
25513
  }
25514
+ /**
25515
+ * The expiry of the session that was actually signed, so the registry root
25516
+ * and the sharing service never outlive or undercut what the SIWE grants
25517
+ * (a user may change the expiry in the OpenKey approval).
25518
+ *
25519
+ * Any expiry the SIWE parses to is used exactly as signed, however near or
25520
+ * far. The configured lifetime is used only when the SIWE cannot be parsed
25521
+ * (the session is already established, so failing sign-in here would be
25522
+ * worse; real sign-ins verify the SIWE first) or when a valid SIWE carries
25523
+ * no `Expiration Time` line, as the other `extractSiweExpiration` callers do.
25524
+ * @internal
25525
+ */
25526
+ getSignedSessionExpiry() {
25527
+ const siwe = this.auth?.tinyCloudSession?.siwe;
25528
+ if (!siwe) return this.getSessionExpiry();
25529
+ let signed2;
25530
+ try {
25531
+ signed2 = extractSiweExpiration(siwe);
25532
+ } catch {
25533
+ return this.getSessionExpiry();
25534
+ }
25535
+ return signed2 ?? this.getSessionExpiry();
25536
+ }
25452
25537
  /**
25453
25538
  * Wrapper for the WASM createDelegation function.
25454
25539
  *
@@ -25478,6 +25563,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25478
25563
  // DID here and rejects the fragment as an invalid audience DID.
25479
25564
  verificationMethod: params.session.verificationMethod.split("#", 1)[0]
25480
25565
  };
25566
+ (0, import_sdk_core10.assertExpiryWithinTechnicalLimit)(params.expirationSecs * 1e3, "ucan");
25481
25567
  const result = this.wasmBindings.createDelegation(
25482
25568
  wasmSession,
25483
25569
  params.delegateDID,
@@ -25510,9 +25596,10 @@ var _TinyCloudNode = class _TinyCloudNode {
25510
25596
  }
25511
25597
  const permissions = ownerDelegationPermissions(params);
25512
25598
  const now = /* @__PURE__ */ new Date();
25513
- if (!(params.expiresAt instanceof Date) || params.expiresAt.getTime() <= now.getTime() || params.expiresAt.getTime() - now.getTime() > import_sdk_core10.EXPIRY.MAX_MS) {
25514
- throw new Error("Owner delegation expiry must be explicit, future, and within EXPIRY.MAX_MS");
25599
+ if (!(params.expiresAt instanceof Date) || params.expiresAt.getTime() <= now.getTime()) {
25600
+ throw new Error("Owner delegation expiry must be explicit, future, and a valid date");
25515
25601
  }
25602
+ (0, import_sdk_core10.assertExpiryWithinTechnicalLimit)(params.expiresAt, "siwe");
25516
25603
  if (!this.signer) throw new Error("Owner wallet signer is required");
25517
25604
  const session = this.currentTinyCloudSession();
25518
25605
  if (!session) throw new Error("Owner session is required");
@@ -25613,6 +25700,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25613
25700
  * not sent.
25614
25701
  */
25615
25702
  async authorizeShareDeliveryV3(input) {
25703
+ (0, import_sdk_core10.assertExpiryWithinTechnicalLimit)(new Date(input.expiresAt), "siwe");
25616
25704
  const envelopeRef = await (0, import_sdk_core10.shortDeliveryLinkBodyCid)(input.shareUrl, input.trustedOrigins);
25617
25705
  const session = this.currentTinyCloudSession();
25618
25706
  const serviceSession = this._serviceContext?.session;
@@ -25665,6 +25753,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25665
25753
  };
25666
25754
  }
25667
25755
  async createRootDelegationForSharing(params, assertActive) {
25756
+ (0, import_sdk_core10.assertExpiryWithinTechnicalLimit)(params.requestedExpiry, "siwe");
25668
25757
  try {
25669
25758
  return (await this.createOwnerDelegation({
25670
25759
  delegateDid: params.shareKeyDID,
@@ -26361,6 +26450,7 @@ var _TinyCloudNode = class _TinyCloudNode {
26361
26450
  if (sessionExpiry !== void 0 && sessionExpiry < expiresAt) {
26362
26451
  expiresAt = sessionExpiry;
26363
26452
  }
26453
+ (0, import_sdk_core10.assertExpiryWithinTechnicalLimit)(expiresAt, "siwe");
26364
26454
  const delegations = [];
26365
26455
  let rawEntriesAttached = false;
26366
26456
  for (const [spaceId, entries] of bySpace) {
@@ -27833,6 +27923,7 @@ var _TinyCloudNode = class _TinyCloudNode {
27833
27923
  const now = /* @__PURE__ */ new Date();
27834
27924
  const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
27835
27925
  const expirationTime = new Date(now.getTime() + expiryMs);
27926
+ (0, import_sdk_core10.assertExpiryWithinTechnicalLimit)(expirationTime, "siwe");
27836
27927
  const prepared = this.wasmBindings.prepareSession({
27837
27928
  abilities,
27838
27929
  address: this.wasmBindings.ensureEip55(session.address),
@@ -27987,6 +28078,7 @@ var _TinyCloudNode = class _TinyCloudNode {
27987
28078
  const { abilities, rawAbilities } = this.buildActivationAbilities(delegation);
27988
28079
  const now = /* @__PURE__ */ new Date();
27989
28080
  const expirationTime = delegation.expiry;
28081
+ (0, import_sdk_core10.assertExpiryWithinTechnicalLimit)(expirationTime, "siwe");
27990
28082
  const prepared = this.wasmBindings.prepareSession({
27991
28083
  abilities,
27992
28084
  address: this.wasmBindings.ensureEip55(mySession.address),
@@ -28106,6 +28198,7 @@ var _TinyCloudNode = class _TinyCloudNode {
28106
28198
  const now = /* @__PURE__ */ new Date();
28107
28199
  const requestedExpiry = params.expiryMs === void 0 ? parentDelegation.expiry : new Date(now.getTime() + params.expiryMs);
28108
28200
  const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
28201
+ (0, import_sdk_core10.assertExpiryWithinTechnicalLimit)(actualExpiry, "siwe");
28109
28202
  const abilities = {};
28110
28203
  const rawAbilities = {};
28111
28204
  for (const resource of requestedResources) {
@@ -28335,7 +28428,9 @@ function wireOpenKeyAuthorize(openkey, opts) {
28335
28428
  host: input.host,
28336
28429
  // Forward the presentation envelope verbatim — the OpenKey SDK
28337
28430
  // validates and size-bounds it before use.
28338
- presentation: input.presentation
28431
+ presentation: input.presentation,
28432
+ // Sent only when true, so a request without it keeps its prior shape.
28433
+ ...input.expiryChangeable === true ? { expiryChangeable: true } : {}
28339
28434
  }, opts);
28340
28435
  if (result.protocolVersion !== 1) {
28341
28436
  throw new Error(