@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.
@@ -5428,6 +5428,37 @@ function serialize(value) {
5428
5428
  function canonicalize(value) {
5429
5429
  return serialize(value);
5430
5430
  }
5431
+ async function signCompactUcanAuthorization(input) {
5432
+ if (input.notBefore >= input.expiresAt || input.expiresAt - input.notBefore > 60) {
5433
+ throw new TypeError("compact invocation lifetime must be between one and 60 seconds");
5434
+ }
5435
+ const principal = input.issuerDid.split("#", 1)[0];
5436
+ const publicKey = ed25519PublicKeyFromDidKey(principal);
5437
+ const header = {
5438
+ alg: "EdDSA",
5439
+ jwk: { alg: "EdDSA", crv: "Ed25519", kty: "OKP", x: toBase64Url(publicKey) },
5440
+ typ: "JWT",
5441
+ ucv: "0.10.0"
5442
+ };
5443
+ const payload = {
5444
+ att: input.attenuation,
5445
+ aud: input.audienceDid,
5446
+ exp: input.expiresAt,
5447
+ fct: input.facts,
5448
+ iss: input.issuerDid.includes("#") ? input.issuerDid : `${principal}#${principal.slice("did:key:".length)}`,
5449
+ nbf: input.notBefore,
5450
+ nnc: input.nonce,
5451
+ prf: input.proofs
5452
+ };
5453
+ const protectedSegment = toBase64Url(new TextEncoder().encode(canonicalize(header)));
5454
+ const payloadSegment = toBase64Url(new TextEncoder().encode(canonicalize(payload)));
5455
+ const signingInput = new TextEncoder().encode(`${protectedSegment}.${payloadSegment}`);
5456
+ const signature = await input.sign(signingInput);
5457
+ if (signature.length !== 64) throw new TypeError("compact Authorization signature must be Ed25519");
5458
+ return verifyCompactUcanAuthorization(
5459
+ `${protectedSegment}.${payloadSegment}.${toBase64Url(signature)}`
5460
+ );
5461
+ }
5431
5462
  function verifyCompactUcanAuthorization(authorization, expectedCid) {
5432
5463
  if (/\s/.test(authorization)) throw new TypeError("compact Authorization contains whitespace");
5433
5464
  const segments = authorization.split(".");
@@ -6618,44 +6649,57 @@ function parseTime(value, label, rootCid) {
6618
6649
  if (!Number.isFinite(time)) throw new ShareRootError("malformed-root", `share root ${label} is missing or invalid`, { rootCid, mustRevoke: true });
6619
6650
  return time;
6620
6651
  }
6621
- function validateSignedRoot(decoded, expected) {
6652
+ function validateSignedOwnerDelegation(decoded, expected) {
6653
+ const { label } = expected;
6622
6654
  const wider = { rootCid: decoded.cid, mustRevoke: true };
6623
6655
  let issuer;
6624
6656
  try {
6625
6657
  issuer = parseOwnerDid(decoded.issuer);
6626
6658
  } catch {
6627
- throw new ShareRootError("issuer-mismatch", "share root is not signed by a did:pkh owner", wider);
6628
- }
6629
- if (!sameOwner(issuer, expected.owner)) throw new ShareRootError("issuer-mismatch", "share root is signed by another owner", wider);
6630
- if (decoded.audience !== expected.keyDid) throw new ShareRootError("audience-mismatch", "share root is not delegated to the share key", wider);
6631
- 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);
6632
- if (decoded.reCapProofs.length !== 0) throw new ShareRootError("resource-mismatch", "share root must not have parent capabilities", wider);
6633
- const wanted = { [`${expected.spaceId}/kv/${SHARE_ROOT_PREFIX}`]: KV_ABILITIES };
6634
- if (expected.scope === "kv+share-decrypt") wanted[shareNetworkId(expected.owner)] = [DECRYPT_ABILITY];
6635
- 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);
6659
+ throw new ShareRootError("issuer-mismatch", `${label} is not signed by a did:pkh owner`, wider);
6660
+ }
6661
+ if (!sameOwner(issuer, expected.owner)) throw new ShareRootError("issuer-mismatch", `${label} is signed by another owner`, wider);
6662
+ if (decoded.audience !== expected.keyDid) throw new ShareRootError("audience-mismatch", `${label} is not delegated to its key`, wider);
6663
+ if (decoded.resources.length !== 1 || !decoded.resources[0].startsWith("urn:recap:")) throw new ShareRootError("resource-mismatch", `${label} must carry exactly one ReCap resource`, wider);
6664
+ if (decoded.reCapProofs.length !== 0) throw new ShareRootError("resource-mismatch", `${label} must not have parent capabilities`, wider);
6665
+ const { wanted } = expected;
6666
+ 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);
6636
6667
  for (const [resource, abilities] of Object.entries(wanted)) {
6637
6668
  const granted = decoded.attenuation[resource];
6638
- if (granted === void 0) throw new ShareRootError("resource-mismatch", "share root is missing one of its resources", wider);
6669
+ if (granted === void 0) throw new ShareRootError("resource-mismatch", `${label} is missing one of its resources`, wider);
6639
6670
  const names = Object.keys(granted);
6640
- if (names.length !== abilities.length || abilities.some((ability) => !names.includes(ability))) throw new ShareRootError("ability-mismatch", "share root abilities differ from the request", wider);
6671
+ if (names.length !== abilities.length || abilities.some((ability) => !names.includes(ability))) throw new ShareRootError("ability-mismatch", `${label} abilities differ from the request`, wider);
6641
6672
  for (const ability of abilities) {
6642
6673
  const caveats = granted[ability];
6643
6674
  if (caveats.length !== 1 || typeof caveats[0] !== "object" || caveats[0] === null || Array.isArray(caveats[0]) || Object.keys(caveats[0]).length !== 0) {
6644
- throw new ShareRootError("ability-mismatch", "share root abilities carry unexpected caveats", wider);
6675
+ throw new ShareRootError("ability-mismatch", `${label} abilities carry unexpected caveats`, wider);
6645
6676
  }
6646
6677
  }
6647
6678
  }
