@tinycloud/node-sdk 3.2.0-beta.4 → 3.2.0-beta.5

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/index.cjs CHANGED
@@ -17040,38 +17040,39 @@ function assertValidReplicationConfig(replication) {
17040
17040
  throw new TypeError("replication.prefixes must be a non-empty array");
17041
17041
  }
17042
17042
  for (const prefix of replication.prefixes) {
17043
- if (typeof prefix !== "string" || prefix === "") {
17044
- throw new TypeError("replication.prefixes must not contain an empty prefix");
17043
+ try {
17044
+ (0, import_prefix.assertFolderPrefix)(prefix);
17045
+ } catch (error) {
17046
+ throw new TypeError(error instanceof Error ? error.message : String(error));
17045
17047
  }
17046
17048
  }
17047
17049
  for (const [index, prefix] of replication.prefixes.entries()) {
17048
17050
  for (const other of replication.prefixes.slice(index + 1)) {
17049
- if ((0, import_sdk_services.kvPrefixCovers)(prefix, other) || (0, import_sdk_services.kvPrefixCovers)(other, prefix)) {
17051
+ if ((0, import_prefix.kvPrefixCovers)(prefix, other) || (0, import_prefix.kvPrefixCovers)(other, prefix)) {
17050
17052
  throw new TypeError(
17051
17053
  `replication.prefixes must not overlap: ${JSON.stringify(prefix)} and ${JSON.stringify(other)}`
17052
17054
  );
17053
17055
  }
17054
17056
  }
17055
- if (prefix.split("/", 1)[0] === "vault" && replication.allowSecrets !== true) {
17057
+ if ((prefix === "/" || prefix.split("/", 1)[0] === "vault") && replication.allowSecrets !== true) {
17056
17058
  throw new TypeError(
17057
17059
  `replication prefix ${JSON.stringify(prefix)} overlaps the vault namespace; pass allowSecrets to opt in`
17058
17060
  );
17059
17061
  }
17060
17062
  }
17061
17063
  }
17062
- var import_sdk_services;
17064
+ var import_prefix;
17063
17065
  var init_config = __esm({
17064
17066
  "src/replication/config.ts"() {
17065
17067
  "use strict";
17066
- import_sdk_services = require("@tinycloud/sdk-services");
17068
+ import_prefix = require("@tinycloud/sdk-services/kv/replication/prefix");
17067
17069
  }
17068
17070
  });
17069
17071
 
17070
17072
  // src/replication/authority-sign-in.ts
17071
17073
  function hasUnrestrictedGetCoverage(entries, spaceId, prefix) {
17072
- const requiredPaths = (0, import_sdk_core2.grantPathsForSelector)(prefix);
17073
17074
  return (0, import_sdk_core2.isCapabilitySubset)(
17074
- requiredPaths.map((path) => ({ service: KV_SERVICE, space: spaceId, path, actions: [KV_GET] })),
17075
+ [{ service: KV_SERVICE, space: spaceId, path: prefix, actions: [KV_GET] }],
17075
17076
  [...entries]
17076
17077
  ).subset;
17077
17078
  }
@@ -17079,30 +17080,43 @@ function augmentSignInEntriesWithReplication(input) {
17079
17080
  const out = [];
17080
17081
  const replication = input.replication;
17081
17082
  if (replication === void 0) return out;
17083
+ const eligiblePaths = [];
17082
17084
  for (const prefix of replication.prefixes) {
17083
- if ((0, import_sdk_services2.requiresSecretsOptIn)(input.primarySpaceId, prefix) && replication.allowSecrets !== true) {
17085
+ (0, import_prefix2.assertFolderPrefix)(prefix);
17086
+ const path = prefix === "/" ? "" : prefix;
17087
+ if ((0, import_sdk_services.requiresSecretsOptIn)(input.primarySpaceId, prefix) && replication.allowSecrets !== true) {
17084
17088
  continue;
17085
17089
  }
17086
- if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, prefix)) {
17090
+ if (!hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, path)) {
17087
17091
  continue;
17088
17092
  }
17089
- for (const path of (0, import_sdk_core2.grantPathsForSelector)(prefix)) {
17090
- out.push({
17091
- service: KV_SERVICE,
17092
- space: input.primarySpaceId,
17093
- path,
17094
- actions: [KV_GET, KV_SYNC]
17095
- });
17096
- }
17093
+ eligiblePaths.push(path);
17094
+ }
17095
+ if (eligiblePaths.length === replication.prefixes.length && eligiblePaths.length > 0 && hasUnrestrictedGetCoverage(input.entries, input.primarySpaceId, "")) {
17096
+ return [{
17097
+ service: KV_SERVICE,
17098
+ space: input.primarySpaceId,
17099
+ path: "",
17100
+ actions: [KV_GET, KV_SYNC]
17101
+ }];
17102
+ }
17103
+ for (const path of eligiblePaths) {
17104
+ out.push({
17105
+ service: KV_SERVICE,
17106
+ space: input.primarySpaceId,
17107
+ path,
17108
+ actions: [KV_GET, KV_SYNC]
17109
+ });
17097
17110
  }
17098
17111
  return out;
17099
17112
  }
17100
- var import_sdk_core2, import_sdk_services2, KV_GET, KV_SYNC, KV_SERVICE;
17113
+ var import_sdk_core2, import_sdk_services, import_prefix2, KV_GET, KV_SYNC, KV_SERVICE;
17101
17114
  var init_authority_sign_in = __esm({
17102
17115
  "src/replication/authority-sign-in.ts"() {
17103
17116
  "use strict";
17104
17117
  import_sdk_core2 = require("@tinycloud/sdk-core");
17105
- import_sdk_services2 = require("@tinycloud/sdk-services");
17118
+ import_sdk_services = require("@tinycloud/sdk-services");
17119
+ import_prefix2 = require("@tinycloud/sdk-services/kv/replication/prefix");
17106
17120
  KV_GET = import_sdk_core2.KV.GET;
17107
17121
  KV_SYNC = import_sdk_core2.KV.SYNC;
17108
17122
  KV_SERVICE = "tinycloud.kv";
@@ -17257,6 +17271,7 @@ var init_runtime = __esm({
17257
17271
  scheduler: this.clock,
17258
17272
  emit,
17259
17273
  fetch: context.fetch,
17274
+ nodeFeatures: input.nodeFeatures,
17260
17275
  previous: previousClose
17261
17276
  });
17262
17277
  this.current = controller;
@@ -17318,12 +17333,12 @@ __export(authority_exports, {
17318
17333
  ucanAttUnconstrainedFor: () => ucanAttUnconstrainedFor
17319
17334
  });
17320
17335
  function replicationEntries(spaceId, prefix) {
17321
- return (0, import_sdk_core11.grantPathsForSelector)(prefix).map((path) => ({
17336
+ return [{
17322
17337
  service: KV_SERVICE2,
17323
17338
  space: spaceId,
17324
- path,
17339
+ path: prefix === "/" ? "" : prefix,
17325
17340
  actions: [KV_GET2, KV_SYNC2]
17326
- }));
17341
+ }];
17327
17342
  }
17328
17343
  function caveatBranchesUnconstrained(value) {
17329
17344
  if (value === void 0 || value === null) return true;
@@ -17338,28 +17353,28 @@ function caveatBranchesUnconstrained(value) {
17338
17353
  }
17339
17354
  }
17340
17355
  function kvPathInResource(resource, space) {
17356
+ for (const root of [`${space}/kv`, `tinycloud://${space}/kv`]) {
17357
+ if (resource === root || resource === `${root}/`) return "/";
17358
+ }
17341
17359
  for (const base2 of [`${space}/kv/`, `tinycloud://${space}/kv/`]) {
17342
17360
  if (resource.startsWith(base2)) return resource.slice(base2.length);
17343
17361
  }
17344
17362
  return void 0;
17345
17363
  }
17346
17364
  function ucanAttCovers(att, space, prefix, ability, options) {
17347
- const requiredPaths = prefix.endsWith("/") ? [prefix] : [prefix, `${prefix}/`];
17348
- return requiredPaths.every((requestedPath) => {
17349
- for (const [resource, abilities] of Object.entries(att)) {
17350
- const path = kvPathInResource(resource, space);
17351
- if (path === void 0) continue;
17352
- for (const [granted, caveats] of Object.entries(abilities)) {
17353
- if (!(0, import_sdk_core11.actionContains)(granted, ability)) continue;
17354
- if (!(0, import_sdk_core11.isCapabilitySubset)(
17355
- [{ service: KV_SERVICE2, space, path: requestedPath, actions: [ability] }],
17356
- [{ service: KV_SERVICE2, space, path, actions: [granted] }]
17357
- ).subset) continue;
17358
- if (options?.ignoreCaveats === true || caveatBranchesUnconstrained(caveats)) return true;
17359
- }
17365
+ for (const [resource, abilities] of Object.entries(att)) {
17366
+ const path = kvPathInResource(resource, space);
17367
+ if (path === void 0) continue;
17368
+ for (const [granted, caveats] of Object.entries(abilities)) {
17369
+ if (!(0, import_sdk_core11.actionContains)(granted, ability)) continue;
17370
+ if (!(0, import_sdk_core11.isCapabilitySubset)(
17371
+ [{ service: KV_SERVICE2, space, path: prefix, actions: [ability] }],
17372
+ [{ service: KV_SERVICE2, space, path, actions: [granted] }]
17373
+ ).subset) continue;
17374
+ if (options?.ignoreCaveats === true || caveatBranchesUnconstrained(caveats)) return true;
17360
17375
  }
17361
- return false;
17362
- });
17376
+ }
17377
+ return false;
17363
17378
  }
17364
17379
  function ucanAttUnconstrainedFor(att, space, prefix) {
17365
17380
  return ucanAttCovers(att, space, prefix, KV_GET2) && ucanAttCovers(att, space, prefix, KV_SYNC2);
@@ -17374,7 +17389,7 @@ function replicationScopeFromSignedSession(input) {
17374
17389
  const bareDid = (value) => value.split("#", 1)[0];
17375
17390
  if (grant.cid !== input.delegationCid || bareDid(grant.audience) !== bareDid(input.verificationMethod)) return void 0;
17376
17391
  for (const resource of Object.keys(grant.att)) {
17377
- const space = resource.match(/^(?:tinycloud:\/\/)?(.+?)\/kv\//)?.[1];
17392
+ const space = resource.match(/^(?:tinycloud:\/\/)?(.+?)\/kv(?:\/|$)/)?.[1];
17378
17393
  if (space !== input.persistedSpace) continue;
17379
17394
  const owner = (0, import_sdk_core11.parseSpaceUri)(space);
17380
17395
  if (owner?.owner && owner.address && owner.chainId) return { space, principal: owner.owner };
@@ -18303,7 +18318,7 @@ function getNodeReplicationLoaders() {
18303
18318
 
18304
18319
  // src/TinyCloudNode.ts
18305
18320
  var import_sdk_core9 = require("@tinycloud/sdk-core");
18306
- var import_sdk_services3 = require("@tinycloud/sdk-services");
18321
+ var import_sdk_services2 = require("@tinycloud/sdk-services");
18307
18322
 
18308
18323
  // src/authorization/NodeUserAuthorization.ts
18309
18324
  var import_node_crypto = require("crypto");
@@ -18446,6 +18461,7 @@ var NodeUserAuthorization = class {
18446
18461
  this.activeSessionKeyId = "default";
18447
18462
  this.extensions = [];
18448
18463
  this._nodeFeatures = [];
18464
+ this._nodeFeaturesKnown = false;
18449
18465
  this._lastActivationSkippedSpaceIds = [];
18450
18466
  this.wasm = config.wasmBindings;
18451
18467
  this.signer = config.signer;
@@ -18569,8 +18585,11 @@ var NodeUserAuthorization = class {
18569
18585
  }
18570
18586
  }
18571
18587
  /** @internal Atomically adopt the state staged by TinyCloudNode.restoreSession. */
18572
- installRestoredSession(sessionManager, session, hosts) {
18588
+ installRestoredSession(sessionManager, session, hosts, nodeFeatures) {
18573
18589
  this.sessionManager = sessionManager;
18590
+ this._nodeFeatures = nodeFeatures === void 0 ? [] : [...nodeFeatures];
18591
+ this._nodeFeaturesKnown = nodeFeatures !== void 0;
18592
+ this._nodeInfoIdentity = void 0;
18574
18593
  this.tinycloudHosts = hosts && hosts.length > 0 ? [...hosts] : void 0;
18575
18594
  this._lastActivationSkippedSpaceIds = [];
18576
18595
  if (!session) {
@@ -18645,6 +18664,16 @@ var NodeUserAuthorization = class {
18645
18664
  get nodeFeatures() {
18646
18665
  return this._nodeFeatures;
18647
18666
  }
18667
+ get nodeFeaturesKnown() {
18668
+ return this._nodeFeaturesKnown;
18669
+ }
18670
+ async refreshNodeInfo() {
18671
+ const infoHost = this.primaryTinyCloudHost;
18672
+ const nodeInfo = await (0, import_sdk_core4.checkNodeInfo)(infoHost, this.wasm.protocolVersion());
18673
+ this._nodeFeatures = nodeInfo.features;
18674
+ this._nodeFeaturesKnown = true;
18675
+ this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
18676
+ }
18648
18677
  /**
18649
18678
  * The node DID reported by `GET /info` for `host`, if the sign-in (or
18650
18679
  * restore) that populated it targeted that same host. Returns `undefined`
@@ -19308,13 +19337,7 @@ var NodeUserAuthorization = class {
19308
19337
  this._tinyCloudSession = tinyCloudSession;
19309
19338
  this._address = address;
19310
19339
  this._chainId = chainId;
19311
- const infoHost = this.primaryTinyCloudHost;
19312
- const nodeInfo = await (0, import_sdk_core4.checkNodeInfo)(
19313
- infoHost,
19314
- this.wasm.protocolVersion()
19315
- );
19316
- this._nodeFeatures = nodeInfo.features;
19317
- this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
19340
+ await this.refreshNodeInfo();
19318
19341
  for (const ext of this.extensions) {
19319
19342
  if (ext.afterSignIn) {
19320
19343
  await ext.afterSignIn(clientSession);
@@ -19480,13 +19503,7 @@ var NodeUserAuthorization = class {
19480
19503
  this._tinyCloudSession = tinyCloudSession;
19481
19504
  this._address = address;
19482
19505
  this._chainId = chainId;
19483
- const infoHost = this.primaryTinyCloudHost;
19484
- const nodeInfo = await (0, import_sdk_core4.checkNodeInfo)(
19485
- infoHost,
19486
- this.wasm.protocolVersion()
19487
- );
19488
- this._nodeFeatures = nodeInfo.features;
19489
- this._nodeInfoIdentity = nodeInfo.nodeId ? { host: infoHost, nodeId: nodeInfo.nodeId } : void 0;
19506
+ await this.refreshNodeInfo();
19490
19507
  for (const ext of this.extensions) {
19491
19508
  if (ext.afterSignIn) {
19492
19509
  await ext.afterSignIn(clientSession);
@@ -21053,7 +21070,11 @@ var _TinyCloudNode = class _TinyCloudNode {
21053
21070
  return this._bootstrapStatus;
21054
21071
  }
21055
21072
  get nodeFeatures() {
21056
- return this.auth?.nodeFeatures ?? [];
21073
+ return this.auth?.nodeFeatures ?? this._restoredNodeFeatures ?? [];
21074
+ }
21075
+ get replicationNodeFeatures() {
21076
+ if (this.auth) return this.auth.nodeFeaturesKnown ? this.auth.nodeFeatures : void 0;
21077
+ return this._restoredNodeFeatures;
21057
21078
  }
21058
21079
  /** SIWE domain — uses config override or defaults to app.tinycloud.xyz */
21059
21080
  get siweDomain() {
@@ -22406,6 +22427,14 @@ var _TinyCloudNode = class _TinyCloudNode {
22406
22427
  sessionData.chainId
22407
22428
  );
22408
22429
  const stagedHost = resolvedHost ?? this.config.host;
22430
+ let restoredNodeFeatures;
22431
+ if (this.config.replication?.enabled) {
22432
+ try {
22433
+ restoredNodeFeatures = (await (0, import_sdk_core9.checkNodeInfo)(stagedHost, this.wasmBindings.protocolVersion())).features;
22434
+ } catch (error) {
22435
+ if (!(error instanceof import_sdk_core9.VersionCheckError)) throw error;
22436
+ }
22437
+ }
22409
22438
  const stagedAddress = restoredAddress;
22410
22439
  const stagedChainId = sessionData.chainId ?? 1;
22411
22440
  let replicationPrincipal;
@@ -22508,8 +22537,9 @@ var _TinyCloudNode = class _TinyCloudNode {
22508
22537
  this._recapOperationsCache = stagedPrimary.cache;
22509
22538
  this.runtimePermissionGrants = stagedPrimary.grants;
22510
22539
  this.confirmedHostedSpaceIds.clear();
22540
+ this._restoredNodeFeatures = restoredNodeFeatures;
22511
22541
  if (this.auth) {
22512
- this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost]);
22542
+ this.auth.installRestoredSession(stagedManager, stagedTcSession, [stagedHost], restoredNodeFeatures);
22513
22543
  this._restoredTcSession = void 0;
22514
22544
  } else {
22515
22545
  this._restoredTcSession = stagedTcSession;
@@ -23339,7 +23369,7 @@ var _TinyCloudNode = class _TinyCloudNode {
23339
23369
  );
23340
23370
  graph.assertActive();
23341
23371
  if (!response.ok) {
23342
- const permissionHint = response.status === 401 || response.status === 403 ? (0, import_sdk_services3.parsePermissionHint)({
23372
+ const permissionHint = response.status === 401 || response.status === 403 ? (0, import_sdk_services2.parsePermissionHint)({
23343
23373
  service: "tinycloud.encryption",
23344
23374
  path: networkId,
23345
23375
  actions: ["tinycloud.encryption/decrypt"]
@@ -24107,7 +24137,7 @@ var _TinyCloudNode = class _TinyCloudNode {
24107
24137
  );
24108
24138
  if (result.status !== "ok") {
24109
24139
  if (result.status === "permission_required") {
24110
- const hint = (0, import_sdk_services3.parsePermissionHint)(result.hint);
24140
+ const hint = (0, import_sdk_services2.parsePermissionHint)(result.hint);
24111
24141
  if (hint === void 0) return { status: "read_failed" };
24112
24142
  return {
24113
24143
  status: "permission_required",
@@ -25083,6 +25113,7 @@ var _TinyCloudNode = class _TinyCloudNode {
25083
25113
  session,
25084
25114
  principal: identityPrincipal,
25085
25115
  authority,
25116
+ nodeFeatures: this.replicationNodeFeatures,
25086
25117
  primaryKV: [{ space: session.spaceId, kv }]
25087
25118
  });
25088
25119
  }
@@ -26405,7 +26436,7 @@ var import_node_crypto2 = require("crypto");
26405
26436
  var import_promises = require("fs/promises");
26406
26437
  var import_node_path = require("path");
26407
26438
  var import_sdk_core13 = require("@tinycloud/sdk-core");
26408
- var import_sdk_services4 = require("@tinycloud/sdk-services");
26439
+ var import_sdk_services3 = require("@tinycloud/sdk-services");
26409
26440
  init_authority();
26410
26441
  var ReplicaStorageError = class extends Error {
26411
26442
  constructor(code, message, options) {
@@ -26453,7 +26484,7 @@ function sha256Base32(material) {
26453
26484
  return out.slice(0, 26);
26454
26485
  }
26455
26486
  function partitionDirOf(root, identity) {
26456
- return (0, import_node_path.join)(root, sha256Base32((0, import_sdk_services4.replicationIdentityKey)(identity)));
26487
+ return (0, import_node_path.join)(root, sha256Base32((0, import_sdk_services3.replicationIdentityKey)(identity)));
26457
26488
  }
26458
26489
  function replicaHashOf(prefix, deviceDid) {
26459
26490
  return sha256Base32(`${prefix}
@@ -26527,8 +26558,8 @@ function deviceRecordOf(value) {
26527
26558
  }
26528
26559
  function sameIdentity(a, b) {
26529
26560
  try {
26530
- const left = (0, import_sdk_services4.canonicalReplicationIdentity)(a);
26531
- const right = (0, import_sdk_services4.canonicalReplicationIdentity)(b);
26561
+ const left = (0, import_sdk_services3.canonicalReplicationIdentity)(a);
26562
+ const right = (0, import_sdk_services3.canonicalReplicationIdentity)(b);
26532
26563
  return left.host === right.host && left.space === right.space && left.principal === right.principal;
26533
26564
  } catch {
26534
26565
  return false;
@@ -26544,7 +26575,7 @@ async function ensurePartitionIdentity(dir, identity, dirSync) {
26544
26575
  const file = (0, import_node_path.join)(dir, "identity.json");
26545
26576
  const existing = await readJsonFile(file);
26546
26577
  if (existing === void 0) {
26547
- await writeJsonAtomic(file, { v: 1, ...(0, import_sdk_services4.canonicalReplicationIdentity)(identity) }, dirSync);
26578
+ await writeJsonAtomic(file, { v: 1, ...(0, import_sdk_services3.canonicalReplicationIdentity)(identity) }, dirSync);
26548
26579
  return;
26549
26580
  }
26550
26581
  const record = identityRecordOf(existing);
@@ -27012,7 +27043,7 @@ function createSqliteReplicaStorage(options) {
27012
27043
  // @ts-ignore — same as above.
27013
27044
  import("@tinycloud/replica/sqlite")
27014
27045
  ]);
27015
- if ((0, import_sdk_services4.requiresSecretsOptIn)(spec.space, spec.prefix) && spec.allowSecrets !== true) {
27046
+ if ((0, import_sdk_services3.requiresSecretsOptIn)(spec.space, spec.prefix) && spec.allowSecrets !== true) {
27016
27047
  throw new replica.ReplicaError(
27017
27048
  replica.ReplicaErrorCode.SECRETS_OPT_IN_REQUIRED,
27018
27049
  `Replicating ${spec.space}/kv/${spec.prefix} copies secret material to disk; pass allowSecrets to opt in.`