@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.js CHANGED
@@ -311,6 +311,7 @@ var CREDENTIAL_ERROR_CODES = [
311
311
  "ISSUER_UNREADY",
312
312
  "OFFLINE",
313
313
  "POPUP_BLOCKED",
314
+ "PROOF_REJECTED",
314
315
  "REQUEST_EXPIRED",
315
316
  "REQUEST_SUBSTITUTED",
316
317
  "SIGNATURE_REJECTED",
@@ -324,6 +325,7 @@ var RECOVERABLE = /* @__PURE__ */ new Set([
324
325
  "ISSUER_UNREADY",
325
326
  "OFFLINE",
326
327
  "POPUP_BLOCKED",
328
+ "PROOF_REJECTED",
327
329
  "REQUEST_EXPIRED",
328
330
  "SIGNATURE_REJECTED",
329
331
  "UNSUPPORTED_PROFILE",
@@ -392,6 +394,7 @@ function holderBindingSigningBytes(binding) {
392
394
 
393
395
  // src/credentials/descriptor.ts
394
396
  var ID = /^[a-z0-9][a-z0-9._-]{0,127}(?:\/v1)?$/;
397
+ var CLAIM_NAME = /^[a-z0-9][A-Za-z0-9._-]{0,127}$/;
395
398
  var STEP_TYPES = /* @__PURE__ */ new Set(["collect_input", "mailbox_otp", "holder_signature"]);
396
399
  var ENDPOINTS = {
397
400
  request: "request",
@@ -472,7 +475,7 @@ function validateCredentialFlowDescriptor(value) {
472
475
  for (const value2 of raw.claims) {
473
476
  const claim = object(value2, "claim");
474
477
  exact(claim, ["name", "matching", "selectiveDisclosure"], "claim");
475
- const name2 = text(claim.name, "claim.name", ID);
478
+ const name2 = text(claim.name, "claim.name", CLAIM_NAME);
476
479
  if (claimNames.has(name2) || claim.matching !== "normalized_exact" || typeof claim.selectiveDisclosure !== "boolean") fail("claim is invalid");
477
480
  claimNames.add(name2);
478
481
  }
@@ -563,9 +566,14 @@ function isDigest(value) {
563
566
  return typeof value === "string" && /^[A-Za-z0-9_-]{43}$/.test(value);
564
567
  }
565
568
 
569
+ // src/credentials/email-domain.ts
570
+ import { canonicalEmailDomain, canonicalMailbox, isCanonicalEmailDomain, mailboxBelongsToDomain } from "@tinycloud/share-envelope";
571
+
566
572
  // src/credentials/requirement.ts
567
573
  var ID2 = /^[a-z0-9][a-z0-9._-]{0,127}(?:\/v1)?$/;
568
574
  var EMAIL_CREDENTIAL_MAX_AGE_SECONDS = 3600;
575
+ var EMAIL_DOMAIN_CREDENTIAL_MAX_AGE_SECONDS = 300;
576
+ var CLAIM_NAME2 = /^[a-z0-9][A-Za-z0-9._-]{0,127}$/;
569
577
  function createEmailCredentialRequirement(input) {
570
578
  return validateCredentialRequirement({
571
579
  type: "TinyCloudCredentialRequirement",
@@ -576,6 +584,17 @@ function createEmailCredentialRequirement(input) {
576
584
  maxAgeSeconds: EMAIL_CREDENTIAL_MAX_AGE_SECONDS
577
585
  });
578
586
  }
587
+ function createEmailDomainCredentialRequirement(input) {
588
+ if (!isCanonicalEmailDomain(input.domain)) throw new CredentialError("UNSUPPORTED_PROFILE", "Email domain is not canonical");
589
+ return validateCredentialRequirement({
590
+ type: "TinyCloudCredentialRequirement",
591
+ version: 1,
592
+ profile: input.profile,
593
+ credentialType: input.credentialType,
594
+ claims: { emailDomain: input.domain },
595
+ maxAgeSeconds: EMAIL_DOMAIN_CREDENTIAL_MAX_AGE_SECONDS
596
+ });
597
+ }
579
598
  function validateCredentialRequirement(value) {
580
599
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new CredentialError("UNSUPPORTED_PROFILE", "Credential requirement is invalid");
581
600
  const raw = value;
@@ -589,7 +608,7 @@ function validateCredentialRequirement(value) {
589
608
  if (typeof raw.claims !== "object" || raw.claims === null || Array.isArray(raw.claims)) throw new CredentialError("UNSUPPORTED_PROFILE", "Credential claims are invalid");
590
609
  const claims = {};
591
610
  for (const [key, value2] of Object.entries(raw.claims).sort(([a], [b]) => a.localeCompare(b))) {
592
- if (!ID2.test(key) || typeof value2 !== "string" || value2.length === 0 || value2.length > 4096) throw new CredentialError("UNSUPPORTED_PROFILE", "Credential claim is invalid");
611
+ if (!CLAIM_NAME2.test(key) || typeof value2 !== "string" || value2.length === 0 || value2.length > 4096) throw new CredentialError("UNSUPPORTED_PROFILE", "Credential claim is invalid");
593
612
  claims[key] = value2;
594
613
  }
595
614
  if (raw.maxAgeSeconds !== void 0 && (!Number.isSafeInteger(raw.maxAgeSeconds) || raw.maxAgeSeconds < 1)) throw new CredentialError("UNSUPPORTED_PROFILE", "Credential freshness is invalid");
@@ -4012,8 +4031,9 @@ async function signCompactUcanAuthorization(input) {
4012
4031
  throw new Error("compact invocation lifetime must be between one and 60 seconds");
4013
4032
  return signCompactUcan(input);
4014
4033
  }
4034
+ var MAX_POLICY_ROOT_SECONDS = 31 * 24 * 60 * 60;
4015
4035
  async function signCompactUcanRootAuthorization(input) {
4016
- if (input.notBefore >= input.expiresAt || input.expiresAt - input.notBefore > 31 * 24 * 60 * 60)
4036
+ if (input.notBefore >= input.expiresAt || input.expiresAt - input.notBefore > MAX_POLICY_ROOT_SECONDS)
4017
4037
  throw new Error("compact policy root lifetime must be between one second and 31 days");
4018
4038
  return signCompactUcan({ ...input, proofs: [] });
4019
4039
  }
@@ -4048,7 +4068,7 @@ function createCompactPolicyInvocation(input) {
4048
4068
  const signature = ed255193.sign(signingInput, input.privateKey);
4049
4069
  return parseCompactUcanAuthorization(`${protectedSegment}.${payloadSegment}.${encodeBase64Url2(signature)}`);
4050
4070
  }
4051
- function createCompactPolicyDescendant(input) {
4071
+ function compactPolicyDescendantClaims(input) {
4052
4072
  const parent = parseCompactUcanAuthorization(input.parentAuthorization, input.parentCid);
4053
4073
  if (parent.payload.aud !== input.issuerDid) throw new Error("policy descendant issuer is not the parent audience");
4054
4074
  const parentFact = parent.payload.fct[0];
@@ -4056,29 +4076,53 @@ function createCompactPolicyDescendant(input) {
4056
4076
  if (!Number.isInteger(depth) || depth <= 0) throw new Error("policy descendant depth is exhausted");
4057
4077
  if (!compactAttenuationContains(parent.payload.att, input.attenuation))
4058
4078
  throw new Error("policy descendant exceeds the parent attenuation");
4079
+ const now = Math.max(input.now ?? parent.payload.nbf + 1, parent.payload.nbf + 1);
4080
+ const exp = Math.min(input.expiresAt ?? parent.payload.exp - 1, parent.payload.exp - 1);
4081
+ if (exp <= now) throw new Error("policy parent has no strictly narrower validity window");
4082
+ return {
4083
+ parentCid: parent.cid,
4084
+ fact: { ...parentFact, remainingRedelegationDepth: depth - 1 },
4085
+ notBefore: now,
4086
+ expiresAt: exp,
4087
+ nonce: input.nonce ?? encodeBase64Url2(crypto.getRandomValues(new Uint8Array(16)))
4088
+ };
4089
+ }
4090
+ function createCompactPolicyDescendant(input) {
4091
+ const claims = compactPolicyDescendantClaims(input);
4059
4092
  const principal = input.issuerDid.split("#", 1)[0];
4060
4093
  const didMaterial = base58btc.decode(principal.slice("did:key:".length));
4061
4094
  const publicKey = ed255193.getPublicKey(input.privateKey);
4062
4095
  if (didMaterial.length !== 34 || !equalBytes(publicKey, didMaterial.slice(2))) throw new Error("policy descendant signer does not bind issuer DID");
4063
- const now = Math.max(input.now ?? Math.floor(Date.now() / 1e3), parent.payload.nbf + 1);
4064
- const exp = Math.min(input.expiresAt ?? now + 60, now + 60, parent.payload.exp - 1);
4065
- if (exp <= now) throw new Error("policy parent has no strictly narrower validity window");
4066
4096
  const header = { alg: "EdDSA", jwk: { alg: "EdDSA", crv: "Ed25519", kty: "OKP", x: encodeBase64Url2(publicKey) }, typ: "JWT", ucv: "0.10.0" };
4067
4097
  const payload = {
4068
4098
  att: input.attenuation,
4069
4099
  aud: input.audienceDid,
4070
- exp,
4071
- fct: [{ ...parentFact, remainingRedelegationDepth: depth - 1 }],
4100
+ exp: claims.expiresAt,
4101
+ fct: [claims.fact],
4072
4102
  iss: input.issuerDid.includes("#") ? input.issuerDid : `${principal}#${principal.slice("did:key:".length)}`,
4073
- nbf: now,
4074
- nnc: input.nonce ?? encodeBase64Url2(crypto.getRandomValues(new Uint8Array(16))),
4075
- prf: [parent.cid]
4103
+ nbf: claims.notBefore,
4104
+ nnc: claims.nonce,
4105
+ prf: [claims.parentCid]
4076
4106
  };
4077
4107
  const protectedSegment = encodeBase64Url2(new TextEncoder().encode(jcsCanonicalize(header)));
4078
4108
  const payloadSegment = encodeBase64Url2(new TextEncoder().encode(jcsCanonicalize(payload)));
4079
4109
  const signature = ed255193.sign(new TextEncoder().encode(`${protectedSegment}.${payloadSegment}`), input.privateKey);
4080
4110
  return parseCompactUcanAuthorization(`${protectedSegment}.${payloadSegment}.${encodeBase64Url2(signature)}`);
4081
4111
  }
4112
+ async function signCompactPolicyDescendant(input) {
4113
+ const claims = compactPolicyDescendantClaims(input);
4114
+ return signCompactUcan({
4115
+ issuerDid: input.issuerDid,
4116
+ audienceDid: input.audienceDid,
4117
+ attenuation: input.attenuation,
4118
+ facts: [claims.fact],
4119
+ proofs: [claims.parentCid],
4120
+ notBefore: claims.notBefore,
4121
+ expiresAt: claims.expiresAt,
4122
+ nonce: claims.nonce,
4123
+ sign: input.sign
4124
+ });
4125
+ }
4082
4126
  function compactAttenuationContains(parent, child) {
4083
4127
  const selector = (value) => {
4084
4128
  if (value.length !== 1 || !isRecordValue(value[0])) return void 0;
@@ -4148,7 +4192,7 @@ function parsePolicySessionUcan(authorization, expectedProofs) {
4148
4192
  throw new Error("policy session UCAN fact is incomplete");
4149
4193
  if (hasV4AuditFactShape && (!LOWER_SHA256_HEX.test(fact.credentialIdAuditDigestHex) || !LOWER_SHA256_HEX.test(fact.presentationJtiAuditDigestHex)))
4150
4194
  throw new Error("policy session UCAN v4 audit facts are invalid");
4151
- 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)
4195
+ 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)
4152
4196
  throw new Error("policy session UCAN fact is invalid");
4153
4197
  return {
4154
4198
  authorization,
@@ -4572,24 +4616,21 @@ async function admitPolicyCredentialV3(input) {
4572
4616
  policy,
4573
4617
  challenge
4574
4618
  });
4575
- const response = await fetchFn(
4619
+ const response = await postPolicyDelegation(
4620
+ fetchFn,
4576
4621
  new URL(`${policyRuntimeBasePath2(input.policyRuntimePath)}/delegations`, nodeOrigin),
4577
4622
  {
4578
- method: "POST",
4579
- redirect: "error",
4580
- signal: input.signal,
4581
- headers: { accept: "application/json", "content-type": "application/json" },
4582
- body: JSON.stringify({
4583
- policyCid: input.policyCid,
4584
- challengeId: challenge.challengeId,
4585
- nonce: challenge.nonce,
4586
- requirement: validateCredentialRequirement(input.requirement),
4587
- credential: envelopeFromVerified(input.credential),
4588
- accountAuthorizationCid,
4589
- credentialSpaceId,
4590
- presentation
4591
- })
4592
- }
4623
+ policyCid: input.policyCid,
4624
+ challengeId: challenge.challengeId,
4625
+ nonce: challenge.nonce,
4626
+ requirement: validateCredentialRequirement(input.requirement),
4627
+ credential: envelopeFromVerified(input.credential),
4628
+ accountAuthorizationCid,
4629
+ credentialSpaceId,
4630
+ presentation
4631
+ },
4632
+ requestedSessionExpiry(input.requestedExpiresAt, policy),
4633
+ input.signal
4593
4634
  );
4594
4635
  if (!response.ok) throw new Error(`policy delegation rejected (${response.status})`);
4595
4636
  const value = record(await response.json(), "policy delegation");
@@ -4630,20 +4671,20 @@ async function admitPolicyCredentialV4(input) {
4630
4671
  policy,
4631
4672
  challenge
4632
4673
  });
4633
- const response = await fetchFn(new URL(`${policyRuntimeBasePath2(input.policyRuntimePath)}/delegations`, nodeOrigin), {
4634
- method: "POST",
4635
- redirect: "error",
4636
- signal: input.signal,
4637
- headers: { accept: "application/json", "content-type": "application/json" },
4638
- body: JSON.stringify({
4674
+ const response = await postPolicyDelegation(
4675
+ fetchFn,
4676
+ new URL(`${policyRuntimeBasePath2(input.policyRuntimePath)}/delegations`, nodeOrigin),
4677
+ {
4639
4678
  policyCid: input.policyCid,
4640
4679
  challengeId: challenge.challengeId,
4641
4680
  nonce: challenge.nonce,
4642
4681
  requirement: validateCredentialRequirement(input.requirement),
4643
4682
  credential: envelopeFromVerified(input.credential),
4644
4683
  presentation
4645
- })
4646
- });
4684
+ },
4685
+ requestedSessionExpiry(input.requestedExpiresAt, policy),
4686
+ input.signal
4687
+ );
4647
4688
  if (!response.ok) throw new Error(`policy delegation rejected (${response.status})`);
4648
4689
  const value = record(await response.json(), "policy delegation");
4649
4690
  if (value.admitted !== true || typeof value.sessionCid !== "string" || typeof value.authorization !== "string") throw new Error("policy delegation response is invalid");
@@ -4667,6 +4708,27 @@ async function admitPolicyCredentialV4(input) {
4667
4708
  input.signal?.throwIfAborted();
4668
4709
  return Object.freeze({ challenge, presentation, session, delegationImported: true });
4669
4710
  }
4711
+ function requestedSessionExpiry(requested, policy) {
4712
+ const value = requested === void 0 ? policy.expiresAt : requested;
4713
+ if (value === null || value === void 0) return void 0;
4714
+ const seconds = Math.floor(Date.parse(value) / 1e3);
4715
+ if (!Number.isFinite(seconds) || seconds <= Math.floor(Date.now() / 1e3) + 60) return void 0;
4716
+ return new Date(seconds * 1e3).toISOString().replace(/\.\d{3}Z$/, "Z");
4717
+ }
4718
+ async function postPolicyDelegation(fetchFn, url, body, requestedExpiresAt, signal) {
4719
+ const send = (payload) => fetchFn(url, {
4720
+ method: "POST",
4721
+ redirect: "error",
4722
+ signal,
4723
+ headers: { accept: "application/json", "content-type": "application/json" },
4724
+ body: JSON.stringify(payload)
4725
+ });
4726
+ if (requestedExpiresAt === void 0) return send(body);
4727
+ const response = await send({ ...body, requestedExpiresAt });
4728
+ if (response.status !== 422) return response;
4729
+ await response.body?.cancel().catch(() => void 0);
4730
+ return send(body);
4731
+ }
4670
4732
  function policyRuntimeBasePath2(value) {
4671
4733
  const path = value ?? "/policy/v3";
4672
4734
  if (!/^\/policy(?:\/[a-z0-9-]+)*$/.test(path)) {
@@ -6042,6 +6104,7 @@ var DelegationErrorSchema = z4.object({
6042
6104
  });
6043
6105
  var DelegationErrorCodes = {
6044
6106
  AUTH_REQUIRED: "AUTH_REQUIRED",
6107
+ AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
6045
6108
  AUTH_EXPIRED: "AUTH_EXPIRED",
6046
6109
  NOT_INITIALIZED: "NOT_INITIALIZED",
6047
6110
  NOT_FOUND: "NOT_FOUND",
@@ -6406,7 +6469,7 @@ var CreateDelegationWasmResultSchema = z4.object({
6406
6469
  // src/expiry.ts
6407
6470
  var EPHEMERAL_MS = 60 * 60 * 1e3;
6408
6471
  var SIGNED_READ_URL_MS = 5 * 60 * 1e3;
6409
- var SESSION_MS = 7 * 24 * 60 * 60 * 1e3;
6472
+ var SESSION_MS = 30 * 24 * 60 * 60 * 1e3;
6410
6473
  var SHARE_MS = 7 * 24 * 60 * 60 * 1e3;
6411
6474
  var APP_MS = 30 * 24 * 60 * 60 * 1e3;
6412
6475
  var MAX_MS = 10 * 365 * 24 * 60 * 60 * 1e3;
@@ -8174,7 +8237,6 @@ import { ed25519 as ed255195 } from "@noble/curves/ed25519";
8174
8237
  import { blake3 as blake33 } from "@noble/hashes/blake3";
8175
8238
  import { sha256 as sha2567 } from "@noble/hashes/sha256";
8176
8239
  var ENVELOPE_SCHEMA = "xyz.tinycloud.share/envelope";
8177
- var INLINE_PREFIX = "tc2:";
8178
8240
  var RAW_CID_CODEC = 85;
8179
8241
  var BLAKE3_256_CODE = 30;
8180
8242
  var DEFAULT_MAX_INLINE_BYTES = 256 * 1024;
@@ -8879,25 +8941,8 @@ function parsePublishedShareLink(link, options2 = {}) {
8879
8941
  }
8880
8942
  const origin2 = normalizeOrigin(url.origin);
8881
8943
  if (options2.trustedOrigins !== void 0 && !options2.trustedOrigins.map(normalizeOrigin).includes(origin2)) throw new ShareEnvelopeError("origin-mismatch", "share link origin is not trusted");
8882
- if (url.search !== "" || url.username !== "" || url.password !== "") throw new ShareEnvelopeError("invalid-link", "share link is not canonical");
8883
- if (url.pathname === "/s/inline" && url.hash.startsWith("#v=2&p=")) {
8884
- let payload;
8885
- try {
8886
- const bytes2 = base64UrlToBytes(url.hash.slice("#v=2&p=".length));
8887
- if (bytes2.length > (options2.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES) * 2) throw new Error("inline payload too large");
8888
- const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes2));
8889
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error("inline payload shape");
8890
- payload = value;
8891
- } catch (error) {
8892
- throw new ShareEnvelopeError("invalid-link", error instanceof Error ? error.message : "invalid inline payload");
8893
- }
8894
- 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");
8895
- const sealed = base64UrlToBytes(payload.c);
8896
- if (!verifyPublishedShareCid(payload.cid, sealed)) throw new ShareEnvelopeError("cid-mismatch", "inline payload CID does not match");
8897
- const key2 = payload.k === void 0 ? void 0 : base64UrlToBytes(payload.k);
8898
- if (key2 !== void 0 && key2.length !== 32) throw new ShareEnvelopeError("invalid-link", "inline payload key is invalid");
8899
- return { origin: origin2, cid: payload.cid, ...key2 === void 0 ? {} : { key: key2 }, sealed };
8900
- }
8944
+ if (url.username !== "" || url.password !== "") throw new ShareEnvelopeError("invalid-link", "share link is not canonical");
8945
+ if (url.search !== "") throw new ShareEnvelopeError("invalid-link", "share link is not canonical");
8901
8946
  const match = /^\/s\/([a-z2-7]+)$/.exec(url.pathname);
8902
8947
  if (match === null || !url.hash.startsWith("#k=")) throw new ShareEnvelopeError("invalid-link", "published share link is invalid");
8903
8948
  const key = base64UrlToBytes(url.hash.slice(3));
@@ -8917,13 +8962,10 @@ function parseShareUrl(link, options2 = {}) {
8917
8962
  } catch {
8918
8963
  throw new ShareEnvelopeError("invalid-link", "share link must be an absolute URL");
8919
8964
  }
8920
- if (url.search !== "") throw new ShareEnvelopeError("invalid-link", "share links must not contain query parameters");
8921
8965
  const origin2 = normalizeOrigin(url.origin);
8922
8966
  if (options2.trustedOrigins !== void 0 && !options2.trustedOrigins.map(normalizeOrigin).includes(origin2)) throw new ShareEnvelopeError("origin-mismatch", "share link origin is not trusted");
8923
- if (url.pathname === "/s/inline" && url.hash.startsWith("#v=2&p=")) {
8924
- const published = parsePublishedShareLink(link, options2);
8925
- return { kind: "inline", origin: origin2, cid: published.cid, key: published.key, url: `${url.origin}${url.pathname}`, protocol: "share-envelope-v2" };
8926
- }
8967
+ if (url.pathname === "/viewer") throw new ShareEnvelopeError("invalid-link", "retired viewer links are not accepted");
8968
+ if (url.search !== "") throw new ShareEnvelopeError("invalid-link", "share links must not contain query parameters");
8927
8969
  const match = url.pathname.match(/^\/s\/(i\/)?([^/]+)$/);
8928
8970
  if (!match) throw new ShareEnvelopeError("invalid-link", "share link path is invalid");
8929
8971
  const fragmentParams = url.hash === "" ? void 0 : new URLSearchParams(url.hash.slice(1));
@@ -8946,9 +8988,10 @@ function parseShareUrl(link, options2 = {}) {
8946
8988
  }
8947
8989
  function isV2ShareLink(link) {
8948
8990
  try {
8949
- return new URL(link).pathname.startsWith("/s/");
8991
+ const url = new URL(link);
8992
+ return url.search === "" && url.pathname.startsWith("/s/");
8950
8993
  } catch {
8951
- return link.startsWith(INLINE_PREFIX);
8994
+ return false;
8952
8995
  }
8953
8996
  }
8954
8997
  async function encryptShareBytes(bytes2, key) {
@@ -10448,6 +10491,13 @@ var ShareRecipientClient = class {
10448
10491
  };
10449
10492
 
10450
10493
  // src/delegations/SharingService.ts
10494
+ function spaceIdsEqual(left, right) {
10495
+ const normalizeOwnerAddress = (space) => {
10496
+ const match = /^(tinycloud:pkh:eip155:\d+:)(0x[0-9a-fA-F]{40})(:.+)$/.exec(space);
10497
+ return match === null ? space : `${match[1]}${match[2].toLowerCase()}${match[3]}`;
10498
+ };
10499
+ return normalizeOwnerAddress(left) === normalizeOwnerAddress(right);
10500
+ }
10451
10501
  function inferShortServiceFromActionUrns(actions) {
10452
10502
  let short;
10453
10503
  for (const action of actions) {
@@ -10683,11 +10733,11 @@ var SharingService = class {
10683
10733
  }
10684
10734
  return result;
10685
10735
  };
10686
- const canSatisfyFromRegistry = this.findSuitableKeyForDelegation(
10736
+ const canSatisfyFromRegistry = this.classifyDelegationAuthority(
10687
10737
  fullPath,
10688
10738
  actions,
10689
10739
  requestedExpiry
10690
- );
10740
+ ) === "ok";
10691
10741
  if (canSatisfyFromRegistry) {
10692
10742
  const delegationResult = await this.createSessionDelegation(plainDID, fullPath, actions, expiry);
10693
10743
  const childDelegationActiveError = this.retiredGraphError();
@@ -10757,49 +10807,58 @@ var SharingService = class {
10757
10807
  return { ok: true, data: shareLink };
10758
10808
  }
10759
10809
  /**
10760
- * Check if any key in the registry can satisfy the delegation request.
10761
- * A key can satisfy if it has a delegation that:
10762
- * 1. Covers the required path (exact match or parent path)
10763
- * 2. Has all required actions
10764
- * 3. Has sufficient expiry (delegation.expiry >= requestedExpiry)
10765
- * 4. Allows sub-delegation
10810
+ * Report, without side effects, whether this session's registry authority
10811
+ * can issue the child delegation {@link generate} would create for the same
10812
+ * `path`, `actions` and `expiry` (defaults and `pathPrefix` applied the
10813
+ * same way):
10814
+ *
10815
+ * - `"ok"`: an uncaveated entry covers it.
10816
+ * - `"caveated"`: only caveated entries cover it. SharingService cannot
10817
+ * carry signed ReCap caveats into a child delegation.
10818
+ * - `"not-covered"`: no entry covers it, or there is no session.
10819
+ *
10820
+ * It does not consult `onRootDelegationNeeded`, so a signer-backed session
10821
+ * may still generate when this is not `"ok"`.
10822
+ */
10823
+ preflightGenerate(params) {
10824
+ if (!this.session || !params.path) return "not-covered";
10825
+ const fullPath = this.pathPrefix ? `${this.pathPrefix}/${params.path}`.replace(/\/+/g, "/") : params.path;
10826
+ return this.classifyDelegationAuthority(
10827
+ fullPath,
10828
+ params.actions ?? DEFAULT_READ_ACTIONS,
10829
+ params.expiry ?? new Date(Date.now() + DEFAULT_EXPIRY_MS)
10830
+ );
10831
+ }
10832
+ /**
10833
+ * Classify the registry's authority for a child delegation. An entry
10834
+ * satisfies it when it is in the session space, valid, lasts at least until
10835
+ * `requestedExpiry`, allows sub-delegation, covers `path` (exact or parent)
10836
+ * and grants every action. Satisfying entries with caveats are unusable.
10766
10837
  * @internal
10767
10838
  */
10768
- findSuitableKeyForDelegation(path, actions, requestedExpiry) {
10769
- const allKeys = this.registry.getAllKeys();
10770
- for (const key of allKeys) {
10771
- const delegations = this.registry.getDelegationsForKey(key.id);
10772
- for (const delegation of delegations) {
10773
- if (delegation.spaceId !== this.session?.spaceId) {
10774
- continue;
10775
- }
10776
- if ((delegation.caveats?.length ?? 0) > 0) {
10777
- continue;
10778
- }
10779
- if (!this.registry.isDelegationValid(delegation)) {
10839
+ classifyDelegationAuthority(path, actions, requestedExpiry) {
10840
+ let coveredOnlyByCaveated = false;
10841
+ for (const key of this.registry.getAllKeys()) {
10842
+ for (const delegation of this.registry.getDelegationsForKey(key.id)) {
10843
+ if (delegation.spaceId === void 0 || !spaceIdsEqual(delegation.spaceId, this.session?.spaceId ?? "")) {
10780
10844
  continue;
10781
10845
  }
10782
- if (delegation.expiry < requestedExpiry) {
10783
- continue;
10784
- }
10785
- if (delegation.allowSubDelegation === false) {
10786
- continue;
10787
- }
10788
- const delegationPath = delegation.path || "";
10789
- if (!this.pathMatches(delegationPath, path)) {
10846
+ if (!this.registry.isDelegationValid(delegation)) continue;
10847
+ if (delegation.expiry < requestedExpiry) continue;
10848
+ if (delegation.allowSubDelegation === false) continue;
10849
+ if (!this.pathMatches(delegation.path || "", path)) continue;
10850
+ const delegationActions = delegation.actions || [];
10851
+ if (!actions.every((action) => delegationActions.includes(action) || delegationActions.includes("*"))) {
10790
10852
  continue;
10791
10853
  }
10792
- const delegationActions = delegation.actions || [];
10793
- const hasAllActions = actions.every(
10794
- (action) => delegationActions.includes(action) || delegationActions.includes("*")
10795
- );
10796
- if (!hasAllActions) {
10854
+ if ((delegation.caveats?.length ?? 0) > 0) {
10855
+ coveredOnlyByCaveated = true;
10797
10856
  continue;
10798
10857
  }
10799
- return true;
10858
+ return "ok";
10800
10859
  }
10801
10860
  }
10802
- return false;
10861
+ return coveredOnlyByCaveated ? "caveated" : "not-covered";
10803
10862
  }
10804
10863
  retiredGraphError() {
10805
10864
  try {
@@ -10898,11 +10957,23 @@ var SharingService = class {
10898
10957
  const errorText = await registerRes.text();
10899
10958
  const registrationTextActiveError = this.retiredGraphError();
10900
10959
  if (registrationTextActiveError) return { ok: false, error: registrationTextActiveError };
10960
+ const message = `Failed to register delegation with server: ${registerRes.status} ${errorText}`;
10961
+ if (registerRes.status === 401 || registerRes.status === 403) {
10962
+ return {
10963
+ ok: false,
10964
+ error: createError2(
10965
+ DelegationErrorCodes.AUTH_UNAUTHORIZED,
10966
+ message,
10967
+ void 0,
10968
+ { status: registerRes.status }
10969
+ )
10970
+ };
10971
+ }
10901
10972
  return {
10902
10973
  ok: false,
10903
10974
  error: createError2(
10904
10975
  DelegationErrorCodes.CREATION_FAILED,
10905
- `Failed to register delegation with server: ${registerRes.status} ${errorText}`
10976
+ message
10906
10977
  )
10907
10978
  };
10908
10979
  }
@@ -11759,114 +11830,13 @@ function validateOwnerSharePolicyRegistrationBytes(bytesValue, expected) {
11759
11830
  return validateOwnerSharePolicyRegistration(value, expected);
11760
11831
  }
11761
11832
 
11762
- // src/delegations/share-delivery.ts
11763
- import { ed25519 as ed255199 } from "@noble/curves/ed25519";
11764
- var SHARE_DELIVERY_AUTHORIZATION_DOMAIN = "xyz.tinycloud.share/delivery-authorization/v2\0";
11765
- function isCanonicalHttpsOrigin(value) {
11766
- let url;
11767
- try {
11768
- url = new URL(value);
11769
- } catch {
11770
- return false;
11771
- }
11772
- return url.protocol === "https:" && url.origin === value && url.pathname === "/" && url.search === "" && url.hash === "" && url.username === "" && url.password === "";
11773
- }
11774
- var AUTHORIZATION_KEYS = [
11775
- "type",
11776
- "version",
11777
- "jti",
11778
- "shareCid",
11779
- "shareId",
11780
- "registrationCid",
11781
- "delegationCid",
11782
- "enforcementDelegationCid",
11783
- "envelopeCid",
11784
- "policyCid",
11785
- "nodeAudience",
11786
- "targetOrigin",
11787
- "openCredentialsAudience",
11788
- "holder",
11789
- "recipientMatcher",
11790
- "deliveryEmail",
11791
- "shareUrl",
11792
- "returnOrigin",
11793
- "documentName",
11794
- "senderDid",
11795
- "senderTrust",
11796
- "contentSource",
11797
- "contentSourceDigest",
11798
- "shareExpiresAt",
11799
- "issuedAt",
11800
- "reportAbuseToken",
11801
- "actions",
11802
- "resource",
11803
- "authorityMaterialHandle",
11804
- "authorityMaterialDigest",
11805
- "requestBodyDigest",
11806
- "idempotencyKey",
11807
- "expiresAt",
11808
- "dataAuthority"
11809
- ];
11810
- function assertObject2(value, keys, label) {
11811
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} is invalid`);
11812
- const record2 = value;
11813
- 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`);
11814
- return record2;
11815
- }
11816
- function fromB642(value) {
11817
- if (!/^[A-Za-z0-9_-]+$/.test(value)) throw new Error("share delivery value is not canonical base64url");
11818
- if (typeof atob === "function") {
11819
- const padded = value.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - value.length % 4) % 4);
11820
- return Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
11821
- }
11822
- if (typeof Buffer !== "undefined") return new Uint8Array(Buffer.from(value, "base64url"));
11823
- throw new Error("base64url decoding is unavailable");
11824
- }
11825
- function validateShareDeliveryAuthorizationBytes(bytesValue, expected) {
11826
- let value;
11827
- try {
11828
- value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytesValue));
11829
- } catch {
11830
- throw new Error("share delivery authorization response is not valid UTF-8 JSON");
11831
- }
11832
- const root = assertObject2(value, ["authorization", "proof"], "share delivery authorization response");
11833
- const authorization = assertObject2(root.authorization, AUTHORIZATION_KEYS, "share delivery authorization");
11834
- const proof = assertObject2(root.proof, ["alg", "kid", "signature"], "share delivery authorization proof");
11835
- const request = expected.request;
11836
- 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) {
11837
- throw new Error("share delivery authorization is not bound to the submitted request");
11838
- }
11839
- if (typeof authorization.issuedAt !== "string" || Date.parse(authorization.issuedAt) > Date.now() + 6e4) throw new Error("share delivery authorization issuedAt is invalid");
11840
- if (new Date(authorization.expiresAt).toISOString() !== authorization.expiresAt) throw new Error("share delivery authorization expiresAt is non-canonical");
11841
- if (typeof authorization.openCredentialsAudience !== "string" || !isCanonicalHttpsOrigin(authorization.openCredentialsAudience) || authorization.openCredentialsAudience !== expected.credentialsAudience || authorization.openCredentialsAudience === authorization.nodeAudience || authorization.openCredentialsAudience === authorization.returnOrigin) {
11842
- throw new Error("share delivery authorization openCredentialsAudience is missing or untrusted");
11843
- }
11844
- if (proof.alg !== "EdDSA" || typeof proof.kid !== "string" || typeof proof.signature !== "string") throw new Error("share delivery authorization proof is invalid");
11845
- if (proof.kid !== expected.nodeProof.kid) throw new Error("share delivery authorization proof key is not trusted");
11846
- const encodedKid = expected.nodeProof.publicKey;
11847
- const publicKey = encodedKid.length === 34 && encodedKid[0] === 237 && encodedKid[1] === 1 ? encodedKid.slice(2) : encodedKid;
11848
- if (publicKey.length !== 32) throw new Error("share delivery authorization proof key is invalid");
11849
- const signatureBytes = fromB642(proof.signature);
11850
- const signedBytes = new TextEncoder().encode(`${SHARE_DELIVERY_AUTHORIZATION_DOMAIN}${canonicalizeSignedObjectUnsigned(authorization)}`);
11851
- if (signatureBytes.length !== 64 || !ed255199.verify(signatureBytes, signedBytes, publicKey)) throw new Error("share delivery authorization proof signature is invalid");
11852
- return { authorization, proof };
11853
- }
11854
- function shareDeliveryTrustedKid(publicKey) {
11855
- if (publicKey.length !== 32) throw new Error("share delivery node key must be a raw 32-byte Ed25519 public key");
11856
- const prefixed = new Uint8Array(34);
11857
- prefixed.set([237, 1]);
11858
- prefixed.set(publicKey, 2);
11859
- const did = `did:key:${base58btc.encode(prefixed)}`;
11860
- return `${did}#${did.slice("did:key:".length)}`;
11861
- }
11862
-
11863
11833
  // src/delegations/share-delivery-v3.ts
11864
- import { ed25519 as ed2551910 } from "@noble/curves/ed25519";
11834
+ import { ed25519 as ed255199 } from "@noble/curves/ed25519";
11865
11835
  import { sha256 as sha25611 } from "@noble/hashes/sha256";
11866
11836
  var CREDENTIAL_INVITATION_REQUEST_DOMAIN = "xyz.tinycloud.credentials/invitation-request/v1\0";
11867
11837
  var DELIVERY_ADMISSION_DOMAIN = "xyz.tinycloud.policy/delivery-admission/v0\0";
11868
- var REQUEST_KEYS = ["schema", "policyId", "recipient", "resource", "credentialType", "returnLink", "envelopeRef", "audience", "issuedAt", "expiresAt", "nonce"];
11869
- var ADMISSION_KEYS = ["schema", "policyId", "ownerDid", "recipient", "resource", "actions", "credentialType", "returnLink", "envelopeRef", "senderKeyDid", "audience", "issuedAt", "expiresAt", "nonce", "signature"];
11838
+ var REQUEST_KEYS = ["schema", "policyId", "recipient", "resource", "credentialType", "returnLink", "envelopeRef", "label", "shareExpiresAt", "audience", "issuedAt", "expiresAt", "nonce"];
11839
+ var ADMISSION_KEYS = ["schema", "policyId", "ownerDid", "recipient", "resource", "actions", "credentialType", "returnLink", "envelopeRef", "label", "shareExpiresAt", "senderKeyDid", "audience", "issuedAt", "expiresAt", "nonce", "signature"];
11870
11840
  function exactObject2(value, keys, label) {
11871
11841
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new Error(`${label} is invalid`);
11872
11842
  const record2 = value;
@@ -11910,15 +11880,16 @@ function validateShareDeliveryAuthorizationV3Bytes(bytes2, expected) {
11910
11880
  const contentSource = object3(envelope.contentSource, "v3 content source");
11911
11881
  const envelopeSignature = object3(envelope.signature, "v3 envelope signature");
11912
11882
  const target = object3(envelope.target, "v3 target");
11913
- const fields = ["policyId", "recipient", "resource", "credentialType", "returnLink", "envelopeRef", "audience", "issuedAt", "expiresAt", "nonce"];
11914
- 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");
11883
+ const binding = object3(envelope.attestedEnforcerBinding, "v3 attested enforcer binding");
11884
+ const fields = ["policyId", "recipient", "resource", "credentialType", "returnLink", "envelopeRef", "label", "shareExpiresAt", "audience", "issuedAt", "expiresAt", "nonce"];
11885
+ 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");
11915
11886
  const issuedAt = Date.parse(String(request.issuedAt));
11916
11887
  const expiresAt = Date.parse(String(request.expiresAt));
11917
11888
  if (!Number.isFinite(issuedAt) || !Number.isFinite(expiresAt) || expiresAt <= issuedAt || expiresAt - issuedAt > 15 * 6e4) throw new Error("v3 share delivery authorization time is invalid");
11918
11889
  const admissionSignature = decodeBase64Url3(signature.value);
11919
11890
  const { signature: _signature, ...unsignedAdmission } = admission;
11920
11891
  const digest4 = sha25611(new TextEncoder().encode(`${DELIVERY_ADMISSION_DOMAIN}${canonicalizeSignedObjectUnsigned(unsignedAdmission)}`));
11921
- if (admissionSignature.length !== 64 || !ed2551910.verify(admissionSignature, digest4, didKeyBytes2(String(signature.signerDid)))) throw new Error("v3 share delivery admission signature is invalid");
11892
+ if (admissionSignature.length !== 64 || !ed255199.verify(admissionSignature, digest4, didKeyBytes2(String(signature.signerDid)))) throw new Error("v3 share delivery admission signature is invalid");
11922
11893
  return { request, admission };
11923
11894
  }
11924
11895
 
@@ -12042,9 +12013,6 @@ import {
12042
12013
  publishPolicyShare,
12043
12014
  claimShare,
12044
12015
  resumeShareAuthorization,
12045
- isLegacyShareLink,
12046
- receiveLegacyShare,
12047
- migrateShare,
12048
12016
  MemoryEncryptedShareHistoryStorage,
12049
12017
  EncryptedSenderShareHistory
12050
12018
  } from "@tinycloud/share-sdk";
@@ -12052,8 +12020,7 @@ import {
12052
12020
  inspectShare,
12053
12021
  receiveShare,
12054
12022
  ShareReceiveError,
12055
- toShareErrorInfo,
12056
- verifyBearerEnvelope
12023
+ toShareErrorInfo
12057
12024
  } from "@tinycloud/share-sdk";
12058
12025
 
12059
12026
  // src/networkId.ts
@@ -17689,7 +17656,7 @@ function portForProtocol(protocol) {
17689
17656
  }
17690
17657
 
17691
17658
  // src/location.ts
17692
- import { ed25519 as ed2551911 } from "@noble/curves/ed25519";
17659
+ import { ed25519 as ed2551910 } from "@noble/curves/ed25519";
17693
17660
  import { verifyMessage as verifyMessage2 } from "viem";
17694
17661
  var DEFAULT_TINYCLOUD_LOCATION_REGISTRY_URL = "https://registry.tinycloud.xyz";
17695
17662
  var DEFAULT_TINYCLOUD_FALLBACK_HOST = "https://node.tinycloud.xyz";
@@ -17697,6 +17664,7 @@ var DEFAULT_LOCAL_NODE_URL = "http://127.0.0.1:8000";
17697
17664
  var LOCAL_LOOPBACK_PROBE_TIMEOUT_MS = 250;
17698
17665
  var LOCAL_LINK_PROBE_TIMEOUT_MS = 750;
17699
17666
  var LOCAL_LINK_HOST_SUFFIX = ".local.tinycloud.link";
17667
+ var OWNER_NODE_BINDING_TIMEOUT_MS = 5e3;
17700
17668
  function createInMemoryLocalNodeIdentityStore() {
17701
17669
  const pins = /* @__PURE__ */ new Map();
17702
17670
  return {
@@ -17747,6 +17715,13 @@ var LocationRecordValidationError = class extends Error {
17747
17715
  this.name = "LocationRecordValidationError";
17748
17716
  }
17749
17717
  };
17718
+ var LocationRegistryHttpError = class extends Error {
17719
+ constructor(message, status) {
17720
+ super(message);
17721
+ this.status = status;
17722
+ this.name = "LocationRegistryHttpError";
17723
+ }
17724
+ };
17750
17725
  var CloudLocationResolutionError = class extends Error {
17751
17726
  constructor(subject, attempts) {
17752
17727
  super(`Unable to resolve TinyCloud location for ${subject}`);
@@ -17829,21 +17804,80 @@ async function verifyLocationRecord(input) {
17829
17804
  }
17830
17805
  return false;
17831
17806
  }
17832
- async function fetchLocationRecord(registryUrl, subject, fetchFn = globalThis.fetch) {
17807
+ async function fetchLocationRecord(registryUrl, subject, fetchFn = globalThis.fetch, signal) {
17833
17808
  const url = `${registryUrl.replace(/\/$/, "")}/v1/locations/${encodeURIComponent(subject)}`;
17834
- const response = await fetchFn(url);
17809
+ const response = await fetchFn(url, {
17810
+ redirect: "error",
17811
+ headers: { accept: "application/json" },
17812
+ ...signal === void 0 ? {} : { signal }
17813
+ });
17835
17814
  if (response.status === 404) {
17836
17815
  return null;
17837
17816
  }
17838
17817
  if (!response.ok) {
17839
- throw new Error(`location registry returned HTTP ${response.status}`);
17818
+ throw new LocationRegistryHttpError(`location registry returned HTTP ${response.status}`, response.status);
17819
+ }
17820
+ const text2 = await boundedResponseText(response, "location registry", 64 * 1024);
17821
+ let body;
17822
+ try {
17823
+ body = JSON.parse(text2);
17824
+ } catch {
17825
+ throw new LocationRecordValidationError("registry response is not JSON");
17840
17826
  }
17841
- const body = await response.json();
17842
17827
  if (body.record === void 0) {
17843
17828
  throw new LocationRecordValidationError("registry response missing record");
17844
17829
  }
17845
17830
  return validateLocationRecord(body.record);
17846
17831
  }
17832
+ async function publishLocationRecord(options2) {
17833
+ validateSubject(options2.subject);
17834
+ const registryUrl = exactHttpOrigin(options2.registryUrl, "registry URL");
17835
+ const nodeOrigin = exactHttpOrigin(options2.nodeOrigin, "node origin");
17836
+ const fetchFn = options2.fetch ?? globalThis.fetch.bind(globalThis);
17837
+ const current = await fetchLocationRecord(registryUrl, options2.subject, fetchFn);
17838
+ if (current !== null) {
17839
+ if (current.subject !== options2.subject || !await verifyLocationRecord(current)) {
17840
+ throw new LocationRecordValidationError("existing location record signature is invalid");
17841
+ }
17842
+ if (publishedHttpOrigins(current).includes(nodeOrigin)) {
17843
+ return current;
17844
+ }
17845
+ }
17846
+ const record2 = await signLocationRecord({
17847
+ version: 1,
17848
+ subject: options2.subject,
17849
+ multiaddrs: [...current?.multiaddrs ?? [], httpUrlToMultiaddr(nodeOrigin)],
17850
+ updated_at: (options2.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
17851
+ sequence: (current?.sequence ?? -1) + 1
17852
+ }, options2.signer);
17853
+ const response = await fetchFn(
17854
+ `${registryUrl}/v1/locations/${encodeURIComponent(options2.subject)}`,
17855
+ {
17856
+ method: "PUT",
17857
+ redirect: "error",
17858
+ headers: { accept: "application/json", "content-type": "application/json" },
17859
+ body: JSON.stringify(record2)
17860
+ }
17861
+ );
17862
+ if (!response.ok) {
17863
+ throw new LocationRegistryHttpError(`location registry publish returned HTTP ${response.status}`, response.status);
17864
+ }
17865
+ const text2 = await boundedResponseText(response, "location registry", 64 * 1024);
17866
+ let body;
17867
+ try {
17868
+ body = JSON.parse(text2);
17869
+ } catch {
17870
+ throw new LocationRecordValidationError("registry publish response is not JSON");
17871
+ }
17872
+ if (body.record === void 0) {
17873
+ throw new LocationRecordValidationError("registry publish response missing record");
17874
+ }
17875
+ const published = validateLocationRecord(body.record);
17876
+ if (canonicalLocationPayload(locationPayloadForRecord(published)) !== canonicalLocationPayload(locationPayloadForRecord(record2)) || published.signature !== record2.signature || !await verifyLocationRecord(published)) {
17877
+ throw new LocationRecordValidationError("registry publish response does not match the signed record");
17878
+ }
17879
+ return published;
17880
+ }
17847
17881
  async function resolveCloudLocation(subject, options2 = {}) {
17848
17882
  validateSubject(subject);
17849
17883
  const verifyRecords = options2.verifyRecords ?? true;
@@ -18094,6 +18128,123 @@ function multiaddrToHttpUrl(input) {
18094
18128
  }
18095
18129
  return uri;
18096
18130
  }
18131
+ function exactHttpOrigin(value, label) {
18132
+ let url;
18133
+ try {
18134
+ url = new URL(value);
18135
+ } catch {
18136
+ throw new LocationRecordValidationError(`${label} must be an absolute URL origin`);
18137
+ }
18138
+ const loopback = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";
18139
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
18140
+ throw new LocationRecordValidationError(`${label} must use HTTPS (or loopback HTTP)`);
18141
+ }
18142
+ if (url.username !== "" || url.password !== "" || url.pathname !== "/" || url.search !== "" || url.hash !== "") {
18143
+ throw new LocationRecordValidationError(`${label} must be an origin without credentials, path, query, or fragment`);
18144
+ }
18145
+ return url.origin;
18146
+ }
18147
+ function publishedHttpOrigins(record2) {
18148
+ const origins = [];
18149
+ for (const address of record2.multiaddrs) {
18150
+ try {
18151
+ origins.push(exactHttpOrigin(multiaddrToHttpUrl(address), "published node URL"));
18152
+ } catch {
18153
+ }
18154
+ }
18155
+ return origins;
18156
+ }
18157
+ async function boundedResponseText(response, label, maxBytes) {
18158
+ const declaredLength = response.headers.get("content-length");
18159
+ if (declaredLength !== null && (!/^\d+$/.test(declaredLength) || Number(declaredLength) > maxBytes)) {
18160
+ throw new LocationRecordValidationError(`${label} response is too large`);
18161
+ }
18162
+ if (response.body === null) return "";
18163
+ const reader = response.body.getReader();
18164
+ const chunks = [];
18165
+ let total = 0;
18166
+ try {
18167
+ while (true) {
18168
+ const { done, value } = await reader.read();
18169
+ if (done) break;
18170
+ if (value === void 0) continue;
18171
+ total += value.byteLength;
18172
+ if (total > maxBytes) {
18173
+ await reader.cancel().catch(() => void 0);
18174
+ throw new LocationRecordValidationError(`${label} response is too large`);
18175
+ }
18176
+ chunks.push(value);
18177
+ }
18178
+ } finally {
18179
+ reader.releaseLock();
18180
+ }
18181
+ const bytes2 = new Uint8Array(total);
18182
+ let offset = 0;
18183
+ for (const chunk of chunks) {
18184
+ bytes2.set(chunk, offset);
18185
+ offset += chunk.byteLength;
18186
+ }
18187
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes2);
18188
+ }
18189
+ function boundedSignal(parent, timeoutMs) {
18190
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
18191
+ throw new LocationRecordValidationError("owner-node binding timeout must be positive");
18192
+ }
18193
+ const controller = new AbortController();
18194
+ const abortFromParent = () => controller.abort(parent?.reason);
18195
+ if (parent?.aborted) abortFromParent();
18196
+ else parent?.addEventListener("abort", abortFromParent, { once: true });
18197
+ const timer = setTimeout(() => controller.abort(new DOMException("owner-node binding timed out", "TimeoutError")), timeoutMs);
18198
+ return {
18199
+ signal: controller.signal,
18200
+ dispose() {
18201
+ clearTimeout(timer);
18202
+ parent?.removeEventListener("abort", abortFromParent);
18203
+ }
18204
+ };
18205
+ }
18206
+ async function verifyOwnerNodeBinding(options2) {
18207
+ validateSubject(options2.ownerDid);
18208
+ validateSubject(options2.nodeDid);
18209
+ const registryUrl = exactHttpOrigin(options2.registryUrl, "registry URL");
18210
+ const nodeOrigin = exactHttpOrigin(options2.nodeOrigin, "node origin");
18211
+ const fetchFn = options2.fetch ?? globalThis.fetch.bind(globalThis);
18212
+ const lookup = boundedSignal(options2.signal, options2.timeoutMs ?? OWNER_NODE_BINDING_TIMEOUT_MS);
18213
+ try {
18214
+ const record2 = await fetchLocationRecord(registryUrl, options2.ownerDid, fetchFn, lookup.signal);
18215
+ if (record2 === null) {
18216
+ throw new LocationRecordValidationError("owner has no published location record");
18217
+ }
18218
+ if (record2.subject !== options2.ownerDid || !await verifyLocationRecord(record2)) {
18219
+ throw new LocationRecordValidationError("owner location record signature is invalid");
18220
+ }
18221
+ const publishedOrigins = publishedHttpOrigins(record2);
18222
+ if (!publishedOrigins.includes(nodeOrigin)) {
18223
+ throw new LocationRecordValidationError("share target is not in the owner's signed location record");
18224
+ }
18225
+ const response = await fetchFn(new URL("/info", nodeOrigin), {
18226
+ redirect: "error",
18227
+ headers: { accept: "application/json" },
18228
+ signal: lookup.signal
18229
+ });
18230
+ if (!response.ok) {
18231
+ throw new LocationRecordValidationError(`target node /info returned HTTP ${response.status}`);
18232
+ }
18233
+ const text2 = await boundedResponseText(response, "target node /info", 16 * 1024);
18234
+ let body;
18235
+ try {
18236
+ body = JSON.parse(text2);
18237
+ } catch {
18238
+ throw new LocationRecordValidationError("target node /info response is not JSON");
18239
+ }
18240
+ if (body === null || typeof body !== "object" || Array.isArray(body) || body.nodeId !== options2.nodeDid) {
18241
+ throw new LocationRecordValidationError("target node identity does not match the share attestation");
18242
+ }
18243
+ return Object.freeze({ record: record2, nodeOrigin, nodeDid: options2.nodeDid });
18244
+ } finally {
18245
+ lookup.dispose();
18246
+ }
18247
+ }
18097
18248
  function httpUrlToMultiaddr(input) {
18098
18249
  const url = new URL(input);
18099
18250
  if (url.protocol !== "http:" && url.protocol !== "https:") {
@@ -18261,7 +18412,7 @@ function verifyDidKeySignature(did, payload, signature) {
18261
18412
  "did:key signature must be a base64url Ed25519 signature"
18262
18413
  );
18263
18414
  }
18264
- return ed2551911.verify(
18415
+ return ed2551910.verify(
18265
18416
  signatureBytes,
18266
18417
  new TextEncoder().encode(payload),
18267
18418
  publicKey
@@ -18269,7 +18420,7 @@ function verifyDidKeySignature(did, payload, signature) {
18269
18420
  }
18270
18421
  function verifyDidKeyEd25519Signature(did, payload, signature) {
18271
18422
  const publicKey = ed25519PublicKeyFromDidKey2(did);
18272
- return ed2551911.verify(signature, payload, publicKey);
18423
+ return ed2551910.verify(signature, payload, publicKey);
18273
18424
  }
18274
18425
  function ed25519PublicKeyFromDidKey2(did) {
18275
18426
  const identifier = did.slice("did:key:".length);
@@ -18392,6 +18543,7 @@ export {
18392
18543
  ED25519_JCS_SIGNATURE_SUITE,
18393
18544
  EIP191_JCS_SIGNATURE_SUITE,
18394
18545
  EMAIL_CREDENTIAL_MAX_AGE_SECONDS,
18546
+ EMAIL_DOMAIN_CREDENTIAL_MAX_AGE_SECONDS,
18395
18547
  ENCRYPTION,
18396
18548
  ENCRYPTION_MANIFEST_SPACE,
18397
18549
  ENCRYPTION_NETWORK_URN_PREFIX,
@@ -18417,6 +18569,7 @@ export {
18417
18569
  LOCAL_LINK_PROBE_TIMEOUT_MS,
18418
18570
  LOCAL_LOOPBACK_PROBE_TIMEOUT_MS,
18419
18571
  LocationRecordValidationError,
18572
+ LocationRegistryHttpError,
18420
18573
  MAX_CONTENT_BYTES,
18421
18574
  MAX_LEGACY_ENVELOPE_EXPIRES_AT,
18422
18575
  MAX_SEALED_SHARE_CONTENT_BYTES,
@@ -18430,6 +18583,7 @@ export {
18430
18583
  NETWORK_NAME_PATTERN,
18431
18584
  NetworkIdError,
18432
18585
  OPENKEY_ACTION_ID_SEPARATOR,
18586
+ OWNER_NODE_BINDING_TIMEOUT_MS,
18433
18587
  OpenKeyManageKeyError,
18434
18588
  POLICY_CAPABILITY_V1_DOMAIN,
18435
18589
  POLICY_CAPABILITY_V1_SCHEMA,
@@ -18469,7 +18623,6 @@ export {
18469
18623
  SECRET_RECORDS_SCHEMA,
18470
18624
  SERVICE_LONG_TO_SHORT,
18471
18625
  SERVICE_SHORT_TO_LONG,
18472
- SHARE_DELIVERY_AUTHORIZATION_DOMAIN,
18473
18626
  SPACE,
18474
18627
  SQL,
18475
18628
  SQLAction,
@@ -18536,8 +18689,10 @@ export {
18536
18689
  buildPolicyCredentialPresentationV4,
18537
18690
  buildSpaceUri,
18538
18691
  canonicalDigest,
18692
+ canonicalEmailDomain,
18539
18693
  canonicalHashHex,
18540
18694
  canonicalLocationPayload,
18695
+ canonicalMailbox,
18541
18696
  canonicalOwnerSharePolicy,
18542
18697
  canonicalSignedResponse,
18543
18698
  canonicalizeAddress,
@@ -18565,6 +18720,7 @@ export {
18565
18720
  createCapabilityKeyRegistry,
18566
18721
  createDelegatedShareKey,
18567
18722
  createEmailCredentialRequirement,
18723
+ createEmailDomainCredentialRequirement,
18568
18724
  createHolderBinding,
18569
18725
  createInMemoryLocalNodeIdentityStore,
18570
18726
  createLocalStorageLocalNodeIdentityStore,
@@ -18637,6 +18793,7 @@ export {
18637
18793
  importPortableDelegation,
18638
18794
  installTinyCloudDebugGlobals,
18639
18795
  intersectShareCapabilities,
18796
+ isCanonicalEmailDomain,
18640
18797
  isCapabilitySubset,
18641
18798
  isDigest,
18642
18799
  isEvmAddress,
@@ -18647,6 +18804,7 @@ export {
18647
18804
  jcsCanonicalize,
18648
18805
  loadManifest,
18649
18806
  locationPayloadForRecord,
18807
+ mailboxBelongsToDomain,
18650
18808
  makePkhSpaceId,
18651
18809
  makePublicSpaceId,
18652
18810
  manifestAbilitiesUnion,
@@ -18685,6 +18843,7 @@ export {
18685
18843
  principalDidEquals,
18686
18844
  projectUnifiedPolicyCapabilities,
18687
18845
  projectUnifiedPolicyCapability,
18846
+ publishLocationRecord,
18688
18847
  recapCaveatsEqual,
18689
18848
  registerPolicyV3,
18690
18849
  renewPolicyRootStatusV3,
@@ -18704,7 +18863,7 @@ export {
18704
18863
  serviceError5 as serviceError,
18705
18864
  sha256Base64Url,
18706
18865
  shareCapabilityAllows,
18707
- shareDeliveryTrustedKid,
18866
+ signCompactPolicyDescendant,
18708
18867
  signCompactUcanAuthorization,
18709
18868
  signCompactUcanRootAuthorization,
18710
18869
  signLocationRecord,
@@ -18741,7 +18900,6 @@ export {
18741
18900
  validatePolicyStatusSignedShape,
18742
18901
  validatePolicyStatusUnsigned,
18743
18902
  validatePolicyUnsigned,
18744
- validateShareDeliveryAuthorizationBytes,
18745
18903
  validateShareDeliveryAuthorizationV3Bytes,
18746
18904
  validateUnifiedPolicyV2,
18747
18905
  verifyDecryptResponse,
@@ -18749,6 +18907,7 @@ export {
18749
18907
  verifyEip191MessageSignature,
18750
18908
  verifyIssuedCredential,
18751
18909
  verifyLocationRecord,
18910
+ verifyOwnerNodeBinding,
18752
18911
  verifyPolicy,
18753
18912
  verifyPolicyEngineRecord,
18754
18913
  verifyPolicyRootRevocationV3,