@push.rocks/smartdb 2.18.1 → 3.0.1

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.
Files changed (40) hide show
  1. package/dist_rust/rustdb_linux_amd64 +0 -0
  2. package/dist_rust/rustdb_linux_arm64 +0 -0
  3. package/dist_ts/00_commitinfo_data.js +2 -2
  4. package/dist_ts/index.d.ts +1 -1
  5. package/dist_ts/index.js +1 -1
  6. package/dist_ts/ts_migration/classes.authmetadatamigrationrunner.d.ts +2 -1
  7. package/dist_ts/ts_migration/classes.authmetadatamigrationrunner.js +3 -3
  8. package/dist_ts/ts_migration/classes.storagemigrator.d.ts +2 -1
  9. package/dist_ts/ts_migration/classes.storagemigrator.js +36 -20
  10. package/dist_ts/ts_migration/index.d.ts +1 -0
  11. package/dist_ts/ts_migration/index.js +2 -1
  12. package/dist_ts/ts_migration/migrators/v0_to_v1.d.ts +7 -1
  13. package/dist_ts/ts_migration/migrators/v0_to_v1.js +162 -47
  14. package/dist_ts/ts_migration/migrators/v1_auth_metadata_permissions.d.ts +3 -2
  15. package/dist_ts/ts_migration/migrators/v1_auth_metadata_permissions.js +3 -3
  16. package/dist_ts/ts_smartdb/index.d.ts +1 -1
  17. package/dist_ts/ts_smartdb/index.js +1 -1
  18. package/dist_ts/ts_smartdb/resource-fencing.d.ts +12 -1
  19. package/dist_ts/ts_smartdb/resource-fencing.js +44 -2
  20. package/dist_ts/ts_smartdb/rust-db-bridge.d.ts +5 -4
  21. package/dist_ts/ts_smartdb/rust-db-bridge.js +33 -10
  22. package/dist_ts/ts_smartdb/server/SmartdbServer.d.ts +4 -2
  23. package/dist_ts/ts_smartdb/server/SmartdbServer.js +84 -9
  24. package/dist_ts/ts_smartdb/service-types.d.ts +47 -2
  25. package/dist_ts/ts_smartdb/service-types.js +7 -2
  26. package/package.json +1 -1
  27. package/readme.md +19 -4
  28. package/readme.plan.md +2 -0
  29. package/ts/00_commitinfo_data.ts +1 -1
  30. package/ts/index.ts +4 -0
  31. package/ts/ts_migration/classes.authmetadatamigrationrunner.ts +3 -1
  32. package/ts/ts_migration/classes.storagemigrator.ts +38 -19
  33. package/ts/ts_migration/index.ts +1 -0
  34. package/ts/ts_migration/migrators/v0_to_v1.ts +224 -44
  35. package/ts/ts_migration/migrators/v1_auth_metadata_permissions.ts +4 -0
  36. package/ts/ts_smartdb/index.ts +4 -0
  37. package/ts/ts_smartdb/resource-fencing.ts +59 -2
  38. package/ts/ts_smartdb/rust-db-bridge.ts +56 -11
  39. package/ts/ts_smartdb/server/SmartdbServer.ts +105 -9
  40. package/ts/ts_smartdb/service-types.ts +59 -2
