@tinycloud/node-sdk 2.11.0-beta.1 → 2.11.0-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{core-CQ_5wXQT.d.cts → core-9mWw3N8i.d.cts} +280 -13
- package/dist/{core-CQ_5wXQT.d.ts → core-9mWw3N8i.d.ts} +280 -13
- package/dist/core.cjs +725 -79
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +2 -2
- package/dist/core.d.ts +2 -2
- package/dist/core.js +741 -86
- package/dist/core.js.map +1 -1
- package/dist/index.cjs +779 -81
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +86 -4
- package/dist/index.d.ts +86 -4
- package/dist/index.js +790 -87
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -17025,10 +17025,13 @@ var require_utils2 = __commonJS({
|
|
|
17025
17025
|
// src/index.ts
|
|
17026
17026
|
var index_exports = {};
|
|
17027
17027
|
__export(index_exports, {
|
|
17028
|
+
ACCEPTED_MARKER_VERSIONS: () => ACCEPTED_MARKER_VERSIONS,
|
|
17028
17029
|
ACCOUNT_REGISTRY_PATH: () => import_sdk_core12.ACCOUNT_REGISTRY_PATH,
|
|
17029
17030
|
ACCOUNT_REGISTRY_SPACE: () => import_sdk_core12.ACCOUNT_REGISTRY_SPACE,
|
|
17030
17031
|
AccountService: () => import_sdk_core3.AccountService,
|
|
17031
17032
|
AutoApproveSpaceCreationHandler: () => import_sdk_core11.AutoApproveSpaceCreationHandler,
|
|
17033
|
+
BOOTSTRAP_COMPLETION_MARKER_KEY: () => BOOTSTRAP_COMPLETION_MARKER_KEY,
|
|
17034
|
+
BOOTSTRAP_COMPLETION_MARKER_VERSION: () => BOOTSTRAP_COMPLETION_MARKER_VERSION,
|
|
17032
17035
|
CapabilityKeyRegistry: () => import_sdk_core20.CapabilityKeyRegistry,
|
|
17033
17036
|
CapabilityKeyRegistryErrorCodes: () => import_sdk_core20.CapabilityKeyRegistryErrorCodes,
|
|
17034
17037
|
CaveatedDelegationUnsupportedError: () => import_sdk_core12.CaveatedDelegationUnsupportedError,
|
|
@@ -17165,7 +17168,8 @@ __export(index_exports, {
|
|
|
17165
17168
|
tinyCloudDebugLogger: () => import_sdk_core11.tinyCloudDebugLogger,
|
|
17166
17169
|
validateEnvelope: () => import_sdk_core17.validateEnvelope,
|
|
17167
17170
|
validateManifest: () => import_sdk_core12.validateManifest,
|
|
17168
|
-
verifyDecryptResponse: () => import_sdk_core17.verifyDecryptResponse
|
|
17171
|
+
verifyDecryptResponse: () => import_sdk_core17.verifyDecryptResponse,
|
|
17172
|
+
wireOpenKeyAuthorize: () => wireOpenKeyAuthorize
|
|
17169
17173
|
});
|
|
17170
17174
|
module.exports = __toCommonJS(index_exports);
|
|
17171
17175
|
|
|
@@ -17278,6 +17282,8 @@ var import_sdk_core7 = require("@tinycloud/sdk-core");
|
|
|
17278
17282
|
var import_sdk_services = require("@tinycloud/sdk-services");
|
|
17279
17283
|
|
|
17280
17284
|
// src/authorization/NodeUserAuthorization.ts
|
|
17285
|
+
var import_node_crypto = require("crypto");
|
|
17286
|
+
var import_siwe = require("siwe");
|
|
17281
17287
|
var import_sdk_core2 = require("@tinycloud/sdk-core");
|
|
17282
17288
|
|
|
17283
17289
|
// src/authorization/strategies.ts
|
|
@@ -17358,6 +17364,27 @@ var MemorySessionStorage = class {
|
|
|
17358
17364
|
// src/authorization/NodeUserAuthorization.ts
|
|
17359
17365
|
var DECRYPT_ACTION = import_sdk_core2.ENCRYPTION.DECRYPT;
|
|
17360
17366
|
var NETWORK_CREATE_ACTION = import_sdk_core2.ENCRYPTION.NETWORK_CREATE;
|
|
17367
|
+
function canonicalizeOpenKeyManifestJson(value) {
|
|
17368
|
+
const json = JSON.stringify(value);
|
|
17369
|
+
if (json === void 0) {
|
|
17370
|
+
throw new TypeError("manifest is not JSON-serializable");
|
|
17371
|
+
}
|
|
17372
|
+
return canonicalizeJsonValue(JSON.parse(json));
|
|
17373
|
+
}
|
|
17374
|
+
function canonicalizeJsonValue(value) {
|
|
17375
|
+
if (value === null || typeof value !== "object") {
|
|
17376
|
+
return JSON.stringify(value);
|
|
17377
|
+
}
|
|
17378
|
+
if (Array.isArray(value)) {
|
|
17379
|
+
return `[${value.map(canonicalizeJsonValue).join(",")}]`;
|
|
17380
|
+
}
|
|
17381
|
+
return `{${Object.keys(value).sort().map(
|
|
17382
|
+
(key2) => `${JSON.stringify(key2)}:${canonicalizeJsonValue(value[key2])}`
|
|
17383
|
+
).join(",")}}`;
|
|
17384
|
+
}
|
|
17385
|
+
function canonicalOpenKeyManifestSha256Hex(value) {
|
|
17386
|
+
return (0, import_node_crypto.createHash)("sha256").update(canonicalizeOpenKeyManifestJson(value)).digest("hex");
|
|
17387
|
+
}
|
|
17361
17388
|
function didPrincipalMatches(actual, expected) {
|
|
17362
17389
|
try {
|
|
17363
17390
|
return (0, import_sdk_core2.principalDidEquals)(actual, expected);
|
|
@@ -17375,6 +17402,16 @@ function addRawAbility(rawAbilities, resource, action) {
|
|
|
17375
17402
|
actions.push(action);
|
|
17376
17403
|
}
|
|
17377
17404
|
}
|
|
17405
|
+
function cloneAbilitiesMap(actions) {
|
|
17406
|
+
return Object.fromEntries(
|
|
17407
|
+
Object.entries(actions).map(([service, paths]) => [
|
|
17408
|
+
service,
|
|
17409
|
+
Object.fromEntries(
|
|
17410
|
+
Object.entries(paths).map(([path, pathActions]) => [path, [...pathActions]])
|
|
17411
|
+
)
|
|
17412
|
+
])
|
|
17413
|
+
);
|
|
17414
|
+
}
|
|
17378
17415
|
var NodeUserAuthorization = class {
|
|
17379
17416
|
constructor(config) {
|
|
17380
17417
|
this.activeSessionKeyId = "default";
|
|
@@ -17578,6 +17615,15 @@ var NodeUserAuthorization = class {
|
|
|
17578
17615
|
get nodeFeatures() {
|
|
17579
17616
|
return this._nodeFeatures;
|
|
17580
17617
|
}
|
|
17618
|
+
/**
|
|
17619
|
+
* The node DID reported by `GET /info` for `host`, if the sign-in (or
|
|
17620
|
+
* restore) that populated it targeted that same host. Returns `undefined`
|
|
17621
|
+
* for any other host, and for nodes whose `/info` omits `nodeId`, so callers
|
|
17622
|
+
* fall back to fetching it themselves rather than using a foreign node's DID.
|
|
17623
|
+
*/
|
|
17624
|
+
nodeIdForHost(host) {
|
|
17625
|
+
return this._nodeInfoIdentity?.host === host ? this._nodeInfoIdentity.nodeId : void 0;
|
|
17626
|
+
}
|
|
17581
17627
|
get lastActivationSkippedSpaceIds() {
|
|
17582
17628
|
return [...this._lastActivationSkippedSpaceIds];
|
|
17583
17629
|
}
|
|
@@ -17637,23 +17683,41 @@ var NodeUserAuthorization = class {
|
|
|
17637
17683
|
const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
|
|
17638
17684
|
const primarySpaceId2 = (0, import_sdk_core2.makePkhSpaceId)(address, chainId, this.spacePrefix);
|
|
17639
17685
|
const secretsSpaceId = (0, import_sdk_core2.makePkhSpaceId)(address, chainId, "secrets");
|
|
17640
|
-
|
|
17641
|
-
|
|
17642
|
-
|
|
17643
|
-
|
|
17644
|
-
|
|
17645
|
-
|
|
17646
|
-
|
|
17647
|
-
|
|
17648
|
-
|
|
17649
|
-
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
|
|
17653
|
-
|
|
17686
|
+
const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === import_sdk_core2.ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
|
|
17687
|
+
const spaceAbilities2 = {
|
|
17688
|
+
[primarySpaceId2]: primaryActions,
|
|
17689
|
+
[secretsSpaceId]: {
|
|
17690
|
+
kv: {
|
|
17691
|
+
"vault/secrets/": [
|
|
17692
|
+
import_sdk_core2.KV.GET,
|
|
17693
|
+
import_sdk_core2.KV.PUT,
|
|
17694
|
+
import_sdk_core2.KV.DEL,
|
|
17695
|
+
import_sdk_core2.KV.LIST,
|
|
17696
|
+
import_sdk_core2.KV.METADATA
|
|
17697
|
+
]
|
|
17698
|
+
}
|
|
17699
|
+
}
|
|
17700
|
+
};
|
|
17701
|
+
if (this.includeAccountRegistryPermissions && this.spacePrefix !== import_sdk_core2.ACCOUNT_REGISTRY_SPACE) {
|
|
17702
|
+
spaceAbilities2[(0, import_sdk_core2.makePkhSpaceId)(address, chainId, import_sdk_core2.ACCOUNT_REGISTRY_SPACE)] = (0, import_sdk_core2.resourceCapabilitiesToAbilitiesMap)(import_sdk_core2.ACCOUNT_MANIFEST_PERMISSIONS);
|
|
17703
|
+
} else if (this.includeAccountRegistryPermissions) {
|
|
17704
|
+
const accountAbilities = (0, import_sdk_core2.resourceCapabilitiesToAbilitiesMap)(
|
|
17705
|
+
import_sdk_core2.ACCOUNT_MANIFEST_PERMISSIONS
|
|
17706
|
+
);
|
|
17707
|
+
for (const [service, paths] of Object.entries(accountAbilities)) {
|
|
17708
|
+
const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
|
|
17709
|
+
for (const [path, actions] of Object.entries(paths)) {
|
|
17710
|
+
const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
|
|
17711
|
+
for (const action of actions) {
|
|
17712
|
+
if (!existingActions.includes(action)) existingActions.push(action);
|
|
17654
17713
|
}
|
|
17655
17714
|
}
|
|
17656
|
-
}
|
|
17715
|
+
}
|
|
17716
|
+
}
|
|
17717
|
+
return {
|
|
17718
|
+
abilities: primaryActions,
|
|
17719
|
+
spaceId: primarySpaceId2,
|
|
17720
|
+
spaceAbilities: spaceAbilities2,
|
|
17657
17721
|
rawAbilities: {
|
|
17658
17722
|
[defaultNetworkId]: [DECRYPT_ACTION, NETWORK_CREATE_ACTION]
|
|
17659
17723
|
}
|
|
@@ -18047,11 +18111,13 @@ var NodeUserAuthorization = class {
|
|
|
18047
18111
|
this._tinyCloudSession = tinyCloudSession;
|
|
18048
18112
|
this._address = address;
|
|
18049
18113
|
this._chainId = chainId;
|
|
18114
|
+
const infoHost = this.primaryTinyCloudHost;
|
|
18050
18115
|
const nodeInfo = await (0, import_sdk_core2.checkNodeInfo)(
|
|
18051
|
-
|
|
18116
|
+
infoHost,
|
|
18052
18117
|
this.wasm.protocolVersion()
|
|
18053
18118
|
);
|
|
18054
18119
|
this._nodeFeatures = nodeInfo.features;
|
|
18120
|
+
this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
|
|
18055
18121
|
for (const ext of this.extensions) {
|
|
18056
18122
|
if (ext.afterSignIn) {
|
|
18057
18123
|
await ext.afterSignIn(clientSession);
|
|
@@ -18217,11 +18283,13 @@ var NodeUserAuthorization = class {
|
|
|
18217
18283
|
this._tinyCloudSession = tinyCloudSession;
|
|
18218
18284
|
this._address = address;
|
|
18219
18285
|
this._chainId = chainId;
|
|
18286
|
+
const infoHost = this.primaryTinyCloudHost;
|
|
18220
18287
|
const nodeInfo = await (0, import_sdk_core2.checkNodeInfo)(
|
|
18221
|
-
|
|
18288
|
+
infoHost,
|
|
18222
18289
|
this.wasm.protocolVersion()
|
|
18223
18290
|
);
|
|
18224
18291
|
this._nodeFeatures = nodeInfo.features;
|
|
18292
|
+
this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
|
|
18225
18293
|
for (const ext of this.extensions) {
|
|
18226
18294
|
if (ext.afterSignIn) {
|
|
18227
18295
|
await ext.afterSignIn(clientSession);
|
|
@@ -18230,6 +18298,398 @@ var NodeUserAuthorization = class {
|
|
|
18230
18298
|
await this.ensureSpaceExists();
|
|
18231
18299
|
return clientSession;
|
|
18232
18300
|
}
|
|
18301
|
+
/**
|
|
18302
|
+
* End-to-end sign-in via OpenKey `authorizeTinyCloud()` (Sol CRITICAL-4).
|
|
18303
|
+
*
|
|
18304
|
+
* This is the production entry point that wires
|
|
18305
|
+
* {@link prepareSessionForSigning} → OpenKey `authorizeTinyCloud()` →
|
|
18306
|
+
* {@link signInWithOpenKeyResult} into one call. Consumers pass in a
|
|
18307
|
+
* caller-provided `authorizeFn` (typically a thin wrapper around
|
|
18308
|
+
* `openkey.authorizeTinyCloud()`) so this method has no direct
|
|
18309
|
+
* dependency on the OpenKey SDK — it works with any transport that
|
|
18310
|
+
* conforms to the versioned protocol.
|
|
18311
|
+
*
|
|
18312
|
+
* The `authorizeFn` MUST:
|
|
18313
|
+
* 1. Send the prepared SIWE bytes to OpenKey.
|
|
18314
|
+
* 2. Return the exact `TinyCloudAuthorizationResultV1` shape (with
|
|
18315
|
+
* `signedMessage` — the bytes the signature actually verifies
|
|
18316
|
+
* against — plus `selectedActionKeys` and `permissions`).
|
|
18317
|
+
*
|
|
18318
|
+
* NodeUserAuthorization then completes the session with `signedMessage`
|
|
18319
|
+
* (never the prepared bytes), verifying the signature, the immutable
|
|
18320
|
+
* SIWE fields, and every subset invariant enforced by
|
|
18321
|
+
* {@link signInWithOpenKeyResult}. This preserves the exact-byte
|
|
18322
|
+
* guarantee: if the user did NOT narrow, the returned `signedMessage`
|
|
18323
|
+
* equals the prepared SIWE byte-for-byte; if the user narrowed, only
|
|
18324
|
+
* the ReCap block differs and every immutable header field survives.
|
|
18325
|
+
*
|
|
18326
|
+
* Legacy exact-byte {@link signIn} callers are unaffected — this method
|
|
18327
|
+
* is opt-in.
|
|
18328
|
+
*
|
|
18329
|
+
* @param authorizeFn Callback that invokes the versioned OpenKey
|
|
18330
|
+
* `authorizeTinyCloud()` protocol and returns the result. Receives
|
|
18331
|
+
* the prepared SIWE bytes plus a `host` hint the widget should bind
|
|
18332
|
+
* into its /authorize-sign-prepare context.
|
|
18333
|
+
* @param options Optional overrides for the OpenKey request.
|
|
18334
|
+
*/
|
|
18335
|
+
async signInWithOpenKey(authorizeFn, options) {
|
|
18336
|
+
if (options?.host !== void 0) {
|
|
18337
|
+
const overrideHost = options.host.trim();
|
|
18338
|
+
if (!overrideHost) {
|
|
18339
|
+
throw new Error("OpenKey authorization host must not be empty");
|
|
18340
|
+
}
|
|
18341
|
+
this.tinycloudHosts = [overrideHost];
|
|
18342
|
+
}
|
|
18343
|
+
const signingAddress = (0, import_sdk_core2.canonicalizeAddress)(await this.signer.getAddress());
|
|
18344
|
+
const signingChainId = await this.signer.getChainId();
|
|
18345
|
+
await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
|
|
18346
|
+
const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
|
|
18347
|
+
const presentation = this.buildPresentationEnvelope(options?.reason);
|
|
18348
|
+
const hostHint = this.primaryTinyCloudHost;
|
|
18349
|
+
const result = await authorizeFn({
|
|
18350
|
+
protocolVersion: 1,
|
|
18351
|
+
siwe: prepared.siwe,
|
|
18352
|
+
jwk,
|
|
18353
|
+
host: hostHint,
|
|
18354
|
+
keyId: options?.openkeyKeyId,
|
|
18355
|
+
presentation
|
|
18356
|
+
});
|
|
18357
|
+
return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
|
|
18358
|
+
}
|
|
18359
|
+
/**
|
|
18360
|
+
* Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
|
|
18361
|
+
* envelope is display-only — the OpenKey widget renders it under an
|
|
18362
|
+
* honest trust label, and the server /authorize-sign-prepare route
|
|
18363
|
+
* decides whether to upgrade the label to `origin-bound` by fetching
|
|
18364
|
+
* the well-known manifest and comparing its digest to
|
|
18365
|
+
* `manifestDigest`.
|
|
18366
|
+
*
|
|
18367
|
+
* Returns `undefined` when no manifest is set — the widget renders as
|
|
18368
|
+
* "no manifest supplied" (unsigned).
|
|
18369
|
+
*
|
|
18370
|
+
* The digest is a deterministic SHA-256 over canonical JSON of the
|
|
18371
|
+
* first manifest payload; OpenKey parses the fetched well-known manifest
|
|
18372
|
+
* and applies the same canonicalization before comparing digests.
|
|
18373
|
+
*/
|
|
18374
|
+
buildPresentationEnvelope(reason) {
|
|
18375
|
+
const manifests = Array.isArray(this._manifest) ? this._manifest : this._manifest ? [this._manifest] : [];
|
|
18376
|
+
if (manifests.length === 0 && !reason) {
|
|
18377
|
+
return void 0;
|
|
18378
|
+
}
|
|
18379
|
+
const primary = manifests[0];
|
|
18380
|
+
let displayName;
|
|
18381
|
+
let manifestId;
|
|
18382
|
+
let manifestDigest;
|
|
18383
|
+
const envelopeManifests = [];
|
|
18384
|
+
for (const m of manifests) {
|
|
18385
|
+
envelopeManifests.push({
|
|
18386
|
+
name: m.name,
|
|
18387
|
+
appId: m.app_id,
|
|
18388
|
+
// Deep-copy to prevent later mutations from affecting the digest.
|
|
18389
|
+
payload: JSON.parse(JSON.stringify(m))
|
|
18390
|
+
});
|
|
18391
|
+
}
|
|
18392
|
+
if (primary) {
|
|
18393
|
+
displayName = primary.name;
|
|
18394
|
+
manifestId = primary.app_id;
|
|
18395
|
+
manifestDigest = canonicalOpenKeyManifestSha256Hex(primary);
|
|
18396
|
+
}
|
|
18397
|
+
return {
|
|
18398
|
+
protocolVersion: 1,
|
|
18399
|
+
displayName,
|
|
18400
|
+
reason: reason && reason.length > 0 ? reason : void 0,
|
|
18401
|
+
manifestId,
|
|
18402
|
+
manifestDigest,
|
|
18403
|
+
manifests: envelopeManifests.length > 0 ? envelopeManifests : void 0
|
|
18404
|
+
};
|
|
18405
|
+
}
|
|
18406
|
+
/**
|
|
18407
|
+
* Complete sign-in with a versioned OpenKey authorization result.
|
|
18408
|
+
*
|
|
18409
|
+
* Where `signInWithPreparedSession` completes with a signature over the
|
|
18410
|
+
* ORIGINAL prepared SIWE, this method completes with the SIWE bytes the
|
|
18411
|
+
* OpenKey widget actually presented and signed (`signedMessage`). It
|
|
18412
|
+
* exists because the versioned authorizeTinyCloud protocol lets the user
|
|
18413
|
+
* narrow capabilities; the returned bytes may differ from the original
|
|
18414
|
+
* request and are the only ones the signature actually verifies against.
|
|
18415
|
+
*
|
|
18416
|
+
* The `result` parameter is validated at the trust boundary via
|
|
18417
|
+
* `validateAuthorizationResultV1` (in sdk-core). Callers should validate
|
|
18418
|
+
* BEFORE calling this method so unknown/missing fields are rejected
|
|
18419
|
+
* before any session state is created.
|
|
18420
|
+
*
|
|
18421
|
+
* Validation performed here (defence-in-depth: even a "valid" v1 payload
|
|
18422
|
+
* must clear these checks before any session state is created):
|
|
18423
|
+
* 1. `result.address` matches the local signer's address.
|
|
18424
|
+
* 2. `result.signature` recovers to `result.address` when verified against
|
|
18425
|
+
* `result.signedMessage`.
|
|
18426
|
+
* 3. Every immutable SIWE header field (domain, address, URI, version,
|
|
18427
|
+
* chainId, nonce, issuedAt) in the original prepared SIWE is preserved
|
|
18428
|
+
* byte-for-byte in `result.signedMessage`. `statement` is NOT in this
|
|
18429
|
+
* list because the WASM renders it from the ReCap contents; it MUST
|
|
18430
|
+
* change when the ReCap is narrowed.
|
|
18431
|
+
* 4. Every capability in `result.signedMessage`'s ReCap block was present
|
|
18432
|
+
* in the original prepared SIWE — narrowing OK, broadening rejected.
|
|
18433
|
+
* 5. Every `result.selectedActionKeys` entry corresponds to a
|
|
18434
|
+
* (resource, action) pair in the returned SIWE.
|
|
18435
|
+
*/
|
|
18436
|
+
async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
|
|
18437
|
+
if (result.protocolVersion !== 1) {
|
|
18438
|
+
throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
|
|
18439
|
+
}
|
|
18440
|
+
if (!result.signedMessage) {
|
|
18441
|
+
throw new Error("OpenKey result must include signedMessage");
|
|
18442
|
+
}
|
|
18443
|
+
if (!prepared.siwe) {
|
|
18444
|
+
throw new Error(
|
|
18445
|
+
"signInWithOpenKeyResult requires prepared.siwe (the SDK-generated SIWE the sign-in was proposed with)"
|
|
18446
|
+
);
|
|
18447
|
+
}
|
|
18448
|
+
const expectedAddress = (0, import_sdk_core2.canonicalizeAddress)(await this.signer.getAddress());
|
|
18449
|
+
if ((0, import_sdk_core2.canonicalizeAddress)(result.address) !== expectedAddress) {
|
|
18450
|
+
throw new Error(
|
|
18451
|
+
`OpenKey returned signature from ${result.address} but expected ${expectedAddress}`
|
|
18452
|
+
);
|
|
18453
|
+
}
|
|
18454
|
+
let parsedSignedSiwe;
|
|
18455
|
+
try {
|
|
18456
|
+
parsedSignedSiwe = new import_siwe.SiweMessage(result.signedMessage);
|
|
18457
|
+
} catch (err) {
|
|
18458
|
+
throw new Error(
|
|
18459
|
+
`OpenKey signedMessage is not a parseable SIWE: ${err.message}`
|
|
18460
|
+
);
|
|
18461
|
+
}
|
|
18462
|
+
const verifyResult = await parsedSignedSiwe.verify(
|
|
18463
|
+
{ signature: result.signature },
|
|
18464
|
+
{ suppressExceptions: true }
|
|
18465
|
+
);
|
|
18466
|
+
if (!verifyResult.success) {
|
|
18467
|
+
const errData = verifyResult.error;
|
|
18468
|
+
const type = errData && typeof errData === "object" && "type" in errData ? errData.type : void 0;
|
|
18469
|
+
const details = errData && typeof errData === "object" && "expected" in errData ? ` (expected=${errData.expected}, received=${errData.received})` : "";
|
|
18470
|
+
throw new Error(
|
|
18471
|
+
`OpenKey signature verification failed${type ? `: ${type}` : ""}${details}`
|
|
18472
|
+
);
|
|
18473
|
+
}
|
|
18474
|
+
if ((0, import_sdk_core2.canonicalizeAddress)(parsedSignedSiwe.address) !== expectedAddress) {
|
|
18475
|
+
throw new Error(
|
|
18476
|
+
`OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
|
|
18477
|
+
);
|
|
18478
|
+
}
|
|
18479
|
+
const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
|
|
18480
|
+
const originalFields = (0, import_sdk_core2.extractImmutableSiweFields)(prepared.siwe);
|
|
18481
|
+
const signedFields = (0, import_sdk_core2.extractImmutableSiweFields)(result.signedMessage);
|
|
18482
|
+
const changedFields = (0, import_sdk_core2.diffImmutableSiweFields)(originalFields, signedFields, {
|
|
18483
|
+
originalHasRecap
|
|
18484
|
+
});
|
|
18485
|
+
if (changedFields.length > 0) {
|
|
18486
|
+
throw new Error(
|
|
18487
|
+
`OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
|
|
18488
|
+
);
|
|
18489
|
+
}
|
|
18490
|
+
let originalCaps;
|
|
18491
|
+
let signedCaps;
|
|
18492
|
+
try {
|
|
18493
|
+
originalCaps = (0, import_sdk_core2.extractRecapAttenuations)(prepared.siwe);
|
|
18494
|
+
} catch (err) {
|
|
18495
|
+
throw new Error(
|
|
18496
|
+
`Failed to decode prepared SIWE recap: ${err.message}`
|
|
18497
|
+
);
|
|
18498
|
+
}
|
|
18499
|
+
try {
|
|
18500
|
+
signedCaps = (0, import_sdk_core2.extractRecapAttenuations)(result.signedMessage);
|
|
18501
|
+
} catch (err) {
|
|
18502
|
+
throw new Error(
|
|
18503
|
+
`Failed to decode OpenKey signedMessage recap: ${err.message}`
|
|
18504
|
+
);
|
|
18505
|
+
}
|
|
18506
|
+
const unauthorized = (0, import_sdk_core2.unauthorizedRecapCapabilities)(signedCaps, originalCaps);
|
|
18507
|
+
if (unauthorized.length > 0) {
|
|
18508
|
+
const previewLimit = 5;
|
|
18509
|
+
const preview = unauthorized.slice(0, previewLimit).map((u) => `${u.resource}::${u.action}`).join(", ");
|
|
18510
|
+
const overflow = unauthorized.length > previewLimit ? ` (and ${unauthorized.length - previewLimit} more)` : "";
|
|
18511
|
+
throw new Error(
|
|
18512
|
+
`OpenKey signedMessage broadened authorization beyond the original request: ${preview}${overflow}`
|
|
18513
|
+
);
|
|
18514
|
+
}
|
|
18515
|
+
if (!result.signedMessage.includes(prepared.spaceId)) {
|
|
18516
|
+
throw new Error(
|
|
18517
|
+
`OpenKey signedMessage does not reference the expected spaceId ${prepared.spaceId}`
|
|
18518
|
+
);
|
|
18519
|
+
}
|
|
18520
|
+
const grantedPairs = /* @__PURE__ */ new Set();
|
|
18521
|
+
const grantedFourPartIndex = /* @__PURE__ */ new Map();
|
|
18522
|
+
for (const [resource, actions] of Object.entries(signedCaps)) {
|
|
18523
|
+
const { space, path } = (0, import_sdk_core2.parseCanonicalRecapResource)(resource);
|
|
18524
|
+
for (const action of Object.keys(actions)) {
|
|
18525
|
+
grantedPairs.add(`${resource}\0${action}`);
|
|
18526
|
+
const slashIdx = action.indexOf("/");
|
|
18527
|
+
const service = slashIdx > 0 ? action.slice(0, slashIdx) : "";
|
|
18528
|
+
if (service) {
|
|
18529
|
+
grantedFourPartIndex.set(
|
|
18530
|
+
`${service}\0${space}\0${path}\0${action}`,
|
|
18531
|
+
`${resource}\0${action}`
|
|
18532
|
+
);
|
|
18533
|
+
}
|
|
18534
|
+
}
|
|
18535
|
+
}
|
|
18536
|
+
const isStructurallyRequired = (action) => action === "tinycloud.capabilities/read" || action === "capabilities/read";
|
|
18537
|
+
const nonRequiredGrantedPairs = /* @__PURE__ */ new Set();
|
|
18538
|
+
for (const pair of grantedPairs) {
|
|
18539
|
+
const idx = pair.indexOf("\0");
|
|
18540
|
+
const action = idx >= 0 ? pair.slice(idx + 1) : pair;
|
|
18541
|
+
if (!isStructurallyRequired(action)) {
|
|
18542
|
+
nonRequiredGrantedPairs.add(pair);
|
|
18543
|
+
}
|
|
18544
|
+
}
|
|
18545
|
+
const allSignedGrantedPairs = new Set(grantedPairs);
|
|
18546
|
+
const seenRawKeys = /* @__PURE__ */ new Set();
|
|
18547
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
18548
|
+
if (seenRawKeys.has(rawKey)) {
|
|
18549
|
+
throw new Error(
|
|
18550
|
+
`OpenKey selectedActionKeys contains duplicate entry ${rawKey}`
|
|
18551
|
+
);
|
|
18552
|
+
}
|
|
18553
|
+
seenRawKeys.add(rawKey);
|
|
18554
|
+
}
|
|
18555
|
+
const selectedResourceActionPairs = /* @__PURE__ */ new Set();
|
|
18556
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
18557
|
+
const parts = rawKey.split("\0");
|
|
18558
|
+
if (parts.length !== 4) {
|
|
18559
|
+
throw new Error(
|
|
18560
|
+
`OpenKey selectedActionKeys entry is malformed (expected canonical 4-part service\\0space\\0path\\0ability): ${rawKey}`
|
|
18561
|
+
);
|
|
18562
|
+
}
|
|
18563
|
+
const canonical = grantedFourPartIndex.get(rawKey);
|
|
18564
|
+
if (!canonical) {
|
|
18565
|
+
throw new Error(
|
|
18566
|
+
`OpenKey selectedActionKeys entry ${rawKey} is not covered by any granted capability`
|
|
18567
|
+
);
|
|
18568
|
+
}
|
|
18569
|
+
selectedResourceActionPairs.add(canonical);
|
|
18570
|
+
}
|
|
18571
|
+
if (nonRequiredGrantedPairs.size > 0) {
|
|
18572
|
+
const missingPairs = [];
|
|
18573
|
+
for (const pair of nonRequiredGrantedPairs) {
|
|
18574
|
+
if (!selectedResourceActionPairs.has(pair)) {
|
|
18575
|
+
missingPairs.push(pair);
|
|
18576
|
+
}
|
|
18577
|
+
}
|
|
18578
|
+
if (missingPairs.length > 0) {
|
|
18579
|
+
const previewLimit = 5;
|
|
18580
|
+
const preview = missingPairs.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
18581
|
+
const overflow = missingPairs.length > previewLimit ? ` (and ${missingPairs.length - previewLimit} more)` : "";
|
|
18582
|
+
throw new Error(
|
|
18583
|
+
`OpenKey selectedActionKeys is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
18584
|
+
);
|
|
18585
|
+
}
|
|
18586
|
+
}
|
|
18587
|
+
if (allSignedGrantedPairs.size > 0 && result.permissions.length === 0) {
|
|
18588
|
+
throw new Error(
|
|
18589
|
+
"OpenKey permissions is empty but signedMessage carries capabilities \u2014 refusing to accept a session without an authoritative grant list"
|
|
18590
|
+
);
|
|
18591
|
+
}
|
|
18592
|
+
const permissionCanonicalKeys = /* @__PURE__ */ new Set();
|
|
18593
|
+
const permissionPairsCovered = /* @__PURE__ */ new Set();
|
|
18594
|
+
for (const perm of result.permissions) {
|
|
18595
|
+
const { service, space, path, actions } = perm;
|
|
18596
|
+
if (typeof service !== "string" || typeof space !== "string" || typeof path !== "string") {
|
|
18597
|
+
throw new Error(
|
|
18598
|
+
`OpenKey permissions entry has non-string service/space/path (service=${service}, space=${space}, path=${path})`
|
|
18599
|
+
);
|
|
18600
|
+
}
|
|
18601
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
18602
|
+
throw new Error(
|
|
18603
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has empty actions \u2014 a capability-bearing entry must list at least one action`
|
|
18604
|
+
);
|
|
18605
|
+
}
|
|
18606
|
+
const canonicalKey = `${service}\0${space}\0${path}`;
|
|
18607
|
+
if (permissionCanonicalKeys.has(canonicalKey)) {
|
|
18608
|
+
throw new Error(
|
|
18609
|
+
`OpenKey permissions has duplicate entry for (${service} ${space} ${path})`
|
|
18610
|
+
);
|
|
18611
|
+
}
|
|
18612
|
+
permissionCanonicalKeys.add(canonicalKey);
|
|
18613
|
+
let expectedResource;
|
|
18614
|
+
if (space.startsWith("tinycloud:")) {
|
|
18615
|
+
const shortService = service.startsWith("tinycloud.") ? service.slice("tinycloud.".length) : service;
|
|
18616
|
+
expectedResource = path ? `${space}/${shortService}/${path}` : `${space}/${shortService}`;
|
|
18617
|
+
} else {
|
|
18618
|
+
if (path) {
|
|
18619
|
+
throw new Error(
|
|
18620
|
+
`OpenKey permissions entry (${service} ${space} ${path}) carries a path on a non-tinycloud: resource URI, which is not a recognized encoding`
|
|
18621
|
+
);
|
|
18622
|
+
}
|
|
18623
|
+
expectedResource = space;
|
|
18624
|
+
}
|
|
18625
|
+
let matchedResource;
|
|
18626
|
+
if (signedCaps[expectedResource] !== void 0) {
|
|
18627
|
+
matchedResource = expectedResource;
|
|
18628
|
+
} else {
|
|
18629
|
+
throw new Error(
|
|
18630
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims grants not confirmed in signedMessage capabilities`
|
|
18631
|
+
);
|
|
18632
|
+
}
|
|
18633
|
+
const grantedActionsForResource = new Set(
|
|
18634
|
+
Object.keys(signedCaps[matchedResource])
|
|
18635
|
+
);
|
|
18636
|
+
for (const action of actions) {
|
|
18637
|
+
if (typeof action !== "string" || !action) {
|
|
18638
|
+
throw new Error(
|
|
18639
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has non-string action ${String(action)}`
|
|
18640
|
+
);
|
|
18641
|
+
}
|
|
18642
|
+
if (!grantedActionsForResource.has(action)) {
|
|
18643
|
+
throw new Error(
|
|
18644
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims action ${action} not present in signedMessage capabilities`
|
|
18645
|
+
);
|
|
18646
|
+
}
|
|
18647
|
+
permissionPairsCovered.add(`${matchedResource}\0${action}`);
|
|
18648
|
+
}
|
|
18649
|
+
}
|
|
18650
|
+
if (allSignedGrantedPairs.size > 0) {
|
|
18651
|
+
const missingFromPerms = [];
|
|
18652
|
+
for (const pair of allSignedGrantedPairs) {
|
|
18653
|
+
if (!permissionPairsCovered.has(pair)) {
|
|
18654
|
+
missingFromPerms.push(pair);
|
|
18655
|
+
}
|
|
18656
|
+
}
|
|
18657
|
+
if (missingFromPerms.length > 0) {
|
|
18658
|
+
const previewLimit = 5;
|
|
18659
|
+
const preview = missingFromPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
18660
|
+
const overflow = missingFromPerms.length > previewLimit ? ` (and ${missingFromPerms.length - previewLimit} more)` : "";
|
|
18661
|
+
throw new Error(
|
|
18662
|
+
`OpenKey permissions is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
18663
|
+
);
|
|
18664
|
+
}
|
|
18665
|
+
const extrasInPerms = [];
|
|
18666
|
+
for (const pair of permissionPairsCovered) {
|
|
18667
|
+
if (!allSignedGrantedPairs.has(pair)) {
|
|
18668
|
+
extrasInPerms.push(pair);
|
|
18669
|
+
}
|
|
18670
|
+
}
|
|
18671
|
+
if (extrasInPerms.length > 0) {
|
|
18672
|
+
const previewLimit = 5;
|
|
18673
|
+
const preview = extrasInPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
18674
|
+
const overflow = extrasInPerms.length > previewLimit ? ` (and ${extrasInPerms.length - previewLimit} more)` : "";
|
|
18675
|
+
throw new Error(
|
|
18676
|
+
`OpenKey permissions contains entries not present in the signed authority: ${preview}${overflow}`
|
|
18677
|
+
);
|
|
18678
|
+
}
|
|
18679
|
+
}
|
|
18680
|
+
void nonRequiredGrantedPairs;
|
|
18681
|
+
return this.signInWithPreparedSession(
|
|
18682
|
+
{
|
|
18683
|
+
siwe: result.signedMessage,
|
|
18684
|
+
jwk: prepared.jwk,
|
|
18685
|
+
spaceId: prepared.spaceId,
|
|
18686
|
+
verificationMethod: prepared.verificationMethod
|
|
18687
|
+
},
|
|
18688
|
+
result.signature,
|
|
18689
|
+
keyId,
|
|
18690
|
+
jwk
|
|
18691
|
+
);
|
|
18692
|
+
}
|
|
18233
18693
|
/**
|
|
18234
18694
|
* Clear persisted session data.
|
|
18235
18695
|
*/
|
|
@@ -18720,6 +19180,9 @@ var DEFAULT_HOST = "https://node.tinycloud.xyz";
|
|
|
18720
19180
|
var DEFAULT_ENCRYPTION_NETWORK_NAME = "default";
|
|
18721
19181
|
var NETWORK_CREATE_ACTION2 = import_sdk_core7.ENCRYPTION.NETWORK_CREATE;
|
|
18722
19182
|
var DECRYPT_ACTION2 = import_sdk_core7.ENCRYPTION.DECRYPT;
|
|
19183
|
+
var BOOTSTRAP_COMPLETION_MARKER_KEY = "system/bootstrap/complete";
|
|
19184
|
+
var BOOTSTRAP_COMPLETION_MARKER_VERSION = 1;
|
|
19185
|
+
var ACCEPTED_MARKER_VERSIONS = [BOOTSTRAP_COMPLETION_MARKER_VERSION];
|
|
18723
19186
|
var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
|
|
18724
19187
|
function isSecretPayload(value) {
|
|
18725
19188
|
return typeof value === "object" && value !== null && "value" in value && typeof value.value === "string" && "createdAt" in value && typeof value.createdAt === "string" && "updatedAt" in value && typeof value.updatedAt === "string";
|
|
@@ -19030,7 +19493,9 @@ function base64UrlDecode(value) {
|
|
|
19030
19493
|
return new Uint8Array(bytes);
|
|
19031
19494
|
}
|
|
19032
19495
|
async function signJwtInputWithJwk(signingInput, jwk) {
|
|
19033
|
-
|
|
19496
|
+
return signBytesWithJwk(new TextEncoder().encode(signingInput), jwk);
|
|
19497
|
+
}
|
|
19498
|
+
async function signBytesWithJwk(bytes, jwk) {
|
|
19034
19499
|
try {
|
|
19035
19500
|
const subtle = globalThis.crypto?.subtle;
|
|
19036
19501
|
if (!subtle) {
|
|
@@ -19111,6 +19576,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19111
19576
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
19112
19577
|
this._secrets = /* @__PURE__ */ new Map();
|
|
19113
19578
|
this.runtimePermissionGrants = [];
|
|
19579
|
+
/**
|
|
19580
|
+
* Owned space ids this sign-in has already confirmed are hosted. Consulted by
|
|
19581
|
+
* {@link ensureOwnedSpaceHostedById}; cleared on every {@link signIn} because
|
|
19582
|
+
* hosting is confirmed against the session that was active at the time.
|
|
19583
|
+
*/
|
|
19584
|
+
this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
|
|
19114
19585
|
/**
|
|
19115
19586
|
* True when the last signIn() detected an interactive signer and skipped
|
|
19116
19587
|
* client-side bootstrap. Apps can read this to know whether bootstrap was
|
|
@@ -19119,9 +19590,15 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19119
19590
|
this._bootstrapSkipped = false;
|
|
19120
19591
|
/**
|
|
19121
19592
|
* Outcome of the last signIn()'s account-bootstrap attempt. `skipped` is
|
|
19122
|
-
* true when bootstrap did not complete (interactive signer,
|
|
19123
|
-
*
|
|
19124
|
-
*
|
|
19593
|
+
* true when bootstrap did not complete in this invocation (interactive signer,
|
|
19594
|
+
* auto-bootstrap disabled, already provisioned, or a decision/provisioning
|
|
19595
|
+
* step failed). `already-provisioned` means the bootstrap was not run here;
|
|
19596
|
+
* it does not mean a prior provisioning attempt failed to complete.
|
|
19597
|
+
* `reason` carries the cause so apps can surface a "finish account setup"
|
|
19598
|
+
* call-to-action. `warnings` is
|
|
19599
|
+
* present when bootstrap completed, but one or more steps recovered from
|
|
19600
|
+
* an ambiguous failure (e.g. a KV batch write reconciled via per-space
|
|
19601
|
+
* fallback) — clean vs. recovered runs are otherwise byte-identical.
|
|
19125
19602
|
*/
|
|
19126
19603
|
this._bootstrapStatus = {
|
|
19127
19604
|
skipped: false
|
|
@@ -19301,7 +19778,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19301
19778
|
nonce: config.nonce,
|
|
19302
19779
|
siweConfig: config.siweConfig,
|
|
19303
19780
|
manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
|
|
19304
|
-
capabilityRequest: useBootstrapSignInRequest ? import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
|
|
19781
|
+
capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? import_sdk_core7.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
|
|
19305
19782
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
|
|
19306
19783
|
});
|
|
19307
19784
|
this.tc = new import_sdk_core7.TinyCloud(this.auth, {
|
|
@@ -19507,6 +19984,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19507
19984
|
this._spaceService = void 0;
|
|
19508
19985
|
this._serviceContext = void 0;
|
|
19509
19986
|
this.runtimePermissionGrants = [];
|
|
19987
|
+
this.confirmedHostedSpaceIds.clear();
|
|
19510
19988
|
await this.tc.signIn(options);
|
|
19511
19989
|
this.syncResolvedHostFromAuth();
|
|
19512
19990
|
const oldGraph = this._serviceGraph;
|
|
@@ -19543,6 +20021,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19543
20021
|
this._bootstrapSkipped = false;
|
|
19544
20022
|
this._bootstrapStatus = { skipped: false };
|
|
19545
20023
|
if (this.config.autoBootstrapAccount === false) {
|
|
20024
|
+
this._bootstrapStatus = { skipped: true, reason: "auto-bootstrap-disabled" };
|
|
19546
20025
|
return false;
|
|
19547
20026
|
}
|
|
19548
20027
|
if (!this.auth || !this._address) {
|
|
@@ -19557,11 +20036,20 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19557
20036
|
return false;
|
|
19558
20037
|
}
|
|
19559
20038
|
const steps = (0, import_sdk_core7.bootstrapSteps)(this._address, this._chainId);
|
|
19560
|
-
if (!await this.isFreshBootstrapAccount(steps)) {
|
|
19561
|
-
return false;
|
|
19562
|
-
}
|
|
19563
20039
|
try {
|
|
19564
|
-
await this.
|
|
20040
|
+
const decision = await this.resolveBootstrapDecision(steps);
|
|
20041
|
+
if (decision.action === "skip") {
|
|
20042
|
+
this._bootstrapStatus = { skipped: true, reason: "already-provisioned" };
|
|
20043
|
+
return false;
|
|
20044
|
+
}
|
|
20045
|
+
const warnings = await this.runAccountBootstrap(steps, { mode: decision.mode });
|
|
20046
|
+
await this.writeBootstrapCompletionMarker(steps);
|
|
20047
|
+
if (warnings.length > 0) {
|
|
20048
|
+
this._bootstrapStatus = { skipped: false, warnings };
|
|
20049
|
+
this.notificationHandler.warning(
|
|
20050
|
+
`Account bootstrap completed with warnings: ${warnings.map((w) => `${w.stepId} (${w.kind}: ${w.code})`).join("; ")}`
|
|
20051
|
+
);
|
|
20052
|
+
}
|
|
19565
20053
|
} catch (err) {
|
|
19566
20054
|
const reason = err instanceof Error ? err.message : String(err);
|
|
19567
20055
|
this._bootstrapSkipped = true;
|
|
@@ -19574,32 +20062,92 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19574
20062
|
}
|
|
19575
20063
|
return true;
|
|
19576
20064
|
}
|
|
19577
|
-
async
|
|
20065
|
+
async resolveBootstrapDecision(steps) {
|
|
19578
20066
|
const enshrinedSpaceIds = /* @__PURE__ */ new Set();
|
|
19579
20067
|
for (const step of steps) {
|
|
19580
20068
|
if (step.kind === "session") {
|
|
19581
20069
|
enshrinedSpaceIds.add(step.spaceId);
|
|
19582
20070
|
}
|
|
19583
20071
|
}
|
|
19584
|
-
const
|
|
20072
|
+
const auth = this.auth;
|
|
20073
|
+
if (!auth) {
|
|
20074
|
+
throw new Error("Account bootstrap requires an active wallet session");
|
|
20075
|
+
}
|
|
20076
|
+
const skipped = auth.lastActivationSkippedSpaceIds;
|
|
19585
20077
|
if (skipped.some((spaceId) => enshrinedSpaceIds.has(spaceId))) {
|
|
19586
|
-
return
|
|
20078
|
+
return { action: "run", mode: "fresh" };
|
|
19587
20079
|
}
|
|
19588
20080
|
try {
|
|
19589
|
-
const
|
|
19590
|
-
|
|
19591
|
-
|
|
20081
|
+
const accountSpaceId = this.ownedSpaceId(import_sdk_core7.ACCOUNT_REGISTRY_SPACE);
|
|
20082
|
+
const markerPermission = {
|
|
20083
|
+
service: "tinycloud.kv",
|
|
20084
|
+
space: accountSpaceId,
|
|
20085
|
+
path: BOOTSTRAP_COMPLETION_MARKER_KEY,
|
|
20086
|
+
actions: ["get"]
|
|
20087
|
+
};
|
|
20088
|
+
if (!this.hasRuntimePermissions([markerPermission])) {
|
|
20089
|
+
return { action: "run", mode: "repair" };
|
|
19592
20090
|
}
|
|
19593
|
-
|
|
20091
|
+
const marker = await this.readBootstrapCompletionMarker();
|
|
20092
|
+
if (marker.ok) {
|
|
20093
|
+
if (this.isAcceptedBootstrapCompletionMarker(marker.data.data)) {
|
|
20094
|
+
return { action: "skip" };
|
|
20095
|
+
}
|
|
20096
|
+
return { action: "run", mode: "repair" };
|
|
20097
|
+
}
|
|
20098
|
+
if (marker.error.code === import_sdk_core7.ErrorCodes.KV_NOT_FOUND && this.markerReadIsUnhostedSpace(marker.error.meta)) {
|
|
20099
|
+
return { action: "run", mode: "fresh" };
|
|
20100
|
+
}
|
|
20101
|
+
} catch (err) {
|
|
20102
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
20103
|
+
this.notificationHandler.warning(
|
|
20104
|
+
`Account bootstrap decision failed; attempting one bounded repair: ${reason}`
|
|
20105
|
+
);
|
|
20106
|
+
console.warn(`[TinyCloudNode] account bootstrap decision failed: ${reason}`);
|
|
20107
|
+
return { action: "run", mode: "repair" };
|
|
19594
20108
|
}
|
|
19595
|
-
|
|
19596
|
-
|
|
19597
|
-
|
|
19598
|
-
|
|
19599
|
-
|
|
20109
|
+
return { action: "run", mode: "repair" };
|
|
20110
|
+
}
|
|
20111
|
+
async readBootstrapCompletionMarker() {
|
|
20112
|
+
return this.kvForSpace(this.ownedSpaceId(import_sdk_core7.ACCOUNT_REGISTRY_SPACE)).get(
|
|
20113
|
+
BOOTSTRAP_COMPLETION_MARKER_KEY
|
|
20114
|
+
);
|
|
20115
|
+
}
|
|
20116
|
+
async writeBootstrapCompletionMarker(steps) {
|
|
20117
|
+
if (!this._address) {
|
|
20118
|
+
throw new Error("Bootstrap completion marker requires an active wallet session");
|
|
19600
20119
|
}
|
|
20120
|
+
const canonicalStepIds = new Set(
|
|
20121
|
+
(0, import_sdk_core7.bootstrapSteps)(this._address, this._chainId).map((step) => step.id)
|
|
20122
|
+
);
|
|
20123
|
+
const providedStepIds = new Set(steps.map((step) => step.id));
|
|
20124
|
+
if (canonicalStepIds.size !== providedStepIds.size || [...canonicalStepIds].some((stepId) => !providedStepIds.has(stepId))) {
|
|
20125
|
+
throw new Error("Bootstrap completion marker requires the canonical bootstrap step set");
|
|
20126
|
+
}
|
|
20127
|
+
const marker = {
|
|
20128
|
+
v: BOOTSTRAP_COMPLETION_MARKER_VERSION,
|
|
20129
|
+
stepIds: [...canonicalStepIds],
|
|
20130
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
20131
|
+
};
|
|
20132
|
+
const written = await this.kvForSpace(this.ownedSpaceId(import_sdk_core7.ACCOUNT_REGISTRY_SPACE)).put(
|
|
20133
|
+
BOOTSTRAP_COMPLETION_MARKER_KEY,
|
|
20134
|
+
marker
|
|
20135
|
+
);
|
|
20136
|
+
if (!written.ok) {
|
|
20137
|
+
throw new Error(`Failed to write bootstrap completion marker: ${written.error.message}`);
|
|
20138
|
+
}
|
|
20139
|
+
}
|
|
20140
|
+
isAcceptedBootstrapCompletionMarker(value) {
|
|
20141
|
+
if (typeof value !== "object" || value === null) return false;
|
|
20142
|
+
const entries = Object.entries(value);
|
|
20143
|
+
const markerValue = (name) => entries.find(([key2]) => key2 === name)?.[1];
|
|
20144
|
+
const version23 = markerValue("v");
|
|
20145
|
+
return typeof version23 === "number" && Number.isInteger(version23) && ACCEPTED_MARKER_VERSIONS.some((acceptedVersion) => acceptedVersion === version23);
|
|
19601
20146
|
}
|
|
19602
|
-
|
|
20147
|
+
markerReadIsUnhostedSpace(meta) {
|
|
20148
|
+
return meta?.status === 404;
|
|
20149
|
+
}
|
|
20150
|
+
async runAccountBootstrap(steps, { mode } = { mode: "fresh" }) {
|
|
19603
20151
|
if (!this.auth || !this._address) {
|
|
19604
20152
|
throw new Error("Account bootstrap requires an active wallet session");
|
|
19605
20153
|
}
|
|
@@ -19610,6 +20158,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19610
20158
|
const auth = this.auth;
|
|
19611
20159
|
const sessions = /* @__PURE__ */ new Map();
|
|
19612
20160
|
const rawAbilitiesBySpace = /* @__PURE__ */ new Map();
|
|
20161
|
+
const warnings = [];
|
|
19613
20162
|
const primarySession = auth.tinyCloudSession;
|
|
19614
20163
|
const defaultSpaceId = this.ownedSpaceId("default");
|
|
19615
20164
|
const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default;
|
|
@@ -19671,32 +20220,44 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19671
20220
|
}
|
|
19672
20221
|
}
|
|
19673
20222
|
if (step.kind === "seed-spaces") {
|
|
19674
|
-
|
|
19675
|
-
|
|
20223
|
+
const registered = await this.account.spaces.registerBatch(
|
|
20224
|
+
step.spaces.map((space) => ({
|
|
19676
20225
|
spaceId: space.spaceId,
|
|
19677
20226
|
name: space.name,
|
|
19678
20227
|
ownerDid: this.did,
|
|
19679
20228
|
type: "owned",
|
|
19680
20229
|
permissions: ["*"],
|
|
19681
20230
|
status: "active"
|
|
20231
|
+
}))
|
|
20232
|
+
);
|
|
20233
|
+
if (!registered.ok) {
|
|
20234
|
+
throw new Error(
|
|
20235
|
+
`Failed to seed account spaces: ${registered.error.message}`
|
|
20236
|
+
);
|
|
20237
|
+
}
|
|
20238
|
+
const batchError = registered.data.recoveredFromBatchError;
|
|
20239
|
+
if (batchError) {
|
|
20240
|
+
warnings.push({
|
|
20241
|
+
stepId: step.id,
|
|
20242
|
+
kind: "batch-write-reconciled",
|
|
20243
|
+
code: batchError.code,
|
|
20244
|
+
message: batchError.message
|
|
19682
20245
|
});
|
|
19683
|
-
if (!registered.ok) {
|
|
19684
|
-
throw new Error(
|
|
19685
|
-
`Failed to seed account space ${space.spaceId}: ${registered.error.message}`
|
|
19686
|
-
);
|
|
19687
|
-
}
|
|
19688
20246
|
}
|
|
19689
20247
|
}
|
|
19690
20248
|
if (step.kind === "seed-applications") {
|
|
19691
20249
|
const registered = await this.account.applications.register(
|
|
19692
|
-
step.manifests.length > 0 ? [...step.manifests] : import_sdk_core7.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST
|
|
20250
|
+
step.manifests.length > 0 ? [...step.manifests] : import_sdk_core7.TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
|
|
20251
|
+
// The write is an INSERT OR REPLACE and is safe to repeat during a
|
|
20252
|
+
// repair, so skip the manifest-hash pre-read.
|
|
20253
|
+
{ assumeUnregistered: true }
|
|
19693
20254
|
);
|
|
19694
20255
|
if (!registered.ok) {
|
|
19695
20256
|
throw new Error(`Failed to seed bootstrap applications: ${registered.error.message}`);
|
|
19696
20257
|
}
|
|
19697
20258
|
}
|
|
19698
20259
|
if (step.kind === "encryption-network-create") {
|
|
19699
|
-
await this.ensureEncryptionNetwork(step.networkId);
|
|
20260
|
+
await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: mode === "fresh" });
|
|
19700
20261
|
}
|
|
19701
20262
|
if (step.kind === "secret-records-schema") {
|
|
19702
20263
|
const db = this.sqlForSpace(step.spaceId).db(step.database);
|
|
@@ -19716,6 +20277,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19716
20277
|
}
|
|
19717
20278
|
}
|
|
19718
20279
|
}
|
|
20280
|
+
return warnings;
|
|
19719
20281
|
}
|
|
19720
20282
|
registerBootstrapRuntimeGrant(session, request, rawAbilities) {
|
|
19721
20283
|
const operations = [];
|
|
@@ -19975,6 +20537,22 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19975
20537
|
await this.ensureEncryptionNetwork(networkId);
|
|
19976
20538
|
}
|
|
19977
20539
|
}
|
|
20540
|
+
/**
|
|
20541
|
+
* Ensure one of this user's owned spaces is hosted, at most once per space
|
|
20542
|
+
* per sign-in.
|
|
20543
|
+
*
|
|
20544
|
+
* Account bootstrap funnels six calls through here (via
|
|
20545
|
+
* `AccountService.ensureAccountSpaceHosted`), and each one re-submits
|
|
20546
|
+
* `activateSessionWithHost` with the *primary* session. That session's recap
|
|
20547
|
+
* covers only `default` and `secrets`, so the account space appears in
|
|
20548
|
+
* neither `activated` nor `skipped` and the guard below returns immediately
|
|
20549
|
+
* — the repeat calls cannot change the outcome, they only cost round trips.
|
|
20550
|
+
*
|
|
20551
|
+
* The memo is per sign-in ({@link confirmedHostedSpaceIds} is cleared in
|
|
20552
|
+
* `signIn`) and only records confirmed successes. A wrong skip is
|
|
20553
|
+
* self-revealing: the next write to the space fails immediately and loudly
|
|
20554
|
+
* with `404 Space not found`.
|
|
20555
|
+
*/
|
|
19978
20556
|
async ensureOwnedSpaceHostedById(spaceId) {
|
|
19979
20557
|
if (!this.auth) {
|
|
19980
20558
|
throw new Error("Owned space hosting requires wallet mode");
|
|
@@ -19983,12 +20561,16 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19983
20561
|
if (!session) {
|
|
19984
20562
|
throw new Error("Owned space hosting requires an active session");
|
|
19985
20563
|
}
|
|
20564
|
+
if (this.confirmedHostedSpaceIds.has(spaceId)) {
|
|
20565
|
+
return;
|
|
20566
|
+
}
|
|
19986
20567
|
const host = this.hosts[0] ?? this.config.host;
|
|
19987
20568
|
if (!host) {
|
|
19988
20569
|
throw new Error("Owned space hosting requires a TinyCloud host");
|
|
19989
20570
|
}
|
|
19990
20571
|
const activation = await (0, import_sdk_core7.activateSessionWithHost)(host, session.delegationHeader);
|
|
19991
20572
|
if (activation.success && !activation.skipped?.includes(spaceId)) {
|
|
20573
|
+
this.confirmedHostedSpaceIds.add(spaceId);
|
|
19992
20574
|
return;
|
|
19993
20575
|
}
|
|
19994
20576
|
if (!activation.success && activation.status !== 404) {
|
|
@@ -20007,6 +20589,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20007
20589
|
`Failed to activate session after creating owned space ${spaceId}: ${retry.error ?? "space was skipped"}`
|
|
20008
20590
|
);
|
|
20009
20591
|
}
|
|
20592
|
+
this.confirmedHostedSpaceIds.add(spaceId);
|
|
20010
20593
|
}
|
|
20011
20594
|
/**
|
|
20012
20595
|
* Host one of this user's owned spaces by name (e.g. `"applications"`).
|
|
@@ -20322,6 +20905,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20322
20905
|
this._delegationManager = stagedGraph.delegationManager;
|
|
20323
20906
|
this._spaceService = stagedGraph.spaceService;
|
|
20324
20907
|
this._serviceGraph = stagedGraph.graph;
|
|
20908
|
+
this._activeServiceSession = serviceSession;
|
|
20325
20909
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
20326
20910
|
this._secrets = /* @__PURE__ */ new Map();
|
|
20327
20911
|
this._publicKV = void 0;
|
|
@@ -20329,6 +20913,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20329
20913
|
this.tc = stagedGraph.core;
|
|
20330
20914
|
this._recapOperationsCache = stagedPrimary.cache;
|
|
20331
20915
|
this.runtimePermissionGrants = stagedPrimary.grants;
|
|
20916
|
+
this.confirmedHostedSpaceIds.clear();
|
|
20332
20917
|
if (this.auth) {
|
|
20333
20918
|
this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost]);
|
|
20334
20919
|
this._restoredTcSession = void 0;
|
|
@@ -20638,6 +21223,28 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20638
21223
|
currentTinyCloudSession() {
|
|
20639
21224
|
return this.auth?.tinyCloudSession ?? this._restoredTcSession;
|
|
20640
21225
|
}
|
|
21226
|
+
/**
|
|
21227
|
+
* Mint a multi-capability invocation through the established session key.
|
|
21228
|
+
* Callers receive only the signed headers; session key material remains
|
|
21229
|
+
* owned by the session manager and is never part of the caller contract.
|
|
21230
|
+
*/
|
|
21231
|
+
invokeAny(entries, facts) {
|
|
21232
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
21233
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
21234
|
+
return this._serviceGraph.invokeAny(session, entries, facts);
|
|
21235
|
+
}
|
|
21236
|
+
/** Sign protocol bytes with the established in-memory session key. */
|
|
21237
|
+
async signSessionBytes(bytes) {
|
|
21238
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
21239
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
21240
|
+
return signBytesWithJwk(bytes, session.jwk);
|
|
21241
|
+
}
|
|
21242
|
+
/** Bind a session-signed invocation to the canonical service audience. */
|
|
21243
|
+
async bindInvocationAudience(authorization, audience) {
|
|
21244
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
21245
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
21246
|
+
return rewriteInvocationAudience(authorization, audience, session.jwk);
|
|
21247
|
+
}
|
|
20641
21248
|
/**
|
|
20642
21249
|
* Connect a wallet to upgrade from session-only mode to wallet mode.
|
|
20643
21250
|
*
|
|
@@ -20699,7 +21306,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20699
21306
|
nonce: this.config.nonce,
|
|
20700
21307
|
siweConfig: this.config.siweConfig,
|
|
20701
21308
|
manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
|
|
20702
|
-
capabilityRequest: useBootstrapSignInRequest ? import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
21309
|
+
capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? import_sdk_core7.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
20703
21310
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
|
|
20704
21311
|
});
|
|
20705
21312
|
this.tc = new import_sdk_core7.TinyCloud(this.auth, {
|
|
@@ -20752,7 +21359,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20752
21359
|
nonce: this.config.nonce,
|
|
20753
21360
|
siweConfig: this.config.siweConfig,
|
|
20754
21361
|
manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
|
|
20755
|
-
capabilityRequest: useBootstrapSignInRequest ? import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
21362
|
+
capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? import_sdk_core7.BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : import_sdk_core7.BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
20756
21363
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
|
|
20757
21364
|
});
|
|
20758
21365
|
this.tc = new import_sdk_core7.TinyCloud(this.auth, {
|
|
@@ -20819,6 +21426,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20819
21426
|
if (this._serviceContext) {
|
|
20820
21427
|
const spaceScopedContext = this._serviceGraph.track(new import_sdk_core7.ServiceContext({
|
|
20821
21428
|
invoke: this._serviceContext.invoke,
|
|
21429
|
+
invokeAny: this._serviceContext.invokeAny,
|
|
20822
21430
|
fetch: this._serviceContext.fetch,
|
|
20823
21431
|
hosts: this._serviceContext.hosts,
|
|
20824
21432
|
telemetry: this.config.telemetry
|
|
@@ -20911,7 +21519,17 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20911
21519
|
verifyNodeSignature: (nodeId, message, signature2) => (0, import_sdk_core7.verifyDidKeyEd25519Signature)(nodeId, message, signature2)
|
|
20912
21520
|
};
|
|
20913
21521
|
}
|
|
21522
|
+
/**
|
|
21523
|
+
* The node DID for {@link config.host}.
|
|
21524
|
+
*
|
|
21525
|
+
* Sign-in already performs `GET /info` for the protocol-version check, and
|
|
21526
|
+
* that response carries `nodeId`, so reuse it rather than fetching `/info` a
|
|
21527
|
+
* second time. Only a value recorded for this exact host qualifies; older
|
|
21528
|
+
* nodes that omit `nodeId` fall through to the fetch.
|
|
21529
|
+
*/
|
|
20914
21530
|
async fetchNodeId() {
|
|
21531
|
+
const cached = this.auth?.nodeIdForHost(this.config.host);
|
|
21532
|
+
if (cached) return cached;
|
|
20915
21533
|
const response = await fetch(`${this.config.host}/info`);
|
|
20916
21534
|
if (!response.ok) {
|
|
20917
21535
|
throw new Error(`Failed to fetch node info: HTTP ${response.status}`);
|
|
@@ -21474,6 +22092,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21474
22092
|
shareUrl: input.shareUrl,
|
|
21475
22093
|
documentName: input.documentName,
|
|
21476
22094
|
jti: base64UrlEncode(crypto.getRandomValues(new Uint8Array(16))),
|
|
22095
|
+
idempotencyKey: input.idempotencyKey,
|
|
21477
22096
|
expiresAt: input.expiresAt
|
|
21478
22097
|
};
|
|
21479
22098
|
const requestBodyDigest = base64UrlEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode((0, import_sdk_core7.canonicalizeEncryptionJson)(body)))));
|
|
@@ -21683,6 +22302,13 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21683
22302
|
body
|
|
21684
22303
|
)
|
|
21685
22304
|
});
|
|
22305
|
+
if (response.status === 409) {
|
|
22306
|
+
const existing = await this.getEncryptionNetwork(networkId);
|
|
22307
|
+
if (existing) return existing;
|
|
22308
|
+
throw new Error(
|
|
22309
|
+
`Encryption network ${networkId} already exists but could not be read back`
|
|
22310
|
+
);
|
|
22311
|
+
}
|
|
21686
22312
|
if (!response.ok) {
|
|
21687
22313
|
throw new Error(
|
|
21688
22314
|
`Failed to create encryption network ${networkId}: HTTP ${response.status} ${await response.text()}`
|
|
@@ -21691,11 +22317,19 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21691
22317
|
const created = await response.json();
|
|
21692
22318
|
return created.descriptor;
|
|
21693
22319
|
}
|
|
21694
|
-
|
|
22320
|
+
/**
|
|
22321
|
+
* Ensure an encryption network exists, creating it if necessary.
|
|
22322
|
+
*
|
|
22323
|
+
* @param nameOrNetworkId - Network name or full `urn:tinycloud:encryption:` id.
|
|
22324
|
+
* @param options - See {@link EnsureEncryptionNetworkOptions}.
|
|
22325
|
+
*/
|
|
22326
|
+
async ensureEncryptionNetwork(nameOrNetworkId = DEFAULT_ENCRYPTION_NETWORK_NAME, options = {}) {
|
|
21695
22327
|
const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
|
|
21696
|
-
|
|
21697
|
-
|
|
21698
|
-
|
|
22328
|
+
if (!options.assumeMissing) {
|
|
22329
|
+
const existing = await this.getEncryptionNetwork(networkId);
|
|
22330
|
+
if (existing) {
|
|
22331
|
+
return existing;
|
|
22332
|
+
}
|
|
21699
22333
|
}
|
|
21700
22334
|
const parsed = (0, import_sdk_core7.parseNetworkId)(networkId);
|
|
21701
22335
|
if (!didPrincipalMatches2(parsed.ownerDid, this.did)) {
|
|
@@ -23474,35 +24108,44 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23474
24108
|
if (parentDelegation.disableSubDelegation) {
|
|
23475
24109
|
throw new Error("Parent delegation does not allow sub-delegation");
|
|
23476
24110
|
}
|
|
23477
|
-
|
|
24111
|
+
const parentResources = parentDelegation.resources ?? [{
|
|
24112
|
+
service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
|
|
24113
|
+
space: parentDelegation.spaceId,
|
|
24114
|
+
path: parentDelegation.path,
|
|
24115
|
+
actions: parentDelegation.actions
|
|
24116
|
+
}];
|
|
24117
|
+
const requestedResources = params.resources ?? [{
|
|
24118
|
+
service: params.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
|
|
24119
|
+
space: parentDelegation.spaceId,
|
|
24120
|
+
path: params.path,
|
|
24121
|
+
actions: params.actions
|
|
24122
|
+
}];
|
|
24123
|
+
const resourceIsContained = (parent, child) => {
|
|
24124
|
+
const parentEncryption = parent.service === "encryption" || parent.service === "tinycloud.encryption";
|
|
24125
|
+
const childEncryption = child.service === "encryption" || child.service === "tinycloud.encryption";
|
|
24126
|
+
if (parentEncryption || childEncryption) return parentEncryption && childEncryption && parent.path === child.path && child.actions.every((action) => parent.actions.includes(action));
|
|
24127
|
+
const pathContained = parent.path === child.path || parent.path.endsWith("/") && child.path.startsWith(parent.path) || child.path.startsWith(`${parent.path}/`);
|
|
24128
|
+
return pathContained && child.actions.every((action) => parent.actions.includes(action));
|
|
24129
|
+
};
|
|
24130
|
+
if (requestedResources.length === 0 || requestedResources.some((child) => !parentResources.some((parent) => resourceIsContained(parent, child)))) {
|
|
23478
24131
|
throw new Error(
|
|
23479
|
-
|
|
24132
|
+
"Sub-delegation resources exceed the received delegation"
|
|
23480
24133
|
);
|
|
23481
24134
|
}
|
|
23482
|
-
const parentActions = new Set(parentDelegation.actions);
|
|
23483
|
-
for (const action of params.actions) {
|
|
23484
|
-
if (!parentActions.has(action)) {
|
|
23485
|
-
throw new Error(
|
|
23486
|
-
`Sub-delegation action "${action}" is not in parent's actions: ${parentDelegation.actions.join(", ")}`
|
|
23487
|
-
);
|
|
23488
|
-
}
|
|
23489
|
-
}
|
|
23490
24135
|
const now = /* @__PURE__ */ new Date();
|
|
23491
24136
|
const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
|
|
23492
24137
|
const requestedExpiry = new Date(now.getTime() + expiryMs);
|
|
23493
24138
|
const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
|
|
23494
24139
|
const abilities = {};
|
|
23495
|
-
const
|
|
23496
|
-
const
|
|
23497
|
-
|
|
23498
|
-
|
|
23499
|
-
|
|
23500
|
-
|
|
23501
|
-
|
|
23502
|
-
abilities
|
|
23503
|
-
|
|
23504
|
-
if (duckdbActions.length > 0) {
|
|
23505
|
-
abilities.duckdb = { [params.path]: duckdbActions };
|
|
24140
|
+
const rawAbilities = {};
|
|
24141
|
+
for (const resource of requestedResources) {
|
|
24142
|
+
const service = resource.service.replace(/^tinycloud\./, "");
|
|
24143
|
+
if (service === "encryption") {
|
|
24144
|
+
rawAbilities[resource.path] = [...resource.actions];
|
|
24145
|
+
continue;
|
|
24146
|
+
}
|
|
24147
|
+
abilities[service] ?? (abilities[service] = {});
|
|
24148
|
+
abilities[service][resource.path] = [...resource.actions];
|
|
23506
24149
|
}
|
|
23507
24150
|
const targetHost = parentDelegation.host ?? this.config.host;
|
|
23508
24151
|
const prepared = this.wasmBindings.prepareSession({
|
|
@@ -23514,7 +24157,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23514
24157
|
expirationTime: actualExpiry.toISOString(),
|
|
23515
24158
|
spaceId: parentDelegation.spaceId,
|
|
23516
24159
|
delegateUri: params.delegateDID,
|
|
23517
|
-
parents: [parentDelegation.cid]
|
|
24160
|
+
parents: [parentDelegation.cid],
|
|
24161
|
+
...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
|
|
23518
24162
|
});
|
|
23519
24163
|
const signature2 = await this.signer.signMessage(prepared.siwe);
|
|
23520
24164
|
const subDelegationSession = this.wasmBindings.completeSessionSetup({
|
|
@@ -23532,14 +24176,20 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23532
24176
|
cid: subDelegationSession.delegationCid,
|
|
23533
24177
|
delegationHeader: subDelegationSession.delegationHeader,
|
|
23534
24178
|
spaceId: parentDelegation.spaceId,
|
|
23535
|
-
path:
|
|
23536
|
-
actions:
|
|
24179
|
+
path: requestedResources[0].path,
|
|
24180
|
+
actions: requestedResources[0].actions,
|
|
23537
24181
|
disableSubDelegation: params.disableSubDelegation ?? false,
|
|
23538
24182
|
expiry: actualExpiry,
|
|
23539
24183
|
delegateDID: params.delegateDID,
|
|
23540
24184
|
ownerAddress: parentDelegation.ownerAddress,
|
|
23541
24185
|
chainId: parentDelegation.chainId,
|
|
23542
|
-
host: targetHost
|
|
24186
|
+
host: targetHost,
|
|
24187
|
+
resources: requestedResources.map((resource) => ({
|
|
24188
|
+
service: resource.service.replace(/^tinycloud\./, ""),
|
|
24189
|
+
space: resource.service.replace(/^tinycloud\./, "") === "encryption" ? "encryption" : resource.space ?? parentDelegation.spaceId,
|
|
24190
|
+
path: resource.path,
|
|
24191
|
+
actions: [...resource.actions]
|
|
24192
|
+
}))
|
|
23543
24193
|
};
|
|
23544
24194
|
}
|
|
23545
24195
|
};
|
|
@@ -23686,6 +24336,50 @@ var FileSessionStorage = class {
|
|
|
23686
24336
|
}
|
|
23687
24337
|
};
|
|
23688
24338
|
|
|
24339
|
+
// src/authorization/openKeyBridge.ts
|
|
24340
|
+
function wireOpenKeyAuthorize(openkey) {
|
|
24341
|
+
return async (input) => {
|
|
24342
|
+
const result = await openkey.authorizeTinyCloud({
|
|
24343
|
+
protocolVersion: 1,
|
|
24344
|
+
siwe: input.siwe,
|
|
24345
|
+
keyId: input.keyId,
|
|
24346
|
+
jwk: input.jwk,
|
|
24347
|
+
host: input.host,
|
|
24348
|
+
// Forward the presentation envelope verbatim — the OpenKey SDK
|
|
24349
|
+
// validates and size-bounds it before use.
|
|
24350
|
+
presentation: input.presentation
|
|
24351
|
+
});
|
|
24352
|
+
if (result.protocolVersion !== 1) {
|
|
24353
|
+
throw new Error(
|
|
24354
|
+
`wireOpenKeyAuthorize: OpenKey returned unsupported protocolVersion ${result.protocolVersion}`
|
|
24355
|
+
);
|
|
24356
|
+
}
|
|
24357
|
+
if (typeof result.signedMessage !== "string" || !result.signedMessage) {
|
|
24358
|
+
throw new Error(
|
|
24359
|
+
"wireOpenKeyAuthorize: OpenKey returned no signedMessage \u2014 cannot complete a TinyCloud session"
|
|
24360
|
+
);
|
|
24361
|
+
}
|
|
24362
|
+
if (!Array.isArray(result.selectedActionKeys)) {
|
|
24363
|
+
throw new Error(
|
|
24364
|
+
"wireOpenKeyAuthorize: OpenKey returned no selectedActionKeys"
|
|
24365
|
+
);
|
|
24366
|
+
}
|
|
24367
|
+
if (!Array.isArray(result.permissions)) {
|
|
24368
|
+
throw new Error(
|
|
24369
|
+
"wireOpenKeyAuthorize: OpenKey returned no permissions"
|
|
24370
|
+
);
|
|
24371
|
+
}
|
|
24372
|
+
return {
|
|
24373
|
+
protocolVersion: 1,
|
|
24374
|
+
address: result.address,
|
|
24375
|
+
signature: result.signature,
|
|
24376
|
+
signedMessage: result.signedMessage,
|
|
24377
|
+
selectedActionKeys: result.selectedActionKeys,
|
|
24378
|
+
permissions: result.permissions
|
|
24379
|
+
};
|
|
24380
|
+
};
|
|
24381
|
+
}
|
|
24382
|
+
|
|
23689
24383
|
// src/index.ts
|
|
23690
24384
|
var import_sdk_core12 = require("@tinycloud/sdk-core");
|
|
23691
24385
|
|
|
@@ -24019,10 +24713,13 @@ var import_sdk_core22 = require("@tinycloud/sdk-core");
|
|
|
24019
24713
|
var import_sdk_core23 = require("@tinycloud/sdk-core");
|
|
24020
24714
|
// Annotate the CommonJS export names for ESM import in node:
|
|
24021
24715
|
0 && (module.exports = {
|
|
24716
|
+
ACCEPTED_MARKER_VERSIONS,
|
|
24022
24717
|
ACCOUNT_REGISTRY_PATH,
|
|
24023
24718
|
ACCOUNT_REGISTRY_SPACE,
|
|
24024
24719
|
AccountService,
|
|
24025
24720
|
AutoApproveSpaceCreationHandler,
|
|
24721
|
+
BOOTSTRAP_COMPLETION_MARKER_KEY,
|
|
24722
|
+
BOOTSTRAP_COMPLETION_MARKER_VERSION,
|
|
24026
24723
|
CapabilityKeyRegistry,
|
|
24027
24724
|
CapabilityKeyRegistryErrorCodes,
|
|
24028
24725
|
CaveatedDelegationUnsupportedError,
|
|
@@ -24159,7 +24856,8 @@ var import_sdk_core23 = require("@tinycloud/sdk-core");
|
|
|
24159
24856
|
tinyCloudDebugLogger,
|
|
24160
24857
|
validateEnvelope,
|
|
24161
24858
|
validateManifest,
|
|
24162
|
-
verifyDecryptResponse
|
|
24859
|
+
verifyDecryptResponse,
|
|
24860
|
+
wireOpenKeyAuthorize
|
|
24163
24861
|
});
|
|
24164
24862
|
/*! Bundled license information:
|
|
24165
24863
|
|