@tinycloud/node-sdk 2.11.0-beta.0 → 2.11.0-beta.10
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-B_i2KfID.d.cts → core-BypriIWK.d.cts} +254 -20
- package/dist/{core-B_i2KfID.d.ts → core-BypriIWK.d.ts} +254 -20
- package/dist/core.cjs +645 -35
- 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 +652 -37
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +693 -37
- 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 +698 -38
- 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);
|
|
@@ -551,6 +574,15 @@ var NodeUserAuthorization = class {
|
|
|
551
574
|
get nodeFeatures() {
|
|
552
575
|
return this._nodeFeatures;
|
|
553
576
|
}
|
|
577
|
+
/**
|
|
578
|
+
* The node DID reported by `GET /info` for `host`, if the sign-in (or
|
|
579
|
+
* restore) that populated it targeted that same host. Returns `undefined`
|
|
580
|
+
* for any other host, and for nodes whose `/info` omits `nodeId`, so callers
|
|
581
|
+
* fall back to fetching it themselves rather than using a foreign node's DID.
|
|
582
|
+
*/
|
|
583
|
+
nodeIdForHost(host) {
|
|
584
|
+
return this._nodeInfoIdentity?.host === host ? this._nodeInfoIdentity.nodeId : void 0;
|
|
585
|
+
}
|
|
554
586
|
get lastActivationSkippedSpaceIds() {
|
|
555
587
|
return [...this._lastActivationSkippedSpaceIds];
|
|
556
588
|
}
|
|
@@ -1020,11 +1052,13 @@ var NodeUserAuthorization = class {
|
|
|
1020
1052
|
this._tinyCloudSession = tinyCloudSession;
|
|
1021
1053
|
this._address = address;
|
|
1022
1054
|
this._chainId = chainId;
|
|
1055
|
+
const infoHost = this.primaryTinyCloudHost;
|
|
1023
1056
|
const nodeInfo = await (0, import_sdk_core3.checkNodeInfo)(
|
|
1024
|
-
|
|
1057
|
+
infoHost,
|
|
1025
1058
|
this.wasm.protocolVersion()
|
|
1026
1059
|
);
|
|
1027
1060
|
this._nodeFeatures = nodeInfo.features;
|
|
1061
|
+
this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
|
|
1028
1062
|
for (const ext of this.extensions) {
|
|
1029
1063
|
if (ext.afterSignIn) {
|
|
1030
1064
|
await ext.afterSignIn(clientSession);
|
|
@@ -1190,11 +1224,13 @@ var NodeUserAuthorization = class {
|
|
|
1190
1224
|
this._tinyCloudSession = tinyCloudSession;
|
|
1191
1225
|
this._address = address;
|
|
1192
1226
|
this._chainId = chainId;
|
|
1227
|
+
const infoHost = this.primaryTinyCloudHost;
|
|
1193
1228
|
const nodeInfo = await (0, import_sdk_core3.checkNodeInfo)(
|
|
1194
|
-
|
|
1229
|
+
infoHost,
|
|
1195
1230
|
this.wasm.protocolVersion()
|
|
1196
1231
|
);
|
|
1197
1232
|
this._nodeFeatures = nodeInfo.features;
|
|
1233
|
+
this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
|
|
1198
1234
|
for (const ext of this.extensions) {
|
|
1199
1235
|
if (ext.afterSignIn) {
|
|
1200
1236
|
await ext.afterSignIn(clientSession);
|
|
@@ -1203,6 +1239,398 @@ var NodeUserAuthorization = class {
|
|
|
1203
1239
|
await this.ensureSpaceExists();
|
|
1204
1240
|
return clientSession;
|
|
1205
1241
|
}
|
|
1242
|
+
/**
|
|
1243
|
+
* End-to-end sign-in via OpenKey `authorizeTinyCloud()` (Sol CRITICAL-4).
|
|
1244
|
+
*
|
|
1245
|
+
* This is the production entry point that wires
|
|
1246
|
+
* {@link prepareSessionForSigning} → OpenKey `authorizeTinyCloud()` →
|
|
1247
|
+
* {@link signInWithOpenKeyResult} into one call. Consumers pass in a
|
|
1248
|
+
* caller-provided `authorizeFn` (typically a thin wrapper around
|
|
1249
|
+
* `openkey.authorizeTinyCloud()`) so this method has no direct
|
|
1250
|
+
* dependency on the OpenKey SDK — it works with any transport that
|
|
1251
|
+
* conforms to the versioned protocol.
|
|
1252
|
+
*
|
|
1253
|
+
* The `authorizeFn` MUST:
|
|
1254
|
+
* 1. Send the prepared SIWE bytes to OpenKey.
|
|
1255
|
+
* 2. Return the exact `TinyCloudAuthorizationResultV1` shape (with
|
|
1256
|
+
* `signedMessage` — the bytes the signature actually verifies
|
|
1257
|
+
* against — plus `selectedActionKeys` and `permissions`).
|
|
1258
|
+
*
|
|
1259
|
+
* NodeUserAuthorization then completes the session with `signedMessage`
|
|
1260
|
+
* (never the prepared bytes), verifying the signature, the immutable
|
|
1261
|
+
* SIWE fields, and every subset invariant enforced by
|
|
1262
|
+
* {@link signInWithOpenKeyResult}. This preserves the exact-byte
|
|
1263
|
+
* guarantee: if the user did NOT narrow, the returned `signedMessage`
|
|
1264
|
+
* equals the prepared SIWE byte-for-byte; if the user narrowed, only
|
|
1265
|
+
* the ReCap block differs and every immutable header field survives.
|
|
1266
|
+
*
|
|
1267
|
+
* Legacy exact-byte {@link signIn} callers are unaffected — this method
|
|
1268
|
+
* is opt-in.
|
|
1269
|
+
*
|
|
1270
|
+
* @param authorizeFn Callback that invokes the versioned OpenKey
|
|
1271
|
+
* `authorizeTinyCloud()` protocol and returns the result. Receives
|
|
1272
|
+
* the prepared SIWE bytes plus a `host` hint the widget should bind
|
|
1273
|
+
* into its /authorize-sign-prepare context.
|
|
1274
|
+
* @param options Optional overrides for the OpenKey request.
|
|
1275
|
+
*/
|
|
1276
|
+
async signInWithOpenKey(authorizeFn, options) {
|
|
1277
|
+
if (options?.host !== void 0) {
|
|
1278
|
+
const overrideHost = options.host.trim();
|
|
1279
|
+
if (!overrideHost) {
|
|
1280
|
+
throw new Error("OpenKey authorization host must not be empty");
|
|
1281
|
+
}
|
|
1282
|
+
this.tinycloudHosts = [overrideHost];
|
|
1283
|
+
}
|
|
1284
|
+
const signingAddress = (0, import_sdk_core3.canonicalizeAddress)(await this.signer.getAddress());
|
|
1285
|
+
const signingChainId = await this.signer.getChainId();
|
|
1286
|
+
await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
|
|
1287
|
+
const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
|
|
1288
|
+
const presentation = this.buildPresentationEnvelope(options?.reason);
|
|
1289
|
+
const hostHint = this.primaryTinyCloudHost;
|
|
1290
|
+
const result = await authorizeFn({
|
|
1291
|
+
protocolVersion: 1,
|
|
1292
|
+
siwe: prepared.siwe,
|
|
1293
|
+
jwk,
|
|
1294
|
+
host: hostHint,
|
|
1295
|
+
keyId: options?.openkeyKeyId,
|
|
1296
|
+
presentation
|
|
1297
|
+
});
|
|
1298
|
+
return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
|
|
1299
|
+
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
|
|
1302
|
+
* envelope is display-only — the OpenKey widget renders it under an
|
|
1303
|
+
* honest trust label, and the server /authorize-sign-prepare route
|
|
1304
|
+
* decides whether to upgrade the label to `origin-bound` by fetching
|
|
1305
|
+
* the well-known manifest and comparing its digest to
|
|
1306
|
+
* `manifestDigest`.
|
|
1307
|
+
*
|
|
1308
|
+
* Returns `undefined` when no manifest is set — the widget renders as
|
|
1309
|
+
* "no manifest supplied" (unsigned).
|
|
1310
|
+
*
|
|
1311
|
+
* The digest is a deterministic SHA-256 over canonical JSON of the
|
|
1312
|
+
* first manifest payload; OpenKey parses the fetched well-known manifest
|
|
1313
|
+
* and applies the same canonicalization before comparing digests.
|
|
1314
|
+
*/
|
|
1315
|
+
buildPresentationEnvelope(reason) {
|
|
1316
|
+
const manifests = Array.isArray(this._manifest) ? this._manifest : this._manifest ? [this._manifest] : [];
|
|
1317
|
+
if (manifests.length === 0 && !reason) {
|
|
1318
|
+
return void 0;
|
|
1319
|
+
}
|
|
1320
|
+
const primary = manifests[0];
|
|
1321
|
+
let displayName;
|
|
1322
|
+
let manifestId;
|
|
1323
|
+
let manifestDigest;
|
|
1324
|
+
const envelopeManifests = [];
|
|
1325
|
+
for (const m of manifests) {
|
|
1326
|
+
envelopeManifests.push({
|
|
1327
|
+
name: m.name,
|
|
1328
|
+
appId: m.app_id,
|
|
1329
|
+
// Deep-copy to prevent later mutations from affecting the digest.
|
|
1330
|
+
payload: JSON.parse(JSON.stringify(m))
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
if (primary) {
|
|
1334
|
+
displayName = primary.name;
|
|
1335
|
+
manifestId = primary.app_id;
|
|
1336
|
+
manifestDigest = canonicalOpenKeyManifestSha256Hex(primary);
|
|
1337
|
+
}
|
|
1338
|
+
return {
|
|
1339
|
+
protocolVersion: 1,
|
|
1340
|
+
displayName,
|
|
1341
|
+
reason: reason && reason.length > 0 ? reason : void 0,
|
|
1342
|
+
manifestId,
|
|
1343
|
+
manifestDigest,
|
|
1344
|
+
manifests: envelopeManifests.length > 0 ? envelopeManifests : void 0
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Complete sign-in with a versioned OpenKey authorization result.
|
|
1349
|
+
*
|
|
1350
|
+
* Where `signInWithPreparedSession` completes with a signature over the
|
|
1351
|
+
* ORIGINAL prepared SIWE, this method completes with the SIWE bytes the
|
|
1352
|
+
* OpenKey widget actually presented and signed (`signedMessage`). It
|
|
1353
|
+
* exists because the versioned authorizeTinyCloud protocol lets the user
|
|
1354
|
+
* narrow capabilities; the returned bytes may differ from the original
|
|
1355
|
+
* request and are the only ones the signature actually verifies against.
|
|
1356
|
+
*
|
|
1357
|
+
* The `result` parameter is validated at the trust boundary via
|
|
1358
|
+
* `validateAuthorizationResultV1` (in sdk-core). Callers should validate
|
|
1359
|
+
* BEFORE calling this method so unknown/missing fields are rejected
|
|
1360
|
+
* before any session state is created.
|
|
1361
|
+
*
|
|
1362
|
+
* Validation performed here (defence-in-depth: even a "valid" v1 payload
|
|
1363
|
+
* must clear these checks before any session state is created):
|
|
1364
|
+
* 1. `result.address` matches the local signer's address.
|
|
1365
|
+
* 2. `result.signature` recovers to `result.address` when verified against
|
|
1366
|
+
* `result.signedMessage`.
|
|
1367
|
+
* 3. Every immutable SIWE header field (domain, address, URI, version,
|
|
1368
|
+
* chainId, nonce, issuedAt) in the original prepared SIWE is preserved
|
|
1369
|
+
* byte-for-byte in `result.signedMessage`. `statement` is NOT in this
|
|
1370
|
+
* list because the WASM renders it from the ReCap contents; it MUST
|
|
1371
|
+
* change when the ReCap is narrowed.
|
|
1372
|
+
* 4. Every capability in `result.signedMessage`'s ReCap block was present
|
|
1373
|
+
* in the original prepared SIWE — narrowing OK, broadening rejected.
|
|
1374
|
+
* 5. Every `result.selectedActionKeys` entry corresponds to a
|
|
1375
|
+
* (resource, action) pair in the returned SIWE.
|
|
1376
|
+
*/
|
|
1377
|
+
async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
|
|
1378
|
+
if (result.protocolVersion !== 1) {
|
|
1379
|
+
throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
|
|
1380
|
+
}
|
|
1381
|
+
if (!result.signedMessage) {
|
|
1382
|
+
throw new Error("OpenKey result must include signedMessage");
|
|
1383
|
+
}
|
|
1384
|
+
if (!prepared.siwe) {
|
|
1385
|
+
throw new Error(
|
|
1386
|
+
"signInWithOpenKeyResult requires prepared.siwe (the SDK-generated SIWE the sign-in was proposed with)"
|
|
1387
|
+
);
|
|
1388
|
+
}
|
|
1389
|
+
const expectedAddress = (0, import_sdk_core3.canonicalizeAddress)(await this.signer.getAddress());
|
|
1390
|
+
if ((0, import_sdk_core3.canonicalizeAddress)(result.address) !== expectedAddress) {
|
|
1391
|
+
throw new Error(
|
|
1392
|
+
`OpenKey returned signature from ${result.address} but expected ${expectedAddress}`
|
|
1393
|
+
);
|
|
1394
|
+
}
|
|
1395
|
+
let parsedSignedSiwe;
|
|
1396
|
+
try {
|
|
1397
|
+
parsedSignedSiwe = new import_siwe.SiweMessage(result.signedMessage);
|
|
1398
|
+
} catch (err) {
|
|
1399
|
+
throw new Error(
|
|
1400
|
+
`OpenKey signedMessage is not a parseable SIWE: ${err.message}`
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
const verifyResult = await parsedSignedSiwe.verify(
|
|
1404
|
+
{ signature: result.signature },
|
|
1405
|
+
{ suppressExceptions: true }
|
|
1406
|
+
);
|
|
1407
|
+
if (!verifyResult.success) {
|
|
1408
|
+
const errData = verifyResult.error;
|
|
1409
|
+
const type = errData && typeof errData === "object" && "type" in errData ? errData.type : void 0;
|
|
1410
|
+
const details = errData && typeof errData === "object" && "expected" in errData ? ` (expected=${errData.expected}, received=${errData.received})` : "";
|
|
1411
|
+
throw new Error(
|
|
1412
|
+
`OpenKey signature verification failed${type ? `: ${type}` : ""}${details}`
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
if ((0, import_sdk_core3.canonicalizeAddress)(parsedSignedSiwe.address) !== expectedAddress) {
|
|
1416
|
+
throw new Error(
|
|
1417
|
+
`OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
|
|
1421
|
+
const originalFields = (0, import_sdk_core3.extractImmutableSiweFields)(prepared.siwe);
|
|
1422
|
+
const signedFields = (0, import_sdk_core3.extractImmutableSiweFields)(result.signedMessage);
|
|
1423
|
+
const changedFields = (0, import_sdk_core3.diffImmutableSiweFields)(originalFields, signedFields, {
|
|
1424
|
+
originalHasRecap
|
|
1425
|
+
});
|
|
1426
|
+
if (changedFields.length > 0) {
|
|
1427
|
+
throw new Error(
|
|
1428
|
+
`OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
let originalCaps;
|
|
1432
|
+
let signedCaps;
|
|
1433
|
+
try {
|
|
1434
|
+
originalCaps = (0, import_sdk_core3.extractRecapAttenuations)(prepared.siwe);
|
|
1435
|
+
} catch (err) {
|
|
1436
|
+
throw new Error(
|
|
1437
|
+
`Failed to decode prepared SIWE recap: ${err.message}`
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1440
|
+
try {
|
|
1441
|
+
signedCaps = (0, import_sdk_core3.extractRecapAttenuations)(result.signedMessage);
|
|
1442
|
+
} catch (err) {
|
|
1443
|
+
throw new Error(
|
|
1444
|
+
`Failed to decode OpenKey signedMessage recap: ${err.message}`
|
|
1445
|
+
);
|
|
1446
|
+
}
|
|
1447
|
+
const unauthorized = (0, import_sdk_core3.unauthorizedRecapCapabilities)(signedCaps, originalCaps);
|
|
1448
|
+
if (unauthorized.length > 0) {
|
|
1449
|
+
const previewLimit = 5;
|
|
1450
|
+
const preview = unauthorized.slice(0, previewLimit).map((u) => `${u.resource}::${u.action}`).join(", ");
|
|
1451
|
+
const overflow = unauthorized.length > previewLimit ? ` (and ${unauthorized.length - previewLimit} more)` : "";
|
|
1452
|
+
throw new Error(
|
|
1453
|
+
`OpenKey signedMessage broadened authorization beyond the original request: ${preview}${overflow}`
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
if (!result.signedMessage.includes(prepared.spaceId)) {
|
|
1457
|
+
throw new Error(
|
|
1458
|
+
`OpenKey signedMessage does not reference the expected spaceId ${prepared.spaceId}`
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
const grantedPairs = /* @__PURE__ */ new Set();
|
|
1462
|
+
const grantedFourPartIndex = /* @__PURE__ */ new Map();
|
|
1463
|
+
for (const [resource, actions] of Object.entries(signedCaps)) {
|
|
1464
|
+
const { space, path } = (0, import_sdk_core3.parseCanonicalRecapResource)(resource);
|
|
1465
|
+
for (const action of Object.keys(actions)) {
|
|
1466
|
+
grantedPairs.add(`${resource}\0${action}`);
|
|
1467
|
+
const slashIdx = action.indexOf("/");
|
|
1468
|
+
const service = slashIdx > 0 ? action.slice(0, slashIdx) : "";
|
|
1469
|
+
if (service) {
|
|
1470
|
+
grantedFourPartIndex.set(
|
|
1471
|
+
`${service}\0${space}\0${path}\0${action}`,
|
|
1472
|
+
`${resource}\0${action}`
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
const isStructurallyRequired = (action) => action === "tinycloud.capabilities/read" || action === "capabilities/read";
|
|
1478
|
+
const nonRequiredGrantedPairs = /* @__PURE__ */ new Set();
|
|
1479
|
+
for (const pair of grantedPairs) {
|
|
1480
|
+
const idx = pair.indexOf("\0");
|
|
1481
|
+
const action = idx >= 0 ? pair.slice(idx + 1) : pair;
|
|
1482
|
+
if (!isStructurallyRequired(action)) {
|
|
1483
|
+
nonRequiredGrantedPairs.add(pair);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
const allSignedGrantedPairs = new Set(grantedPairs);
|
|
1487
|
+
const seenRawKeys = /* @__PURE__ */ new Set();
|
|
1488
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
1489
|
+
if (seenRawKeys.has(rawKey)) {
|
|
1490
|
+
throw new Error(
|
|
1491
|
+
`OpenKey selectedActionKeys contains duplicate entry ${rawKey}`
|
|
1492
|
+
);
|
|
1493
|
+
}
|
|
1494
|
+
seenRawKeys.add(rawKey);
|
|
1495
|
+
}
|
|
1496
|
+
const selectedResourceActionPairs = /* @__PURE__ */ new Set();
|
|
1497
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
1498
|
+
const parts = rawKey.split("\0");
|
|
1499
|
+
if (parts.length !== 4) {
|
|
1500
|
+
throw new Error(
|
|
1501
|
+
`OpenKey selectedActionKeys entry is malformed (expected canonical 4-part service\\0space\\0path\\0ability): ${rawKey}`
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
const canonical = grantedFourPartIndex.get(rawKey);
|
|
1505
|
+
if (!canonical) {
|
|
1506
|
+
throw new Error(
|
|
1507
|
+
`OpenKey selectedActionKeys entry ${rawKey} is not covered by any granted capability`
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
selectedResourceActionPairs.add(canonical);
|
|
1511
|
+
}
|
|
1512
|
+
if (nonRequiredGrantedPairs.size > 0) {
|
|
1513
|
+
const missingPairs = [];
|
|
1514
|
+
for (const pair of nonRequiredGrantedPairs) {
|
|
1515
|
+
if (!selectedResourceActionPairs.has(pair)) {
|
|
1516
|
+
missingPairs.push(pair);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (missingPairs.length > 0) {
|
|
1520
|
+
const previewLimit = 5;
|
|
1521
|
+
const preview = missingPairs.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
1522
|
+
const overflow = missingPairs.length > previewLimit ? ` (and ${missingPairs.length - previewLimit} more)` : "";
|
|
1523
|
+
throw new Error(
|
|
1524
|
+
`OpenKey selectedActionKeys is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
if (allSignedGrantedPairs.size > 0 && result.permissions.length === 0) {
|
|
1529
|
+
throw new Error(
|
|
1530
|
+
"OpenKey permissions is empty but signedMessage carries capabilities \u2014 refusing to accept a session without an authoritative grant list"
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
const permissionCanonicalKeys = /* @__PURE__ */ new Set();
|
|
1534
|
+
const permissionPairsCovered = /* @__PURE__ */ new Set();
|
|
1535
|
+
for (const perm of result.permissions) {
|
|
1536
|
+
const { service, space, path, actions } = perm;
|
|
1537
|
+
if (typeof service !== "string" || typeof space !== "string" || typeof path !== "string") {
|
|
1538
|
+
throw new Error(
|
|
1539
|
+
`OpenKey permissions entry has non-string service/space/path (service=${service}, space=${space}, path=${path})`
|
|
1540
|
+
);
|
|
1541
|
+
}
|
|
1542
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
1543
|
+
throw new Error(
|
|
1544
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has empty actions \u2014 a capability-bearing entry must list at least one action`
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
const canonicalKey = `${service}\0${space}\0${path}`;
|
|
1548
|
+
if (permissionCanonicalKeys.has(canonicalKey)) {
|
|
1549
|
+
throw new Error(
|
|
1550
|
+
`OpenKey permissions has duplicate entry for (${service} ${space} ${path})`
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
permissionCanonicalKeys.add(canonicalKey);
|
|
1554
|
+
let expectedResource;
|
|
1555
|
+
if (space.startsWith("tinycloud:")) {
|
|
1556
|
+
const shortService = service.startsWith("tinycloud.") ? service.slice("tinycloud.".length) : service;
|
|
1557
|
+
expectedResource = path ? `${space}/${shortService}/${path}` : `${space}/${shortService}`;
|
|
1558
|
+
} else {
|
|
1559
|
+
if (path) {
|
|
1560
|
+
throw new Error(
|
|
1561
|
+
`OpenKey permissions entry (${service} ${space} ${path}) carries a path on a non-tinycloud: resource URI, which is not a recognized encoding`
|
|
1562
|
+
);
|
|
1563
|
+
}
|
|
1564
|
+
expectedResource = space;
|
|
1565
|
+
}
|
|
1566
|
+
let matchedResource;
|
|
1567
|
+
if (signedCaps[expectedResource] !== void 0) {
|
|
1568
|
+
matchedResource = expectedResource;
|
|
1569
|
+
} else {
|
|
1570
|
+
throw new Error(
|
|
1571
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims grants not confirmed in signedMessage capabilities`
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
const grantedActionsForResource = new Set(
|
|
1575
|
+
Object.keys(signedCaps[matchedResource])
|
|
1576
|
+
);
|
|
1577
|
+
for (const action of actions) {
|
|
1578
|
+
if (typeof action !== "string" || !action) {
|
|
1579
|
+
throw new Error(
|
|
1580
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has non-string action ${String(action)}`
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
if (!grantedActionsForResource.has(action)) {
|
|
1584
|
+
throw new Error(
|
|
1585
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims action ${action} not present in signedMessage capabilities`
|
|
1586
|
+
);
|
|
1587
|
+
}
|
|
1588
|
+
permissionPairsCovered.add(`${matchedResource}\0${action}`);
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
if (allSignedGrantedPairs.size > 0) {
|
|
1592
|
+
const missingFromPerms = [];
|
|
1593
|
+
for (const pair of allSignedGrantedPairs) {
|
|
1594
|
+
if (!permissionPairsCovered.has(pair)) {
|
|
1595
|
+
missingFromPerms.push(pair);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
if (missingFromPerms.length > 0) {
|
|
1599
|
+
const previewLimit = 5;
|
|
1600
|
+
const preview = missingFromPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
1601
|
+
const overflow = missingFromPerms.length > previewLimit ? ` (and ${missingFromPerms.length - previewLimit} more)` : "";
|
|
1602
|
+
throw new Error(
|
|
1603
|
+
`OpenKey permissions is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
1604
|
+
);
|
|
1605
|
+
}
|
|
1606
|
+
const extrasInPerms = [];
|
|
1607
|
+
for (const pair of permissionPairsCovered) {
|
|
1608
|
+
if (!allSignedGrantedPairs.has(pair)) {
|
|
1609
|
+
extrasInPerms.push(pair);
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
if (extrasInPerms.length > 0) {
|
|
1613
|
+
const previewLimit = 5;
|
|
1614
|
+
const preview = extrasInPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
1615
|
+
const overflow = extrasInPerms.length > previewLimit ? ` (and ${extrasInPerms.length - previewLimit} more)` : "";
|
|
1616
|
+
throw new Error(
|
|
1617
|
+
`OpenKey permissions contains entries not present in the signed authority: ${preview}${overflow}`
|
|
1618
|
+
);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
void nonRequiredGrantedPairs;
|
|
1622
|
+
return this.signInWithPreparedSession(
|
|
1623
|
+
{
|
|
1624
|
+
siwe: result.signedMessage,
|
|
1625
|
+
jwk: prepared.jwk,
|
|
1626
|
+
spaceId: prepared.spaceId,
|
|
1627
|
+
verificationMethod: prepared.verificationMethod
|
|
1628
|
+
},
|
|
1629
|
+
result.signature,
|
|
1630
|
+
keyId,
|
|
1631
|
+
jwk
|
|
1632
|
+
);
|
|
1633
|
+
}
|
|
1206
1634
|
/**
|
|
1207
1635
|
* Clear persisted session data.
|
|
1208
1636
|
*/
|
|
@@ -1890,19 +2318,87 @@ function persistedExpiry(value) {
|
|
|
1890
2318
|
function sameInstant(left, right) {
|
|
1891
2319
|
return left.getTime() === right.getTime();
|
|
1892
2320
|
}
|
|
1893
|
-
function
|
|
1894
|
-
|
|
2321
|
+
function ownerDelegationPermissions(params) {
|
|
2322
|
+
const hasPermissions = Object.prototype.hasOwnProperty.call(params, "permissions");
|
|
2323
|
+
const hasLegacy = Object.prototype.hasOwnProperty.call(params, "path") || Object.prototype.hasOwnProperty.call(params, "actions");
|
|
2324
|
+
if (hasPermissions === hasLegacy) {
|
|
2325
|
+
throw new Error("Owner delegation requires exactly one authority shape: path/actions or permissions");
|
|
2326
|
+
}
|
|
2327
|
+
const permissions = [];
|
|
2328
|
+
if (hasPermissions) {
|
|
2329
|
+
if (!Array.isArray(params.permissions)) {
|
|
2330
|
+
throw new Error("Owner delegation permissions are invalid");
|
|
2331
|
+
}
|
|
2332
|
+
permissions.push(...params.permissions.map((permission) => ({
|
|
2333
|
+
service: permission.service,
|
|
2334
|
+
path: permission.path,
|
|
2335
|
+
actions: [...permission.actions]
|
|
2336
|
+
})));
|
|
2337
|
+
} else {
|
|
2338
|
+
if (typeof params.path !== "string" || !Array.isArray(params.actions)) {
|
|
2339
|
+
throw new Error("Owner delegation requires bounded capabilities");
|
|
2340
|
+
}
|
|
2341
|
+
const byService = /* @__PURE__ */ new Map();
|
|
2342
|
+
for (const action of params.actions) {
|
|
2343
|
+
if (typeof action !== "string") {
|
|
2344
|
+
throw new Error("Owner delegation capabilities are unsupported");
|
|
2345
|
+
}
|
|
2346
|
+
const slash = action.indexOf("/");
|
|
2347
|
+
const service = slash === -1 ? "" : action.slice(0, slash);
|
|
2348
|
+
if (import_sdk_core8.SERVICE_LONG_TO_SHORT[service] === void 0) {
|
|
2349
|
+
throw new Error("Owner delegation capabilities are unsupported");
|
|
2350
|
+
}
|
|
2351
|
+
const actions = byService.get(service) ?? [];
|
|
2352
|
+
actions.push(action);
|
|
2353
|
+
byService.set(service, actions);
|
|
2354
|
+
}
|
|
2355
|
+
for (const [service, actions] of byService) {
|
|
2356
|
+
permissions.push({ service, path: params.path, actions });
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
if (permissions.length === 0 || permissions.length > 16) {
|
|
2360
|
+
throw new Error("Owner delegation requires bounded capabilities");
|
|
2361
|
+
}
|
|
2362
|
+
const resources = /* @__PURE__ */ new Set();
|
|
2363
|
+
for (const permission of permissions) {
|
|
2364
|
+
if (typeof permission.service !== "string" || import_sdk_core8.SERVICE_LONG_TO_SHORT[permission.service] === void 0 || typeof permission.path !== "string" || permission.path.length === 0 || permission.path.trim() !== permission.path || /[\u0000-\u001f\u007f\\*]/.test(permission.path) || !Array.isArray(permission.actions) || permission.actions.length === 0 || permission.actions.length > 32) {
|
|
2365
|
+
throw new Error("Owner delegation requires bounded capabilities");
|
|
2366
|
+
}
|
|
2367
|
+
const resourceKey = `${permission.service}\0${permission.path}`;
|
|
2368
|
+
if (resources.has(resourceKey)) {
|
|
2369
|
+
throw new Error("Owner delegation permissions contain duplicate resources");
|
|
2370
|
+
}
|
|
2371
|
+
resources.add(resourceKey);
|
|
2372
|
+
const actions = /* @__PURE__ */ new Set();
|
|
2373
|
+
for (const action of permission.actions) {
|
|
2374
|
+
if (typeof action !== "string" || !action.startsWith(`${permission.service}/`) || action.length === permission.service.length + 1 || actions.has(action)) {
|
|
2375
|
+
throw new Error("Owner delegation capabilities are unsupported");
|
|
2376
|
+
}
|
|
2377
|
+
actions.add(action);
|
|
2378
|
+
}
|
|
2379
|
+
if (permission.service === import_sdk_core8.ENCRYPTION_PERMISSION_SERVICE) {
|
|
2380
|
+
try {
|
|
2381
|
+
(0, import_sdk_core8.parseNetworkId)(permission.path);
|
|
2382
|
+
} catch {
|
|
2383
|
+
throw new Error("Owner delegation encryption permission requires a canonical network URN");
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
return permissions;
|
|
2388
|
+
}
|
|
2389
|
+
function ownerPermissionsToAbilities(permissions) {
|
|
1895
2390
|
const abilities = {};
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
if (
|
|
1899
|
-
|
|
1900
|
-
|
|
2391
|
+
const rawAbilities = {};
|
|
2392
|
+
for (const permission of permissions) {
|
|
2393
|
+
if (permission.service === import_sdk_core8.ENCRYPTION_PERMISSION_SERVICE) {
|
|
2394
|
+
rawAbilities[permission.path] = [...permission.actions];
|
|
2395
|
+
continue;
|
|
2396
|
+
}
|
|
2397
|
+
const shortService = import_sdk_core8.SERVICE_LONG_TO_SHORT[permission.service];
|
|
1901
2398
|
abilities[shortService] ?? (abilities[shortService] = {});
|
|
1902
|
-
|
|
1903
|
-
abilities[shortService][path].push(action);
|
|
2399
|
+
abilities[shortService][permission.path] = [...permission.actions];
|
|
1904
2400
|
}
|
|
1905
|
-
return
|
|
2401
|
+
return { abilities, rawAbilities };
|
|
1906
2402
|
}
|
|
1907
2403
|
function base64UrlEncode(bytes) {
|
|
1908
2404
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
@@ -1939,7 +2435,9 @@ function base64UrlDecode(value) {
|
|
|
1939
2435
|
return new Uint8Array(bytes);
|
|
1940
2436
|
}
|
|
1941
2437
|
async function signJwtInputWithJwk(signingInput, jwk) {
|
|
1942
|
-
|
|
2438
|
+
return signBytesWithJwk(new TextEncoder().encode(signingInput), jwk);
|
|
2439
|
+
}
|
|
2440
|
+
async function signBytesWithJwk(bytes, jwk) {
|
|
1943
2441
|
try {
|
|
1944
2442
|
const subtle = globalThis.crypto?.subtle;
|
|
1945
2443
|
if (!subtle) {
|
|
@@ -2020,6 +2518,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2020
2518
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
2021
2519
|
this._secrets = /* @__PURE__ */ new Map();
|
|
2022
2520
|
this.runtimePermissionGrants = [];
|
|
2521
|
+
/**
|
|
2522
|
+
* Owned space ids this sign-in has already confirmed are hosted. Consulted by
|
|
2523
|
+
* {@link ensureOwnedSpaceHostedById}; cleared on every {@link signIn} because
|
|
2524
|
+
* hosting is confirmed against the session that was active at the time.
|
|
2525
|
+
*/
|
|
2526
|
+
this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
|
|
2023
2527
|
/**
|
|
2024
2528
|
* True when the last signIn() detected an interactive signer and skipped
|
|
2025
2529
|
* client-side bootstrap. Apps can read this to know whether bootstrap was
|
|
@@ -2030,7 +2534,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2030
2534
|
* Outcome of the last signIn()'s account-bootstrap attempt. `skipped` is
|
|
2031
2535
|
* true when bootstrap did not complete (interactive signer, auto-sign
|
|
2032
2536
|
* denied, or a bootstrap step failed); `reason` carries the cause so apps
|
|
2033
|
-
* can surface a "finish account setup" call-to-action.
|
|
2537
|
+
* can surface a "finish account setup" call-to-action. `warnings` is
|
|
2538
|
+
* present when bootstrap completed, but one or more steps recovered from
|
|
2539
|
+
* an ambiguous failure (e.g. a KV batch write reconciled via per-space
|
|
2540
|
+
* fallback) — clean vs. recovered runs are otherwise byte-identical.
|
|
2034
2541
|
*/
|
|
2035
2542
|
this._bootstrapStatus = {
|
|
2036
2543
|
skipped: false
|
|
@@ -2416,6 +2923,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2416
2923
|
this._spaceService = void 0;
|
|
2417
2924
|
this._serviceContext = void 0;
|
|
2418
2925
|
this.runtimePermissionGrants = [];
|
|
2926
|
+
this.confirmedHostedSpaceIds.clear();
|
|
2419
2927
|
await this.tc.signIn(options);
|
|
2420
2928
|
this.syncResolvedHostFromAuth();
|
|
2421
2929
|
const oldGraph = this._serviceGraph;
|
|
@@ -2470,7 +2978,13 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2470
2978
|
return false;
|
|
2471
2979
|
}
|
|
2472
2980
|
try {
|
|
2473
|
-
await this.runAccountBootstrap(steps);
|
|
2981
|
+
const warnings = await this.runAccountBootstrap(steps);
|
|
2982
|
+
if (warnings.length > 0) {
|
|
2983
|
+
this._bootstrapStatus = { skipped: false, warnings };
|
|
2984
|
+
this.notificationHandler.warning(
|
|
2985
|
+
`Account bootstrap completed with warnings: ${warnings.map((w) => `${w.stepId} (${w.kind}: ${w.code})`).join("; ")}`
|
|
2986
|
+
);
|
|
2987
|
+
}
|
|
2474
2988
|
} catch (err) {
|
|
2475
2989
|
const reason = err instanceof Error ? err.message : String(err);
|
|
2476
2990
|
this._bootstrapSkipped = true;
|
|
@@ -2519,6 +3033,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2519
3033
|
const auth = this.auth;
|
|
2520
3034
|
const sessions = /* @__PURE__ */ new Map();
|
|
2521
3035
|
const rawAbilitiesBySpace = /* @__PURE__ */ new Map();
|
|
3036
|
+
const warnings = [];
|
|
2522
3037
|
const primarySession = auth.tinyCloudSession;
|
|
2523
3038
|
const defaultSpaceId = this.ownedSpaceId("default");
|
|
2524
3039
|
const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === import_sdk_core8.BOOTSTRAP_SESSION_REQUESTS.default;
|
|
@@ -2580,32 +3095,45 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2580
3095
|
}
|
|
2581
3096
|
}
|
|
2582
3097
|
if (step.kind === "seed-spaces") {
|
|
2583
|
-
|
|
2584
|
-
|
|
3098
|
+
const registered = await this.account.spaces.registerBatch(
|
|
3099
|
+
step.spaces.map((space) => ({
|
|
2585
3100
|
spaceId: space.spaceId,
|
|
2586
3101
|
name: space.name,
|
|
2587
3102
|
ownerDid: this.did,
|
|
2588
3103
|
type: "owned",
|
|
2589
3104
|
permissions: ["*"],
|
|
2590
3105
|
status: "active"
|
|
3106
|
+
}))
|
|
3107
|
+
);
|
|
3108
|
+
if (!registered.ok) {
|
|
3109
|
+
throw new Error(
|
|
3110
|
+
`Failed to seed account spaces: ${registered.error.message}`
|
|
3111
|
+
);
|
|
3112
|
+
}
|
|
3113
|
+
const batchError = registered.data.recoveredFromBatchError;
|
|
3114
|
+
if (batchError) {
|
|
3115
|
+
warnings.push({
|
|
3116
|
+
stepId: step.id,
|
|
3117
|
+
kind: "batch-write-reconciled",
|
|
3118
|
+
code: batchError.code,
|
|
3119
|
+
message: batchError.message
|
|
2591
3120
|
});
|
|
2592
|
-
if (!registered.ok) {
|
|
2593
|
-
throw new Error(
|
|
2594
|
-
`Failed to seed account space ${space.spaceId}: ${registered.error.message}`
|
|
2595
|
-
);
|
|
2596
|
-
}
|
|
2597
3121
|
}
|
|
2598
3122
|
}
|
|
2599
3123
|
if (step.kind === "seed-applications") {
|
|
2600
3124
|
const registered = await this.account.applications.register(
|
|
2601
|
-
step.manifests.length > 0 ? [...step.manifests] : import_sdk_core8.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST
|
|
3125
|
+
step.manifests.length > 0 ? [...step.manifests] : import_sdk_core8.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
|
|
3126
|
+
// Bootstrap only runs on an account with no registry records, so the
|
|
3127
|
+
// manifest-hash pre-read is definitionally a miss. Skip it; the write
|
|
3128
|
+
// it guards is an INSERT OR REPLACE and is safe to repeat.
|
|
3129
|
+
{ assumeUnregistered: true }
|
|
2602
3130
|
);
|
|
2603
3131
|
if (!registered.ok) {
|
|
2604
3132
|
throw new Error(`Failed to seed bootstrap applications: ${registered.error.message}`);
|
|
2605
3133
|
}
|
|
2606
3134
|
}
|
|
2607
3135
|
if (step.kind === "encryption-network-create") {
|
|
2608
|
-
await this.ensureEncryptionNetwork(step.networkId);
|
|
3136
|
+
await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: true });
|
|
2609
3137
|
}
|
|
2610
3138
|
if (step.kind === "secret-records-schema") {
|
|
2611
3139
|
const db = this.sqlForSpace(step.spaceId).db(step.database);
|
|
@@ -2625,6 +3153,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2625
3153
|
}
|
|
2626
3154
|
}
|
|
2627
3155
|
}
|
|
3156
|
+
return warnings;
|
|
2628
3157
|
}
|
|
2629
3158
|
registerBootstrapRuntimeGrant(session, request, rawAbilities) {
|
|
2630
3159
|
const operations = [];
|
|
@@ -2884,6 +3413,22 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2884
3413
|
await this.ensureEncryptionNetwork(networkId);
|
|
2885
3414
|
}
|
|
2886
3415
|
}
|
|
3416
|
+
/**
|
|
3417
|
+
* Ensure one of this user's owned spaces is hosted, at most once per space
|
|
3418
|
+
* per sign-in.
|
|
3419
|
+
*
|
|
3420
|
+
* Account bootstrap funnels six calls through here (via
|
|
3421
|
+
* `AccountService.ensureAccountSpaceHosted`), and each one re-submits
|
|
3422
|
+
* `activateSessionWithHost` with the *primary* session. That session's recap
|
|
3423
|
+
* covers only `default` and `secrets`, so the account space appears in
|
|
3424
|
+
* neither `activated` nor `skipped` and the guard below returns immediately
|
|
3425
|
+
* — the repeat calls cannot change the outcome, they only cost round trips.
|
|
3426
|
+
*
|
|
3427
|
+
* The memo is per sign-in ({@link confirmedHostedSpaceIds} is cleared in
|
|
3428
|
+
* `signIn`) and only records confirmed successes. A wrong skip is
|
|
3429
|
+
* self-revealing: the next write to the space fails immediately and loudly
|
|
3430
|
+
* with `404 Space not found`.
|
|
3431
|
+
*/
|
|
2887
3432
|
async ensureOwnedSpaceHostedById(spaceId) {
|
|
2888
3433
|
if (!this.auth) {
|
|
2889
3434
|
throw new Error("Owned space hosting requires wallet mode");
|
|
@@ -2892,12 +3437,16 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2892
3437
|
if (!session) {
|
|
2893
3438
|
throw new Error("Owned space hosting requires an active session");
|
|
2894
3439
|
}
|
|
3440
|
+
if (this.confirmedHostedSpaceIds.has(spaceId)) {
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
2895
3443
|
const host = this.hosts[0] ?? this.config.host;
|
|
2896
3444
|
if (!host) {
|
|
2897
3445
|
throw new Error("Owned space hosting requires a TinyCloud host");
|
|
2898
3446
|
}
|
|
2899
3447
|
const activation = await (0, import_sdk_core8.activateSessionWithHost)(host, session.delegationHeader);
|
|
2900
3448
|
if (activation.success && !activation.skipped?.includes(spaceId)) {
|
|
3449
|
+
this.confirmedHostedSpaceIds.add(spaceId);
|
|
2901
3450
|
return;
|
|
2902
3451
|
}
|
|
2903
3452
|
if (!activation.success && activation.status !== 404) {
|
|
@@ -2916,6 +3465,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2916
3465
|
`Failed to activate session after creating owned space ${spaceId}: ${retry.error ?? "space was skipped"}`
|
|
2917
3466
|
);
|
|
2918
3467
|
}
|
|
3468
|
+
this.confirmedHostedSpaceIds.add(spaceId);
|
|
2919
3469
|
}
|
|
2920
3470
|
/**
|
|
2921
3471
|
* Host one of this user's owned spaces by name (e.g. `"applications"`).
|
|
@@ -3231,6 +3781,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3231
3781
|
this._delegationManager = stagedGraph.delegationManager;
|
|
3232
3782
|
this._spaceService = stagedGraph.spaceService;
|
|
3233
3783
|
this._serviceGraph = stagedGraph.graph;
|
|
3784
|
+
this._activeServiceSession = serviceSession;
|
|
3234
3785
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
3235
3786
|
this._secrets = /* @__PURE__ */ new Map();
|
|
3236
3787
|
this._publicKV = void 0;
|
|
@@ -3238,6 +3789,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3238
3789
|
this.tc = stagedGraph.core;
|
|
3239
3790
|
this._recapOperationsCache = stagedPrimary.cache;
|
|
3240
3791
|
this.runtimePermissionGrants = stagedPrimary.grants;
|
|
3792
|
+
this.confirmedHostedSpaceIds.clear();
|
|
3241
3793
|
if (this.auth) {
|
|
3242
3794
|
this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost]);
|
|
3243
3795
|
this._restoredTcSession = void 0;
|
|
@@ -3547,6 +4099,28 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3547
4099
|
currentTinyCloudSession() {
|
|
3548
4100
|
return this.auth?.tinyCloudSession ?? this._restoredTcSession;
|
|
3549
4101
|
}
|
|
4102
|
+
/**
|
|
4103
|
+
* Mint a multi-capability invocation through the established session key.
|
|
4104
|
+
* Callers receive only the signed headers; session key material remains
|
|
4105
|
+
* owned by the session manager and is never part of the caller contract.
|
|
4106
|
+
*/
|
|
4107
|
+
invokeAny(entries, facts) {
|
|
4108
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
4109
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
4110
|
+
return this._serviceGraph.invokeAny(session, entries, facts);
|
|
4111
|
+
}
|
|
4112
|
+
/** Sign protocol bytes with the established in-memory session key. */
|
|
4113
|
+
async signSessionBytes(bytes) {
|
|
4114
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
4115
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
4116
|
+
return signBytesWithJwk(bytes, session.jwk);
|
|
4117
|
+
}
|
|
4118
|
+
/** Bind a session-signed invocation to the canonical service audience. */
|
|
4119
|
+
async bindInvocationAudience(authorization, audience) {
|
|
4120
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
4121
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
4122
|
+
return rewriteInvocationAudience(authorization, audience, session.jwk);
|
|
4123
|
+
}
|
|
3550
4124
|
/**
|
|
3551
4125
|
* Connect a wallet to upgrade from session-only mode to wallet mode.
|
|
3552
4126
|
*
|
|
@@ -3728,6 +4302,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3728
4302
|
if (this._serviceContext) {
|
|
3729
4303
|
const spaceScopedContext = this._serviceGraph.track(new import_sdk_core8.ServiceContext({
|
|
3730
4304
|
invoke: this._serviceContext.invoke,
|
|
4305
|
+
invokeAny: this._serviceContext.invokeAny,
|
|
3731
4306
|
fetch: this._serviceContext.fetch,
|
|
3732
4307
|
hosts: this._serviceContext.hosts,
|
|
3733
4308
|
telemetry: this.config.telemetry
|
|
@@ -3820,7 +4395,17 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3820
4395
|
verifyNodeSignature: (nodeId, message, signature) => (0, import_sdk_core8.verifyDidKeyEd25519Signature)(nodeId, message, signature)
|
|
3821
4396
|
};
|
|
3822
4397
|
}
|
|
4398
|
+
/**
|
|
4399
|
+
* The node DID for {@link config.host}.
|
|
4400
|
+
*
|
|
4401
|
+
* Sign-in already performs `GET /info` for the protocol-version check, and
|
|
4402
|
+
* that response carries `nodeId`, so reuse it rather than fetching `/info` a
|
|
4403
|
+
* second time. Only a value recorded for this exact host qualifies; older
|
|
4404
|
+
* nodes that omit `nodeId` fall through to the fetch.
|
|
4405
|
+
*/
|
|
3823
4406
|
async fetchNodeId() {
|
|
4407
|
+
const cached = this.auth?.nodeIdForHost(this.config.host);
|
|
4408
|
+
if (cached) return cached;
|
|
3824
4409
|
const response = await fetch(`${this.config.host}/info`);
|
|
3825
4410
|
if (!response.ok) {
|
|
3826
4411
|
throw new Error(`Failed to fetch node info: HTTP ${response.status}`);
|
|
@@ -4276,21 +4861,29 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
4276
4861
|
async createOwnerDelegation(params, assertActive) {
|
|
4277
4862
|
const assertOwnerGraphActive = assertActive ?? this._serviceGraph.assertActive.bind(this._serviceGraph);
|
|
4278
4863
|
assertOwnerGraphActive();
|
|
4279
|
-
if (!params.delegateDid.startsWith("did:key:")
|
|
4864
|
+
if (!params.delegateDid.startsWith("did:key:")) {
|
|
4280
4865
|
throw new Error("Owner delegation requires an external did:key audience and bounded capabilities");
|
|
4281
4866
|
}
|
|
4867
|
+
const permissions = ownerDelegationPermissions(params);
|
|
4282
4868
|
const now = /* @__PURE__ */ new Date();
|
|
4283
|
-
if (params.expiresAt.getTime() <= now.getTime() || params.expiresAt.getTime() - now.getTime() > import_sdk_core8.EXPIRY.MAX_MS) {
|
|
4869
|
+
if (!(params.expiresAt instanceof Date) || params.expiresAt.getTime() <= now.getTime() || params.expiresAt.getTime() - now.getTime() > import_sdk_core8.EXPIRY.MAX_MS) {
|
|
4284
4870
|
throw new Error("Owner delegation expiry must be explicit, future, and within EXPIRY.MAX_MS");
|
|
4285
4871
|
}
|
|
4286
4872
|
if (!this.signer) throw new Error("Owner wallet signer is required");
|
|
4287
4873
|
const session = this.currentTinyCloudSession();
|
|
4288
4874
|
if (!session) throw new Error("Owner session is required");
|
|
4289
|
-
const
|
|
4290
|
-
|
|
4875
|
+
const ownerDid = (0, import_sdk_core8.pkhDid)(session.address, session.chainId);
|
|
4876
|
+
for (const permission of permissions) {
|
|
4877
|
+
if (permission.service === import_sdk_core8.ENCRYPTION_PERMISSION_SERVICE && !(0, import_sdk_core8.principalDidEquals)((0, import_sdk_core8.parseNetworkId)(permission.path).ownerDid, ownerDid)) {
|
|
4878
|
+
throw new Error("Owner delegation cannot grant a foreign encryption network");
|
|
4879
|
+
}
|
|
4880
|
+
}
|
|
4881
|
+
const { abilities, rawAbilities } = ownerPermissionsToAbilities(permissions);
|
|
4882
|
+
const primary = permissions[0];
|
|
4291
4883
|
const host = this.config.host;
|
|
4292
4884
|
const prepared = this.wasmBindings.prepareSession({
|
|
4293
4885
|
abilities,
|
|
4886
|
+
...Object.keys(rawAbilities).length === 0 ? {} : { rawAbilities },
|
|
4294
4887
|
address: this.wasmBindings.ensureEip55(session.address),
|
|
4295
4888
|
chainId: session.chainId,
|
|
4296
4889
|
domain: this.siweDomain,
|
|
@@ -4310,10 +4903,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
4310
4903
|
const delegation = {
|
|
4311
4904
|
cid: delegationSession.delegationCid,
|
|
4312
4905
|
delegateDID: params.delegateDid,
|
|
4313
|
-
delegatorDID:
|
|
4906
|
+
delegatorDID: ownerDid,
|
|
4314
4907
|
spaceId: params.spaceId,
|
|
4315
|
-
path:
|
|
4316
|
-
actions: [...
|
|
4908
|
+
path: primary.path,
|
|
4909
|
+
actions: [...primary.actions],
|
|
4317
4910
|
expiry: params.expiresAt,
|
|
4318
4911
|
isRevoked: false,
|
|
4319
4912
|
allowSubDelegation: true,
|
|
@@ -4324,6 +4917,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
4324
4917
|
delegation,
|
|
4325
4918
|
signedDagCbor: decodeAuthorizationBytes(delegationSession.delegationHeader.Authorization),
|
|
4326
4919
|
delegationCid: delegationSession.delegationCid,
|
|
4920
|
+
permissions,
|
|
4327
4921
|
nodeReceipt: {
|
|
4328
4922
|
commitEventCid: activation.commitEventCid,
|
|
4329
4923
|
activated: activation.activated ?? [],
|
|
@@ -4374,6 +4968,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
4374
4968
|
shareUrl: input.shareUrl,
|
|
4375
4969
|
documentName: input.documentName,
|
|
4376
4970
|
jti: base64UrlEncode(crypto.getRandomValues(new Uint8Array(16))),
|
|
4971
|
+
idempotencyKey: input.idempotencyKey,
|
|
4377
4972
|
expiresAt: input.expiresAt
|
|
4378
4973
|
};
|
|
4379
4974
|
const requestBodyDigest = base64UrlEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode((0, import_sdk_core8.canonicalizeEncryptionJson)(body)))));
|
|
@@ -4583,6 +5178,13 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
4583
5178
|
body
|
|
4584
5179
|
)
|
|
4585
5180
|
});
|
|
5181
|
+
if (response.status === 409) {
|
|
5182
|
+
const existing = await this.getEncryptionNetwork(networkId);
|
|
5183
|
+
if (existing) return existing;
|
|
5184
|
+
throw new Error(
|
|
5185
|
+
`Encryption network ${networkId} already exists but could not be read back`
|
|
5186
|
+
);
|
|
5187
|
+
}
|
|
4586
5188
|
if (!response.ok) {
|
|
4587
5189
|
throw new Error(
|
|
4588
5190
|
`Failed to create encryption network ${networkId}: HTTP ${response.status} ${await response.text()}`
|
|
@@ -4591,11 +5193,19 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
4591
5193
|
const created = await response.json();
|
|
4592
5194
|
return created.descriptor;
|
|
4593
5195
|
}
|
|
4594
|
-
|
|
5196
|
+
/**
|
|
5197
|
+
* Ensure an encryption network exists, creating it if necessary.
|
|
5198
|
+
*
|
|
5199
|
+
* @param nameOrNetworkId - Network name or full `urn:tinycloud:encryption:` id.
|
|
5200
|
+
* @param options - See {@link EnsureEncryptionNetworkOptions}.
|
|
5201
|
+
*/
|
|
5202
|
+
async ensureEncryptionNetwork(nameOrNetworkId = DEFAULT_ENCRYPTION_NETWORK_NAME, options = {}) {
|
|
4595
5203
|
const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
5204
|
+
if (!options.assumeMissing) {
|
|
5205
|
+
const existing = await this.getEncryptionNetwork(networkId);
|
|
5206
|
+
if (existing) {
|
|
5207
|
+
return existing;
|
|
5208
|
+
}
|
|
4599
5209
|
}
|
|
4600
5210
|
const parsed = (0, import_sdk_core8.parseNetworkId)(networkId);
|
|
4601
5211
|
if (!didPrincipalMatches2(parsed.ownerDid, this.did)) {
|