6648
6679
  const issuedAt = parseTime(decoded.issuedAt, "issued-at time", decoded.cid);
6649
6680
  const expiresAt = parseTime(decoded.expiresAt, "expiration time", decoded.cid);
6650
- 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 });
6651
- const cap = PROFILES[expected.kind].maxLifetimeMs;
6652
- 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);
6653
- if (expiresAt <= expected.now) throw new ShareRootError("expired", "share root has expired", { rootCid: decoded.cid });
6681
+ 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 });
6682
+ const cap = expected.maxLifetimeMs;
6683
+ 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);
6684
+ if (expiresAt <= expected.now) throw new ShareRootError("expired", `${label} has expired`, { rootCid: decoded.cid });
6654
6685
  if (expected.requestedExpiresAt !== void 0 && seconds(expiresAt) !== seconds(expected.requestedExpiresAt.getTime())) {
6655
- throw new ShareRootError("expiry-mismatch", "share root was signed with a different expiry than requested", { rootCid: decoded.cid, mustRevoke: expiresAt > expected.requestedExpiresAt.getTime() });
6686
+ throw new ShareRootError("expiry-mismatch", `${label} was signed with a different expiry than requested`, { rootCid: decoded.cid, mustRevoke: expiresAt > expected.requestedExpiresAt.getTime() });
6656
6687
  }
6657
6688
  return { issuedAt: new Date(issuedAt), expiresAt: new Date(expiresAt) };
6658
6689
  }
6690
+ function validateSignedRoot(decoded, expected) {
6691
+ const wanted = { [`${expected.spaceId}/kv/${SHARE_ROOT_PREFIX}`]: KV_ABILITIES };
6692
+ if (expected.scope === "kv+share-decrypt") wanted[shareNetworkId(expected.owner)] = [DECRYPT_ABILITY];
6693
+ return validateSignedOwnerDelegation(decoded, {
6694
+ label: `${expected.kind} root`,
6695
+ owner: expected.owner,
6696
+ keyDid: expected.keyDid,
6697
+ wanted,
6698
+ maxLifetimeMs: PROFILES[expected.kind].maxLifetimeMs,
6699
+ ...expected.requestedExpiresAt === void 0 ? {} : { requestedExpiresAt: expected.requestedExpiresAt },
6700
+ now: expected.now
6701
+ });
6702
+ }
6659
6703
  function assertLinkPath(path) {
6660
6704
  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");
6661
6705
  }
@@ -6690,6 +6734,43 @@ function policyRootContents(input) {
6690
6734
  }
6691
6735
  return { facts, attenuation };
6692
6736
  }
