@gotcos/glasses-server 6.12.6 → 6.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,735 @@
1
+ import { createHash, randomUUID, timingSafeEqual } from 'node:crypto'
2
+ import {
3
+ chmodSync,
4
+ closeSync,
5
+ existsSync,
6
+ fsyncSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ openSync,
10
+ readFileSync,
11
+ renameSync,
12
+ rmSync,
13
+ writeFileSync,
14
+ } from 'node:fs'
15
+ import { homedir } from 'node:os'
16
+ import { dirname, join } from 'node:path'
17
+ import { getServerGenerationId, isManagedRuntime } from './managed-runtime.js'
18
+ import { serverMetrics } from './server-metrics.js'
19
+ import { getServerInstanceId } from './server-instance-id.js'
20
+
21
+ const GATE_VERSION = 2
22
+ const DEFAULT_LEASE_MS = 5 * 60_000
23
+ const MIN_LEASE_MS = 30_000
24
+ const MAX_LEASE_MS = 15 * 60_000
25
+ const ID_RE = /^[A-Za-z0-9._:-]{1,160}$/
26
+ const NONCE_RE = /^[A-Za-z0-9_-]{32,172}$/
27
+ const SHA256_RE = /^[0-9a-f]{64}$/
28
+ const DEFAULT_GATE_PATH = join(
29
+ homedir(),
30
+ 'Library',
31
+ 'Application Support',
32
+ 'COS Glasses',
33
+ 'control',
34
+ 'maintenance-gate.json',
35
+ )
36
+
37
+ export type MaintenanceWorkKind =
38
+ | 'api_mutation'
39
+ | 'durable_query'
40
+ | 'legacy_query'
41
+ | 'openai_query'
42
+ | 'query_attachment_write'
43
+ | 'one_shot_transcription'
44
+ | 'recording_chunk'
45
+ | 'meeting_save'
46
+ | 'meeting_batch_finalization'
47
+ | 'prompt_draft_write'
48
+ | 'prompt_draft_warm'
49
+ | 'prompt_draft_finalize'
50
+
51
+ export type MaintenanceWorkPhase = 'queued' | 'active'
52
+ export type MaintenanceOperationScope = 'same_boot' | 'cross_boot'
53
+ export type MaintenanceOperationKind =
54
+ | 'same_boot_maintenance'
55
+ | 'server_restart'
56
+ | 'server_update'
57
+ | 'server_rollback'
58
+ | 'server_stop'
59
+ export type MaintenancePostcondition = 'same_boot_idle' | 'authorized_successor_adopted'
60
+
61
+ interface AdoptedSuccessor {
62
+ serverInstanceId: string
63
+ bootId: string
64
+ generationId: string
65
+ adoptedAt: string
66
+ }
67
+
68
+ interface DurableDrainGateV2 {
69
+ version: typeof GATE_VERSION
70
+ leaseId: string
71
+ operationId: string
72
+ operationKind: MaintenanceOperationKind
73
+ scope: MaintenanceOperationScope
74
+ postcondition: MaintenancePostcondition
75
+ nonceSha256: string
76
+ authorizedSuccessorGenerations: string[]
77
+ serverInstanceId: string
78
+ sourceBootId: string
79
+ sourceGenerationId: string
80
+ startedAt: string
81
+ expiresAt: string | null
82
+ adoptedSuccessor?: AdoptedSuccessor
83
+ }
84
+
85
+ type BlockedGateReason = 'legacy_v1' | 'unknown_schema' | 'invalid_schema' | 'corrupt_json'
86
+
87
+ interface WorkEntry {
88
+ kind: MaintenanceWorkKind
89
+ phase: MaintenanceWorkPhase
90
+ startedAtMs: number
91
+ }
92
+
93
+ interface MaintenanceLifecycleOptions {
94
+ path?: string
95
+ now?: () => number
96
+ bootId?: () => string
97
+ serverInstanceId?: () => string | null
98
+ generationId?: () => string | null
99
+ managed?: () => boolean
100
+ }
101
+
102
+ export interface MaintenanceDrainRequest {
103
+ serverInstanceId: string
104
+ bootId: string
105
+ generationId: string
106
+ operationId: string
107
+ operationKind: MaintenanceOperationKind
108
+ scope: MaintenanceOperationScope
109
+ postcondition: MaintenancePostcondition
110
+ nonceSha256: string
111
+ authorizedSuccessorGenerations: string[]
112
+ ttlMs?: number
113
+ }
114
+
115
+ export interface MaintenanceOperationIdentity {
116
+ serverInstanceId: string
117
+ bootId: string
118
+ generationId: string
119
+ operationId: string
120
+ }
121
+
122
+ export interface MaintenanceOperationCredentials {
123
+ leaseId?: string
124
+ operationId?: string
125
+ nonce?: string
126
+ }
127
+
128
+ export interface MaintenanceWorkLease {
129
+ readonly id: string
130
+ setPhase(phase: MaintenanceWorkPhase): void
131
+ release(): void
132
+ }
133
+
134
+ export class MaintenanceLifecycleError extends Error {
135
+ readonly status: number
136
+ readonly retryable: boolean
137
+ readonly retryAfterSeconds?: number
138
+
139
+ constructor(
140
+ readonly code: string,
141
+ message: string,
142
+ options: { status?: number; retryable?: boolean; retryAfterSeconds?: number } = {},
143
+ ) {
144
+ super(message)
145
+ this.name = 'MaintenanceLifecycleError'
146
+ this.status = options.status ?? 409
147
+ this.retryable = options.retryable ?? false
148
+ this.retryAfterSeconds = options.retryAfterSeconds
149
+ }
150
+ }
151
+
152
+ function isStringId(value: unknown): value is string {
153
+ return typeof value === 'string' && ID_RE.test(value)
154
+ }
155
+
156
+ function isOperationKind(value: unknown): value is MaintenanceOperationKind {
157
+ return value === 'same_boot_maintenance'
158
+ || value === 'server_restart'
159
+ || value === 'server_update'
160
+ || value === 'server_rollback'
161
+ || value === 'server_stop'
162
+ }
163
+
164
+ function isScope(value: unknown): value is MaintenanceOperationScope {
165
+ return value === 'same_boot' || value === 'cross_boot'
166
+ }
167
+
168
+ function isPostcondition(value: unknown): value is MaintenancePostcondition {
169
+ return value === 'same_boot_idle' || value === 'authorized_successor_adopted'
170
+ }
171
+
172
+ function parseAdoptedSuccessor(value: unknown): AdoptedSuccessor | undefined | null {
173
+ if (value == null) return undefined
174
+ if (!value || typeof value !== 'object') return null
175
+ const item = value as Record<string, unknown>
176
+ if (!isStringId(item.serverInstanceId)
177
+ || !isStringId(item.bootId)
178
+ || !isStringId(item.generationId)
179
+ || typeof item.adoptedAt !== 'string'
180
+ || !Number.isFinite(Date.parse(item.adoptedAt))) return null
181
+ return item as unknown as AdoptedSuccessor
182
+ }
183
+
184
+ function parseGateV2(value: unknown): DurableDrainGateV2 | null {
185
+ if (!value || typeof value !== 'object') return null
186
+ const gate = value as Record<string, unknown>
187
+ const adoptedSuccessor = parseAdoptedSuccessor(gate.adoptedSuccessor)
188
+ const successors = Array.isArray(gate.authorizedSuccessorGenerations)
189
+ ? gate.authorizedSuccessorGenerations
190
+ : []
191
+ if (gate.version !== GATE_VERSION
192
+ || !isStringId(gate.leaseId)
193
+ || !isStringId(gate.operationId)
194
+ || !isOperationKind(gate.operationKind)
195
+ || !isScope(gate.scope)
196
+ || !isPostcondition(gate.postcondition)
197
+ || typeof gate.nonceSha256 !== 'string'
198
+ || !SHA256_RE.test(gate.nonceSha256)
199
+ || successors.length < 1
200
+ || successors.length > 8
201
+ || successors.some(value => !isStringId(value))
202
+ || new Set(successors).size !== successors.length
203
+ || !isStringId(gate.serverInstanceId)
204
+ || !isStringId(gate.sourceBootId)
205
+ || !isStringId(gate.sourceGenerationId)
206
+ || typeof gate.startedAt !== 'string'
207
+ || !Number.isFinite(Date.parse(gate.startedAt))
208
+ || adoptedSuccessor === null) return null
209
+
210
+ if (gate.scope === 'same_boot') {
211
+ if (gate.operationKind !== 'same_boot_maintenance'
212
+ || gate.postcondition !== 'same_boot_idle'
213
+ || typeof gate.expiresAt !== 'string'
214
+ || !Number.isFinite(Date.parse(gate.expiresAt))) return null
215
+ } else if (gate.operationKind === 'same_boot_maintenance'
216
+ || gate.postcondition !== 'authorized_successor_adopted'
217
+ || gate.expiresAt !== null) return null
218
+
219
+ return {
220
+ ...(gate as unknown as DurableDrainGateV2),
221
+ authorizedSuccessorGenerations: [...successors] as string[],
222
+ ...(adoptedSuccessor ? { adoptedSuccessor } : {}),
223
+ }
224
+ }
225
+
226
+ function syncDirectory(path: string): void {
227
+ const fd = openSync(path, 'r')
228
+ try { fsyncSync(fd) } finally { closeSync(fd) }
229
+ }
230
+
231
+ function assertOwnedSafeDirectory(path: string): void {
232
+ const stat = lstatSync(path)
233
+ const uid = typeof process.getuid === 'function' ? process.getuid() : stat.uid
234
+ if (!stat.isDirectory() || stat.isSymbolicLink() || stat.uid !== uid || (stat.mode & 0o022) !== 0) {
235
+ throw new Error(`Unsafe maintenance state directory: ${path}`)
236
+ }
237
+ }
238
+
239
+ function ensureOwnedSafeDirectory(path: string): void {
240
+ mkdirSync(path, { recursive: true, mode: 0o700 })
241
+ assertOwnedSafeDirectory(path)
242
+ chmodSync(path, 0o700)
243
+ }
244
+
245
+ function assertOwnedSafeGateFile(path: string): void {
246
+ const stat = lstatSync(path)
247
+ const uid = typeof process.getuid === 'function' ? process.getuid() : stat.uid
248
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.uid !== uid || (stat.mode & 0o077) !== 0) {
249
+ throw new Error(`Unsafe maintenance gate file: ${path}`)
250
+ }
251
+ }
252
+
253
+ function persistGate(path: string, gate: DurableDrainGateV2): void {
254
+ const directory = dirname(path)
255
+ ensureOwnedSafeDirectory(directory)
256
+ const temp = `${path}.tmp-${process.pid}-${randomUUID()}`
257
+ let fd: number | null = null
258
+ try {
259
+ fd = openSync(temp, 'wx', 0o600)
260
+ writeFileSync(fd, `${JSON.stringify(gate)}\n`, 'utf8')
261
+ fsyncSync(fd)
262
+ closeSync(fd)
263
+ fd = null
264
+ renameSync(temp, path)
265
+ chmodSync(path, 0o600)
266
+ assertOwnedSafeGateFile(path)
267
+ syncDirectory(directory)
268
+ } catch (error) {
269
+ if (fd != null) closeSync(fd)
270
+ rmSync(temp, { force: true })
271
+ throw error
272
+ }
273
+ }
274
+
275
+ function removeGate(path: string): void {
276
+ const directory = dirname(path)
277
+ if (!existsSync(directory)) return
278
+ assertOwnedSafeDirectory(directory)
279
+ if (existsSync(path)) {
280
+ assertOwnedSafeGateFile(path)
281
+ rmSync(path)
282
+ }
283
+ syncDirectory(directory)
284
+ }
285
+
286
+ function hashNonce(nonce: string): string {
287
+ return createHash('sha256').update(nonce, 'utf8').digest('hex')
288
+ }
289
+
290
+ function secureDigestEqual(left: string, right: string): boolean {
291
+ if (!SHA256_RE.test(left) || !SHA256_RE.test(right)) return false
292
+ return timingSafeEqual(Buffer.from(left, 'hex'), Buffer.from(right, 'hex'))
293
+ }
294
+
295
+ /**
296
+ * Process-wide admission gate and work ledger. Version-2 cross-boot gates are
297
+ * committed operations, not expiring leases: they remain closed until an
298
+ * explicitly authorized successor adopts the operation and proves the
299
+ * controller-held nonce. Unknown/legacy state is decoded into a typed blocked
300
+ * state instead of being deleted or interpreted optimistically.
301
+ */
302
+ export class MaintenanceLifecycle {
303
+ private readonly path: string
304
+ private readonly now: () => number
305
+ private readonly currentBootId: () => string
306
+ private readonly currentServerInstanceId: () => string | null
307
+ private readonly currentGenerationId: () => string | null
308
+ private readonly managed: () => boolean
309
+ private gate: DurableDrainGateV2 | null = null
310
+ private blockedGateReason: BlockedGateReason | null = null
311
+ private blockedGateVersion: number | null = null
312
+ private readonly work = new Map<string, WorkEntry>()
313
+
314
+ constructor(options: MaintenanceLifecycleOptions = {}) {
315
+ this.path = options.path ?? process.env.COS_MAINTENANCE_GATE_PATH?.trim() ?? DEFAULT_GATE_PATH
316
+ this.now = options.now ?? (() => Date.now())
317
+ this.currentBootId = options.bootId ?? (() => serverMetrics.bootId)
318
+ this.currentServerInstanceId = options.serverInstanceId ?? getServerInstanceId
319
+ this.currentGenerationId = options.generationId ?? getServerGenerationId
320
+ this.managed = options.managed ?? isManagedRuntime
321
+ this.loadGateSync()
322
+ }
323
+
324
+ private loadGateSync(): void {
325
+ let value: unknown
326
+ try {
327
+ assertOwnedSafeDirectory(dirname(this.path))
328
+ assertOwnedSafeGateFile(this.path)
329
+ value = JSON.parse(readFileSync(this.path, 'utf8'))
330
+ } catch (error) {
331
+ if ((error as NodeJS.ErrnoException).code !== 'ENOENT') this.blockedGateReason = 'corrupt_json'
332
+ return
333
+ }
334
+ const version = value && typeof value === 'object'
335
+ ? Number((value as Record<string, unknown>).version)
336
+ : Number.NaN
337
+ this.blockedGateVersion = Number.isInteger(version) ? version : null
338
+ if (version === GATE_VERSION) {
339
+ const parsed = parseGateV2(value)
340
+ if (parsed) this.gate = parsed
341
+ else this.blockedGateReason = 'invalid_schema'
342
+ } else if (version === 1) {
343
+ this.blockedGateReason = 'legacy_v1'
344
+ } else {
345
+ this.blockedGateReason = 'unknown_schema'
346
+ }
347
+ this.expireSameBootGateIfPermitted()
348
+ }
349
+
350
+ private expireSameBootGateIfPermitted(): void {
351
+ if (!this.gate
352
+ || this.gate.scope !== 'same_boot'
353
+ || this.gate.sourceBootId !== this.currentBootId()
354
+ || !this.gate.expiresAt
355
+ || Date.parse(this.gate.expiresAt) > this.now()) return
356
+ try {
357
+ removeGate(this.path)
358
+ this.gate = null
359
+ } catch {
360
+ this.blockedGateReason = 'invalid_schema'
361
+ }
362
+ }
363
+
364
+ private credentialsMatch(credentials: MaintenanceOperationCredentials): {
365
+ leaseMatches: boolean
366
+ operationMatches: boolean
367
+ nonceMatches: boolean
368
+ } {
369
+ const leaseMatches = Boolean(this.gate && credentials.leaseId === this.gate.leaseId)
370
+ const operationMatches = Boolean(this.gate && credentials.operationId === this.gate.operationId)
371
+ const nonceDigest = credentials.nonce && NONCE_RE.test(credentials.nonce)
372
+ ? hashNonce(credentials.nonce)
373
+ : ''
374
+ const nonceMatches = Boolean(this.gate && secureDigestEqual(nonceDigest, this.gate.nonceSha256))
375
+ return { leaseMatches, operationMatches, nonceMatches }
376
+ }
377
+
378
+ private requireValidGate(): DurableDrainGateV2 {
379
+ this.expireSameBootGateIfPermitted()
380
+ if (this.blockedGateReason || !this.gate) {
381
+ throw new MaintenanceLifecycleError(
382
+ 'maintenance_gate_unavailable',
383
+ 'A valid maintenance operation gate is not available.',
384
+ { status: 503 },
385
+ )
386
+ }
387
+ return this.gate
388
+ }
389
+
390
+ private requireCredentials(credentials: MaintenanceOperationCredentials): DurableDrainGateV2 {
391
+ const gate = this.requireValidGate()
392
+ const proof = this.credentialsMatch(credentials)
393
+ if (!proof.leaseMatches || !proof.operationMatches || !proof.nonceMatches) {
394
+ throw new MaintenanceLifecycleError(
395
+ 'maintenance_operation_proof_invalid',
396
+ 'Maintenance operation credentials are not valid.',
397
+ )
398
+ }
399
+ return gate
400
+ }
401
+
402
+ acquire(
403
+ kind: MaintenanceWorkKind,
404
+ options: { allowDuringDrain?: boolean; phase?: MaintenanceWorkPhase } = {},
405
+ ): MaintenanceWorkLease {
406
+ this.expireSameBootGateIfPermitted()
407
+ // Unknown, corrupt, or unsafe durable state is never a continuation gate.
408
+ // Only a valid decoded gate may allow already-admitted work to finish.
409
+ if (this.blockedGateReason || (this.gate && !options.allowDuringDrain)) {
410
+ const retryAfterSeconds = this.gate?.scope === 'same_boot' && this.gate.expiresAt
411
+ && this.gate.sourceBootId === this.currentBootId()
412
+ ? Math.max(1, Math.ceil((Date.parse(this.gate.expiresAt) - this.now()) / 1_000))
413
+ : undefined
414
+ throw new MaintenanceLifecycleError(
415
+ 'maintenance_drain_active',
416
+ 'Server is closed for a committed maintenance operation.',
417
+ { status: 503, retryable: true, retryAfterSeconds },
418
+ )
419
+ }
420
+
421
+ const id = randomUUID()
422
+ this.work.set(id, { kind, phase: options.phase ?? 'active', startedAtMs: this.now() })
423
+ let released = false
424
+ return {
425
+ id,
426
+ setPhase: (phase) => {
427
+ if (released) return
428
+ const entry = this.work.get(id)
429
+ if (entry) entry.phase = phase
430
+ },
431
+ release: () => {
432
+ if (released) return
433
+ released = true
434
+ this.work.delete(id)
435
+ },
436
+ }
437
+ }
438
+
439
+ beginDrain(request: MaintenanceDrainRequest): string {
440
+ this.expireSameBootGateIfPermitted()
441
+ if (!this.managed()) {
442
+ throw new MaintenanceLifecycleError('server_not_managed', 'Server is not managed.')
443
+ }
444
+ if (this.blockedGateReason) {
445
+ throw new MaintenanceLifecycleError(
446
+ 'maintenance_gate_blocked',
447
+ 'Existing durable maintenance state requires local repair.',
448
+ { status: 503 },
449
+ )
450
+ }
451
+ if (this.gate) {
452
+ throw new MaintenanceLifecycleError(
453
+ 'maintenance_operation_already_active',
454
+ 'A maintenance operation is already active.',
455
+ { retryable: true },
456
+ )
457
+ }
458
+
459
+ const current = {
460
+ serverInstanceId: this.currentServerInstanceId(),
461
+ bootId: this.currentBootId(),
462
+ generationId: this.currentGenerationId(),
463
+ }
464
+ const successors = request.authorizedSuccessorGenerations
465
+ if (!current.serverInstanceId || !current.generationId
466
+ || request.serverInstanceId !== current.serverInstanceId
467
+ || request.bootId !== current.bootId
468
+ || request.generationId !== current.generationId) {
469
+ throw new MaintenanceLifecycleError(
470
+ 'server_identity_mismatch',
471
+ 'Maintenance request does not match the running server identity.',
472
+ )
473
+ }
474
+ if (!isStringId(request.operationId)
475
+ || !isOperationKind(request.operationKind)
476
+ || !isScope(request.scope)
477
+ || !isPostcondition(request.postcondition)
478
+ || !SHA256_RE.test(request.nonceSha256)
479
+ || !Array.isArray(successors)
480
+ || successors.length < 1
481
+ || successors.length > 8
482
+ || successors.some(value => !isStringId(value))
483
+ || new Set(successors).size !== successors.length) {
484
+ throw new MaintenanceLifecycleError('invalid_maintenance_operation', 'Maintenance operation is invalid.', { status: 400 })
485
+ }
486
+ if ((request.scope === 'same_boot'
487
+ && (request.operationKind !== 'same_boot_maintenance'
488
+ || request.postcondition !== 'same_boot_idle'
489
+ || successors.length !== 1
490
+ || successors[0] !== current.generationId))
491
+ || (request.scope === 'cross_boot'
492
+ && (request.operationKind === 'same_boot_maintenance'
493
+ || request.postcondition !== 'authorized_successor_adopted'))) {
494
+ throw new MaintenanceLifecycleError('invalid_maintenance_operation_contract', 'Maintenance operation scope and postcondition do not agree.', { status: 400 })
495
+ }
496
+
497
+ const started = this.now()
498
+ const ttlMs = request.scope === 'same_boot'
499
+ ? Math.min(MAX_LEASE_MS, Math.max(MIN_LEASE_MS, request.ttlMs ?? DEFAULT_LEASE_MS))
500
+ : null
501
+ const gate: DurableDrainGateV2 = {
502
+ version: GATE_VERSION,
503
+ leaseId: randomUUID(),
504
+ operationId: request.operationId,
505
+ operationKind: request.operationKind,
506
+ scope: request.scope,
507
+ postcondition: request.postcondition,
508
+ nonceSha256: request.nonceSha256,
509
+ authorizedSuccessorGenerations: [...successors],
510
+ serverInstanceId: current.serverInstanceId,
511
+ sourceBootId: current.bootId,
512
+ sourceGenerationId: current.generationId,
513
+ startedAt: new Date(started).toISOString(),
514
+ expiresAt: ttlMs == null ? null : new Date(started + ttlMs).toISOString(),
515
+ }
516
+ try {
517
+ persistGate(this.path, gate)
518
+ } catch {
519
+ throw new MaintenanceLifecycleError(
520
+ 'maintenance_gate_persist_failed',
521
+ 'Could not durably commit the maintenance operation.',
522
+ { status: 503, retryable: true },
523
+ )
524
+ }
525
+ this.gate = gate
526
+ return gate.leaseId
527
+ }
528
+
529
+ adoptDrain(identity: MaintenanceOperationIdentity, credentials: MaintenanceOperationCredentials): void {
530
+ const gate = this.requireCredentials(credentials)
531
+ const current = {
532
+ serverInstanceId: this.currentServerInstanceId(),
533
+ bootId: this.currentBootId(),
534
+ generationId: this.currentGenerationId(),
535
+ }
536
+ if (gate.scope !== 'cross_boot'
537
+ || identity.operationId !== gate.operationId
538
+ || !current.serverInstanceId
539
+ || !current.generationId
540
+ || identity.serverInstanceId !== current.serverInstanceId
541
+ || identity.bootId !== current.bootId
542
+ || identity.generationId !== current.generationId
543
+ || current.serverInstanceId !== gate.serverInstanceId
544
+ || current.bootId === gate.sourceBootId
545
+ || !gate.authorizedSuccessorGenerations.includes(current.generationId)) {
546
+ throw new MaintenanceLifecycleError(
547
+ 'maintenance_successor_unauthorized',
548
+ 'Running server is not an authorized successor for this operation.',
549
+ )
550
+ }
551
+ const next: DurableDrainGateV2 = {
552
+ ...gate,
553
+ adoptedSuccessor: {
554
+ serverInstanceId: current.serverInstanceId,
555
+ bootId: current.bootId,
556
+ generationId: current.generationId,
557
+ adoptedAt: new Date(this.now()).toISOString(),
558
+ },
559
+ }
560
+ try {
561
+ persistGate(this.path, next)
562
+ } catch {
563
+ throw new MaintenanceLifecycleError(
564
+ 'maintenance_adoption_persist_failed',
565
+ 'Could not durably adopt the maintenance operation.',
566
+ { status: 503, retryable: true },
567
+ )
568
+ }
569
+ this.gate = next
570
+ }
571
+
572
+ releaseDrain(identity: MaintenanceOperationIdentity, credentials: MaintenanceOperationCredentials): void {
573
+ const gate = this.requireCredentials(credentials)
574
+ const current = {
575
+ serverInstanceId: this.currentServerInstanceId(),
576
+ bootId: this.currentBootId(),
577
+ generationId: this.currentGenerationId(),
578
+ }
579
+ const identityMatches = Boolean(current.serverInstanceId && current.generationId
580
+ && identity.operationId === gate.operationId
581
+ && identity.serverInstanceId === current.serverInstanceId
582
+ && identity.bootId === current.bootId
583
+ && identity.generationId === current.generationId
584
+ && current.serverInstanceId === gate.serverInstanceId
585
+ && gate.authorizedSuccessorGenerations.includes(current.generationId))
586
+ const postconditionSatisfied = gate.scope === 'same_boot'
587
+ ? gate.postcondition === 'same_boot_idle' && current.bootId === gate.sourceBootId
588
+ : gate.postcondition === 'authorized_successor_adopted'
589
+ && gate.adoptedSuccessor?.serverInstanceId === current.serverInstanceId
590
+ && gate.adoptedSuccessor?.bootId === current.bootId
591
+ && gate.adoptedSuccessor?.generationId === current.generationId
592
+ if (!identityMatches || !postconditionSatisfied) {
593
+ throw new MaintenanceLifecycleError(
594
+ 'maintenance_release_postcondition_failed',
595
+ 'Maintenance operation release postcondition is not satisfied.',
596
+ )
597
+ }
598
+ try {
599
+ removeGate(this.path)
600
+ } catch {
601
+ throw new MaintenanceLifecycleError(
602
+ 'maintenance_gate_release_failed',
603
+ 'Could not durably clear the maintenance operation.',
604
+ { status: 503, retryable: true },
605
+ )
606
+ }
607
+ this.gate = null
608
+ }
609
+
610
+ cancelDrain(identity: MaintenanceOperationIdentity, credentials: MaintenanceOperationCredentials): void {
611
+ const gate = this.requireCredentials(credentials)
612
+ if (identity.operationId !== gate.operationId
613
+ || identity.serverInstanceId !== gate.serverInstanceId
614
+ || identity.bootId !== gate.sourceBootId
615
+ || identity.generationId !== gate.sourceGenerationId
616
+ || this.currentServerInstanceId() !== gate.serverInstanceId
617
+ || this.currentBootId() !== gate.sourceBootId
618
+ || this.currentGenerationId() !== gate.sourceGenerationId) {
619
+ throw new MaintenanceLifecycleError(
620
+ 'maintenance_cancel_identity_mismatch',
621
+ 'Only the source boot may cancel this maintenance operation.',
622
+ )
623
+ }
624
+ try {
625
+ removeGate(this.path)
626
+ } catch {
627
+ throw new MaintenanceLifecycleError(
628
+ 'maintenance_gate_cancel_failed',
629
+ 'Could not durably cancel the maintenance operation.',
630
+ { status: 503, retryable: true },
631
+ )
632
+ }
633
+ this.gate = null
634
+ }
635
+
636
+ snapshot(credentials: MaintenanceOperationCredentials = {}, extraActiveByKind: Record<string, number> = {}) {
637
+ this.expireSameBootGateIfPermitted()
638
+ const activeByKind: Record<string, number> = {}
639
+ let queuedTransitions = 0
640
+ let oldestStartedAtMs: number | null = null
641
+ for (const entry of this.work.values()) {
642
+ activeByKind[entry.kind] = (activeByKind[entry.kind] ?? 0) + 1
643
+ if (entry.phase === 'queued') queuedTransitions++
644
+ oldestStartedAtMs = oldestStartedAtMs == null
645
+ ? entry.startedAtMs
646
+ : Math.min(oldestStartedAtMs, entry.startedAtMs)
647
+ }
648
+ for (const [kind, rawCount] of Object.entries(extraActiveByKind)) {
649
+ const count = Number.isFinite(rawCount) ? Math.max(0, Math.floor(rawCount)) : 0
650
+ if (count > 0) activeByKind[kind] = (activeByKind[kind] ?? 0) + count
651
+ }
652
+ const activeTotal = Object.values(activeByKind).reduce((sum, count) => sum + count, 0)
653
+ const currentBootId = this.currentBootId()
654
+ const currentGenerationId = this.currentGenerationId()
655
+ const currentServerInstanceId = this.currentServerInstanceId()
656
+ const credentialProof = this.credentialsMatch(credentials)
657
+ const sourceIdentityMatches = Boolean(this.gate
658
+ && this.gate.serverInstanceId === currentServerInstanceId
659
+ && this.gate.sourceBootId === currentBootId
660
+ && this.gate.sourceGenerationId === currentGenerationId)
661
+ const candidateAdopted = Boolean(this.gate?.adoptedSuccessor)
662
+ const candidateIdentityMatches = Boolean(this.gate?.adoptedSuccessor
663
+ && this.gate.adoptedSuccessor.serverInstanceId === currentServerInstanceId
664
+ && this.gate.adoptedSuccessor.bootId === currentBootId
665
+ && this.gate.adoptedSuccessor.generationId === currentGenerationId)
666
+ const proofValid = credentialProof.leaseMatches
667
+ && credentialProof.operationMatches
668
+ && credentialProof.nonceMatches
669
+ && sourceIdentityMatches
670
+ && !this.blockedGateReason
671
+ return {
672
+ state: this.blockedGateReason ? `blocked_${this.blockedGateReason}` : this.gate ? 'draining' : 'accepting',
673
+ admissionsOpen: !this.gate && !this.blockedGateReason,
674
+ blockedGate: this.blockedGateReason ? {
675
+ reason: this.blockedGateReason,
676
+ version: this.blockedGateVersion,
677
+ } : null,
678
+ activeTotal,
679
+ activeByKind,
680
+ queuedTransitions,
681
+ idle: activeTotal === 0,
682
+ oldestWorkStartedAt: oldestStartedAtMs == null ? null : new Date(oldestStartedAtMs).toISOString(),
683
+ operation: this.gate ? {
684
+ version: this.gate.version,
685
+ operationId: this.gate.operationId,
686
+ operationKind: this.gate.operationKind,
687
+ scope: this.gate.scope,
688
+ postcondition: this.gate.postcondition,
689
+ nonceSha256: this.gate.nonceSha256,
690
+ authorizedSuccessorGenerations: [...this.gate.authorizedSuccessorGenerations],
691
+ serverInstanceId: this.gate.serverInstanceId,
692
+ sourceBootId: this.gate.sourceBootId,
693
+ sourceGenerationId: this.gate.sourceGenerationId,
694
+ startedAt: this.gate.startedAt,
695
+ expiresAt: this.gate.expiresAt,
696
+ carriedAcrossBoot: this.gate.sourceBootId !== currentBootId,
697
+ adoptedSuccessor: this.gate.adoptedSuccessor ?? null,
698
+ } : null,
699
+ restartProof: {
700
+ valid: proofValid,
701
+ ...credentialProof,
702
+ sourceIdentityMatches,
703
+ candidateAdopted,
704
+ candidateIdentityMatches,
705
+ serverInstanceId: currentServerInstanceId,
706
+ bootId: currentBootId,
707
+ generationId: currentGenerationId,
708
+ },
709
+ safeToRestart: this.managed() && proofValid && activeTotal === 0,
710
+ }
711
+ }
712
+ }
713
+
714
+ export const maintenanceLifecycle = new MaintenanceLifecycle()
715
+
716
+ export function acquireMaintenanceWork(
717
+ kind: MaintenanceWorkKind,
718
+ options?: { allowDuringDrain?: boolean; phase?: MaintenanceWorkPhase },
719
+ ): MaintenanceWorkLease {
720
+ return maintenanceLifecycle.acquire(kind, options)
721
+ }
722
+
723
+ /** Read-only boot/background-worker admission check. */
724
+ export function maintenanceAdmissionsOpen(): boolean {
725
+ return maintenanceLifecycle.snapshot().admissionsOpen
726
+ }
727
+
728
+ export function maintenanceErrorPayload(error: MaintenanceLifecycleError) {
729
+ return {
730
+ error: error.code,
731
+ message: error.message,
732
+ retryable: error.retryable,
733
+ ...(error.retryAfterSeconds != null ? { retryAfterSeconds: error.retryAfterSeconds } : {}),
734
+ }
735
+ }