@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.js
CHANGED
|
@@ -17179,12 +17179,14 @@ import {
|
|
|
17179
17179
|
SharingService,
|
|
17180
17180
|
UnsupportedFeatureError,
|
|
17181
17181
|
makePublicSpaceId,
|
|
17182
|
-
ACCOUNT_REGISTRY_SPACE,
|
|
17182
|
+
ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE2,
|
|
17183
|
+
BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST,
|
|
17183
17184
|
BOOTSTRAP_SESSION_REQUESTS,
|
|
17184
17185
|
SECRET_RECORDS_SCHEMA,
|
|
17185
17186
|
SECRETS_SPACE as SECRETS_SPACE2,
|
|
17186
17187
|
TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
|
|
17187
17188
|
bootstrapSteps,
|
|
17189
|
+
ErrorCodes as ErrorCodes2,
|
|
17188
17190
|
ENCRYPTION_PERMISSION_SERVICE as ENCRYPTION_PERMISSION_SERVICE2,
|
|
17189
17191
|
CaveatedDelegationUnsupportedError,
|
|
17190
17192
|
PermissionNotInManifestError,
|
|
@@ -17218,6 +17220,8 @@ import {
|
|
|
17218
17220
|
} from "@tinycloud/sdk-services";
|
|
17219
17221
|
|
|
17220
17222
|
// src/authorization/NodeUserAuthorization.ts
|
|
17223
|
+
import { createHash } from "crypto";
|
|
17224
|
+
import { SiweMessage } from "siwe";
|
|
17221
17225
|
import {
|
|
17222
17226
|
fetchPeerId,
|
|
17223
17227
|
submitHostDelegation,
|
|
@@ -17236,6 +17240,13 @@ import {
|
|
|
17236
17240
|
canonicalizeAddress,
|
|
17237
17241
|
makePkhSpaceId,
|
|
17238
17242
|
pkhDid,
|
|
17243
|
+
extractImmutableSiweFields,
|
|
17244
|
+
diffImmutableSiweFields,
|
|
17245
|
+
extractRecapAttenuations,
|
|
17246
|
+
unauthorizedRecapCapabilities,
|
|
17247
|
+
parseCanonicalRecapResource,
|
|
17248
|
+
ACCOUNT_REGISTRY_SPACE,
|
|
17249
|
+
ACCOUNT_MANIFEST_PERMISSIONS,
|
|
17239
17250
|
KV,
|
|
17240
17251
|
SQL,
|
|
17241
17252
|
DUCKDB,
|
|
@@ -17322,6 +17333,27 @@ var MemorySessionStorage = class {
|
|
|
17322
17333
|
// src/authorization/NodeUserAuthorization.ts
|
|
17323
17334
|
var DECRYPT_ACTION = ENCRYPTION.DECRYPT;
|
|
17324
17335
|
var NETWORK_CREATE_ACTION = ENCRYPTION.NETWORK_CREATE;
|
|
17336
|
+
function canonicalizeOpenKeyManifestJson(value) {
|
|
17337
|
+
const json = JSON.stringify(value);
|
|
17338
|
+
if (json === void 0) {
|
|
17339
|
+
throw new TypeError("manifest is not JSON-serializable");
|
|
17340
|
+
}
|
|
17341
|
+
return canonicalizeJsonValue(JSON.parse(json));
|
|
17342
|
+
}
|
|
17343
|
+
function canonicalizeJsonValue(value) {
|
|
17344
|
+
if (value === null || typeof value !== "object") {
|
|
17345
|
+
return JSON.stringify(value);
|
|
17346
|
+
}
|
|
17347
|
+
if (Array.isArray(value)) {
|
|
17348
|
+
return `[${value.map(canonicalizeJsonValue).join(",")}]`;
|
|
17349
|
+
}
|
|
17350
|
+
return `{${Object.keys(value).sort().map(
|
|
17351
|
+
(key2) => `${JSON.stringify(key2)}:${canonicalizeJsonValue(value[key2])}`
|
|
17352
|
+
).join(",")}}`;
|
|
17353
|
+
}
|
|
17354
|
+
function canonicalOpenKeyManifestSha256Hex(value) {
|
|
17355
|
+
return createHash("sha256").update(canonicalizeOpenKeyManifestJson(value)).digest("hex");
|
|
17356
|
+
}
|
|
17325
17357
|
function didPrincipalMatches(actual, expected) {
|
|
17326
17358
|
try {
|
|
17327
17359
|
return principalDidEquals(actual, expected);
|
|
@@ -17339,6 +17371,16 @@ function addRawAbility(rawAbilities, resource, action) {
|
|
|
17339
17371
|
actions.push(action);
|
|
17340
17372
|
}
|
|
17341
17373
|
}
|
|
17374
|
+
function cloneAbilitiesMap(actions) {
|
|
17375
|
+
return Object.fromEntries(
|
|
17376
|
+
Object.entries(actions).map(([service, paths]) => [
|
|
17377
|
+
service,
|
|
17378
|
+
Object.fromEntries(
|
|
17379
|
+
Object.entries(paths).map(([path, pathActions]) => [path, [...pathActions]])
|
|
17380
|
+
)
|
|
17381
|
+
])
|
|
17382
|
+
);
|
|
17383
|
+
}
|
|
17342
17384
|
var NodeUserAuthorization = class {
|
|
17343
17385
|
constructor(config) {
|
|
17344
17386
|
this.activeSessionKeyId = "default";
|
|
@@ -17542,6 +17584,15 @@ var NodeUserAuthorization = class {
|
|
|
17542
17584
|
get nodeFeatures() {
|
|
17543
17585
|
return this._nodeFeatures;
|
|
17544
17586
|
}
|
|
17587
|
+
/**
|
|
17588
|
+
* The node DID reported by `GET /info` for `host`, if the sign-in (or
|
|
17589
|
+
* restore) that populated it targeted that same host. Returns `undefined`
|
|
17590
|
+
* for any other host, and for nodes whose `/info` omits `nodeId`, so callers
|
|
17591
|
+
* fall back to fetching it themselves rather than using a foreign node's DID.
|
|
17592
|
+
*/
|
|
17593
|
+
nodeIdForHost(host) {
|
|
17594
|
+
return this._nodeInfoIdentity?.host === host ? this._nodeInfoIdentity.nodeId : void 0;
|
|
17595
|
+
}
|
|
17545
17596
|
get lastActivationSkippedSpaceIds() {
|
|
17546
17597
|
return [...this._lastActivationSkippedSpaceIds];
|
|
17547
17598
|
}
|
|
@@ -17601,23 +17652,41 @@ var NodeUserAuthorization = class {
|
|
|
17601
17652
|
const defaultNetworkId = this.defaultEncryptionNetworkId(address, chainId);
|
|
17602
17653
|
const primarySpaceId2 = makePkhSpaceId(address, chainId, this.spacePrefix);
|
|
17603
17654
|
const secretsSpaceId = makePkhSpaceId(address, chainId, "secrets");
|
|
17604
|
-
|
|
17605
|
-
|
|
17606
|
-
|
|
17607
|
-
|
|
17608
|
-
|
|
17609
|
-
|
|
17610
|
-
|
|
17611
|
-
|
|
17612
|
-
|
|
17613
|
-
|
|
17614
|
-
|
|
17615
|
-
|
|
17616
|
-
|
|
17617
|
-
|
|
17655
|
+
const primaryActions = this.includeAccountRegistryPermissions && this.spacePrefix === ACCOUNT_REGISTRY_SPACE ? cloneAbilitiesMap(this.defaultActions) : this.defaultActions;
|
|
17656
|
+
const spaceAbilities2 = {
|
|
17657
|
+
[primarySpaceId2]: primaryActions,
|
|
17658
|
+
[secretsSpaceId]: {
|
|
17659
|
+
kv: {
|
|
17660
|
+
"vault/secrets/": [
|
|
17661
|
+
KV.GET,
|
|
17662
|
+
KV.PUT,
|
|
17663
|
+
KV.DEL,
|
|
17664
|
+
KV.LIST,
|
|
17665
|
+
KV.METADATA
|
|
17666
|
+
]
|
|
17667
|
+
}
|
|
17668
|
+
}
|
|
17669
|
+
};
|
|
17670
|
+
if (this.includeAccountRegistryPermissions && this.spacePrefix !== ACCOUNT_REGISTRY_SPACE) {
|
|
17671
|
+
spaceAbilities2[makePkhSpaceId(address, chainId, ACCOUNT_REGISTRY_SPACE)] = resourceCapabilitiesToAbilitiesMap(ACCOUNT_MANIFEST_PERMISSIONS);
|
|
17672
|
+
} else if (this.includeAccountRegistryPermissions) {
|
|
17673
|
+
const accountAbilities = resourceCapabilitiesToAbilitiesMap(
|
|
17674
|
+
ACCOUNT_MANIFEST_PERMISSIONS
|
|
17675
|
+
);
|
|
17676
|
+
for (const [service, paths] of Object.entries(accountAbilities)) {
|
|
17677
|
+
const existingPaths = primaryActions[service] ?? (primaryActions[service] = {});
|
|
17678
|
+
for (const [path, actions] of Object.entries(paths)) {
|
|
17679
|
+
const existingActions = existingPaths[path] ?? (existingPaths[path] = []);
|
|
17680
|
+
for (const action of actions) {
|
|
17681
|
+
if (!existingActions.includes(action)) existingActions.push(action);
|
|
17618
17682
|
}
|
|
17619
17683
|
}
|
|
17620
|
-
}
|
|
17684
|
+
}
|
|
17685
|
+
}
|
|
17686
|
+
return {
|
|
17687
|
+
abilities: primaryActions,
|
|
17688
|
+
spaceId: primarySpaceId2,
|
|
17689
|
+
spaceAbilities: spaceAbilities2,
|
|
17621
17690
|
rawAbilities: {
|
|
17622
17691
|
[defaultNetworkId]: [DECRYPT_ACTION, NETWORK_CREATE_ACTION]
|
|
17623
17692
|
}
|
|
@@ -18011,11 +18080,13 @@ var NodeUserAuthorization = class {
|
|
|
18011
18080
|
this._tinyCloudSession = tinyCloudSession;
|
|
18012
18081
|
this._address = address;
|
|
18013
18082
|
this._chainId = chainId;
|
|
18083
|
+
const infoHost = this.primaryTinyCloudHost;
|
|
18014
18084
|
const nodeInfo = await checkNodeInfo(
|
|
18015
|
-
|
|
18085
|
+
infoHost,
|
|
18016
18086
|
this.wasm.protocolVersion()
|
|
18017
18087
|
);
|
|
18018
18088
|
this._nodeFeatures = nodeInfo.features;
|
|
18089
|
+
this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
|
|
18019
18090
|
for (const ext of this.extensions) {
|
|
18020
18091
|
if (ext.afterSignIn) {
|
|
18021
18092
|
await ext.afterSignIn(clientSession);
|
|
@@ -18181,11 +18252,13 @@ var NodeUserAuthorization = class {
|
|
|
18181
18252
|
this._tinyCloudSession = tinyCloudSession;
|
|
18182
18253
|
this._address = address;
|
|
18183
18254
|
this._chainId = chainId;
|
|
18255
|
+
const infoHost = this.primaryTinyCloudHost;
|
|
18184
18256
|
const nodeInfo = await checkNodeInfo(
|
|
18185
|
-
|
|
18257
|
+
infoHost,
|
|
18186
18258
|
this.wasm.protocolVersion()
|
|
18187
18259
|
);
|
|
18188
18260
|
this._nodeFeatures = nodeInfo.features;
|
|
18261
|
+
this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
|
|
18189
18262
|
for (const ext of this.extensions) {
|
|
18190
18263
|
if (ext.afterSignIn) {
|
|
18191
18264
|
await ext.afterSignIn(clientSession);
|
|
@@ -18194,6 +18267,398 @@ var NodeUserAuthorization = class {
|
|
|
18194
18267
|
await this.ensureSpaceExists();
|
|
18195
18268
|
return clientSession;
|
|
18196
18269
|
}
|
|
18270
|
+
/**
|
|
18271
|
+
* End-to-end sign-in via OpenKey `authorizeTinyCloud()` (Sol CRITICAL-4).
|
|
18272
|
+
*
|
|
18273
|
+
* This is the production entry point that wires
|
|
18274
|
+
* {@link prepareSessionForSigning} → OpenKey `authorizeTinyCloud()` →
|
|
18275
|
+
* {@link signInWithOpenKeyResult} into one call. Consumers pass in a
|
|
18276
|
+
* caller-provided `authorizeFn` (typically a thin wrapper around
|
|
18277
|
+
* `openkey.authorizeTinyCloud()`) so this method has no direct
|
|
18278
|
+
* dependency on the OpenKey SDK — it works with any transport that
|
|
18279
|
+
* conforms to the versioned protocol.
|
|
18280
|
+
*
|
|
18281
|
+
* The `authorizeFn` MUST:
|
|
18282
|
+
* 1. Send the prepared SIWE bytes to OpenKey.
|
|
18283
|
+
* 2. Return the exact `TinyCloudAuthorizationResultV1` shape (with
|
|
18284
|
+
* `signedMessage` — the bytes the signature actually verifies
|
|
18285
|
+
* against — plus `selectedActionKeys` and `permissions`).
|
|
18286
|
+
*
|
|
18287
|
+
* NodeUserAuthorization then completes the session with `signedMessage`
|
|
18288
|
+
* (never the prepared bytes), verifying the signature, the immutable
|
|
18289
|
+
* SIWE fields, and every subset invariant enforced by
|
|
18290
|
+
* {@link signInWithOpenKeyResult}. This preserves the exact-byte
|
|
18291
|
+
* guarantee: if the user did NOT narrow, the returned `signedMessage`
|
|
18292
|
+
* equals the prepared SIWE byte-for-byte; if the user narrowed, only
|
|
18293
|
+
* the ReCap block differs and every immutable header field survives.
|
|
18294
|
+
*
|
|
18295
|
+
* Legacy exact-byte {@link signIn} callers are unaffected — this method
|
|
18296
|
+
* is opt-in.
|
|
18297
|
+
*
|
|
18298
|
+
* @param authorizeFn Callback that invokes the versioned OpenKey
|
|
18299
|
+
* `authorizeTinyCloud()` protocol and returns the result. Receives
|
|
18300
|
+
* the prepared SIWE bytes plus a `host` hint the widget should bind
|
|
18301
|
+
* into its /authorize-sign-prepare context.
|
|
18302
|
+
* @param options Optional overrides for the OpenKey request.
|
|
18303
|
+
*/
|
|
18304
|
+
async signInWithOpenKey(authorizeFn, options) {
|
|
18305
|
+
if (options?.host !== void 0) {
|
|
18306
|
+
const overrideHost = options.host.trim();
|
|
18307
|
+
if (!overrideHost) {
|
|
18308
|
+
throw new Error("OpenKey authorization host must not be empty");
|
|
18309
|
+
}
|
|
18310
|
+
this.tinycloudHosts = [overrideHost];
|
|
18311
|
+
}
|
|
18312
|
+
const signingAddress = canonicalizeAddress(await this.signer.getAddress());
|
|
18313
|
+
const signingChainId = await this.signer.getChainId();
|
|
18314
|
+
await this.resolveTinyCloudHostsForSignIn(signingAddress, signingChainId);
|
|
18315
|
+
const { prepared, keyId, jwk } = await this.prepareSessionForSigning();
|
|
18316
|
+
const presentation = this.buildPresentationEnvelope(options?.reason);
|
|
18317
|
+
const hostHint = this.primaryTinyCloudHost;
|
|
18318
|
+
const result = await authorizeFn({
|
|
18319
|
+
protocolVersion: 1,
|
|
18320
|
+
siwe: prepared.siwe,
|
|
18321
|
+
jwk,
|
|
18322
|
+
host: hostHint,
|
|
18323
|
+
keyId: options?.openkeyKeyId,
|
|
18324
|
+
presentation
|
|
18325
|
+
});
|
|
18326
|
+
return this.signInWithOpenKeyResult(result, prepared, keyId, jwk);
|
|
18327
|
+
}
|
|
18328
|
+
/**
|
|
18329
|
+
* Build a `CapabilityPresentationEnvelopeV1` from `this._manifest`. The
|
|
18330
|
+
* envelope is display-only — the OpenKey widget renders it under an
|
|
18331
|
+
* honest trust label, and the server /authorize-sign-prepare route
|
|
18332
|
+
* decides whether to upgrade the label to `origin-bound` by fetching
|
|
18333
|
+
* the well-known manifest and comparing its digest to
|
|
18334
|
+
* `manifestDigest`.
|
|
18335
|
+
*
|
|
18336
|
+
* Returns `undefined` when no manifest is set — the widget renders as
|
|
18337
|
+
* "no manifest supplied" (unsigned).
|
|
18338
|
+
*
|
|
18339
|
+
* The digest is a deterministic SHA-256 over canonical JSON of the
|
|
18340
|
+
* first manifest payload; OpenKey parses the fetched well-known manifest
|
|
18341
|
+
* and applies the same canonicalization before comparing digests.
|
|
18342
|
+
*/
|
|
18343
|
+
buildPresentationEnvelope(reason) {
|
|
18344
|
+
const manifests = Array.isArray(this._manifest) ? this._manifest : this._manifest ? [this._manifest] : [];
|
|
18345
|
+
if (manifests.length === 0 && !reason) {
|
|
18346
|
+
return void 0;
|
|
18347
|
+
}
|
|
18348
|
+
const primary = manifests[0];
|
|
18349
|
+
let displayName;
|
|
18350
|
+
let manifestId;
|
|
18351
|
+
let manifestDigest;
|
|
18352
|
+
const envelopeManifests = [];
|
|
18353
|
+
for (const m of manifests) {
|
|
18354
|
+
envelopeManifests.push({
|
|
18355
|
+
name: m.name,
|
|
18356
|
+
appId: m.app_id,
|
|
18357
|
+
// Deep-copy to prevent later mutations from affecting the digest.
|
|
18358
|
+
payload: JSON.parse(JSON.stringify(m))
|
|
18359
|
+
});
|
|
18360
|
+
}
|
|
18361
|
+
if (primary) {
|
|
18362
|
+
displayName = primary.name;
|
|
18363
|
+
manifestId = primary.app_id;
|
|
18364
|
+
manifestDigest = canonicalOpenKeyManifestSha256Hex(primary);
|
|
18365
|
+
}
|
|
18366
|
+
return {
|
|
18367
|
+
protocolVersion: 1,
|
|
18368
|
+
displayName,
|
|
18369
|
+
reason: reason && reason.length > 0 ? reason : void 0,
|
|
18370
|
+
manifestId,
|
|
18371
|
+
manifestDigest,
|
|
18372
|
+
manifests: envelopeManifests.length > 0 ? envelopeManifests : void 0
|
|
18373
|
+
};
|
|
18374
|
+
}
|
|
18375
|
+
/**
|
|
18376
|
+
* Complete sign-in with a versioned OpenKey authorization result.
|
|
18377
|
+
*
|
|
18378
|
+
* Where `signInWithPreparedSession` completes with a signature over the
|
|
18379
|
+
* ORIGINAL prepared SIWE, this method completes with the SIWE bytes the
|
|
18380
|
+
* OpenKey widget actually presented and signed (`signedMessage`). It
|
|
18381
|
+
* exists because the versioned authorizeTinyCloud protocol lets the user
|
|
18382
|
+
* narrow capabilities; the returned bytes may differ from the original
|
|
18383
|
+
* request and are the only ones the signature actually verifies against.
|
|
18384
|
+
*
|
|
18385
|
+
* The `result` parameter is validated at the trust boundary via
|
|
18386
|
+
* `validateAuthorizationResultV1` (in sdk-core). Callers should validate
|
|
18387
|
+
* BEFORE calling this method so unknown/missing fields are rejected
|
|
18388
|
+
* before any session state is created.
|
|
18389
|
+
*
|
|
18390
|
+
* Validation performed here (defence-in-depth: even a "valid" v1 payload
|
|
18391
|
+
* must clear these checks before any session state is created):
|
|
18392
|
+
* 1. `result.address` matches the local signer's address.
|
|
18393
|
+
* 2. `result.signature` recovers to `result.address` when verified against
|
|
18394
|
+
* `result.signedMessage`.
|
|
18395
|
+
* 3. Every immutable SIWE header field (domain, address, URI, version,
|
|
18396
|
+
* chainId, nonce, issuedAt) in the original prepared SIWE is preserved
|
|
18397
|
+
* byte-for-byte in `result.signedMessage`. `statement` is NOT in this
|
|
18398
|
+
* list because the WASM renders it from the ReCap contents; it MUST
|
|
18399
|
+
* change when the ReCap is narrowed.
|
|
18400
|
+
* 4. Every capability in `result.signedMessage`'s ReCap block was present
|
|
18401
|
+
* in the original prepared SIWE — narrowing OK, broadening rejected.
|
|
18402
|
+
* 5. Every `result.selectedActionKeys` entry corresponds to a
|
|
18403
|
+
* (resource, action) pair in the returned SIWE.
|
|
18404
|
+
*/
|
|
18405
|
+
async signInWithOpenKeyResult(result, prepared, keyId, jwk) {
|
|
18406
|
+
if (result.protocolVersion !== 1) {
|
|
18407
|
+
throw new Error(`Unsupported OpenKey protocol version ${result.protocolVersion}`);
|
|
18408
|
+
}
|
|
18409
|
+
if (!result.signedMessage) {
|
|
18410
|
+
throw new Error("OpenKey result must include signedMessage");
|
|
18411
|
+
}
|
|
18412
|
+
if (!prepared.siwe) {
|
|
18413
|
+
throw new Error(
|
|
18414
|
+
"signInWithOpenKeyResult requires prepared.siwe (the SDK-generated SIWE the sign-in was proposed with)"
|
|
18415
|
+
);
|
|
18416
|
+
}
|
|
18417
|
+
const expectedAddress = canonicalizeAddress(await this.signer.getAddress());
|
|
18418
|
+
if (canonicalizeAddress(result.address) !== expectedAddress) {
|
|
18419
|
+
throw new Error(
|
|
18420
|
+
`OpenKey returned signature from ${result.address} but expected ${expectedAddress}`
|
|
18421
|
+
);
|
|
18422
|
+
}
|
|
18423
|
+
let parsedSignedSiwe;
|
|
18424
|
+
try {
|
|
18425
|
+
parsedSignedSiwe = new SiweMessage(result.signedMessage);
|
|
18426
|
+
} catch (err) {
|
|
18427
|
+
throw new Error(
|
|
18428
|
+
`OpenKey signedMessage is not a parseable SIWE: ${err.message}`
|
|
18429
|
+
);
|
|
18430
|
+
}
|
|
18431
|
+
const verifyResult = await parsedSignedSiwe.verify(
|
|
18432
|
+
{ signature: result.signature },
|
|
18433
|
+
{ suppressExceptions: true }
|
|
18434
|
+
);
|
|
18435
|
+
if (!verifyResult.success) {
|
|
18436
|
+
const errData = verifyResult.error;
|
|
18437
|
+
const type = errData && typeof errData === "object" && "type" in errData ? errData.type : void 0;
|
|
18438
|
+
const details = errData && typeof errData === "object" && "expected" in errData ? ` (expected=${errData.expected}, received=${errData.received})` : "";
|
|
18439
|
+
throw new Error(
|
|
18440
|
+
`OpenKey signature verification failed${type ? `: ${type}` : ""}${details}`
|
|
18441
|
+
);
|
|
18442
|
+
}
|
|
18443
|
+
if (canonicalizeAddress(parsedSignedSiwe.address) !== expectedAddress) {
|
|
18444
|
+
throw new Error(
|
|
18445
|
+
`OpenKey signedMessage.address (${parsedSignedSiwe.address}) does not match local signer address (${expectedAddress})`
|
|
18446
|
+
);
|
|
18447
|
+
}
|
|
18448
|
+
const originalHasRecap = /(?:^|\n)-\s+urn:recap:/i.test(prepared.siwe);
|
|
18449
|
+
const originalFields = extractImmutableSiweFields(prepared.siwe);
|
|
18450
|
+
const signedFields = extractImmutableSiweFields(result.signedMessage);
|
|
18451
|
+
const changedFields = diffImmutableSiweFields(originalFields, signedFields, {
|
|
18452
|
+
originalHasRecap
|
|
18453
|
+
});
|
|
18454
|
+
if (changedFields.length > 0) {
|
|
18455
|
+
throw new Error(
|
|
18456
|
+
`OpenKey signedMessage altered immutable SIWE fields: ${changedFields.join(", ")}`
|
|
18457
|
+
);
|
|
18458
|
+
}
|
|
18459
|
+
let originalCaps;
|
|
18460
|
+
let signedCaps;
|
|
18461
|
+
try {
|
|
18462
|
+
originalCaps = extractRecapAttenuations(prepared.siwe);
|
|
18463
|
+
} catch (err) {
|
|
18464
|
+
throw new Error(
|
|
18465
|
+
`Failed to decode prepared SIWE recap: ${err.message}`
|
|
18466
|
+
);
|
|
18467
|
+
}
|
|
18468
|
+
try {
|
|
18469
|
+
signedCaps = extractRecapAttenuations(result.signedMessage);
|
|
18470
|
+
} catch (err) {
|
|
18471
|
+
throw new Error(
|
|
18472
|
+
`Failed to decode OpenKey signedMessage recap: ${err.message}`
|
|
18473
|
+
);
|
|
18474
|
+
}
|
|
18475
|
+
const unauthorized = unauthorizedRecapCapabilities(signedCaps, originalCaps);
|
|
18476
|
+
if (unauthorized.length > 0) {
|
|
18477
|
+
const previewLimit = 5;
|
|
18478
|
+
const preview = unauthorized.slice(0, previewLimit).map((u) => `${u.resource}::${u.action}`).join(", ");
|
|
18479
|
+
const overflow = unauthorized.length > previewLimit ? ` (and ${unauthorized.length - previewLimit} more)` : "";
|
|
18480
|
+
throw new Error(
|
|
18481
|
+
`OpenKey signedMessage broadened authorization beyond the original request: ${preview}${overflow}`
|
|
18482
|
+
);
|
|
18483
|
+
}
|
|
18484
|
+
if (!result.signedMessage.includes(prepared.spaceId)) {
|
|
18485
|
+
throw new Error(
|
|
18486
|
+
`OpenKey signedMessage does not reference the expected spaceId ${prepared.spaceId}`
|
|
18487
|
+
);
|
|
18488
|
+
}
|
|
18489
|
+
const grantedPairs = /* @__PURE__ */ new Set();
|
|
18490
|
+
const grantedFourPartIndex = /* @__PURE__ */ new Map();
|
|
18491
|
+
for (const [resource, actions] of Object.entries(signedCaps)) {
|
|
18492
|
+
const { space, path } = parseCanonicalRecapResource(resource);
|
|
18493
|
+
for (const action of Object.keys(actions)) {
|
|
18494
|
+
grantedPairs.add(`${resource}\0${action}`);
|
|
18495
|
+
const slashIdx = action.indexOf("/");
|
|
18496
|
+
const service = slashIdx > 0 ? action.slice(0, slashIdx) : "";
|
|
18497
|
+
if (service) {
|
|
18498
|
+
grantedFourPartIndex.set(
|
|
18499
|
+
`${service}\0${space}\0${path}\0${action}`,
|
|
18500
|
+
`${resource}\0${action}`
|
|
18501
|
+
);
|
|
18502
|
+
}
|
|
18503
|
+
}
|
|
18504
|
+
}
|
|
18505
|
+
const isStructurallyRequired = (action) => action === "tinycloud.capabilities/read" || action === "capabilities/read";
|
|
18506
|
+
const nonRequiredGrantedPairs = /* @__PURE__ */ new Set();
|
|
18507
|
+
for (const pair of grantedPairs) {
|
|
18508
|
+
const idx = pair.indexOf("\0");
|
|
18509
|
+
const action = idx >= 0 ? pair.slice(idx + 1) : pair;
|
|
18510
|
+
if (!isStructurallyRequired(action)) {
|
|
18511
|
+
nonRequiredGrantedPairs.add(pair);
|
|
18512
|
+
}
|
|
18513
|
+
}
|
|
18514
|
+
const allSignedGrantedPairs = new Set(grantedPairs);
|
|
18515
|
+
const seenRawKeys = /* @__PURE__ */ new Set();
|
|
18516
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
18517
|
+
if (seenRawKeys.has(rawKey)) {
|
|
18518
|
+
throw new Error(
|
|
18519
|
+
`OpenKey selectedActionKeys contains duplicate entry ${rawKey}`
|
|
18520
|
+
);
|
|
18521
|
+
}
|
|
18522
|
+
seenRawKeys.add(rawKey);
|
|
18523
|
+
}
|
|
18524
|
+
const selectedResourceActionPairs = /* @__PURE__ */ new Set();
|
|
18525
|
+
for (const rawKey of result.selectedActionKeys) {
|
|
18526
|
+
const parts = rawKey.split("\0");
|
|
18527
|
+
if (parts.length !== 4) {
|
|
18528
|
+
throw new Error(
|
|
18529
|
+
`OpenKey selectedActionKeys entry is malformed (expected canonical 4-part service\\0space\\0path\\0ability): ${rawKey}`
|
|
18530
|
+
);
|
|
18531
|
+
}
|
|
18532
|
+
const canonical = grantedFourPartIndex.get(rawKey);
|
|
18533
|
+
if (!canonical) {
|
|
18534
|
+
throw new Error(
|
|
18535
|
+
`OpenKey selectedActionKeys entry ${rawKey} is not covered by any granted capability`
|
|
18536
|
+
);
|
|
18537
|
+
}
|
|
18538
|
+
selectedResourceActionPairs.add(canonical);
|
|
18539
|
+
}
|
|
18540
|
+
if (nonRequiredGrantedPairs.size > 0) {
|
|
18541
|
+
const missingPairs = [];
|
|
18542
|
+
for (const pair of nonRequiredGrantedPairs) {
|
|
18543
|
+
if (!selectedResourceActionPairs.has(pair)) {
|
|
18544
|
+
missingPairs.push(pair);
|
|
18545
|
+
}
|
|
18546
|
+
}
|
|
18547
|
+
if (missingPairs.length > 0) {
|
|
18548
|
+
const previewLimit = 5;
|
|
18549
|
+
const preview = missingPairs.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
18550
|
+
const overflow = missingPairs.length > previewLimit ? ` (and ${missingPairs.length - previewLimit} more)` : "";
|
|
18551
|
+
throw new Error(
|
|
18552
|
+
`OpenKey selectedActionKeys is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
18553
|
+
);
|
|
18554
|
+
}
|
|
18555
|
+
}
|
|
18556
|
+
if (allSignedGrantedPairs.size > 0 && result.permissions.length === 0) {
|
|
18557
|
+
throw new Error(
|
|
18558
|
+
"OpenKey permissions is empty but signedMessage carries capabilities \u2014 refusing to accept a session without an authoritative grant list"
|
|
18559
|
+
);
|
|
18560
|
+
}
|
|
18561
|
+
const permissionCanonicalKeys = /* @__PURE__ */ new Set();
|
|
18562
|
+
const permissionPairsCovered = /* @__PURE__ */ new Set();
|
|
18563
|
+
for (const perm of result.permissions) {
|
|
18564
|
+
const { service, space, path, actions } = perm;
|
|
18565
|
+
if (typeof service !== "string" || typeof space !== "string" || typeof path !== "string") {
|
|
18566
|
+
throw new Error(
|
|
18567
|
+
`OpenKey permissions entry has non-string service/space/path (service=${service}, space=${space}, path=${path})`
|
|
18568
|
+
);
|
|
18569
|
+
}
|
|
18570
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
18571
|
+
throw new Error(
|
|
18572
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has empty actions \u2014 a capability-bearing entry must list at least one action`
|
|
18573
|
+
);
|
|
18574
|
+
}
|
|
18575
|
+
const canonicalKey = `${service}\0${space}\0${path}`;
|
|
18576
|
+
if (permissionCanonicalKeys.has(canonicalKey)) {
|
|
18577
|
+
throw new Error(
|
|
18578
|
+
`OpenKey permissions has duplicate entry for (${service} ${space} ${path})`
|
|
18579
|
+
);
|
|
18580
|
+
}
|
|
18581
|
+
permissionCanonicalKeys.add(canonicalKey);
|
|
18582
|
+
let expectedResource;
|
|
18583
|
+
if (space.startsWith("tinycloud:")) {
|
|
18584
|
+
const shortService = service.startsWith("tinycloud.") ? service.slice("tinycloud.".length) : service;
|
|
18585
|
+
expectedResource = path ? `${space}/${shortService}/${path}` : `${space}/${shortService}`;
|
|
18586
|
+
} else {
|
|
18587
|
+
if (path) {
|
|
18588
|
+
throw new Error(
|
|
18589
|
+
`OpenKey permissions entry (${service} ${space} ${path}) carries a path on a non-tinycloud: resource URI, which is not a recognized encoding`
|
|
18590
|
+
);
|
|
18591
|
+
}
|
|
18592
|
+
expectedResource = space;
|
|
18593
|
+
}
|
|
18594
|
+
let matchedResource;
|
|
18595
|
+
if (signedCaps[expectedResource] !== void 0) {
|
|
18596
|
+
matchedResource = expectedResource;
|
|
18597
|
+
} else {
|
|
18598
|
+
throw new Error(
|
|
18599
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims grants not confirmed in signedMessage capabilities`
|
|
18600
|
+
);
|
|
18601
|
+
}
|
|
18602
|
+
const grantedActionsForResource = new Set(
|
|
18603
|
+
Object.keys(signedCaps[matchedResource])
|
|
18604
|
+
);
|
|
18605
|
+
for (const action of actions) {
|
|
18606
|
+
if (typeof action !== "string" || !action) {
|
|
18607
|
+
throw new Error(
|
|
18608
|
+
`OpenKey permissions entry (${service} ${space} ${path}) has non-string action ${String(action)}`
|
|
18609
|
+
);
|
|
18610
|
+
}
|
|
18611
|
+
if (!grantedActionsForResource.has(action)) {
|
|
18612
|
+
throw new Error(
|
|
18613
|
+
`OpenKey permissions entry (${service} ${space} ${path}) claims action ${action} not present in signedMessage capabilities`
|
|
18614
|
+
);
|
|
18615
|
+
}
|
|
18616
|
+
permissionPairsCovered.add(`${matchedResource}\0${action}`);
|
|
18617
|
+
}
|
|
18618
|
+
}
|
|
18619
|
+
if (allSignedGrantedPairs.size > 0) {
|
|
18620
|
+
const missingFromPerms = [];
|
|
18621
|
+
for (const pair of allSignedGrantedPairs) {
|
|
18622
|
+
if (!permissionPairsCovered.has(pair)) {
|
|
18623
|
+
missingFromPerms.push(pair);
|
|
18624
|
+
}
|
|
18625
|
+
}
|
|
18626
|
+
if (missingFromPerms.length > 0) {
|
|
18627
|
+
const previewLimit = 5;
|
|
18628
|
+
const preview = missingFromPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
18629
|
+
const overflow = missingFromPerms.length > previewLimit ? ` (and ${missingFromPerms.length - previewLimit} more)` : "";
|
|
18630
|
+
throw new Error(
|
|
18631
|
+
`OpenKey permissions is missing entries for signedMessage capabilities: ${preview}${overflow}`
|
|
18632
|
+
);
|
|
18633
|
+
}
|
|
18634
|
+
const extrasInPerms = [];
|
|
18635
|
+
for (const pair of permissionPairsCovered) {
|
|
18636
|
+
if (!allSignedGrantedPairs.has(pair)) {
|
|
18637
|
+
extrasInPerms.push(pair);
|
|
18638
|
+
}
|
|
18639
|
+
}
|
|
18640
|
+
if (extrasInPerms.length > 0) {
|
|
18641
|
+
const previewLimit = 5;
|
|
18642
|
+
const preview = extrasInPerms.slice(0, previewLimit).map((p) => p.replace("\0", "::")).join(", ");
|
|
18643
|
+
const overflow = extrasInPerms.length > previewLimit ? ` (and ${extrasInPerms.length - previewLimit} more)` : "";
|
|
18644
|
+
throw new Error(
|
|
18645
|
+
`OpenKey permissions contains entries not present in the signed authority: ${preview}${overflow}`
|
|
18646
|
+
);
|
|
18647
|
+
}
|
|
18648
|
+
}
|
|
18649
|
+
void nonRequiredGrantedPairs;
|
|
18650
|
+
return this.signInWithPreparedSession(
|
|
18651
|
+
{
|
|
18652
|
+
siwe: result.signedMessage,
|
|
18653
|
+
jwk: prepared.jwk,
|
|
18654
|
+
spaceId: prepared.spaceId,
|
|
18655
|
+
verificationMethod: prepared.verificationMethod
|
|
18656
|
+
},
|
|
18657
|
+
result.signature,
|
|
18658
|
+
keyId,
|
|
18659
|
+
jwk
|
|
18660
|
+
);
|
|
18661
|
+
}
|
|
18197
18662
|
/**
|
|
18198
18663
|
* Clear persisted session data.
|
|
18199
18664
|
*/
|
|
@@ -18449,7 +18914,7 @@ function createWasmKeyProvider(sessionManager) {
|
|
|
18449
18914
|
// src/delegateToHelpers.ts
|
|
18450
18915
|
import {
|
|
18451
18916
|
parseExpiry,
|
|
18452
|
-
SiweMessage,
|
|
18917
|
+
SiweMessage as SiweMessage2,
|
|
18453
18918
|
EXPIRY as EXPIRY2
|
|
18454
18919
|
} from "@tinycloud/sdk-core";
|
|
18455
18920
|
function legacyParamsToPermissionEntries(actions, path, spaceIdOverride) {
|
|
@@ -18498,7 +18963,7 @@ function resolveExpiryMs(expiry) {
|
|
|
18498
18963
|
return parseExpiry(expiry);
|
|
18499
18964
|
}
|
|
18500
18965
|
function extractSiweExpiration(siwe) {
|
|
18501
|
-
const parsed = new
|
|
18966
|
+
const parsed = new SiweMessage2(siwe);
|
|
18502
18967
|
if (parsed.expirationTime === void 0 || parsed.expirationTime === null) {
|
|
18503
18968
|
return void 0;
|
|
18504
18969
|
}
|
|
@@ -18700,6 +19165,9 @@ var DEFAULT_HOST = "https://node.tinycloud.xyz";
|
|
|
18700
19165
|
var DEFAULT_ENCRYPTION_NETWORK_NAME = "default";
|
|
18701
19166
|
var NETWORK_CREATE_ACTION2 = ENCRYPTION2.NETWORK_CREATE;
|
|
18702
19167
|
var DECRYPT_ACTION2 = ENCRYPTION2.DECRYPT;
|
|
19168
|
+
var BOOTSTRAP_COMPLETION_MARKER_KEY = "system/bootstrap/complete";
|
|
19169
|
+
var BOOTSTRAP_COMPLETION_MARKER_VERSION = 1;
|
|
19170
|
+
var ACCEPTED_MARKER_VERSIONS = [BOOTSTRAP_COMPLETION_MARKER_VERSION];
|
|
18703
19171
|
var NETWORK_ADMIN_TYPE = "tinycloud.encryption.network-admin/v1";
|
|
18704
19172
|
function isSecretPayload(value) {
|
|
18705
19173
|
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";
|
|
@@ -19010,7 +19478,9 @@ function base64UrlDecode(value) {
|
|
|
19010
19478
|
return new Uint8Array(bytes);
|
|
19011
19479
|
}
|
|
19012
19480
|
async function signJwtInputWithJwk(signingInput, jwk) {
|
|
19013
|
-
|
|
19481
|
+
return signBytesWithJwk(new TextEncoder().encode(signingInput), jwk);
|
|
19482
|
+
}
|
|
19483
|
+
async function signBytesWithJwk(bytes, jwk) {
|
|
19014
19484
|
try {
|
|
19015
19485
|
const subtle = globalThis.crypto?.subtle;
|
|
19016
19486
|
if (!subtle) {
|
|
@@ -19091,6 +19561,12 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19091
19561
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
19092
19562
|
this._secrets = /* @__PURE__ */ new Map();
|
|
19093
19563
|
this.runtimePermissionGrants = [];
|
|
19564
|
+
/**
|
|
19565
|
+
* Owned space ids this sign-in has already confirmed are hosted. Consulted by
|
|
19566
|
+
* {@link ensureOwnedSpaceHostedById}; cleared on every {@link signIn} because
|
|
19567
|
+
* hosting is confirmed against the session that was active at the time.
|
|
19568
|
+
*/
|
|
19569
|
+
this.confirmedHostedSpaceIds = /* @__PURE__ */ new Set();
|
|
19094
19570
|
/**
|
|
19095
19571
|
* True when the last signIn() detected an interactive signer and skipped
|
|
19096
19572
|
* client-side bootstrap. Apps can read this to know whether bootstrap was
|
|
@@ -19099,9 +19575,15 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19099
19575
|
this._bootstrapSkipped = false;
|
|
19100
19576
|
/**
|
|
19101
19577
|
* Outcome of the last signIn()'s account-bootstrap attempt. `skipped` is
|
|
19102
|
-
* true when bootstrap did not complete (interactive signer,
|
|
19103
|
-
*
|
|
19104
|
-
*
|
|
19578
|
+
* true when bootstrap did not complete in this invocation (interactive signer,
|
|
19579
|
+
* auto-bootstrap disabled, already provisioned, or a decision/provisioning
|
|
19580
|
+
* step failed). `already-provisioned` means the bootstrap was not run here;
|
|
19581
|
+
* it does not mean a prior provisioning attempt failed to complete.
|
|
19582
|
+
* `reason` carries the cause so apps can surface a "finish account setup"
|
|
19583
|
+
* call-to-action. `warnings` is
|
|
19584
|
+
* present when bootstrap completed, but one or more steps recovered from
|
|
19585
|
+
* an ambiguous failure (e.g. a KV batch write reconciled via per-space
|
|
19586
|
+
* fallback) — clean vs. recovered runs are otherwise byte-identical.
|
|
19105
19587
|
*/
|
|
19106
19588
|
this._bootstrapStatus = {
|
|
19107
19589
|
skipped: false
|
|
@@ -19281,7 +19763,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19281
19763
|
nonce: config.nonce,
|
|
19282
19764
|
siweConfig: config.siweConfig,
|
|
19283
19765
|
manifest: useBootstrapSignInRequest ? void 0 : config.manifest,
|
|
19284
|
-
capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
|
|
19766
|
+
capabilityRequest: useBootstrapSignInRequest ? config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : config.capabilityRequest,
|
|
19285
19767
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : config.includeAccountRegistryPermissions
|
|
19286
19768
|
});
|
|
19287
19769
|
this.tc = new TinyCloud(this.auth, {
|
|
@@ -19421,7 +19903,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19421
19903
|
if (!this._address) {
|
|
19422
19904
|
return void 0;
|
|
19423
19905
|
}
|
|
19424
|
-
return this.wasmBindings.makeSpaceId(this._address, this._chainId,
|
|
19906
|
+
return this.wasmBindings.makeSpaceId(this._address, this._chainId, ACCOUNT_REGISTRY_SPACE2);
|
|
19425
19907
|
}
|
|
19426
19908
|
/**
|
|
19427
19909
|
* Account-level application and delegation helpers.
|
|
@@ -19487,6 +19969,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19487
19969
|
this._spaceService = void 0;
|
|
19488
19970
|
this._serviceContext = void 0;
|
|
19489
19971
|
this.runtimePermissionGrants = [];
|
|
19972
|
+
this.confirmedHostedSpaceIds.clear();
|
|
19490
19973
|
await this.tc.signIn(options);
|
|
19491
19974
|
this.syncResolvedHostFromAuth();
|
|
19492
19975
|
const oldGraph = this._serviceGraph;
|
|
@@ -19523,6 +20006,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19523
20006
|
this._bootstrapSkipped = false;
|
|
19524
20007
|
this._bootstrapStatus = { skipped: false };
|
|
19525
20008
|
if (this.config.autoBootstrapAccount === false) {
|
|
20009
|
+
this._bootstrapStatus = { skipped: true, reason: "auto-bootstrap-disabled" };
|
|
19526
20010
|
return false;
|
|
19527
20011
|
}
|
|
19528
20012
|
if (!this.auth || !this._address) {
|
|
@@ -19537,11 +20021,20 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19537
20021
|
return false;
|
|
19538
20022
|
}
|
|
19539
20023
|
const steps = bootstrapSteps(this._address, this._chainId);
|
|
19540
|
-
if (!await this.isFreshBootstrapAccount(steps)) {
|
|
19541
|
-
return false;
|
|
19542
|
-
}
|
|
19543
20024
|
try {
|
|
19544
|
-
await this.
|
|
20025
|
+
const decision = await this.resolveBootstrapDecision(steps);
|
|
20026
|
+
if (decision.action === "skip") {
|
|
20027
|
+
this._bootstrapStatus = { skipped: true, reason: "already-provisioned" };
|
|
20028
|
+
return false;
|
|
20029
|
+
}
|
|
20030
|
+
const warnings = await this.runAccountBootstrap(steps, { mode: decision.mode });
|
|
20031
|
+
await this.writeBootstrapCompletionMarker(steps);
|
|
20032
|
+
if (warnings.length > 0) {
|
|
20033
|
+
this._bootstrapStatus = { skipped: false, warnings };
|
|
20034
|
+
this.notificationHandler.warning(
|
|
20035
|
+
`Account bootstrap completed with warnings: ${warnings.map((w) => `${w.stepId} (${w.kind}: ${w.code})`).join("; ")}`
|
|
20036
|
+
);
|
|
20037
|
+
}
|
|
19545
20038
|
} catch (err) {
|
|
19546
20039
|
const reason = err instanceof Error ? err.message : String(err);
|
|
19547
20040
|
this._bootstrapSkipped = true;
|
|
@@ -19554,32 +20047,92 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19554
20047
|
}
|
|
19555
20048
|
return true;
|
|
19556
20049
|
}
|
|
19557
|
-
async
|
|
20050
|
+
async resolveBootstrapDecision(steps) {
|
|
19558
20051
|
const enshrinedSpaceIds = /* @__PURE__ */ new Set();
|
|
19559
20052
|
for (const step of steps) {
|
|
19560
20053
|
if (step.kind === "session") {
|
|
19561
20054
|
enshrinedSpaceIds.add(step.spaceId);
|
|
19562
20055
|
}
|
|
19563
20056
|
}
|
|
19564
|
-
const
|
|
20057
|
+
const auth = this.auth;
|
|
20058
|
+
if (!auth) {
|
|
20059
|
+
throw new Error("Account bootstrap requires an active wallet session");
|
|
20060
|
+
}
|
|
20061
|
+
const skipped = auth.lastActivationSkippedSpaceIds;
|
|
19565
20062
|
if (skipped.some((spaceId) => enshrinedSpaceIds.has(spaceId))) {
|
|
19566
|
-
return
|
|
20063
|
+
return { action: "run", mode: "fresh" };
|
|
19567
20064
|
}
|
|
19568
20065
|
try {
|
|
19569
|
-
const
|
|
19570
|
-
|
|
19571
|
-
|
|
20066
|
+
const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
|
|
20067
|
+
const markerPermission = {
|
|
20068
|
+
service: "tinycloud.kv",
|
|
20069
|
+
space: accountSpaceId,
|
|
20070
|
+
path: BOOTSTRAP_COMPLETION_MARKER_KEY,
|
|
20071
|
+
actions: ["get"]
|
|
20072
|
+
};
|
|
20073
|
+
if (!this.hasRuntimePermissions([markerPermission])) {
|
|
20074
|
+
return { action: "run", mode: "repair" };
|
|
19572
20075
|
}
|
|
19573
|
-
|
|
20076
|
+
const marker = await this.readBootstrapCompletionMarker();
|
|
20077
|
+
if (marker.ok) {
|
|
20078
|
+
if (this.isAcceptedBootstrapCompletionMarker(marker.data.data)) {
|
|
20079
|
+
return { action: "skip" };
|
|
20080
|
+
}
|
|
20081
|
+
return { action: "run", mode: "repair" };
|
|
20082
|
+
}
|
|
20083
|
+
if (marker.error.code === ErrorCodes2.KV_NOT_FOUND && this.markerReadIsUnhostedSpace(marker.error.meta)) {
|
|
20084
|
+
return { action: "run", mode: "fresh" };
|
|
20085
|
+
}
|
|
20086
|
+
} catch (err) {
|
|
20087
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
20088
|
+
this.notificationHandler.warning(
|
|
20089
|
+
`Account bootstrap decision failed; attempting one bounded repair: ${reason}`
|
|
20090
|
+
);
|
|
20091
|
+
console.warn(`[TinyCloudNode] account bootstrap decision failed: ${reason}`);
|
|
20092
|
+
return { action: "run", mode: "repair" };
|
|
19574
20093
|
}
|
|
19575
|
-
|
|
19576
|
-
|
|
19577
|
-
|
|
19578
|
-
|
|
19579
|
-
|
|
20094
|
+
return { action: "run", mode: "repair" };
|
|
20095
|
+
}
|
|
20096
|
+
async readBootstrapCompletionMarker() {
|
|
20097
|
+
return this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).get(
|
|
20098
|
+
BOOTSTRAP_COMPLETION_MARKER_KEY
|
|
20099
|
+
);
|
|
20100
|
+
}
|
|
20101
|
+
async writeBootstrapCompletionMarker(steps) {
|
|
20102
|
+
if (!this._address) {
|
|
20103
|
+
throw new Error("Bootstrap completion marker requires an active wallet session");
|
|
19580
20104
|
}
|
|
20105
|
+
const canonicalStepIds = new Set(
|
|
20106
|
+
bootstrapSteps(this._address, this._chainId).map((step) => step.id)
|
|
20107
|
+
);
|
|
20108
|
+
const providedStepIds = new Set(steps.map((step) => step.id));
|
|
20109
|
+
if (canonicalStepIds.size !== providedStepIds.size || [...canonicalStepIds].some((stepId) => !providedStepIds.has(stepId))) {
|
|
20110
|
+
throw new Error("Bootstrap completion marker requires the canonical bootstrap step set");
|
|
20111
|
+
}
|
|
20112
|
+
const marker = {
|
|
20113
|
+
v: BOOTSTRAP_COMPLETION_MARKER_VERSION,
|
|
20114
|
+
stepIds: [...canonicalStepIds],
|
|
20115
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
20116
|
+
};
|
|
20117
|
+
const written = await this.kvForSpace(this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2)).put(
|
|
20118
|
+
BOOTSTRAP_COMPLETION_MARKER_KEY,
|
|
20119
|
+
marker
|
|
20120
|
+
);
|
|
20121
|
+
if (!written.ok) {
|
|
20122
|
+
throw new Error(`Failed to write bootstrap completion marker: ${written.error.message}`);
|
|
20123
|
+
}
|
|
20124
|
+
}
|
|
20125
|
+
isAcceptedBootstrapCompletionMarker(value) {
|
|
20126
|
+
if (typeof value !== "object" || value === null) return false;
|
|
20127
|
+
const entries = Object.entries(value);
|
|
20128
|
+
const markerValue = (name) => entries.find(([key2]) => key2 === name)?.[1];
|
|
20129
|
+
const version23 = markerValue("v");
|
|
20130
|
+
return typeof version23 === "number" && Number.isInteger(version23) && ACCEPTED_MARKER_VERSIONS.some((acceptedVersion) => acceptedVersion === version23);
|
|
20131
|
+
}
|
|
20132
|
+
markerReadIsUnhostedSpace(meta) {
|
|
20133
|
+
return meta?.status === 404;
|
|
19581
20134
|
}
|
|
19582
|
-
async runAccountBootstrap(steps) {
|
|
20135
|
+
async runAccountBootstrap(steps, { mode } = { mode: "fresh" }) {
|
|
19583
20136
|
if (!this.auth || !this._address) {
|
|
19584
20137
|
throw new Error("Account bootstrap requires an active wallet session");
|
|
19585
20138
|
}
|
|
@@ -19590,6 +20143,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19590
20143
|
const auth = this.auth;
|
|
19591
20144
|
const sessions = /* @__PURE__ */ new Map();
|
|
19592
20145
|
const rawAbilitiesBySpace = /* @__PURE__ */ new Map();
|
|
20146
|
+
const warnings = [];
|
|
19593
20147
|
const primarySession = auth.tinyCloudSession;
|
|
19594
20148
|
const defaultSpaceId = this.ownedSpaceId("default");
|
|
19595
20149
|
const canReusePrimaryBootstrapSession = primarySession?.spaceId === defaultSpaceId && auth.capabilityRequest === BOOTSTRAP_SESSION_REQUESTS.default;
|
|
@@ -19651,32 +20205,44 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19651
20205
|
}
|
|
19652
20206
|
}
|
|
19653
20207
|
if (step.kind === "seed-spaces") {
|
|
19654
|
-
|
|
19655
|
-
|
|
20208
|
+
const registered = await this.account.spaces.registerBatch(
|
|
20209
|
+
step.spaces.map((space) => ({
|
|
19656
20210
|
spaceId: space.spaceId,
|
|
19657
20211
|
name: space.name,
|
|
19658
20212
|
ownerDid: this.did,
|
|
19659
20213
|
type: "owned",
|
|
19660
20214
|
permissions: ["*"],
|
|
19661
20215
|
status: "active"
|
|
20216
|
+
}))
|
|
20217
|
+
);
|
|
20218
|
+
if (!registered.ok) {
|
|
20219
|
+
throw new Error(
|
|
20220
|
+
`Failed to seed account spaces: ${registered.error.message}`
|
|
20221
|
+
);
|
|
20222
|
+
}
|
|
20223
|
+
const batchError = registered.data.recoveredFromBatchError;
|
|
20224
|
+
if (batchError) {
|
|
20225
|
+
warnings.push({
|
|
20226
|
+
stepId: step.id,
|
|
20227
|
+
kind: "batch-write-reconciled",
|
|
20228
|
+
code: batchError.code,
|
|
20229
|
+
message: batchError.message
|
|
19662
20230
|
});
|
|
19663
|
-
if (!registered.ok) {
|
|
19664
|
-
throw new Error(
|
|
19665
|
-
`Failed to seed account space ${space.spaceId}: ${registered.error.message}`
|
|
19666
|
-
);
|
|
19667
|
-
}
|
|
19668
20231
|
}
|
|
19669
20232
|
}
|
|
19670
20233
|
if (step.kind === "seed-applications") {
|
|
19671
20234
|
const registered = await this.account.applications.register(
|
|
19672
|
-
step.manifests.length > 0 ? [...step.manifests] : TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST
|
|
20235
|
+
step.manifests.length > 0 ? [...step.manifests] : TINYCLOUD_SECRETS_BOOTSTRAP_MANIFEST,
|
|
20236
|
+
// The write is an INSERT OR REPLACE and is safe to repeat during a
|
|
20237
|
+
// repair, so skip the manifest-hash pre-read.
|
|
20238
|
+
{ assumeUnregistered: true }
|
|
19673
20239
|
);
|
|
19674
20240
|
if (!registered.ok) {
|
|
19675
20241
|
throw new Error(`Failed to seed bootstrap applications: ${registered.error.message}`);
|
|
19676
20242
|
}
|
|
19677
20243
|
}
|
|
19678
20244
|
if (step.kind === "encryption-network-create") {
|
|
19679
|
-
await this.ensureEncryptionNetwork(step.networkId);
|
|
20245
|
+
await this.ensureEncryptionNetwork(step.networkId, { assumeMissing: mode === "fresh" });
|
|
19680
20246
|
}
|
|
19681
20247
|
if (step.kind === "secret-records-schema") {
|
|
19682
20248
|
const db = this.sqlForSpace(step.spaceId).db(step.database);
|
|
@@ -19696,6 +20262,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19696
20262
|
}
|
|
19697
20263
|
}
|
|
19698
20264
|
}
|
|
20265
|
+
return warnings;
|
|
19699
20266
|
}
|
|
19700
20267
|
registerBootstrapRuntimeGrant(session, request, rawAbilities) {
|
|
19701
20268
|
const operations = [];
|
|
@@ -19858,7 +20425,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19858
20425
|
if (!this.auth || !this.signer) {
|
|
19859
20426
|
throw new Error("Manifest registry write requires wallet mode");
|
|
19860
20427
|
}
|
|
19861
|
-
const accountSpaceId = this.ownedSpaceId(
|
|
20428
|
+
const accountSpaceId = this.ownedSpaceId(ACCOUNT_REGISTRY_SPACE2);
|
|
19862
20429
|
await this.ensureOwnedSpaceHostedById(accountSpaceId);
|
|
19863
20430
|
const result = await this.account.applications.register(request.manifests);
|
|
19864
20431
|
if (!result.ok) {
|
|
@@ -19955,6 +20522,22 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19955
20522
|
await this.ensureEncryptionNetwork(networkId);
|
|
19956
20523
|
}
|
|
19957
20524
|
}
|
|
20525
|
+
/**
|
|
20526
|
+
* Ensure one of this user's owned spaces is hosted, at most once per space
|
|
20527
|
+
* per sign-in.
|
|
20528
|
+
*
|
|
20529
|
+
* Account bootstrap funnels six calls through here (via
|
|
20530
|
+
* `AccountService.ensureAccountSpaceHosted`), and each one re-submits
|
|
20531
|
+
* `activateSessionWithHost` with the *primary* session. That session's recap
|
|
20532
|
+
* covers only `default` and `secrets`, so the account space appears in
|
|
20533
|
+
* neither `activated` nor `skipped` and the guard below returns immediately
|
|
20534
|
+
* — the repeat calls cannot change the outcome, they only cost round trips.
|
|
20535
|
+
*
|
|
20536
|
+
* The memo is per sign-in ({@link confirmedHostedSpaceIds} is cleared in
|
|
20537
|
+
* `signIn`) and only records confirmed successes. A wrong skip is
|
|
20538
|
+
* self-revealing: the next write to the space fails immediately and loudly
|
|
20539
|
+
* with `404 Space not found`.
|
|
20540
|
+
*/
|
|
19958
20541
|
async ensureOwnedSpaceHostedById(spaceId) {
|
|
19959
20542
|
if (!this.auth) {
|
|
19960
20543
|
throw new Error("Owned space hosting requires wallet mode");
|
|
@@ -19963,12 +20546,16 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19963
20546
|
if (!session) {
|
|
19964
20547
|
throw new Error("Owned space hosting requires an active session");
|
|
19965
20548
|
}
|
|
20549
|
+
if (this.confirmedHostedSpaceIds.has(spaceId)) {
|
|
20550
|
+
return;
|
|
20551
|
+
}
|
|
19966
20552
|
const host = this.hosts[0] ?? this.config.host;
|
|
19967
20553
|
if (!host) {
|
|
19968
20554
|
throw new Error("Owned space hosting requires a TinyCloud host");
|
|
19969
20555
|
}
|
|
19970
20556
|
const activation = await activateSessionWithHost2(host, session.delegationHeader);
|
|
19971
20557
|
if (activation.success && !activation.skipped?.includes(spaceId)) {
|
|
20558
|
+
this.confirmedHostedSpaceIds.add(spaceId);
|
|
19972
20559
|
return;
|
|
19973
20560
|
}
|
|
19974
20561
|
if (!activation.success && activation.status !== 404) {
|
|
@@ -19987,6 +20574,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
19987
20574
|
`Failed to activate session after creating owned space ${spaceId}: ${retry.error ?? "space was skipped"}`
|
|
19988
20575
|
);
|
|
19989
20576
|
}
|
|
20577
|
+
this.confirmedHostedSpaceIds.add(spaceId);
|
|
19990
20578
|
}
|
|
19991
20579
|
/**
|
|
19992
20580
|
* Host one of this user's owned spaces by name (e.g. `"applications"`).
|
|
@@ -20302,6 +20890,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20302
20890
|
this._delegationManager = stagedGraph.delegationManager;
|
|
20303
20891
|
this._spaceService = stagedGraph.spaceService;
|
|
20304
20892
|
this._serviceGraph = stagedGraph.graph;
|
|
20893
|
+
this._activeServiceSession = serviceSession;
|
|
20305
20894
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
20306
20895
|
this._secrets = /* @__PURE__ */ new Map();
|
|
20307
20896
|
this._publicKV = void 0;
|
|
@@ -20309,6 +20898,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20309
20898
|
this.tc = stagedGraph.core;
|
|
20310
20899
|
this._recapOperationsCache = stagedPrimary.cache;
|
|
20311
20900
|
this.runtimePermissionGrants = stagedPrimary.grants;
|
|
20901
|
+
this.confirmedHostedSpaceIds.clear();
|
|
20312
20902
|
if (this.auth) {
|
|
20313
20903
|
this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost]);
|
|
20314
20904
|
this._restoredTcSession = void 0;
|
|
@@ -20618,6 +21208,28 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20618
21208
|
currentTinyCloudSession() {
|
|
20619
21209
|
return this.auth?.tinyCloudSession ?? this._restoredTcSession;
|
|
20620
21210
|
}
|
|
21211
|
+
/**
|
|
21212
|
+
* Mint a multi-capability invocation through the established session key.
|
|
21213
|
+
* Callers receive only the signed headers; session key material remains
|
|
21214
|
+
* owned by the session manager and is never part of the caller contract.
|
|
21215
|
+
*/
|
|
21216
|
+
invokeAny(entries, facts) {
|
|
21217
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
21218
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
21219
|
+
return this._serviceGraph.invokeAny(session, entries, facts);
|
|
21220
|
+
}
|
|
21221
|
+
/** Sign protocol bytes with the established in-memory session key. */
|
|
21222
|
+
async signSessionBytes(bytes) {
|
|
21223
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
21224
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
21225
|
+
return signBytesWithJwk(bytes, session.jwk);
|
|
21226
|
+
}
|
|
21227
|
+
/** Bind a session-signed invocation to the canonical service audience. */
|
|
21228
|
+
async bindInvocationAudience(authorization, audience) {
|
|
21229
|
+
const session = this.currentTinyCloudSession() ?? this._activeServiceSession;
|
|
21230
|
+
if (session === void 0) throw new Error("Not signed in. Call signIn() first.");
|
|
21231
|
+
return rewriteInvocationAudience(authorization, audience, session.jwk);
|
|
21232
|
+
}
|
|
20621
21233
|
/**
|
|
20622
21234
|
* Connect a wallet to upgrade from session-only mode to wallet mode.
|
|
20623
21235
|
*
|
|
@@ -20679,7 +21291,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20679
21291
|
nonce: this.config.nonce,
|
|
20680
21292
|
siweConfig: this.config.siweConfig,
|
|
20681
21293
|
manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
|
|
20682
|
-
capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
21294
|
+
capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
20683
21295
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
|
|
20684
21296
|
});
|
|
20685
21297
|
this.tc = new TinyCloud(this.auth, {
|
|
@@ -20732,7 +21344,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20732
21344
|
nonce: this.config.nonce,
|
|
20733
21345
|
siweConfig: this.config.siweConfig,
|
|
20734
21346
|
manifest: useBootstrapSignInRequest ? void 0 : this.config.manifest,
|
|
20735
|
-
capabilityRequest: useBootstrapSignInRequest ? BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
21347
|
+
capabilityRequest: useBootstrapSignInRequest ? this.config.includeAccountRegistryPermissions === false ? BOOTSTRAP_DEFAULT_ONLY_SESSION_REQUEST : BOOTSTRAP_SESSION_REQUESTS.default : this.config.capabilityRequest,
|
|
20736
21348
|
includeAccountRegistryPermissions: useBootstrapSignInRequest ? false : this.config.includeAccountRegistryPermissions
|
|
20737
21349
|
});
|
|
20738
21350
|
this.tc = new TinyCloud(this.auth, {
|
|
@@ -20799,6 +21411,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20799
21411
|
if (this._serviceContext) {
|
|
20800
21412
|
const spaceScopedContext = this._serviceGraph.track(new ServiceContext2({
|
|
20801
21413
|
invoke: this._serviceContext.invoke,
|
|
21414
|
+
invokeAny: this._serviceContext.invokeAny,
|
|
20802
21415
|
fetch: this._serviceContext.fetch,
|
|
20803
21416
|
hosts: this._serviceContext.hosts,
|
|
20804
21417
|
telemetry: this.config.telemetry
|
|
@@ -20891,7 +21504,17 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
20891
21504
|
verifyNodeSignature: (nodeId, message, signature2) => verifyDidKeyEd25519Signature(nodeId, message, signature2)
|
|
20892
21505
|
};
|
|
20893
21506
|
}
|
|
21507
|
+
/**
|
|
21508
|
+
* The node DID for {@link config.host}.
|
|
21509
|
+
*
|
|
21510
|
+
* Sign-in already performs `GET /info` for the protocol-version check, and
|
|
21511
|
+
* that response carries `nodeId`, so reuse it rather than fetching `/info` a
|
|
21512
|
+
* second time. Only a value recorded for this exact host qualifies; older
|
|
21513
|
+
* nodes that omit `nodeId` fall through to the fetch.
|
|
21514
|
+
*/
|
|
20894
21515
|
async fetchNodeId() {
|
|
21516
|
+
const cached = this.auth?.nodeIdForHost(this.config.host);
|
|
21517
|
+
if (cached) return cached;
|
|
20895
21518
|
const response = await fetch(`${this.config.host}/info`);
|
|
20896
21519
|
if (!response.ok) {
|
|
20897
21520
|
throw new Error(`Failed to fetch node info: HTTP ${response.status}`);
|
|
@@ -21454,6 +22077,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21454
22077
|
shareUrl: input.shareUrl,
|
|
21455
22078
|
documentName: input.documentName,
|
|
21456
22079
|
jti: base64UrlEncode(crypto.getRandomValues(new Uint8Array(16))),
|
|
22080
|
+
idempotencyKey: input.idempotencyKey,
|
|
21457
22081
|
expiresAt: input.expiresAt
|
|
21458
22082
|
};
|
|
21459
22083
|
const requestBodyDigest = base64UrlEncode(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalizeEncryptionJson(body)))));
|
|
@@ -21663,6 +22287,13 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21663
22287
|
body
|
|
21664
22288
|
)
|
|
21665
22289
|
});
|
|
22290
|
+
if (response.status === 409) {
|
|
22291
|
+
const existing = await this.getEncryptionNetwork(networkId);
|
|
22292
|
+
if (existing) return existing;
|
|
22293
|
+
throw new Error(
|
|
22294
|
+
`Encryption network ${networkId} already exists but could not be read back`
|
|
22295
|
+
);
|
|
22296
|
+
}
|
|
21666
22297
|
if (!response.ok) {
|
|
21667
22298
|
throw new Error(
|
|
21668
22299
|
`Failed to create encryption network ${networkId}: HTTP ${response.status} ${await response.text()}`
|
|
@@ -21671,11 +22302,19 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21671
22302
|
const created = await response.json();
|
|
21672
22303
|
return created.descriptor;
|
|
21673
22304
|
}
|
|
21674
|
-
|
|
22305
|
+
/**
|
|
22306
|
+
* Ensure an encryption network exists, creating it if necessary.
|
|
22307
|
+
*
|
|
22308
|
+
* @param nameOrNetworkId - Network name or full `urn:tinycloud:encryption:` id.
|
|
22309
|
+
* @param options - See {@link EnsureEncryptionNetworkOptions}.
|
|
22310
|
+
*/
|
|
22311
|
+
async ensureEncryptionNetwork(nameOrNetworkId = DEFAULT_ENCRYPTION_NETWORK_NAME, options = {}) {
|
|
21675
22312
|
const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
|
|
21676
|
-
|
|
21677
|
-
|
|
21678
|
-
|
|
22313
|
+
if (!options.assumeMissing) {
|
|
22314
|
+
const existing = await this.getEncryptionNetwork(networkId);
|
|
22315
|
+
if (existing) {
|
|
22316
|
+
return existing;
|
|
22317
|
+
}
|
|
21679
22318
|
}
|
|
21680
22319
|
const parsed = parseNetworkId2(networkId);
|
|
21681
22320
|
if (!didPrincipalMatches2(parsed.ownerDid, this.did)) {
|
|
@@ -23454,35 +24093,44 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23454
24093
|
if (parentDelegation.disableSubDelegation) {
|
|
23455
24094
|
throw new Error("Parent delegation does not allow sub-delegation");
|
|
23456
24095
|
}
|
|
23457
|
-
|
|
24096
|
+
const parentResources = parentDelegation.resources ?? [{
|
|
24097
|
+
service: parentDelegation.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
|
|
24098
|
+
space: parentDelegation.spaceId,
|
|
24099
|
+
path: parentDelegation.path,
|
|
24100
|
+
actions: parentDelegation.actions
|
|
24101
|
+
}];
|
|
24102
|
+
const requestedResources = params.resources ?? [{
|
|
24103
|
+
service: params.actions[0]?.split("/", 1)[0]?.replace(/^tinycloud\./, "") ?? "kv",
|
|
24104
|
+
space: parentDelegation.spaceId,
|
|
24105
|
+
path: params.path,
|
|
24106
|
+
actions: params.actions
|
|
24107
|
+
}];
|
|
24108
|
+
const resourceIsContained = (parent, child) => {
|
|
24109
|
+
const parentEncryption = parent.service === "encryption" || parent.service === "tinycloud.encryption";
|
|
24110
|
+
const childEncryption = child.service === "encryption" || child.service === "tinycloud.encryption";
|
|
24111
|
+
if (parentEncryption || childEncryption) return parentEncryption && childEncryption && parent.path === child.path && child.actions.every((action) => parent.actions.includes(action));
|
|
24112
|
+
const pathContained = parent.path === child.path || parent.path.endsWith("/") && child.path.startsWith(parent.path) || child.path.startsWith(`${parent.path}/`);
|
|
24113
|
+
return pathContained && child.actions.every((action) => parent.actions.includes(action));
|
|
24114
|
+
};
|
|
24115
|
+
if (requestedResources.length === 0 || requestedResources.some((child) => !parentResources.some((parent) => resourceIsContained(parent, child)))) {
|
|
23458
24116
|
throw new Error(
|
|
23459
|
-
|
|
24117
|
+
"Sub-delegation resources exceed the received delegation"
|
|
23460
24118
|
);
|
|
23461
24119
|
}
|
|
23462
|
-
const parentActions = new Set(parentDelegation.actions);
|
|
23463
|
-
for (const action of params.actions) {
|
|
23464
|
-
if (!parentActions.has(action)) {
|
|
23465
|
-
throw new Error(
|
|
23466
|
-
`Sub-delegation action "${action}" is not in parent's actions: ${parentDelegation.actions.join(", ")}`
|
|
23467
|
-
);
|
|
23468
|
-
}
|
|
23469
|
-
}
|
|
23470
24120
|
const now = /* @__PURE__ */ new Date();
|
|
23471
24121
|
const expiryMs = params.expiryMs ?? 60 * 60 * 1e3;
|
|
23472
24122
|
const requestedExpiry = new Date(now.getTime() + expiryMs);
|
|
23473
24123
|
const actualExpiry = requestedExpiry > parentDelegation.expiry ? parentDelegation.expiry : requestedExpiry;
|
|
23474
24124
|
const abilities = {};
|
|
23475
|
-
const
|
|
23476
|
-
const
|
|
23477
|
-
|
|
23478
|
-
|
|
23479
|
-
|
|
23480
|
-
|
|
23481
|
-
|
|
23482
|
-
abilities
|
|
23483
|
-
|
|
23484
|
-
if (duckdbActions.length > 0) {
|
|
23485
|
-
abilities.duckdb = { [params.path]: duckdbActions };
|
|
24125
|
+
const rawAbilities = {};
|
|
24126
|
+
for (const resource of requestedResources) {
|
|
24127
|
+
const service = resource.service.replace(/^tinycloud\./, "");
|
|
24128
|
+
if (service === "encryption") {
|
|
24129
|
+
rawAbilities[resource.path] = [...resource.actions];
|
|
24130
|
+
continue;
|
|
24131
|
+
}
|
|
24132
|
+
abilities[service] ?? (abilities[service] = {});
|
|
24133
|
+
abilities[service][resource.path] = [...resource.actions];
|
|
23486
24134
|
}
|
|
23487
24135
|
const targetHost = parentDelegation.host ?? this.config.host;
|
|
23488
24136
|
const prepared = this.wasmBindings.prepareSession({
|
|
@@ -23494,7 +24142,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23494
24142
|
expirationTime: actualExpiry.toISOString(),
|
|
23495
24143
|
spaceId: parentDelegation.spaceId,
|
|
23496
24144
|
delegateUri: params.delegateDID,
|
|
23497
|
-
parents: [parentDelegation.cid]
|
|
24145
|
+
parents: [parentDelegation.cid],
|
|
24146
|
+
...Object.keys(rawAbilities).length > 0 ? { rawAbilities } : {}
|
|
23498
24147
|
});
|
|
23499
24148
|
const signature2 = await this.signer.signMessage(prepared.siwe);
|
|
23500
24149
|
const subDelegationSession = this.wasmBindings.completeSessionSetup({
|
|
@@ -23512,14 +24161,20 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23512
24161
|
cid: subDelegationSession.delegationCid,
|
|
23513
24162
|
delegationHeader: subDelegationSession.delegationHeader,
|
|
23514
24163
|
spaceId: parentDelegation.spaceId,
|
|
23515
|
-
path:
|
|
23516
|
-
actions:
|
|
24164
|
+
path: requestedResources[0].path,
|
|
24165
|
+
actions: requestedResources[0].actions,
|
|
23517
24166
|
disableSubDelegation: params.disableSubDelegation ?? false,
|
|
23518
24167
|
expiry: actualExpiry,
|
|
23519
24168
|
delegateDID: params.delegateDID,
|
|
23520
24169
|
ownerAddress: parentDelegation.ownerAddress,
|
|
23521
24170
|
chainId: parentDelegation.chainId,
|
|
23522
|
-
host: targetHost
|
|
24171
|
+
host: targetHost,
|
|
24172
|
+
resources: requestedResources.map((resource) => ({
|
|
24173
|
+
service: resource.service.replace(/^tinycloud\./, ""),
|
|
24174
|
+
space: resource.service.replace(/^tinycloud\./, "") === "encryption" ? "encryption" : resource.space ?? parentDelegation.spaceId,
|
|
24175
|
+
path: resource.path,
|
|
24176
|
+
actions: [...resource.actions]
|
|
24177
|
+
}))
|
|
23523
24178
|
};
|
|
23524
24179
|
}
|
|
23525
24180
|
};
|
|
@@ -23692,10 +24347,54 @@ var FileSessionStorage = class {
|
|
|
23692
24347
|
}
|
|
23693
24348
|
};
|
|
23694
24349
|
|
|
24350
|
+
// src/authorization/openKeyBridge.ts
|
|
24351
|
+
function wireOpenKeyAuthorize(openkey) {
|
|
24352
|
+
return async (input) => {
|
|
24353
|
+
const result = await openkey.authorizeTinyCloud({
|
|
24354
|
+
protocolVersion: 1,
|
|
24355
|
+
siwe: input.siwe,
|
|
24356
|
+
keyId: input.keyId,
|
|
24357
|
+
jwk: input.jwk,
|
|
24358
|
+
host: input.host,
|
|
24359
|
+
// Forward the presentation envelope verbatim — the OpenKey SDK
|
|
24360
|
+
// validates and size-bounds it before use.
|
|
24361
|
+
presentation: input.presentation
|
|
24362
|
+
});
|
|
24363
|
+
if (result.protocolVersion !== 1) {
|
|
24364
|
+
throw new Error(
|
|
24365
|
+
`wireOpenKeyAuthorize: OpenKey returned unsupported protocolVersion ${result.protocolVersion}`
|
|
24366
|
+
);
|
|
24367
|
+
}
|
|
24368
|
+
if (typeof result.signedMessage !== "string" || !result.signedMessage) {
|
|
24369
|
+
throw new Error(
|
|
24370
|
+
"wireOpenKeyAuthorize: OpenKey returned no signedMessage \u2014 cannot complete a TinyCloud session"
|
|
24371
|
+
);
|
|
24372
|
+
}
|
|
24373
|
+
if (!Array.isArray(result.selectedActionKeys)) {
|
|
24374
|
+
throw new Error(
|
|
24375
|
+
"wireOpenKeyAuthorize: OpenKey returned no selectedActionKeys"
|
|
24376
|
+
);
|
|
24377
|
+
}
|
|
24378
|
+
if (!Array.isArray(result.permissions)) {
|
|
24379
|
+
throw new Error(
|
|
24380
|
+
"wireOpenKeyAuthorize: OpenKey returned no permissions"
|
|
24381
|
+
);
|
|
24382
|
+
}
|
|
24383
|
+
return {
|
|
24384
|
+
protocolVersion: 1,
|
|
24385
|
+
address: result.address,
|
|
24386
|
+
signature: result.signature,
|
|
24387
|
+
signedMessage: result.signedMessage,
|
|
24388
|
+
selectedActionKeys: result.selectedActionKeys,
|
|
24389
|
+
permissions: result.permissions
|
|
24390
|
+
};
|
|
24391
|
+
};
|
|
24392
|
+
}
|
|
24393
|
+
|
|
23695
24394
|
// src/index.ts
|
|
23696
24395
|
import {
|
|
23697
24396
|
ACCOUNT_REGISTRY_PATH,
|
|
23698
|
-
ACCOUNT_REGISTRY_SPACE as
|
|
24397
|
+
ACCOUNT_REGISTRY_SPACE as ACCOUNT_REGISTRY_SPACE3,
|
|
23699
24398
|
DEFAULT_MANIFEST_SPACE as DEFAULT_MANIFEST_SPACE2,
|
|
23700
24399
|
DEFAULT_MANIFEST_VERSION,
|
|
23701
24400
|
VAULT_PERMISSION_SERVICE,
|
|
@@ -24129,10 +24828,13 @@ import {
|
|
|
24129
24828
|
} from "@tinycloud/sdk-core";
|
|
24130
24829
|
import { ServiceContext as ServiceContext3 } from "@tinycloud/sdk-core";
|
|
24131
24830
|
export {
|
|
24831
|
+
ACCEPTED_MARKER_VERSIONS,
|
|
24132
24832
|
ACCOUNT_REGISTRY_PATH,
|
|
24133
|
-
|
|
24833
|
+
ACCOUNT_REGISTRY_SPACE3 as ACCOUNT_REGISTRY_SPACE,
|
|
24134
24834
|
AccountService,
|
|
24135
24835
|
AutoApproveSpaceCreationHandler2 as AutoApproveSpaceCreationHandler,
|
|
24836
|
+
BOOTSTRAP_COMPLETION_MARKER_KEY,
|
|
24837
|
+
BOOTSTRAP_COMPLETION_MARKER_VERSION,
|
|
24136
24838
|
CapabilityKeyRegistry2 as CapabilityKeyRegistry,
|
|
24137
24839
|
CapabilityKeyRegistryErrorCodes,
|
|
24138
24840
|
CaveatedDelegationUnsupportedError2 as CaveatedDelegationUnsupportedError,
|
|
@@ -24269,7 +24971,8 @@ export {
|
|
|
24269
24971
|
tinyCloudDebugLogger,
|
|
24270
24972
|
validateEnvelope,
|
|
24271
24973
|
validateManifest,
|
|
24272
|
-
verifyDecryptResponse
|
|
24974
|
+
verifyDecryptResponse,
|
|
24975
|
+
wireOpenKeyAuthorize
|
|
24273
24976
|
};
|
|
24274
24977
|
/*! Bundled license information:
|
|
24275
24978
|
|