@tinycloud/sdk-core 3.0.0-beta.17 → 3.0.0-beta.19

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.
package/dist/index.cjs CHANGED
@@ -421,6 +421,7 @@ __export(index_exports, {
421
421
  serviceError: () => import_sdk_services8.serviceError,
422
422
  sha256Base64Url: () => sha256Base64Url,
423
423
  shareCapabilityAllows: () => shareCapabilityAllows,
424
+ signCompactPolicyDescendant: () => signCompactPolicyDescendant,
424
425
  signCompactUcanAuthorization: () => signCompactUcanAuthorization,
425
426
  signCompactUcanRootAuthorization: () => signCompactUcanRootAuthorization,
426
427
  signLocationRecord: () => signLocationRecord,
@@ -4495,8 +4496,9 @@ async function signCompactUcanAuthorization(input) {
4495
4496
  throw new Error("compact invocation lifetime must be between one and 60 seconds");
4496
4497
  return signCompactUcan(input);
4497
4498
  }
4499
+ var MAX_POLICY_ROOT_SECONDS = 31 * 24 * 60 * 60;
4498
4500
  async function signCompactUcanRootAuthorization(input) {
4499
- if (input.notBefore >= input.expiresAt || input.expiresAt - input.notBefore > 31 * 24 * 60 * 60)
4501
+ if (input.notBefore >= input.expiresAt || input.expiresAt - input.notBefore > MAX_POLICY_ROOT_SECONDS)
4500
4502
  throw new Error("compact policy root lifetime must be between one second and 31 days");
4501
4503
  return signCompactUcan({ ...input, proofs: [] });
4502
4504
  }
@@ -4531,7 +4533,7 @@ function createCompactPolicyInvocation(input) {
4531
4533
  const signature = import_ed255193.ed25519.sign(signingInput, input.privateKey);
4532
4534
  return parseCompactUcanAuthorization(`${protectedSegment}.${payloadSegment}.${encodeBase64Url2(signature)}`);
4533
4535
  }
4534
- function createCompactPolicyDescendant(input) {
4536
+ function compactPolicyDescendantClaims(input) {
4535
4537
  const parent = parseCompactUcanAuthorization(input.parentAuthorization, input.parentCid);
4536
4538
  if (parent.payload.aud !== input.issuerDid) throw new Error("policy descendant issuer is not the parent audience");
4537
4539
  const parentFact = parent.payload.fct[0];
@@ -4539,29 +4541,53 @@ function createCompactPolicyDescendant(input) {
4539
4541
  if (!Number.isInteger(depth) || depth <= 0) throw new Error("policy descendant depth is exhausted");
4540
4542
  if (!compactAttenuationContains(parent.payload.att, input.attenuation))
4541
4543
  throw new Error("policy descendant exceeds the parent attenuation");
4544
+ const now = Math.max(input.now ?? parent.payload.nbf + 1, parent.payload.nbf + 1);
4545
+ const exp = Math.min(input.expiresAt ?? parent.payload.exp - 1, parent.payload.exp - 1);
4546
+ if (exp <= now) throw new Error("policy parent has no strictly narrower validity window");
4547
+ return {
4548
+ parentCid: parent.cid,
4549
+ fact: { ...parentFact, remainingRedelegationDepth: depth - 1 },
4550
+ notBefore: now,
4551
+ expiresAt: exp,
4552
+ nonce: input.nonce ?? encodeBase64Url2(crypto.getRandomValues(new Uint8Array(16)))
4553
+ };
4554
+ }
4555
+ function createCompactPolicyDescendant(input) {
4556
+ const claims = compactPolicyDescendantClaims(input);
4542
4557
  const principal = input.issuerDid.split("#", 1)[0];
4543
4558
  const didMaterial = base58btc.decode(principal.slice("did:key:".length));
4544
4559
  const publicKey = import_ed255193.ed25519.getPublicKey(input.privateKey);
4545
4560
  if (didMaterial.length !== 34 || !equalBytes(publicKey, didMaterial.slice(2))) throw new Error("policy descendant signer does not bind issuer DID");
4546
- const now = Math.max(input.now ?? Math.floor(Date.now() / 1e3), parent.payload.nbf + 1);
4547
- const exp = Math.min(input.expiresAt ?? now + 60, now + 60, parent.payload.exp - 1);
4548
- if (exp <= now) throw new Error("policy parent has no strictly narrower validity window");
4549
4561
  const header = { alg: "EdDSA", jwk: { alg: "EdDSA", crv: "Ed25519", kty: "OKP", x: encodeBase64Url2(publicKey) }, typ: "JWT", ucv: "0.10.0" };
4550
4562
  const payload = {
4551
4563
  att: input.attenuation,
4552
4564
  aud: input.audienceDid,
4553
- exp,
4554
- fct: [{ ...parentFact, remainingRedelegationDepth: depth - 1 }],
4565
+ exp: claims.expiresAt,
4566
+ fct: [claims.fact],
4555
4567
  iss: input.issuerDid.includes("#") ? input.issuerDid : `${principal}#${principal.slice("did:key:".length)}`,
4556
- nbf: now,
4557
- nnc: input.nonce ?? encodeBase64Url2(crypto.getRandomValues(new Uint8Array(16))),
4558
- prf: [parent.cid]
4568
+ nbf: claims.notBefore,
4569
+ nnc: claims.nonce,
4570
+ prf: [claims.parentCid]
4559
4571
  };
4560
4572
  const protectedSegment = encodeBase64Url2(new TextEncoder().encode(jcsCanonicalize(header)));
4561
4573
  const payloadSegment = encodeBase64Url2(new TextEncoder().encode(jcsCanonicalize(payload)));
4562
4574
  const signature = import_ed255193.ed25519.sign(new TextEncoder().encode(`${protectedSegment}.${payloadSegment}`), input.privateKey);
4563
4575
  return parseCompactUcanAuthorization(`${protectedSegment}.${payloadSegment}.${encodeBase64Url2(signature)}`);
4564
4576
  }
4577
+ async function signCompactPolicyDescendant(input) {
4578
+ const claims = compactPolicyDescendantClaims(input);
4579
+ return signCompactUcan({
4580
+ issuerDid: input.issuerDid,
4581
+ audienceDid: input.audienceDid,
4582
+ attenuation: input.attenuation,
4583
+ facts: [claims.fact],
4584
+ proofs: [claims.parentCid],
4585
+ notBefore: claims.notBefore,
4586
+ expiresAt: claims.expiresAt,
4587
+ nonce: claims.nonce,
4588
+ sign: input.sign
4589
+ });
4590
+ }
4565
4591
  function compactAttenuationContains(parent, child) {
4566
4592
  const selector = (value) => {
4567
4593
  if (value.length !== 1 || !isRecordValue(value[0])) return void 0;
@@ -4631,7 +4657,7 @@ function parsePolicySessionUcan(authorization, expectedProofs) {
4631
4657
  throw new Error("policy session UCAN fact is incomplete");
4632
4658
  if (hasV4AuditFactShape && (!LOWER_SHA256_HEX.test(fact.credentialIdAuditDigestHex) || !LOWER_SHA256_HEX.test(fact.presentationJtiAuditDigestHex)))
4633
4659
  throw new Error("policy session UCAN v4 audit facts are invalid");
4634
- if (fact.profile !== POLICY_SESSION_UCAN_V1_PROFILE || typeof fact.remainingRedelegationDepth !== "number" || !Number.isInteger(fact.remainingRedelegationDepth) || fact.remainingRedelegationDepth < 0 || fact.remainingRedelegationDepth > 8 || parsed.payload.exp - parsed.payload.nbf > 60 || parsed.payload.iss.split("#", 1)[0] !== fact.nodeAudience || parsed.payload.aud !== fact.recipientDid)
4660
+ if (fact.profile !== POLICY_SESSION_UCAN_V1_PROFILE || typeof fact.remainingRedelegationDepth !== "number" || !Number.isInteger(fact.remainingRedelegationDepth) || fact.remainingRedelegationDepth < 0 || fact.remainingRedelegationDepth > 8 || parsed.payload.exp - parsed.payload.nbf > MAX_POLICY_ROOT_SECONDS || parsed.payload.iss.split("#", 1)[0] !== fact.nodeAudience || parsed.payload.aud !== fact.recipientDid)
4635
4661
  throw new Error("policy session UCAN fact is invalid");
4636
4662
  return {
4637
4663
  authorization,
@@ -5055,24 +5081,21 @@ async function admitPolicyCredentialV3(input) {
5055
5081
  policy,
5056
5082
  challenge
5057
5083
  });
5058
- const response = await fetchFn(
5084
+ const response = await postPolicyDelegation(
5085
+ fetchFn,
5059
5086
  new URL(`${policyRuntimeBasePath2(input.policyRuntimePath)}/delegations`, nodeOrigin),
5060
5087
  {
5061
- method: "POST",
5062
- redirect: "error",
5063
- signal: input.signal,
5064
- headers: { accept: "application/json", "content-type": "application/json" },
5065
- body: JSON.stringify({
5066
- policyCid: input.policyCid,
5067
- challengeId: challenge.challengeId,
5068
- nonce: challenge.nonce,
5069
- requirement: validateCredentialRequirement(input.requirement),
5070
- credential: envelopeFromVerified(input.credential),
5071
- accountAuthorizationCid,
5072
- credentialSpaceId,
5073
- presentation
5074
- })
5075
- }
5088
+ policyCid: input.policyCid,
5089
+ challengeId: challenge.challengeId,
5090
+ nonce: challenge.nonce,
5091
+ requirement: validateCredentialRequirement(input.requirement),
5092
+ credential: envelopeFromVerified(input.credential),
5093
+ accountAuthorizationCid,
5094
+ credentialSpaceId,
5095
+ presentation
5096
+ },
5097
+ requestedSessionExpiry(input.requestedExpiresAt, policy),
5098
+ input.signal
5076
5099
  );
5077
5100
  if (!response.ok) throw new Error(`policy delegation rejected (${response.status})`);
5078
5101
  const value = record(await response.json(), "policy delegation");
@@ -5113,20 +5136,20 @@ async function admitPolicyCredentialV4(input) {
5113
5136
  policy,
5114
5137
  challenge
5115
5138
  });
5116
- const response = await fetchFn(new URL(`${policyRuntimeBasePath2(input.policyRuntimePath)}/delegations`, nodeOrigin), {
5117
- method: "POST",
5118
- redirect: "error",
5119
- signal: input.signal,
5120
- headers: { accept: "application/json", "content-type": "application/json" },
5121
- body: JSON.stringify({
5139
+ const response = await postPolicyDelegation(
5140
+ fetchFn,
5141
+ new URL(`${policyRuntimeBasePath2(input.policyRuntimePath)}/delegations`, nodeOrigin),
5142
+ {
5122
5143
  policyCid: input.policyCid,
5123
5144
  challengeId: challenge.challengeId,
5124
5145
  nonce: challenge.nonce,
5125
5146
  requirement: validateCredentialRequirement(input.requirement),
5126
5147
  credential: envelopeFromVerified(input.credential),
5127
5148
  presentation
5128
- })
5129
- });
5149
+ },
5150
+ requestedSessionExpiry(input.requestedExpiresAt, policy),
5151
+ input.signal
5152
+ );
5130
5153
  if (!response.ok) throw new Error(`policy delegation rejected (${response.status})`);
5131
5154
  const value = record(await response.json(), "policy delegation");
5132
5155
  if (value.admitted !== true || typeof value.sessionCid !== "string" || typeof value.authorization !== "string") throw new Error("policy delegation response is invalid");
@@ -5150,6 +5173,27 @@ async function admitPolicyCredentialV4(input) {
5150
5173
  input.signal?.throwIfAborted();
5151
5174
  return Object.freeze({ challenge, presentation, session, delegationImported: true });
5152
5175
  }
5176
+ function requestedSessionExpiry(requested, policy) {
5177
+ const value = requested === void 0 ? policy.expiresAt : requested;
5178
+ if (value === null || value === void 0) return void 0;
5179
+ const seconds = Math.floor(Date.parse(value) / 1e3);
5180
+ if (!Number.isFinite(seconds) || seconds <= Math.floor(Date.now() / 1e3) + 60) return void 0;
5181
+ return new Date(seconds * 1e3).toISOString().replace(/\.\d{3}Z$/, "Z");
5182
+ }
5183
+ async function postPolicyDelegation(fetchFn, url, body, requestedExpiresAt, signal) {
5184
+ const send = (payload) => fetchFn(url, {
5185
+ method: "POST",
5186
+ redirect: "error",
5187
+ signal,
5188
+ headers: { accept: "application/json", "content-type": "application/json" },
5189
+ body: JSON.stringify(payload)
5190
+ });
5191
+ if (requestedExpiresAt === void 0) return send(body);
5192
+ const response = await send({ ...body, requestedExpiresAt });
5193
+ if (response.status !== 422) return response;
5194
+ await response.body?.cancel().catch(() => void 0);
5195
+ return send(body);
5196
+ }
5153
5197
  function policyRuntimeBasePath2(value) {
5154
5198
  const path = value ?? "/policy/v3";
5155
5199
  if (!/^\/policy(?:\/[a-z0-9-]+)*$/.test(path)) {
@@ -11150,11 +11194,11 @@ var SharingService = class {
11150
11194
  }
11151
11195
  return result;
11152
11196
  };
11153
- const canSatisfyFromRegistry = this.findSuitableKeyForDelegation(
11197
+ const canSatisfyFromRegistry = this.classifyDelegationAuthority(
11154
11198
  fullPath,
11155
11199
  actions,
11156
11200
  requestedExpiry
11157
- );
11201
+ ) === "ok";
11158
11202
  if (canSatisfyFromRegistry) {
11159
11203
  const delegationResult = await this.createSessionDelegation(plainDID, fullPath, actions, expiry);
11160
11204
  const childDelegationActiveError = this.retiredGraphError();
@@ -11224,49 +11268,58 @@ var SharingService = class {
11224
11268
  return { ok: true, data: shareLink };
11225
11269
  }
11226
11270
  /**
11227
- * Check if any key in the registry can satisfy the delegation request.
11228
- * A key can satisfy if it has a delegation that:
11229
- * 1. Covers the required path (exact match or parent path)
11230
- * 2. Has all required actions
11231
- * 3. Has sufficient expiry (delegation.expiry >= requestedExpiry)
11232
- * 4. Allows sub-delegation
11271
+ * Report, without side effects, whether this session's registry authority
11272
+ * can issue the child delegation {@link generate} would create for the same
11273
+ * `path`, `actions` and `expiry` (defaults and `pathPrefix` applied the
11274
+ * same way):
11275
+ *
11276
+ * - `"ok"`: an uncaveated entry covers it.
11277
+ * - `"caveated"`: only caveated entries cover it. SharingService cannot
11278
+ * carry signed ReCap caveats into a child delegation.
11279
+ * - `"not-covered"`: no entry covers it, or there is no session.
11280
+ *
11281
+ * It does not consult `onRootDelegationNeeded`, so a signer-backed session
11282
+ * may still generate when this is not `"ok"`.
11283
+ */
11284
+ preflightGenerate(params) {
11285
+ if (!this.session || !params.path) return "not-covered";
11286
+ const fullPath = this.pathPrefix ? `${this.pathPrefix}/${params.path}`.replace(/\/+/g, "/") : params.path;
11287
+ return this.classifyDelegationAuthority(
11288
+ fullPath,
11289
+ params.actions ?? DEFAULT_READ_ACTIONS,
11290
+ params.expiry ?? new Date(Date.now() + DEFAULT_EXPIRY_MS)
11291
+ );
11292
+ }
11293
+ /**
11294
+ * Classify the registry's authority for a child delegation. An entry
11295
+ * satisfies it when it is in the session space, valid, lasts at least until
11296
+ * `requestedExpiry`, allows sub-delegation, covers `path` (exact or parent)
11297
+ * and grants every action. Satisfying entries with caveats are unusable.
11233
11298
  * @internal
11234
11299
  */
11235
- findSuitableKeyForDelegation(path, actions, requestedExpiry) {
11236
- const allKeys = this.registry.getAllKeys();
11237
- for (const key of allKeys) {
11238
- const delegations = this.registry.getDelegationsForKey(key.id);
11239
- for (const delegation of delegations) {
11300
+ classifyDelegationAuthority(path, actions, requestedExpiry) {
11301
+ let coveredOnlyByCaveated = false;
11302
+ for (const key of this.registry.getAllKeys()) {
11303
+ for (const delegation of this.registry.getDelegationsForKey(key.id)) {
11240
11304
  if (delegation.spaceId === void 0 || !spaceIdsEqual(delegation.spaceId, this.session?.spaceId ?? "")) {
11241
11305
  continue;
11242
11306
  }
11243
- if ((delegation.caveats?.length ?? 0) > 0) {
11244
- continue;
11245
- }
11246
- if (!this.registry.isDelegationValid(delegation)) {
11247
- continue;
11248
- }
11249
- if (delegation.expiry < requestedExpiry) {
11250
- continue;
11251
- }
11252
- if (delegation.allowSubDelegation === false) {
11253
- continue;
11254
- }
11255
- const delegationPath = delegation.path || "";
11256
- if (!this.pathMatches(delegationPath, path)) {
11307
+ if (!this.registry.isDelegationValid(delegation)) continue;
11308
+ if (delegation.expiry < requestedExpiry) continue;
11309
+ if (delegation.allowSubDelegation === false) continue;
11310
+ if (!this.pathMatches(delegation.path || "", path)) continue;
11311
+ const delegationActions = delegation.actions || [];
11312
+ if (!actions.every((action) => delegationActions.includes(action) || delegationActions.includes("*"))) {
11257
11313
  continue;
11258
11314
  }
11259
- const delegationActions = delegation.actions || [];
11260
- const hasAllActions = actions.every(
11261
- (action) => delegationActions.includes(action) || delegationActions.includes("*")
11262
- );
11263
- if (!hasAllActions) {
11315
+ if ((delegation.caveats?.length ?? 0) > 0) {
11316
+ coveredOnlyByCaveated = true;
11264
11317
  continue;
11265
11318
  }
11266
- return true;
11319
+ return "ok";
11267
11320
  }
11268
11321
  }
11269
- return false;
11322
+ return coveredOnlyByCaveated ? "caveated" : "not-covered";
11270
11323
  }
11271
11324
  retiredGraphError() {
11272
11325
  try {