@modelprofile.com/authswitch 7.0.0 → 8.1.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.
Files changed (86) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/authority-contract.d.ts +1 -3
  3. package/dist_ts/authority-contract.js +1 -1
  4. package/dist_ts/authority-import-contract.d.ts +207 -0
  5. package/dist_ts/authority-import-contract.js +18 -0
  6. package/dist_ts/classes.authoritybroker.js +1 -5
  7. package/dist_ts/classes.authoritycli.d.ts +4 -0
  8. package/dist_ts/classes.authoritycli.js +149 -12
  9. package/dist_ts/classes.authorityclient.d.ts +7 -0
  10. package/dist_ts/classes.authorityclient.js +13 -1
  11. package/dist_ts/classes.authoritydaemon.d.ts +4 -0
  12. package/dist_ts/classes.authoritydaemon.js +75 -9
  13. package/dist_ts/classes.authoritydatabase.d.ts +10 -26
  14. package/dist_ts/classes.authoritydatabase.js +68 -133
  15. package/dist_ts/classes.authorityimport.d.ts +161 -0
  16. package/dist_ts/classes.authorityimport.js +638 -0
  17. package/dist_ts/classes.authoritymodels.d.ts +39 -43
  18. package/dist_ts/classes.authoritymodels.js +209 -243
  19. package/dist_ts/classes.authoritypreuse.js +1 -2
  20. package/dist_ts/classes.authorityregistry.d.ts +2 -17
  21. package/dist_ts/classes.authorityregistry.js +18 -74
  22. package/dist_ts/classes.authoritysecrets.d.ts +1 -1
  23. package/dist_ts/classes.authoritysecrets.js +2 -2
  24. package/dist_ts/classes.authorityusage.d.ts +3 -3
  25. package/dist_ts/classes.claudeauthority.js +3 -7
  26. package/dist_ts/classes.claudenative.d.ts +1 -8
  27. package/dist_ts/classes.claudenative.js +2 -25
  28. package/dist_ts/classes.codexmanaged.d.ts +1 -1
  29. package/dist_ts/index.d.ts +1 -0
  30. package/dist_ts/index.js +6 -3
  31. package/dist_ts/plugins.d.ts +7 -7
  32. package/dist_ts/plugins.js +8 -8
  33. package/dist_ts/ts_migration/0001_authority_meta.d.ts +13 -3
  34. package/dist_ts/ts_migration/0001_authority_meta.js +29 -17
  35. package/dist_ts/ts_migration/0002_remove_backup_records.d.ts +17 -0
  36. package/dist_ts/ts_migration/0002_remove_backup_records.js +48 -0
  37. package/dist_ts/ts_migration/index.d.ts +6 -3
  38. package/dist_ts/ts_migration/index.js +10 -4
  39. package/dist_ts/ts_migration/legacysources/authswitchstores.d.ts +2 -0
  40. package/dist_ts/ts_migration/legacysources/authswitchstores.js +244 -0
  41. package/dist_ts/ts_migration/legacysources/index.d.ts +18 -0
  42. package/dist_ts/ts_migration/legacysources/index.js +24 -0
  43. package/dist_ts/ts_migration/legacysources/material.d.ts +37 -0
  44. package/dist_ts/ts_migration/legacysources/material.js +120 -0
  45. package/dist_ts/ts_migration/legacysources/nativestores.d.ts +2 -0
  46. package/dist_ts/ts_migration/legacysources/nativestores.js +207 -0
  47. package/dist_ts/ts_migration/legacysources/shared.d.ts +124 -0
  48. package/dist_ts/ts_migration/legacysources/shared.js +142 -0
  49. package/package.json +6 -5
  50. package/readme.md +68 -26
  51. package/ts/00_commitinfo_data.ts +1 -1
  52. package/ts/authority-contract.ts +1 -3
  53. package/ts/authority-import-contract.ts +216 -0
  54. package/ts/classes.authoritybroker.ts +0 -4
  55. package/ts/classes.authoritycli.ts +165 -11
  56. package/ts/classes.authorityclient.ts +22 -0
  57. package/ts/classes.authoritydaemon.ts +75 -8
  58. package/ts/classes.authoritydatabase.ts +70 -145
  59. package/ts/classes.authorityimport.ts +718 -0
  60. package/ts/classes.authoritymodels.ts +253 -269
  61. package/ts/classes.authoritypreuse.ts +0 -1
  62. package/ts/classes.authorityregistry.ts +20 -91
  63. package/ts/classes.authoritysecrets.ts +1 -1
  64. package/ts/classes.authorityusage.ts +3 -3
  65. package/ts/classes.claudeauthority.ts +2 -6
  66. package/ts/classes.claudenative.ts +1 -32
  67. package/ts/classes.codexmanaged.ts +1 -1
  68. package/ts/index.ts +5 -2
  69. package/ts/plugins.ts +7 -7
  70. package/ts/ts_migration/0001_authority_meta.ts +29 -17
  71. package/ts/ts_migration/0002_remove_backup_records.ts +49 -0
  72. package/ts/ts_migration/index.ts +10 -5
  73. package/ts/ts_migration/legacysources/authswitchstores.ts +211 -0
  74. package/ts/ts_migration/legacysources/index.ts +35 -0
  75. package/ts/ts_migration/legacysources/material.ts +134 -0
  76. package/ts/ts_migration/legacysources/nativestores.ts +204 -0
  77. package/ts/ts_migration/legacysources/shared.ts +220 -0
  78. package/dist_ts/authorityrehearsal.child.d.ts +0 -1
  79. package/dist_ts/authorityrehearsal.child.js +0 -395
  80. package/dist_ts/classes.authoritybackup.d.ts +0 -134
  81. package/dist_ts/classes.authoritybackup.js +0 -769
  82. package/dist_ts/classes.authorityrehearsal.d.ts +0 -127
  83. package/dist_ts/classes.authorityrehearsal.js +0 -632
  84. package/ts/authorityrehearsal.child.ts +0 -330
  85. package/ts/classes.authoritybackup.ts +0 -780
  86. package/ts/classes.authorityrehearsal.ts +0 -625
