@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/index.cjs
CHANGED
|
@@ -17165,7 +17165,8 @@ __export(index_exports, {
|
|
|
17165
17165
|
tinyCloudDebugLogger: () => import_sdk_core11.tinyCloudDebugLogger,
|
|
17166
17166
|
validateEnvelope: () => import_sdk_core17.validateEnvelope,
|
|
17167
17167
|
validateManifest: () => import_sdk_core12.validateManifest,
|
|
17168
|
-
verifyDecryptResponse: () => import_sdk_core17.verifyDecryptResponse
|
|
17168
|
+
verifyDecryptResponse: () => import_sdk_core17.verifyDecryptResponse,
|
|
17169
|
+
wireOpenKeyAuthorize: () => wireOpenKeyAuthorize
|
|
17169
17170
|
});
|
|
17170
17171
|
module.exports = __toCommonJS(index_exports);
|
|
17171
17172
|
|
|
@@ -17278,6 +17279,8 @@ var import_sdk_core7 = require("@tinycloud/sdk-core");
|
|
|
17278
17279
|
var import_sdk_services = require("@tinycloud/sdk-services");
|
|
17279
17280
|
|
|
17280
17281
|
// src/authorization/NodeUserAuthorization.ts
|
|
17282
|
+
var import_node_crypto = require("crypto");
|
|
17283
|
+
var import_siwe = require("siwe");
|
|
17281
17284
|
var import_sdk_core2 = require("@tinycloud/sdk-core");
|
|
17282
17285
|
|
|
17283
17286
|
// src/authorization/strategies.ts
|
|
@@ -17358,6 +17361,27 @@ var MemorySessionStorage = class {
|
|
|
17358
17361
|
// src/authorization/NodeUserAuthorization.ts
|
|
17359
17362
|
var DECRYPT_ACTION = import_sdk_core2.ENCRYPTION.DECRYPT;
|
|
17360
17363
|
var NETWORK_CREATE_ACTION = import_sdk_core2.ENCRYPTION.NETWORK_CREATE;
|
|
17364
|
+
function canonicalizeOpenKeyManifestJson(value) {
|
|
17365
|
+
const json = JSON.stringify(value);
|
|
17366
|
+
if (json === void 0) {
|
|
17367
|
+
throw new TypeError("manifest is not JSON-serializable");
|
|
17368
|
+
}
|
|
17369
|
+
return canonicalizeJsonValue(JSON.parse(json));
|
|
17370
|
+
}
|
|
17371
|
+
function canonicalizeJsonValue(value) {
|
|
17372
|
+
if (value === null || typeof value !== "object") {
|
|
17373
|
+
return JSON.stringify(value);
|
|
17374
|
+
}
|
|
17375
|
+
if (Array.isArray(value)) {
|
|
17376
|
+
return `[${value.map(canonicalizeJsonValue).join(",")}]`;
|
|
17377
|
+
}
|
|
17378
|
+
return `{${Object.keys(value).sort().map(
|
|
17379
|
+
(key2) => `${JSON.stringify(key2)}:${canonicalizeJsonValue(value[key2])}`
|
|
17380
|
+
).join(",")}}`;
|
|
17381
|
+
}
|
|
17382
|
+
function canonicalOpenKeyManifestSha256Hex(value) {
|
|
17383
|
+
return (0, import_node_crypto.createHash)("sha256").update(canonicalizeOpenKeyManifestJson(value)).digest("hex");
|
|
17384
|
+
}
|
|
17361
17385
|
function didPrincipalMatches(actual, expected) {
|
|
17362
17386
|
try {
|
|
17363
17387
|
return (0, import_sdk_core2.principalDidEquals)(actual, expected);
|
|
@@ -17578,6 +17602,15 @@ var NodeUserAuthorization = class {
|
|
|
17578
17602
|
get nodeFeatures() {
|
|
17579
17603
|
return this._nodeFeatures;
|
|
17580
17604
|
}
|
|
17605
|
+
/**
|
|
17606
|
+
* The node DID reported by `GET /info` for `host`, if the sign-in (or
|
|
17607
|
+
* restore) that populated it targeted that same host. Returns `undefined`
|
|
17608
|
+
* for any other host, and for nodes whose `/info` omits `nodeId`, so callers
|
|
17609
|
+
* fall back to fetching it themselves rather than using a foreign node's DID.
|
|
17610
|
+
*/
|
|
17611
|
+
nodeIdForHost(host) {
|
|
17612
|
+
return this._nodeInfoIdentity?.host === host ? this._nodeInfoIdentity.nodeId : void 0;
|
|
17613
|
+
}
|
|
17581
17614
|
get lastActivationSkippedSpaceIds() {
|
|
17582
17615
|
return [...this._lastActivationSkippedSpaceIds];
|
|
17583
17616
|
}
|
|
@@ -18047,11 +18080,13 @@ var NodeUserAuthorization = class {
|
|
|
18047
18080
|
this._tinyCloudSession = tinyCloudSession;
|
|
18048
18081
|
this._address = address;
|
|
18049
18082
|
this._chainId = chainId;
|
|
18083
|
+
const infoHost = this.primaryTinyCloudHost;
|
|
18050
18084
|
const nodeInfo = await (0, import_sdk_core2.checkNodeInfo)(
|
|
18051
|
-
|
|
18085
|
+
infoHost,
|
|
18052
18086
|
this.wasm.protocolVersion()
|
|
18053
18087
|
);
|
|
18054
18088
|
this._nodeFeatures = nodeInfo.features;
|
|
18089
|
+
this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
|
|
18055
18090
|
for (const ext of this.extensions) {
|
|
18056
18091
|
if (ext.afterSignIn) {
|
|
18057
18092
|
await ext.afterSignIn(clientSession);
|
|
@@ -18217,11 +18252,13 @@ var NodeUserAuthorization = class {
|
|
|
18217
18252
|
this._tinyCloudSession = tinyCloudSession;
|
|
18218
18253
|
this._address = address;
|
|
18219
18254
|
this._chainId = chainId;
|
|
18255
|
+
const infoHost = this.primaryTinyCloudHost;
|
|
18220
18256
|
const nodeInfo = await (0, import_sdk_core2.checkNodeInfo)(
|
|
18221
|
-
|
|
18257
|
+
infoHost,
|
|
18222
18258
|
this.wasm.protocolVersion()
|
|
18223
18259
|
);
|
|
18224
18260
|
this._nodeFeatures = nodeInfo.features;
|
|
18261
|
+
this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
|
|
18225
18262
|
for (const ext of this.extensions) {
|
|
18226
18263
|
if (ext.afterSignIn) {
|
|
18227
18264
|
await ext.afterSignIn(clientSession);
|
|
@@ -18230,6 +18267,398 @@ var NodeUserAuthorization = class {
|
|
|
18230
18267
|
await this.ensureSpaceExists();
|
|
18231
18268
|
return clientSession;
|
|
18232
18269
|
}
|
|
18270
|
+
/**
|
|
18271
|
+
* End-to-end sign-in via OpenKey `authorizeTinyCloud()` (Sol CRITICAL-4).
|
|
18272
|
+
*
|
|
18273
|
+
* This is the production entry point that wires
|
|
18274
|
+
* {@link prepareSessionForSigning} → OpenKey `authorizeTinyCloud()` →
|
|
18275
|
+
* {@link signInWithOpenKeyResult} into one call. Consumers pass in a
|
|
18276
|
+
* caller-provided `authorizeFn` (typically a thin wrapper around
|
|
18277
|
+
* `openkey.authorizeTinyCloud()`) so this method has no direct
|
|
18278
|
+
* dependency on the OpenKey SDK — it works with any transport that
|
|
18279
|
+
* conforms to the versioned protocol.
|
|
18280
|
+
*
|
|
18281
|
+
* The `authorizeFn` MUST:
|
|
18282
|
+
* 1. Send the prepared SIWE bytes to OpenKey.
|
|
18283
|
+
* 2. Return the exact `TinyCloudAuthorizationResultV1` shape (with
|
|
18284
|
+
* `signedMessage` — the bytes the signature actually verifies
|
|
18285
|
+
* against — plus `selectedActionKeys` and `permissions`).
|
|
18286
|
+
*
|
|
18287
|
+
* NodeUserAuthorization then completes the session with `signedMessage`
|
|
18288
|
+
* (never the prepared bytes), verifying the signature, the immutable
|
|
18289
|
+
* SIWE fields, and every subset invariant enforced by
|
|
18290
|
+
* {@link signInWithOpenKeyResult}. This preserves the exact-byte
|
|
18291
|
+
* guarantee: if the user did NOT narrow, the returned `signedMessage`
|
|
18292
|
+
* equals the prepared SIWE byte-for-byte; if the user narrowed, only
|
|
18293
|
+
* the ReCap block differs and every immutable header field survives.
|
|
18294
|
+
*
|
|
18295
|
+
* Legacy exact-byte {@link signIn} callers are unaffected — this method
|
|
18296
|
+
* is opt-in.
|
|
18297
|
+
*
|
|
18298
|
+
* @param authorizeFn Callback that invokes the versioned OpenKey
|
|
18299
|
+
* `authorizeTinyCloud()` protocol and returns the result. Receives
|
|
18300
|
+
* the prepared SIWE bytes plus a `host` hint the widget should bind
|
|
18301
|
+
* into its /authorize-sign-prepare context.
|
|
18302
|
+
* @param options Optional overrides for the OpenKey request.
|
|
18303
|
+
*/
|
|
18304
|
+
async signInWithOpenKey(authorizeFn, options) {
|
|
18305
|
+
if (options?.host !== void 0) {
|
|
18306
|
+
const overrideHost = options.host.trim();
|
|
18307
|
+
if (!overrideHost) {
|
|
18308
|
+
throw new Error("OpenKey authorization host must not be empty");
|
|
18309
|
+
}
|
|
18310
|
+
this.tinycloudHosts = [overrideHost];
|
|
18311
|
+
}
|
|
18312
|
+
const signingAddress = (0, import_sdk_core2.canonicalizeAddress)(await this.signer.getAddress());
|
|
18313
|
+
const signingChainId = await this.signer.getChainId();
|
|
18314
|
+
await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
|
|
18315
|
+
const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
|
|
18316
|
+
const presentation = this.buildPresentationEnvelope(options?.reason);
|
|
18317
|
+
const hostHint = this.primaryTinyCloudHost;
|
|
18318
|
+
const result = await authorizeFn({
|
|
18319
|
+
protocolVersion: 1,
|
|
18320
|
+
siwe: prepared.siwe,
|
|
18321
|
+
jwk,
|
|
18322
|
+
host: hostHint,
|
|
18323
|
+
keyId: options?.openkeyKeyId,
|
|
18324
|
+
presentation
|
|
18325
|
+
});
|
|
18326
|
+
return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
|
|
18327
|
+
}
|
|
18328
|
+
/**
|
|
18329
|
+
* Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
|
|
18330
|
+
* envelope is display-only — the OpenKey widget renders it under an
|
|
18331
|
+
* honest trust label, and the server /authorize-sign-prepare route
|
|
18332
|
+
* decides whether to upgrade the label to `origin-bound` by fetching
|
|
18333
|
+
* the well-known manifest and comparing its digest to
|
|
18334
|
+
* `manifestDigest`.
|
|
18335
|
+
*
|
|
18336
|
+
* Returns `undefined` when no manifest is set — the widget renders as
|
|
18337
|
+
* "no manifest supplied" (unsigned).
|
|
18338
|
+
*
|
|
18339
|
+
* The digest is a deterministic SHA-256 over canonical JSON of the
|
|
18340
|
+
* first manifest payload; OpenKey parses the fetched well-known manifest
|
|
18341
|
+
* and applies the same canonicalization before comparing digests.
|
|
18342
|
+
*/
|
|
18343
|
+
buildPresentationEnvelope(reason) {
|
|
18344
|
+
const manifests = Array.isArray(this._manifest) ? this._manifest : this._manifest ? [this._manifest] : [];
|
|
18345
|
+
if (manifests.length === 0 && !reason) {
|
|
18346
|
+
return void 0;
|
|
18347
|
+
}
|
|
18348
|
+
const primary = manifests[0];
|
|
18349
|
+
let displayName;
|
|
18350
|
+
let manifestId;
|
|
18351
|
+
let manifestDigest;
|
|
18352
|
+
const envelopeManifests = [];
|
|
18353
|
+
for (const m of manifests) {
|
|
18354
|
+
envelopeManifests.push({
|
|
18355
|
+
name: m.name,
|
|
18356
|
+
appId: m.app_id,
|
|
18357
|
+
// Deep-copy to prevent later mutations from affecting the digest.
|
|
18358
|
+
payload: JSON.parse(JSON.stringify(m))
|
|
18359
|
+
});
|
|
18360
|
+
}
|
|
18361
|
+
if (primary) {
|
|
18362
|
+
displayName = primary.name;
|
|
18363
|
+
manifestId = primary.app_id;
|
|
18364
|
+
manifestDigest = canonicalOpenKeyManifestSha256Hex(primary);
|
|
18365
|
+
}
|
|
18366
|
+
return {
|
|
18367
|
+
protocolVersion: 1,
|
|
18368
|
+
displayName,
|
|
18369
|
+
reason: reason && reason.length > 0 ? reason : void 0,
|
|
18370
|
+
manifestId,
|
|
18371
|
+
manifestDigest,
|
|
18372
|
+
manifests: envelopeManifests.length > 0 ? envelopeManifests : void 0
|
|
18373
|
+
};
|
|
18374
|
+
}
|
|
18375
|
+
/**
|
|
18376
|
+
* Complete sign-in with a versioned OpenKey authorization result.
|
|
18377
|
+
*
|
|
18378
|
+
* Where `signInWithPreparedSession` completes with a signature over the
|
|
18379
|
+
* ORIGINAL prepared SIWE, this method completes with the SIWE bytes the
|
|
18380
|
+
* OpenKey widget actually presented and signed (`signedMessage`). It
|
|
18381
|
+
* exists because the versioned authorizeTinyCloud protocol lets the user
|
|
18382
|
+
* narrow capabilities; the returned bytes may differ from the original
|
|
18383
|
+
* request and are the only ones the signature actually verifies against.
|
|
18384
|
+
*
|
|
18385
|
+
* The `result` parameter is validated at the trust boundary via
|
|
18386
|
+
* `validateAuthorizationResultV1` (in sdk-core). Callers should validate
|
|
18387
|
+
* BEFORE calling this method so unknown/missing fields are rejected
|
|
18388
|
+
* before any session state is created.
|
|
18389
|
+
*
|
|
18390
|
+
* Validation performed here (defence-in-depth: even a "valid" v1 payload
|
|
18391
|
+
* must clear these checks before any session state is created):
|
|
18392
|
+
* 1. `result.address` matches the local signer's address.
|
|
18393
|
+
* 2. `result.signature` recovers to `result.address` when verified against
|
|
18394
|
+
* `result.signedMessage`.
|
|
18395
|
+
* 3. Every immutable SIWE header field (domain, address, URI, version,
|
|
18396
|
+
* chainId, nonce, issuedAt) in the original prepared SIWE is preserved
|
|
18397
|
+
* byte-for-byte in `result.signedMessage`. `statement` is NOT in this
|
|
18398
|
+
* list because the WASM renders it from the ReCap contents; it MUST
|
|
18399
|
+
* change when the ReCap is narrowed.
|
|
18400
|
+
* 4. Every capability in `result.signedMessage`'s ReCap block was present
|
|
18401
|
+
* in the original prepared SIWE — narrowing OK, broadening rejected.
|
|
18402
|
+
* 5. Every `result.selectedActionKeys` entry corresponds to a
|
|
18403
|
+
* (resource, action) pair in the returned SIWE.
|
|
18404
|
+
*/
|
|
18405
|
+
async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
|
|
18406
|
+
if (result.protocolVersion !== 1) {
|
|
18407
|
+
throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
|
|
18408
|
+
}
|
|
18409
|
+
if (!result.signedMessage) {
|
|
18410
|
+
throw new Error("OpenKey result must include signedMessage");
|
|
18411
|
+
}
|
|
18412
|
+
if (!prepared.siwe) {
|
|
18413
|
+
throw new Error(
|
|
18414
|
+
"signInWithOpenKeyResult requires prepared.siwe (the SDK-generated SIWE the sign-in was proposed with)"
|
|
18415
|
+
);
|
|
18416
|
+
}
|
|
18417
|
+
const expectedAddress = (0, import_sdk_core2.canonicalizeAddress)(await this.signer.getAddress());
|
|
18418
|
+
if ((0, import_sdk_core2.canonicalizeAddress)(result.address) !== expectedAddress) {
|
|
18419
|
+
throw new Error(
|
|
18420
|
+
`OpenKey returned signature from ${result.address} but expected ${expectedAddress}`
|
|
18421
|
+
);
|
|
18422
|
+
}
|
|
18423
|
+
let parsedSignedSiwe;
|
|
18424
|
+
try {
|
|
18425
|
+
parsedSignedSiwe = new import_siwe.SiweMessage(result.signedMessage);
|
|
18426
|
+
} catch (err) {
|
|
18427
|
+
throw new Error(
|
|
18428
|
+
`OpenKey signedMessage is not a parseable SIWE: ${err.message}`
|
|
18429
|
+
);
|
|
18430
|
+
}
|
|
18431
|
+
const verifyResult = await parsedSignedSiwe.verify(
|
|
18432
|
+
{ signature: result.signature },
|
|
18433
|
+
{ suppressExceptions: true }
|
|
18434
|
+
);
|
|
18435
|
+
if (!verifyResult.success) {
|
|
18436
|
+
const errData = verifyResult.error;
|
|
18437
|
+
const type = errData && typeof errData === "object" && "type" in errData ? errData.type : void 0;
|
|
18438
|
+
const details = errData && typeof errData === "object" && "expected" in errData ? ` (expected=${errData.expected}, received=${errData.received})` : "";
|
|
18439
|
+
throw new Error(
|
|
18440
|
+
`OpenKey signature verification failed${type ? `: ${type}` : ""}${details}`
|
|
18441
|
+
);
|
|
18442
|
+
}
|
|
18443
|
+
if ((0, import_sdk_core2.canonicalizeAddress)(parsedSignedSiwe.address) !== expectedAddress) {
|
|
18444
|
+
throw new Error(
|
|
18445
|
+
`OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
|
|
18446
|
+
);
|
|
18447
|
+
}
|
|
18448
|
+
const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
|
|
18449
|
+
const originalFields = (0, import_sdk_core2.extractImmutableSiweFields)(prepared.siwe);
|
|
18450
|
+
const signedFields = (0, import_sdk_core2.extractImmutableSiweFields)(result.signedMessage);
|
|
18451
|
+
const changedFields = (0, import_sdk_core2.diffImmutableSiweFields)(originalFields, signedFields, {
|
|
18452
|
+
originalHasRecap
|
|
18453
|
+
});
|
|
18454
|
+
if (changedFields.length > 0) {
|
|
18455
|
+
throw new Error(
|
|
18456
|
+
`OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
|
|
18457
|
+
);
|
|
18458
|
+
}
|
|
18459
|
+
let originalCaps;
|
|
18460
|
+
let signedCaps;
|
|
18461
|
+
try {
|
|
18462
|
+
originalCaps = (0, import_sdk_core2.extractRecapAttenuations)(prepared.siwe);
|
|
18463
|
+
} catch (err) {
|
|
18464
|
+
throw new Error(
|
|
18465
|
+
`Failed to decode prepared SIWE recap: ${err.message}`
|
|
18466
|
+
);
|
|
18467
|
+
}
|
|
18468
|
+
try {
|
|
18469
|
+
signedCaps = (0, import_sdk_core2.extractRecapAttenuations)(result.signedMessage);
|
|
18470
|
+
} catch (err) {
|
|
18471
|
+
throw new Error(
|
|
18472
|
+
`Failed to decode OpenKey signedMessage recap: ${err.message}`
|
|
18473
|
+
);
|
|
18474
|
+
}
|
|
18475
|
+
const unauthorized = (0, import_sdk_core2.unauthorizedRecapCapabilities)(signedCaps, originalCaps);
|
|
18476
|
+
if (unauthorized.length > 0) {
|
|
18477
|
+
const previewLimit = 5;
|
|
18478
|
+
const preview = unauthorized.slice(0, previewLimit).map((u) => `${u.resource}::${u.action}`).join(", ");
|
|
18479
|
+
const overflow = unauthorized.length > previewLimit ? ` (and ${unauthorized.length - previewLimit} more)` : "";
|
|
18480
|
+
throw new Error(
|
|
18481
|
+
`OpenKey signedMessage broadened authorization beyond the original request: ${preview}${overflow}`
|
|
18482
|
+
);
|
|
18483
|
+
}
|
|
18484
|
+
if (!result.signedMessage.includes(prepared.spaceId)) {
|
|
18485
|
+
throw new Error(
|
|
18486
|
+
`OpenKey signedMessage does not reference the expected spaceId ${prepared.spaceId}`
|
|
18487
|
+
);
|
|
18488
|
+
}
|
|
18489
|
+
const grantedPairs = /* @__PURE__ */ new Set();
|
|
18490
|
+
const grantedFourPartIndex = /* @__PURE__ */ new Map();
|
|
18491
|
+
for (const [resource, actions] of Object.entries(signedCaps)) {
|
|
18492
|
+
const { space, path } = (0, import_sdk_core2.parseCanonicalRecapResource)(resource);
|
|
18493
|
+
for (const action of Object.keys(actions)) {
|
|
18494
|
+
grantedPairs.add(`${resource}\0${action}`);
|
|
18495
|
+
const slashIdx = action.indexOf("/");
|
|
18496
|
+
const service = slashIdx > 0 ? action.slice(0, slashIdx) : "";
|
|
18497
|
+
if (service) {
|
|
18498
|
+
grantedFourPartIndex.set(
|
|
18499
|
+
`${service}\0${space}\0${path}\0${action}`,
|
|
18500
|
+
`${resource}\0${action}`
|
|
18501
|
+
);
|
|
18502
|
+
}
|
|
18503
|
+
}
|
|
18504
|
+
}
|
|
18505
|
+
const isStructurallyRequired = (action) => action === "tinycloud.capabilities/read" || action === "capabilities/read";
|
|
18506
|
+
const nonRequiredGrantedPairs = /* @__PURE__ */ new Set();
|
|
18507
|
+
for (const pair of grantedPairs) {
|
|
18508
|
+
const idx = pair.indexOf("\0");
|
|
18509
|
+
const action = idx >= 0 ? pair.slice(idx + 1) : pair;
|
|
18510
|
+
if (!isStructurallyRequired(action)) {
|
|
18511
|
+
nonRequiredGrantedPairs.add(pair);
|
|
18512
|
+
}
|
|
18513
|
+
}
|
|
18514
|
+
const allSignedGrantedPairs = new Set(grantedPairs);
|
|
18515
|
+
const seenRawKeys = /* @__PURE__ */ new Set();
|
|
18516
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
18517
|
+
if (seenRawKeys.has(rawKey)) {
|
|
18518
|
+
throw new Error(
|
|
18519
|
+
`OpenKey selectedActionKeys contains duplicate entry ${rawKey}`
|
|
18520
|
+
);
|
|
18521
|
+
}
|
|
18522
|
+
seenRawKeys.add(rawKey);
|
|
18523
|
+
}
|
|
18524
|
+
const selectedResourceActionPairs = /* @__PURE__ */ new Set();
|
|
18525
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
18526
|
+
const parts = rawKey.split("\0");
|
|
18527
|
+
if (parts.length !== 4) {
|
|
18528
|
+
throw new Error(
|
|
18529
|
+
`OpenKey selectedActionKeys entry is malformed (expected canonical 4-part service\\0space\\0path\\0ability): ${rawKey}`
|
|
18530
|
+
);
|
|
18531
|
+
}
|
|
18532
|
+
const canonical = grantedFourPartIndex.get(rawKey);
|
|
18533
|
+
if (!canonical) {
|
|
18534
|
+
throw new Error(
|
|
18535
|
+
`OpenKey selectedActionKeys entry ${rawKey} is not covered by any granted capability`
|
|
18536
|
+
);
|
|
18537
|
+
}
|
|
18538
|
+
selectedResourceActionPairs.add(canonical);
|
|
18539
|
+
}
|
|
18540
|
+
if (nonRequiredGrantedPairs.size > 0) {
|
|
18541
|
+
const missingPairs = [];
|
|
18542
|
+
for (const pair of nonRequiredGrantedPairs) {
|
|
18543
|
+
if (!selectedResourceActionPairs.has(pair)) {
|
|
18544
|
+
missingPairs.push(pair);
|
|
18545
|
+
}
|
|
18546
|
+
}
|
|
18547
|
+
if (missingPairs.length > 0) {
|
|
18548
|
+
const previewLimit = 5;
|
|
18549
|
+
const preview = missingPairs.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
18550
|
+
const overflow = missingPairs.length > previewLimit ? ` (and ${missingPairs.length - previewLimit} more)` : "";
|
|
18551
|
+
throw new Error(
|
|
18552
|
+
`OpenKey selectedActionKeys is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
18553
|
+
);
|
|
18554
|
+
}
|
|
18555
|
+
}
|
|
18556
|
+
if (allSignedGrantedPairs.size > 0 && result.permissions.length === 0) {
|
|
18557
|
+
throw new Error(
|
|
18558
|
+
"OpenKey permissions is empty but signedMessage carries capabilities \u2014 refusing to accept a session without an authoritative grant list"
|
|
18559
|
+
);
|
|
18560
|
+
}
|
|
18561
|
+
const permissionCanonicalKeys = /* @__PURE__ */ new Set();
|
|
18562
|
+
const permissionPairsCovered = /* @__PURE__ */ new Set();
|
|
18563
|
+
for (const perm of result.permissions) {
|
|
18564
|
+
const { service, space, path, actions } = perm;
|
|
18565
|
+
if (typeof service !== "string" || typeof space !== "string" || typeof path !== "string") {
|
|
18566
|
+
throw new Error(
|
|
18567
|
+
`OpenKey permissions entry has non-string service/space/path (service=${service}, space=${space}, path=${path})`
|
|
18568
|
+
);
|
|
18569
|
+
}
|
|
18570
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
18571
|
+
throw new Error(
|
|
18572
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has empty actions \u2014 a capability-bearing entry must list at least one action`
|
|
18573
|
+
);
|
|
18574
|
+
}
|
|
18575
|
+
const canonicalKey = `${service}\0${space}\0${path}`;
|
|
18576
|
+
if (permissionCanonicalKeys.has(canonicalKey)) {
|
|
18577
|
+
throw new Error(
|
|
18578
|
+
`OpenKey permissions has duplicate entry for (${service} ${space} ${path})`
|
|
18579
|
+
);
|
|
18580
|
+
}
|
|
18581
|
+
permissionCanonicalKeys.add(canonicalKey);
|
|
18582
|
+
let expectedResource;
|
|
18583
|
+
if (space.startsWith("tinycloud:")) {
|
|
18584
|
+
const shortService = service.startsWith("tinycloud.") ? service.slice("tinycloud.".length) : service;
|
|
18585
|
+
expectedResource = path ? `${space}/${shortService}/${path}` : `${space}/${shortService}`;
|
|
18586
|
+
} else {
|
|
18587
|
+
if (path) {
|
|
18588
|
+
throw new Error(
|
|
18589
|
+
`OpenKey permissions entry (${service} ${space} ${path}) carries a path on a non-tinycloud: resource URI, which is not a recognized encoding`
|
|
18590
|
+
);
|
|
18591
|
+
}
|
|
18592
|
+
expectedResource = space;
|
|
18593
|
+
}
|
|
18594
|
+
let matchedResource;
|
|
18595
|
+
if (signedCaps[expectedResource] !== void 0) {
|
|
18596
|
+
matchedResource = expectedResource;
|
|
18597
|
+
} else {
|
|
18598
|
+
throw new Error(
|
|
18599
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims grants not confirmed in signedMessage capabilities`
|
|
18600
|
+
);
|
|
18601
|
+
}
|
|
18602
|
+
const grantedActionsForResource = new Set(
|
|
18603
|
+
Object.keys(signedCaps[matchedResource])
|
|
18604
|
+
);
|
|
18605
|
+
for (const action of actions) {
|
|
18606
|
+
if (typeof action !== "string" || !action) {
|
|
18607
|
+
throw new Error(
|
|
18608
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has non-string action ${String(action)}`
|
|
18609
|
+
);
|
|
18610
|
+
}
|
|
18611
|
+
if (!grantedActionsForResource.has(action)) {
|
|
18612
|
+
throw new Error(
|
|
18613
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims action ${action} not present in signedMessage capabilities`
|
|
18614
|
+
);
|
|
18615
|
+
}
|
|
18616
|
+
permissionPairsCovered.add(`${matchedResource}\0${action}`);
|
|
18617
|
+
}
|
|
18618
|
+
}
|
|
18619
|
+
if (allSignedGrantedPairs.size > 0) {
|
|
18620
|
+
const missingFromPerms = [];
|
|
18621
|
+
for (const pair of allSignedGrantedPairs) {
|
|
18622
|
+
if (!permissionPairsCovered.has(pair)) {
|
|
18623
|
+
missingFromPerms.push(pair);
|
|
18624
|
+
}
|
|
18625
|
+
}
|
|
18626
|
+
if (missingFromPerms.length > 0) {
|
|
18627
|
+
const previewLimit = 5;
|
|
18628
|
+
const preview = missingFromPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
18629
|
+
const overflow = missingFromPerms.length > previewLimit ? ` (and ${missingFromPerms.length - previewLimit} more)` : "";
|
|
18630
|
+
throw new Error(
|
|
18631
|
+
`OpenKey permissions is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
18632
|
+
);
|
|
18633
|
+
}
|
|
18634
|
+
const extrasInPerms = [];
|
|
18635
|
+
for (const pair of permissionPairsCovered) {
|
|
18636
|
+
if (!allSignedGrantedPairs.has(pair)) {
|
|
18637
|
+
extrasInPerms.push(pair);
|
|
18638
|
+
}
|
|
18639
|
+
}
|
|
18640
|
+
if (extrasInPerms.length > 0) {
|
|
18641
|
+
const previewLimit = 5;
|
|
18642
|
+
const preview = extrasInPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
18643
|
+
const overflow = extrasInPerms.length > previewLimit ? ` (and ${extrasInPerms.length - previewLimit} more)` : "";
|
|
18644
|
+
throw new Error(
|
|
18645
|
+
`OpenKey permissions contains entries not present in the signed authority: ${preview}${overflow}`
|
|
18646
|
+
);
|
|
18647
|
+
}
|
|
18648
|
+
}
|
|
18649
|
+
void nonRequiredGrantedPairs;
|
|
18650
|
+
return this.signInWithPreparedSession(
|
|
18651
|
+
{
|
|
18652
|
+
siwe: result.signedMessage,
|
|
18653
|
+
jwk: prepared.jwk,
|
|
18654
|
+
spaceId: prepared.spaceId,
|
|
18655
|
+
verificationMethod: prepared.verificationMethod
|
|
18656
|
+
},
|
|
18657
|
+
result.signature,
|
|
18658
|
+
keyId,
|
|
18659
|
+
jwk
|
|
18660
|
+
);
|
|
18661
|
+
}
|
|
18233
18662
|
/**
|
|
18234
18663
|
* Clear persisted session data.
|
|
18235
18664
|
*/
|
|
@@ -18913,19 +19342,87 @@ function persistedExpiry(value) {
|
|
|
18913
19342
|
function sameInstant(left, right) {
|
|
18914
19343
|
return left.getTime() === right.getTime();
|
|
18915
19344
|
}
|
|
18916
|
-
function
|
|
18917
|
-
|
|
19345
|
+
function ownerDelegationPermissions(params) {
|
|
19346
|
+
const hasPermissions = Object.prototype.hasOwnProperty.call(params, "permissions");
|
|
19347
|
+
const hasLegacy = Object.prototype.hasOwnProperty.call(params, "path") || Object.prototype.hasOwnProperty.call(params, "actions");
|
|
19348
|
+
if (hasPermissions === hasLegacy) {
|
|
19349
|
+
throw new Error("Owner delegation requires exactly one authority shape: path/actions or permissions");
|
|
19350
|
+
}
|
|
19351
|
+
const permissions = [];
|
|
19352
|
+
if (hasPermissions) {
|
|
19353
|
+
if (!Array.isArray(params.permissions)) {
|
|
19354
|
+
throw new Error("Owner delegation permissions are invalid");
|
|
19355
|
+
}
|
|
19356
|
+
permissions.push(...params.permissions.map((permission) => ({
|
|
19357
|
+
service: permission.service,
|
|
19358
|
+
path: permission.path,
|
|
19359
|
+
actions: [...permission.actions]
|
|
19360
|
+
})));
|
|
19361
|
+
} else {
|
|
19362
|
+
if (typeof params.path !== "string" || !Array.isArray(params.actions)) {
|
|
19363
|
+
throw new Error("Owner delegation requires bounded capabilities");
|
|
19364
|
+
}
|
|
19365
|
+
const byService = /* @__PURE__ */ new Map();
|
|
19366
|
+
for (const action of params.actions) {
|
|
19367
|
+
if (typeof action !== "string") {
|
|
19368
|
+
throw new Error("Owner delegation capabilities are unsupported");
|
|
19369
|
+
}
|
|
19370
|
+
const slash = action.indexOf("/");
|
|
19371
|
+
const service = slash === -1 ? "" : action.slice(0, slash);
|
|
19372
|
+
if (import_sdk_core7.SERVICE_LONG_TO_SHORT[service] === void 0) {
|
|
19373
|
+
throw new Error("Owner delegation capabilities are unsupported");
|
|
19374
|
+
}
|
|
19375
|
+
const actions = byService.get(service) ?? [];
|
|
19376
|
+
actions.push(action);
|
|
19377
|
+
byService.set(service, actions);
|
|
19378
|
+
}
|
|
19379
|
+
for (const [service, actions] of byService) {
|
|
19380
|
+
permissions.push({ service, path: params.path, actions });
|
|
19381
|
+
}
|
|
19382
|
+
}
|
|
19383
|
+
if (permissions.length === 0 || permissions.length > 16) {
|
|
19384
|
+
throw new Error("Owner delegation requires bounded capabilities");
|
|
19385
|
+
}
|
|
19386
|
+
const resources = /* @__PURE__ */ new Set();
|
|
19387
|
+
for (const permission of permissions) {
|
|
19388
|
+
if (typeof permission.service !== "string" || import_sdk_core7.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) {
|
|
19389
|
+
throw new Error("Owner delegation requires bounded capabilities");
|
|
19390
|
+
}
|
|
19391
|
+
const resourceKey = `${permission.service}\0${permission.path}`;
|
|
19392
|
+
if (resources.has(resourceKey)) {
|
|
19393
|
+
throw new Error("Owner delegation permissions contain duplicate resources");
|
|
19394
|
+
}
|
|
19395
|
+
resources.add(resourceKey);
|
|
19396
|
+
const actions = /* @__PURE__ */ new Set();
|
|
19397
|
+
for (const action of permission.actions) {
|
|
19398
|
+
if (typeof action !== "string" || !action.startsWith(`${permission.service}/`) || action.length === permission.service.length + 1 || actions.has(action)) {
|
|
19399
|
+
throw new Error("Owner delegation capabilities are unsupported");
|
|
19400
|
+
}
|
|
19401
|
+
actions.add(action);
|
|
19402
|
+
}
|
|
19403
|
+
if (permission.service === import_sdk_core7.ENCRYPTION_PERMISSION_SERVICE) {
|
|
19404
|
+
try {
|
|
19405
|
+
(0, import_sdk_core7.parseNetworkId)(permission.path);
|
|
19406
|
+
} catch {
|
|
19407
|
+
throw new Error("Owner delegation encryption permission requires a canonical network URN");
|
|
19408
|
+
}
|
|
19409
|
+
}
|
|
19410
|
+
}
|
|
19411
|
+
return permissions;
|
|
19412
|
+
}
|
|
19413
|
+
function ownerPermissionsToAbilities(permissions) {
|
|
18918
19414
|
const abilities = {};
|
|
18919
|
-
|
|
18920
|
-
|
|
18921
|
-
if (
|
|
18922
|
-
|
|
18923
|
-
|
|
19415
|
+
const rawAbilities = {};
|
|
19416
|
+
for (const permission of permissions) {
|
|
19417
|
+
if (permission.service === import_sdk_core7.ENCRYPTION_PERMISSION_SERVICE) {
|
|
19418
|
+
rawAbilities[permission.path] = [...permission.actions];
|
|
19419
|
+
continue;
|
|
19420
|
+
}
|
|
19421
|
+
const shortService = import_sdk_core7.SERVICE_LONG_TO_SHORT[permission.service];
|
|
18924
19422
|
abilities[shortService] ?? (abilities[shortService] = {});
|
|
18925
|
-
|
|
18926
|
-
abilities[shortService][path].push(action);
|
|
19423
|
+
abilities[shortService][permission.path] = [...permission.actions];
|
|
18927
19424
|
}
|
|
18928
|
-
return
|
|
19425
|
+
return { abilities, rawAbilities };
|
|
18929
19426
|
}
|
|
18930
19427
|
function base64UrlEncode(bytes) {
|
|
18931
19428
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
@@ -18962,7 +19459,9 @@ function base64UrlDecode(value) {
|
|
|
18962
19459
|
return new Uint8Array(bytes);
|
|
18963
19460
|
}
|
|
18964
19461
|
async function signJwtInputWithJwk(signingInput, jwk) {
|
|
18965
|
-
|
|
19462
|
+
return signBytesWithJwk(new TextEncoder().encode(signingInput), jwk);
|
|
19463
|
+
}
|
|
19464
|
+
async function signBytesWithJwk(bytes, jwk) {
|
|
18966
19465
|
try {
|
|
18967
19466
|
const subtle = globalThis.crypto?.subtle;
|
|
18968
19467
|
if (!subtle) {
|
|
@@ -19043,6 +19542,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19043
19542
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
19044
19543
|
this._secrets = /* @__PURE__ */ new Map();
|
|
19045
19544
|
this.runtimePermissionGrants = [];
|
|
19545
|
+
/**
|
|
19546
|
+
* Owned space ids this sign-in has already confirmed are hosted. Consulted by
|
|
19547
|
+
* {@link ensureOwnedSpaceHostedById}; cleared on every {@link signIn} because
|
|
19548
|
+
* hosting is confirmed against the session that was active at the time.
|
|
19549
|
+
*/
|
|
19550
|
+
this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
|
|
19046
19551
|
/**
|
|
19047
19552
|
* True when the last signIn() detected an interactive signer and skipped
|
|
19048
19553
|
* client-side bootstrap. Apps can read this to know whether bootstrap was
|
|
@@ -19053,7 +19558,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19053
19558
|
* Outcome of the last signIn()'s account-bootstrap attempt. `skipped` is
|
|
19054
19559
|
* true when bootstrap did not complete (interactive signer, auto-sign
|
|
19055
19560
|
* denied, or a bootstrap step failed); `reason` carries the cause so apps
|
|
19056
|
-
* can surface a "finish account setup" call-to-action.
|
|
19561
|
+
* can surface a "finish account setup" call-to-action. `warnings` is
|
|
19562
|
+
* present when bootstrap completed, but one or more steps recovered from
|
|
19563
|
+
* an ambiguous failure (e.g. a KV batch write reconciled via per-space
|
|
19564
|
+
* fallback) — clean vs. recovered runs are otherwise byte-identical.
|
|
19057
19565
|
*/
|
|
19058
19566
|
this._bootstrapStatus = {
|
|
19059
19567
|
skipped: false
|
|
@@ -19439,6 +19947,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19439
19947
|
this._spaceService = void 0;
|
|
19440
19948
|
this._serviceContext = void 0;
|
|
19441
19949
|
this.runtimePermissionGrants = [];
|
|
19950
|
+
this.confirmedHostedSpaceIds.clear();
|
|
19442
19951
|
await this.tc.signIn(options);
|
|
19443
19952
|
this.syncResolvedHostFromAuth();
|
|
19444
19953
|
const oldGraph = this._serviceGraph;
|
|
@@ -19493,7 +20002,13 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19493
20002
|
return false;
|
|
19494
20003
|
}
|
|
19495
20004
|
try {
|
|
19496
|
-
await this.runAccountBootstrap(steps);
|
|
20005
|
+
const warnings = await this.runAccountBootstrap(steps);
|
|
20006
|
+
if (warnings.length > 0) {
|
|
20007
|
+
this._bootstrapStatus = { skipped: false, warnings };
|
|
20008
|
+
this.notificationHandler.warning(
|
|
20009
|
+
`Account bootstrap completed with warnings: ${warnings.map((w) => `${w.stepId} (${w.kind}: ${w.code})`).join("; ")}`
|
|
20010
|
+
);
|
|
20011
|
+
}
|
|
19497
20012
|
} catch (err) {
|
|
19498
20013
|
const reason = err instanceof Error ? err.message : String(err);
|
|
19499
20014
|
this._bootstrapSkipped = true;
|
|
@@ -19542,6 +20057,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19542
20057
|
const auth = this.auth;
|
|
19543
20058
|
const sessions = /* @__PURE__ */ new Map();
|
|
19544
20059
|
const rawAbilitiesBySpace = /* @__PURE__ */ new Map();
|
|
20060
|
+
const warnings = [];
|
|
19545
20061
|
const primarySession = auth.tinyCloudSession;
|
|
19546
20062
|
const defaultSpaceId = this.ownedSpaceId("default");
|
|
19547
20063
|
const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default;
|
|
@@ -19603,32 +20119,45 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19603
20119
|
}
|
|
19604
20120
|
}
|
|
19605
20121
|
if (step.kind === "seed-spaces") {
|
|
19606
|
-
|
|
19607
|
-
|
|
20122
|
+
const registered = await this.account.spaces.registerBatch(
|
|
20123
|
+
step.spaces.map((space) => ({
|
|
19608
20124
|
spaceId: space.spaceId,
|
|
19609
20125
|
name: space.name,
|
|
19610
20126
|
ownerDid: this.did,
|
|
19611
20127
|
type: "owned",
|
|
19612
20128
|
permissions: ["*"],
|
|
19613
20129
|
status: "active"
|
|
20130
|
+
}))
|
|
20131
|
+
);
|
|
20132
|
+
if (!registered.ok) {
|
|
20133
|
+
throw new Error(
|
|
20134
|
+
`Failed to seed account spaces: ${registered.error.message}`
|
|
20135
|
+
);
|
|
20136
|
+
}
|
|
20137
|
+
const batchError = registered.data.recoveredFromBatchError;
|
|
20138
|
+
if (batchError) {
|
|
20139
|
+
warnings.push({
|
|
20140
|
+
stepId: step.id,
|
|
20141
|
+
kind: "batch-write-reconciled",
|
|
20142
|
+
code: batchError.code,
|
|
20143
|
+
message: batchError.message
|
|
19614
20144
|
});
|
|
19615
|
-
if (!registered.ok) {
|
|
19616
|
-
throw new Error(
|
|
19617
|
-
`Failed to seed account space ${space.spaceId}: ${registered.error.message}`
|
|
19618
|
-
);
|
|
19619
|
-
}
|
|
19620
20145
|
}
|
|
19621
20146
|
}
|
|
19622
20147
|
if (step.kind === "seed-applications") {
|
|
19623
20148
|
const registered = await this.account.applications.register(
|
|
19624
|
-
step.manifests.length > 0 ? [...step.manifests] : import_sdk_core7.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST
|
|
20149
|
+
step.manifests.length > 0 ? [...step.manifests] : import_sdk_core7.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
|
|
20150
|
+
// Bootstrap only runs on an account with no registry records, so the
|
|
20151
|
+
// manifest-hash pre-read is definitionally a miss. Skip it; the write
|
|
20152
|
+
// it guards is an INSERT OR REPLACE and is safe to repeat.
|
|
20153
|
+
{ assumeUnregistered: true }
|
|
19625
20154
|
);
|
|
19626
20155
|
if (!registered.ok) {
|
|
19627
20156
|
throw new Error(`Failed to seed bootstrap applications: ${registered.error.message}`);
|
|
19628
20157
|
}
|
|
19629
20158
|
}
|
|
19630
20159
|
if (step.kind === "encryption-network-create") {
|
|
19631
|
-
await this.ensureEncryptionNetwork(step.networkId);
|
|
20160
|
+
await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: true });
|
|
19632
20161
|
}
|
|
19633
20162
|
if (step.kind === "secret-records-schema") {
|
|
19634
20163
|
const db = this.sqlForSpace(step.spaceId).db(step.database);
|
|
@@ -19648,6 +20177,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19648
20177
|
}
|
|
19649
20178
|
}
|
|
19650
20179
|
}
|
|
20180
|
+
return warnings;
|
|
19651
20181
|
}
|
|
19652
20182
|
registerBootstrapRuntimeGrant(session, request, rawAbilities) {
|
|
19653
20183
|
const operations = [];
|
|
@@ -19907,6 +20437,22 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19907
20437
|
await this.ensureEncryptionNetwork(networkId);
|
|
19908
20438
|
}
|
|
19909
20439
|
}
|
|
20440
|
+
/**
|
|
20441
|
+
* Ensure one of this user's owned spaces is hosted, at most once per space
|
|
20442
|
+
* per sign-in.
|
|
20443
|
+
*
|
|
20444
|
+
* Account bootstrap funnels six calls through here (via
|
|
20445
|
+
* `AccountService.ensureAccountSpaceHosted`), and each one re-submits
|
|
20446
|
+
* `activateSessionWithHost` with the *primary* session. That session's recap
|
|
20447
|
+
* covers only `default` and `secrets`, so the account space appears in
|
|
20448
|
+
* neither `activated` nor `skipped` and the guard below returns immediately
|
|
20449
|
+
* — the repeat calls cannot change the outcome, they only cost round trips.
|
|
20450
|
+
*
|
|
20451
|
+
* The memo is per sign-in ({@link confirmedHostedSpaceIds} is cleared in
|
|
20452
|
+
* `signIn`) and only records confirmed successes. A wrong skip is
|
|
20453
|
+
* self-revealing: the next write to the space fails immediately and loudly
|
|
20454
|
+
* with `404 Space not found`.
|
|
20455
|
+
*/
|
|
19910
20456
|
async ensureOwnedSpaceHostedById(spaceId) {
|
|
19911
20457
|
if (!this.auth) {
|
|
19912
20458
|
throw new Error("Owned space hosting requires wallet mode");
|
|
@@ -19915,12 +20461,16 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19915
20461
|
if (!session) {
|
|
19916
20462
|
throw new Error("Owned space hosting requires an active session");
|
|
19917
20463
|
}
|
|
20464
|
+
if (this.confirmedHostedSpaceIds.has(spaceId)) {
|
|
20465
|
+
return;
|
|
20466
|
+
}
|
|
19918
20467
|
const host = this.hosts[0] ?? this.config.host;
|
|
19919
20468
|
if (!host) {
|
|
19920
20469
|
throw new Error("Owned space hosting requires a TinyCloud host");
|
|
19921
20470
|
}
|
|
19922
20471
|
const activation = await (0, import_sdk_core7.activateSessionWithHost)(host, session.delegationHeader);
|
|
19923
20472
|
if (activation.success && !activation.skipped?.includes(spaceId)) {
|
|
20473
|
+
this.confirmedHostedSpaceIds.add(spaceId);
|
|
19924
20474
|
return;
|
|
19925
20475
|
}
|
|
19926
20476
|
if (!activation.success && activation.status !== 404) {
|
|
@@ -19939,6 +20489,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19939
20489
|
`Failed to activate session after creating owned space ${spaceId}: ${retry.error ?? "space was skipped"}`
|
|
19940
20490
|
);
|
|
19941
20491
|
}
|
|
20492
|
+
this.confirmedHostedSpaceIds.add(spaceId);
|
|
19942
20493
|
}
|
|
19943
20494
|
/**
|
|
19944
20495
|
* Host one of this user's owned spaces by name (e.g. `"applications"`).
|
|
@@ -20254,6 +20805,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20254
20805
|
this._delegationManager = stagedGraph.delegationManager;
|
|
20255
20806
|
this._spaceService = stagedGraph.spaceService;
|
|
20256
20807
|
this._serviceGraph = stagedGraph.graph;
|
|
20808
|
+
this._activeServiceSession = serviceSession;
|
|
20257
20809
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
20258
20810
|
this._secrets = /* @__PURE__ */ new Map();
|
|
20259
20811
|
this._publicKV = void 0;
|
|
@@ -20261,6 +20813,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20261
20813
|
this.tc = stagedGraph.core;
|
|
20262
20814
|
this._recapOperationsCache = stagedPrimary.cache;
|
|
20263
20815
|
this.runtimePermissionGrants = stagedPrimary.grants;
|
|
20816
|
+
this.confirmedHostedSpaceIds.clear();
|
|
20264
20817
|
if (this.auth) {
|
|
20265
20818
|
this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost]);
|
|
20266
20819
|
this._restoredTcSession = void 0;
|
|
@@ -20570,6 +21123,28 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20570
21123
|
currentTinyCloudSession() {
|
|
20571
21124
|
return this.auth?.tinyCloudSession ?? this._restoredTcSession;
|
|
20572
21125
|
}
|
|
21126
|
+
/**
|
|
21127
|
+
* Mint a multi-capability invocation through the established session key.
|
|
21128
|
+
* Callers receive only the signed headers; session key material remains
|
|
21129
|
+
* owned by the session manager and is never part of the caller contract.
|
|
21130
|
+
*/
|
|
21131
|
+
invokeAny(entries, facts) {
|
|
21132
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
21133
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
21134
|
+
return this._serviceGraph.invokeAny(session, entries, facts);
|
|
21135
|
+
}
|
|
21136
|
+
/** Sign protocol bytes with the established in-memory session key. */
|
|
21137
|
+
async signSessionBytes(bytes) {
|
|
21138
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
21139
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
21140
|
+
return signBytesWithJwk(bytes, session.jwk);
|
|
21141
|
+
}
|
|
21142
|
+
/** Bind a session-signed invocation to the canonical service audience. */
|
|
21143
|
+
async bindInvocationAudience(authorization, audience) {
|
|
21144
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
21145
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
21146
|
+
return rewriteInvocationAudience(authorization, audience, session.jwk);
|
|
21147
|
+
}
|
|
20573
21148
|
/**
|
|
20574
21149
|
* Connect a wallet to upgrade from session-only mode to wallet mode.
|
|
20575
21150
|
*
|
|
@@ -20751,6 +21326,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20751
21326
|
if (this._serviceContext) {
|
|
20752
21327
|
const spaceScopedContext = this._serviceGraph.track(new import_sdk_core7.ServiceContext({
|
|
20753
21328
|
invoke: this._serviceContext.invoke,
|
|
21329
|
+
invokeAny: this._serviceContext.invokeAny,
|
|
20754
21330
|
fetch: this._serviceContext.fetch,
|
|
20755
21331
|
hosts: this._serviceContext.hosts,
|
|
20756
21332
|
telemetry: this.config.telemetry
|
|
@@ -20843,7 +21419,17 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20843
21419
|
verifyNodeSignature: (nodeId, message, signature2) => (0, import_sdk_core7.verifyDidKeyEd25519Signature)(nodeId, message, signature2)
|
|
20844
21420
|
};
|
|
20845
21421
|
}
|
|
21422
|
+
/**
|
|
21423
|
+
* The node DID for {@link config.host}.
|
|
21424
|
+
*
|
|
21425
|
+
* Sign-in already performs `GET /info` for the protocol-version check, and
|
|
21426
|
+
* that response carries `nodeId`, so reuse it rather than fetching `/info` a
|
|
21427
|
+
* second time. Only a value recorded for this exact host qualifies; older
|
|
21428
|
+
* nodes that omit `nodeId` fall through to the fetch.
|
|
21429
|
+
*/
|
|
20846
21430
|
async fetchNodeId() {
|
|
21431
|
+
const cached = this.auth?.nodeIdForHost(this.config.host);
|
|
21432
|
+
if (cached) return cached;
|
|
20847
21433
|
const response = await fetch(`${this.config.host}/info`);
|
|
20848
21434
|
if (!response.ok) {
|
|
20849
21435
|
throw new Error(`Failed to fetch node info: HTTP ${response.status}`);
|
|
@@ -21299,21 +21885,29 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21299
21885
|
async createOwnerDelegation(params, assertActive) {
|
|
21300
21886
|
const assertOwnerGraphActive = assertActive ?? this._serviceGraph.assertActive.bind(this._serviceGraph);
|
|
21301
21887
|
assertOwnerGraphActive();
|
|
21302
|
-
if (!params.delegateDid.startsWith("did:key:")
|
|
21888
|
+
if (!params.delegateDid.startsWith("did:key:")) {
|
|
21303
21889
|
throw new Error("Owner delegation requires an external did:key audience and bounded capabilities");
|
|
21304
21890
|
}
|
|
21891
|
+
const permissions = ownerDelegationPermissions(params);
|
|
21305
21892
|
const now = /* @__PURE__ */ new Date();
|
|
21306
|
-
if (params.expiresAt.getTime() <= now.getTime() || params.expiresAt.getTime() - now.getTime() > import_sdk_core7.EXPIRY.MAX_MS) {
|
|
21893
|
+
if (!(params.expiresAt instanceof Date) || params.expiresAt.getTime() <= now.getTime() || params.expiresAt.getTime() - now.getTime() > import_sdk_core7.EXPIRY.MAX_MS) {
|
|
21307
21894
|
throw new Error("Owner delegation expiry must be explicit, future, and within EXPIRY.MAX_MS");
|
|
21308
21895
|
}
|
|
21309
21896
|
if (!this.signer) throw new Error("Owner wallet signer is required");
|
|
21310
21897
|
const session = this.currentTinyCloudSession();
|
|
21311
21898
|
if (!session) throw new Error("Owner session is required");
|
|
21312
|
-
const
|
|
21313
|
-
|
|
21899
|
+
const ownerDid = (0, import_sdk_core7.pkhDid)(session.address, session.chainId);
|
|
21900
|
+
for (const permission of permissions) {
|
|
21901
|
+
if (permission.service === import_sdk_core7.ENCRYPTION_PERMISSION_SERVICE && !(0, import_sdk_core7.principalDidEquals)((0, import_sdk_core7.parseNetworkId)(permission.path).ownerDid, ownerDid)) {
|
|
21902
|
+
throw new Error("Owner delegation cannot grant a foreign encryption network");
|
|
21903
|
+
}
|
|
21904
|
+
}
|
|
21905
|
+
const { abilities, rawAbilities } = ownerPermissionsToAbilities(permissions);
|
|
21906
|
+
const primary = permissions[0];
|
|
21314
21907
|
const host = this.config.host;
|
|
21315
21908
|
const prepared = this.wasmBindings.prepareSession({
|
|
21316
21909
|
abilities,
|
|
21910
|
+
...Object.keys(rawAbilities).length === 0 ? {} : { rawAbilities },
|
|
21317
21911
|
address: this.wasmBindings.ensureEip55(session.address),
|
|
21318
21912
|
chainId: session.chainId,
|
|
21319
21913
|
domain: this.siweDomain,
|
|
@@ -21333,10 +21927,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21333
21927
|
const delegation = {
|
|
21334
21928
|
cid: delegationSession.delegationCid,
|
|
21335
21929
|
delegateDID: params.delegateDid,
|
|
21336
|
-
delegatorDID:
|
|
21930
|
+
delegatorDID: ownerDid,
|
|
21337
21931
|
spaceId: params.spaceId,
|
|
21338
|
-
path:
|
|
21339
|
-
actions: [...
|
|
21932
|
+
path: primary.path,
|
|
21933
|
+
actions: [...primary.actions],
|
|
21340
21934
|
expiry: params.expiresAt,
|
|
21341
21935
|
isRevoked: false,
|
|
21342
21936
|
allowSubDelegation: true,
|
|
@@ -21347,6 +21941,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21347
21941
|
delegation,
|
|
21348
21942
|
signedDagCbor: decodeAuthorizationBytes(delegationSession.delegationHeader.Authorization),
|
|
21349
21943
|
delegationCid: delegationSession.delegationCid,
|
|
21944
|
+
permissions,
|
|
21350
21945
|
nodeReceipt: {
|
|
21351
21946
|
commitEventCid: activation.commitEventCid,
|
|
21352
21947
|
activated: activation.activated ?? [],
|
|
@@ -21397,6 +21992,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21397
21992
|
shareUrl: input.shareUrl,
|
|
21398
21993
|
documentName: input.documentName,
|
|
21399
21994
|
jti: base64UrlEncode(crypto.getRandomValues(new Uint8Array(16))),
|
|
21995
|
+
idempotencyKey: input.idempotencyKey,
|
|
21400
21996
|
expiresAt: input.expiresAt
|
|
21401
21997
|
};
|
|
21402
21998
|
const requestBodyDigest = base64UrlEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode((0, import_sdk_core7.canonicalizeEncryptionJson)(body)))));
|
|
@@ -21606,6 +22202,13 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21606
22202
|
body
|
|
21607
22203
|
)
|
|
21608
22204
|
});
|
|
22205
|
+
if (response.status === 409) {
|
|
22206
|
+
const existing = await this.getEncryptionNetwork(networkId);
|
|
22207
|
+
if (existing) return existing;
|
|
22208
|
+
throw new Error(
|
|
22209
|
+
`Encryption network ${networkId} already exists but could not be read back`
|
|
22210
|
+
);
|
|
22211
|
+
}
|
|
21609
22212
|
if (!response.ok) {
|
|
21610
22213
|
throw new Error(
|
|
21611
22214
|
`Failed to create encryption network ${networkId}: HTTP ${response.status} ${await response.text()}`
|
|
@@ -21614,11 +22217,19 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21614
22217
|
const created = await response.json();
|
|
21615
22218
|
return created.descriptor;
|
|
21616
22219
|
}
|
|
21617
|
-
|
|
22220
|
+
/**
|
|
22221
|
+
* Ensure an encryption network exists, creating it if necessary.
|
|
22222
|
+
*
|
|
22223
|
+
* @param nameOrNetworkId - Network name or full `urn:tinycloud:encryption:` id.
|
|
22224
|
+
* @param options - See {@link EnsureEncryptionNetworkOptions}.
|
|
22225
|
+
*/
|
|
22226
|
+
async ensureEncryptionNetwork(nameOrNetworkId = DEFAULT_ENCRYPTION_NETWORK_NAME, options = {}) {
|
|
21618
22227
|
const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
|
|
21619
|
-
|
|
21620
|
-
|
|
21621
|
-
|
|
22228
|
+
if (!options.assumeMissing) {
|
|
22229
|
+
const existing = await this.getEncryptionNetwork(networkId);
|
|
22230
|
+
if (existing) {
|
|
22231
|
+
return existing;
|
|
22232
|
+
}
|
|
21622
22233
|
}
|
|
21623
22234
|
const parsed = (0, import_sdk_core7.parseNetworkId)(networkId);
|
|
21624
22235
|
if (!didPrincipalMatches2(parsed.ownerDid, this.did)) {
|
|
@@ -23609,6 +24220,50 @@ var FileSessionStorage = class {
|
|
|
23609
24220
|
}
|
|
23610
24221
|
};
|
|
23611
24222
|
|
|
24223
|
+
// src/authorization/openKeyBridge.ts
|
|
24224
|
+
function wireOpenKeyAuthorize(openkey) {
|
|
24225
|
+
return async (input) => {
|
|
24226
|
+
const result = await openkey.authorizeTinyCloud({
|
|
24227
|
+
protocolVersion: 1,
|
|
24228
|
+
siwe: input.siwe,
|
|
24229
|
+
keyId: input.keyId,
|
|
24230
|
+
jwk: input.jwk,
|
|
24231
|
+
host: input.host,
|
|
24232
|
+
// Forward the presentation envelope verbatim — the OpenKey SDK
|
|
24233
|
+
// validates and size-bounds it before use.
|
|
24234
|
+
presentation: input.presentation
|
|
24235
|
+
});
|
|
24236
|
+
if (result.protocolVersion !== 1) {
|
|
24237
|
+
throw new Error(
|
|
24238
|
+
`wireOpenKeyAuthorize: OpenKey returned unsupported protocolVersion ${result.protocolVersion}`
|
|
24239
|
+
);
|
|
24240
|
+
}
|
|
24241
|
+
if (typeof result.signedMessage !== "string" || !result.signedMessage) {
|
|
24242
|
+
throw new Error(
|
|
24243
|
+
"wireOpenKeyAuthorize: OpenKey returned no signedMessage \u2014 cannot complete a TinyCloud session"
|
|
24244
|
+
);
|
|
24245
|
+
}
|
|
24246
|
+
if (!Array.isArray(result.selectedActionKeys)) {
|
|
24247
|
+
throw new Error(
|
|
24248
|
+
"wireOpenKeyAuthorize: OpenKey returned no selectedActionKeys"
|
|
24249
|
+
);
|
|
24250
|
+
}
|
|
24251
|
+
if (!Array.isArray(result.permissions)) {
|
|
24252
|
+
throw new Error(
|
|
24253
|
+
"wireOpenKeyAuthorize: OpenKey returned no permissions"
|
|
24254
|
+
);
|
|
24255
|
+
}
|
|
24256
|
+
return {
|
|
24257
|
+
protocolVersion: 1,
|
|
24258
|
+
address: result.address,
|
|
24259
|
+
signature: result.signature,
|
|
24260
|
+
signedMessage: result.signedMessage,
|
|
24261
|
+
selectedActionKeys: result.selectedActionKeys,
|
|
24262
|
+
permissions: result.permissions
|
|
24263
|
+
};
|
|
24264
|
+
};
|
|
24265
|
+
}
|
|
24266
|
+
|
|
23612
24267
|
// src/index.ts
|
|
23613
24268
|
var import_sdk_core12 = require("@tinycloud/sdk-core");
|
|
23614
24269
|
|
|
@@ -24082,7 +24737,8 @@ var import_sdk_core23 = require("@tinycloud/sdk-core");
|
|
|
24082
24737
|
tinyCloudDebugLogger,
|
|
24083
24738
|
validateEnvelope,
|
|
24084
24739
|
validateManifest,
|
|
24085
|
-
verifyDecryptResponse
|
|
24740
|
+
verifyDecryptResponse,
|
|
24741
|
+
wireOpenKeyAuthorize
|
|
24086
24742
|
});
|
|
24087
24743
|
/*! Bundled license information:
|
|
24088
24744
|
|