@tinycloud/node-sdk 3.8.0-beta.1 → 3.8.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{TinyCloudNode-Ckqu_fqH.d.ts → TinyCloudNode-DnD-SH6t.d.ts} +26 -3
- package/dist/{TinyCloudNode-DDfx8Fx5.d.cts → TinyCloudNode-Dy65bxqs.d.cts} +26 -3
- package/dist/{authority-DcyJpajL.d.ts → authority-BQnTX3fI.d.ts} +20 -4
- package/dist/{authority-BwM_s3Ya.d.cts → authority-Rbye1U4o.d.cts} +20 -4
- package/dist/core.cjs +189 -25
- package/dist/core.cjs.map +1 -1
- package/dist/core.d.cts +5 -4
- package/dist/core.d.ts +5 -4
- package/dist/core.js +183 -21
- package/dist/core.js.map +1 -1
- package/dist/credentials-core.d.cts +1 -1
- package/dist/credentials-core.d.ts +1 -1
- package/dist/credentials.d.cts +1 -1
- package/dist/credentials.d.ts +1 -1
- package/dist/{delegation-0LKZkmP2.d.cts → delegation-Ckc3UETl.d.cts} +1 -1
- package/dist/{delegation-0LKZkmP2.d.ts → delegation-Ckc3UETl.d.ts} +1 -1
- package/dist/encryption-keys.d.cts +2 -2
- package/dist/encryption-keys.d.ts +2 -2
- package/dist/index.cjs +239 -37
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +226 -24
- package/dist/index.js.map +1 -1
- package/dist/replication/authority.cjs +39 -0
- package/dist/replication/authority.cjs.map +1 -1
- package/dist/replication/authority.d.cts +4 -3
- package/dist/replication/authority.d.ts +4 -3
- package/dist/replication/authority.js +38 -0
- package/dist/replication/authority.js.map +1 -1
- package/dist/replication/runtime.cjs +1 -0
- package/dist/replication/runtime.cjs.map +1 -1
- package/dist/replication/runtime.d.cts +1 -1
- package/dist/replication/runtime.d.ts +1 -1
- package/dist/replication/runtime.js +1 -0
- package/dist/replication/runtime.js.map +1 -1
- package/dist/{runtime-v62FzQHz.d.ts → runtime-CO4VXnPq.d.ts} +2 -1
- package/dist/{runtime-D1o9Eybt.d.cts → runtime-mRhSu-vJ.d.cts} +2 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -17089,6 +17089,9 @@ function assertValidReplicationConfig(replication) {
|
|
|
17089
17089
|
if (replication?.syncScope !== void 0 && replication.syncScope !== "prefixes") {
|
|
17090
17090
|
throw new TypeError('replication.syncScope must be "prefixes"');
|
|
17091
17091
|
}
|
|
17092
|
+
if (replication?.contentFetch !== void 0 && replication.contentFetch !== "eager" && replication.contentFetch !== "lazy") {
|
|
17093
|
+
throw new TypeError('replication.contentFetch must be "eager" or "lazy"');
|
|
17094
|
+
}
|
|
17092
17095
|
if (replication === void 0 || !replication.enabled) return;
|
|
17093
17096
|
if (replication.storage === void 0 || replication.storage === null) {
|
|
17094
17097
|
throw new TypeError("replication.enabled requires replication.storage");
|
|
@@ -17143,6 +17146,7 @@ function resolvedOptions(options) {
|
|
|
17143
17146
|
maxStalenessMs: options.maxStalenessMs ?? 12e4,
|
|
17144
17147
|
staleSyncTimeoutMs: options.staleSyncTimeoutMs ?? 5e3,
|
|
17145
17148
|
allowSecrets: options.allowSecrets === true,
|
|
17149
|
+
contentFetch: options.contentFetch ?? "eager",
|
|
17146
17150
|
verify: options.verify === true
|
|
17147
17151
|
};
|
|
17148
17152
|
}
|
|
@@ -17414,6 +17418,7 @@ __export(authority_exports, {
|
|
|
17414
17418
|
assertValidReplicationConfig: () => assertValidReplicationConfig,
|
|
17415
17419
|
augmentSignInEntriesWithReplication: () => augmentSignInEntriesWithReplication,
|
|
17416
17420
|
createReplicationAuthority: () => createReplicationAuthority,
|
|
17421
|
+
eraseReplicaLocalCopy: () => eraseReplicaLocalCopy,
|
|
17417
17422
|
establishReplicaSessionStamp: () => establishReplicaSessionStamp,
|
|
17418
17423
|
hasUnrestrictedGetCoverage: () => hasUnrestrictedGetCoverage,
|
|
17419
17424
|
mintNodeReplicationGrant: () => mintNodeReplicationGrant,
|
|
@@ -17444,6 +17449,38 @@ function reportReplicaDenial(result, reason, onEvent, context) {
|
|
|
17444
17449
|
} catch {
|
|
17445
17450
|
}
|
|
17446
17451
|
}
|
|
17452
|
+
async function eraseReplicaLocalCopy(lifecycle, trigger, denyStatus, handlers) {
|
|
17453
|
+
if (lifecycle?.kind !== "indexeddb" || lifecycle.eraseLocalCopy === void 0) return void 0;
|
|
17454
|
+
let outcome = { status: "failed", code: "STORAGE_ERROR", marker: false };
|
|
17455
|
+
for (let attempt = 0; attempt <= ERASE_RETRY_DELAYS_MS.length; attempt++) {
|
|
17456
|
+
if (attempt > 0) await new Promise((resolve2) => setTimeout(resolve2, ERASE_RETRY_DELAYS_MS[attempt - 1]));
|
|
17457
|
+
try {
|
|
17458
|
+
outcome = await lifecycle.eraseLocalCopy({ signal: AbortSignal.timeout(ERASE_ATTEMPT_MS) });
|
|
17459
|
+
} catch {
|
|
17460
|
+
outcome = { status: "failed", code: "STORAGE_ERROR", marker: false };
|
|
17461
|
+
}
|
|
17462
|
+
if (outcome.status !== "failed") break;
|
|
17463
|
+
}
|
|
17464
|
+
const keysKept = denyStatus === "marker" || denyStatus === "unguarded";
|
|
17465
|
+
const purge = outcome.status === "erased" ? { ok: true } : { ok: false, retryNow: outcome.status === "failed" };
|
|
17466
|
+
if (outcome.status === "erased" || outcome.status === "refused" && !keysKept) return { outcome, purge };
|
|
17467
|
+
const logoutPending = outcome.status === "failed" && outcome.marker;
|
|
17468
|
+
const event = {
|
|
17469
|
+
type: "replica.purge.failed",
|
|
17470
|
+
trigger,
|
|
17471
|
+
reason: logoutPending ? "logout_pending" : "gate_unavailable",
|
|
17472
|
+
code: outcome.code,
|
|
17473
|
+
retryNow: outcome.status === "failed"
|
|
17474
|
+
};
|
|
17475
|
+
for (const handler of handlers) {
|
|
17476
|
+
try {
|
|
17477
|
+
handler(event);
|
|
17478
|
+
} catch (error) {
|
|
17479
|
+
console.error('Error in "replica.purge.failed" handler:', error);
|
|
17480
|
+
}
|
|
17481
|
+
}
|
|
17482
|
+
return logoutPending ? { outcome, purge, logoutPending } : { outcome, purge };
|
|
17483
|
+
}
|
|
17447
17484
|
function verifiedOwnerConsentAnchor(session, wasmBindings, exactVerifiedRecap2) {
|
|
17448
17485
|
if (typeof session.siwe !== "string" || typeof session.signature !== "string" || typeof session.address !== "string" || typeof session.chainId !== "number") return null;
|
|
17449
17486
|
try {
|
|
@@ -17702,7 +17739,7 @@ function createReplicationAuthority(host) {
|
|
|
17702
17739
|
};
|
|
17703
17740
|
return authority;
|
|
17704
17741
|
}
|
|
17705
|
-
var import_siwe3, import_sdk_core12, import_replica, parseUcanGrant, KV_GET2, KV_SYNC2, KV_SERVICE2, AUTHORITY_EXPIRY_MARGIN_MS;
|
|
17742
|
+
var import_siwe3, import_sdk_core12, import_replica, parseUcanGrant, ERASE_RETRY_DELAYS_MS, ERASE_ATTEMPT_MS, KV_GET2, KV_SYNC2, KV_SERVICE2, AUTHORITY_EXPIRY_MARGIN_MS;
|
|
17706
17743
|
var init_authority = __esm({
|
|
17707
17744
|
"src/replication/authority.ts"() {
|
|
17708
17745
|
"use strict";
|
|
@@ -17712,6 +17749,8 @@ var init_authority = __esm({
|
|
|
17712
17749
|
init_config();
|
|
17713
17750
|
init_authority_sign_in();
|
|
17714
17751
|
parseUcanGrant = import_replica.parseUcanGrant;
|
|
17752
|
+
ERASE_RETRY_DELAYS_MS = [150, 450];
|
|
17753
|
+
ERASE_ATTEMPT_MS = 3e3;
|
|
17715
17754
|
KV_GET2 = import_sdk_core12.KV.GET;
|
|
17716
17755
|
KV_SYNC2 = import_sdk_core12.KV.SYNC;
|
|
17717
17756
|
KV_SERVICE2 = "tinycloud.kv";
|
|
@@ -18789,13 +18828,42 @@ function getNodeReplicationLoaders() {
|
|
|
18789
18828
|
|
|
18790
18829
|
// src/TinyCloudNode.ts
|
|
18791
18830
|
var import_sdk_core10 = require("@tinycloud/sdk-core");
|
|
18792
|
-
var
|
|
18831
|
+
var import_sdk_services4 = require("@tinycloud/sdk-services");
|
|
18793
18832
|
var import_requestLog2 = require("@tinycloud/sdk-services/telemetry/requestLog");
|
|
18833
|
+
var import_resource2 = require("@tinycloud/sdk-services/encryption/resource");
|
|
18794
18834
|
|
|
18795
18835
|
// src/authorization/NodeUserAuthorization.ts
|
|
18796
18836
|
var import_node_crypto = require("crypto");
|
|
18797
18837
|
var import_siwe = require("siwe");
|
|
18798
18838
|
var import_sdk_core5 = require("@tinycloud/sdk-core");
|
|
18839
|
+
var import_resource = require("@tinycloud/sdk-services/encryption/resource");
|
|
18840
|
+
var import_sdk_services3 = require("@tinycloud/sdk-services");
|
|
18841
|
+
|
|
18842
|
+
// src/authorization/appKeyHostBinding.ts
|
|
18843
|
+
var DOMAIN_LABEL = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
18844
|
+
var IPV4 = /^\d+\.\d+\.\d+\.\d+$/;
|
|
18845
|
+
var APP_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
18846
|
+
function appKeyHostBinding(domain, appId) {
|
|
18847
|
+
if (!APP_ID.test(appId)) return false;
|
|
18848
|
+
if (domain.endsWith(":")) return false;
|
|
18849
|
+
let url;
|
|
18850
|
+
try {
|
|
18851
|
+
url = new URL(`https://${domain}`);
|
|
18852
|
+
} catch {
|
|
18853
|
+
return false;
|
|
18854
|
+
}
|
|
18855
|
+
if (url.protocol !== "https:" || url.port !== "" || url.username !== "" || url.password !== "") return false;
|
|
18856
|
+
if (url.pathname !== "/" || url.search !== "" || url.hash !== "") return false;
|
|
18857
|
+
const host = url.hostname;
|
|
18858
|
+
if (host.startsWith("[") || host.endsWith(".") || IPV4.test(host)) return false;
|
|
18859
|
+
const labels = host.split(".");
|
|
18860
|
+
if (labels.length < 2 || labels[labels.length - 1] === "localhost") return false;
|
|
18861
|
+
if (!labels.every((label) => DOMAIN_LABEL.test(label))) return false;
|
|
18862
|
+
const reversed = labels.reverse().join(".");
|
|
18863
|
+
return appId === reversed || appId.startsWith(`${reversed}.`);
|
|
18864
|
+
}
|
|
18865
|
+
|
|
18866
|
+
// src/authorization/NodeUserAuthorization.ts
|
|
18799
18867
|
init_authority_sign_in();
|
|
18800
18868
|
|
|
18801
18869
|
// src/authorization/caveatRefusal.ts
|
|
@@ -19307,6 +19375,23 @@ var NodeUserAuthorization = class {
|
|
|
19307
19375
|
);
|
|
19308
19376
|
return this._capabilityRequest;
|
|
19309
19377
|
}
|
|
19378
|
+
async refreshNamedKeyFeatureForSignIn(address, chainId) {
|
|
19379
|
+
const request = this.getCapabilityRequest();
|
|
19380
|
+
const siweDomain = this.siweConfig?.domain ?? this.domain;
|
|
19381
|
+
const hasNamedKeys = request?.manifests.some((manifest) => manifest.keys?.length && appKeyHostBinding(siweDomain, manifest.keys_app_id ?? manifest.app_id));
|
|
19382
|
+
if (hasNamedKeys) {
|
|
19383
|
+
await this.resolveTinyCloudHostsForSignIn(address, chainId);
|
|
19384
|
+
await this.refreshNodeInfo();
|
|
19385
|
+
}
|
|
19386
|
+
}
|
|
19387
|
+
/** Manifest key paths that this sign-in cannot grant. */
|
|
19388
|
+
get unavailableKeyPaths() {
|
|
19389
|
+
const request = this.getCapabilityRequest();
|
|
19390
|
+
const siweDomain = this.siweConfig?.domain ?? this.domain;
|
|
19391
|
+
return (request?.manifests ?? []).flatMap(
|
|
19392
|
+
(manifest) => this._nodeFeatures.includes("encryption-named-keys-v1") && appKeyHostBinding(siweDomain, manifest.keys_app_id ?? manifest.app_id) ? [] : (manifest.keys ?? []).map((entry) => "path" in entry ? entry.path : entry.prefix)
|
|
19393
|
+
);
|
|
19394
|
+
}
|
|
19310
19395
|
resolveSpaceName(space, address, chainId) {
|
|
19311
19396
|
if (space.startsWith("tinycloud:")) {
|
|
19312
19397
|
return space;
|
|
@@ -19498,15 +19583,31 @@ var NodeUserAuthorization = class {
|
|
|
19498
19583
|
}
|
|
19499
19584
|
const rawAbilities = {};
|
|
19500
19585
|
const currentDid = (0, import_sdk_core5.pkhDid)(address, chainId);
|
|
19586
|
+
if (this._nodeFeatures.includes("encryption-named-keys-v1")) {
|
|
19587
|
+
const siweDomain = this.siweConfig?.domain ?? this.domain;
|
|
19588
|
+
for (const manifest of request.manifests) {
|
|
19589
|
+
if (!appKeyHostBinding(siweDomain, manifest.keys_app_id ?? manifest.app_id)) continue;
|
|
19590
|
+
for (const declaration of manifest.keys ?? []) {
|
|
19591
|
+
const path = "path" in declaration ? declaration.path : declaration.prefix;
|
|
19592
|
+
const resource = `urn:tinycloud:encryption:${currentDid}:default/${path}`;
|
|
19593
|
+
for (const ability of declaration.abilities) {
|
|
19594
|
+
addRawAbility(rawAbilities, resource, `tinycloud.encryption/${ability}`);
|
|
19595
|
+
}
|
|
19596
|
+
}
|
|
19597
|
+
}
|
|
19598
|
+
}
|
|
19501
19599
|
const spaceResources = request.resources.filter((entry) => {
|
|
19502
19600
|
if (entry.service !== import_sdk_core5.ENCRYPTION_PERMISSION_SERVICE) {
|
|
19503
19601
|
return true;
|
|
19504
19602
|
}
|
|
19603
|
+
const parsed = entry.path.includes("/") ? (0, import_resource.parseEncryptionResource)(entry.path) : { ...(0, import_sdk_services3.parseNetworkId)(entry.path), kind: "network" };
|
|
19604
|
+
if (parsed.kind !== "network") {
|
|
19605
|
+
throw new import_sdk_core5.ManifestValidationError("named keys go in manifest.keys");
|
|
19606
|
+
}
|
|
19505
19607
|
for (const action of entry.actions) {
|
|
19506
19608
|
addRawAbility(rawAbilities, entry.path, action);
|
|
19507
19609
|
}
|
|
19508
|
-
if (entry.actions.includes(DECRYPT_ACTION)) {
|
|
19509
|
-
const parsed = (0, import_sdk_core5.parseNetworkId)(entry.path);
|
|
19610
|
+
if (entry.actions.includes(DECRYPT_ACTION) && parsed.kind === "network") {
|
|
19510
19611
|
if (didPrincipalMatches(parsed.ownerDid, currentDid)) {
|
|
19511
19612
|
addRawAbility(rawAbilities, entry.path, NETWORK_CREATE_ACTION);
|
|
19512
19613
|
}
|
|
@@ -19902,6 +20003,8 @@ var NodeUserAuthorization = class {
|
|
|
19902
20003
|
this._chainId = chainId;
|
|
19903
20004
|
await this.resolveTinyCloudHostsForSignIn(address, chainId);
|
|
19904
20005
|
this.assertSignInCurrent(generation);
|
|
20006
|
+
await this.refreshNamedKeyFeatureForSignIn(address, chainId);
|
|
20007
|
+
this.assertSignInCurrent(generation);
|
|
19905
20008
|
const keyId = `session-${Date.now()}`;
|
|
19906
20009
|
this.sessionManager.renameSessionKeyId(this.activeSessionKeyId, keyId);
|
|
19907
20010
|
this.activeSessionKeyId = keyId;
|
|
@@ -20048,6 +20151,7 @@ var NodeUserAuthorization = class {
|
|
|
20048
20151
|
refuseCaveated(this.getCapabilityRequest()?.manifests);
|
|
20049
20152
|
const address = (0, import_sdk_core5.canonicalizeAddress)(await this.signer.getAddress());
|
|
20050
20153
|
const chainId = await this.signer.getChainId();
|
|
20154
|
+
await this.refreshNamedKeyFeatureForSignIn(address, chainId);
|
|
20051
20155
|
const keyId = `session-${Date.now()}`;
|
|
20052
20156
|
this.sessionManager.renameSessionKeyId(this.activeSessionKeyId, keyId);
|
|
20053
20157
|
this.activeSessionKeyId = keyId;
|
|
@@ -21101,6 +21205,7 @@ var ROOT_DELEGATION_ACTIONS = [
|
|
|
21101
21205
|
import_sdk_core10.DUCKDB.ALL
|
|
21102
21206
|
];
|
|
21103
21207
|
var DEFAULT_SESSION_EXPIRATION_MS = import_sdk_core10.EXPIRY.SESSION_MS;
|
|
21208
|
+
var OWNER_REVOCATION_WINDOW_MS = 10 * 6e4;
|
|
21104
21209
|
function unifiedRootAttenuation(capabilities) {
|
|
21105
21210
|
const attenuation = {};
|
|
21106
21211
|
for (const capability of capabilities) {
|
|
@@ -21565,6 +21670,7 @@ function replicaRestoreRefusal(error) {
|
|
|
21565
21670
|
case "purge_pending":
|
|
21566
21671
|
case "logout_pending":
|
|
21567
21672
|
case "gate_unavailable":
|
|
21673
|
+
case "wallet_provider_required":
|
|
21568
21674
|
return error.gate;
|
|
21569
21675
|
}
|
|
21570
21676
|
}
|
|
@@ -21599,6 +21705,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21599
21705
|
this.auth = null;
|
|
21600
21706
|
this.tc = null;
|
|
21601
21707
|
this._chainId = 1;
|
|
21708
|
+
this.replicaPurgeFailedHandlers = /* @__PURE__ */ new Set();
|
|
21602
21709
|
/** Mints the replica session stamp on each fresh sign-in (TC-858 §4.1). Undefined when replication is off. */
|
|
21603
21710
|
this.establishReplicaStamp = async (session) => {
|
|
21604
21711
|
const replication = this.config.replication;
|
|
@@ -21630,6 +21737,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21630
21737
|
return void 0;
|
|
21631
21738
|
}
|
|
21632
21739
|
};
|
|
21740
|
+
this._keysUnavailable = [];
|
|
21633
21741
|
this._baseVaults = /* @__PURE__ */ new Map();
|
|
21634
21742
|
this._baseSecrets = /* @__PURE__ */ new Map();
|
|
21635
21743
|
this._secrets = /* @__PURE__ */ new Map();
|
|
@@ -21805,6 +21913,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21805
21913
|
return instance ? instance.control.purge(input) : runtime.then((ready) => ready.control.purge(input));
|
|
21806
21914
|
},
|
|
21807
21915
|
clearPending: async () => (await runtime).control.clearPending(),
|
|
21916
|
+
purgeLocalCopy: async () => (await this.eraseReplicaLocalCopy("purge_local_copy"))?.purge ?? { ok: false, retryNow: false },
|
|
21808
21917
|
close: async () => (await runtime).control.close()
|
|
21809
21918
|
};
|
|
21810
21919
|
}
|
|
@@ -21859,18 +21968,14 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
21859
21968
|
get freshEncryptionNetworkCreated() {
|
|
21860
21969
|
return this._freshEncryptionNetworkCreated;
|
|
21861
21970
|
}
|
|
21862
|
-
/**
|
|
21863
|
-
* Subscribe to `storage.full`. It fires on the first write any service had
|
|
21864
|
-
* rejected because the owner's TinyCloud storage is full
|
|
21865
|
-
* (`STORAGE_QUOTA_EXCEEDED` or `STORAGE_LIMIT_REACHED`), so an app can show
|
|
21866
|
-
* one read-only banner. Reads keep working. For the signed-in account's own
|
|
21867
|
-
* storage it fires once per episode: the episode ends only when a write to
|
|
21868
|
-
* that storage succeeds, or on sign-out. A `storage.status()` read does not
|
|
21869
|
-
* end it. A rejection on another owner's delegated space fires once per
|
|
21870
|
-
* owner, until that owner's `storage.status()` reports space again. Returns
|
|
21871
|
-
* the unsubscribe function.
|
|
21872
|
-
*/
|
|
21873
21971
|
on(event, handler) {
|
|
21972
|
+
if (event === "replica.purge.failed") {
|
|
21973
|
+
const listener = handler;
|
|
21974
|
+
this.replicaPurgeFailedHandlers.add(listener);
|
|
21975
|
+
return () => {
|
|
21976
|
+
this.replicaPurgeFailedHandlers.delete(listener);
|
|
21977
|
+
};
|
|
21978
|
+
}
|
|
21874
21979
|
if (event !== "storage.full") {
|
|
21875
21980
|
throw new Error(`Unknown TinyCloud event: ${String(event)}`);
|
|
21876
21981
|
}
|
|
@@ -22351,6 +22456,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22351
22456
|
const leavingAuth = this.auth;
|
|
22352
22457
|
const leavingContext = this._serviceContext;
|
|
22353
22458
|
let denied;
|
|
22459
|
+
let erased;
|
|
22354
22460
|
try {
|
|
22355
22461
|
this.teardownSession();
|
|
22356
22462
|
} finally {
|
|
@@ -22361,6 +22467,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22361
22467
|
try {
|
|
22362
22468
|
await this.accountRegistryTail;
|
|
22363
22469
|
await this.quiesceReplicaRuntime();
|
|
22470
|
+
if (denied) erased = await this.eraseReplicaLocalCopy("sign_out", denied.status);
|
|
22364
22471
|
} finally {
|
|
22365
22472
|
if (leavingAddress) {
|
|
22366
22473
|
await (leavingAuth ? leavingAuth.clearPersistedSession(leavingAddress) : this.config.sessionStorage?.clear(leavingAddress));
|
|
@@ -22368,8 +22475,37 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22368
22475
|
}
|
|
22369
22476
|
}
|
|
22370
22477
|
}
|
|
22371
|
-
if (denied?.status === "unguarded") {
|
|
22372
|
-
throw
|
|
22478
|
+
if (denied?.status === "unguarded" && erased?.outcome.status !== "erased") {
|
|
22479
|
+
throw Object.assign(
|
|
22480
|
+
new Error(`Replica gate was not denied (${denied.code ?? "unknown"}); the replica may still be restorable.`),
|
|
22481
|
+
{
|
|
22482
|
+
code: "REPLICA_PURGE_FAILED",
|
|
22483
|
+
reason: "gate_unavailable",
|
|
22484
|
+
retryNow: erased?.outcome.status === "failed"
|
|
22485
|
+
}
|
|
22486
|
+
);
|
|
22487
|
+
}
|
|
22488
|
+
}
|
|
22489
|
+
/**
|
|
22490
|
+
* Erases the browser local copy after a deny (retry, event and next-load marker live in the lazy
|
|
22491
|
+
* replication authority). Never rejects; `undefined` when replication is off or the storage keeps
|
|
22492
|
+
* no browser database.
|
|
22493
|
+
*/
|
|
22494
|
+
async eraseReplicaLocalCopy(trigger, denyStatus) {
|
|
22495
|
+
const replication = this.config.replication;
|
|
22496
|
+
if (!replication?.enabled) return void 0;
|
|
22497
|
+
try {
|
|
22498
|
+
const { eraseReplicaLocalCopy: eraseReplicaLocalCopy2 } = await getNodeReplicationLoaders().authority();
|
|
22499
|
+
const erased = await eraseReplicaLocalCopy2(
|
|
22500
|
+
replication.storage.lifecycle,
|
|
22501
|
+
trigger,
|
|
22502
|
+
denyStatus,
|
|
22503
|
+
this.replicaPurgeFailedHandlers
|
|
22504
|
+
);
|
|
22505
|
+
if (erased?.logoutPending) this.replicaRestoreGate = "logout_pending";
|
|
22506
|
+
return erased;
|
|
22507
|
+
} catch {
|
|
22508
|
+
return void 0;
|
|
22373
22509
|
}
|
|
22374
22510
|
}
|
|
22375
22511
|
/**
|
|
@@ -22429,6 +22565,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22429
22565
|
this._hooks = void 0;
|
|
22430
22566
|
this._vault = void 0;
|
|
22431
22567
|
this._encryption = void 0;
|
|
22568
|
+
this._namedKeyClient = void 0;
|
|
22569
|
+
this._keysUnavailable = [];
|
|
22432
22570
|
this._baseVaults.clear();
|
|
22433
22571
|
this._baseSecrets.clear();
|
|
22434
22572
|
this._secrets.clear();
|
|
@@ -22533,6 +22671,10 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22533
22671
|
const priorPrincipal = this.replicaSessionPrincipal ?? (this.address ? (0, import_sdk_core10.pkhDid)(this.address, this._chainId) : void 0);
|
|
22534
22672
|
if (this.config.replication?.enabled && priorPrincipal && priorPrincipal !== nextPrincipal) {
|
|
22535
22673
|
const denial = await this.denyReplicaGate({ reason: "account_switch", principal: priorPrincipal, next: nextPrincipal });
|
|
22674
|
+
if (denial) {
|
|
22675
|
+
await this.quiesceReplicaRuntime();
|
|
22676
|
+
await this.eraseReplicaLocalCopy("account_switch", denial.status);
|
|
22677
|
+
}
|
|
22536
22678
|
this.replicaRestoreGate = denial?.status === "unguarded" ? "gate_unavailable" : "stamp_stale";
|
|
22537
22679
|
}
|
|
22538
22680
|
if (address !== this._address || chainId !== this._chainId) {
|
|
@@ -22547,6 +22689,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22547
22689
|
this._hooks = void 0;
|
|
22548
22690
|
this._vault = void 0;
|
|
22549
22691
|
this._encryption = void 0;
|
|
22692
|
+
this._namedKeyClient = void 0;
|
|
22693
|
+
this._keysUnavailable = [];
|
|
22550
22694
|
this._baseVaults.clear();
|
|
22551
22695
|
this._baseSecrets.clear();
|
|
22552
22696
|
this._secrets.clear();
|
|
@@ -22599,6 +22743,26 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
22599
22743
|
this.assertSessionEpoch(epoch, "Sign-in");
|
|
22600
22744
|
await this.ensureRequestedEncryptionNetworks();
|
|
22601
22745
|
this.assertSessionEpoch(epoch, "Sign-in");
|
|
22746
|
+
this._keysUnavailable = this.auth?.unavailableKeyPaths ?? [];
|
|
22747
|
+
if (this._keysUnavailable.length > 0) {
|
|
22748
|
+
this.notificationHandler.warning(`Named keys unavailable: ${this._keysUnavailable.join(", ")}`);
|
|
22749
|
+
}
|
|
22750
|
+
if (this.nodeFeatures.includes("encryption-named-keys-v1")) {
|
|
22751
|
+
for (const manifest of this.auth?.capabilityRequest?.manifests ?? []) {
|
|
22752
|
+
for (const declaration of manifest.keys ?? []) {
|
|
22753
|
+
if ("path" in declaration && declaration.abilities.includes("key.create") && !this._keysUnavailable.includes(declaration.path)) {
|
|
22754
|
+
try {
|
|
22755
|
+
await this.keys.ensureKey(declaration.path);
|
|
22756
|
+
} catch (error) {
|
|
22757
|
+
this._keysUnavailable.push(declaration.path);
|
|
22758
|
+
const code = error && typeof error === "object" && "code" in error && typeof error.code === "string" && /^[A-Z_]+$/.test(error.code) ? error.code : "UNKNOWN";
|
|
22759
|
+
this.notificationHandler.warning(`Named key unavailable: ${declaration.path} (${code})`);
|
|
22760
|
+
}
|
|
22761
|
+
this.assertSessionEpoch(epoch, "Sign-in");
|
|
22762
|
+
}
|
|
22763
|
+
}
|
|
22764
|
+
}
|
|
22765
|
+
}
|
|
22602
22766
|
if (!bootstrapped && this.config.manifest === void 0 && this.config.capabilityRequest === void 0) {
|
|
22603
22767
|
await this.ensureOwnedSpaceHostedById(this.ownedSpaceId(import_sdk_core10.SECRETS_SPACE));
|
|
22604
22768
|
}
|
|
@@ -23175,7 +23339,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23175
23339
|
if (!this.currentSessionCoversAccountRegistry()) {
|
|
23176
23340
|
if (!_TinyCloudNode.accountRegistrySkipLogged) {
|
|
23177
23341
|
_TinyCloudNode.accountRegistrySkipLogged = true;
|
|
23178
|
-
|
|
23342
|
+
import_sdk_services4.tinyCloudDebugLogger.log("registry.skip");
|
|
23179
23343
|
}
|
|
23180
23344
|
return;
|
|
23181
23345
|
}
|
|
@@ -23386,7 +23550,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23386
23550
|
const networkIds = /* @__PURE__ */ new Set();
|
|
23387
23551
|
for (const resource of request.resources) {
|
|
23388
23552
|
if (resource.service === import_sdk_core10.ENCRYPTION_PERMISSION_SERVICE && resource.path.startsWith("urn:tinycloud:encryption:") && resource.actions.includes(DECRYPT_ACTION2)) {
|
|
23389
|
-
|
|
23553
|
+
const parsed = resource.path.includes("/") ? (0, import_resource2.parseEncryptionResource)(resource.path) : { ...(0, import_sdk_core10.parseNetworkId)(resource.path), kind: "network" };
|
|
23554
|
+
if (parsed.kind === "network") networkIds.add(resource.path);
|
|
23390
23555
|
}
|
|
23391
23556
|
}
|
|
23392
23557
|
return [...networkIds];
|
|
@@ -23936,6 +24101,8 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
23936
24101
|
this._hooks = stagedGraph.hooks;
|
|
23937
24102
|
this._vault = stagedGraph.vault;
|
|
23938
24103
|
this._encryption = stagedGraph.encryption;
|
|
24104
|
+
this._namedKeyClient = void 0;
|
|
24105
|
+
this._keysUnavailable = [];
|
|
23939
24106
|
this._capabilityRegistry = stagedGraph.capabilityRegistry;
|
|
23940
24107
|
this._keyProvider = stagedGraph.keyProvider;
|
|
23941
24108
|
this._sharingService = stagedGraph.sharingService;
|
|
@@ -24845,7 +25012,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
24845
25012
|
);
|
|
24846
25013
|
graph.assertActive();
|
|
24847
25014
|
if (!response.ok) {
|
|
24848
|
-
const permissionHint = response.status === 401 || response.status === 403 ? (0,
|
|
25015
|
+
const permissionHint = response.status === 401 || response.status === 403 ? (0, import_sdk_services4.parsePermissionHint)({
|
|
24849
25016
|
service: "tinycloud.encryption",
|
|
24850
25017
|
path: networkId,
|
|
24851
25018
|
actions: ["tinycloud.encryption/decrypt"]
|
|
@@ -25289,7 +25456,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25289
25456
|
* Sign a wallet (CACAO) revocation of the delegation `cid` for the node's
|
|
25290
25457
|
* `POST /revoke`, and return its Authorization header value. The node
|
|
25291
25458
|
* accepts it when the owner delegated `cid` or owns a space its abilities
|
|
25292
|
-
* target, whichever key issued it. Valid for
|
|
25459
|
+
* target, whichever key issued it. Valid for 10 minutes. Requires the
|
|
25293
25460
|
* owner wallet signer.
|
|
25294
25461
|
*/
|
|
25295
25462
|
async signOwnerRevocation(cid) {
|
|
@@ -25305,7 +25472,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25305
25472
|
chainId: session.chainId,
|
|
25306
25473
|
nonce: (0, import_siwe2.generateNonce)(),
|
|
25307
25474
|
issuedAt: issuedAt.toISOString(),
|
|
25308
|
-
expirationTime: new Date(issuedAt.getTime() +
|
|
25475
|
+
expirationTime: new Date(issuedAt.getTime() + OWNER_REVOCATION_WINDOW_MS).toISOString()
|
|
25309
25476
|
}).prepareMessage();
|
|
25310
25477
|
const signature2 = await this.signer.signMessage(siwe);
|
|
25311
25478
|
return this.wasmBindings.siweToDelegationHeaders({ siwe, signature: signature2 }).Authorization;
|
|
@@ -25532,6 +25699,40 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25532
25699
|
get encryption() {
|
|
25533
25700
|
return this.getEncryptionService();
|
|
25534
25701
|
}
|
|
25702
|
+
/** Named keys bound to the current session. */
|
|
25703
|
+
get keys() {
|
|
25704
|
+
return new Proxy({}, {
|
|
25705
|
+
get: (_target, property) => {
|
|
25706
|
+
if (property === "refreshNodeInfo") {
|
|
25707
|
+
return () => {
|
|
25708
|
+
void this._namedKeyClient?.then((client) => client.refreshNodeInfo());
|
|
25709
|
+
};
|
|
25710
|
+
}
|
|
25711
|
+
if (typeof property !== "string" || ![
|
|
25712
|
+
"getPublicKey",
|
|
25713
|
+
"describeKey",
|
|
25714
|
+
"createKey",
|
|
25715
|
+
"blockKey",
|
|
25716
|
+
"unblockKey",
|
|
25717
|
+
"ensureKey",
|
|
25718
|
+
"encrypt",
|
|
25719
|
+
"decrypt",
|
|
25720
|
+
"decryptMany"
|
|
25721
|
+
].includes(property)) return void 0;
|
|
25722
|
+
return (...args) => {
|
|
25723
|
+
this._namedKeyClient ?? (this._namedKeyClient = import("./encryption-keys.cjs").then(({ createNamedKeyClient }) => createNamedKeyClient(this)));
|
|
25724
|
+
return this._namedKeyClient.then((client) => {
|
|
25725
|
+
const method = client[property];
|
|
25726
|
+
return method.apply(client, args);
|
|
25727
|
+
});
|
|
25728
|
+
};
|
|
25729
|
+
}
|
|
25730
|
+
});
|
|
25731
|
+
}
|
|
25732
|
+
/** Manifest key paths unavailable during the latest `signIn()` attempt. */
|
|
25733
|
+
get keysUnavailable() {
|
|
25734
|
+
return this._keysUnavailable;
|
|
25735
|
+
}
|
|
25535
25736
|
async getEncryptionNetwork(nameOrNetworkId = this.getDefaultEncryptionNetworkId()) {
|
|
25536
25737
|
const networkId = nameOrNetworkId.startsWith("urn:tinycloud:encryption:") ? nameOrNetworkId : this.getDefaultEncryptionNetworkId(nameOrNetworkId);
|
|
25537
25738
|
return this.fetchEncryptionNetworkAt(
|
|
@@ -25659,7 +25860,7 @@ var _TinyCloudNode = class _TinyCloudNode {
|
|
|
25659
25860
|
);
|
|
25660
25861
|
if (result.status !== "ok") {
|
|
25661
25862
|
if (result.status === "permission_required") {
|
|
25662
|
-
const hint = (0,
|
|
25863
|
+
const hint = (0, import_sdk_services4.parsePermissionHint)(result.hint);
|
|
25663
25864
|
if (hint === void 0) return { status: "read_failed" };
|
|
25664
25865
|
return {
|
|
25665
25866
|
status: "permission_required",
|
|
@@ -28004,7 +28205,7 @@ var import_node_fs = require("fs");
|
|
|
28004
28205
|
var import_promises = require("fs/promises");
|
|
28005
28206
|
var import_node_path = require("path");
|
|
28006
28207
|
var import_sdk_core14 = require("@tinycloud/sdk-core");
|
|
28007
|
-
var
|
|
28208
|
+
var import_sdk_services5 = require("@tinycloud/sdk-services");
|
|
28008
28209
|
var import_contractConstants = require("@tinycloud/sdk-services/kv/replication/contractConstants");
|
|
28009
28210
|
init_authority();
|
|
28010
28211
|
var REPLICA_LOGOUT_PENDING_FILE = "replica-logout-pending.json";
|
|
@@ -28566,7 +28767,7 @@ function sha256Base32(material) {
|
|
|
28566
28767
|
return out.slice(0, 26);
|
|
28567
28768
|
}
|
|
28568
28769
|
function partitionDirOf(root, identity) {
|
|
28569
|
-
return (0, import_node_path.join)(root, sha256Base32((0,
|
|
28770
|
+
return (0, import_node_path.join)(root, sha256Base32((0, import_sdk_services5.replicationIdentityKey)(identity)));
|
|
28570
28771
|
}
|
|
28571
28772
|
function replicaHashOf(prefix, deviceDid) {
|
|
28572
28773
|
return sha256Base32(`${prefix}
|
|
@@ -28640,8 +28841,8 @@ function deviceRecordOf(value) {
|
|
|
28640
28841
|
}
|
|
28641
28842
|
function sameIdentity(a, b) {
|
|
28642
28843
|
try {
|
|
28643
|
-
const left = (0,
|
|
28644
|
-
const right = (0,
|
|
28844
|
+
const left = (0, import_sdk_services5.canonicalReplicationIdentity)(a);
|
|
28845
|
+
const right = (0, import_sdk_services5.canonicalReplicationIdentity)(b);
|
|
28645
28846
|
return left.host === right.host && left.space === right.space && left.principal === right.principal;
|
|
28646
28847
|
} catch {
|
|
28647
28848
|
return false;
|
|
@@ -28657,7 +28858,7 @@ async function ensurePartitionIdentity(dir, identity, dirSync) {
|
|
|
28657
28858
|
const file = (0, import_node_path.join)(dir, "identity.json");
|
|
28658
28859
|
const existing = await readJsonFile(file);
|
|
28659
28860
|
if (existing === void 0) {
|
|
28660
|
-
await writeJsonAtomic(file, { v: 1, ...(0,
|
|
28861
|
+
await writeJsonAtomic(file, { v: 1, ...(0, import_sdk_services5.canonicalReplicationIdentity)(identity) }, dirSync);
|
|
28661
28862
|
return;
|
|
28662
28863
|
}
|
|
28663
28864
|
const record = identityRecordOf(existing);
|
|
@@ -28756,7 +28957,7 @@ withGate_fn = async function(section) {
|
|
|
28756
28957
|
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
28757
28958
|
const [principalHash, identityKeyHash] = await Promise.all([
|
|
28758
28959
|
gate.principalHashOf(snapshot.salt, this.identity.principal),
|
|
28759
|
-
gate.identityHashOf(snapshot.salt, (0,
|
|
28960
|
+
gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(this.identity))
|
|
28760
28961
|
]);
|
|
28761
28962
|
const access = {
|
|
28762
28963
|
stamp: __privateGet(this, _stamp),
|
|
@@ -29027,7 +29228,8 @@ var SqliteReplicaHandle = class {
|
|
|
29027
29228
|
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
29028
29229
|
const engine = new (__privateGet(this, _replica)).Replica({
|
|
29029
29230
|
store: __privateGet(this, _store),
|
|
29030
|
-
transportFor
|
|
29231
|
+
transportFor,
|
|
29232
|
+
...this.spec.contentFetch === void 0 ? {} : { contentFetch: this.spec.contentFetch }
|
|
29031
29233
|
});
|
|
29032
29234
|
const running = engine.sync({ signal: abort.signal });
|
|
29033
29235
|
const registration = { promise: running, abort: () => abort.abort() };
|
|
@@ -29256,7 +29458,7 @@ function createSqliteReplicaStorage(options) {
|
|
|
29256
29458
|
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
29257
29459
|
const [principalHash, identityKeyHash] = await Promise.all([
|
|
29258
29460
|
gate.principalHashOf(snapshot.salt, principal),
|
|
29259
|
-
identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, (0,
|
|
29461
|
+
identity === void 0 ? Promise.resolve(void 0) : gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(identity))
|
|
29260
29462
|
]);
|
|
29261
29463
|
return profileSection(async () => {
|
|
29262
29464
|
assertLogoutClear(options.logoutPendingFile);
|
|
@@ -29279,7 +29481,7 @@ function createSqliteReplicaStorage(options) {
|
|
|
29279
29481
|
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
29280
29482
|
const [principalHash, identityKeyHash] = await Promise.all([
|
|
29281
29483
|
gate.principalHashOf(snapshot.salt, spec.identity.principal),
|
|
29282
|
-
gate.identityHashOf(snapshot.salt, (0,
|
|
29484
|
+
gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(spec.identity))
|
|
29283
29485
|
]);
|
|
29284
29486
|
const access = {
|
|
29285
29487
|
stamp: spec.stamp,
|
|
@@ -29363,7 +29565,7 @@ function createSqliteReplicaStorage(options) {
|
|
|
29363
29565
|
const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(idDir, "identity.json")));
|
|
29364
29566
|
if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${idDir}.`);
|
|
29365
29567
|
if (await gate.principalHashOf(salt, identity.principal) !== principalHash) continue;
|
|
29366
|
-
const identityKeyHash = await gate.identityHashOf(salt, (0,
|
|
29568
|
+
const identityKeyHash = await gate.identityHashOf(salt, (0, import_sdk_services5.replicationIdentityKey)(identity));
|
|
29367
29569
|
const recordedAt = Date.now();
|
|
29368
29570
|
if (await pathExists((0, import_node_path.join)(idDir, "pending.json")) || await pathExists((0, import_node_path.join)(idDir, "device.jwk"))) {
|
|
29369
29571
|
intents.push({ id: `pending:${identityKeyHash}`, kind: "pending", identityKeyHash, principalHash, recordedAt });
|
|
@@ -30041,7 +30243,7 @@ function createSqliteReplicaStorage(options) {
|
|
|
30041
30243
|
for (const dir of await identityDirs()) {
|
|
30042
30244
|
const identity = identityRecordOf(await readJsonFile((0, import_node_path.join)(dir, "identity.json")));
|
|
30043
30245
|
if (identity === void 0) throw new ReplicaStorageError("STORAGE_ERROR", `Missing identity at ${dir}.`);
|
|
30044
|
-
const hash3 = await gate.identityHashOf(state.salt, (0,
|
|
30246
|
+
const hash3 = await gate.identityHashOf(state.salt, (0, import_sdk_services5.replicationIdentityKey)(identity));
|
|
30045
30247
|
if (intent.kind === "pending" ? hash3 === intent.identityKeyHash : intent.partitionId.startsWith(`${hash3}:`)) {
|
|
30046
30248
|
idDir = dir;
|
|
30047
30249
|
break;
|
|
@@ -30187,7 +30389,7 @@ function createSqliteReplicaStorage(options) {
|
|
|
30187
30389
|
const gate = await fileGate();
|
|
30188
30390
|
const snapshot = await gate.readGateSnapshot(profileDir);
|
|
30189
30391
|
if (snapshot === void 0) throw gated("gate_unavailable");
|
|
30190
|
-
const identityKeyHash = await gate.identityHashOf(snapshot.salt, (0,
|
|
30392
|
+
const identityKeyHash = await gate.identityHashOf(snapshot.salt, (0, import_sdk_services5.replicationIdentityKey)(spec.identity));
|
|
30191
30393
|
const resolvePolicy = async (at, recordFirst) => {
|
|
30192
30394
|
const policy = spec.grantPolicy;
|
|
30193
30395
|
if (!Number.isSafeInteger(policy.grantTtlMs) || policy.grantTtlMs <= 0 || policy.grantTtlMs > import_contractConstants.MAX_GRANT_TTL_MS) {
|
|
@@ -30209,7 +30411,7 @@ function createSqliteReplicaStorage(options) {
|
|
|
30209
30411
|
let partitionId;
|
|
30210
30412
|
const storeGuard = (section) => guarded(spec.identity.principal, spec.stamp, spec.identity, partitionId, section);
|
|
30211
30413
|
return guarded(spec.identity.principal, spec.stamp, spec.identity, void 0, async () => {
|
|
30212
|
-
if ((0,
|
|
30414
|
+
if ((0, import_sdk_services5.requiresSecretsOptIn)(spec.space, spec.prefix) && spec.allowSecrets !== true) {
|
|
30213
30415
|
throw new replica.ReplicaError(
|
|
30214
30416
|
replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
|
|
30215
30417
|
`Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`
|
|
@@ -30455,7 +30657,7 @@ function createSqliteReplicaStorage(options) {
|
|
|
30455
30657
|
const gate = await fileGate();
|
|
30456
30658
|
const currentGate = await gate.readGateSnapshot(profileDir);
|
|
30457
30659
|
if (currentGate !== void 0 && currentGate.principalHash !== null && currentGate.principalHash === await gate.principalHashOf(currentGate.salt, target.identity.principal)) {
|
|
30458
|
-
const identityKeyHash = await gate.identityHashOf(currentGate.salt, (0,
|
|
30660
|
+
const identityKeyHash = await gate.identityHashOf(currentGate.salt, (0, import_sdk_services5.replicationIdentityKey)(target.identity));
|
|
30459
30661
|
await retainGrantInstalls(gate, currentGate, identityKeyHash, store);
|
|
30460
30662
|
}
|
|
30461
30663
|
if (state.revoked === null) {
|