@tinycloud/node-sdk 3.2.0-beta.4 → 3.2.0-beta.6
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/{WasmKeyProvider-DjfRu9aL.d.cts → WasmKeyProvider-C3NFgOmv.d.cts} +24 -5
- package/dist/{WasmKeyProvider-DjfRu9aL.d.ts → WasmKeyProvider-C3NFgOmv.d.ts} +24 -5
- package/dist/{authority-mTQEgN56.d.cts → authority-DDGtccBK.d.cts} +5 -6
- package/dist/{authority-mTQEgN56.d.ts → authority-DDGtccBK.d.ts} +5 -6
- package/dist/core.cjs +78 -44
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +3 -3
- package/dist/core.d.ts +3 -3
- package/dist/core.js +80 -45
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +130 -73
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +121 -65
- package/dist/index.js.map +1 -1
- package/dist/replication/authority.cjs +51 -37
- package/dist/replication/authority.cjs.map +1 -1
- package/dist/replication/authority.d.cts +1 -1
- package/dist/replication/authority.d.ts +1 -1
- package/dist/replication/authority.js +48 -36
- package/dist/replication/authority.js.map +1 -1
- package/dist/replication/runtime.cjs +1 -0
- package/dist/replication/runtime.cjs.map +1 -1
- package/dist/replication/runtime.d.cts +1 -1
- package/dist/replication/runtime.d.ts +1 -1
- package/dist/replication/runtime.js +1 -0
- package/dist/replication/runtime.js.map +1 -1
- package/dist/{runtime-DyYwHW8j.d.cts → runtime-B5P5SRpC.d.cts} +1 -0
- package/dist/{runtime-DyYwHW8j.d.ts → runtime-B5P5SRpC.d.ts} +1 -0
- package/package.json +4 -4
|
@@ -34,7 +34,7 @@ var import_sdk_core2 = require("@tinycloud/sdk-core");
|
|
|
34
34
|
var import_replica = require("@tinycloud/replica");
|
|
35
35
|
|
|
36
36
|
// src/replication/config.ts
|
|
37
|
-
var
|
|
37
|
+
var import_prefix = require("@tinycloud/sdk-services/kv/replication/prefix");
|
|
38
38
|
function assertValidReplicationConfig(replication) {
|
|
39
39
|
if (replication === void 0 || replication.enabled !== true) return;
|
|
40
40
|
if (replication.storage === void 0 || replication.storage === null) {
|
|
@@ -44,19 +44,21 @@ function assertValidReplicationConfig(replication) {
|
|
|
44
44
|
throw new TypeError("replication.prefixes must be a non-empty array");
|
|
45
45
|
}
|
|
46
46
|
for (const prefix of replication.prefixes) {
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
try {
|
|
48
|
+
(0, import_prefix.assertFolderPrefix)(prefix);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new TypeError(error instanceof Error ? error.message : String(error));
|
|
49
51
|
}
|
|
50
52
|
}
|
|
51
53
|
for (const [index, prefix] of replication.prefixes.entries()) {
|
|
52
54
|
for (const other of replication.prefixes.slice(index + 1)) {
|
|
53
|
-
if ((0,
|
|
55
|
+
if ((0, import_prefix.kvPrefixCovers)(prefix, other) || (0, import_prefix.kvPrefixCovers)(other, prefix)) {
|
|
54
56
|
throw new TypeError(
|
|
55
57
|
`replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`
|
|
56
58
|
);
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
|
-
if (prefix.split("/", 1)[0] === "vault" && replication.allowSecrets !== true) {
|
|
61
|
+
if ((prefix === "/" || prefix.split("/", 1)[0] === "vault") && replication.allowSecrets !== true) {
|
|
60
62
|
throw new TypeError(
|
|
61
63
|
`replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`
|
|
62
64
|
);
|
|
@@ -66,14 +68,14 @@ function assertValidReplicationConfig(replication) {
|
|
|
66
68
|
|
|
67
69
|
// src/replication/authority-sign-in.ts
|
|
68
70
|
var import_sdk_core = require("@tinycloud/sdk-core");
|
|
69
|
-
var
|
|
71
|
+
var import_sdk_services = require("@tinycloud/sdk-services");
|
|
72
|
+
var import_prefix2 = require("@tinycloud/sdk-services/kv/replication/prefix");
|
|
70
73
|
var KV_GET = import_sdk_core.KV.GET;
|
|
71
74
|
var KV_SYNC = import_sdk_core.KV.SYNC;
|
|
72
75
|
var KV_SERVICE = "tinycloud.kv";
|
|
73
76
|
function hasUnrestrictedGetCoverage(entries, spaceId, prefix) {
|
|
74
|
-
const requiredPaths = (0, import_sdk_core.grantPathsForSelector)(prefix);
|
|
75
77
|
return (0, import_sdk_core.isCapabilitySubset)(
|
|
76
|
-
|
|
78
|
+
[{ service: KV_SERVICE, space: spaceId, path: prefix, actions: [KV_GET] }],
|
|
77
79
|
[...entries]
|
|
78
80
|
).subset;
|
|
79
81
|
}
|
|
@@ -81,21 +83,33 @@ function augmentSignInEntriesWithReplication(input) {
|
|
|
81
83
|
const out = [];
|
|
82
84
|
const replication = input.replication;
|
|
83
85
|
if (replication === void 0) return out;
|
|
86
|
+
const eligiblePaths = [];
|
|
84
87
|
for (const prefix of replication.prefixes) {
|
|
85
|
-
|
|
88
|
+
(0, import_prefix2.assertFolderPrefix)(prefix);
|
|
89
|
+
const path = prefix === "/" ? "" : prefix;
|
|
90
|
+
if ((0, import_sdk_services.requiresSecretsOptIn)(input.primarySpaceId, prefix) && replication.allowSecrets !== true) {
|
|
86
91
|
continue;
|
|
87
92
|
}
|
|
88
|
-
if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId,
|
|
93
|
+
if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, path)) {
|
|
89
94
|
continue;
|
|
90
95
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
96
|
+
eligiblePaths.push(path);
|
|
97
|
+
}
|
|
98
|
+
if (eligiblePaths.length === replication.prefixes.length && eligiblePaths.length > 0 && hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, "")) {
|
|
99
|
+
return [{
|
|
100
|
+
service: KV_SERVICE,
|
|
101
|
+
space: input.primarySpaceId,
|
|
102
|
+
path: "",
|
|
103
|
+
actions: [KV_GET, KV_SYNC]
|
|
104
|
+
}];
|
|
105
|
+
}
|
|
106
|
+
for (const path of eligiblePaths) {
|
|
107
|
+
out.push({
|
|
108
|
+
service: KV_SERVICE,
|
|
109
|
+
space: input.primarySpaceId,
|
|
110
|
+
path,
|
|
111
|
+
actions: [KV_GET, KV_SYNC]
|
|
112
|
+
});
|
|
99
113
|
}
|
|
100
114
|
return out;
|
|
101
115
|
}
|
|
@@ -106,12 +120,12 @@ var KV_GET2 = import_sdk_core2.KV.GET;
|
|
|
106
120
|
var KV_SYNC2 = import_sdk_core2.KV.SYNC;
|
|
107
121
|
var KV_SERVICE2 = "tinycloud.kv";
|
|
108
122
|
function replicationEntries(spaceId, prefix) {
|
|
109
|
-
return
|
|
123
|
+
return [{
|
|
110
124
|
service: KV_SERVICE2,
|
|
111
125
|
space: spaceId,
|
|
112
|
-
path,
|
|
126
|
+
path: prefix === "/" ? "" : prefix,
|
|
113
127
|
actions: [KV_GET2, KV_SYNC2]
|
|
114
|
-
}
|
|
128
|
+
}];
|
|
115
129
|
}
|
|
116
130
|
function caveatBranchesUnconstrained(value) {
|
|
117
131
|
if (value === void 0 || value === null) return true;
|
|
@@ -126,28 +140,28 @@ function caveatBranchesUnconstrained(value) {
|
|
|
126
140
|
}
|
|
127
141
|
}
|
|
128
142
|
function kvPathInResource(resource, space) {
|
|
143
|
+
for (const root of [`${space}/kv`, `tinycloud://${space}/kv`]) {
|
|
144
|
+
if (resource === root || resource === `${root}/`) return "/";
|
|
145
|
+
}
|
|
129
146
|
for (const base of [`${space}/kv/`, `tinycloud://${space}/kv/`]) {
|
|
130
147
|
if (resource.startsWith(base)) return resource.slice(base.length);
|
|
131
148
|
}
|
|
132
149
|
return void 0;
|
|
133
150
|
}
|
|
134
151
|
function ucanAttCovers(att, space, prefix, ability, options) {
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
).subset) continue;
|
|
146
|
-
if (options?.ignoreCaveats === true || caveatBranchesUnconstrained(caveats)) return true;
|
|
147
|
-
}
|
|
152
|
+
for (const [resource, abilities] of Object.entries(att)) {
|
|
153
|
+
const path = kvPathInResource(resource, space);
|
|
154
|
+
if (path === void 0) continue;
|
|
155
|
+
for (const [granted, caveats] of Object.entries(abilities)) {
|
|
156
|
+
if (!(0, import_sdk_core2.actionContains)(granted, ability)) continue;
|
|
157
|
+
if (!(0, import_sdk_core2.isCapabilitySubset)(
|
|
158
|
+
[{ service: KV_SERVICE2, space, path: prefix, actions: [ability] }],
|
|
159
|
+
[{ service: KV_SERVICE2, space, path, actions: [granted] }]
|
|
160
|
+
).subset) continue;
|
|
161
|
+
if (options?.ignoreCaveats === true || caveatBranchesUnconstrained(caveats)) return true;
|
|
148
162
|
}
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
151
165
|
}
|
|
152
166
|
function ucanAttUnconstrainedFor(att, space, prefix) {
|
|
153
167
|
return ucanAttCovers(att, space, prefix, KV_GET2) && ucanAttCovers(att, space, prefix, KV_SYNC2);
|
|
@@ -163,7 +177,7 @@ function replicationScopeFromSignedSession(input) {
|
|
|
163
177
|
const bareDid = (value) => value.split("#", 1)[0];
|
|
164
178
|
if (grant.cid !== input.delegationCid || bareDid(grant.audience) !== bareDid(input.verificationMethod)) return void 0;
|
|
165
179
|
for (const resource of Object.keys(grant.att)) {
|
|
166
|
-
const space = resource.match(/^(?:tinycloud:\/\/)?(.+?)\/kv
|
|
180
|
+
const space = resource.match(/^(?:tinycloud:\/\/)?(.+?)\/kv(?:\/|$)/)?.[1];
|
|
167
181
|
if (space !== input.persistedSpace) continue;
|
|
168
182
|
const owner = (0, import_sdk_core2.parseSpaceUri)(space);
|
|
169
183
|
if (owner?.owner && owner.address && owner.chainId) return { space, principal: owner.owner };
|
|
@@ -1 +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"]}
|
|
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 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 folder prefix. */\nfunction replicationEntries(spaceId: string, prefix: string): PermissionEntry[] {\n return [{\n service: KV_SERVICE,\n space: spaceId,\n path: prefix === \"/\" ? \"\" : prefix,\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 root of [`${space}/kv`, `tinycloud://${space}/kv`]) {\n if (resource === root || resource === `${root}/`) return \"/\";\n }\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 folder prefix, with\n * unrestricted authority unless explicitly asked to ignore caveats.\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 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: prefix, 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/** `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 { assertFolderPrefix as assertPureFolderPrefix, kvPrefixCovers } from \"@tinycloud/sdk-services/kv/replication/prefix\";\nimport type { ReplicationOptions } from \"@tinycloud/sdk-core\";\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 try {\n assertPureFolderPrefix(prefix);\n } catch (error) {\n throw new TypeError(error instanceof Error ? error.message : String(error));\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 === \"/\" || 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 KV,\n isCapabilitySubset,\n type PermissionEntry,\n type ReplicationOptions,\n} from \"@tinycloud/sdk-core\";\nimport { requiresSecretsOptIn } from \"@tinycloud/sdk-services\";\nimport { assertFolderPrefix } from \"@tinycloud/sdk-services/kv/replication/prefix\";\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 return isCapabilitySubset(\n [{ service: KV_SERVICE, space: spaceId, path: prefix, 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 const eligiblePaths: string[] = [];\n for (const prefix of replication.prefixes) {\n assertFolderPrefix(prefix);\n const path = prefix === \"/\" ? \"\" : prefix;\n if (\n requiresSecretsOptIn(input.primarySpaceId, prefix) &&\n replication.allowSecrets !== true\n ) {\n continue;\n }\n if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, path)) {\n continue;\n }\n eligiblePaths.push(path);\n }\n if (\n eligiblePaths.length === replication.prefixes.length &&\n eligiblePaths.length > 0 &&\n hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, \"\")\n ) {\n return [{\n service: KV_SERVICE,\n space: input.primarySpaceId,\n path: \"\",\n actions: [KV_GET, KV_SYNC],\n }];\n }\n for (const path of eligiblePaths) {\n out.push({\n service: KV_SERVICE,\n space: input.primarySpaceId,\n path,\n actions: [KV_GET, KV_SYNC],\n });\n }\n return out;\n}\n"],"mappings":"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","names":["import_sdk_core","assertPureFolderPrefix","import_prefix","replicaParseUcanGrant","KV_GET","KV_SYNC","KV_SERVICE"]}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '@tinycloud/sdk-core';
|
|
2
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-
|
|
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-DDGtccBK.cjs';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import '@tinycloud/sdk-core';
|
|
2
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-
|
|
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-DDGtccBK.js';
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
// src/replication/authority.ts
|
|
2
2
|
import {
|
|
3
|
-
grantPathsForSelector as grantPathsForSelector2,
|
|
4
3
|
actionContains,
|
|
5
4
|
KV as KV2,
|
|
6
5
|
canonicalizeRecapCaveats,
|
|
@@ -10,7 +9,7 @@ import {
|
|
|
10
9
|
import { parseUcanGrant as replicaParseUcanGrant } from "@tinycloud/replica";
|
|
11
10
|
|
|
12
11
|
// src/replication/config.ts
|
|
13
|
-
import { kvPrefixCovers } from "@tinycloud/sdk-services";
|
|
12
|
+
import { assertFolderPrefix as assertPureFolderPrefix, kvPrefixCovers } from "@tinycloud/sdk-services/kv/replication/prefix";
|
|
14
13
|
function assertValidReplicationConfig(replication) {
|
|
15
14
|
if (replication === void 0 || replication.enabled !== true) return;
|
|
16
15
|
if (replication.storage === void 0 || replication.storage === null) {
|
|
@@ -20,8 +19,10 @@ function assertValidReplicationConfig(replication) {
|
|
|
20
19
|
throw new TypeError("replication.prefixes must be a non-empty array");
|
|
21
20
|
}
|
|
22
21
|
for (const prefix of replication.prefixes) {
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
try {
|
|
23
|
+
assertPureFolderPrefix(prefix);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new TypeError(error instanceof Error ? error.message : String(error));
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
for (const [index, prefix] of replication.prefixes.entries()) {
|
|
@@ -32,7 +33,7 @@ function assertValidReplicationConfig(replication) {
|
|
|
32
33
|
);
|
|
33
34
|
}
|
|
34
35
|
}
|
|
35
|
-
if (prefix.split("/", 1)[0] === "vault" && replication.allowSecrets !== true) {
|
|
36
|
+
if ((prefix === "/" || prefix.split("/", 1)[0] === "vault") && replication.allowSecrets !== true) {
|
|
36
37
|
throw new TypeError(
|
|
37
38
|
`replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`
|
|
38
39
|
);
|
|
@@ -42,18 +43,17 @@ function assertValidReplicationConfig(replication) {
|
|
|
42
43
|
|
|
43
44
|
// src/replication/authority-sign-in.ts
|
|
44
45
|
import {
|
|
45
|
-
grantPathsForSelector,
|
|
46
46
|
KV,
|
|
47
47
|
isCapabilitySubset
|
|
48
48
|
} from "@tinycloud/sdk-core";
|
|
49
49
|
import { requiresSecretsOptIn } from "@tinycloud/sdk-services";
|
|
50
|
+
import { assertFolderPrefix } from "@tinycloud/sdk-services/kv/replication/prefix";
|
|
50
51
|
var KV_GET = KV.GET;
|
|
51
52
|
var KV_SYNC = KV.SYNC;
|
|
52
53
|
var KV_SERVICE = "tinycloud.kv";
|
|
53
54
|
function hasUnrestrictedGetCoverage(entries, spaceId, prefix) {
|
|
54
|
-
const requiredPaths = grantPathsForSelector(prefix);
|
|
55
55
|
return isCapabilitySubset(
|
|
56
|
-
|
|
56
|
+
[{ service: KV_SERVICE, space: spaceId, path: prefix, actions: [KV_GET] }],
|
|
57
57
|
[...entries]
|
|
58
58
|
).subset;
|
|
59
59
|
}
|
|
@@ -61,21 +61,33 @@ function augmentSignInEntriesWithReplication(input) {
|
|
|
61
61
|
const out = [];
|
|
62
62
|
const replication = input.replication;
|
|
63
63
|
if (replication === void 0) return out;
|
|
64
|
+
const eligiblePaths = [];
|
|
64
65
|
for (const prefix of replication.prefixes) {
|
|
66
|
+
assertFolderPrefix(prefix);
|
|
67
|
+
const path = prefix === "/" ? "" : prefix;
|
|
65
68
|
if (requiresSecretsOptIn(input.primarySpaceId, prefix) && replication.allowSecrets !== true) {
|
|
66
69
|
continue;
|
|
67
70
|
}
|
|
68
|
-
if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId,
|
|
71
|
+
if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, path)) {
|
|
69
72
|
continue;
|
|
70
73
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
eligiblePaths.push(path);
|
|
75
|
+
}
|
|
76
|
+
if (eligiblePaths.length === replication.prefixes.length && eligiblePaths.length > 0 && hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, "")) {
|
|
77
|
+
return [{
|
|
78
|
+
service: KV_SERVICE,
|
|
79
|
+
space: input.primarySpaceId,
|
|
80
|
+
path: "",
|
|
81
|
+
actions: [KV_GET, KV_SYNC]
|
|
82
|
+
}];
|
|
83
|
+
}
|
|
84
|
+
for (const path of eligiblePaths) {
|
|
85
|
+
out.push({
|
|
86
|
+
service: KV_SERVICE,
|
|
87
|
+
space: input.primarySpaceId,
|
|
88
|
+
path,
|
|
89
|
+
actions: [KV_GET, KV_SYNC]
|
|
90
|
+
});
|
|
79
91
|
}
|
|
80
92
|
return out;
|
|
81
93
|
}
|
|
@@ -86,12 +98,12 @@ var KV_GET2 = KV2.GET;
|
|
|
86
98
|
var KV_SYNC2 = KV2.SYNC;
|
|
87
99
|
var KV_SERVICE2 = "tinycloud.kv";
|
|
88
100
|
function replicationEntries(spaceId, prefix) {
|
|
89
|
-
return
|
|
101
|
+
return [{
|
|
90
102
|
service: KV_SERVICE2,
|
|
91
103
|
space: spaceId,
|
|
92
|
-
path,
|
|
104
|
+
path: prefix === "/" ? "" : prefix,
|
|
93
105
|
actions: [KV_GET2, KV_SYNC2]
|
|
94
|
-
}
|
|
106
|
+
}];
|
|
95
107
|
}
|
|
96
108
|
function caveatBranchesUnconstrained(value) {
|
|
97
109
|
if (value === void 0 || value === null) return true;
|
|
@@ -106,28 +118,28 @@ function caveatBranchesUnconstrained(value) {
|
|
|
106
118
|
}
|
|
107
119
|
}
|
|
108
120
|
function kvPathInResource(resource, space) {
|
|
121
|
+
for (const root of [`${space}/kv`, `tinycloud://${space}/kv`]) {
|
|
122
|
+
if (resource === root || resource === `${root}/`) return "/";
|
|
123
|
+
}
|
|
109
124
|
for (const base of [`${space}/kv/`, `tinycloud://${space}/kv/`]) {
|
|
110
125
|
if (resource.startsWith(base)) return resource.slice(base.length);
|
|
111
126
|
}
|
|
112
127
|
return void 0;
|
|
113
128
|
}
|
|
114
129
|
function ucanAttCovers(att, space, prefix, ability, options) {
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
).subset) continue;
|
|
126
|
-
if (options?.ignoreCaveats === true || caveatBranchesUnconstrained(caveats)) return true;
|
|
127
|
-
}
|
|
130
|
+
for (const [resource, abilities] of Object.entries(att)) {
|
|
131
|
+
const path = kvPathInResource(resource, space);
|
|
132
|
+
if (path === void 0) continue;
|
|
133
|
+
for (const [granted, caveats] of Object.entries(abilities)) {
|
|
134
|
+
if (!actionContains(granted, ability)) continue;
|
|
135
|
+
if (!isCapabilitySubset2(
|
|
136
|
+
[{ service: KV_SERVICE2, space, path: prefix, actions: [ability] }],
|
|
137
|
+
[{ service: KV_SERVICE2, space, path, actions: [granted] }]
|
|
138
|
+
).subset) continue;
|
|
139
|
+
if (options?.ignoreCaveats === true || caveatBranchesUnconstrained(caveats)) return true;
|
|
128
140
|
}
|
|
129
|
-
|
|
130
|
-
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
131
143
|
}
|
|
132
144
|
function ucanAttUnconstrainedFor(att, space, prefix) {
|
|
133
145
|
return ucanAttCovers(att, space, prefix, KV_GET2) && ucanAttCovers(att, space, prefix, KV_SYNC2);
|
|
@@ -143,7 +155,7 @@ function replicationScopeFromSignedSession(input) {
|
|
|
143
155
|
const bareDid = (value) => value.split("#", 1)[0];
|
|
144
156
|
if (grant.cid !== input.delegationCid || bareDid(grant.audience) !== bareDid(input.verificationMethod)) return void 0;
|
|
145
157
|
for (const resource of Object.keys(grant.att)) {
|
|
146
|
-
const space = resource.match(/^(?:tinycloud:\/\/)?(.+?)\/kv
|
|
158
|
+
const space = resource.match(/^(?:tinycloud:\/\/)?(.+?)\/kv(?:\/|$)/)?.[1];
|
|
147
159
|
if (space !== input.persistedSpace) continue;
|
|
148
160
|
const owner = parseSpaceUri(space);
|
|
149
161
|
if (owner?.owner && owner.address && owner.chainId) return { space, principal: owner.owner };
|
|
@@ -1 +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"]}
|
|
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 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 folder prefix. */\nfunction replicationEntries(spaceId: string, prefix: string): PermissionEntry[] {\n return [{\n service: KV_SERVICE,\n space: spaceId,\n path: prefix === \"/\" ? \"\" : prefix,\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 root of [`${space}/kv`, `tinycloud://${space}/kv`]) {\n if (resource === root || resource === `${root}/`) return \"/\";\n }\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 folder prefix, with\n * unrestricted authority unless explicitly asked to ignore caveats.\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 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: prefix, 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/** `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 { assertFolderPrefix as assertPureFolderPrefix, kvPrefixCovers } from \"@tinycloud/sdk-services/kv/replication/prefix\";\nimport type { ReplicationOptions } from \"@tinycloud/sdk-core\";\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 try {\n assertPureFolderPrefix(prefix);\n } catch (error) {\n throw new TypeError(error instanceof Error ? error.message : String(error));\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 === \"/\" || 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 KV,\n isCapabilitySubset,\n type PermissionEntry,\n type ReplicationOptions,\n} from \"@tinycloud/sdk-core\";\nimport { requiresSecretsOptIn } from \"@tinycloud/sdk-services\";\nimport { assertFolderPrefix } from \"@tinycloud/sdk-services/kv/replication/prefix\";\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 return isCapabilitySubset(\n [{ service: KV_SERVICE, space: spaceId, path: prefix, 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 const eligiblePaths: string[] = [];\n for (const prefix of replication.prefixes) {\n assertFolderPrefix(prefix);\n const path = prefix === \"/\" ? \"\" : prefix;\n if (\n requiresSecretsOptIn(input.primarySpaceId, prefix) &&\n replication.allowSecrets !== true\n ) {\n continue;\n }\n if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, path)) {\n continue;\n }\n eligiblePaths.push(path);\n }\n if (\n eligiblePaths.length === replication.prefixes.length &&\n eligiblePaths.length > 0 &&\n hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, \"\")\n ) {\n return [{\n service: KV_SERVICE,\n space: input.primarySpaceId,\n path: \"\",\n actions: [KV_GET, KV_SYNC],\n }];\n }\n for (const path of eligiblePaths) {\n out.push({\n service: KV_SERVICE,\n space: input.primarySpaceId,\n path,\n actions: [KV_GET, KV_SYNC],\n });\n }\n return out;\n}\n"],"mappings":"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","names":["KV","isCapabilitySubset","KV_GET","KV","KV_SYNC","KV_SERVICE","isCapabilitySubset"]}
|