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

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.
@@ -28,15 +28,18 @@ __export(share_root_entry_exports, {
28
28
  SHARE_ROOT_MAX_LIFETIME_MS: () => SHARE_ROOT_MAX_LIFETIME_MS,
29
29
  SHARE_ROOT_PREFIX: () => SHARE_ROOT_PREFIX,
30
30
  SHARE_ROOT_REQUEST_MARGIN_MS: () => SHARE_ROOT_REQUEST_MARGIN_MS,
31
+ ShareGrant: () => ShareGrant,
31
32
  ShareRoot: () => ShareRoot,
32
33
  ShareRootError: () => ShareRootError,
33
34
  assertNonExtractableShareKey: () => assertNonExtractableShareKey,
34
35
  createPolicyOwnerModeProbe: () => createPolicyOwnerModeProbe,
35
36
  decodeOwnerRootCacao: () => decodeOwnerRootCacao,
36
37
  defaultPolicyOwnerModeProbe: () => defaultPolicyOwnerModeProbe,
38
+ ensureShareNetwork: () => ensureShareNetwork,
37
39
  ensureShareRoot: () => ensureShareRoot,
38
40
  ownerRootCacaoCid: () => ownerRootCacaoCid,
39
41
  readPolicyOwnerAuthorityMode: () => readPolicyOwnerAuthorityMode,
42
+ shareNetworkIdFor: () => shareNetworkIdFor,
40
43
  signShareKeyDelegation: () => signShareKeyDelegation
41
44
  });
42
45
  module.exports = __toCommonJS(share_root_entry_exports);
@@ -5465,6 +5468,37 @@ function serialize(value) {
5465
5468
  function canonicalize(value) {
5466
5469
  return serialize(value);
5467
5470
  }
5471
+ async function signCompactUcanAuthorization(input) {
5472
+ if (input.notBefore >= input.expiresAt || input.expiresAt - input.notBefore > 60) {
5473
+ throw new TypeError("compact invocation lifetime must be between one and 60 seconds");
5474
+ }
5475
+ const principal = input.issuerDid.split("#", 1)[0];
5476
+ const publicKey = ed25519PublicKeyFromDidKey(principal);
5477
+ const header = {
5478
+ alg: "EdDSA",
5479
+ jwk: { alg: "EdDSA", crv: "Ed25519", kty: "OKP", x: toBase64Url(publicKey) },
5480
+ typ: "JWT",
5481
+ ucv: "0.10.0"
5482
+ };
5483
+ const payload = {
5484
+ att: input.attenuation,
5485
+ aud: input.audienceDid,
5486
+ exp: input.expiresAt,
5487
+ fct: input.facts,
5488
+ iss: input.issuerDid.includes("#") ? input.issuerDid : `${principal}#${principal.slice("did:key:".length)}`,
5489
+ nbf: input.notBefore,
5490
+ nnc: input.nonce,
5491
+ prf: input.proofs
5492
+ };
5493
+ const protectedSegment = toBase64Url(new TextEncoder().encode(canonicalize(header)));
5494
+ const payloadSegment = toBase64Url(new TextEncoder().encode(canonicalize(payload)));
5495
+ const signingInput = new TextEncoder().encode(`${protectedSegment}.${payloadSegment}`);
5496
+ const signature = await input.sign(signingInput);
5497
+ if (signature.length !== 64) throw new TypeError("compact Authorization signature must be Ed25519");
5498
+ return verifyCompactUcanAuthorization(
5499
+ `${protectedSegment}.${payloadSegment}.${toBase64Url(signature)}`
5500
+ );
5501
+ }
5468
5502
  function verifyCompactUcanAuthorization(authorization, expectedCid) {
5469
5503
  if (/\s/.test(authorization)) throw new TypeError("compact Authorization contains whitespace");
5470
5504
  const segments = authorization.split(".");
@@ -6655,44 +6689,57 @@ function parseTime(value, label, rootCid) {
6655
6689
  if (!Number.isFinite(time)) throw new ShareRootError("malformed-root", `share root ${label} is missing or invalid`, { rootCid, mustRevoke: true });
6656
6690
  return time;
6657
6691
  }
6658
- function validateSignedRoot(decoded, expected) {
6692
+ function validateSignedOwnerDelegation(decoded, expected) {
6693
+ const { label } = expected;
6659
6694
  const wider = { rootCid: decoded.cid, mustRevoke: true };
6660
6695
  let issuer;
6661
6696
  try {
6662
6697
  issuer = parseOwnerDid(decoded.issuer);
6663
6698
  } 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);
6699
+ throw new ShareRootError("issuer-mismatch", `${label} is not signed by a did:pkh owner`, wider);
6700
+ }
6701
+ if (!sameOwner(issuer, expected.owner)) throw new ShareRootError("issuer-mismatch", `${label} is signed by another owner`, wider);
6702
+ if (decoded.audience !== expected.keyDid) throw new ShareRootError("audience-mismatch", `${label} is not delegated to its key`, wider);
6703
+ if (decoded.resources.length !== 1 || !decoded.resources[0].startsWith("urn:recap:")) throw new ShareRootError("resource-mismatch", `${label} must carry exactly one ReCap resource`, wider);
6704
+ if (decoded.reCapProofs.length !== 0) throw new ShareRootError("resource-mismatch", `${label} must not have parent capabilities`, wider);
6705
+ const { wanted } = expected;
6706
+ for (const resource of Object.keys(decoded.attenuation)) if (wanted[resource] === void 0) throw new ShareRootError("resource-mismatch", `${label} grants a resource outside its scope`, wider);
6673
6707
  for (const [resource, abilities] of Object.entries(wanted)) {
6674
6708
  const granted = decoded.attenuation[resource];
6675
- if (granted === void 0) throw new ShareRootError("resource-mismatch", "share root is missing one of its resources", wider);
6709
+ if (granted === void 0) throw new ShareRootError("resource-mismatch", `${label} is missing one of its resources`, wider);
6676
6710
  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);
6711
+ if (names.length !== abilities.length || abilities.some((ability) => !names.includes(ability))) throw new ShareRootError("ability-mismatch", `${label} abilities differ from the request`, wider);
6678
6712
  for (const ability of abilities) {
6679
6713
  const caveats = granted[ability];
6680
6714
  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);
6715
+ throw new ShareRootError("ability-mismatch", `${label} abilities carry unexpected caveats`, wider);
6682
6716
  }
6683
6717
  }
