borgmcp 2.0.3 → 2.0.4
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/README.md +2 -2
- package/dist/assimilate-cmd.d.ts +0 -2
- package/dist/assimilate-cmd.d.ts.map +1 -1
- package/dist/assimilate-cmd.js +51 -43
- package/dist/assimilate-cmd.js.map +1 -1
- package/dist/assimilate-deps.d.ts.map +1 -1
- package/dist/assimilate-deps.js +0 -1
- package/dist/assimilate-deps.js.map +1 -1
- package/dist/cleanup-cmd.d.ts +1 -1
- package/dist/cleanup-cmd.d.ts.map +1 -1
- package/dist/cleanup-cmd.js +3 -1
- package/dist/cleanup-cmd.js.map +1 -1
- package/dist/cli-help.d.ts.map +1 -1
- package/dist/cli-help.js +2 -1
- package/dist/cli-help.js.map +1 -1
- package/dist/cubes.d.ts +0 -2
- package/dist/cubes.d.ts.map +1 -1
- package/dist/cubes.js +0 -2
- package/dist/cubes.js.map +1 -1
- package/dist/launch-all-cmd.d.ts.map +1 -1
- package/dist/launch-all-cmd.js +13 -7
- package/dist/launch-all-cmd.js.map +1 -1
- package/dist/log-stream.d.ts +0 -2
- package/dist/log-stream.d.ts.map +1 -1
- package/dist/log-stream.js +3 -30
- package/dist/log-stream.js.map +1 -1
- package/dist/remote-client.d.ts.map +1 -1
- package/dist/remote-client.js +8 -23
- package/dist/remote-client.js.map +1 -1
- package/dist/seat-probe.d.ts +4 -4
- package/dist/seat-probe.d.ts.map +1 -1
- package/dist/seat-probe.js +2 -0
- package/dist/seat-probe.js.map +1 -1
- package/dist/seats.d.ts +0 -74
- package/dist/seats.d.ts.map +1 -1
- package/dist/seats.js +31 -131
- package/dist/seats.js.map +1 -1
- package/dist/server-errors.d.ts +6 -1
- package/dist/server-errors.d.ts.map +1 -1
- package/dist/server-errors.js +9 -0
- package/dist/server-errors.js.map +1 -1
- package/dist/server-facade.d.ts +1 -1
- package/dist/server-facade.d.ts.map +1 -1
- package/dist/server-facade.js +2 -2
- package/dist/server-facade.js.map +1 -1
- package/dist/server-handshake.d.ts +0 -2
- package/dist/server-handshake.d.ts.map +1 -1
- package/dist/server-handshake.js +0 -5
- package/dist/server-handshake.js.map +1 -1
- package/docs/EXTRACTION_PROVENANCE.md +5 -5
- package/docs/LOCAL_SERVER.md +7 -3
- package/docs/RELEASING.md +10 -1
- package/package.json +1 -1
- package/src/assimilate-cmd.ts +54 -51
- package/src/assimilate-deps.ts +0 -1
- package/src/cleanup-cmd.ts +5 -2
- package/src/cli-help.ts +2 -1
- package/src/cubes.ts +0 -4
- package/src/launch-all-cmd.ts +15 -7
- package/src/log-stream.ts +2 -30
- package/src/remote-client.ts +10 -22
- package/src/seat-probe.ts +5 -3
- package/src/seats.ts +35 -186
- package/src/server-errors.ts +9 -2
- package/src/server-facade.ts +2 -2
- package/src/server-handshake.ts +1 -10
- package/dist/session-continuity.d.ts +0 -33
- package/dist/session-continuity.d.ts.map +0 -1
- package/dist/session-continuity.js +0 -220
- package/dist/session-continuity.js.map +0 -1
- package/src/session-continuity.ts +0 -280
package/src/seats.ts
CHANGED
|
@@ -21,6 +21,7 @@ import os from 'os';
|
|
|
21
21
|
import path from 'path';
|
|
22
22
|
import { createHash } from 'node:crypto';
|
|
23
23
|
import { readStoreFile, withStore } from './seat-store.js';
|
|
24
|
+
import { LegacySessionCredentialCollisionError } from './server-errors.js';
|
|
24
25
|
|
|
25
26
|
export const SEATS_FILE = path.join(os.homedir(), '.config', 'borgmcp', 'seats.json');
|
|
26
27
|
const SEATS_VERSION = 1 as const;
|
|
@@ -47,7 +48,6 @@ export interface SeatRecord {
|
|
|
47
48
|
// server metadata (active only)
|
|
48
49
|
droneId?: string;
|
|
49
50
|
sessionId?: string;
|
|
50
|
-
expiresAt?: string;
|
|
51
51
|
// binding + display (set atomically at FINALIZE; absent while pending)
|
|
52
52
|
worktree?: string;
|
|
53
53
|
name?: string;
|
|
@@ -55,9 +55,6 @@ export interface SeatRecord {
|
|
|
55
55
|
roleName?: string;
|
|
56
56
|
roleClass?: 'queen' | 'worker';
|
|
57
57
|
isHumanSeat?: boolean;
|
|
58
|
-
// One crash-safe replacement may coexist with an ACTIVE session until the
|
|
59
|
-
// server confirms the same-seat restore. It is never hydratable or bound.
|
|
60
|
-
replacement?: { credential: string };
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
interface SeatsFile {
|
|
@@ -122,8 +119,8 @@ function isValidOperation(value: unknown): value is SeatOperation {
|
|
|
122
119
|
* checked — the ref is well-formed and self-consistent (the map key equals the
|
|
123
120
|
* record's derived ref), state ∈ {pending,active}, the credential is a non-empty
|
|
124
121
|
* string, the operation is well-shaped, an ACTIVE record carries ALL its required
|
|
125
|
-
* server + binding fields, and a PENDING record carries NO active-only
|
|
126
|
-
*
|
|
122
|
+
* server + binding fields, and a PENDING record carries NO active-only session
|
|
123
|
+
* id. A single invalid entry ⇒ the whole store is rejected
|
|
127
124
|
* (fail closed at the caller, bytes preserved) — never a silent cast.
|
|
128
125
|
*/
|
|
129
126
|
function isValidSeatRecord(ref: string, value: unknown): value is SeatRecord {
|
|
@@ -151,32 +148,12 @@ function isValidSeatRecord(ref: string, value: unknown): value is SeatRecord {
|
|
|
151
148
|
if (r.worktree !== undefined && typeof r.worktree !== 'string') return false;
|
|
152
149
|
if (r.droneId !== undefined && (typeof r.droneId !== 'string' || !UUID_RE.test(r.droneId))) return false;
|
|
153
150
|
if (r.sessionId !== undefined && (typeof r.sessionId !== 'string' || !UUID_RE.test(r.sessionId))) return false;
|
|
154
|
-
if (
|
|
155
|
-
r.expiresAt !== undefined &&
|
|
156
|
-
(typeof r.expiresAt !== 'string' || !Number.isFinite(Date.parse(r.expiresAt)))
|
|
157
|
-
) {
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
if (r.replacement !== undefined) {
|
|
161
|
-
if (
|
|
162
|
-
r.state !== 'active' ||
|
|
163
|
-
r.replacement === null ||
|
|
164
|
-
typeof r.replacement !== 'object' ||
|
|
165
|
-
Array.isArray(r.replacement) ||
|
|
166
|
-
Object.keys(r.replacement as object).length !== 1 ||
|
|
167
|
-
typeof (r.replacement as { credential?: unknown }).credential !== 'string' ||
|
|
168
|
-
!/^[A-Za-z0-9_-]{43}$/.test((r.replacement as { credential: string }).credential)
|
|
169
|
-
) {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
151
|
// State-consistency invariants (no inconsistent active|pending).
|
|
174
152
|
if (r.state === 'active') {
|
|
175
153
|
// An ACTIVE record MUST carry its full server session + worktree binding.
|
|
176
154
|
if (
|
|
177
155
|
typeof r.droneId !== 'string' ||
|
|
178
156
|
typeof r.sessionId !== 'string' ||
|
|
179
|
-
typeof r.expiresAt !== 'string' ||
|
|
180
157
|
typeof r.worktree !== 'string' ||
|
|
181
158
|
typeof r.name !== 'string' ||
|
|
182
159
|
typeof r.droneLabel !== 'string'
|
|
@@ -185,7 +162,7 @@ function isValidSeatRecord(ref: string, value: unknown): value is SeatRecord {
|
|
|
185
162
|
}
|
|
186
163
|
} else {
|
|
187
164
|
// A PENDING record must NOT carry active-only server session fields.
|
|
188
|
-
if (r.sessionId !== undefined
|
|
165
|
+
if (r.sessionId !== undefined) return false;
|
|
189
166
|
}
|
|
190
167
|
// The map key must equal the record's derived ref (no cross-key aliasing).
|
|
191
168
|
return seatRef(value as SeatRecord) === ref;
|
|
@@ -214,6 +191,37 @@ function parseStore(raw: string): SeatsFile | null {
|
|
|
214
191
|
) {
|
|
215
192
|
const seats = candidate.seats as Record<string, unknown>;
|
|
216
193
|
for (const [ref, record] of Object.entries(seats)) {
|
|
194
|
+
if (
|
|
195
|
+
record !== null &&
|
|
196
|
+
typeof record === 'object' &&
|
|
197
|
+
!Array.isArray(record) &&
|
|
198
|
+
Object.prototype.hasOwnProperty.call(record, 'replacement')
|
|
199
|
+
) {
|
|
200
|
+
const { replacement, ...withoutReplacement } = record as Record<string, unknown>;
|
|
201
|
+
const exactReplacement =
|
|
202
|
+
replacement !== null &&
|
|
203
|
+
typeof replacement === 'object' &&
|
|
204
|
+
!Array.isArray(replacement) &&
|
|
205
|
+
Object.keys(replacement).length === 1 &&
|
|
206
|
+
typeof (replacement as { credential?: unknown }).credential === 'string' &&
|
|
207
|
+
/^[A-Za-z0-9_-]{43}$/.test((replacement as { credential: string }).credential);
|
|
208
|
+
let canonicalOrigin = false;
|
|
209
|
+
try {
|
|
210
|
+
const origin = new URL(String(withoutReplacement.origin));
|
|
211
|
+
canonicalOrigin = origin.protocol === 'https:' && origin.origin === withoutReplacement.origin;
|
|
212
|
+
} catch {
|
|
213
|
+
canonicalOrigin = false;
|
|
214
|
+
}
|
|
215
|
+
if (
|
|
216
|
+
withoutReplacement.state === 'active' &&
|
|
217
|
+
exactReplacement &&
|
|
218
|
+
canonicalOrigin &&
|
|
219
|
+
isValidSeatRecord(ref, withoutReplacement)
|
|
220
|
+
) {
|
|
221
|
+
throw new LegacySessionCredentialCollisionError(withoutReplacement.origin as string);
|
|
222
|
+
}
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
217
225
|
if (!isValidSeatRecord(ref, record)) return null;
|
|
218
226
|
}
|
|
219
227
|
return { version: SEATS_VERSION, seats: seats as Record<string, SeatRecord> };
|
|
@@ -297,17 +305,6 @@ export async function getActiveSeatCredential(
|
|
|
297
305
|
return record.credential;
|
|
298
306
|
}
|
|
299
307
|
|
|
300
|
-
/** Exact ACTIVE record for the internal continuity coordinator. */
|
|
301
|
-
export async function getActiveSeat(
|
|
302
|
-
ref: string,
|
|
303
|
-
binding: { origin: string; trustIdentity: string; cubeId: string },
|
|
304
|
-
): Promise<SeatRecord | null> {
|
|
305
|
-
if (!REF_RE.test(ref)) return null;
|
|
306
|
-
const store = await readStore();
|
|
307
|
-
const record = store.seats[ref];
|
|
308
|
-
return recordMatches(record, ref, binding) && record.state === 'active' ? record : null;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
308
|
// ─── PREPARE / mint (no worktree yet) ────────────────────────────────────────
|
|
312
309
|
|
|
313
310
|
/**
|
|
@@ -471,7 +468,6 @@ export async function activateAndBindSeat(input: {
|
|
|
471
468
|
operation: SeatOperation;
|
|
472
469
|
droneId: string;
|
|
473
470
|
sessionId: string;
|
|
474
|
-
expiresAt: string;
|
|
475
471
|
expectedPendingDigest: string;
|
|
476
472
|
worktree: string;
|
|
477
473
|
name: string;
|
|
@@ -483,9 +479,6 @@ export async function activateAndBindSeat(input: {
|
|
|
483
479
|
if (!UUID_RE.test(input.droneId) || !UUID_RE.test(input.sessionId)) {
|
|
484
480
|
throw new Error('invalid Borg server session identity');
|
|
485
481
|
}
|
|
486
|
-
if (typeof input.expiresAt !== 'string' || !Number.isFinite(Date.parse(input.expiresAt))) {
|
|
487
|
-
throw new Error('invalid Borg server session expiry');
|
|
488
|
-
}
|
|
489
482
|
const ref = seatRef(input);
|
|
490
483
|
const binding = { origin: input.origin, trustIdentity: input.trustIdentity, cubeId: input.cubeId };
|
|
491
484
|
return withStore<SeatsFile, ActivateSeatOutcome>(SEATS_FILE, emptyStore, parseStore, async (txn) => {
|
|
@@ -497,7 +490,6 @@ export async function activateAndBindSeat(input: {
|
|
|
497
490
|
state: 'active',
|
|
498
491
|
droneId: input.droneId,
|
|
499
492
|
sessionId: input.sessionId,
|
|
500
|
-
expiresAt: input.expiresAt,
|
|
501
493
|
worktree: input.worktree,
|
|
502
494
|
name: input.name,
|
|
503
495
|
droneLabel: input.droneLabel,
|
|
@@ -510,149 +502,6 @@ export async function activateAndBindSeat(input: {
|
|
|
510
502
|
});
|
|
511
503
|
}
|
|
512
504
|
|
|
513
|
-
export type PrepareSeatReplacementOutcome =
|
|
514
|
-
| { ok: true; credential: string; digest: string }
|
|
515
|
-
| { ok: false; reason: 'expectation-mismatch' };
|
|
516
|
-
|
|
517
|
-
/**
|
|
518
|
-
* Preserve the exact ACTIVE seat while preparing one durable replacement bearer.
|
|
519
|
-
* A lost response or process restart reuses the stored replacement; a changed
|
|
520
|
-
* authority, drone, or active bearer fails before any mutation.
|
|
521
|
-
*/
|
|
522
|
-
export async function prepareSeatReplacement(input: {
|
|
523
|
-
ref: string;
|
|
524
|
-
binding: { origin: string; trustIdentity: string; cubeId: string };
|
|
525
|
-
expectedDroneId: string;
|
|
526
|
-
expectedActiveDigest: string;
|
|
527
|
-
replacementCredential: string;
|
|
528
|
-
}): Promise<PrepareSeatReplacementOutcome> {
|
|
529
|
-
if (!REF_RE.test(input.ref) || !/^[A-Za-z0-9_-]{43}$/.test(input.replacementCredential)) {
|
|
530
|
-
return { ok: false, reason: 'expectation-mismatch' };
|
|
531
|
-
}
|
|
532
|
-
return withStore<SeatsFile, PrepareSeatReplacementOutcome>(SEATS_FILE, emptyStore, parseStore, async (txn) => {
|
|
533
|
-
const record = txn.data.seats[input.ref];
|
|
534
|
-
if (
|
|
535
|
-
!recordMatches(record, input.ref, input.binding) ||
|
|
536
|
-
record.state !== 'active' ||
|
|
537
|
-
record.droneId !== input.expectedDroneId ||
|
|
538
|
-
digestOf(record.credential) !== input.expectedActiveDigest
|
|
539
|
-
) {
|
|
540
|
-
return { ok: false as const, reason: 'expectation-mismatch' as const };
|
|
541
|
-
}
|
|
542
|
-
if (record.replacement) {
|
|
543
|
-
return {
|
|
544
|
-
ok: true as const,
|
|
545
|
-
credential: record.replacement.credential,
|
|
546
|
-
digest: digestOf(record.replacement.credential),
|
|
547
|
-
};
|
|
548
|
-
}
|
|
549
|
-
record.replacement = { credential: input.replacementCredential };
|
|
550
|
-
await txn.commit();
|
|
551
|
-
return {
|
|
552
|
-
ok: true as const,
|
|
553
|
-
credential: input.replacementCredential,
|
|
554
|
-
digest: digestOf(input.replacementCredential),
|
|
555
|
-
};
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
export type PromoteSeatReplacementOutcome = 'promoted' | 'missing' | 'replaced';
|
|
560
|
-
|
|
561
|
-
/** Update only the committed expiry for an exact same-bearer session renewal. */
|
|
562
|
-
export async function refreshActiveSeatSession(input: {
|
|
563
|
-
ref: string;
|
|
564
|
-
binding: { origin: string; trustIdentity: string; cubeId: string };
|
|
565
|
-
expectedDroneId: string;
|
|
566
|
-
expectedActiveDigest: string;
|
|
567
|
-
expectedSessionId: string;
|
|
568
|
-
expiresAt: string;
|
|
569
|
-
}): Promise<boolean> {
|
|
570
|
-
if (!UUID_RE.test(input.expectedDroneId) || !UUID_RE.test(input.expectedSessionId)) {
|
|
571
|
-
throw new Error('invalid Borg server session identity');
|
|
572
|
-
}
|
|
573
|
-
if (!Number.isFinite(Date.parse(input.expiresAt))) {
|
|
574
|
-
throw new Error('invalid Borg server session expiry');
|
|
575
|
-
}
|
|
576
|
-
return withStore<SeatsFile, boolean>(SEATS_FILE, emptyStore, parseStore, async (txn) => {
|
|
577
|
-
const record = txn.data.seats[input.ref];
|
|
578
|
-
if (
|
|
579
|
-
!recordMatches(record, input.ref, input.binding) ||
|
|
580
|
-
record.state !== 'active' ||
|
|
581
|
-
record.droneId !== input.expectedDroneId ||
|
|
582
|
-
record.sessionId !== input.expectedSessionId ||
|
|
583
|
-
digestOf(record.credential) !== input.expectedActiveDigest
|
|
584
|
-
) {
|
|
585
|
-
return false;
|
|
586
|
-
}
|
|
587
|
-
record.expiresAt = input.expiresAt;
|
|
588
|
-
await txn.commit();
|
|
589
|
-
return true;
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/** Atomically promote only the response-bound replacement onto the same ACTIVE seat. */
|
|
594
|
-
export async function promoteSeatReplacement(input: {
|
|
595
|
-
ref: string;
|
|
596
|
-
binding: { origin: string; trustIdentity: string; cubeId: string };
|
|
597
|
-
expectedDroneId: string;
|
|
598
|
-
expectedActiveDigest: string;
|
|
599
|
-
expectedReplacementDigest: string;
|
|
600
|
-
sessionId: string;
|
|
601
|
-
expiresAt: string;
|
|
602
|
-
}): Promise<PromoteSeatReplacementOutcome> {
|
|
603
|
-
if (!UUID_RE.test(input.expectedDroneId) || !UUID_RE.test(input.sessionId)) {
|
|
604
|
-
throw new Error('invalid Borg server session identity');
|
|
605
|
-
}
|
|
606
|
-
if (!Number.isFinite(Date.parse(input.expiresAt))) {
|
|
607
|
-
throw new Error('invalid Borg server session expiry');
|
|
608
|
-
}
|
|
609
|
-
return withStore<SeatsFile, PromoteSeatReplacementOutcome>(SEATS_FILE, emptyStore, parseStore, async (txn) => {
|
|
610
|
-
const record = txn.data.seats[input.ref];
|
|
611
|
-
if (!recordMatches(record, input.ref, input.binding) || record.state !== 'active') return 'missing';
|
|
612
|
-
if (
|
|
613
|
-
record.droneId !== input.expectedDroneId ||
|
|
614
|
-
digestOf(record.credential) !== input.expectedActiveDigest ||
|
|
615
|
-
!record.replacement ||
|
|
616
|
-
digestOf(record.replacement.credential) !== input.expectedReplacementDigest
|
|
617
|
-
) {
|
|
618
|
-
return 'replaced';
|
|
619
|
-
}
|
|
620
|
-
const { replacement, ...active } = record;
|
|
621
|
-
txn.data.seats[input.ref] = {
|
|
622
|
-
...active,
|
|
623
|
-
credential: replacement.credential,
|
|
624
|
-
sessionId: input.sessionId,
|
|
625
|
-
expiresAt: input.expiresAt,
|
|
626
|
-
};
|
|
627
|
-
await txn.commit();
|
|
628
|
-
return 'promoted';
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
/** Remove only the caller's exact pending replacement, never the ACTIVE seat. */
|
|
633
|
-
export async function scrubSeatReplacement(input: {
|
|
634
|
-
ref: string;
|
|
635
|
-
binding: { origin: string; trustIdentity: string; cubeId: string };
|
|
636
|
-
expectedActiveDigest: string;
|
|
637
|
-
expectedReplacementDigest: string;
|
|
638
|
-
}): Promise<boolean> {
|
|
639
|
-
return withStore<SeatsFile, boolean>(SEATS_FILE, emptyStore, parseStore, async (txn) => {
|
|
640
|
-
const record = txn.data.seats[input.ref];
|
|
641
|
-
if (
|
|
642
|
-
!recordMatches(record, input.ref, input.binding) ||
|
|
643
|
-
record.state !== 'active' ||
|
|
644
|
-
digestOf(record.credential) !== input.expectedActiveDigest ||
|
|
645
|
-
!record.replacement ||
|
|
646
|
-
digestOf(record.replacement.credential) !== input.expectedReplacementDigest
|
|
647
|
-
) {
|
|
648
|
-
return false;
|
|
649
|
-
}
|
|
650
|
-
delete record.replacement;
|
|
651
|
-
await txn.commit();
|
|
652
|
-
return true;
|
|
653
|
-
});
|
|
654
|
-
}
|
|
655
|
-
|
|
656
505
|
// ─── CR#2: bind a PENDING record to a preserved worktree (no activation) ─────
|
|
657
506
|
|
|
658
507
|
export type BindPendingSeatOutcome = 'bound' | 'missing' | 'replaced';
|
package/src/server-errors.ts
CHANGED
|
@@ -5,8 +5,7 @@ export type BorgServerErrorCode =
|
|
|
5
5
|
| 'CREATE_CUBE_DENIED'
|
|
6
6
|
| 'ATTACH_CONFLICT'
|
|
7
7
|
| 'SESSION_REJECTED'
|
|
8
|
-
| 'SESSION_REVOKED'
|
|
9
|
-
| 'AUTH_EXPIRED';
|
|
8
|
+
| 'SESSION_REVOKED';
|
|
10
9
|
|
|
11
10
|
/** Safe, non-secret state code for deterministic authority recovery copy. */
|
|
12
11
|
export class BorgServerError extends Error {
|
|
@@ -92,4 +91,12 @@ export class BorgServerUnreachableError extends Error {
|
|
|
92
91
|
this.name = 'BorgServerUnreachableError';
|
|
93
92
|
}
|
|
94
93
|
}
|
|
94
|
+
|
|
95
|
+
/** Exact retired TTL-replacement state: two saved bearers and no safe implicit winner. */
|
|
96
|
+
export class LegacySessionCredentialCollisionError extends Error {
|
|
97
|
+
constructor(public readonly origin: string) {
|
|
98
|
+
super('Local session credential collision detected');
|
|
99
|
+
this.name = 'LegacySessionCredentialCollisionError';
|
|
100
|
+
}
|
|
101
|
+
}
|
|
95
102
|
import type { ErrorCode } from 'borgmcp-shared/protocol';
|
package/src/server-facade.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn as spawnChild, type SpawnOptions } from 'node:child_process';
|
|
|
2
2
|
import { constants } from 'node:os';
|
|
3
3
|
import { serverHelpText } from './cli-help.js';
|
|
4
4
|
|
|
5
|
-
export const SERVER_LIFECYCLE_COMMANDS = ['setup', 'start', 'status', 'update', 'invite'] as const;
|
|
5
|
+
export const SERVER_LIFECYCLE_COMMANDS = ['setup', 'start', 'stop', 'status', 'update', 'invite'] as const;
|
|
6
6
|
export type ServerLifecycleCommand = typeof SERVER_LIFECYCLE_COMMANDS[number];
|
|
7
7
|
|
|
8
8
|
export type ParsedServerFacadeArgs =
|
|
@@ -86,7 +86,7 @@ function inertCommand(command: string): string {
|
|
|
86
86
|
export function unknownServerCommandText(command: string): string {
|
|
87
87
|
return (
|
|
88
88
|
`Unknown server command: ${inertCommand(command)}.\n` +
|
|
89
|
-
`Available commands: setup, start, status, update, invite.\n` +
|
|
89
|
+
`Available commands: setup, start, stop, status, update, invite.\n` +
|
|
90
90
|
`Next: run borg server --help.\n`
|
|
91
91
|
);
|
|
92
92
|
}
|
package/src/server-handshake.ts
CHANGED
|
@@ -175,7 +175,6 @@ export interface ServerAttachResult {
|
|
|
175
175
|
session: {
|
|
176
176
|
credentialRef: string;
|
|
177
177
|
sessionId: string;
|
|
178
|
-
expiresAt: string;
|
|
179
178
|
};
|
|
180
179
|
result: 'created' | 'reused';
|
|
181
180
|
}
|
|
@@ -202,7 +201,7 @@ export interface PreparedServerAttach {
|
|
|
202
201
|
cube: ServerAttachResult['cube'];
|
|
203
202
|
role: ServerAttachResult['role'];
|
|
204
203
|
drone: ServerAttachResult['drone'];
|
|
205
|
-
session: { sessionId: string
|
|
204
|
+
session: { sessionId: string };
|
|
206
205
|
result: 'created' | 'reused';
|
|
207
206
|
credentialRef: string;
|
|
208
207
|
pendingBearerDigest: string;
|
|
@@ -304,12 +303,6 @@ export async function sendBorgServerAttach(
|
|
|
304
303
|
'Borg server rejected the session: the seat is already bound to another session',
|
|
305
304
|
);
|
|
306
305
|
}
|
|
307
|
-
if (rejectedCode === ErrorCode.AUTH_EXPIRED) {
|
|
308
|
-
throw new BorgServerError(
|
|
309
|
-
'AUTH_EXPIRED',
|
|
310
|
-
'Borg server session expired',
|
|
311
|
-
);
|
|
312
|
-
}
|
|
313
306
|
if (rejectedCode === ErrorCode.SESSION_REVOKED) {
|
|
314
307
|
throw new BorgServerError(
|
|
315
308
|
'SESSION_REVOKED',
|
|
@@ -358,7 +351,6 @@ export async function sendBorgServerAttach(
|
|
|
358
351
|
drone: decoded.drone,
|
|
359
352
|
session: {
|
|
360
353
|
sessionId: decoded.session.id,
|
|
361
|
-
expiresAt: decoded.session.expires_at,
|
|
362
354
|
},
|
|
363
355
|
result: decoded.result,
|
|
364
356
|
credentialRef,
|
|
@@ -374,7 +366,6 @@ export async function sendBorgServerAttach(
|
|
|
374
366
|
...seatInput,
|
|
375
367
|
droneId: decoded.drone.id,
|
|
376
368
|
sessionId: decoded.session.id,
|
|
377
|
-
expiresAt: decoded.session.expires_at,
|
|
378
369
|
expectedPendingDigest: pendingBearerDigest,
|
|
379
370
|
worktree: binding.worktree,
|
|
380
371
|
name: binding.name,
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { type ActiveCube } from './cubes.js';
|
|
2
|
-
import { prepareSeatReplacement, promoteSeatReplacement, refreshActiveSeatSession, scrubSeatReplacement, type SeatRecord } from './seats.js';
|
|
3
|
-
export declare const SESSION_RENEWAL_LEAD_MS: number;
|
|
4
|
-
interface AttachOutcome {
|
|
5
|
-
result: 'created' | 'reused';
|
|
6
|
-
droneId: string;
|
|
7
|
-
sessionId: string;
|
|
8
|
-
expiresAt: string;
|
|
9
|
-
}
|
|
10
|
-
interface SessionContinuityDeps {
|
|
11
|
-
now(): number;
|
|
12
|
-
randomCredential(): string;
|
|
13
|
-
getSeat(ref: string, binding: {
|
|
14
|
-
origin: string;
|
|
15
|
-
trustIdentity: string;
|
|
16
|
-
cubeId: string;
|
|
17
|
-
}): Promise<SeatRecord | null>;
|
|
18
|
-
getActive(): Promise<ActiveCube | null>;
|
|
19
|
-
getParentCredential(origin: string, trustIdentity: string): Promise<string | null>;
|
|
20
|
-
sendAttach(record: SeatRecord, bearer: string, parentCredential: string): Promise<AttachOutcome>;
|
|
21
|
-
prepareReplacement: typeof prepareSeatReplacement;
|
|
22
|
-
refreshSession: typeof refreshActiveSeatSession;
|
|
23
|
-
promoteReplacement: typeof promoteSeatReplacement;
|
|
24
|
-
scrubReplacement: typeof scrubSeatReplacement;
|
|
25
|
-
}
|
|
26
|
-
export declare function ensureLocalSessionFresh(active: ActiveCube): Promise<ActiveCube>;
|
|
27
|
-
export declare function recoverExpiredLocalSession(active: ActiveCube): Promise<ActiveCube>;
|
|
28
|
-
/** @internal */
|
|
29
|
-
export declare function __renewLocalSessionForTest(active: ActiveCube, mode: 'proactive' | 'expired', deps: SessionContinuityDeps): Promise<ActiveCube>;
|
|
30
|
-
/** @internal */
|
|
31
|
-
export declare function __resetSessionContinuityForTest(): void;
|
|
32
|
-
export {};
|
|
33
|
-
//# sourceMappingURL=session-continuity.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"session-continuity.d.ts","sourceRoot":"","sources":["../src/session-continuity.ts"],"names":[],"mappings":"AAEA,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAK5D,OAAO,EAEL,sBAAsB,EACtB,sBAAsB,EACtB,wBAAwB,EACxB,oBAAoB,EAEpB,KAAK,UAAU,EAChB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,uBAAuB,QAAa,CAAC;AAElD,UAAU,aAAa;IACrB,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,qBAAqB;IAC7B,GAAG,IAAI,MAAM,CAAC;IACd,gBAAgB,IAAI,MAAM,CAAC;IAC3B,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACrH,SAAS,IAAI,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACxC,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACnF,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACjG,kBAAkB,EAAE,OAAO,sBAAsB,CAAC;IAClD,cAAc,EAAE,OAAO,wBAAwB,CAAC;IAChD,kBAAkB,EAAE,OAAO,sBAAsB,CAAC;IAClD,gBAAgB,EAAE,OAAO,oBAAoB,CAAC;CAC/C;AA8ND,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAE/E;AAED,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAElF;AAED,gBAAgB;AAChB,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,UAAU,EAClB,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAErB;AAED,gBAAgB;AAChB,wBAAgB,+BAA+B,IAAI,IAAI,CAEtD"}
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
-
import { getServerCredential } from './config.js';
|
|
3
|
-
import { getActiveCube } from './cubes.js';
|
|
4
|
-
import { DroneEvictedError } from './drone-lifecycle.js';
|
|
5
|
-
import { sendBorgServerAttach } from './server-handshake.js';
|
|
6
|
-
import { BorgServerError, BorgServerTrustError } from './server-errors.js';
|
|
7
|
-
import { loadBorgServerTrust } from './server-trust.js';
|
|
8
|
-
import { getActiveSeat, prepareSeatReplacement, promoteSeatReplacement, refreshActiveSeatSession, scrubSeatReplacement, seatRef, } from './seats.js';
|
|
9
|
-
export const SESSION_RENEWAL_LEAD_MS = 5 * 60_000;
|
|
10
|
-
const defaultDeps = {
|
|
11
|
-
now: Date.now,
|
|
12
|
-
randomCredential: () => randomBytes(32).toString('base64url'),
|
|
13
|
-
getSeat: getActiveSeat,
|
|
14
|
-
getActive: getActiveCube,
|
|
15
|
-
getParentCredential: getServerCredential,
|
|
16
|
-
async sendAttach(record, bearer, parentCredential) {
|
|
17
|
-
const trust = await loadBorgServerTrust(record.origin);
|
|
18
|
-
if (trust.identity !== record.trustIdentity) {
|
|
19
|
-
throw new BorgServerTrustError('Borg server trust identity changed; refusing session renewal');
|
|
20
|
-
}
|
|
21
|
-
const attached = await sendBorgServerAttach(record.origin, record.trustIdentity, parentCredential, {
|
|
22
|
-
cubeId: record.cubeId,
|
|
23
|
-
roleId: record.roleId,
|
|
24
|
-
operation: record.operation,
|
|
25
|
-
priorDroneId: record.droneId,
|
|
26
|
-
}, bearer, { fetchImpl: trust.fetchImpl });
|
|
27
|
-
return {
|
|
28
|
-
result: attached.result,
|
|
29
|
-
droneId: attached.drone.id,
|
|
30
|
-
sessionId: attached.session.sessionId,
|
|
31
|
-
expiresAt: attached.session.expiresAt,
|
|
32
|
-
};
|
|
33
|
-
},
|
|
34
|
-
prepareReplacement: prepareSeatReplacement,
|
|
35
|
-
refreshSession: refreshActiveSeatSession,
|
|
36
|
-
promoteReplacement: promoteSeatReplacement,
|
|
37
|
-
scrubReplacement: scrubSeatReplacement,
|
|
38
|
-
};
|
|
39
|
-
const inflight = new Map();
|
|
40
|
-
const digestOf = (value) => createHash('sha256').update(value).digest('hex');
|
|
41
|
-
function sameSeat(active, candidate) {
|
|
42
|
-
return candidate !== null &&
|
|
43
|
-
candidate.apiUrl === active.apiUrl &&
|
|
44
|
-
candidate.serverTrustIdentity === active.serverTrustIdentity &&
|
|
45
|
-
candidate.cubeId === active.cubeId &&
|
|
46
|
-
candidate.droneId === active.droneId &&
|
|
47
|
-
candidate.localSessionCredentialRef === active.localSessionCredentialRef;
|
|
48
|
-
}
|
|
49
|
-
function seatBinding(record) {
|
|
50
|
-
return {
|
|
51
|
-
origin: record.origin,
|
|
52
|
-
trustIdentity: record.trustIdentity,
|
|
53
|
-
cubeId: record.cubeId,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
function validActiveRecord(active, record) {
|
|
57
|
-
return record !== null &&
|
|
58
|
-
record.state === 'active' &&
|
|
59
|
-
record.origin === active.apiUrl &&
|
|
60
|
-
record.trustIdentity === active.serverTrustIdentity &&
|
|
61
|
-
record.cubeId === active.cubeId &&
|
|
62
|
-
record.droneId === active.droneId &&
|
|
63
|
-
seatRef(record) === active.localSessionCredentialRef;
|
|
64
|
-
}
|
|
65
|
-
function deterministicAttachFailure(error) {
|
|
66
|
-
return error instanceof BorgServerError ||
|
|
67
|
-
error instanceof BorgServerTrustError ||
|
|
68
|
-
error instanceof DroneEvictedError;
|
|
69
|
-
}
|
|
70
|
-
async function reloadSameSeat(active, deps, requireFreshCredential = false) {
|
|
71
|
-
const current = await deps.getActive();
|
|
72
|
-
if (!sameSeat(active, current) || (requireFreshCredential && current.sessionToken === active.sessionToken)) {
|
|
73
|
-
throw new Error('Session renewal no longer targets the same saved seat');
|
|
74
|
-
}
|
|
75
|
-
return current;
|
|
76
|
-
}
|
|
77
|
-
async function performRenewal(active, mode, deps) {
|
|
78
|
-
if (!active.serverTrustIdentity || !active.localSessionCredentialRef) {
|
|
79
|
-
throw new Error('Selected Borg server authority state is missing or unreadable');
|
|
80
|
-
}
|
|
81
|
-
const binding = {
|
|
82
|
-
origin: active.apiUrl,
|
|
83
|
-
trustIdentity: active.serverTrustIdentity,
|
|
84
|
-
cubeId: active.cubeId,
|
|
85
|
-
};
|
|
86
|
-
const record = await deps.getSeat(active.localSessionCredentialRef, binding);
|
|
87
|
-
if (!validActiveRecord(active, record)) {
|
|
88
|
-
throw new Error('Session renewal no longer targets the same saved seat');
|
|
89
|
-
}
|
|
90
|
-
const activeDigest = digestOf(active.sessionToken);
|
|
91
|
-
if (digestOf(record.credential) !== activeDigest) {
|
|
92
|
-
return reloadSameSeat(active, deps, true);
|
|
93
|
-
}
|
|
94
|
-
const expiresIn = Date.parse(record.expiresAt) - deps.now();
|
|
95
|
-
if (mode === 'proactive' && expiresIn > SESSION_RENEWAL_LEAD_MS && record.replacement === undefined) {
|
|
96
|
-
return active;
|
|
97
|
-
}
|
|
98
|
-
const parentCredential = await deps.getParentCredential(record.origin, record.trustIdentity);
|
|
99
|
-
if (!parentCredential)
|
|
100
|
-
throw new Error('Selected Borg server parent credential is missing or unreadable');
|
|
101
|
-
const recordBinding = seatBinding(record);
|
|
102
|
-
let restoreRequired = mode === 'expired' || expiresIn <= 0 || record.replacement !== undefined;
|
|
103
|
-
if (!restoreRequired) {
|
|
104
|
-
let attached = null;
|
|
105
|
-
try {
|
|
106
|
-
attached = await deps.sendAttach(record, record.credential, parentCredential);
|
|
107
|
-
}
|
|
108
|
-
catch (error) {
|
|
109
|
-
if (error instanceof BorgServerError && error.code === 'AUTH_EXPIRED') {
|
|
110
|
-
restoreRequired = true;
|
|
111
|
-
}
|
|
112
|
-
else {
|
|
113
|
-
throw error;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (!restoreRequired) {
|
|
117
|
-
if (!attached)
|
|
118
|
-
throw new Error('Session renewal returned no result');
|
|
119
|
-
const renewedExpiry = Date.parse(attached.expiresAt);
|
|
120
|
-
if (attached.result !== 'reused' ||
|
|
121
|
-
attached.droneId !== record.droneId ||
|
|
122
|
-
attached.sessionId !== record.sessionId ||
|
|
123
|
-
!Number.isFinite(renewedExpiry) ||
|
|
124
|
-
renewedExpiry <= Date.parse(record.expiresAt)) {
|
|
125
|
-
throw new Error('Session renewal did not preserve the same saved seat');
|
|
126
|
-
}
|
|
127
|
-
const refreshed = await deps.refreshSession({
|
|
128
|
-
ref: active.localSessionCredentialRef,
|
|
129
|
-
binding: recordBinding,
|
|
130
|
-
expectedDroneId: record.droneId,
|
|
131
|
-
expectedActiveDigest: activeDigest,
|
|
132
|
-
expectedSessionId: record.sessionId,
|
|
133
|
-
expiresAt: attached.expiresAt,
|
|
134
|
-
});
|
|
135
|
-
if (!refreshed)
|
|
136
|
-
return reloadSameSeat(active, deps);
|
|
137
|
-
return reloadSameSeat(active, deps);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
const replacement = await deps.prepareReplacement({
|
|
141
|
-
ref: active.localSessionCredentialRef,
|
|
142
|
-
binding: recordBinding,
|
|
143
|
-
expectedDroneId: record.droneId,
|
|
144
|
-
expectedActiveDigest: activeDigest,
|
|
145
|
-
replacementCredential: deps.randomCredential(),
|
|
146
|
-
});
|
|
147
|
-
if (!replacement.ok)
|
|
148
|
-
return reloadSameSeat(active, deps, true);
|
|
149
|
-
let attached;
|
|
150
|
-
try {
|
|
151
|
-
attached = await deps.sendAttach(record, replacement.credential, parentCredential);
|
|
152
|
-
}
|
|
153
|
-
catch (error) {
|
|
154
|
-
if (deterministicAttachFailure(error)) {
|
|
155
|
-
await deps.scrubReplacement({
|
|
156
|
-
ref: active.localSessionCredentialRef,
|
|
157
|
-
binding: recordBinding,
|
|
158
|
-
expectedActiveDigest: activeDigest,
|
|
159
|
-
expectedReplacementDigest: replacement.digest,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
throw error;
|
|
163
|
-
}
|
|
164
|
-
const restoredExpiry = Date.parse(attached.expiresAt);
|
|
165
|
-
if ((attached.result !== 'created' && attached.result !== 'reused') ||
|
|
166
|
-
attached.droneId !== record.droneId ||
|
|
167
|
-
attached.sessionId === record.sessionId ||
|
|
168
|
-
!Number.isFinite(restoredExpiry) ||
|
|
169
|
-
restoredExpiry <= deps.now()) {
|
|
170
|
-
await deps.scrubReplacement({
|
|
171
|
-
ref: active.localSessionCredentialRef,
|
|
172
|
-
binding: recordBinding,
|
|
173
|
-
expectedActiveDigest: activeDigest,
|
|
174
|
-
expectedReplacementDigest: replacement.digest,
|
|
175
|
-
});
|
|
176
|
-
throw new Error('Session renewal did not preserve the same saved seat');
|
|
177
|
-
}
|
|
178
|
-
const promoted = await deps.promoteReplacement({
|
|
179
|
-
ref: active.localSessionCredentialRef,
|
|
180
|
-
binding: recordBinding,
|
|
181
|
-
expectedDroneId: record.droneId,
|
|
182
|
-
expectedActiveDigest: activeDigest,
|
|
183
|
-
expectedReplacementDigest: replacement.digest,
|
|
184
|
-
sessionId: attached.sessionId,
|
|
185
|
-
expiresAt: attached.expiresAt,
|
|
186
|
-
});
|
|
187
|
-
if (promoted !== 'promoted')
|
|
188
|
-
return reloadSameSeat(active, deps, true);
|
|
189
|
-
return reloadSameSeat(active, deps, true);
|
|
190
|
-
}
|
|
191
|
-
function renewLocalSession(active, mode, deps) {
|
|
192
|
-
const key = active.localSessionCredentialRef;
|
|
193
|
-
if (!key)
|
|
194
|
-
return Promise.reject(new Error('Selected Borg server authority state is missing or unreadable'));
|
|
195
|
-
const existing = inflight.get(key);
|
|
196
|
-
if (existing)
|
|
197
|
-
return existing;
|
|
198
|
-
const renewal = performRenewal(active, mode, deps);
|
|
199
|
-
inflight.set(key, renewal);
|
|
200
|
-
void renewal.finally(() => {
|
|
201
|
-
if (inflight.get(key) === renewal)
|
|
202
|
-
inflight.delete(key);
|
|
203
|
-
}).catch(() => { });
|
|
204
|
-
return renewal;
|
|
205
|
-
}
|
|
206
|
-
export function ensureLocalSessionFresh(active) {
|
|
207
|
-
return renewLocalSession(active, 'proactive', defaultDeps);
|
|
208
|
-
}
|
|
209
|
-
export function recoverExpiredLocalSession(active) {
|
|
210
|
-
return renewLocalSession(active, 'expired', defaultDeps);
|
|
211
|
-
}
|
|
212
|
-
/** @internal */
|
|
213
|
-
export function __renewLocalSessionForTest(active, mode, deps) {
|
|
214
|
-
return renewLocalSession(active, mode, deps);
|
|
215
|
-
}
|
|
216
|
-
/** @internal */
|
|
217
|
-
export function __resetSessionContinuityForTest() {
|
|
218
|
-
inflight.clear();
|
|
219
|
-
}
|
|
220
|
-
//# sourceMappingURL=session-continuity.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"session-continuity.js","sourceRoot":"","sources":["../src/session-continuity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,EAAE,aAAa,EAAmB,MAAM,YAAY,CAAC;AAC5D,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EACL,aAAa,EACb,sBAAsB,EACtB,sBAAsB,EACtB,wBAAwB,EACxB,oBAAoB,EACpB,OAAO,GAER,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,GAAG,MAAM,CAAC;AAsBlD,MAAM,WAAW,GAA0B;IACzC,GAAG,EAAE,IAAI,CAAC,GAAG;IACb,gBAAgB,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC;IAC7D,OAAO,EAAE,aAAa;IACtB,SAAS,EAAE,aAAa;IACxB,mBAAmB,EAAE,mBAAmB;IACxC,KAAK,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,gBAAgB;QAC/C,MAAM,KAAK,GAAG,MAAM,mBAAmB,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACvD,IAAI,KAAK,CAAC,QAAQ,KAAK,MAAM,CAAC,aAAa,EAAE,CAAC;YAC5C,MAAM,IAAI,oBAAoB,CAAC,8DAA8D,CAAC,CAAC;QACjG,CAAC;QACD,MAAM,QAAQ,GAAG,MAAM,oBAAoB,CACzC,MAAM,CAAC,MAAM,EACb,MAAM,CAAC,aAAa,EACpB,gBAAgB,EAChB;YACE,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,SAAS,EAAE,MAAM,CAAC,SAAS;YAC3B,YAAY,EAAE,MAAM,CAAC,OAAO;SAC7B,EACD,MAAM,EACN,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAC/B,CAAC;QACF,OAAO;YACL,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,OAAO,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE;YAC1B,SAAS,EAAE,QAAQ,CAAC,OAAO,CAAC,SAAS;YACrC,SAAS,EAAE,QAAQ,CAAC,OAAO,CAAC,SAAS;SACtC,CAAC;IACJ,CAAC;IACD,kBAAkB,EAAE,sBAAsB;IAC1C,cAAc,EAAE,wBAAwB;IACxC,kBAAkB,EAAE,sBAAsB;IAC1C,gBAAgB,EAAE,oBAAoB;CACvC,CAAC;AAEF,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA+B,CAAC;AACxD,MAAM,QAAQ,GAAG,CAAC,KAAa,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAErF,SAAS,QAAQ,CAAC,MAAkB,EAAE,SAA4B;IAChE,OAAO,SAAS,KAAK,IAAI;QACvB,SAAS,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM;QAClC,SAAS,CAAC,mBAAmB,KAAK,MAAM,CAAC,mBAAmB;QAC5D,SAAS,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM;QAClC,SAAS,CAAC,OAAO,KAAK,MAAM,CAAC,OAAO;QACpC,SAAS,CAAC,yBAAyB,KAAK,MAAM,CAAC,yBAAyB,CAAC;AAC7E,CAAC;AAED,SAAS,WAAW,CAAC,MAAkB;IACrC,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,MAAM,EAAE,MAAM,CAAC,MAAM;KACtB,CAAC;AACJ,CAAC;AAED,SAAS,iBAAiB,CAAC,MAAkB,EAAE,MAAyB;IACtE,OAAO,MAAM,KAAK,IAAI;QACpB,MAAM,CAAC,KAAK,KAAK,QAAQ;QACzB,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM;QAC/B,MAAM,CAAC,aAAa,KAAK,MAAM,CAAC,mBAAmB;QACnD,MAAM,CAAC,MAAM,KAAK,MAAM,CAAC,MAAM;QAC/B,MAAM,CAAC,OAAO,KAAK,MAAM,CAAC,OAAO;QACjC,OAAO,CAAC,MAAM,CAAC,KAAK,MAAM,CAAC,yBAAyB,CAAC;AACzD,CAAC;AAED,SAAS,0BAA0B,CAAC,KAAc;IAChD,OAAO,KAAK,YAAY,eAAe;QACrC,KAAK,YAAY,oBAAoB;QACrC,KAAK,YAAY,iBAAiB,CAAC;AACvC,CAAC;AAED,KAAK,UAAU,cAAc,CAC3B,MAAkB,EAClB,IAA2B,EAC3B,sBAAsB,GAAG,KAAK;IAE9B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,CAAC;IACvC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,sBAAsB,IAAI,OAAO,CAAC,YAAY,KAAK,MAAM,CAAC,YAAY,CAAC,EAAE,CAAC;QAC3G,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,KAAK,UAAU,cAAc,CAC3B,MAAkB,EAClB,IAA6B,EAC7B,IAA2B;IAE3B,IAAI,CAAC,MAAM,CAAC,mBAAmB,IAAI,CAAC,MAAM,CAAC,yBAAyB,EAAE,CAAC;QACrE,MAAM,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC;IACnF,CAAC;IACD,MAAM,OAAO,GAAG;QACd,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,aAAa,EAAE,MAAM,CAAC,mBAAmB;QACzC,MAAM,EAAE,MAAM,CAAC,MAAM;KACtB,CAAC;IACF,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,yBAAyB,EAAE,OAAO,CAAC,CAAC;IAC7E,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;IAC3E,CAAC;IACD,MAAM,YAAY,GAAG,QAAQ,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC;IACnD,IAAI,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,YAAY,EAAE,CAAC;QACjD,OAAO,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAC5C,CAAC;IACD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAU,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7D,IAAI,IAAI,KAAK,WAAW,IAAI,SAAS,GAAG,uBAAuB,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACpG,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,MAAM,gBAAgB,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC;IAC7F,IAAI,CAAC,gBAAgB;QAAE,MAAM,IAAI,KAAK,CAAC,iEAAiE,CAAC,CAAC;IAC1G,MAAM,aAAa,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;IAE1C,IAAI,eAAe,GAAG,IAAI,KAAK,SAAS,IAAI,SAAS,IAAI,CAAC,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,CAAC;IAC/F,IAAI,CAAC,eAAe,EAAE,CAAC;QACrB,IAAI,QAAQ,GAAyB,IAAI,CAAC;QAC1C,IAAI,CAAC;YACH,QAAQ,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,MAAM,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAC;QAChF,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,KAAK,YAAY,eAAe,IAAI,KAAK,CAAC,IAAI,KAAK,cAAc,EAAE,CAAC;gBACtE,eAAe,GAAG,IAAI,CAAC;YACzB,CAAC;iBAAM,CAAC;gBACN,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC;QACD,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,IAAI,CAAC,QAAQ;gBAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;YACrE,MAAM,aAAa,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;YACrD,IACE,QAAQ,CAAC,MAAM,KAAK,QAAQ;gBAC5B,QAAQ,CAAC,OAAO,KAAK,MAAM,CAAC,OAAO;gBACnC,QAAQ,CAAC,SAAS,KAAK,MAAM,CAAC,SAAS;gBACvC,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC;gBAC/B,aAAa,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAU,CAAC,EAC9C,CAAC;gBACD,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;YAC1E,CAAC;YACD,MAAM,SAAS,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC;gBAC1C,GAAG,EAAE,MAAM,CAAC,yBAAyB;gBACrC,OAAO,EAAE,aAAa;gBACtB,eAAe,EAAE,MAAM,CAAC,OAAQ;gBAChC,oBAAoB,EAAE,YAAY;gBAClC,iBAAiB,EAAE,MAAM,CAAC,SAAU;gBACpC,SAAS,EAAE,QAAQ,CAAC,SAAS;aAC9B,CAAC,CAAC;YACH,IAAI,CAAC,SAAS;gBAAE,OAAO,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YACpD,OAAO,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;IAED,MAAM,WAAW,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC;QAChD,GAAG,EAAE,MAAM,CAAC,yBAAyB;QACrC,OAAO,EAAE,aAAa;QACtB,eAAe,EAAE,MAAM,CAAC,OAAQ;QAChC,oBAAoB,EAAE,YAAY;QAClC,qBAAqB,EAAE,IAAI,CAAC,gBAAgB,EAAE;KAC/C,CAAC,CAAC;IACH,IAAI,CAAC,WAAW,CAAC,EAAE;QAAE,OAAO,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IAE/D,IAAI,QAAuB,CAAC;IAC5B,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,WAAW,CAAC,UAAU,EAAE,gBAAgB,CAAC,CAAC;IACrF,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,0BAA0B,CAAC,KAAK,CAAC,EAAE,CAAC;YACtC,MAAM,IAAI,CAAC,gBAAgB,CAAC;gBAC1B,GAAG,EAAE,MAAM,CAAC,yBAAyB;gBACrC,OAAO,EAAE,aAAa;gBACtB,oBAAoB,EAAE,YAAY;gBAClC,yBAAyB,EAAE,WAAW,CAAC,MAAM;aAC9C,CAAC,CAAC;QACL,CAAC;QACD,MAAM,KAAK,CAAC;IACd,CAAC;IACD,MAAM,cAAc,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;IACtD,IACE,CAAC,QAAQ,CAAC,MAAM,KAAK,SAAS,IAAI,QAAQ,CAAC,MAAM,KAAK,QAAQ,CAAC;QAC/D,QAAQ,CAAC,OAAO,KAAK,MAAM,CAAC,OAAO;QACnC,QAAQ,CAAC,SAAS,KAAK,MAAM,CAAC,SAAS;QACvC,CAAC,MAAM,CAAC,QAAQ,CAAC,cAAc,CAAC;QAChC,cAAc,IAAI,IAAI,CAAC,GAAG,EAAE,EAC5B,CAAC;QACD,MAAM,IAAI,CAAC,gBAAgB,CAAC;YAC1B,GAAG,EAAE,MAAM,CAAC,yBAAyB;YACrC,OAAO,EAAE,aAAa;YACtB,oBAAoB,EAAE,YAAY;YAClC,yBAAyB,EAAE,WAAW,CAAC,MAAM;SAC9C,CAAC,CAAC;QACH,MAAM,IAAI,KAAK,CAAC,sDAAsD,CAAC,CAAC;IAC1E,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC;QAC7C,GAAG,EAAE,MAAM,CAAC,yBAAyB;QACrC,OAAO,EAAE,aAAa;QACtB,eAAe,EAAE,MAAM,CAAC,OAAQ;QAChC,oBAAoB,EAAE,YAAY;QAClC,yBAAyB,EAAE,WAAW,CAAC,MAAM;QAC7C,SAAS,EAAE,QAAQ,CAAC,SAAS;QAC7B,SAAS,EAAE,QAAQ,CAAC,SAAS;KAC9B,CAAC,CAAC;IACH,IAAI,QAAQ,KAAK,UAAU;QAAE,OAAO,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACvE,OAAO,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAC5C,CAAC;AAED,SAAS,iBAAiB,CACxB,MAAkB,EAClB,IAA6B,EAC7B,IAA2B;IAE3B,MAAM,GAAG,GAAG,MAAM,CAAC,yBAAyB,CAAC;IAC7C,IAAI,CAAC,GAAG;QAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,+DAA+D,CAAC,CAAC,CAAC;IAC5G,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACnC,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,MAAM,OAAO,GAAG,cAAc,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;IACnD,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC3B,KAAK,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE;QACxB,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,OAAO;YAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC1D,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACnB,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,MAAkB;IACxD,OAAO,iBAAiB,CAAC,MAAM,EAAE,WAAW,EAAE,WAAW,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,0BAA0B,CAAC,MAAkB;IAC3D,OAAO,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,WAAW,CAAC,CAAC;AAC3D,CAAC;AAED,gBAAgB;AAChB,MAAM,UAAU,0BAA0B,CACxC,MAAkB,EAClB,IAA6B,EAC7B,IAA2B;IAE3B,OAAO,iBAAiB,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAC/C,CAAC;AAED,gBAAgB;AAChB,MAAM,UAAU,+BAA+B;IAC7C,QAAQ,CAAC,KAAK,EAAE,CAAC;AACnB,CAAC"}
|