@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.js
CHANGED
|
@@ -17218,6 +17218,8 @@ import {
|
|
|
17218
17218
|
} from "@tinycloud/sdk-services";
|
|
17219
17219
|
|
|
17220
17220
|
// src/authorization/NodeUserAuthorization.ts
|
|
17221
|
+
import { createHash } from "crypto";
|
|
17222
|
+
import { SiweMessage } from "siwe";
|
|
17221
17223
|
import {
|
|
17222
17224
|
fetchPeerId,
|
|
17223
17225
|
submitHostDelegation,
|
|
@@ -17236,6 +17238,11 @@ import {
|
|
|
17236
17238
|
canonicalizeAddress,
|
|
17237
17239
|
makePkhSpaceId,
|
|
17238
17240
|
pkhDid,
|
|
17241
|
+
extractImmutableSiweFields,
|
|
17242
|
+
diffImmutableSiweFields,
|
|
17243
|
+
extractRecapAttenuations,
|
|
17244
|
+
unauthorizedRecapCapabilities,
|
|
17245
|
+
parseCanonicalRecapResource,
|
|
17239
17246
|
KV,
|
|
17240
17247
|
SQL,
|
|
17241
17248
|
DUCKDB,
|
|
@@ -17322,6 +17329,27 @@ var MemorySessionStorage = class {
|
|
|
17322
17329
|
// src/authorization/NodeUserAuthorization.ts
|
|
17323
17330
|
var DECRYPT_ACTION = ENCRYPTION.DECRYPT;
|
|
17324
17331
|
var NETWORK_CREATE_ACTION = ENCRYPTION.NETWORK_CREATE;
|
|
17332
|
+
function canonicalizeOpenKeyManifestJson(value) {
|
|
17333
|
+
const json = JSON.stringify(value);
|
|
17334
|
+
if (json === void 0) {
|
|
17335
|
+
throw new TypeError("manifest is not JSON-serializable");
|
|
17336
|
+
}
|
|
17337
|
+
return canonicalizeJsonValue(JSON.parse(json));
|
|
17338
|
+
}
|
|
17339
|
+
function canonicalizeJsonValue(value) {
|
|
17340
|
+
if (value === null || typeof value !== "object") {
|
|
17341
|
+
return JSON.stringify(value);
|
|
17342
|
+
}
|
|
17343
|
+
if (Array.isArray(value)) {
|
|
17344
|
+
return `[${value.map(canonicalizeJsonValue).join(",")}]`;
|
|
17345
|
+
}
|
|
17346
|
+
return `{${Object.keys(value).sort().map(
|
|
17347
|
+
(key2) => `${JSON.stringify(key2)}:${canonicalizeJsonValue(value[key2])}`
|
|
17348
|
+
).join(",")}}`;
|
|
17349
|
+
}
|
|
17350
|
+
function canonicalOpenKeyManifestSha256Hex(value) {
|
|
17351
|
+
return createHash("sha256").update(canonicalizeOpenKeyManifestJson(value)).digest("hex");
|
|
17352
|
+
}
|
|
17325
17353
|
function didPrincipalMatches(actual, expected) {
|
|
17326
17354
|
try {
|
|
17327
17355
|
return principalDidEquals(actual, expected);
|
|
@@ -17542,6 +17570,15 @@ var NodeUserAuthorization = class {
|
|
|
17542
17570
|
get nodeFeatures() {
|
|
17543
17571
|
return this._nodeFeatures;
|
|
17544
17572
|
}
|
|
17573
|
+
/**
|
|
17574
|
+
* The node DID reported by `GET /info` for `host`, if the sign-in (or
|
|
17575
|
+
* restore) that populated it targeted that same host. Returns `undefined`
|
|
17576
|
+
* for any other host, and for nodes whose `/info` omits `nodeId`, so callers
|
|
17577
|
+
* fall back to fetching it themselves rather than using a foreign node's DID.
|
|
17578
|
+
*/
|
|
17579
|
+
nodeIdForHost(host) {
|
|
17580
|
+
return this._nodeInfoIdentity?.host === host ? this._nodeInfoIdentity.nodeId : void 0;
|
|
17581
|
+
}
|
|
17545
17582
|
get lastActivationSkippedSpaceIds() {
|
|
17546
17583
|
return [...this._lastActivationSkippedSpaceIds];
|
|
17547
17584
|
}
|
|
@@ -18011,11 +18048,13 @@ var NodeUserAuthorization = class {
|
|
|
18011
18048
|
this._tinyCloudSession = tinyCloudSession;
|
|
18012
18049
|
this._address = address;
|
|
18013
18050
|
this._chainId = chainId;
|
|
18051
|
+
const infoHost = this.primaryTinyCloudHost;
|
|
18014
18052
|
const nodeInfo = await checkNodeInfo(
|
|
18015
|
-
|
|
18053
|
+
infoHost,
|
|
18016
18054
|
this.wasm.protocolVersion()
|
|
18017
18055
|
);
|
|
18018
18056
|
this._nodeFeatures = nodeInfo.features;
|
|
18057
|
+
this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
|
|
18019
18058
|
for (const ext of this.extensions) {
|
|
18020
18059
|
if (ext.afterSignIn) {
|
|
18021
18060
|
await ext.afterSignIn(clientSession);
|
|
@@ -18181,11 +18220,13 @@ var NodeUserAuthorization = class {
|
|
|
18181
18220
|
this._tinyCloudSession = tinyCloudSession;
|
|
18182
18221
|
this._address = address;
|
|
18183
18222
|
this._chainId = chainId;
|
|
18223
|
+
const infoHost = this.primaryTinyCloudHost;
|
|
18184
18224
|
const nodeInfo = await checkNodeInfo(
|
|
18185
|
-
|
|
18225
|
+
infoHost,
|
|
18186
18226
|
this.wasm.protocolVersion()
|
|
18187
18227
|
);
|
|
18188
18228
|
this._nodeFeatures = nodeInfo.features;
|
|
18229
|
+
this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
|
|
18189
18230
|
for (const ext of this.extensions) {
|
|
18190
18231
|
if (ext.afterSignIn) {
|
|
18191
18232
|
await ext.afterSignIn(clientSession);
|
|
@@ -18194,6 +18235,398 @@ var NodeUserAuthorization = class {
|
|
|
18194
18235
|
await this.ensureSpaceExists();
|
|
18195
18236
|
return clientSession;
|
|
18196
18237
|
}
|
|
18238
|
+
/**
|
|
18239
|
+
* End-to-end sign-in via OpenKey `authorizeTinyCloud()` (Sol CRITICAL-4).
|
|
18240
|
+
*
|
|
18241
|
+
* This is the production entry point that wires
|
|
18242
|
+
* {@link prepareSessionForSigning} → OpenKey `authorizeTinyCloud()` →
|
|
18243
|
+
* {@link signInWithOpenKeyResult} into one call. Consumers pass in a
|
|
18244
|
+
* caller-provided `authorizeFn` (typically a thin wrapper around
|
|
18245
|
+
* `openkey.authorizeTinyCloud()`) so this method has no direct
|
|
18246
|
+
* dependency on the OpenKey SDK — it works with any transport that
|
|
18247
|
+
* conforms to the versioned protocol.
|
|
18248
|
+
*
|
|
18249
|
+
* The `authorizeFn` MUST:
|
|
18250
|
+
* 1. Send the prepared SIWE bytes to OpenKey.
|
|
18251
|
+
* 2. Return the exact `TinyCloudAuthorizationResultV1` shape (with
|
|
18252
|
+
* `signedMessage` — the bytes the signature actually verifies
|
|
18253
|
+
* against — plus `selectedActionKeys` and `permissions`).
|
|
18254
|
+
*
|
|
18255
|
+
* NodeUserAuthorization then completes the session with `signedMessage`
|
|
18256
|
+
* (never the prepared bytes), verifying the signature, the immutable
|
|
18257
|
+
* SIWE fields, and every subset invariant enforced by
|
|
18258
|
+
* {@link signInWithOpenKeyResult}. This preserves the exact-byte
|
|
18259
|
+
* guarantee: if the user did NOT narrow, the returned `signedMessage`
|
|
18260
|
+
* equals the prepared SIWE byte-for-byte; if the user narrowed, only
|
|
18261
|
+
* the ReCap block differs and every immutable header field survives.
|
|
18262
|
+
*
|
|
18263
|
+
* Legacy exact-byte {@link signIn} callers are unaffected — this method
|
|
18264
|
+
* is opt-in.
|
|
18265
|
+
*
|
|
18266
|
+
* @param authorizeFn Callback that invokes the versioned OpenKey
|
|
18267
|
+
* `authorizeTinyCloud()` protocol and returns the result. Receives
|
|
18268
|
+
* the prepared SIWE bytes plus a `host` hint the widget should bind
|
|
18269
|
+
* into its /authorize-sign-prepare context.
|
|
18270
|
+
* @param options Optional overrides for the OpenKey request.
|
|
18271
|
+
*/
|
|
18272
|
+
async signInWithOpenKey(authorizeFn, options) {
|
|
18273
|
+
if (options?.host !== void 0) {
|
|
18274
|
+
const overrideHost = options.host.trim();
|
|
18275
|
+
if (!overrideHost) {
|
|
18276
|
+
throw new Error("OpenKey authorization host must not be empty");
|
|
18277
|
+
}
|
|
18278
|
+
this.tinycloudHosts = [overrideHost];
|
|
18279
|
+
}
|
|
18280
|
+
const signingAddress = canonicalizeAddress(await this.signer.getAddress());
|
|
18281
|
+
const signingChainId = await this.signer.getChainId();
|
|
18282
|
+
await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
|
|
18283
|
+
const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
|
|
18284
|
+
const presentation = this.buildPresentationEnvelope(options?.reason);
|
|
18285
|
+
const hostHint = this.primaryTinyCloudHost;
|
|
18286
|
+
const result = await authorizeFn({
|
|
18287
|
+
protocolVersion: 1,
|
|
18288
|
+
siwe: prepared.siwe,
|
|
18289
|
+
jwk,
|
|
18290
|
+
host: hostHint,
|
|
18291
|
+
keyId: options?.openkeyKeyId,
|
|
18292
|
+
presentation
|
|
18293
|
+
});
|
|
18294
|
+
return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
|
|
18295
|
+
}
|
|
18296
|
+
/**
|
|
18297
|
+
* Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
|
|
18298
|
+
* envelope is display-only — the OpenKey widget renders it under an
|
|
18299
|
+
* honest trust label, and the server /authorize-sign-prepare route
|
|
18300
|
+
* decides whether to upgrade the label to `origin-bound` by fetching
|
|
18301
|
+
* the well-known manifest and comparing its digest to
|
|
18302
|
+
* `manifestDigest`.
|
|
18303
|
+
*
|
|
18304
|
+
* Returns `undefined` when no manifest is set — the widget renders as
|
|
18305
|
+
* "no manifest supplied" (unsigned).
|
|
18306
|
+
*
|
|
18307
|
+
* The digest is a deterministic SHA-256 over canonical JSON of the
|
|
18308
|
+
* first manifest payload; OpenKey parses the fetched well-known manifest
|
|
18309
|
+
* and applies the same canonicalization before comparing digests.
|
|
18310
|
+
*/
|
|
18311
|
+
buildPresentationEnvelope(reason) {
|
|
18312
|
+
const manifests = Array.isArray(this._manifest) ? this._manifest : this._manifest ? [this._manifest] : [];
|
|
18313
|
+
if (manifests.length === 0 && !reason) {
|
|
18314
|
+
return void 0;
|
|
18315
|
+
}
|
|
18316
|
+
const primary = manifests[0];
|
|
18317
|
+
let displayName;
|
|
18318
|
+
let manifestId;
|
|
18319
|
+
let manifestDigest;
|
|
18320
|
+
const envelopeManifests = [];
|
|
18321
|
+
for (const m of manifests) {
|
|
18322
|
+
envelopeManifests.push({
|
|
18323
|
+
name: m.name,
|
|
18324
|
+
appId: m.app_id,
|
|
18325
|
+
// Deep-copy to prevent later mutations from affecting the digest.
|
|
18326
|
+
payload: JSON.parse(JSON.stringify(m))
|
|
18327
|
+
});
|
|
18328
|
+
}
|
|
18329
|
+
if (primary) {
|
|
18330
|
+
displayName = primary.name;
|
|
18331
|
+
manifestId = primary.app_id;
|
|
18332
|
+
manifestDigest = canonicalOpenKeyManifestSha256Hex(primary);
|
|
18333
|
+
}
|
|
18334
|
+
return {
|
|
18335
|
+
protocolVersion: 1,
|
|
18336
|
+
displayName,
|
|
18337
|
+
reason: reason && reason.length > 0 ? reason : void 0,
|
|
18338
|
+
manifestId,
|
|
18339
|
+
manifestDigest,
|
|
18340
|
+
manifests: envelopeManifests.length > 0 ? envelopeManifests : void 0
|
|
18341
|
+
};
|
|
18342
|
+
}
|
|
18343
|
+
/**
|
|
18344
|
+
* Complete sign-in with a versioned OpenKey authorization result.
|
|
18345
|
+
*
|
|
18346
|
+
* Where `signInWithPreparedSession` completes with a signature over the
|
|
18347
|
+
* ORIGINAL prepared SIWE, this method completes with the SIWE bytes the
|
|
18348
|
+
* OpenKey widget actually presented and signed (`signedMessage`). It
|
|
18349
|
+
* exists because the versioned authorizeTinyCloud protocol lets the user
|
|
18350
|
+
* narrow capabilities; the returned bytes may differ from the original
|
|
18351
|
+
* request and are the only ones the signature actually verifies against.
|
|
18352
|
+
*
|
|
18353
|
+
* The `result` parameter is validated at the trust boundary via
|
|
18354
|
+
* `validateAuthorizationResultV1` (in sdk-core). Callers should validate
|
|
18355
|
+
* BEFORE calling this method so unknown/missing fields are rejected
|
|
18356
|
+
* before any session state is created.
|
|
18357
|
+
*
|
|
18358
|
+
* Validation performed here (defence-in-depth: even a "valid" v1 payload
|
|
18359
|
+
* must clear these checks before any session state is created):
|
|
18360
|
+
* 1. `result.address` matches the local signer's address.
|
|
18361
|
+
* 2. `result.signature` recovers to `result.address` when verified against
|
|
18362
|
+
* `result.signedMessage`.
|
|
18363
|
+
* 3. Every immutable SIWE header field (domain, address, URI, version,
|
|
18364
|
+
* chainId, nonce, issuedAt) in the original prepared SIWE is preserved
|
|
18365
|
+
* byte-for-byte in `result.signedMessage`. `statement` is NOT in this
|
|
18366
|
+
* list because the WASM renders it from the ReCap contents; it MUST
|
|
18367
|
+
* change when the ReCap is narrowed.
|
|
18368
|
+
* 4. Every capability in `result.signedMessage`'s ReCap block was present
|
|
18369
|
+
* in the original prepared SIWE — narrowing OK, broadening rejected.
|
|
18370
|
+
* 5. Every `result.selectedActionKeys` entry corresponds to a
|
|
18371
|
+
* (resource, action) pair in the returned SIWE.
|
|
18372
|
+
*/
|
|
18373
|
+
async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
|
|
18374
|
+
if (result.protocolVersion !== 1) {
|
|
18375
|
+
throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
|
|
18376
|
+
}
|
|
18377
|
+
if (!result.signedMessage) {
|
|
18378
|
+
throw new Error("OpenKey result must include signedMessage");
|
|
18379
|
+
}
|
|
18380
|
+
if (!prepared.siwe) {
|
|
18381
|
+
throw new Error(
|
|
18382
|
+
"signInWithOpenKeyResult requires prepared.siwe (the SDK-generated SIWE the sign-in was proposed with)"
|
|
18383
|
+
);
|
|
18384
|
+
}
|
|
18385
|
+
const expectedAddress = canonicalizeAddress(await this.signer.getAddress());
|
|
18386
|
+
if (canonicalizeAddress(result.address) !== expectedAddress) {
|
|
18387
|
+
throw new Error(
|
|
18388
|
+
`OpenKey returned signature from ${result.address} but expected ${expectedAddress}`
|
|
18389
|
+
);
|
|
18390
|
+
}
|
|
18391
|
+
let parsedSignedSiwe;
|
|
18392
|
+
try {
|
|
18393
|
+
parsedSignedSiwe = new SiweMessage(result.signedMessage);
|
|
18394
|
+
} catch (err) {
|
|
18395
|
+
throw new Error(
|
|
18396
|
+
`OpenKey signedMessage is not a parseable SIWE: ${err.message}`
|
|
18397
|
+
);
|
|
18398
|
+
}
|
|
18399
|
+
const verifyResult = await parsedSignedSiwe.verify(
|
|
18400
|
+
{ signature: result.signature },
|
|
18401
|
+
{ suppressExceptions: true }
|
|
18402
|
+
);
|
|
18403
|
+
if (!verifyResult.success) {
|
|
18404
|
+
const errData = verifyResult.error;
|
|
18405
|
+
const type = errData && typeof errData === "object" && "type" in errData ? errData.type : void 0;
|
|
18406
|
+
const details = errData && typeof errData === "object" && "expected" in errData ? ` (expected=${errData.expected}, received=${errData.received})` : "";
|
|
18407
|
+
throw new Error(
|
|
18408
|
+
`OpenKey signature verification failed${type ? `: ${type}` : ""}${details}`
|
|
18409
|
+
);
|
|
18410
|
+
}
|
|
18411
|
+
if (canonicalizeAddress(parsedSignedSiwe.address) !== expectedAddress) {
|
|
18412
|
+
throw new Error(
|
|
18413
|
+
`OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
|
|
18414
|
+
);
|
|
18415
|
+
}
|
|
18416
|
+
const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
|
|
18417
|
+
const originalFields = extractImmutableSiweFields(prepared.siwe);
|
|
18418
|
+
const signedFields = extractImmutableSiweFields(result.signedMessage);
|
|
18419
|
+
const changedFields = diffImmutableSiweFields(originalFields, signedFields, {
|
|
18420
|
+
originalHasRecap
|
|
18421
|
+
});
|
|
18422
|
+
if (changedFields.length > 0) {
|
|
18423
|
+
throw new Error(
|
|
18424
|
+
`OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
|
|
18425
|
+
);
|
|
18426
|
+
}
|
|
18427
|
+
let originalCaps;
|
|
18428
|
+
let signedCaps;
|
|
18429
|
+
try {
|
|
18430
|
+
originalCaps = extractRecapAttenuations(prepared.siwe);
|
|
18431
|
+
} catch (err) {
|
|
18432
|
+
throw new Error(
|
|
18433
|
+
`Failed to decode prepared SIWE recap: ${err.message}`
|
|
18434
|
+
);
|
|
18435
|
+
}
|
|
18436
|
+
try {
|
|
18437
|
+
signedCaps = extractRecapAttenuations(result.signedMessage);
|
|
18438
|
+
} catch (err) {
|
|
18439
|
+
throw new Error(
|
|
18440
|
+
`Failed to decode OpenKey signedMessage recap: ${err.message}`
|
|
18441
|
+
);
|
|
18442
|
+
}
|
|
18443
|
+
const unauthorized = unauthorizedRecapCapabilities(signedCaps, originalCaps);
|
|
18444
|
+
if (unauthorized.length > 0) {
|
|
18445
|
+
const previewLimit = 5;
|
|
18446
|
+
const preview = unauthorized.slice(0, previewLimit).map((u) => `${u.resource}::${u.action}`).join(", ");
|
|
18447
|
+
const overflow = unauthorized.length > previewLimit ? ` (and ${unauthorized.length - previewLimit} more)` : "";
|
|
18448
|
+
throw new Error(
|
|
18449
|
+
`OpenKey signedMessage broadened authorization beyond the original request: ${preview}${overflow}`
|
|
18450
|
+
);
|
|
18451
|
+
}
|
|
18452
|
+
if (!result.signedMessage.includes(prepared.spaceId)) {
|
|
18453
|
+
throw new Error(
|
|
18454
|
+
`OpenKey signedMessage does not reference the expected spaceId ${prepared.spaceId}`
|
|
18455
|
+
);
|
|
18456
|
+
}
|
|
18457
|
+
const grantedPairs = /* @__PURE__ */ new Set();
|
|
18458
|
+
const grantedFourPartIndex = /* @__PURE__ */ new Map();
|
|
18459
|
+
for (const [resource, actions] of Object.entries(signedCaps)) {
|
|
18460
|
+
const { space, path } = parseCanonicalRecapResource(resource);
|
|
18461
|
+
for (const action of Object.keys(actions)) {
|
|
18462
|
+
grantedPairs.add(`${resource}\0${action}`);
|
|
18463
|
+
const slashIdx = action.indexOf("/");
|
|
18464
|
+
const service = slashIdx > 0 ? action.slice(0, slashIdx) : "";
|
|
18465
|
+
if (service) {
|
|
18466
|
+
grantedFourPartIndex.set(
|
|
18467
|
+
`${service}\0${space}\0${path}\0${action}`,
|
|
18468
|
+
`${resource}\0${action}`
|
|
18469
|
+
);
|
|
18470
|
+
}
|
|
18471
|
+
}
|
|
18472
|
+
}
|
|
18473
|
+
const isStructurallyRequired = (action) => action === "tinycloud.capabilities/read" || action === "capabilities/read";
|
|
18474
|
+
const nonRequiredGrantedPairs = /* @__PURE__ */ new Set();
|
|
18475
|
+
for (const pair of grantedPairs) {
|
|
18476
|
+
const idx = pair.indexOf("\0");
|
|
18477
|
+
const action = idx >= 0 ? pair.slice(idx + 1) : pair;
|
|
18478
|
+
if (!isStructurallyRequired(action)) {
|
|
18479
|
+
nonRequiredGrantedPairs.add(pair);
|
|
18480
|
+
}
|
|
18481
|
+
}
|
|
18482
|
+
const allSignedGrantedPairs = new Set(grantedPairs);
|
|
18483
|
+
const seenRawKeys = /* @__PURE__ */ new Set();
|
|
18484
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
18485
|
+
if (seenRawKeys.has(rawKey)) {
|
|
18486
|
+
throw new Error(
|
|
18487
|
+
`OpenKey selectedActionKeys contains duplicate entry ${rawKey}`
|
|
18488
|
+
);
|
|
18489
|
+
}
|
|
18490
|
+
seenRawKeys.add(rawKey);
|
|
18491
|
+
}
|
|
18492
|
+
const selectedResourceActionPairs = /* @__PURE__ */ new Set();
|
|
18493
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
18494
|
+
const parts = rawKey.split("\0");
|
|
18495
|
+
if (parts.length !== 4) {
|
|
18496
|
+
throw new Error(
|
|
18497
|
+
`OpenKey selectedActionKeys entry is malformed (expected canonical 4-part service\\0space\\0path\\0ability): ${rawKey}`
|
|
18498
|
+
);
|
|
18499
|
+
}
|
|
18500
|
+
const canonical = grantedFourPartIndex.get(rawKey);
|
|
18501
|
+
if (!canonical) {
|
|
18502
|
+
throw new Error(
|
|
18503
|
+
`OpenKey selectedActionKeys entry ${rawKey} is not covered by any granted capability`
|
|
18504
|
+
);
|
|
18505
|
+
}
|
|
18506
|
+
selectedResourceActionPairs.add(canonical);
|
|
18507
|
+
}
|
|
18508
|
+
if (nonRequiredGrantedPairs.size > 0) {
|
|
18509
|
+
const missingPairs = [];
|
|
18510
|
+
for (const pair of nonRequiredGrantedPairs) {
|
|
18511
|
+
if (!selectedResourceActionPairs.has(pair)) {
|
|
18512
|
+
missingPairs.push(pair);
|
|
18513
|
+
}
|
|
18514
|
+
}
|
|
18515
|
+
if (missingPairs.length > 0) {
|
|
18516
|
+
const previewLimit = 5;
|
|
18517
|
+
const preview = missingPairs.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
18518
|
+
const overflow = missingPairs.length > previewLimit ? ` (and ${missingPairs.length - previewLimit} more)` : "";
|
|
18519
|
+
throw new Error(
|
|
18520
|
+
`OpenKey selectedActionKeys is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
18521
|
+
);
|
|
18522
|
+
}
|
|
18523
|
+
}
|
|
18524
|
+
if (allSignedGrantedPairs.size > 0 && result.permissions.length === 0) {
|
|
18525
|
+
throw new Error(
|
|
18526
|
+
"OpenKey permissions is empty but signedMessage carries capabilities \u2014 refusing to accept a session without an authoritative grant list"
|
|
18527
|
+
);
|
|
18528
|
+
}
|
|
18529
|
+
const permissionCanonicalKeys = /* @__PURE__ */ new Set();
|
|
18530
|
+
const permissionPairsCovered = /* @__PURE__ */ new Set();
|
|
18531
|
+
for (const perm of result.permissions) {
|
|
18532
|
+
const { service, space, path, actions } = perm;
|
|
18533
|
+
if (typeof service !== "string" || typeof space !== "string" || typeof path !== "string") {
|
|
18534
|
+
throw new Error(
|
|
18535
|
+
`OpenKey permissions entry has non-string service/space/path (service=${service}, space=${space}, path=${path})`
|
|
18536
|
+
);
|
|
18537
|
+
}
|
|
18538
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
18539
|
+
throw new Error(
|
|
18540
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has empty actions \u2014 a capability-bearing entry must list at least one action`
|
|
18541
|
+
);
|
|
18542
|
+
}
|
|
18543
|
+
const canonicalKey = `${service}\0${space}\0${path}`;
|
|
18544
|
+
if (permissionCanonicalKeys.has(canonicalKey)) {
|
|
18545
|
+
throw new Error(
|
|
18546
|
+
`OpenKey permissions has duplicate entry for (${service} ${space} ${path})`
|
|
18547
|
+
);
|
|
18548
|
+
}
|
|
18549
|
+
permissionCanonicalKeys.add(canonicalKey);
|
|
18550
|
+
let expectedResource;
|
|
18551
|
+
if (space.startsWith("tinycloud:")) {
|
|
18552
|
+
const shortService = service.startsWith("tinycloud.") ? service.slice("tinycloud.".length) : service;
|
|
18553
|
+
expectedResource = path ? `${space}/${shortService}/${path}` : `${space}/${shortService}`;
|
|
18554
|
+
} else {
|
|
18555
|
+
if (path) {
|
|
18556
|
+
throw new Error(
|
|
18557
|
+
`OpenKey permissions entry (${service} ${space} ${path}) carries a path on a non-tinycloud: resource URI, which is not a recognized encoding`
|
|
18558
|
+
);
|
|
18559
|
+
}
|
|
18560
|
+
expectedResource = space;
|
|
18561
|
+
}
|
|
18562
|
+
let matchedResource;
|
|
18563
|
+
if (signedCaps[expectedResource] !== void 0) {
|
|
18564
|
+
matchedResource = expectedResource;
|
|
18565
|
+
} else {
|
|
18566
|
+
throw new Error(
|
|
18567
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims grants not confirmed in signedMessage capabilities`
|
|
18568
|
+
);
|
|
18569
|
+
}
|
|
18570
|
+
const grantedActionsForResource = new Set(
|
|
18571
|
+
Object.keys(signedCaps[matchedResource])
|
|
18572
|
+
);
|
|
18573
|
+
for (const action of actions) {
|
|
18574
|
+
if (typeof action !== "string" || !action) {
|
|
18575
|
+
throw new Error(
|
|
18576
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has non-string action ${String(action)}`
|
|
18577
|
+
);
|
|
18578
|
+
}
|
|
18579
|
+
if (!grantedActionsForResource.has(action)) {
|
|
18580
|
+
throw new Error(
|
|
18581
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims action ${action} not present in signedMessage capabilities`
|
|
18582
|
+
);
|
|
18583
|
+
}
|
|
18584
|
+
permissionPairsCovered.add(`${matchedResource}\0${action}`);
|
|
18585
|
+
}
|
|
18586
|
+
}
|
|
18587
|
+
if (allSignedGrantedPairs.size > 0) {
|
|
18588
|
+
const missingFromPerms = [];
|
|
18589
|
+
for (const pair of allSignedGrantedPairs) {
|
|
18590
|
+
if (!permissionPairsCovered.has(pair)) {
|
|
18591
|
+
missingFromPerms.push(pair);
|
|
18592
|
+
}
|
|
18593
|
+
}
|
|
18594
|
+
if (missingFromPerms.length > 0) {
|
|
18595
|
+
const previewLimit = 5;
|
|
18596
|
+
const preview = missingFromPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
18597
|
+
const overflow = missingFromPerms.length > previewLimit ? ` (and ${missingFromPerms.length - previewLimit} more)` : "";
|
|
18598
|
+
throw new Error(
|
|
18599
|
+
`OpenKey permissions is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
18600
|
+
);
|
|
18601
|
+
}
|
|
18602
|
+
const extrasInPerms = [];
|
|
18603
|
+
for (const pair of permissionPairsCovered) {
|
|
18604
|
+
if (!allSignedGrantedPairs.has(pair)) {
|
|
18605
|
+
extrasInPerms.push(pair);
|
|
18606
|
+
}
|
|
18607
|
+
}
|
|
18608
|
+
if (extrasInPerms.length > 0) {
|
|
18609
|
+
const previewLimit = 5;
|
|
18610
|
+
const preview = extrasInPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
18611
|
+
const overflow = extrasInPerms.length > previewLimit ? ` (and ${extrasInPerms.length - previewLimit} more)` : "";
|
|
18612
|
+
throw new Error(
|
|
18613
|
+
`OpenKey permissions contains entries not present in the signed authority: ${preview}${overflow}`
|
|
18614
|
+
);
|
|
18615
|
+
}
|
|
18616
|
+
}
|
|
18617
|
+
void nonRequiredGrantedPairs;
|
|
18618
|
+
return this.signInWithPreparedSession(
|
|
18619
|
+
{
|
|
18620
|
+
siwe: result.signedMessage,
|
|
18621
|
+
jwk: prepared.jwk,
|
|
18622
|
+
spaceId: prepared.spaceId,
|
|
18623
|
+
verificationMethod: prepared.verificationMethod
|
|
18624
|
+
},
|
|
18625
|
+
result.signature,
|
|
18626
|
+
keyId,
|
|
18627
|
+
jwk
|
|
18628
|
+
);
|
|
18629
|
+
}
|
|
18197
18630
|
/**
|
|
18198
18631
|
* Clear persisted session data.
|
|
18199
18632
|
*/
|
|
@@ -18449,7 +18882,7 @@ function createWasmKeyProvider(sessionManager) {
|
|
|
18449
18882
|
// src/delegateToHelpers.ts
|
|
18450
18883
|
import {
|
|
18451
18884
|
parseExpiry,
|
|
18452
|
-
SiweMessage,
|
|
18885
|
+
SiweMessage as SiweMessage2,
|
|
18453
18886
|
EXPIRY as EXPIRY2
|
|
18454
18887
|
} from "@tinycloud/sdk-core";
|
|
18455
18888
|
function legacyParamsToPermissionEntries(actions, path, spaceIdOverride) {
|
|
@@ -18498,7 +18931,7 @@ function resolveExpiryMs(expiry) {
|
|
|
18498
18931
|
return parseExpiry(expiry);
|
|
18499
18932
|
}
|
|
18500
18933
|
function extractSiweExpiration(siwe) {
|
|
18501
|
-
const parsed = new
|
|
18934
|
+
const parsed = new SiweMessage2(siwe);
|
|
18502
18935
|
if (parsed.expirationTime === void 0 || parsed.expirationTime === null) {
|
|
18503
18936
|
return void 0;
|
|
18504
18937
|
}
|
|
@@ -18893,19 +19326,87 @@ function persistedExpiry(value) {
|
|
|
18893
19326
|
function sameInstant(left, right) {
|
|
18894
19327
|
return left.getTime() === right.getTime();
|
|
18895
19328
|
}
|
|
18896
|
-
function
|
|
18897
|
-
|
|
19329
|
+
function ownerDelegationPermissions(params) {
|
|
19330
|
+
const hasPermissions = Object.prototype.hasOwnProperty.call(params, "permissions");
|
|
19331
|
+
const hasLegacy = Object.prototype.hasOwnProperty.call(params, "path") || Object.prototype.hasOwnProperty.call(params, "actions");
|
|
19332
|
+
if (hasPermissions === hasLegacy) {
|
|
19333
|
+
throw new Error("Owner delegation requires exactly one authority shape: path/actions or permissions");
|
|
19334
|
+
}
|
|
19335
|
+
const permissions = [];
|
|
19336
|
+
if (hasPermissions) {
|
|
19337
|
+
if (!Array.isArray(params.permissions)) {
|
|
19338
|
+
throw new Error("Owner delegation permissions are invalid");
|
|
19339
|
+
}
|
|
19340
|
+
permissions.push(...params.permissions.map((permission) => ({
|
|
19341
|
+
service: permission.service,
|
|
19342
|
+
path: permission.path,
|
|
19343
|
+
actions: [...permission.actions]
|
|
19344
|
+
})));
|
|
19345
|
+
} else {
|
|
19346
|
+
if (typeof params.path !== "string" || !Array.isArray(params.actions)) {
|
|
19347
|
+
throw new Error("Owner delegation requires bounded capabilities");
|
|
19348
|
+
}
|
|
19349
|
+
const byService = /* @__PURE__ */ new Map();
|
|
19350
|
+
for (const action of params.actions) {
|
|
19351
|
+
if (typeof action !== "string") {
|
|
19352
|
+
throw new Error("Owner delegation capabilities are unsupported");
|
|
19353
|
+
}
|
|
19354
|
+
const slash = action.indexOf("/");
|
|
19355
|
+
const service = slash === -1 ? "" : action.slice(0, slash);
|
|
19356
|
+
if (SERVICE_LONG_TO_SHORT[service] === void 0) {
|
|
19357
|
+
throw new Error("Owner delegation capabilities are unsupported");
|
|
19358
|
+
}
|
|
19359
|
+
const actions = byService.get(service) ?? [];
|
|
19360
|
+
actions.push(action);
|
|
19361
|
+
byService.set(service, actions);
|
|
19362
|
+
}
|
|
19363
|
+
for (const [service, actions] of byService) {
|
|
19364
|
+
permissions.push({ service, path: params.path, actions });
|
|
19365
|
+
}
|
|
19366
|
+
}
|
|
19367
|
+
if (permissions.length === 0 || permissions.length > 16) {
|
|
19368
|
+
throw new Error("Owner delegation requires bounded capabilities");
|
|
19369
|
+
}
|
|
19370
|
+
const resources = /* @__PURE__ */ new Set();
|
|
19371
|
+
for (const permission of permissions) {
|
|
19372
|
+
if (typeof permission.service !== "string" || 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) {
|
|
19373
|
+
throw new Error("Owner delegation requires bounded capabilities");
|
|
19374
|
+
}
|
|
19375
|
+
const resourceKey = `${permission.service}\0${permission.path}`;
|
|
19376
|
+
if (resources.has(resourceKey)) {
|
|
19377
|
+
throw new Error("Owner delegation permissions contain duplicate resources");
|
|
19378
|
+
}
|
|
19379
|
+
resources.add(resourceKey);
|
|
19380
|
+
const actions = /* @__PURE__ */ new Set();
|
|
19381
|
+
for (const action of permission.actions) {
|
|
19382
|
+
if (typeof action !== "string" || !action.startsWith(`${permission.service}/`) || action.length === permission.service.length + 1 || actions.has(action)) {
|
|
19383
|
+
throw new Error("Owner delegation capabilities are unsupported");
|
|
19384
|
+
}
|
|
19385
|
+
actions.add(action);
|
|
19386
|
+
}
|
|
19387
|
+
if (permission.service === ENCRYPTION_PERMISSION_SERVICE2) {
|
|
19388
|
+
try {
|
|
19389
|
+
parseNetworkId2(permission.path);
|
|
19390
|
+
} catch {
|
|
19391
|
+
throw new Error("Owner delegation encryption permission requires a canonical network URN");
|
|
19392
|
+
}
|
|
19393
|
+
}
|
|
19394
|
+
}
|
|
19395
|
+
return permissions;
|
|
19396
|
+
}
|
|
19397
|
+
function ownerPermissionsToAbilities(permissions) {
|
|
18898
19398
|
const abilities = {};
|
|
18899
|
-
|
|
18900
|
-
|
|
18901
|
-
if (
|
|
18902
|
-
|
|
18903
|
-
|
|
19399
|
+
const rawAbilities = {};
|
|
19400
|
+
for (const permission of permissions) {
|
|
19401
|
+
if (permission.service === ENCRYPTION_PERMISSION_SERVICE2) {
|
|
19402
|
+
rawAbilities[permission.path] = [...permission.actions];
|
|
19403
|
+
continue;
|
|
19404
|
+
}
|
|
19405
|
+
const shortService = SERVICE_LONG_TO_SHORT[permission.service];
|
|
18904
19406
|
abilities[shortService] ?? (abilities[shortService] = {});
|
|
18905
|
-
|
|
18906
|
-
abilities[shortService][path].push(action);
|
|
19407
|
+
abilities[shortService][permission.path] = [...permission.actions];
|
|
18907
19408
|
}
|
|
18908
|
-
return
|
|
19409
|
+
return { abilities, rawAbilities };
|
|
18909
19410
|
}
|
|
18910
19411
|
function base64UrlEncode(bytes) {
|
|
18911
19412
|
const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
@@ -18942,7 +19443,9 @@ function base64UrlDecode(value) {
|
|
|
18942
19443
|
return new Uint8Array(bytes);
|
|
18943
19444
|
}
|
|
18944
19445
|
async function signJwtInputWithJwk(signingInput, jwk) {
|
|
18945
|
-
|
|
19446
|
+
return signBytesWithJwk(new TextEncoder().encode(signingInput), jwk);
|
|
19447
|
+
}
|
|
19448
|
+
async function signBytesWithJwk(bytes, jwk) {
|
|
18946
19449
|
try {
|
|
18947
19450
|
const subtle = globalThis.crypto?.subtle;
|
|
18948
19451
|
if (!subtle) {
|
|
@@ -19023,6 +19526,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19023
19526
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
19024
19527
|
this._secrets = /* @__PURE__ */ new Map();
|
|
19025
19528
|
this.runtimePermissionGrants = [];
|
|
19529
|
+
/**
|
|
19530
|
+
* Owned space ids this sign-in has already confirmed are hosted. Consulted by
|
|
19531
|
+
* {@link ensureOwnedSpaceHostedById}; cleared on every {@link signIn} because
|
|
19532
|
+
* hosting is confirmed against the session that was active at the time.
|
|
19533
|
+
*/
|
|
19534
|
+
this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
|
|
19026
19535
|
/**
|
|
19027
19536
|
* True when the last signIn() detected an interactive signer and skipped
|
|
19028
19537
|
* client-side bootstrap. Apps can read this to know whether bootstrap was
|
|
@@ -19033,7 +19542,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19033
19542
|
* Outcome of the last signIn()'s account-bootstrap attempt. `skipped` is
|
|
19034
19543
|
* true when bootstrap did not complete (interactive signer, auto-sign
|
|
19035
19544
|
* denied, or a bootstrap step failed); `reason` carries the cause so apps
|
|
19036
|
-
* can surface a "finish account setup" call-to-action.
|
|
19545
|
+
* can surface a "finish account setup" call-to-action. `warnings` is
|
|
19546
|
+
* present when bootstrap completed, but one or more steps recovered from
|
|
19547
|
+
* an ambiguous failure (e.g. a KV batch write reconciled via per-space
|
|
19548
|
+
* fallback) — clean vs. recovered runs are otherwise byte-identical.
|
|
19037
19549
|
*/
|
|
19038
19550
|
this._bootstrapStatus = {
|
|
19039
19551
|
skipped: false
|
|
@@ -19419,6 +19931,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19419
19931
|
this._spaceService = void 0;
|
|
19420
19932
|
this._serviceContext = void 0;
|
|
19421
19933
|
this.runtimePermissionGrants = [];
|
|
19934
|
+
this.confirmedHostedSpaceIds.clear();
|
|
19422
19935
|
await this.tc.signIn(options);
|
|
19423
19936
|
this.syncResolvedHostFromAuth();
|
|
19424
19937
|
const oldGraph = this._serviceGraph;
|
|
@@ -19473,7 +19986,13 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19473
19986
|
return false;
|
|
19474
19987
|
}
|
|
19475
19988
|
try {
|
|
19476
|
-
await this.runAccountBootstrap(steps);
|
|
19989
|
+
const warnings = await this.runAccountBootstrap(steps);
|
|
19990
|
+
if (warnings.length > 0) {
|
|
19991
|
+
this._bootstrapStatus = { skipped: false, warnings };
|
|
19992
|
+
this.notificationHandler.warning(
|
|
19993
|
+
`Account bootstrap completed with warnings: ${warnings.map((w) => `${w.stepId} (${w.kind}: ${w.code})`).join("; ")}`
|
|
19994
|
+
);
|
|
19995
|
+
}
|
|
19477
19996
|
} catch (err) {
|
|
19478
19997
|
const reason = err instanceof Error ? err.message : String(err);
|
|
19479
19998
|
this._bootstrapSkipped = true;
|
|
@@ -19522,6 +20041,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19522
20041
|
const auth = this.auth;
|
|
19523
20042
|
const sessions = /* @__PURE__ */ new Map();
|
|
19524
20043
|
const rawAbilitiesBySpace = /* @__PURE__ */ new Map();
|
|
20044
|
+
const warnings = [];
|
|
19525
20045
|
const primarySession = auth.tinyCloudSession;
|
|
19526
20046
|
const defaultSpaceId = this.ownedSpaceId("default");
|
|
19527
20047
|
const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === BOOTSTRAP_SESSION_REQUESTS.default;
|
|
@@ -19583,32 +20103,45 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19583
20103
|
}
|
|
19584
20104
|
}
|
|
19585
20105
|
if (step.kind === "seed-spaces") {
|
|
19586
|
-
|
|
19587
|
-
|
|
20106
|
+
const registered = await this.account.spaces.registerBatch(
|
|
20107
|
+
step.spaces.map((space) => ({
|
|
19588
20108
|
spaceId: space.spaceId,
|
|
19589
20109
|
name: space.name,
|
|
19590
20110
|
ownerDid: this.did,
|
|
19591
20111
|
type: "owned",
|
|
19592
20112
|
permissions: ["*"],
|
|
19593
20113
|
status: "active"
|
|
20114
|
+
}))
|
|
20115
|
+
);
|
|
20116
|
+
if (!registered.ok) {
|
|
20117
|
+
throw new Error(
|
|
20118
|
+
`Failed to seed account spaces: ${registered.error.message}`
|
|
20119
|
+
);
|
|
20120
|
+
}
|
|
20121
|
+
const batchError = registered.data.recoveredFromBatchError;
|
|
20122
|
+
if (batchError) {
|
|
20123
|
+
warnings.push({
|
|
20124
|
+
stepId: step.id,
|
|
20125
|
+
kind: "batch-write-reconciled",
|
|
20126
|
+
code: batchError.code,
|
|
20127
|
+
message: batchError.message
|
|
19594
20128
|
});
|
|
19595
|
-
if (!registered.ok) {
|
|
19596
|
-
throw new Error(
|
|
19597
|
-
`Failed to seed account space ${space.spaceId}: ${registered.error.message}`
|
|
19598
|
-
);
|
|
19599
|
-
}
|
|
19600
20129
|
}
|
|
19601
20130
|
}
|
|
19602
20131
|
if (step.kind === "seed-applications") {
|
|
19603
20132
|
const registered = await this.account.applications.register(
|
|
19604
|
-
step.manifests.length > 0 ? [...step.manifests] : TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST
|
|
20133
|
+
step.manifests.length > 0 ? [...step.manifests] : TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
|
|
20134
|
+
// Bootstrap only runs on an account with no registry records, so the
|
|
20135
|
+
// manifest-hash pre-read is definitionally a miss. Skip it; the write
|
|
20136
|
+
// it guards is an INSERT OR REPLACE and is safe to repeat.
|
|
20137
|
+
{ assumeUnregistered: true }
|
|
19605
20138
|
);
|
|
19606
20139
|
if (!registered.ok) {
|
|
19607
20140
|
throw new Error(`Failed to seed bootstrap applications: ${registered.error.message}`);
|
|
19608
20141
|
}
|
|
19609
20142
|
}
|
|
19610
20143
|
if (step.kind === "encryption-network-create") {
|
|
19611
|
-
await this.ensureEncryptionNetwork(step.networkId);
|
|
20144
|
+
await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: true });
|
|
19612
20145
|
}
|
|
19613
20146
|
if (step.kind === "secret-records-schema") {
|
|
19614
20147
|
const db = this.sqlForSpace(step.spaceId).db(step.database);
|
|
@@ -19628,6 +20161,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19628
20161
|
}
|
|
19629
20162
|
}
|
|
19630
20163
|
}
|
|
20164
|
+
return warnings;
|
|
19631
20165
|
}
|
|
19632
20166
|
registerBootstrapRuntimeGrant(session, request, rawAbilities) {
|
|
19633
20167
|
const operations = [];
|
|
@@ -19887,6 +20421,22 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19887
20421
|
await this.ensureEncryptionNetwork(networkId);
|
|
19888
20422
|
}
|
|
19889
20423
|
}
|
|
20424
|
+
/**
|
|
20425
|
+
* Ensure one of this user's owned spaces is hosted, at most once per space
|
|
20426
|
+
* per sign-in.
|
|
20427
|
+
*
|
|
20428
|
+
* Account bootstrap funnels six calls through here (via
|
|
20429
|
+
* `AccountService.ensureAccountSpaceHosted`), and each one re-submits
|
|
20430
|
+
* `activateSessionWithHost` with the *primary* session. That session's recap
|
|
20431
|
+
* covers only `default` and `secrets`, so the account space appears in
|
|
20432
|
+
* neither `activated` nor `skipped` and the guard below returns immediately
|
|
20433
|
+
* — the repeat calls cannot change the outcome, they only cost round trips.
|
|
20434
|
+
*
|
|
20435
|
+
* The memo is per sign-in ({@link confirmedHostedSpaceIds} is cleared in
|
|
20436
|
+
* `signIn`) and only records confirmed successes. A wrong skip is
|
|
20437
|
+
* self-revealing: the next write to the space fails immediately and loudly
|
|
20438
|
+
* with `404 Space not found`.
|
|
20439
|
+
*/
|
|
19890
20440
|
async ensureOwnedSpaceHostedById(spaceId) {
|
|
19891
20441
|
if (!this.auth) {
|
|
19892
20442
|
throw new Error("Owned space hosting requires wallet mode");
|
|
@@ -19895,12 +20445,16 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19895
20445
|
if (!session) {
|
|
19896
20446
|
throw new Error("Owned space hosting requires an active session");
|
|
19897
20447
|
}
|
|
20448
|
+
if (this.confirmedHostedSpaceIds.has(spaceId)) {
|
|
20449
|
+
return;
|
|
20450
|
+
}
|
|
19898
20451
|
const host = this.hosts[0] ?? this.config.host;
|
|
19899
20452
|
if (!host) {
|
|
19900
20453
|
throw new Error("Owned space hosting requires a TinyCloud host");
|
|
19901
20454
|
}
|
|
19902
20455
|
const activation = await activateSessionWithHost2(host, session.delegationHeader);
|
|
19903
20456
|
if (activation.success && !activation.skipped?.includes(spaceId)) {
|
|
20457
|
+
this.confirmedHostedSpaceIds.add(spaceId);
|
|
19904
20458
|
return;
|
|
19905
20459
|
}
|
|
19906
20460
|
if (!activation.success && activation.status !== 404) {
|
|
@@ -19919,6 +20473,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19919
20473
|
`Failed to activate session after creating owned space ${spaceId}: ${retry.error ?? "space was skipped"}`
|
|
19920
20474
|
);
|
|
19921
20475
|
}
|
|
20476
|
+
this.confirmedHostedSpaceIds.add(spaceId);
|
|
19922
20477
|
}
|
|
19923
20478
|
/**
|
|
19924
20479
|
* Host one of this user's owned spaces by name (e.g. `"applications"`).
|
|
@@ -20234,6 +20789,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20234
20789
|
this._delegationManager = stagedGraph.delegationManager;
|
|
20235
20790
|
this._spaceService = stagedGraph.spaceService;
|
|
20236
20791
|
this._serviceGraph = stagedGraph.graph;
|
|
20792
|
+
this._activeServiceSession = serviceSession;
|
|
20237
20793
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
20238
20794
|
this._secrets = /* @__PURE__ */ new Map();
|
|
20239
20795
|
this._publicKV = void 0;
|
|
@@ -20241,6 +20797,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20241
20797
|
this.tc = stagedGraph.core;
|
|
20242
20798
|
this._recapOperationsCache = stagedPrimary.cache;
|
|
20243
20799
|
this.runtimePermissionGrants = stagedPrimary.grants;
|
|
20800
|
+
this.confirmedHostedSpaceIds.clear();
|
|
20244
20801
|
if (this.auth) {
|
|
20245
20802
|
this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost]);
|
|
20246
20803
|
this._restoredTcSession = void 0;
|
|
@@ -20550,6 +21107,28 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20550
21107
|
currentTinyCloudSession() {
|
|
20551
21108
|
return this.auth?.tinyCloudSession ?? this._restoredTcSession;
|
|
20552
21109
|
}
|
|
21110
|
+
/**
|
|
21111
|
+
* Mint a multi-capability invocation through the established session key.
|
|
21112
|
+
* Callers receive only the signed headers; session key material remains
|
|
21113
|
+
* owned by the session manager and is never part of the caller contract.
|
|
21114
|
+
*/
|
|
21115
|
+
invokeAny(entries, facts) {
|
|
21116
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
21117
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
21118
|
+
return this._serviceGraph.invokeAny(session, entries, facts);
|
|
21119
|
+
}
|
|
21120
|
+
/** Sign protocol bytes with the established in-memory session key. */
|
|
21121
|
+
async signSessionBytes(bytes) {
|
|
21122
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
21123
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
21124
|
+
return signBytesWithJwk(bytes, session.jwk);
|
|
21125
|
+
}
|
|
21126
|
+
/** Bind a session-signed invocation to the canonical service audience. */
|
|
21127
|
+
async bindInvocationAudience(authorization, audience) {
|
|
21128
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
21129
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
21130
|
+
return rewriteInvocationAudience(authorization, audience, session.jwk);
|
|
21131
|
+
}
|
|
20553
21132
|
/**
|
|
20554
21133
|
* Connect a wallet to upgrade from session-only mode to wallet mode.
|
|
20555
21134
|
*
|
|
@@ -20731,6 +21310,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20731
21310
|
if (this._serviceContext) {
|
|
20732
21311
|
const spaceScopedContext = this._serviceGraph.track(new ServiceContext2({
|
|
20733
21312
|
invoke: this._serviceContext.invoke,
|
|
21313
|
+
invokeAny: this._serviceContext.invokeAny,
|
|
20734
21314
|
fetch: this._serviceContext.fetch,
|
|
20735
21315
|
hosts: this._serviceContext.hosts,
|
|
20736
21316
|
telemetry: this.config.telemetry
|
|
@@ -20823,7 +21403,17 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20823
21403
|
verifyNodeSignature: (nodeId, message, signature2) => verifyDidKeyEd25519Signature(nodeId, message, signature2)
|
|
20824
21404
|
};
|
|
20825
21405
|
}
|
|
21406
|
+
/**
|
|
21407
|
+
* The node DID for {@link config.host}.
|
|
21408
|
+
*
|
|
21409
|
+
* Sign-in already performs `GET /info` for the protocol-version check, and
|
|
21410
|
+
* that response carries `nodeId`, so reuse it rather than fetching `/info` a
|
|
21411
|
+
* second time. Only a value recorded for this exact host qualifies; older
|
|
21412
|
+
* nodes that omit `nodeId` fall through to the fetch.
|
|
21413
|
+
*/
|
|
20826
21414
|
async fetchNodeId() {
|
|
21415
|
+
const cached = this.auth?.nodeIdForHost(this.config.host);
|
|
21416
|
+
if (cached) return cached;
|
|
20827
21417
|
const response = await fetch(`${this.config.host}/info`);
|
|
20828
21418
|
if (!response.ok) {
|
|
20829
21419
|
throw new Error(`Failed to fetch node info: HTTP ${response.status}`);
|
|
@@ -21279,21 +21869,29 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21279
21869
|
async createOwnerDelegation(params, assertActive) {
|
|
21280
21870
|
const assertOwnerGraphActive = assertActive ?? this._serviceGraph.assertActive.bind(this._serviceGraph);
|
|
21281
21871
|
assertOwnerGraphActive();
|
|
21282
|
-
if (!params.delegateDid.startsWith("did:key:")
|
|
21872
|
+
if (!params.delegateDid.startsWith("did:key:")) {
|
|
21283
21873
|
throw new Error("Owner delegation requires an external did:key audience and bounded capabilities");
|
|
21284
21874
|
}
|
|
21875
|
+
const permissions = ownerDelegationPermissions(params);
|
|
21285
21876
|
const now = /* @__PURE__ */ new Date();
|
|
21286
|
-
if (params.expiresAt.getTime() <= now.getTime() || params.expiresAt.getTime() - now.getTime() > EXPIRY3.MAX_MS) {
|
|
21877
|
+
if (!(params.expiresAt instanceof Date) || params.expiresAt.getTime() <= now.getTime() || params.expiresAt.getTime() - now.getTime() > EXPIRY3.MAX_MS) {
|
|
21287
21878
|
throw new Error("Owner delegation expiry must be explicit, future, and within EXPIRY.MAX_MS");
|
|
21288
21879
|
}
|
|
21289
21880
|
if (!this.signer) throw new Error("Owner wallet signer is required");
|
|
21290
21881
|
const session = this.currentTinyCloudSession();
|
|
21291
21882
|
if (!session) throw new Error("Owner session is required");
|
|
21292
|
-
const
|
|
21293
|
-
|
|
21883
|
+
const ownerDid = pkhDid2(session.address, session.chainId);
|
|
21884
|
+
for (const permission of permissions) {
|
|
21885
|
+
if (permission.service === ENCRYPTION_PERMISSION_SERVICE2 && !principalDidEquals2(parseNetworkId2(permission.path).ownerDid, ownerDid)) {
|
|
21886
|
+
throw new Error("Owner delegation cannot grant a foreign encryption network");
|
|
21887
|
+
}
|
|
21888
|
+
}
|
|
21889
|
+
const { abilities, rawAbilities } = ownerPermissionsToAbilities(permissions);
|
|
21890
|
+
const primary = permissions[0];
|
|
21294
21891
|
const host = this.config.host;
|
|
21295
21892
|
const prepared = this.wasmBindings.prepareSession({
|
|
21296
21893
|
abilities,
|
|
21894
|
+
...Object.keys(rawAbilities).length === 0 ? {} : { rawAbilities },
|
|
21297
21895
|
address: this.wasmBindings.ensureEip55(session.address),
|
|
21298
21896
|
chainId: session.chainId,
|
|
21299
21897
|
domain: this.siweDomain,
|
|
@@ -21313,10 +21911,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21313
21911
|
const delegation = {
|
|
21314
21912
|
cid: delegationSession.delegationCid,
|
|
21315
21913
|
delegateDID: params.delegateDid,
|
|
21316
|
-
delegatorDID:
|
|
21914
|
+
delegatorDID: ownerDid,
|
|
21317
21915
|
spaceId: params.spaceId,
|
|
21318
|
-
path:
|
|
21319
|
-
actions: [...
|
|
21916
|
+
path: primary.path,
|
|
21917
|
+
actions: [...primary.actions],
|
|
21320
21918
|
expiry: params.expiresAt,
|
|
21321
21919
|
isRevoked: false,
|
|
21322
21920
|
allowSubDelegation: true,
|
|
@@ -21327,6 +21925,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21327
21925
|
delegation,
|
|
21328
21926
|
signedDagCbor: decodeAuthorizationBytes(delegationSession.delegationHeader.Authorization),
|
|
21329
21927
|
delegationCid: delegationSession.delegationCid,
|
|
21928
|
+
permissions,
|
|
21330
21929
|
nodeReceipt: {
|
|
21331
21930
|
commitEventCid: activation.commitEventCid,
|
|
21332
21931
|
activated: activation.activated ?? [],
|
|
@@ -21377,6 +21976,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21377
21976
|
shareUrl: input.shareUrl,
|
|
21378
21977
|
documentName: input.documentName,
|
|
21379
21978
|
jti: base64UrlEncode(crypto.getRandomValues(new Uint8Array(16))),
|
|
21979
|
+
idempotencyKey: input.idempotencyKey,
|
|
21380
21980
|
expiresAt: input.expiresAt
|
|
21381
21981
|
};
|
|
21382
21982
|
const requestBodyDigest = base64UrlEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalizeEncryptionJson(body)))));
|
|
@@ -21586,6 +22186,13 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21586
22186
|
body
|
|
21587
22187
|
)
|
|
21588
22188
|
});
|
|
22189
|
+
if (response.status === 409) {
|
|
22190
|
+
const existing = await this.getEncryptionNetwork(networkId);
|
|
22191
|
+
if (existing) return existing;
|
|
22192
|
+
throw new Error(
|
|
22193
|
+
`Encryption network ${networkId} already exists but could not be read back`
|
|
22194
|
+
);
|
|
22195
|
+
}
|
|
21589
22196
|
if (!response.ok) {
|
|
21590
22197
|
throw new Error(
|
|
21591
22198
|
`Failed to create encryption network ${networkId}: HTTP ${response.status} ${await response.text()}`
|
|
@@ -21594,11 +22201,19 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21594
22201
|
const created = await response.json();
|
|
21595
22202
|
return created.descriptor;
|
|
21596
22203
|
}
|
|
21597
|
-
|
|
22204
|
+
/**
|
|
22205
|
+
* Ensure an encryption network exists, creating it if necessary.
|
|
22206
|
+
*
|
|
22207
|
+
* @param nameOrNetworkId - Network name or full `urn:tinycloud:encryption:` id.
|
|
22208
|
+
* @param options - See {@link EnsureEncryptionNetworkOptions}.
|
|
22209
|
+
*/
|
|
22210
|
+
async ensureEncryptionNetwork(nameOrNetworkId = DEFAULT_ENCRYPTION_NETWORK_NAME, options = {}) {
|
|
21598
22211
|
const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
|
|
21599
|
-
|
|
21600
|
-
|
|
21601
|
-
|
|
22212
|
+
if (!options.assumeMissing) {
|
|
22213
|
+
const existing = await this.getEncryptionNetwork(networkId);
|
|
22214
|
+
if (existing) {
|
|
22215
|
+
return existing;
|
|
22216
|
+
}
|
|
21602
22217
|
}
|
|
21603
22218
|
const parsed = parseNetworkId2(networkId);
|
|
21604
22219
|
if (!didPrincipalMatches2(parsed.ownerDid, this.did)) {
|
|
@@ -23615,6 +24230,50 @@ var FileSessionStorage = class {
|
|
|
23615
24230
|
}
|
|
23616
24231
|
};
|
|
23617
24232
|
|
|
24233
|
+
// src/authorization/openKeyBridge.ts
|
|
24234
|
+
function wireOpenKeyAuthorize(openkey) {
|
|
24235
|
+
return async (input) => {
|
|
24236
|
+
const result = await openkey.authorizeTinyCloud({
|
|
24237
|
+
protocolVersion: 1,
|
|
24238
|
+
siwe: input.siwe,
|
|
24239
|
+
keyId: input.keyId,
|
|
24240
|
+
jwk: input.jwk,
|
|
24241
|
+
host: input.host,
|
|
24242
|
+
// Forward the presentation envelope verbatim — the OpenKey SDK
|
|
24243
|
+
// validates and size-bounds it before use.
|
|
24244
|
+
presentation: input.presentation
|
|
24245
|
+
});
|
|
24246
|
+
if (result.protocolVersion !== 1) {
|
|
24247
|
+
throw new Error(
|
|
24248
|
+
`wireOpenKeyAuthorize: OpenKey returned unsupported protocolVersion ${result.protocolVersion}`
|
|
24249
|
+
);
|
|
24250
|
+
}
|
|
24251
|
+
if (typeof result.signedMessage !== "string" || !result.signedMessage) {
|
|
24252
|
+
throw new Error(
|
|
24253
|
+
"wireOpenKeyAuthorize: OpenKey returned no signedMessage \u2014 cannot complete a TinyCloud session"
|
|
24254
|
+
);
|
|
24255
|
+
}
|
|
24256
|
+
if (!Array.isArray(result.selectedActionKeys)) {
|
|
24257
|
+
throw new Error(
|
|
24258
|
+
"wireOpenKeyAuthorize: OpenKey returned no selectedActionKeys"
|
|
24259
|
+
);
|
|
24260
|
+
}
|
|
24261
|
+
if (!Array.isArray(result.permissions)) {
|
|
24262
|
+
throw new Error(
|
|
24263
|
+
"wireOpenKeyAuthorize: OpenKey returned no permissions"
|
|
24264
|
+
);
|
|
24265
|
+
}
|
|
24266
|
+
return {
|
|
24267
|
+
protocolVersion: 1,
|
|
24268
|
+
address: result.address,
|
|
24269
|
+
signature: result.signature,
|
|
24270
|
+
signedMessage: result.signedMessage,
|
|
24271
|
+
selectedActionKeys: result.selectedActionKeys,
|
|
24272
|
+
permissions: result.permissions
|
|
24273
|
+
};
|
|
24274
|
+
};
|
|
24275
|
+
}
|
|
24276
|
+
|
|
23618
24277
|
// src/index.ts
|
|
23619
24278
|
import {
|
|
23620
24279
|
ACCOUNT_REGISTRY_PATH,
|
|
@@ -24192,7 +24851,8 @@ export {
|
|
|
24192
24851
|
tinyCloudDebugLogger,
|
|
24193
24852
|
validateEnvelope,
|
|
24194
24853
|
validateManifest,
|
|
24195
|
-
verifyDecryptResponse
|
|
24854
|
+
verifyDecryptResponse,
|
|
24855
|
+
wireOpenKeyAuthorize
|
|
24196
24856
|
};
|
|
24197
24857
|
/*! Bundled license information:
|
|
24198
24858
|
|