@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.
@@ -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;
@@ -747,6 +766,21 @@ declare class NodeUserAuthorization implements IUserAuthorization {
747
766
  openkeyKeyId?: string;
748
767
  /** Caller-supplied display reason, not signed authority. */
749
768
  reason?: string;
769
+ /**
770
+ * Let the user change the session expiry in the OpenKey approval. Sent
771
+ * to OpenKey as `expiryChangeable: true` (`@openkey/sdk` >= 0.13.0
772
+ * (beta: 0.13.0-beta.0) forwards it; 0.12.x ignores it). The SIGNED
773
+ * expiry is then the session's expiry.
774
+ */
775
+ expiryChangeable?: boolean;
776
+ /**
777
+ * The latest signed expiry the caller will accept, used only with `expiryChangeable`: a signed expiry beyond it
778
+ * is refused before any session is built, stored or activated. Epoch MILLISECONDS or a `Date`; a function is
779
+ * called when the result arrives (so a slow approval does not count against it) and returns either. It is not
780
+ * sent to OpenKey and not persisted. It limits a CHANGED expiry only: an expiry equal to the one this SDK
781
+ * requested is accepted, so keep the configured session lifetime within the maximum.
782
+ */
783
+ maxExpiresAt?: OpenKeyMaxExpiresAt;
750
784
  /**
751
785
  * Supply another candidate without regenerating the SIWE or session key.
752
786
  * Called only after transport or pre-commit verification rejects a result.
@@ -803,8 +837,18 @@ declare class NodeUserAuthorization implements IUserAuthorization {
803
837
  * in the original prepared SIWE — narrowing OK, broadening rejected.
804
838
  * 5. Every `result.selectedActionKeys` entry corresponds to a
805
839
  * (resource, action) pair in the returned SIWE.
840
+ *
841
+ * `options.expiryChangeable` is true only when the request sent to OpenKey
842
+ * carried `expiryChangeable: true`. Then (and only then) the signed
843
+ * `Expiration Time` may differ from the prepared one and is the expiry the
844
+ * session is built with; the changed expiry must pass
845
+ * `checkChangedExpiration` and every other field in (3) must still match.
846
+ * Without it, any expiry difference is refused as in (3).
806
847
  */
807
- signInWithOpenKeyResult(result: OpenKeyAuthorizationResultV1, prepared: PreparedOpenKeySession, keyId: string, jwk: Record<string, unknown>): Promise<ClientSession>;
848
+ signInWithOpenKeyResult(result: OpenKeyAuthorizationResultV1, prepared: PreparedOpenKeySession, keyId: string, jwk: Record<string, unknown>, options?: {
849
+ expiryChangeable?: boolean;
850
+ maxExpiresAt?: OpenKeyMaxExpiresAt;
851
+ }): Promise<ClientSession>;
808
852
  /** Validate a candidate without activating a network session, so paste retries reuse one preparation. */
809
853
  private verifyOpenKeyResult;
810
854
  /**
@@ -1686,7 +1730,7 @@ declare class TinyCloudNode {
1686
1730
  */
1687
1731
  signInWithOpenKey(authorizeFn: Parameters<NodeUserAuthorization["signInWithOpenKey"]>[0], options?: Parameters<NodeUserAuthorization["signInWithOpenKey"]>[1]): Promise<void>;
1688
1732
  /** Prepare the OpenKey request before the approval click. */
1689
- prepareOpenKeySignIn(): Promise<PreparedOpenKeyAuthorization>;
1733
+ prepareOpenKeySignIn(options?: Parameters<NodeUserAuthorization["prepareOpenKeySignIn"]>[0]): Promise<PreparedOpenKeyAuthorization>;
1690
1734
  /** Start OpenKey authorization synchronously, then complete normal session setup. */
1691
1735
  signInWithPreparedOpenKey(pending: PreparedOpenKeyAuthorization, authorizeFn: Parameters<NodeUserAuthorization["signInWithOpenKey"]>[0]): Promise<void>;
1692
1736
  private signInWith;
@@ -2095,6 +2139,19 @@ declare class TinyCloudNode {
2095
2139
  * @internal
2096
2140
  */
2097
2141
  private getSessionExpiry;
2142
+ /**
2143
+ * The expiry of the session that was actually signed, so the registry root
2144
+ * and the sharing service never outlive or undercut what the SIWE grants
2145
+ * (a user may change the expiry in the OpenKey approval).
2146
+ *
2147
+ * Any expiry the SIWE parses to is used exactly as signed, however near or
2148
+ * far. The configured lifetime is used only when the SIWE cannot be parsed
2149
+ * (the session is already established, so failing sign-in here would be
2150
+ * worse; real sign-ins verify the SIWE first) or when a valid SIWE carries
2151
+ * no `Expiration Time` line, as the other `extractSiweExpiration` callers do.
2152
+ * @internal
2153
+ */
2154
+ private getSignedSessionExpiry;
2098
2155
  /**
2099
2156
  * Wrapper for the WASM createDelegation function.
2100
2157
  *
@@ -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;
@@ -747,6 +766,21 @@ declare class NodeUserAuthorization implements IUserAuthorization {
747
766
  openkeyKeyId?: string;
748
767
  /** Caller-supplied display reason, not signed authority. */
749
768
  reason?: string;
769
+ /**
770
+ * Let the user change the session expiry in the OpenKey approval. Sent
771
+ * to OpenKey as `expiryChangeable: true` (`@openkey/sdk` >= 0.13.0
772
+ * (beta: 0.13.0-beta.0) forwards it; 0.12.x ignores it). The SIGNED
773
+ * expiry is then the session's expiry.
774
+ */
775
+ expiryChangeable?: boolean;
776
+ /**
777
+ * The latest signed expiry the caller will accept, used only with `expiryChangeable`: a signed expiry beyond it
778
+ * is refused before any session is built, stored or activated. Epoch MILLISECONDS or a `Date`; a function is
779
+ * called when the result arrives (so a slow approval does not count against it) and returns either. It is not
780
+ * sent to OpenKey and not persisted. It limits a CHANGED expiry only: an expiry equal to the one this SDK
781
+ * requested is accepted, so keep the configured session lifetime within the maximum.
782
+ */
783
+ maxExpiresAt?: OpenKeyMaxExpiresAt;
750
784
  /**
751
785
  * Supply another candidate without regenerating the SIWE or session key.
752
786
  * Called only after transport or pre-commit verification rejects a result.
@@ -803,8 +837,18 @@ declare class NodeUserAuthorization implements IUserAuthorization {
803
837
  * in the original prepared SIWE — narrowing OK, broadening rejected.
804
838
  * 5. Every `result.selectedActionKeys` entry corresponds to a
805
839
  * (resource, action) pair in the returned SIWE.
840
+ *
841
+ * `options.expiryChangeable` is true only when the request sent to OpenKey
842
+ * carried `expiryChangeable: true`. Then (and only then) the signed
843
+ * `Expiration Time` may differ from the prepared one and is the expiry the
844
+ * session is built with; the changed expiry must pass
845
+ * `checkChangedExpiration` and every other field in (3) must still match.
846
+ * Without it, any expiry difference is refused as in (3).
806
847
  */
807
- signInWithOpenKeyResult(result: OpenKeyAuthorizationResultV1, prepared: PreparedOpenKeySession, keyId: string, jwk: Record<string, unknown>): Promise<ClientSession>;
848
+ signInWithOpenKeyResult(result: OpenKeyAuthorizationResultV1, prepared: PreparedOpenKeySession, keyId: string, jwk: Record<string, unknown>, options?: {
849
+ expiryChangeable?: boolean;
850
+ maxExpiresAt?: OpenKeyMaxExpiresAt;
851
+ }): Promise<ClientSession>;
808
852
  /** Validate a candidate without activating a network session, so paste retries reuse one preparation. */
809
853
  private verifyOpenKeyResult;
810
854
  /**
@@ -1686,7 +1730,7 @@ declare class TinyCloudNode {
1686
1730
  */
1687
1731
  signInWithOpenKey(authorizeFn: Parameters<NodeUserAuthorization["signInWithOpenKey"]>[0], options?: Parameters<NodeUserAuthorization["signInWithOpenKey"]>[1]): Promise<void>;
1688
1732
  /** Prepare the OpenKey request before the approval click. */
1689
- prepareOpenKeySignIn(): Promise<PreparedOpenKeyAuthorization>;
1733
+ prepareOpenKeySignIn(options?: Parameters<NodeUserAuthorization["prepareOpenKeySignIn"]>[0]): Promise<PreparedOpenKeyAuthorization>;
1690
1734
  /** Start OpenKey authorization synchronously, then complete normal session setup. */
1691
1735
  signInWithPreparedOpenKey(pending: PreparedOpenKeyAuthorization, authorizeFn: Parameters<NodeUserAuthorization["signInWithOpenKey"]>[0]): Promise<void>;
1692
1736
  private signInWith;
@@ -2095,6 +2139,19 @@ declare class TinyCloudNode {
2095
2139
  * @internal
2096
2140
  */
2097
2141
  private getSessionExpiry;
2142
+ /**
2143
+ * The expiry of the session that was actually signed, so the registry root
2144
+ * and the sharing service never outlive or undercut what the SIWE grants
2145
+ * (a user may change the expiry in the OpenKey approval).
2146
+ *
2147
+ * Any expiry the SIWE parses to is used exactly as signed, however near or
2148
+ * far. The configured lifetime is used only when the SIWE cannot be parsed
2149
+ * (the session is already established, so failing sign-in here would be
2150
+ * worse; real sign-ins verify the SIWE first) or when a valid SIWE carries
2151
+ * no `Expiration Time` line, as the other `extractSiweExpiration` callers do.
2152
+ * @internal
2153
+ */
2154
+ private getSignedSessionExpiry;
2098
2155
  /**
2099
2156
  * Wrapper for the WASM createDelegation function.
2100
2157
  *
@@ -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-wAQN1Ndh.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-BpolXzu5.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]}`;
@@ -1746,10 +1756,8 @@ var NodeUserAuthorization = class {
1746
1756
  siwe: prepared.siwe,
1747
1757
  signature
1748
1758
  };
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();
1759
+ const expiresAt = lastAnchoredLine(prepared.siwe ?? "", /^Expiration Time: (.+)$/gm) ?? new Date(Date.now() + this.sessionExpirationMs).toISOString();
1760
+ const createdAt = lastAnchoredLine(prepared.siwe ?? "", /^Issued At: (.+)$/gm) ?? (/* @__PURE__ */ new Date()).toISOString();
1753
1761
  const replicaStamp = await this.establishReplicaStamp?.(tinyCloudSession);
1754
1762
  const persistedData = this.persistedData(tinyCloudSession, expiresAt, createdAt, replicaStamp);
1755
1763
  await this.sessionStorage.save(address, persistedData);
@@ -1808,6 +1816,7 @@ var NodeUserAuthorization = class {
1808
1816
  /** Finish address, host and SIWE preparation before an approval click. */
1809
1817
  async prepareOpenKeySignIn(options) {
1810
1818
  refuseCaveated(this.getCapabilityRequest()?.manifests);
1819
+ assertMaxExpiresAt(options?.maxExpiresAt);
1811
1820
  if (options?.host !== void 0) {
1812
1821
  const overrideHost = options.host.trim();
1813
1822
  if (!overrideHost) {
@@ -1828,7 +1837,9 @@ var NodeUserAuthorization = class {
1828
1837
  verificationMethod: prepared.verificationMethod,
1829
1838
  host: hostHint,
1830
1839
  keyId: options?.openkeyKeyId,
1831
- presentation
1840
+ presentation,
1841
+ // Present only when set, so the default request shape is unchanged.
1842
+ ...options?.expiryChangeable === true ? { expiryChangeable: true } : {}
1832
1843
  };
1833
1844
  return {
1834
1845
  request: originalRequest,
@@ -1838,7 +1849,10 @@ var NodeUserAuthorization = class {
1838
1849
  address,
1839
1850
  chainId,
1840
1851
  generation: this.sessionGeneration,
1841
- createdAt: Date.now()
1852
+ createdAt: Date.now(),
1853
+ // Kept in memory with the pending request. It is not part of `request`, so it is never sent to OpenKey, and
1854
+ // nothing here is persisted.
1855
+ ...options?.maxExpiresAt === void 0 ? {} : { maxExpiresAt: options.maxExpiresAt }
1842
1856
  };
1843
1857
  }
1844
1858
  /** Complete a prepared approval. The caller starts authorization before awaiting this method. */
@@ -1856,7 +1870,11 @@ var NodeUserAuthorization = class {
1856
1870
  for (let attempt = 0; ; ) {
1857
1871
  try {
1858
1872
  result ?? (result = await authorization);
1859
- signed = await this.verifyOpenKeyResult(result, prepared);
1873
+ signed = await this.verifyOpenKeyResult(result, prepared, {
1874
+ // Relax only for a request that actually carried the opt-in.
1875
+ allowExpirationChange: originalRequest.expiryChangeable === true,
1876
+ maxExpiresAt: pending.maxExpiresAt
1877
+ });
1860
1878
  signature = result.signature;
1861
1879
  break;
1862
1880
  } catch (error) {
@@ -1948,13 +1966,23 @@ var NodeUserAuthorization = class {
1948
1966
  * in the original prepared SIWE — narrowing OK, broadening rejected.
1949
1967
  * 5. Every `result.selectedActionKeys` entry corresponds to a
1950
1968
  * (resource, action) pair in the returned SIWE.
1951
- */
1952
- async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
1953
- const signed = await this.verifyOpenKeyResult(result, prepared);
1969
+ *
1970
+ * `options.expiryChangeable` is true only when the request sent to OpenKey
1971
+ * carried `expiryChangeable: true`. Then (and only then) the signed
1972
+ * `Expiration Time` may differ from the prepared one and is the expiry the
1973
+ * session is built with; the changed expiry must pass
1974
+ * `checkChangedExpiration` and every other field in (3) must still match.
1975
+ * Without it, any expiry difference is refused as in (3).
1976
+ */
1977
+ async signInWithOpenKeyResult(result, prepared, keyId, jwk, options) {
1978
+ const signed = await this.verifyOpenKeyResult(result, prepared, {
1979
+ allowExpirationChange: options?.expiryChangeable === true,
1980
+ maxExpiresAt: options?.maxExpiresAt
1981
+ });
1954
1982
  return this.signInWithPreparedSession(signed, result.signature, keyId, jwk);
1955
1983
  }
1956
1984
  /** Validate a candidate without activating a network session, so paste retries reuse one preparation. */
1957
- async verifyOpenKeyResult(result, prepared) {
1985
+ async verifyOpenKeyResult(result, prepared, options) {
1958
1986
  if (result.protocolVersion !== 1) {
1959
1987
  throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
1960
1988
  }
@@ -1997,16 +2025,39 @@ var NodeUserAuthorization = class {
1997
2025
  `OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
1998
2026
  );
1999
2027
  }
2028
+ const parsedExpiry = parsedSignedSiwe.expirationTime;
2029
+ const lineExpiry = lastAnchoredLine(result.signedMessage, /^Expiration Time: (.+)$/gm);
2030
+ if (parsedExpiry === void 0 || parsedExpiry === null || lineExpiry === void 0) {
2031
+ throw new Error("OpenKey signedMessage has no Expiration Time line of its own");
2032
+ }
2033
+ const parsedExpiryMs = Date.parse(parsedExpiry);
2034
+ if (!Number.isFinite(parsedExpiryMs) || parsedExpiryMs !== Date.parse(lineExpiry)) {
2035
+ throw new Error("OpenKey signedMessage Expiration Time line does not match the message's own expiry");
2036
+ }
2000
2037
  const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
2001
2038
  const originalFields = (0, import_sdk_core5.extractImmutableSiweFields)(prepared.siwe);
2002
2039
  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
- );
2040
+ if (options?.allowExpirationChange === true) {
2041
+ const maxExpiresAt = typeof options.maxExpiresAt === "function" ? options.maxExpiresAt() : options.maxExpiresAt;
2042
+ const refusal = (0, import_sdk_core5.checkChangedExpiration)({
2043
+ preparedSiwe: prepared.siwe,
2044
+ signedSiwe: result.signedMessage,
2045
+ now: Date.now(),
2046
+ originalHasRecap,
2047
+ ...maxExpiresAt === void 0 ? {} : { maxExpiresAt }
2048
+ });
2049
+ if (refusal !== null) {
2050
+ throw new Error(`OpenKey signedMessage ${refusal}`);
2051
+ }
2052
+ } else {
2053
+ const changedFields = (0, import_sdk_core5.diffImmutableSiweFields)(originalFields, signedFields, {
2054
+ originalHasRecap
2055
+ });
2056
+ if (changedFields.length > 0) {
2057
+ throw new Error(
2058
+ `OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
2059
+ );
2060
+ }
2010
2061
  }
2011
2062
  let originalCaps;
2012
2063
  let signedCaps;
@@ -2287,7 +2338,9 @@ function wireOpenKeyAuthorize(openkey, opts) {
2287
2338
  host: input.host,
2288
2339
  // Forward the presentation envelope verbatim — the OpenKey SDK
2289
2340
  // validates and size-bounds it before use.
2290
- presentation: input.presentation
2341
+ presentation: input.presentation,
2342
+ // Sent only when true, so a request without it keeps its prior shape.
2343
+ ...input.expiryChangeable === true ? { expiryChangeable: true } : {}
2291
2344
  }, opts);
2292
2345
  if (result.protocolVersion !== 1) {
2293
2346
  throw new Error(
@@ -4882,14 +4935,14 @@ var _TinyCloudNode = class _TinyCloudNode {
4882
4935
  await this.signInWith(() => this.auth.signInWithOpenKey(authorizeFn, options));
4883
4936
  }
4884
4937
  /** Prepare the OpenKey request before the approval click. */
4885
- async prepareOpenKeySignIn() {
4938
+ async prepareOpenKeySignIn(options) {
4886
4939
  if (!this.signer || !this.tc || !this.auth) {
4887
4940
  throw new Error("Cannot prepare OpenKey sign-in in session-only mode. Provide a signer in config.");
4888
4941
  }
4889
4942
  await this.signOutInFlight;
4890
4943
  await this.accountRegistryTail;
4891
4944
  await this.wasmBindings.ensureInitialized?.();
4892
- return this.auth.prepareOpenKeySignIn();
4945
+ return this.auth.prepareOpenKeySignIn(options);
4893
4946
  }
4894
4947
  /** Start OpenKey authorization synchronously, then complete normal session setup. */
4895
4948
  signInWithPreparedOpenKey(pending, authorizeFn) {
@@ -7473,7 +7526,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7473
7526
  path: "",
7474
7527
  // Root access
7475
7528
  actions: [...ROOT_DELEGATION_ACTIONS],
7476
- expiry: this.getSessionExpiry(),
7529
+ expiry: this.getSignedSessionExpiry(),
7477
7530
  isRevoked: false,
7478
7531
  allowSubDelegation: true
7479
7532
  };
@@ -7486,7 +7539,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7486
7539
  spaceId,
7487
7540
  path: "",
7488
7541
  actions: [...ROOT_DELEGATION_ACTIONS],
7489
- expiry: this.getSessionExpiry(),
7542
+ expiry: this.getSignedSessionExpiry(),
7490
7543
  isRevoked: false,
7491
7544
  allowSubDelegation: true
7492
7545
  });
@@ -7578,7 +7631,7 @@ var _TinyCloudNode = class _TinyCloudNode {
7578
7631
  assertActive: () => graph.assertActive(),
7579
7632
  keyProvider: this._keyProvider,
7580
7633
  registry: this._capabilityRegistry,
7581
- sessionExpiry: this.getSessionExpiry(),
7634
+ sessionExpiry: this.getSignedSessionExpiry(),
7582
7635
  createDelegationWasm: (params) => {
7583
7636
  graph.assertActive();
7584
7637
  return this.createDelegationWrapper(params);
@@ -7625,6 +7678,29 @@ var _TinyCloudNode = class _TinyCloudNode {
7625
7678
  const expirationMs = this.config.sessionExpirationMs ?? DEFAULT_SESSION_EXPIRATION_MS;
7626
7679
  return new Date(Date.now() + expirationMs);
7627
7680
  }
7681
+ /**
7682
+ * The expiry of the session that was actually signed, so the registry root
7683
+ * and the sharing service never outlive or undercut what the SIWE grants
7684
+ * (a user may change the expiry in the OpenKey approval).
7685
+ *
7686
+ * Any expiry the SIWE parses to is used exactly as signed, however near or
7687
+ * far. The configured lifetime is used only when the SIWE cannot be parsed
7688
+ * (the session is already established, so failing sign-in here would be
7689
+ * worse; real sign-ins verify the SIWE first) or when a valid SIWE carries
7690
+ * no `Expiration Time` line, as the other `extractSiweExpiration` callers do.
7691
+ * @internal
7692
+ */
7693
+ getSignedSessionExpiry() {
7694
+ const siwe = this.auth?.tinyCloudSession?.siwe;
7695
+ if (!siwe) return this.getSessionExpiry();
7696
+ let signed;
7697
+ try {
7698
+ signed = extractSiweExpiration(siwe);
7699
+ } catch {
7700
+ return this.getSessionExpiry();
7701
+ }
7702
+ return signed ?? this.getSessionExpiry();
7703
+ }
7628
7704
  /**
7629
7705
  * Wrapper for the WASM createDelegation function.
7630
7706
  *