@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.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";
|
|
@@ -473,6 +513,15 @@ var NodeUserAuthorization = class {
|
|
|
473
513
|
get nodeFeatures() {
|
|
474
514
|
return this._nodeFeatures;
|
|
475
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* The node DID reported by `GET /info` for `host`, if the sign-in (or
|
|
518
|
+
* restore) that populated it targeted that same host. Returns `undefined`
|
|
519
|
+
* for any other host, and for nodes whose `/info` omits `nodeId`, so callers
|
|
520
|
+
* fall back to fetching it themselves rather than using a foreign node's DID.
|
|
521
|
+
*/
|
|
522
|
+
nodeIdForHost(host) {
|
|
523
|
+
return this._nodeInfoIdentity?.host === host ? this._nodeInfoIdentity.nodeId : void 0;
|
|
524
|
+
}
|
|
476
525
|
get lastActivationSkippedSpaceIds() {
|
|
477
526
|
return [...this._lastActivationSkippedSpaceIds];
|
|
478
527
|
}
|
|
@@ -532,23 +581,41 @@ var NodeUserAuthorization = class {
|
|
|
532
581
|
const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
|
|
533
582
|
const primarySpaceId2 = makePkhSpaceId(address, chainId, this.spacePrefix);
|
|
534
583
|
const secretsSpaceId = makePkhSpaceId(address, chainId, "secrets");
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
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);
|
|
549
611
|
}
|
|
550
612
|
}
|
|
551
|
-
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
abilities: primaryActions,
|
|
617
|
+
spaceId: primarySpaceId2,
|
|
618
|
+
spaceAbilities: spaceAbilities2,
|
|
552
619
|
rawAbilities: {
|
|
553
620
|
[defaultNetworkId]: [DECRYPT_ACTION, NETWORK_CREATE_ACTION]
|
|
554
621
|
}
|
|
@@ -942,11 +1009,13 @@ var NodeUserAuthorization = class {
|
|
|
942
1009
|
this._tinyCloudSession = tinyCloudSession;
|
|
943
1010
|
this._address = address;
|
|
944
1011
|
this._chainId = chainId;
|
|
1012
|
+
const infoHost = this.primaryTinyCloudHost;
|
|
945
1013
|
const nodeInfo = await checkNodeInfo(
|
|
946
|
-
|
|
1014
|
+
infoHost,
|
|
947
1015
|
this.wasm.protocolVersion()
|
|
948
1016
|
);
|
|
949
1017
|
this._nodeFeatures = nodeInfo.features;
|
|
1018
|
+
this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
|
|
950
1019
|
for (const ext of this.extensions) {
|
|
951
1020
|
if (ext.afterSignIn) {
|
|
952
1021
|
await ext.afterSignIn(clientSession);
|
|
@@ -1112,11 +1181,13 @@ var NodeUserAuthorization = class {
|
|
|
1112
1181
|
this._tinyCloudSession = tinyCloudSession;
|
|
1113
1182
|
this._address = address;
|
|
1114
1183
|
this._chainId = chainId;
|
|
1184
|
+
const infoHost = this.primaryTinyCloudHost;
|
|
1115
1185
|
const nodeInfo = await checkNodeInfo(
|
|
1116
|
-
|
|
1186
|
+
infoHost,
|
|
1117
1187
|
this.wasm.protocolVersion()
|
|
1118
1188
|
);
|
|
1119
1189
|
this._nodeFeatures = nodeInfo.features;
|
|
1190
|
+
this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
|
|
1120
1191
|
for (const ext of this.extensions) {
|
|
1121
1192
|
if (ext.afterSignIn) {
|
|
1122
1193
|
await ext.afterSignIn(clientSession);
|
|
@@ -1125,6 +1196,398 @@ var NodeUserAuthorization = class {
|
|
|
1125
1196
|
await this.ensureSpaceExists();
|
|
1126
1197
|
return clientSession;
|
|
1127
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
|
+
}
|
|
1128
1591
|
/**
|
|
1129
1592
|
* Clear persisted session data.
|
|
1130
1593
|
*/
|
|
@@ -1216,12 +1679,14 @@ import {
|
|
|
1216
1679
|
SharingService,
|
|
1217
1680
|
UnsupportedFeatureError,
|
|
1218
1681
|
makePublicSpaceId,
|
|
1219
|
-
ACCOUNT_REGISTRY_SPACE,
|
|
1682
|
+
ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2,
|
|
1683
|
+
BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST,
|
|
1220
1684
|
BOOTSTRAP_SESSION_REQUESTS,
|
|
1221
1685
|
SECRET_RECORDS_SCHEMA,
|
|
1222
1686
|
SECRETS_SPACE as SECRETS_SPACE2,
|
|
1223
1687
|
TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
|
|
1224
1688
|
bootstrapSteps,
|
|
1689
|
+
ErrorCodes as ErrorCodes2,
|
|
1225
1690
|
ENCRYPTION_PERMISSION_SERVICE as ENCRYPTION_PERMISSION_SERVICE2,
|
|
1226
1691
|
CaveatedDelegationUnsupportedError,
|
|
1227
1692
|
PermissionNotInManifestError,
|
|
@@ -1439,7 +1904,7 @@ function createWasmKeyProvider(sessionManager) {
|
|
|
1439
1904
|
// src/delegateToHelpers.ts
|
|
1440
1905
|
import {
|
|
1441
1906
|
parseExpiry,
|
|
1442
|
-
SiweMessage,
|
|
1907
|
+
SiweMessage as SiweMessage2,
|
|
1443
1908
|
EXPIRY as EXPIRY2
|
|
1444
1909
|
} from "@tinycloud/sdk-core";
|
|
1445
1910
|
function legacyParamsToPermissionEntries(actions, path, spaceIdOverride) {
|
|
@@ -1488,7 +1953,7 @@ function resolveExpiryMs(expiry) {
|
|
|
1488
1953
|
return parseExpiry(expiry);
|
|
1489
1954
|
}
|
|
1490
1955
|
function extractSiweExpiration(siwe) {
|
|
1491
|
-
const parsed = new
|
|
1956
|
+
const parsed = new SiweMessage2(siwe);
|
|
1492
1957
|
if (parsed.expirationTime === void 0 || parsed.expirationTime === null) {
|
|
1493
1958
|
return void 0;
|
|
1494
1959
|
}
|
|
@@ -1690,6 +2155,9 @@ var DEFAULT_HOST = "https://node.tinycloud.xyz";
|
|
|
1690
2155
|
var DEFAULT_ENCRYPTION_NETWORK_NAME = "default";
|
|
1691
2156
|
var NETWORK_CREATE_ACTION2 = ENCRYPTION2.NETWORK_CREATE;
|
|
1692
2157
|
var DECRYPT_ACTION2 = ENCRYPTION2.DECRYPT;
|
|
2158
|
+
var BOOTSTRAP_COMPLETION_MARKER_KEY = "system/bootstrap/complete";
|
|
2159
|
+
var BOOTSTRAP_COMPLETION_MARKER_VERSION = 1;
|
|
2160
|
+
var ACCEPTED_MARKER_VERSIONS = [BOOTSTRAP_COMPLETION_MARKER_VERSION];
|
|
1693
2161
|
var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
|
|
1694
2162
|
function isSecretPayload(value) {
|
|
1695
2163
|
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";
|
|
@@ -2000,7 +2468,9 @@ function base64UrlDecode(value) {
|
|
|
2000
2468
|
return new Uint8Array(bytes);
|
|
2001
2469
|
}
|
|
2002
2470
|
async function signJwtInputWithJwk(signingInput, jwk) {
|
|
2003
|
-
|
|
2471
|
+
return signBytesWithJwk(new TextEncoder().encode(signingInput), jwk);
|
|
2472
|
+
}
|
|
2473
|
+
async function signBytesWithJwk(bytes, jwk) {
|
|
2004
2474
|
try {
|
|
2005
2475
|
const subtle = globalThis.crypto?.subtle;
|
|
2006
2476
|
if (!subtle) {
|
|
@@ -2081,6 +2551,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2081
2551
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
2082
2552
|
this._secrets = /* @__PURE__ */ new Map();
|
|
2083
2553
|
this.runtimePermissionGrants = [];
|
|
2554
|
+
/**
|
|
2555
|
+
* Owned space ids this sign-in has already confirmed are hosted. Consulted by
|
|
2556
|
+
* {@link ensureOwnedSpaceHostedById}; cleared on every {@link signIn} because
|
|
2557
|
+
* hosting is confirmed against the session that was active at the time.
|
|
2558
|
+
*/
|
|
2559
|
+
this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
|
|
2084
2560
|
/**
|
|
2085
2561
|
* True when the last signIn() detected an interactive signer and skipped
|
|
2086
2562
|
* client-side bootstrap. Apps can read this to know whether bootstrap was
|
|
@@ -2089,9 +2565,15 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2089
2565
|
this._bootstrapSkipped = false;
|
|
2090
2566
|
/**
|
|
2091
2567
|
* Outcome of the last signIn()'s account-bootstrap attempt. `skipped` is
|
|
2092
|
-
* true when bootstrap did not complete (interactive signer,
|
|
2093
|
-
*
|
|
2094
|
-
*
|
|
2568
|
+
* true when bootstrap did not complete in this invocation (interactive signer,
|
|
2569
|
+
* auto-bootstrap disabled, already provisioned, or a decision/provisioning
|
|
2570
|
+
* step failed). `already-provisioned` means the bootstrap was not run here;
|
|
2571
|
+
* it does not mean a prior provisioning attempt failed to complete.
|
|
2572
|
+
* `reason` carries the cause so apps can surface a "finish account setup"
|
|
2573
|
+
* call-to-action. `warnings` is
|
|
2574
|
+
* present when bootstrap completed, but one or more steps recovered from
|
|
2575
|
+
* an ambiguous failure (e.g. a KV batch write reconciled via per-space
|
|
2576
|
+
* fallback) — clean vs. recovered runs are otherwise byte-identical.
|
|
2095
2577
|
*/
|
|
2096
2578
|
this._bootstrapStatus = {
|
|
2097
2579
|
skipped: false
|
|
@@ -2271,7 +2753,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2271
2753
|
nonce: config.nonce,
|
|
2272
2754
|
siweConfig: config.siweConfig,
|
|
2273
2755
|
manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
|
|
2274
|
-
capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
|
|
2756
|
+
capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
|
|
2275
2757
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
|
|
2276
2758
|
});
|
|
2277
2759
|
this.tc = new TinyCloud(this.auth, {
|
|
@@ -2411,7 +2893,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2411
2893
|
if (!this._address) {
|
|
2412
2894
|
return void 0;
|
|
2413
2895
|
}
|
|
2414
|
-
return this.wasmBindings.makeSpaceId(this._address, this._chainId,
|
|
2896
|
+
return this.wasmBindings.makeSpaceId(this._address, this._chainId, ACCOUNT_REGISTRY_SPACE2);
|
|
2415
2897
|
}
|
|
2416
2898
|
/**
|
|
2417
2899
|
* Account-level application and delegation helpers.
|
|
@@ -2477,6 +2959,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2477
2959
|
this._spaceService = void 0;
|
|
2478
2960
|
this._serviceContext = void 0;
|
|
2479
2961
|
this.runtimePermissionGrants = [];
|
|
2962
|
+
this.confirmedHostedSpaceIds.clear();
|
|
2480
2963
|
await this.tc.signIn(options);
|
|
2481
2964
|
this.syncResolvedHostFromAuth();
|
|
2482
2965
|
const oldGraph = this._serviceGraph;
|
|
@@ -2513,6 +2996,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2513
2996
|
this._bootstrapSkipped = false;
|
|
2514
2997
|
this._bootstrapStatus = { skipped: false };
|
|
2515
2998
|
if (this.config.autoBootstrapAccount === false) {
|
|
2999
|
+
this._bootstrapStatus = { skipped: true, reason: "auto-bootstrap-disabled" };
|
|
2516
3000
|
return false;
|
|
2517
3001
|
}
|
|
2518
3002
|
if (!this.auth || !this._address) {
|
|
@@ -2527,11 +3011,20 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2527
3011
|
return false;
|
|
2528
3012
|
}
|
|
2529
3013
|
const steps = bootstrapSteps(this._address, this._chainId);
|
|
2530
|
-
if (!await this.isFreshBootstrapAccount(steps)) {
|
|
2531
|
-
return false;
|
|
2532
|
-
}
|
|
2533
3014
|
try {
|
|
2534
|
-
await this.
|
|
3015
|
+
const decision = await this.resolveBootstrapDecision(steps);
|
|
3016
|
+
if (decision.action === "skip") {
|
|
3017
|
+
this._bootstrapStatus = { skipped: true, reason: "already-provisioned" };
|
|
3018
|
+
return false;
|
|
3019
|
+
}
|
|
3020
|
+
const warnings = await this.runAccountBootstrap(steps, { mode: decision.mode });
|
|
3021
|
+
await this.writeBootstrapCompletionMarker(steps);
|
|
3022
|
+
if (warnings.length > 0) {
|
|
3023
|
+
this._bootstrapStatus = { skipped: false, warnings };
|
|
3024
|
+
this.notificationHandler.warning(
|
|
3025
|
+
`Account bootstrap completed with warnings: ${warnings.map((w) => `${w.stepId} (${w.kind}: ${w.code})`).join("; ")}`
|
|
3026
|
+
);
|
|
3027
|
+
}
|
|
2535
3028
|
} catch (err) {
|
|
2536
3029
|
const reason = err instanceof Error ? err.message : String(err);
|
|
2537
3030
|
this._bootstrapSkipped = true;
|
|
@@ -2544,32 +3037,92 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2544
3037
|
}
|
|
2545
3038
|
return true;
|
|
2546
3039
|
}
|
|
2547
|
-
async
|
|
3040
|
+
async resolveBootstrapDecision(steps) {
|
|
2548
3041
|
const enshrinedSpaceIds = /* @__PURE__ */ new Set();
|
|
2549
3042
|
for (const step of steps) {
|
|
2550
3043
|
if (step.kind === "session") {
|
|
2551
3044
|
enshrinedSpaceIds.add(step.spaceId);
|
|
2552
3045
|
}
|
|
2553
3046
|
}
|
|
2554
|
-
const
|
|
3047
|
+
const auth = this.auth;
|
|
3048
|
+
if (!auth) {
|
|
3049
|
+
throw new Error("Account bootstrap requires an active wallet session");
|
|
3050
|
+
}
|
|
3051
|
+
const skipped = auth.lastActivationSkippedSpaceIds;
|
|
2555
3052
|
if (skipped.some((spaceId) => enshrinedSpaceIds.has(spaceId))) {
|
|
2556
|
-
return
|
|
3053
|
+
return { action: "run", mode: "fresh" };
|
|
2557
3054
|
}
|
|
2558
3055
|
try {
|
|
2559
|
-
const
|
|
2560
|
-
|
|
2561
|
-
|
|
3056
|
+
const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
|
|
3057
|
+
const markerPermission = {
|
|
3058
|
+
service: "tinycloud.kv",
|
|
3059
|
+
space: accountSpaceId,
|
|
3060
|
+
path: BOOTSTRAP_COMPLETION_MARKER_KEY,
|
|
3061
|
+
actions: ["get"]
|
|
3062
|
+
};
|
|
3063
|
+
if (!this.hasRuntimePermissions([markerPermission])) {
|
|
3064
|
+
return { action: "run", mode: "repair" };
|
|
2562
3065
|
}
|
|
2563
|
-
|
|
3066
|
+
const marker = await this.readBootstrapCompletionMarker();
|
|
3067
|
+
if (marker.ok) {
|
|
3068
|
+
if (this.isAcceptedBootstrapCompletionMarker(marker.data.data)) {
|
|
3069
|
+
return { action: "skip" };
|
|
3070
|
+
}
|
|
3071
|
+
return { action: "run", mode: "repair" };
|
|
3072
|
+
}
|
|
3073
|
+
if (marker.error.code === ErrorCodes2.KV_NOT_FOUND && this.markerReadIsUnhostedSpace(marker.error.meta)) {
|
|
3074
|
+
return { action: "run", mode: "fresh" };
|
|
3075
|
+
}
|
|
3076
|
+
} catch (err) {
|
|
3077
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3078
|
+
this.notificationHandler.warning(
|
|
3079
|
+
`Account bootstrap decision failed; attempting one bounded repair: ${reason}`
|
|
3080
|
+
);
|
|
3081
|
+
console.warn(`[TinyCloudNode] account bootstrap decision failed: ${reason}`);
|
|
3082
|
+
return { action: "run", mode: "repair" };
|
|
2564
3083
|
}
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
3084
|
+
return { action: "run", mode: "repair" };
|
|
3085
|
+
}
|
|
3086
|
+
async readBootstrapCompletionMarker() {
|
|
3087
|
+
return this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).get(
|
|
3088
|
+
BOOTSTRAP_COMPLETION_MARKER_KEY
|
|
3089
|
+
);
|
|
3090
|
+
}
|
|
3091
|
+
async writeBootstrapCompletionMarker(steps) {
|
|
3092
|
+
if (!this._address) {
|
|
3093
|
+
throw new Error("Bootstrap completion marker requires an active wallet session");
|
|
3094
|
+
}
|
|
3095
|
+
const canonicalStepIds = new Set(
|
|
3096
|
+
bootstrapSteps(this._address, this._chainId).map((step) => step.id)
|
|
3097
|
+
);
|
|
3098
|
+
const providedStepIds = new Set(steps.map((step) => step.id));
|
|
3099
|
+
if (canonicalStepIds.size !== providedStepIds.size || [...canonicalStepIds].some((stepId) => !providedStepIds.has(stepId))) {
|
|
3100
|
+
throw new Error("Bootstrap completion marker requires the canonical bootstrap step set");
|
|
3101
|
+
}
|
|
3102
|
+
const marker = {
|
|
3103
|
+
v: BOOTSTRAP_COMPLETION_MARKER_VERSION,
|
|
3104
|
+
stepIds: [...canonicalStepIds],
|
|
3105
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3106
|
+
};
|
|
3107
|
+
const written = await this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).put(
|
|
3108
|
+
BOOTSTRAP_COMPLETION_MARKER_KEY,
|
|
3109
|
+
marker
|
|
3110
|
+
);
|
|
3111
|
+
if (!written.ok) {
|
|
3112
|
+
throw new Error(`Failed to write bootstrap completion marker: ${written.error.message}`);
|
|
2570
3113
|
}
|
|
2571
3114
|
}
|
|
2572
|
-
|
|
3115
|
+
isAcceptedBootstrapCompletionMarker(value) {
|
|
3116
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3117
|
+
const entries = Object.entries(value);
|
|
3118
|
+
const markerValue = (name) => entries.find(([key]) => key === name)?.[1];
|
|
3119
|
+
const version = markerValue("v");
|
|
3120
|
+
return typeof version === "number" && Number.isInteger(version) && ACCEPTED_MARKER_VERSIONS.some((acceptedVersion) => acceptedVersion === version);
|
|
3121
|
+
}
|
|
3122
|
+
markerReadIsUnhostedSpace(meta) {
|
|
3123
|
+
return meta?.status === 404;
|
|
3124
|
+
}
|
|
3125
|
+
async runAccountBootstrap(steps, { mode } = { mode: "fresh" }) {
|
|
2573
3126
|
if (!this.auth || !this._address) {
|
|
2574
3127
|
throw new Error("Account bootstrap requires an active wallet session");
|
|
2575
3128
|
}
|
|
@@ -2580,6 +3133,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2580
3133
|
const auth = this.auth;
|
|
2581
3134
|
const sessions = /* @__PURE__ */ new Map();
|
|
2582
3135
|
const rawAbilitiesBySpace = /* @__PURE__ */ new Map();
|
|
3136
|
+
const warnings = [];
|
|
2583
3137
|
const primarySession = auth.tinyCloudSession;
|
|
2584
3138
|
const defaultSpaceId = this.ownedSpaceId("default");
|
|
2585
3139
|
const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === BOOTSTRAP_SESSION_REQUESTS.default;
|
|
@@ -2641,32 +3195,44 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2641
3195
|
}
|
|
2642
3196
|
}
|
|
2643
3197
|
if (step.kind === "seed-spaces") {
|
|
2644
|
-
|
|
2645
|
-
|
|
3198
|
+
const registered = await this.account.spaces.registerBatch(
|
|
3199
|
+
step.spaces.map((space) => ({
|
|
2646
3200
|
spaceId: space.spaceId,
|
|
2647
3201
|
name: space.name,
|
|
2648
3202
|
ownerDid: this.did,
|
|
2649
3203
|
type: "owned",
|
|
2650
3204
|
permissions: ["*"],
|
|
2651
3205
|
status: "active"
|
|
3206
|
+
}))
|
|
3207
|
+
);
|
|
3208
|
+
if (!registered.ok) {
|
|
3209
|
+
throw new Error(
|
|
3210
|
+
`Failed to seed account spaces: ${registered.error.message}`
|
|
3211
|
+
);
|
|
3212
|
+
}
|
|
3213
|
+
const batchError = registered.data.recoveredFromBatchError;
|
|
3214
|
+
if (batchError) {
|
|
3215
|
+
warnings.push({
|
|
3216
|
+
stepId: step.id,
|
|
3217
|
+
kind: "batch-write-reconciled",
|
|
3218
|
+
code: batchError.code,
|
|
3219
|
+
message: batchError.message
|
|
2652
3220
|
});
|
|
2653
|
-
if (!registered.ok) {
|
|
2654
|
-
throw new Error(
|
|
2655
|
-
`Failed to seed account space ${space.spaceId}: ${registered.error.message}`
|
|
2656
|
-
);
|
|
2657
|
-
}
|
|
2658
3221
|
}
|
|
2659
3222
|
}
|
|
2660
3223
|
if (step.kind === "seed-applications") {
|
|
2661
3224
|
const registered = await this.account.applications.register(
|
|
2662
|
-
step.manifests.length > 0 ? [...step.manifests] : TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST
|
|
3225
|
+
step.manifests.length > 0 ? [...step.manifests] : TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
|
|
3226
|
+
// The write is an INSERT OR REPLACE and is safe to repeat during a
|
|
3227
|
+
// repair, so skip the manifest-hash pre-read.
|
|
3228
|
+
{ assumeUnregistered: true }
|
|
2663
3229
|
);
|
|
2664
3230
|
if (!registered.ok) {
|
|
2665
3231
|
throw new Error(`Failed to seed bootstrap applications: ${registered.error.message}`);
|
|
2666
3232
|
}
|
|
2667
3233
|
}
|
|
2668
3234
|
if (step.kind === "encryption-network-create") {
|
|
2669
|
-
await this.ensureEncryptionNetwork(step.networkId);
|
|
3235
|
+
await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: mode === "fresh" });
|
|
2670
3236
|
}
|
|
2671
3237
|
if (step.kind === "secret-records-schema") {
|
|
2672
3238
|
const db = this.sqlForSpace(step.spaceId).db(step.database);
|
|
@@ -2686,6 +3252,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2686
3252
|
}
|
|
2687
3253
|
}
|
|
2688
3254
|
}
|
|
3255
|
+
return warnings;
|
|
2689
3256
|
}
|
|
2690
3257
|
registerBootstrapRuntimeGrant(session, request, rawAbilities) {
|
|
2691
3258
|
const operations = [];
|
|
@@ -2848,7 +3415,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2848
3415
|
if (!this.auth || !this.signer) {
|
|
2849
3416
|
throw new Error("Manifest registry write requires wallet mode");
|
|
2850
3417
|
}
|
|
2851
|
-
const accountSpaceId = this.ownedSpaceId(
|
|
3418
|
+
const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
|
|
2852
3419
|
await this.ensureOwnedSpaceHostedById(accountSpaceId);
|
|
2853
3420
|
const result = await this.account.applications.register(request.manifests);
|
|
2854
3421
|
if (!result.ok) {
|
|
@@ -2945,6 +3512,22 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2945
3512
|
await this.ensureEncryptionNetwork(networkId);
|
|
2946
3513
|
}
|
|
2947
3514
|
}
|
|
3515
|
+
/**
|
|
3516
|
+
* Ensure one of this user's owned spaces is hosted, at most once per space
|
|
3517
|
+
* per sign-in.
|
|
3518
|
+
*
|
|
3519
|
+
* Account bootstrap funnels six calls through here (via
|
|
3520
|
+
* `AccountService.ensureAccountSpaceHosted`), and each one re-submits
|
|
3521
|
+
* `activateSessionWithHost` with the *primary* session. That session's recap
|
|
3522
|
+
* covers only `default` and `secrets`, so the account space appears in
|
|
3523
|
+
* neither `activated` nor `skipped` and the guard below returns immediately
|
|
3524
|
+
* — the repeat calls cannot change the outcome, they only cost round trips.
|
|
3525
|
+
*
|
|
3526
|
+
* The memo is per sign-in ({@link confirmedHostedSpaceIds} is cleared in
|
|
3527
|
+
* `signIn`) and only records confirmed successes. A wrong skip is
|
|
3528
|
+
* self-revealing: the next write to the space fails immediately and loudly
|
|
3529
|
+
* with `404 Space not found`.
|
|
3530
|
+
*/
|
|
2948
3531
|
async ensureOwnedSpaceHostedById(spaceId) {
|
|
2949
3532
|
if (!this.auth) {
|
|
2950
3533
|
throw new Error("Owned space hosting requires wallet mode");
|
|
@@ -2953,12 +3536,16 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2953
3536
|
if (!session) {
|
|
2954
3537
|
throw new Error("Owned space hosting requires an active session");
|
|
2955
3538
|
}
|
|
3539
|
+
if (this.confirmedHostedSpaceIds.has(spaceId)) {
|
|
3540
|
+
return;
|
|
3541
|
+
}
|
|
2956
3542
|
const host = this.hosts[0] ?? this.config.host;
|
|
2957
3543
|
if (!host) {
|
|
2958
3544
|
throw new Error("Owned space hosting requires a TinyCloud host");
|
|
2959
3545
|
}
|
|
2960
3546
|
const activation = await activateSessionWithHost2(host, session.delegationHeader);
|
|
2961
3547
|
if (activation.success && !activation.skipped?.includes(spaceId)) {
|
|
3548
|
+
this.confirmedHostedSpaceIds.add(spaceId);
|
|
2962
3549
|
return;
|
|
2963
3550
|
}
|
|
2964
3551
|
if (!activation.success && activation.status !== 404) {
|
|
@@ -2977,6 +3564,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2977
3564
|
`Failed to activate session after creating owned space ${spaceId}: ${retry.error ?? "space was skipped"}`
|
|
2978
3565
|
);
|
|
2979
3566
|
}
|
|
3567
|
+
this.confirmedHostedSpaceIds.add(spaceId);
|
|
2980
3568
|
}
|
|
2981
3569
|
/**
|
|
2982
3570
|
* Host one of this user's owned spaces by name (e.g. `"applications"`).
|
|
@@ -3292,6 +3880,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3292
3880
|
this._delegationManager = stagedGraph.delegationManager;
|
|
3293
3881
|
this._spaceService = stagedGraph.spaceService;
|
|
3294
3882
|
this._serviceGraph = stagedGraph.graph;
|
|
3883
|
+
this._activeServiceSession = serviceSession;
|
|
3295
3884
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
3296
3885
|
this._secrets = /* @__PURE__ */ new Map();
|
|
3297
3886
|
this._publicKV = void 0;
|
|
@@ -3299,6 +3888,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3299
3888
|
this.tc = stagedGraph.core;
|
|
3300
3889
|
this._recapOperationsCache = stagedPrimary.cache;
|
|
3301
3890
|
this.runtimePermissionGrants = stagedPrimary.grants;
|
|
3891
|
+
this.confirmedHostedSpaceIds.clear();
|
|
3302
3892
|
if (this.auth) {
|
|
3303
3893
|
this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost]);
|
|
3304
3894
|
this._restoredTcSession = void 0;
|
|
@@ -3608,6 +4198,28 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3608
4198
|
currentTinyCloudSession() {
|
|
3609
4199
|
return this.auth?.tinyCloudSession ?? this._restoredTcSession;
|
|
3610
4200
|
}
|
|
4201
|
+
/**
|
|
4202
|
+
* Mint a multi-capability invocation through the established session key.
|
|
4203
|
+
* Callers receive only the signed headers; session key material remains
|
|
4204
|
+
* owned by the session manager and is never part of the caller contract.
|
|
4205
|
+
*/
|
|
4206
|
+
invokeAny(entries, facts) {
|
|
4207
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
4208
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
4209
|
+
return this._serviceGraph.invokeAny(session, entries, facts);
|
|
4210
|
+
}
|
|
4211
|
+
/** Sign protocol bytes with the established in-memory session key. */
|
|
4212
|
+
async signSessionBytes(bytes) {
|
|
4213
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
4214
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
4215
|
+
return signBytesWithJwk(bytes, session.jwk);
|
|
4216
|
+
}
|
|
4217
|
+
/** Bind a session-signed invocation to the canonical service audience. */
|
|
4218
|
+
async bindInvocationAudience(authorization, audience) {
|
|
4219
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
4220
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
4221
|
+
return rewriteInvocationAudience(authorization, audience, session.jwk);
|
|
4222
|
+
}
|
|
3611
4223
|
/**
|
|
3612
4224
|
* Connect a wallet to upgrade from session-only mode to wallet mode.
|
|
3613
4225
|
*
|
|
@@ -3669,7 +4281,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3669
4281
|
nonce: this.config.nonce,
|
|
3670
4282
|
siweConfig: this.config.siweConfig,
|
|
3671
4283
|
manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
|
|
3672
|
-
capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
4284
|
+
capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
3673
4285
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
|
|
3674
4286
|
});
|
|
3675
4287
|
this.tc = new TinyCloud(this.auth, {
|
|
@@ -3722,7 +4334,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3722
4334
|
nonce: this.config.nonce,
|
|
3723
4335
|
siweConfig: this.config.siweConfig,
|
|
3724
4336
|
manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
|
|
3725
|
-
capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
4337
|
+
capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
3726
4338
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
|
|
3727
4339
|
});
|
|
3728
4340
|
this.tc = new TinyCloud(this.auth, {
|
|
@@ -3789,6 +4401,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3789
4401
|
if (this._serviceContext) {
|
|
3790
4402
|
const spaceScopedContext = this._serviceGraph.track(new ServiceContext2({
|
|
3791
4403
|
invoke: this._serviceContext.invoke,
|
|
4404
|
+
invokeAny: this._serviceContext.invokeAny,
|
|
3792
4405
|
fetch: this._serviceContext.fetch,
|
|
3793
4406
|
hosts: this._serviceContext.hosts,
|
|
3794
4407
|
telemetry: this.config.telemetry
|
|
@@ -3881,7 +4494,17 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3881
4494
|
verifyNodeSignature: (nodeId, message, signature) => verifyDidKeyEd25519Signature(nodeId, message, signature)
|
|
3882
4495
|
};
|
|
3883
4496
|
}
|
|
4497
|
+
/**
|
|
4498
|
+
* The node DID for {@link config.host}.
|
|
4499
|
+
*
|
|
4500
|
+
* Sign-in already performs `GET /info` for the protocol-version check, and
|
|
4501
|
+
* that response carries `nodeId`, so reuse it rather than fetching `/info` a
|
|
4502
|
+
* second time. Only a value recorded for this exact host qualifies; older
|
|
4503
|
+
* nodes that omit `nodeId` fall through to the fetch.
|
|
4504
|
+
*/
|
|
3884
4505
|
async fetchNodeId() {
|
|
4506
|
+
const cached = this.auth?.nodeIdForHost(this.config.host);
|
|
4507
|
+
if (cached) return cached;
|
|
3885
4508
|
const response = await fetch(`${this.config.host}/info`);
|
|
3886
4509
|
if (!response.ok) {
|
|
3887
4510
|
throw new Error(`Failed to fetch node info: HTTP ${response.status}`);
|
|
@@ -4444,6 +5067,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
4444
5067
|
shareUrl: input.shareUrl,
|
|
4445
5068
|
documentName: input.documentName,
|
|
4446
5069
|
jti: base64UrlEncode(crypto.getRandomValues(new Uint8Array(16))),
|
|
5070
|
+
idempotencyKey: input.idempotencyKey,
|
|
4447
5071
|
expiresAt: input.expiresAt
|
|
4448
5072
|
};
|
|
4449
5073
|
const requestBodyDigest = base64UrlEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalizeEncryptionJson(body)))));
|
|
@@ -4653,6 +5277,13 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
4653
5277
|
body
|
|
4654
5278
|
)
|
|
4655
5279
|
});
|
|
5280
|
+
if (response.status === 409) {
|
|
5281
|
+
const existing = await this.getEncryptionNetwork(networkId);
|
|
5282
|
+
if (existing) return existing;
|
|
5283
|
+
throw new Error(
|
|
5284
|
+
`Encryption network ${networkId} already exists but could not be read back`
|
|
5285
|
+
);
|
|
5286
|
+
}
|
|
4656
5287
|
if (!response.ok) {
|
|
4657
5288
|
throw new Error(
|
|
4658
5289
|
`Failed to create encryption network ${networkId}: HTTP ${response.status} ${await response.text()}`
|
|
@@ -4661,11 +5292,19 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
4661
5292
|
const created = await response.json();
|
|
4662
5293
|
return created.descriptor;
|
|
4663
5294
|
}
|
|
4664
|
-
|
|
5295
|
+
/**
|
|
5296
|
+
* Ensure an encryption network exists, creating it if necessary.
|
|
5297
|
+
*
|
|
5298
|
+
* @param nameOrNetworkId - Network name or full `urn:tinycloud:encryption:` id.
|
|
5299
|
+
* @param options - See {@link EnsureEncryptionNetworkOptions}.
|
|
5300
|
+
*/
|
|
5301
|
+
async ensureEncryptionNetwork(nameOrNetworkId = DEFAULT_ENCRYPTION_NETWORK_NAME, options = {}) {
|
|
4665
5302
|
const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
5303
|
+
if (!options.assumeMissing) {
|
|
5304
|
+
const existing = await this.getEncryptionNetwork(networkId);
|
|
5305
|
+
if (existing) {
|
|
5306
|
+
return existing;
|
|
5307
|
+
}
|
|
4669
5308
|
}
|
|
4670
5309
|
const parsed = parseNetworkId2(networkId);
|
|
4671
5310
|
if (!didPrincipalMatches2(parsed.ownerDid, this.did)) {
|
|
@@ -6444,35 +7083,44 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
6444
7083
|
if (parentDelegation.disableSubDelegation) {
|
|
6445
7084
|
throw new Error("Parent delegation does not allow sub-delegation");
|
|
6446
7085
|
}
|
|
6447
|
-
|
|
7086
|
+
const parentResources = parentDelegation.resources ?? [{
|
|
7087
|
+
service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
|
|
7088
|
+
space: parentDelegation.spaceId,
|
|
7089
|
+
path: parentDelegation.path,
|
|
7090
|
+
actions: parentDelegation.actions
|
|
7091
|
+
}];
|
|
7092
|
+
const requestedResources = params.resources ?? [{
|
|
7093
|
+
service: params.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
|
|
7094
|
+
space: parentDelegation.spaceId,
|
|
7095
|
+
path: params.path,
|
|
7096
|
+
actions: params.actions
|
|
7097
|
+
}];
|
|
7098
|
+
const resourceIsContained = (parent, child) => {
|
|
7099
|
+
const parentEncryption = parent.service === "encryption" || parent.service === "tinycloud.encryption";
|
|
7100
|
+
const childEncryption = child.service === "encryption" || child.service === "tinycloud.encryption";
|
|
7101
|
+
if (parentEncryption || childEncryption) return parentEncryption && childEncryption && parent.path === child.path && child.actions.every((action) => parent.actions.includes(action));
|
|
7102
|
+
const pathContained = parent.path === child.path || parent.path.endsWith("/") && child.path.startsWith(parent.path) || child.path.startsWith(`${parent.path}/`);
|
|
7103
|
+
return pathContained && child.actions.every((action) => parent.actions.includes(action));
|
|
7104
|
+
};
|
|
7105
|
+
if (requestedResources.length === 0 || requestedResources.some((child) => !parentResources.some((parent) => resourceIsContained(parent, child)))) {
|
|
6448
7106
|
throw new Error(
|
|
6449
|
-
|
|
7107
|
+
"Sub-delegation resources exceed the received delegation"
|
|
6450
7108
|
);
|
|
6451
7109
|
}
|
|
6452
|
-
const parentActions = new Set(parentDelegation.actions);
|
|
6453
|
-
for (const action of params.actions) {
|
|
6454
|
-
if (!parentActions.has(action)) {
|
|
6455
|
-
throw new Error(
|
|
6456
|
-
`Sub-delegation action "${action}" is not in parent's actions: ${parentDelegation.actions.join(", ")}`
|
|
6457
|
-
);
|
|
6458
|
-
}
|
|
6459
|
-
}
|
|
6460
7110
|
const now = /* @__PURE__ */ new Date();
|
|
6461
7111
|
const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
|
|
6462
7112
|
const requestedExpiry = new Date(now.getTime() + expiryMs);
|
|
6463
7113
|
const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
|
|
6464
7114
|
const abilities = {};
|
|
6465
|
-
const
|
|
6466
|
-
const
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
abilities
|
|
6473
|
-
|
|
6474
|
-
if (duckdbActions.length > 0) {
|
|
6475
|
-
abilities.duckdb = { [params.path]: duckdbActions };
|
|
7115
|
+
const rawAbilities = {};
|
|
7116
|
+
for (const resource of requestedResources) {
|
|
7117
|
+
const service = resource.service.replace(/^tinycloud\./, "");
|
|
7118
|
+
if (service === "encryption") {
|
|
7119
|
+
rawAbilities[resource.path] = [...resource.actions];
|
|
7120
|
+
continue;
|
|
7121
|
+
}
|
|
7122
|
+
abilities[service] ?? (abilities[service] = {});
|
|
7123
|
+
abilities[service][resource.path] = [...resource.actions];
|
|
6476
7124
|
}
|
|
6477
7125
|
const targetHost = parentDelegation.host ?? this.config.host;
|
|
6478
7126
|
const prepared = this.wasmBindings.prepareSession({
|
|
@@ -6484,7 +7132,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
6484
7132
|
expirationTime: actualExpiry.toISOString(),
|
|
6485
7133
|
spaceId: parentDelegation.spaceId,
|
|
6486
7134
|
delegateUri: params.delegateDID,
|
|
6487
|
-
parents: [parentDelegation.cid]
|
|
7135
|
+
parents: [parentDelegation.cid],
|
|
7136
|
+
...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
|
|
6488
7137
|
});
|
|
6489
7138
|
const signature = await this.signer.signMessage(prepared.siwe);
|
|
6490
7139
|
const subDelegationSession = this.wasmBindings.completeSessionSetup({
|
|
@@ -6502,14 +7151,20 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
6502
7151
|
cid: subDelegationSession.delegationCid,
|
|
6503
7152
|
delegationHeader: subDelegationSession.delegationHeader,
|
|
6504
7153
|
spaceId: parentDelegation.spaceId,
|
|
6505
|
-
path:
|
|
6506
|
-
actions:
|
|
7154
|
+
path: requestedResources[0].path,
|
|
7155
|
+
actions: requestedResources[0].actions,
|
|
6507
7156
|
disableSubDelegation: params.disableSubDelegation ?? false,
|
|
6508
7157
|
expiry: actualExpiry,
|
|
6509
7158
|
delegateDID: params.delegateDID,
|
|
6510
7159
|
ownerAddress: parentDelegation.ownerAddress,
|
|
6511
7160
|
chainId: parentDelegation.chainId,
|
|
6512
|
-
host: targetHost
|
|
7161
|
+
host: targetHost,
|
|
7162
|
+
resources: requestedResources.map((resource) => ({
|
|
7163
|
+
service: resource.service.replace(/^tinycloud\./, ""),
|
|
7164
|
+
space: resource.service.replace(/^tinycloud\./, "") === "encryption" ? "encryption" : resource.space ?? parentDelegation.spaceId,
|
|
7165
|
+
path: resource.path,
|
|
7166
|
+
actions: [...resource.actions]
|
|
7167
|
+
}))
|
|
6513
7168
|
};
|
|
6514
7169
|
}
|
|
6515
7170
|
};
|
|
@@ -6529,7 +7184,7 @@ var TinyCloudNode = _TinyCloudNode;
|
|
|
6529
7184
|
// src/core.ts
|
|
6530
7185
|
import {
|
|
6531
7186
|
ACCOUNT_REGISTRY_PATH,
|
|
6532
|
-
ACCOUNT_REGISTRY_SPACE as
|
|
7187
|
+
ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE3,
|
|
6533
7188
|
DEFAULT_MANIFEST_SPACE as DEFAULT_MANIFEST_SPACE2,
|
|
6534
7189
|
DEFAULT_MANIFEST_VERSION,
|
|
6535
7190
|
VAULT_PERMISSION_SERVICE,
|
|
@@ -6621,7 +7276,7 @@ import {
|
|
|
6621
7276
|
import { ServiceContext as ServiceContext3 } from "@tinycloud/sdk-core";
|
|
6622
7277
|
export {
|
|
6623
7278
|
ACCOUNT_REGISTRY_PATH,
|
|
6624
|
-
|
|
7279
|
+
ACCOUNT_REGISTRY_SPACE3 as ACCOUNT_REGISTRY_SPACE,
|
|
6625
7280
|
AccountService,
|
|
6626
7281
|
AutoApproveSpaceCreationHandler2 as AutoApproveSpaceCreationHandler,
|
|
6627
7282
|
CapabilityKeyRegistry2 as CapabilityKeyRegistry,
|