@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.
- package/dist/share-root.cjs +380 -54
- package/dist/share-root.cjs.map +1 -1
- package/dist/share-root.d.cts +166 -30
- package/dist/share-root.d.ts +166 -30
- package/dist/share-root.js +377 -54
- package/dist/share-root.js.map +1 -1
- package/package.json +1 -1
package/dist/share-root.cjs
CHANGED
|
@@ -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
|
|
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",
|
|
6665
|
-
}
|
|
6666
|
-
if (!sameOwner(issuer, expected.owner)) throw new ShareRootError("issuer-mismatch",
|
|
6667
|
-
if (decoded.audience !== expected.keyDid) throw new ShareRootError("audience-mismatch",
|
|
6668
|
-
if (decoded.resources.length !== 1 || !decoded.resources[0].startsWith("urn:recap:")) throw new ShareRootError("resource-mismatch",
|
|
6669
|
-
if (decoded.reCapProofs.length !== 0) throw new ShareRootError("resource-mismatch",
|
|
6670
|
-
const wanted =
|
|
6671
|
-
|
|
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",
|
|
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",
|
|
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",
|
|
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",
|
|
6688
|
-
const cap =
|
|
6689
|
-
if (expiresAt - issuedAt > cap + SHARE_ROOT_CLOCK_SKEW_MS || expiresAt - expected.now > cap) throw new ShareRootError("lifetime-exceeds-cap", `${
|
|
6690
|
-
if (expiresAt <= expected.now) throw new ShareRootError("expired",
|
|
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",
|
|
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
|
-
|
|
6955
|
-
return {
|
|
7037
|
+
return ownerPolicySigner({
|
|
6956
7038
|
ownerDid: this.keyDid,
|
|
6957
|
-
|
|
6958
|
-
notAfter,
|
|
6959
|
-
|
|
6960
|
-
|
|
6961
|
-
|
|
6962
|
-
|
|
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
|