@tinycloud/share-sdk 1.2.0-beta.1 → 1.2.0-beta.3

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.
@@ -21,22 +21,29 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var share_root_entry_exports = {};
22
22
  __export(share_root_entry_exports, {
23
23
  AGENT_SHARE_ROOT_MAX_LIFETIME_MS: () => AGENT_SHARE_ROOT_MAX_LIFETIME_MS,
24
- POLICY_OWNER_AUTHORITY_FEATURES: () => POLICY_OWNER_AUTHORITY_FEATURES,
25
- POLICY_OWNER_AUTHORITY_PROBE_TTL_MS: () => POLICY_OWNER_AUTHORITY_PROBE_TTL_MS,
24
+ EDIT_SHARE_READER_MAX_LIFETIME_MS: () => EDIT_SHARE_READER_MAX_LIFETIME_MS,
25
+ SHARE_CONTROL_HEADER_BUDGET_BYTES: () => SHARE_CONTROL_HEADER_BUDGET_BYTES,
26
+ SHARE_CONTROL_LIFETIME_MS: () => SHARE_CONTROL_LIFETIME_MS,
27
+ SHARE_NETWORK_NAME_PREFIX: () => SHARE_NETWORK_NAME_PREFIX,
26
28
  SHARE_ROOT_CLOCK_SKEW_MS: () => SHARE_ROOT_CLOCK_SKEW_MS,
27
29
  SHARE_ROOT_LINK_MAX_LIFETIME_MS: () => SHARE_ROOT_LINK_MAX_LIFETIME_MS,
28
30
  SHARE_ROOT_MAX_LIFETIME_MS: () => SHARE_ROOT_MAX_LIFETIME_MS,
29
31
  SHARE_ROOT_PREFIX: () => SHARE_ROOT_PREFIX,
30
32
  SHARE_ROOT_REQUEST_MARGIN_MS: () => SHARE_ROOT_REQUEST_MARGIN_MS,
33
+ ShareGrant: () => ShareGrant,
31
34
  ShareRoot: () => ShareRoot,
32
35
  ShareRootError: () => ShareRootError,
36
+ assertNetworkSafeShareId: () => assertNetworkSafeShareId,
33
37
  assertNonExtractableShareKey: () => assertNonExtractableShareKey,
34
- createPolicyOwnerModeProbe: () => createPolicyOwnerModeProbe,
38
+ assertShareGrantPath: () => assertShareGrantPath,
35
39
  decodeOwnerRootCacao: () => decodeOwnerRootCacao,
36
- defaultPolicyOwnerModeProbe: () => defaultPolicyOwnerModeProbe,
37
40
  ensureShareRoot: () => ensureShareRoot,
41
+ openEditShare: () => openEditShare,
38
42
  ownerRootCacaoCid: () => ownerRootCacaoCid,
39
- readPolicyOwnerAuthorityMode: () => readPolicyOwnerAuthorityMode,
43
+ perShareNetworkIdFor: () => perShareNetworkIdFor,
44
+ planShareControlBatches: () => planShareControlBatches,
45
+ revokeShareGrant: () => revokeShareGrant,
46
+ runShareControlBatch: () => runShareControlBatch,
40
47
  signShareKeyDelegation: () => signShareKeyDelegation
41
48
  });
42
49
  module.exports = __toCommonJS(share_root_entry_exports);
