@tinycloud/node-sdk 2.11.0-beta.1 → 2.11.0-beta.11
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-CQ_5wXQT.d.cts → core-9mWw3N8i.d.cts} +280 -13
- package/dist/{core-CQ_5wXQT.d.ts → core-9mWw3N8i.d.ts} +280 -13
- package/dist/core.cjs +725 -79
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +2 -2
- package/dist/core.d.ts +2 -2
- package/dist/core.js +741 -86
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +779 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +86 -4
- package/dist/index.d.ts +86 -4
- package/dist/index.js +790 -87
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/core.cjs
CHANGED
|
@@ -322,6 +322,8 @@ var FileSessionStorage = class {
|
|
|
322
322
|
};
|
|
323
323
|
|
|
324
324
|
// src/authorization/NodeUserAuthorization.ts
|
|
325
|
+
var import_node_crypto = require("crypto");
|
|
326
|
+
var import_siwe = require("siwe");
|
|
325
327
|
var import_sdk_core3 = require("@tinycloud/sdk-core");
|
|
326
328
|
|
|
327
329
|
// src/authorization/strategies.ts
|
|
@@ -331,6 +333,27 @@ var defaultSignStrategy = { type: "auto-sign" };
|
|
|
331
333
|
// src/authorization/NodeUserAuthorization.ts
|
|
332
334
|
var DECRYPT_ACTION = import_sdk_core3.ENCRYPTION.DECRYPT;
|
|
333
335
|
var NETWORK_CREATE_ACTION = import_sdk_core3.ENCRYPTION.NETWORK_CREATE;
|
|
336
|
+
function canonicalizeOpenKeyManifestJson(value) {
|
|
337
|
+
const json = JSON.stringify(value);
|
|
338
|
+
if (json === void 0) {
|
|
339
|
+
throw new TypeError("manifest is not JSON-serializable");
|
|
340
|
+
}
|
|
341
|
+
return canonicalizeJsonValue(JSON.parse(json));
|
|
342
|
+
}
|
|
343
|
+
function canonicalizeJsonValue(value) {
|
|
344
|
+
if (value === null || typeof value !== "object") {
|
|
345
|
+
return JSON.stringify(value);
|
|
346
|
+
}
|
|
347
|
+
if (Array.isArray(value)) {
|
|
348
|
+
return `[${value.map(canonicalizeJsonValue).join(",")}]`;
|
|
349
|
+
}
|
|
350
|
+
return `{${Object.keys(value).sort().map(
|
|
351
|
+
(key) => `${JSON.stringify(key)}:${canonicalizeJsonValue(value[key])}`
|
|
352
|
+
).join(",")}}`;
|
|
353
|
+
}
|
|
354
|
+
function canonicalOpenKeyManifestSha256Hex(value) {
|
|
355
|
+
return (0, import_node_crypto.createHash)("sha256").update(canonicalizeOpenKeyManifestJson(value)).digest("hex");
|
|
356
|
+
}
|
|
334
357
|
function didPrincipalMatches(actual, expected) {
|
|
335
358
|
try {
|
|
336
359
|
return (0, import_sdk_core3.principalDidEquals)(actual, expected);
|
|
@@ -348,6 +371,16 @@ function addRawAbility(rawAbilities, resource, action) {
|
|
|
348
371
|
actions.push(action);
|
|
349
372
|
}
|
|
350
373
|
}
|
|
374
|
+
function cloneAbilitiesMap(actions) {
|
|
375
|
+
return Object.fromEntries(
|
|
376
|
+
Object.entries(actions).map(([service, paths]) => [
|
|
377
|
+
service,
|
|
378
|
+
Object.fromEntries(
|
|
379
|
+
Object.entries(paths).map(([path, pathActions]) => [path, [...pathActions]])
|
|
380
|
+
)
|
|
381
|
+
])
|
|
382
|
+
);
|
|
383
|
+
}
|
|
351
384
|
var NodeUserAuthorization = class {
|
|
352
385
|
constructor(config) {
|
|
353
386
|
this.activeSessionKeyId = "default";
|
|
@@ -551,6 +584,15 @@ var NodeUserAuthorization = class {
|
|
|
551
584
|
get nodeFeatures() {
|
|
552
585
|
return this._nodeFeatures;
|
|
553
586
|
}
|
|
587
|
+
/**
|
|
588
|
+
* The node DID reported by `GET /info` for `host`, if the sign-in (or
|
|
589
|
+
* restore) that populated it targeted that same host. Returns `undefined`
|
|
590
|
+
* for any other host, and for nodes whose `/info` omits `nodeId`, so callers
|
|
591
|
+
* fall back to fetching it themselves rather than using a foreign node's DID.
|
|
592
|
+
*/
|
|
593
|
+
nodeIdForHost(host) {
|
|
594
|
+
return this._nodeInfoIdentity?.host === host ? this._nodeInfoIdentity.nodeId : void 0;
|
|
595
|
+
}
|
|
554
596
|
get lastActivationSkippedSpaceIds() {
|
|
555
597
|
return [...this._lastActivationSkippedSpaceIds];
|
|
556
598
|
}
|
|
@@ -610,23 +652,41 @@ var NodeUserAuthorization = class {
|
|
|
610
652
|
const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
|
|
611
653
|
const primarySpaceId2 = (0, import_sdk_core3.makePkhSpaceId)(address, chainId, this.spacePrefix);
|
|
612
654
|
const secretsSpaceId = (0, import_sdk_core3.makePkhSpaceId)(address, chainId, "secrets");
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
655
|
+
const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === import_sdk_core3.ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
|
|
656
|
+
const spaceAbilities2 = {
|
|
657
|
+
[primarySpaceId2]: primaryActions,
|
|
658
|
+
[secretsSpaceId]: {
|
|
659
|
+
kv: {
|
|
660
|
+
"vault/secrets/": [
|
|
661
|
+
import_sdk_core3.KV.GET,
|
|
662
|
+
import_sdk_core3.KV.PUT,
|
|
663
|
+
import_sdk_core3.KV.DEL,
|
|
664
|
+
import_sdk_core3.KV.LIST,
|
|
665
|
+
import_sdk_core3.KV.METADATA
|
|
666
|
+
]
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
if (this.includeAccountRegistryPermissions && this.spacePrefix !== import_sdk_core3.ACCOUNT_REGISTRY_SPACE) {
|
|
671
|
+
spaceAbilities2[(0, import_sdk_core3.makePkhSpaceId)(address, chainId, import_sdk_core3.ACCOUNT_REGISTRY_SPACE)] = (0, import_sdk_core3.resourceCapabilitiesToAbilitiesMap)(import_sdk_core3.ACCOUNT_MANIFEST_PERMISSIONS);
|
|
672
|
+
} else if (this.includeAccountRegistryPermissions) {
|
|
673
|
+
const accountAbilities = (0, import_sdk_core3.resourceCapabilitiesToAbilitiesMap)(
|
|
674
|
+
import_sdk_core3.ACCOUNT_MANIFEST_PERMISSIONS
|
|
675
|
+
);
|
|
676
|
+
for (const [service, paths] of Object.entries(accountAbilities)) {
|
|
677
|
+
const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
|
|
678
|
+
for (const [path, actions] of Object.entries(paths)) {
|
|
679
|
+
const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
|
|
680
|
+
for (const action of actions) {
|
|
681
|
+
if (!existingActions.includes(action)) existingActions.push(action);
|
|
627
682
|
}
|
|
628
683
|
}
|
|
629
|
-
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
abilities: primaryActions,
|
|
688
|
+
spaceId: primarySpaceId2,
|
|
689
|
+
spaceAbilities: spaceAbilities2,
|
|
630
690
|
rawAbilities: {
|
|
631
691
|
[defaultNetworkId]: [DECRYPT_ACTION, NETWORK_CREATE_ACTION]
|
|
632
692
|
}
|
|
@@ -1020,11 +1080,13 @@ var NodeUserAuthorization = class {
|
|
|
1020
1080
|
this._tinyCloudSession = tinyCloudSession;
|
|
1021
1081
|
this._address = address;
|
|
1022
1082
|
this._chainId = chainId;
|
|
1083
|
+
const infoHost = this.primaryTinyCloudHost;
|
|
1023
1084
|
const nodeInfo = await (0, import_sdk_core3.checkNodeInfo)(
|
|
1024
|
-
|
|
1085
|
+
infoHost,
|
|
1025
1086
|
this.wasm.protocolVersion()
|
|
1026
1087
|
);
|
|
1027
1088
|
this._nodeFeatures = nodeInfo.features;
|
|
1089
|
+
this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
|
|
1028
1090
|
for (const ext of this.extensions) {
|
|
1029
1091
|
if (ext.afterSignIn) {
|
|
1030
1092
|
await ext.afterSignIn(clientSession);
|
|
@@ -1190,11 +1252,13 @@ var NodeUserAuthorization = class {
|
|
|
1190
1252
|
this._tinyCloudSession = tinyCloudSession;
|
|
1191
1253
|
this._address = address;
|
|
1192
1254
|
this._chainId = chainId;
|
|
1255
|
+
const infoHost = this.primaryTinyCloudHost;
|
|
1193
1256
|
const nodeInfo = await (0, import_sdk_core3.checkNodeInfo)(
|
|
1194
|
-
|
|
1257
|
+
infoHost,
|
|
1195
1258
|
this.wasm.protocolVersion()
|
|
1196
1259
|
);
|
|
1197
1260
|
this._nodeFeatures = nodeInfo.features;
|
|
1261
|
+
this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
|
|
1198
1262
|
for (const ext of this.extensions) {
|
|
1199
1263
|
if (ext.afterSignIn) {
|
|
1200
1264
|
await ext.afterSignIn(clientSession);
|
|
@@ -1203,6 +1267,398 @@ var NodeUserAuthorization = class {
|
|
|
1203
1267
|
await this.ensureSpaceExists();
|
|
1204
1268
|
return clientSession;
|
|
1205
1269
|
}
|
|
1270
|
+
/**
|
|
1271
|
+
* End-to-end sign-in via OpenKey `authorizeTinyCloud()` (Sol CRITICAL-4).
|
|
1272
|
+
*
|
|
1273
|
+
* This is the production entry point that wires
|
|
1274
|
+
* {@link prepareSessionForSigning} → OpenKey `authorizeTinyCloud()` →
|
|
1275
|
+
* {@link signInWithOpenKeyResult} into one call. Consumers pass in a
|
|
1276
|
+
* caller-provided `authorizeFn` (typically a thin wrapper around
|
|
1277
|
+
* `openkey.authorizeTinyCloud()`) so this method has no direct
|
|
1278
|
+
* dependency on the OpenKey SDK — it works with any transport that
|
|
1279
|
+
* conforms to the versioned protocol.
|
|
1280
|
+
*
|
|
1281
|
+
* The `authorizeFn` MUST:
|
|
1282
|
+
* 1. Send the prepared SIWE bytes to OpenKey.
|
|
1283
|
+
* 2. Return the exact `TinyCloudAuthorizationResultV1` shape (with
|
|
1284
|
+
* `signedMessage` — the bytes the signature actually verifies
|
|
1285
|
+
* against — plus `selectedActionKeys` and `permissions`).
|
|
1286
|
+
*
|
|
1287
|
+
* NodeUserAuthorization then completes the session with `signedMessage`
|
|
1288
|
+
* (never the prepared bytes), verifying the signature, the immutable
|
|
1289
|
+
* SIWE fields, and every subset invariant enforced by
|
|
1290
|
+
* {@link signInWithOpenKeyResult}. This preserves the exact-byte
|
|
1291
|
+
* guarantee: if the user did NOT narrow, the returned `signedMessage`
|
|
1292
|
+
* equals the prepared SIWE byte-for-byte; if the user narrowed, only
|
|
1293
|
+
* the ReCap block differs and every immutable header field survives.
|
|
1294
|
+
*
|
|
1295
|
+
* Legacy exact-byte {@link signIn} callers are unaffected — this method
|
|
1296
|
+
* is opt-in.
|
|
1297
|
+
*
|
|
1298
|
+
* @param authorizeFn Callback that invokes the versioned OpenKey
|
|
1299
|
+
* `authorizeTinyCloud()` protocol and returns the result. Receives
|
|
1300
|
+
* the prepared SIWE bytes plus a `host` hint the widget should bind
|
|
1301
|
+
* into its /authorize-sign-prepare context.
|
|
1302
|
+
* @param options Optional overrides for the OpenKey request.
|
|
1303
|
+
*/
|
|
1304
|
+
async signInWithOpenKey(authorizeFn, options) {
|
|
1305
|
+
if (options?.host !== void 0) {
|
|
1306
|
+
const overrideHost = options.host.trim();
|
|
1307
|
+
if (!overrideHost) {
|
|
1308
|
+
throw new Error("OpenKey authorization host must not be empty");
|
|
1309
|
+
}
|
|
1310
|
+
this.tinycloudHosts = [overrideHost];
|
|
1311
|
+
}
|
|
1312
|
+
const signingAddress = (0, import_sdk_core3.canonicalizeAddress)(await this.signer.getAddress());
|
|
1313
|
+
const signingChainId = await this.signer.getChainId();
|
|
1314
|
+
await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
|
|
1315
|
+
const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
|
|
1316
|
+
const presentation = this.buildPresentationEnvelope(options?.reason);
|
|
1317
|
+
const hostHint = this.primaryTinyCloudHost;
|
|
1318
|
+
const result = await authorizeFn({
|
|
1319
|
+
protocolVersion: 1,
|
|
1320
|
+
siwe: prepared.siwe,
|
|
1321
|
+
jwk,
|
|
1322
|
+
host: hostHint,
|
|
1323
|
+
keyId: options?.openkeyKeyId,
|
|
1324
|
+
presentation
|
|
1325
|
+
});
|
|
1326
|
+
return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
|
|
1327
|
+
}
|
|
1328
|
+
/**
|
|
1329
|
+
* Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
|
|
1330
|
+
* envelope is display-only — the OpenKey widget renders it under an
|
|
1331
|
+
* honest trust label, and the server /authorize-sign-prepare route
|
|
1332
|
+
* decides whether to upgrade the label to `origin-bound` by fetching
|
|
1333
|
+
* the well-known manifest and comparing its digest to
|
|
1334
|
+
* `manifestDigest`.
|
|
1335
|
+
*
|
|
1336
|
+
* Returns `undefined` when no manifest is set — the widget renders as
|
|
1337
|
+
* "no manifest supplied" (unsigned).
|
|
1338
|
+
*
|
|
1339
|
+
* The digest is a deterministic SHA-256 over canonical JSON of the
|
|
1340
|
+
* first manifest payload; OpenKey parses the fetched well-known manifest
|
|
1341
|
+
* and applies the same canonicalization before comparing digests.
|
|
1342
|
+
*/
|
|
1343
|
+
buildPresentationEnvelope(reason) {
|
|
1344
|
+
const manifests = Array.isArray(this._manifest) ? this._manifest : this._manifest ? [this._manifest] : [];
|
|
1345
|
+
if (manifests.length === 0 && !reason) {
|
|
1346
|
+
return void 0;
|
|
1347
|
+
}
|
|
1348
|
+
const primary = manifests[0];
|
|
1349
|
+
let displayName;
|
|
1350
|
+
let manifestId;
|
|
1351
|
+
let manifestDigest;
|
|
1352
|
+
const envelopeManifests = [];
|
|
1353
|
+
for (const m of manifests) {
|
|
1354
|
+
envelopeManifests.push({
|
|
1355
|
+
name: m.name,
|
|
1356
|
+
appId: m.app_id,
|
|
1357
|
+
// Deep-copy to prevent later mutations from affecting the digest.
|
|
1358
|
+
payload: JSON.parse(JSON.stringify(m))
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
if (primary) {
|
|
1362
|
+
displayName = primary.name;
|
|
1363
|
+
manifestId = primary.app_id;
|
|
1364
|
+
manifestDigest = canonicalOpenKeyManifestSha256Hex(primary);
|
|
1365
|
+
}
|
|
1366
|
+
return {
|
|
1367
|
+
protocolVersion: 1,
|
|
1368
|
+
displayName,
|
|
1369
|
+
reason: reason && reason.length > 0 ? reason : void 0,
|
|
1370
|
+
manifestId,
|
|
1371
|
+
manifestDigest,
|
|
1372
|
+
manifests: envelopeManifests.length > 0 ? envelopeManifests : void 0
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Complete sign-in with a versioned OpenKey authorization result.
|
|
1377
|
+
*
|
|
1378
|
+
* Where `signInWithPreparedSession` completes with a signature over the
|
|
1379
|
+
* ORIGINAL prepared SIWE, this method completes with the SIWE bytes the
|
|
1380
|
+
* OpenKey widget actually presented and signed (`signedMessage`). It
|
|
1381
|
+
* exists because the versioned authorizeTinyCloud protocol lets the user
|
|
1382
|
+
* narrow capabilities; the returned bytes may differ from the original
|
|
1383
|
+
* request and are the only ones the signature actually verifies against.
|
|
1384
|
+
*
|
|
1385
|
+
* The `result` parameter is validated at the trust boundary via
|
|
1386
|
+
* `validateAuthorizationResultV1` (in sdk-core). Callers should validate
|
|
1387
|
+
* BEFORE calling this method so unknown/missing fields are rejected
|
|
1388
|
+
* before any session state is created.
|
|
1389
|
+
*
|
|
1390
|
+
* Validation performed here (defence-in-depth: even a "valid" v1 payload
|
|
1391
|
+
* must clear these checks before any session state is created):
|
|
1392
|
+
* 1. `result.address` matches the local signer's address.
|
|
1393
|
+
* 2. `result.signature` recovers to `result.address` when verified against
|
|
1394
|
+
* `result.signedMessage`.
|
|
1395
|
+
* 3. Every immutable SIWE header field (domain, address, URI, version,
|
|
1396
|
+
* chainId, nonce, issuedAt) in the original prepared SIWE is preserved
|
|
1397
|
+
* byte-for-byte in `result.signedMessage`. `statement` is NOT in this
|
|
1398
|
+
* list because the WASM renders it from the ReCap contents; it MUST
|
|
1399
|
+
* change when the ReCap is narrowed.
|
|
1400
|
+
* 4. Every capability in `result.signedMessage`'s ReCap block was present
|
|
1401
|
+
* in the original prepared SIWE — narrowing OK, broadening rejected.
|
|
1402
|
+
* 5. Every `result.selectedActionKeys` entry corresponds to a
|
|
1403
|
+
* (resource, action) pair in the returned SIWE.
|
|
1404
|
+
*/
|
|
1405
|
+
async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
|
|
1406
|
+
if (result.protocolVersion !== 1) {
|
|
1407
|
+
throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
|
|
1408
|
+
}
|
|
1409
|
+
if (!result.signedMessage) {
|
|
1410
|
+
throw new Error("OpenKey result must include signedMessage");
|
|
1411
|
+
}
|
|
1412
|
+
if (!prepared.siwe) {
|
|
1413
|
+
throw new Error(
|
|
1414
|
+
"signInWithOpenKeyResult requires prepared.siwe (the SDK-generated SIWE the sign-in was proposed with)"
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
const expectedAddress = (0, import_sdk_core3.canonicalizeAddress)(await this.signer.getAddress());
|
|
1418
|
+
if ((0, import_sdk_core3.canonicalizeAddress)(result.address) !== expectedAddress) {
|
|
1419
|
+
throw new Error(
|
|
1420
|
+
`OpenKey returned signature from ${result.address} but expected ${expectedAddress}`
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
let parsedSignedSiwe;
|
|
1424
|
+
try {
|
|
1425
|
+
parsedSignedSiwe = new import_siwe.SiweMessage(result.signedMessage);
|
|
1426
|
+
} catch (err) {
|
|
1427
|
+
throw new Error(
|
|
1428
|
+
`OpenKey signedMessage is not a parseable SIWE: ${err.message}`
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
const verifyResult = await parsedSignedSiwe.verify(
|
|
1432
|
+
{ signature: result.signature },
|
|
1433
|
+
{ suppressExceptions: true }
|
|
1434
|
+
);
|
|
1435
|
+
if (!verifyResult.success) {
|
|
1436
|
+
const errData = verifyResult.error;
|
|
1437
|
+
const type = errData && typeof errData === "object" && "type" in errData ? errData.type : void 0;
|
|
1438
|
+
const details = errData && typeof errData === "object" && "expected" in errData ? ` (expected=${errData.expected}, received=${errData.received})` : "";
|
|
1439
|
+
throw new Error(
|
|
1440
|
+
`OpenKey signature verification failed${type ? `: ${type}` : ""}${details}`
|
|
1441
|
+
);
|
|
1442
|
+
}
|
|
1443
|
+
if ((0, import_sdk_core3.canonicalizeAddress)(parsedSignedSiwe.address) !== expectedAddress) {
|
|
1444
|
+
throw new Error(
|
|
1445
|
+
`OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
|
|
1446
|
+
);
|
|
1447
|
+
}
|
|
1448
|
+
const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
|
|
1449
|
+
const originalFields = (0, import_sdk_core3.extractImmutableSiweFields)(prepared.siwe);
|
|
1450
|
+
const signedFields = (0, import_sdk_core3.extractImmutableSiweFields)(result.signedMessage);
|
|
1451
|
+
const changedFields = (0, import_sdk_core3.diffImmutableSiweFields)(originalFields, signedFields, {
|
|
1452
|
+
originalHasRecap
|
|
1453
|
+
});
|
|
1454
|
+
if (changedFields.length > 0) {
|
|
1455
|
+
throw new Error(
|
|
1456
|
+
`OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
|
|
1457
|
+
);
|
|
1458
|
+
}
|
|
1459
|
+
let originalCaps;
|
|
1460
|
+
let signedCaps;
|
|
1461
|
+
try {
|
|
1462
|
+
originalCaps = (0, import_sdk_core3.extractRecapAttenuations)(prepared.siwe);
|
|
1463
|
+
} catch (err) {
|
|
1464
|
+
throw new Error(
|
|
1465
|
+
`Failed to decode prepared SIWE recap: ${err.message}`
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
try {
|
|
1469
|
+
signedCaps = (0, import_sdk_core3.extractRecapAttenuations)(result.signedMessage);
|
|
1470
|
+
} catch (err) {
|
|
1471
|
+
throw new Error(
|
|
1472
|
+
`Failed to decode OpenKey signedMessage recap: ${err.message}`
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
const unauthorized = (0, import_sdk_core3.unauthorizedRecapCapabilities)(signedCaps, originalCaps);
|
|
1476
|
+
if (unauthorized.length > 0) {
|
|
1477
|
+
const previewLimit = 5;
|
|
1478
|
+
const preview = unauthorized.slice(0, previewLimit).map((u) => `${u.resource}::${u.action}`).join(", ");
|
|
1479
|
+
const overflow = unauthorized.length > previewLimit ? ` (and ${unauthorized.length - previewLimit} more)` : "";
|
|
1480
|
+
throw new Error(
|
|
1481
|
+
`OpenKey signedMessage broadened authorization beyond the original request: ${preview}${overflow}`
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
if (!result.signedMessage.includes(prepared.spaceId)) {
|
|
1485
|
+
throw new Error(
|
|
1486
|
+
`OpenKey signedMessage does not reference the expected spaceId ${prepared.spaceId}`
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
const grantedPairs = /* @__PURE__ */ new Set();
|
|
1490
|
+
const grantedFourPartIndex = /* @__PURE__ */ new Map();
|
|
1491
|
+
for (const [resource, actions] of Object.entries(signedCaps)) {
|
|
1492
|
+
const { space, path } = (0, import_sdk_core3.parseCanonicalRecapResource)(resource);
|
|
1493
|
+
for (const action of Object.keys(actions)) {
|
|
1494
|
+
grantedPairs.add(`${resource}\0${action}`);
|
|
1495
|
+
const slashIdx = action.indexOf("/");
|
|
1496
|
+
const service = slashIdx > 0 ? action.slice(0, slashIdx) : "";
|
|
1497
|
+
if (service) {
|
|
1498
|
+
grantedFourPartIndex.set(
|
|
1499
|
+
`${service}\0${space}\0${path}\0${action}`,
|
|
1500
|
+
`${resource}\0${action}`
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
const isStructurallyRequired = (action) => action === "tinycloud.capabilities/read" || action === "capabilities/read";
|
|
1506
|
+
const nonRequiredGrantedPairs = /* @__PURE__ */ new Set();
|
|
1507
|
+
for (const pair of grantedPairs) {
|
|
1508
|
+
const idx = pair.indexOf("\0");
|
|
1509
|
+
const action = idx >= 0 ? pair.slice(idx + 1) : pair;
|
|
1510
|
+
if (!isStructurallyRequired(action)) {
|
|
1511
|
+
nonRequiredGrantedPairs.add(pair);
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
const allSignedGrantedPairs = new Set(grantedPairs);
|
|
1515
|
+
const seenRawKeys = /* @__PURE__ */ new Set();
|
|
1516
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
1517
|
+
if (seenRawKeys.has(rawKey)) {
|
|
1518
|
+
throw new Error(
|
|
1519
|
+
`OpenKey selectedActionKeys contains duplicate entry ${rawKey}`
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
seenRawKeys.add(rawKey);
|
|
1523
|
+
}
|
|
1524
|
+
const selectedResourceActionPairs = /* @__PURE__ */ new Set();
|
|
1525
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
1526
|
+
const parts = rawKey.split("\0");
|
|
1527
|
+
if (parts.length !== 4) {
|
|
1528
|
+
throw new Error(
|
|
1529
|
+
`OpenKey selectedActionKeys entry is malformed (expected canonical 4-part service\\0space\\0path\\0ability): ${rawKey}`
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
const canonical = grantedFourPartIndex.get(rawKey);
|
|
1533
|
+
if (!canonical) {
|
|
1534
|
+
throw new Error(
|
|
1535
|
+
`OpenKey selectedActionKeys entry ${rawKey} is not covered by any granted capability`
|
|
1536
|
+
);
|
|
1537
|
+
}
|
|
1538
|
+
selectedResourceActionPairs.add(canonical);
|
|
1539
|
+
}
|
|
1540
|
+
if (nonRequiredGrantedPairs.size > 0) {
|
|
1541
|
+
const missingPairs = [];
|
|
1542
|
+
for (const pair of nonRequiredGrantedPairs) {
|
|
1543
|
+
if (!selectedResourceActionPairs.has(pair)) {
|
|
1544
|
+
missingPairs.push(pair);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
if (missingPairs.length > 0) {
|
|
1548
|
+
const previewLimit = 5;
|
|
1549
|
+
const preview = missingPairs.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
1550
|
+
const overflow = missingPairs.length > previewLimit ? ` (and ${missingPairs.length - previewLimit} more)` : "";
|
|
1551
|
+
throw new Error(
|
|
1552
|
+
`OpenKey selectedActionKeys is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
1553
|
+
);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
if (allSignedGrantedPairs.size > 0 && result.permissions.length === 0) {
|
|
1557
|
+
throw new Error(
|
|
1558
|
+
"OpenKey permissions is empty but signedMessage carries capabilities \u2014 refusing to accept a session without an authoritative grant list"
|
|
1559
|
+
);
|
|
1560
|
+
}
|
|
1561
|
+
const permissionCanonicalKeys = /* @__PURE__ */ new Set();
|
|
1562
|
+
const permissionPairsCovered = /* @__PURE__ */ new Set();
|
|
1563
|
+
for (const perm of result.permissions) {
|
|
1564
|
+
const { service, space, path, actions } = perm;
|
|
1565
|
+
if (typeof service !== "string" || typeof space !== "string" || typeof path !== "string") {
|
|
1566
|
+
throw new Error(
|
|
1567
|
+
`OpenKey permissions entry has non-string service/space/path (service=${service}, space=${space}, path=${path})`
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
1571
|
+
throw new Error(
|
|
1572
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has empty actions \u2014 a capability-bearing entry must list at least one action`
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
const canonicalKey = `${service}\0${space}\0${path}`;
|
|
1576
|
+
if (permissionCanonicalKeys.has(canonicalKey)) {
|
|
1577
|
+
throw new Error(
|
|
1578
|
+
`OpenKey permissions has duplicate entry for (${service} ${space} ${path})`
|
|
1579
|
+
);
|
|
1580
|
+
}
|
|
1581
|
+
permissionCanonicalKeys.add(canonicalKey);
|
|
1582
|
+
let expectedResource;
|
|
1583
|
+
if (space.startsWith("tinycloud:")) {
|
|
1584
|
+
const shortService = service.startsWith("tinycloud.") ? service.slice("tinycloud.".length) : service;
|
|
1585
|
+
expectedResource = path ? `${space}/${shortService}/${path}` : `${space}/${shortService}`;
|
|
1586
|
+
} else {
|
|
1587
|
+
if (path) {
|
|
1588
|
+
throw new Error(
|
|
1589
|
+
`OpenKey permissions entry (${service} ${space} ${path}) carries a path on a non-tinycloud: resource URI, which is not a recognized encoding`
|
|
1590
|
+
);
|
|
1591
|
+
}
|
|
1592
|
+
expectedResource = space;
|
|
1593
|
+
}
|
|
1594
|
+
let matchedResource;
|
|
1595
|
+
if (signedCaps[expectedResource] !== void 0) {
|
|
1596
|
+
matchedResource = expectedResource;
|
|
1597
|
+
} else {
|
|
1598
|
+
throw new Error(
|
|
1599
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims grants not confirmed in signedMessage capabilities`
|
|
1600
|
+
);
|
|
1601
|
+
}
|
|
1602
|
+
const grantedActionsForResource = new Set(
|
|
1603
|
+
Object.keys(signedCaps[matchedResource])
|
|
1604
|
+
);
|
|
1605
|
+
for (const action of actions) {
|
|
1606
|
+
if (typeof action !== "string" || !action) {
|
|
1607
|
+
throw new Error(
|
|
1608
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has non-string action ${String(action)}`
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
if (!grantedActionsForResource.has(action)) {
|
|
1612
|
+
throw new Error(
|
|
1613
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims action ${action} not present in signedMessage capabilities`
|
|
1614
|
+
);
|
|
1615
|
+
}
|
|
1616
|
+
permissionPairsCovered.add(`${matchedResource}\0${action}`);
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
if (allSignedGrantedPairs.size > 0) {
|
|
1620
|
+
const missingFromPerms = [];
|
|
1621
|
+
for (const pair of allSignedGrantedPairs) {
|
|
1622
|
+
if (!permissionPairsCovered.has(pair)) {
|
|
1623
|
+
missingFromPerms.push(pair);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
if (missingFromPerms.length > 0) {
|
|
1627
|
+
const previewLimit = 5;
|
|
1628
|
+
const preview = missingFromPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
1629
|
+
const overflow = missingFromPerms.length > previewLimit ? ` (and ${missingFromPerms.length - previewLimit} more)` : "";
|
|
1630
|
+
throw new Error(
|
|
1631
|
+
`OpenKey permissions is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1634
|
+
const extrasInPerms = [];
|
|
1635
|
+
for (const pair of permissionPairsCovered) {
|
|
1636
|
+
if (!allSignedGrantedPairs.has(pair)) {
|
|
1637
|
+
extrasInPerms.push(pair);
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
if (extrasInPerms.length > 0) {
|
|
1641
|
+
const previewLimit = 5;
|
|
1642
|
+
const preview = extrasInPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
1643
|
+
const overflow = extrasInPerms.length > previewLimit ? ` (and ${extrasInPerms.length - previewLimit} more)` : "";
|
|
1644
|
+
throw new Error(
|
|
1645
|
+
`OpenKey permissions contains entries not present in the signed authority: ${preview}${overflow}`
|
|
1646
|
+
);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
void nonRequiredGrantedPairs;
|
|
1650
|
+
return this.signInWithPreparedSession(
|
|
1651
|
+
{
|
|
1652
|
+
siwe: result.signedMessage,
|
|
1653
|
+
jwk: prepared.jwk,
|
|
1654
|
+
spaceId: prepared.spaceId,
|
|
1655
|
+
verificationMethod: prepared.verificationMethod
|
|
1656
|
+
},
|
|
1657
|
+
result.signature,
|
|
1658
|
+
keyId,
|
|
1659
|
+
jwk
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1206
1662
|
/**
|
|
1207
1663
|
* Clear persisted session data.
|
|
1208
1664
|
*/
|
|
@@ -1697,6 +2153,9 @@ var DEFAULT_HOST = "https://node.tinycloud.xyz";
|
|
|
1697
2153
|
var DEFAULT_ENCRYPTION_NETWORK_NAME = "default";
|
|
1698
2154
|
var NETWORK_CREATE_ACTION2 = import_sdk_core8.ENCRYPTION.NETWORK_CREATE;
|
|
1699
2155
|
var DECRYPT_ACTION2 = import_sdk_core8.ENCRYPTION.DECRYPT;
|
|
2156
|
+
var BOOTSTRAP_COMPLETION_MARKER_KEY = "system/bootstrap/complete";
|
|
2157
|
+
var BOOTSTRAP_COMPLETION_MARKER_VERSION = 1;
|
|
2158
|
+
var ACCEPTED_MARKER_VERSIONS = [BOOTSTRAP_COMPLETION_MARKER_VERSION];
|
|
1700
2159
|
var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
|
|
1701
2160
|
function isSecretPayload(value) {
|
|
1702
2161
|
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";
|
|
@@ -2007,7 +2466,9 @@ function base64UrlDecode(value) {
|
|
|
2007
2466
|
return new Uint8Array(bytes);
|
|
2008
2467
|
}
|
|
2009
2468
|
async function signJwtInputWithJwk(signingInput, jwk) {
|
|
2010
|
-
|
|
2469
|
+
return signBytesWithJwk(new TextEncoder().encode(signingInput), jwk);
|
|
2470
|
+
}
|
|
2471
|
+
async function signBytesWithJwk(bytes, jwk) {
|
|
2011
2472
|
try {
|
|
2012
2473
|
const subtle = globalThis.crypto?.subtle;
|
|
2013
2474
|
if (!subtle) {
|
|
@@ -2088,6 +2549,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2088
2549
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
2089
2550
|
this._secrets = /* @__PURE__ */ new Map();
|
|
2090
2551
|
this.runtimePermissionGrants = [];
|
|
2552
|
+
/**
|
|
2553
|
+
* Owned space ids this sign-in has already confirmed are hosted. Consulted by
|
|
2554
|
+
* {@link ensureOwnedSpaceHostedById}; cleared on every {@link signIn} because
|
|
2555
|
+
* hosting is confirmed against the session that was active at the time.
|
|
2556
|
+
*/
|
|
2557
|
+
this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
|
|
2091
2558
|
/**
|
|
2092
2559
|
* True when the last signIn() detected an interactive signer and skipped
|
|
2093
2560
|
* client-side bootstrap. Apps can read this to know whether bootstrap was
|
|
@@ -2096,9 +2563,15 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2096
2563
|
this._bootstrapSkipped = false;
|
|
2097
2564
|
/**
|
|
2098
2565
|
* Outcome of the last signIn()'s account-bootstrap attempt. `skipped` is
|
|
2099
|
-
* true when bootstrap did not complete (interactive signer,
|
|
2100
|
-
*
|
|
2101
|
-
*
|
|
2566
|
+
* true when bootstrap did not complete in this invocation (interactive signer,
|
|
2567
|
+
* auto-bootstrap disabled, already provisioned, or a decision/provisioning
|
|
2568
|
+
* step failed). `already-provisioned` means the bootstrap was not run here;
|
|
2569
|
+
* it does not mean a prior provisioning attempt failed to complete.
|
|
2570
|
+
* `reason` carries the cause so apps can surface a "finish account setup"
|
|
2571
|
+
* call-to-action. `warnings` is
|
|
2572
|
+
* present when bootstrap completed, but one or more steps recovered from
|
|
2573
|
+
* an ambiguous failure (e.g. a KV batch write reconciled via per-space
|
|
2574
|
+
* fallback) — clean vs. recovered runs are otherwise byte-identical.
|
|
2102
2575
|
*/
|
|
2103
2576
|
this._bootstrapStatus = {
|
|
2104
2577
|
skipped: false
|
|
@@ -2278,7 +2751,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2278
2751
|
nonce: config.nonce,
|
|
2279
2752
|
siweConfig: config.siweConfig,
|
|
2280
2753
|
manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
|
|
2281
|
-
capabilityRequest: useBootstrapSignInRequest ? import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
|
|
2754
|
+
capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? import_sdk_core8.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
|
|
2282
2755
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
|
|
2283
2756
|
});
|
|
2284
2757
|
this.tc = new import_sdk_core8.TinyCloud(this.auth, {
|
|
@@ -2484,6 +2957,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2484
2957
|
this._spaceService = void 0;
|
|
2485
2958
|
this._serviceContext = void 0;
|
|
2486
2959
|
this.runtimePermissionGrants = [];
|
|
2960
|
+
this.confirmedHostedSpaceIds.clear();
|
|
2487
2961
|
await this.tc.signIn(options);
|
|
2488
2962
|
this.syncResolvedHostFromAuth();
|
|
2489
2963
|
const oldGraph = this._serviceGraph;
|
|
@@ -2520,6 +2994,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2520
2994
|
this._bootstrapSkipped = false;
|
|
2521
2995
|
this._bootstrapStatus = { skipped: false };
|
|
2522
2996
|
if (this.config.autoBootstrapAccount === false) {
|
|
2997
|
+
this._bootstrapStatus = { skipped: true, reason: "auto-bootstrap-disabled" };
|
|
2523
2998
|
return false;
|
|
2524
2999
|
}
|
|
2525
3000
|
if (!this.auth || !this._address) {
|
|
@@ -2534,11 +3009,20 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2534
3009
|
return false;
|
|
2535
3010
|
}
|
|
2536
3011
|
const steps = (0, import_sdk_core8.bootstrapSteps)(this._address, this._chainId);
|
|
2537
|
-
if (!await this.isFreshBootstrapAccount(steps)) {
|
|
2538
|
-
return false;
|
|
2539
|
-
}
|
|
2540
3012
|
try {
|
|
2541
|
-
await this.
|
|
3013
|
+
const decision = await this.resolveBootstrapDecision(steps);
|
|
3014
|
+
if (decision.action === "skip") {
|
|
3015
|
+
this._bootstrapStatus = { skipped: true, reason: "already-provisioned" };
|
|
3016
|
+
return false;
|
|
3017
|
+
}
|
|
3018
|
+
const warnings = await this.runAccountBootstrap(steps, { mode: decision.mode });
|
|
3019
|
+
await this.writeBootstrapCompletionMarker(steps);
|
|
3020
|
+
if (warnings.length > 0) {
|
|
3021
|
+
this._bootstrapStatus = { skipped: false, warnings };
|
|
3022
|
+
this.notificationHandler.warning(
|
|
3023
|
+
`Account bootstrap completed with warnings: ${warnings.map((w) => `${w.stepId} (${w.kind}: ${w.code})`).join("; ")}`
|
|
3024
|
+
);
|
|
3025
|
+
}
|
|
2542
3026
|
} catch (err) {
|
|
2543
3027
|
const reason = err instanceof Error ? err.message : String(err);
|
|
2544
3028
|
this._bootstrapSkipped = true;
|
|
@@ -2551,32 +3035,92 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2551
3035
|
}
|
|
2552
3036
|
return true;
|
|
2553
3037
|
}
|
|
2554
|
-
async
|
|
3038
|
+
async resolveBootstrapDecision(steps) {
|
|
2555
3039
|
const enshrinedSpaceIds = /* @__PURE__ */ new Set();
|
|
2556
3040
|
for (const step of steps) {
|
|
2557
3041
|
if (step.kind === "session") {
|
|
2558
3042
|
enshrinedSpaceIds.add(step.spaceId);
|
|
2559
3043
|
}
|
|
2560
3044
|
}
|
|
2561
|
-
const
|
|
3045
|
+
const auth = this.auth;
|
|
3046
|
+
if (!auth) {
|
|
3047
|
+
throw new Error("Account bootstrap requires an active wallet session");
|
|
3048
|
+
}
|
|
3049
|
+
const skipped = auth.lastActivationSkippedSpaceIds;
|
|
2562
3050
|
if (skipped.some((spaceId) => enshrinedSpaceIds.has(spaceId))) {
|
|
2563
|
-
return
|
|
3051
|
+
return { action: "run", mode: "fresh" };
|
|
2564
3052
|
}
|
|
2565
3053
|
try {
|
|
2566
|
-
const
|
|
2567
|
-
|
|
2568
|
-
|
|
3054
|
+
const accountSpaceId = this.ownedSpaceId(import_sdk_core8.ACCOUNT_REGISTRY_SPACE);
|
|
3055
|
+
const markerPermission = {
|
|
3056
|
+
service: "tinycloud.kv",
|
|
3057
|
+
space: accountSpaceId,
|
|
3058
|
+
path: BOOTSTRAP_COMPLETION_MARKER_KEY,
|
|
3059
|
+
actions: ["get"]
|
|
3060
|
+
};
|
|
3061
|
+
if (!this.hasRuntimePermissions([markerPermission])) {
|
|
3062
|
+
return { action: "run", mode: "repair" };
|
|
2569
3063
|
}
|
|
2570
|
-
|
|
3064
|
+
const marker = await this.readBootstrapCompletionMarker();
|
|
3065
|
+
if (marker.ok) {
|
|
3066
|
+
if (this.isAcceptedBootstrapCompletionMarker(marker.data.data)) {
|
|
3067
|
+
return { action: "skip" };
|
|
3068
|
+
}
|
|
3069
|
+
return { action: "run", mode: "repair" };
|
|
3070
|
+
}
|
|
3071
|
+
if (marker.error.code === import_sdk_core8.ErrorCodes.KV_NOT_FOUND && this.markerReadIsUnhostedSpace(marker.error.meta)) {
|
|
3072
|
+
return { action: "run", mode: "fresh" };
|
|
3073
|
+
}
|
|
3074
|
+
} catch (err) {
|
|
3075
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3076
|
+
this.notificationHandler.warning(
|
|
3077
|
+
`Account bootstrap decision failed; attempting one bounded repair: ${reason}`
|
|
3078
|
+
);
|
|
3079
|
+
console.warn(`[TinyCloudNode] account bootstrap decision failed: ${reason}`);
|
|
3080
|
+
return { action: "run", mode: "repair" };
|
|
2571
3081
|
}
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
3082
|
+
return { action: "run", mode: "repair" };
|
|
3083
|
+
}
|
|
3084
|
+
async readBootstrapCompletionMarker() {
|
|
3085
|
+
return this.kvForSpace(this.ownedSpaceId(import_sdk_core8.ACCOUNT_REGISTRY_SPACE)).get(
|
|
3086
|
+
BOOTSTRAP_COMPLETION_MARKER_KEY
|
|
3087
|
+
);
|
|
3088
|
+
}
|
|
3089
|
+
async writeBootstrapCompletionMarker(steps) {
|
|
3090
|
+
if (!this._address) {
|
|
3091
|
+
throw new Error("Bootstrap completion marker requires an active wallet session");
|
|
3092
|
+
}
|
|
3093
|
+
const canonicalStepIds = new Set(
|
|
3094
|
+
(0, import_sdk_core8.bootstrapSteps)(this._address, this._chainId).map((step) => step.id)
|
|
3095
|
+
);
|
|
3096
|
+
const providedStepIds = new Set(steps.map((step) => step.id));
|
|
3097
|
+
if (canonicalStepIds.size !== providedStepIds.size || [...canonicalStepIds].some((stepId) => !providedStepIds.has(stepId))) {
|
|
3098
|
+
throw new Error("Bootstrap completion marker requires the canonical bootstrap step set");
|
|
3099
|
+
}
|
|
3100
|
+
const marker = {
|
|
3101
|
+
v: BOOTSTRAP_COMPLETION_MARKER_VERSION,
|
|
3102
|
+
stepIds: [...canonicalStepIds],
|
|
3103
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3104
|
+
};
|
|
3105
|
+
const written = await this.kvForSpace(this.ownedSpaceId(import_sdk_core8.ACCOUNT_REGISTRY_SPACE)).put(
|
|
3106
|
+
BOOTSTRAP_COMPLETION_MARKER_KEY,
|
|
3107
|
+
marker
|
|
3108
|
+
);
|
|
3109
|
+
if (!written.ok) {
|
|
3110
|
+
throw new Error(`Failed to write bootstrap completion marker: ${written.error.message}`);
|
|
2577
3111
|
}
|
|
2578
3112
|
}
|
|
2579
|
-
|
|
3113
|
+
isAcceptedBootstrapCompletionMarker(value) {
|
|
3114
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3115
|
+
const entries = Object.entries(value);
|
|
3116
|
+
const markerValue = (name) => entries.find(([key]) => key === name)?.[1];
|
|
3117
|
+
const version = markerValue("v");
|
|
3118
|
+
return typeof version === "number" && Number.isInteger(version) && ACCEPTED_MARKER_VERSIONS.some((acceptedVersion) => acceptedVersion === version);
|
|
3119
|
+
}
|
|
3120
|
+
markerReadIsUnhostedSpace(meta) {
|
|
3121
|
+
return meta?.status === 404;
|
|
3122
|
+
}
|
|
3123
|
+
async runAccountBootstrap(steps, { mode } = { mode: "fresh" }) {
|
|
2580
3124
|
if (!this.auth || !this._address) {
|
|
2581
3125
|
throw new Error("Account bootstrap requires an active wallet session");
|
|
2582
3126
|
}
|
|
@@ -2587,6 +3131,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2587
3131
|
const auth = this.auth;
|
|
2588
3132
|
const sessions = /* @__PURE__ */ new Map();
|
|
2589
3133
|
const rawAbilitiesBySpace = /* @__PURE__ */ new Map();
|
|
3134
|
+
const warnings = [];
|
|
2590
3135
|
const primarySession = auth.tinyCloudSession;
|
|
2591
3136
|
const defaultSpaceId = this.ownedSpaceId("default");
|
|
2592
3137
|
const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default;
|
|
@@ -2648,32 +3193,44 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2648
3193
|
}
|
|
2649
3194
|
}
|
|
2650
3195
|
if (step.kind === "seed-spaces") {
|
|
2651
|
-
|
|
2652
|
-
|
|
3196
|
+
const registered = await this.account.spaces.registerBatch(
|
|
3197
|
+
step.spaces.map((space) => ({
|
|
2653
3198
|
spaceId: space.spaceId,
|
|
2654
3199
|
name: space.name,
|
|
2655
3200
|
ownerDid: this.did,
|
|
2656
3201
|
type: "owned",
|
|
2657
3202
|
permissions: ["*"],
|
|
2658
3203
|
status: "active"
|
|
3204
|
+
}))
|
|
3205
|
+
);
|
|
3206
|
+
if (!registered.ok) {
|
|
3207
|
+
throw new Error(
|
|
3208
|
+
`Failed to seed account spaces: ${registered.error.message}`
|
|
3209
|
+
);
|
|
3210
|
+
}
|
|
3211
|
+
const batchError = registered.data.recoveredFromBatchError;
|
|
3212
|
+
if (batchError) {
|
|
3213
|
+
warnings.push({
|
|
3214
|
+
stepId: step.id,
|
|
3215
|
+
kind: "batch-write-reconciled",
|
|
3216
|
+
code: batchError.code,
|
|
3217
|
+
message: batchError.message
|
|
2659
3218
|
});
|
|
2660
|
-
if (!registered.ok) {
|
|
2661
|
-
throw new Error(
|
|
2662
|
-
`Failed to seed account space ${space.spaceId}: ${registered.error.message}`
|
|
2663
|
-
);
|
|
2664
|
-
}
|
|
2665
3219
|
}
|
|
2666
3220
|
}
|
|
2667
3221
|
if (step.kind === "seed-applications") {
|
|
2668
3222
|
const registered = await this.account.applications.register(
|
|
2669
|
-
step.manifests.length > 0 ? [...step.manifests] : import_sdk_core8.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST
|
|
3223
|
+
step.manifests.length > 0 ? [...step.manifests] : import_sdk_core8.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
|
|
3224
|
+
// The write is an INSERT OR REPLACE and is safe to repeat during a
|
|
3225
|
+
// repair, so skip the manifest-hash pre-read.
|
|
3226
|
+
{ assumeUnregistered: true }
|
|
2670
3227
|
);
|
|
2671
3228
|
if (!registered.ok) {
|
|
2672
3229
|
throw new Error(`Failed to seed bootstrap applications: ${registered.error.message}`);
|
|
2673
3230
|
}
|
|
2674
3231
|
}
|
|
2675
3232
|
if (step.kind === "encryption-network-create") {
|
|
2676
|
-
await this.ensureEncryptionNetwork(step.networkId);
|
|
3233
|
+
await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: mode === "fresh" });
|
|
2677
3234
|
}
|
|
2678
3235
|
if (step.kind === "secret-records-schema") {
|
|
2679
3236
|
const db = this.sqlForSpace(step.spaceId).db(step.database);
|
|
@@ -2693,6 +3250,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2693
3250
|
}
|
|
2694
3251
|
}
|
|
2695
3252
|
}
|
|
3253
|
+
return warnings;
|
|
2696
3254
|
}
|
|
2697
3255
|
registerBootstrapRuntimeGrant(session, request, rawAbilities) {
|
|
2698
3256
|
const operations = [];
|
|
@@ -2952,6 +3510,22 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2952
3510
|
await this.ensureEncryptionNetwork(networkId);
|
|
2953
3511
|
}
|
|
2954
3512
|
}
|
|
3513
|
+
/**
|
|
3514
|
+
* Ensure one of this user's owned spaces is hosted, at most once per space
|
|
3515
|
+
* per sign-in.
|
|
3516
|
+
*
|
|
3517
|
+
* Account bootstrap funnels six calls through here (via
|
|
3518
|
+
* `AccountService.ensureAccountSpaceHosted`), and each one re-submits
|
|
3519
|
+
* `activateSessionWithHost` with the *primary* session. That session's recap
|
|
3520
|
+
* covers only `default` and `secrets`, so the account space appears in
|
|
3521
|
+
* neither `activated` nor `skipped` and the guard below returns immediately
|
|
3522
|
+
* — the repeat calls cannot change the outcome, they only cost round trips.
|
|
3523
|
+
*
|
|
3524
|
+
* The memo is per sign-in ({@link confirmedHostedSpaceIds} is cleared in
|
|
3525
|
+
* `signIn`) and only records confirmed successes. A wrong skip is
|
|
3526
|
+
* self-revealing: the next write to the space fails immediately and loudly
|
|
3527
|
+
* with `404 Space not found`.
|
|
3528
|
+
*/
|
|
2955
3529
|
async ensureOwnedSpaceHostedById(spaceId) {
|
|
2956
3530
|
if (!this.auth) {
|
|
2957
3531
|
throw new Error("Owned space hosting requires wallet mode");
|
|
@@ -2960,12 +3534,16 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2960
3534
|
if (!session) {
|
|
2961
3535
|
throw new Error("Owned space hosting requires an active session");
|
|
2962
3536
|
}
|
|
3537
|
+
if (this.confirmedHostedSpaceIds.has(spaceId)) {
|
|
3538
|
+
return;
|
|
3539
|
+
}
|
|
2963
3540
|
const host = this.hosts[0] ?? this.config.host;
|
|
2964
3541
|
if (!host) {
|
|
2965
3542
|
throw new Error("Owned space hosting requires a TinyCloud host");
|
|
2966
3543
|
}
|
|
2967
3544
|
const activation = await (0, import_sdk_core8.activateSessionWithHost)(host, session.delegationHeader);
|
|
2968
3545
|
if (activation.success && !activation.skipped?.includes(spaceId)) {
|
|
3546
|
+
this.confirmedHostedSpaceIds.add(spaceId);
|
|
2969
3547
|
return;
|
|
2970
3548
|
}
|
|
2971
3549
|
if (!activation.success && activation.status !== 404) {
|
|
@@ -2984,6 +3562,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2984
3562
|
`Failed to activate session after creating owned space ${spaceId}: ${retry.error ?? "space was skipped"}`
|
|
2985
3563
|
);
|
|
2986
3564
|
}
|
|
3565
|
+
this.confirmedHostedSpaceIds.add(spaceId);
|
|
2987
3566
|
}
|
|
2988
3567
|
/**
|
|
2989
3568
|
* Host one of this user's owned spaces by name (e.g. `"applications"`).
|
|
@@ -3299,6 +3878,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3299
3878
|
this._delegationManager = stagedGraph.delegationManager;
|
|
3300
3879
|
this._spaceService = stagedGraph.spaceService;
|
|
3301
3880
|
this._serviceGraph = stagedGraph.graph;
|
|
3881
|
+
this._activeServiceSession = serviceSession;
|
|
3302
3882
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
3303
3883
|
this._secrets = /* @__PURE__ */ new Map();
|
|
3304
3884
|
this._publicKV = void 0;
|
|
@@ -3306,6 +3886,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3306
3886
|
this.tc = stagedGraph.core;
|
|
3307
3887
|
this._recapOperationsCache = stagedPrimary.cache;
|
|
3308
3888
|
this.runtimePermissionGrants = stagedPrimary.grants;
|
|
3889
|
+
this.confirmedHostedSpaceIds.clear();
|
|
3309
3890
|
if (this.auth) {
|
|
3310
3891
|
this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost]);
|
|
3311
3892
|
this._restoredTcSession = void 0;
|
|
@@ -3615,6 +4196,28 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3615
4196
|
currentTinyCloudSession() {
|
|
3616
4197
|
return this.auth?.tinyCloudSession ?? this._restoredTcSession;
|
|
3617
4198
|
}
|
|
4199
|
+
/**
|
|
4200
|
+
* Mint a multi-capability invocation through the established session key.
|
|
4201
|
+
* Callers receive only the signed headers; session key material remains
|
|
4202
|
+
* owned by the session manager and is never part of the caller contract.
|
|
4203
|
+
*/
|
|
4204
|
+
invokeAny(entries, facts) {
|
|
4205
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
4206
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
4207
|
+
return this._serviceGraph.invokeAny(session, entries, facts);
|
|
4208
|
+
}
|
|
4209
|
+
/** Sign protocol bytes with the established in-memory session key. */
|
|
4210
|
+
async signSessionBytes(bytes) {
|
|
4211
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
4212
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
4213
|
+
return signBytesWithJwk(bytes, session.jwk);
|
|
4214
|
+
}
|
|
4215
|
+
/** Bind a session-signed invocation to the canonical service audience. */
|
|
4216
|
+
async bindInvocationAudience(authorization, audience) {
|
|
4217
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
4218
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
4219
|
+
return rewriteInvocationAudience(authorization, audience, session.jwk);
|
|
4220
|
+
}
|
|
3618
4221
|
/**
|
|
3619
4222
|
* Connect a wallet to upgrade from session-only mode to wallet mode.
|
|
3620
4223
|
*
|
|
@@ -3676,7 +4279,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3676
4279
|
nonce: this.config.nonce,
|
|
3677
4280
|
siweConfig: this.config.siweConfig,
|
|
3678
4281
|
manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
|
|
3679
|
-
capabilityRequest: useBootstrapSignInRequest ? import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
4282
|
+
capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? import_sdk_core8.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
3680
4283
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
|
|
3681
4284
|
});
|
|
3682
4285
|
this.tc = new import_sdk_core8.TinyCloud(this.auth, {
|
|
@@ -3729,7 +4332,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3729
4332
|
nonce: this.config.nonce,
|
|
3730
4333
|
siweConfig: this.config.siweConfig,
|
|
3731
4334
|
manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
|
|
3732
|
-
capabilityRequest: useBootstrapSignInRequest ? import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
4335
|
+
capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? import_sdk_core8.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
3733
4336
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
|
|
3734
4337
|
});
|
|
3735
4338
|
this.tc = new import_sdk_core8.TinyCloud(this.auth, {
|
|
@@ -3796,6 +4399,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3796
4399
|
if (this._serviceContext) {
|
|
3797
4400
|
const spaceScopedContext = this._serviceGraph.track(new import_sdk_core8.ServiceContext({
|
|
3798
4401
|
invoke: this._serviceContext.invoke,
|
|
4402
|
+
invokeAny: this._serviceContext.invokeAny,
|
|
3799
4403
|
fetch: this._serviceContext.fetch,
|
|
3800
4404
|
hosts: this._serviceContext.hosts,
|
|
3801
4405
|
telemetry: this.config.telemetry
|
|
@@ -3888,7 +4492,17 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3888
4492
|
verifyNodeSignature: (nodeId, message, signature) => (0, import_sdk_core8.verifyDidKeyEd25519Signature)(nodeId, message, signature)
|
|
3889
4493
|
};
|
|
3890
4494
|
}
|
|
4495
|
+
/**
|
|
4496
|
+
* The node DID for {@link config.host}.
|
|
4497
|
+
*
|
|
4498
|
+
* Sign-in already performs `GET /info` for the protocol-version check, and
|
|
4499
|
+
* that response carries `nodeId`, so reuse it rather than fetching `/info` a
|
|
4500
|
+
* second time. Only a value recorded for this exact host qualifies; older
|
|
4501
|
+
* nodes that omit `nodeId` fall through to the fetch.
|
|
4502
|
+
*/
|
|
3891
4503
|
async fetchNodeId() {
|
|
4504
|
+
const cached = this.auth?.nodeIdForHost(this.config.host);
|
|
4505
|
+
if (cached) return cached;
|
|
3892
4506
|
const response = await fetch(`${this.config.host}/info`);
|
|
3893
4507
|
if (!response.ok) {
|
|
3894
4508
|
throw new Error(`Failed to fetch node info: HTTP ${response.status}`);
|
|
@@ -4451,6 +5065,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
4451
5065
|
shareUrl: input.shareUrl,
|
|
4452
5066
|
documentName: input.documentName,
|
|
4453
5067
|
jti: base64UrlEncode(crypto.getRandomValues(new Uint8Array(16))),
|
|
5068
|
+
idempotencyKey: input.idempotencyKey,
|
|
4454
5069
|
expiresAt: input.expiresAt
|
|
4455
5070
|
};
|
|
4456
5071
|
const requestBodyDigest = base64UrlEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode((0, import_sdk_core8.canonicalizeEncryptionJson)(body)))));
|
|
@@ -4660,6 +5275,13 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
4660
5275
|
body
|
|
4661
5276
|
)
|
|
4662
5277
|
});
|
|
5278
|
+
if (response.status === 409) {
|
|
5279
|
+
const existing = await this.getEncryptionNetwork(networkId);
|
|
5280
|
+
if (existing) return existing;
|
|
5281
|
+
throw new Error(
|
|
5282
|
+
`Encryption network ${networkId} already exists but could not be read back`
|
|
5283
|
+
);
|
|
5284
|
+
}
|
|
4663
5285
|
if (!response.ok) {
|
|
4664
5286
|
throw new Error(
|
|
4665
5287
|
`Failed to create encryption network ${networkId}: HTTP ${response.status} ${await response.text()}`
|
|
@@ -4668,11 +5290,19 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
4668
5290
|
const created = await response.json();
|
|
4669
5291
|
return created.descriptor;
|
|
4670
5292
|
}
|
|
4671
|
-
|
|
5293
|
+
/**
|
|
5294
|
+
* Ensure an encryption network exists, creating it if necessary.
|
|
5295
|
+
*
|
|
5296
|
+
* @param nameOrNetworkId - Network name or full `urn:tinycloud:encryption:` id.
|
|
5297
|
+
* @param options - See {@link EnsureEncryptionNetworkOptions}.
|
|
5298
|
+
*/
|
|
5299
|
+
async ensureEncryptionNetwork(nameOrNetworkId = DEFAULT_ENCRYPTION_NETWORK_NAME, options = {}) {
|
|
4672
5300
|
const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
5301
|
+
if (!options.assumeMissing) {
|
|
5302
|
+
const existing = await this.getEncryptionNetwork(networkId);
|
|
5303
|
+
if (existing) {
|
|
5304
|
+
return existing;
|
|
5305
|
+
}
|
|
4676
5306
|
}
|
|
4677
5307
|
const parsed = (0, import_sdk_core8.parseNetworkId)(networkId);
|
|
4678
5308
|
if (!didPrincipalMatches2(parsed.ownerDid, this.did)) {
|
|
@@ -6451,35 +7081,44 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
6451
7081
|
if (parentDelegation.disableSubDelegation) {
|
|
6452
7082
|
throw new Error("Parent delegation does not allow sub-delegation");
|
|
6453
7083
|
}
|
|
6454
|
-
|
|
7084
|
+
const parentResources = parentDelegation.resources ?? [{
|
|
7085
|
+
service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
|
|
7086
|
+
space: parentDelegation.spaceId,
|
|
7087
|
+
path: parentDelegation.path,
|
|
7088
|
+
actions: parentDelegation.actions
|
|
7089
|
+
}];
|
|
7090
|
+
const requestedResources = params.resources ?? [{
|
|
7091
|
+
service: params.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
|
|
7092
|
+
space: parentDelegation.spaceId,
|
|
7093
|
+
path: params.path,
|
|
7094
|
+
actions: params.actions
|
|
7095
|
+
}];
|
|
7096
|
+
const resourceIsContained = (parent, child) => {
|
|
7097
|
+
const parentEncryption = parent.service === "encryption" || parent.service === "tinycloud.encryption";
|
|
7098
|
+
const childEncryption = child.service === "encryption" || child.service === "tinycloud.encryption";
|
|
7099
|
+
if (parentEncryption || childEncryption) return parentEncryption && childEncryption && parent.path === child.path && child.actions.every((action) => parent.actions.includes(action));
|
|
7100
|
+
const pathContained = parent.path === child.path || parent.path.endsWith("/") && child.path.startsWith(parent.path) || child.path.startsWith(`${parent.path}/`);
|
|
7101
|
+
return pathContained && child.actions.every((action) => parent.actions.includes(action));
|
|
7102
|
+
};
|
|
7103
|
+
if (requestedResources.length === 0 || requestedResources.some((child) => !parentResources.some((parent) => resourceIsContained(parent, child)))) {
|
|
6455
7104
|
throw new Error(
|
|
6456
|
-
|
|
7105
|
+
"Sub-delegation resources exceed the received delegation"
|
|
6457
7106
|
);
|
|
6458
7107
|
}
|
|
6459
|
-
const parentActions = new Set(parentDelegation.actions);
|
|
6460
|
-
for (const action of params.actions) {
|
|
6461
|
-
if (!parentActions.has(action)) {
|
|
6462
|
-
throw new Error(
|
|
6463
|
-
`Sub-delegation action "${action}" is not in parent's actions: ${parentDelegation.actions.join(", ")}`
|
|
6464
|
-
);
|
|
6465
|
-
}
|
|
6466
|
-
}
|
|
6467
7108
|
const now = /* @__PURE__ */ new Date();
|
|
6468
7109
|
const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
|
|
6469
7110
|
const requestedExpiry = new Date(now.getTime() + expiryMs);
|
|
6470
7111
|
const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
|
|
6471
7112
|
const abilities = {};
|
|
6472
|
-
const
|
|
6473
|
-
const
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
abilities
|
|
6480
|
-
|
|
6481
|
-
if (duckdbActions.length > 0) {
|
|
6482
|
-
abilities.duckdb = { [params.path]: duckdbActions };
|
|
7113
|
+
const rawAbilities = {};
|
|
7114
|
+
for (const resource of requestedResources) {
|
|
7115
|
+
const service = resource.service.replace(/^tinycloud\./, "");
|
|
7116
|
+
if (service === "encryption") {
|
|
7117
|
+
rawAbilities[resource.path] = [...resource.actions];
|
|
7118
|
+
continue;
|
|
7119
|
+
}
|
|
7120
|
+
abilities[service] ?? (abilities[service] = {});
|
|
7121
|
+
abilities[service][resource.path] = [...resource.actions];
|
|
6483
7122
|
}
|
|
6484
7123
|
const targetHost = parentDelegation.host ?? this.config.host;
|
|
6485
7124
|
const prepared = this.wasmBindings.prepareSession({
|
|
@@ -6491,7 +7130,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
6491
7130
|
expirationTime: actualExpiry.toISOString(),
|
|
6492
7131
|
spaceId: parentDelegation.spaceId,
|
|
6493
7132
|
delegateUri: params.delegateDID,
|
|
6494
|
-
parents: [parentDelegation.cid]
|
|
7133
|
+
parents: [parentDelegation.cid],
|
|
7134
|
+
...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
|
|
6495
7135
|
});
|
|
6496
7136
|
const signature = await this.signer.signMessage(prepared.siwe);
|
|
6497
7137
|
const subDelegationSession = this.wasmBindings.completeSessionSetup({
|
|
@@ -6509,14 +7149,20 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
6509
7149
|
cid: subDelegationSession.delegationCid,
|
|
6510
7150
|
delegationHeader: subDelegationSession.delegationHeader,
|
|
6511
7151
|
spaceId: parentDelegation.spaceId,
|
|
6512
|
-
path:
|
|
6513
|
-
actions:
|
|
7152
|
+
path: requestedResources[0].path,
|
|
7153
|
+
actions: requestedResources[0].actions,
|
|
6514
7154
|
disableSubDelegation: params.disableSubDelegation ?? false,
|
|
6515
7155
|
expiry: actualExpiry,
|
|
6516
7156
|
delegateDID: params.delegateDID,
|
|
6517
7157
|
ownerAddress: parentDelegation.ownerAddress,
|
|
6518
7158
|
chainId: parentDelegation.chainId,
|
|
6519
|
-
host: targetHost
|
|
7159
|
+
host: targetHost,
|
|
7160
|
+
resources: requestedResources.map((resource) => ({
|
|
7161
|
+
service: resource.service.replace(/^tinycloud\./, ""),
|
|
7162
|
+
space: resource.service.replace(/^tinycloud\./, "") === "encryption" ? "encryption" : resource.space ?? parentDelegation.spaceId,
|
|
7163
|
+
path: resource.path,
|
|
7164
|
+
actions: [...resource.actions]
|
|
7165
|
+
}))
|
|
6520
7166
|
};
|
|
6521
7167
|
}
|
|
6522
7168
|
};
|