@push.rocks/smartdb 2.12.2 → 2.13.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@push.rocks/smartdb",
3
- "version": "2.12.2",
3
+ "version": "2.13.0",
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": {
@@ -15,11 +15,11 @@
15
15
  "@api.global/typedserver": "^8.4.6",
16
16
  "@design.estate/dees-element": "^2.2.4",
17
17
  "@git.zone/tsbuild": "^4.4.2",
18
- "@git.zone/tsbundle": "^2.10.4",
19
- "@git.zone/tsrun": "^2.0.4",
20
- "@git.zone/tsrust": "^1.4.1",
18
+ "@git.zone/tsbundle": "^2.11.2",
19
+ "@git.zone/tsrun": "^2.0.5",
20
+ "@git.zone/tsrust": "^1.6.0",
21
21
  "@git.zone/tstest": "^3.6.6",
22
- "@types/node": "^25.9.1",
22
+ "@types/node": "^26.1.1",
23
23
  "mongodb": "^7.1.1"
24
24
  },
25
25
  "dependencies": {
package/readme.md CHANGED
@@ -314,7 +314,53 @@ Authentication verifies SCRAM credentials, denies unauthenticated commands, and
314
314
 
315
315
  Supported built-in role names are `root`, `read`, `readWrite`, `dbAdmin`, `userAdmin`, `clusterMonitor`, plus `readAnyDatabase`, `readWriteAnyDatabase`, `dbAdminAnyDatabase`, and `userAdminAnyDatabase`. When `usersPath` is set, SmartDB persists SCRAM credential material atomically and does not store plaintext passwords.
316
316
 
317
- 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.
317
+ 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.
318
+
319
+ 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.
320
+
321
+ ### Durable Database Publication Holds
322
+
323
+ File-backed SmartDB can keep a replaced or deleted database closed after the local mutation is durable and until a downstream coordinator confirms its own fsync. Check the explicit health contract before using this flow:
324
+
325
+ ```typescript
326
+ const health = await server.getHealth();
327
+ if (
328
+ health.publicationHoldVersion !== 1 ||
329
+ !health.publicationHoldSupported ||
330
+ health.publicationHoldRequiresExternalDrain
331
+ ) {
332
+ throw new Error('SmartDB publication holds are unavailable');
333
+ }
334
+ ```
335
+
336
+ Set `holdPublication: true` on an exact fenced `importDatabase()` or `deleteDatabaseTenant()` operation. The result contains a held `resourceFence` with a one-time `publicationCapability`. Treat that capability as a secret: do not log it or persist it outside protected control-plane state. After downstream state is durable, submit the exact receipt to `commitDatabasePublication()`:
337
+
338
+ ```typescript
339
+ import type { ISmartDbHeldPublicationReceipt } from '@push.rocks/smartdb';
340
+
341
+ const held = await server.importDatabase({
342
+ databaseName: 'tenant_a',
343
+ source: snapshot,
344
+ username: 'tenant_a_user',
345
+ fence: {
346
+ version: 1,
347
+ scopeId: 'corestore-node-1.tenant-a',
348
+ token: 42,
349
+ mutationId: 'restore-42',
350
+ payloadSha256: controlPlanePayloadSha256,
351
+ },
352
+ holdPublication: true,
353
+ });
354
+
355
+ const receipt = held.resourceFence as ISmartDbHeldPublicationReceipt;
356
+ await persistAndFsyncDownstreamState(receipt);
357
+ const released = await server.commitDatabasePublication({
358
+ databaseName: 'tenant_a',
359
+ resourceFence: receipt,
360
+ });
361
+ ```
362
+
363
+ 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.
318
364
 
319
365
  Basic user management commands are available for authenticated users with `root` or `userAdmin` privileges:
320
366
 
@@ -345,6 +391,10 @@ await client.db('admin').command({ usersInfo: 'reader' });
345
391
  | `revertToSeq(seq, dryRun?)` | `Promise<IRevertResult>` | Revert to a specific oplog sequence (must be within retained oplog history) |
346
392
  | `getCollections(db?)` | `Promise<ICollectionInfo[]>` | List all collections with counts |
347
393
  | `getDocuments(db, coll, limit?, skip?)` | `Promise<IDocumentsResult>` | Browse documents with pagination |
394
+ | `getHealth()` | `Promise<ISmartDbHealth>` | Read readiness and explicit publication-hold capability fields |
395
+ | `importDatabase(params)` | `Promise<ISmartDbImportDatabaseResult>` | Durably replace one database, optionally leaving publication held |
396
+ | `deleteDatabaseTenant(params)` | `Promise<ISmartDbDeleteDatabaseTenantResult>` | Durably delete an exact tenant database/user, optionally leaving publication held |
397
+ | `commitDatabasePublication(params)` | `Promise<TSmartDbCommitDatabasePublicationResult>` | Idempotently release an exact held publication receipt |
348
398
 
349
399
  ### LocalSmartDb
350
400
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartdb',
6
- version: '2.12.2',
6
+ version: '2.13.0',
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
@@ -25,6 +25,9 @@ export type {
25
25
  ISmartDbHealth,
26
26
  ISmartDbResourceFence,
27
27
  ISmartDbResourceFenceReceipt,
28
+ ISmartDbHeldPublicationReceipt,
29
+ ISmartDbCommitDatabasePublicationInput,
30
+ TSmartDbCommitDatabasePublicationResult,
28
31
  TSmartDbResourceFenceErrorCode,
29
32
  ISmartDbDatabaseTenantInput,
30
33
  ISmartDbEnsureDatabaseTenantInput,
@@ -29,6 +29,9 @@ export type {
29
29
  ISmartDbHealth,
30
30
  ISmartDbResourceFence,
31
31
  ISmartDbResourceFenceReceipt,
32
+ ISmartDbHeldPublicationReceipt,
33
+ ISmartDbCommitDatabasePublicationInput,
34
+ TSmartDbCommitDatabasePublicationResult,
32
35
  TSmartDbResourceFenceErrorCode,
33
36
  ISmartDbDatabaseTenantInput,
34
37
  ISmartDbEnsureDatabaseTenantInput,
@@ -9,12 +9,14 @@ export type TSmartDbEffectivePayloadInputV1 =
9
9
  source: ISmartDbDatabaseExport;
10
10
  /** Exact exclusive tenant owner; omission retains the legacy v1 digest. */
11
11
  username?: string;
12
+ holdPublication?: boolean;
12
13
  }
13
14
  | {
14
15
  version: 1;
15
16
  kind: 'smartdb.database.delete.v1';
16
17
  databaseName: string;
17
- username: string;
18
+ username?: string;
19
+ holdPublication?: boolean;
18
20
  }
19
21
  | {
20
22
  version: 1;
@@ -212,6 +214,12 @@ export const createSmartDbEffectivePayloadSha256V1 = (
212
214
  ) {
213
215
  throw new TypeError('SmartDB exact replace username must be a string');
214
216
  }
217
+ if (
218
+ inputArg.holdPublication !== undefined &&
219
+ typeof inputArg.holdPublication !== 'boolean'
220
+ ) {
221
+ throw new TypeError('SmartDB holdPublication must be a Boolean');
222
+ }
215
223
  effectivePayload = {
216
224
  version: 1,
217
225
  kind: inputArg.kind,
@@ -220,8 +228,12 @@ export const createSmartDbEffectivePayloadSha256V1 = (
220
228
  ...(inputArg.username === undefined
221
229
  ? {}
222
230
  : { username: inputArg.username }),
231
+ ...(inputArg.holdPublication === true
232
+ ? { holdPublication: true }
233
+ : {}),
223
234
  };
224
- useTypedEncoding = inputArg.username !== undefined;
235
+ useTypedEncoding =
236
+ inputArg.username !== undefined || inputArg.holdPublication === true;
225
237
  break;
226
238
  case 'smartdb.database.ensure.v1': {
227
239
  if (typeof inputArg.username !== 'string') {
@@ -256,15 +268,28 @@ export const createSmartDbEffectivePayloadSha256V1 = (
256
268
  break;
257
269
  }
258
270
  case 'smartdb.database.delete.v1':
259
- if (typeof inputArg.username !== 'string') {
260
- throw new TypeError('SmartDB exact delete username must be a string');
271
+ if (
272
+ inputArg.username !== undefined &&
273
+ typeof inputArg.username !== 'string'
274
+ ) {
275
+ throw new TypeError('SmartDB exact delete username must be a string when supplied');
276
+ }
277
+ if (
278
+ inputArg.holdPublication !== undefined &&
279
+ typeof inputArg.holdPublication !== 'boolean'
280
+ ) {
281
+ throw new TypeError('SmartDB holdPublication must be a Boolean');
261
282
  }
262
283
  effectivePayload = {
263
284
  version: 1,
264
285
  kind: inputArg.kind,
265
286
  databaseName: inputArg.databaseName,
266
- username: inputArg.username,
267
- rawDatabaseOnly: false,
287
+ ...(inputArg.username === undefined
288
+ ? { rawDatabaseOnly: true }
289
+ : { username: inputArg.username, rawDatabaseOnly: false }),
290
+ ...(inputArg.holdPublication === true
291
+ ? { holdPublication: true }
292
+ : {}),
268
293
  };
269
294
  break;
270
295
  default:
@@ -15,6 +15,8 @@ import type {
15
15
  ISmartDbDatabaseExport,
16
16
  ISmartDbImportDatabaseInput,
17
17
  ISmartDbImportDatabaseResult,
18
+ ISmartDbCommitDatabasePublicationInput,
19
+ TSmartDbCommitDatabasePublicationResult,
18
20
  } from './service-types.js';
19
21
 
20
22
  export type {
@@ -30,6 +32,9 @@ export type {
30
32
  ISmartDbDatabaseExport,
31
33
  ISmartDbImportDatabaseInput,
32
34
  ISmartDbImportDatabaseResult,
35
+ ISmartDbHeldPublicationReceipt,
36
+ ISmartDbCommitDatabasePublicationInput,
37
+ TSmartDbCommitDatabasePublicationResult,
33
38
  } from './service-types.js';
34
39
 
35
40
  /**
@@ -155,6 +160,10 @@ type TSmartDbCommands = {
155
160
  params: ISmartDbImportDatabaseInput;
156
161
  result: ISmartDbImportDatabaseResult;
157
162
  };
163
+ commitDatabasePublication: {
164
+ params: ISmartDbCommitDatabasePublicationInput;
165
+ result: TSmartDbCommitDatabasePublicationResult;
166
+ };
158
167
  getOpLog: {
159
168
  params: { sinceSeq?: number; limit?: number; db?: string; collection?: string };
160
169
  result: IOpLogResult;
@@ -254,6 +263,14 @@ function buildLocalPaths(): string[] {
254
263
 
255
264
  return paths;
256
265
  }
266
+ export function getRustDbAllocatorEnv(
267
+ environment: NodeJS.ProcessEnv = process.env,
268
+ ): Record<string, string> {
269
+ return {
270
+ MIMALLOC_ARENA_EAGER_COMMIT: environment.MIMALLOC_ARENA_EAGER_COMMIT ?? '0',
271
+ MIMALLOC_ALLOW_LARGE_OS_PAGES: environment.MIMALLOC_ALLOW_LARGE_OS_PAGES ?? '0',
272
+ };
273
+ }
257
274
 
258
275
  /**
259
276
  * Bridge between TypeScript SmartdbServer and the Rust binary.
@@ -271,6 +288,7 @@ export class RustDbBridge extends EventEmitter {
271
288
  platformPackagePrefix: '@push.rocks/smartdb',
272
289
  localPaths: buildLocalPaths(),
273
290
  maxPayloadSize: 100 * 1024 * 1024, // database exports/imports can be larger than command replies
291
+ env: getRustDbAllocatorEnv(),
274
292
  });
275
293
 
276
294
  // Forward events from the inner bridge
@@ -383,6 +401,19 @@ export class RustDbBridge extends EventEmitter {
383
401
  }
384
402
  }
385
403
 
404
+ public async commitDatabasePublication(
405
+ params: ISmartDbCommitDatabasePublicationInput,
406
+ ): Promise<TSmartDbCommitDatabasePublicationResult> {
407
+ try {
408
+ return await this.bridge.sendCommand(
409
+ 'commitDatabasePublication',
410
+ params,
411
+ ) as TSmartDbCommitDatabasePublicationResult;
412
+ } catch (error) {
413
+ throw normalizeSmartDbResourceFenceError(error);
414
+ }
415
+ }
416
+
386
417
  public async getOpLog(params: {
387
418
  sinceSeq?: number;
388
419
  limit?: number;
@@ -21,6 +21,8 @@ import type {
21
21
  ISmartDbDatabaseExport,
22
22
  ISmartDbImportDatabaseInput,
23
23
  ISmartDbImportDatabaseResult,
24
+ ISmartDbCommitDatabasePublicationInput,
25
+ TSmartDbCommitDatabasePublicationResult,
24
26
  } from '../service-types.js';
25
27
 
26
28
  /**
@@ -298,6 +300,13 @@ export class SmartdbServer {
298
300
  return this.bridge.importDatabase(params);
299
301
  }
300
302
 
303
+ /** Release an exact held database publication after downstream fsync. */
304
+ async commitDatabasePublication(
305
+ params: ISmartDbCommitDatabasePublicationInput,
306
+ ): Promise<TSmartDbCommitDatabasePublicationResult> {
307
+ return this.bridge.commitDatabasePublication(params);
308
+ }
309
+
301
310
  /**
302
311
  * Get readiness/health details for long-running service use.
303
312
  */
@@ -310,6 +319,9 @@ export class SmartdbServer {
310
319
  resourceFencingVersion: this.options.storage === 'file' ? 1 : undefined,
311
320
  resourceFencingSupported: this.options.storage === 'file' ? false : undefined,
312
321
  resourceFencingRequiresDrain: this.options.storage === 'file' ? true : undefined,
322
+ publicationHoldVersion: 1,
323
+ publicationHoldSupported: false,
324
+ publicationHoldRequiresExternalDrain: false,
313
325
  authEnabled: Boolean(this.options.auth?.enabled),
314
326
  authUsers: this.options.auth?.users?.length ?? 0,
315
327
  usersPathConfigured: Boolean(this.options.auth?.usersPath),
@@ -5,6 +5,9 @@ export interface ISmartDbHealth {
5
5
  resourceFencingVersion?: 1;
6
6
  resourceFencingSupported?: boolean;
7
7
  resourceFencingRequiresDrain?: true;
8
+ publicationHoldVersion: 1;
9
+ publicationHoldSupported: boolean;
10
+ publicationHoldRequiresExternalDrain: boolean;
8
11
  authEnabled?: boolean;
9
12
  authUsers?: number;
10
13
  usersPathConfigured?: boolean;
@@ -23,6 +26,9 @@ export const smartDbResourceFenceErrorCodes = [
23
26
  'EFENCE_STATE_CORRUPT',
24
27
  'EFENCE_RECEIPT_LIMIT',
25
28
  'EFENCE_BUSY',
29
+ 'EFENCE_PUBLICATION_HELD',
30
+ 'EFENCE_CAPABILITY_MISMATCH',
31
+ 'EFENCE_NOT_HELD',
26
32
  ] as const;
27
33
 
28
34
  export type TSmartDbResourceFenceErrorCode =
@@ -44,6 +50,34 @@ export interface ISmartDbResourceFenceReceipt
44
50
  | 'smartdb.database.ensure.v1';
45
51
  /** Digest of the canonical payload interpretation derived by SmartDB. */
46
52
  effectivePayloadSha256: string;
53
+ provider?: 'smartdb';
54
+ resourceIdSha256?: string;
55
+ holdPublication?: true;
56
+ publicationState?: 'held' | 'released';
57
+ bindingSha256?: string;
58
+ resultSha256?: string;
59
+ receiptSha256?: string;
60
+ publicationCapabilitySha256?: string;
61
+ /** Present only while publication is held; never returned after release. */
62
+ publicationCapability?: string;
63
+ }
64
+
65
+ export interface ISmartDbHeldPublicationReceipt
66
+ extends ISmartDbResourceFenceReceipt {
67
+ provider: 'smartdb';
68
+ resourceIdSha256: string;
69
+ holdPublication: true;
70
+ publicationState: 'held';
71
+ bindingSha256: string;
72
+ resultSha256: string;
73
+ receiptSha256: string;
74
+ publicationCapabilitySha256: string;
75
+ publicationCapability: string;
76
+ }
77
+
78
+ export interface ISmartDbCommitDatabasePublicationInput {
79
+ databaseName: string;
80
+ resourceFence: ISmartDbHeldPublicationReceipt;
47
81
  }
48
82
 
49
83
  export class SmartDbResourceFenceError extends Error {
@@ -107,6 +141,8 @@ export interface ISmartDbDeleteDatabaseTenantInput {
107
141
  */
108
142
  username?: string;
109
143
  fence?: ISmartDbResourceFence;
144
+ /** Keep the database data plane closed until commitDatabasePublication. */
145
+ holdPublication?: boolean;
110
146
  }
111
147
 
112
148
  export interface ISmartDbRotateDatabaseTenantPasswordInput {
@@ -168,6 +204,8 @@ export interface ISmartDbImportDatabaseInput {
168
204
  */
169
205
  username?: string;
170
206
  fence?: ISmartDbResourceFence;
207
+ /** Keep the database data plane closed until commitDatabasePublication. */
208
+ holdPublication?: boolean;
171
209
  }
172
210
 
173
211
  export interface ISmartDbImportDatabaseResult {
@@ -176,3 +214,7 @@ export interface ISmartDbImportDatabaseResult {
176
214
  documents: number;
177
215
  resourceFence?: ISmartDbResourceFenceReceipt;
178
216
  }
217
+
218
+ export type TSmartDbCommitDatabasePublicationResult =
219
+ | ISmartDbImportDatabaseResult
220
+ | ISmartDbDeleteDatabaseTenantResult;