@push.rocks/smartsecret 1.0.2 → 1.2.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/.smartconfig.json +55 -0
- package/dist_rust/smartsecret-kernel_linux_amd64 +0 -0
- package/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/index.d.ts +7 -0
- package/dist_ts/index.js +5 -1
- package/dist_ts/smartsecret.kernel.error.d.ts +17 -0
- package/dist_ts/smartsecret.kernel.error.js +47 -0
- package/dist_ts/smartsecret.kernel.protocol.d.ts +159 -0
- package/dist_ts/smartsecret.kernel.protocol.js +424 -0
- package/dist_ts/smartsecret.kernelstore.d.ts +49 -0
- package/dist_ts/smartsecret.kernelstore.js +442 -0
- package/dist_ts/smartsecret.keyring.d.ts +81 -0
- package/dist_ts/smartsecret.keyring.error.d.ts +17 -0
- package/dist_ts/smartsecret.keyring.error.js +46 -0
- package/dist_ts/smartsecret.keyring.js +758 -0
- package/dist_ts/smartsecret.keyring.protocol.d.ts +57 -0
- package/dist_ts/smartsecret.keyring.protocol.js +290 -0
- package/dist_ts/smartsecret.plugins.d.ts +5 -1
- package/dist_ts/smartsecret.plugins.js +7 -2
- package/package.json +23 -15
- package/readme.hints.md +19 -1
- package/readme.md +182 -4
- package/third-party-notices.md +177 -0
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/index.ts +16 -0
- package/ts/smartsecret.kernel.error.ts +78 -0
- package/ts/smartsecret.kernel.protocol.ts +657 -0
- package/ts/smartsecret.kernelstore.ts +664 -0
- package/ts/smartsecret.keyring.error.ts +76 -0
- package/ts/smartsecret.keyring.protocol.ts +377 -0
- package/ts/smartsecret.keyring.ts +955 -0
- package/ts/smartsecret.plugins.ts +7 -1
- package/npmextra.json +0 -44
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import * as plugins from './smartsecret.plugins.js';
|
|
2
|
+
import {
|
|
3
|
+
SmartSecretKernelStoreError,
|
|
4
|
+
createSmartSecretKernelStoreError,
|
|
5
|
+
} from './smartsecret.kernel.error.js';
|
|
6
|
+
import {
|
|
7
|
+
deriveSmartSecretKernelEntryDescription,
|
|
8
|
+
deriveSmartSecretKernelMutexNamespace,
|
|
9
|
+
deriveSmartSecretKernelServiceDescription,
|
|
10
|
+
encodeCanonicalBase64Url,
|
|
11
|
+
normalizeDevIdpV1LegacyAccount,
|
|
12
|
+
normalizeSmartSecretKernelAccount,
|
|
13
|
+
normalizeSmartSecretKernelOperationOptions,
|
|
14
|
+
normalizeSmartSecretKernelStoreOptions,
|
|
15
|
+
normalizeSmartSecretKernelValue,
|
|
16
|
+
parseSmartSecretKernelDeleteResult,
|
|
17
|
+
parseSmartSecretKernelGetResult,
|
|
18
|
+
parseSmartSecretKernelLegacyDeleteResult,
|
|
19
|
+
parseSmartSecretKernelLegacyReadResult,
|
|
20
|
+
parseSmartSecretKernelSetResult,
|
|
21
|
+
smartSecretKernelDefaultTimeoutMs,
|
|
22
|
+
smartSecretKernelLegacyService,
|
|
23
|
+
smartSecretKernelMaximumMessageBytes,
|
|
24
|
+
type INormalizedSmartSecretKernelOperationOptions,
|
|
25
|
+
type ISmartSecretKernelDomainErrorResult,
|
|
26
|
+
type ISmartSecretKernelLegacyDeleteParams,
|
|
27
|
+
type ISmartSecretKernelOperationOptions,
|
|
28
|
+
type ISmartSecretKernelStoreOptions,
|
|
29
|
+
type TSmartSecretKernelCommands,
|
|
30
|
+
} from './smartsecret.kernel.protocol.js';
|
|
31
|
+
|
|
32
|
+
export type {
|
|
33
|
+
ISmartSecretKernelOperationOptions,
|
|
34
|
+
ISmartSecretKernelStoreOptions,
|
|
35
|
+
} from './smartsecret.kernel.protocol.js';
|
|
36
|
+
|
|
37
|
+
const legacyReceiptBrand = Symbol('SmartSecretDevIdpV1LegacyReceipt');
|
|
38
|
+
const kernelStoreConstructionToken = Symbol('SmartSecretKernelStoreConstruction');
|
|
39
|
+
|
|
40
|
+
export type TDevIdpV1LegacyReceipt = {
|
|
41
|
+
readonly [legacyReceiptBrand]: true;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export interface IDevIdpV1LegacyRead {
|
|
45
|
+
readonly value: Uint8Array;
|
|
46
|
+
readonly receipt: TDevIdpV1LegacyReceipt;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type TStoreLifecycleState = 'ready' | 'closing' | 'closed';
|
|
50
|
+
type TReceiptState = 'available' | 'deleting' | 'consumed';
|
|
51
|
+
|
|
52
|
+
interface IOperationContext {
|
|
53
|
+
readonly deadline: number;
|
|
54
|
+
readonly signal?: AbortSignal;
|
|
55
|
+
readonly mutation: boolean;
|
|
56
|
+
dispatched: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface IBridgeRequestOptions {
|
|
60
|
+
readonly timeoutMs: number;
|
|
61
|
+
readonly signal?: AbortSignal;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface ILegacyReceiptRecord {
|
|
65
|
+
readonly generation: symbol;
|
|
66
|
+
readonly params: ISmartSecretKernelLegacyDeleteParams;
|
|
67
|
+
state: TReceiptState;
|
|
68
|
+
deletionPromise?: Promise<'deleted' | 'alreadyAbsent'>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface IDeferred<T> {
|
|
72
|
+
readonly promise: Promise<T>;
|
|
73
|
+
readonly resolve: (valueArg: T | PromiseLike<T>) => void;
|
|
74
|
+
readonly reject: (reasonArg?: unknown) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
type TOperationOutcome<T> =
|
|
78
|
+
| { succeeded: true; value: T }
|
|
79
|
+
| { succeeded: false; error: SmartSecretKernelStoreError };
|
|
80
|
+
|
|
81
|
+
const createDeferred = <T>(): IDeferred<T> => {
|
|
82
|
+
let resolve: (valueArg: T | PromiseLike<T>) => void = () => {};
|
|
83
|
+
let reject: (reasonArg?: unknown) => void = () => {};
|
|
84
|
+
const promise = new Promise<T>((resolveArg, rejectArg) => {
|
|
85
|
+
resolve = resolveArg;
|
|
86
|
+
reject = rejectArg;
|
|
87
|
+
});
|
|
88
|
+
return { promise, resolve, reject };
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const createOperationContext = (
|
|
92
|
+
optionsArg: INormalizedSmartSecretKernelOperationOptions,
|
|
93
|
+
mutationArg: boolean,
|
|
94
|
+
): IOperationContext => ({
|
|
95
|
+
deadline: performance.now() + optionsArg.timeoutMs,
|
|
96
|
+
...(optionsArg.signal === undefined ? {} : { signal: optionsArg.signal }),
|
|
97
|
+
mutation: mutationArg,
|
|
98
|
+
dispatched: false,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const isSupportedRuntime = (): boolean => {
|
|
102
|
+
const versionParts = process.versions.node.split('.');
|
|
103
|
+
return (
|
|
104
|
+
versionParts[0] === '25'
|
|
105
|
+
&& process.platform === 'linux'
|
|
106
|
+
&& process.arch === 'x64'
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Strict Linux kernel-keyring storage for small same-UID process secrets.
|
|
112
|
+
*
|
|
113
|
+
* This class deliberately has no filesystem, Secret Service, or executable
|
|
114
|
+
* discovery fallback. Use create() so the Rust worker is ready before access.
|
|
115
|
+
*/
|
|
116
|
+
export class SmartSecretKernelStore {
|
|
117
|
+
public readonly service: string;
|
|
118
|
+
|
|
119
|
+
private readonly mutex: plugins.smartipc.NamedMutex;
|
|
120
|
+
private readonly bridge: plugins.smartrust.RustBridge<TSmartSecretKernelCommands>;
|
|
121
|
+
private readonly generation = Symbol('SmartSecretKernelStoreGeneration');
|
|
122
|
+
private readonly receiptRecords = new WeakMap<
|
|
123
|
+
TDevIdpV1LegacyReceipt,
|
|
124
|
+
ILegacyReceiptRecord
|
|
125
|
+
>();
|
|
126
|
+
|
|
127
|
+
private lifecycleState: TStoreLifecycleState = 'ready';
|
|
128
|
+
private poisoned = false;
|
|
129
|
+
private operationTail: Promise<void> = Promise.resolve();
|
|
130
|
+
private closePromise: Promise<void> | undefined;
|
|
131
|
+
private terminationPromise: Promise<boolean> | undefined;
|
|
132
|
+
|
|
133
|
+
private constructor(
|
|
134
|
+
constructionTokenArg: symbol,
|
|
135
|
+
serviceArg: string,
|
|
136
|
+
mutexArg: plugins.smartipc.NamedMutex,
|
|
137
|
+
bridgeArg: plugins.smartrust.RustBridge<TSmartSecretKernelCommands>,
|
|
138
|
+
) {
|
|
139
|
+
if (constructionTokenArg !== kernelStoreConstructionToken) {
|
|
140
|
+
throw createSmartSecretKernelStoreError('INVALID_ARGUMENT');
|
|
141
|
+
}
|
|
142
|
+
this.service = serviceArg;
|
|
143
|
+
this.mutex = mutexArg;
|
|
144
|
+
this.bridge = bridgeArg;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
public static async create(
|
|
148
|
+
optionsArg: ISmartSecretKernelStoreOptions,
|
|
149
|
+
): Promise<SmartSecretKernelStore> {
|
|
150
|
+
const options = normalizeSmartSecretKernelStoreOptions(optionsArg);
|
|
151
|
+
if (!isSupportedRuntime()) {
|
|
152
|
+
throw createSmartSecretKernelStoreError('UNSUPPORTED_RUNTIME');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const serviceDescription = deriveSmartSecretKernelServiceDescription(options.service);
|
|
156
|
+
let mutex: plugins.smartipc.NamedMutex;
|
|
157
|
+
try {
|
|
158
|
+
mutex = new plugins.smartipc.NamedMutex(
|
|
159
|
+
deriveSmartSecretKernelMutexNamespace(options.service),
|
|
160
|
+
);
|
|
161
|
+
} catch {
|
|
162
|
+
throw createSmartSecretKernelStoreError('MUTEX_FAILED');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const packageRoot = plugins.path.resolve(
|
|
166
|
+
plugins.path.dirname(plugins.url.fileURLToPath(import.meta.url)),
|
|
167
|
+
'..',
|
|
168
|
+
);
|
|
169
|
+
const binaryPath = plugins.path.join(
|
|
170
|
+
packageRoot,
|
|
171
|
+
'dist_rust',
|
|
172
|
+
'smartsecret-kernel_linux_amd64',
|
|
173
|
+
);
|
|
174
|
+
const bridge = new plugins.smartrust.RustBridge<TSmartSecretKernelCommands>({
|
|
175
|
+
binaryName: 'smartsecret-kernel',
|
|
176
|
+
binaryPath,
|
|
177
|
+
localPaths: [],
|
|
178
|
+
searchSystemPath: false,
|
|
179
|
+
cliArgs: ['--service-ring-description', serviceDescription],
|
|
180
|
+
requestTimeoutMs: smartSecretKernelDefaultTimeoutMs,
|
|
181
|
+
readyTimeoutMs: smartSecretKernelDefaultTimeoutMs,
|
|
182
|
+
maxPayloadSize: smartSecretKernelMaximumMessageBytes,
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
let spawned = false;
|
|
186
|
+
try {
|
|
187
|
+
spawned = await bridge.spawn();
|
|
188
|
+
} catch {
|
|
189
|
+
spawned = false;
|
|
190
|
+
}
|
|
191
|
+
if (!spawned) {
|
|
192
|
+
let cleanupConfirmed = true;
|
|
193
|
+
try {
|
|
194
|
+
await bridge.terminate({ gracePeriodMs: 0 });
|
|
195
|
+
} catch {
|
|
196
|
+
cleanupConfirmed = false;
|
|
197
|
+
}
|
|
198
|
+
throw createSmartSecretKernelStoreError(
|
|
199
|
+
cleanupConfirmed ? 'WORKER_UNAVAILABLE' : 'WORKER_INTEGRITY_FAILED',
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return new SmartSecretKernelStore(
|
|
204
|
+
kernelStoreConstructionToken,
|
|
205
|
+
options.service,
|
|
206
|
+
mutex,
|
|
207
|
+
bridge,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public getEntry(
|
|
212
|
+
accountArg: string,
|
|
213
|
+
optionsArg?: ISmartSecretKernelOperationOptions,
|
|
214
|
+
): Promise<Uint8Array | null> {
|
|
215
|
+
const account = normalizeSmartSecretKernelAccount(accountArg);
|
|
216
|
+
const options = normalizeSmartSecretKernelOperationOptions(optionsArg);
|
|
217
|
+
const context = createOperationContext(options, false);
|
|
218
|
+
const description = deriveSmartSecretKernelEntryDescription(this.service, account);
|
|
219
|
+
return this.reserveOperation(context, () => this.withKernelLease(context, async () => {
|
|
220
|
+
const parsed = await this.dispatchRead(context, async (requestOptionsArg) =>
|
|
221
|
+
parseSmartSecretKernelGetResult(await this.bridge.sendCommand(
|
|
222
|
+
'getEntry',
|
|
223
|
+
{ description },
|
|
224
|
+
requestOptionsArg,
|
|
225
|
+
))
|
|
226
|
+
);
|
|
227
|
+
if (parsed.kind === 'domainError') {
|
|
228
|
+
return this.throwDomainError(parsed.domainError, context);
|
|
229
|
+
}
|
|
230
|
+
return parsed.found ? parsed.value : null;
|
|
231
|
+
}));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
public setEntry(
|
|
235
|
+
accountArg: string,
|
|
236
|
+
valueArg: Uint8Array,
|
|
237
|
+
optionsArg?: ISmartSecretKernelOperationOptions,
|
|
238
|
+
): Promise<void> {
|
|
239
|
+
const account = normalizeSmartSecretKernelAccount(accountArg);
|
|
240
|
+
const options = normalizeSmartSecretKernelOperationOptions(optionsArg);
|
|
241
|
+
const context = createOperationContext(options, true);
|
|
242
|
+
const description = deriveSmartSecretKernelEntryDescription(this.service, account);
|
|
243
|
+
const value = normalizeSmartSecretKernelValue(valueArg);
|
|
244
|
+
let encodedValue: string;
|
|
245
|
+
try {
|
|
246
|
+
encodedValue = encodeCanonicalBase64Url(value);
|
|
247
|
+
} finally {
|
|
248
|
+
value.fill(0);
|
|
249
|
+
}
|
|
250
|
+
return this.reserveOperation(context, () => this.withKernelLease(context, async () => {
|
|
251
|
+
const parsed = await this.dispatchMutation(context, async (requestOptionsArg) =>
|
|
252
|
+
parseSmartSecretKernelSetResult(await this.bridge.sendCommand(
|
|
253
|
+
'setEntry',
|
|
254
|
+
{ description, value: encodedValue },
|
|
255
|
+
requestOptionsArg,
|
|
256
|
+
))
|
|
257
|
+
);
|
|
258
|
+
if (parsed.kind === 'domainError') {
|
|
259
|
+
return this.throwDomainError(parsed.domainError, context);
|
|
260
|
+
}
|
|
261
|
+
}));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public deleteEntry(
|
|
265
|
+
accountArg: string,
|
|
266
|
+
optionsArg?: ISmartSecretKernelOperationOptions,
|
|
267
|
+
): Promise<boolean> {
|
|
268
|
+
const account = normalizeSmartSecretKernelAccount(accountArg);
|
|
269
|
+
const options = normalizeSmartSecretKernelOperationOptions(optionsArg);
|
|
270
|
+
const context = createOperationContext(options, true);
|
|
271
|
+
const description = deriveSmartSecretKernelEntryDescription(this.service, account);
|
|
272
|
+
return this.reserveOperation(context, () => this.withKernelLease(context, async () => {
|
|
273
|
+
const parsed = await this.dispatchMutation(context, async (requestOptionsArg) =>
|
|
274
|
+
parseSmartSecretKernelDeleteResult(await this.bridge.sendCommand(
|
|
275
|
+
'deleteEntry',
|
|
276
|
+
{ description },
|
|
277
|
+
requestOptionsArg,
|
|
278
|
+
))
|
|
279
|
+
);
|
|
280
|
+
if (parsed.kind === 'domainError') {
|
|
281
|
+
return this.throwDomainError(parsed.domainError, context);
|
|
282
|
+
}
|
|
283
|
+
return parsed.deleted;
|
|
284
|
+
}));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
public readDevIdpV1Legacy(
|
|
288
|
+
accountArg: string,
|
|
289
|
+
optionsArg?: ISmartSecretKernelOperationOptions,
|
|
290
|
+
): Promise<IDevIdpV1LegacyRead | null> {
|
|
291
|
+
this.requireLegacyService();
|
|
292
|
+
const account = normalizeDevIdpV1LegacyAccount(accountArg);
|
|
293
|
+
const options = normalizeSmartSecretKernelOperationOptions(optionsArg);
|
|
294
|
+
const context = createOperationContext(options, false);
|
|
295
|
+
return this.reserveOperation(context, () => this.withKernelLease(context, async () => {
|
|
296
|
+
const parsed = await this.dispatchRead(context, async (requestOptionsArg) =>
|
|
297
|
+
parseSmartSecretKernelLegacyReadResult(await this.bridge.sendCommand(
|
|
298
|
+
'readDevIdpV1Legacy',
|
|
299
|
+
{ account },
|
|
300
|
+
requestOptionsArg,
|
|
301
|
+
), account)
|
|
302
|
+
);
|
|
303
|
+
if (parsed.kind === 'domainError') {
|
|
304
|
+
return this.throwDomainError(parsed.domainError, context);
|
|
305
|
+
}
|
|
306
|
+
if (!parsed.found) return null;
|
|
307
|
+
|
|
308
|
+
const receipt: TDevIdpV1LegacyReceipt = Object.freeze({
|
|
309
|
+
[legacyReceiptBrand]: true,
|
|
310
|
+
});
|
|
311
|
+
this.receiptRecords.set(receipt, {
|
|
312
|
+
generation: this.generation,
|
|
313
|
+
params: Object.freeze(parsed.receiptParams),
|
|
314
|
+
state: 'available',
|
|
315
|
+
});
|
|
316
|
+
return Object.freeze({ value: parsed.value, receipt });
|
|
317
|
+
}));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
public deleteDevIdpV1Legacy(
|
|
321
|
+
receiptArg: TDevIdpV1LegacyReceipt,
|
|
322
|
+
optionsArg?: ISmartSecretKernelOperationOptions,
|
|
323
|
+
): Promise<'deleted' | 'alreadyAbsent'> {
|
|
324
|
+
this.requireLegacyService();
|
|
325
|
+
const options = normalizeSmartSecretKernelOperationOptions(optionsArg);
|
|
326
|
+
const record = this.requireReceiptRecord(receiptArg);
|
|
327
|
+
if (record.state === 'deleting') {
|
|
328
|
+
if (record.deletionPromise) return record.deletionPromise;
|
|
329
|
+
return Promise.reject(createSmartSecretKernelStoreError('RECEIPT_INVALID'));
|
|
330
|
+
}
|
|
331
|
+
if (record.state === 'consumed') {
|
|
332
|
+
return Promise.reject(createSmartSecretKernelStoreError('RECEIPT_INVALID'));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
this.assertCanReserve();
|
|
336
|
+
const context = createOperationContext(options, true);
|
|
337
|
+
record.state = 'deleting';
|
|
338
|
+
const operationPromise = this.reserveOperation(context, () =>
|
|
339
|
+
this.withKernelLease(context, async () => {
|
|
340
|
+
const parsed = await this.dispatchMutation(
|
|
341
|
+
context,
|
|
342
|
+
async (requestOptionsArg) => parseSmartSecretKernelLegacyDeleteResult(
|
|
343
|
+
await this.bridge.sendCommand(
|
|
344
|
+
'deleteDevIdpV1Legacy',
|
|
345
|
+
record.params,
|
|
346
|
+
requestOptionsArg,
|
|
347
|
+
),
|
|
348
|
+
),
|
|
349
|
+
);
|
|
350
|
+
if (parsed.kind === 'domainError') {
|
|
351
|
+
return this.throwDomainError(parsed.domainError, context);
|
|
352
|
+
}
|
|
353
|
+
return parsed.status;
|
|
354
|
+
}),
|
|
355
|
+
);
|
|
356
|
+
const trackedPromise = operationPromise.then(
|
|
357
|
+
(statusArg) => {
|
|
358
|
+
record.state = 'consumed';
|
|
359
|
+
return statusArg;
|
|
360
|
+
},
|
|
361
|
+
(errorArg: unknown) => {
|
|
362
|
+
const publicError = errorArg instanceof SmartSecretKernelStoreError
|
|
363
|
+
? errorArg
|
|
364
|
+
: createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
365
|
+
if (
|
|
366
|
+
!context.dispatched
|
|
367
|
+
&& (
|
|
368
|
+
publicError.code === 'OPERATION_ABORTED'
|
|
369
|
+
|| publicError.code === 'OPERATION_TIMEOUT'
|
|
370
|
+
)
|
|
371
|
+
) {
|
|
372
|
+
record.state = 'available';
|
|
373
|
+
record.deletionPromise = undefined;
|
|
374
|
+
} else {
|
|
375
|
+
record.state = 'consumed';
|
|
376
|
+
}
|
|
377
|
+
throw publicError;
|
|
378
|
+
},
|
|
379
|
+
);
|
|
380
|
+
record.deletionPromise = trackedPromise;
|
|
381
|
+
return trackedPromise;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
public close(): Promise<void> {
|
|
385
|
+
if (this.closePromise) return this.closePromise;
|
|
386
|
+
this.lifecycleState = 'closing';
|
|
387
|
+
const operationTail = this.operationTail;
|
|
388
|
+
this.closePromise = (async (): Promise<void> => {
|
|
389
|
+
await operationTail;
|
|
390
|
+
const terminated = await this.terminateBridgeConfirmed();
|
|
391
|
+
this.lifecycleState = 'closed';
|
|
392
|
+
if (!terminated) {
|
|
393
|
+
this.poisoned = true;
|
|
394
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
395
|
+
}
|
|
396
|
+
})();
|
|
397
|
+
return this.closePromise;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private assertCanReserve(): void {
|
|
401
|
+
if (this.poisoned) {
|
|
402
|
+
throw createSmartSecretKernelStoreError('STORE_POISONED');
|
|
403
|
+
}
|
|
404
|
+
if (this.lifecycleState !== 'ready') {
|
|
405
|
+
throw createSmartSecretKernelStoreError('STORE_CLOSED');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private reserveOperation<T>(
|
|
410
|
+
contextArg: IOperationContext,
|
|
411
|
+
operationArg: () => Promise<T>,
|
|
412
|
+
): Promise<T> {
|
|
413
|
+
this.assertCanReserve();
|
|
414
|
+
|
|
415
|
+
const predecessor = this.operationTail;
|
|
416
|
+
const tailGate = createDeferred<void>();
|
|
417
|
+
const publicResult = createDeferred<T>();
|
|
418
|
+
this.operationTail = tailGate.promise;
|
|
419
|
+
|
|
420
|
+
let queuedFailure: SmartSecretKernelStoreError | undefined;
|
|
421
|
+
let queueTimer: ReturnType<typeof setTimeout> | undefined;
|
|
422
|
+
let abortListener: (() => void) | undefined;
|
|
423
|
+
|
|
424
|
+
const failWhileQueued = (codeArg: 'OPERATION_ABORTED' | 'OPERATION_TIMEOUT'): void => {
|
|
425
|
+
if (queuedFailure) return;
|
|
426
|
+
queuedFailure = createSmartSecretKernelStoreError(codeArg);
|
|
427
|
+
publicResult.reject(queuedFailure);
|
|
428
|
+
};
|
|
429
|
+
const removeQueueWatchers = (): void => {
|
|
430
|
+
if (queueTimer) {
|
|
431
|
+
clearTimeout(queueTimer);
|
|
432
|
+
queueTimer = undefined;
|
|
433
|
+
}
|
|
434
|
+
if (abortListener) {
|
|
435
|
+
contextArg.signal?.removeEventListener('abort', abortListener);
|
|
436
|
+
abortListener = undefined;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
if (contextArg.signal?.aborted) {
|
|
441
|
+
failWhileQueued('OPERATION_ABORTED');
|
|
442
|
+
} else {
|
|
443
|
+
abortListener = () => failWhileQueued('OPERATION_ABORTED');
|
|
444
|
+
contextArg.signal?.addEventListener('abort', abortListener, { once: true });
|
|
445
|
+
queueTimer = setTimeout(
|
|
446
|
+
() => failWhileQueued('OPERATION_TIMEOUT'),
|
|
447
|
+
Math.max(0, contextArg.deadline - performance.now()),
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const execution = predecessor.then(async (): Promise<void> => {
|
|
452
|
+
removeQueueWatchers();
|
|
453
|
+
if (queuedFailure) {
|
|
454
|
+
tailGate.resolve();
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
try {
|
|
458
|
+
if (this.poisoned) {
|
|
459
|
+
throw createSmartSecretKernelStoreError('STORE_POISONED');
|
|
460
|
+
}
|
|
461
|
+
publicResult.resolve(await operationArg());
|
|
462
|
+
} catch (errorArg) {
|
|
463
|
+
if (errorArg instanceof SmartSecretKernelStoreError) {
|
|
464
|
+
publicResult.reject(errorArg);
|
|
465
|
+
} else {
|
|
466
|
+
await this.poisonAndTerminate();
|
|
467
|
+
publicResult.reject(createSmartSecretKernelStoreError(
|
|
468
|
+
contextArg.mutation && contextArg.dispatched
|
|
469
|
+
? 'MUTATION_OUTCOME_UNKNOWN'
|
|
470
|
+
: 'WORKER_INTEGRITY_FAILED',
|
|
471
|
+
));
|
|
472
|
+
}
|
|
473
|
+
} finally {
|
|
474
|
+
tailGate.resolve();
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
void execution.catch(async () => {
|
|
478
|
+
removeQueueWatchers();
|
|
479
|
+
await this.poisonAndTerminate();
|
|
480
|
+
publicResult.reject(createSmartSecretKernelStoreError(
|
|
481
|
+
contextArg.mutation && contextArg.dispatched
|
|
482
|
+
? 'MUTATION_OUTCOME_UNKNOWN'
|
|
483
|
+
: 'WORKER_INTEGRITY_FAILED',
|
|
484
|
+
));
|
|
485
|
+
tailGate.resolve();
|
|
486
|
+
});
|
|
487
|
+
return publicResult.promise;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private requireLegacyService(): void {
|
|
491
|
+
if (this.service !== smartSecretKernelLegacyService) {
|
|
492
|
+
throw createSmartSecretKernelStoreError('INVALID_ARGUMENT');
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
private requireReceiptRecord(
|
|
497
|
+
receiptArg: TDevIdpV1LegacyReceipt,
|
|
498
|
+
): ILegacyReceiptRecord {
|
|
499
|
+
if (receiptArg === null || typeof receiptArg !== 'object') {
|
|
500
|
+
throw createSmartSecretKernelStoreError('RECEIPT_INVALID');
|
|
501
|
+
}
|
|
502
|
+
const record = this.receiptRecords.get(receiptArg);
|
|
503
|
+
if (!record || record.generation !== this.generation) {
|
|
504
|
+
throw createSmartSecretKernelStoreError('RECEIPT_INVALID');
|
|
505
|
+
}
|
|
506
|
+
return record;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
private remainingMilliseconds(contextArg: IOperationContext): number {
|
|
510
|
+
if (contextArg.signal?.aborted) {
|
|
511
|
+
throw createSmartSecretKernelStoreError('OPERATION_ABORTED');
|
|
512
|
+
}
|
|
513
|
+
const remaining = contextArg.deadline - performance.now();
|
|
514
|
+
if (remaining <= 0) {
|
|
515
|
+
throw createSmartSecretKernelStoreError('OPERATION_TIMEOUT');
|
|
516
|
+
}
|
|
517
|
+
return Math.max(1, Math.ceil(remaining));
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private createBridgeRequestOptions(
|
|
521
|
+
contextArg: IOperationContext,
|
|
522
|
+
): IBridgeRequestOptions {
|
|
523
|
+
const timeoutMs = this.remainingMilliseconds(contextArg);
|
|
524
|
+
return {
|
|
525
|
+
timeoutMs,
|
|
526
|
+
...(contextArg.signal === undefined ? {} : { signal: contextArg.signal }),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
private async acquireKernelLease(
|
|
531
|
+
contextArg: IOperationContext,
|
|
532
|
+
): Promise<plugins.smartipc.NamedMutexLease> {
|
|
533
|
+
const timeoutMs = this.remainingMilliseconds(contextArg);
|
|
534
|
+
try {
|
|
535
|
+
return await this.mutex.acquire({
|
|
536
|
+
timeoutMs,
|
|
537
|
+
...(contextArg.signal === undefined ? {} : { signal: contextArg.signal }),
|
|
538
|
+
});
|
|
539
|
+
} catch (errorArg) {
|
|
540
|
+
if (errorArg instanceof plugins.smartipc.NamedMutexError) {
|
|
541
|
+
if (errorArg.code === 'ABORTED') {
|
|
542
|
+
throw createSmartSecretKernelStoreError('OPERATION_ABORTED');
|
|
543
|
+
}
|
|
544
|
+
if (errorArg.code === 'TIMEOUT') {
|
|
545
|
+
throw createSmartSecretKernelStoreError('OPERATION_TIMEOUT');
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
throw createSmartSecretKernelStoreError('MUTEX_FAILED');
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private async withKernelLease<T>(
|
|
553
|
+
contextArg: IOperationContext,
|
|
554
|
+
operationArg: () => Promise<T>,
|
|
555
|
+
): Promise<T> {
|
|
556
|
+
const lease = await this.acquireKernelLease(contextArg);
|
|
557
|
+
let outcome: TOperationOutcome<T>;
|
|
558
|
+
try {
|
|
559
|
+
outcome = { succeeded: true, value: await operationArg() };
|
|
560
|
+
} catch (errorArg) {
|
|
561
|
+
if (errorArg instanceof SmartSecretKernelStoreError) {
|
|
562
|
+
outcome = { succeeded: false, error: errorArg };
|
|
563
|
+
} else {
|
|
564
|
+
await this.poisonAndTerminate();
|
|
565
|
+
outcome = {
|
|
566
|
+
succeeded: false,
|
|
567
|
+
error: createSmartSecretKernelStoreError(
|
|
568
|
+
contextArg.mutation && contextArg.dispatched
|
|
569
|
+
? 'MUTATION_OUTCOME_UNKNOWN'
|
|
570
|
+
: 'WORKER_INTEGRITY_FAILED',
|
|
571
|
+
),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
try {
|
|
577
|
+
await lease.release();
|
|
578
|
+
} catch {
|
|
579
|
+
await this.poisonAndTerminate();
|
|
580
|
+
throw createSmartSecretKernelStoreError(
|
|
581
|
+
contextArg.mutation ? 'MUTATION_OUTCOME_UNKNOWN' : 'MUTEX_FAILED',
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
if (!outcome.succeeded) throw outcome.error;
|
|
586
|
+
return outcome.value;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private async dispatchRead<T>(
|
|
590
|
+
contextArg: IOperationContext,
|
|
591
|
+
dispatchArg: (requestOptionsArg: IBridgeRequestOptions) => Promise<T>,
|
|
592
|
+
): Promise<T> {
|
|
593
|
+
const requestOptions = this.createBridgeRequestOptions(contextArg);
|
|
594
|
+
contextArg.dispatched = true;
|
|
595
|
+
try {
|
|
596
|
+
return await dispatchArg(requestOptions);
|
|
597
|
+
} catch {
|
|
598
|
+
await this.poisonAndTerminate();
|
|
599
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private async dispatchMutation<T>(
|
|
604
|
+
contextArg: IOperationContext,
|
|
605
|
+
dispatchArg: (requestOptionsArg: IBridgeRequestOptions) => Promise<T>,
|
|
606
|
+
): Promise<T> {
|
|
607
|
+
const requestOptions = this.createBridgeRequestOptions(contextArg);
|
|
608
|
+
contextArg.dispatched = true;
|
|
609
|
+
try {
|
|
610
|
+
return await dispatchArg(requestOptions);
|
|
611
|
+
} catch {
|
|
612
|
+
await this.poisonAndTerminate();
|
|
613
|
+
throw createSmartSecretKernelStoreError('MUTATION_OUTCOME_UNKNOWN');
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
private async throwDomainError(
|
|
618
|
+
domainErrorArg: ISmartSecretKernelDomainErrorResult,
|
|
619
|
+
contextArg: IOperationContext,
|
|
620
|
+
): Promise<never> {
|
|
621
|
+
if (domainErrorArg.code === 'KERNEL_UNAVAILABLE') {
|
|
622
|
+
throw createSmartSecretKernelStoreError('KERNEL_UNAVAILABLE');
|
|
623
|
+
}
|
|
624
|
+
if (domainErrorArg.code === 'ROOT_CONFLICT') {
|
|
625
|
+
throw createSmartSecretKernelStoreError('ROOT_CONFLICT');
|
|
626
|
+
}
|
|
627
|
+
if (domainErrorArg.code === 'ENTRY_CORRUPT') {
|
|
628
|
+
throw createSmartSecretKernelStoreError('ENTRY_CORRUPT');
|
|
629
|
+
}
|
|
630
|
+
if (domainErrorArg.code === 'SOURCE_CHANGED') {
|
|
631
|
+
throw createSmartSecretKernelStoreError('SOURCE_CHANGED');
|
|
632
|
+
}
|
|
633
|
+
if (
|
|
634
|
+
domainErrorArg.code === 'MUTATION_OUTCOME_UNKNOWN'
|
|
635
|
+
&& contextArg.mutation
|
|
636
|
+
) {
|
|
637
|
+
await this.poisonAndTerminate();
|
|
638
|
+
throw createSmartSecretKernelStoreError('MUTATION_OUTCOME_UNKNOWN');
|
|
639
|
+
}
|
|
640
|
+
await this.poisonAndTerminate();
|
|
641
|
+
throw createSmartSecretKernelStoreError('WORKER_INTEGRITY_FAILED');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
private terminateBridgeConfirmed(): Promise<boolean> {
|
|
645
|
+
if (this.terminationPromise) return this.terminationPromise;
|
|
646
|
+
let termination: Promise<void>;
|
|
647
|
+
try {
|
|
648
|
+
termination = this.bridge.terminate({ gracePeriodMs: 0 });
|
|
649
|
+
} catch {
|
|
650
|
+
this.terminationPromise = Promise.resolve(false);
|
|
651
|
+
return this.terminationPromise;
|
|
652
|
+
}
|
|
653
|
+
this.terminationPromise = termination.then(
|
|
654
|
+
() => true,
|
|
655
|
+
() => false,
|
|
656
|
+
);
|
|
657
|
+
return this.terminationPromise;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
private async poisonAndTerminate(): Promise<void> {
|
|
661
|
+
this.poisoned = true;
|
|
662
|
+
await this.terminateBridgeConfirmed();
|
|
663
|
+
}
|
|
664
|
+
}
|