6737
+ function ownerPolicySigner(input) {
6738
+ const { ownerDid, authorityCid, notAfter, gate } = input;
6739
+ return {
6740
+ ownerDid,
6741
+ rootCid: authorityCid,
6742
+ notAfter,
6743
+ createOwnerRoot: async (root) => {
6744
+ const key = await gate();
6745
+ const now = input.now();
6746
+ const notBefore = root.notBefore instanceof Date ? root.notBefore.getTime() : Number.NaN;
6747
+ const expiry = root.expiresAt instanceof Date ? root.expiresAt.getTime() : Number.NaN;
6748
+ 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");
6749
+ if (!Number.isFinite(notBefore) || !Number.isFinite(expiry) || notBefore >= expiry || expiry <= now) throw new ShareRootError("invalid-request", "policy root validity is invalid");
6750
+ 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 });
6751
+ for (const capability of root.capabilities) {
6752
+ if (!input.inScope(capability)) throw new ShareRootError("invalid-request", "policy root capability is outside the owner delegation");
6753
+ }
6754
+ const { facts, attenuation } = policyRootContents(root);
6755
+ const signed = await signShareKeyDelegation({
6756
+ key,
6757
+ audienceDid: root.audienceDid,
6758
+ attenuation,
6759
+ facts: [facts],
6760
+ proofs: [],
6761
+ notBefore: seconds(notBefore),
6762
+ expiresAt: seconds(expiry),
6763
+ nonce: toBase64Url(crypto.getRandomValues(new Uint8Array(16)))
6764
+ });
6765
+ return { cid: signed.cid, delegationHeader: { Authorization: signed.authorization } };
6766
+ },
6767
+ sign: async (digest) => {
6768
+ const key = await gate();
6769
+ if (!(digest instanceof Uint8Array) || digest.length !== 32) throw new ShareRootError("invalid-request", "policy signer signs 32-byte digests only");
6770
+ return key.sign(digest);
6771
+ }
6772
+ };
6773
+ }
6693
6774
  function readPolicyOwnerAuthorityMode(features) {
6694
6775
  if (!Array.isArray(features) || !features.includes(POLICY_OWNER_AUTHORITY_FEATURES.marker)) return void 0;
6695
6776
  const enforce = features.includes(POLICY_OWNER_AUTHORITY_FEATURES.enforce);
@@ -6912,44 +6993,15 @@ var ShareRoot = class _ShareRoot {
6912
6993
  */
6913
6994
  async policyRootSigner() {
6914
6995
  await this.#assertPolicyGate();
6915
- const gate = () => this.#assertPolicyGate();
6916
6996
  const owner = parseOwnerDid(this.ownerDid);
6917
- const notAfter = new Date(this.expiresAt);
6918
- return {
6997
+ return ownerPolicySigner({
6919
6998
  ownerDid: this.keyDid,
6920
- rootCid: this.cid,
6921
- notAfter,
6922
- createOwnerRoot: async (input) => {
6923
- const key = await gate();
6924
- const now = this.#now();
6925
- const notBefore = input.notBefore instanceof Date ? input.notBefore.getTime() : Number.NaN;
6926
- const expiry = input.expiresAt instanceof Date ? input.expiresAt.getTime() : Number.NaN;
6927
- 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");
6928
- if (!Number.isFinite(notBefore) || !Number.isFinite(expiry) || notBefore >= expiry || expiry <= now) throw new ShareRootError("invalid-request", "policy root validity is invalid");
6929
- 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 });
6930
- for (const capability of input.capabilities) {
6931
- 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));
6932
- if (!inScope) throw new ShareRootError("invalid-request", "policy root capability is outside the share root");
6933
- }
6934
- const { facts, attenuation } = policyRootContents(input);
6935
- const root = await signShareKeyDelegation({
6936
- key,
6937
- audienceDid: input.audienceDid,
6938
- attenuation,
6939
- facts: [facts],
6940
- proofs: [],
6941
- notBefore: seconds(notBefore),
6942
- expiresAt: seconds(expiry),
6943
- nonce: toBase64Url(crypto.getRandomValues(new Uint8Array(16)))
6944
- });
6945
- return { cid: root.cid, delegationHeader: { Authorization: root.authorization } };
6946
- },
6947
- sign: async (digest) => {
6948
- const key = await gate();
6949
- if (!(digest instanceof Uint8Array) || digest.length !== 32) throw new ShareRootError("invalid-request", "policy signer signs 32-byte digests only");
6950
- return key.sign(digest);
6951
- }
6952
- };
6999
+ authorityCid: this.cid,
7000
+ notAfter: new Date(this.expiresAt),
7001
+ gate: () => this.#assertPolicyGate(),
7002
+ now: () => this.#now(),
7003
+ 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))
7004
+ });
6953
7005
  }
