@tinycloud/node-sdk 2.11.0-beta.8 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{core-Bkptd8qZ.d.cts → core-Bq8TMY7Y.d.cts} +188 -8
- package/dist/{core-Bkptd8qZ.d.ts → core-Bq8TMY7Y.d.ts} +188 -8
- package/dist/core.cjs +675 -67
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.js +693 -75
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +752 -69
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +85 -3
- package/dist/index.d.ts +85 -3
- package/dist/index.js +765 -76
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/core.js
CHANGED
|
@@ -220,6 +220,8 @@ var FileSessionStorage = class {
|
|
|
220
220
|
};
|
|
221
221
|
|
|
222
222
|
// src/authorization/NodeUserAuthorization.ts
|
|
223
|
+
import { createHash } from "crypto";
|
|
224
|
+
import { SiweMessage } from "siwe";
|
|
223
225
|
import {
|
|
224
226
|
fetchPeerId,
|
|
225
227
|
submitHostDelegation,
|
|
@@ -238,6 +240,13 @@ import {
|
|
|
238
240
|
canonicalizeAddress,
|
|
239
241
|
makePkhSpaceId,
|
|
240
242
|
pkhDid,
|
|
243
|
+
extractImmutableSiweFields,
|
|
244
|
+
diffImmutableSiweFields,
|
|
245
|
+
extractRecapAttenuations,
|
|
246
|
+
unauthorizedRecapCapabilities,
|
|
247
|
+
parseCanonicalRecapResource,
|
|
248
|
+
ACCOUNT_REGISTRY_SPACE,
|
|
249
|
+
ACCOUNT_MANIFEST_PERMISSIONS,
|
|
241
250
|
KV,
|
|
242
251
|
SQL,
|
|
243
252
|
DUCKDB,
|
|
@@ -253,6 +262,27 @@ var defaultSignStrategy = { type: "auto-sign" };
|
|
|
253
262
|
// src/authorization/NodeUserAuthorization.ts
|
|
254
263
|
var DECRYPT_ACTION = ENCRYPTION.DECRYPT;
|
|
255
264
|
var NETWORK_CREATE_ACTION = ENCRYPTION.NETWORK_CREATE;
|
|
265
|
+
function canonicalizeOpenKeyManifestJson(value) {
|
|
266
|
+
const json = JSON.stringify(value);
|
|
267
|
+
if (json === void 0) {
|
|
268
|
+
throw new TypeError("manifest is not JSON-serializable");
|
|
269
|
+
}
|
|
270
|
+
return canonicalizeJsonValue(JSON.parse(json));
|
|
271
|
+
}
|
|
272
|
+
function canonicalizeJsonValue(value) {
|
|
273
|
+
if (value === null || typeof value !== "object") {
|
|
274
|
+
return JSON.stringify(value);
|
|
275
|
+
}
|
|
276
|
+
if (Array.isArray(value)) {
|
|
277
|
+
return `[${value.map(canonicalizeJsonValue).join(",")}]`;
|
|
278
|
+
}
|
|
279
|
+
return `{${Object.keys(value).sort().map(
|
|
280
|
+
(key) => `${JSON.stringify(key)}:${canonicalizeJsonValue(value[key])}`
|
|
281
|
+
).join(",")}}`;
|
|
282
|
+
}
|
|
283
|
+
function canonicalOpenKeyManifestSha256Hex(value) {
|
|
284
|
+
return createHash("sha256").update(canonicalizeOpenKeyManifestJson(value)).digest("hex");
|
|
285
|
+
}
|
|
256
286
|
function didPrincipalMatches(actual, expected) {
|
|
257
287
|
try {
|
|
258
288
|
return principalDidEquals(actual, expected);
|
|
@@ -270,6 +300,16 @@ function addRawAbility(rawAbilities, resource, action) {
|
|
|
270
300
|
actions.push(action);
|
|
271
301
|
}
|
|
272
302
|
}
|
|
303
|
+
function cloneAbilitiesMap(actions) {
|
|
304
|
+
return Object.fromEntries(
|
|
305
|
+
Object.entries(actions).map(([service, paths]) => [
|
|
306
|
+
service,
|
|
307
|
+
Object.fromEntries(
|
|
308
|
+
Object.entries(paths).map(([path, pathActions]) => [path, [...pathActions]])
|
|
309
|
+
)
|
|
310
|
+
])
|
|
311
|
+
);
|
|
312
|
+
}
|
|
273
313
|
var NodeUserAuthorization = class {
|
|
274
314
|
constructor(config) {
|
|
275
315
|
this.activeSessionKeyId = "default";
|
|
@@ -541,23 +581,41 @@ var NodeUserAuthorization = class {
|
|
|
541
581
|
const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
|
|
542
582
|
const primarySpaceId2 = makePkhSpaceId(address, chainId, this.spacePrefix);
|
|
543
583
|
const secretsSpaceId = makePkhSpaceId(address, chainId, "secrets");
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
584
|
+
const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
|
|
585
|
+
const spaceAbilities2 = {
|
|
586
|
+
[primarySpaceId2]: primaryActions,
|
|
587
|
+
[secretsSpaceId]: {
|
|
588
|
+
kv: {
|
|
589
|
+
"vault/secrets/": [
|
|
590
|
+
KV.GET,
|
|
591
|
+
KV.PUT,
|
|
592
|
+
KV.DEL,
|
|
593
|
+
KV.LIST,
|
|
594
|
+
KV.METADATA
|
|
595
|
+
]
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
if (this.includeAccountRegistryPermissions && this.spacePrefix !== ACCOUNT_REGISTRY_SPACE) {
|
|
600
|
+
spaceAbilities2[makePkhSpaceId(address, chainId, ACCOUNT_REGISTRY_SPACE)] = resourceCapabilitiesToAbilitiesMap(ACCOUNT_MANIFEST_PERMISSIONS);
|
|
601
|
+
} else if (this.includeAccountRegistryPermissions) {
|
|
602
|
+
const accountAbilities = resourceCapabilitiesToAbilitiesMap(
|
|
603
|
+
ACCOUNT_MANIFEST_PERMISSIONS
|
|
604
|
+
);
|
|
605
|
+
for (const [service, paths] of Object.entries(accountAbilities)) {
|
|
606
|
+
const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
|
|
607
|
+
for (const [path, actions] of Object.entries(paths)) {
|
|
608
|
+
const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
|
|
609
|
+
for (const action of actions) {
|
|
610
|
+
if (!existingActions.includes(action)) existingActions.push(action);
|
|
558
611
|
}
|
|
559
612
|
}
|
|
560
|
-
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
abilities: primaryActions,
|
|
617
|
+
spaceId: primarySpaceId2,
|
|
618
|
+
spaceAbilities: spaceAbilities2,
|
|
561
619
|
rawAbilities: {
|
|
562
620
|
[defaultNetworkId]: [DECRYPT_ACTION, NETWORK_CREATE_ACTION]
|
|
563
621
|
}
|
|
@@ -1138,6 +1196,398 @@ var NodeUserAuthorization = class {
|
|
|
1138
1196
|
await this.ensureSpaceExists();
|
|
1139
1197
|
return clientSession;
|
|
1140
1198
|
}
|
|
1199
|
+
/**
|
|
1200
|
+
* End-to-end sign-in via OpenKey `authorizeTinyCloud()` (Sol CRITICAL-4).
|
|
1201
|
+
*
|
|
1202
|
+
* This is the production entry point that wires
|
|
1203
|
+
* {@link prepareSessionForSigning} → OpenKey `authorizeTinyCloud()` →
|
|
1204
|
+
* {@link signInWithOpenKeyResult} into one call. Consumers pass in a
|
|
1205
|
+
* caller-provided `authorizeFn` (typically a thin wrapper around
|
|
1206
|
+
* `openkey.authorizeTinyCloud()`) so this method has no direct
|
|
1207
|
+
* dependency on the OpenKey SDK — it works with any transport that
|
|
1208
|
+
* conforms to the versioned protocol.
|
|
1209
|
+
*
|
|
1210
|
+
* The `authorizeFn` MUST:
|
|
1211
|
+
* 1. Send the prepared SIWE bytes to OpenKey.
|
|
1212
|
+
* 2. Return the exact `TinyCloudAuthorizationResultV1` shape (with
|
|
1213
|
+
* `signedMessage` — the bytes the signature actually verifies
|
|
1214
|
+
* against — plus `selectedActionKeys` and `permissions`).
|
|
1215
|
+
*
|
|
1216
|
+
* NodeUserAuthorization then completes the session with `signedMessage`
|
|
1217
|
+
* (never the prepared bytes), verifying the signature, the immutable
|
|
1218
|
+
* SIWE fields, and every subset invariant enforced by
|
|
1219
|
+
* {@link signInWithOpenKeyResult}. This preserves the exact-byte
|
|
1220
|
+
* guarantee: if the user did NOT narrow, the returned `signedMessage`
|
|
1221
|
+
* equals the prepared SIWE byte-for-byte; if the user narrowed, only
|
|
1222
|
+
* the ReCap block differs and every immutable header field survives.
|
|
1223
|
+
*
|
|
1224
|
+
* Legacy exact-byte {@link signIn} callers are unaffected — this method
|
|
1225
|
+
* is opt-in.
|
|
1226
|
+
*
|
|
1227
|
+
* @param authorizeFn Callback that invokes the versioned OpenKey
|
|
1228
|
+
* `authorizeTinyCloud()` protocol and returns the result. Receives
|
|
1229
|
+
* the prepared SIWE bytes plus a `host` hint the widget should bind
|
|
1230
|
+
* into its /authorize-sign-prepare context.
|
|
1231
|
+
* @param options Optional overrides for the OpenKey request.
|
|
1232
|
+
*/
|
|
1233
|
+
async signInWithOpenKey(authorizeFn, options) {
|
|
1234
|
+
if (options?.host !== void 0) {
|
|
1235
|
+
const overrideHost = options.host.trim();
|
|
1236
|
+
if (!overrideHost) {
|
|
1237
|
+
throw new Error("OpenKey authorization host must not be empty");
|
|
1238
|
+
}
|
|
1239
|
+
this.tinycloudHosts = [overrideHost];
|
|
1240
|
+
}
|
|
1241
|
+
const signingAddress = canonicalizeAddress(await this.signer.getAddress());
|
|
1242
|
+
const signingChainId = await this.signer.getChainId();
|
|
1243
|
+
await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
|
|
1244
|
+
const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
|
|
1245
|
+
const presentation = this.buildPresentationEnvelope(options?.reason);
|
|
1246
|
+
const hostHint = this.primaryTinyCloudHost;
|
|
1247
|
+
const result = await authorizeFn({
|
|
1248
|
+
protocolVersion: 1,
|
|
1249
|
+
siwe: prepared.siwe,
|
|
1250
|
+
jwk,
|
|
1251
|
+
host: hostHint,
|
|
1252
|
+
keyId: options?.openkeyKeyId,
|
|
1253
|
+
presentation
|
|
1254
|
+
});
|
|
1255
|
+
return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
|
|
1256
|
+
}
|
|
1257
|
+
/**
|
|
1258
|
+
* Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
|
|
1259
|
+
* envelope is display-only — the OpenKey widget renders it under an
|
|
1260
|
+
* honest trust label, and the server /authorize-sign-prepare route
|
|
1261
|
+
* decides whether to upgrade the label to `origin-bound` by fetching
|
|
1262
|
+
* the well-known manifest and comparing its digest to
|
|
1263
|
+
* `manifestDigest`.
|
|
1264
|
+
*
|
|
1265
|
+
* Returns `undefined` when no manifest is set — the widget renders as
|
|
1266
|
+
* "no manifest supplied" (unsigned).
|
|
1267
|
+
*
|
|
1268
|
+
* The digest is a deterministic SHA-256 over canonical JSON of the
|
|
1269
|
+
* first manifest payload; OpenKey parses the fetched well-known manifest
|
|
1270
|
+
* and applies the same canonicalization before comparing digests.
|
|
1271
|
+
*/
|
|
1272
|
+
buildPresentationEnvelope(reason) {
|
|
1273
|
+
const manifests = Array.isArray(this._manifest) ? this._manifest : this._manifest ? [this._manifest] : [];
|
|
1274
|
+
if (manifests.length === 0 && !reason) {
|
|
1275
|
+
return void 0;
|
|
1276
|
+
}
|
|
1277
|
+
const primary = manifests[0];
|
|
1278
|
+
let displayName;
|
|
1279
|
+
let manifestId;
|
|
1280
|
+
let manifestDigest;
|
|
1281
|
+
const envelopeManifests = [];
|
|
1282
|
+
for (const m of manifests) {
|
|
1283
|
+
envelopeManifests.push({
|
|
1284
|
+
name: m.name,
|
|
1285
|
+
appId: m.app_id,
|
|
1286
|
+
// Deep-copy to prevent later mutations from affecting the digest.
|
|
1287
|
+
payload: JSON.parse(JSON.stringify(m))
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
if (primary) {
|
|
1291
|
+
displayName = primary.name;
|
|
1292
|
+
manifestId = primary.app_id;
|
|
1293
|
+
manifestDigest = canonicalOpenKeyManifestSha256Hex(primary);
|
|
1294
|
+
}
|
|
1295
|
+
return {
|
|
1296
|
+
protocolVersion: 1,
|
|
1297
|
+
displayName,
|
|
1298
|
+
reason: reason && reason.length > 0 ? reason : void 0,
|
|
1299
|
+
manifestId,
|
|
1300
|
+
manifestDigest,
|
|
1301
|
+
manifests: envelopeManifests.length > 0 ? envelopeManifests : void 0
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
/**
|
|
1305
|
+
* Complete sign-in with a versioned OpenKey authorization result.
|
|
1306
|
+
*
|
|
1307
|
+
* Where `signInWithPreparedSession` completes with a signature over the
|
|
1308
|
+
* ORIGINAL prepared SIWE, this method completes with the SIWE bytes the
|
|
1309
|
+
* OpenKey widget actually presented and signed (`signedMessage`). It
|
|
1310
|
+
* exists because the versioned authorizeTinyCloud protocol lets the user
|
|
1311
|
+
* narrow capabilities; the returned bytes may differ from the original
|
|
1312
|
+
* request and are the only ones the signature actually verifies against.
|
|
1313
|
+
*
|
|
1314
|
+
* The `result` parameter is validated at the trust boundary via
|
|
1315
|
+
* `validateAuthorizationResultV1` (in sdk-core). Callers should validate
|
|
1316
|
+
* BEFORE calling this method so unknown/missing fields are rejected
|
|
1317
|
+
* before any session state is created.
|
|
1318
|
+
*
|
|
1319
|
+
* Validation performed here (defence-in-depth: even a "valid" v1 payload
|
|
1320
|
+
* must clear these checks before any session state is created):
|
|
1321
|
+
* 1. `result.address` matches the local signer's address.
|
|
1322
|
+
* 2. `result.signature` recovers to `result.address` when verified against
|
|
1323
|
+
* `result.signedMessage`.
|
|
1324
|
+
* 3. Every immutable SIWE header field (domain, address, URI, version,
|
|
1325
|
+
* chainId, nonce, issuedAt) in the original prepared SIWE is preserved
|
|
1326
|
+
* byte-for-byte in `result.signedMessage`. `statement` is NOT in this
|
|
1327
|
+
* list because the WASM renders it from the ReCap contents; it MUST
|
|
1328
|
+
* change when the ReCap is narrowed.
|
|
1329
|
+
* 4. Every capability in `result.signedMessage`'s ReCap block was present
|
|
1330
|
+
* in the original prepared SIWE — narrowing OK, broadening rejected.
|
|
1331
|
+
* 5. Every `result.selectedActionKeys` entry corresponds to a
|
|
1332
|
+
* (resource, action) pair in the returned SIWE.
|
|
1333
|
+
*/
|
|
1334
|
+
async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
|
|
1335
|
+
if (result.protocolVersion !== 1) {
|
|
1336
|
+
throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
|
|
1337
|
+
}
|
|
1338
|
+
if (!result.signedMessage) {
|
|
1339
|
+
throw new Error("OpenKey result must include signedMessage");
|
|
1340
|
+
}
|
|
1341
|
+
if (!prepared.siwe) {
|
|
1342
|
+
throw new Error(
|
|
1343
|
+
"signInWithOpenKeyResult requires prepared.siwe (the SDK-generated SIWE the sign-in was proposed with)"
|
|
1344
|
+
);
|
|
1345
|
+
}
|
|
1346
|
+
const expectedAddress = canonicalizeAddress(await this.signer.getAddress());
|
|
1347
|
+
if (canonicalizeAddress(result.address) !== expectedAddress) {
|
|
1348
|
+
throw new Error(
|
|
1349
|
+
`OpenKey returned signature from ${result.address} but expected ${expectedAddress}`
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
let parsedSignedSiwe;
|
|
1353
|
+
try {
|
|
1354
|
+
parsedSignedSiwe = new SiweMessage(result.signedMessage);
|
|
1355
|
+
} catch (err) {
|
|
1356
|
+
throw new Error(
|
|
1357
|
+
`OpenKey signedMessage is not a parseable SIWE: ${err.message}`
|
|
1358
|
+
);
|
|
1359
|
+
}
|
|
1360
|
+
const verifyResult = await parsedSignedSiwe.verify(
|
|
1361
|
+
{ signature: result.signature },
|
|
1362
|
+
{ suppressExceptions: true }
|
|
1363
|
+
);
|
|
1364
|
+
if (!verifyResult.success) {
|
|
1365
|
+
const errData = verifyResult.error;
|
|
1366
|
+
const type = errData && typeof errData === "object" && "type" in errData ? errData.type : void 0;
|
|
1367
|
+
const details = errData && typeof errData === "object" && "expected" in errData ? ` (expected=${errData.expected}, received=${errData.received})` : "";
|
|
1368
|
+
throw new Error(
|
|
1369
|
+
`OpenKey signature verification failed${type ? `: ${type}` : ""}${details}`
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
if (canonicalizeAddress(parsedSignedSiwe.address) !== expectedAddress) {
|
|
1373
|
+
throw new Error(
|
|
1374
|
+
`OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
|
|
1375
|
+
);
|
|
1376
|
+
}
|
|
1377
|
+
const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
|
|
1378
|
+
const originalFields = extractImmutableSiweFields(prepared.siwe);
|
|
1379
|
+
const signedFields = extractImmutableSiweFields(result.signedMessage);
|
|
1380
|
+
const changedFields = diffImmutableSiweFields(originalFields, signedFields, {
|
|
1381
|
+
originalHasRecap
|
|
1382
|
+
});
|
|
1383
|
+
if (changedFields.length > 0) {
|
|
1384
|
+
throw new Error(
|
|
1385
|
+
`OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
let originalCaps;
|
|
1389
|
+
let signedCaps;
|
|
1390
|
+
try {
|
|
1391
|
+
originalCaps = extractRecapAttenuations(prepared.siwe);
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
throw new Error(
|
|
1394
|
+
`Failed to decode prepared SIWE recap: ${err.message}`
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
try {
|
|
1398
|
+
signedCaps = extractRecapAttenuations(result.signedMessage);
|
|
1399
|
+
} catch (err) {
|
|
1400
|
+
throw new Error(
|
|
1401
|
+
`Failed to decode OpenKey signedMessage recap: ${err.message}`
|
|
1402
|
+
);
|
|
1403
|
+
}
|
|
1404
|
+
const unauthorized = unauthorizedRecapCapabilities(signedCaps, originalCaps);
|
|
1405
|
+
if (unauthorized.length > 0) {
|
|
1406
|
+
const previewLimit = 5;
|
|
1407
|
+
const preview = unauthorized.slice(0, previewLimit).map((u) => `${u.resource}::${u.action}`).join(", ");
|
|
1408
|
+
const overflow = unauthorized.length > previewLimit ? ` (and ${unauthorized.length - previewLimit} more)` : "";
|
|
1409
|
+
throw new Error(
|
|
1410
|
+
`OpenKey signedMessage broadened authorization beyond the original request: ${preview}${overflow}`
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
if (!result.signedMessage.includes(prepared.spaceId)) {
|
|
1414
|
+
throw new Error(
|
|
1415
|
+
`OpenKey signedMessage does not reference the expected spaceId ${prepared.spaceId}`
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
const grantedPairs = /* @__PURE__ */ new Set();
|
|
1419
|
+
const grantedFourPartIndex = /* @__PURE__ */ new Map();
|
|
1420
|
+
for (const [resource, actions] of Object.entries(signedCaps)) {
|
|
1421
|
+
const { space, path } = parseCanonicalRecapResource(resource);
|
|
1422
|
+
for (const action of Object.keys(actions)) {
|
|
1423
|
+
grantedPairs.add(`${resource}\0${action}`);
|
|
1424
|
+
const slashIdx = action.indexOf("/");
|
|
1425
|
+
const service = slashIdx > 0 ? action.slice(0, slashIdx) : "";
|
|
1426
|
+
if (service) {
|
|
1427
|
+
grantedFourPartIndex.set(
|
|
1428
|
+
`${service}\0${space}\0${path}\0${action}`,
|
|
1429
|
+
`${resource}\0${action}`
|
|
1430
|
+
);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
const isStructurallyRequired = (action) => action === "tinycloud.capabilities/read" || action === "capabilities/read";
|
|
1435
|
+
const nonRequiredGrantedPairs = /* @__PURE__ */ new Set();
|
|
1436
|
+
for (const pair of grantedPairs) {
|
|
1437
|
+
const idx = pair.indexOf("\0");
|
|
1438
|
+
const action = idx >= 0 ? pair.slice(idx + 1) : pair;
|
|
1439
|
+
if (!isStructurallyRequired(action)) {
|
|
1440
|
+
nonRequiredGrantedPairs.add(pair);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
const allSignedGrantedPairs = new Set(grantedPairs);
|
|
1444
|
+
const seenRawKeys = /* @__PURE__ */ new Set();
|
|
1445
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
1446
|
+
if (seenRawKeys.has(rawKey)) {
|
|
1447
|
+
throw new Error(
|
|
1448
|
+
`OpenKey selectedActionKeys contains duplicate entry ${rawKey}`
|
|
1449
|
+
);
|
|
1450
|
+
}
|
|
1451
|
+
seenRawKeys.add(rawKey);
|
|
1452
|
+
}
|
|
1453
|
+
const selectedResourceActionPairs = /* @__PURE__ */ new Set();
|
|
1454
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
1455
|
+
const parts = rawKey.split("\0");
|
|
1456
|
+
if (parts.length !== 4) {
|
|
1457
|
+
throw new Error(
|
|
1458
|
+
`OpenKey selectedActionKeys entry is malformed (expected canonical 4-part service\\0space\\0path\\0ability): ${rawKey}`
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
const canonical = grantedFourPartIndex.get(rawKey);
|
|
1462
|
+
if (!canonical) {
|
|
1463
|
+
throw new Error(
|
|
1464
|
+
`OpenKey selectedActionKeys entry ${rawKey} is not covered by any granted capability`
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
selectedResourceActionPairs.add(canonical);
|
|
1468
|
+
}
|
|
1469
|
+
if (nonRequiredGrantedPairs.size > 0) {
|
|
1470
|
+
const missingPairs = [];
|
|
1471
|
+
for (const pair of nonRequiredGrantedPairs) {
|
|
1472
|
+
if (!selectedResourceActionPairs.has(pair)) {
|
|
1473
|
+
missingPairs.push(pair);
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
if (missingPairs.length > 0) {
|
|
1477
|
+
const previewLimit = 5;
|
|
1478
|
+
const preview = missingPairs.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
1479
|
+
const overflow = missingPairs.length > previewLimit ? ` (and ${missingPairs.length - previewLimit} more)` : "";
|
|
1480
|
+
throw new Error(
|
|
1481
|
+
`OpenKey selectedActionKeys is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
1482
|
+
);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
if (allSignedGrantedPairs.size > 0 && result.permissions.length === 0) {
|
|
1486
|
+
throw new Error(
|
|
1487
|
+
"OpenKey permissions is empty but signedMessage carries capabilities \u2014 refusing to accept a session without an authoritative grant list"
|
|
1488
|
+
);
|
|
1489
|
+
}
|
|
1490
|
+
const permissionCanonicalKeys = /* @__PURE__ */ new Set();
|
|
1491
|
+
const permissionPairsCovered = /* @__PURE__ */ new Set();
|
|
1492
|
+
for (const perm of result.permissions) {
|
|
1493
|
+
const { service, space, path, actions } = perm;
|
|
1494
|
+
if (typeof service !== "string" || typeof space !== "string" || typeof path !== "string") {
|
|
1495
|
+
throw new Error(
|
|
1496
|
+
`OpenKey permissions entry has non-string service/space/path (service=${service}, space=${space}, path=${path})`
|
|
1497
|
+
);
|
|
1498
|
+
}
|
|
1499
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
1500
|
+
throw new Error(
|
|
1501
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has empty actions \u2014 a capability-bearing entry must list at least one action`
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
const canonicalKey = `${service}\0${space}\0${path}`;
|
|
1505
|
+
if (permissionCanonicalKeys.has(canonicalKey)) {
|
|
1506
|
+
throw new Error(
|
|
1507
|
+
`OpenKey permissions has duplicate entry for (${service} ${space} ${path})`
|
|
1508
|
+
);
|
|
1509
|
+
}
|
|
1510
|
+
permissionCanonicalKeys.add(canonicalKey);
|
|
1511
|
+
let expectedResource;
|
|
1512
|
+
if (space.startsWith("tinycloud:")) {
|
|
1513
|
+
const shortService = service.startsWith("tinycloud.") ? service.slice("tinycloud.".length) : service;
|
|
1514
|
+
expectedResource = path ? `${space}/${shortService}/${path}` : `${space}/${shortService}`;
|
|
1515
|
+
} else {
|
|
1516
|
+
if (path) {
|
|
1517
|
+
throw new Error(
|
|
1518
|
+
`OpenKey permissions entry (${service} ${space} ${path}) carries a path on a non-tinycloud: resource URI, which is not a recognized encoding`
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
expectedResource = space;
|
|
1522
|
+
}
|
|
1523
|
+
let matchedResource;
|
|
1524
|
+
if (signedCaps[expectedResource] !== void 0) {
|
|
1525
|
+
matchedResource = expectedResource;
|
|
1526
|
+
} else {
|
|
1527
|
+
throw new Error(
|
|
1528
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims grants not confirmed in signedMessage capabilities`
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
const grantedActionsForResource = new Set(
|
|
1532
|
+
Object.keys(signedCaps[matchedResource])
|
|
1533
|
+
);
|
|
1534
|
+
for (const action of actions) {
|
|
1535
|
+
if (typeof action !== "string" || !action) {
|
|
1536
|
+
throw new Error(
|
|
1537
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has non-string action ${String(action)}`
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
if (!grantedActionsForResource.has(action)) {
|
|
1541
|
+
throw new Error(
|
|
1542
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims action ${action} not present in signedMessage capabilities`
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
permissionPairsCovered.add(`${matchedResource}\0${action}`);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
if (allSignedGrantedPairs.size > 0) {
|
|
1549
|
+
const missingFromPerms = [];
|
|
1550
|
+
for (const pair of allSignedGrantedPairs) {
|
|
1551
|
+
if (!permissionPairsCovered.has(pair)) {
|
|
1552
|
+
missingFromPerms.push(pair);
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
if (missingFromPerms.length > 0) {
|
|
1556
|
+
const previewLimit = 5;
|
|
1557
|
+
const preview = missingFromPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
1558
|
+
const overflow = missingFromPerms.length > previewLimit ? ` (and ${missingFromPerms.length - previewLimit} more)` : "";
|
|
1559
|
+
throw new Error(
|
|
1560
|
+
`OpenKey permissions is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
const extrasInPerms = [];
|
|
1564
|
+
for (const pair of permissionPairsCovered) {
|
|
1565
|
+
if (!allSignedGrantedPairs.has(pair)) {
|
|
1566
|
+
extrasInPerms.push(pair);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
if (extrasInPerms.length > 0) {
|
|
1570
|
+
const previewLimit = 5;
|
|
1571
|
+
const preview = extrasInPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
1572
|
+
const overflow = extrasInPerms.length > previewLimit ? ` (and ${extrasInPerms.length - previewLimit} more)` : "";
|
|
1573
|
+
throw new Error(
|
|
1574
|
+
`OpenKey permissions contains entries not present in the signed authority: ${preview}${overflow}`
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
void nonRequiredGrantedPairs;
|
|
1579
|
+
return this.signInWithPreparedSession(
|
|
1580
|
+
{
|
|
1581
|
+
siwe: result.signedMessage,
|
|
1582
|
+
jwk: prepared.jwk,
|
|
1583
|
+
spaceId: prepared.spaceId,
|
|
1584
|
+
verificationMethod: prepared.verificationMethod
|
|
1585
|
+
},
|
|
1586
|
+
result.signature,
|
|
1587
|
+
keyId,
|
|
1588
|
+
jwk
|
|
1589
|
+
);
|
|
1590
|
+
}
|
|
1141
1591
|
/**
|
|
1142
1592
|
* Clear persisted session data.
|
|
1143
1593
|
*/
|
|
@@ -1229,12 +1679,14 @@ import {
|
|
|
1229
1679
|
SharingService,
|
|
1230
1680
|
UnsupportedFeatureError,
|
|
1231
1681
|
makePublicSpaceId,
|
|
1232
|
-
ACCOUNT_REGISTRY_SPACE,
|
|
1682
|
+
ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2,
|
|
1683
|
+
BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST,
|
|
1233
1684
|
BOOTSTRAP_SESSION_REQUESTS,
|
|
1234
1685
|
SECRET_RECORDS_SCHEMA,
|
|
1235
1686
|
SECRETS_SPACE as SECRETS_SPACE2,
|
|
1236
1687
|
TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
|
|
1237
1688
|
bootstrapSteps,
|
|
1689
|
+
ErrorCodes as ErrorCodes2,
|
|
1238
1690
|
ENCRYPTION_PERMISSION_SERVICE as ENCRYPTION_PERMISSION_SERVICE2,
|
|
1239
1691
|
CaveatedDelegationUnsupportedError,
|
|
1240
1692
|
PermissionNotInManifestError,
|
|
@@ -1261,7 +1713,8 @@ import {
|
|
|
1261
1713
|
parseNetworkId as parseNetworkId2,
|
|
1262
1714
|
resolveSecretPath as resolveSecretPath2,
|
|
1263
1715
|
validateOwnerSharePolicyRegistrationBytes,
|
|
1264
|
-
validateShareDeliveryAuthorizationBytes
|
|
1716
|
+
validateShareDeliveryAuthorizationBytes,
|
|
1717
|
+
verifyEip191MessageSignature
|
|
1265
1718
|
} from "@tinycloud/sdk-core";
|
|
1266
1719
|
import {
|
|
1267
1720
|
parsePermissionHint
|
|
@@ -1452,7 +1905,7 @@ function createWasmKeyProvider(sessionManager) {
|
|
|
1452
1905
|
// src/delegateToHelpers.ts
|
|
1453
1906
|
import {
|
|
1454
1907
|
parseExpiry,
|
|
1455
|
-
SiweMessage,
|
|
1908
|
+
SiweMessage as SiweMessage2,
|
|
1456
1909
|
EXPIRY as EXPIRY2
|
|
1457
1910
|
} from "@tinycloud/sdk-core";
|
|
1458
1911
|
function legacyParamsToPermissionEntries(actions, path, spaceIdOverride) {
|
|
@@ -1501,7 +1954,7 @@ function resolveExpiryMs(expiry) {
|
|
|
1501
1954
|
return parseExpiry(expiry);
|
|
1502
1955
|
}
|
|
1503
1956
|
function extractSiweExpiration(siwe) {
|
|
1504
|
-
const parsed = new
|
|
1957
|
+
const parsed = new SiweMessage2(siwe);
|
|
1505
1958
|
if (parsed.expirationTime === void 0 || parsed.expirationTime === null) {
|
|
1506
1959
|
return void 0;
|
|
1507
1960
|
}
|
|
@@ -1703,6 +2156,9 @@ var DEFAULT_HOST = "https://node.tinycloud.xyz";
|
|
|
1703
2156
|
var DEFAULT_ENCRYPTION_NETWORK_NAME = "default";
|
|
1704
2157
|
var NETWORK_CREATE_ACTION2 = ENCRYPTION2.NETWORK_CREATE;
|
|
1705
2158
|
var DECRYPT_ACTION2 = ENCRYPTION2.DECRYPT;
|
|
2159
|
+
var BOOTSTRAP_COMPLETION_MARKER_KEY = "system/bootstrap/complete";
|
|
2160
|
+
var BOOTSTRAP_COMPLETION_MARKER_VERSION = 1;
|
|
2161
|
+
var ACCEPTED_MARKER_VERSIONS = [BOOTSTRAP_COMPLETION_MARKER_VERSION];
|
|
1706
2162
|
var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
|
|
1707
2163
|
function isSecretPayload(value) {
|
|
1708
2164
|
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";
|
|
@@ -1745,6 +2201,9 @@ function decodeAuthorizationBytes(authorization) {
|
|
|
1745
2201
|
function isOpenKeyAutoSignStrategy(strategy) {
|
|
1746
2202
|
return strategy?.openKeyAutoSign === true;
|
|
1747
2203
|
}
|
|
2204
|
+
function openKeyApproval(strategy) {
|
|
2205
|
+
return strategy?.openKeyRequestApproval;
|
|
2206
|
+
}
|
|
1748
2207
|
function isInteractiveSigner(config) {
|
|
1749
2208
|
if (config.privateKey) {
|
|
1750
2209
|
return false;
|
|
@@ -2110,9 +2569,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2110
2569
|
this._bootstrapSkipped = false;
|
|
2111
2570
|
/**
|
|
2112
2571
|
* Outcome of the last signIn()'s account-bootstrap attempt. `skipped` is
|
|
2113
|
-
* true when bootstrap did not complete (interactive signer,
|
|
2114
|
-
*
|
|
2115
|
-
*
|
|
2572
|
+
* true when bootstrap did not complete in this invocation (interactive signer,
|
|
2573
|
+
* auto-bootstrap disabled, already provisioned, or a decision/provisioning
|
|
2574
|
+
* step failed). `already-provisioned` means the bootstrap was not run here;
|
|
2575
|
+
* it does not mean a prior provisioning attempt failed to complete.
|
|
2576
|
+
* `reason` carries the cause so apps can surface a "finish account setup"
|
|
2577
|
+
* call-to-action. `warnings` is
|
|
2116
2578
|
* present when bootstrap completed, but one or more steps recovered from
|
|
2117
2579
|
* an ambiguous failure (e.g. a KV batch write reconciled via per-space
|
|
2118
2580
|
* fallback) — clean vs. recovered runs are otherwise byte-identical.
|
|
@@ -2295,7 +2757,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2295
2757
|
nonce: config.nonce,
|
|
2296
2758
|
siweConfig: config.siweConfig,
|
|
2297
2759
|
manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
|
|
2298
|
-
capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
|
|
2760
|
+
capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
|
|
2299
2761
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
|
|
2300
2762
|
});
|
|
2301
2763
|
this.tc = new TinyCloud(this.auth, {
|
|
@@ -2385,6 +2847,15 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2385
2847
|
get sessionDid() {
|
|
2386
2848
|
return this.sessionManager.getDID(this.sessionKeyId);
|
|
2387
2849
|
}
|
|
2850
|
+
/** Bare did:key principal used as the credential subject and holder binding. */
|
|
2851
|
+
get credentialHolderDid() {
|
|
2852
|
+
return this.sessionDid.split("#", 1)[0];
|
|
2853
|
+
}
|
|
2854
|
+
/** Canonical verification method for the active credential holder key. */
|
|
2855
|
+
get credentialHolderKid() {
|
|
2856
|
+
const holder = this.credentialHolderDid;
|
|
2857
|
+
return `${holder}#${holder.slice("did:key:".length)}`;
|
|
2858
|
+
}
|
|
2388
2859
|
/**
|
|
2389
2860
|
* Return the current session's signed ReCap capabilities after the session
|
|
2390
2861
|
* has been authenticated or restored. This is intentionally distinct from
|
|
@@ -2435,7 +2906,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2435
2906
|
if (!this._address) {
|
|
2436
2907
|
return void 0;
|
|
2437
2908
|
}
|
|
2438
|
-
return this.wasmBindings.makeSpaceId(this._address, this._chainId,
|
|
2909
|
+
return this.wasmBindings.makeSpaceId(this._address, this._chainId, ACCOUNT_REGISTRY_SPACE2);
|
|
2439
2910
|
}
|
|
2440
2911
|
/**
|
|
2441
2912
|
* Account-level application and delegation helpers.
|
|
@@ -2538,6 +3009,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2538
3009
|
this._bootstrapSkipped = false;
|
|
2539
3010
|
this._bootstrapStatus = { skipped: false };
|
|
2540
3011
|
if (this.config.autoBootstrapAccount === false) {
|
|
3012
|
+
this._bootstrapStatus = { skipped: true, reason: "auto-bootstrap-disabled" };
|
|
2541
3013
|
return false;
|
|
2542
3014
|
}
|
|
2543
3015
|
if (!this.auth || !this._address) {
|
|
@@ -2552,11 +3024,14 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2552
3024
|
return false;
|
|
2553
3025
|
}
|
|
2554
3026
|
const steps = bootstrapSteps(this._address, this._chainId);
|
|
2555
|
-
if (!await this.isFreshBootstrapAccount(steps)) {
|
|
2556
|
-
return false;
|
|
2557
|
-
}
|
|
2558
3027
|
try {
|
|
2559
|
-
const
|
|
3028
|
+
const decision = await this.resolveBootstrapDecision(steps);
|
|
3029
|
+
if (decision.action === "skip") {
|
|
3030
|
+
this._bootstrapStatus = { skipped: true, reason: "already-provisioned" };
|
|
3031
|
+
return false;
|
|
3032
|
+
}
|
|
3033
|
+
const warnings = await this.runAccountBootstrap(steps, { mode: decision.mode });
|
|
3034
|
+
await this.writeBootstrapCompletionMarker(steps);
|
|
2560
3035
|
if (warnings.length > 0) {
|
|
2561
3036
|
this._bootstrapStatus = { skipped: false, warnings };
|
|
2562
3037
|
this.notificationHandler.warning(
|
|
@@ -2575,32 +3050,92 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2575
3050
|
}
|
|
2576
3051
|
return true;
|
|
2577
3052
|
}
|
|
2578
|
-
async
|
|
3053
|
+
async resolveBootstrapDecision(steps) {
|
|
2579
3054
|
const enshrinedSpaceIds = /* @__PURE__ */ new Set();
|
|
2580
3055
|
for (const step of steps) {
|
|
2581
3056
|
if (step.kind === "session") {
|
|
2582
3057
|
enshrinedSpaceIds.add(step.spaceId);
|
|
2583
3058
|
}
|
|
2584
3059
|
}
|
|
2585
|
-
const
|
|
3060
|
+
const auth = this.auth;
|
|
3061
|
+
if (!auth) {
|
|
3062
|
+
throw new Error("Account bootstrap requires an active wallet session");
|
|
3063
|
+
}
|
|
3064
|
+
const skipped = auth.lastActivationSkippedSpaceIds;
|
|
2586
3065
|
if (skipped.some((spaceId) => enshrinedSpaceIds.has(spaceId))) {
|
|
2587
|
-
return
|
|
3066
|
+
return { action: "run", mode: "fresh" };
|
|
2588
3067
|
}
|
|
2589
3068
|
try {
|
|
2590
|
-
const
|
|
2591
|
-
|
|
2592
|
-
|
|
3069
|
+
const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
|
|
3070
|
+
const markerPermission = {
|
|
3071
|
+
service: "tinycloud.kv",
|
|
3072
|
+
space: accountSpaceId,
|
|
3073
|
+
path: BOOTSTRAP_COMPLETION_MARKER_KEY,
|
|
3074
|
+
actions: ["get"]
|
|
3075
|
+
};
|
|
3076
|
+
if (!this.hasRuntimePermissions([markerPermission])) {
|
|
3077
|
+
return { action: "run", mode: "repair" };
|
|
2593
3078
|
}
|
|
2594
|
-
|
|
3079
|
+
const marker = await this.readBootstrapCompletionMarker();
|
|
3080
|
+
if (marker.ok) {
|
|
3081
|
+
if (this.isAcceptedBootstrapCompletionMarker(marker.data.data)) {
|
|
3082
|
+
return { action: "skip" };
|
|
3083
|
+
}
|
|
3084
|
+
return { action: "run", mode: "repair" };
|
|
3085
|
+
}
|
|
3086
|
+
if (marker.error.code === ErrorCodes2.KV_NOT_FOUND && this.markerReadIsUnhostedSpace(marker.error.meta)) {
|
|
3087
|
+
return { action: "run", mode: "fresh" };
|
|
3088
|
+
}
|
|
3089
|
+
} catch (err) {
|
|
3090
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
3091
|
+
this.notificationHandler.warning(
|
|
3092
|
+
`Account bootstrap decision failed; attempting one bounded repair: ${reason}`
|
|
3093
|
+
);
|
|
3094
|
+
console.warn(`[TinyCloudNode] account bootstrap decision failed: ${reason}`);
|
|
3095
|
+
return { action: "run", mode: "repair" };
|
|
2595
3096
|
}
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
3097
|
+
return { action: "run", mode: "repair" };
|
|
3098
|
+
}
|
|
3099
|
+
async readBootstrapCompletionMarker() {
|
|
3100
|
+
return this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).get(
|
|
3101
|
+
BOOTSTRAP_COMPLETION_MARKER_KEY
|
|
3102
|
+
);
|
|
3103
|
+
}
|
|
3104
|
+
async writeBootstrapCompletionMarker(steps) {
|
|
3105
|
+
if (!this._address) {
|
|
3106
|
+
throw new Error("Bootstrap completion marker requires an active wallet session");
|
|
2601
3107
|
}
|
|
3108
|
+
const canonicalStepIds = new Set(
|
|
3109
|
+
bootstrapSteps(this._address, this._chainId).map((step) => step.id)
|
|
3110
|
+
);
|
|
3111
|
+
const providedStepIds = new Set(steps.map((step) => step.id));
|
|
3112
|
+
if (canonicalStepIds.size !== providedStepIds.size || [...canonicalStepIds].some((stepId) => !providedStepIds.has(stepId))) {
|
|
3113
|
+
throw new Error("Bootstrap completion marker requires the canonical bootstrap step set");
|
|
3114
|
+
}
|
|
3115
|
+
const marker = {
|
|
3116
|
+
v: BOOTSTRAP_COMPLETION_MARKER_VERSION,
|
|
3117
|
+
stepIds: [...canonicalStepIds],
|
|
3118
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3119
|
+
};
|
|
3120
|
+
const written = await this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).put(
|
|
3121
|
+
BOOTSTRAP_COMPLETION_MARKER_KEY,
|
|
3122
|
+
marker
|
|
3123
|
+
);
|
|
3124
|
+
if (!written.ok) {
|
|
3125
|
+
throw new Error(`Failed to write bootstrap completion marker: ${written.error.message}`);
|
|
3126
|
+
}
|
|
3127
|
+
}
|
|
3128
|
+
isAcceptedBootstrapCompletionMarker(value) {
|
|
3129
|
+
if (typeof value !== "object" || value === null) return false;
|
|
3130
|
+
const entries = Object.entries(value);
|
|
3131
|
+
const markerValue = (name) => entries.find(([key]) => key === name)?.[1];
|
|
3132
|
+
const version = markerValue("v");
|
|
3133
|
+
return typeof version === "number" && Number.isInteger(version) && ACCEPTED_MARKER_VERSIONS.some((acceptedVersion) => acceptedVersion === version);
|
|
2602
3134
|
}
|
|
2603
|
-
|
|
3135
|
+
markerReadIsUnhostedSpace(meta) {
|
|
3136
|
+
return meta?.status === 404;
|
|
3137
|
+
}
|
|
3138
|
+
async runAccountBootstrap(steps, { mode } = { mode: "fresh" }) {
|
|
2604
3139
|
if (!this.auth || !this._address) {
|
|
2605
3140
|
throw new Error("Account bootstrap requires an active wallet session");
|
|
2606
3141
|
}
|
|
@@ -2701,9 +3236,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2701
3236
|
if (step.kind === "seed-applications") {
|
|
2702
3237
|
const registered = await this.account.applications.register(
|
|
2703
3238
|
step.manifests.length > 0 ? [...step.manifests] : TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
|
|
2704
|
-
//
|
|
2705
|
-
// manifest-hash pre-read
|
|
2706
|
-
// it guards is an INSERT OR REPLACE and is safe to repeat.
|
|
3239
|
+
// The write is an INSERT OR REPLACE and is safe to repeat during a
|
|
3240
|
+
// repair, so skip the manifest-hash pre-read.
|
|
2707
3241
|
{ assumeUnregistered: true }
|
|
2708
3242
|
);
|
|
2709
3243
|
if (!registered.ok) {
|
|
@@ -2711,7 +3245,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2711
3245
|
}
|
|
2712
3246
|
}
|
|
2713
3247
|
if (step.kind === "encryption-network-create") {
|
|
2714
|
-
await this.ensureEncryptionNetwork(step.networkId, { assumeMissing:
|
|
3248
|
+
await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: mode === "fresh" });
|
|
2715
3249
|
}
|
|
2716
3250
|
if (step.kind === "secret-records-schema") {
|
|
2717
3251
|
const db = this.sqlForSpace(step.spaceId).db(step.database);
|
|
@@ -2894,7 +3428,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
2894
3428
|
if (!this.auth || !this.signer) {
|
|
2895
3429
|
throw new Error("Manifest registry write requires wallet mode");
|
|
2896
3430
|
}
|
|
2897
|
-
const accountSpaceId = this.ownedSpaceId(
|
|
3431
|
+
const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
|
|
2898
3432
|
await this.ensureOwnedSpaceHostedById(accountSpaceId);
|
|
2899
3433
|
const result = await this.account.applications.register(request.manifests);
|
|
2900
3434
|
if (!result.ok) {
|
|
@@ -3150,6 +3684,14 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3150
3684
|
}
|
|
3151
3685
|
return hosted;
|
|
3152
3686
|
}
|
|
3687
|
+
/** Resolve and authenticate the owner encoded by an owned-space URI. */
|
|
3688
|
+
credentialSpaceOwnerDid(spaceId) {
|
|
3689
|
+
const ownerDid = this.ownerDidFromSpaceId(spaceId);
|
|
3690
|
+
if (ownerDid === void 0 || !didPrincipalMatches2(ownerDid, this.did)) {
|
|
3691
|
+
throw new Error("Credential space owner does not match the active TinyCloud owner");
|
|
3692
|
+
}
|
|
3693
|
+
return ownerDid;
|
|
3694
|
+
}
|
|
3153
3695
|
/**
|
|
3154
3696
|
* Check whether an owned space is already registered/hosted by consulting the
|
|
3155
3697
|
* account spaces registry.
|
|
@@ -3693,6 +4235,66 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3693
4235
|
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
3694
4236
|
return signBytesWithJwk(bytes, session.jwk);
|
|
3695
4237
|
}
|
|
4238
|
+
/** Apply only an already-enabled signing policy to the exact credential request. */
|
|
4239
|
+
async autoSignCredentialBytes(bytes) {
|
|
4240
|
+
const strategy = this.config.signStrategy;
|
|
4241
|
+
if (strategy?.type === "auto-sign") return this.signSessionBytes(bytes);
|
|
4242
|
+
if (strategy?.type !== "callback" || !isOpenKeyAutoSignStrategy(strategy)) return void 0;
|
|
4243
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
4244
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
4245
|
+
const request = {
|
|
4246
|
+
address: this.address ?? "",
|
|
4247
|
+
chainId: this.session?.chainId ?? this._chainId,
|
|
4248
|
+
message: new TextDecoder().decode(bytes),
|
|
4249
|
+
type: "message",
|
|
4250
|
+
purpose: "message"
|
|
4251
|
+
};
|
|
4252
|
+
const decision = await strategy.handler(request);
|
|
4253
|
+
if (!decision.approved) {
|
|
4254
|
+
if (decision.needsApproval) return void 0;
|
|
4255
|
+
throw new Error(decision.reason ?? "OpenKey automatic credential signing was rejected");
|
|
4256
|
+
}
|
|
4257
|
+
if (typeof decision.signature !== "string" || !await verifyEip191MessageSignature(
|
|
4258
|
+
request.message,
|
|
4259
|
+
decision.signature,
|
|
4260
|
+
request.address
|
|
4261
|
+
)) {
|
|
4262
|
+
throw new Error("OpenKey credential signature evidence is invalid");
|
|
4263
|
+
}
|
|
4264
|
+
return this.signSessionBytes(bytes);
|
|
4265
|
+
}
|
|
4266
|
+
/** Invoke the configured interactive approval strategy before session signing. */
|
|
4267
|
+
async approveCredentialBytes(bytes) {
|
|
4268
|
+
const strategy = this.config.signStrategy;
|
|
4269
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
4270
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
4271
|
+
const request = {
|
|
4272
|
+
address: this.address ?? "",
|
|
4273
|
+
chainId: this.session?.chainId ?? this._chainId,
|
|
4274
|
+
message: new TextDecoder().decode(bytes),
|
|
4275
|
+
type: "message",
|
|
4276
|
+
purpose: "message"
|
|
4277
|
+
};
|
|
4278
|
+
const approval = isOpenKeyAutoSignStrategy(strategy) ? openKeyApproval(strategy) : strategy?.type === "callback" ? strategy.handler : void 0;
|
|
4279
|
+
if (approval) {
|
|
4280
|
+
const decision = await approval(request);
|
|
4281
|
+
if (!decision.approved) throw new Error(decision.reason ?? "Credential signing was rejected");
|
|
4282
|
+
if (decision.signature !== void 0 && !await verifyEip191MessageSignature(
|
|
4283
|
+
request.message,
|
|
4284
|
+
decision.signature,
|
|
4285
|
+
request.address
|
|
4286
|
+
)) {
|
|
4287
|
+
throw new Error("OpenKey credential signature evidence is invalid");
|
|
4288
|
+
}
|
|
4289
|
+
return this.signSessionBytes(bytes);
|
|
4290
|
+
}
|
|
4291
|
+
if (strategy?.type === "auto-reject") throw new Error("Credential signing was rejected");
|
|
4292
|
+
if (this.config.signer) {
|
|
4293
|
+
await this.config.signer.signMessage(request.message);
|
|
4294
|
+
return this.signSessionBytes(bytes);
|
|
4295
|
+
}
|
|
4296
|
+
throw new Error("Interactive OpenKey approval is not configured");
|
|
4297
|
+
}
|
|
3696
4298
|
/** Bind a session-signed invocation to the canonical service audience. */
|
|
3697
4299
|
async bindInvocationAudience(authorization, audience) {
|
|
3698
4300
|
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
@@ -3760,7 +4362,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3760
4362
|
nonce: this.config.nonce,
|
|
3761
4363
|
siweConfig: this.config.siweConfig,
|
|
3762
4364
|
manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
|
|
3763
|
-
capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
4365
|
+
capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
3764
4366
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
|
|
3765
4367
|
});
|
|
3766
4368
|
this.tc = new TinyCloud(this.auth, {
|
|
@@ -3813,7 +4415,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
3813
4415
|
nonce: this.config.nonce,
|
|
3814
4416
|
siweConfig: this.config.siweConfig,
|
|
3815
4417
|
manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
|
|
3816
|
-
capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
4418
|
+
capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
3817
4419
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
|
|
3818
4420
|
});
|
|
3819
4421
|
this.tc = new TinyCloud(this.auth, {
|
|
@@ -6562,35 +7164,44 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
6562
7164
|
if (parentDelegation.disableSubDelegation) {
|
|
6563
7165
|
throw new Error("Parent delegation does not allow sub-delegation");
|
|
6564
7166
|
}
|
|
6565
|
-
|
|
7167
|
+
const parentResources = parentDelegation.resources ?? [{
|
|
7168
|
+
service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
|
|
7169
|
+
space: parentDelegation.spaceId,
|
|
7170
|
+
path: parentDelegation.path,
|
|
7171
|
+
actions: parentDelegation.actions
|
|
7172
|
+
}];
|
|
7173
|
+
const requestedResources = params.resources ?? [{
|
|
7174
|
+
service: params.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
|
|
7175
|
+
space: parentDelegation.spaceId,
|
|
7176
|
+
path: params.path,
|
|
7177
|
+
actions: params.actions
|
|
7178
|
+
}];
|
|
7179
|
+
const resourceIsContained = (parent, child) => {
|
|
7180
|
+
const parentEncryption = parent.service === "encryption" || parent.service === "tinycloud.encryption";
|
|
7181
|
+
const childEncryption = child.service === "encryption" || child.service === "tinycloud.encryption";
|
|
7182
|
+
if (parentEncryption || childEncryption) return parentEncryption && childEncryption && parent.path === child.path && child.actions.every((action) => parent.actions.includes(action));
|
|
7183
|
+
const pathContained = parent.path === child.path || parent.path.endsWith("/") && child.path.startsWith(parent.path) || child.path.startsWith(`${parent.path}/`);
|
|
7184
|
+
return pathContained && child.actions.every((action) => parent.actions.includes(action));
|
|
7185
|
+
};
|
|
7186
|
+
if (requestedResources.length === 0 || requestedResources.some((child) => !parentResources.some((parent) => resourceIsContained(parent, child)))) {
|
|
6566
7187
|
throw new Error(
|
|
6567
|
-
|
|
7188
|
+
"Sub-delegation resources exceed the received delegation"
|
|
6568
7189
|
);
|
|
6569
7190
|
}
|
|
6570
|
-
const parentActions = new Set(parentDelegation.actions);
|
|
6571
|
-
for (const action of params.actions) {
|
|
6572
|
-
if (!parentActions.has(action)) {
|
|
6573
|
-
throw new Error(
|
|
6574
|
-
`Sub-delegation action "${action}" is not in parent's actions: ${parentDelegation.actions.join(", ")}`
|
|
6575
|
-
);
|
|
6576
|
-
}
|
|
6577
|
-
}
|
|
6578
7191
|
const now = /* @__PURE__ */ new Date();
|
|
6579
7192
|
const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
|
|
6580
7193
|
const requestedExpiry = new Date(now.getTime() + expiryMs);
|
|
6581
7194
|
const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
|
|
6582
7195
|
const abilities = {};
|
|
6583
|
-
const
|
|
6584
|
-
const
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6590
|
-
abilities
|
|
6591
|
-
|
|
6592
|
-
if (duckdbActions.length > 0) {
|
|
6593
|
-
abilities.duckdb = { [params.path]: duckdbActions };
|
|
7196
|
+
const rawAbilities = {};
|
|
7197
|
+
for (const resource of requestedResources) {
|
|
7198
|
+
const service = resource.service.replace(/^tinycloud\./, "");
|
|
7199
|
+
if (service === "encryption") {
|
|
7200
|
+
rawAbilities[resource.path] = [...resource.actions];
|
|
7201
|
+
continue;
|
|
7202
|
+
}
|
|
7203
|
+
abilities[service] ?? (abilities[service] = {});
|
|
7204
|
+
abilities[service][resource.path] = [...resource.actions];
|
|
6594
7205
|
}
|
|
6595
7206
|
const targetHost = parentDelegation.host ?? this.config.host;
|
|
6596
7207
|
const prepared = this.wasmBindings.prepareSession({
|
|
@@ -6602,7 +7213,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
6602
7213
|
expirationTime: actualExpiry.toISOString(),
|
|
6603
7214
|
spaceId: parentDelegation.spaceId,
|
|
6604
7215
|
delegateUri: params.delegateDID,
|
|
6605
|
-
parents: [parentDelegation.cid]
|
|
7216
|
+
parents: [parentDelegation.cid],
|
|
7217
|
+
...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
|
|
6606
7218
|
});
|
|
6607
7219
|
const signature = await this.signer.signMessage(prepared.siwe);
|
|
6608
7220
|
const subDelegationSession = this.wasmBindings.completeSessionSetup({
|
|
@@ -6620,14 +7232,20 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
6620
7232
|
cid: subDelegationSession.delegationCid,
|
|
6621
7233
|
delegationHeader: subDelegationSession.delegationHeader,
|
|
6622
7234
|
spaceId: parentDelegation.spaceId,
|
|
6623
|
-
path:
|
|
6624
|
-
actions:
|
|
7235
|
+
path: requestedResources[0].path,
|
|
7236
|
+
actions: requestedResources[0].actions,
|
|
6625
7237
|
disableSubDelegation: params.disableSubDelegation ?? false,
|
|
6626
7238
|
expiry: actualExpiry,
|
|
6627
7239
|
delegateDID: params.delegateDID,
|
|
6628
7240
|
ownerAddress: parentDelegation.ownerAddress,
|
|
6629
7241
|
chainId: parentDelegation.chainId,
|
|
6630
|
-
host: targetHost
|
|
7242
|
+
host: targetHost,
|
|
7243
|
+
resources: requestedResources.map((resource) => ({
|
|
7244
|
+
service: resource.service.replace(/^tinycloud\./, ""),
|
|
7245
|
+
space: resource.service.replace(/^tinycloud\./, "") === "encryption" ? "encryption" : resource.space ?? parentDelegation.spaceId,
|
|
7246
|
+
path: resource.path,
|
|
7247
|
+
actions: [...resource.actions]
|
|
7248
|
+
}))
|
|
6631
7249
|
};
|
|
6632
7250
|
}
|
|
6633
7251
|
};
|
|
@@ -6647,7 +7265,7 @@ var TinyCloudNode = _TinyCloudNode;
|
|
|
6647
7265
|
// src/core.ts
|
|
6648
7266
|
import {
|
|
6649
7267
|
ACCOUNT_REGISTRY_PATH,
|
|
6650
|
-
ACCOUNT_REGISTRY_SPACE as
|
|
7268
|
+
ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE3,
|
|
6651
7269
|
DEFAULT_MANIFEST_SPACE as DEFAULT_MANIFEST_SPACE2,
|
|
6652
7270
|
DEFAULT_MANIFEST_VERSION,
|
|
6653
7271
|
VAULT_PERMISSION_SERVICE,
|
|
@@ -6739,7 +7357,7 @@ import {
|
|
|
6739
7357
|
import { ServiceContext as ServiceContext3 } from "@tinycloud/sdk-core";
|
|
6740
7358
|
export {
|
|
6741
7359
|
ACCOUNT_REGISTRY_PATH,
|
|
6742
|
-
|
|
7360
|
+
ACCOUNT_REGISTRY_SPACE3 as ACCOUNT_REGISTRY_SPACE,
|
|
6743
7361
|
AccountService,
|
|
6744
7362
|
AutoApproveSpaceCreationHandler2 as AutoApproveSpaceCreationHandler,
|
|
6745
7363
|
CapabilityKeyRegistry2 as CapabilityKeyRegistry,
|