@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
@@ -1,6 +1,6 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import * as crypto from 'crypto';
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import * as crypto from 'node:crypto';
4
4
  import { BSON } from 'bson';
5
5
 
6
6
  // ---------------------------------------------------------------------------
@@ -100,6 +100,141 @@ function crc32(data: Buffer): number {
100
100
  return (crc ^ 0xFFFFFFFF) >>> 0;
101
101
  }
102
102
 
103
+ const pathExists = async (pathArg: string): Promise<boolean> => {
104
+ try {
105
+ await fs.stat(pathArg);
106
+ return true;
107
+ } catch (error) {
108
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
109
+ throw error;
110
+ }
111
+ };
112
+
113
+ export class SmartDbStorageMigrationCleanupError extends AggregateError {
114
+ constructor(operationErrorArg: unknown, cleanupErrorsArg: unknown[]) {
115
+ super(
116
+ [operationErrorArg, ...cleanupErrorsArg],
117
+ 'SmartDB legacy storage migration failed and staging cleanup was incomplete',
118
+ { cause: operationErrorArg },
119
+ );
120
+ this.name = 'SmartDbStorageMigrationCleanupError';
121
+ }
122
+ }
123
+
124
+ export const runWithOwnedMigrationFileHandle = async <
125
+ TResult,
126
+ TFileHandle extends { close(): Promise<void> },
127
+ >(
128
+ fileHandleArg: TFileHandle,
129
+ operationArg: (
130
+ fileHandleArg: TFileHandle,
131
+ ) => Promise<TResult>,
132
+ signalArg?: AbortSignal,
133
+ ): Promise<TResult> => {
134
+ let operationResult: TResult | undefined;
135
+ let operationError: unknown;
136
+ let operationCompleted = false;
137
+ try {
138
+ operationResult = await operationArg(fileHandleArg);
139
+ operationCompleted = true;
140
+ } catch (error) {
141
+ operationError = error;
142
+ }
143
+ try {
144
+ await fileHandleArg.close();
145
+ } catch (closeError) {
146
+ if (operationError !== undefined) {
147
+ throw new SmartDbStorageMigrationCleanupError(operationError, [closeError]);
148
+ }
149
+ if (signalArg?.aborted) {
150
+ throw new SmartDbStorageMigrationCleanupError(signalArg.reason, [closeError]);
151
+ }
152
+ throw closeError;
153
+ }
154
+ if (!operationCompleted) throw operationError;
155
+ return operationResult as TResult;
156
+ };
157
+
158
+ const withFileHandle = async <TResult>(
159
+ filePathArg: string,
160
+ flagsArg: 'r' | 'wx',
161
+ operationArg: (
162
+ fileHandleArg: Awaited<ReturnType<typeof fs.open>>,
163
+ ) => Promise<TResult>,
164
+ signalArg?: AbortSignal,
165
+ ): Promise<TResult> => runWithOwnedMigrationFileHandle(
166
+ await fs.open(filePathArg, flagsArg),
167
+ operationArg,
168
+ signalArg,
169
+ );
170
+
171
+ const writeBuffer = async (
172
+ fileHandleArg: Awaited<ReturnType<typeof fs.open>>,
173
+ bufferArg: Buffer,
174
+ signalArg?: AbortSignal,
175
+ ): Promise<void> => {
176
+ let offset = 0;
177
+ while (offset < bufferArg.length) {
178
+ signalArg?.throwIfAborted();
179
+ const { bytesWritten } = await fileHandleArg.write(
180
+ bufferArg,
181
+ offset,
182
+ bufferArg.length - offset,
183
+ null,
184
+ );
185
+ if (bytesWritten <= 0) {
186
+ throw new Error('SmartDB storage migration write made no progress');
187
+ }
188
+ offset += bytesWritten;
189
+ }
190
+ signalArg?.throwIfAborted();
191
+ };
192
+
193
+ const syncDirectory = async (
194
+ directoryPathArg: string,
195
+ signalArg?: AbortSignal,
196
+ ): Promise<void> => {
197
+ try {
198
+ await withFileHandle(directoryPathArg, 'r', async (directoryHandleArg) => {
199
+ await directoryHandleArg.sync();
200
+ }, signalArg);
201
+ } catch (error) {
202
+ if (!['EINVAL', 'ENOTSUP'].includes((error as NodeJS.ErrnoException).code || '')) {
203
+ throw error;
204
+ }
205
+ }
206
+ };
207
+
208
+ const syncFile = async (filePathArg: string, signalArg?: AbortSignal): Promise<void> => {
209
+ await withFileHandle(filePathArg, 'r', async (fileHandleArg) => {
210
+ await fileHandleArg.sync();
211
+ }, signalArg);
212
+ };
213
+
214
+ const assertPublishedMigrationComplete = async (
215
+ collectionDirectoryArg: string,
216
+ ): Promise<void> => {
217
+ for (const fileName of ['data.rdb', 'keydir.hint', 'indexes.json']) {
218
+ let fileStat: Awaited<ReturnType<typeof fs.lstat>>;
219
+ try {
220
+ fileStat = await fs.lstat(path.join(collectionDirectoryArg, fileName));
221
+ } catch (error) {
222
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
223
+ throw new Error(
224
+ `SmartDB legacy migration target is incomplete: ${collectionDirectoryArg}`,
225
+ { cause: error },
226
+ );
227
+ }
228
+ throw error;
229
+ }
230
+ if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
231
+ throw new Error(
232
+ `SmartDB legacy migration target is incomplete: ${collectionDirectoryArg}`,
233
+ );
234
+ }
235
+ }
236
+ };
237
+
103
238
  // ---------------------------------------------------------------------------