6684
6718
  }
6685
6719
  const issuedAt = parseTime(decoded.issuedAt, "issued-at time", decoded.cid);
6686
6720
  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 });
6721
+ 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 });
6722
+ const cap = expected.maxLifetimeMs;
6723
+ 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);
6724
+ if (expiresAt <= expected.now) throw new ShareRootError("expired", `${label} has expired`, { rootCid: decoded.cid });
6691
6725
  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() });
6726
+ throw new ShareRootError("expiry-mismatch", `${label} was signed with a different expiry than requested`, { rootCid: decoded.cid, mustRevoke: expiresAt > expected.requestedExpiresAt.getTime() });
6693
6727
  }
6694
6728
  return { issuedAt: new Date(issuedAt), expiresAt: new Date(expiresAt) };
6695
6729
  }
6730
+ function validateSignedRoot(decoded, expected) {
6731
+ const wanted = { [`${expected.spaceId}/kv/${SHARE_ROOT_PREFIX}`]: KV_ABILITIES };
6732
+ if (expected.scope === "kv+share-decrypt") wanted[shareNetworkId(expected.owner)] = [DECRYPT_ABILITY];
6733
+ return validateSignedOwnerDelegation(decoded, {
6734
+ label: `${expected.kind} root`,
6735
+ owner: expected.owner,
6736
+ keyDid: expected.keyDid,
6737
+ wanted,
6738
+ maxLifetimeMs: PROFILES[expected.kind].maxLifetimeMs,
6739
+ ...expected.requestedExpiresAt === void 0 ? {} : { requestedExpiresAt: expected.requestedExpiresAt },
6740
+ now: expected.now
6741
+ });
6742
+ }
6696
6743
  function assertLinkPath(path) {
6697
6744
  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");
6698
6745
  }
@@ -6727,6 +6774,43 @@ function policyRootContents(input) {
6727
6774
  }
6728
6775
  return { facts, attenuation };
6729
6776
  }
