@tinycloud/node-sdk 2.11.0-beta.9 → 2.11.1-beta.0
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-Bkptd8qZ.d.cts → core-C6Czyr52.d.cts} +233 -8
- package/dist/{core-Bkptd8qZ.d.ts → core-C6Czyr52.d.ts} +233 -8
- package/dist/core.cjs +1120 -97
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.js +1139 -105
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +903 -101
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -3
- package/dist/index.d.ts +85 -3
- package/dist/index.js +917 -108
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/core.js
CHANGED
|
@@ -220,6 +220,8 @@ var FileSessionStorage = class {
|
|
|
220
220
|
};
|
|
221
221
|
|
|
222
222
|
// src/authorization/NodeUserAuthorization.ts
|
|
223
|
+
import { createHash } from "crypto";
|
|
224
|
+
import { SiweMessage } from "siwe";
|
|
223
225
|
import {
|
|
224
226
|
fetchPeerId,
|
|
225
227
|
submitHostDelegation,
|
|
@@ -238,6 +240,13 @@ import {
|
|
|
238
240
|
canonicalizeAddress,
|
|
239
241
|
makePkhSpaceId,
|
|
240
242
|
pkhDid,
|
|
243
|
+
extractImmutableSiweFields,
|
|
244
|
+
diffImmutableSiweFields,
|
|
245
|
+
extractRecapAttenuations,
|
|
246
|
+
unauthorizedRecapCapabilities,
|
|
247
|
+
parseCanonicalRecapResource,
|
|
248
|
+
ACCOUNT_REGISTRY_SPACE,
|
|
249
|
+
ACCOUNT_MANIFEST_PERMISSIONS,
|
|
241
250
|
KV,
|
|
242
251
|
SQL,
|
|
243
252
|
DUCKDB,
|
|
@@ -253,6 +262,27 @@ var defaultSignStrategy = { type: "auto-sign" };
|
|
|
253
262
|
// src/authorization/NodeUserAuthorization.ts
|
|
254
263
|
var DECRYPT_ACTION = ENCRYPTION.DECRYPT;
|
|
255
264
|
var NETWORK_CREATE_ACTION = ENCRYPTION.NETWORK_CREATE;
|
|
265
|
+
function canonicalizeOpenKeyManifestJson(value) {
|
|
266
|
+
const json = JSON.stringify(value);
|
|
267
|
+
if (json === void 0) {
|
|
268
|
+
throw new TypeError("manifest is not JSON-serializable");
|
|
269
|
+
}
|
|
270
|
+
return canonicalizeJsonValue(JSON.parse(json));
|
|
271
|
+
}
|
|
272
|
+
function canonicalizeJsonValue(value) {
|
|
273
|
+
if (value === null || typeof value !== "object") {
|
|
274
|
+
return JSON.stringify(value);
|
|
275
|
+
}
|
|
276
|
+
if (Array.isArray(value)) {
|
|
277
|
+
return `[${value.map(canonicalizeJsonValue).join(",")}]`;
|
|
278
|
+
}
|
|
279
|
+
return `{${Object.keys(value).sort().map(
|
|
280
|
+
(key) => `${JSON.stringify(key)}:${canonicalizeJsonValue(value[key])}`
|
|
281
|
+
).join(",")}}`;
|
|
282
|
+
}
|
|
283
|
+
function canonicalOpenKeyManifestSha256Hex(value) {
|
|
284
|
+
return createHash("sha256").update(canonicalizeOpenKeyManifestJson(value)).digest("hex");
|
|
285
|
+
}
|
|
256
286
|
function didPrincipalMatches(actual, expected) {
|
|
257
287
|
try {
|
|
258
288
|
return principalDidEquals(actual, expected);
|
|
@@ -270,6 +300,16 @@ function addRawAbility(rawAbilities, resource, action) {
|
|
|
270
300
|
actions.push(action);
|
|
271
301
|
}
|
|
272
302
|
}
|
|
303
|
+
function cloneAbilitiesMap(actions) {
|
|
304
|
+
return Object.fromEntries(
|
|
305
|
+
Object.entries(actions).map(([service, paths]) => [
|
|
306
|
+
service,
|
|
307
|
+
Object.fromEntries(
|
|
308
|
+
Object.entries(paths).map(([path, pathActions]) => [path, [...pathActions]])
|
|
309
|
+
)
|
|
310
|
+
])
|
|
311
|
+
);
|
|
312
|
+
}
|
|
273
313
|
var NodeUserAuthorization = class {
|
|
274
314
|
constructor(config) {
|
|
275
315
|
this.activeSessionKeyId = "default";
|
|
@@ -541,23 +581,41 @@ var NodeUserAuthorization = class {
|
|
|
541
581
|
const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
|
|
542
582
|
const primarySpaceId2 = makePkhSpaceId(address, chainId, this.spacePrefix);
|
|
543
583
|
const secretsSpaceId = makePkhSpaceId(address, chainId, "secrets");
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
584
|
+
const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
|
|
585
|
+
const spaceAbilities2 = {
|
|
586
|
+
[primarySpaceId2]: primaryActions,
|
|
587
|
+
[secretsSpaceId]: {
|
|
588
|
+
kv: {
|
|
589
|
+
"vault/secrets/": [
|
|
590
|
+
KV.GET,
|
|
591
|
+
KV.PUT,
|
|
592
|
+
KV.DEL,
|
|
593
|
+
KV.LIST,
|
|
594
|
+
KV.METADATA
|
|
595
|
+
]
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
if (this.includeAccountRegistryPermissions && this.spacePrefix !== ACCOUNT_REGISTRY_SPACE) {
|
|
600
|
+
spaceAbilities2[makePkhSpaceId(address, chainId, ACCOUNT_REGISTRY_SPACE)] = resourceCapabilitiesToAbilitiesMap(ACCOUNT_MANIFEST_PERMISSIONS);
|
|
601
|
+
} else if (this.includeAccountRegistryPermissions) {
|
|
602
|
+
const accountAbilities = resourceCapabilitiesToAbilitiesMap(
|
|
603
|
+
ACCOUNT_MANIFEST_PERMISSIONS
|
|
604
|
+
);
|
|
605
|
+
for (const [service, paths] of Object.entries(accountAbilities)) {
|
|
606
|
+
const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
|
|
607
|
+
for (const [path, actions] of Object.entries(paths)) {
|
|
608
|
+
const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
|
|
609
|
+
for (const action of actions) {
|
|
610
|
+
if (!existingActions.includes(action)) existingActions.push(action);
|
|
558
611
|
}
|
|
559
612
|
}
|
|
560
|
-
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
abilities: primaryActions,
|
|
617
|
+
spaceId: primarySpaceId2,
|
|
618
|
+
spaceAbilities: spaceAbilities2,
|
|
561
619
|
rawAbilities: {
|
|
562
620
|
[defaultNetworkId]: [DECRYPT_ACTION, NETWORK_CREATE_ACTION]
|
|
563
621
|
}
|
|
@@ -1138,6 +1196,398 @@ var NodeUserAuthorization = class {
|
|
|
1138
1196
|
await this.ensureSpaceExists();
|
|
1139
1197
|
return clientSession;
|
|
1140
1198
|
}
|
|
1199
|
+
/**
|
|
1200
|
+
* End-to-end sign-in via OpenKey `authorizeTinyCloud()` (Sol CRITICAL-4).
|
|
1201
|
+
*
|
|
1202
|
+
* This is the production entry point that wires
|
|
1203
|
+
* {@link prepareSessionForSigning} → OpenKey `authorizeTinyCloud()` →
|
|
1204
|
+
* {@link signInWithOpenKeyResult} into one call. Consumers pass in a
|
|
1205
|
+
* caller-provided `authorizeFn` (typically a thin wrapper around
|
|
1206
|
+
* `openkey.authorizeTinyCloud()`) so this method has no direct
|
|
1207
|
+
* dependency on the OpenKey SDK — it works with any transport that
|
|
1208
|
+
* conforms to the versioned protocol.
|
|
1209
|
+
*
|
|
1210
|
+
* The `authorizeFn` MUST:
|
|
1211
|
+
* 1. Send the prepared SIWE bytes to OpenKey.
|
|
1212
|
+
* 2. Return the exact `TinyCloudAuthorizationResultV1` shape (with
|
|
1213
|
+
* `signedMessage` — the bytes the signature actually verifies
|
|
1214
|
+
* against — plus `selectedActionKeys` and `permissions`).
|
|
1215
|
+
*
|
|
1216
|
+
* NodeUserAuthorization then completes the session with `signedMessage`
|
|
1217
|
+
* (never the prepared bytes), verifying the signature, the immutable
|
|
1218
|
+
* SIWE fields, and every subset invariant enforced by
|
|
1219
|
+
* {@link signInWithOpenKeyResult}. This preserves the exact-byte
|
|
1220
|
+
* guarantee: if the user did NOT narrow, the returned `signedMessage`
|
|
1221
|
+
* equals the prepared SIWE byte-for-byte; if the user narrowed, only
|
|
1222
|
+
* the ReCap block differs and every immutable header field survives.
|
|
1223
|
+
*
|
|
1224
|
+
* Legacy exact-byte {@link signIn} callers are unaffected — this method
|
|
1225
|
+
* is opt-in.
|
|
1226
|
+
*
|
|
1227
|
+
* @param authorizeFn Callback that invokes the versioned OpenKey
|
|
1228
|
+
* `authorizeTinyCloud()` protocol and returns the result. Receives
|
|
1229
|
+
* the prepared SIWE bytes plus a `host` hint the widget should bind
|
|
1230
|
+
* into its /authorize-sign-prepare context.
|
|
1231
|
+
* @param options Optional overrides for the OpenKey request.
|
|
1232
|
+
*/
|
|
1233
|
+
async signInWithOpenKey(authorizeFn, options) {
|
|
1234
|
+
if (options?.host !== void 0) {
|
|
1235
|
+
const overrideHost = options.host.trim();
|
|
1236
|
+
if (!overrideHost) {
|
|
1237
|
+
throw new Error("OpenKey authorization host must not be empty");
|
|
1238
|
+
}
|
|
1239
|
+
this.tinycloudHosts = [overrideHost];
|
|
1240
|
+
}
|
|
1241
|
+
const signingAddress = canonicalizeAddress(await this.signer.getAddress());
|
|
1242
|
+
const signingChainId = await this.signer.getChainId();
|
|
1243
|
+
await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
|
|
1244
|
+
const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
|
|
1245
|
+
const presentation = this.buildPresentationEnvelope(options?.reason);
|
|
1246
|
+
const hostHint = this.primaryTinyCloudHost;
|
|
1247
|
+
const result = await authorizeFn({
|
|
1248
|
+
protocolVersion: 1,
|
|
1249
|
+
siwe: prepared.siwe,
|
|
1250
|
+
jwk,
|
|
1251
|
+
host: hostHint,
|
|
1252
|
+
keyId: options?.openkeyKeyId,
|
|
1253
|
+
presentation
|
|
1254
|
+
});
|
|
1255
|
+
return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
|
|
1259
|
+
* envelope is display-only — the OpenKey widget renders it under an
|
|
1260
|
+
* honest trust label, and the server /authorize-sign-prepare route
|
|
1261
|
+
* decides whether to upgrade the label to `origin-bound` by fetching
|
|
1262
|
+
* the well-known manifest and comparing its digest to
|
|
1263
|
+
* `manifestDigest`.
|
|
1264
|
+
*
|
|
1265
|
+
* Returns `undefined` when no manifest is set — the widget renders as
|
|
1266
|
+
* "no manifest supplied" (unsigned).
|
|
1267
|
+
*
|
|
1268
|
+
* The digest is a deterministic SHA-256 over canonical JSON of the
|
|
1269
|
+
* first manifest payload; OpenKey parses the fetched well-known manifest
|
|
1270
|
+
* and applies the same canonicalization before comparing digests.
|
|
1271
|
+
*/
|
|
1272
|
+
buildPresentationEnvelope(reason) {
|
|
1273
|
+
const manifests = Array.isArray(this._manifest) ? this._manifest : this._manifest ? [this._manifest] : [];
|
|
1274
|
+
if (manifests.length === 0 && !reason) {
|
|
1275
|
+
return void 0;
|
|
1276
|
+
}
|
|
1277
|
+
const primary = manifests[0];
|
|
1278
|
+
let displayName;
|
|
1279
|
+
let manifestId;
|
|
1280
|
+
let manifestDigest;
|
|
1281
|
+
const envelopeManifests = [];
|
|
1282
|
+
for (const m of manifests) {
|
|
1283
|
+
envelopeManifests.push({
|
|
1284
|
+
name: m.name,
|
|
1285
|
+
appId: m.app_id,
|
|
1286
|
+
// Deep-copy to prevent later mutations from affecting the digest.
|
|
1287
|
+
payload: JSON.parse(JSON.stringify(m))
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
if (primary) {
|
|
1291
|
+
displayName = primary.name;
|
|
1292
|
+
manifestId = primary.app_id;
|
|
1293
|
+
manifestDigest = canonicalOpenKeyManifestSha256Hex(primary);
|
|
1294
|
+
}
|
|
1295
|
+
return {
|
|
1296
|
+
protocolVersion: 1,
|
|
1297
|
+
displayName,
|
|
1298
|
+
reason: reason && reason.length > 0 ? reason : void 0,
|
|
1299
|
+
manifestId,
|
|
1300
|
+
manifestDigest,
|
|
1301
|
+
manifests: envelopeManifests.length > 0 ? envelopeManifests : void 0
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Complete sign-in with a versioned OpenKey authorization result.
|
|
1306
|
+
*
|
|
1307
|
+
* Where `signInWithPreparedSession` completes with a signature over the
|
|
1308
|
+
* ORIGINAL prepared SIWE, this method completes with the SIWE bytes the
|
|
1309
|
+
* OpenKey widget actually presented and signed (`signedMessage`). It
|
|
1310
|
+
* exists because the versioned authorizeTinyCloud protocol lets the user
|
|
1311
|
+
* narrow capabilities; the returned bytes may differ from the original
|
|
1312
|
+
* request and are the only ones the signature actually verifies against.
|
|
1313
|
+
*
|
|
1314
|
+
* The `result` parameter is validated at the trust boundary via
|
|
1315
|
+
* `validateAuthorizationResultV1` (in sdk-core). Callers should validate
|
|
1316
|
+
* BEFORE calling this method so unknown/missing fields are rejected
|
|
1317
|
+
* before any session state is created.
|
|
1318
|
+
*
|
|
1319
|
+
* Validation performed here (defence-in-depth: even a "valid" v1 payload
|
|
1320
|
+
* must clear these checks before any session state is created):
|
|
1321
|
+
* 1. `result.address` matches the local signer's address.
|
|
1322
|
+
* 2. `result.signature` recovers to `result.address` when verified against
|
|
1323
|
+
* `result.signedMessage`.
|
|
1324
|
+
* 3. Every immutable SIWE header field (domain, address, URI, version,
|
|
1325
|
+
* chainId, nonce, issuedAt) in the original prepared SIWE is preserved
|
|
1326
|
+
* byte-for-byte in `result.signedMessage`. `statement` is NOT in this
|
|
1327
|
+
* list because the WASM renders it from the ReCap contents; it MUST
|
|
1328
|
+
* change when the ReCap is narrowed.
|
|
1329
|
+
* 4. Every capability in `result.signedMessage`'s ReCap block was present
|
|
1330
|
+
* in the original prepared SIWE — narrowing OK, broadening rejected.
|
|
1331
|
+
* 5. Every `result.selectedActionKeys` entry corresponds to a
|
|
1332
|
+
* (resource, action) pair in the returned SIWE.
|
|
1333
|
+
*/
|
|
1334
|
+
async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
|
|
1335
|
+
if (result.protocolVersion !== 1) {
|
|
1336
|
+
throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
|
|
1337
|
+
}
|
|
1338
|
+
if (!result.signedMessage) {
|
|
1339
|
+
throw new Error("OpenKey result must include signedMessage");
|
|
1340
|
+
}
|
|
1341
|
+
if (!prepared.siwe) {
|
|
1342
|
+
throw new Error(
|
|
1343
|
+
"signInWithOpenKeyResult requires prepared.siwe (the SDK-generated SIWE the sign-in was proposed with)"
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
const expectedAddress = canonicalizeAddress(await this.signer.getAddress());
|
|
1347
|
+
if (canonicalizeAddress(result.address) !== expectedAddress) {
|
|
1348
|
+
throw new Error(
|
|
1349
|
+
`OpenKey returned signature from ${result.address} but expected ${expectedAddress}`
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
let parsedSignedSiwe;
|
|
1353
|
+
try {
|
|
1354
|
+
parsedSignedSiwe = new SiweMessage(result.signedMessage);
|
|
1355
|
+
} catch (err) {
|
|
1356
|
+
throw new Error(
|
|
1357
|
+
`OpenKey signedMessage is not a parseable SIWE: ${err.message}`
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
const verifyResult = await parsedSignedSiwe.verify(
|
|
1361
|
+
{ signature: result.signature },
|
|
1362
|
+
{ suppressExceptions: true }
|
|
1363
|
+
);
|
|
1364
|
+
if (!verifyResult.success) {
|
|
1365
|
+
const errData = verifyResult.error;
|
|
1366
|
+
const type = errData && typeof errData === "object" && "type" in errData ? errData.type : void 0;
|
|
1367
|
+
const details = errData && typeof errData === "object" && "expected" in errData ? ` (expected=${errData.expected}, received=${errData.received})` : "";
|
|
1368
|
+
throw new Error(
|
|
1369
|
+
`OpenKey signature verification failed${type ? `: ${type}` : ""}${details}`
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
if (canonicalizeAddress(parsedSignedSiwe.address) !== expectedAddress) {
|
|
1373
|
+
throw new Error(
|
|
1374
|
+
`OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
|
|
1378
|
+
const originalFields = extractImmutableSiweFields(prepared.siwe);
|
|
1379
|
+
const signedFields = extractImmutableSiweFields(result.signedMessage);
|
|
1380
|
+
const changedFields = diffImmutableSiweFields(originalFields, signedFields, {
|
|
1381
|
+
originalHasRecap
|
|
1382
|
+
});
|
|
1383
|
+
if (changedFields.length > 0) {
|
|
1384
|
+
throw new Error(
|
|
1385
|
+
`OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
let originalCaps;
|
|
1389
|
+
let signedCaps;
|
|
1390
|
+
try {
|
|
1391
|
+
originalCaps = extractRecapAttenuations(prepared.siwe);
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
throw new Error(
|
|
1394
|
+
`Failed to decode prepared SIWE recap: ${err.message}`
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
try {
|
|
1398
|
+
signedCaps = extractRecapAttenuations(result.signedMessage);
|
|
1399
|
+
} catch (err) {
|
|
1400
|
+
throw new Error(
|
|
1401
|
+
`Failed to decode OpenKey signedMessage recap: ${err.message}`
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
const unauthorized = unauthorizedRecapCapabilities(signedCaps, originalCaps);
|
|
1405
|
+
if (unauthorized.length > 0) {
|
|
1406
|
+
const previewLimit = 5;
|
|
1407
|
+
const preview = unauthorized.slice(0, previewLimit).map((u) => `${u.resource}::${u.action}`).join(", ");
|
|
1408
|
+
const overflow = unauthorized.length > previewLimit ? ` (and ${unauthorized.length - previewLimit} more)` : "";
|
|
1409
|
+
throw new Error(
|
|
1410
|
+
`OpenKey signedMessage broadened authorization beyond the original request: ${preview}${overflow}`
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
if (!result.signedMessage.includes(prepared.spaceId)) {
|
|
1414
|
+
throw new Error(
|
|
1415
|
+
`OpenKey signedMessage does not reference the expected spaceId ${prepared.spaceId}`
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
const grantedPairs = /* @__PURE__ */ new Set();
|
|
1419
|
+
const grantedFourPartIndex = /* @__PURE__ */ new Map();
|
|
1420
|
+
for (const [resource, actions] of Object.entries(signedCaps)) {
|
|
1421
|
+
const { space, path } = parseCanonicalRecapResource(resource);
|
|
1422
|
+
for (const action of Object.keys(actions)) {
|
|
1423
|
+
grantedPairs.add(`${resource}\0${action}`);
|
|
1424
|
+
const slashIdx = action.indexOf("/");
|
|
1425
|
+
const service = slashIdx > 0 ? action.slice(0, slashIdx) : "";
|
|
1426
|
+
if (service) {
|
|
1427
|
+
grantedFourPartIndex.set(
|
|
1428
|
+
`${service}\0${space}\0${path}\0${action}`,
|
|
1429
|
+
`${resource}\0${action}`
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
const isStructurallyRequired = (action) => action === "tinycloud.capabilities/read" || action === "capabilities/read";
|
|
1435
|
+
const nonRequiredGrantedPairs = /* @__PURE__ */ new Set();
|
|
1436
|
+
for (const pair of grantedPairs) {
|
|
1437
|
+
const idx = pair.indexOf("\0");
|
|
1438
|
+
const action = idx >= 0 ? pair.slice(idx + 1) : pair;
|
|
1439
|
+
if (!isStructurallyRequired(action)) {
|
|
1440
|
+
nonRequiredGrantedPairs.add(pair);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
const allSignedGrantedPairs = new Set(grantedPairs);
|
|
1444
|
+
const seenRawKeys = /* @__PURE__ */ new Set();
|
|
1445
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
1446
|
+
if (seenRawKeys.has(rawKey)) {
|
|
1447
|
+
throw new Error(
|
|
1448
|
+
`OpenKey selectedActionKeys contains duplicate entry ${rawKey}`
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
seenRawKeys.add(rawKey);
|
|
1452
|
+
}
|
|
1453
|
+
const selectedResourceActionPairs = /* @__PURE__ */ new Set();
|
|
1454
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
1455
|
+
const parts = rawKey.split("\0");
|
|
1456
|
+
if (parts.length !== 4) {
|
|
1457
|
+
throw new Error(
|
|
1458
|
+
`OpenKey selectedActionKeys entry is malformed (expected canonical 4-part service\\0space\\0path\\0ability): ${rawKey}`
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
const canonical = grantedFourPartIndex.get(rawKey);
|
|
1462
|
+
if (!canonical) {
|
|
1463
|
+
throw new Error(
|
|
1464
|
+
`OpenKey selectedActionKeys entry ${rawKey} is not covered by any granted capability`
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
selectedResourceActionPairs.add(canonical);
|
|
1468
|
+
}
|
|
1469
|
+
if (nonRequiredGrantedPairs.size > 0) {
|
|
1470
|
+
const missingPairs = [];
|
|
1471
|
+
for (const pair of nonRequiredGrantedPairs) {
|
|
1472
|
+
if (!selectedResourceActionPairs.has(pair)) {
|
|
1473
|
+
missingPairs.push(pair);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (missingPairs.length > 0) {
|
|
1477
|
+
const previewLimit = 5;
|
|
1478
|
+
const preview = missingPairs.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
1479
|
+
const overflow = missingPairs.length > previewLimit ? ` (and ${missingPairs.length - previewLimit} more)` : "";
|
|
1480
|
+
throw new Error(
|
|
1481
|
+
`OpenKey selectedActionKeys is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
if (allSignedGrantedPairs.size > 0 && result.permissions.length === 0) {
|
|
1486
|
+
throw new Error(
|
|
1487
|
+
"OpenKey permissions is empty but signedMessage carries capabilities \u2014 refusing to accept a session without an authoritative grant list"
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
const permissionCanonicalKeys = /* @__PURE__ */ new Set();
|
|
1491
|
+
const permissionPairsCovered = /* @__PURE__ */ new Set();
|
|
1492
|
+
for (const perm of result.permissions) {
|
|
1493
|
+
const { service, space, path, actions } = perm;
|
|
1494
|
+
if (typeof service !== "string" || typeof space !== "string" || typeof path !== "string") {
|
|
1495
|
+
throw new Error(
|
|
1496
|
+
`OpenKey permissions entry has non-string service/space/path (service=${service}, space=${space}, path=${path})`
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
1500
|
+
throw new Error(
|
|
1501
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has empty actions \u2014 a capability-bearing entry must list at least one action`
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
const canonicalKey = `${service}\0${space}\0${path}`;
|
|
1505
|
+
if (permissionCanonicalKeys.has(canonicalKey)) {
|
|
1506
|
+
throw new Error(
|
|
1507
|
+
`OpenKey permissions has duplicate entry for (${service} ${space} ${path})`
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
permissionCanonicalKeys.add(canonicalKey);
|
|
1511
|
+
let expectedResource;
|
|
1512
|
+
if (space.startsWith("tinycloud:")) {
|
|
1513
|
+
const shortService = service.startsWith("tinycloud.") ? service.slice("tinycloud.".length) : service;
|
|
1514
|
+
expectedResource = path ? `${space}/${shortService}/${path}` : `${space}/${shortService}`;
|
|
1515
|
+
} else {
|
|
1516
|
+
if (path) {
|
|
1517
|
+
throw new Error(
|
|
1518
|
+
`OpenKey permissions entry (${service} ${space} ${path}) carries a path on a non-tinycloud: resource URI, which is not a recognized encoding`
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
expectedResource = space;
|
|
1522
|
+
}
|
|
1523
|
+
let matchedResource;
|
|
1524
|
+
if (signedCaps[expectedResource] !== void 0) {
|
|
1525
|
+
matchedResource = expectedResource;
|
|
1526
|
+
} else {
|
|
1527
|
+
throw new Error(
|
|
1528
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims grants not confirmed in signedMessage capabilities`
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
const grantedActionsForResource = new Set(
|
|
1532
|
+
Object.keys(signedCaps[matchedResource])
|
|
1533
|
+
);
|
|
1534
|
+
for (const action of actions) {
|
|
1535
|
+
if (typeof action !== "string" || !action) {
|
|
1536
|
+
throw new Error(
|
|
1537
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has non-string action ${String(action)}`
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
if (!grantedActionsForResource.has(action)) {
|
|
1541
|
+
throw new Error(
|
|
1542
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims action ${action} not present in signedMessage capabilities`
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
permissionPairsCovered.add(`${matchedResource}\0${action}`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
if (allSignedGrantedPairs.size > 0) {
|
|
1549
|
+
const missingFromPerms = [];
|
|
1550
|
+
for (const pair of allSignedGrantedPairs) {
|
|
1551
|
+
if (!permissionPairsCovered.has(pair)) {
|
|
1552
|
+
missingFromPerms.push(pair);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
if (missingFromPerms.length > 0) {
|
|
1556
|
+
const previewLimit = 5;
|
|
1557
|
+
const preview = missingFromPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
1558
|
+
const overflow = missingFromPerms.length > previewLimit ? ` (and ${missingFromPerms.length - previewLimit} more)` : "";
|
|
1559
|
+
throw new Error(
|
|
1560
|
+
`OpenKey permissions is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
const extrasInPerms = [];
|
|
1564
|
+
for (const pair of permissionPairsCovered) {
|
|
1565
|
+
if (!allSignedGrantedPairs.has(pair)) {
|
|
1566
|
+
extrasInPerms.push(pair);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
if (extrasInPerms.length > 0) {
|
|
1570
|
+
const previewLimit = 5;
|
|
1571
|
+
const preview = extrasInPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
1572
|
+
const overflow = extrasInPerms.length > previewLimit ? ` (and ${extrasInPerms.length - previewLimit} more)` : "";
|
|
1573
|
+
throw new Error(
|
|
1574
|
+
`OpenKey permissions contains entries not present in the signed authority: ${preview}${overflow}`
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
void nonRequiredGrantedPairs;
|
|
1579
|
+
return this.signInWithPreparedSession(
|
|
1580
|
+
{
|
|
1581
|
+
siwe: result.signedMessage,
|
|
1582
|
+
jwk: prepared.jwk,
|
|
1583
|
+
spaceId: prepared.spaceId,
|
|
1584
|
+
verificationMethod: prepared.verificationMethod
|
|
1585
|
+
},
|
|
1586
|
+
result.signature,
|
|
1587
|
+
keyId,
|
|
1588
|
+
jwk
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1141
1591
|
/**
|
|
1142
1592
|
* Clear persisted session data.
|
|
1143
1593
|
*/
|
|
@@ -1229,12 +1679,14 @@ import {
|
|
|
1229
1679
|
SharingService,
|
|
1230
1680
|
UnsupportedFeatureError,
|
|
1231
1681
|
makePublicSpaceId,
|
|
1232
|
-
ACCOUNT_REGISTRY_SPACE,
|
|
1682
|
+
ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2,
|
|
1683
|
+
BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST,
|
|
1233
1684
|
BOOTSTRAP_SESSION_REQUESTS,
|
|
1234
1685
|
SECRET_RECORDS_SCHEMA,
|
|
1235
1686
|
SECRETS_SPACE as SECRETS_SPACE2,
|
|
1236
1687
|
TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
|
|
1237
1688
|
bootstrapSteps,
|
|
1689
|
+
ErrorCodes as ErrorCodes2,
|
|
1238
1690
|
ENCRYPTION_PERMISSION_SERVICE as ENCRYPTION_PERMISSION_SERVICE2,
|
|
1239
1691
|
CaveatedDelegationUnsupportedError,
|
|
1240
1692
|
PermissionNotInManifestError,
|
|
@@ -1261,7 +1713,9 @@ import {
|
|
|
1261
1713
|
parseNetworkId as parseNetworkId2,
|
|
1262
1714
|
resolveSecretPath as resolveSecretPath2,
|
|
1263
1715
|
validateOwnerSharePolicyRegistrationBytes,
|
|
1264
|
-
validateShareDeliveryAuthorizationBytes
|
|
1716
|
+
validateShareDeliveryAuthorizationBytes,
|
|
1717
|
+
verifyEip191MessageSignature,
|
|
1718
|
+
signCompactUcanRootAuthorization
|
|
1265
1719
|
} from "@tinycloud/sdk-core";
|
|
1266
1720
|
import {
|
|
1267
1721
|
parsePermissionHint
|
|
@@ -1452,7 +1906,7 @@ function createWasmKeyProvider(sessionManager) {
|
|
|
1452
1906
|
// src/delegateToHelpers.ts
|
|
1453
1907
|
import {
|
|
1454
1908
|
parseExpiry,
|
|
1455
|
-
SiweMessage,
|
|
1909
|
+
SiweMessage as SiweMessage2,
|
|
1456
1910
|
EXPIRY as EXPIRY2
|
|
1457
1911
|
} from "@tinycloud/sdk-core";
|
|
1458
1912
|
function legacyParamsToPermissionEntries(actions, path, spaceIdOverride) {
|
|
@@ -1501,7 +1955,7 @@ function resolveExpiryMs(expiry) {
|
|
|
1501
1955
|
return parseExpiry(expiry);
|
|
1502
1956
|
}
|
|
1503
1957
|
function extractSiweExpiration(siwe) {
|
|
1504
|
-
const parsed = new
|
|
1958
|
+
const parsed = new SiweMessage2(siwe);
|
|
1505
1959
|
if (parsed.expirationTime === void 0 || parsed.expirationTime === null) {
|
|
1506
1960
|
return void 0;
|
|
1507
1961
|
}
|
|
@@ -1703,6 +2157,9 @@ var DEFAULT_HOST = "https://node.tinycloud.xyz";
|
|
|
1703
2157
|
var DEFAULT_ENCRYPTION_NETWORK_NAME = "default";
|
|
1704
2158
|
var NETWORK_CREATE_ACTION2 = ENCRYPTION2.NETWORK_CREATE;
|
|
1705
2159
|
var DECRYPT_ACTION2 = ENCRYPTION2.DECRYPT;
|
|
2160
|
+
var BOOTSTRAP_COMPLETION_MARKER_KEY = "system/bootstrap/complete";
|
|
2161
|
+
var BOOTSTRAP_COMPLETION_MARKER_VERSION = 1;
|
|
2162
|
+
var ACCEPTED_MARKER_VERSIONS = [BOOTSTRAP_COMPLETION_MARKER_VERSION];
|
|
1706
2163
|
var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
|
|
1707
2164
|
function isSecretPayload(value) {
|
|
1708
2165
|
return typeof value === "object" && value !== null && "value" in value && typeof value.value === "string" && "createdAt" in value && typeof value.createdAt === "string" && "updatedAt" in value && typeof value.updatedAt === "string";
|
|
@@ -1726,6 +2183,18 @@ var ROOT_DELEGATION_ACTIONS = [
|
|
|
1726
2183
|
DUCKDB2.ALL
|
|
1727
2184
|
];
|
|
1728
2185
|
var DEFAULT_SESSION_EXPIRATION_MS = EXPIRY3.SESSION_MS;
|
|
2186
|
+
function unifiedRootAttenuation(capabilities) {
|
|
2187
|
+
const attenuation = {};
|
|
2188
|
+
for (const capability of capabilities) {
|
|
2189
|
+
if (attenuation[capability.resource] !== void 0) throw new Error("unified owner root contains a duplicate resource");
|
|
2190
|
+
attenuation[capability.resource] = capability.kind === "encryption" ? { [capability.action]: [{}] } : Object.fromEntries(capability.actions.map((action) => [action, [{
|
|
2191
|
+
type: "xyz.tinycloud.resource/selector",
|
|
2192
|
+
kind: capability.selector,
|
|
2193
|
+
value: capability.resource
|
|
2194
|
+
}]]));
|
|
2195
|
+
}
|
|
2196
|
+
return attenuation;
|
|
2197
|
+
}
|
|
1729
2198
|
function decodeAuthorizationBytes(authorization) {
|
|
1730
2199
|
const encoded = authorization.replace(/^Bearer /i, "");
|
|
1731
2200
|
const match = /^([A-Za-z0-9_-]+)(={1,2})?$/.exec(encoded);
|
|
@@ -1745,6 +2214,9 @@ function decodeAuthorizationBytes(authorization) {
|
|
|
1745
2214
|
function isOpenKeyAutoSignStrategy(strategy) {
|
|
1746
2215
|
return strategy?.openKeyAutoSign === true;
|
|
1747
2216
|
}
|
|
2217
|
+
function openKeyApproval(strategy) {
|
|
2218
|
+
return strategy?.openKeyRequestApproval;
|
|
2219
|
+
}
|
|
1748
2220
|
function isInteractiveSigner(config) {
|
|
1749
2221
|
if (config.privateKey) {
|
|
1750
2222
|
return false;
|
|
@@ -2102,6 +2574,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2102
2574
|
* hosting is confirmed against the session that was active at the time.
|
|
2103
2575
|
*/
|
|
2104
2576
|
this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
|
|
2577
|
+
/** Serializes account-registry writes for the active node instance. */
|
|
2578
|
+
this.accountRegistryTail = Promise.resolve();
|
|
2105
2579
|
/**
|
|
2106
2580
|
* True when the last signIn() detected an interactive signer and skipped
|
|
2107
2581
|
* client-side bootstrap. Apps can read this to know whether bootstrap was
|
|
@@ -2110,9 +2584,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2110
2584
|
this._bootstrapSkipped = false;
|
|
2111
2585
|
/**
|
|
2112
2586
|
* Outcome of the last signIn()'s account-bootstrap attempt. `skipped` is
|
|
2113
|
-
* true when bootstrap did not complete (interactive signer,
|
|
2114
|
-
*
|
|
2115
|
-
*
|
|
2587
|
+
* true when bootstrap did not complete in this invocation (interactive signer,
|
|
2588
|
+
* auto-bootstrap disabled, already provisioned, or a decision/provisioning
|
|
2589
|
+
* step failed). `already-provisioned` means the bootstrap was not run here;
|
|
2590
|
+
* it does not mean a prior provisioning attempt failed to complete.
|
|
2591
|
+
* `reason` carries the cause so apps can surface a "finish account setup"
|
|
2592
|
+
* call-to-action. `warnings` is
|
|
2116
2593
|
* present when bootstrap completed, but one or more steps recovered from
|
|
2117
2594
|
* an ambiguous failure (e.g. a KV batch write reconciled via per-space
|
|
2118
2595
|
* fallback) — clean vs. recovered runs are otherwise byte-identical.
|
|
@@ -2295,7 +2772,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2295
2772
|
nonce: config.nonce,
|
|
2296
2773
|
siweConfig: config.siweConfig,
|
|
2297
2774
|
manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
|
|
2298
|
-
capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
|
|
2775
|
+
capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
|
|
2299
2776
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
|
|
2300
2777
|
});
|
|
2301
2778
|
this.tc = new TinyCloud(this.auth, {
|
|
@@ -2385,6 +2862,15 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2385
2862
|
get sessionDid() {
|
|
2386
2863
|
return this.sessionManager.getDID(this.sessionKeyId);
|
|
2387
2864
|
}
|
|
2865
|
+
/** Bare did:key principal used as the credential subject and holder binding. */
|
|
2866
|
+
get credentialHolderDid() {
|
|
2867
|
+
return this.sessionDid.split("#", 1)[0];
|
|
2868
|
+
}
|
|
2869
|
+
/** Canonical verification method for the active credential holder key. */
|
|
2870
|
+
get credentialHolderKid() {
|
|
2871
|
+
const holder = this.credentialHolderDid;
|
|
2872
|
+
return `${holder}#${holder.slice("did:key:".length)}`;
|
|
2873
|
+
}
|
|
2388
2874
|
/**
|
|
2389
2875
|
* Return the current session's signed ReCap capabilities after the session
|
|
2390
2876
|
* has been authenticated or restored. This is intentionally distinct from
|
|
@@ -2435,7 +2921,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2435
2921
|
if (!this._address) {
|
|
2436
2922
|
return void 0;
|
|
2437
2923
|
}
|
|
2438
|
-
return this.wasmBindings.makeSpaceId(this._address, this._chainId,
|
|
2924
|
+
return this.wasmBindings.makeSpaceId(this._address, this._chainId, ACCOUNT_REGISTRY_SPACE2);
|
|
2439
2925
|
}
|
|
2440
2926
|
/**
|
|
2441
2927
|
* Account-level application and delegation helpers.
|
|
@@ -2486,6 +2972,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2486
2972
|
"Cannot signIn() in session-only mode. Provide a privateKey in config to create your own space."
|
|
2487
2973
|
);
|
|
2488
2974
|
}
|
|
2975
|
+
await this.accountRegistryTail;
|
|
2489
2976
|
await this.wasmBindings.ensureInitialized?.();
|
|
2490
2977
|
this._address = canonicalizeAddress2(await this.signer.getAddress());
|
|
2491
2978
|
this._chainId = await this.signer.getChainId();
|
|
@@ -2538,6 +3025,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2538
3025
|
this._bootstrapSkipped = false;
|
|
2539
3026
|
this._bootstrapStatus = { skipped: false };
|
|
2540
3027
|
if (this.config.autoBootstrapAccount === false) {
|
|
3028
|
+
this._bootstrapStatus = { skipped: true, reason: "auto-bootstrap-disabled" };
|
|
2541
3029
|
return false;
|
|
2542
3030
|
}
|
|
2543
3031
|
if (!this.auth || !this._address) {
|
|
@@ -2552,11 +3040,14 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2552
3040
|
return false;
|
|
2553
3041
|
}
|
|
2554
3042
|
const steps = bootstrapSteps(this._address, this._chainId);
|
|
2555
|
-
if (!await this.isFreshBootstrapAccount(steps)) {
|
|
2556
|
-
return false;
|
|
2557
|
-
}
|
|
2558
3043
|
try {
|
|
2559
|
-
const
|
|
3044
|
+
const decision = await this.resolveBootstrapDecision(steps);
|
|
3045
|
+
if (decision.action === "skip") {
|
|
3046
|
+
this._bootstrapStatus = { skipped: true, reason: "already-provisioned" };
|
|
3047
|
+
return false;
|
|
3048
|
+
}
|
|
3049
|
+
const warnings = await this.runAccountBootstrap(steps, { mode: decision.mode });
|
|
3050
|
+
await this.writeBootstrapCompletionMarker(steps);
|
|
2560
3051
|
if (warnings.length > 0) {
|
|
2561
3052
|
this._bootstrapStatus = { skipped: false, warnings };
|
|
2562
3053
|
this.notificationHandler.warning(
|
|
@@ -2575,32 +3066,92 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2575
3066
|
}
|
|
2576
3067
|
return true;
|
|
2577
3068
|
}
|
|
2578
|
-
async
|
|
3069
|
+
async resolveBootstrapDecision(steps) {
|
|
2579
3070
|
const enshrinedSpaceIds = /* @__PURE__ */ new Set();
|
|
2580
3071
|
for (const step of steps) {
|
|
2581
3072
|
if (step.kind === "session") {
|
|
2582
3073
|
enshrinedSpaceIds.add(step.spaceId);
|
|
2583
3074
|
}
|
|
2584
3075
|
}
|
|
2585
|
-
const
|
|
3076
|
+
const auth = this.auth;
|
|
3077
|
+
if (!auth) {
|
|
3078
|
+
throw new Error("Account bootstrap requires an active wallet session");
|
|
3079
|
+
}
|
|
3080
|
+
const skipped = auth.lastActivationSkippedSpaceIds;
|
|
2586
3081
|
if (skipped.some((spaceId) => enshrinedSpaceIds.has(spaceId))) {
|
|
2587
|
-
return
|
|
3082
|
+
return { action: "run", mode: "fresh" };
|
|
2588
3083
|
}
|
|
2589
3084
|
try {
|
|
2590
|
-
const
|
|
2591
|
-
|
|
2592
|
-
|
|
3085
|
+
const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
|
|
3086
|
+
const markerPermission = {
|
|
3087
|
+
service: "tinycloud.kv",
|
|
3088
|
+
space: accountSpaceId,
|
|
3089
|
+
path: BOOTSTRAP_COMPLETION_MARKER_KEY,
|
|
3090
|
+
actions: ["get"]
|
|
3091
|
+
};
|
|
3092
|
+
if (!this.hasRuntimePermissions([markerPermission])) {
|
|
3093
|
+
return { action: "run", mode: "repair" };
|
|
2593
3094
|
}
|
|
2594
|
-
|
|
3095
|
+
const marker = await this.readBootstrapCompletionMarker();
|
|
3096
|
+
if (marker.ok) {
|
|
3097
|
+
if (this.isAcceptedBootstrapCompletionMarker(marker.data.data)) {
|
|
3098
|
+
return { action: "skip" };
|
|
3099
|
+
}
|
|
3100
|
+
return { action: "run", mode: "repair" };
|
|
3101
|
+
}
|
|
3102
|
+
if (marker.error.code === ErrorCodes2.KV_NOT_FOUND && this.markerReadIsUnhostedSpace(marker.error.meta)) {
|
|
3103
|
+
return { action: "run", mode: "fresh" };
|
|
3104
|
+
}
|
|
3105
|
+
} catch (err) {
|
|
3106
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3107
|
+
this.notificationHandler.warning(
|
|
3108
|
+
`Account bootstrap decision failed; attempting one bounded repair: ${reason}`
|
|
3109
|
+
);
|
|
3110
|
+
console.warn(`[TinyCloudNode] account bootstrap decision failed: ${reason}`);
|
|
3111
|
+
return { action: "run", mode: "repair" };
|
|
2595
3112
|
}
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
3113
|
+
return { action: "run", mode: "repair" };
|
|
3114
|
+
}
|
|
3115
|
+
async readBootstrapCompletionMarker() {
|
|
3116
|
+
return this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).get(
|
|
3117
|
+
BOOTSTRAP_COMPLETION_MARKER_KEY
|
|
3118
|
+
);
|
|
3119
|
+
}
|
|
3120
|
+
async writeBootstrapCompletionMarker(steps) {
|
|
3121
|
+
if (!this._address) {
|
|
3122
|
+
throw new Error("Bootstrap completion marker requires an active wallet session");
|
|
2601
3123
|
}
|
|
3124
|
+
const canonicalStepIds = new Set(
|
|
3125
|
+
bootstrapSteps(this._address, this._chainId).map((step) => step.id)
|
|
3126
|
+
);
|
|
3127
|
+
const providedStepIds = new Set(steps.map((step) => step.id));
|
|
3128
|
+
if (canonicalStepIds.size !== providedStepIds.size || [...canonicalStepIds].some((stepId) => !providedStepIds.has(stepId))) {
|
|
3129
|
+
throw new Error("Bootstrap completion marker requires the canonical bootstrap step set");
|
|
3130
|
+
}
|
|
3131
|
+
const marker = {
|
|
3132
|
+
v: BOOTSTRAP_COMPLETION_MARKER_VERSION,
|
|
3133
|
+
stepIds: [...canonicalStepIds],
|
|
3134
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3135
|
+
};
|
|
3136
|
+
const written = await this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).put(
|
|
3137
|
+
BOOTSTRAP_COMPLETION_MARKER_KEY,
|
|
3138
|
+
marker
|
|
3139
|
+
);
|
|
3140
|
+
if (!written.ok) {
|
|
3141
|
+
throw new Error(`Failed to write bootstrap completion marker: ${written.error.message}`);
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
isAcceptedBootstrapCompletionMarker(value) {
|
|
3145
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3146
|
+
const entries = Object.entries(value);
|
|
3147
|
+
const markerValue = (name) => entries.find(([key]) => key === name)?.[1];
|
|
3148
|
+
const version = markerValue("v");
|
|
3149
|
+
return typeof version === "number" && Number.isInteger(version) && ACCEPTED_MARKER_VERSIONS.some((acceptedVersion) => acceptedVersion === version);
|
|
3150
|
+
}
|
|
3151
|
+
markerReadIsUnhostedSpace(meta) {
|
|
3152
|
+
return meta?.status === 404;
|
|
2602
3153
|
}
|
|
2603
|
-
async runAccountBootstrap(steps) {
|
|
3154
|
+
async runAccountBootstrap(steps, { mode } = { mode: "fresh" }) {
|
|
2604
3155
|
if (!this.auth || !this._address) {
|
|
2605
3156
|
throw new Error("Account bootstrap requires an active wallet session");
|
|
2606
3157
|
}
|
|
@@ -2701,9 +3252,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2701
3252
|
if (step.kind === "seed-applications") {
|
|
2702
3253
|
const registered = await this.account.applications.register(
|
|
2703
3254
|
step.manifests.length > 0 ? [...step.manifests] : TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
|
|
2704
|
-
//
|
|
2705
|
-
// manifest-hash pre-read
|
|
2706
|
-
// it guards is an INSERT OR REPLACE and is safe to repeat.
|
|
3255
|
+
// The write is an INSERT OR REPLACE and is safe to repeat during a
|
|
3256
|
+
// repair, so skip the manifest-hash pre-read.
|
|
2707
3257
|
{ assumeUnregistered: true }
|
|
2708
3258
|
);
|
|
2709
3259
|
if (!registered.ok) {
|
|
@@ -2711,7 +3261,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2711
3261
|
}
|
|
2712
3262
|
}
|
|
2713
3263
|
if (step.kind === "encryption-network-create") {
|
|
2714
|
-
await this.ensureEncryptionNetwork(step.networkId, { assumeMissing:
|
|
3264
|
+
await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: mode === "fresh" });
|
|
2715
3265
|
}
|
|
2716
3266
|
if (step.kind === "secret-records-schema") {
|
|
2717
3267
|
const db = this.sqlForSpace(step.spaceId).db(step.database);
|
|
@@ -2894,7 +3444,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2894
3444
|
if (!this.auth || !this.signer) {
|
|
2895
3445
|
throw new Error("Manifest registry write requires wallet mode");
|
|
2896
3446
|
}
|
|
2897
|
-
const accountSpaceId = this.ownedSpaceId(
|
|
3447
|
+
const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
|
|
2898
3448
|
await this.ensureOwnedSpaceHostedById(accountSpaceId);
|
|
2899
3449
|
const result = await this.account.applications.register(request.manifests);
|
|
2900
3450
|
if (!result.ok) {
|
|
@@ -2904,16 +3454,48 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2904
3454
|
}
|
|
2905
3455
|
}
|
|
2906
3456
|
scheduleAccountRegistrySync() {
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
3457
|
+
const session = this.currentTinyCloudSession();
|
|
3458
|
+
if (!session || this.pendingAccountRegistrySync?.session === session) {
|
|
3459
|
+
return;
|
|
3460
|
+
}
|
|
3461
|
+
const promise = this.enqueueAccountRegistryOperation(session, async () => {
|
|
3462
|
+
await this.withAccountRegistryRetry(async () => {
|
|
3463
|
+
if (this.currentTinyCloudSession() !== session) {
|
|
3464
|
+
return;
|
|
3465
|
+
}
|
|
3466
|
+
await this.account.index.ensure();
|
|
3467
|
+
if (this.currentTinyCloudSession() !== session) {
|
|
3468
|
+
return;
|
|
3469
|
+
}
|
|
3470
|
+
await this.writeManifestRegistryRecords();
|
|
3471
|
+
if (this.currentTinyCloudSession() !== session) {
|
|
3472
|
+
return;
|
|
2914
3473
|
}
|
|
3474
|
+
if (this.currentSessionCanListSpaces()) {
|
|
3475
|
+
const spaces = await this.account.spaces.syncAccessible();
|
|
3476
|
+
if (!spaces.ok) {
|
|
3477
|
+
throw new Error(`Failed to sync account spaces: ${spaces.error.message}`);
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
});
|
|
3481
|
+
});
|
|
3482
|
+
this.pendingAccountRegistrySync = { session, promise };
|
|
3483
|
+
const clearPendingSync = () => {
|
|
3484
|
+
if (this.pendingAccountRegistrySync?.promise === promise) {
|
|
3485
|
+
this.pendingAccountRegistrySync = void 0;
|
|
2915
3486
|
}
|
|
3487
|
+
};
|
|
3488
|
+
void promise.then(clearPendingSync, clearPendingSync);
|
|
3489
|
+
}
|
|
3490
|
+
enqueueAccountRegistryOperation(session, task) {
|
|
3491
|
+
const operation = this.accountRegistryTail.then(async () => {
|
|
3492
|
+
if (this.currentTinyCloudSession() === session) {
|
|
3493
|
+
await task();
|
|
3494
|
+
}
|
|
3495
|
+
});
|
|
3496
|
+
this.accountRegistryTail = operation.catch(() => {
|
|
2916
3497
|
});
|
|
3498
|
+
return operation;
|
|
2917
3499
|
}
|
|
2918
3500
|
/**
|
|
2919
3501
|
* Whether the current primary session may invoke `tinycloud.space/list`.
|
|
@@ -3066,7 +3648,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3066
3648
|
* @returns The hosted space URI.
|
|
3067
3649
|
*/
|
|
3068
3650
|
async hostOwnedSpace(name) {
|
|
3069
|
-
|
|
3651
|
+
const session = this.auth?.tinyCloudSession;
|
|
3652
|
+
if (!this.auth || !session) {
|
|
3070
3653
|
throw new Error("Not signed in. Call signIn() first.");
|
|
3071
3654
|
}
|
|
3072
3655
|
const spaceId = this.ownedSpaceId(name);
|
|
@@ -3082,22 +3665,26 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3082
3665
|
}
|
|
3083
3666
|
const activation = await activateSessionWithHost2(
|
|
3084
3667
|
host,
|
|
3085
|
-
|
|
3668
|
+
session.delegationHeader
|
|
3086
3669
|
);
|
|
3087
3670
|
if (!activation.success || activation.skipped?.includes(spaceId)) {
|
|
3088
3671
|
throw new Error(
|
|
3089
3672
|
`Failed to activate session for owned space ${spaceId}: ${activation.error ?? "space was skipped"}`
|
|
3090
3673
|
);
|
|
3091
3674
|
}
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3675
|
+
try {
|
|
3676
|
+
await this.enqueueAccountRegistryOperation(session, async () => {
|
|
3677
|
+
await this.account.spaces.register({
|
|
3678
|
+
spaceId,
|
|
3679
|
+
name,
|
|
3680
|
+
ownerDid: this.did,
|
|
3681
|
+
type: "owned",
|
|
3682
|
+
permissions: ["*"],
|
|
3683
|
+
status: "active"
|
|
3684
|
+
});
|
|
3685
|
+
});
|
|
3686
|
+
} catch {
|
|
3687
|
+
}
|
|
3101
3688
|
return spaceId;
|
|
3102
3689
|
}
|
|
3103
3690
|
/**
|
|
@@ -3132,23 +3719,20 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3132
3719
|
if (!this.auth || !this.auth.tinyCloudSession) {
|
|
3133
3720
|
throw new Error("Not signed in. Call signIn() first.");
|
|
3134
3721
|
}
|
|
3722
|
+
await this.accountRegistryTail;
|
|
3135
3723
|
const spaceId = this.ownedSpaceId(name);
|
|
3136
3724
|
if (await this.isOwnedSpaceRegistered(spaceId)) {
|
|
3137
3725
|
return spaceId;
|
|
3138
3726
|
}
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
permissions: ["*"],
|
|
3147
|
-
status: "active"
|
|
3148
|
-
});
|
|
3149
|
-
} catch {
|
|
3727
|
+
return this.hostOwnedSpace(name);
|
|
3728
|
+
}
|
|
3729
|
+
/** Resolve and authenticate the owner encoded by an owned-space URI. */
|
|
3730
|
+
credentialSpaceOwnerDid(spaceId) {
|
|
3731
|
+
const ownerDid = this.ownerDidFromSpaceId(spaceId);
|
|
3732
|
+
if (ownerDid === void 0 || !didPrincipalMatches2(ownerDid, this.did)) {
|
|
3733
|
+
throw new Error("Credential space owner does not match the active TinyCloud owner");
|
|
3150
3734
|
}
|
|
3151
|
-
return
|
|
3735
|
+
return ownerDid;
|
|
3152
3736
|
}
|
|
3153
3737
|
/**
|
|
3154
3738
|
* Check whether an owned space is already registered/hosted by consulting the
|
|
@@ -3693,6 +4277,106 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3693
4277
|
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
3694
4278
|
return signBytesWithJwk(bytes, session.jwk);
|
|
3695
4279
|
}
|
|
4280
|
+
/** Author a proofless Policy/v3 owner root with the authenticated holder key. */
|
|
4281
|
+
async createUnifiedOwnerRoot(input) {
|
|
4282
|
+
const ownerDid = this.credentialHolderDid;
|
|
4283
|
+
if (input.ownerDid !== ownerDid) throw new Error("unified owner root signer does not match owner DID");
|
|
4284
|
+
const facts = {
|
|
4285
|
+
role: input.role,
|
|
4286
|
+
mode: input.role === "policy-authority" ? "policy-source" : "conditional-mint",
|
|
4287
|
+
ownerDid,
|
|
4288
|
+
policyId: input.policyId,
|
|
4289
|
+
policyDigestHex: input.policyDigestHex,
|
|
4290
|
+
policyCid: input.policyCid,
|
|
4291
|
+
contentSourceDigestHex: input.contentSourceDigestHex,
|
|
4292
|
+
capabilityCeilingHashHex: input.capabilityCeilingHashHex,
|
|
4293
|
+
nativeProjectionHashHex: input.nativeProjectionHashHex,
|
|
4294
|
+
nodeAudience: input.nodeAudience,
|
|
4295
|
+
...input.role === "policy-enforcement" ? { enforcerDid: input.audienceDid } : {}
|
|
4296
|
+
};
|
|
4297
|
+
const root = await signCompactUcanRootAuthorization({
|
|
4298
|
+
issuerDid: ownerDid,
|
|
4299
|
+
audienceDid: input.audienceDid,
|
|
4300
|
+
attenuation: unifiedRootAttenuation(input.capabilities),
|
|
4301
|
+
facts: [facts],
|
|
4302
|
+
notBefore: Math.floor(input.notBefore.getTime() / 1e3),
|
|
4303
|
+
expiresAt: Math.floor(input.expiresAt.getTime() / 1e3),
|
|
4304
|
+
nonce: base64UrlEncode(crypto.getRandomValues(new Uint8Array(16))),
|
|
4305
|
+
sign: (bytes) => this.signSessionBytes(bytes)
|
|
4306
|
+
});
|
|
4307
|
+
const kv = input.capabilities.find((capability) => capability.kind === "kv");
|
|
4308
|
+
const resource = kv?.resource ?? "";
|
|
4309
|
+
const marker = resource.indexOf("/kv/");
|
|
4310
|
+
return {
|
|
4311
|
+
cid: root.cid,
|
|
4312
|
+
delegationHeader: { Authorization: root.authorization },
|
|
4313
|
+
delegateDID: input.audienceDid,
|
|
4314
|
+
spaceId: marker === -1 ? "" : resource.slice("tinycloud://".length, marker),
|
|
4315
|
+
path: marker === -1 ? resource : resource.slice(marker + 4),
|
|
4316
|
+
actions: kv?.actions ?? [],
|
|
4317
|
+
expiry: input.expiresAt
|
|
4318
|
+
};
|
|
4319
|
+
}
|
|
4320
|
+
/** Apply only an already-enabled signing policy to the exact credential request. */
|
|
4321
|
+
async autoSignCredentialBytes(bytes) {
|
|
4322
|
+
const strategy = this.config.signStrategy;
|
|
4323
|
+
if (strategy?.type === "auto-sign") return this.signSessionBytes(bytes);
|
|
4324
|
+
if (strategy?.type !== "callback" || !isOpenKeyAutoSignStrategy(strategy)) return void 0;
|
|
4325
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
4326
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
4327
|
+
const request = {
|
|
4328
|
+
address: this.address ?? "",
|
|
4329
|
+
chainId: this.session?.chainId ?? this._chainId,
|
|
4330
|
+
message: new TextDecoder().decode(bytes),
|
|
4331
|
+
type: "message",
|
|
4332
|
+
purpose: "message"
|
|
4333
|
+
};
|
|
4334
|
+
const decision = await strategy.handler(request);
|
|
4335
|
+
if (!decision.approved) {
|
|
4336
|
+
if (decision.needsApproval) return void 0;
|
|
4337
|
+
throw new Error(decision.reason ?? "OpenKey automatic credential signing was rejected");
|
|
4338
|
+
}
|
|
4339
|
+
if (typeof decision.signature !== "string" || !await verifyEip191MessageSignature(
|
|
4340
|
+
request.message,
|
|
4341
|
+
decision.signature,
|
|
4342
|
+
request.address
|
|
4343
|
+
)) {
|
|
4344
|
+
throw new Error("OpenKey credential signature evidence is invalid");
|
|
4345
|
+
}
|
|
4346
|
+
return this.signSessionBytes(bytes);
|
|
4347
|
+
}
|
|
4348
|
+
/** Invoke the configured interactive approval strategy before session signing. */
|
|
4349
|
+
async approveCredentialBytes(bytes) {
|
|
4350
|
+
const strategy = this.config.signStrategy;
|
|
4351
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
4352
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
4353
|
+
const request = {
|
|
4354
|
+
address: this.address ?? "",
|
|
4355
|
+
chainId: this.session?.chainId ?? this._chainId,
|
|
4356
|
+
message: new TextDecoder().decode(bytes),
|
|
4357
|
+
type: "message",
|
|
4358
|
+
purpose: "message"
|
|
4359
|
+
};
|
|
4360
|
+
const approval = isOpenKeyAutoSignStrategy(strategy) ? openKeyApproval(strategy) : strategy?.type === "callback" ? strategy.handler : void 0;
|
|
4361
|
+
if (approval) {
|
|
4362
|
+
const decision = await approval(request);
|
|
4363
|
+
if (!decision.approved) throw new Error(decision.reason ?? "Credential signing was rejected");
|
|
4364
|
+
if (decision.signature !== void 0 && !await verifyEip191MessageSignature(
|
|
4365
|
+
request.message,
|
|
4366
|
+
decision.signature,
|
|
4367
|
+
request.address
|
|
4368
|
+
)) {
|
|
4369
|
+
throw new Error("OpenKey credential signature evidence is invalid");
|
|
4370
|
+
}
|
|
4371
|
+
return this.signSessionBytes(bytes);
|
|
4372
|
+
}
|
|
4373
|
+
if (strategy?.type === "auto-reject") throw new Error("Credential signing was rejected");
|
|
4374
|
+
if (this.config.signer) {
|
|
4375
|
+
await this.config.signer.signMessage(request.message);
|
|
4376
|
+
return this.signSessionBytes(bytes);
|
|
4377
|
+
}
|
|
4378
|
+
throw new Error("Interactive OpenKey approval is not configured");
|
|
4379
|
+
}
|
|
3696
4380
|
/** Bind a session-signed invocation to the canonical service audience. */
|
|
3697
4381
|
async bindInvocationAudience(authorization, audience) {
|
|
3698
4382
|
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
@@ -3760,7 +4444,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3760
4444
|
nonce: this.config.nonce,
|
|
3761
4445
|
siweConfig: this.config.siweConfig,
|
|
3762
4446
|
manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
|
|
3763
|
-
capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
4447
|
+
capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
3764
4448
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
|
|
3765
4449
|
});
|
|
3766
4450
|
this.tc = new TinyCloud(this.auth, {
|
|
@@ -3813,7 +4497,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3813
4497
|
nonce: this.config.nonce,
|
|
3814
4498
|
siweConfig: this.config.siweConfig,
|
|
3815
4499
|
manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
|
|
3816
|
-
capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
4500
|
+
capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
3817
4501
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
|
|
3818
4502
|
});
|
|
3819
4503
|
this.tc = new TinyCloud(this.auth, {
|
|
@@ -6562,35 +7246,44 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
6562
7246
|
if (parentDelegation.disableSubDelegation) {
|
|
6563
7247
|
throw new Error("Parent delegation does not allow sub-delegation");
|
|
6564
7248
|
}
|
|
6565
|
-
|
|
7249
|
+
const parentResources = parentDelegation.resources ?? [{
|
|
7250
|
+
service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
|
|
7251
|
+
space: parentDelegation.spaceId,
|
|
7252
|
+
path: parentDelegation.path,
|
|
7253
|
+
actions: parentDelegation.actions
|
|
7254
|
+
}];
|
|
7255
|
+
const requestedResources = params.resources ?? [{
|
|
7256
|
+
service: params.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
|
|
7257
|
+
space: parentDelegation.spaceId,
|
|
7258
|
+
path: params.path,
|
|
7259
|
+
actions: params.actions
|
|
7260
|
+
}];
|
|
7261
|
+
const resourceIsContained = (parent, child) => {
|
|
7262
|
+
const parentEncryption = parent.service === "encryption" || parent.service === "tinycloud.encryption";
|
|
7263
|
+
const childEncryption = child.service === "encryption" || child.service === "tinycloud.encryption";
|
|
7264
|
+
if (parentEncryption || childEncryption) return parentEncryption && childEncryption && parent.path === child.path && child.actions.every((action) => parent.actions.includes(action));
|
|
7265
|
+
const pathContained = parent.path === child.path || parent.path.endsWith("/") && child.path.startsWith(parent.path) || child.path.startsWith(`${parent.path}/`);
|
|
7266
|
+
return pathContained && child.actions.every((action) => parent.actions.includes(action));
|
|
7267
|
+
};
|
|
7268
|
+
if (requestedResources.length === 0 || requestedResources.some((child) => !parentResources.some((parent) => resourceIsContained(parent, child)))) {
|
|
6566
7269
|
throw new Error(
|
|
6567
|
-
|
|
7270
|
+
"Sub-delegation resources exceed the received delegation"
|
|
6568
7271
|
);
|
|
6569
7272
|
}
|
|
6570
|
-
const parentActions = new Set(parentDelegation.actions);
|
|
6571
|
-
for (const action of params.actions) {
|
|
6572
|
-
if (!parentActions.has(action)) {
|
|
6573
|
-
throw new Error(
|
|
6574
|
-
`Sub-delegation action "${action}" is not in parent's actions: ${parentDelegation.actions.join(", ")}`
|
|
6575
|
-
);
|
|
6576
|
-
}
|
|
6577
|
-
}
|
|
6578
7273
|
const now = /* @__PURE__ */ new Date();
|
|
6579
7274
|
const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
|
|
6580
7275
|
const requestedExpiry = new Date(now.getTime() + expiryMs);
|
|
6581
7276
|
const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
|
|
6582
7277
|
const abilities = {};
|
|
6583
|
-
const
|
|
6584
|
-
const
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
abilities
|
|
6591
|
-
|
|
6592
|
-
if (duckdbActions.length > 0) {
|
|
6593
|
-
abilities.duckdb = { [params.path]: duckdbActions };
|
|
7278
|
+
const rawAbilities = {};
|
|
7279
|
+
for (const resource of requestedResources) {
|
|
7280
|
+
const service = resource.service.replace(/^tinycloud\./, "");
|
|
7281
|
+
if (service === "encryption") {
|
|
7282
|
+
rawAbilities[resource.path] = [...resource.actions];
|
|
7283
|
+
continue;
|
|
7284
|
+
}
|
|
7285
|
+
abilities[service] ?? (abilities[service] = {});
|
|
7286
|
+
abilities[service][resource.path] = [...resource.actions];
|
|
6594
7287
|
}
|
|
6595
7288
|
const targetHost = parentDelegation.host ?? this.config.host;
|
|
6596
7289
|
const prepared = this.wasmBindings.prepareSession({
|
|
@@ -6602,7 +7295,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
6602
7295
|
expirationTime: actualExpiry.toISOString(),
|
|
6603
7296
|
spaceId: parentDelegation.spaceId,
|
|
6604
7297
|
delegateUri: params.delegateDID,
|
|
6605
|
-
parents: [parentDelegation.cid]
|
|
7298
|
+
parents: [parentDelegation.cid],
|
|
7299
|
+
...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
|
|
6606
7300
|
});
|
|
6607
7301
|
const signature = await this.signer.signMessage(prepared.siwe);
|
|
6608
7302
|
const subDelegationSession = this.wasmBindings.completeSessionSetup({
|
|
@@ -6620,14 +7314,20 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
6620
7314
|
cid: subDelegationSession.delegationCid,
|
|
6621
7315
|
delegationHeader: subDelegationSession.delegationHeader,
|
|
6622
7316
|
spaceId: parentDelegation.spaceId,
|
|
6623
|
-
path:
|
|
6624
|
-
actions:
|
|
7317
|
+
path: requestedResources[0].path,
|
|
7318
|
+
actions: requestedResources[0].actions,
|
|
6625
7319
|
disableSubDelegation: params.disableSubDelegation ?? false,
|
|
6626
7320
|
expiry: actualExpiry,
|
|
6627
7321
|
delegateDID: params.delegateDID,
|
|
6628
7322
|
ownerAddress: parentDelegation.ownerAddress,
|
|
6629
7323
|
chainId: parentDelegation.chainId,
|
|
6630
|
-
host: targetHost
|
|
7324
|
+
host: targetHost,
|
|
7325
|
+
resources: requestedResources.map((resource) => ({
|
|
7326
|
+
service: resource.service.replace(/^tinycloud\./, ""),
|
|
7327
|
+
space: resource.service.replace(/^tinycloud\./, "") === "encryption" ? "encryption" : resource.space ?? parentDelegation.spaceId,
|
|
7328
|
+
path: resource.path,
|
|
7329
|
+
actions: [...resource.actions]
|
|
7330
|
+
}))
|
|
6631
7331
|
};
|
|
6632
7332
|
}
|
|
6633
7333
|
};
|
|
@@ -6647,7 +7347,7 @@ var TinyCloudNode = _TinyCloudNode;
|
|
|
6647
7347
|
// src/core.ts
|
|
6648
7348
|
import {
|
|
6649
7349
|
ACCOUNT_REGISTRY_PATH,
|
|
6650
|
-
ACCOUNT_REGISTRY_SPACE as
|
|
7350
|
+
ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE3,
|
|
6651
7351
|
DEFAULT_MANIFEST_SPACE as DEFAULT_MANIFEST_SPACE2,
|
|
6652
7352
|
DEFAULT_MANIFEST_VERSION,
|
|
6653
7353
|
VAULT_PERMISSION_SERVICE,
|
|
@@ -6670,8 +7370,341 @@ import {
|
|
|
6670
7370
|
// src/delegation.ts
|
|
6671
7371
|
import {
|
|
6672
7372
|
canonicalizeRecapCaveats as canonicalizeRecapCaveats2,
|
|
7373
|
+
decodeBase64Url,
|
|
6673
7374
|
principalDidEquals as principalDidEquals3
|
|
6674
7375
|
} from "@tinycloud/sdk-core";
|
|
7376
|
+
var SERVICE_BY_SHORT_NAME = {
|
|
7377
|
+
kv: "tinycloud.kv",
|
|
7378
|
+
sql: "tinycloud.sql",
|
|
7379
|
+
duckdb: "tinycloud.duckdb",
|
|
7380
|
+
hooks: "tinycloud.hooks"
|
|
7381
|
+
};
|
|
7382
|
+
function authorizationWithoutBearer(authorization) {
|
|
7383
|
+
return authorization.replace(/^Bearer /i, "");
|
|
7384
|
+
}
|
|
7385
|
+
function normalizedHost(host) {
|
|
7386
|
+
if (typeof host !== "string" || host.length === 0) {
|
|
7387
|
+
throw new Error("Validated runtime delegation requires a non-empty host.");
|
|
7388
|
+
}
|
|
7389
|
+
return host.replace(/\/+$/, "");
|
|
7390
|
+
}
|
|
7391
|
+
function didPrincipalsMatch(left, right) {
|
|
7392
|
+
try {
|
|
7393
|
+
return principalDidEquals3(left, right);
|
|
7394
|
+
} catch {
|
|
7395
|
+
return left === right;
|
|
7396
|
+
}
|
|
7397
|
+
}
|
|
7398
|
+
function compactUcanPayload(authorization) {
|
|
7399
|
+
const compact = authorizationWithoutBearer(authorization);
|
|
7400
|
+
const parts = compact.split(".");
|
|
7401
|
+
if (parts.length !== 3 || parts.some((part) => part.length === 0)) {
|
|
7402
|
+
throw new Error(
|
|
7403
|
+
"Validated runtime delegation requires a compact UCAN authorization so its signed authority can be derived."
|
|
7404
|
+
);
|
|
7405
|
+
}
|
|
7406
|
+
try {
|
|
7407
|
+
const payload = JSON.parse(
|
|
7408
|
+
new TextDecoder("utf-8", { fatal: true }).decode(decodeBase64Url(parts[1]))
|
|
7409
|
+
);
|
|
7410
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
7411
|
+
throw new Error("payload is not an object");
|
|
7412
|
+
}
|
|
7413
|
+
return payload;
|
|
7414
|
+
} catch {
|
|
7415
|
+
throw new Error(
|
|
7416
|
+
"Validated runtime delegation authorization has an invalid compact UCAN payload."
|
|
7417
|
+
);
|
|
7418
|
+
}
|
|
7419
|
+
}
|
|
7420
|
+
function canonicalPermissionEntries(permissions) {
|
|
7421
|
+
return permissions.map((permission) => ({
|
|
7422
|
+
...permission,
|
|
7423
|
+
actions: [...permission.actions].sort()
|
|
7424
|
+
})).sort(
|
|
7425
|
+
(left, right) => left.service.localeCompare(right.service) || (left.space ?? "").localeCompare(right.space ?? "") || left.path.localeCompare(right.path) || left.actions.join("\0").localeCompare(right.actions.join("\0")) || canonicalizeRecapCaveats2(left.caveats).localeCompare(canonicalizeRecapCaveats2(right.caveats))
|
|
7426
|
+
);
|
|
7427
|
+
}
|
|
7428
|
+
function signedCaveats(value) {
|
|
7429
|
+
if (!Array.isArray(value) || value.some(
|
|
7430
|
+
(caveat) => caveat === null || typeof caveat !== "object" || Array.isArray(caveat)
|
|
7431
|
+
)) {
|
|
7432
|
+
throw new Error("Validated runtime delegation has malformed signed ReCap caveats.");
|
|
7433
|
+
}
|
|
7434
|
+
const cloned = JSON.parse(JSON.stringify(value));
|
|
7435
|
+
return canonicalizeRecapCaveats2(cloned) === canonicalizeRecapCaveats2(void 0) ? void 0 : cloned;
|
|
7436
|
+
}
|
|
7437
|
+
function groupedSignedEntries(service, space, path, abilities) {
|
|
7438
|
+
const groups = /* @__PURE__ */ new Map();
|
|
7439
|
+
for (const [action, rawCaveats] of Object.entries(abilities)) {
|
|
7440
|
+
const caveats = signedCaveats(rawCaveats);
|
|
7441
|
+
const identity = canonicalizeRecapCaveats2(caveats);
|
|
7442
|
+
const group = groups.get(identity) ?? { actions: [], caveats };
|
|
7443
|
+
group.actions.push(action);
|
|
7444
|
+
groups.set(identity, group);
|
|
7445
|
+
}
|
|
7446
|
+
return [...groups.values()].map((group) => ({
|
|
7447
|
+
service,
|
|
7448
|
+
...space === void 0 ? {} : { space },
|
|
7449
|
+
path,
|
|
7450
|
+
actions: group.actions.sort(),
|
|
7451
|
+
...group.caveats === void 0 ? {} : { caveats: group.caveats }
|
|
7452
|
+
}));
|
|
7453
|
+
}
|
|
7454
|
+
function signedAuthorityFromCompactUcan(authorization) {
|
|
7455
|
+
const payload = compactUcanPayload(authorization);
|
|
7456
|
+
if (typeof payload.aud !== "string" || payload.aud.length === 0) {
|
|
7457
|
+
throw new Error("Validated runtime delegation is missing a signed audience.");
|
|
7458
|
+
}
|
|
7459
|
+
if (typeof payload.iss !== "string" || payload.iss.length === 0) {
|
|
7460
|
+
throw new Error("Validated runtime delegation is missing a signed issuer.");
|
|
7461
|
+
}
|
|
7462
|
+
if (!Array.isArray(payload.prf) || payload.prf.some(
|
|
7463
|
+
(proof) => typeof proof !== "string" || proof.length === 0
|
|
7464
|
+
)) {
|
|
7465
|
+
throw new Error("Validated runtime delegation has malformed signed proofs.");
|
|
7466
|
+
}
|
|
7467
|
+
if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
|
|
7468
|
+
throw new Error("Validated runtime delegation is missing a signed expiry.");
|
|
7469
|
+
}
|
|
7470
|
+
const expiry = new Date(payload.exp * 1e3);
|
|
7471
|
+
if (Number.isNaN(expiry.getTime())) {
|
|
7472
|
+
throw new Error("Validated runtime delegation has an invalid signed expiry.");
|
|
7473
|
+
}
|
|
7474
|
+
if (payload.att === null || typeof payload.att !== "object" || Array.isArray(payload.att)) {
|
|
7475
|
+
throw new Error("Validated runtime delegation is missing a signed attenuation.");
|
|
7476
|
+
}
|
|
7477
|
+
const fact = Array.isArray(payload.fct) && payload.fct.length > 0 && payload.fct[0] !== null && typeof payload.fct[0] === "object" && !Array.isArray(payload.fct[0]) ? payload.fct[0] : void 0;
|
|
7478
|
+
const remainingRedelegationDepth = fact?.remainingRedelegationDepth;
|
|
7479
|
+
if (remainingRedelegationDepth !== void 0 && (!Number.isInteger(remainingRedelegationDepth) || remainingRedelegationDepth < 0 || remainingRedelegationDepth > 8)) {
|
|
7480
|
+
throw new Error("Validated runtime delegation has invalid signed redelegation depth.");
|
|
7481
|
+
}
|
|
7482
|
+
const permissions = [];
|
|
7483
|
+
for (const [resource, abilities] of Object.entries(payload.att)) {
|
|
7484
|
+
if (abilities === null || typeof abilities !== "object" || Array.isArray(abilities)) {
|
|
7485
|
+
throw new Error("Validated runtime delegation has an invalid signed attenuation.");
|
|
7486
|
+
}
|
|
7487
|
+
const abilityEntries = Object.entries(abilities);
|
|
7488
|
+
if (abilityEntries.length === 0) {
|
|
7489
|
+
throw new Error("Validated runtime delegation has an empty signed capability.");
|
|
7490
|
+
}
|
|
7491
|
+
if (resource.startsWith("urn:tinycloud:encryption:")) {
|
|
7492
|
+
permissions.push(...groupedSignedEntries(
|
|
7493
|
+
"tinycloud.encryption",
|
|
7494
|
+
void 0,
|
|
7495
|
+
resource,
|
|
7496
|
+
abilities
|
|
7497
|
+
));
|
|
7498
|
+
continue;
|
|
7499
|
+
}
|
|
7500
|
+
const nativeMatch = /^(tinycloud:[^/]+)\/(kv|sql|duckdb|hooks)\/(.*)$/.exec(resource);
|
|
7501
|
+
const policyMatch = /^tinycloud:\/\/([^/]+)\/(kv|sql|duckdb|hooks)\/(.*)$/.exec(resource);
|
|
7502
|
+
const match = nativeMatch ?? policyMatch;
|
|
7503
|
+
if (!match) {
|
|
7504
|
+
throw new Error(
|
|
7505
|
+
`Validated runtime delegation has an unsupported signed resource '${resource}'.`
|
|
7506
|
+
);
|
|
7507
|
+
}
|
|
7508
|
+
const [, space, shortService, path] = match;
|
|
7509
|
+
const service = SERVICE_BY_SHORT_NAME[shortService];
|
|
7510
|
+
if (service === void 0) {
|
|
7511
|
+
throw new Error(
|
|
7512
|
+
`Validated runtime delegation has an unsupported signed service '${shortService}'.`
|
|
7513
|
+
);
|
|
7514
|
+
}
|
|
7515
|
+
permissions.push(...groupedSignedEntries(
|
|
7516
|
+
service,
|
|
7517
|
+
space,
|
|
7518
|
+
path,
|
|
7519
|
+
abilities
|
|
7520
|
+
));
|
|
7521
|
+
}
|
|
7522
|
+
if (permissions.length === 0) {
|
|
7523
|
+
throw new Error("Validated runtime delegation has no signed capabilities.");
|
|
7524
|
+
}
|
|
7525
|
+
const canonicalPermissions = canonicalPermissionEntries(permissions);
|
|
7526
|
+
const canonicalResources = canonicalPermissions.map((permission) => ({
|
|
7527
|
+
service: permission.service === "tinycloud.encryption" ? "encryption" : permission.service.slice("tinycloud.".length),
|
|
7528
|
+
space: permission.service === "tinycloud.encryption" ? "encryption" : permission.space,
|
|
7529
|
+
path: permission.path,
|
|
7530
|
+
actions: [...permission.actions],
|
|
7531
|
+
...permission.caveats === void 0 ? {} : { caveats: permission.caveats }
|
|
7532
|
+
}));
|
|
7533
|
+
return {
|
|
7534
|
+
issuer: payload.iss,
|
|
7535
|
+
audience: payload.aud,
|
|
7536
|
+
expiry,
|
|
7537
|
+
proofs: [...payload.prf],
|
|
7538
|
+
...remainingRedelegationDepth === void 0 ? {} : { remainingRedelegationDepth },
|
|
7539
|
+
permissions: canonicalPermissions,
|
|
7540
|
+
resources: canonicalResources
|
|
7541
|
+
};
|
|
7542
|
+
}
|
|
7543
|
+
function canonicalResourcesFromPortableDelegation(delegation) {
|
|
7544
|
+
if (delegation.resources !== void 0 && delegation.resources.length > 0) {
|
|
7545
|
+
return delegation.resources.map((resource) => ({ ...resource, actions: [...resource.actions].sort() })).sort(
|
|
7546
|
+
(left, right) => left.service.localeCompare(right.service) || left.space.localeCompare(right.space) || left.path.localeCompare(right.path) || left.actions.join("\0").localeCompare(right.actions.join("\0")) || canonicalizeRecapCaveats2(left.caveats).localeCompare(canonicalizeRecapCaveats2(right.caveats))
|
|
7547
|
+
);
|
|
7548
|
+
}
|
|
7549
|
+
const byService = /* @__PURE__ */ new Map();
|
|
7550
|
+
for (const action of delegation.actions) {
|
|
7551
|
+
const [service] = action.split("/", 1);
|
|
7552
|
+
const shortService = service.replace(/^tinycloud\./, "");
|
|
7553
|
+
const values = byService.get(shortService) ?? [];
|
|
7554
|
+
values.push(action);
|
|
7555
|
+
byService.set(shortService, values);
|
|
7556
|
+
}
|
|
7557
|
+
return [...byService.entries()].map(([service, actions]) => ({
|
|
7558
|
+
service,
|
|
7559
|
+
space: service === "encryption" ? "encryption" : delegation.spaceId,
|
|
7560
|
+
path: delegation.path,
|
|
7561
|
+
actions: [...new Set(actions)].sort(),
|
|
7562
|
+
...delegation.caveats === void 0 ? {} : { caveats: delegation.caveats }
|
|
7563
|
+
})).sort(
|
|
7564
|
+
(left, right) => left.service.localeCompare(right.service) || left.space.localeCompare(right.space) || left.path.localeCompare(right.path)
|
|
7565
|
+
);
|
|
7566
|
+
}
|
|
7567
|
+
function resourcesMatch(actual, expected) {
|
|
7568
|
+
const comparable = (resource) => ({
|
|
7569
|
+
...resource,
|
|
7570
|
+
caveats: canonicalizeRecapCaveats2(resource.caveats)
|
|
7571
|
+
});
|
|
7572
|
+
return JSON.stringify(actual.map(comparable)) === JSON.stringify(expected.map(comparable));
|
|
7573
|
+
}
|
|
7574
|
+
async function activateValidatedRuntimeDelegation(node, delegation, options) {
|
|
7575
|
+
const host = normalizedHost(options.host);
|
|
7576
|
+
if (delegation.host !== void 0 && normalizedHost(delegation.host) !== host) {
|
|
7577
|
+
throw new Error(
|
|
7578
|
+
`Runtime delegation host '${delegation.host}' does not match expected host '${options.host}'.`
|
|
7579
|
+
);
|
|
7580
|
+
}
|
|
7581
|
+
if (!(delegation.expiry instanceof Date) || Number.isNaN(delegation.expiry.getTime())) {
|
|
7582
|
+
throw new Error("Runtime delegation has an invalid expiry.");
|
|
7583
|
+
}
|
|
7584
|
+
if (delegation.expiry.getTime() <= Date.now()) {
|
|
7585
|
+
throw new Error("Runtime delegation is expired.");
|
|
7586
|
+
}
|
|
7587
|
+
const authorization = delegation.delegationHeader?.Authorization;
|
|
7588
|
+
if (typeof authorization !== "string" || authorization.length === 0) {
|
|
7589
|
+
throw new Error("Runtime delegation is missing authorization bytes.");
|
|
7590
|
+
}
|
|
7591
|
+
const cid = node.computeDelegationCid(authorization);
|
|
7592
|
+
if (cid !== delegation.cid) {
|
|
7593
|
+
throw new Error("Runtime delegation CID does not match authorization bytes.");
|
|
7594
|
+
}
|
|
7595
|
+
const signed = signedAuthorityFromCompactUcan(authorization);
|
|
7596
|
+
if (signed.expiry.getTime() <= Date.now()) {
|
|
7597
|
+
throw new Error("Runtime delegation is expired.");
|
|
7598
|
+
}
|
|
7599
|
+
if (delegation.expiry.getTime() !== signed.expiry.getTime()) {
|
|
7600
|
+
throw new Error("Runtime delegation expiry does not match signed authority.");
|
|
7601
|
+
}
|
|
7602
|
+
if (!didPrincipalsMatch(signed.audience, node.sessionDid)) {
|
|
7603
|
+
throw new Error(
|
|
7604
|
+
`Runtime delegation targets ${signed.audience} but this session is ${node.sessionDid}.`
|
|
7605
|
+
);
|
|
7606
|
+
}
|
|
7607
|
+
if (!didPrincipalsMatch(delegation.delegateDID, signed.audience)) {
|
|
7608
|
+
throw new Error("Runtime delegation audience does not match signed authority.");
|
|
7609
|
+
}
|
|
7610
|
+
const declaredResources = canonicalResourcesFromPortableDelegation(delegation);
|
|
7611
|
+
if (!resourcesMatch(declaredResources, signed.resources)) {
|
|
7612
|
+
throw new Error("Runtime delegation resources do not match signed authority.");
|
|
7613
|
+
}
|
|
7614
|
+
const previouslyInstalled = node.getRuntimePermissionDelegations().find((candidate) => candidate.cid === cid);
|
|
7615
|
+
if (previouslyInstalled !== void 0 && previouslyInstalled.delegationHeader.Authorization !== authorization) {
|
|
7616
|
+
throw new Error(
|
|
7617
|
+
"A different authorization is already installed for this runtime delegation CID."
|
|
7618
|
+
);
|
|
7619
|
+
}
|
|
7620
|
+
const primary = signed.resources[0];
|
|
7621
|
+
const signedSpace = signed.permissions.find(
|
|
7622
|
+
(permission) => permission.service !== "tinycloud.encryption"
|
|
7623
|
+
)?.space;
|
|
7624
|
+
const installedCandidate = {
|
|
7625
|
+
cid,
|
|
7626
|
+
delegationHeader: { Authorization: authorization },
|
|
7627
|
+
ownerAddress: delegation.ownerAddress,
|
|
7628
|
+
chainId: delegation.chainId,
|
|
7629
|
+
delegatorDID: signed.issuer,
|
|
7630
|
+
...signed.proofs.length === 1 ? { parentCid: signed.proofs[0] } : {},
|
|
7631
|
+
...signed.remainingRedelegationDepth === void 0 ? {} : {
|
|
7632
|
+
allowSubDelegation: signed.remainingRedelegationDepth > 0,
|
|
7633
|
+
disableSubDelegation: signed.remainingRedelegationDepth === 0
|
|
7634
|
+
},
|
|
7635
|
+
authHeader: authorization,
|
|
7636
|
+
spaceId: signedSpace ?? delegation.spaceId,
|
|
7637
|
+
path: primary.path,
|
|
7638
|
+
actions: [...primary.actions],
|
|
7639
|
+
...primary.caveats === void 0 ? {} : { caveats: primary.caveats },
|
|
7640
|
+
resources: signed.resources.map((resource) => ({
|
|
7641
|
+
...resource,
|
|
7642
|
+
actions: [...resource.actions]
|
|
7643
|
+
})),
|
|
7644
|
+
expiry: new Date(signed.expiry),
|
|
7645
|
+
delegateDID: signed.audience,
|
|
7646
|
+
host
|
|
7647
|
+
};
|
|
7648
|
+
if (node.isSessionOnly === true && typeof node.did === "string" && principalDidEquals3(node.did, node.sessionDid) && typeof node.useDelegation === "function") {
|
|
7649
|
+
await node.useDelegation(installedCandidate);
|
|
7650
|
+
} else {
|
|
7651
|
+
await node.useRuntimeDelegation(installedCandidate);
|
|
7652
|
+
}
|
|
7653
|
+
const installed = node.getRuntimePermissionDelegations().find((candidate) => candidate.cid === cid);
|
|
7654
|
+
if (!installed) {
|
|
7655
|
+
throw new Error("Runtime delegation activation did not install the validated authority.");
|
|
7656
|
+
}
|
|
7657
|
+
if (installed.delegationHeader.Authorization !== authorization || !didPrincipalsMatch(installed.delegateDID, signed.audience) || installed.expiry.getTime() !== signed.expiry.getTime() || !resourcesMatch(
|
|
7658
|
+
canonicalResourcesFromPortableDelegation(installed),
|
|
7659
|
+
signed.resources
|
|
7660
|
+
) || normalizedHost(installed.host ?? "") !== host) {
|
|
7661
|
+
throw new Error(
|
|
7662
|
+
"Runtime delegation activation installed authority that differs from the validated authority."
|
|
7663
|
+
);
|
|
7664
|
+
}
|
|
7665
|
+
return {
|
|
7666
|
+
cid,
|
|
7667
|
+
delegation: installed,
|
|
7668
|
+
effectivePermissions: signed.permissions,
|
|
7669
|
+
expiry: new Date(signed.expiry),
|
|
7670
|
+
audience: signed.audience,
|
|
7671
|
+
host: normalizedHost(installed.host ?? host)
|
|
7672
|
+
};
|
|
7673
|
+
}
|
|
7674
|
+
async function activateCompactRuntimeDelegation(node, input) {
|
|
7675
|
+
const authorization = input.authorization;
|
|
7676
|
+
const cid = node.computeDelegationCid(authorization);
|
|
7677
|
+
if (cid !== input.cid) {
|
|
7678
|
+
throw new Error("Compact runtime delegation CID does not match authorization bytes.");
|
|
7679
|
+
}
|
|
7680
|
+
const signed = signedAuthorityFromCompactUcan(authorization);
|
|
7681
|
+
const primary = signed.resources[0];
|
|
7682
|
+
const signedSpace = signed.permissions.find(
|
|
7683
|
+
(permission) => permission.service !== "tinycloud.encryption"
|
|
7684
|
+
)?.space;
|
|
7685
|
+
const delegation = {
|
|
7686
|
+
cid,
|
|
7687
|
+
delegationHeader: { Authorization: authorization },
|
|
7688
|
+
ownerAddress: input.ownerAddress,
|
|
7689
|
+
chainId: input.chainId,
|
|
7690
|
+
delegatorDID: signed.issuer,
|
|
7691
|
+
authHeader: authorization,
|
|
7692
|
+
spaceId: signedSpace ?? primary.space,
|
|
7693
|
+
path: primary.path,
|
|
7694
|
+
actions: [...primary.actions],
|
|
7695
|
+
...primary.caveats === void 0 ? {} : { caveats: primary.caveats },
|
|
7696
|
+
resources: signed.resources.map((resource) => ({
|
|
7697
|
+
...resource,
|
|
7698
|
+
actions: [...resource.actions]
|
|
7699
|
+
})),
|
|
7700
|
+
expiry: new Date(signed.expiry),
|
|
7701
|
+
delegateDID: signed.audience,
|
|
7702
|
+
host: normalizedHost(input.host)
|
|
7703
|
+
};
|
|
7704
|
+
return activateValidatedRuntimeDelegation(node, delegation, {
|
|
7705
|
+
host: input.host
|
|
7706
|
+
});
|
|
7707
|
+
}
|
|
6675
7708
|
function serializeDelegation(delegation) {
|
|
6676
7709
|
return JSON.stringify({
|
|
6677
7710
|
...delegation,
|
|
@@ -6739,7 +7772,7 @@ import {
|
|
|
6739
7772
|
import { ServiceContext as ServiceContext3 } from "@tinycloud/sdk-core";
|
|
6740
7773
|
export {
|
|
6741
7774
|
ACCOUNT_REGISTRY_PATH,
|
|
6742
|
-
|
|
7775
|
+
ACCOUNT_REGISTRY_SPACE3 as ACCOUNT_REGISTRY_SPACE,
|
|
6743
7776
|
AccountService,
|
|
6744
7777
|
AutoApproveSpaceCreationHandler2 as AutoApproveSpaceCreationHandler,
|
|
6745
7778
|
CapabilityKeyRegistry2 as CapabilityKeyRegistry,
|
|
@@ -6786,6 +7819,7 @@ export {
|
|
|
6786
7819
|
VaultPublicSpaceKVActions,
|
|
6787
7820
|
VersionCheckError,
|
|
6788
7821
|
WasmKeyProvider,
|
|
7822
|
+
activateCompactRuntimeDelegation,
|
|
6789
7823
|
addressStorageKey,
|
|
6790
7824
|
buildSpaceUri,
|
|
6791
7825
|
canonicalizeAddress3 as canonicalizeAddress,
|