104
239
  // Migration: v0 (JSON) → v1 (Bitcask binary)
105
240
  // ---------------------------------------------------------------------------
@@ -119,21 +254,27 @@ interface IKeyDirEntry {
119
254
  * - Returns a list of old files that can be safely deleted.
120
255
  * - On failure, cleans up any partial new files and throws.
121
256
  */
122
- export async function migrateV0ToV1(storagePath: string): Promise<string[]> {
257
+ export async function migrateV0ToV1(
258
+ storagePath: string,
259
+ signalArg?: AbortSignal,
260
+ ): Promise<string[]> {
123
261
  const deletableFiles: string[] = [];
124
- const createdDirs: string[] = [];
262
+ const stagingDirectories = new Set<string>();
125
263
 
126
264
  try {
127
- const dbEntries = fs.readdirSync(storagePath, { withFileTypes: true });
265
+ signalArg?.throwIfAborted();
266
+ const dbEntries = await fs.readdir(storagePath, { withFileTypes: true });
128
267
 
129
268
  for (const dbEntry of dbEntries) {
269
+ signalArg?.throwIfAborted();
130
270
  if (!dbEntry.isDirectory()) continue;
131
271
  if (dbEntry.name.startsWith('.__rustdb_')) continue;
132
272
 
133
273
  const dbDir = path.join(storagePath, dbEntry.name);
134
- const collFiles = fs.readdirSync(dbDir, { withFileTypes: true });
274
+ const collFiles = await fs.readdir(dbDir, { withFileTypes: true });
135
275
 
136
276
  for (const collFile of collFiles) {
277
+ signalArg?.throwIfAborted();
137
278
  if (!collFile.isFile()) continue;
138
279
  if (collFile.name.startsWith('.__rustdb_')) continue;
139
280
  if (!collFile.name.endsWith('.json')) continue;
@@ -145,35 +286,46 @@ export async function migrateV0ToV1(storagePath: string): Promise<string[]> {
145
286
 
146
287
  // Target directory
147
288
  const collDir = path.join(dbDir, collName);
148
- if (fs.existsSync(collDir)) {
149
- // Already migrated
289
+ if (await pathExists(collDir)) {
290
+ await assertPublishedMigrationComplete(collDir);
291
+ if (await pathExists(indexJsonPath)) deletableFiles.push(indexJsonPath);
292
+ deletableFiles.push(jsonPath);
150
293
  continue;
151
294
  }
152
295
 
153
296
  console.log(`[smartdb] Migrating ${dbEntry.name}.${collName}...`);
154
297
 
155
298
  // Read the JSON collection
156
- const jsonData = fs.readFileSync(jsonPath, 'utf-8');
299
+ const jsonData = await fs.readFile(jsonPath, {
300
+ encoding: 'utf8',
301
+ signal: signalArg,
302
+ });
303
+ signalArg?.throwIfAborted();
157
304
  const docs: any[] = JSON.parse(jsonData);
158
305
 
159
- // Create collection directory
160
- fs.mkdirSync(collDir, { recursive: true });
161
- createdDirs.push(collDir);
306
+ // Build the complete collection in a hidden sibling and publish it
307
+ // atomically so cancellation can never expose partial v1 state.
308
+ const stagingDir = path.join(
309
+ dbDir,
310
+ `.__rustdb_migration_${collName}_${process.pid}_${crypto.randomBytes(8).toString('hex')}`,
311
+ );
312
+ await fs.mkdir(stagingDir);
313
+ stagingDirectories.add(stagingDir);
314
+ signalArg?.throwIfAborted();
162
315
 
163
316
  // Write data.rdb
164
- const dataPath = path.join(collDir, 'data.rdb');
165
- const fd = fs.openSync(dataPath, 'w');
166
-
167
- try {
317
+ const dataPath = path.join(stagingDir, 'data.rdb');
318
+ await withFileHandle(dataPath, 'wx', async (dataFileHandle) => {
168
319
  // File header
169
320
  const headerBuf = writeFileHeader(FILE_TYPE_DATA);
170
- fs.writeSync(fd, headerBuf);
321
+ await writeBuffer(dataFileHandle, headerBuf, signalArg);
171
322
 
172
323
  let currentOffset = BigInt(FILE_HEADER_SIZE);
173
324
  const keydir: Map<string, IKeyDirEntry> = new Map();
174
325
  const ts = BigInt(Date.now());
175
326
 
176
327
  for (const doc of docs) {
328
+ signalArg?.throwIfAborted();
177
329
  // Extract _id
178
330
  let idHex: string;
179
331
  if (doc._id && doc._id.$oid) {
@@ -194,7 +346,7 @@ export async function migrateV0ToV1(storagePath: string): Promise<string[]> {
194
346
  const valueBuf = Buffer.from(bsonBytes);
195
347
 
196
348
  const record = encodeDataRecord(ts, keyBuf, valueBuf);
197
- fs.writeSync(fd, record);
349
+ await writeBuffer(dataFileHandle, record, signalArg);
198
350
 
199
351
  keydir.set(idHex, {
200
352
  offset: currentOffset,
@@ -206,47 +358,75 @@ export async function migrateV0ToV1(storagePath: string): Promise<string[]> {
206
358
  currentOffset += BigInt(record.length);
207
359
  }
208
360
 
209
- fs.fsyncSync(fd);
210
- fs.closeSync(fd);
361
+ await dataFileHandle.sync();
362
+ signalArg?.throwIfAborted();
211
363
 
212
364
  // Write keydir.hint
213
- const hintPath = path.join(collDir, 'keydir.hint');
214
- const hintFd = fs.openSync(hintPath, 'w');
215
- fs.writeSync(hintFd, writeFileHeader(FILE_TYPE_HINT));
216
- for (const [key, entry] of keydir) {
217
- fs.writeSync(hintFd, encodeHintEntry(key, entry.offset, entry.recordLen, entry.valueLen, entry.timestamp));
218
- }
219
- fs.fsyncSync(hintFd);
220
- fs.closeSync(hintFd);
221
-
222
- } catch (writeErr) {
223
- // Clean up on write failure
224
- try { fs.closeSync(fd); } catch {}
225
- throw writeErr;
226
- }
365
+ const hintPath = path.join(stagingDir, 'keydir.hint');
366
+ await withFileHandle(hintPath, 'wx', async (hintFileHandle) => {
367
+ await writeBuffer(hintFileHandle, writeFileHeader(FILE_TYPE_HINT), signalArg);
368
+ for (const [key, entry] of keydir) {
369
+ signalArg?.throwIfAborted();
370
+ await writeBuffer(
371
+ hintFileHandle,
372
+ encodeHintEntry(
373
+ key,
374
+ entry.offset,
375
+ entry.recordLen,
376
+ entry.valueLen,
377
+ entry.timestamp,
378
+ ),
379
+ signalArg,
380
+ );
381
+ }
382
+ await hintFileHandle.sync();
383
+ signalArg?.throwIfAborted();
384
+ }, signalArg);
385
+ }, signalArg);
227
386
 
228
387
  // Copy indexes.json if it exists
229
- if (fs.existsSync(indexJsonPath)) {
230
- const destIndexPath = path.join(collDir, 'indexes.json');
231
- fs.copyFileSync(indexJsonPath, destIndexPath);
388
+ if (await pathExists(indexJsonPath)) {
389
+ const destIndexPath = path.join(stagingDir, 'indexes.json');
390
+ await fs.copyFile(indexJsonPath, destIndexPath);
391
+ await syncFile(destIndexPath, signalArg);
392
+ signalArg?.throwIfAborted();
232
393
  deletableFiles.push(indexJsonPath);
233
394
  } else {
234
395
  // Write default _id index
235
- const destIndexPath = path.join(collDir, 'indexes.json');
236
- fs.writeFileSync(destIndexPath, JSON.stringify([{ name: '_id_', key: { _id: 1 } }], null, 2));
396
+ const destIndexPath = path.join(stagingDir, 'indexes.json');
397
+ await fs.writeFile(
398
+ destIndexPath,
399
+ JSON.stringify([{ name: '_id_', key: { _id: 1 } }], null, 2),
400
+ { signal: signalArg },
401
+ );
402
+ await syncFile(destIndexPath, signalArg);
403
+ signalArg?.throwIfAborted();
237
404
  }
238
405
 
406
+ await assertPublishedMigrationComplete(stagingDir);
407
+ await syncDirectory(stagingDir, signalArg);
408
+ signalArg?.throwIfAborted();
409
+ await fs.rename(stagingDir, collDir);
410
+ stagingDirectories.delete(stagingDir);
411
+ await syncDirectory(dbDir, signalArg);
412
+ signalArg?.throwIfAborted();
239
413
  deletableFiles.push(jsonPath);
240
414
 
241
415
  console.log(`[smartdb] Migrated ${dbEntry.name}.${collName}: ${docs.length} documents`);
242
416
  }
243
417
  }
244
418
  } catch (err) {
245
- // Clean up any partially created directories
246
- for (const dir of createdDirs) {
419
+ const cleanupErrors: unknown[] = [];
420
+ for (const dir of Array.from(stagingDirectories).reverse()) {
247
421
  try {
248
- fs.rmSync(dir, { recursive: true, force: true });
249
- } catch {}
422
+ await fs.rm(dir, { recursive: true, force: true });
423
+ await syncDirectory(path.dirname(dir));
424
+ } catch (cleanupError) {
425
+ cleanupErrors.push(cleanupError);
426
+ }
427
+ }
428
+ if (cleanupErrors.length > 0) {
429
+ throw new SmartDbStorageMigrationCleanupError(err, cleanupErrors);
250
430
  }
251
431
  throw err;
252
432
  }
@@ -1,4 +1,5 @@
1
1
  import type { TAuthMetadataPermissionTransitionStatus } from '../../ts_smartdb/rust-db-bridge.js';
2
+ import type { ISmartDbManagementOperationOptions } from '../../ts_smartdb/service-types.js';
2
3
 
3
4
  const legacyAuthMetadataMode = 0o644;
4
5
  const privateAuthMetadataMode = 0o600;
@@ -8,6 +9,7 @@ export interface IAuthMetadataPermissionMigrationBridge {
8
9
  usersPathArg: string,
9
10
  expectedModeArg: number,
10
11
  targetModeArg: number,
12
+ optionsArg?: ISmartDbManagementOperationOptions,
11
13
  ): Promise<TAuthMetadataPermissionTransitionStatus>;
12
14
  }
13
15
 
@@ -22,9 +24,11 @@ export type TAuthMetadataPermissionMigrationStatus =
22
24
  export const migrateV1AuthMetadataPermissions = async (
23
25
  bridgeArg: IAuthMetadataPermissionMigrationBridge,
24
26
  usersPathArg: string,
27
+ optionsArg?: ISmartDbManagementOperationOptions,
25
28
  ): Promise<TAuthMetadataPermissionMigrationStatus> => bridgeArg
26
29
  .transitionAuthMetadataPermissions(
27
30
  usersPathArg,
28
31
  legacyAuthMetadataMode,
29
32
  privateAuthMetadataMode,
33
+ optionsArg,
30
34
  );
@@ -36,6 +36,8 @@ export type {
36
36
  ISmartDbCommitDatabasePublicationInput,
37
37
  ISmartDbManagementOperationOptions,
38
38
  ISmartDbDatabaseResourceFenceState,
39
+ ISmartDbDatabaseAllocationIdentity,
40
+ ISmartDbDatabaseAllocationLifecycle,
39
41
  ILocalSmartDbOfflineInspectionLimits,
40
42
  ILocalSmartDbOfflineStringValueInspection,
41
43
  TLocalSmartDbOfflineStringValueInspectionResult,
@@ -44,6 +46,8 @@ export type {
44
46
  ISmartDbDatabaseTenantInput,
45
47
  ISmartDbEnsureDatabaseTenantInput,
46
48
  ISmartDbEnsureDatabaseTenantResult,
49
+ ISmartDbAllocateDatabaseTenantInput,
50
+ ISmartDbAllocateDatabaseTenantResult,
47
51
  ISmartDbDeleteDatabaseTenantInput,
48
52
  ISmartDbRotateDatabaseTenantPasswordInput,
49
53
  ISmartDbDatabaseTenantDescriptor,
@@ -1,5 +1,8 @@
1
1
  import * as plugins from './plugins.js';
2
- import type { ISmartDbDatabaseExport } from './service-types.js';
2
+ import type {
3
+ ISmartDbDatabaseAllocationIdentity,
4
+ ISmartDbDatabaseExport,
5
+ } from './service-types.js';
3
6
 
4
7
  export type TSmartDbEffectivePayloadInputV1 =
5
8
  | {
@@ -9,6 +12,7 @@ export type TSmartDbEffectivePayloadInputV1 =
9
12
  source: ISmartDbDatabaseExport;
10
13
  /** Exact exclusive tenant owner; omission retains the legacy v1 digest. */
11
14
  username?: string;
15
+ allocation?: ISmartDbDatabaseAllocationIdentity;
12
16
  holdPublication?: boolean;
13
17
  }
14
18
  | {
@@ -16,6 +20,7 @@ export type TSmartDbEffectivePayloadInputV1 =
16
20
  kind: 'smartdb.database.delete.v1';
17
21
  databaseName: string;
18
22
  username?: string;
23
+ allocation?: ISmartDbDatabaseAllocationIdentity;
19
24
  holdPublication?: boolean;
20
25
  }
21
26
  | {
@@ -25,6 +30,16 @@ export type TSmartDbEffectivePayloadInputV1 =
25
30
  username: string;
26
31
  password: string;
27
32
  roles?: string[];
33
+ allocation?: ISmartDbDatabaseAllocationIdentity;
34
+ }
35
+ | {
36
+ version: 1;
37
+ kind: 'smartdb.database.allocate.v1';
38
+ databaseName: string;
39
+ expectedAbsent: true;
40
+ username: string;
41
+ password: string;
42
+ roles?: string[];
28
43
  };
29
44
 
30
45
  type TCanonicalJsonValue =
@@ -228,12 +243,17 @@ export const createSmartDbEffectivePayloadSha256V1 = (
228
243
  ...(inputArg.username === undefined
229
244
  ? {}
230
245
  : { username: inputArg.username }),
246
+ ...(inputArg.allocation === undefined
247
+ ? {}
248
+ : { allocation: inputArg.allocation }),
231
249
  ...(inputArg.holdPublication === true
232
250
  ? { holdPublication: true }
233
251
  : {}),
234
252
  };
235
253
  useTypedEncoding =
236
- inputArg.username !== undefined || inputArg.holdPublication === true;
254
+ inputArg.username !== undefined ||
255
+ inputArg.allocation !== undefined ||
256
+ inputArg.holdPublication === true;
237
257
  break;
238
258
  case 'smartdb.database.ensure.v1': {
239
259
  if (typeof inputArg.username !== 'string') {
@@ -264,7 +284,40 @@ export const createSmartDbEffectivePayloadSha256V1 = (
264
284
  .update(inputArg.password, 'utf8')
265
285
  .digest('hex'),
266
286
  roles: canonicalRoles,
287
+ ...(inputArg.allocation === undefined
288
+ ? {}
289
+ : { allocation: inputArg.allocation }),
267
290
  };
291
+ useTypedEncoding = inputArg.allocation !== undefined;
292
+ break;
293
+ }
294
+ case 'smartdb.database.allocate.v1': {
295
+ if (inputArg.expectedAbsent !== true) {
296
+ throw new TypeError('SmartDB allocation expectedAbsent must be true');
297
+ }
298
+ if (typeof inputArg.username !== 'string') {
299
+ throw new TypeError('SmartDB allocation username must be a string');
300
+ }
301
+ if (typeof inputArg.password !== 'string') {
302
+ throw new TypeError('SmartDB allocation password must be a string');
303
+ }
304
+ const roles = inputArg.roles ?? ['readWrite', 'dbAdmin'];
305
+ if (!Array.isArray(roles) || roles.some((roleArg) => typeof roleArg !== 'string' || roleArg.length === 0)) {
306
+ throw new TypeError('SmartDB allocation roles must be an array of non-empty strings');
307
+ }
308
+ effectivePayload = {
309
+ version: 1,
310
+ kind: inputArg.kind,
311
+ databaseName: inputArg.databaseName,
312
+ expectedAbsent: true,
313
+ username: inputArg.username,
314
+ passwordSha256: plugins.crypto
315
+ .createHash('sha256')
316
+ .update(inputArg.password, 'utf8')
317
+ .digest('hex'),
318
+ roles: [...new Set(roles)].sort(compareUtf8Keys),
319
+ };
320
+ useTypedEncoding = true;
268
321
  break;
269
322
  }
270
323
  case 'smartdb.database.delete.v1':
@@ -287,10 +340,14 @@ export const createSmartDbEffectivePayloadSha256V1 = (
287
340
  ...(inputArg.username === undefined
288
341
  ? { rawDatabaseOnly: true }
289
342
  : { username: inputArg.username, rawDatabaseOnly: false }),
343
+ ...(inputArg.allocation === undefined
344
+ ? {}
345
+ : { allocation: inputArg.allocation }),
290
346
  ...(inputArg.holdPublication === true
291
347
  ? { holdPublication: true }
292
348
  : {}),
293
349
  };
350
+ useTypedEncoding = inputArg.allocation !== undefined;
294
351
  break;
295
352
  default:
296
353
  throw new TypeError('SmartDB mutation kind has no version 1 payload profile');
@@ -12,6 +12,8 @@ import type {
12
12
  ISmartDbDatabaseTenantInput,
13
13
  ISmartDbEnsureDatabaseTenantInput,
14
14
  ISmartDbEnsureDatabaseTenantResult,
15
+ ISmartDbAllocateDatabaseTenantInput,
16
+ ISmartDbAllocateDatabaseTenantResult,
15
17
  ISmartDbDeleteDatabaseTenantInput,
16
18
  ISmartDbRotateDatabaseTenantPasswordInput,
17
19
  ISmartDbDatabaseTenantDescriptor,
@@ -35,6 +37,8 @@ export type {
35
37
  ISmartDbDatabaseTenantInput,
36
38
  ISmartDbEnsureDatabaseTenantInput,
37
39
  ISmartDbEnsureDatabaseTenantResult,
40
+ ISmartDbAllocateDatabaseTenantInput,
41
+ ISmartDbAllocateDatabaseTenantResult,
38
42
  ISmartDbDeleteDatabaseTenantInput,
39
43
  ISmartDbRotateDatabaseTenantPasswordInput,
40
44
  ISmartDbDatabaseTenantDescriptor,
@@ -167,6 +171,10 @@ type TSmartDbCommands = {
167
171
  params: ISmartDbDatabaseTenantInput;
168
172
  result: ISmartDbDatabaseTenantDescriptor;
169
173
  };
174
+ allocateDatabaseTenant: {
175
+ params: ISmartDbAllocateDatabaseTenantInput;
176
+ result: ISmartDbAllocateDatabaseTenantResult;
177
+ };
170
178
  ensureDatabaseTenant: {
171
179
  params: ISmartDbEnsureDatabaseTenantInput;
172
180
  result: ISmartDbEnsureDatabaseTenantResult;
@@ -529,20 +537,41 @@ export class RustDbBridge extends EventEmitter {
529
537
  usersPathArg: string,
530
538
  expectedModeArg: number,
531
539
  targetModeArg: number,
540
+ optionsArg?: ISmartDbManagementOperationOptions,
532
541
  ): Promise<TAuthMetadataPermissionTransitionStatus> {
533
- const result = await this.bridge.sendCommand(
534
- 'transitionAuthMetadataPermissions',
535
- {
536
- usersPath: usersPathArg,
537
- expectedMode: expectedModeArg,
538
- targetMode: targetModeArg,
539
- },
540
- ) as { status: TAuthMetadataPermissionTransitionStatus };
541
- return result.status;
542
+ validateSmartDbManagementOperationOptions(optionsArg);
543
+ try {
544
+ const result = await this.bridge.sendCommand(
545
+ 'transitionAuthMetadataPermissions',
546
+ {
547
+ usersPath: usersPathArg,
548
+ expectedMode: expectedModeArg,
549
+ targetMode: targetModeArg,
550
+ },
551
+ optionsArg,
552
+ ) as { status: TAuthMetadataPermissionTransitionStatus };
553
+ return result.status;
554
+ } catch (error) {
555
+ await this.awaitManagementOperationTermination(error);
556
+ throw error;
557
+ }
542
558
  }
543
559
 
544
- public async startDb(config: ISmartDbRustConfig): Promise<{ connectionUri: string }> {
545
- return await this.bridge.sendCommand('start', { config }) as { connectionUri: string };
560
+ public async startDb(
561
+ config: ISmartDbRustConfig,
562
+ optionsArg?: ISmartDbManagementOperationOptions,
563
+ ): Promise<{ connectionUri: string }> {
564
+ validateSmartDbManagementOperationOptions(optionsArg);
565
+ try {
566
+ return await this.bridge.sendCommand(
567
+ 'start',
568
+ { config },
569
+ optionsArg,
570
+ ) as { connectionUri: string };
571
+ } catch (error) {
572
+ await this.awaitManagementOperationTermination(error);
573
+ throw error;
574
+ }
546
575
  }
547
576
 
548
577
  public async stopDb(): Promise<void> {
@@ -598,6 +627,22 @@ export class RustDbBridge extends EventEmitter {
598
627
  }
599
628
  }
600
629
 
630
+ public async allocateDatabaseTenant(
631
+ params: ISmartDbAllocateDatabaseTenantInput,
632
+ optionsArg?: ISmartDbManagementOperationOptions,
633
+ ): Promise<ISmartDbAllocateDatabaseTenantResult> {
634
+ try {
635
+ return await this.bridge.sendCommand(
636
+ 'allocateDatabaseTenant',
637
+ params,
638
+ optionsArg,
639
+ ) as ISmartDbAllocateDatabaseTenantResult;
640
+ } catch (error) {
641
+ await this.awaitManagementOperationTermination(error);
642
+ throw normalizeSmartDbResourceFenceError(error);
643
+ }
644
+ }
645
+
601
646
  public async deleteDatabaseTenant(
602
647
  params: ISmartDbDeleteDatabaseTenantInput,
603
648
  optionsArg?: ISmartDbManagementOperationOptions,