6777
+ function ownerPolicySigner(input) {
6778
+ const { ownerDid, authorityCid, notAfter, gate } = input;
6779
+ return {
6780
+ ownerDid,
6781
+ rootCid: authorityCid,
6782
+ notAfter,
6783
+ createOwnerRoot: async (root) => {
6784
+ const key = await gate();
6785
+ const now = input.now();
6786
+ const notBefore = root.notBefore instanceof Date ? root.notBefore.getTime() : Number.NaN;
6787
+ const expiry = root.expiresAt instanceof Date ? root.expiresAt.getTime() : Number.NaN;
6788
+ 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");
6789
+ if (!Number.isFinite(notBefore) || !Number.isFinite(expiry) || notBefore >= expiry || expiry <= now) throw new ShareRootError("invalid-request", "policy root validity is invalid");
6790
+ 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 });
6791
+ for (const capability of root.capabilities) {
6792
+ if (!input.inScope(capability)) throw new ShareRootError("invalid-request", "policy root capability is outside the owner delegation");
6793
+ }
6794
+ const { facts, attenuation } = policyRootContents(root);
6795
+ const signed = await signShareKeyDelegation({
6796
+ key,
6797
+ audienceDid: root.audienceDid,
6798
+ attenuation,
6799
+ facts: [facts],
6800
+ proofs: [],
6801
+ notBefore: seconds(notBefore),
6802
+ expiresAt: seconds(expiry),
6803
+ nonce: toBase64Url(crypto.getRandomValues(new Uint8Array(16)))
6804
+ });
6805
+ return { cid: signed.cid, delegationHeader: { Authorization: signed.authorization } };
6806
+ },
6807
+ sign: async (digest) => {
6808
+ const key = await gate();
6809
+ if (!(digest instanceof Uint8Array) || digest.length !== 32) throw new ShareRootError("invalid-request", "policy signer signs 32-byte digests only");
6810
+ return key.sign(digest);
6811
+ }
6812
+ };
6813
+ }
6730
6814
  function readPolicyOwnerAuthorityMode(features) {
6731
6815
  if (!Array.isArray(features) || !features.includes(POLICY_OWNER_AUTHORITY_FEATURES.marker)) return void 0;
6732
6816
  const enforce = features.includes(POLICY_OWNER_AUTHORITY_FEATURES.enforce);
@@ -6949,44 +7033,15 @@ var ShareRoot = class _ShareRoot {
6949
7033
  */
6950
7034
  async policyRootSigner() {
6951
7035
  await this.#assertPolicyGate();
6952
- const gate = () => this.#assertPolicyGate();
6953
7036
  const owner = parseOwnerDid(this.ownerDid);
6954
- const notAfter = new Date(this.expiresAt);
6955
- return {
7037
+ return ownerPolicySigner({
6956
7038
  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
- };
7039
+ authorityCid: this.cid,
7040
+ notAfter: new Date(this.expiresAt),
7041
+ gate: () => this.#assertPolicyGate(),
7042
+ now: () => this.#now(),
7043
+ inScope: (capability) => 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))
7044
+ });
6990
7045
  }
6991
7046
  /** Drop this root's key handle. It does not revoke the root: only the owner does that. */
6992
7047
  clear() {
@@ -7044,6 +7099,274 @@ async function ensureShareRoot(input) {
7044
7099
  }
7045
7100
  return { root, rerooted: true };
7046
7101
  }