@@ -5465,6 +5472,37 @@ function serialize(value) {
5465
5472
  function canonicalize(value) {
5466
5473
  return serialize(value);
5467
5474
  }
5475
+ async function signCompactUcanAuthorization(input) {
5476
+ if (input.notBefore >= input.expiresAt || input.expiresAt - input.notBefore > 60) {
5477
+ throw new TypeError("compact invocation lifetime must be between one and 60 seconds");
5478
+ }
5479
+ const principal = input.issuerDid.split("#", 1)[0];
5480
+ const publicKey = ed25519PublicKeyFromDidKey(principal);
5481
+ const header = {
5482
+ alg: "EdDSA",
5483
+ jwk: { alg: "EdDSA", crv: "Ed25519", kty: "OKP", x: toBase64Url(publicKey) },
5484
+ typ: "JWT",
5485
+ ucv: "0.10.0"
5486
+ };
5487
+ const payload = {
5488
+ att: input.attenuation,
5489
+ aud: input.audienceDid,
5490
+ exp: input.expiresAt,
5491
+ fct: input.facts,
5492
+ iss: input.issuerDid.includes("#") ? input.issuerDid : `${principal}#${principal.slice("did:key:".length)}`,
5493
+ nbf: input.notBefore,
5494
+ nnc: input.nonce,
5495
+ prf: input.proofs
5496
+ };
5497
+ const protectedSegment = toBase64Url(new TextEncoder().encode(canonicalize(header)));
5498
+ const payloadSegment = toBase64Url(new TextEncoder().encode(canonicalize(payload)));
5499
+ const signingInput = new TextEncoder().encode(`${protectedSegment}.${payloadSegment}`);
5500
+ const signature = await input.sign(signingInput);
5501
+ if (signature.length !== 64) throw new TypeError("compact Authorization signature must be Ed25519");
5502
+ return verifyCompactUcanAuthorization(
5503
+ `${protectedSegment}.${payloadSegment}.${toBase64Url(signature)}`
5504
+ );
5505
+ }
5468
5506
  function verifyCompactUcanAuthorization(authorization, expectedCid) {
5469
5507
  if (/\s/.test(authorization)) throw new TypeError("compact Authorization contains whitespace");
5470
5508
  const segments = authorization.split(".");
@@ -6583,27 +6621,20 @@ var AGENT_SHARE_ROOT_MAX_LIFETIME_MS = 30 * DAY_MS;
6583
6621
  var SHARE_ROOT_LINK_MAX_LIFETIME_MS = 30 * DAY_MS;
6584
6622
  var SHARE_ROOT_CLOCK_SKEW_MS = 5 * 6e4;
6585
6623
  var SHARE_ROOT_REQUEST_MARGIN_MS = 10 * 6e4;
6586
- var POLICY_OWNER_AUTHORITY_FEATURES = {
6587
- marker: "policy-owner-authority-v1",
6588
- record: "policy-owner-authority-record",
6589
- enforce: "policy-owner-authority-enforce"
6590
- };
6591
- var POLICY_OWNER_AUTHORITY_PROBE_TTL_MS = 5 * 6e4;
6592
- var MAX_INFO_BYTES = 64 * 1024;
6624
+ var MAX_NODE_ANSWER_BYTES = 64 * 1024;
6593
6625
  var MAX_POLICY_ROOT_MS = 31 * DAY_MS;
6594
6626
  var PROFILES = {
6595
6627
  share: { spaceName: "applications", maxLifetimeMs: SHARE_ROOT_MAX_LIFETIME_MS },
6596
6628
  agent: { spaceName: "default", maxLifetimeMs: AGENT_SHARE_ROOT_MAX_LIFETIME_MS }
6597
6629
  };
6598
6630
  var KV_ABILITIES = ["tinycloud.kv/get", "tinycloud.kv/metadata"];
6599
- var DECRYPT_ABILITY = "tinycloud.encryption/decrypt";
6600
6631
  var ShareRootError = class extends Error {
6601
6632
  code;
6602
6633
  rootCid;
6603
6634
  signedCid;
6604
6635
  mustRevoke;
6605
6636
  constructor(code3, message, details = {}) {
6606
- super(details.mustRevoke === true && details.rootCid !== void 0 ? `${message}; the Node imported root ${details.rootCid}, which may grant more than requested: the owner must revoke it` : message);
6637
+ super(details.mustRevoke === true && details.rootCid !== void 0 ? `${message}; the Node imported ${details.rootCid}, which may grant more than requested: the owner must revoke it` : message);
6607
6638
  this.name = "ShareRootError";
6608
6639
  this.code = code3;
6609
6640
  this.mustRevoke = details.mustRevoke === true;
@@ -6613,7 +6644,7 @@ var ShareRootError = class extends Error {
6613
6644
  };
6614
6645
  function parseOwnerDid(ownerDid) {
6615
6646
  const match = /^did:pkh:eip155:([1-9]\d*):(0x[0-9a-fA-F]{40})$/.exec(ownerDid);
6616
- if (match === null) throw new ShareRootError("invalid-request", "share root owner must be did:pkh:eip155");
6647
+ if (match === null) throw new ShareRootError("invalid-request", "share owner must be did:pkh:eip155");
6617
6648
  return { chainId: match[1], address: match[2].toLowerCase() };
6618
6649
  }
6619
6650
  function sameOwner(left, right) {
@@ -6622,16 +6653,9 @@ function sameOwner(left, right) {
6622
6653
  function assertSpace(spaceId, owner, kind) {
6623
6654
  const match = /^tinycloud:pkh:eip155:([1-9]\d*):(0x[0-9a-fA-F]{40}):([A-Za-z0-9_-]+)$/.exec(spaceId);
6624
6655
  if (match === null || !sameOwner({ chainId: match[1], address: match[2].toLowerCase() }, owner) || match[3] !== PROFILES[kind].spaceName) {
6625
- throw new ShareRootError("invalid-request", `${kind} root must target the owner's ${PROFILES[kind].spaceName} space`);
6656
+ throw new ShareRootError("invalid-request", `${kind} delegations must target the owner's ${PROFILES[kind].spaceName} space`);
6626
6657
  }
6627
6658
  }
6628
- function shareNetworkId(owner) {
6629
- return `urn:tinycloud:encryption:did:pkh:eip155:${owner.chainId}:${owner.address}:share`;
6630
- }
6631
- function isShareNetworkOf(resource, owner) {
6632
- const match = /^urn:tinycloud:encryption:did:pkh:eip155:([1-9]\d*):(0x[0-9a-fA-F]{40}):share$/.exec(resource);
6633
- return match !== null && sameOwner({ chainId: match[1], address: match[2].toLowerCase() }, owner);
6634
- }
6635
6659
  function nodeOriginOf(value) {
6636
6660
  let url;
6637
6661
  try {
@@ -6652,49 +6676,66 @@ function seconds(ms) {
6652
6676
  }
6653
6677
  function parseTime(value, label, rootCid) {
6654
6678
  const time = value === void 0 ? Number.NaN : Date.parse(value);
6655
- if (!Number.isFinite(time)) throw new ShareRootError("malformed-root", `share root ${label} is missing or invalid`, { rootCid, mustRevoke: true });
6679
+ if (!Number.isFinite(time)) throw new ShareRootError("malformed-root", `delegation ${label} is missing or invalid`, { rootCid, mustRevoke: true });
6656
6680
  return time;
6657
6681
  }
6658
- function validateSignedRoot(decoded, expected) {
6682
+ function validateSignedOwnerDelegation(decoded, expected) {
6683
+ const { label } = expected;
6659
6684
  const wider = { rootCid: decoded.cid, mustRevoke: true };
6660
6685
  let issuer;
6661
6686
  try {
6662
6687
  issuer = parseOwnerDid(decoded.issuer);
6663
6688
  } catch {
6664
- throw new ShareRootError("issuer-mismatch", "share root is not signed by a did:pkh owner", wider);
6665
- }
6666
- if (!sameOwner(issuer, expected.owner)) throw new ShareRootError("issuer-mismatch", "share root is signed by another owner", wider);
6667
- if (decoded.audience !== expected.keyDid) throw new ShareRootError("audience-mismatch", "share root is not delegated to the share key", wider);
6668
- if (decoded.resources.length !== 1 || !decoded.resources[0].startsWith("urn:recap:")) throw new ShareRootError("resource-mismatch", "share root must carry exactly one ReCap resource", wider);
6669
- if (decoded.reCapProofs.length !== 0) throw new ShareRootError("resource-mismatch", "share root must not have parent capabilities", wider);
6670
- const wanted = { [`${expected.spaceId}/kv/${SHARE_ROOT_PREFIX}`]: KV_ABILITIES };
6671
- if (expected.scope === "kv+share-decrypt") wanted[shareNetworkId(expected.owner)] = [DECRYPT_ABILITY];
6672
- for (const resource of Object.keys(decoded.attenuation)) if (wanted[resource] === void 0) throw new ShareRootError("resource-mismatch", "share root grants a resource outside its scope", wider);
6673
- for (const [resource, abilities] of Object.entries(wanted)) {
6674
- const granted = decoded.attenuation[resource];
6675
- if (granted === void 0) throw new ShareRootError("resource-mismatch", "share root is missing one of its resources", wider);
6676
- const names = Object.keys(granted);
6677
- if (names.length !== abilities.length || abilities.some((ability) => !names.includes(ability))) throw new ShareRootError("ability-mismatch", "share root abilities differ from the request", wider);
6678
- for (const ability of abilities) {
6679
- const caveats = granted[ability];
6689
+ throw new ShareRootError("issuer-mismatch", `${label} is not signed by a did:pkh owner`, wider);
6690
+ }
6691
+ if (!sameOwner(issuer, expected.owner)) throw new ShareRootError("issuer-mismatch", `${label} is signed by another owner`, wider);
6692
+ if (decoded.audience !== expected.keyDid) throw new ShareRootError("audience-mismatch", `${label} is not delegated to its key`, wider);
6693
+ if (decoded.resources.length !== 1 || !decoded.resources[0].startsWith("urn:recap:")) throw new ShareRootError("resource-mismatch", `${label} must carry exactly one ReCap resource`, wider);
6694
+ if (decoded.reCapProofs.length !== 0) throw new ShareRootError("resource-mismatch", `${label} must not have parent capabilities`, wider);
6695
+ const { wanted } = expected;
6696
+ for (const [resource, granted] of Object.entries(decoded.attenuation)) {
6697
+ const abilities = wanted[resource];
6698
+ if (abilities === void 0) throw new ShareRootError("resource-mismatch", `${label} grants a resource outside its scope`, wider);
6699
+ for (const [ability, caveats] of Object.entries(granted)) {
6700
+ if (!abilities.includes(ability)) throw new ShareRootError("ability-mismatch", `${label} grants an ability it was not asked for`, wider);
6680
6701
  if (caveats.length !== 1 || typeof caveats[0] !== "object" || caveats[0] === null || Array.isArray(caveats[0]) || Object.keys(caveats[0]).length !== 0) {
6681
- throw new ShareRootError("ability-mismatch", "share root abilities carry unexpected caveats", wider);
6702
+ throw new ShareRootError("ability-mismatch", `${label} abilities carry unexpected caveats`, wider);
6682
6703
  }
6683
6704
  }
6684
6705
  }
6706
+ const narrower = { rootCid: decoded.cid };
6707
+ for (const [resource, abilities] of Object.entries(wanted)) {
6708
+ const granted = decoded.attenuation[resource];
6709
+ if (granted === void 0) throw new ShareRootError("resource-mismatch", `${label} is narrower than requested: a resource is missing`, narrower);
6710
+ if (abilities.some((ability) => granted[ability] === void 0)) throw new ShareRootError("ability-mismatch", `${label} is narrower than requested: an ability is missing`, narrower);
6711
+ }
6685
6712
  const issuedAt = parseTime(decoded.issuedAt, "issued-at time", decoded.cid);
6686
6713
  const expiresAt = parseTime(decoded.expiresAt, "expiration time", decoded.cid);
6687
- if (decoded.notBefore !== void 0 && parseTime(decoded.notBefore, "not-before time", decoded.cid) > expected.now) throw new ShareRootError("malformed-root", "share root is not valid yet", { rootCid: decoded.cid });
6688
- const cap = PROFILES[expected.kind].maxLifetimeMs;
6689
- if (expiresAt - issuedAt > cap + SHARE_ROOT_CLOCK_SKEW_MS || expiresAt - expected.now > cap) throw new ShareRootError("lifetime-exceeds-cap", `${expected.kind} root lifetime exceeds ${cap / DAY_MS} days`, wider);
6690
- if (expiresAt <= expected.now) throw new ShareRootError("expired", "share root has expired", { rootCid: decoded.cid });
6714
+ if (decoded.notBefore !== void 0 && parseTime(decoded.notBefore, "not-before time", decoded.cid) > expected.now) throw new ShareRootError("malformed-root", `${label} is not valid yet`, { rootCid: decoded.cid });
6715
+ const cap = expected.maxLifetimeMs;
6716
+ if (expiresAt - issuedAt > cap + SHARE_ROOT_CLOCK_SKEW_MS || expiresAt - expected.now > cap) throw new ShareRootError("lifetime-exceeds-cap", `${label} lifetime exceeds its cap`, wider);
6717
+ if (expiresAt <= expected.now) throw new ShareRootError("expired", `${label} has expired`, { rootCid: decoded.cid });
6691
6718
  if (expected.requestedExpiresAt !== void 0 && seconds(expiresAt) !== seconds(expected.requestedExpiresAt.getTime())) {
6692
- throw new ShareRootError("expiry-mismatch", "share root was signed with a different expiry than requested", { rootCid: decoded.cid, mustRevoke: expiresAt > expected.requestedExpiresAt.getTime() });
6719
+ throw new ShareRootError("expiry-mismatch", `${label} was signed with a different expiry than requested`, { rootCid: decoded.cid, mustRevoke: expiresAt > expected.requestedExpiresAt.getTime() });
6693
6720
  }
6694
6721
  return { issuedAt: new Date(issuedAt), expiresAt: new Date(expiresAt) };
6695
6722
  }
6723
+ function validateSignedRoot(decoded, expected) {
6724
+ return validateSignedOwnerDelegation(decoded, {
6725
+ label: `${expected.kind} root`,
6726
+ owner: expected.owner,
6727
+ keyDid: expected.keyDid,
6728
+ wanted: { [`${expected.spaceId}/kv/${SHARE_ROOT_PREFIX}`]: KV_ABILITIES },
6729
+ maxLifetimeMs: PROFILES[expected.kind].maxLifetimeMs,
6730
+ ...expected.requestedExpiresAt === void 0 ? {} : { requestedExpiresAt: expected.requestedExpiresAt },
6731
+ now: expected.now
6732
+ });
6733
+ }
6734
+ function isExactKeyUnder(path, prefix) {
6735
+ return typeof path === "string" && path.startsWith(prefix) && path.length > prefix.length && !path.endsWith("/") && !path.includes("//") && !path.split("/").some((segment) => segment === "." || segment === "..") && !/[\u0000-\u001f\u007f\\*?#]/.test(path);
6736
+ }
6696
6737
  function assertLinkPath(path) {
6697
- if (typeof path !== "string" || !path.startsWith(SHARE_ROOT_PREFIX) || path.length === SHARE_ROOT_PREFIX.length || path.endsWith("/") || path.includes("//") || path.split("/").some((segment) => segment === "." || segment === "..") || /[\u0000-\u001f\u007f\\*?#]/.test(path)) throw new ShareRootError("link-out-of-scope", "link path must be one exact key under the share prefix");
6738
+ if (!isExactKeyUnder(path, SHARE_ROOT_PREFIX)) throw new ShareRootError("link-out-of-scope", "link path must be one exact key under the share prefix");
6698
6739
  }
6699
6740
  function errorMessage(error) {
6700
6741
  return error instanceof Error ? error.message : String(error);
@@ -6706,6 +6747,43 @@ async function assertUsableKey(key) {
6706
6747
  throw new ShareRootError("invalid-key", errorMessage(error));
6707
6748
  }
6708
6749
  }
6750
+ async function readJsonCapped(response) {
6751
+ if (!/^application\/json\s*(;|$)/i.test(response.headers.get("content-type") ?? "")) throw new Error("node answer is not JSON");
6752
+ const reader = response.body?.getReader();
6753
+ if (reader === void 0) throw new Error("node answer has no body");
6754
+ const chunks = [];
6755
+ let total = 0;
6756
+ for (; ; ) {
6757
+ const { done, value } = await reader.read();
6758
+ if (done) break;
6759
+ total += value.byteLength;
6760
+ if (total > MAX_NODE_ANSWER_BYTES) {
6761
+ await reader.cancel();
6762
+ throw new Error("node answer is too large");
6763
+ }
6764
+ chunks.push(value);
6765
+ }
6766
+ const bytes2 = new Uint8Array(total);
6767
+ let offset = 0;
6768
+ for (const chunk of chunks) {
6769
+ bytes2.set(chunk, offset);
6770
+ offset += chunk.byteLength;
6771
+ }
6772
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes2));
6773
+ }
6774
+ async function isRevokeReceiptFor(response, cid) {
6775
+ if (!response.ok) {
6776
+ await response.body?.cancel();
6777
+ return false;
6778
+ }
6779
+ let receipt;
6780
+ try {
6781
+ receipt = await readJsonCapped(response);
6782
+ } catch {
6783
+ return false;
6784
+ }
6785
+ return typeof receipt === "object" && receipt !== null && "revoked" in receipt && receipt.revoked === true && "cid" in receipt && receipt.cid === cid;
6786
+ }
6709
6787
  function policyRootContents(input) {
6710
6788
  const facts = {
6711
6789
  role: input.role,
@@ -6727,69 +6805,45 @@ function policyRootContents(input) {
6727
6805
  }
6728
6806
  return { facts, attenuation };
6729
6807
  }
6730
- function readPolicyOwnerAuthorityMode(features) {
6731
- if (!Array.isArray(features) || !features.includes(POLICY_OWNER_AUTHORITY_FEATURES.marker)) return void 0;
6732
- const enforce = features.includes(POLICY_OWNER_AUTHORITY_FEATURES.enforce);
6733
- const record = features.includes(POLICY_OWNER_AUTHORITY_FEATURES.record);
6734
- if (enforce === record) return void 0;
6735
- return enforce ? "enforce" : "record";
6736
- }
6737
- async function readJsonCapped(response) {
6738
- if (!/^application\/json\s*(;|$)/i.test(response.headers.get("content-type") ?? "")) throw new Error("node info is not JSON");
6739
- const reader = response.body?.getReader();
6740
- if (reader === void 0) throw new Error("node info has no body");
6741
- const chunks = [];
6742
- let total = 0;
6743
- for (; ; ) {
6744
- const { done, value } = await reader.read();
6745
- if (done) break;
6746
- total += value.byteLength;
6747
- if (total > MAX_INFO_BYTES) {
6748
- await reader.cancel();
6749
- throw new Error("node info is too large");
6750
- }
6751
- chunks.push(value);
6752
- }
6753
- const bytes2 = new Uint8Array(total);
6754
- let offset = 0;
6755
- for (const chunk of chunks) {
6756
- bytes2.set(chunk, offset);
6757
- offset += chunk.byteLength;
6758
- }
6759
- return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes2));
6760
- }
6761
- function createPolicyOwnerModeProbe(options = {}) {
6762
- const fetchFn = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
6763
- const ttlMs = Math.min(options.ttlMs ?? POLICY_OWNER_AUTHORITY_PROBE_TTL_MS, POLICY_OWNER_AUTHORITY_PROBE_TTL_MS);
6764
- const now = options.now ?? Date.now;
6765
- const cache3 = /* @__PURE__ */ new Map();
6808
+ function ownerPolicySigner(input) {
6809
+ const { ownerDid, authorityCid, notAfter, gate } = input;
6766
6810
  return {
6767
- async isEnforced(nodeOrigin) {
6768
- const origin = nodeOriginOf(nodeOrigin);
6769
- if (origin === void 0) return false;
6770
- const cached = cache3.get(origin);
6771
- if (cached !== void 0 && now() - cached.checkedAt < ttlMs) return cached.enforced;
6772
- let enforced = false;
6773
- try {
6774
- const response = await fetchFn(new URL("/info", origin), { redirect: "error", signal: AbortSignal.timeout(5e3) });
6775
- const info = response.ok ? await readJsonCapped(response) : void 0;
6776
- enforced = typeof info === "object" && info !== null && "features" in info && readPolicyOwnerAuthorityMode(info.features) === "enforce";
6777
- } catch {
6778
- enforced = false;
6811
+ ownerDid,
6812
+ rootCid: authorityCid,
6813
+ notAfter,
6814
+ createOwnerRoot: async (root) => {
6815
+ const key = await gate();
6816
+ const now = input.now();
6817
+ const notBefore = root.notBefore instanceof Date ? root.notBefore.getTime() : Number.NaN;
6818
+ const expiry = root.expiresAt instanceof Date ? root.expiresAt.getTime() : Number.NaN;
6819
+ if (root.ownerDid !== ownerDid || root.role !== "policy-authority" && root.role !== "policy-enforcement") throw new ShareRootError("invalid-request", "policy root must be owned by this key");
6820
+ if (!Number.isFinite(notBefore) || !Number.isFinite(expiry) || notBefore >= expiry || expiry <= now) throw new ShareRootError("invalid-request", "policy root validity is invalid");
6821
+ if (expiry > notAfter.getTime() || expiry - notBefore > MAX_POLICY_ROOT_MS) throw new ShareRootError("lifetime-exceeds-cap", "policy root must not outlive its owner delegation or 31 days", { rootCid: authorityCid });
6822
+ for (const capability of root.capabilities) {
6823
+ if (!input.inScope(capability)) throw new ShareRootError("invalid-request", "policy root capability is outside the owner delegation");
6779
6824
  }
6780
- cache3.set(origin, { enforced, checkedAt: now() });
6781
- return enforced;
6825
+ const { facts, attenuation } = policyRootContents(root);
6826
+ const signed = await signShareKeyDelegation({
6827
+ key,
6828
+ audienceDid: root.audienceDid,
6829
+ attenuation,
6830
+ facts: [facts],
6831
+ proofs: [],
6832
+ notBefore: seconds(notBefore),
6833
+ expiresAt: seconds(expiry),
6834
+ nonce: toBase64Url(crypto.getRandomValues(new Uint8Array(16)))
6835
+ });
6836
+ return { cid: signed.cid, delegationHeader: { Authorization: signed.authorization } };
6782
6837
  },
6783
- observe(nodeOrigin, features) {
6784
- const origin = nodeOriginOf(nodeOrigin);
6785
- if (origin !== void 0) cache3.set(origin, { enforced: readPolicyOwnerAuthorityMode(features) === "enforce", checkedAt: now() });
6838
+ sign: async (digest) => {
6839
+ const key = await gate();
6840
+ if (!(digest instanceof Uint8Array) || digest.length !== 32) throw new ShareRootError("invalid-request", "policy signer signs 32-byte digests only");
6841
+ return key.sign(digest);
6786
6842
  }
6787
6843
  };
6788
6844
  }
6789
- var defaultPolicyOwnerModeProbe = createPolicyOwnerModeProbe();
6790
6845
  var ShareRoot = class _ShareRoot {
6791
6846
  kind;
6792
- scope;
6793
6847
  nodeOrigin;
6794
6848
  cid;
6795
6849
  ownerDid;
@@ -6802,7 +6856,6 @@ var ShareRoot = class _ShareRoot {
6802
6856
  #options;
6803
6857
  constructor(input) {
6804
6858
  this.kind = input.kind;
6805
- this.scope = input.scope;
6806
6859
  this.nodeOrigin = input.nodeOrigin;
6807
6860
  this.cid = input.cid;
6808
6861
  this.ownerDid = input.ownerDid;
@@ -6815,12 +6868,10 @@ var ShareRoot = class _ShareRoot {
6815
6868
  this.#options = input.options;
6816
6869
  }
6817
6870
  /**
6818
- * Ask the owner for a new root (one approval) for a fresh share key. The
6819
- * scope is chosen now: `kv+share-decrypt` only for a `share` root with the
6820
- * flag on and `nodeOrigin` advertising `enforce`, otherwise `kv`. All request
6821
- * checks run before anything is signed. If the signed result is rejected,
6822
- * the key is cleared and the error carries the imported `rootCid` (and
6823
- * `mustRevoke` when the root may be wider than requested).
6871
+ * Ask the owner for a new root (one approval) for a fresh share key. All
6872
+ * request checks run before anything is signed. If the signed result is
6873
+ * rejected, the key is cleared and the error carries the imported `rootCid`
6874
+ * (and `mustRevoke` when the root may be wider than requested).
6824
6875
  *
6825
6876
  * `keyFactory` defaults to a fresh non-extractable WebCrypto key that only
6826
6877
  * this root holds. A custom factory must not persist the key before
@@ -6836,7 +6887,6 @@ var ShareRoot = class _ShareRoot {
6836
6887
  const expiry = input.expiresAt instanceof Date ? input.expiresAt.getTime() : Number.NaN;
6837
6888
  if (!Number.isFinite(expiry) || expiry <= now) throw new ShareRootError("invalid-request", "share root expiry must be in the future");
6838
6889
  if (expiry - now > PROFILES[kind].maxLifetimeMs) throw new ShareRootError("lifetime-exceeds-cap", `${kind} root lifetime exceeds ${PROFILES[kind].maxLifetimeMs / DAY_MS} days`);
6839
- const scope = kind === "share" && options.shareRootPolicyRoots === true && await (options.policyOwnerModeProbe ?? defaultPolicyOwnerModeProbe).isEnforced(nodeOrigin) ? "kv+share-decrypt" : "kv";
6840
6890
  const key = await (input.keyFactory ?? (() => createDelegatedShareKey({ extractable: false })))();
6841
6891
  try {
6842
6892
  await assertUsableKey(key);
@@ -6845,7 +6895,6 @@ var ShareRoot = class _ShareRoot {
6845
6895
  throw error;
6846
6896
  }
6847
6897
  const permissions = [{ service: "tinycloud.kv", path: SHARE_ROOT_PREFIX, actions: [...KV_ABILITIES] }];
6848
- if (scope === "kv+share-decrypt") permissions.push({ service: "tinycloud.encryption", path: shareNetworkId(owner), actions: [DECRYPT_ABILITY] });
6849
6898
  let receipt;
6850
6899
  try {
6851
6900
  receipt = await input.owner.createOwnerDelegation({ delegateDid: key.did, spaceId: input.spaceId, expiresAt: input.expiresAt, permissions });
@@ -6863,8 +6912,8 @@ var ShareRoot = class _ShareRoot {
6863
6912
  } catch (error) {
6864
6913
  throw new ShareRootError("malformed-root", errorMessage(error), { rootCid: receipt.delegationCid, mustRevoke: true });
6865
6914
  }
6866
- const times = validateSignedRoot(decoded, { kind, scope, owner, spaceId: input.spaceId, keyDid: key.did, requestedExpiresAt: input.expiresAt, now: (options.now ?? Date.now)() });
6867
- return new _ShareRoot({ kind, scope, nodeOrigin, cid: decoded.cid, ownerDid: input.owner.ownerDid, spaceId: input.spaceId, ...times, authorization, key, options });
6915
+ const times = validateSignedRoot(decoded, { kind, owner, spaceId: input.spaceId, keyDid: key.did, requestedExpiresAt: input.expiresAt, now: (options.now ?? Date.now)() });
6916
+ return new _ShareRoot({ kind, nodeOrigin, cid: decoded.cid, ownerDid: input.owner.ownerDid, spaceId: input.spaceId, ...times, authorization, key, options });
6868
6917
  } catch (error) {
6869
6918
  key.clear();
6870
6919
  if (error instanceof ShareRootError) throw error;
@@ -6875,9 +6924,7 @@ var ShareRoot = class _ShareRoot {
6875
6924
  static async restore(input) {
6876
6925
  const { record } = input;
6877
6926
  const options = { ...input.options, kind: record.kind };
6878
- if (record.version !== 1 || record.kind !== "share" && record.kind !== "agent" || record.scope !== "kv" && record.scope !== "kv+share-decrypt" || record.kind === "agent" && record.scope !== "kv") {
6879
- throw new ShareRootError("malformed-root", "share root record is not supported");
6880
- }
6927
+ if (record.version !== 1 || record.kind !== "share" && record.kind !== "agent") throw new ShareRootError("malformed-root", "share root record is not supported");
6881
6928
  if (record.keyDid !== input.key.did) throw new ShareRootError("audience-mismatch", "share root record belongs to another key");
6882
6929
  const owner = parseOwnerDid(record.ownerDid);
6883
6930
  assertSpace(record.spaceId, owner, record.kind);
@@ -6891,19 +6938,19 @@ var ShareRoot = class _ShareRoot {
6891
6938
  } catch (error) {
6892
6939
  throw new ShareRootError("malformed-root", errorMessage(error), { rootCid: record.cid });
6893
6940
  }
6894
- const times = validateSignedRoot(decoded, { kind: record.kind, scope: record.scope, owner, spaceId: record.spaceId, keyDid: input.key.did, now: (options.now ?? Date.now)() });
6895
- return new _ShareRoot({ kind: record.kind, scope: record.scope, nodeOrigin, cid: decoded.cid, ownerDid: record.ownerDid, spaceId: record.spaceId, ...times, authorization, key: input.key, options });
6941
+ const times = validateSignedRoot(decoded, { kind: record.kind, owner, spaceId: record.spaceId, keyDid: input.key.did, now: (options.now ?? Date.now)() });
6942
+ return new _ShareRoot({ kind: record.kind, nodeOrigin, cid: decoded.cid, ownerDid: record.ownerDid, spaceId: record.spaceId, ...times, authorization, key: input.key, options });
6896
6943
  }
6897
- /** The `Authorization` value the Node knows this root by (base64url CACAO). */
6944
+ /**
6945
+ * This root's CACAO as base64url, UNPADDED (the record form). As a Node
6946
+ * `Authorization` header it must be padded to a multiple of 4 characters:
6947
+ * the Node refuses an unpadded value otherwise.
6948
+ */
6898
6949
  get authorization() {
6899
6950
  return toBase64Url(this.#authorization);
6900
6951
  }
6901
- /** True when the root holds `:share` decrypt and may act as a policy owner (still gated per call). */
6902
- get policyCapable() {
6903
- return this.scope === "kv+share-decrypt";
6904
- }
6905
6952
  toRecord() {
6906
- return { version: 1, kind: this.kind, scope: this.scope, nodeOrigin: this.nodeOrigin, cid: this.cid, authorization: this.authorization, spaceId: this.spaceId, ownerDid: this.ownerDid, keyDid: this.keyDid, expiresAt: this.expiresAt.toISOString() };
6953
+ return { version: 1, kind: this.kind, nodeOrigin: this.nodeOrigin, cid: this.cid, authorization: this.authorization, spaceId: this.spaceId, ownerDid: this.ownerDid, keyDid: this.keyDid, expiresAt: this.expiresAt.toISOString() };
6907
6954
  }
6908
6955
  /** True while the root is live, its key is held, and it lasts at least until `expiresAt`. Revocation is not visible here. */
6909
6956
  covers(expiresAt, now = this.#now()) {
@@ -6940,54 +6987,6 @@ var ShareRoot = class _ShareRoot {
6940
6987
  });
6941
6988
  return { rootCid: this.cid, linkDid: input.linkDid, path: input.path, expiresAt: new Date(seconds(expiry) * 1e3), delegation };
6942
6989
  }
6943
- /**
6944
- * The only ShareRoot API that signs Policy/v3 (email/domain) owner material.
6945
- * Needs a `kv+share-decrypt` root, the `shareRootPolicyRoots` flag, a live
6946
- * root and key, and the root's Node advertising `enforce`; the returned
6947
- * signer re-checks all of it on every call. Until then addressed shares use
6948
- * per-share owner grants. Agent roots are bearer-only.
6949
- */
6950
- async policyRootSigner() {
6951
- await this.#assertPolicyGate();
6952
- const gate = () => this.#assertPolicyGate();
6953
- const owner = parseOwnerDid(this.ownerDid);
6954
- const notAfter = new Date(this.expiresAt);
6955
- return {
6956
- ownerDid: this.keyDid,
6957
- rootCid: this.cid,
6958
- notAfter,
6959
- createOwnerRoot: async (input) => {
6960
- const key = await gate();
6961
- const now = this.#now();
6962
- const notBefore = input.notBefore instanceof Date ? input.notBefore.getTime() : Number.NaN;
6963
- const expiry = input.expiresAt instanceof Date ? input.expiresAt.getTime() : Number.NaN;
6964
- if (input.ownerDid !== this.keyDid || input.role !== "policy-authority" && input.role !== "policy-enforcement") throw new ShareRootError("invalid-request", "policy root must be owned by this share key");
6965
- if (!Number.isFinite(notBefore) || !Number.isFinite(expiry) || notBefore >= expiry || expiry <= now) throw new ShareRootError("invalid-request", "policy root validity is invalid");
6966
- if (expiry > notAfter.getTime() || expiry - notBefore > MAX_POLICY_ROOT_MS) throw new ShareRootError("lifetime-exceeds-cap", "policy root must not outlive its share root or 31 days", { rootCid: this.cid });
6967
- for (const capability of input.capabilities) {
6968
- const inScope = capability.kind === "encryption" ? isShareNetworkOf(capability.resource, owner) && capability.action === DECRYPT_ABILITY : capability.resource.startsWith(`${this.spaceId}/kv/${SHARE_ROOT_PREFIX}`) && capability.actions.every((action) => KV_ABILITIES.includes(action));
6969
- if (!inScope) throw new ShareRootError("invalid-request", "policy root capability is outside the share root");
6970
- }
6971
- const { facts, attenuation } = policyRootContents(input);
6972
- const root = await signShareKeyDelegation({
6973
- key,
6974
- audienceDid: input.audienceDid,
6975
- attenuation,
6976
- facts: [facts],
6977
- proofs: [],
6978
- notBefore: seconds(notBefore),
6979
- expiresAt: seconds(expiry),
6980
- nonce: toBase64Url(crypto.getRandomValues(new Uint8Array(16)))
6981
- });
6982
- return { cid: root.cid, delegationHeader: { Authorization: root.authorization } };
6983
- },
6984
- sign: async (digest) => {
6985
- const key = await gate();
6986
- if (!(digest instanceof Uint8Array) || digest.length !== 32) throw new ShareRootError("invalid-request", "policy signer signs 32-byte digests only");
6987
- return key.sign(digest);
6988
- }
6989
- };
6990
- }
6991
6990
  /** Drop this root's key handle. It does not revoke the root: only the owner does that. */
6992
6991
  clear() {
6993
6992
  this.#key?.clear();
@@ -6997,17 +6996,6 @@ var ShareRoot = class _ShareRoot {
6997
6996
  if (this.#key === void 0) throw new ShareRootError("key-cleared", "share root key has been cleared", { rootCid: this.cid });
6998
6997
  return this.#key;
6999
6998
  }
7000
- async #assertPolicyGate() {
7001
- const key = this.#activeKey();
7002
- if (this.#options.shareRootPolicyRoots !== true) throw new ShareRootError("policy-roots-disabled", "share root policy roots are disabled (shareRootPolicyRoots is off)", { rootCid: this.cid });
7003
- if (this.kind !== "share") throw new ShareRootError("policy-roots-disabled", "agent roots are bearer-only", { rootCid: this.cid });
7004
- if (!this.policyCapable) throw new ShareRootError("root-not-policy-capable", "this share root holds no :share decrypt; re-root once the Node enforces", { rootCid: this.cid });
7005
- if (this.#now() >= this.expiresAt.getTime()) throw new ShareRootError("expired", "share root has expired", { rootCid: this.cid });
7006
- if (!await (this.#options.policyOwnerModeProbe ?? defaultPolicyOwnerModeProbe).isEnforced(this.nodeOrigin)) {
7007
- throw new ShareRootError("policy-roots-not-enforced-by-node", "the Node does not advertise owner-authority mode enforce", { rootCid: this.cid });
7008
- }
7009
- return key;
7010
- }
7011
6999
  #now() {
7012
7000
  return (this.#options.now ?? Date.now)();
7013
7001
  }
@@ -7015,17 +7003,14 @@ var ShareRoot = class _ShareRoot {
7015
7003
  async function ensureShareRoot(input) {
7016
7004
  const options = input.options ?? {};
7017
7005
  const kind = options.kind ?? "share";
7018
- const purpose = input.purpose ?? "bearer";
7019
7006
  const now = (options.now ?? Date.now)();
7020
7007
  const nodeOrigin = requireNodeOrigin(input.nodeOrigin);
7021
7008
  const lifetime = PROFILES[kind].maxLifetimeMs - SHARE_ROOT_REQUEST_MARGIN_MS;
7022
7009
  if (input.expiresAt.getTime() - now > Math.min(lifetime, SHARE_ROOT_LINK_MAX_LIFETIME_MS)) throw new ShareRootError("lifetime-exceeds-cap", "no share root can cover this expiry");
7023
- if (purpose === "policy") {
7024
- if (options.shareRootPolicyRoots !== true || kind !== "share") throw new ShareRootError("policy-roots-disabled", "share root policy roots are disabled");
7025
- if (!await (options.policyOwnerModeProbe ?? defaultPolicyOwnerModeProbe).isEnforced(nodeOrigin)) throw new ShareRootError("policy-roots-not-enforced-by-node", "the Node does not advertise owner-authority mode enforce");
7026
- }
7027
7010
  const { current } = input;
7028
- if (current !== void 0 && current.kind === kind && current.spaceId === input.spaceId && current.nodeOrigin === nodeOrigin && current.covers(input.expiresAt, now) && (purpose === "bearer" || current.policyCapable)) return { root: current, rerooted: false };
7011
+ if (current !== void 0 && current.kind === kind && current.spaceId === input.spaceId && current.nodeOrigin === nodeOrigin && current.covers(input.expiresAt, now)) {
7012
+ return { root: current, rerooted: false };
7013
+ }
7029
7014
  const factory = input.keyFactory ?? (() => createDelegatedShareKey({ extractable: false }));
7030
7015
  const root = await ShareRoot.create({
7031
7016
  owner: input.owner,
@@ -7039,30 +7024,744 @@ async function ensureShareRoot(input) {
7039
7024
  },
7040
7025
  options
7041
7026
  });
7042
- if (purpose === "policy" && !root.policyCapable) {
7043
- throw new ShareRootError("policy-roots-not-enforced-by-node", "the Node stopped advertising enforce while re-rooting; the new root is kv-only", { rootCid: root.cid });
7044
- }
7045
7027
  return { root, rerooted: true };
7046
7028
  }
7029
+
7030
+ // src/artifact.ts
7031
+ var import_sha2563 = require("@noble/hashes/sha256");
7032
+ function parseArtifactVersionPath(path) {
7033
+ const match = /^artifacts\/([a-z0-9][a-z0-9_-]{2,62})\/versions\/([1-9][0-9]{0,14})\/([^/]+)$/.exec(path);
7034
+ if (match === null) return void 0;
7035
+ return { artifactId: match[1], version: Number(match[2]), file: match[3] };
7036
+ }
7037
+
7038
+ // src/share-network.ts
7039
+ var import_sha2564 = require("@noble/hashes/sha256");
7040
+ var SHARE_NETWORK_NAME_PREFIX = "share-";
7041
+ var NETWORK_SAFE_SHARE_ID = /^[a-z0-9][a-z0-9-]{15,35}$/;
7042
+ var NETWORK_URN = /^urn:tinycloud:encryption:did:pkh:eip155:([1-9]\d*):(0x[0-9a-fA-F]{40}):([a-z0-9][a-z0-9-]*)$/;
7043
+ var NETWORK_CREATE_ABILITY = "tinycloud.encryption/network.create";
7044
+ var NETWORK_REVOKE_ABILITY = "tinycloud.encryption/network.revoke";
7045
+ var DECRYPT_ABILITY = "tinycloud.encryption/decrypt";
7046
+ var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
7047
+ var NETWORK_ALG = "x25519-aes256gcm/v1";
7048
+ function assertNetworkSafeShareId(shareId) {
7049
+ if (typeof shareId !== "string" || !NETWORK_SAFE_SHARE_ID.test(shareId)) {
7050
+ throw new ShareRootError("share-id-not-network-safe", "shareId must match ^[a-z0-9][a-z0-9-]{15,35}$ to name its encryption network");
7051
+ }
7052
+ }
7053
+ function perShareNetworkIdFor(ownerDid, shareId) {
7054
+ const owner = parseOwnerDid(ownerDid);
7055
+ assertNetworkSafeShareId(shareId);
7056
+ return `urn:tinycloud:encryption:did:pkh:eip155:${owner.chainId}:${owner.address}:${SHARE_NETWORK_NAME_PREFIX}${shareId}`;
7057
+ }
7058
+ function isPerShareNetworkOf(ownerDid, networkId) {
7059
+ const parsed = typeof networkId === "string" ? parseOwnerNetworkUrn(networkId) : void 0;
7060
+ if (parsed === void 0) return false;
7061
+ const shareId = parsed.name.slice(SHARE_NETWORK_NAME_PREFIX.length);
7062
+ if (!NETWORK_SAFE_SHARE_ID.test(shareId)) return false;
7063
+ return networkId === perShareNetworkIdFor(ownerDid, shareId);
7064
+ }
7065
+ function parseOwnerNetworkUrn(urn) {
7066
+ const match = typeof urn === "string" ? NETWORK_URN.exec(urn) : null;
7067
+ return match === null ? void 0 : { owner: { chainId: match[1], address: match[2].toLowerCase() }, name: match[3] };
7068
+ }
7069
+ function sameNetworkUrn(left, right) {
7070
+ const a = parseOwnerNetworkUrn(left);
7071
+ const b = parseOwnerNetworkUrn(right);
7072
+ return a !== void 0 && b !== void 0 && sameOwner(a.owner, b.owner) && a.name === b.name;
7073
+ }
7074
+ async function nodeDidOf(nodeOrigin, fetchFn) {
7075
+ let info;
7076
+ try {
7077
+ info = await readJsonCapped(await fetchFn(new URL("/info", nodeOrigin), { redirect: "error" }));
7078
+ } catch (error) {
7079
+ throw new ShareRootError("node-refused", `node identity is unavailable: ${errorMessage(error)}`);
7080
+ }
7081
+ const nodeDid = typeof info === "object" && info !== null && "nodeId" in info ? info.nodeId : void 0;
7082
+ if (typeof nodeDid !== "string" || !nodeDid.startsWith("did:key:")) throw new ShareRootError("node-refused", "node identity is unavailable");
7083
+ return nodeDid;
7084
+ }
7085
+ async function lookupShareNetwork(nodeOrigin, networkId, fetchFn) {
7086
+ const response = await fetchFn(new URL(`/encryption/networks/${encodeURIComponent(networkId)}`, nodeOrigin), { redirect: "error" });
7087
+ if (response.status === 404) return { state: "absent" };
7088
+ if (!response.ok) throw new ShareRootError("node-refused", `share network lookup failed: HTTP ${response.status}`);
7089
+ let body;
7090
+ try {
7091
+ body = await readJsonCapped(response);
7092
+ } catch (error) {
7093
+ throw new ShareRootError("node-refused", `share network descriptor is unreadable: ${errorMessage(error)}`);
7094
+ }
7095
+ const descriptor = typeof body === "object" && body !== null && "descriptor" in body ? body.descriptor : body;
7096
+ if (typeof descriptor !== "object" || descriptor === null || Array.isArray(descriptor) || !("state" in descriptor) || typeof descriptor.state !== "string" || !("networkId" in descriptor) || typeof descriptor.networkId !== "string") {
7097
+ throw new ShareRootError("node-refused", "share network descriptor is malformed");
7098
+ }
7099
+ return { state: descriptor.state, networkId: descriptor.networkId, descriptor };
7100
+ }
7101
+ function assertActiveShareNetwork(lookup, expectedNetworkId) {
7102
+ if (lookup.state !== "active" || !("descriptor" in lookup)) throw new ShareRootError("network-not-active", `share network is ${lookup.state}`);
7103
+ const { descriptor } = lookup;
7104
+ const expected = parseOwnerNetworkUrn(expectedNetworkId);
7105
+ if (expected === void 0) throw new ShareRootError("invalid-request", "network id must be a did:pkh-owned network URN");
7106
+ const threshold = descriptor.threshold;
7107
+ const ownerDid = descriptor.ownerDid;
7108
+ let ownerMatches = false;
7109
+ try {
7110
+ ownerMatches = typeof ownerDid === "string" && sameOwner(parseOwnerDid(ownerDid), expected.owner);
7111
+ } catch {
7112
+ ownerMatches = false;
7113
+ }
7114
+ if (!sameNetworkUrn(lookup.networkId, expectedNetworkId) || !ownerMatches || descriptor.name !== expected.name || descriptor.alg !== NETWORK_ALG || descriptor.keyVersion !== 1 || typeof threshold !== "object" || threshold === null || !("n" in threshold) || !("t" in threshold) || threshold.n !== 1 || threshold.t !== 1) throw new ShareRootError("network-not-active", "share network descriptor does not match the share");
7115
+ }
7116
+ async function sendNetworkAdmin(input) {
7117
+ const parsed = parseOwnerNetworkUrn(input.networkId);
7118
+ if (parsed === void 0) throw new ShareRootError("invalid-request", "network id must be a did:pkh-owned network URN");
7119
+ const body = input.action === NETWORK_CREATE_ABILITY ? canonicalize({ name: parsed.name, ownerDid: `did:pkh:eip155:${parsed.owner.chainId}:${parsed.owner.address}`, threshold: { n: 1, t: 1 } }) : canonicalize({});
7120
+ const now = seconds((input.now ?? Date.now)());
7121
+ const bodyHash = [...(0, import_sha2564.sha256)(new TextEncoder().encode(body))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
7122
+ const invocation = await signCompactUcanAuthorization({
7123
+ issuerDid: input.signer.did,
7124
+ audienceDid: input.nodeDid,
7125
+ attenuation: { [input.networkId]: { [input.action]: [{}] } },
7126
+ facts: [{ type: NETWORK_ADMIN_TYPE, targetNode: input.nodeDid, networkId: input.networkId, bodyHash, action: input.action }],
7127
+ proofs: [input.proof],
7128
+ notBefore: now,
7129
+ expiresAt: now + 60,
7130
+ nonce: toBase64Url(crypto.getRandomValues(new Uint8Array(16))),
7131
+ sign: input.signer.sign
7132
+ });
7133
+ const path = input.action === NETWORK_CREATE_ABILITY ? "/encryption/networks" : `/encryption/networks/${encodeURIComponent(input.networkId)}/revoke`;
7134
+ const response = await input.fetch(new URL(path, input.nodeOrigin), {
7135
+ method: "POST",
7136
+ redirect: "error",
7137
+ headers: { Authorization: invocation.authorization, ...input.action === NETWORK_CREATE_ABILITY ? { "content-type": "application/json" } : {} },
7138
+ ...input.action === NETWORK_CREATE_ABILITY ? { body } : {}
7139
+ });
7140
+ await response.body?.cancel();
7141
+ return response.status;
7142
+ }
7143
+ async function retireShareNetwork(input) {
7144
+ const status = await sendNetworkAdmin({ ...input, action: NETWORK_REVOKE_ABILITY });
7145
+ const lookup = await lookupShareNetwork(input.nodeOrigin, input.networkId, input.fetch);
7146
+ if (lookup.state === "absent" && (status === 404 || status === 204)) return "absent";
7147
+ if (lookup.state === "revoked") return "retired";
7148
+ throw new ShareRootError("node-refused", `share network retire failed: HTTP ${status}, state ${lookup.state}`);
7149
+ }
7150
+
7151
+ // src/share-grant.ts
7152
+ var KV_ACTIONS = {
7153
+ view: ["tinycloud.kv/get", "tinycloud.kv/metadata"],
7154
+ edit: ["tinycloud.kv/get", "tinycloud.kv/metadata", "tinycloud.kv/put"]
7155
+ };
7156
+ var NETWORK_ACTIONS = [NETWORK_CREATE_ABILITY, NETWORK_REVOKE_ABILITY, DECRYPT_ABILITY];
7157
+ var SHARE_KV_ROOT = "xyz.tinycloud.share/";
7158
+ var EDIT_SHARE_READER_MAX_LIFETIME_MS = 60 * 6e4;
7159
+ var defaultFetch = (url, init) => globalThis.fetch(url, init);
7160
+ function assertShareGrantPath(path, shareId, access) {
7161
+ const inShare = isExactKeyUnder(path, `${SHARE_KV_ROOT}shares/${shareId}/`) && !path.slice(`${SHARE_KV_ROOT}shares/${shareId}/`.length).includes("/");
7162
+ const inArtifact = isExactKeyUnder(path, `${SHARE_KV_ROOT}artifacts/`) && parseArtifactVersionPath(path.slice(SHARE_KV_ROOT.length)) !== void 0;
7163
+ if (!inShare && !inArtifact) throw new ShareRootError("invalid-request", "share grant path must be xyz.tinycloud.share/shares/<shareId>/<file> or an artifact version file");
7164
+ if (inArtifact && access !== "view") throw new ShareRootError("invalid-request", "artifact versions are immutable: only view grants may name a version file");
7165
+ }
7166
+ function wantedFor(spec) {
7167
+ return { [`${spec.spaceId}/kv/${spec.path}`]: KV_ACTIONS[spec.access], [spec.networkId]: NETWORK_ACTIONS };
7168
+ }
7169
+ function rowOf(spec, keyDid, state, grantCid) {
7170
+ return { shareId: spec.shareId, nw: spec.networkId.slice(spec.networkId.lastIndexOf(":") + 1), networkId: spec.networkId, keyDid, access: spec.access, path: spec.path, state, ...grantCid === void 0 ? {} : { grantCid } };
7171
+ }
7172
+ var ShareGrant = class _ShareGrant {
7173
+ cid;
7174
+ ownerDid;
7175
+ spaceId;
7176
+ keyDid;
7177
+ nodeOrigin;
7178
+ shareId;
7179
+ networkId;
7180
+ path;
7181
+ access;
7182
+ issuedAt;
7183
+ expiresAt;
7184
+ #spec;
7185
+ #authorization;
7186
+ #key;
7187
+ #now;
7188
+ #fetch;
7189
+ constructor(input) {
7190
+ this.#spec = input.spec;
7191
+ this.cid = input.cid;
7192
+ this.ownerDid = input.spec.ownerDid;
7193
+ this.spaceId = input.spec.spaceId;
7194
+ this.keyDid = input.key.did;
7195
+ this.nodeOrigin = input.spec.nodeOrigin;
7196
+ this.shareId = input.spec.shareId;
7197
+ this.networkId = input.spec.networkId;
7198
+ this.path = input.spec.path;
7199
+ this.access = input.spec.access;
7200
+ this.issuedAt = input.issuedAt;
7201
+ this.expiresAt = input.expiresAt;
7202
+ this.#authorization = input.authorization;
7203
+ this.#key = input.key;
7204
+ this.#now = input.now;
7205
+ this.#fetch = input.fetch;
7206
+ }
7207
+ /**
7208
+ * Step 0: check everything that does not need the owner, and make the key.
7209
+ * Also looks the share's network up (no approval): a shareId whose network
7210
+ * is retired (burned), or already exists from an earlier grant, is refused
7211
+ * with `share-id-in-use` before the prompt (see `resumeExistingNetwork`).
7212
+ * Throws `share-id-not-network-safe`, `invalid-request`, `lifetime-exceeds-cap`,
7213
+ * `share-id-in-use`, `node-refused` or `invalid-key` with no approval spent.
7214
+ */
7215
+ static async prepare(input) {
7216
+ const clock = input.now ?? Date.now;
7217
+ const now = clock();
7218
+ const owner = parseOwnerDid(input.owner.ownerDid);
7219
+ const networkId = perShareNetworkIdFor(input.owner.ownerDid, input.shareId);
7220
+ assertSpace(input.spaceId, owner, "share");
7221
+ const nodeOrigin = requireNodeOrigin(input.nodeOrigin);
7222
+ if (input.access !== "view" && input.access !== "edit") throw new ShareRootError("invalid-request", "share grant access must be view or edit");
7223
+ assertShareGrantPath(input.path, input.shareId, input.access);
7224
+ const expiry = input.expiresAt instanceof Date ? input.expiresAt.getTime() : Number.NaN;
7225
+ if (!Number.isFinite(expiry) || expiry <= now) throw new ShareRootError("invalid-request", "share grant expiry must be in the future");
7226
+ if (expiry - now > SHARE_ROOT_LINK_MAX_LIFETIME_MS) throw new ShareRootError("lifetime-exceeds-cap", "share grant lifetime exceeds 30 days");
7227
+ const existing = await lookupShareNetwork(nodeOrigin, networkId, input.fetch ?? defaultFetch);
7228
+ if (existing.state !== "absent") {
7229
+ if (existing.state !== "active") throw new ShareRootError("share-id-in-use", `this shareId's network is ${existing.state}; a retired shareId is never reused`);
7230
+ if (input.resumeExistingNetwork !== true) throw new ShareRootError("share-id-in-use", "this shareId already has an active network from an earlier grant");
7231
+ assertActiveShareNetwork(existing, networkId);
7232
+ }
7233
+ const key = await (input.keyFactory ?? (() => createDelegatedShareKey({ extractable: false })))();
7234
+ try {
7235
+ await assertUsableKey(key);
7236
+ } catch (error) {
7237
+ key.clear();
7238
+ throw error;
7239
+ }
7240
+ const spec = { owner, ownerDid: input.owner.ownerDid, spaceId: input.spaceId, nodeOrigin, shareId: input.shareId, networkId, path: input.path, access: input.access };
7241
+ let settled = false;
7242
+ return {
7243
+ shareId: spec.shareId,
7244
+ networkId,
7245
+ keyDid: key.did,
7246
+ row: rowOf(spec, key.did, "pending"),
7247
+ approve: async () => {
7248
+ if (settled) throw new ShareRootError("invalid-request", "this pending grant was already approved or discarded");
7249
+ settled = true;
7250
+ return _ShareGrant.#approve(spec, key, input.owner, input.expiresAt, clock, input.fetch ?? defaultFetch);
7251
+ },
7252
+ discard: () => {
7253
+ settled = true;
7254
+ key.clear();
7255
+ }
7256
+ };
7257
+ }
7258
+ /** `prepare` then `approve`, for callers that keep no `pending` row (tests, one-shot tools). */
7259
+ static async create(input) {
7260
+ return (await _ShareGrant.prepare(input)).approve();
7261
+ }
7262
+ static async #approve(spec, key, owner, expiresAt, clock, fetchFn) {
7263
+ const permissions = [
7264
+ { service: "tinycloud.kv", path: spec.path, actions: [...KV_ACTIONS[spec.access]] },
7265
+ { service: "tinycloud.encryption", path: spec.networkId, actions: [...NETWORK_ACTIONS] }
7266
+ ];
7267
+ let receipt;
7268
+ try {
7269
+ receipt = await owner.createOwnerDelegation({ delegateDid: key.did, spaceId: spec.spaceId, expiresAt, permissions });
7270
+ } catch (error) {
7271
+ key.clear();
7272
+ throw error;
7273
+ }
7274
+ try {
7275
+ const authorization = new Uint8Array(receipt.signedDagCbor);
7276
+ const signedCid = ownerRootCacaoCid(authorization);
7277
+ if (signedCid !== receipt.delegationCid) throw new ShareRootError("malformed-root", "signed grant bytes do not match the imported delegation CID", { rootCid: receipt.delegationCid, signedCid, mustRevoke: true });
7278
+ let decoded;
7279
+ try {
7280
+ decoded = decodeOwnerRootCacao(authorization);
7281
+ } catch (error) {
7282
+ throw new ShareRootError("malformed-root", errorMessage(error), { rootCid: receipt.delegationCid, mustRevoke: true });
7283
+ }
7284
+ const times = validateSignedOwnerDelegation(decoded, { label: "per-share grant", owner: spec.owner, keyDid: key.did, wanted: wantedFor(spec), maxLifetimeMs: SHARE_ROOT_LINK_MAX_LIFETIME_MS, requestedExpiresAt: expiresAt, now: clock() });
7285
+ return new _ShareGrant({ spec, cid: decoded.cid, ...times, authorization, key, now: clock, fetch: fetchFn });
7286
+ } catch (error) {
7287
+ key.clear();
7288
+ if (error instanceof ShareRootError) throw error;
7289
+ throw new ShareRootError("malformed-root", errorMessage(error), { rootCid: receipt.delegationCid, mustRevoke: true });
7290
+ }
7291
+ }
7292
+ /** Rebuild a stored grant with its key (e.g. a CLI 0600 key file); the signed bytes are checked again. Only record v2. */
7293
+ static async restore(input) {
7294
+ const { record } = input;
7295
+ const clock = input.now ?? Date.now;
7296
+ if (record.version !== 2) throw new ShareRootError("malformed-root", "share grant record is not supported (only per-share network grants, record v2)");
7297
+ if (record.access !== "view" && record.access !== "edit") throw new ShareRootError("malformed-root", "share grant record access is invalid");
7298
+ if (record.keyDid !== input.key.did) throw new ShareRootError("audience-mismatch", "share grant record belongs to another key");
7299
+ const owner = parseOwnerDid(record.ownerDid);
7300
+ const networkId = perShareNetworkIdFor(record.ownerDid, record.shareId);
7301
+ if (!sameNetworkUrn(networkId, record.networkId)) throw new ShareRootError("malformed-root", "share grant record network does not match its shareId");
7302
+ assertSpace(record.spaceId, owner, "share");
7303
+ const nodeOrigin = requireNodeOrigin(record.nodeOrigin);
7304
+ assertShareGrantPath(record.path, record.shareId, record.access);
7305
+ await assertUsableKey(input.key);
7306
+ const spec = { owner, ownerDid: record.ownerDid, spaceId: record.spaceId, nodeOrigin, shareId: record.shareId, networkId, path: record.path, access: record.access };
7307
+ let authorization;
7308
+ let decoded;
7309
+ try {
7310
+ authorization = fromBase64Url(record.authorization);
7311
+ decoded = decodeOwnerRootCacao(authorization, record.cid);
7312
+ } catch (error) {
7313
+ throw new ShareRootError("malformed-root", errorMessage(error), { rootCid: record.cid });
7314
+ }
7315
+ const times = validateSignedOwnerDelegation(decoded, { label: "per-share grant", owner, keyDid: input.key.did, wanted: wantedFor(spec), maxLifetimeMs: SHARE_ROOT_LINK_MAX_LIFETIME_MS, now: clock() });
7316
+ return new _ShareGrant({ spec, cid: decoded.cid, ...times, authorization, key: input.key, now: clock, fetch: input.fetch ?? defaultFetch });
7317
+ }
7318
+ /**
7319
+ * This grant's CACAO as base64url, UNPADDED (the record form). As a Node
7320
+ * `Authorization` header it must be padded to a multiple of 4 characters:
7321
+ * the Node refuses an unpadded value otherwise.
7322
+ */
7323
+ get authorization() {
7324
+ return toBase64Url(this.#authorization);
7325
+ }
7326
+ /** The exact KV resource this grant covers. */
7327
+ get resource() {
7328
+ return `${this.spaceId}/kv/${this.path}`;
7329
+ }
7330
+ toRecord() {
7331
+ return { version: 2, cid: this.cid, authorization: this.authorization, spaceId: this.spaceId, ownerDid: this.ownerDid, keyDid: this.keyDid, nodeOrigin: this.nodeOrigin, shareId: this.shareId, networkId: this.networkId, path: this.path, access: this.access, expiresAt: this.expiresAt.toISOString() };
7332
+ }
7333
+ /** The history/index row for `state`. */
7334
+ row(state) {
7335
+ return rowOf(this.#spec, this.keyDid, state, this.cid);
7336
+ }
7337
+ /**
7338
+ * Step 3: make sure the share's network exists and is usable. Creates it
7339
+ * when absent (key-signed, no approval), then decides on a fresh lookup
7340
+ * after any answer. Returns `networkId` (the grant's lowercase spelling;
7341
+ * the Node echoes the owner in EIP-55 case). A retired network is never
7342
+ * re-created (the Node answers 409): `network-not-active`.
7343
+ */
7344
+ async ensureNetwork() {
7345
+ const signer = this.#signer();
7346
+ const first = await lookupShareNetwork(this.nodeOrigin, this.networkId, this.#fetch);
7347
+ if (first.state === "absent") {
7348
+ const nodeDid = await nodeDidOf(this.nodeOrigin, this.#fetch);
7349
+ await sendNetworkAdmin({ nodeOrigin: this.nodeOrigin, nodeDid, networkId: this.networkId, action: NETWORK_CREATE_ABILITY, signer, proof: this.cid, fetch: this.#fetch, now: this.#now });
7350
+ return this.assertNetworkActive();
7351
+ }
7352
+ assertActiveShareNetwork(first, this.networkId);
7353
+ return this.networkId;
7354
+ }
7355
+ /** Throw `network-not-active` unless the share's network is `active` now (fresh lookup). */
7356
+ async assertNetworkActive() {
7357
+ assertActiveShareNetwork(await lookupShareNetwork(this.nodeOrigin, this.networkId, this.#fetch), this.networkId);
7358
+ return this.networkId;
7359
+ }
7360
+ /**
7361
+ * Retire the share's network with the per-share key (no approval):
7362
+ * PERMANENT, and every holder of decrypt on it (recipients included) is
7363
+ * refused at their next request. Idempotent: a repeat or a never-created
7364
+ * network counts as done.
7365
+ */
7366
+ async retireNetwork() {
7367
+ const signer = this.#signer();
7368
+ const nodeDid = await nodeDidOf(this.nodeOrigin, this.#fetch);
7369
+ return retireShareNetwork({ nodeOrigin: this.nodeOrigin, nodeDid, networkId: this.networkId, signer, proof: this.cid, fetch: this.#fetch, now: this.#now });
7370
+ }
7371
+ /** Step 4 check (I1): the stored object belongs to this share, on its exact key and network. */
7372
+ assertContentSource(contentSource) {
7373
+ if (contentSource.shareId !== this.shareId || contentSource.kvResource !== this.resource || contentSource.selector !== "exact" || !sameNetworkUrn(contentSource.encryptionNetwork, this.networkId)) throw new ShareRootError("invalid-request", "content source does not belong to this share's grant (shareId, exact key and network)");
7374
+ }
7375
+ /**
7376
+ * Policy/v3 owner signing for this one share. Every call checks the key is
7377
+ * held and the grant is live. Policy roots must be owned by the per-share
7378
+ * key, expire no later than the grant, and name only this grant's exact
7379
+ * KV resource (no action the grant lacks) and decrypt on this share's
7380
+ * network (parsed equality).
7381
+ */
7382
+ policySigner() {
7383
+ const actions = KV_ACTIONS[this.access];
7384
+ return ownerPolicySigner({
7385
+ ownerDid: this.keyDid,
7386
+ authorityCid: this.cid,
7387
+ notAfter: new Date(this.expiresAt),
7388
+ gate: async () => this.#activeKey(),
7389
+ now: this.#now,
7390
+ inScope: (capability) => capability.kind === "encryption" ? sameNetworkUrn(capability.resource, this.networkId) && capability.action === DECRYPT_ABILITY : capability.resource === this.resource && capability.selector === "exact" && capability.actions.every((action) => actions.includes(action))
7391
+ });
7392
+ }
7393
+ /**
7394
+ * Step 5: `publishAddressedShare`'s `authority` for this share. Its
7395
+ * `registerPolicy` first re-reads the network and refuses unless it is
7396
+ * `active` (released Nodes accept registrations on missing or retired
7397
+ * networks).
7398
+ */
7399
+ publishAuthority(registerPolicy) {
7400
+ return {
7401
+ ...this.policySigner(),
7402
+ registerPolicy: async (request) => {
7403
+ await this.assertNetworkActive();
7404
+ return registerPolicy(request);
7405
+ }
7406
+ };
7407
+ }
7408
+ /** Drop the per-share key. It does not retire the network or revoke the grant. */
7409
+ clear() {
7410
+ this.#key?.clear();
7411
+ this.#key = void 0;
7412
+ }
7413
+ #activeKey() {
7414
+ if (this.#key === void 0) throw new ShareRootError("key-cleared", "share grant key has been cleared", { rootCid: this.cid });
7415
+ if (this.#now() >= this.expiresAt.getTime()) throw new ShareRootError("expired", "share grant has expired", { rootCid: this.cid });
7416
+ return this.#key;
7417
+ }
7418
+ #signer() {
7419
+ const key = this.#activeKey();
7420
+ return { did: key.did, sign: (bytes2) => key.sign(bytes2) };
7421
+ }
7422
+ };
7423
+ async function revokeShareGrant(input) {
7424
+ const fetchFn = input.fetch ?? defaultFetch;
7425
+ const network = await input.grant.retireNetwork();
7426
+ try {
7427
+ const response = await fetchFn(new URL("/revoke", input.grant.nodeOrigin), { method: "POST", redirect: "error", headers: { Authorization: await input.owner.signOwnerRevocation(input.grant.cid) } });
7428
+ const status = response.status;
7429
+ if (!await isRevokeReceiptFor(response, input.grant.cid)) throw new Error(`HTTP ${status}: no receipt confirming this grant`);
7430
+ } catch (error) {
7431
+ throw new ShareRootError("grant-revoke-incomplete", `the share's network is retired (decrypt cut for everyone), but the grant revoke failed: ${errorMessage(error)}`, { rootCid: input.grant.cid });
7432
+ }
7433
+ return { network, grant: "revoked" };
7434
+ }
7435
+ async function openEditShare(input) {
7436
+ const clock = input.now ?? Date.now;
7437
+ const lifetime = input.lifetimeMs ?? EDIT_SHARE_READER_MAX_LIFETIME_MS;
7438
+ if (!Number.isFinite(lifetime) || lifetime <= 0 || lifetime > EDIT_SHARE_READER_MAX_LIFETIME_MS) throw new ShareRootError("lifetime-exceeds-cap", "an edit-share reader lives at most one hour");
7439
+ const owner = parseOwnerDid(input.owner.ownerDid);
7440
+ if (!sameOwner(owner, parseOwnerDid(input.record.ownerDid))) throw new ShareRootError("invalid-request", "the share belongs to another owner");
7441
+ const networkId = perShareNetworkIdFor(input.record.ownerDid, input.record.shareId);
7442
+ if (!sameNetworkUrn(networkId, input.record.networkId)) throw new ShareRootError("invalid-request", "share network does not match its shareId");
7443
+ assertSpace(input.record.spaceId, owner, "share");
7444
+ assertShareGrantPath(input.record.path, input.record.shareId, "edit");
7445
+ const key = await createDelegatedShareKey({ extractable: false });
7446
+ const expiresAt = new Date(clock() + lifetime);
7447
+ let receipt;
7448
+ try {
7449
+ receipt = await input.owner.createOwnerDelegation({
7450
+ delegateDid: key.did,
7451
+ spaceId: input.record.spaceId,
7452
+ expiresAt,
7453
+ permissions: [
7454
+ { service: "tinycloud.kv", path: input.record.path, actions: ["tinycloud.kv/get"] },
7455
+ { service: "tinycloud.encryption", path: networkId, actions: [DECRYPT_ABILITY] }
7456
+ ]
7457
+ });
7458
+ } catch (error) {
7459
+ key.clear();
7460
+ throw error;
7461
+ }
7462
+ try {
7463
+ const authorization = new Uint8Array(receipt.signedDagCbor);
7464
+ const signedCid = ownerRootCacaoCid(authorization);
7465
+ if (signedCid !== receipt.delegationCid) throw new ShareRootError("malformed-root", "signed reader grant bytes do not match the imported delegation CID", { rootCid: receipt.delegationCid, signedCid, mustRevoke: true });
7466
+ validateSignedOwnerDelegation(decodeOwnerRootCacao(authorization), {
7467
+ label: "edit-share reader",
7468
+ owner,
7469
+ keyDid: key.did,
7470
+ wanted: { [`${input.record.spaceId}/kv/${input.record.path}`]: ["tinycloud.kv/get"], [networkId]: [DECRYPT_ABILITY] },
7471
+ maxLifetimeMs: EDIT_SHARE_READER_MAX_LIFETIME_MS,
7472
+ requestedExpiresAt: expiresAt,
7473
+ now: clock()
7474
+ });
7475
+ } catch (error) {
7476
+ key.clear();
7477
+ if (error instanceof ShareRootError) throw error;
7478
+ throw new ShareRootError("malformed-root", errorMessage(error), { rootCid: receipt.delegationCid, mustRevoke: true });
7479
+ }
7480
+ let live = key;
7481
+ const clear = () => {
7482
+ live?.clear();
7483
+ live = void 0;
7484
+ };
7485
+ return {
7486
+ cid: receipt.delegationCid,
7487
+ keyDid: key.did,
7488
+ networkId,
7489
+ path: input.record.path,
7490
+ expiresAt,
7491
+ signer: {
7492
+ did: key.did,
7493
+ sign: async (bytes2) => {
7494
+ if (live === void 0 || clock() >= expiresAt.getTime()) throw new ShareRootError("expired", "edit-share reader has expired or was cleared");
7495
+ return live.sign(bytes2);
7496
+ }
7497
+ },
7498
+ clear
7499
+ };
7500
+ }
7501
+
7502
+ // src/share-control.ts
7503
+ var import_sha3 = require("@noble/hashes/sha3");
7504
+ var SHARE_CONTROL_HEADER_BUDGET_BYTES = 3e4;
7505
+ var SHARE_CONTROL_LIFETIME_MS = 10 * 6e4;
7506
+ var DELEGATION_REVOKE_ABILITY = "tinycloud.delegation/revoke";
7507
+ var RECAP_STATEMENT = "I further authorize the stated URI to perform the following actions on my behalf:";
7508
+ var PLACEHOLDER_KEY_DID = `did:key:z6Mk${"1".repeat(44)}`;
7509
+ function checksumAddress(address) {
7510
+ const lower = address.toLowerCase().slice(2);
7511
+ const hash = (0, import_sha3.keccak_256)(new TextEncoder().encode(lower));
7512
+ let out = "0x";
7513
+ for (let index = 0; index < lower.length; index++) {
7514
+ const nibble = hash[index >> 1] >> (index % 2 === 0 ? 4 : 0) & 15;
7515
+ out += nibble >= 8 ? lower[index].toUpperCase() : lower[index];
7516
+ }
7517
+ return out;
7518
+ }
7519
+ function reCapStatement(attenuation) {
7520
+ let index = 0;
7521
+ let lines = "";
7522
+ for (const resource of Object.keys(attenuation).sort()) {
7523
+ const namespaces = {};
7524
+ for (const ability of Object.keys(attenuation[resource]).sort()) {
7525
+ const slash = ability.indexOf("/");
7526
+ (namespaces[ability.slice(0, slash)] ??= []).push(ability.slice(slash + 1));
7527
+ }
7528
+ for (const namespace of Object.keys(namespaces).sort()) {
7529
+ lines += ` (${++index}) '${namespace}': ${namespaces[namespace].map((name) => `'${name}'`).join(", ")} for '${resource}'.`;
7530
+ }
7531
+ }
7532
+ return `${RECAP_STATEMENT}${lines}`;
7533
+ }
7534
+ function encodeDagCbor(value) {
7535
+ const output = [];
7536
+ const head = (major, argument) => {
7537
+ if (argument < 24) output.push(major << 5 | argument);
7538
+ else if (argument < 256) output.push(major << 5 | 24, argument);
7539
+ else if (argument < 65536) output.push(major << 5 | 25, argument >> 8, argument & 255);
7540
+ else output.push(major << 5 | 26, argument >>> 24 & 255, argument >>> 16 & 255, argument >>> 8 & 255, argument & 255);
7541
+ };
7542
+ const write = (item) => {
7543
+ if (typeof item === "number" && Number.isSafeInteger(item) && item >= 0 && item < 4294967296) head(0, item);
7544
+ else if (typeof item === "string") {
7545
+ const encoded = new TextEncoder().encode(item);
7546
+ head(3, encoded.length);
7547
+ output.push(...encoded);
7548
+ } else if (item instanceof Uint8Array) {
7549
+ head(2, item.length);
7550
+ output.push(...item);
7551
+ } else if (Array.isArray(item)) {
7552
+ head(4, item.length);
7553
+ item.forEach(write);
7554
+ } else if (typeof item === "object" && item !== null) {
7555
+ const entries = Object.entries(item).map(([key, entry]) => [new TextEncoder().encode(key), entry]);
7556
+ entries.sort(([left], [right]) => left.length - right.length || compareBytes(left, right));
7557
+ head(5, entries.length);
7558
+ for (const [key, entry] of entries) {
7559
+ head(3, key.length);
7560
+ output.push(...key);
7561
+ write(entry);
7562
+ }
7563
+ } else throw new Error("value is not encodable as a CACAO field");
7564
+ };
7565
+ write(value);
7566
+ return Uint8Array.from(output);
7567
+ }
7568
+ function compareBytes(left, right) {
7569
+ for (let index = 0; index < left.length; index++) if (left[index] !== right[index]) return left[index] - right[index];
7570
+ return 0;
7571
+ }
7572
+ function unsignedControl(input) {
7573
+ const address = checksumAddress(input.owner.address);
7574
+ const statement = reCapStatement(input.attenuation);
7575
+ const recap = `urn:recap:${toBase64Url(new TextEncoder().encode(JSON.stringify({ att: input.attenuation, prf: [] })))}`;
7576
+ const message = [
7577
+ `${input.domain} wants you to sign in with your Ethereum account:`,
7578
+ address,
7579
+ "",
7580
+ statement,
7581
+ "",
7582
+ `URI: ${input.keyDid}`,
7583
+ "Version: 1",
7584
+ `Chain ID: ${input.owner.chainId}`,
7585
+ `Nonce: ${input.nonce}`,
7586
+ `Issued At: ${input.issuedAt}`,
7587
+ `Expiration Time: ${input.expiresAt}`,
7588
+ "Resources:",
7589
+ `- ${recap}`
7590
+ ].join("\n");
7591
+ const payload = { aud: input.keyDid, domain: input.domain, exp: input.expiresAt, iat: input.issuedAt, iss: `did:pkh:eip155:${input.owner.chainId}:${address}`, nonce: input.nonce, resources: [recap], statement, version: 1 };
7592
+ return { message, payload };
7593
+ }
7594
+ function signedControlBytes(payload, signature) {
7595
+ return encodeDagCbor({ h: { t: "eip4361" }, p: payload, s: { t: "eip191", s: signature } });
7596
+ }
7597
+ function attenuationFor(targets, spaceDelegationResources) {
7598
+ const attenuation = {};
7599
+ for (const resource of spaceDelegationResources) attenuation[resource] = { [DELEGATION_REVOKE_ABILITY]: [{}] };
7600
+ for (const target of targets) attenuation[target.networkId] = { [DELEGATION_REVOKE_ABILITY]: [{}], [NETWORK_REVOKE_ABILITY]: [{}] };
7601
+ return attenuation;
7602
+ }
7603
+ var ISO_PLACEHOLDER = (/* @__PURE__ */ new Date(0)).toISOString();
7604
+ var NONCE_PLACEHOLDER = "0".repeat(32);
7605
+ function headerValue(bytes2) {
7606
+ const text2 = toBase64Url(bytes2);
7607
+ return text2 + "=".repeat((4 - text2.length % 4) % 4);
7608
+ }
7609
+ function headerLength(byteLength) {
7610
+ return 4 * Math.ceil(byteLength / 3);
7611
+ }
7612
+ function headerBytesOf(owner, domain, targets, spaceDelegationResources) {
7613
+ const { payload } = unsignedControl({ owner, domain, keyDid: PLACEHOLDER_KEY_DID, attenuation: attenuationFor(targets, spaceDelegationResources), issuedAt: ISO_PLACEHOLDER, expiresAt: ISO_PLACEHOLDER, nonce: NONCE_PLACEHOLDER });
7614
+ return headerLength(signedControlBytes(payload, new Uint8Array(65)).length);
7615
+ }
7616
+ var CONTROL_DOMAIN = /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?)*(?::\d{1,5})?$/;
7617
+ var CONTROL_SPACE_ROW = /^tinycloud:pkh:eip155:([1-9]\d*):(0x[0-9a-fA-F]{40}):([A-Za-z0-9_-]+)\/delegation$/;
7618
+ var GRANT_CID = /^b[a-z2-7]{20,}$/;
7619
+ function assertControlInputs(ownerDid, owner, domain, targets, spaceDelegationResources) {
7620
+ if (typeof domain !== "string" || !CONTROL_DOMAIN.test(domain)) throw new ShareRootError("invalid-request", "control domain must be a host name, optionally with a port");
7621
+ const seenRows = /* @__PURE__ */ new Set();
7622
+ for (const row of spaceDelegationResources) {
7623
+ const match = typeof row === "string" ? CONTROL_SPACE_ROW.exec(row) : null;
7624
+ if (match === null || !sameOwner({ chainId: match[1], address: match[2].toLowerCase() }, owner)) throw new ShareRootError("control-target-invalid", "control space rows must be <owner space>/delegation");
7625
+ if (seenRows.has(row)) throw new ShareRootError("control-target-invalid", "control space rows must not repeat");
7626
+ seenRows.add(row);
7627
+ }
7628
+ const seen = /* @__PURE__ */ new Set();
7629
+ for (const target of targets) {
7630
+ if (!isPerShareNetworkOf(ownerDid, target.networkId)) throw new ShareRootError("control-target-invalid", "control targets must be this owner's per-share networks (share-<shareId>)");
7631
+ if (target.grantCid !== void 0 && (typeof target.grantCid !== "string" || !GRANT_CID.test(target.grantCid))) throw new ShareRootError("control-target-invalid", "control target grant CID is invalid");
7632
+ if (seen.has(target.networkId)) throw new ShareRootError("control-target-invalid", "control targets must not repeat a network");
7633
+ seen.add(target.networkId);
7634
+ }
7635
+ }
7636
+ function planShareControlBatches(input) {
7637
+ const owner = parseOwnerDid(input.owner.ownerDid);
7638
+ const budget = input.budgetBytes ?? SHARE_CONTROL_HEADER_BUDGET_BYTES;
7639
+ if (!Number.isSafeInteger(budget) || budget <= 0 || budget > SHARE_CONTROL_HEADER_BUDGET_BYTES) throw new ShareRootError("invalid-request", `control budget must be 1..${SHARE_CONTROL_HEADER_BUDGET_BYTES} bytes`);
7640
+ const rows = (input.spaceIds ?? []).map((spaceId) => `${spaceId}/delegation`);
7641
+ assertControlInputs(input.owner.ownerDid, owner, input.owner.domain, input.targets, rows);
7642
+ const batches = [];
7643
+ let current = [];
7644
+ let size = 0;
7645
+ for (const target of input.targets) {
7646
+ const next = headerBytesOf(owner, input.owner.domain, [...current, target], rows);
7647
+ if (next <= budget) {
7648
+ current.push(target);
7649
+ size = next;
7650
+ continue;
7651
+ }
7652
+ if (current.length === 0) throw new ShareRootError("invalid-request", "one target and the space rows exceed the control budget");
7653
+ batches.push({ targets: current, spaceDelegationResources: rows, headerBytes: size });
7654
+ current = [target];
7655
+ size = headerBytesOf(owner, input.owner.domain, current, rows);
7656
+ if (size > budget) throw new ShareRootError("invalid-request", "one target and the space rows exceed the control budget");
7657
+ }
7658
+ if (current.length > 0) {
7659
+ batches.push({ targets: current, spaceDelegationResources: rows, headerBytes: size });
7660
+ } else if (rows.length > 0) {
7661
+ size = headerBytesOf(owner, input.owner.domain, [], rows);
7662
+ if (size > budget) throw new ShareRootError("invalid-request", "the space rows alone exceed the control budget");
7663
+ batches.push({ targets: [], spaceDelegationResources: rows, headerBytes: size });
7664
+ }
7665
+ return batches;
7666
+ }
7667
+ function signatureBytes(hex) {
7668
+ if (typeof hex !== "string" || !/^0x[0-9a-fA-F]{130}$/.test(hex)) throw new ShareRootError("invalid-request", "owner signature must be 65 bytes of 0x hex");
7669
+ const bytes2 = new Uint8Array(65);
7670
+ for (let index = 0; index < 65; index++) bytes2[index] = Number.parseInt(hex.slice(2 + index * 2, 4 + index * 2), 16);
7671
+ return bytes2;
7672
+ }
7673
+ async function runShareControlBatch(input) {
7674
+ const fetchFn = input.fetch ?? ((url, init) => globalThis.fetch(url, init));
7675
+ const clock = input.now ?? Date.now;
7676
+ const nodeOrigin = requireNodeOrigin(input.nodeOrigin);
7677
+ const owner = parseOwnerDid(input.owner.ownerDid);
7678
+ const { batch } = input;
7679
+ assertControlInputs(input.owner.ownerDid, owner, input.owner.domain, batch.targets, batch.spaceDelegationResources);
7680
+ if (batch.targets.length === 0 && batch.spaceDelegationResources.length === 0) throw new ShareRootError("invalid-request", "control batch is empty");
7681
+ const key = await (input.keyFactory ?? (() => createDelegatedShareKey({ extractable: false })))();
7682
+ try {
7683
+ const now = clock();
7684
+ const issuedAt = new Date(now).toISOString();
7685
+ const expiresAt = new Date(now + SHARE_CONTROL_LIFETIME_MS).toISOString();
7686
+ const nonce = [...crypto.getRandomValues(new Uint8Array(16))].map((byte) => byte.toString(16).padStart(2, "0")).join("");
7687
+ const control = unsignedControl({ owner, domain: input.owner.domain, keyDid: key.did, attenuation: attenuationFor(batch.targets, batch.spaceDelegationResources), issuedAt, expiresAt, nonce });
7688
+ const sized = headerLength(signedControlBytes(control.payload, new Uint8Array(65)).length);
7689
+ if (sized > SHARE_CONTROL_HEADER_BUDGET_BYTES) throw new ShareRootError("invalid-request", `control is ${sized} bytes, above the ${SHARE_CONTROL_HEADER_BUDGET_BYTES}-byte budget`);
7690
+ const bytes2 = signedControlBytes(control.payload, signatureBytes(await input.owner.signMessage(control.message)));
7691
+ const controlCid = ownerRootCacaoCid(bytes2);
7692
+ const imported = await fetchFn(new URL("/delegate", nodeOrigin), { method: "POST", redirect: "error", headers: { Authorization: headerValue(bytes2) } });
7693
+ if (!imported.ok) throw new ShareRootError("node-refused", `owner control import failed: HTTP ${imported.status} ${(await imported.text()).slice(0, 120)}`);
7694
+ await imported.body?.cancel();
7695
+ const nodeDid = await nodeDidOf(nodeOrigin, fetchFn);
7696
+ const signer = { did: key.did, sign: (data) => key.sign(data) };
7697
+ const networks = [];
7698
+ for (const target of batch.targets) {
7699
+ try {
7700
+ networks.push({ networkId: target.networkId, result: await retireShareNetwork({ nodeOrigin, nodeDid, networkId: target.networkId, signer, proof: controlCid, fetch: fetchFn, now: clock }) });
7701
+ } catch (error) {
7702
+ networks.push({ networkId: target.networkId, result: "failed", error: errorMessage(error) });
7703
+ }
7704
+ }
7705
+ const grants = [];
7706
+ for (const target of batch.targets) {
7707
+ if (target.grantCid === void 0) continue;
7708
+ const at = seconds(clock());
7709
+ const invocation = await signCompactUcanAuthorization({
7710
+ issuerDid: key.did,
7711
+ audienceDid: key.did,
7712
+ attenuation: { [`urn:cid:${target.grantCid}`]: { [DELEGATION_REVOKE_ABILITY]: [{}] } },
7713
+ facts: [{}],
7714
+ proofs: [controlCid],
7715
+ notBefore: at,
7716
+ expiresAt: at + 60,
7717
+ nonce: `urn:uuid:${crypto.randomUUID()}`,
7718
+ sign: signer.sign
7719
+ });
7720
+ const response = await fetchFn(new URL("/revoke", nodeOrigin), { method: "POST", redirect: "error", headers: { Authorization: invocation.authorization } });
7721
+ const status = response.status;
7722
+ if (await isRevokeReceiptFor(response, target.grantCid)) {
7723
+ grants.push({ grantCid: target.grantCid, result: "revoked", status });
7724
+ continue;
7725
+ }
7726
+ if (input.revokeFallback === void 0) {
7727
+ grants.push({ grantCid: target.grantCid, result: "refused", status });
7728
+ continue;
7729
+ }
7730
+ const fallback = await fetchFn(new URL("/revoke", nodeOrigin), { method: "POST", redirect: "error", headers: { Authorization: await input.revokeFallback.signOwnerRevocation(target.grantCid) } });
7731
+ const fallbackStatus = fallback.status;
7732
+ grants.push({ grantCid: target.grantCid, result: await isRevokeReceiptFor(fallback, target.grantCid) ? "revoked-by-owner" : "refused", status: fallbackStatus });
7733
+ }
7734
+ return { controlCid, networks, grants };
7735
+ } finally {
7736
+ key.clear();
7737
+ }
7738
+ }
7047
7739
  // Annotate the CommonJS export names for ESM import in node:
7048
7740
  0 && (module.exports = {
7049
7741
  AGENT_SHARE_ROOT_MAX_LIFETIME_MS,
7050
- POLICY_OWNER_AUTHORITY_FEATURES,
7051
- POLICY_OWNER_AUTHORITY_PROBE_TTL_MS,
7742
+ EDIT_SHARE_READER_MAX_LIFETIME_MS,
7743
+ SHARE_CONTROL_HEADER_BUDGET_BYTES,
7744
+ SHARE_CONTROL_LIFETIME_MS,
7745
+ SHARE_NETWORK_NAME_PREFIX,
7052
7746
  SHARE_ROOT_CLOCK_SKEW_MS,
7053
7747
  SHARE_ROOT_LINK_MAX_LIFETIME_MS,
7054
7748
  SHARE_ROOT_MAX_LIFETIME_MS,
7055
7749
  SHARE_ROOT_PREFIX,
7056
7750
  SHARE_ROOT_REQUEST_MARGIN_MS,
7751
+ ShareGrant,
7057
7752
  ShareRoot,
7058
7753
  ShareRootError,
7754
+ assertNetworkSafeShareId,
7059
7755
  assertNonExtractableShareKey,
7060
- createPolicyOwnerModeProbe,
7756
+ assertShareGrantPath,
7061
7757
  decodeOwnerRootCacao,
7062
- defaultPolicyOwnerModeProbe,
7063
7758
  ensureShareRoot,
7759
+ openEditShare,
7064
7760
  ownerRootCacaoCid,
7065
- readPolicyOwnerAuthorityMode,
7761
+ perShareNetworkIdFor,
7762
+ planShareControlBatches,
7763
+ revokeShareGrant,
7764
+ runShareControlBatch,
7066
7765
  signShareKeyDelegation
7067
7766
  });
7068
7767
  //# sourceMappingURL=share-root.cjs.map