@tinycloud/sdk-core 3.0.0-beta.9 → 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,
@@ -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,
@@ -414,6 +421,7 @@ __export(index_exports, {
414
421
  serviceError: () => import_sdk_services8.serviceError,
415
422
  sha256Base64Url: () => sha256Base64Url,
416
423
  shareCapabilityAllows: () => shareCapabilityAllows,
424
+ signCompactPolicyDescendant: () => signCompactPolicyDescendant,
417
425
  signCompactUcanAuthorization: () => signCompactUcanAuthorization,
418
426
  signCompactUcanRootAuthorization: () => signCompactUcanRootAuthorization,
419
427
  signLocationRecord: () => signLocationRecord,
@@ -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;
@@ -9333,31 +9403,6 @@ function parsePublishedShareLink(link, options2 = {}) {
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
9405
  if (url.username !== "" || url.password !== "") throw new ShareEnvelopeError("invalid-link", "share link is not canonical");
9336
- if (url.pathname === "/viewer") {
9337
- const secretPayload = url.search === "" && url.hash.startsWith("#tc2=") ? url.hash.slice("#tc2=".length) : void 0;
9338
- const publicPayload = url.hash === "" && url.searchParams.size === 1 ? url.searchParams.get("tc2") ?? void 0 : void 0;
9339
- if (secretPayload === void 0 && publicPayload === void 0) throw new ShareEnvelopeError("invalid-link", "published share link is invalid");
9340
- if (publicPayload !== void 0 && url.search !== `?tc2=${publicPayload}`) throw new ShareEnvelopeError("invalid-link", "published share link is not canonical");
9341
- let payload;
9342
- try {
9343
- const bytes2 = base64UrlToBytes(secretPayload ?? publicPayload);
9344
- if (bytes2.length > (options2.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES) * 2) throw new Error("inline payload too large");
9345
- const text2 = new TextDecoder("utf-8", { fatal: true }).decode(bytes2);
9346
- const value = JSON.parse(text2);
9347
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("inline payload shape");
9348
- if (canonicalJson(value) !== text2) throw new Error("inline payload is not canonical JSON");
9349
- payload = value;
9350
- } catch (error) {
9351
- throw new ShareEnvelopeError("invalid-link", error instanceof Error ? error.message : "invalid inline payload");
9352
- }
9353
- const payloadKeys = Object.keys(payload);
9354
- if (payload.v !== 2 || typeof payload.c !== "string" || typeof payload.cid !== "string" || payload.k !== void 0 && typeof payload.k !== "string" || payloadKeys.some((key3) => !["v", "c", "cid", "k"].includes(key3)) || publicPayload !== void 0 && (payloadKeys.length !== 3 || payload.k !== void 0)) throw new ShareEnvelopeError("invalid-link", "inline payload fields are invalid");
9355
- const sealed = base64UrlToBytes(payload.c);
9356
- if (!verifyPublishedShareCid(payload.cid, sealed)) throw new ShareEnvelopeError("cid-mismatch", "inline payload CID does not match");
9357
- const key2 = payload.k === void 0 ? void 0 : base64UrlToBytes(payload.k);
9358
- if (key2 !== void 0 && key2.length !== 32) throw new ShareEnvelopeError("invalid-link", "inline payload key is invalid");
9359
- return { origin: origin2, cid: payload.cid, ...key2 === void 0 ? {} : { key: key2 }, sealed };
9360
- }
9361
9406
  if (url.search !== "") throw new ShareEnvelopeError("invalid-link", "share link is not canonical");
9362
9407
  const match = /^\/s\/([a-z2-7]+)$/.exec(url.pathname);
9363
9408
  if (match === null || !url.hash.startsWith("#k=")) throw new ShareEnvelopeError("invalid-link", "published share link is invalid");
@@ -9380,10 +9425,7 @@ function parseShareUrl(link, options2 = {}) {
9380
9425
  }
9381
9426
  const origin2 = normalizeOrigin(url.origin);
9382
9427
  if (options2.trustedOrigins !== void 0 && !options2.trustedOrigins.map(normalizeOrigin).includes(origin2)) throw new ShareEnvelopeError("origin-mismatch", "share link origin is not trusted");
9383
- if (url.pathname === "/viewer" && (url.hash.startsWith("#tc2=") || url.searchParams.has("tc2"))) {
9384
- const published = parsePublishedShareLink(link, options2);
9385
- return { kind: "inline", origin: origin2, cid: published.cid, key: published.key, url: `${url.origin}${url.pathname}`, protocol: "share-envelope-v2" };
9386
- }
9428
+ if (url.pathname === "/viewer") throw new ShareEnvelopeError("invalid-link", "retired viewer links are not accepted");
9387
9429
  if (url.search !== "") throw new ShareEnvelopeError("invalid-link", "share links must not contain query parameters");
9388
9430
  const match = url.pathname.match(/^\/s\/(i\/)?([^/]+)$/);
9389
9431
  if (!match) throw new ShareEnvelopeError("invalid-link", "share link path is invalid");
@@ -9408,9 +9450,9 @@ function parseShareUrl(link, options2 = {}) {
9408
9450
  function isV2ShareLink(link) {
9409
9451
  try {
9410
9452
  const url = new URL(link);
9411
- return url.pathname.startsWith("/s/") || url.pathname === "/viewer";
9453
+ return url.search === "" && url.pathname.startsWith("/s/");
9412
9454
  } catch {
9413
- return link.startsWith(INLINE_PREFIX);
9455
+ return false;
9414
9456
  }
9415
9457
  }
9416
9458
  async function encryptShareBytes(bytes2, key) {
@@ -10910,6 +10952,13 @@ var ShareRecipientClient = class {
10910
10952
  };
10911
10953
 
10912
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
+ }
10913
10962
  function inferShortServiceFromActionUrns(actions) {
10914
10963
  let short;
10915
10964
  for (const action of actions) {
@@ -11145,11 +11194,11 @@ var SharingService = class {
11145
11194
  }
11146
11195
  return result;
11147
11196
  };
11148
- const canSatisfyFromRegistry = this.findSuitableKeyForDelegation(
11197
+ const canSatisfyFromRegistry = this.classifyDelegationAuthority(
11149
11198
  fullPath,
11150
11199
  actions,
11151
11200
  requestedExpiry
11152
- );
11201
+ ) === "ok";
11153
11202
  if (canSatisfyFromRegistry) {
11154
11203
  const delegationResult = await this.createSessionDelegation(plainDID, fullPath, actions, expiry);
11155
11204
  const childDelegationActiveError = this.retiredGraphError();
@@ -11219,49 +11268,58 @@ var SharingService = class {
11219
11268
  return { ok: true, data: shareLink };
11220
11269
  }
11221
11270
  /**
11222
- * Check if any key in the registry can satisfy the delegation request.
11223
- * A key can satisfy if it has a delegation that:
11224
- * 1. Covers the required path (exact match or parent path)
11225
- * 2. Has all required actions
11226
- * 3. Has sufficient expiry (delegation.expiry >= requestedExpiry)
11227
- * 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.
11228
11298
  * @internal
11229
11299
  */
11230
- findSuitableKeyForDelegation(path, actions, requestedExpiry) {
11231
- const allKeys = this.registry.getAllKeys();
11232
- for (const key of allKeys) {
11233
- const delegations = this.registry.getDelegationsForKey(key.id);
11234
- for (const delegation of delegations) {
11235
- if (delegation.spaceId !== this.session?.spaceId) {
11236
- continue;
11237
- }
11238
- if ((delegation.caveats?.length ?? 0) > 0) {
11239
- continue;
11240
- }
11241
- if (!this.registry.isDelegationValid(delegation)) {
11242
- continue;
11243
- }
11244
- if (delegation.expiry < requestedExpiry) {
11245
- continue;
11246
- }
11247
- if (delegation.allowSubDelegation === false) {
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 ?? "")) {
11248
11305
  continue;
11249
11306
  }
11250
- const delegationPath = delegation.path || "";
11251
- 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("*"))) {
11252
11313
  continue;
11253
11314
  }
11254
- const delegationActions = delegation.actions || [];
11255
- const hasAllActions = actions.every(
11256
- (action) => delegationActions.includes(action) || delegationActions.includes("*")
11257
- );
11258
- if (!hasAllActions) {
11315
+ if ((delegation.caveats?.length ?? 0) > 0) {
11316
+ coveredOnlyByCaveated = true;
11259
11317
  continue;
11260
11318
  }
11261
- return true;
11319
+ return "ok";
11262
11320
  }
11263
11321
  }
11264
- return false;
11322
+ return coveredOnlyByCaveated ? "caveated" : "not-covered";
11265
11323
  }
11266
11324
  retiredGraphError() {
11267
11325
  try {
@@ -11360,11 +11418,23 @@ var SharingService = class {
11360
11418
  const errorText = await registerRes.text();
11361
11419
  const registrationTextActiveError = this.retiredGraphError();
11362
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
+ }
11363
11433
  return {
11364
11434
  ok: false,
11365
11435
  error: createError2(
11366
11436
  DelegationErrorCodes.CREATION_FAILED,
11367
- `Failed to register delegation with server: ${registerRes.status} ${errorText}`
11437
+ message
11368
11438
  )
11369
11439
  };
11370
11440
  }
@@ -17964,6 +18034,13 @@ var LocationRecordValidationError = class extends Error {
17964
18034
  this.name = "LocationRecordValidationError";
17965
18035
  }
17966
18036
  };
18037
+ var LocationRegistryHttpError = class extends Error {
18038
+ constructor(message, status) {
18039
+ super(message);
18040
+ this.status = status;
18041
+ this.name = "LocationRegistryHttpError";
18042
+ }
18043
+ };
17967
18044
  var CloudLocationResolutionError = class extends Error {
17968
18045
  constructor(subject, attempts) {
17969
18046
  super(`Unable to resolve TinyCloud location for ${subject}`);
@@ -18057,7 +18134,7 @@ async function fetchLocationRecord(registryUrl, subject, fetchFn = globalThis.fe
18057
18134
  return null;
18058
18135
  }
18059
18136
  if (!response.ok) {
18060
- throw new Error(`location registry returned HTTP ${response.status}`);
18137
+ throw new LocationRegistryHttpError(`location registry returned HTTP ${response.status}`, response.status);
18061
18138
  }
18062
18139
  const text2 = await boundedResponseText(response, "location registry", 64 * 1024);
18063
18140
  let body;
@@ -18102,7 +18179,7 @@ async function publishLocationRecord(options2) {
18102
18179
  }
18103
18180
  );
18104
18181
  if (!response.ok) {
18105
- throw new Error(`location registry publish returned HTTP ${response.status}`);
18182
+ throw new LocationRegistryHttpError(`location registry publish returned HTTP ${response.status}`, response.status);
18106
18183
  }
18107
18184
  const text2 = await boundedResponseText(response, "location registry", 64 * 1024);
18108
18185
  let body;