@push.rocks/smartdb 2.12.1 → 2.12.3
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_rust/rustdb_linux_amd64 +0 -0
- package/dist_rust/rustdb_linux_arm64 +0 -0
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/index.d.ts +2 -2
- package/dist_ts/index.js +2 -2
- package/dist_ts/ts_smartdb/index.d.ts +3 -1
- package/dist_ts/ts_smartdb/index.js +2 -1
- package/dist_ts/ts_smartdb/plugins.d.ts +3 -0
- package/dist_ts/ts_smartdb/plugins.js +5 -1
- package/dist_ts/ts_smartdb/resource-fencing.d.ts +26 -0
- package/dist_ts/ts_smartdb/resource-fencing.js +212 -0
- package/dist_ts/ts_smartdb/rust-db-bridge.d.ts +4 -2
- package/dist_ts/ts_smartdb/rust-db-bridge.js +22 -2
- package/dist_ts/ts_smartdb/server/SmartdbServer.d.ts +5 -1
- package/dist_ts/ts_smartdb/server/SmartdbServer.js +8 -1
- package/dist_ts/ts_smartdb/service-types.d.ts +17 -3
- package/dist_ts/ts_smartdb/service-types.js +2 -1
- package/dist_ts_debugserver/bundled.js +2 -2
- package/package.json +5 -5
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/index.ts +4 -0
- package/ts/ts_smartdb/index.ts +4 -0
- package/ts/ts_smartdb/plugins.ts +6 -0
- package/ts/ts_smartdb/resource-fencing.ts +286 -0
- package/ts/ts_smartdb/rust-db-bridge.ts +32 -1
- package/ts/ts_smartdb/server/SmartdbServer.ts +15 -3
- package/ts/ts_smartdb/service-types.ts +22 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@push.rocks/smartdb",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.3",
|
|
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.
|
|
19
|
-
"@git.zone/tsrun": "^2.0.
|
|
20
|
-
"@git.zone/tsrust": "^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": "^
|
|
22
|
+
"@types/node": "^26.1.1",
|
|
23
23
|
"mongodb": "^7.1.1"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
package/ts/00_commitinfo_data.ts
CHANGED
package/ts/index.ts
CHANGED
|
@@ -27,6 +27,8 @@ export type {
|
|
|
27
27
|
ISmartDbResourceFenceReceipt,
|
|
28
28
|
TSmartDbResourceFenceErrorCode,
|
|
29
29
|
ISmartDbDatabaseTenantInput,
|
|
30
|
+
ISmartDbEnsureDatabaseTenantInput,
|
|
31
|
+
ISmartDbEnsureDatabaseTenantResult,
|
|
30
32
|
ISmartDbDeleteDatabaseTenantInput,
|
|
31
33
|
ISmartDbRotateDatabaseTenantPasswordInput,
|
|
32
34
|
ISmartDbDatabaseTenantDescriptor,
|
|
@@ -35,9 +37,11 @@ export type {
|
|
|
35
37
|
ISmartDbDatabaseExport,
|
|
36
38
|
ISmartDbImportDatabaseInput,
|
|
37
39
|
ISmartDbImportDatabaseResult,
|
|
40
|
+
TSmartDbEffectivePayloadInputV1,
|
|
38
41
|
} from './ts_smartdb/index.js';
|
|
39
42
|
export {
|
|
40
43
|
SmartDbResourceFenceError,
|
|
41
44
|
normalizeSmartDbResourceFenceError,
|
|
42
45
|
smartDbResourceFenceErrorCodes,
|
|
46
|
+
createSmartDbEffectivePayloadSha256V1,
|
|
43
47
|
} from './ts_smartdb/index.js';
|
package/ts/ts_smartdb/index.ts
CHANGED
|
@@ -31,6 +31,8 @@ export type {
|
|
|
31
31
|
ISmartDbResourceFenceReceipt,
|
|
32
32
|
TSmartDbResourceFenceErrorCode,
|
|
33
33
|
ISmartDbDatabaseTenantInput,
|
|
34
|
+
ISmartDbEnsureDatabaseTenantInput,
|
|
35
|
+
ISmartDbEnsureDatabaseTenantResult,
|
|
34
36
|
ISmartDbDeleteDatabaseTenantInput,
|
|
35
37
|
ISmartDbRotateDatabaseTenantPasswordInput,
|
|
36
38
|
ISmartDbDatabaseTenantDescriptor,
|
|
@@ -45,3 +47,5 @@ export {
|
|
|
45
47
|
normalizeSmartDbResourceFenceError,
|
|
46
48
|
smartDbResourceFenceErrorCodes,
|
|
47
49
|
} from './service-types.js';
|
|
50
|
+
export type { TSmartDbEffectivePayloadInputV1 } from './resource-fencing.js';
|
|
51
|
+
export { createSmartDbEffectivePayloadSha256V1 } from './resource-fencing.js';
|
package/ts/ts_smartdb/plugins.ts
CHANGED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import * as plugins from './plugins.js';
|
|
2
|
+
import type { ISmartDbDatabaseExport } from './service-types.js';
|
|
3
|
+
|
|
4
|
+
export type TSmartDbEffectivePayloadInputV1 =
|
|
5
|
+
| {
|
|
6
|
+
version: 1;
|
|
7
|
+
kind: 'smartdb.database.replace.v1';
|
|
8
|
+
databaseName: string;
|
|
9
|
+
source: ISmartDbDatabaseExport;
|
|
10
|
+
/** Exact exclusive tenant owner; omission retains the legacy v1 digest. */
|
|
11
|
+
username?: string;
|
|
12
|
+
}
|
|
13
|
+
| {
|
|
14
|
+
version: 1;
|
|
15
|
+
kind: 'smartdb.database.delete.v1';
|
|
16
|
+
databaseName: string;
|
|
17
|
+
username: string;
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
version: 1;
|
|
21
|
+
kind: 'smartdb.database.ensure.v1';
|
|
22
|
+
databaseName: string;
|
|
23
|
+
username: string;
|
|
24
|
+
password: string;
|
|
25
|
+
roles?: string[];
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type TCanonicalJsonValue =
|
|
29
|
+
| null
|
|
30
|
+
| boolean
|
|
31
|
+
| number
|
|
32
|
+
| string
|
|
33
|
+
| TCanonicalJsonValue[]
|
|
34
|
+
| { [key: string]: TCanonicalJsonValue };
|
|
35
|
+
|
|
36
|
+
const typedPayloadPrefix = plugins.buffer.Buffer.from(
|
|
37
|
+
'smartdb.effective-payload.typed.v1\0',
|
|
38
|
+
'utf8',
|
|
39
|
+
);
|
|
40
|
+
const minimumI64 = -9_223_372_036_854_775_808n;
|
|
41
|
+
const maximumU64 = 18_446_744_073_709_551_615n;
|
|
42
|
+
|
|
43
|
+
const compareUtf8Keys = (leftArg: string, rightArg: string): number =>
|
|
44
|
+
plugins.buffer.Buffer.compare(
|
|
45
|
+
plugins.buffer.Buffer.from(leftArg, 'utf8'),
|
|
46
|
+
plugins.buffer.Buffer.from(rightArg, 'utf8'),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const formatSerdeJsonFloat = (valueArg: number): string => {
|
|
50
|
+
const transportNumber = JSON.stringify(valueArg);
|
|
51
|
+
if (/^-?\d+$/.test(transportNumber)) {
|
|
52
|
+
const integer = BigInt(transportNumber);
|
|
53
|
+
if (integer >= minimumI64 && integer <= maximumU64) {
|
|
54
|
+
return transportNumber;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const negative = valueArg < 0;
|
|
59
|
+
const source = Math.abs(valueArg).toString().toLowerCase();
|
|
60
|
+
let digits: string;
|
|
61
|
+
let decimalExponent: number;
|
|
62
|
+
if (source.includes('e')) {
|
|
63
|
+
const [coefficient, exponent] = source.split('e');
|
|
64
|
+
digits = coefficient.replace('.', '').replace(/^0+|0+$/g, '');
|
|
65
|
+
decimalExponent = Number.parseInt(exponent, 10);
|
|
66
|
+
} else {
|
|
67
|
+
const [integerPart, fractionPart = ''] = source.split('.');
|
|
68
|
+
const rawDigits = `${integerPart}${fractionPart}`;
|
|
69
|
+
const firstSignificant = rawDigits.search(/[1-9]/);
|
|
70
|
+
const lastSignificant = rawDigits.search(/[1-9](?!.*[1-9])/);
|
|
71
|
+
if (firstSignificant < 0) {
|
|
72
|
+
return '0.0';
|
|
73
|
+
}
|
|
74
|
+
digits = rawDigits.slice(firstSignificant, lastSignificant + 1);
|
|
75
|
+
decimalExponent = integerPart.length - firstSignificant - 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let rendered: string;
|
|
79
|
+
if (decimalExponent >= -5 && decimalExponent <= 15) {
|
|
80
|
+
const integerDigits = decimalExponent + 1;
|
|
81
|
+
if (digits.length <= integerDigits) {
|
|
82
|
+
rendered = `${digits}${'0'.repeat(integerDigits - digits.length)}.0`;
|
|
83
|
+
} else if (integerDigits > 0) {
|
|
84
|
+
rendered = `${digits.slice(0, integerDigits)}.${digits.slice(integerDigits)}`;
|
|
85
|
+
} else {
|
|
86
|
+
rendered = `0.${'0'.repeat(-integerDigits)}${digits}`;
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
const significand =
|
|
90
|
+
digits.length === 1 ? digits : `${digits[0]}.${digits.slice(1)}`;
|
|
91
|
+
rendered = `${significand}e${decimalExponent >= 0 ? '+' : ''}${decimalExponent}`;
|
|
92
|
+
}
|
|
93
|
+
return negative ? `-${rendered}` : rendered;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const canonicalJsonStringify = (valueArg: TCanonicalJsonValue): string => {
|
|
97
|
+
if (typeof valueArg === 'number') {
|
|
98
|
+
return formatSerdeJsonFloat(valueArg);
|
|
99
|
+
}
|
|
100
|
+
if (valueArg === null || typeof valueArg !== 'object') {
|
|
101
|
+
const serialized = JSON.stringify(valueArg);
|
|
102
|
+
if (serialized === undefined) {
|
|
103
|
+
throw new TypeError('SmartDB effective payload contains unsupported JSON');
|
|
104
|
+
}
|
|
105
|
+
return serialized;
|
|
106
|
+
}
|
|
107
|
+
if (Array.isArray(valueArg)) {
|
|
108
|
+
return `[${valueArg.map((entryArg) => canonicalJsonStringify(entryArg)).join(',')}]`;
|
|
109
|
+
}
|
|
110
|
+
return `{${Object.keys(valueArg)
|
|
111
|
+
.map(
|
|
112
|
+
(keyArg) =>
|
|
113
|
+
`${JSON.stringify(keyArg)}:${canonicalJsonStringify(valueArg[keyArg])}`,
|
|
114
|
+
)
|
|
115
|
+
.join(',')}}`;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const encodeTypedLength = (lengthArg: number): Uint8Array => {
|
|
119
|
+
const encoded = plugins.buffer.Buffer.allocUnsafe(8);
|
|
120
|
+
encoded.writeBigUInt64BE(BigInt(lengthArg));
|
|
121
|
+
return encoded;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const encodeTypedBytes = (valueArg: Uint8Array): Uint8Array[] => [
|
|
125
|
+
encodeTypedLength(valueArg.byteLength),
|
|
126
|
+
valueArg,
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
const encodeTypedValue = (
|
|
130
|
+
valueArg: TCanonicalJsonValue,
|
|
131
|
+
chunksArg: Uint8Array[],
|
|
132
|
+
): void => {
|
|
133
|
+
if (valueArg === null) {
|
|
134
|
+
chunksArg.push(plugins.buffer.Buffer.from([0]));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (valueArg === false) {
|
|
138
|
+
chunksArg.push(plugins.buffer.Buffer.from([1]));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
if (valueArg === true) {
|
|
142
|
+
chunksArg.push(plugins.buffer.Buffer.from([2]));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if (typeof valueArg === 'number') {
|
|
146
|
+
if (!Number.isFinite(valueArg)) {
|
|
147
|
+
throw new TypeError('SmartDB exact replace contains a non-finite number');
|
|
148
|
+
}
|
|
149
|
+
const encoded = plugins.buffer.Buffer.allocUnsafe(9);
|
|
150
|
+
encoded[0] = 3;
|
|
151
|
+
encoded.writeDoubleBE(valueArg, 1);
|
|
152
|
+
chunksArg.push(encoded);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (typeof valueArg === 'string') {
|
|
156
|
+
const encoded = plugins.buffer.Buffer.from(valueArg, 'utf8');
|
|
157
|
+
chunksArg.push(plugins.buffer.Buffer.from([4]), ...encodeTypedBytes(encoded));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (Array.isArray(valueArg)) {
|
|
161
|
+
chunksArg.push(
|
|
162
|
+
plugins.buffer.Buffer.from([5]),
|
|
163
|
+
encodeTypedLength(valueArg.length),
|
|
164
|
+
);
|
|
165
|
+
for (const value of valueArg) {
|
|
166
|
+
encodeTypedValue(value, chunksArg);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const keys = Object.keys(valueArg).sort(compareUtf8Keys);
|
|
171
|
+
chunksArg.push(
|
|
172
|
+
plugins.buffer.Buffer.from([6]),
|
|
173
|
+
encodeTypedLength(keys.length),
|
|
174
|
+
);
|
|
175
|
+
for (const key of keys) {
|
|
176
|
+
const encodedKey = plugins.buffer.Buffer.from(key, 'utf8');
|
|
177
|
+
chunksArg.push(
|
|
178
|
+
plugins.buffer.Buffer.from([4]),
|
|
179
|
+
...encodeTypedBytes(encodedKey),
|
|
180
|
+
);
|
|
181
|
+
encodeTypedValue(valueArg[key], chunksArg);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const encodeTypedPayload = (valueArg: TCanonicalJsonValue): Uint8Array => {
|
|
186
|
+
const chunks: Uint8Array[] = [typedPayloadPrefix];
|
|
187
|
+
encodeTypedValue(valueArg, chunks);
|
|
188
|
+
return plugins.buffer.Buffer.concat(chunks);
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Computes the provider-derived SHA-256 identity for SmartDB's version 1
|
|
193
|
+
* fenced replace, exact-user delete, and exact-user ensure payload profiles.
|
|
194
|
+
*/
|
|
195
|
+
export const createSmartDbEffectivePayloadSha256V1 = (
|
|
196
|
+
inputArg: TSmartDbEffectivePayloadInputV1,
|
|
197
|
+
): string => {
|
|
198
|
+
if (inputArg.version !== 1) {
|
|
199
|
+
throw new TypeError('SmartDB effective payload version must be 1');
|
|
200
|
+
}
|
|
201
|
+
if (typeof inputArg.databaseName !== 'string') {
|
|
202
|
+
throw new TypeError('SmartDB effective payload databaseName must be a string');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
let effectivePayload: object;
|
|
206
|
+
let useTypedEncoding = false;
|
|
207
|
+
switch (inputArg.kind) {
|
|
208
|
+
case 'smartdb.database.replace.v1':
|
|
209
|
+
if (
|
|
210
|
+
inputArg.username !== undefined &&
|
|
211
|
+
typeof inputArg.username !== 'string'
|
|
212
|
+
) {
|
|
213
|
+
throw new TypeError('SmartDB exact replace username must be a string');
|
|
214
|
+
}
|
|
215
|
+
effectivePayload = {
|
|
216
|
+
version: 1,
|
|
217
|
+
kind: inputArg.kind,
|
|
218
|
+
databaseName: inputArg.databaseName,
|
|
219
|
+
source: inputArg.source,
|
|
220
|
+
...(inputArg.username === undefined
|
|
221
|
+
? {}
|
|
222
|
+
: { username: inputArg.username }),
|
|
223
|
+
};
|
|
224
|
+
useTypedEncoding = inputArg.username !== undefined;
|
|
225
|
+
break;
|
|
226
|
+
case 'smartdb.database.ensure.v1': {
|
|
227
|
+
if (typeof inputArg.username !== 'string') {
|
|
228
|
+
throw new TypeError('SmartDB exact ensure username must be a string');
|
|
229
|
+
}
|
|
230
|
+
if (typeof inputArg.password !== 'string') {
|
|
231
|
+
throw new TypeError('SmartDB exact ensure password must be a string');
|
|
232
|
+
}
|
|
233
|
+
const roles = inputArg.roles ?? ['readWrite', 'dbAdmin'];
|
|
234
|
+
if (
|
|
235
|
+
!Array.isArray(roles) ||
|
|
236
|
+
roles.some(
|
|
237
|
+
(roleArg) => typeof roleArg !== 'string' || roleArg.length === 0,
|
|
238
|
+
)
|
|
239
|
+
) {
|
|
240
|
+
throw new TypeError(
|
|
241
|
+
'SmartDB exact ensure roles must be an array of non-empty strings',
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
const canonicalRoles = [...new Set(roles)].sort(compareUtf8Keys);
|
|
245
|
+
effectivePayload = {
|
|
246
|
+
version: 1,
|
|
247
|
+
kind: inputArg.kind,
|
|
248
|
+
databaseName: inputArg.databaseName,
|
|
249
|
+
username: inputArg.username,
|
|
250
|
+
passwordSha256: plugins.crypto
|
|
251
|
+
.createHash('sha256')
|
|
252
|
+
.update(inputArg.password, 'utf8')
|
|
253
|
+
.digest('hex'),
|
|
254
|
+
roles: canonicalRoles,
|
|
255
|
+
};
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case 'smartdb.database.delete.v1':
|
|
259
|
+
if (typeof inputArg.username !== 'string') {
|
|
260
|
+
throw new TypeError('SmartDB exact delete username must be a string');
|
|
261
|
+
}
|
|
262
|
+
effectivePayload = {
|
|
263
|
+
version: 1,
|
|
264
|
+
kind: inputArg.kind,
|
|
265
|
+
databaseName: inputArg.databaseName,
|
|
266
|
+
username: inputArg.username,
|
|
267
|
+
rawDatabaseOnly: false,
|
|
268
|
+
};
|
|
269
|
+
break;
|
|
270
|
+
default:
|
|
271
|
+
throw new TypeError('SmartDB mutation kind has no version 1 payload profile');
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const transportJson = JSON.stringify(effectivePayload);
|
|
275
|
+
if (transportJson === undefined) {
|
|
276
|
+
throw new TypeError('SmartDB effective payload cannot be encoded as JSON');
|
|
277
|
+
}
|
|
278
|
+
const normalizedPayload = JSON.parse(transportJson) as TCanonicalJsonValue;
|
|
279
|
+
const encodedPayload = useTypedEncoding
|
|
280
|
+
? encodeTypedPayload(normalizedPayload)
|
|
281
|
+
: canonicalJsonStringify(normalizedPayload);
|
|
282
|
+
return plugins.crypto
|
|
283
|
+
.createHash('sha256')
|
|
284
|
+
.update(encodedPayload)
|
|
285
|
+
.digest('hex');
|
|
286
|
+
};
|
|
@@ -6,6 +6,8 @@ import { normalizeSmartDbResourceFenceError } from './service-types.js';
|
|
|
6
6
|
import type {
|
|
7
7
|
ISmartDbHealth,
|
|
8
8
|
ISmartDbDatabaseTenantInput,
|
|
9
|
+
ISmartDbEnsureDatabaseTenantInput,
|
|
10
|
+
ISmartDbEnsureDatabaseTenantResult,
|
|
9
11
|
ISmartDbDeleteDatabaseTenantInput,
|
|
10
12
|
ISmartDbRotateDatabaseTenantPasswordInput,
|
|
11
13
|
ISmartDbDatabaseTenantDescriptor,
|
|
@@ -18,6 +20,8 @@ import type {
|
|
|
18
20
|
export type {
|
|
19
21
|
ISmartDbHealth,
|
|
20
22
|
ISmartDbDatabaseTenantInput,
|
|
23
|
+
ISmartDbEnsureDatabaseTenantInput,
|
|
24
|
+
ISmartDbEnsureDatabaseTenantResult,
|
|
21
25
|
ISmartDbDeleteDatabaseTenantInput,
|
|
22
26
|
ISmartDbRotateDatabaseTenantPasswordInput,
|
|
23
27
|
ISmartDbDatabaseTenantDescriptor,
|
|
@@ -123,6 +127,10 @@ type TSmartDbCommands = {
|
|
|
123
127
|
params: ISmartDbDatabaseTenantInput;
|
|
124
128
|
result: ISmartDbDatabaseTenantDescriptor;
|
|
125
129
|
};
|
|
130
|
+
ensureDatabaseTenant: {
|
|
131
|
+
params: ISmartDbEnsureDatabaseTenantInput;
|
|
132
|
+
result: ISmartDbEnsureDatabaseTenantResult;
|
|
133
|
+
};
|
|
126
134
|
deleteDatabaseTenant: {
|
|
127
135
|
params: ISmartDbDeleteDatabaseTenantInput;
|
|
128
136
|
result: ISmartDbDeleteDatabaseTenantResult;
|
|
@@ -246,6 +254,14 @@ function buildLocalPaths(): string[] {
|
|
|
246
254
|
|
|
247
255
|
return paths;
|
|
248
256
|
}
|
|
257
|
+
export function getRustDbAllocatorEnv(
|
|
258
|
+
environment: NodeJS.ProcessEnv = process.env,
|
|
259
|
+
): Record<string, string> {
|
|
260
|
+
return {
|
|
261
|
+
MIMALLOC_ARENA_EAGER_COMMIT: environment.MIMALLOC_ARENA_EAGER_COMMIT ?? '0',
|
|
262
|
+
MIMALLOC_ALLOW_LARGE_OS_PAGES: environment.MIMALLOC_ALLOW_LARGE_OS_PAGES ?? '0',
|
|
263
|
+
};
|
|
264
|
+
}
|
|
249
265
|
|
|
250
266
|
/**
|
|
251
267
|
* Bridge between TypeScript SmartdbServer and the Rust binary.
|
|
@@ -263,6 +279,7 @@ export class RustDbBridge extends EventEmitter {
|
|
|
263
279
|
platformPackagePrefix: '@push.rocks/smartdb',
|
|
264
280
|
localPaths: buildLocalPaths(),
|
|
265
281
|
maxPayloadSize: 100 * 1024 * 1024, // database exports/imports can be larger than command replies
|
|
282
|
+
env: getRustDbAllocatorEnv(),
|
|
266
283
|
});
|
|
267
284
|
|
|
268
285
|
// Forward events from the inner bridge
|
|
@@ -318,7 +335,21 @@ export class RustDbBridge extends EventEmitter {
|
|
|
318
335
|
public async createDatabaseTenant(
|
|
319
336
|
params: ISmartDbDatabaseTenantInput,
|
|
320
337
|
): Promise<ISmartDbDatabaseTenantDescriptor> {
|
|
321
|
-
|
|
338
|
+
try {
|
|
339
|
+
return await this.bridge.sendCommand('createDatabaseTenant', params) as ISmartDbDatabaseTenantDescriptor;
|
|
340
|
+
} catch (error) {
|
|
341
|
+
throw normalizeSmartDbResourceFenceError(error);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
public async ensureDatabaseTenant(
|
|
346
|
+
params: ISmartDbEnsureDatabaseTenantInput,
|
|
347
|
+
): Promise<ISmartDbEnsureDatabaseTenantResult> {
|
|
348
|
+
try {
|
|
349
|
+
return await this.bridge.sendCommand('ensureDatabaseTenant', params) as ISmartDbEnsureDatabaseTenantResult;
|
|
350
|
+
} catch (error) {
|
|
351
|
+
throw normalizeSmartDbResourceFenceError(error);
|
|
352
|
+
}
|
|
322
353
|
}
|
|
323
354
|
|
|
324
355
|
public async deleteDatabaseTenant(
|
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
import type {
|
|
13
13
|
ISmartDbHealth,
|
|
14
14
|
ISmartDbDatabaseTenantInput,
|
|
15
|
+
ISmartDbEnsureDatabaseTenantInput,
|
|
16
|
+
ISmartDbEnsureDatabaseTenantResult,
|
|
15
17
|
ISmartDbDeleteDatabaseTenantInput,
|
|
16
18
|
ISmartDbRotateDatabaseTenantPasswordInput,
|
|
17
19
|
ISmartDbDatabaseTenantDescriptor,
|
|
@@ -236,6 +238,16 @@ export class SmartdbServer {
|
|
|
236
238
|
return this.withTenantMongoUri(descriptor, params.password);
|
|
237
239
|
}
|
|
238
240
|
|
|
241
|
+
/**
|
|
242
|
+
* Idempotently provision or rotate one exclusively owned database tenant.
|
|
243
|
+
*/
|
|
244
|
+
async ensureDatabaseTenant(
|
|
245
|
+
params: ISmartDbEnsureDatabaseTenantInput,
|
|
246
|
+
): Promise<ISmartDbEnsureDatabaseTenantResult> {
|
|
247
|
+
const descriptor = await this.bridge.ensureDatabaseTenant(params);
|
|
248
|
+
return this.withTenantMongoUri(descriptor, params.password);
|
|
249
|
+
}
|
|
250
|
+
|
|
239
251
|
/**
|
|
240
252
|
* Delete a tenant database and its tenant user(s).
|
|
241
253
|
*/
|
|
@@ -363,10 +375,10 @@ export class SmartdbServer {
|
|
|
363
375
|
return this.bridge.getMetrics();
|
|
364
376
|
}
|
|
365
377
|
|
|
366
|
-
private withTenantMongoUri(
|
|
367
|
-
descriptor:
|
|
378
|
+
private withTenantMongoUri<TDescriptor extends ISmartDbDatabaseTenantDescriptor>(
|
|
379
|
+
descriptor: TDescriptor,
|
|
368
380
|
password: string,
|
|
369
|
-
):
|
|
381
|
+
): TDescriptor & { mongodbUri: string } {
|
|
370
382
|
return {
|
|
371
383
|
...descriptor,
|
|
372
384
|
mongodbUri: this.buildTenantMongoUri(descriptor.databaseName, descriptor.username, password),
|
|
@@ -19,6 +19,7 @@ export const smartDbResourceFenceErrorCodes = [
|
|
|
19
19
|
'EFENCE_SCOPE_MISMATCH',
|
|
20
20
|
'EFENCE_UNSUPPORTED',
|
|
21
21
|
'EFENCE_MUTATION_MISMATCH',
|
|
22
|
+
'EFENCE_OWNERSHIP_MISMATCH',
|
|
22
23
|
'EFENCE_STATE_CORRUPT',
|
|
23
24
|
'EFENCE_RECEIPT_LIMIT',
|
|
24
25
|
'EFENCE_BUSY',
|
|
@@ -39,7 +40,8 @@ export interface ISmartDbResourceFenceReceipt
|
|
|
39
40
|
extends ISmartDbResourceFence {
|
|
40
41
|
kind:
|
|
41
42
|
| 'smartdb.database.replace.v1'
|
|
42
|
-
| 'smartdb.database.delete.v1'
|
|
43
|
+
| 'smartdb.database.delete.v1'
|
|
44
|
+
| 'smartdb.database.ensure.v1';
|
|
43
45
|
/** Digest of the canonical payload interpretation derived by SmartDB. */
|
|
44
46
|
effectivePayloadSha256: string;
|
|
45
47
|
}
|
|
@@ -88,6 +90,11 @@ export interface ISmartDbDatabaseTenantInput {
|
|
|
88
90
|
roles?: string[];
|
|
89
91
|
}
|
|
90
92
|
|
|
93
|
+
export interface ISmartDbEnsureDatabaseTenantInput
|
|
94
|
+
extends ISmartDbDatabaseTenantInput {
|
|
95
|
+
fence: ISmartDbResourceFence;
|
|
96
|
+
}
|
|
97
|
+
|
|
91
98
|
export interface ISmartDbDeleteDatabaseTenantInput {
|
|
92
99
|
databaseName: string;
|
|
93
100
|
/**
|
|
@@ -95,7 +102,8 @@ export interface ISmartDbDeleteDatabaseTenantInput {
|
|
|
95
102
|
* included in the durable mutation and is removed idempotently after the
|
|
96
103
|
* database deletion is durable. Omitting it from a fenced deletion
|
|
97
104
|
* preserves auth users; an unfenced omission retains the legacy behavior
|
|
98
|
-
* of deleting every user for the database.
|
|
105
|
+
* of deleting every user for the database. If the database exists, a
|
|
106
|
+
* fenced deletion fails closed unless this exact user currently owns it.
|
|
99
107
|
*/
|
|
100
108
|
username?: string;
|
|
101
109
|
fence?: ISmartDbResourceFence;
|
|
@@ -112,6 +120,12 @@ export interface ISmartDbDatabaseTenantDescriptor {
|
|
|
112
120
|
roles: string[];
|
|
113
121
|
authSource: string;
|
|
114
122
|
mongodbUri?: string;
|
|
123
|
+
resourceFence?: ISmartDbResourceFenceReceipt;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ISmartDbEnsureDatabaseTenantResult
|
|
127
|
+
extends ISmartDbDatabaseTenantDescriptor {
|
|
128
|
+
resourceFence: ISmartDbResourceFenceReceipt;
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
export interface ISmartDbDeleteDatabaseTenantResult {
|
|
@@ -147,6 +161,12 @@ export interface ISmartDbDatabaseExport {
|
|
|
147
161
|
export interface ISmartDbImportDatabaseInput {
|
|
148
162
|
databaseName: string;
|
|
149
163
|
source: ISmartDbDatabaseExport;
|
|
164
|
+
/**
|
|
165
|
+
* Exact exclusive tenant owner required for this replacement. When used
|
|
166
|
+
* with a fence, the username is included in the provider-derived mutation
|
|
167
|
+
* identity. Omission preserves the original version 1 replacement profile.
|
|
168
|
+
*/
|
|
169
|
+
username?: string;
|
|
150
170
|
fence?: ISmartDbResourceFence;
|
|
151
171
|
}
|
|
152
172
|
|