7102
+
7103
+ // src/share-grant.ts
7104
+ var ACTIONS = {
7105
+ view: ["tinycloud.kv/get", "tinycloud.kv/metadata"],
7106
+ edit: ["tinycloud.kv/get", "tinycloud.kv/metadata", "tinycloud.kv/put"]
7107
+ };
7108
+ function wantedFor(spaceId, path, access, owner) {
7109
+ return { [`${spaceId}/kv/${path}`]: ACTIONS[access], [shareNetworkId(owner)]: [DECRYPT_ABILITY] };
7110
+ }
7111
+ var ShareGrant = class _ShareGrant {
7112
+ cid;
7113
+ ownerDid;
7114
+ spaceId;
7115
+ keyDid;
7116
+ nodeOrigin;
7117
+ path;
7118
+ access;
7119
+ issuedAt;
7120
+ expiresAt;
7121
+ #authorization;
7122
+ #key;
7123
+ #now;
7124
+ constructor(input) {
7125
+ this.cid = input.cid;
7126
+ this.ownerDid = input.ownerDid;
7127
+ this.spaceId = input.spaceId;
7128
+ this.keyDid = input.key.did;
7129
+ this.nodeOrigin = input.nodeOrigin;
7130
+ this.path = input.path;
7131
+ this.access = input.access;
7132
+ this.issuedAt = input.issuedAt;
7133
+ this.expiresAt = input.expiresAt;
7134
+ this.#authorization = input.authorization;
7135
+ this.#key = input.key;
7136
+ this.#now = input.now;
7137
+ }
7138
+ /**
7139
+ * Ask the owner for one per-share grant (one approval). Path, access,
7140
+ * expiry, space and Node origin are checked before the wallet is asked; the
7141
+ * signed CACAO is decoded and must match exactly, or the key is cleared and
7142
+ * the error carries the imported CID (`mustRevoke` when it may be wider).
7143
+ */
7144
+ static async create(input) {
7145
+ const clock = input.now ?? Date.now;
7146
+ const now = clock();
7147
+ const owner = parseOwnerDid(input.owner.ownerDid);
7148
+ assertSpace(input.spaceId, owner, "share");
7149
+ const nodeOrigin = requireNodeOrigin(input.nodeOrigin);
7150
+ assertLinkPath(input.path);
7151
+ if (input.access !== "view" && input.access !== "edit") throw new ShareRootError("invalid-request", "share grant access must be view or edit");
7152
+ const expiry = input.expiresAt instanceof Date ? input.expiresAt.getTime() : Number.NaN;
7153
+ if (!Number.isFinite(expiry) || expiry <= now) throw new ShareRootError("invalid-request", "share grant expiry must be in the future");
7154
+ if (expiry - now > SHARE_ROOT_LINK_MAX_LIFETIME_MS) throw new ShareRootError("lifetime-exceeds-cap", "share grant lifetime exceeds 30 days");
7155
+ const key = await (input.keyFactory ?? (() => createDelegatedShareKey({ extractable: false })))();
7156
+ try {
7157
+ await assertUsableKey(key);
7158
+ } catch (error) {
7159
+ key.clear();
7160
+ throw error;
7161
+ }
7162
+ const permissions = [
7163
+ { service: "tinycloud.kv", path: input.path, actions: [...ACTIONS[input.access]] },
7164
+ { service: "tinycloud.encryption", path: shareNetworkId(owner), actions: [DECRYPT_ABILITY] }
7165
+ ];
7166
+ let receipt;
7167
+ try {
7168
+ receipt = await input.owner.createOwnerDelegation({ delegateDid: key.did, spaceId: input.spaceId, expiresAt: input.expiresAt, permissions });
7169
+ } catch (error) {
7170
+ key.clear();
7171
+ throw error;
7172
+ }
7173
+ try {
7174
+ const authorization = new Uint8Array(receipt.signedDagCbor);
7175
+ const signedCid = ownerRootCacaoCid(authorization);
7176
+ 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 });
7177
+ let decoded;
7178
+ try {
7179
+ decoded = decodeOwnerRootCacao(authorization);
7180
+ } catch (error) {
7181
+ throw new ShareRootError("malformed-root", errorMessage(error), { rootCid: receipt.delegationCid, mustRevoke: true });
7182
+ }
7183
+ const times = validateSignedOwnerDelegation(decoded, {
7184
+ label: "per-share grant",
7185
+ owner,
7186
+ keyDid: key.did,
7187
+ wanted: wantedFor(input.spaceId, input.path, input.access, owner),
7188
+ maxLifetimeMs: SHARE_ROOT_LINK_MAX_LIFETIME_MS,
7189
+ requestedExpiresAt: input.expiresAt,
7190
+ now: clock()
7191
+ });
7192
+ return new _ShareGrant({ cid: decoded.cid, ownerDid: input.owner.ownerDid, spaceId: input.spaceId, nodeOrigin, path: input.path, access: input.access, ...times, authorization, key, now: clock });
7193
+ } catch (error) {
7194
+ key.clear();
7195
+ if (error instanceof ShareRootError) throw error;
7196
+ throw new ShareRootError("malformed-root", errorMessage(error), { rootCid: receipt.delegationCid, mustRevoke: true });
7197
+ }
7198
+ }
7199
+ /** Rebuild a stored grant with its key (e.g. a CLI 0600 key file); the signed bytes are checked again. */
7200
+ static async restore(input) {
7201
+ const { record } = input;
7202
+ const clock = input.now ?? Date.now;
7203
+ if (record.version !== 1 || record.access !== "view" && record.access !== "edit") throw new ShareRootError("malformed-root", "share grant record is not supported");
7204
+ if (record.keyDid !== input.key.did) throw new ShareRootError("audience-mismatch", "share grant record belongs to another key");
7205
+ const owner = parseOwnerDid(record.ownerDid);
7206
+ assertSpace(record.spaceId, owner, "share");
7207
+ const nodeOrigin = requireNodeOrigin(record.nodeOrigin);
7208
+ assertLinkPath(record.path);
7209
+ await assertUsableKey(input.key);
7210
+ let authorization;
7211
+ let decoded;
7212
+ try {
7213
+ authorization = fromBase64Url(record.authorization);
7214
+ decoded = decodeOwnerRootCacao(authorization, record.cid);
7215
+ } catch (error) {
7216
+ throw new ShareRootError("malformed-root", errorMessage(error), { rootCid: record.cid });
7217
+ }
7218
+ const times = validateSignedOwnerDelegation(decoded, {
7219
+ label: "per-share grant",
7220
+ owner,
7221
+ keyDid: input.key.did,
7222
+ wanted: wantedFor(record.spaceId, record.path, record.access, owner),
7223
+ maxLifetimeMs: SHARE_ROOT_LINK_MAX_LIFETIME_MS,
7224
+ now: clock()
7225
+ });
7226
+ return new _ShareGrant({ cid: decoded.cid, ownerDid: record.ownerDid, spaceId: record.spaceId, nodeOrigin, path: record.path, access: record.access, ...times, authorization, key: input.key, now: clock });
7227
+ }
7228
+ /** The `Authorization` value the Node knows this grant by (base64url CACAO). */
7229
+ get authorization() {
7230
+ return toBase64Url(this.#authorization);
7231
+ }
7232
+ /** The exact KV resource this grant covers. */
7233
+ get resource() {
7234
+ return `${this.spaceId}/kv/${this.path}`;
7235
+ }
7236
+ toRecord() {
7237
+ return { version: 1, cid: this.cid, authorization: this.authorization, spaceId: this.spaceId, ownerDid: this.ownerDid, keyDid: this.keyDid, nodeOrigin: this.nodeOrigin, path: this.path, access: this.access, expiresAt: this.expiresAt.toISOString() };
7238
+ }
7239
+ /**
7240
+ * Policy/v3 owner signing for this one share. Every call checks the key is
7241
+ * held and the grant is live. Policy roots must be owned by the per-share
7242
+ * key, expire no later than the grant, and name only this grant's exact
7243
+ * KV resource (with no action the grant lacks) and decrypt on `:share`.
7244
+ */
7245
+ policySigner() {
7246
+ const owner = parseOwnerDid(this.ownerDid);
7247
+ const actions = ACTIONS[this.access];
7248
+ return ownerPolicySigner({
7249
+ ownerDid: this.keyDid,
7250
+ authorityCid: this.cid,
7251
+ notAfter: new Date(this.expiresAt),
7252
+ gate: async () => this.#activeKey(),
7253
+ now: this.#now,
7254
+ inScope: (capability) => capability.kind === "encryption" ? isShareNetworkOf(capability.resource, owner) && capability.action === DECRYPT_ABILITY : capability.resource === this.resource && capability.selector === "exact" && capability.actions.every((action) => actions.includes(action))
7255
+ });
7256
+ }
7257
+ /** `publishAddressedShare`'s `authority` for this share: the policy signer plus the caller's `registerPolicy`. */
7258
+ publishAuthority(registerPolicy) {
7259
+ return { ...this.policySigner(), registerPolicy };
7260
+ }
7261
+ /** Drop the per-share key. It does not revoke the grant: only the owner does that. */
7262
+ clear() {
7263
+ this.#key?.clear();
7264
+ this.#key = void 0;
7265
+ }
7266
+ #activeKey() {
7267
+ if (this.#key === void 0) throw new ShareRootError("key-cleared", "share grant key has been cleared", { rootCid: this.cid });
7268
+ if (this.#now() >= this.expiresAt.getTime()) throw new ShareRootError("expired", "share grant has expired", { rootCid: this.cid });
7269
+ return this.#key;
7270
+ }
7271
+ };
7272
+
7273
+ // src/share-network.ts
7274
+ var import_sha2563 = require("@noble/hashes/sha256");
7275
+ var NETWORK_CREATE_ABILITY = "tinycloud.encryption/network.create";
7276
+ var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
7277
+ var NETWORK_GRANT_LIFETIME_MS = 10 * 6e4;
7278
+ function shareNetworkIdFor(ownerDid) {
7279
+ parseOwnerDid(ownerDid);
7280
+ return `urn:tinycloud:encryption:${ownerDid}:share`;
7281
+ }
7282
+ function hex(bytes2) {
7283
+ return [...bytes2].map((byte) => byte.toString(16).padStart(2, "0")).join("");
7284
+ }
7285
+ async function existingShareNetwork(nodeOrigin, networkId, ownerDid, fetchFn) {
7286
+ const response = await fetchFn(new URL(`/encryption/networks/${encodeURIComponent(networkId)}`, nodeOrigin), { redirect: "error" });
7287
+ if (response.status === 404) return void 0;
7288
+ if (!response.ok) throw new ShareRootError("invalid-request", `share network lookup failed: HTTP ${response.status}`);
7289
+ const body = await response.json();
7290
+ const descriptor = typeof body === "object" && body !== null && "descriptor" in body ? body.descriptor : body;
7291
+ if (typeof descriptor !== "object" || descriptor === null || !("networkId" in descriptor) || !("ownerDid" in descriptor) || !("state" in descriptor)) {
7292
+ throw new ShareRootError("invalid-request", "share network descriptor is malformed");
7293
+ }
7294
+ const { networkId: foundId, ownerDid: foundOwner, state } = descriptor;
7295
+ const owner = parseOwnerDid(ownerDid);
7296
+ if (typeof foundId !== "string" || !isShareNetworkOf(foundId, owner) || typeof foundOwner !== "string" || !sameOwner(parseOwnerDid(foundOwner), owner)) {
7297
+ throw new ShareRootError("invalid-request", "share network is not owned by this owner");
7298
+ }
7299
+ if (state === "revoked" || state === "failed") throw new ShareRootError("invalid-request", `share network is ${String(state)}`);
7300
+ return foundId;
7301
+ }
7302
+ async function ensureShareNetwork(input) {
7303
+ const clock = input.now ?? Date.now;
7304
+ const fetchFn = input.fetch ?? ((url, init) => globalThis.fetch(url, init));
7305
+ const owner = parseOwnerDid(input.owner.ownerDid);
7306
+ assertSpace(input.spaceId, owner, "share");
7307
+ const nodeOrigin = requireNodeOrigin(input.nodeOrigin);
7308
+ const networkId = shareNetworkIdFor(input.owner.ownerDid);
7309
+ const existing = await existingShareNetwork(nodeOrigin, networkId, input.owner.ownerDid, fetchFn);
7310
+ if (existing !== void 0) return { networkId: existing, approvals: 0 };
7311
+ const info = await (await fetchFn(new URL("/info", nodeOrigin), { redirect: "error" })).json();
7312
+ const nodeDid = typeof info === "object" && info !== null && "nodeId" in info ? info.nodeId : void 0;
7313
+ if (typeof nodeDid !== "string" || !nodeDid.startsWith("did:key:")) throw new ShareRootError("invalid-request", "node identity is unavailable");
7314
+ const key = await (input.keyFactory ?? (() => createDelegatedShareKey({ extractable: false })))();
7315
+ try {
7316
+ await assertUsableKey(key);
7317
+ const expiresAt = new Date(clock() + NETWORK_GRANT_LIFETIME_MS);
7318
+ const receipt = await input.owner.createOwnerDelegation({
7319
+ delegateDid: key.did,
7320
+ spaceId: input.spaceId,
7321
+ expiresAt,
7322
+ permissions: [{ service: "tinycloud.encryption", path: networkId, actions: [NETWORK_CREATE_ABILITY] }]
7323
+ });
7324
+ const authorization = new Uint8Array(receipt.signedDagCbor);
7325
+ const signedCid = ownerRootCacaoCid(authorization);
7326
+ if (signedCid !== receipt.delegationCid) throw new ShareRootError("malformed-root", "signed network grant bytes do not match the imported delegation CID", { rootCid: receipt.delegationCid, signedCid, mustRevoke: true });
7327
+ let decoded;
7328
+ try {
7329
+ decoded = decodeOwnerRootCacao(authorization);
7330
+ } catch (error) {
7331
+ throw new ShareRootError("malformed-root", errorMessage(error), { rootCid: receipt.delegationCid, mustRevoke: true });
7332
+ }
7333
+ validateSignedOwnerDelegation(decoded, {
7334
+ label: "share network grant",
7335
+ owner,
7336
+ keyDid: key.did,
7337
+ wanted: { [networkId]: [NETWORK_CREATE_ABILITY] },
7338
+ maxLifetimeMs: NETWORK_GRANT_LIFETIME_MS,
7339
+ requestedExpiresAt: expiresAt,
7340
+ now: clock()
7341
+ });
7342
+ const body = { name: "share", ownerDid: input.owner.ownerDid, threshold: { n: 1, t: 1 } };
7343
+ const canonicalBody = canonicalize(body);
7344
+ const now = seconds(clock());
7345
+ const invocation = await signCompactUcanAuthorization({
7346
+ issuerDid: key.did,
7347
+ audienceDid: nodeDid,
7348
+ attenuation: { [networkId]: { [NETWORK_CREATE_ABILITY]: [{}] } },
7349
+ facts: [{ type: NETWORK_ADMIN_TYPE, targetNode: nodeDid, networkId, bodyHash: hex((0, import_sha2563.sha256)(new TextEncoder().encode(canonicalBody))), action: NETWORK_CREATE_ABILITY }],
7350
+ proofs: [decoded.cid],
7351
+ notBefore: now,
7352
+ expiresAt: now + 60,
7353
+ nonce: toBase64Url(crypto.getRandomValues(new Uint8Array(16))),
7354
+ sign: (bytes2) => key.sign(bytes2)
7355
+ });
7356
+ const response = await fetchFn(new URL("/encryption/networks", nodeOrigin), {
7357
+ method: "POST",
7358
+ redirect: "error",
7359
+ headers: { Authorization: invocation.authorization, "content-type": "application/json" },
7360
+ body: canonicalBody
7361
+ });
7362
+ if (!response.ok && response.status !== 409) throw new ShareRootError("invalid-request", `share network creation failed: HTTP ${response.status} ${(await response.text()).slice(0, 120)}`);
7363
+ const created = await existingShareNetwork(nodeOrigin, networkId, input.owner.ownerDid, fetchFn);
7364
+ if (created === void 0) throw new ShareRootError("invalid-request", "share network was not created");
7365
+ return { networkId: created, approvals: 1 };
7366
+ } finally {
7367
+ key.clear();
7368
+ }
7369
+ }
7047
7370
  // Annotate the CommonJS export names for ESM import in node:
7048
7371
  0 && (module.exports = {
7049
7372
  AGENT_SHARE_ROOT_MAX_LIFETIME_MS,
@@ -7054,15 +7377,18 @@ async function ensureShareRoot(input) {
7054
7377
  SHARE_ROOT_MAX_LIFETIME_MS,
7055
7378
  SHARE_ROOT_PREFIX,
7056
7379
  SHARE_ROOT_REQUEST_MARGIN_MS,
7380
+ ShareGrant,
7057
7381
  ShareRoot,
7058
7382
  ShareRootError,
7059
7383
  assertNonExtractableShareKey,
7060
7384
  createPolicyOwnerModeProbe,
7061
7385
  decodeOwnerRootCacao,
7062
7386
  defaultPolicyOwnerModeProbe,
7387
+ ensureShareNetwork,
7063
7388
  ensureShareRoot,
7064
7389
  ownerRootCacaoCid,
7065
7390
  readPolicyOwnerAuthorityMode,
7391
+ shareNetworkIdFor,
7066
7392
  signShareKeyDelegation
7067
7393
  });
7068
7394
  //# sourceMappingURL=share-root.cjs.map