@tinycloud/sdk-core 3.0.0-beta.8 → 3.0.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.
package/dist/index.cjs CHANGED
@@ -101,6 +101,7 @@ __export(index_exports, {
101
101
  ED25519_JCS_SIGNATURE_SUITE: () => ED25519_JCS_SIGNATURE_SUITE,
102
102
  EIP191_JCS_SIGNATURE_SUITE: () => EIP191_JCS_SIGNATURE_SUITE,
103
103
  EMAIL_CREDENTIAL_MAX_AGE_SECONDS: () => EMAIL_CREDENTIAL_MAX_AGE_SECONDS,
104
+ EMAIL_DOMAIN_CREDENTIAL_MAX_AGE_SECONDS: () => EMAIL_DOMAIN_CREDENTIAL_MAX_AGE_SECONDS,
104
105
  ENCRYPTION: () => import_bootstrap4.ENCRYPTION,
105
106
  ENCRYPTION_MANIFEST_SPACE: () => ENCRYPTION_MANIFEST_SPACE,
106
107
  ENCRYPTION_NETWORK_URN_PREFIX: () => import_sdk_services8.ENCRYPTION_NETWORK_URN_PREFIX,
@@ -126,6 +127,7 @@ __export(index_exports, {
126
127
  LOCAL_LINK_PROBE_TIMEOUT_MS: () => LOCAL_LINK_PROBE_TIMEOUT_MS,
127
128
  LOCAL_LOOPBACK_PROBE_TIMEOUT_MS: () => LOCAL_LOOPBACK_PROBE_TIMEOUT_MS,
128
129
  LocationRecordValidationError: () => LocationRecordValidationError,
130
+ LocationRegistryHttpError: () => LocationRegistryHttpError,
129
131
  MAX_CONTENT_BYTES: () => MAX_CONTENT_BYTES,
130
132
  MAX_LEGACY_ENVELOPE_EXPIRES_AT: () => MAX_LEGACY_ENVELOPE_EXPIRES_AT,
131
133
  MAX_SEALED_SHARE_CONTENT_BYTES: () => MAX_SEALED_SHARE_CONTENT_BYTES,
@@ -139,6 +141,7 @@ __export(index_exports, {
139
141
  NETWORK_NAME_PATTERN: () => import_sdk_services8.NETWORK_NAME_PATTERN,
140
142
  NetworkIdError: () => import_sdk_services8.NetworkIdError,
141
143
  OPENKEY_ACTION_ID_SEPARATOR: () => OPENKEY_ACTION_ID_SEPARATOR,
144
+ OWNER_NODE_BINDING_TIMEOUT_MS: () => OWNER_NODE_BINDING_TIMEOUT_MS,
142
145
  OpenKeyManageKeyError: () => OpenKeyManageKeyError,
143
146
  POLICY_CAPABILITY_V1_DOMAIN: () => POLICY_CAPABILITY_V1_DOMAIN,
144
147
  POLICY_CAPABILITY_V1_SCHEMA: () => POLICY_CAPABILITY_V1_SCHEMA,
@@ -178,7 +181,6 @@ __export(index_exports, {
178
181
  SECRET_RECORDS_SCHEMA: () => import_bootstrap4.SECRET_RECORDS_SCHEMA,
179
182
  SERVICE_LONG_TO_SHORT: () => SERVICE_LONG_TO_SHORT,
180
183
  SERVICE_SHORT_TO_LONG: () => SERVICE_SHORT_TO_LONG,
181
- SHARE_DELIVERY_AUTHORIZATION_DOMAIN: () => SHARE_DELIVERY_AUTHORIZATION_DOMAIN,
182
184
  SPACE: () => import_bootstrap4.SPACE,
183
185
  SQL: () => import_bootstrap4.SQL,
184
186
  SQLAction: () => import_sdk_services8.SQLAction,
@@ -245,8 +247,10 @@ __export(index_exports, {
245
247
  buildPolicyCredentialPresentationV4: () => buildPolicyCredentialPresentationV4,
246
248
  buildSpaceUri: () => buildSpaceUri,
247
249
  canonicalDigest: () => canonicalDigest,
250
+ canonicalEmailDomain: () => import_share_envelope.canonicalEmailDomain,
248
251
  canonicalHashHex: () => import_sdk_services8.canonicalHashHex,
249
252
  canonicalLocationPayload: () => canonicalLocationPayload,
253
+ canonicalMailbox: () => import_share_envelope.canonicalMailbox,
250
254
  canonicalOwnerSharePolicy: () => canonicalOwnerSharePolicy,
251
255
  canonicalSignedResponse: () => import_sdk_services8.canonicalSignedResponse,
252
256
  canonicalizeAddress: () => canonicalizeAddress,
@@ -274,6 +278,7 @@ __export(index_exports, {
274
278
  createCapabilityKeyRegistry: () => createCapabilityKeyRegistry,
275
279
  createDelegatedShareKey: () => createDelegatedShareKey,
276
280
  createEmailCredentialRequirement: () => createEmailCredentialRequirement,
281
+ createEmailDomainCredentialRequirement: () => createEmailDomainCredentialRequirement,
277
282
  createHolderBinding: () => createHolderBinding,
278
283
  createInMemoryLocalNodeIdentityStore: () => createInMemoryLocalNodeIdentityStore,
279
284
  createLocalStorageLocalNodeIdentityStore: () => createLocalStorageLocalNodeIdentityStore,
@@ -346,6 +351,7 @@ __export(index_exports, {
346
351
  importPortableDelegation: () => importPortableDelegation,
347
352
  installTinyCloudDebugGlobals: () => import_sdk_services8.installTinyCloudDebugGlobals,
348
353
  intersectShareCapabilities: () => intersectShareCapabilities,
354
+ isCanonicalEmailDomain: () => import_share_envelope.isCanonicalEmailDomain,
349
355
  isCapabilitySubset: () => isCapabilitySubset,
350
356
  isDigest: () => isDigest,
351
357
  isEvmAddress: () => isEvmAddress,
@@ -356,6 +362,7 @@ __export(index_exports, {
356
362
  jcsCanonicalize: () => jcsCanonicalize,
357
363
  loadManifest: () => loadManifest,
358
364
  locationPayloadForRecord: () => locationPayloadForRecord,
365
+ mailboxBelongsToDomain: () => import_share_envelope.mailboxBelongsToDomain,
359
366
  makePkhSpaceId: () => makePkhSpaceId,
360
367
  makePublicSpaceId: () => makePublicSpaceId,
361
368
  manifestAbilitiesUnion: () => manifestAbilitiesUnion,
@@ -394,6 +401,7 @@ __export(index_exports, {
394
401
  principalDidEquals: () => principalDidEquals,
395
402
  projectUnifiedPolicyCapabilities: () => projectUnifiedPolicyCapabilities,
396
403
  projectUnifiedPolicyCapability: () => projectUnifiedPolicyCapability,
404
+ publishLocationRecord: () => publishLocationRecord,
397
405
  recapCaveatsEqual: () => recapCaveatsEqual,
398
406
  registerPolicyV3: () => registerPolicyV3,
399
407
  renewPolicyRootStatusV3: () => renewPolicyRootStatusV3,
@@ -413,7 +421,7 @@ __export(index_exports, {
413
421
  serviceError: () => import_sdk_services8.serviceError,
414
422
  sha256Base64Url: () => sha256Base64Url,
415
423
  shareCapabilityAllows: () => shareCapabilityAllows,
416
- shareDeliveryTrustedKid: () => shareDeliveryTrustedKid,
424
+ signCompactPolicyDescendant: () => signCompactPolicyDescendant,
417
425
  signCompactUcanAuthorization: () => signCompactUcanAuthorization,
418
426
  signCompactUcanRootAuthorization: () => signCompactUcanRootAuthorization,
419
427
  signLocationRecord: () => signLocationRecord,
@@ -450,7 +458,6 @@ __export(index_exports, {
450
458
  validatePolicyStatusSignedShape: () => validatePolicyStatusSignedShape,
451
459
  validatePolicyStatusUnsigned: () => validatePolicyStatusUnsigned,
452
460
  validatePolicyUnsigned: () => validatePolicyUnsigned,
453
- validateShareDeliveryAuthorizationBytes: () => validateShareDeliveryAuthorizationBytes,
454
461
  validateShareDeliveryAuthorizationV3Bytes: () => validateShareDeliveryAuthorizationV3Bytes,
455
462
  validateUnifiedPolicyV2: () => validateUnifiedPolicyV2,
456
463
  verifyDecryptResponse: () => import_sdk_services8.verifyDecryptResponse,
@@ -458,6 +465,7 @@ __export(index_exports, {
458
465
  verifyEip191MessageSignature: () => verifyEip191MessageSignature,
459
466
  verifyIssuedCredential: () => verifyIssuedCredential,
460
467
  verifyLocationRecord: () => verifyLocationRecord,
468
+ verifyOwnerNodeBinding: () => verifyOwnerNodeBinding,
461
469
  verifyPolicy: () => verifyPolicy,
462
470
  verifyPolicyEngineRecord: () => verifyPolicyEngineRecord,
463
471
  verifyPolicyRootRevocationV3: () => verifyPolicyRootRevocationV3,
@@ -768,6 +776,7 @@ var CREDENTIAL_ERROR_CODES = [
768
776
  "ISSUER_UNREADY",
769
777
  "OFFLINE",
770
778
  "POPUP_BLOCKED",
779
+ "PROOF_REJECTED",
771
780
  "REQUEST_EXPIRED",
772
781
  "REQUEST_SUBSTITUTED",
773
782
  "SIGNATURE_REJECTED",
@@ -781,6 +790,7 @@ var RECOVERABLE = /* @__PURE__ */ new Set([
781
790
  "ISSUER_UNREADY",
782
791
  "OFFLINE",
783
792
  "POPUP_BLOCKED",
793
+ "PROOF_REJECTED",
784
794
  "REQUEST_EXPIRED",
785
795
  "SIGNATURE_REJECTED",
786
796
  "UNSUPPORTED_PROFILE",
@@ -849,6 +859,7 @@ function holderBindingSigningBytes(binding) {
849
859
 
850
860
  // src/credentials/descriptor.ts
851
861
  var ID = /^[a-z0-9][a-z0-9._-]{0,127}(?:\/v1)?$/;
862
+ var CLAIM_NAME = /^[a-z0-9][A-Za-z0-9._-]{0,127}$/;
852
863
  var STEP_TYPES = /* @__PURE__ */ new Set(["collect_input", "mailbox_otp", "holder_signature"]);
853
864
  var ENDPOINTS = {
854
865
  request: "request",
@@ -929,7 +940,7 @@ function validateCredentialFlowDescriptor(value) {
929
940
  for (const value2 of raw.claims) {
930
941
  const claim = object(value2, "claim");
931
942
  exact(claim, ["name", "matching", "selectiveDisclosure"], "claim");
932
- const name2 = text(claim.name, "claim.name", ID);
943
+ const name2 = text(claim.name, "claim.name", CLAIM_NAME);
933
944
  if (claimNames.has(name2) || claim.matching !== "normalized_exact" || typeof claim.selectiveDisclosure !== "boolean") fail("claim is invalid");
934
945
  claimNames.add(name2);
935
946
  }
@@ -1020,9 +1031,14 @@ function isDigest(value) {
1020
1031
  return typeof value === "string" && /^[A-Za-z0-9_-]{43}$/.test(value);
1021
1032
  }
1022
1033
 
1034
+ // src/credentials/email-domain.ts
1035
+ var import_share_envelope = require("@tinycloud/share-envelope");
1036
+
1023
1037
  // src/credentials/requirement.ts
1024
1038
  var ID2 = /^[a-z0-9][a-z0-9._-]{0,127}(?:\/v1)?$/;
1025
1039
  var EMAIL_CREDENTIAL_MAX_AGE_SECONDS = 3600;
1040
+ var EMAIL_DOMAIN_CREDENTIAL_MAX_AGE_SECONDS = 300;
1041
+ var CLAIM_NAME2 = /^[a-z0-9][A-Za-z0-9._-]{0,127}$/;
1026
1042
  function createEmailCredentialRequirement(input) {
1027
1043
  return validateCredentialRequirement({
1028
1044
  type: "TinyCloudCredentialRequirement",
@@ -1033,6 +1049,17 @@ function createEmailCredentialRequirement(input) {
1033
1049
  maxAgeSeconds: EMAIL_CREDENTIAL_MAX_AGE_SECONDS
1034
1050
  });
1035
1051
  }
1052
+ function createEmailDomainCredentialRequirement(input) {
1053
+ if (!(0, import_share_envelope.isCanonicalEmailDomain)(input.domain)) throw new CredentialError("UNSUPPORTED_PROFILE", "Email domain is not canonical");
1054
+ return validateCredentialRequirement({
1055
+ type: "TinyCloudCredentialRequirement",
1056
+ version: 1,
1057
+ profile: input.profile,
1058
+ credentialType: input.credentialType,
1059
+ claims: { emailDomain: input.domain },
1060
+ maxAgeSeconds: EMAIL_DOMAIN_CREDENTIAL_MAX_AGE_SECONDS
1061
+ });
1062
+ }
1036
1063
  function validateCredentialRequirement(value) {
1037
1064
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new CredentialError("UNSUPPORTED_PROFILE", "Credential requirement is invalid");
1038
1065
  const raw = value;
@@ -1046,7 +1073,7 @@ function validateCredentialRequirement(value) {
1046
1073
  if (typeof raw.claims !== "object" || raw.claims === null || Array.isArray(raw.claims)) throw new CredentialError("UNSUPPORTED_PROFILE", "Credential claims are invalid");
1047
1074
  const claims = {};
1048
1075
  for (const [key, value2] of Object.entries(raw.claims).sort(([a], [b]) => a.localeCompare(b))) {
1049
- if (!ID2.test(key) || typeof value2 !== "string" || value2.length === 0 || value2.length > 4096) throw new CredentialError("UNSUPPORTED_PROFILE", "Credential claim is invalid");
1076
+ if (!CLAIM_NAME2.test(key) || typeof value2 !== "string" || value2.length === 0 || value2.length > 4096) throw new CredentialError("UNSUPPORTED_PROFILE", "Credential claim is invalid");
1050
1077
  claims[key] = value2;
1051
1078
  }
1052
1079
  if (raw.maxAgeSeconds !== void 0 && (!Number.isSafeInteger(raw.maxAgeSeconds) || raw.maxAgeSeconds < 1)) throw new CredentialError("UNSUPPORTED_PROFILE", "Credential freshness is invalid");
@@ -4469,8 +4496,9 @@ async function signCompactUcanAuthorization(input) {
4469
4496
  throw new Error("compact invocation lifetime must be between one and 60 seconds");
4470
4497
  return signCompactUcan(input);
4471
4498
  }
4499
+ var MAX_POLICY_ROOT_SECONDS = 31 * 24 * 60 * 60;
4472
4500
  async function signCompactUcanRootAuthorization(input) {
4473
- 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)
4474
4502
  throw new Error("compact policy root lifetime must be between one second and 31 days");
4475
4503
  return signCompactUcan({ ...input, proofs: [] });
4476
4504
  }
@@ -4505,7 +4533,7 @@ function createCompactPolicyInvocation(input) {
4505
4533
  const signature = import_ed255193.ed25519.sign(signingInput, input.privateKey);
4506
4534
  return parseCompactUcanAuthorization(`${protectedSegment}.${payloadSegment}.${encodeBase64Url2(signature)}`);
4507
4535
  }
4508
- function createCompactPolicyDescendant(input) {
4536
+ function compactPolicyDescendantClaims(input) {
4509
4537
  const parent = parseCompactUcanAuthorization(input.parentAuthorization, input.parentCid);
4510
4538
  if (parent.payload.aud !== input.issuerDid) throw new Error("policy descendant issuer is not the parent audience");
4511
4539
  const parentFact = parent.payload.fct[0];
@@ -4513,29 +4541,53 @@ function createCompactPolicyDescendant(input) {
4513
4541
  if (!Number.isInteger(depth) || depth <= 0) throw new Error("policy descendant depth is exhausted");
4514
4542
  if (!compactAttenuationContains(parent.payload.att, input.attenuation))
4515
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);
4516
4557
  const principal = input.issuerDid.split("#", 1)[0];
4517
4558
  const didMaterial = base58btc.decode(principal.slice("did:key:".length));
4518
4559
  const publicKey = import_ed255193.ed25519.getPublicKey(input.privateKey);
4519
4560
  if (didMaterial.length !== 34 || !equalBytes(publicKey, didMaterial.slice(2))) throw new Error("policy descendant signer does not bind issuer DID");
4520
- const now = Math.max(input.now ?? Math.floor(Date.now() / 1e3), parent.payload.nbf + 1);
4521
- const exp = Math.min(input.expiresAt ?? now + 60, now + 60, parent.payload.exp - 1);
4522
- if (exp <= now) throw new Error("policy parent has no strictly narrower validity window");
4523
4561
  const header = { alg: "EdDSA", jwk: { alg: "EdDSA", crv: "Ed25519", kty: "OKP", x: encodeBase64Url2(publicKey) }, typ: "JWT", ucv: "0.10.0" };
4524
4562
  const payload = {
4525
4563
  att: input.attenuation,
4526
4564
  aud: input.audienceDid,
4527
- exp,
4528
- fct: [{ ...parentFact, remainingRedelegationDepth: depth - 1 }],
4565
+ exp: claims.expiresAt,
4566
+ fct: [claims.fact],
4529
4567
  iss: input.issuerDid.includes("#") ? input.issuerDid : `${principal}#${principal.slice("did:key:".length)}`,
4530
- nbf: now,
4531
- nnc: input.nonce ?? encodeBase64Url2(crypto.getRandomValues(new Uint8Array(16))),
4532
- prf: [parent.cid]
4568
+ nbf: claims.notBefore,
4569
+ nnc: claims.nonce,
4570
+ prf: [claims.parentCid]
4533
4571
  };
4534
4572
  const protectedSegment = encodeBase64Url2(new TextEncoder().encode(jcsCanonicalize(header)));
4535
4573
  const payloadSegment = encodeBase64Url2(new TextEncoder().encode(jcsCanonicalize(payload)));
4536
4574
  const signature = import_ed255193.ed25519.sign(new TextEncoder().encode(`${protectedSegment}.${payloadSegment}`), input.privateKey);
4537
4575
  return parseCompactUcanAuthorization(`${protectedSegment}.${payloadSegment}.${encodeBase64Url2(signature)}`);
4538
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
+ }
4539
4591
  function compactAttenuationContains(parent, child) {
4540
4592
  const selector = (value) => {
4541
4593
  if (value.length !== 1 || !isRecordValue(value[0])) return void 0;
@@ -4605,7 +4657,7 @@ function parsePolicySessionUcan(authorization, expectedProofs) {
4605
4657
  throw new Error("policy session UCAN fact is incomplete");
4606
4658
  if (hasV4AuditFactShape && (!LOWER_SHA256_HEX.test(fact.credentialIdAuditDigestHex) || !LOWER_SHA256_HEX.test(fact.presentationJtiAuditDigestHex)))
4607
4659
  throw new Error("policy session UCAN v4 audit facts are invalid");
4608
- 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)
4609
4661
  throw new Error("policy session UCAN fact is invalid");
4610
4662
  return {
4611
4663
  authorization,
@@ -5029,24 +5081,21 @@ async function admitPolicyCredentialV3(input) {
5029
5081
  policy,
5030
5082
  challenge
5031
5083
  });
5032
- const response = await fetchFn(
5084
+ const response = await postPolicyDelegation(
5085
+ fetchFn,
5033
5086
  new URL(`${policyRuntimeBasePath2(input.policyRuntimePath)}/delegations`, nodeOrigin),
5034
5087
  {
5035
- method: "POST",
5036
- redirect: "error",
5037
- signal: input.signal,
5038
- headers: { accept: "application/json", "content-type": "application/json" },
5039
- body: JSON.stringify({
5040
- policyCid: input.policyCid,
5041
- challengeId: challenge.challengeId,
5042
- nonce: challenge.nonce,
5043
- requirement: validateCredentialRequirement(input.requirement),
5044
- credential: envelopeFromVerified(input.credential),
5045
- accountAuthorizationCid,
5046
- credentialSpaceId,
5047
- presentation
5048
- })
5049
- }
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
5050
5099
  );
5051
5100
  if (!response.ok) throw new Error(`policy delegation rejected (${response.status})`);
5052
5101
  const value = record(await response.json(), "policy delegation");
@@ -5087,20 +5136,20 @@ async function admitPolicyCredentialV4(input) {
5087
5136
  policy,
5088
5137
  challenge
5089
5138
  });
5090
- const response = await fetchFn(new URL(`${policyRuntimeBasePath2(input.policyRuntimePath)}/delegations`, nodeOrigin), {
5091
- method: "POST",
5092
- redirect: "error",
5093
- signal: input.signal,
5094
- headers: { accept: "application/json", "content-type": "application/json" },
5095
- body: JSON.stringify({
5139
+ const response = await postPolicyDelegation(
5140
+ fetchFn,
5141
+ new URL(`${policyRuntimeBasePath2(input.policyRuntimePath)}/delegations`, nodeOrigin),
5142
+ {
5096
5143
  policyCid: input.policyCid,
5097
5144
  challengeId: challenge.challengeId,
5098
5145
  nonce: challenge.nonce,
5099
5146
  requirement: validateCredentialRequirement(input.requirement),
5100
5147
  credential: envelopeFromVerified(input.credential),
5101
5148
  presentation
5102
- })
5103
- });
5149
+ },
5150
+ requestedSessionExpiry(input.requestedExpiresAt, policy),
5151
+ input.signal
5152
+ );
5104
5153
  if (!response.ok) throw new Error(`policy delegation rejected (${response.status})`);
5105
5154
  const value = record(await response.json(), "policy delegation");
5106
5155
  if (value.admitted !== true || typeof value.sessionCid !== "string" || typeof value.authorization !== "string") throw new Error("policy delegation response is invalid");
@@ -5124,6 +5173,27 @@ async function admitPolicyCredentialV4(input) {
5124
5173
  input.signal?.throwIfAborted();
5125
5174
  return Object.freeze({ challenge, presentation, session, delegationImported: true });
5126
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
+ }
5127
5197
  function policyRuntimeBasePath2(value) {
5128
5198
  const path = value ?? "/policy/v3";
5129
5199
  if (!/^\/policy(?:\/[a-z0-9-]+)*$/.test(path)) {
@@ -6499,6 +6569,7 @@ var DelegationErrorSchema = import_zod4.z.object({
6499
6569
  });
6500
6570
  var DelegationErrorCodes = {
6501
6571
  AUTH_REQUIRED: "AUTH_REQUIRED",
6572
+ AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
6502
6573
  AUTH_EXPIRED: "AUTH_EXPIRED",
6503
6574
  NOT_INITIALIZED: "NOT_INITIALIZED",
6504
6575
  NOT_FOUND: "NOT_FOUND",
@@ -6863,7 +6934,7 @@ var CreateDelegationWasmResultSchema = import_zod4.z.object({
6863
6934
  // src/expiry.ts
6864
6935
  var EPHEMERAL_MS = 60 * 60 * 1e3;
6865
6936
  var SIGNED_READ_URL_MS = 5 * 60 * 1e3;
6866
- var SESSION_MS = 7 * 24 * 60 * 60 * 1e3;
6937
+ var SESSION_MS = 30 * 24 * 60 * 60 * 1e3;
6867
6938
  var SHARE_MS = 7 * 24 * 60 * 60 * 1e3;
6868
6939
  var APP_MS = 30 * 24 * 60 * 60 * 1e3;
6869
6940
  var MAX_MS = 10 * 365 * 24 * 60 * 60 * 1e3;
@@ -8627,7 +8698,6 @@ var import_ed255195 = require("@noble/curves/ed25519");
8627
8698
  var import_blake33 = require("@noble/hashes/blake3");
8628
8699
  var import_sha2563 = require("@noble/hashes/sha256");
8629
8700
  var ENVELOPE_SCHEMA = "xyz.tinycloud.share/envelope";
8630
- var INLINE_PREFIX = "tc2:";
8631
8701
  var RAW_CID_CODEC = 85;
8632
8702
  var BLAKE3_256_CODE = 30;
8633
8703
  var DEFAULT_MAX_INLINE_BYTES = 256 * 1024;
@@ -9332,25 +9402,8 @@ function parsePublishedShareLink(link, options2 = {}) {
9332
9402
  }
9333
9403
  const origin2 = normalizeOrigin(url.origin);
9334
9404
  if (options2.trustedOrigins !== void 0 && !options2.trustedOrigins.map(normalizeOrigin).includes(origin2)) throw new ShareEnvelopeError("origin-mismatch", "share link origin is not trusted");
9335
- if (url.search !== "" || url.username !== "" || url.password !== "") throw new ShareEnvelopeError("invalid-link", "share link is not canonical");
9336
- if (url.pathname === "/s/inline" && url.hash.startsWith("#v=2&p=")) {
9337
- let payload;
9338
- try {
9339
- const bytes2 = base64UrlToBytes(url.hash.slice("#v=2&p=".length));
9340
- if (bytes2.length > (options2.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES) * 2) throw new Error("inline payload too large");
9341
- const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes2));
9342
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("inline payload shape");
9343
- payload = value;
9344
- } catch (error) {
9345
- throw new ShareEnvelopeError("invalid-link", error instanceof Error ? error.message : "invalid inline payload");
9346
- }
9347
- if (payload.v !== 2 || typeof payload.c !== "string" || typeof payload.cid !== "string" || payload.k !== void 0 && typeof payload.k !== "string" || Object.keys(payload).some((key3) => !["v", "c", "cid", "k"].includes(key3))) throw new ShareEnvelopeError("invalid-link", "inline payload fields are invalid");
9348
- const sealed = base64UrlToBytes(payload.c);
9349
- if (!verifyPublishedShareCid(payload.cid, sealed)) throw new ShareEnvelopeError("cid-mismatch", "inline payload CID does not match");
9350
- const key2 = payload.k === void 0 ? void 0 : base64UrlToBytes(payload.k);
9351
- if (key2 !== void 0 && key2.length !== 32) throw new ShareEnvelopeError("invalid-link", "inline payload key is invalid");
9352
- return { origin: origin2, cid: payload.cid, ...key2 === void 0 ? {} : { key: key2 }, sealed };
9353
- }
9405
+ if (url.username !== "" || url.password !== "") throw new ShareEnvelopeError("invalid-link", "share link is not canonical");
9406
+ if (url.search !== "") throw new ShareEnvelopeError("invalid-link", "share link is not canonical");
9354
9407
  const match = /^\/s\/([a-z2-7]+)$/.exec(url.pathname);
9355
9408
  if (match === null || !url.hash.startsWith("#k=")) throw new ShareEnvelopeError("invalid-link", "published share link is invalid");
9356
9409
  const key = base64UrlToBytes(url.hash.slice(3));
@@ -9370,13 +9423,10 @@ function parseShareUrl(link, options2 = {}) {
9370
9423
  } catch {
9371
9424
  throw new ShareEnvelopeError("invalid-link", "share link must be an absolute URL");
9372
9425
  }
9373
- if (url.search !== "") throw new ShareEnvelopeError("invalid-link", "share links must not contain query parameters");
9374
9426
  const origin2 = normalizeOrigin(url.origin);
9375
9427
  if (options2.trustedOrigins !== void 0 && !options2.trustedOrigins.map(normalizeOrigin).includes(origin2)) throw new ShareEnvelopeError("origin-mismatch", "share link origin is not trusted");
9376
- if (url.pathname === "/s/inline" && url.hash.startsWith("#v=2&p=")) {
9377
- const published = parsePublishedShareLink(link, options2);
9378
- return { kind: "inline", origin: origin2, cid: published.cid, key: published.key, url: `${url.origin}${url.pathname}`, protocol: "share-envelope-v2" };
9379
- }
9428
+ if (url.pathname === "/viewer") throw new ShareEnvelopeError("invalid-link", "retired viewer links are not accepted");
9429
+ if (url.search !== "") throw new ShareEnvelopeError("invalid-link", "share links must not contain query parameters");
9380
9430
  const match = url.pathname.match(/^\/s\/(i\/)?([^/]+)$/);
9381
9431
  if (!match) throw new ShareEnvelopeError("invalid-link", "share link path is invalid");
9382
9432
  const fragmentParams = url.hash === "" ? void 0 : new URLSearchParams(url.hash.slice(1));
@@ -9399,9 +9449,10 @@ function parseShareUrl(link, options2 = {}) {
9399
9449
  }
9400
9450
  function isV2ShareLink(link) {
9401
9451
  try {
9402
- return new URL(link).pathname.startsWith("/s/");
9452
+ const url = new URL(link);
9453
+ return url.search === "" && url.pathname.startsWith("/s/");
9403
9454
  } catch {
9404
- return link.startsWith(INLINE_PREFIX);
9455
+ return false;
9405
9456
  }
9406
9457
  }
9407
9458
  async function encryptShareBytes(bytes2, key) {
@@ -10901,6 +10952,13 @@ var ShareRecipientClient = class {
10901
10952
  };
10902
10953
 
10903
10954
  // src/delegations/SharingService.ts
10955
+ function spaceIdsEqual(left, right) {
10956
+ const normalizeOwnerAddress = (space) => {
10957
+ const match = /^(tinycloud:pkh:eip155:\d+:)(0x[0-9a-fA-F]{40})(:.+)$/.exec(space);
10958
+ return match === null ? space : `${match[1]}${match[2].toLowerCase()}${match[3]}`;
10959
+ };
10960
+ return normalizeOwnerAddress(left) === normalizeOwnerAddress(right);
10961
+ }
10904
10962
  function inferShortServiceFromActionUrns(actions) {
10905
10963
  let short;
10906
10964
  for (const action of actions) {
@@ -11136,11 +11194,11 @@ var SharingService = class {
11136
11194
  }
11137
11195
  return result;
11138
11196
  };
11139
- const canSatisfyFromRegistry = this.findSuitableKeyForDelegation(
11197
+ const canSatisfyFromRegistry = this.classifyDelegationAuthority(
11140
11198
  fullPath,
11141
11199
  actions,
11142
11200
  requestedExpiry
11143
- );
11201
+ ) === "ok";
11144
11202
  if (canSatisfyFromRegistry) {
11145
11203
  const delegationResult = await this.createSessionDelegation(plainDID, fullPath, actions, expiry);
11146
11204
  const childDelegationActiveError = this.retiredGraphError();
@@ -11210,49 +11268,58 @@ var SharingService = class {
11210
11268
  return { ok: true, data: shareLink };
11211
11269
  }
11212
11270
  /**
11213
- * Check if any key in the registry can satisfy the delegation request.
11214
- * A key can satisfy if it has a delegation that:
11215
- * 1. Covers the required path (exact match or parent path)
11216
- * 2. Has all required actions
11217
- * 3. Has sufficient expiry (delegation.expiry >= requestedExpiry)
11218
- * 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.
11219
11298
  * @internal
11220
11299
  */
11221
- findSuitableKeyForDelegation(path, actions, requestedExpiry) {
11222
- const allKeys = this.registry.getAllKeys();
11223
- for (const key of allKeys) {
11224
- const delegations = this.registry.getDelegationsForKey(key.id);
11225
- for (const delegation of delegations) {
11226
- if (delegation.spaceId !== this.session?.spaceId) {
11227
- continue;
11228
- }
11229
- if ((delegation.caveats?.length ?? 0) > 0) {
11230
- continue;
11231
- }
11232
- if (!this.registry.isDelegationValid(delegation)) {
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)) {
11304
+ if (delegation.spaceId === void 0 || !spaceIdsEqual(delegation.spaceId, this.session?.spaceId ?? "")) {
11233
11305
  continue;
11234
11306
  }
11235
- if (delegation.expiry < requestedExpiry) {
11236
- continue;
11237
- }
11238
- if (delegation.allowSubDelegation === false) {
11239
- continue;
11240
- }
11241
- const delegationPath = delegation.path || "";
11242
- 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("*"))) {
11243
11313
  continue;
11244
11314
  }
11245
- const delegationActions = delegation.actions || [];
11246
- const hasAllActions = actions.every(
11247
- (action) => delegationActions.includes(action) || delegationActions.includes("*")
11248
- );
11249
- if (!hasAllActions) {
11315
+ if ((delegation.caveats?.length ?? 0) > 0) {
11316
+ coveredOnlyByCaveated = true;
11250
11317
  continue;
11251
11318
  }
11252
- return true;
11319
+ return "ok";
11253
11320
  }
11254
11321
  }
11255
- return false;
11322
+ return coveredOnlyByCaveated ? "caveated" : "not-covered";
11256
11323
  }
11257
11324
  retiredGraphError() {
11258
11325
  try {
@@ -11351,11 +11418,23 @@ var SharingService = class {
11351
11418
  const errorText = await registerRes.text();
11352
11419
  const registrationTextActiveError = this.retiredGraphError();
11353
11420
  if (registrationTextActiveError) return { ok: false, error: registrationTextActiveError };
11421
+ const message = `Failed to register delegation with server: ${registerRes.status} ${errorText}`;
11422
+ if (registerRes.status === 401 || registerRes.status === 403) {
11423
+ return {
11424
+ ok: false,
11425
+ error: createError2(
11426
+ DelegationErrorCodes.AUTH_UNAUTHORIZED,
11427
+ message,
11428
+ void 0,
11429
+ { status: registerRes.status }
11430
+ )
11431
+ };
11432
+ }
11354
11433
  return {
11355
11434
  ok: false,
11356
11435
  error: createError2(
11357
11436
  DelegationErrorCodes.CREATION_FAILED,
11358
- `Failed to register delegation with server: ${registerRes.status} ${errorText}`
11437
+ message
11359
11438
  )
11360
11439
  };
11361
11440
  }
@@ -12212,114 +12291,13 @@ function validateOwnerSharePolicyRegistrationBytes(bytesValue, expected) {
12212
12291
  return validateOwnerSharePolicyRegistration(value, expected);
12213
12292
  }
12214
12293
 
12215
- // src/delegations/share-delivery.ts
12216
- var import_ed255199 = require("@noble/curves/ed25519");
12217
- var SHARE_DELIVERY_AUTHORIZATION_DOMAIN = "xyz.tinycloud.share/delivery-authorization/v2\0";
12218
- function isCanonicalHttpsOrigin(value) {
12219
- let url;
12220
- try {
12221
- url = new URL(value);
12222
- } catch {
12223
- return false;
12224
- }
12225
- return url.protocol === "https:" && url.origin === value && url.pathname === "/" && url.search === "" && url.hash === "" && url.username === "" && url.password === "";
12226
- }
12227
- var AUTHORIZATION_KEYS = [
12228
- "type",
12229
- "version",
12230
- "jti",
12231
- "shareCid",
12232
- "shareId",
12233
- "registrationCid",
12234
- "delegationCid",
12235
- "enforcementDelegationCid",
12236
- "envelopeCid",
12237
- "policyCid",
12238
- "nodeAudience",
12239
- "targetOrigin",
12240
- "openCredentialsAudience",
12241
- "holder",
12242
- "recipientMatcher",
12243
- "deliveryEmail",
12244
- "shareUrl",
12245
- "returnOrigin",
12246
- "documentName",
12247
- "senderDid",
12248
- "senderTrust",
12249
- "contentSource",
12250
- "contentSourceDigest",
12251
- "shareExpiresAt",
12252
- "issuedAt",
12253
- "reportAbuseToken",
12254
- "actions",
12255
- "resource",
12256
- "authorityMaterialHandle",
12257
- "authorityMaterialDigest",
12258
- "requestBodyDigest",
12259
- "idempotencyKey",
12260
- "expiresAt",
12261
- "dataAuthority"
12262
- ];
12263
- function assertObject2(value, keys, label) {
12264
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} is invalid`);
12265
- const record2 = value;
12266
- if (Object.keys(record2).length !== keys.length || keys.some((key) => !Object.prototype.hasOwnProperty.call(record2, key))) throw new Error(`${label} has unknown or missing fields`);
12267
- return record2;
12268
- }
12269
- function fromB642(value) {
12270
- if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("share delivery value is not canonical base64url");
12271
- if (typeof atob === "function") {
12272
- const padded = value.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - value.length % 4) % 4);
12273
- return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
12274
- }
12275
- if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(value, "base64url"));
12276
- throw new Error("base64url decoding is unavailable");
12277
- }
12278
- function validateShareDeliveryAuthorizationBytes(bytesValue, expected) {
12279
- let value;
12280
- try {
12281
- value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytesValue));
12282
- } catch {
12283
- throw new Error("share delivery authorization response is not valid UTF-8 JSON");
12284
- }
12285
- const root = assertObject2(value, ["authorization", "proof"], "share delivery authorization response");
12286
- const authorization = assertObject2(root.authorization, AUTHORIZATION_KEYS, "share delivery authorization");
12287
- const proof = assertObject2(root.proof, ["alg", "kid", "signature"], "share delivery authorization proof");
12288
- const request = expected.request;
12289
- if (authorization.type !== "TinyCloudShareDeliveryAuthorization" || authorization.version !== 2 || authorization.dataAuthority !== false || authorization.jti !== request.jti || authorization.shareCid !== request.shareCid || authorization.shareId !== request.shareId || authorization.registrationCid !== request.registrationCid || authorization.delegationCid !== request.delegationCid || authorization.enforcementDelegationCid !== request.enforcementDelegationCid || authorization.envelopeCid !== request.envelopeCid || authorization.policyCid !== request.policyCid || authorization.deliveryEmail !== request.recipientEmail || authorization.shareUrl !== request.shareUrl || authorization.documentName !== request.documentName || authorization.requestBodyDigest !== request.requestBodyDigest || authorization.expiresAt !== request.expiresAt || authorization.idempotencyKey !== request.idempotencyKey || authorization.reportAbuseToken !== request.jti || authorization.authorityMaterialHandle !== request.registrationCid) {
12290
- throw new Error("share delivery authorization is not bound to the submitted request");
12291
- }
12292
- if (typeof authorization.issuedAt !== "string" || Date.parse(authorization.issuedAt) > Date.now() + 6e4) throw new Error("share delivery authorization issuedAt is invalid");
12293
- if (new Date(authorization.expiresAt).toISOString() !== authorization.expiresAt) throw new Error("share delivery authorization expiresAt is non-canonical");
12294
- if (typeof authorization.openCredentialsAudience !== "string" || !isCanonicalHttpsOrigin(authorization.openCredentialsAudience) || authorization.openCredentialsAudience !== expected.credentialsAudience || authorization.openCredentialsAudience === authorization.nodeAudience || authorization.openCredentialsAudience === authorization.returnOrigin) {
12295
- throw new Error("share delivery authorization openCredentialsAudience is missing or untrusted");
12296
- }
12297
- if (proof.alg !== "EdDSA" || typeof proof.kid !== "string" || typeof proof.signature !== "string") throw new Error("share delivery authorization proof is invalid");
12298
- if (proof.kid !== expected.nodeProof.kid) throw new Error("share delivery authorization proof key is not trusted");
12299
- const encodedKid = expected.nodeProof.publicKey;
12300
- const publicKey = encodedKid.length === 34 && encodedKid[0] === 237 && encodedKid[1] === 1 ? encodedKid.slice(2) : encodedKid;
12301
- if (publicKey.length !== 32) throw new Error("share delivery authorization proof key is invalid");
12302
- const signatureBytes = fromB642(proof.signature);
12303
- const signedBytes = new TextEncoder().encode(`${SHARE_DELIVERY_AUTHORIZATION_DOMAIN}${canonicalizeSignedObjectUnsigned(authorization)}`);
12304
- if (signatureBytes.length !== 64 || !import_ed255199.ed25519.verify(signatureBytes, signedBytes, publicKey)) throw new Error("share delivery authorization proof signature is invalid");
12305
- return { authorization, proof };
12306
- }
12307
- function shareDeliveryTrustedKid(publicKey) {
12308
- if (publicKey.length !== 32) throw new Error("share delivery node key must be a raw 32-byte Ed25519 public key");
12309
- const prefixed = new Uint8Array(34);
12310
- prefixed.set([237, 1]);
12311
- prefixed.set(publicKey, 2);
12312
- const did = `did:key:${base58btc.encode(prefixed)}`;
12313
- return `${did}#${did.slice("did:key:".length)}`;
12314
- }
12315
-
12316
12294
  // src/delegations/share-delivery-v3.ts
12317
- var import_ed2551910 = require("@noble/curves/ed25519");
12295
+ var import_ed255199 = require("@noble/curves/ed25519");
12318
12296
  var import_sha2567 = require("@noble/hashes/sha256");
12319
12297
  var CREDENTIAL_INVITATION_REQUEST_DOMAIN = "xyz.tinycloud.credentials/invitation-request/v1\0";
12320
12298
  var DELIVERY_ADMISSION_DOMAIN = "xyz.tinycloud.policy/delivery-admission/v0\0";
12321
- var REQUEST_KEYS = ["schema", "policyId", "recipient", "resource", "credentialType", "returnLink", "envelopeRef", "audience", "issuedAt", "expiresAt", "nonce"];
12322
- var ADMISSION_KEYS = ["schema", "policyId", "ownerDid", "recipient", "resource", "actions", "credentialType", "returnLink", "envelopeRef", "senderKeyDid", "audience", "issuedAt", "expiresAt", "nonce", "signature"];
12299
+ var REQUEST_KEYS = ["schema", "policyId", "recipient", "resource", "credentialType", "returnLink", "envelopeRef", "label", "shareExpiresAt", "audience", "issuedAt", "expiresAt", "nonce"];
12300
+ var ADMISSION_KEYS = ["schema", "policyId", "ownerDid", "recipient", "resource", "actions", "credentialType", "returnLink", "envelopeRef", "label", "shareExpiresAt", "senderKeyDid", "audience", "issuedAt", "expiresAt", "nonce", "signature"];
12323
12301
  function exactObject2(value, keys, label) {
12324
12302
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} is invalid`);
12325
12303
  const record2 = value;
@@ -12363,15 +12341,16 @@ function validateShareDeliveryAuthorizationV3Bytes(bytes2, expected) {
12363
12341
  const contentSource = object3(envelope.contentSource, "v3 content source");
12364
12342
  const envelopeSignature = object3(envelope.signature, "v3 envelope signature");
12365
12343
  const target = object3(envelope.target, "v3 target");
12366
- const fields = ["policyId", "recipient", "resource", "credentialType", "returnLink", "envelopeRef", "audience", "issuedAt", "expiresAt", "nonce"];
12367
- if (request.schema !== "xyz.tinycloud.credentials/invitation-request/v1" || admission.schema !== "xyz.tinycloud.policy/delivery-admission/v0" || fields.some((field) => request[field] !== admission[field]) || request.policyId !== envelope.policyCid || request.recipient !== expected.request.recipientEmail || request.resource !== contentSource.kvResource || request.credentialType !== credentialType.id || request.credentialType !== "opencredentials.email/v1" || request.returnLink !== expected.request.shareUrl || request.envelopeRef !== expected.request.shareCid || request.audience !== expected.credentialsAudience || request.expiresAt !== expected.request.expiresAt || request.nonce !== expected.request.jti || admission.ownerDid !== envelopeSignature.signerDid || admission.senderKeyDid !== expected.senderKeyDid || canonicalizeSignedObjectUnsigned(admission.actions) !== canonicalizeSignedObjectUnsigned(["tinycloud.kv/get"]) || signature.suite !== "eddsa-ed25519-sha256-jcs-v1" || signature.signerDid !== target.nodeAudience) throw new Error("v3 share delivery authorization is not bound to the submitted request");
12344
+ const binding = object3(envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
12345
+ const fields = ["policyId", "recipient", "resource", "credentialType", "returnLink", "envelopeRef", "label", "shareExpiresAt", "audience", "issuedAt", "expiresAt", "nonce"];
12346
+ if (request.schema !== "xyz.tinycloud.credentials/invitation-request/v1" || admission.schema !== "xyz.tinycloud.policy/delivery-admission/v0" || fields.some((field) => request[field] !== admission[field]) || request.policyId !== envelope.policyCid || request.recipient !== expected.request.recipientEmail || request.resource !== contentSource.kvResource || request.credentialType !== credentialType.id || request.credentialType !== "opencredentials.email/v1" || request.returnLink !== expected.request.shareUrl || request.envelopeRef !== expected.request.shareCid || request.label !== expected.request.documentName || request.shareExpiresAt !== envelope.expiry || request.audience !== expected.deliveryAudience || request.expiresAt !== expected.request.expiresAt || request.nonce !== expected.request.jti || admission.ownerDid !== envelopeSignature.signerDid || admission.senderKeyDid !== expected.senderKeyDid || canonicalizeSignedObjectUnsigned(admission.actions) !== canonicalizeSignedObjectUnsigned(["tinycloud.kv/get"]) || signature.suite !== "eddsa-ed25519-sha256-jcs-v1" || target.nodeAudience !== binding.nodeAudience || signature.signerDid !== binding.nodeAudience) throw new Error("v3 share delivery authorization is not bound to the submitted request");
12368
12347
  const issuedAt = Date.parse(String(request.issuedAt));
12369
12348
  const expiresAt = Date.parse(String(request.expiresAt));
12370
12349
  if (!Number.isFinite(issuedAt) || !Number.isFinite(expiresAt) || expiresAt <= issuedAt || expiresAt - issuedAt > 15 * 6e4) throw new Error("v3 share delivery authorization time is invalid");
12371
12350
  const admissionSignature = decodeBase64Url3(signature.value);
12372
12351
  const { signature: _signature, ...unsignedAdmission } = admission;
12373
12352
  const digest4 = (0, import_sha2567.sha256)(new TextEncoder().encode(`${DELIVERY_ADMISSION_DOMAIN}${canonicalizeSignedObjectUnsigned(unsignedAdmission)}`));
12374
- if (admissionSignature.length !== 64 || !import_ed2551910.ed25519.verify(admissionSignature, digest4, didKeyBytes2(String(signature.signerDid)))) throw new Error("v3 share delivery admission signature is invalid");
12353
+ if (admissionSignature.length !== 64 || !import_ed255199.ed25519.verify(admissionSignature, digest4, didKeyBytes2(String(signature.signerDid)))) throw new Error("v3 share delivery admission signature is invalid");
12375
12354
  return { request, admission };
12376
12355
  }
12377
12356
 
@@ -17996,7 +17975,7 @@ function portForProtocol(protocol) {
17996
17975
  }
17997
17976
 
17998
17977
  // src/location.ts
17999
- var import_ed2551911 = require("@noble/curves/ed25519");
17978
+ var import_ed2551910 = require("@noble/curves/ed25519");
18000
17979
  var import_viem5 = require("viem");
18001
17980
  var DEFAULT_TINYCLOUD_LOCATION_REGISTRY_URL = "https://registry.tinycloud.xyz";
18002
17981
  var DEFAULT_TINYCLOUD_FALLBACK_HOST = "https://node.tinycloud.xyz";
@@ -18004,6 +17983,7 @@ var DEFAULT_LOCAL_NODE_URL = "http://127.0.0.1:8000";
18004
17983
  var LOCAL_LOOPBACK_PROBE_TIMEOUT_MS = 250;
18005
17984
  var LOCAL_LINK_PROBE_TIMEOUT_MS = 750;
18006
17985
  var LOCAL_LINK_HOST_SUFFIX = ".local.tinycloud.link";
17986
+ var OWNER_NODE_BINDING_TIMEOUT_MS = 5e3;
18007
17987
  function createInMemoryLocalNodeIdentityStore() {
18008
17988
  const pins = /* @__PURE__ */ new Map();
18009
17989
  return {
@@ -18054,6 +18034,13 @@ var LocationRecordValidationError = class extends Error {
18054
18034
  this.name = "LocationRecordValidationError";
18055
18035
  }
18056
18036
  };
18037
+ var LocationRegistryHttpError = class extends Error {
18038
+ constructor(message, status) {
18039
+ super(message);
18040
+ this.status = status;
18041
+ this.name = "LocationRegistryHttpError";
18042
+ }
18043
+ };
18057
18044
  var CloudLocationResolutionError = class extends Error {
18058
18045
  constructor(subject, attempts) {
18059
18046
  super(`Unable to resolve TinyCloud location for ${subject}`);
@@ -18136,21 +18123,80 @@ async function verifyLocationRecord(input) {
18136
18123
  }
18137
18124
  return false;
18138
18125
  }
18139
- async function fetchLocationRecord(registryUrl, subject, fetchFn = globalThis.fetch) {
18126
+ async function fetchLocationRecord(registryUrl, subject, fetchFn = globalThis.fetch, signal) {
18140
18127
  const url = `${registryUrl.replace(/\/$/, "")}/v1/locations/${encodeURIComponent(subject)}`;
18141
- const response = await fetchFn(url);
18128
+ const response = await fetchFn(url, {
18129
+ redirect: "error",
18130
+ headers: { accept: "application/json" },
18131
+ ...signal === void 0 ? {} : { signal }
18132
+ });
18142
18133
  if (response.status === 404) {
18143
18134
  return null;
18144
18135
  }
18145
18136
  if (!response.ok) {
18146
- throw new Error(`location registry returned HTTP ${response.status}`);
18137
+ throw new LocationRegistryHttpError(`location registry returned HTTP ${response.status}`, response.status);
18138
+ }
18139
+ const text2 = await boundedResponseText(response, "location registry", 64 * 1024);
18140
+ let body;
18141
+ try {
18142
+ body = JSON.parse(text2);
18143
+ } catch {
18144
+ throw new LocationRecordValidationError("registry response is not JSON");
18147
18145
  }
18148
- const body = await response.json();
18149
18146
  if (body.record === void 0) {
18150
18147
  throw new LocationRecordValidationError("registry response missing record");
18151
18148
  }
18152
18149
  return validateLocationRecord(body.record);
18153
18150
  }
18151
+ async function publishLocationRecord(options2) {
18152
+ validateSubject(options2.subject);
18153
+ const registryUrl = exactHttpOrigin(options2.registryUrl, "registry URL");
18154
+ const nodeOrigin = exactHttpOrigin(options2.nodeOrigin, "node origin");
18155
+ const fetchFn = options2.fetch ?? globalThis.fetch.bind(globalThis);
18156
+ const current = await fetchLocationRecord(registryUrl, options2.subject, fetchFn);
18157
+ if (current !== null) {
18158
+ if (current.subject !== options2.subject || !await verifyLocationRecord(current)) {
18159
+ throw new LocationRecordValidationError("existing location record signature is invalid");
18160
+ }
18161
+ if (publishedHttpOrigins(current).includes(nodeOrigin)) {
18162
+ return current;
18163
+ }
18164
+ }
18165
+ const record2 = await signLocationRecord({
18166
+ version: 1,
18167
+ subject: options2.subject,
18168
+ multiaddrs: [...current?.multiaddrs ?? [], httpUrlToMultiaddr(nodeOrigin)],
18169
+ updated_at: (options2.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
18170
+ sequence: (current?.sequence ?? -1) + 1
18171
+ }, options2.signer);
18172
+ const response = await fetchFn(
18173
+ `${registryUrl}/v1/locations/${encodeURIComponent(options2.subject)}`,
18174
+ {
18175
+ method: "PUT",
18176
+ redirect: "error",
18177
+ headers: { accept: "application/json", "content-type": "application/json" },
18178
+ body: JSON.stringify(record2)
18179
+ }
18180
+ );
18181
+ if (!response.ok) {
18182
+ throw new LocationRegistryHttpError(`location registry publish returned HTTP ${response.status}`, response.status);
18183
+ }
18184
+ const text2 = await boundedResponseText(response, "location registry", 64 * 1024);
18185
+ let body;
18186
+ try {
18187
+ body = JSON.parse(text2);
18188
+ } catch {
18189
+ throw new LocationRecordValidationError("registry publish response is not JSON");
18190
+ }
18191
+ if (body.record === void 0) {
18192
+ throw new LocationRecordValidationError("registry publish response missing record");
18193
+ }
18194
+ const published = validateLocationRecord(body.record);
18195
+ if (canonicalLocationPayload(locationPayloadForRecord(published)) !== canonicalLocationPayload(locationPayloadForRecord(record2)) || published.signature !== record2.signature || !await verifyLocationRecord(published)) {
18196
+ throw new LocationRecordValidationError("registry publish response does not match the signed record");
18197
+ }
18198
+ return published;
18199
+ }
18154
18200
  async function resolveCloudLocation(subject, options2 = {}) {
18155
18201
  validateSubject(subject);
18156
18202
  const verifyRecords = options2.verifyRecords ?? true;
@@ -18401,6 +18447,123 @@ function multiaddrToHttpUrl(input) {
18401
18447
  }
18402
18448
  return uri;
18403
18449
  }
18450
+ function exactHttpOrigin(value, label) {
18451
+ let url;
18452
+ try {
18453
+ url = new URL(value);
18454
+ } catch {
18455
+ throw new LocationRecordValidationError(`${label} must be an absolute URL origin`);
18456
+ }
18457
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
18458
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
18459
+ throw new LocationRecordValidationError(`${label} must use HTTPS (or loopback HTTP)`);
18460
+ }
18461
+ if (url.username !== "" || url.password !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "") {
18462
+ throw new LocationRecordValidationError(`${label} must be an origin without credentials, path, query, or fragment`);
18463
+ }
18464
+ return url.origin;
18465
+ }
18466
+ function publishedHttpOrigins(record2) {
18467
+ const origins = [];
18468
+ for (const address of record2.multiaddrs) {
18469
+ try {
18470
+ origins.push(exactHttpOrigin(multiaddrToHttpUrl(address), "published node URL"));
18471
+ } catch {
18472
+ }
18473
+ }
18474
+ return origins;
18475
+ }
18476
+ async function boundedResponseText(response, label, maxBytes) {
18477
+ const declaredLength = response.headers.get("content-length");
18478
+ if (declaredLength !== null && (!/^\d+$/.test(declaredLength) || Number(declaredLength) > maxBytes)) {
18479
+ throw new LocationRecordValidationError(`${label} response is too large`);
18480
+ }
18481
+ if (response.body === null) return "";
18482
+ const reader = response.body.getReader();
18483
+ const chunks = [];
18484
+ let total = 0;
18485
+ try {
18486
+ while (true) {
18487
+ const { done, value } = await reader.read();
18488
+ if (done) break;
18489
+ if (value === void 0) continue;
18490
+ total += value.byteLength;
18491
+ if (total > maxBytes) {
18492
+ await reader.cancel().catch(() => void 0);
18493
+ throw new LocationRecordValidationError(`${label} response is too large`);
18494
+ }
18495
+ chunks.push(value);
18496
+ }
18497
+ } finally {
18498
+ reader.releaseLock();
18499
+ }
18500
+ const bytes2 = new Uint8Array(total);
18501
+ let offset = 0;
18502
+ for (const chunk of chunks) {
18503
+ bytes2.set(chunk, offset);
18504
+ offset += chunk.byteLength;
18505
+ }
18506
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes2);
18507
+ }
18508
+ function boundedSignal(parent, timeoutMs) {
18509
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
18510
+ throw new LocationRecordValidationError("owner-node binding timeout must be positive");
18511
+ }
18512
+ const controller = new AbortController();
18513
+ const abortFromParent = () => controller.abort(parent?.reason);
18514
+ if (parent?.aborted) abortFromParent();
18515
+ else parent?.addEventListener("abort", abortFromParent, { once: true });
18516
+ const timer = setTimeout(() => controller.abort(new DOMException("owner-node binding timed out", "TimeoutError")), timeoutMs);
18517
+ return {
18518
+ signal: controller.signal,
18519
+ dispose() {
18520
+ clearTimeout(timer);
18521
+ parent?.removeEventListener("abort", abortFromParent);
18522
+ }
18523
+ };
18524
+ }
18525
+ async function verifyOwnerNodeBinding(options2) {
18526
+ validateSubject(options2.ownerDid);
18527
+ validateSubject(options2.nodeDid);
18528
+ const registryUrl = exactHttpOrigin(options2.registryUrl, "registry URL");
18529
+ const nodeOrigin = exactHttpOrigin(options2.nodeOrigin, "node origin");
18530
+ const fetchFn = options2.fetch ?? globalThis.fetch.bind(globalThis);
18531
+ const lookup = boundedSignal(options2.signal, options2.timeoutMs ?? OWNER_NODE_BINDING_TIMEOUT_MS);
18532
+ try {
18533
+ const record2 = await fetchLocationRecord(registryUrl, options2.ownerDid, fetchFn, lookup.signal);
18534
+ if (record2 === null) {
18535
+ throw new LocationRecordValidationError("owner has no published location record");
18536
+ }
18537
+ if (record2.subject !== options2.ownerDid || !await verifyLocationRecord(record2)) {
18538
+ throw new LocationRecordValidationError("owner location record signature is invalid");
18539
+ }
18540
+ const publishedOrigins = publishedHttpOrigins(record2);
18541
+ if (!publishedOrigins.includes(nodeOrigin)) {
18542
+ throw new LocationRecordValidationError("share target is not in the owner's signed location record");
18543
+ }
18544
+ const response = await fetchFn(new URL("/info", nodeOrigin), {
18545
+ redirect: "error",
18546
+ headers: { accept: "application/json" },
18547
+ signal: lookup.signal
18548
+ });
18549
+ if (!response.ok) {
18550
+ throw new LocationRecordValidationError(`target node /info returned HTTP ${response.status}`);
18551
+ }
18552
+ const text2 = await boundedResponseText(response, "target node /info", 16 * 1024);
18553
+ let body;
18554
+ try {
18555
+ body = JSON.parse(text2);
18556
+ } catch {
18557
+ throw new LocationRecordValidationError("target node /info response is not JSON");
18558
+ }
18559
+ if (body === null || typeof body !== "object" || Array.isArray(body) || body.nodeId !== options2.nodeDid) {
18560
+ throw new LocationRecordValidationError("target node identity does not match the share attestation");
18561
+ }
18562
+ return Object.freeze({ record: record2, nodeOrigin, nodeDid: options2.nodeDid });
18563
+ } finally {
18564
+ lookup.dispose();
18565
+ }
18566
+ }
18404
18567
  function httpUrlToMultiaddr(input) {
18405
18568
  const url = new URL(input);
18406
18569
  if (url.protocol !== "http:" && url.protocol !== "https:") {
@@ -18568,7 +18731,7 @@ function verifyDidKeySignature(did, payload, signature) {
18568
18731
  "did:key signature must be a base64url Ed25519 signature"
18569
18732
  );
18570
18733
  }
18571
- return import_ed2551911.ed25519.verify(
18734
+ return import_ed2551910.ed25519.verify(
18572
18735
  signatureBytes,
18573
18736
  new TextEncoder().encode(payload),
18574
18737
  publicKey
@@ -18576,7 +18739,7 @@ function verifyDidKeySignature(did, payload, signature) {
18576
18739
  }
18577
18740
  function verifyDidKeyEd25519Signature(did, payload, signature) {
18578
18741
  const publicKey = ed25519PublicKeyFromDidKey2(did);
18579
- return import_ed2551911.ed25519.verify(signature, payload, publicKey);
18742
+ return import_ed2551910.ed25519.verify(signature, payload, publicKey);
18580
18743
  }
18581
18744
  function ed25519PublicKeyFromDidKey2(did) {
18582
18745
  const identifier = did.slice("did:key:".length);