@@ -5,6 +5,9 @@ export interface ISmartDbHealth {
5
5
  resourceFencingVersion?: 1;
6
6
  resourceFencingSupported?: boolean;
7
7
  resourceFencingRequiresDrain?: true;
8
+ allocationFencingVersion?: 1;
9
+ allocationFencingSupported?: boolean;
10
+ allocationFencingRequiresDrain?: true;
8
11
  publicationHoldVersion: 1;
9
12
  publicationHoldSupported: boolean;
10
13
  publicationHoldRequiresExternalDrain: boolean;
@@ -39,7 +42,7 @@ export type TLocalSmartDbOfflineStringValueInspectionResult = {
39
42
  status: 'found';
40
43
  value: string;
41
44
  };
42
- export declare const smartDbResourceFenceErrorCodes: readonly ["EFENCE_REQUIRED", "EFENCE_STALE", "EFENCE_SCOPE_MISMATCH", "EFENCE_UNSUPPORTED", "EFENCE_MUTATION_MISMATCH", "EFENCE_OWNERSHIP_MISMATCH", "EFENCE_STATE_CORRUPT", "EFENCE_RECEIPT_LIMIT", "EFENCE_BUSY", "EFENCE_PUBLICATION_HELD", "EFENCE_CAPABILITY_MISMATCH", "EFENCE_NOT_HELD"];
45
+ export declare const smartDbResourceFenceErrorCodes: readonly ["EFENCE_REQUIRED", "EFENCE_STALE", "EFENCE_SCOPE_MISMATCH", "EFENCE_UNSUPPORTED", "EFENCE_MUTATION_MISMATCH", "EFENCE_OWNERSHIP_MISMATCH", "EFENCE_STATE_CORRUPT", "EFENCE_RECEIPT_LIMIT", "EFENCE_BUSY", "EFENCE_PUBLICATION_HELD", "EFENCE_CAPABILITY_MISMATCH", "EFENCE_NOT_HELD", "EALLOCATION_EXPECTED_ABSENT_CONFLICT", "EALLOCATION_REQUIRED", "EALLOCATION_MISMATCH", "EALLOCATION_GENERATION_EXHAUSTED", "EALLOCATION_DEPROVISIONED"];
43
46
  export type TSmartDbResourceFenceErrorCode = (typeof smartDbResourceFenceErrorCodes)[number];
44
47
  export interface ISmartDbResourceFence {
45
48
  version: 1;
@@ -49,7 +52,7 @@ export interface ISmartDbResourceFence {
49
52
  payloadSha256: string;
50
53
  }
51
54
  export interface ISmartDbResourceFenceReceipt extends ISmartDbResourceFence {
52
- kind: 'smartdb.database.replace.v1' | 'smartdb.database.delete.v1' | 'smartdb.database.ensure.v1';
55
+ kind: 'smartdb.database.replace.v1' | 'smartdb.database.delete.v1' | 'smartdb.database.ensure.v1' | 'smartdb.database.allocate.v1';
53
56
  /** Digest of the canonical payload interpretation derived by SmartDB. */
54
57
  effectivePayloadSha256: string;
55
58
  provider?: 'smartdb';
@@ -62,6 +65,33 @@ export interface ISmartDbResourceFenceReceipt extends ISmartDbResourceFence {
62
65
  publicationCapabilitySha256?: string;
63
66
  /** Present only while publication is held; never returned after release. */
64
67
  publicationCapability?: string;
68
+ allocation?: ISmartDbDatabaseAllocationIdentity;
69
+ }
70
+ export interface ISmartDbDatabaseAllocationIdentity {
71
+ version: 1;
72
+ provider: 'smartdb';
73
+ state: 'active';
74
+ databaseName: string;
75
+ username: string;
76
+ allocationId: string;
77
+ generation: number;
78
+ principalId: string;
79
+ resourceIdSha256: string;
80
+ bindingSha256: string;
81
+ receiptSha256: string;
82
+ }
83
+ export interface ISmartDbDatabaseAllocationLifecycle {
84
+ version: 1;
85
+ provider: 'smartdb';
86
+ state: 'allocating' | 'active' | 'deprovisioning' | 'deprovisioned';
87
+ databaseName: string;
88
+ username: string;
89
+ allocationId: string;
90
+ generation: number;
91
+ principalId: string;
92
+ resourceIdSha256: string;
93
+ bindingSha256: string;
94
+ receiptSha256: string;
65
95
  }
66
96
  export interface ISmartDbHeldPublicationReceipt extends ISmartDbResourceFenceReceipt {
67
97
  provider: 'smartdb';
@@ -94,6 +124,7 @@ export interface ISmartDbDatabaseResourceFenceState {
94
124
  scopeId: string;
95
125
  highestToken: number;
96
126
  publicationState: 'requested' | 'publishing' | 'held' | 'released';
127
+ allocation?: ISmartDbDatabaseAllocationLifecycle;
97
128
  }
98
129
  export declare class SmartDbResourceFenceError extends Error {
99
130
  readonly code: TSmartDbResourceFenceErrorCode;
@@ -108,6 +139,11 @@ export interface ISmartDbDatabaseTenantInput {
108
139
  }
109
140
  export interface ISmartDbEnsureDatabaseTenantInput extends ISmartDbDatabaseTenantInput {
110
141
  fence: ISmartDbResourceFence;
142
+ allocation?: ISmartDbDatabaseAllocationIdentity;
143
+ }
144
+ export interface ISmartDbAllocateDatabaseTenantInput extends ISmartDbDatabaseTenantInput {
145
+ expectedAbsent: true;
146
+ fence: ISmartDbResourceFence;
111
147
  }
112
148
  export interface ISmartDbDeleteDatabaseTenantInput {
113
149
  databaseName: string;
@@ -121,6 +157,7 @@ export interface ISmartDbDeleteDatabaseTenantInput {
121
157
  */
122
158
  username?: string;
123
159
  fence?: ISmartDbResourceFence;
160
+ allocation?: ISmartDbDatabaseAllocationIdentity;
124
161
  /** Keep the database data plane closed until commitDatabasePublication. */
125
162
  holdPublication?: boolean;
126
163
  }
@@ -135,10 +172,15 @@ export interface ISmartDbDatabaseTenantDescriptor {
135
172
  authSource: string;
136
173
  mongodbUri?: string;
137
174
  resourceFence?: ISmartDbResourceFenceReceipt;
175
+ allocation?: ISmartDbDatabaseAllocationIdentity;
138
176
  }
139
177
  export interface ISmartDbEnsureDatabaseTenantResult extends ISmartDbDatabaseTenantDescriptor {
140
178
  resourceFence: ISmartDbResourceFenceReceipt;
141
179
  }
180
+ export interface ISmartDbAllocateDatabaseTenantResult extends ISmartDbDatabaseTenantDescriptor {
181
+ resourceFence: ISmartDbResourceFenceReceipt;
182
+ allocation: ISmartDbDatabaseAllocationIdentity;
183
+ }
142
184
  export interface ISmartDbDeleteDatabaseTenantResult {
143
185
  databaseName: string;
144
186
  /**
@@ -154,6 +196,7 @@ export interface ISmartDbDeleteDatabaseTenantResult {
154
196
  */
155
197
  usersPreserved?: boolean;
156
198
  resourceFence?: ISmartDbResourceFenceReceipt;
199
+ allocation?: ISmartDbDatabaseAllocationIdentity;
157
200
  }
158
201
  export interface ISmartDbDatabaseExportCollection {
159
202
  name: string;
@@ -206,6 +249,7 @@ export interface ISmartDbImportDatabaseInput {
206
249
  */
207
250
  username?: string;
208
251
  fence?: ISmartDbResourceFence;
252
+ allocation?: ISmartDbDatabaseAllocationIdentity;
209
253
  /** Keep the database data plane closed until commitDatabasePublication. */
210
254
  holdPublication?: boolean;
211
255
  }
@@ -214,5 +258,6 @@ export interface ISmartDbImportDatabaseResult {
214
258
  collections: number;
215
259
  documents: number;
216
260
  resourceFence?: ISmartDbResourceFenceReceipt;
261
+ allocation?: ISmartDbDatabaseAllocationIdentity;
217
262
  }
218
263
  export type TSmartDbCommitDatabasePublicationResult = ISmartDbImportDatabaseResult | ISmartDbDeleteDatabaseTenantResult;
@@ -11,6 +11,11 @@ export const smartDbResourceFenceErrorCodes = [
11
11
  'EFENCE_PUBLICATION_HELD',
12
12
  'EFENCE_CAPABILITY_MISMATCH',
13
13
  'EFENCE_NOT_HELD',
14
+ 'EALLOCATION_EXPECTED_ABSENT_CONFLICT',
15
+ 'EALLOCATION_REQUIRED',
16
+ 'EALLOCATION_MISMATCH',
17
+ 'EALLOCATION_GENERATION_EXHAUSTED',
18
+ 'EALLOCATION_DEPROVISIONED',
14
19
  ];
15
20
  export class SmartDbResourceFenceError extends Error {
16
21
  constructor(codeArg, messageArg, causeArg) {
@@ -23,11 +28,11 @@ export const normalizeSmartDbResourceFenceError = (errorArg) => {
23
28
  const error = errorArg instanceof Error
24
29
  ? errorArg
25
30
  : new Error(String(errorArg));
26
- const match = /^(EFENCE_[A-Z_]+):\s*(.*)$/s.exec(error.message);
31
+ const match = /^((?:EFENCE|EALLOCATION)_[A-Z_]+):\s*(.*)$/s.exec(error.message);
27
32
  if (!match ||
28
33
  !smartDbResourceFenceErrorCodes.includes(match[1])) {
29
34
  return error;
30
35
  }
31
36
  return new SmartDbResourceFenceError(match[1], match[2], error);
32
37
  };
33
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS10eXBlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3RzX3NtYXJ0ZGIvc2VydmljZS10eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUEwQ0EsTUFBTSxDQUFDLE1BQU0sOEJBQThCLEdBQUc7SUFDNUMsaUJBQWlCO0lBQ2pCLGNBQWM7SUFDZCx1QkFBdUI7SUFDdkIsb0JBQW9CO0lBQ3BCLDBCQUEwQjtJQUMxQiwyQkFBMkI7SUFDM0Isc0JBQXNCO0lBQ3RCLHNCQUFzQjtJQUN0QixhQUFhO0lBQ2IseUJBQXlCO0lBQ3pCLDRCQUE0QjtJQUM1QixpQkFBaUI7Q0FDVCxDQUFDO0FBc0VYLE1BQU0sT0FBTyx5QkFBMEIsU0FBUSxLQUFLO0lBR2xELFlBQ0UsT0FBdUMsRUFDdkMsVUFBa0IsRUFDbEIsUUFBa0I7UUFFbEIsS0FBSyxDQUFDLEdBQUcsT0FBTyxLQUFLLFVBQVUsRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUM7UUFDeEQsSUFBSSxDQUFDLElBQUksR0FBRywyQkFBMkIsQ0FBQztRQUN4QyxJQUFJLENBQUMsSUFBSSxHQUFHLE9BQU8sQ0FBQztJQUN0QixDQUFDO0NBQ0Y7QUFFRCxNQUFNLENBQUMsTUFBTSxrQ0FBa0MsR0FBRyxDQUNoRCxRQUFpQixFQUNWLEVBQUU7SUFDVCxNQUFNLEtBQUssR0FDVCxRQUFRLFlBQVksS0FBSztRQUN2QixDQUFDLENBQUMsUUFBUTtRQUNWLENBQUMsQ0FBQyxJQUFJLEtBQUssQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQztJQUNsQyxNQUFNLEtBQUssR0FBRyw2QkFBNkIsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ2hFLElBQ0UsQ0FBQyxLQUFLO1FBQ04sQ0FBQyw4QkFBOEIsQ0FBQyxRQUFRLENBQ3RDLEtBQUssQ0FBQyxDQUFDLENBQW1DLENBQzNDLEVBQ0QsQ0FBQztRQUNELE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztJQUNELE9BQU8sSUFBSSx5QkFBeUIsQ0FDbEMsS0FBSyxDQUFDLENBQUMsQ0FBbUMsRUFDMUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUNSLEtBQUssQ0FDTixDQUFDO0FBQ0osQ0FBQyxDQUFDIn0=
38
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoic2VydmljZS10eXBlcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzL3RzX3NtYXJ0ZGIvc2VydmljZS10eXBlcy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUE2Q0EsTUFBTSxDQUFDLE1BQU0sOEJBQThCLEdBQUc7SUFDNUMsaUJBQWlCO0lBQ2pCLGNBQWM7SUFDZCx1QkFBdUI7SUFDdkIsb0JBQW9CO0lBQ3BCLDBCQUEwQjtJQUMxQiwyQkFBMkI7SUFDM0Isc0JBQXNCO0lBQ3RCLHNCQUFzQjtJQUN0QixhQUFhO0lBQ2IseUJBQXlCO0lBQ3pCLDRCQUE0QjtJQUM1QixpQkFBaUI7SUFDakIsc0NBQXNDO0lBQ3RDLHNCQUFzQjtJQUN0QixzQkFBc0I7SUFDdEIsa0NBQWtDO0lBQ2xDLDJCQUEyQjtDQUNuQixDQUFDO0FBcUdYLE1BQU0sT0FBTyx5QkFBMEIsU0FBUSxLQUFLO0lBR2xELFlBQ0UsT0FBdUMsRUFDdkMsVUFBa0IsRUFDbEIsUUFBa0I7UUFFbEIsS0FBSyxDQUFDLEdBQUcsT0FBTyxLQUFLLFVBQVUsRUFBRSxFQUFFLEVBQUUsS0FBSyxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUM7UUFDeEQsSUFBSSxDQUFDLElBQUksR0FBRywyQkFBMkIsQ0FBQztRQUN4QyxJQUFJLENBQUMsSUFBSSxHQUFHLE9BQU8sQ0FBQztJQUN0QixDQUFDO0NBQ0Y7QUFFRCxNQUFNLENBQUMsTUFBTSxrQ0FBa0MsR0FBRyxDQUNoRCxRQUFpQixFQUNWLEVBQUU7SUFDVCxNQUFNLEtBQUssR0FDVCxRQUFRLFlBQVksS0FBSztRQUN2QixDQUFDLENBQUMsUUFBUTtRQUNWLENBQUMsQ0FBQyxJQUFJLEtBQUssQ0FBQyxNQUFNLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQztJQUNsQyxNQUFNLEtBQUssR0FBRyw2Q0FBNkMsQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQyxDQUFDO0lBQ2hGLElBQ0UsQ0FBQyxLQUFLO1FBQ04sQ0FBQyw4QkFBOEIsQ0FBQyxRQUFRLENBQ3RDLEtBQUssQ0FBQyxDQUFDLENBQW1DLENBQzNDLEVBQ0QsQ0FBQztRQUNELE9BQU8sS0FBSyxDQUFDO0lBQ2YsQ0FBQztJQUNELE9BQU8sSUFBSSx5QkFBeUIsQ0FDbEMsS0FBSyxDQUFDLENBQUMsQ0FBbUMsRUFDMUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxFQUNSLEtBQUssQ0FDTixDQUFDO0FBQ0osQ0FBQyxDQUFDIn0=
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartdb",
3
- "version": "2.18.1",
3
+ "version": "3.0.1",
4
4
  "private": false,
5
5
  "description": "A MongoDB-compatible embedded database server with wire protocol support, backed by a high-performance Rust engine.",
6
6
  "exports": {
package/readme.md CHANGED
@@ -272,7 +272,11 @@ const server = new SmartdbServer({
272
272
  storage: 'memory', // 'memory' or 'file' (default: 'memory')
273
273
  storagePath: './data', // Required when storage is 'file'
274
274
  });
275
- await server.start();
275
+ const startupController = new AbortController();
276
+ await server.start({
277
+ signal: startupController.signal,
278
+ timeoutMs: 30_000,
279
+ });
276
280
  console.log(server.port); // Actual bound port while running
277
281
  console.log(server.getConnectionUri()); // mongodb://127.0.0.1:<actual-port>
278
282
 
@@ -346,6 +350,8 @@ Supported built-in role names are `root`, `read`, `readWrite`, `dbAdmin`, `userA
346
350
 
347
351
  On Linux, startup performs one narrow compatibility migration before the Rust engine opens `usersPath`: an effective-user-owned, single-link regular file with the exact legacy mode `0644` is changed to `0600`. Current `0600` files are left unchanged. Symlinks, hard links, unexpected modes or ownership, unsafe parent directories, cross-device targets, and identity changes remain fail-closed errors.
348
352
 
353
+ Legacy v0 JSON collections are converted into hidden sibling staging directories. SmartDB fsyncs every generated file and the staging directory before atomically publishing a complete v1 collection, while the v0 files remain unchanged. Cancellation removes unpublished staging and preserves the v0 input. Startup fails closed if it finds staging left by an unclean process exit or an incomplete published target, so neither state is mistaken for a complete migration.
354
+
349
355
  Persisted users also carry a random principal identity and a monotonic generation. SmartDB reloads and resolves that identity for every authenticated command, so password or role changes take effect immediately and stale sockets are rejected. Deleting and recreating the same username creates a different principal; an old connection cannot inherit the replacement user's authority. Cross-process user updates are serialized through the persisted users-file lock.
350
356
 
351
357
  Single-node transactions are supported through official MongoDB driver sessions. Writes with `startTransaction` and `autocommit: false` are buffered per logical session, reads inside the transaction see the buffered overlay, `commitTransaction` applies the write set with conflict checks, and `abortTransaction` discards it. Live logical sessions remain resumable across socket disconnects. Bounded background cleanup aborts expired transactions, removes expired sessions, and releases publication leases; explicit `endSessions` and `killSessions` do the same for their active transactions.
@@ -394,13 +400,21 @@ const released = await server.commitDatabasePublication({
394
400
 
395
401
  The held barrier survives restart and blocks wire commands, transactions, startup recovery, compaction, index restoration, and close-time hint writes for that database while unrelated databases remain available. Commit is exact and idempotent. A successful commit removes the raw capability from durable state and retains only protected verification material. Provider/root identity, durable fenced-mode markers, and bounded startup validation make copied, missing, corrupt, or legacy-active publication state fail closed. A higher fencing token compacts resolved older receipts, so long-lived coordinators do not exhaust receipt capacity.
396
402
 
397
- Coordinators that do not yet have their own durable record can call `getDatabaseResourceFenceState({ databaseName })` before creating it. File-backed SmartDB returns the durable scope, highest token, and current publication phase, or `null` only when both fence state and its durable marker are absent. The inspection never returns mutation IDs, receipts, payload digests, binding digests, or publication capabilities. Treat any active phase, identity mismatch, corrupt state, unsupported storage, or unsafe token as a hard stop rather than creating coordinator state.
403
+ Coordinators that do not yet have their own durable record can call `getDatabaseResourceFenceState({ databaseName })` before creating it. File-backed SmartDB returns the durable scope, highest token, current publication phase, and allocation lifecycle/identity when the name is allocation-managed, or `null` only when both fence state and its durable marker are absent. The inspection never returns mutation IDs, mutation payload digests, publication receipts, or publication capabilities. Treat any active phase, identity mismatch, corrupt state, unsupported storage, or unsafe token as a hard stop rather than creating coordinator state.
404
+
405
+ ### Authoritative Database Allocation
406
+
407
+ `allocateDatabaseTenant()` is the create-only control-plane API for permanently allocation-managed database names. It is available only when `allocationFencingVersion === 1`, `allocationFencingSupported === true`, and `allocationFencingRequiresDrain === true`. The request requires literal `expectedAbsent: true`, an `ISmartDbResourceFence`, and the tenant database, username, and password. `roles` is optional and defaults to `['readWrite', 'dbAdmin']`. SmartDB proves database-root and auth-principal absence while holding the database resource lock, maintenance write gate, and a bounded cross-process reservation on the durable auth store, then returns an `ISmartDbAllocateDatabaseTenantResult`; its `.allocation` field is the durable `ISmartDbDatabaseAllocationIdentity`. Exact fence replay returns the same allocation ID, generation, principal ID, and receipt digests.
408
+
409
+ Pass the returned `allocation` identity to every later `ensureDatabaseTenant()`, `importDatabase()`, and `deleteDatabaseTenant()` request for that name. Managed import and delete requests also require the exact allocation `username`; the identity alone is insufficient. Each distinct managed mutation requires a strictly newer fence token; the current token is accepted only for exact receipt replay or continuation, and older receipts become explicitly stale after advancement. Held publication receipts carry the allocation into `commitDatabasePublication()`. Managed requests without it and unmanaged requests with it fail closed. Deprovision persists `deprovisioning`, durably deletes the exact marked root, atomically removes only the exact principal, then persists a permanent `deprovisioned` tombstone. A later allocation requires a newer fence token, increments generation, and receives a fresh allocation and principal identity. Wire data access is available only while active and only to the current allocation principal; database/user ownership-changing wire commands are rejected.
410
+
411
+ The provider marker `.__rustdb_allocation.json` is created and preserved by SmartDB, never accepted from an import payload, and never included in logical exports. The resource lock uses the distinct `smartdb-resource-allocation-managed-v1` marker. Pre-allocation binaries, including 2.18.1, interpret that lock marker as invalid and fail closed. Consequently, N-1 rollback of a storage root after allocation fencing has touched a name is intentionally unavailable; restore the allocation-aware binary rather than removing or editing provider metadata.
398
412
 
399
413
  `exportDatabase()` drains live MongoDB wire commands for the selected database and holds an exclusive local and cross-process database lease while producing the snapshot. The export is therefore consistent across all collections. It enforces server ceilings while visiting documents instead of first materializing an unbounded database; callers can request lower `maxEncodedBytes`, `maxCollections`, `maxDocuments`, and `maxIndexes` limits. The result emits canonical MongoDB Extended JSON so every BSON type, including 64-bit integers, survives the JSON management channel exactly. `importDatabase()` accepts canonical or relaxed Extended JSON. Database migration clients should preserve the exported objects as JSON values and must not coerce Extended JSON numeric wrappers into JavaScript numbers.
400
414
 
401
415
  `getDatabaseContentDigest()` computes a bounded, database-name-independent SHA-256 over collection names, exact BSON document bytes, and persisted index specifications. Collection, document, and index enumeration order is normalized; BSON field order and compound-index key order remain significant. `ISmartDbGetDatabaseContentDigestInput` accepts `databaseName` and optional `ISmartDbDatabaseContentDigestLimits` fields `maxScannedBsonBytes`, `maxCollections`, `maxDocuments`, and `maxIndexes`. Callers may lower the server ceilings of 96 MiB scanned BSON, 10,000 collections, 1,000,000 documents, and 100,000 indexes. The `ISmartDbDatabaseContentDigest` result reports format `smartdb.database.content-digest.v1`, algorithm `sha256`, the lowercase digest, and exact scan counters.
402
416
 
403
- The lifecycle-critical health and fence-state inspections, digest, export, import, publication-commit, tenant-ensure, and tenant-delete methods accept optional `ISmartDbManagementOperationOptions` with `signal?: AbortSignal` and `timeoutMs?: number`. `timeoutMs`, when provided, must be a positive safe integer. Cancellation and deadlines are fail-stop: SmartDB attempts to terminate the Rust engine before rejecting. Once termination is confirmed, the operation cannot continue after the caller releases ownership. If termination itself fails, the rejection is a `SmartDbManagementOperationTerminationError`; bridge ownership is retained and the service owner must retry `stop()` until it succeeds. After any cancellation or deadline, restart SmartDB before accepting more traffic.
417
+ `SmartdbServer.start()` and the lifecycle-critical health, fence-state, digest, export, import, publication-commit, tenant-allocation, tenant-ensure, and tenant-delete methods accept optional `ISmartDbManagementOperationOptions` with `signal?: AbortSignal` and `timeoutMs?: number`. `timeoutMs`, when provided, must be a positive safe integer no greater than `2,147,483,647`. Startup applies both cancellation and one deadline across legacy storage migration, sidecar spawn, auth metadata migration, and database readiness. Cancellation and deadlines are fail-stop once the sidecar is owned: SmartDB attempts to terminate the Rust engine before rejecting. Once termination is confirmed, the operation cannot continue after the caller releases ownership. If termination itself fails, the rejection retains bridge ownership and the service owner must retry `stop()` until it succeeds. After cancellation or deadline termination, restart SmartDB before accepting more traffic.
404
418
 
405
419
  ```typescript
406
420
  const controller = new AbortController();
@@ -426,7 +440,7 @@ await client.db('admin').command({ usersInfo: 'reader' });
426
440
 
427
441
  | Method / Property | Type | Description |
428
442
  |---|---|---|
429
- | `start()` | `Promise<void>` | Start the server (spawns Rust binary) |
443
+ | `start(options?)` | `Promise<void>` | Start under one optional cancellation/deadline budget; join partial sidecars before rejection or retain ownership for `stop()` retry if termination fails |
430
444
  | `stop()` | `Promise<void>` | Wait for active startup work, then stop the server and confirm Rust bridge cleanup, including partial startup cleanup |
431
445
  | `getConnectionUri()` | `string` | Get the active `mongodb://` URI; before start and after stop, port `0` remains unresolved |
432
446
  | `running` | `boolean` | Whether the server is currently running |
@@ -440,6 +454,7 @@ await client.db('admin').command({ usersInfo: 'reader' });
440
454
  | `getCollections(db?)` | `Promise<ICollectionInfo[]>` | List all collections with counts |
441
455
  | `getDocuments(db, coll, limit?, skip?)` | `Promise<IDocumentsResult>` | Browse documents with pagination |
442
456
  | `getHealth(options?)` | `Promise<ISmartDbHealth>` | Read readiness and explicit publication-hold capability fields with optional fail-stop cancellation/deadline ownership |
457
+ | `allocateDatabaseTenant(params, options?)` | `Promise<ISmartDbAllocateDatabaseTenantResult>` | Authoritatively allocate an absent file-backed tenant and return its durable allocation identity; supports fail-stop cancellation and deadlines |
443
458
  | `ensureDatabaseTenant(params, options?)` | `Promise<ISmartDbEnsureDatabaseTenantResult>` | Idempotently ensure an exact fenced tenant; supports fail-stop cancellation and deadlines |
444
459
  | `getDatabaseResourceFenceState(params, options?)` | `Promise<ISmartDbDatabaseResourceFenceState \| null>` | Safely inspect a file-backed database fence high-water mark and publication phase without exposing receipts or capability material; supports fail-stop cancellation and deadlines |
445
460
  | `getDatabaseContentDigest(params, options?)` | `Promise<ISmartDbDatabaseContentDigest>` | Compute a bounded, database-name-independent digest over exact BSON documents and persisted index specifications; supports fail-stop cancellation and deadlines |
package/readme.plan.md CHANGED
@@ -77,6 +77,8 @@ Provable property: "PSS is a function of live data, not of history."
77
77
  - **Offline toolbox**: `rustdb verify | dump | compact | repair-doc | backup` against a data directory.
78
78
  - **On-disk format manifest** with versioning and an N-1 rollback contract: any release can downgrade one release; format changes require a migration note.
79
79
 
80
+ Allocation fencing exception: once a database name has an allocation-managed resource-lock marker, pre-allocation binaries intentionally fail closed. N-1 rollback must restore an allocation-aware binary; deleting or rewriting provider allocation metadata is not a supported downgrade path.
81
+
80
82
  Provable property: "any resource question an operator asks during an incident is answerable from serverStatus or the toolbox."
81
83
 
82
84
  ## Fork in the road (decision rule, written now)
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartdb',
6
- version: '2.18.1',
6
+ version: '3.0.1',
7
7
  description: 'A MongoDB-compatible embedded database server with wire protocol support, backed by a high-performance Rust engine.'
8
8
  }
package/ts/index.ts CHANGED
@@ -40,11 +40,15 @@ export type {
40
40
  ISmartDbCommitDatabasePublicationInput,
41
41
  ISmartDbManagementOperationOptions,
42
42
  ISmartDbDatabaseResourceFenceState,
43
+ ISmartDbDatabaseAllocationIdentity,
44
+ ISmartDbDatabaseAllocationLifecycle,
43
45
  TSmartDbCommitDatabasePublicationResult,
44
46
  TSmartDbResourceFenceErrorCode,
45
47
  ISmartDbDatabaseTenantInput,
46
48
  ISmartDbEnsureDatabaseTenantInput,
47
49
  ISmartDbEnsureDatabaseTenantResult,
50
+ ISmartDbAllocateDatabaseTenantInput,
51
+ ISmartDbAllocateDatabaseTenantResult,
48
52
  ISmartDbDeleteDatabaseTenantInput,
49
53
  ISmartDbRotateDatabaseTenantPasswordInput,
50
54
  ISmartDbDatabaseTenantDescriptor,
@@ -3,6 +3,7 @@ import {
3
3
  type IAuthMetadataPermissionMigrationBridge,
4
4
  type TAuthMetadataPermissionMigrationStatus,
5
5
  } from './migrators/v1_auth_metadata_permissions.js';
6
+ import type { ISmartDbManagementOperationOptions } from '../ts_smartdb/service-types.js';
6
7
 
7
8
  const authMetadataMigrations = [migrateV1AuthMetadataPermissions] as const;
8
9
 
@@ -15,11 +16,12 @@ export class AuthMetadataMigrationRunner {
15
16
 
16
17
  public async run(
17
18
  bridgeArg: IAuthMetadataPermissionMigrationBridge,
19
+ optionsArg?: ISmartDbManagementOperationOptions,
18
20
  ): Promise<TAuthMetadataMigrationRunnerStatus> {
19
21
  if (process.platform !== 'linux') return 'not-applicable';
20
22
  let status: TAuthMetadataPermissionMigrationStatus = 'not-present';
21
23
  for (const migration of authMetadataMigrations) {
22
- status = await migration(bridgeArg, this.usersPath);
24
+ status = await migration(bridgeArg, this.usersPath, optionsArg);
23
25
  }
24
26
  return status;
25
27
  }
@@ -1,5 +1,5 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
3
  import { migrateV0ToV1 } from './migrators/v0_to_v1.js';
4
4
 
5
5
  /**
@@ -28,12 +28,14 @@ export class StorageMigrator {
28
28
  /**
29
29
  * Run any needed migrations. Safe to call even if storage is already current.
30
30
  */
31
- async run(): Promise<void> {
32
- if (!fs.existsSync(this.storagePath)) {
31
+ async run(signalArg?: AbortSignal): Promise<void> {
32
+ signalArg?.throwIfAborted();
33
+ if (!await this.pathExists(this.storagePath)) {
33
34
  return; // No data yet — nothing to migrate
34
35
  }
35
36
 
36
- const version = this.detectVersion();
37
+ const version = await this.detectVersion(signalArg);
38
+ signalArg?.throwIfAborted();
37
39
 
38
40
  if (version === 1) {
39
41
  return; // Already current
@@ -43,7 +45,8 @@ export class StorageMigrator {
43
45
  console.log(`[smartdb] Detected v0 (JSON) storage format at ${this.storagePath}`);
44
46
  console.log(`[smartdb] Running migration v0 → v1 (Bitcask binary format)...`);
45
47
 
46
- const deletableFiles = await migrateV0ToV1(this.storagePath);
48
+ const deletableFiles = await migrateV0ToV1(this.storagePath, signalArg);
49
+ signalArg?.throwIfAborted();
47
50
 
48
51
  if (deletableFiles.length > 0) {
49
52
  console.log(`[smartdb] Migration v0 → v1 complete.`);
@@ -63,33 +66,49 @@ export class StorageMigrator {
63
66
  * v0: {db}/{coll}.json files exist
64
67
  * v1: {db}/{coll}/data.rdb directories exist
65
68
  */
66
- private detectVersion(): TStorageVersion {
67
- const entries = fs.readdirSync(this.storagePath, { withFileTypes: true });
69
+ private async detectVersion(signalArg?: AbortSignal): Promise<TStorageVersion> {
70
+ signalArg?.throwIfAborted();
71
+ const entries = await fs.readdir(this.storagePath, { withFileTypes: true });
72
+ let hasLegacyCollections = false;
68
73
 
69
74
  for (const entry of entries) {
75
+ signalArg?.throwIfAborted();
70
76
  if (!entry.isDirectory()) continue;
71
77
  if (entry.name.startsWith('.__rustdb_')) continue;
72
78
 
73
79
  const dbDir = path.join(this.storagePath, entry.name);
74
- const dbEntries = fs.readdirSync(dbDir, { withFileTypes: true });
80
+ const dbEntries = await fs.readdir(dbDir, { withFileTypes: true });
75
81
 
76
82
  for (const dbEntry of dbEntries) {
77
- if (dbEntry.name.startsWith('.__rustdb_')) continue;
78
- // v1: subdirectory with data.rdb
79
- if (dbEntry.isDirectory()) {
80
- const dataRdb = path.join(dbDir, dbEntry.name, 'data.rdb');
81
- if (fs.existsSync(dataRdb)) {
82
- return 1;
83
- }
83
+ signalArg?.throwIfAborted();
84
+ if (
85
+ dbEntry.isDirectory()
86
+ && dbEntry.name.startsWith('.__rustdb_migration_')
87
+ ) {
88
+ throw new Error(
89
+ `SmartDB legacy migration staging is orphaned: ${path.join(dbDir, dbEntry.name)}`,
90
+ );
84
91
  }
92
+ if (dbEntry.name.startsWith('.__rustdb_')) continue;
85
93
  // v0: .json file (not .indexes.json)
86
94
  if (dbEntry.isFile() && dbEntry.name.endsWith('.json') && !dbEntry.name.endsWith('.indexes.json')) {
87
- return 0;
95
+ hasLegacyCollections = true;
88
96
  }
89
97
  }
90
98
  }
91
99
 
92
- // Empty or unrecognized treat as v1 (fresh start)
93
- return 1;
100
+ // A mixed layout is an interrupted/resumable migration, not proof that the
101
+ // complete storage root is already current.
102
+ return hasLegacyCollections ? 0 : 1;
103
+ }
104
+
105
+ private async pathExists(pathArg: string): Promise<boolean> {
106
+ try {
107
+ await fs.stat(pathArg);
108
+ return true;
109
+ } catch (error) {
110
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
111
+ throw error;
112
+ }
94
113
  }
95
114
  }
@@ -1,4 +1,5 @@
1
1
  export { StorageMigrator } from './classes.storagemigrator.js';
2
+ export { SmartDbStorageMigrationCleanupError } from './migrators/v0_to_v1.js';
2
3
  export {
3
4
  AuthMetadataMigrationRunner,
4
5
  type IAuthMetadataPermissionMigrationBridge,