@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.
@@ -81,6 +81,12 @@ interface OpenKeyAuthorizeTinyCloud {
81
81
  * validates and size-bounds it before use.
82
82
  */
83
83
  presentation?: CapabilityPresentationEnvelopeV1;
84
+ /**
85
+ * Ask OpenKey to let the user change the session expiry in the approval.
86
+ * Sent only when true. `@openkey/sdk` >= 0.13.0 (beta: 0.13.0-beta.0)
87
+ * forwards it; 0.12.x ignores it.
88
+ */
89
+ expiryChangeable?: boolean;
84
90
  }, opts?: {
85
91
  mode?: "iframe" | "popup" | "redirect";
86
92
  }): Promise<{
@@ -114,6 +120,12 @@ interface OpenKeyBridgeInput {
114
120
  * manifest. Display-only.
115
121
  */
116
122
  presentation?: CapabilityPresentationEnvelopeV1;
123
+ /**
124
+ * Ask OpenKey to let the user change the session expiry. Forwarded only
125
+ * when true. The SDK accepts a changed expiry only for a request that
126
+ * carried this flag, and then uses the SIGNED expiry.
127
+ */
128
+ expiryChangeable?: boolean;
117
129
  }
118
130
  type NodeUserAuthorizationAuthorizeFn = (input: OpenKeyBridgeInput) => Promise<{
119
131
  protocolVersion: 1;
@@ -160,7 +172,14 @@ interface PreparedOpenKeyAuthorization {
160
172
  readonly chainId: number;
161
173
  readonly generation: number;
162
174
  readonly createdAt: number;
175
+ /** The app-level maximum for a changed signed expiry, if one was given. In memory only; never sent or persisted. */
176
+ readonly maxExpiresAt?: OpenKeyMaxExpiresAt;
163
177
  }
178
+ /**
179
+ * A latest acceptable signed expiry: epoch MILLISECONDS, a `Date`, or a function returning either. A function is read
180
+ * when the OpenKey result arrives, not when the request is prepared.
181
+ */
182
+ type OpenKeyMaxExpiresAt = number | Date | (() => number | Date);
164
183
  type OpenKeyAuthorizationResultV1 = Awaited<ReturnType<NodeUserAuthorizationAuthorizeFn>>;
165
184
  type PreparedOpenKeySession = {
166
185
  siwe: string;
@@ -636,6 +655,13 @@ declare class NodeUserAuthorization implements IUserAuthorization {
636
655
  private guardSpaceCreation;
637
656
  private recordActivationSkippedSpaces;
638
657
  createBootstrapSession(options: CreateBootstrapSessionOptions): Promise<TinyCloudSession>;
658
+ /**
659
+ * When a session that starts at `now` ends. Refused with a dated message when
660
+ * the configured lifetime reaches past what a sign-in message can carry,
661
+ * instead of the bare `RangeError` or WASM parse error `toISOString()` and
662
+ * `prepareSession` would otherwise give.
663
+ */
664
+ private sessionExpiryFrom;
639
665
  private persistedData;
640
666
  /**
641
667
  * Sign in and create a new session.
@@ -747,6 +773,21 @@ declare class NodeUserAuthorization implements IUserAuthorization {
747
773
  openkeyKeyId?: string;
748
774
  /** Caller-supplied display reason, not signed authority. */
749
775
  reason?: string;
776
+ /**
777
+ * Let the user change the session expiry in the OpenKey approval. Sent
778
+ * to OpenKey as `expiryChangeable: true` (`@openkey/sdk` >= 0.13.0
779
+ * (beta: 0.13.0-beta.0) forwards it; 0.12.x ignores it). The SIGNED
780
+ * expiry is then the session's expiry.
781
+ */
782
+ expiryChangeable?: boolean;
783
+ /**
784
+ * The latest signed expiry the caller will accept, used only with `expiryChangeable`: a signed expiry beyond it
785
+ * is refused before any session is built, stored or activated. Epoch MILLISECONDS or a `Date`; a function is
786
+ * called when the result arrives (so a slow approval does not count against it) and returns either. It is not
787
+ * sent to OpenKey and not persisted. It limits a CHANGED expiry only: an expiry equal to the one this SDK
788
+ * requested is accepted, so keep the configured session lifetime within the maximum.
789
+ */
790
+ maxExpiresAt?: OpenKeyMaxExpiresAt;
750
791
  /**
751
792
  * Supply another candidate without regenerating the SIWE or session key.
752
793
  * Called only after transport or pre-commit verification rejects a result.
@@ -803,8 +844,18 @@ declare class NodeUserAuthorization implements IUserAuthorization {
803
844
  * in the original prepared SIWE — narrowing OK, broadening rejected.
804
845
  * 5. Every `result.selectedActionKeys` entry corresponds to a
805
846
  * (resource, action) pair in the returned SIWE.
847
+ *
848
+ * `options.expiryChangeable` is true only when the request sent to OpenKey
849
+ * carried `expiryChangeable: true`. Then (and only then) the signed
850
+ * `Expiration Time` may differ from the prepared one and is the expiry the
851
+ * session is built with; the changed expiry must pass
852
+ * `checkChangedExpiration` and every other field in (3) must still match.
853
+ * Without it, any expiry difference is refused as in (3).
806
854
  */
807
- signInWithOpenKeyResult(result: OpenKeyAuthorizationResultV1, prepared: PreparedOpenKeySession, keyId: string, jwk: Record<string, unknown>): Promise<ClientSession>;
855
+ signInWithOpenKeyResult(result: OpenKeyAuthorizationResultV1, prepared: PreparedOpenKeySession, keyId: string, jwk: Record<string, unknown>, options?: {
856
+ expiryChangeable?: boolean;
857
+ maxExpiresAt?: OpenKeyMaxExpiresAt;
858
+ }): Promise<ClientSession>;
808
859
  /** Validate a candidate without activating a network session, so paste retries reuse one preparation. */
809
860
  private verifyOpenKeyResult;
810
861
  /**
@@ -1686,7 +1737,7 @@ declare class TinyCloudNode {
1686
1737
  */
1687
1738
  signInWithOpenKey(authorizeFn: Parameters<NodeUserAuthorization["signInWithOpenKey"]>[0], options?: Parameters<NodeUserAuthorization["signInWithOpenKey"]>[1]): Promise<void>;
1688
1739
  /** Prepare the OpenKey request before the approval click. */
1689
- prepareOpenKeySignIn(): Promise<PreparedOpenKeyAuthorization>;
1740
+ prepareOpenKeySignIn(options?: Parameters<NodeUserAuthorization["prepareOpenKeySignIn"]>[0]): Promise<PreparedOpenKeyAuthorization>;
1690
1741
  /** Start OpenKey authorization synchronously, then complete normal session setup. */
1691
1742
  signInWithPreparedOpenKey(pending: PreparedOpenKeyAuthorization, authorizeFn: Parameters<NodeUserAuthorization["signInWithOpenKey"]>[0]): Promise<void>;
1692
1743
  private signInWith;
@@ -2095,6 +2146,19 @@ declare class TinyCloudNode {
2095
2146
  * @internal
2096
2147
  */
2097
2148
  private getSessionExpiry;
2149
+ /**
2150
+ * The expiry of the session that was actually signed, so the registry root
2151
+ * and the sharing service never outlive or undercut what the SIWE grants
2152
+ * (a user may change the expiry in the OpenKey approval).
2153
+ *
2154
+ * Any expiry the SIWE parses to is used exactly as signed, however near or
2155
+ * far. The configured lifetime is used only when the SIWE cannot be parsed
2156
+ * (the session is already established, so failing sign-in here would be
2157
+ * worse; real sign-ins verify the SIWE first) or when a valid SIWE carries
2158
+ * no `Expiration Time` line, as the other `extractSiweExpiration` callers do.
2159
+ * @internal
2160
+ */
2161
+ private getSignedSessionExpiry;
2098
2162
  /**
2099
2163
  * Wrapper for the WASM createDelegation function.
2100
2164
  *
@@ -81,6 +81,12 @@ interface OpenKeyAuthorizeTinyCloud {
81
81
  * validates and size-bounds it before use.
82
82
  */
83
83
  presentation?: CapabilityPresentationEnvelopeV1;
84
+ /**
85
+ * Ask OpenKey to let the user change the session expiry in the approval.
86
+ * Sent only when true. `@openkey/sdk` >= 0.13.0 (beta: 0.13.0-beta.0)
87
+ * forwards it; 0.12.x ignores it.
88
+ */
89
+ expiryChangeable?: boolean;
84
90
  }, opts?: {
85
91
  mode?: "iframe" | "popup" | "redirect";
86
92
  }): Promise<{
@@ -114,6 +120,12 @@ interface OpenKeyBridgeInput {
114
120
  * manifest. Display-only.
115
121
  */
116
122
  presentation?: CapabilityPresentationEnvelopeV1;
123
+ /**
124
+ * Ask OpenKey to let the user change the session expiry. Forwarded only
125
+ * when true. The SDK accepts a changed expiry only for a request that
126
+ * carried this flag, and then uses the SIGNED expiry.
127
+ */
128
+ expiryChangeable?: boolean;
117
129
  }
118
130
  type NodeUserAuthorizationAuthorizeFn = (input: OpenKeyBridgeInput) => Promise<{
119
131
  protocolVersion: 1;
@@ -160,7 +172,14 @@ interface PreparedOpenKeyAuthorization {
160
172
  readonly chainId: number;
161
173
  readonly generation: number;
162
174
  readonly createdAt: number;
175
+ /** The app-level maximum for a changed signed expiry, if one was given. In memory only; never sent or persisted. */
176
+ readonly maxExpiresAt?: OpenKeyMaxExpiresAt;
163
177
  }
178
+ /**
179
+ * A latest acceptable signed expiry: epoch MILLISECONDS, a `Date`, or a function returning either. A function is read
180
+ * when the OpenKey result arrives, not when the request is prepared.
181
+ */
182
+ type OpenKeyMaxExpiresAt = number | Date | (() => number | Date);
164
183
  type OpenKeyAuthorizationResultV1 = Awaited<ReturnType<NodeUserAuthorizationAuthorizeFn>>;
165
184
  type PreparedOpenKeySession = {
166
185
  siwe: string;
@@ -636,6 +655,13 @@ declare class NodeUserAuthorization implements IUserAuthorization {
636
655
  private guardSpaceCreation;
637
656
  private recordActivationSkippedSpaces;
638
657
  createBootstrapSession(options: CreateBootstrapSessionOptions): Promise<TinyCloudSession>;
658
+ /**
659
+ * When a session that starts at `now` ends. Refused with a dated message when
660
+ * the configured lifetime reaches past what a sign-in message can carry,
661
+ * instead of the bare `RangeError` or WASM parse error `toISOString()` and
662
+ * `prepareSession` would otherwise give.
663
+ */
664
+ private sessionExpiryFrom;
639
665
  private persistedData;
640
666
  /**
641
667
  * Sign in and create a new session.
@@ -747,6 +773,21 @@ declare class NodeUserAuthorization implements IUserAuthorization {
747
773
  openkeyKeyId?: string;
748
774
  /** Caller-supplied display reason, not signed authority. */
749
775
  reason?: string;
776
+ /**
777
+ * Let the user change the session expiry in the OpenKey approval. Sent
778
+ * to OpenKey as `expiryChangeable: true` (`@openkey/sdk` >= 0.13.0
779
+ * (beta: 0.13.0-beta.0) forwards it; 0.12.x ignores it). The SIGNED
780
+ * expiry is then the session's expiry.
781
+ */
782
+ expiryChangeable?: boolean;
783
+ /**
784
+ * The latest signed expiry the caller will accept, used only with `expiryChangeable`: a signed expiry beyond it
785
+ * is refused before any session is built, stored or activated. Epoch MILLISECONDS or a `Date`; a function is
786
+ * called when the result arrives (so a slow approval does not count against it) and returns either. It is not
787
+ * sent to OpenKey and not persisted. It limits a CHANGED expiry only: an expiry equal to the one this SDK
788
+ * requested is accepted, so keep the configured session lifetime within the maximum.
789
+ */
790
+ maxExpiresAt?: OpenKeyMaxExpiresAt;
750
791
  /**
751
792
  * Supply another candidate without regenerating the SIWE or session key.
752
793
  * Called only after transport or pre-commit verification rejects a result.
@@ -803,8 +844,18 @@ declare class NodeUserAuthorization implements IUserAuthorization {
803
844
  * in the original prepared SIWE — narrowing OK, broadening rejected.
804
845
  * 5. Every `result.selectedActionKeys` entry corresponds to a
805
846
  * (resource, action) pair in the returned SIWE.
847
+ *
848
+ * `options.expiryChangeable` is true only when the request sent to OpenKey
849
+ * carried `expiryChangeable: true`. Then (and only then) the signed
850
+ * `Expiration Time` may differ from the prepared one and is the expiry the
851
+ * session is built with; the changed expiry must pass
852
+ * `checkChangedExpiration` and every other field in (3) must still match.
853
+ * Without it, any expiry difference is refused as in (3).
806
854
  */
807
- signInWithOpenKeyResult(result: OpenKeyAuthorizationResultV1, prepared: PreparedOpenKeySession, keyId: string, jwk: Record<string, unknown>): Promise<ClientSession>;
855
+ signInWithOpenKeyResult(result: OpenKeyAuthorizationResultV1, prepared: PreparedOpenKeySession, keyId: string, jwk: Record<string, unknown>, options?: {
856
+ expiryChangeable?: boolean;
857
+ maxExpiresAt?: OpenKeyMaxExpiresAt;
858
+ }): Promise<ClientSession>;
808
859
  /** Validate a candidate without activating a network session, so paste retries reuse one preparation. */
809
860
  private verifyOpenKeyResult;
810
861
  /**
@@ -1686,7 +1737,7 @@ declare class TinyCloudNode {
1686
1737
  */
1687
1738
  signInWithOpenKey(authorizeFn: Parameters<NodeUserAuthorization["signInWithOpenKey"]>[0], options?: Parameters<NodeUserAuthorization["signInWithOpenKey"]>[1]): Promise<void>;
1688
1739
  /** Prepare the OpenKey request before the approval click. */
1689
- prepareOpenKeySignIn(): Promise<PreparedOpenKeyAuthorization>;
1740
+ prepareOpenKeySignIn(options?: Parameters<NodeUserAuthorization["prepareOpenKeySignIn"]>[0]): Promise<PreparedOpenKeyAuthorization>;
1690
1741
  /** Start OpenKey authorization synchronously, then complete normal session setup. */
1691
1742
  signInWithPreparedOpenKey(pending: PreparedOpenKeyAuthorization, authorizeFn: Parameters<NodeUserAuthorization["signInWithOpenKey"]>[0]): Promise<void>;
1692
1743
  private signInWith;
@@ -2095,6 +2146,19 @@ declare class TinyCloudNode {
2095
2146
  * @internal
2096
2147
  */
2097
2148
  private getSessionExpiry;
2149
+ /**
2150
+ * The expiry of the session that was actually signed, so the registry root
2151
+ * and the sharing service never outlive or undercut what the SIWE grants
2152
+ * (a user may change the expiry in the OpenKey approval).
2153
+ *
2154
+ * Any expiry the SIWE parses to is used exactly as signed, however near or
2155
+ * far. The configured lifetime is used only when the SIWE cannot be parsed
2156
+ * (the session is already established, so failing sign-in here would be
2157
+ * worse; real sign-ins verify the SIWE first) or when a valid SIWE carries
2158
+ * no `Expiration Time` line, as the other `extractSiweExpiration` callers do.
2159
+ * @internal
2160
+ */
2161
+ private getSignedSessionExpiry;
2098
2162
  /**
2099
2163
  * Wrapper for the WASM createDelegation function.
2100
2164
  *
@@ -1,7 +1,7 @@
1
1
  import { ReplicationOptions, PermissionEntry, TinyCloudSession, KVReplicaStorage, ServiceContext, IWasmBindings, WasmRecapEntry } from '@tinycloud/sdk-core';
2
2
  import { ReplicaConsentAnchor, AuthorityRefusal, ReplicationAuthority } from '@tinycloud/sdk-services';
3
3
  import { ReplicaLocalEraseOutcome, ReplicaPurgeLocalCopyResult, ReplicaPurgeTrigger, ReplicaPurgeFailedEvent, ReplicaSessionStamp, ReplicaDenyInput } from '@tinycloud/sdk-services/kv/replication';
4
- import { T as TinyCloudNode } from './TinyCloudNode-BhWBbCay.cjs';
4
+ import { T as TinyCloudNode } from './TinyCloudNode-CnVaWnAX.cjs';
5
5
 
6
6
  /** Constructor-time validation; the space-dependent secrets check runs at sign-in and open. */
7
7
  declare function assertValidReplicationConfig(replication: (ReplicationOptions & {
@@ -1,7 +1,7 @@
1
1
  import { ReplicationOptions, PermissionEntry, TinyCloudSession, KVReplicaStorage, ServiceContext, IWasmBindings, WasmRecapEntry } from '@tinycloud/sdk-core';
2
2
  import { ReplicaConsentAnchor, AuthorityRefusal, ReplicationAuthority } from '@tinycloud/sdk-services';
3
3
  import { ReplicaLocalEraseOutcome, ReplicaPurgeLocalCopyResult, ReplicaPurgeTrigger, ReplicaPurgeFailedEvent, ReplicaSessionStamp, ReplicaDenyInput } from '@tinycloud/sdk-services/kv/replication';
4
- import { T as TinyCloudNode } from './TinyCloudNode-BjJJIifr.js';
4
+ import { T as TinyCloudNode } from './TinyCloudNode-BlNcvo1c.js';
5
5
 
6
6
  /** Constructor-time validation; the space-dependent secrets check runs at sign-in and open. */
7
7
  declare function assertValidReplicationConfig(replication: (ReplicationOptions & {
package/dist/core.cjs CHANGED
@@ -454,6 +454,16 @@ var import_sdk_core4 = require("@tinycloud/sdk-core");
454
454
  var defaultSignStrategy = { type: "auto-sign" };
455
455
 
456
456
  // src/authorization/NodeUserAuthorization.ts
457
+ function assertMaxExpiresAt(value) {
458
+ if (value === void 0 || typeof value === "function") return;
459
+ const ms = value instanceof Date ? value.getTime() : value;
460
+ if (typeof ms !== "number" || !Number.isFinite(new Date(ms).getTime())) {
461
+ throw new Error("maxExpiresAt must be a finite number of epoch milliseconds, a valid Date, or a function returning one");
462
+ }
463
+ }
464
+ function lastAnchoredLine(siwe, pattern) {
465
+ return [...siwe.matchAll(pattern)].at(-1)?.[1];
466
+ }
457
467
  function pkhSpaceIdentity(spaceId) {
458
468
  const match = /^(tinycloud:pkh:eip155:\d+:)(0x[0-9a-fA-F]{40})(:.+)$/.exec(spaceId);
459
469
  return match === null ? spaceId : `${match[1]}${match[2].toLowerCase()}${match[3]}`;
@@ -1427,7 +1437,7 @@ var NodeUserAuthorization = class {
1427
1437
  }
1428
1438
  const jwk = JSON.parse(jwkString);
1429
1439
  const now = /* @__PURE__ */ new Date();
1430
- const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
1440
+ const expirationTime = this.sessionExpiryFrom(now);
1431
1441
  const primaryIdentity = pkhSpaceIdentity(options.spaceId);
1432
1442
  const resourcesBySpace = /* @__PURE__ */ new Map();
1433
1443
  for (const resource of options.capabilityRequest.resources) {
@@ -1482,6 +1492,17 @@ var NodeUserAuthorization = class {
1482
1492
  signature
1483
1493
  };
1484
1494
  }
1495
+ /**
1496
+ * When a session that starts at `now` ends. Refused with a dated message when
1497
+ * the configured lifetime reaches past what a sign-in message can carry,
1498
+ * instead of the bare `RangeError` or WASM parse error `toISOString()` and
1499
+ * `prepareSession` would otherwise give.
1500
+ */
1501
+ sessionExpiryFrom(now) {
1502
+ const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
1503
+ (0, import_sdk_core5.assertExpiryWithinTechnicalLimit)(expirationTime, "siwe");
1504
+ return expirationTime;
1505
+ }
1485
1506
  persistedData(session, expiresAt, createdAt, replicaStamp) {
1486
1507
  return {
1487
1508
  address: session.address,
@@ -1537,7 +1558,7 @@ var NodeUserAuthorization = class {
1537
1558
  const capabilityPlan = this.resolveSignInCapabilities(address, chainId);
1538
1559
  const spaceId = capabilityPlan.spaceId;
1539
1560
  const now = /* @__PURE__ */ new Date();
1540
- const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
1561
+ const expirationTime = this.sessionExpiryFrom(now);
1541
1562
  const prepared = this.prepareSessionWithResources({
1542
1563
  abilities: capabilityPlan.abilities,
1543
1564
  ...capabilityPlan.spaceAbilities !== void 0 ? { spaceAbilities: capabilityPlan.spaceAbilities } : {},
@@ -1683,7 +1704,7 @@ var NodeUserAuthorization = class {
1683
1704
  const capabilityPlan = this.resolveSignInCapabilities(address, chainId);
1684
1705
  const spaceId = capabilityPlan.spaceId;
1685
1706
  const now = /* @__PURE__ */ new Date();
1686
- const expirationTime = new Date(now.getTime() + this.sessionExpirationMs);
1707
+ const expirationTime = this.sessionExpiryFrom(now);
1687
1708
  const prepared = this.prepareSessionWithResources({
1688
1709
  abilities: capabilityPlan.abilities,
1689
1710
  ...capabilityPlan.spaceAbilities !== void 0 ? { spaceAbilities: capabilityPlan.spaceAbilities } : {},
@@ -1746,10 +1767,8 @@ var NodeUserAuthorization = class {
1746
1767
  siwe: prepared.siwe,
1747
1768
  signature
1748
1769
  };
1749
- const expirationMatch = prepared.siwe?.match(/Expiration Time: (.+)/);
1750
- const issuedAtMatch = prepared.siwe?.match(/Issued At: (.+)/);
1751
- const expiresAt = expirationMatch?.[1] ?? new Date(Date.now() + this.sessionExpirationMs).toISOString();
1752
- const createdAt = issuedAtMatch?.[1] ?? (/* @__PURE__ */ new Date()).toISOString();
1770
+ const expiresAt = lastAnchoredLine(prepared.siwe ?? "", /^Expiration Time: (.+)$/gm) ?? this.sessionExpiryFrom(/* @__PURE__ */ new Date()).toISOString();
1771
+ const createdAt = lastAnchoredLine(prepared.siwe ?? "", /^Issued At: (.+)$/gm) ?? (/* @__PURE__ */ new Date()).toISOString();
1753
1772
  const replicaStamp = await this.establishReplicaStamp?.(tinyCloudSession);
1754
1773
  const persistedData = this.persistedData(tinyCloudSession, expiresAt, createdAt, replicaStamp);
1755
1774
  await this.sessionStorage.save(address, persistedData);
@@ -1808,6 +1827,7 @@ var NodeUserAuthorization = class {
1808
1827
  /** Finish address, host and SIWE preparation before an approval click. */
1809
1828
  async prepareOpenKeySignIn(options) {
1810
1829
  refuseCaveated(this.getCapabilityRequest()?.manifests);
1830
+ assertMaxExpiresAt(options?.maxExpiresAt);
1811
1831
  if (options?.host !== void 0) {
1812
1832
  const overrideHost = options.host.trim();
1813
1833
  if (!overrideHost) {
@@ -1828,7 +1848,9 @@ var NodeUserAuthorization = class {
1828
1848
  verificationMethod: prepared.verificationMethod,
1829
1849
  host: hostHint,
1830
1850
  keyId: options?.openkeyKeyId,
1831
- presentation
1851
+ presentation,
1852
+ // Present only when set, so the default request shape is unchanged.
1853
+ ...options?.expiryChangeable === true ? { expiryChangeable: true } : {}
1832
1854
  };
1833
1855
  return {
1834
1856
  request: originalRequest,
@@ -1838,7 +1860,10 @@ var NodeUserAuthorization = class {
1838
1860
  address,
1839
1861
  chainId,
1840
1862
  generation: this.sessionGeneration,
1841
- createdAt: Date.now()
1863
+ createdAt: Date.now(),
1864
+ // Kept in memory with the pending request. It is not part of `request`, so it is never sent to OpenKey, and
1865
+ // nothing here is persisted.
1866
+ ...options?.maxExpiresAt === void 0 ? {} : { maxExpiresAt: options.maxExpiresAt }
1842
1867
  };
1843
1868
  }
1844
1869
  /** Complete a prepared approval. The caller starts authorization before awaiting this method. */
@@ -1856,7 +1881,11 @@ var NodeUserAuthorization = class {
1856
1881
  for (let attempt = 0; ; ) {
1857
1882
  try {
1858
1883
  result ?? (result = await authorization);
1859
- signed = await this.verifyOpenKeyResult(result, prepared);
1884
+ signed = await this.verifyOpenKeyResult(result, prepared, {
1885
+ // Relax only for a request that actually carried the opt-in.
1886
+ allowExpirationChange: originalRequest.expiryChangeable === true,
1887
+ maxExpiresAt: pending.maxExpiresAt
1888
+ });
1860
1889
  signature = result.signature;
1861
1890
  break;
1862
1891
  } catch (error) {
@@ -1948,13 +1977,23 @@ var NodeUserAuthorization = class {
1948
1977
  * in the original prepared SIWE — narrowing OK, broadening rejected.
1949
1978
  * 5. Every `result.selectedActionKeys` entry corresponds to a
1950
1979
  * (resource, action) pair in the returned SIWE.
1951
- */
1952
- async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
1953
- const signed = await this.verifyOpenKeyResult(result, prepared);
1980
+ *
1981
+ * `options.expiryChangeable` is true only when the request sent to OpenKey
1982
+ * carried `expiryChangeable: true`. Then (and only then) the signed
1983
+ * `Expiration Time` may differ from the prepared one and is the expiry the
1984
+ * session is built with; the changed expiry must pass
1985
+ * `checkChangedExpiration` and every other field in (3) must still match.
1986
+ * Without it, any expiry difference is refused as in (3).
1987
+ */
1988
+ async signInWithOpenKeyResult(result, prepared, keyId, jwk, options) {
1989
+ const signed = await this.verifyOpenKeyResult(result, prepared, {
1990
+ allowExpirationChange: options?.expiryChangeable === true,
1991
+ maxExpiresAt: options?.maxExpiresAt
1992
+ });
1954
1993
  return this.signInWithPreparedSession(signed, result.signature, keyId, jwk);
1955
1994
  }
1956
1995
  /** Validate a candidate without activating a network session, so paste retries reuse one preparation. */
1957
- async verifyOpenKeyResult(result, prepared) {
1996
+ async verifyOpenKeyResult(result, prepared, options) {
1958
1997
  if (result.protocolVersion !== 1) {
1959
1998
  throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
1960
1999
  }
@@ -1997,16 +2036,39 @@ var NodeUserAuthorization = class {
1997
2036
  `OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
1998
2037
  );
1999
2038
  }
2039
+ const parsedExpiry = parsedSignedSiwe.expirationTime;
2040
+ const lineExpiry = lastAnchoredLine(result.signedMessage, /^Expiration Time: (.+)$/gm);
2041
+ if (parsedExpiry === void 0 || parsedExpiry === null || lineExpiry === void 0) {
2042
+ throw new Error("OpenKey signedMessage has no Expiration Time line of its own");
2043
+ }
2044
+ const parsedExpiryMs = Date.parse(parsedExpiry);
2045
+ if (!Number.isFinite(parsedExpiryMs) || parsedExpiryMs !== Date.parse(lineExpiry)) {
2046
+ throw new Error("OpenKey signedMessage Expiration Time line does not match the message's own expiry");
2047
+ }
2000
2048
  const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
2001
2049
  const originalFields = (0, import_sdk_core5.extractImmutableSiweFields)(prepared.siwe);
2002
2050
  const signedFields = (0, import_sdk_core5.extractImmutableSiweFields)(result.signedMessage);
2003
- const changedFields = (0, import_sdk_core5.diffImmutableSiweFields)(originalFields, signedFields, {
2004
- originalHasRecap
2005
- });
2006
- if (changedFields.length > 0) {
2007
- throw new Error(
2008
- `OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
2009
- );
2051
+ if (options?.allowExpirationChange === true) {
2052
+ const maxExpiresAt = typeof options.maxExpiresAt === "function" ? options.maxExpiresAt() : options.maxExpiresAt;
2053
+ const refusal = (0, import_sdk_core5.checkChangedExpiration)({
2054
+ preparedSiwe: prepared.siwe,
2055
+ signedSiwe: result.signedMessage,
2056
+ now: Date.now(),
2057
+ originalHasRecap,
2058
+ ...maxExpiresAt === void 0 ? {} : { maxExpiresAt }
2059
+ });
2060
+ if (refusal !== null) {
2061
+ throw new Error(`OpenKey signedMessage ${refusal}`);
2062
+ }
2063
+ } else {
2064
+ const changedFields = (0, import_sdk_core5.diffImmutableSiweFields)(originalFields, signedFields, {
2065
+ originalHasRecap
2066
+ });
2067
+ if (changedFields.length > 0) {
2068
+ throw new Error(
2069
+ `OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
2070
+ );
2071
+ }
2010
2072
  }
2011
2073
  let originalCaps;
2012
2074
  let signedCaps;
@@ -2287,7 +2349,9 @@ function wireOpenKeyAuthorize(openkey, opts) {
2287
2349
  host: input.host,
2288
2350
  // Forward the presentation envelope verbatim — the OpenKey SDK
2289
2351
  // validates and size-bounds it before use.
2290
- presentation: input.presentation
2352
+ presentation: input.presentation,
2353
+ // Sent only when true, so a request without it keeps its prior shape.
2354
+ ...input.expiryChangeable === true ? { expiryChangeable: true } : {}
2291
2355
  }, opts);
2292
2356
  if (result.protocolVersion !== 1) {
2293
2357
  throw new Error(
@@ -4882,14 +4946,14 @@ var _TinyCloudNode = class _TinyCloudNode {
4882
4946
  await this.signInWith(() => this.auth.signInWithOpenKey(authorizeFn, options));
4883
4947
  }
4884
4948
  /** Prepare the OpenKey request before the approval click. */
4885
- async prepareOpenKeySignIn() {
4949
+ async prepareOpenKeySignIn(options) {
4886
4950
  if (!this.signer || !this.tc || !this.auth) {
4887
4951
  throw new Error("Cannot prepare OpenKey sign-in in session-only mode. Provide a signer in config.");
4888
4952
  }
4889
4953
  await this.signOutInFlight;
4890
4954
  await this.accountRegistryTail;
4891
4955
  await this.wasmBindings.ensureInitialized?.();
4892
- return this.auth.prepareOpenKeySignIn();
4956
+ return this.auth.prepareOpenKeySignIn(options);
4893
4957
  }
4894
4958
  /** Start OpenKey authorization synchronously, then complete normal session setup. */
4895
4959
  signInWithPreparedOpenKey(pending, authorizeFn) {
@@ -7473,7 +7537,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7473
7537
  path: "",
7474
7538
  // Root access
7475
7539
  actions: [...ROOT_DELEGATION_ACTIONS],
7476
- expiry: this.getSessionExpiry(),
7540
+ expiry: this.getSignedSessionExpiry(),
7477
7541
  isRevoked: false,
7478
7542
  allowSubDelegation: true
7479
7543
  };
@@ -7486,7 +7550,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7486
7550
  spaceId,
7487
7551
  path: "",
7488
7552
  actions: [...ROOT_DELEGATION_ACTIONS],
7489
- expiry: this.getSessionExpiry(),
7553
+ expiry: this.getSignedSessionExpiry(),
7490
7554
  isRevoked: false,
7491
7555
  allowSubDelegation: true
7492
7556
  });
@@ -7578,7 +7642,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7578
7642
  assertActive: () => graph.assertActive(),
7579
7643
  keyProvider: this._keyProvider,
7580
7644
  registry: this._capabilityRegistry,
7581
- sessionExpiry: this.getSessionExpiry(),
7645
+ sessionExpiry: this.getSignedSessionExpiry(),
7582
7646
  createDelegationWasm: (params) => {
7583
7647
  graph.assertActive();
7584
7648
  return this.createDelegationWrapper(params);
@@ -7625,6 +7689,29 @@ var _TinyCloudNode = class _TinyCloudNode {
7625
7689
  const expirationMs = this.config.sessionExpirationMs ?? DEFAULT_SESSION_EXPIRATION_MS;
7626
7690
  return new Date(Date.now() + expirationMs);
7627
7691
  }
7692
+ /**
7693
+ * The expiry of the session that was actually signed, so the registry root
7694
+ * and the sharing service never outlive or undercut what the SIWE grants
7695
+ * (a user may change the expiry in the OpenKey approval).
7696
+ *
7697
+ * Any expiry the SIWE parses to is used exactly as signed, however near or
7698
+ * far. The configured lifetime is used only when the SIWE cannot be parsed
7699
+ * (the session is already established, so failing sign-in here would be
7700
+ * worse; real sign-ins verify the SIWE first) or when a valid SIWE carries
7701
+ * no `Expiration Time` line, as the other `extractSiweExpiration` callers do.
7702
+ * @internal
7703
+ */
7704
+ getSignedSessionExpiry() {
7705
+ const siwe = this.auth?.tinyCloudSession?.siwe;
7706
+ if (!siwe) return this.getSessionExpiry();
7707
+ let signed;
7708
+ try {
7709
+ signed = extractSiweExpiration(siwe);
7710
+ } catch {
7711
+ return this.getSessionExpiry();
7712
+ }
7713
+ return signed ?? this.getSessionExpiry();
7714
+ }
7628
7715
  /**
7629
7716
  * Wrapper for the WASM createDelegation function.
7630
7717
  *
@@ -7654,6 +7741,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7654
7741
  // DID here and rejects the fragment as an invalid audience DID.
7655
7742
  verificationMethod: params.session.verificationMethod.split("#", 1)[0]
7656
7743
  };
7744
+ (0, import_sdk_core11.assertExpiryWithinTechnicalLimit)(params.expirationSecs * 1e3, "ucan");
7657
7745
  const result = this.wasmBindings.createDelegation(
7658
7746
  wasmSession,
7659
7747
  params.delegateDID,
@@ -7686,9 +7774,10 @@ var _TinyCloudNode = class _TinyCloudNode {
7686
7774
  }
7687
7775
  const permissions = ownerDelegationPermissions(params);
7688
7776
  const now = /* @__PURE__ */ new Date();
7689
- if (!(params.expiresAt instanceof Date) || params.expiresAt.getTime() <= now.getTime() || params.expiresAt.getTime() - now.getTime() > import_sdk_core11.EXPIRY.MAX_MS) {
7690
- throw new Error("Owner delegation expiry must be explicit, future, and within EXPIRY.MAX_MS");
7777
+ if (!(params.expiresAt instanceof Date) || params.expiresAt.getTime() <= now.getTime()) {
7778
+ throw new Error("Owner delegation expiry must be explicit, future, and a valid date");
7691
7779
  }
7780
+ (0, import_sdk_core11.assertExpiryWithinTechnicalLimit)(params.expiresAt, "siwe");
7692
7781
  if (!this.signer) throw new Error("Owner wallet signer is required");
7693
7782
  const session = this.currentTinyCloudSession();
7694
7783
  if (!session) throw new Error("Owner session is required");
@@ -7789,6 +7878,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7789
7878
  * not sent.
7790
7879
  */
7791
7880
  async authorizeShareDeliveryV3(input) {
7881
+ (0, import_sdk_core11.assertExpiryWithinTechnicalLimit)(new Date(input.expiresAt), "siwe");
7792
7882
  const envelopeRef = await (0, import_sdk_core11.shortDeliveryLinkBodyCid)(input.shareUrl, input.trustedOrigins);
7793
7883
  const session = this.currentTinyCloudSession();
7794
7884
  const serviceSession = this._serviceContext?.session;
@@ -7841,6 +7931,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7841
7931
  };
7842
7932
  }
7843
7933
  async createRootDelegationForSharing(params, assertActive) {
7934
+ (0, import_sdk_core11.assertExpiryWithinTechnicalLimit)(params.requestedExpiry, "siwe");
7844
7935
  try {
7845
7936
  return (await this.createOwnerDelegation({
7846
7937
  delegateDid: params.shareKeyDID,
@@ -8537,6 +8628,7 @@ var _TinyCloudNode = class _TinyCloudNode {
8537
8628
  if (sessionExpiry !== void 0 && sessionExpiry < expiresAt) {
8538
8629
  expiresAt = sessionExpiry;
8539
8630
  }
8631
+ (0, import_sdk_core11.assertExpiryWithinTechnicalLimit)(expiresAt, "siwe");
8540
8632
  const delegations = [];
8541
8633
  let rawEntriesAttached = false;
8542
8634
  for (const [spaceId, entries] of bySpace) {
@@ -10009,6 +10101,7 @@ var _TinyCloudNode = class _TinyCloudNode {
10009
10101
  const now = /* @__PURE__ */ new Date();
10010
10102
  const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
10011
10103
  const expirationTime = new Date(now.getTime() + expiryMs);
10104
+ (0, import_sdk_core11.assertExpiryWithinTechnicalLimit)(expirationTime, "siwe");
10012
10105
  const prepared = this.wasmBindings.prepareSession({
10013
10106
  abilities,
10014
10107
  address: this.wasmBindings.ensureEip55(session.address),
@@ -10163,6 +10256,7 @@ var _TinyCloudNode = class _TinyCloudNode {
10163
10256
  const { abilities, rawAbilities } = this.buildActivationAbilities(delegation);
10164
10257
  const now = /* @__PURE__ */ new Date();
10165
10258
  const expirationTime = delegation.expiry;
10259
+ (0, import_sdk_core11.assertExpiryWithinTechnicalLimit)(expirationTime, "siwe");
10166
10260
  const prepared = this.wasmBindings.prepareSession({
10167
10261
  abilities,
10168
10262
  address: this.wasmBindings.ensureEip55(mySession.address),
@@ -10282,6 +10376,7 @@ var _TinyCloudNode = class _TinyCloudNode {
10282
10376
  const now = /* @__PURE__ */ new Date();
10283
10377
  const requestedExpiry = params.expiryMs === void 0 ? parentDelegation.expiry : new Date(now.getTime() + params.expiryMs);
10284
10378
  const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
10379
+ (0, import_sdk_core11.assertExpiryWithinTechnicalLimit)(actualExpiry, "siwe");
10285
10380
  const abilities = {};
10286
10381
  const rawAbilities = {};
10287
10382
  for (const resource of requestedResources) {