@@ -1,625 +0,0 @@
1
- import * as plugins from './plugins.js';
2
- import { commitinfo } from './00_commitinfo_data.js';
3
- import { assertAuthorityStoredRecord } from './classes.authorityregistry.js';
4
- import type {
5
- IAuthorityBackupRehearsal,
6
- IAuthorityBackupRehearsalReadback,
7
- IAuthorityRestoredRecord,
8
- IAuthorityRestoredSource,
9
- TAuthorityRestoredEntry,
10
- } from './classes.authoritybackup.js';
11
-
12
- export const authorityRehearsalProtocolVersion = 1 as const;
13
- export const authorityRehearsalMaximumFrameBytes = 8 * 1024 * 1024;
14
- export const authorityRehearsalMaximumRecordBytes = 1024 * 1024;
15
- export const authorityRehearsalMaximumSourceBytes = 4 * 1024 * 1024;
16
- const defaultRequestTimeoutMs = 120_000;
17
- const terminationGraceMs = 2_000;
18
-
19
- const object = (value: unknown): value is Record<string, unknown> => value !== null
20
- && typeof value === 'object' && !Array.isArray(value)
21
- && [Object.prototype, null].includes(Object.getPrototypeOf(value));
22
- const exactKeys = (value: Record<string, unknown>, keys: readonly string[]): boolean =>
23
- Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
24
- const name = (value: unknown): value is string => typeof value === 'string'
25
- && /^[a-zA-Z0-9_.:-]{1,256}$/.test(value);
26
- const hash = (value: unknown): value is string => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
27
- const sequence = (value: unknown): value is number => Number.isSafeInteger(value) && Number(value) >= 0;
28
- const digestField = (value: unknown): value is IAuthorityRestoredSource['linkedDigestField'] =>
29
- value === 'sourceDigest' || value === 'before.credentialDigest' || value === 'before.configDigest';
30
- const sourceDigest = (bytes: Uint8Array): string =>
31
- plugins.crypto.createHash('sha256').update(bytes).digest('hex');
32
-
33
- export interface IAuthorityRehearsalWireSource {
34
- kind: 'source';
35
- sourceKind: string;
36
- sourceId: string;
37
- sourceDigest: string;
38
- linkedModel: string;
39
- linkedId: string;
40
- linkedDigestField: IAuthorityRestoredSource['linkedDigestField'];
41
- bytesBase64url: string;
42
- }
43
-
44
- export type TAuthorityRehearsalWireEntry = IAuthorityRestoredRecord | IAuthorityRehearsalWireSource;
45
-
46
- export type TAuthorityRehearsalParentMessage =
47
- | { version: 1; type: 'stage'; sequence: number; entry: TAuthorityRehearsalWireEntry }
48
- | { version: 1; type: 'finish'; sequence: number }
49
- | { version: 1; type: 'restore.next' }
50
- | { version: 1; type: 'record.next' }
51
- | { version: 1; type: 'source.next' }
52
- | { version: 1; type: 'close' };
53
-
54
- export type TAuthorityRehearsalChildMessage =
55
- | { version: 1; type: 'ready' }
56
- | { version: 1; type: 'staged'; sequence: number }
57
- | { version: 1; type: 'input.complete' }
58
- | { version: 1; type: 'restore.progress'; done: boolean }
59
- | { version: 1; type: 'record'; record: IAuthorityRestoredRecord | null }
60
- | { version: 1; type: 'source'; source: IAuthorityRehearsalWireSource | null }
61
- | { version: 1; type: 'closed' }
62
- | { version: 1; type: 'failed'; phase: 'protocol' | 'staging' | 'restore' | 'readback' | 'cleanup' };
63
-
64
- const assertWireSource: (value: unknown) => asserts value is IAuthorityRehearsalWireSource = value => {
65
- if (!object(value) || !exactKeys(value, ['kind', 'sourceKind', 'sourceId', 'sourceDigest',
66
- 'linkedModel', 'linkedId', 'linkedDigestField', 'bytesBase64url'])
67
- || value.kind !== 'source' || !name(value.sourceKind) || !name(value.sourceId)
68
- || !hash(value.sourceDigest) || !name(value.linkedModel) || !name(value.linkedId)
69
- || !digestField(value.linkedDigestField) || typeof value.bytesBase64url !== 'string'
70
- || value.bytesBase64url.length === 0
71
- || value.bytesBase64url.length > Math.ceil(authorityRehearsalMaximumSourceBytes * 4 / 3)
72
- || !/^[A-Za-z0-9_-]+$/.test(value.bytesBase64url)) {
73
- throw new Error('Invalid rehearsal source frame.');
74
- }
75
- const bytes = Buffer.from(value.bytesBase64url, 'base64url');
76
- try {
77
- if (bytes.byteLength === 0 || bytes.byteLength > authorityRehearsalMaximumSourceBytes
78
- || bytes.toString('base64url') !== value.bytesBase64url
79
- || sourceDigest(bytes) !== value.sourceDigest) throw new Error('Invalid rehearsal source bytes.');
80
- } finally { bytes.fill(0); }
81
- };
82
-
83
- const assertWireRecord: (value: unknown) => asserts value is IAuthorityRestoredRecord = value => {
84
- if (!object(value) || !exactKeys(value, ['model', 'id', 'value'])
85
- || !name(value.model) || !name(value.id)) throw new Error('Invalid rehearsal record frame.');
86
- const encoded = Buffer.from(JSON.stringify(value.value), 'utf8');
87
- try {
88
- if (encoded.byteLength > authorityRehearsalMaximumRecordBytes) {
89
- throw new Error('Rehearsal record exceeds its size bound.');
90
- }
91
- } finally { encoded.fill(0); }
92
- assertAuthorityStoredRecord(value as unknown as IAuthorityRestoredRecord);
93
- };
94
-
95
- export const parseAuthorityRehearsalWireEntry = (value: unknown): TAuthorityRehearsalWireEntry => {
96
- if (object(value) && value.kind === 'source') {
97
- assertWireSource(value);
98
- return value;
99
- }
100
- assertWireRecord(value);
101
- return value;
102
- };
103
-
104
- export const restoredEntryToWire = (entry: TAuthorityRestoredEntry): TAuthorityRehearsalWireEntry => {
105
- if ('kind' in entry) {
106
- if (!(entry.bytes instanceof Uint8Array) || entry.bytes.byteLength === 0
107
- || entry.bytes.byteLength > authorityRehearsalMaximumSourceBytes
108
- || sourceDigest(entry.bytes) !== entry.sourceDigest) throw new Error('Invalid rehearsal source entry.');
109
- const wire: IAuthorityRehearsalWireSource = {
110
- kind: 'source', sourceKind: entry.sourceKind, sourceId: entry.sourceId,
111
- sourceDigest: entry.sourceDigest, linkedModel: entry.linkedModel, linkedId: entry.linkedId,
112
- linkedDigestField: entry.linkedDigestField,
113
- bytesBase64url: Buffer.from(entry.bytes).toString('base64url'),
114
- };
115
- assertWireSource(wire);
116
- return wire;
117
- }
118
- assertWireRecord(entry);
119
- return entry;
120
- };
121
-
122
- export const wireSourceToRestored = (source: IAuthorityRehearsalWireSource): IAuthorityRestoredSource => {
123
- assertWireSource(source);
124
- return {
125
- kind: 'source', sourceKind: source.sourceKind, sourceId: source.sourceId,
126
- sourceDigest: source.sourceDigest, linkedModel: source.linkedModel, linkedId: source.linkedId,
127
- linkedDigestField: source.linkedDigestField, bytes: Buffer.from(source.bytesBase64url, 'base64url'),
128
- };
129
- };
130
-
131
- export const parseAuthorityRehearsalParentMessage = (value: unknown): TAuthorityRehearsalParentMessage => {
132
- if (!object(value) || value.version !== authorityRehearsalProtocolVersion || typeof value.type !== 'string') {
133
- throw new Error('Invalid rehearsal parent frame.');
134
- }
135
- if (value.type === 'stage') {
136
- if (!exactKeys(value, ['version', 'type', 'sequence', 'entry']) || !sequence(value.sequence)) {
137
- throw new Error('Invalid rehearsal stage frame.');
138
- }
139
- return { version: 1, type: 'stage', sequence: value.sequence, entry: parseAuthorityRehearsalWireEntry(value.entry) };
140
- }
141
- if (value.type === 'finish') {
142
- if (!exactKeys(value, ['version', 'type', 'sequence']) || !sequence(value.sequence)) {
143
- throw new Error('Invalid rehearsal finish frame.');
144
- }
145
- return value as TAuthorityRehearsalParentMessage;
146
- }
147
- if (!['restore.next', 'record.next', 'source.next', 'close'].includes(value.type)
148
- || !exactKeys(value, ['version', 'type'])) throw new Error('Invalid rehearsal control frame.');
149
- return value as TAuthorityRehearsalParentMessage;
150
- };
151
-
152
- export const parseAuthorityRehearsalChildMessage = (value: unknown): TAuthorityRehearsalChildMessage => {
153
- if (!object(value) || value.version !== authorityRehearsalProtocolVersion || typeof value.type !== 'string') {
154
- throw new Error('Invalid rehearsal child frame.');
155
- }
156
- if (['ready', 'input.complete', 'closed'].includes(value.type)) {
157
- if (!exactKeys(value, ['version', 'type'])) throw new Error('Invalid rehearsal lifecycle frame.');
158
- return value as TAuthorityRehearsalChildMessage;
159
- }
160
- if (value.type === 'staged') {
161
- if (!exactKeys(value, ['version', 'type', 'sequence']) || !sequence(value.sequence)) {
162
- throw new Error('Invalid rehearsal staged frame.');
163
- }
164
- return value as TAuthorityRehearsalChildMessage;
165
- }
166
- if (value.type === 'restore.progress') {
167
- if (!exactKeys(value, ['version', 'type', 'done']) || typeof value.done !== 'boolean') {
168
- throw new Error('Invalid rehearsal restore frame.');
169
- }
170
- return value as TAuthorityRehearsalChildMessage;
171
- }
172
- if (value.type === 'record') {
173
- if (!exactKeys(value, ['version', 'type', 'record'])) throw new Error('Invalid rehearsal record response.');
174
- if (value.record !== null) assertWireRecord(value.record);
175
- return value as TAuthorityRehearsalChildMessage;
176
- }
177
- if (value.type === 'source') {
178
- if (!exactKeys(value, ['version', 'type', 'source'])) throw new Error('Invalid rehearsal source response.');
179
- if (value.source !== null) assertWireSource(value.source);
180
- return value as TAuthorityRehearsalChildMessage;
181
- }
182
- if (value.type === 'failed') {
183
- if (!exactKeys(value, ['version', 'type', 'phase'])
184
- || !['protocol', 'staging', 'restore', 'readback', 'cleanup'].includes(String(value.phase))) {
185
- throw new Error('Invalid rehearsal failure frame.');
186
- }
187
- return value as TAuthorityRehearsalChildMessage;
188
- }
189
- throw new Error('Unknown rehearsal child frame.');
190
- };
191
-
192
- /** Single-reader/single-writer bounded transport for the isolated rehearsal child. */
193
- export class AuthorityRehearsalFramedTransport<TIncoming, TOutgoing> {
194
- private readonly header = Buffer.allocUnsafe(4);
195
- private headerOffset = 0;
196
- private body?: Buffer;
197
- private bodyOffset = 0;
198
- private queued?: TIncoming;
199
- private waiting?: { resolve: (value: TIncoming) => void; reject: (error: Error) => void };
200
- private failure?: Error;
201
- private closed = false;
202
- private sending = false;
203
-
204
- constructor(private readonly options: {
205
- readable: plugins.stream.Readable;
206
- writable: plugins.stream.Writable;
207
- parseIncoming: (value: unknown) => TIncoming;
208
- validateOutgoing: (value: unknown) => TOutgoing;
209
- }) {
210
- options.readable.on('data', this.onData);
211
- options.readable.once('error', this.onReadError);
212
- options.readable.once('end', this.onReadableClose);
213
- options.readable.once('close', this.onReadableClose);
214
- options.writable.once('error', this.onWriteError);
215
- }
216
-
217
- public async send(message: TOutgoing, timeoutMs: number, signal?: AbortSignal): Promise<void> {
218
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) throw new Error('Invalid rehearsal write deadline.');
219
- signal?.throwIfAborted();
220
- if (this.closed || this.failure || this.sending || this.options.writable.destroyed
221
- || this.options.writable.writableEnded) throw new Error('Rehearsal transport is unavailable.');
222
- this.options.validateOutgoing(message);
223
- const serialized = Buffer.from(JSON.stringify(message), 'utf8');
224
- if (serialized.byteLength > authorityRehearsalMaximumFrameBytes) {
225
- serialized.fill(0);
226
- throw new Error('Rehearsal IPC frame exceeds its size bound.');
227
- }
228
- const frame = Buffer.allocUnsafe(4 + serialized.byteLength);
229
- frame.writeUInt32BE(serialized.byteLength, 0);
230
- serialized.copy(frame, 4);
231
- serialized.fill(0);
232
- this.sending = true;
233
- try {
234
- await new Promise<void>((resolve, reject) => {
235
- let settled = false;
236
- let timer: ReturnType<typeof setTimeout> | undefined;
237
- const cleanup = () => {
238
- if (timer) clearTimeout(timer);
239
- signal?.removeEventListener('abort', abort);
240
- this.options.writable.removeListener('close', closed);
241
- };
242
- const finish = (error?: Error | null) => {
243
- if (settled) return;
244
- settled = true;
245
- cleanup();
246
- if (error) reject(error); else resolve();
247
- };
248
- const interrupt = (messageArg: string) => {
249
- const error = new Error(messageArg);
250
- this.fail(error);
251
- // Writable.destroy() releases pending chunks before its close event. Do not settle or wipe here.
252
- };
253
- const abort = () => interrupt('Rehearsal write was cancelled.');
254
- const closed = () => finish(this.failure ?? new Error('Rehearsal transport closed during its write.'));
255
- timer = setTimeout(() => interrupt('Rehearsal child exceeded its write deadline.'), timeoutMs);
256
- timer.unref?.();
257
- this.options.writable.once('close', closed);
258
- signal?.addEventListener('abort', abort, { once: true });
259
- if (signal?.aborted) abort();
260
- if (!this.failure) {
261
- try {
262
- this.options.writable.write(frame, (error?: Error | null) => finish(error ?? this.failure));
263
- } catch (error) {
264
- finish(error instanceof Error ? error : new Error('Rehearsal IPC write failed.'));
265
- }
266
- }
267
- });
268
- } finally {
269
- this.sending = false;
270
- frame.fill(0);
271
- }
272
- }
273
-
274
- public receive(timeoutMs: number, signal?: AbortSignal): Promise<TIncoming> {
275
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) throw new Error('Invalid rehearsal request deadline.');
276
- signal?.throwIfAborted();
277
- if (this.queued !== undefined) {
278
- const value = this.queued;
279
- this.queued = undefined;
280
- return Promise.resolve(value);
281
- }
282
- if (this.failure) return Promise.reject(this.failure);
283
- if (this.closed) return Promise.reject(new Error('Rehearsal transport closed before its response.'));
284
- if (this.waiting) return Promise.reject(new Error('Rehearsal transport already has a waiting reader.'));
285
- return new Promise<TIncoming>((resolve, reject) => {
286
- let timer: ReturnType<typeof setTimeout> | undefined;
287
- const abort = () => this.fail(new Error('Rehearsal request was cancelled.'));
288
- const finish = (error?: Error, value?: TIncoming) => {
289
- if (this.waiting?.resolve !== waiting.resolve) return;
290
- this.waiting = undefined;
291
- if (timer) clearTimeout(timer);
292
- signal?.removeEventListener('abort', abort);
293
- if (error) reject(error); else resolve(value!);
294
- };
295
- const waiting = { resolve: (value: TIncoming) => finish(undefined, value), reject: (error: Error) => finish(error) };
296
- this.waiting = waiting;
297
- timer = setTimeout(() => this.fail(new Error('Rehearsal child exceeded its response deadline.')), timeoutMs);
298
- timer.unref?.();
299
- signal?.addEventListener('abort', abort, { once: true });
300
- });
301
- }
302
-
303
- public async finishWritable(): Promise<void> {
304
- if (this.options.writable.destroyed || this.options.writable.writableEnded) return;
305
- await new Promise<void>((resolve, reject) => {
306
- this.options.writable.end((error?: Error | null) => error ? reject(error) : resolve());
307
- });
308
- }
309
-
310
- public destroy(): void {
311
- this.closed = true;
312
- this.detach();
313
- this.body?.fill(0);
314
- this.body = undefined;
315
- this.options.readable.destroy();
316
- this.options.writable.destroy();
317
- this.waiting?.reject(new Error('Rehearsal transport was destroyed.'));
318
- }
319
-
320
- private readonly onData = (chunkValue: unknown): void => {
321
- if (this.closed || this.failure) return;
322
- const chunk = chunkValue instanceof Uint8Array
323
- ? Buffer.from(chunkValue.buffer, chunkValue.byteOffset, chunkValue.byteLength) : undefined;
324
- if (!chunk) return this.fail(new Error('Rehearsal IPC received a non-byte chunk.'));
325
- let offset = 0;
326
- try {
327
- while (offset < chunk.byteLength) {
328
- if (!this.body) {
329
- const count = Math.min(4 - this.headerOffset, chunk.byteLength - offset);
330
- chunk.copy(this.header, this.headerOffset, offset, offset + count);
331
- this.headerOffset += count; offset += count;
332
- if (this.headerOffset < 4) continue;
333
- const length = this.header.readUInt32BE(0);
334
- if (length < 1 || length > authorityRehearsalMaximumFrameBytes) {
335
- throw new Error('Rehearsal IPC frame exceeds its size bound.');
336
- }
337
- this.body = Buffer.allocUnsafe(length); this.bodyOffset = 0;
338
- continue;
339
- }
340
- const count = Math.min(this.body.byteLength - this.bodyOffset, chunk.byteLength - offset);
341
- chunk.copy(this.body, this.bodyOffset, offset, offset + count);
342
- this.bodyOffset += count; offset += count;
343
- if (this.bodyOffset === this.body.byteLength) this.finishBody();
344
- }
345
- } catch (error) { this.fail(error instanceof Error ? error : new Error('Rehearsal IPC framing failed.')); }
346
- };
347
-
348
- private finishBody(): void {
349
- const body = this.body;
350
- this.body = undefined; this.bodyOffset = 0; this.headerOffset = 0;
351
- if (!body) throw new Error('Rehearsal IPC lost its frame body.');
352
- let value: unknown;
353
- try { value = JSON.parse(body.toString('utf8')) as unknown; }
354
- catch { throw new Error('Rehearsal IPC frame contains invalid JSON.'); }
355
- finally { body.fill(0); }
356
- const message = this.options.parseIncoming(value);
357
- if (this.waiting) this.waiting.resolve(message);
358
- else if (this.queued === undefined) this.queued = message;
359
- else throw new Error('Rehearsal peer violated serialized request flow.');
360
- }
361
-
362
- private readonly onReadError = (error: Error): void => this.fail(error);
363
- private readonly onWriteError = (error: Error): void => this.fail(error);
364
- private readonly onReadableClose = (): void => {
365
- if (this.failure) return;
366
- if (this.headerOffset || this.body) return this.fail(new Error('Rehearsal IPC ended within a frame.'));
367
- this.fail(new Error('Rehearsal IPC closed before its response.'));
368
- };
369
-
370
- private fail(error: Error): void {
371
- if (this.failure) return;
372
- this.failure = error;
373
- this.closed = true;
374
- this.detach();
375
- this.body?.fill(0); this.body = undefined;
376
- this.waiting?.reject(error);
377
- this.options.readable.destroy();
378
- this.options.writable.destroy();
379
- }
380
-
381
- private detach(): void {
382
- this.options.readable.removeListener('data', this.onData);
383
- this.options.readable.removeListener('error', this.onReadError);
384
- this.options.readable.removeListener('end', this.onReadableClose);
385
- this.options.readable.removeListener('close', this.onReadableClose);
386
- this.options.writable.removeListener('error', this.onWriteError);
387
- }
388
- }
389
-
390
- export interface IAuthorityRehearsalChildLaunch {
391
- entryPath: string;
392
- args: string[];
393
- options: plugins.childProcess.SpawnOptions;
394
- }
395
-
396
- export type TAuthorityRehearsalChildFactory =
397
- (launch: IAuthorityRehearsalChildLaunch) => plugins.childProcess.ChildProcess;
398
-
399
- export interface IAuthSwitchAuthorityRehearsalOptions {
400
- signal?: AbortSignal;
401
- requestTimeoutMs?: number;
402
- /** Test-only source runner seam. Never populate this from CLI, RPC or management input. */
403
- childFactory?: TAuthorityRehearsalChildFactory;
404
- }
405
-
406
- const sanitizedEnvironment = (): NodeJS.ProcessEnv => {
407
- const result: NodeJS.ProcessEnv = {};
408
- for (const key of ['PATH', 'PATHEXT', 'SYSTEMROOT', 'WINDIR', 'COMSPEC', 'TEMP', 'TMP', 'TMPDIR',
409
- 'LANG', 'LC_ALL', 'TZ'] as const) {
410
- if (process.env[key] !== undefined) result[key] = process.env[key];
411
- }
412
- return result;
413
- };
414
-
415
- export const resolveAuthorityRehearsalChildEntryPath = (): string => {
416
- const sourceDirectory = plugins.path.dirname(plugins.fileURLToPath(import.meta.url));
417
- const packageDirectory = plugins.fs.realpathSync.native(plugins.path.dirname(sourceDirectory));
418
- const manifestPath = plugins.fs.realpathSync.native(plugins.path.join(packageDirectory, 'package.json'));
419
- const manifest = JSON.parse(plugins.fs.readFileSync(manifestPath, 'utf8')) as unknown;
420
- if (!object(manifest) || manifest.name !== commitinfo.name) {
421
- throw new Error('The rehearsal child package identity is invalid.');
422
- }
423
- const entryPath = plugins.fs.realpathSync.native(
424
- plugins.path.join(packageDirectory, 'dist_ts', 'authorityrehearsal.child.js'));
425
- if (plugins.path.relative(packageDirectory, entryPath)
426
- !== plugins.path.join('dist_ts', 'authorityrehearsal.child.js')
427
- || !plugins.fs.statSync(entryPath).isFile()) {
428
- throw new Error('The rehearsal child entry escaped its package.');
429
- }
430
- return entryPath;
431
- };
432
-
433
- interface IChildOutcome { code: number | null; signal: NodeJS.Signals | null; error?: Error }
434
-
435
- const observeClose = (child: plugins.childProcess.ChildProcess): Promise<IChildOutcome> =>
436
- new Promise(resolve => {
437
- let error: Error | undefined;
438
- child.once('error', value => { error = value; });
439
- child.once('close', (code, signal) => resolve({ code, signal, error }));
440
- });
441
-
442
- const within = async <T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> => {
443
- let timer: ReturnType<typeof setTimeout> | undefined;
444
- try {
445
- return await Promise.race([promise, new Promise<never>((_, reject) => {
446
- timer = setTimeout(() => reject(new Error(message)), timeoutMs);
447
- timer.unref?.();
448
- })]);
449
- } finally { if (timer) clearTimeout(timer); }
450
- };
451
-
452
- export class AuthSwitchAuthorityRehearsal implements IAuthorityBackupRehearsal {
453
- private readonly requestTimeoutMs: number;
454
-
455
- constructor(private readonly options: IAuthSwitchAuthorityRehearsalOptions = {}) {
456
- this.requestTimeoutMs = options.requestTimeoutMs ?? defaultRequestTimeoutMs;
457
- if (!Number.isSafeInteger(this.requestTimeoutMs) || this.requestTimeoutMs < 1) {
458
- throw new Error('Invalid rehearsal child deadline.');
459
- }
460
- }
461
-
462
- public async restoreClosed(entries: AsyncIterable<TAuthorityRestoredEntry>): Promise<IAuthorityBackupRehearsalReadback> {
463
- this.options.signal?.throwIfAborted();
464
- const root = await plugins.fs.promises.mkdtemp(plugins.path.join(plugins.os.tmpdir(), 'authswitch-rehearsal-'));
465
- await plugins.fs.promises.chmod(root, 0o700);
466
- let child: plugins.childProcess.ChildProcess | undefined;
467
- let transport: AuthorityRehearsalFramedTransport<TAuthorityRehearsalChildMessage,
468
- TAuthorityRehearsalParentMessage> | undefined;
469
- let lifecycle: Promise<IChildOutcome> | undefined;
470
- let handedOff = false;
471
- try {
472
- const sourceEntry = plugins.fileURLToPath(new URL('./authorityrehearsal.child.ts', import.meta.url));
473
- const entryPath = this.options.childFactory ? sourceEntry : resolveAuthorityRehearsalChildEntryPath();
474
- const packageDirectory = plugins.path.dirname(plugins.path.dirname(entryPath));
475
- const launch: IAuthorityRehearsalChildLaunch = { entryPath, args: [root], options: {
476
- cwd: packageDirectory, env: sanitizedEnvironment(), shell: false, windowsHide: true,
477
- stdio: ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'],
478
- } };
479
- child = (this.options.childFactory ?? (input =>
480
- plugins.childProcess.spawn(process.execPath, [input.entryPath, ...input.args], input.options)))(launch);
481
- lifecycle = observeClose(child);
482
- if (!child.pid) throw new Error('The rehearsal child did not start.');
483
- const writer = child.stdio[3];
484
- const reader = child.stdio[4];
485
- if (!(writer instanceof plugins.stream.Writable) || !(reader instanceof plugins.stream.Readable)) {
486
- throw new Error('The rehearsal child control pipes were not created.');
487
- }
488
- transport = new AuthorityRehearsalFramedTransport({ readable: reader, writable: writer,
489
- parseIncoming: parseAuthorityRehearsalChildMessage,
490
- validateOutgoing: parseAuthorityRehearsalParentMessage });
491
- const first = await transport.receive(this.requestTimeoutMs, this.options.signal);
492
- this.expect(first, 'ready');
493
- let itemSequence = 0;
494
- for await (const entry of entries) {
495
- this.options.signal?.throwIfAborted();
496
- const response = await this.exchange(transport,
497
- { version: 1, type: 'stage', sequence: itemSequence, entry: restoredEntryToWire(entry) },
498
- this.options.signal);
499
- if (response.type !== 'staged' || response.sequence !== itemSequence) {
500
- throw new Error('The rehearsal child did not acknowledge the staged entry.');
501
- }
502
- itemSequence++;
503
- }
504
- this.expect(await this.exchange(transport,
505
- { version: 1, type: 'finish', sequence: itemSequence }, this.options.signal), 'input.complete');
506
- while (true) {
507
- const response = await this.exchange(transport, { version: 1, type: 'restore.next' }, this.options.signal);
508
- if (response.type !== 'restore.progress') throw new Error('The rehearsal child returned an invalid restore response.');
509
- if (response.done) break;
510
- }
511
-
512
- let recordsStarted = false;
513
- let recordsDone = false;
514
- let sourcesStarted = false;
515
- let sourcesDone = false;
516
- let closePromise: Promise<void> | undefined;
517
- const close = (): Promise<void> => {
518
- if (closePromise) return closePromise;
519
- closePromise = this.closeReadback({ child: child!, lifecycle: lifecycle!, transport: transport!, root,
520
- complete: recordsDone && sourcesDone });
521
- return closePromise;
522
- };
523
- const self = this;
524
- const records = async function* (): AsyncGenerator<IAuthorityRestoredRecord> {
525
- if (recordsStarted) throw new Error('Rehearsal records may only be read once.');
526
- recordsStarted = true;
527
- while (true) {
528
- const response = await self.exchange(transport!, { version: 1, type: 'record.next' }, self.options.signal);
529
- if (response.type !== 'record') throw new Error('The rehearsal child returned an invalid record response.');
530
- if (response.record === null) { recordsDone = true; return; }
531
- yield response.record;
532
- }
533
- };
534
- const sources = async function* (): AsyncGenerator<IAuthorityRestoredSource> {
535
- if (!recordsDone) throw new Error('Rehearsal sources require complete record readback first.');
536
- if (sourcesStarted) throw new Error('Rehearsal sources may only be read once.');
537
- sourcesStarted = true;
538
- while (true) {
539
- const response = await self.exchange(transport!, { version: 1, type: 'source.next' }, self.options.signal);
540
- if (response.type !== 'source') throw new Error('The rehearsal child returned an invalid source response.');
541
- if (response.source === null) { sourcesDone = true; return; }
542
- yield wireSourceToRestored(response.source);
543
- }
544
- };
545
- handedOff = true;
546
- return { records: records(), sources: sources(), close };
547
- } catch (error) {
548
- if (child && lifecycle) {
549
- try { await this.abortChild(child, lifecycle, transport, !this.options.signal?.aborted); }
550
- catch (cleanupError) { throw new AggregateError([error, cleanupError], 'Rehearsal restore and cleanup failed.'); }
551
- }
552
- throw error;
553
- } finally {
554
- if (!handedOff) await plugins.fs.promises.rm(root, { recursive: true, force: true });
555
- }
556
- }
557
-
558
- private async exchange(transport: AuthorityRehearsalFramedTransport<TAuthorityRehearsalChildMessage,
559
- TAuthorityRehearsalParentMessage>, message: TAuthorityRehearsalParentMessage,
560
- signal?: AbortSignal): Promise<TAuthorityRehearsalChildMessage> {
561
- const deadline = Date.now() + this.requestTimeoutMs;
562
- await transport.send(message, this.requestTimeoutMs, signal);
563
- const remaining = deadline - Date.now();
564
- if (remaining < 1) {
565
- transport.destroy();
566
- throw new Error('The rehearsal child exceeded its request deadline.');
567
- }
568
- const response = await transport.receive(remaining, signal);
569
- if (response.type === 'failed') throw new Error(`The rehearsal child failed during ${response.phase}.`);
570
- return response;
571
- }
572
-
573
- private expect(message: TAuthorityRehearsalChildMessage, type: TAuthorityRehearsalChildMessage['type']): void {
574
- if (message.type === 'failed') throw new Error(`The rehearsal child failed during ${message.phase}.`);
575
- if (message.type !== type) throw new Error(`The rehearsal child did not return ${type}.`);
576
- }
577
-
578
- private async closeReadback(input: { child: plugins.childProcess.ChildProcess; lifecycle: Promise<IChildOutcome>;
579
- transport: AuthorityRehearsalFramedTransport<TAuthorityRehearsalChildMessage, TAuthorityRehearsalParentMessage>;
580
- root: string; complete: boolean }): Promise<void> {
581
- let failure: unknown;
582
- try {
583
- if (!input.complete) throw new Error('The rehearsal readback was not consumed completely.');
584
- this.expect(await this.exchange(input.transport, { version: 1, type: 'close' }), 'closed');
585
- await input.transport.finishWritable();
586
- const outcome = await within(input.lifecycle, this.requestTimeoutMs,
587
- 'The rehearsal child did not close after cleanup.');
588
- if (outcome.error || outcome.signal !== null || outcome.code !== 0) {
589
- throw new Error('The rehearsal child did not close cleanly.');
590
- }
591
- } catch (error) {
592
- failure = error;
593
- try { await this.abortChild(input.child, input.lifecycle, input.transport, !this.options.signal?.aborted); }
594
- catch (cleanupError) { failure = new AggregateError([error, cleanupError], 'Rehearsal close cleanup failed.'); }
595
- } finally {
596
- input.transport.destroy();
597
- try { await plugins.fs.promises.rm(input.root, { recursive: true, force: false }); }
598
- catch (error) { failure = failure
599
- ? new AggregateError([failure, error], 'Rehearsal artifact cleanup failed.') : error; }
600
- }
601
- if (failure) throw failure;
602
- }
603
-
604
- private async abortChild(child: plugins.childProcess.ChildProcess, lifecycle: Promise<IChildOutcome>,
605
- transport?: AuthorityRehearsalFramedTransport<TAuthorityRehearsalChildMessage,
606
- TAuthorityRehearsalParentMessage>, graceful = true): Promise<void> {
607
- if (transport && graceful) {
608
- try {
609
- const response = await within(this.exchange(transport, { version: 1, type: 'close' }),
610
- terminationGraceMs, 'Rehearsal child close request timed out.');
611
- if (response.type === 'closed') await transport.finishWritable();
612
- } catch { /* The child may already be failed or gone. */ }
613
- }
614
- if (!graceful) transport?.destroy();
615
- try { await within(lifecycle, terminationGraceMs, 'Rehearsal child did not close voluntarily.'); }
616
- catch {
617
- child.kill('SIGTERM');
618
- try { await within(lifecycle, terminationGraceMs, 'Rehearsal child ignored SIGTERM.'); }
619
- catch {
620
- child.kill('SIGKILL');
621
- await within(lifecycle, this.requestTimeoutMs, 'Rehearsal child exit could not be confirmed.');
622
- }
623
- } finally { transport?.destroy(); }
624
- }
625
- }