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