6954
7006
  /** Drop this root's key handle. It does not revoke the root: only the owner does that. */
6955
7007
  clear() {
@@ -7007,6 +7059,274 @@ async function ensureShareRoot(input) {
7007
7059
  }
7008
7060
  return { root, rerooted: true };
7009
7061
  }
7062
+
7063
+ // src/share-grant.ts
7064
+ var ACTIONS = {
7065
+ view: ["tinycloud.kv/get", "tinycloud.kv/metadata"],
7066
+ edit: ["tinycloud.kv/get", "tinycloud.kv/metadata", "tinycloud.kv/put"]
7067
+ };
7068
+ function wantedFor(spaceId, path, access, owner) {
7069
+ return { [`${spaceId}/kv/${path}`]: ACTIONS[access], [shareNetworkId(owner)]: [DECRYPT_ABILITY] };
7070
+ }
7071
+ var ShareGrant = class _ShareGrant {
7072
+ cid;
7073
+ ownerDid;
7074
+ spaceId;
7075
+ keyDid;
7076
+ nodeOrigin;
7077
+ path;
7078
+ access;
7079
+ issuedAt;
7080
+ expiresAt;
7081
+ #authorization;
7082
+ #key;
7083
+ #now;
7084
+ constructor(input) {
7085
+ this.cid = input.cid;
7086
+ this.ownerDid = input.ownerDid;
7087
+ this.spaceId = input.spaceId;
7088
+ this.keyDid = input.key.did;
7089
+ this.nodeOrigin = input.nodeOrigin;
7090
+ this.path = input.path;
7091
+ this.access = input.access;
7092
+ this.issuedAt = input.issuedAt;
7093
+ this.expiresAt = input.expiresAt;
7094
+ this.#authorization = input.authorization;
7095
+ this.#key = input.key;
7096
+ this.#now = input.now;
7097
+ }
7098
+ /**
7099
+ * Ask the owner for one per-share grant (one approval). Path, access,
7100
+ * expiry, space and Node origin are checked before the wallet is asked; the
7101
+ * signed CACAO is decoded and must match exactly, or the key is cleared and
7102
+ * the error carries the imported CID (`mustRevoke` when it may be wider).
7103
+ */
7104
+ static async create(input) {
7105
+ const clock = input.now ?? Date.now;
7106
+ const now = clock();
7107
+ const owner = parseOwnerDid(input.owner.ownerDid);
7108
+ assertSpace(input.spaceId, owner, "share");
7109
+ const nodeOrigin = requireNodeOrigin(input.nodeOrigin);
7110
+ assertLinkPath(input.path);
7111
+ if (input.access !== "view" && input.access !== "edit") throw new ShareRootError("invalid-request", "share grant access must be view or edit");
7112
+ const expiry = input.expiresAt instanceof Date ? input.expiresAt.getTime() : Number.NaN;
7113
+ if (!Number.isFinite(expiry) || expiry <= now) throw new ShareRootError("invalid-request", "share grant expiry must be in the future");
7114
+ if (expiry - now > SHARE_ROOT_LINK_MAX_LIFETIME_MS) throw new ShareRootError("lifetime-exceeds-cap", "share grant lifetime exceeds 30 days");
7115
+ const key = await (input.keyFactory ?? (() => createDelegatedShareKey({ extractable: false })))();
7116
+ try {
7117
+ await assertUsableKey(key);
7118
+ } catch (error) {
7119
+ key.clear();
7120
+ throw error;
7121
+ }
7122
+ const permissions = [
7123
+ { service: "tinycloud.kv", path: input.path, actions: [...ACTIONS[input.access]] },
7124
+ { service: "tinycloud.encryption", path: shareNetworkId(owner), actions: [DECRYPT_ABILITY] }
7125
+ ];
7126
+ let receipt;
7127
+ try {
7128
+ receipt = await input.owner.createOwnerDelegation({ delegateDid: key.did, spaceId: input.spaceId, expiresAt: input.expiresAt, permissions });
7129
+ } catch (error) {
7130
+ key.clear();
7131
+ throw error;
7132
+ }
7133
+ try {
7134
+ const authorization = new Uint8Array(receipt.signedDagCbor);
7135
+ const signedCid = ownerRootCacaoCid(authorization);
7136
+ 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 });
7137
+ let decoded;
7138
+ try {
7139
+ decoded = decodeOwnerRootCacao(authorization);
7140
+ } catch (error) {
7141
+ throw new ShareRootError("malformed-root", errorMessage(error), { rootCid: receipt.delegationCid, mustRevoke: true });
7142
+ }
7143
+ const times = validateSignedOwnerDelegation(decoded, {
7144
+ label: "per-share grant",
7145
+ owner,
7146
+ keyDid: key.did,
7147
+ wanted: wantedFor(input.spaceId, input.path, input.access, owner),
7148
+ maxLifetimeMs: SHARE_ROOT_LINK_MAX_LIFETIME_MS,
7149
+ requestedExpiresAt: input.expiresAt,
7150
+ now: clock()
7151
+ });
7152
+ 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 });
7153
+ } catch (error) {
7154
+ key.clear();
7155
+ if (error instanceof ShareRootError) throw error;
7156
+ throw new ShareRootError("malformed-root", errorMessage(error), { rootCid: receipt.delegationCid, mustRevoke: true });
7157
+ }
7158
+ }
7159
+ /** Rebuild a stored grant with its key (e.g. a CLI 0600 key file); the signed bytes are checked again. */
7160
+ static async restore(input) {
7161
+ const { record } = input;
7162
+ const clock = input.now ?? Date.now;
7163
+ if (record.version !== 1 || record.access !== "view" && record.access !== "edit") throw new ShareRootError("malformed-root", "share grant record is not supported");
7164
+ if (record.keyDid !== input.key.did) throw new ShareRootError("audience-mismatch", "share grant record belongs to another key");
7165
+ const owner = parseOwnerDid(record.ownerDid);
7166
+ assertSpace(record.spaceId, owner, "share");
7167
+ const nodeOrigin = requireNodeOrigin(record.nodeOrigin);
7168
+ assertLinkPath(record.path);
7169
+ await assertUsableKey(input.key);
7170
+ let authorization;
7171
+ let decoded;
7172
+ try {
7173
+ authorization = fromBase64Url(record.authorization);
7174
+ decoded = decodeOwnerRootCacao(authorization, record.cid);
7175
+ } catch (error) {
7176
+ throw new ShareRootError("malformed-root", errorMessage(error), { rootCid: record.cid });
7177
+ }
7178
+ const times = validateSignedOwnerDelegation(decoded, {
7179
+ label: "per-share grant",
7180
+ owner,
7181
+ keyDid: input.key.did,
7182
+ wanted: wantedFor(record.spaceId, record.path, record.access, owner),
7183
+ maxLifetimeMs: SHARE_ROOT_LINK_MAX_LIFETIME_MS,
7184
+ now: clock()
7185
+ });
7186
+ 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 });
7187
+ }
7188
+ /** The `Authorization` value the Node knows this grant by (base64url CACAO). */
7189
+ get authorization() {
7190
+ return toBase64Url(this.#authorization);
7191
+ }
7192
+ /** The exact KV resource this grant covers. */
7193
+ get resource() {
7194
+ return `${this.spaceId}/kv/${this.path}`;
7195
+ }
7196
+ toRecord() {
7197
+ 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() };
7198
+ }
7199
+ /**
7200
+ * Policy/v3 owner signing for this one share. Every call checks the key is
7201
+ * held and the grant is live. Policy roots must be owned by the per-share
7202
+ * key, expire no later than the grant, and name only this grant's exact
7203
+ * KV resource (with no action the grant lacks) and decrypt on `:share`.
7204
+ */
7205
+ policySigner() {
7206
+ const owner = parseOwnerDid(this.ownerDid);
7207
+ const actions = ACTIONS[this.access];
7208
+ return ownerPolicySigner({
7209
+ ownerDid: this.keyDid,
7210
+ authorityCid: this.cid,
7211
+ notAfter: new Date(this.expiresAt),
7212
+ gate: async () => this.#activeKey(),
7213
+ now: this.#now,
7214
+ 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))
7215
+ });
7216
+ }
7217
+ /** `publishAddressedShare`'s `authority` for this share: the policy signer plus the caller's `registerPolicy`. */
7218
+ publishAuthority(registerPolicy) {
7219
+ return { ...this.policySigner(), registerPolicy };
7220
+ }
7221
+ /** Drop the per-share key. It does not revoke the grant: only the owner does that. */
7222
+ clear() {
7223
+ this.#key?.clear();
7224
+ this.#key = void 0;
7225
+ }
7226
+ #activeKey() {
7227
+ if (this.#key === void 0) throw new ShareRootError("key-cleared", "share grant key has been cleared", { rootCid: this.cid });
7228
+ if (this.#now() >= this.expiresAt.getTime()) throw new ShareRootError("expired", "share grant has expired", { rootCid: this.cid });
7229
+ return this.#key;
7230
+ }
7231
+ };
7232
+
7233
+ // src/share-network.ts
7234
+ import { sha256 as sha2564 } from "@noble/hashes/sha256";
7235
+ var NETWORK_CREATE_ABILITY = "tinycloud.encryption/network.create";
7236
+ var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
7237
+ var NETWORK_GRANT_LIFETIME_MS = 10 * 6e4;
7238
+ function shareNetworkIdFor(ownerDid) {
7239
+ parseOwnerDid(ownerDid);
7240
+ return `urn:tinycloud:encryption:${ownerDid}:share`;
7241
+ }
7242
+ function hex(bytes2) {
7243
+ return [...bytes2].map((byte) => byte.toString(16).padStart(2, "0")).join("");
7244
+ }
7245
+ async function existingShareNetwork(nodeOrigin, networkId, ownerDid, fetchFn) {
7246
+ const response = await fetchFn(new URL(`/encryption/networks/${encodeURIComponent(networkId)}`, nodeOrigin), { redirect: "error" });
7247
+ if (response.status === 404) return void 0;
7248
+ if (!response.ok) throw new ShareRootError("invalid-request", `share network lookup failed: HTTP ${response.status}`);
7249
+ const body = await response.json();
7250
+ const descriptor = typeof body === "object" && body !== null && "descriptor" in body ? body.descriptor : body;
7251
+ if (typeof descriptor !== "object" || descriptor === null || !("networkId" in descriptor) || !("ownerDid" in descriptor) || !("state" in descriptor)) {
7252
+ throw new ShareRootError("invalid-request", "share network descriptor is malformed");
7253
+ }
7254
+ const { networkId: foundId, ownerDid: foundOwner, state } = descriptor;
7255
+ const owner = parseOwnerDid(ownerDid);
7256
+ if (typeof foundId !== "string" || !isShareNetworkOf(foundId, owner) || typeof foundOwner !== "string" || !sameOwner(parseOwnerDid(foundOwner), owner)) {
7257
+ throw new ShareRootError("invalid-request", "share network is not owned by this owner");
7258
+ }
7259
+ if (state === "revoked" || state === "failed") throw new ShareRootError("invalid-request", `share network is ${String(state)}`);
7260
+ return foundId;
7261
+ }
7262
+ async function ensureShareNetwork(input) {
7263
+ const clock = input.now ?? Date.now;
7264
+ const fetchFn = input.fetch ?? ((url, init) => globalThis.fetch(url, init));
7265
+ const owner = parseOwnerDid(input.owner.ownerDid);
7266
+ assertSpace(input.spaceId, owner, "share");
7267
+ const nodeOrigin = requireNodeOrigin(input.nodeOrigin);
7268
+ const networkId = shareNetworkIdFor(input.owner.ownerDid);
7269
+ const existing = await existingShareNetwork(nodeOrigin, networkId, input.owner.ownerDid, fetchFn);
7270
+ if (existing !== void 0) return { networkId: existing, approvals: 0 };
7271
+ const info = await (await fetchFn(new URL("/info", nodeOrigin), { redirect: "error" })).json();
7272
+ const nodeDid = typeof info === "object" && info !== null && "nodeId" in info ? info.nodeId : void 0;
7273
+ if (typeof nodeDid !== "string" || !nodeDid.startsWith("did:key:")) throw new ShareRootError("invalid-request", "node identity is unavailable");
7274
+ const key = await (input.keyFactory ?? (() => createDelegatedShareKey({ extractable: false })))();
7275
+ try {
7276
+ await assertUsableKey(key);
7277
+ const expiresAt = new Date(clock() + NETWORK_GRANT_LIFETIME_MS);
7278
+ const receipt = await input.owner.createOwnerDelegation({
7279
+ delegateDid: key.did,
7280
+ spaceId: input.spaceId,
7281
+ expiresAt,
7282
+ permissions: [{ service: "tinycloud.encryption", path: networkId, actions: [NETWORK_CREATE_ABILITY] }]
7283
+ });
7284
+ const authorization = new Uint8Array(receipt.signedDagCbor);
7285
+ const signedCid = ownerRootCacaoCid(authorization);
7286
+ 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 });
7287
+ let decoded;
7288
+ try {
7289
+ decoded = decodeOwnerRootCacao(authorization);
7290
+ } catch (error) {
7291
+ throw new ShareRootError("malformed-root", errorMessage(error), { rootCid: receipt.delegationCid, mustRevoke: true });
7292
+ }
7293
+ validateSignedOwnerDelegation(decoded, {
7294
+ label: "share network grant",
7295
+ owner,
7296
+ keyDid: key.did,
7297
+ wanted: { [networkId]: [NETWORK_CREATE_ABILITY] },
7298
+ maxLifetimeMs: NETWORK_GRANT_LIFETIME_MS,
7299
+ requestedExpiresAt: expiresAt,
7300
+ now: clock()
7301
+ });
7302
+ const body = { name: "share", ownerDid: input.owner.ownerDid, threshold: { n: 1, t: 1 } };
7303
+ const canonicalBody = canonicalize(body);
7304
+ const now = seconds(clock());
7305
+ const invocation = await signCompactUcanAuthorization({
7306
+ issuerDid: key.did,
7307
+ audienceDid: nodeDid,
7308
+ attenuation: { [networkId]: { [NETWORK_CREATE_ABILITY]: [{}] } },
7309
+ facts: [{ type: NETWORK_ADMIN_TYPE, targetNode: nodeDid, networkId, bodyHash: hex(sha2564(new TextEncoder().encode(canonicalBody))), action: NETWORK_CREATE_ABILITY }],
7310
+ proofs: [decoded.cid],
7311
+ notBefore: now,
7312
+ expiresAt: now + 60,
7313
+ nonce: toBase64Url(crypto.getRandomValues(new Uint8Array(16))),
7314
+ sign: (bytes2) => key.sign(bytes2)
7315
+ });
7316
+ const response = await fetchFn(new URL("/encryption/networks", nodeOrigin), {
7317
+ method: "POST",
7318
+ redirect: "error",
7319
+ headers: { Authorization: invocation.authorization, "content-type": "application/json" },
7320
+ body: canonicalBody
7321
+ });
7322
+ if (!response.ok && response.status !== 409) throw new ShareRootError("invalid-request", `share network creation failed: HTTP ${response.status} ${(await response.text()).slice(0, 120)}`);
7323
+ const created = await existingShareNetwork(nodeOrigin, networkId, input.owner.ownerDid, fetchFn);
7324
+ if (created === void 0) throw new ShareRootError("invalid-request", "share network was not created");
7325
+ return { networkId: created, approvals: 1 };
7326
+ } finally {
7327
+ key.clear();
7328
+ }
7329
+ }
7010
7330
  export {
7011
7331
  AGENT_SHARE_ROOT_MAX_LIFETIME_MS,
7012
7332
  POLICY_OWNER_AUTHORITY_FEATURES,
@@ -7016,15 +7336,18 @@ export {
7016
7336
  SHARE_ROOT_MAX_LIFETIME_MS,
7017
7337
  SHARE_ROOT_PREFIX,
7018
7338
  SHARE_ROOT_REQUEST_MARGIN_MS,
7339
+ ShareGrant,
7019
7340
  ShareRoot,
7020
7341
  ShareRootError,
7021
7342
  assertNonExtractableShareKey,
7022
7343
  createPolicyOwnerModeProbe,
7023
7344
  decodeOwnerRootCacao,
7024
7345
  defaultPolicyOwnerModeProbe,
7346
+ ensureShareNetwork,
7025
7347
  ensureShareRoot,
7026
7348
  ownerRootCacaoCid,
7027
7349
  readPolicyOwnerAuthorityMode,
7350
+ shareNetworkIdFor,
7028
7351
  signShareKeyDelegation
7029
7352
  };
7030
7353
  //# sourceMappingURL=share-root.js.map