@tinycloud/node-sdk 3.2.0-beta.2 → 3.2.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{core-DjLtgeVT.d.cts → WasmKeyProvider-DjfRu9aL.d.cts} +162 -2
- package/dist/{core-DjLtgeVT.d.ts → WasmKeyProvider-DjfRu9aL.d.ts} +162 -2
- package/dist/authority-mTQEgN56.d.cts +97 -0
- package/dist/authority-mTQEgN56.d.ts +97 -0
- package/dist/core.cjs +1049 -443
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +13 -3
- package/dist/core.d.ts +13 -3
- package/dist/core.js +725 -115
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +2348 -494
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +34 -4
- package/dist/index.d.ts +34 -4
- package/dist/index.js +1996 -128
- package/dist/index.js.map +1 -1
- package/dist/replication/authority.cjs +278 -0
- package/dist/replication/authority.cjs.map +1 -0
- package/dist/replication/authority.d.cts +3 -0
- package/dist/replication/authority.d.ts +3 -0
- package/dist/replication/authority.js +257 -0
- package/dist/replication/authority.js.map +1 -0
- package/dist/replication/runtime.cjs +224 -0
- package/dist/replication/runtime.cjs.map +1 -0
- package/dist/replication/runtime.d.cts +3 -0
- package/dist/replication/runtime.d.ts +3 -0
- package/dist/replication/runtime.js +189 -0
- package/dist/replication/runtime.js.map +1 -0
- package/dist/runtime-DyYwHW8j.d.cts +64 -0
- package/dist/runtime-DyYwHW8j.d.ts +64 -0
- package/package.json +25 -3
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/replication/authority.ts
|
|
21
|
+
var authority_exports = {};
|
|
22
|
+
__export(authority_exports, {
|
|
23
|
+
AUTHORITY_EXPIRY_MARGIN_MS: () => AUTHORITY_EXPIRY_MARGIN_MS,
|
|
24
|
+
assertValidReplicationConfig: () => assertValidReplicationConfig,
|
|
25
|
+
augmentSignInEntriesWithReplication: () => augmentSignInEntriesWithReplication,
|
|
26
|
+
createReplicationAuthority: () => createReplicationAuthority,
|
|
27
|
+
hasUnrestrictedGetCoverage: () => hasUnrestrictedGetCoverage,
|
|
28
|
+
replicationScopeFromSignedSession: () => replicationScopeFromSignedSession,
|
|
29
|
+
ucanAttCovers: () => ucanAttCovers,
|
|
30
|
+
ucanAttUnconstrainedFor: () => ucanAttUnconstrainedFor
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(authority_exports);
|
|
33
|
+
var import_sdk_core2 = require("@tinycloud/sdk-core");
|
|
34
|
+
var import_replica = require("@tinycloud/replica");
|
|
35
|
+
|
|
36
|
+
// src/replication/config.ts
|
|
37
|
+
var import_sdk_services = require("@tinycloud/sdk-services");
|
|
38
|
+
function assertValidReplicationConfig(replication) {
|
|
39
|
+
if (replication === void 0 || replication.enabled !== true) return;
|
|
40
|
+
if (replication.storage === void 0 || replication.storage === null) {
|
|
41
|
+
throw new TypeError("replication.enabled requires replication.storage");
|
|
42
|
+
}
|
|
43
|
+
if (!Array.isArray(replication.prefixes) || replication.prefixes.length === 0) {
|
|
44
|
+
throw new TypeError("replication.prefixes must be a non-empty array");
|
|
45
|
+
}
|
|
46
|
+
for (const prefix of replication.prefixes) {
|
|
47
|
+
if (typeof prefix !== "string" || prefix === "") {
|
|
48
|
+
throw new TypeError("replication.prefixes must not contain an empty prefix");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (const [index, prefix] of replication.prefixes.entries()) {
|
|
52
|
+
for (const other of replication.prefixes.slice(index + 1)) {
|
|
53
|
+
if ((0, import_sdk_services.kvPrefixCovers)(prefix, other) || (0, import_sdk_services.kvPrefixCovers)(other, prefix)) {
|
|
54
|
+
throw new TypeError(
|
|
55
|
+
`replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (prefix.split("/", 1)[0] === "vault" && replication.allowSecrets !== true) {
|
|
60
|
+
throw new TypeError(
|
|
61
|
+
`replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// src/replication/authority-sign-in.ts
|
|
68
|
+
var import_sdk_core = require("@tinycloud/sdk-core");
|
|
69
|
+
var import_sdk_services2 = require("@tinycloud/sdk-services");
|
|
70
|
+
var KV_GET = import_sdk_core.KV.GET;
|
|
71
|
+
var KV_SYNC = import_sdk_core.KV.SYNC;
|
|
72
|
+
var KV_SERVICE = "tinycloud.kv";
|
|
73
|
+
function hasUnrestrictedGetCoverage(entries, spaceId, prefix) {
|
|
74
|
+
const requiredPaths = (0, import_sdk_core.grantPathsForSelector)(prefix);
|
|
75
|
+
return (0, import_sdk_core.isCapabilitySubset)(
|
|
76
|
+
requiredPaths.map((path) => ({ service: KV_SERVICE, space: spaceId, path, actions: [KV_GET] })),
|
|
77
|
+
[...entries]
|
|
78
|
+
).subset;
|
|
79
|
+
}
|
|
80
|
+
function augmentSignInEntriesWithReplication(input) {
|
|
81
|
+
const out = [];
|
|
82
|
+
const replication = input.replication;
|
|
83
|
+
if (replication === void 0) return out;
|
|
84
|
+
for (const prefix of replication.prefixes) {
|
|
85
|
+
if ((0, import_sdk_services2.requiresSecretsOptIn)(input.primarySpaceId, prefix) && replication.allowSecrets !== true) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, prefix)) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
for (const path of (0, import_sdk_core.grantPathsForSelector)(prefix)) {
|
|
92
|
+
out.push({
|
|
93
|
+
service: KV_SERVICE,
|
|
94
|
+
space: input.primarySpaceId,
|
|
95
|
+
path,
|
|
96
|
+
actions: [KV_GET, KV_SYNC]
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/replication/authority.ts
|
|
104
|
+
var parseUcanGrant = import_replica.parseUcanGrant;
|
|
105
|
+
var KV_GET2 = import_sdk_core2.KV.GET;
|
|
106
|
+
var KV_SYNC2 = import_sdk_core2.KV.SYNC;
|
|
107
|
+
var KV_SERVICE2 = "tinycloud.kv";
|
|
108
|
+
function replicationEntries(spaceId, prefix) {
|
|
109
|
+
return (0, import_sdk_core2.grantPathsForSelector)(prefix).map((path) => ({
|
|
110
|
+
service: KV_SERVICE2,
|
|
111
|
+
space: spaceId,
|
|
112
|
+
path,
|
|
113
|
+
actions: [KV_GET2, KV_SYNC2]
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
function caveatBranchesUnconstrained(value) {
|
|
117
|
+
if (value === void 0 || value === null) return true;
|
|
118
|
+
if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
if (!Array.isArray(value)) return false;
|
|
122
|
+
try {
|
|
123
|
+
return (0, import_sdk_core2.canonicalizeRecapCaveats)(value) === (0, import_sdk_core2.canonicalizeRecapCaveats)(void 0);
|
|
124
|
+
} catch {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
function kvPathInResource(resource, space) {
|
|
129
|
+
for (const base of [`${space}/kv/`, `tinycloud://${space}/kv/`]) {
|
|
130
|
+
if (resource.startsWith(base)) return resource.slice(base.length);
|
|
131
|
+
}
|
|
132
|
+
return void 0;
|
|
133
|
+
}
|
|
134
|
+
function ucanAttCovers(att, space, prefix, ability, options) {
|
|
135
|
+
const requiredPaths = prefix.endsWith("/") ? [prefix] : [prefix, `${prefix}/`];
|
|
136
|
+
return requiredPaths.every((requestedPath) => {
|
|
137
|
+
for (const [resource, abilities] of Object.entries(att)) {
|
|
138
|
+
const path = kvPathInResource(resource, space);
|
|
139
|
+
if (path === void 0) continue;
|
|
140
|
+
for (const [granted, caveats] of Object.entries(abilities)) {
|
|
141
|
+
if (!(0, import_sdk_core2.actionContains)(granted, ability)) continue;
|
|
142
|
+
if (!(0, import_sdk_core2.isCapabilitySubset)(
|
|
143
|
+
[{ service: KV_SERVICE2, space, path: requestedPath, actions: [ability] }],
|
|
144
|
+
[{ service: KV_SERVICE2, space, path, actions: [granted] }]
|
|
145
|
+
).subset) continue;
|
|
146
|
+
if (options?.ignoreCaveats === true || caveatBranchesUnconstrained(caveats)) return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function ucanAttUnconstrainedFor(att, space, prefix) {
|
|
153
|
+
return ucanAttCovers(att, space, prefix, KV_GET2) && ucanAttCovers(att, space, prefix, KV_SYNC2);
|
|
154
|
+
}
|
|
155
|
+
var AUTHORITY_EXPIRY_MARGIN_MS = 6e4;
|
|
156
|
+
function replicationScopeFromSignedSession(input) {
|
|
157
|
+
let grant;
|
|
158
|
+
try {
|
|
159
|
+
grant = parseUcanGrant(input.delegationHeader.Authorization);
|
|
160
|
+
} catch {
|
|
161
|
+
return void 0;
|
|
162
|
+
}
|
|
163
|
+
const bareDid = (value) => value.split("#", 1)[0];
|
|
164
|
+
if (grant.cid !== input.delegationCid || bareDid(grant.audience) !== bareDid(input.verificationMethod)) return void 0;
|
|
165
|
+
for (const resource of Object.keys(grant.att)) {
|
|
166
|
+
const space = resource.match(/^(?:tinycloud:\/\/)?(.+?)\/kv\//)?.[1];
|
|
167
|
+
if (space !== input.persistedSpace) continue;
|
|
168
|
+
const owner = (0, import_sdk_core2.parseSpaceUri)(space);
|
|
169
|
+
if (owner?.owner && owner.address && owner.chainId) return { space, principal: owner.owner };
|
|
170
|
+
}
|
|
171
|
+
return void 0;
|
|
172
|
+
}
|
|
173
|
+
function createReplicationAuthority(host) {
|
|
174
|
+
const planOrRefusal = (entries) => {
|
|
175
|
+
const plan = host.planDelegation(entries);
|
|
176
|
+
if ("refused" in plan) return plan;
|
|
177
|
+
return { path: plan.path, parentCid: plan.parentCid, expiresAt: plan.expiresAt };
|
|
178
|
+
};
|
|
179
|
+
const sessionAudience = (value) => value.split("#", 1)[0];
|
|
180
|
+
const signedSessionGrant = (session) => {
|
|
181
|
+
const grant = parseUcanGrant(session.delegationHeader.Authorization);
|
|
182
|
+
if (grant.cid !== session.delegationCid || sessionAudience(grant.audience) !== sessionAudience(session.verificationMethod)) return void 0;
|
|
183
|
+
return grant;
|
|
184
|
+
};
|
|
185
|
+
const authority = {
|
|
186
|
+
get sessionOnly() {
|
|
187
|
+
const session = host.replicationSession();
|
|
188
|
+
return session !== void 0 && session.siwe === void 0;
|
|
189
|
+
},
|
|
190
|
+
sessionGrant(prefix) {
|
|
191
|
+
const session = host.replicationSession();
|
|
192
|
+
if (session === void 0) return { refused: "SESSION_EXPIRING" };
|
|
193
|
+
const siweExpiry = session.siwe ? host.siweExpiration(session.siwe)?.getTime() : void 0;
|
|
194
|
+
if (siweExpiry !== void 0 && siweExpiry <= Date.now() + AUTHORITY_EXPIRY_MARGIN_MS) {
|
|
195
|
+
return { refused: "SESSION_EXPIRING" };
|
|
196
|
+
}
|
|
197
|
+
let grant;
|
|
198
|
+
try {
|
|
199
|
+
const verified = signedSessionGrant(session);
|
|
200
|
+
if (!verified) return { refused: "NOT_COVERED" };
|
|
201
|
+
grant = verified;
|
|
202
|
+
} catch {
|
|
203
|
+
return { refused: "NOT_COVERED" };
|
|
204
|
+
}
|
|
205
|
+
const tokenExpiry = grant.expiresAt === null ? void 0 : grant.expiresAt * 1e3;
|
|
206
|
+
if (tokenExpiry === void 0 || tokenExpiry <= Date.now() + AUTHORITY_EXPIRY_MARGIN_MS) {
|
|
207
|
+
return { refused: "SESSION_EXPIRING" };
|
|
208
|
+
}
|
|
209
|
+
const covered = ucanAttCovers(grant.att, session.spaceId, prefix, KV_GET2, { ignoreCaveats: true }) && ucanAttCovers(grant.att, session.spaceId, prefix, KV_SYNC2, { ignoreCaveats: true });
|
|
210
|
+
if (!covered) return { refused: "NOT_COVERED" };
|
|
211
|
+
if (!ucanAttUnconstrainedFor(grant.att, session.spaceId, prefix)) {
|
|
212
|
+
return { refused: "CAVEATED_AUTHORITY" };
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
ucan: grant.jwt,
|
|
216
|
+
device: { did: session.verificationMethod, jwk: session.jwk }
|
|
217
|
+
};
|
|
218
|
+
},
|
|
219
|
+
plan(prefix) {
|
|
220
|
+
const session = host.replicationSession();
|
|
221
|
+
if (session === void 0) return { refused: "SESSION_EXPIRING" };
|
|
222
|
+
const sessionGrant = this.sessionGrant(prefix);
|
|
223
|
+
if (!("refused" in sessionGrant)) {
|
|
224
|
+
const signed = signedSessionGrant(session);
|
|
225
|
+
if (!signed || signed.expiresAt === null) return { refused: "NOT_COVERED" };
|
|
226
|
+
return {
|
|
227
|
+
path: "session",
|
|
228
|
+
parentCid: session.delegationCid,
|
|
229
|
+
expiresAt: signed.expiresAt * 1e3
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
if (authority.sessionOnly) return sessionGrant;
|
|
233
|
+
return planOrRefusal(replicationEntries(session.spaceId, prefix));
|
|
234
|
+
},
|
|
235
|
+
async mint(deviceDid, prefix, signal) {
|
|
236
|
+
if (authority.sessionOnly) throw new Error("compact delegate sessions cannot mint device grants");
|
|
237
|
+
const session = host.replicationSession();
|
|
238
|
+
if (session === void 0) throw new Error("replication mint without a session");
|
|
239
|
+
const entries = replicationEntries(session.spaceId, prefix);
|
|
240
|
+
const plan = planOrRefusal(entries);
|
|
241
|
+
if ("refused" in plan) {
|
|
242
|
+
throw Object.assign(
|
|
243
|
+
new Error(`replication authority refused the mint: ${plan.refused}`),
|
|
244
|
+
{ refused: plan.refused }
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
signal.throwIfAborted();
|
|
248
|
+
const minted = await new Promise((resolve, reject) => {
|
|
249
|
+
const onAbort = () => reject(signal.reason ?? new Error("replication mint aborted"));
|
|
250
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
251
|
+
host.mintDelegation(deviceDid, entries).then(
|
|
252
|
+
(result) => {
|
|
253
|
+
signal.removeEventListener("abort", onAbort);
|
|
254
|
+
resolve(result);
|
|
255
|
+
},
|
|
256
|
+
(error) => {
|
|
257
|
+
signal.removeEventListener("abort", onAbort);
|
|
258
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
return { ucan: minted.ucan, parentCid: plan.parentCid, expiresAt: minted.expiresAt };
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
return authority;
|
|
266
|
+
}
|
|
267
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
268
|
+
0 && (module.exports = {
|
|
269
|
+
AUTHORITY_EXPIRY_MARGIN_MS,
|
|
270
|
+
assertValidReplicationConfig,
|
|
271
|
+
augmentSignInEntriesWithReplication,
|
|
272
|
+
createReplicationAuthority,
|
|
273
|
+
hasUnrestrictedGetCoverage,
|
|
274
|
+
replicationScopeFromSignedSession,
|
|
275
|
+
ucanAttCovers,
|
|
276
|
+
ucanAttUnconstrainedFor
|
|
277
|
+
});
|
|
278
|
+
//# sourceMappingURL=authority.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/replication/authority.ts","../../src/replication/config.ts","../../src/replication/authority-sign-in.ts"],"sourcesContent":["/**\n * Replication authority (TC-858, plan v3 §4).\n *\n * `ReplicationAuthority` answers, per configured prefix, whether this node's\n * session can back a local replica: the session UCAN itself (delegate\n * posture), a child UCAN minted through `delegateTo`, or nothing (the\n * controller then reports `grant_missing` and reads from the network).\n *\n * The unrestricted-authority rule is enforced here, before any network work:\n * a prefix covered only by a caveated `get`/`sync` refuses with\n * `CAVEATED_AUTHORITY` — on the session path, the runtime-grant path, and on\n * any reuse of an installed grant (the adapter marks every grant's\n * `unconstrained` flag; the controller enforces it on reuse).\n */\n\nimport {\n grantPathsForSelector,\n actionContains,\n KV,\n canonicalizeRecapCaveats,\n isCapabilitySubset,\n parseSpaceUri,\n type PermissionEntry,\n type TinyCloudSession,\n} from \"@tinycloud/sdk-core\";\nimport {\n type AuthorityRefusal,\n type ReplicaDevice,\n type ReplicationAuthority,\n} from \"@tinycloud/sdk-services\";\nimport type { ReplicaRuntime } from \"./replica-contract\";\n// @ts-ignore -- node-sdk builds before replica declarations; keep the literal import for bundlers.\nimport { parseUcanGrant as replicaParseUcanGrant } from \"@tinycloud/replica\";\nconst parseUcanGrant = replicaParseUcanGrant as ReplicaRuntime[\"parseUcanGrant\"];\nexport { assertValidReplicationConfig } from \"./config\";\nexport { augmentSignInEntriesWithReplication, hasUnrestrictedGetCoverage } from \"./authority-sign-in\";\n\nconst KV_GET = KV.GET;\nconst KV_SYNC = KV.SYNC;\nconst KV_SERVICE = \"tinycloud.kv\";\n\n/** The delegation the replication grant delegates: get + sync on one prefix of the session space. */\nfunction replicationEntries(spaceId: string, prefix: string): PermissionEntry[] {\n return grantPathsForSelector(prefix).map((path) => ({\n service: KV_SERVICE,\n space: spaceId,\n path,\n actions: [KV_GET, KV_SYNC],\n }));\n}\n\n\n\n\n/**\n * The signed UCAN attenuation (`att`: resource → ability → caveat branches).\n * Only array branch sets are known; anything else is treated as constrained —\n * fail closed, never assume unconstrained.\n */\nfunction caveatBranchesUnconstrained(value: unknown): boolean {\n if (value === undefined || value === null) return true;\n if (typeof value === \"object\" && !Array.isArray(value) && Object.keys(value).length === 0) {\n return true;\n }\n if (!Array.isArray(value)) return false;\n try {\n return (\n canonicalizeRecapCaveats(value as Record<string, unknown>[]) ===\n canonicalizeRecapCaveats(undefined)\n );\n } catch {\n return false;\n }\n}\n\n/** The KV path a signed UCAN resource names in `space`, like replica's `kvPathIn`. */\nfunction kvPathInResource(resource: string, space: string): string | undefined {\n for (const base of [`${space}/kv/`, `tinycloud://${space}/kv/`]) {\n if (resource.startsWith(base)) return resource.slice(base.length);\n }\n return undefined;\n}\n\n/**\n * Whether the signed `att` grants `ability` over the selected namespace, with\n * each grant unrestricted (no caveats). A bare selector requires both its\n * exact key and its slash descendants; a trailing-slash selector is one range.\n */\nexport function ucanAttCovers(\n att: Record<string, Record<string, unknown>>,\n space: string,\n prefix: string,\n ability: string,\n options?: { ignoreCaveats?: boolean },\n): boolean {\n const requiredPaths = prefix.endsWith(\"/\") ? [prefix] : [prefix, `${prefix}/`];\n return requiredPaths.every((requestedPath) => {\n for (const [resource, abilities] of Object.entries(att)) {\n const path = kvPathInResource(resource, space);\n if (path === undefined) continue;\n for (const [granted, caveats] of Object.entries(abilities)) {\n if (!actionContains(granted, ability)) continue;\n if (!isCapabilitySubset(\n [{ service: KV_SERVICE, space, path: requestedPath, actions: [ability] }],\n [{ service: KV_SERVICE, space, path, actions: [granted] }],\n ).subset) continue;\n if (options?.ignoreCaveats === true || caveatBranchesUnconstrained(caveats)) return true;\n }\n }\n return false;\n });\n}\n\n/** `unconstrained` for `ReplicaGrantInfo` (§2.1): get AND sync on the prefix carry no caveats. */\nexport function ucanAttUnconstrainedFor(\n att: Record<string, Record<string, unknown>>,\n space: string,\n prefix: string,\n): boolean {\n return (\n ucanAttCovers(att, space, prefix, KV_GET) &&\n ucanAttCovers(att, space, prefix, KV_SYNC)\n );\n}\n\nexport interface DelegationPlanResult {\n path: \"session\" | \"runtime\";\n parentCid: string;\n expiresAt: number;\n}\n\n/**\n * The `TinyCloudNode` internals the authority needs. `planDelegation` is the\n * pure extraction of `delegateTo`'s parent selection (§4.3), so `plan` and\n * `mint` can never disagree about which parent or expiry a delegation gets.\n */\nexport type ReplicationAuthoritySession = Pick<\n TinyCloudSession,\n \"delegationHeader\" | \"delegationCid\" | \"spaceId\" | \"verificationMethod\" | \"jwk\"\n> & Partial<Pick<TinyCloudSession, \"siwe\">>;\n\nexport interface ReplicationAuthorityHost {\n replicationSession(): ReplicationAuthoritySession | undefined;\n /** The SIWE \"Expiration Time\" of the session — the delegateTo delegation cap. */\n siweExpiration(siwe: string): Date | undefined;\n planDelegation(\n entries: PermissionEntry[],\n options?: { expiry?: string | number },\n ): ({ grant?: unknown } & DelegationPlanResult) | AuthorityRefusal;\n mintDelegation(\n deviceDid: string,\n entries: PermissionEntry[],\n ): Promise<{ ucan: string; expiresAt: number }>;\n}\n\n/** Milliseconds of validity a session or parent must have left for authority reuse (delegateTo's margin). */\nexport const AUTHORITY_EXPIRY_MARGIN_MS = 60_000;\n\n\n/** Verify a compact session delegation before using any persisted space metadata. */\nexport function replicationScopeFromSignedSession(input: {\n delegationHeader: { Authorization: string };\n delegationCid: string;\n verificationMethod: string;\n persistedSpace: string;\n}): { space: string; principal: string } | undefined {\n let grant: ReturnType<typeof parseUcanGrant>;\n try {\n grant = parseUcanGrant(input.delegationHeader.Authorization);\n } catch {\n return undefined;\n }\n const bareDid = (value: string): string => value.split(\"#\", 1)[0]!;\n if (\n grant.cid !== input.delegationCid ||\n bareDid(grant.audience) !== bareDid(input.verificationMethod)\n ) return undefined;\n for (const resource of Object.keys(grant.att)) {\n const space = resource.match(/^(?:tinycloud:\\/\\/)?(.+?)\\/kv\\//)?.[1];\n if (space !== input.persistedSpace) continue;\n const owner = parseSpaceUri(space);\n if (owner?.owner && owner.address && owner.chainId) return { space, principal: owner.owner };\n }\n return undefined;\n}\nexport function createReplicationAuthority(host: ReplicationAuthorityHost): ReplicationAuthority {\n const planOrRefusal = (entries: PermissionEntry[]): DelegationPlanResult | AuthorityRefusal => {\n const plan = host.planDelegation(entries);\n if (\"refused\" in plan) return plan;\n return { path: plan.path, parentCid: plan.parentCid, expiresAt: plan.expiresAt };\n };\n const sessionAudience = (value: string): string => value.split(\"#\", 1)[0]!;\n const signedSessionGrant = (session: ReplicationAuthoritySession) => {\n const grant = parseUcanGrant(session.delegationHeader.Authorization);\n if (\n grant.cid !== session.delegationCid ||\n sessionAudience(grant.audience) !== sessionAudience(session.verificationMethod)\n ) return undefined;\n return grant;\n };\n const authority: ReplicationAuthority = {\n get sessionOnly() {\n const session = host.replicationSession();\n return session !== undefined && session.siwe === undefined;\n },\n sessionGrant(prefix: string): { ucan: string; device: ReplicaDevice } | AuthorityRefusal {\n const session = host.replicationSession();\n if (session === undefined) return { refused: \"SESSION_EXPIRING\" };\n const siweExpiry = session.siwe ? host.siweExpiration(session.siwe)?.getTime() : undefined;\n if (siweExpiry !== undefined && siweExpiry <= Date.now() + AUTHORITY_EXPIRY_MARGIN_MS) {\n return { refused: \"SESSION_EXPIRING\" };\n }\n let grant: ReturnType<typeof parseUcanGrant>;\n try {\n const verified = signedSessionGrant(session);\n if (!verified) return { refused: \"NOT_COVERED\" };\n grant = verified;\n } catch {\n return { refused: \"NOT_COVERED\" };\n }\n const tokenExpiry = grant.expiresAt === null ? undefined : grant.expiresAt * 1000;\n if (tokenExpiry === undefined || tokenExpiry <= Date.now() + AUTHORITY_EXPIRY_MARGIN_MS) {\n return { refused: \"SESSION_EXPIRING\" };\n }\n const covered = ucanAttCovers(grant.att, session.spaceId, prefix, KV_GET, { ignoreCaveats: true }) &&\n ucanAttCovers(grant.att, session.spaceId, prefix, KV_SYNC, { ignoreCaveats: true });\n if (!covered) return { refused: \"NOT_COVERED\" };\n if (!ucanAttUnconstrainedFor(grant.att, session.spaceId, prefix)) {\n return { refused: \"CAVEATED_AUTHORITY\" };\n }\n return {\n ucan: grant.jwt,\n device: { did: session.verificationMethod, jwk: session.jwk },\n };\n },\n plan(prefix: string): DelegationPlanResult | AuthorityRefusal {\n const session = host.replicationSession();\n if (session === undefined) return { refused: \"SESSION_EXPIRING\" };\n const sessionGrant = this.sessionGrant(prefix);\n if (!(\"refused\" in sessionGrant)) {\n const signed = signedSessionGrant(session);\n if (!signed || signed.expiresAt === null) return { refused: \"NOT_COVERED\" };\n return {\n path: \"session\",\n parentCid: session.delegationCid,\n expiresAt: signed.expiresAt * 1000,\n };\n }\n if (authority.sessionOnly) return sessionGrant;\n return planOrRefusal(replicationEntries(session.spaceId, prefix));\n },\n async mint(\n deviceDid: string,\n prefix: string,\n signal: AbortSignal,\n ): Promise<{ ucan: string; parentCid: string; expiresAt: number }> {\n if (authority.sessionOnly) throw new Error(\"compact delegate sessions cannot mint device grants\");\n const session = host.replicationSession();\n if (session === undefined) throw new Error(\"replication mint without a session\");\n const entries = replicationEntries(session.spaceId, prefix);\n const plan = planOrRefusal(entries);\n if (\"refused\" in plan) {\n throw Object.assign(\n new Error(`replication authority refused the mint: ${plan.refused}`),\n { refused: plan.refused },\n );\n }\n signal.throwIfAborted();\n const minted = await new Promise<{ ucan: string; expiresAt: number }>((resolve, reject) => {\n const onAbort = (): void => reject(signal.reason ?? new Error(\"replication mint aborted\"));\n signal.addEventListener(\"abort\", onAbort, { once: true });\n host.mintDelegation(deviceDid, entries).then(\n (result) => {\n signal.removeEventListener(\"abort\", onAbort);\n resolve(result);\n },\n (error: unknown) => {\n signal.removeEventListener(\"abort\", onAbort);\n reject(error instanceof Error ? error : new Error(String(error)));\n },\n );\n });\n return { ucan: minted.ucan, parentCid: plan.parentCid, expiresAt: minted.expiresAt };\n },\n };\n return authority;\n}\n","import { kvPrefixCovers, type ReplicationOptions } from \"@tinycloud/sdk-services\";\n\n/** Constructor-time validation; the space-dependent secrets check runs at sign-in and open. */\nexport function assertValidReplicationConfig(\n replication: (ReplicationOptions & { storage?: unknown }) | undefined,\n): void {\n if (replication === undefined || replication.enabled !== true) return;\n if (replication.storage === undefined || replication.storage === null) {\n throw new TypeError(\"replication.enabled requires replication.storage\");\n }\n if (!Array.isArray(replication.prefixes) || replication.prefixes.length === 0) {\n throw new TypeError(\"replication.prefixes must be a non-empty array\");\n }\n for (const prefix of replication.prefixes) {\n if (typeof prefix !== \"string\" || prefix === \"\") {\n throw new TypeError(\"replication.prefixes must not contain an empty prefix\");\n }\n }\n for (const [index, prefix] of replication.prefixes.entries()) {\n for (const other of replication.prefixes.slice(index + 1)) {\n if (kvPrefixCovers(prefix, other) || kvPrefixCovers(other, prefix)) {\n throw new TypeError(\n `replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`,\n );\n }\n }\n if (prefix.split(\"/\", 1)[0] === \"vault\" && replication.allowSecrets !== true) {\n throw new TypeError(\n `replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`,\n );\n }\n }\n}\n","import {\n grantPathsForSelector,\n KV,\n isCapabilitySubset,\n type PermissionEntry,\n type ReplicationOptions,\n} from \"@tinycloud/sdk-core\";\nimport { requiresSecretsOptIn } from \"@tinycloud/sdk-services\";\n\nconst KV_GET = KV.GET;\nconst KV_SYNC = KV.SYNC;\nconst KV_SERVICE = \"tinycloud.kv\";\n\nexport function hasUnrestrictedGetCoverage(\n entries: readonly PermissionEntry[],\n spaceId: string,\n prefix: string,\n): boolean {\n const requiredPaths = grantPathsForSelector(prefix);\n return isCapabilitySubset(\n requiredPaths.map((path) => ({ service: KV_SERVICE, space: spaceId, path, actions: [KV_GET] })),\n [...entries],\n ).subset;\n}\n\nexport function augmentSignInEntriesWithReplication(input: {\n entries: readonly PermissionEntry[];\n primarySpaceId: string;\n replication?: Pick<ReplicationOptions, \"prefixes\" | \"allowSecrets\">;\n}): PermissionEntry[] {\n const out: PermissionEntry[] = [];\n const replication = input.replication;\n if (replication === undefined) return out;\n for (const prefix of replication.prefixes) {\n if (\n requiresSecretsOptIn(input.primarySpaceId, prefix) &&\n replication.allowSecrets !== true\n ) {\n continue;\n }\n if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, prefix)) {\n continue;\n }\n for (const path of grantPathsForSelector(prefix)) {\n out.push({\n service: KV_SERVICE,\n space: input.primarySpaceId,\n path,\n actions: [KV_GET, KV_SYNC],\n });\n }\n }\n return out;\n}\n"],"mappings":"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","names":["import_sdk_core","import_sdk_services","replicaParseUcanGrant","KV_GET","KV_SYNC","KV_SERVICE"]}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import '@tinycloud/sdk-core';
|
|
2
|
+
import '@tinycloud/sdk-services';
|
|
3
|
+
export { A as AUTHORITY_EXPIRY_MARGIN_MS, D as DelegationPlanResult, R as ReplicationAuthorityHost, a as ReplicationAuthoritySession, b as assertValidReplicationConfig, c as augmentSignInEntriesWithReplication, d as createReplicationAuthority, h as hasUnrestrictedGetCoverage, r as replicationScopeFromSignedSession, u as ucanAttCovers, e as ucanAttUnconstrainedFor } from '../authority-mTQEgN56.cjs';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import '@tinycloud/sdk-core';
|
|
2
|
+
import '@tinycloud/sdk-services';
|
|
3
|
+
export { A as AUTHORITY_EXPIRY_MARGIN_MS, D as DelegationPlanResult, R as ReplicationAuthorityHost, a as ReplicationAuthoritySession, b as assertValidReplicationConfig, c as augmentSignInEntriesWithReplication, d as createReplicationAuthority, h as hasUnrestrictedGetCoverage, r as replicationScopeFromSignedSession, u as ucanAttCovers, e as ucanAttUnconstrainedFor } from '../authority-mTQEgN56.js';
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
// src/replication/authority.ts
|
|
2
|
+
import {
|
|
3
|
+
grantPathsForSelector as grantPathsForSelector2,
|
|
4
|
+
actionContains,
|
|
5
|
+
KV as KV2,
|
|
6
|
+
canonicalizeRecapCaveats,
|
|
7
|
+
isCapabilitySubset as isCapabilitySubset2,
|
|
8
|
+
parseSpaceUri
|
|
9
|
+
} from "@tinycloud/sdk-core";
|
|
10
|
+
import { parseUcanGrant as replicaParseUcanGrant } from "@tinycloud/replica";
|
|
11
|
+
|
|
12
|
+
// src/replication/config.ts
|
|
13
|
+
import { kvPrefixCovers } from "@tinycloud/sdk-services";
|
|
14
|
+
function assertValidReplicationConfig(replication) {
|
|
15
|
+
if (replication === void 0 || replication.enabled !== true) return;
|
|
16
|
+
if (replication.storage === void 0 || replication.storage === null) {
|
|
17
|
+
throw new TypeError("replication.enabled requires replication.storage");
|
|
18
|
+
}
|
|
19
|
+
if (!Array.isArray(replication.prefixes) || replication.prefixes.length === 0) {
|
|
20
|
+
throw new TypeError("replication.prefixes must be a non-empty array");
|
|
21
|
+
}
|
|
22
|
+
for (const prefix of replication.prefixes) {
|
|
23
|
+
if (typeof prefix !== "string" || prefix === "") {
|
|
24
|
+
throw new TypeError("replication.prefixes must not contain an empty prefix");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
for (const [index, prefix] of replication.prefixes.entries()) {
|
|
28
|
+
for (const other of replication.prefixes.slice(index + 1)) {
|
|
29
|
+
if (kvPrefixCovers(prefix, other) || kvPrefixCovers(other, prefix)) {
|
|
30
|
+
throw new TypeError(
|
|
31
|
+
`replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (prefix.split("/", 1)[0] === "vault" && replication.allowSecrets !== true) {
|
|
36
|
+
throw new TypeError(
|
|
37
|
+
`replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/replication/authority-sign-in.ts
|
|
44
|
+
import {
|
|
45
|
+
grantPathsForSelector,
|
|
46
|
+
KV,
|
|
47
|
+
isCapabilitySubset
|
|
48
|
+
} from "@tinycloud/sdk-core";
|
|
49
|
+
import { requiresSecretsOptIn } from "@tinycloud/sdk-services";
|
|
50
|
+
var KV_GET = KV.GET;
|
|
51
|
+
var KV_SYNC = KV.SYNC;
|
|
52
|
+
var KV_SERVICE = "tinycloud.kv";
|
|
53
|
+
function hasUnrestrictedGetCoverage(entries, spaceId, prefix) {
|
|
54
|
+
const requiredPaths = grantPathsForSelector(prefix);
|
|
55
|
+
return isCapabilitySubset(
|
|
56
|
+
requiredPaths.map((path) => ({ service: KV_SERVICE, space: spaceId, path, actions: [KV_GET] })),
|
|
57
|
+
[...entries]
|
|
58
|
+
).subset;
|
|
59
|
+
}
|
|
60
|
+
function augmentSignInEntriesWithReplication(input) {
|
|
61
|
+
const out = [];
|
|
62
|
+
const replication = input.replication;
|
|
63
|
+
if (replication === void 0) return out;
|
|
64
|
+
for (const prefix of replication.prefixes) {
|
|
65
|
+
if (requiresSecretsOptIn(input.primarySpaceId, prefix) && replication.allowSecrets !== true) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, prefix)) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
for (const path of grantPathsForSelector(prefix)) {
|
|
72
|
+
out.push({
|
|
73
|
+
service: KV_SERVICE,
|
|
74
|
+
space: input.primarySpaceId,
|
|
75
|
+
path,
|
|
76
|
+
actions: [KV_GET, KV_SYNC]
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/replication/authority.ts
|
|
84
|
+
var parseUcanGrant = replicaParseUcanGrant;
|
|
85
|
+
var KV_GET2 = KV2.GET;
|
|
86
|
+
var KV_SYNC2 = KV2.SYNC;
|
|
87
|
+
var KV_SERVICE2 = "tinycloud.kv";
|
|
88
|
+
function replicationEntries(spaceId, prefix) {
|
|
89
|
+
return grantPathsForSelector2(prefix).map((path) => ({
|
|
90
|
+
service: KV_SERVICE2,
|
|
91
|
+
space: spaceId,
|
|
92
|
+
path,
|
|
93
|
+
actions: [KV_GET2, KV_SYNC2]
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
function caveatBranchesUnconstrained(value) {
|
|
97
|
+
if (value === void 0 || value === null) return true;
|
|
98
|
+
if (typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (!Array.isArray(value)) return false;
|
|
102
|
+
try {
|
|
103
|
+
return canonicalizeRecapCaveats(value) === canonicalizeRecapCaveats(void 0);
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function kvPathInResource(resource, space) {
|
|
109
|
+
for (const base of [`${space}/kv/`, `tinycloud://${space}/kv/`]) {
|
|
110
|
+
if (resource.startsWith(base)) return resource.slice(base.length);
|
|
111
|
+
}
|
|
112
|
+
return void 0;
|
|
113
|
+
}
|
|
114
|
+
function ucanAttCovers(att, space, prefix, ability, options) {
|
|
115
|
+
const requiredPaths = prefix.endsWith("/") ? [prefix] : [prefix, `${prefix}/`];
|
|
116
|
+
return requiredPaths.every((requestedPath) => {
|
|
117
|
+
for (const [resource, abilities] of Object.entries(att)) {
|
|
118
|
+
const path = kvPathInResource(resource, space);
|
|
119
|
+
if (path === void 0) continue;
|
|
120
|
+
for (const [granted, caveats] of Object.entries(abilities)) {
|
|
121
|
+
if (!actionContains(granted, ability)) continue;
|
|
122
|
+
if (!isCapabilitySubset2(
|
|
123
|
+
[{ service: KV_SERVICE2, space, path: requestedPath, actions: [ability] }],
|
|
124
|
+
[{ service: KV_SERVICE2, space, path, actions: [granted] }]
|
|
125
|
+
).subset) continue;
|
|
126
|
+
if (options?.ignoreCaveats === true || caveatBranchesUnconstrained(caveats)) return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function ucanAttUnconstrainedFor(att, space, prefix) {
|
|
133
|
+
return ucanAttCovers(att, space, prefix, KV_GET2) && ucanAttCovers(att, space, prefix, KV_SYNC2);
|
|
134
|
+
}
|
|
135
|
+
var AUTHORITY_EXPIRY_MARGIN_MS = 6e4;
|
|
136
|
+
function replicationScopeFromSignedSession(input) {
|
|
137
|
+
let grant;
|
|
138
|
+
try {
|
|
139
|
+
grant = parseUcanGrant(input.delegationHeader.Authorization);
|
|
140
|
+
} catch {
|
|
141
|
+
return void 0;
|
|
142
|
+
}
|
|
143
|
+
const bareDid = (value) => value.split("#", 1)[0];
|
|
144
|
+
if (grant.cid !== input.delegationCid || bareDid(grant.audience) !== bareDid(input.verificationMethod)) return void 0;
|
|
145
|
+
for (const resource of Object.keys(grant.att)) {
|
|
146
|
+
const space = resource.match(/^(?:tinycloud:\/\/)?(.+?)\/kv\//)?.[1];
|
|
147
|
+
if (space !== input.persistedSpace) continue;
|
|
148
|
+
const owner = parseSpaceUri(space);
|
|
149
|
+
if (owner?.owner && owner.address && owner.chainId) return { space, principal: owner.owner };
|
|
150
|
+
}
|
|
151
|
+
return void 0;
|
|
152
|
+
}
|
|
153
|
+
function createReplicationAuthority(host) {
|
|
154
|
+
const planOrRefusal = (entries) => {
|
|
155
|
+
const plan = host.planDelegation(entries);
|
|
156
|
+
if ("refused" in plan) return plan;
|
|
157
|
+
return { path: plan.path, parentCid: plan.parentCid, expiresAt: plan.expiresAt };
|
|
158
|
+
};
|
|
159
|
+
const sessionAudience = (value) => value.split("#", 1)[0];
|
|
160
|
+
const signedSessionGrant = (session) => {
|
|
161
|
+
const grant = parseUcanGrant(session.delegationHeader.Authorization);
|
|
162
|
+
if (grant.cid !== session.delegationCid || sessionAudience(grant.audience) !== sessionAudience(session.verificationMethod)) return void 0;
|
|
163
|
+
return grant;
|
|
164
|
+
};
|
|
165
|
+
const authority = {
|
|
166
|
+
get sessionOnly() {
|
|
167
|
+
const session = host.replicationSession();
|
|
168
|
+
return session !== void 0 && session.siwe === void 0;
|
|
169
|
+
},
|
|
170
|
+
sessionGrant(prefix) {
|
|
171
|
+
const session = host.replicationSession();
|
|
172
|
+
if (session === void 0) return { refused: "SESSION_EXPIRING" };
|
|
173
|
+
const siweExpiry = session.siwe ? host.siweExpiration(session.siwe)?.getTime() : void 0;
|
|
174
|
+
if (siweExpiry !== void 0 && siweExpiry <= Date.now() + AUTHORITY_EXPIRY_MARGIN_MS) {
|
|
175
|
+
return { refused: "SESSION_EXPIRING" };
|
|
176
|
+
}
|
|
177
|
+
let grant;
|
|
178
|
+
try {
|
|
179
|
+
const verified = signedSessionGrant(session);
|
|
180
|
+
if (!verified) return { refused: "NOT_COVERED" };
|
|
181
|
+
grant = verified;
|
|
182
|
+
} catch {
|
|
183
|
+
return { refused: "NOT_COVERED" };
|
|
184
|
+
}
|
|
185
|
+
const tokenExpiry = grant.expiresAt === null ? void 0 : grant.expiresAt * 1e3;
|
|
186
|
+
if (tokenExpiry === void 0 || tokenExpiry <= Date.now() + AUTHORITY_EXPIRY_MARGIN_MS) {
|
|
187
|
+
return { refused: "SESSION_EXPIRING" };
|
|
188
|
+
}
|
|
189
|
+
const covered = ucanAttCovers(grant.att, session.spaceId, prefix, KV_GET2, { ignoreCaveats: true }) && ucanAttCovers(grant.att, session.spaceId, prefix, KV_SYNC2, { ignoreCaveats: true });
|
|
190
|
+
if (!covered) return { refused: "NOT_COVERED" };
|
|
191
|
+
if (!ucanAttUnconstrainedFor(grant.att, session.spaceId, prefix)) {
|
|
192
|
+
return { refused: "CAVEATED_AUTHORITY" };
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
ucan: grant.jwt,
|
|
196
|
+
device: { did: session.verificationMethod, jwk: session.jwk }
|
|
197
|
+
};
|
|
198
|
+
},
|
|
199
|
+
plan(prefix) {
|
|
200
|
+
const session = host.replicationSession();
|
|
201
|
+
if (session === void 0) return { refused: "SESSION_EXPIRING" };
|
|
202
|
+
const sessionGrant = this.sessionGrant(prefix);
|
|
203
|
+
if (!("refused" in sessionGrant)) {
|
|
204
|
+
const signed = signedSessionGrant(session);
|
|
205
|
+
if (!signed || signed.expiresAt === null) return { refused: "NOT_COVERED" };
|
|
206
|
+
return {
|
|
207
|
+
path: "session",
|
|
208
|
+
parentCid: session.delegationCid,
|
|
209
|
+
expiresAt: signed.expiresAt * 1e3
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (authority.sessionOnly) return sessionGrant;
|
|
213
|
+
return planOrRefusal(replicationEntries(session.spaceId, prefix));
|
|
214
|
+
},
|
|
215
|
+
async mint(deviceDid, prefix, signal) {
|
|
216
|
+
if (authority.sessionOnly) throw new Error("compact delegate sessions cannot mint device grants");
|
|
217
|
+
const session = host.replicationSession();
|
|
218
|
+
if (session === void 0) throw new Error("replication mint without a session");
|
|
219
|
+
const entries = replicationEntries(session.spaceId, prefix);
|
|
220
|
+
const plan = planOrRefusal(entries);
|
|
221
|
+
if ("refused" in plan) {
|
|
222
|
+
throw Object.assign(
|
|
223
|
+
new Error(`replication authority refused the mint: ${plan.refused}`),
|
|
224
|
+
{ refused: plan.refused }
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
signal.throwIfAborted();
|
|
228
|
+
const minted = await new Promise((resolve, reject) => {
|
|
229
|
+
const onAbort = () => reject(signal.reason ?? new Error("replication mint aborted"));
|
|
230
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
231
|
+
host.mintDelegation(deviceDid, entries).then(
|
|
232
|
+
(result) => {
|
|
233
|
+
signal.removeEventListener("abort", onAbort);
|
|
234
|
+
resolve(result);
|
|
235
|
+
},
|
|
236
|
+
(error) => {
|
|
237
|
+
signal.removeEventListener("abort", onAbort);
|
|
238
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
239
|
+
}
|
|
240
|
+
);
|
|
241
|
+
});
|
|
242
|
+
return { ucan: minted.ucan, parentCid: plan.parentCid, expiresAt: minted.expiresAt };
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
return authority;
|
|
246
|
+
}
|
|
247
|
+
export {
|
|
248
|
+
AUTHORITY_EXPIRY_MARGIN_MS,
|
|
249
|
+
assertValidReplicationConfig,
|
|
250
|
+
augmentSignInEntriesWithReplication,
|
|
251
|
+
createReplicationAuthority,
|
|
252
|
+
hasUnrestrictedGetCoverage,
|
|
253
|
+
replicationScopeFromSignedSession,
|
|
254
|
+
ucanAttCovers,
|
|
255
|
+
ucanAttUnconstrainedFor
|
|
256
|
+
};
|
|
257
|
+
//# sourceMappingURL=authority.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/replication/authority.ts","../../src/replication/config.ts","../../src/replication/authority-sign-in.ts"],"sourcesContent":["/**\n * Replication authority (TC-858, plan v3 §4).\n *\n * `ReplicationAuthority` answers, per configured prefix, whether this node's\n * session can back a local replica: the session UCAN itself (delegate\n * posture), a child UCAN minted through `delegateTo`, or nothing (the\n * controller then reports `grant_missing` and reads from the network).\n *\n * The unrestricted-authority rule is enforced here, before any network work:\n * a prefix covered only by a caveated `get`/`sync` refuses with\n * `CAVEATED_AUTHORITY` — on the session path, the runtime-grant path, and on\n * any reuse of an installed grant (the adapter marks every grant's\n * `unconstrained` flag; the controller enforces it on reuse).\n */\n\nimport {\n grantPathsForSelector,\n actionContains,\n KV,\n canonicalizeRecapCaveats,\n isCapabilitySubset,\n parseSpaceUri,\n type PermissionEntry,\n type TinyCloudSession,\n} from \"@tinycloud/sdk-core\";\nimport {\n type AuthorityRefusal,\n type ReplicaDevice,\n type ReplicationAuthority,\n} from \"@tinycloud/sdk-services\";\nimport type { ReplicaRuntime } from \"./replica-contract\";\n// @ts-ignore -- node-sdk builds before replica declarations; keep the literal import for bundlers.\nimport { parseUcanGrant as replicaParseUcanGrant } from \"@tinycloud/replica\";\nconst parseUcanGrant = replicaParseUcanGrant as ReplicaRuntime[\"parseUcanGrant\"];\nexport { assertValidReplicationConfig } from \"./config\";\nexport { augmentSignInEntriesWithReplication, hasUnrestrictedGetCoverage } from \"./authority-sign-in\";\n\nconst KV_GET = KV.GET;\nconst KV_SYNC = KV.SYNC;\nconst KV_SERVICE = \"tinycloud.kv\";\n\n/** The delegation the replication grant delegates: get + sync on one prefix of the session space. */\nfunction replicationEntries(spaceId: string, prefix: string): PermissionEntry[] {\n return grantPathsForSelector(prefix).map((path) => ({\n service: KV_SERVICE,\n space: spaceId,\n path,\n actions: [KV_GET, KV_SYNC],\n }));\n}\n\n\n\n\n/**\n * The signed UCAN attenuation (`att`: resource → ability → caveat branches).\n * Only array branch sets are known; anything else is treated as constrained —\n * fail closed, never assume unconstrained.\n */\nfunction caveatBranchesUnconstrained(value: unknown): boolean {\n if (value === undefined || value === null) return true;\n if (typeof value === \"object\" && !Array.isArray(value) && Object.keys(value).length === 0) {\n return true;\n }\n if (!Array.isArray(value)) return false;\n try {\n return (\n canonicalizeRecapCaveats(value as Record<string, unknown>[]) ===\n canonicalizeRecapCaveats(undefined)\n );\n } catch {\n return false;\n }\n}\n\n/** The KV path a signed UCAN resource names in `space`, like replica's `kvPathIn`. */\nfunction kvPathInResource(resource: string, space: string): string | undefined {\n for (const base of [`${space}/kv/`, `tinycloud://${space}/kv/`]) {\n if (resource.startsWith(base)) return resource.slice(base.length);\n }\n return undefined;\n}\n\n/**\n * Whether the signed `att` grants `ability` over the selected namespace, with\n * each grant unrestricted (no caveats). A bare selector requires both its\n * exact key and its slash descendants; a trailing-slash selector is one range.\n */\nexport function ucanAttCovers(\n att: Record<string, Record<string, unknown>>,\n space: string,\n prefix: string,\n ability: string,\n options?: { ignoreCaveats?: boolean },\n): boolean {\n const requiredPaths = prefix.endsWith(\"/\") ? [prefix] : [prefix, `${prefix}/`];\n return requiredPaths.every((requestedPath) => {\n for (const [resource, abilities] of Object.entries(att)) {\n const path = kvPathInResource(resource, space);\n if (path === undefined) continue;\n for (const [granted, caveats] of Object.entries(abilities)) {\n if (!actionContains(granted, ability)) continue;\n if (!isCapabilitySubset(\n [{ service: KV_SERVICE, space, path: requestedPath, actions: [ability] }],\n [{ service: KV_SERVICE, space, path, actions: [granted] }],\n ).subset) continue;\n if (options?.ignoreCaveats === true || caveatBranchesUnconstrained(caveats)) return true;\n }\n }\n return false;\n });\n}\n\n/** `unconstrained` for `ReplicaGrantInfo` (§2.1): get AND sync on the prefix carry no caveats. */\nexport function ucanAttUnconstrainedFor(\n att: Record<string, Record<string, unknown>>,\n space: string,\n prefix: string,\n): boolean {\n return (\n ucanAttCovers(att, space, prefix, KV_GET) &&\n ucanAttCovers(att, space, prefix, KV_SYNC)\n );\n}\n\nexport interface DelegationPlanResult {\n path: \"session\" | \"runtime\";\n parentCid: string;\n expiresAt: number;\n}\n\n/**\n * The `TinyCloudNode` internals the authority needs. `planDelegation` is the\n * pure extraction of `delegateTo`'s parent selection (§4.3), so `plan` and\n * `mint` can never disagree about which parent or expiry a delegation gets.\n */\nexport type ReplicationAuthoritySession = Pick<\n TinyCloudSession,\n \"delegationHeader\" | \"delegationCid\" | \"spaceId\" | \"verificationMethod\" | \"jwk\"\n> & Partial<Pick<TinyCloudSession, \"siwe\">>;\n\nexport interface ReplicationAuthorityHost {\n replicationSession(): ReplicationAuthoritySession | undefined;\n /** The SIWE \"Expiration Time\" of the session — the delegateTo delegation cap. */\n siweExpiration(siwe: string): Date | undefined;\n planDelegation(\n entries: PermissionEntry[],\n options?: { expiry?: string | number },\n ): ({ grant?: unknown } & DelegationPlanResult) | AuthorityRefusal;\n mintDelegation(\n deviceDid: string,\n entries: PermissionEntry[],\n ): Promise<{ ucan: string; expiresAt: number }>;\n}\n\n/** Milliseconds of validity a session or parent must have left for authority reuse (delegateTo's margin). */\nexport const AUTHORITY_EXPIRY_MARGIN_MS = 60_000;\n\n\n/** Verify a compact session delegation before using any persisted space metadata. */\nexport function replicationScopeFromSignedSession(input: {\n delegationHeader: { Authorization: string };\n delegationCid: string;\n verificationMethod: string;\n persistedSpace: string;\n}): { space: string; principal: string } | undefined {\n let grant: ReturnType<typeof parseUcanGrant>;\n try {\n grant = parseUcanGrant(input.delegationHeader.Authorization);\n } catch {\n return undefined;\n }\n const bareDid = (value: string): string => value.split(\"#\", 1)[0]!;\n if (\n grant.cid !== input.delegationCid ||\n bareDid(grant.audience) !== bareDid(input.verificationMethod)\n ) return undefined;\n for (const resource of Object.keys(grant.att)) {\n const space = resource.match(/^(?:tinycloud:\\/\\/)?(.+?)\\/kv\\//)?.[1];\n if (space !== input.persistedSpace) continue;\n const owner = parseSpaceUri(space);\n if (owner?.owner && owner.address && owner.chainId) return { space, principal: owner.owner };\n }\n return undefined;\n}\nexport function createReplicationAuthority(host: ReplicationAuthorityHost): ReplicationAuthority {\n const planOrRefusal = (entries: PermissionEntry[]): DelegationPlanResult | AuthorityRefusal => {\n const plan = host.planDelegation(entries);\n if (\"refused\" in plan) return plan;\n return { path: plan.path, parentCid: plan.parentCid, expiresAt: plan.expiresAt };\n };\n const sessionAudience = (value: string): string => value.split(\"#\", 1)[0]!;\n const signedSessionGrant = (session: ReplicationAuthoritySession) => {\n const grant = parseUcanGrant(session.delegationHeader.Authorization);\n if (\n grant.cid !== session.delegationCid ||\n sessionAudience(grant.audience) !== sessionAudience(session.verificationMethod)\n ) return undefined;\n return grant;\n };\n const authority: ReplicationAuthority = {\n get sessionOnly() {\n const session = host.replicationSession();\n return session !== undefined && session.siwe === undefined;\n },\n sessionGrant(prefix: string): { ucan: string; device: ReplicaDevice } | AuthorityRefusal {\n const session = host.replicationSession();\n if (session === undefined) return { refused: \"SESSION_EXPIRING\" };\n const siweExpiry = session.siwe ? host.siweExpiration(session.siwe)?.getTime() : undefined;\n if (siweExpiry !== undefined && siweExpiry <= Date.now() + AUTHORITY_EXPIRY_MARGIN_MS) {\n return { refused: \"SESSION_EXPIRING\" };\n }\n let grant: ReturnType<typeof parseUcanGrant>;\n try {\n const verified = signedSessionGrant(session);\n if (!verified) return { refused: \"NOT_COVERED\" };\n grant = verified;\n } catch {\n return { refused: \"NOT_COVERED\" };\n }\n const tokenExpiry = grant.expiresAt === null ? undefined : grant.expiresAt * 1000;\n if (tokenExpiry === undefined || tokenExpiry <= Date.now() + AUTHORITY_EXPIRY_MARGIN_MS) {\n return { refused: \"SESSION_EXPIRING\" };\n }\n const covered = ucanAttCovers(grant.att, session.spaceId, prefix, KV_GET, { ignoreCaveats: true }) &&\n ucanAttCovers(grant.att, session.spaceId, prefix, KV_SYNC, { ignoreCaveats: true });\n if (!covered) return { refused: \"NOT_COVERED\" };\n if (!ucanAttUnconstrainedFor(grant.att, session.spaceId, prefix)) {\n return { refused: \"CAVEATED_AUTHORITY\" };\n }\n return {\n ucan: grant.jwt,\n device: { did: session.verificationMethod, jwk: session.jwk },\n };\n },\n plan(prefix: string): DelegationPlanResult | AuthorityRefusal {\n const session = host.replicationSession();\n if (session === undefined) return { refused: \"SESSION_EXPIRING\" };\n const sessionGrant = this.sessionGrant(prefix);\n if (!(\"refused\" in sessionGrant)) {\n const signed = signedSessionGrant(session);\n if (!signed || signed.expiresAt === null) return { refused: \"NOT_COVERED\" };\n return {\n path: \"session\",\n parentCid: session.delegationCid,\n expiresAt: signed.expiresAt * 1000,\n };\n }\n if (authority.sessionOnly) return sessionGrant;\n return planOrRefusal(replicationEntries(session.spaceId, prefix));\n },\n async mint(\n deviceDid: string,\n prefix: string,\n signal: AbortSignal,\n ): Promise<{ ucan: string; parentCid: string; expiresAt: number }> {\n if (authority.sessionOnly) throw new Error(\"compact delegate sessions cannot mint device grants\");\n const session = host.replicationSession();\n if (session === undefined) throw new Error(\"replication mint without a session\");\n const entries = replicationEntries(session.spaceId, prefix);\n const plan = planOrRefusal(entries);\n if (\"refused\" in plan) {\n throw Object.assign(\n new Error(`replication authority refused the mint: ${plan.refused}`),\n { refused: plan.refused },\n );\n }\n signal.throwIfAborted();\n const minted = await new Promise<{ ucan: string; expiresAt: number }>((resolve, reject) => {\n const onAbort = (): void => reject(signal.reason ?? new Error(\"replication mint aborted\"));\n signal.addEventListener(\"abort\", onAbort, { once: true });\n host.mintDelegation(deviceDid, entries).then(\n (result) => {\n signal.removeEventListener(\"abort\", onAbort);\n resolve(result);\n },\n (error: unknown) => {\n signal.removeEventListener(\"abort\", onAbort);\n reject(error instanceof Error ? error : new Error(String(error)));\n },\n );\n });\n return { ucan: minted.ucan, parentCid: plan.parentCid, expiresAt: minted.expiresAt };\n },\n };\n return authority;\n}\n","import { kvPrefixCovers, type ReplicationOptions } from \"@tinycloud/sdk-services\";\n\n/** Constructor-time validation; the space-dependent secrets check runs at sign-in and open. */\nexport function assertValidReplicationConfig(\n replication: (ReplicationOptions & { storage?: unknown }) | undefined,\n): void {\n if (replication === undefined || replication.enabled !== true) return;\n if (replication.storage === undefined || replication.storage === null) {\n throw new TypeError(\"replication.enabled requires replication.storage\");\n }\n if (!Array.isArray(replication.prefixes) || replication.prefixes.length === 0) {\n throw new TypeError(\"replication.prefixes must be a non-empty array\");\n }\n for (const prefix of replication.prefixes) {\n if (typeof prefix !== \"string\" || prefix === \"\") {\n throw new TypeError(\"replication.prefixes must not contain an empty prefix\");\n }\n }\n for (const [index, prefix] of replication.prefixes.entries()) {\n for (const other of replication.prefixes.slice(index + 1)) {\n if (kvPrefixCovers(prefix, other) || kvPrefixCovers(other, prefix)) {\n throw new TypeError(\n `replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`,\n );\n }\n }\n if (prefix.split(\"/\", 1)[0] === \"vault\" && replication.allowSecrets !== true) {\n throw new TypeError(\n `replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`,\n );\n }\n }\n}\n","import {\n grantPathsForSelector,\n KV,\n isCapabilitySubset,\n type PermissionEntry,\n type ReplicationOptions,\n} from \"@tinycloud/sdk-core\";\nimport { requiresSecretsOptIn } from \"@tinycloud/sdk-services\";\n\nconst KV_GET = KV.GET;\nconst KV_SYNC = KV.SYNC;\nconst KV_SERVICE = \"tinycloud.kv\";\n\nexport function hasUnrestrictedGetCoverage(\n entries: readonly PermissionEntry[],\n spaceId: string,\n prefix: string,\n): boolean {\n const requiredPaths = grantPathsForSelector(prefix);\n return isCapabilitySubset(\n requiredPaths.map((path) => ({ service: KV_SERVICE, space: spaceId, path, actions: [KV_GET] })),\n [...entries],\n ).subset;\n}\n\nexport function augmentSignInEntriesWithReplication(input: {\n entries: readonly PermissionEntry[];\n primarySpaceId: string;\n replication?: Pick<ReplicationOptions, \"prefixes\" | \"allowSecrets\">;\n}): PermissionEntry[] {\n const out: PermissionEntry[] = [];\n const replication = input.replication;\n if (replication === undefined) return out;\n for (const prefix of replication.prefixes) {\n if (\n requiresSecretsOptIn(input.primarySpaceId, prefix) &&\n replication.allowSecrets !== true\n ) {\n continue;\n }\n if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, prefix)) {\n continue;\n }\n for (const path of grantPathsForSelector(prefix)) {\n out.push({\n service: KV_SERVICE,\n space: input.primarySpaceId,\n path,\n actions: [KV_GET, KV_SYNC],\n });\n }\n }\n return out;\n}\n"],"mappings":"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","names":["grantPathsForSelector","KV","isCapabilitySubset","KV_GET","KV","KV_SYNC","KV_SERVICE","grantPathsForSelector","isCapabilitySubset"]}
|