@opengeni/interaction 0.2.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/controller-core.d.ts +6 -1
- package/dist/index.d.ts +35 -3
- package/dist/index.js +160 -6
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/controller-core.ts +17 -4
- package/src/index.ts +213 -2
|
@@ -43,7 +43,7 @@ type InteractionDriver<TCommand extends InteractionCoreCommand, TTarget extends
|
|
|
43
43
|
target(targetId: string): Promise<TTarget | null>;
|
|
44
44
|
observe(targetId: string): Promise<TObservation>;
|
|
45
45
|
validate?(command: TCommand, target: TTarget): Promise<void> | void;
|
|
46
|
-
dispatch(command: TCommand): Promise<TObservation>;
|
|
46
|
+
dispatch(command: TCommand): Promise<TObservation | null>;
|
|
47
47
|
};
|
|
48
48
|
type InteractionCoreAdapter<TCommand extends InteractionCoreCommand, TTarget extends InteractionCoreTarget, TObservation extends InteractionCoreObservation<TTarget>, TReceipt extends InteractionCoreReceipt<TObservation>> = {
|
|
49
49
|
resourceLabel: string;
|
|
@@ -76,6 +76,10 @@ export type InteractionControllerCoreOptions<TCommand extends InteractionCoreCom
|
|
|
76
76
|
now?: () => Date;
|
|
77
77
|
initialJournal?: readonly InteractionOperationJournalRecord<TReceipt>[];
|
|
78
78
|
onJournalRecord?: (record: InteractionOperationJournalRecord<TReceipt>) => Promise<void> | void;
|
|
79
|
+
/** Override only when a private command contains ephemeral secret material.
|
|
80
|
+
* The callback must bind every non-secret semantic input while excluding the
|
|
81
|
+
* value bytes themselves and return lowercase SHA-256. */
|
|
82
|
+
commandDigest?: (command: TCommand) => string;
|
|
79
83
|
};
|
|
80
84
|
/**
|
|
81
85
|
* Resource-neutral placement mutation authority. Public Browser and Computer
|
|
@@ -89,6 +93,7 @@ export declare class InteractionControllerCore<TCommand extends InteractionCoreC
|
|
|
89
93
|
private readonly maxJournalEntries;
|
|
90
94
|
private readonly now;
|
|
91
95
|
private readonly onJournalRecord;
|
|
96
|
+
private readonly commandDigest;
|
|
92
97
|
private readonly journal;
|
|
93
98
|
private readonly targetTails;
|
|
94
99
|
constructor(options: InteractionControllerCoreOptions<TCommand, TTarget, TObservation, TReceipt>);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { type BrowserActionCommand as BrowserActionCommandValue, type BrowserActionReceipt as BrowserActionReceiptValue, type BrowserObservation as BrowserObservationValue, type BrowserTarget as BrowserTargetValue, type ComputerActionCommand as ComputerActionCommandValue, type ComputerActionReceipt as ComputerActionReceiptValue, type ComputerObservation as ComputerObservationValue, type ComputerTarget as ComputerTargetValue } from "@opengeni/contracts";
|
|
1
|
+
import { type BrowserActionCommand as BrowserActionCommandValue, type BrowserActionReceipt as BrowserActionReceiptValue, type BrowserObservation as BrowserObservationValue, type BrowserProtectedAuthFillCommand as BrowserProtectedAuthFillCommandValue, type BrowserProtectedAuthFillReceipt as BrowserProtectedAuthFillReceiptValue, type BrowserProtectedAuthObservation as BrowserProtectedAuthObservationValue, type BrowserTarget as BrowserTargetValue, type ComputerActionCommand as ComputerActionCommandValue, type ComputerActionReceipt as ComputerActionReceiptValue, type ComputerObservation as ComputerObservationValue, type ComputerTarget as ComputerTargetValue } from "@opengeni/contracts";
|
|
2
2
|
import { InteractionControllerError, InteractionDefiniteDriverError, type InteractionControllerErrorCode, type InteractionOperationJournalRecord } from "./controller-core.js";
|
|
3
3
|
export { InteractionControllerError, InteractionDefiniteDriverError, type InteractionControllerErrorCode, };
|
|
4
4
|
export type BrowserInteractionDriver = {
|
|
5
5
|
target(targetId: string): Promise<BrowserTargetValue | null>;
|
|
6
6
|
observe(targetId: string): Promise<BrowserObservationValue>;
|
|
7
7
|
validate?(command: BrowserActionCommandValue, target: BrowserTargetValue): Promise<void> | void;
|
|
8
|
-
dispatch(command: BrowserActionCommandValue): Promise<BrowserObservationValue>;
|
|
8
|
+
dispatch(command: BrowserActionCommandValue): Promise<BrowserObservationValue | null>;
|
|
9
9
|
};
|
|
10
10
|
export type BrowserInteractionAuthority = {
|
|
11
11
|
authorizeDispatch(command: BrowserActionCommandValue): Promise<void> | void;
|
|
@@ -32,11 +32,43 @@ export declare class BrowserInteractionController {
|
|
|
32
32
|
waitForIdle(): Promise<void>;
|
|
33
33
|
}
|
|
34
34
|
export declare function recoverBrowserOperationJournalRecord(record: BrowserOperationJournalRecord, settledAt: string): BrowserOperationJournalRecord;
|
|
35
|
+
export type BrowserProtectedAuthDriver = {
|
|
36
|
+
target(targetId: string): Promise<BrowserTargetValue | null>;
|
|
37
|
+
observe(targetId: string): Promise<BrowserProtectedAuthObservationValue>;
|
|
38
|
+
validate?(command: BrowserProtectedAuthFillCommandValue, target: BrowserTargetValue): Promise<void> | void;
|
|
39
|
+
dispatch(command: BrowserProtectedAuthFillCommandValue): Promise<BrowserProtectedAuthObservationValue>;
|
|
40
|
+
};
|
|
41
|
+
export type BrowserProtectedAuthAuthority = {
|
|
42
|
+
authorizeDispatch(command: BrowserProtectedAuthFillCommandValue): Promise<void> | void;
|
|
43
|
+
};
|
|
44
|
+
export type BrowserProtectedAuthOperationJournalRecord = InteractionOperationJournalRecord<BrowserProtectedAuthFillReceiptValue>;
|
|
45
|
+
export type BrowserProtectedAuthControllerOptions = {
|
|
46
|
+
browserSessionId: string;
|
|
47
|
+
controllerGeneration: string;
|
|
48
|
+
driver: BrowserProtectedAuthDriver;
|
|
49
|
+
authority?: BrowserProtectedAuthAuthority;
|
|
50
|
+
maxJournalEntries?: number;
|
|
51
|
+
now?: () => Date;
|
|
52
|
+
initialJournal?: readonly BrowserProtectedAuthOperationJournalRecord[];
|
|
53
|
+
onJournalRecord?: (record: BrowserProtectedAuthOperationJournalRecord) => Promise<void> | void;
|
|
54
|
+
};
|
|
55
|
+
/** Secret-bearing commands are admitted only through this controller-private
|
|
56
|
+
* authority. Its durable digest binds all semantics plus credential version,
|
|
57
|
+
* but deliberately excludes ephemeral password/TOTP value bytes. */
|
|
58
|
+
export declare class BrowserProtectedAuthController {
|
|
59
|
+
private readonly core;
|
|
60
|
+
constructor(options: BrowserProtectedAuthControllerOptions);
|
|
61
|
+
run(command: BrowserProtectedAuthFillCommandValue): Promise<BrowserProtectedAuthFillReceiptValue>;
|
|
62
|
+
receipt(operationId: string): BrowserProtectedAuthFillReceiptValue | null;
|
|
63
|
+
journalSnapshot(): BrowserProtectedAuthOperationJournalRecord[];
|
|
64
|
+
waitForIdle(): Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
export declare function recoverBrowserProtectedAuthOperationJournalRecord(record: BrowserProtectedAuthOperationJournalRecord, settledAt: string): BrowserProtectedAuthOperationJournalRecord;
|
|
35
67
|
export type ComputerInteractionDriver = {
|
|
36
68
|
target(targetId: string): Promise<ComputerTargetValue | null>;
|
|
37
69
|
observe(targetId: string): Promise<ComputerObservationValue>;
|
|
38
70
|
validate?(command: ComputerActionCommandValue, target: ComputerTargetValue): Promise<void> | void;
|
|
39
|
-
dispatch(command: ComputerActionCommandValue): Promise<ComputerObservationValue>;
|
|
71
|
+
dispatch(command: ComputerActionCommandValue): Promise<ComputerObservationValue | null>;
|
|
40
72
|
};
|
|
41
73
|
export type ComputerInteractionAuthority = {
|
|
42
74
|
authorizeDispatch(command: ComputerActionCommandValue): Promise<void> | void;
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
+
import { createHash as createHash2 } from "crypto";
|
|
2
3
|
import {
|
|
3
4
|
BrowserActionCommand,
|
|
4
5
|
BrowserActionReceipt,
|
|
5
6
|
BrowserObservation,
|
|
7
|
+
BrowserProtectedAuthFillCommand,
|
|
8
|
+
BrowserProtectedAuthFillReceipt,
|
|
9
|
+
BrowserProtectedAuthObservation,
|
|
6
10
|
BrowserTarget,
|
|
7
11
|
ComputerActionCommand,
|
|
8
12
|
ComputerActionReceipt,
|
|
@@ -40,6 +44,7 @@ var InteractionControllerCore = class {
|
|
|
40
44
|
maxJournalEntries;
|
|
41
45
|
now;
|
|
42
46
|
onJournalRecord;
|
|
47
|
+
commandDigest;
|
|
43
48
|
journal = /* @__PURE__ */ new Map();
|
|
44
49
|
targetTails = /* @__PURE__ */ new Map();
|
|
45
50
|
constructor(options) {
|
|
@@ -49,6 +54,7 @@ var InteractionControllerCore = class {
|
|
|
49
54
|
this.maxJournalEntries = options.maxJournalEntries ?? 1e4;
|
|
50
55
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
51
56
|
this.onJournalRecord = options.onJournalRecord;
|
|
57
|
+
this.commandDigest = options.commandDigest ?? digestJson;
|
|
52
58
|
if (!Number.isSafeInteger(this.maxJournalEntries) || this.maxJournalEntries < 1) {
|
|
53
59
|
throw new Error("maxJournalEntries must be a positive safe integer");
|
|
54
60
|
}
|
|
@@ -63,7 +69,10 @@ var InteractionControllerCore = class {
|
|
|
63
69
|
run(commandInput) {
|
|
64
70
|
const command = this.adapter.parseCommand(commandInput);
|
|
65
71
|
this.adapter.assertCommandAuthority(command);
|
|
66
|
-
const commandDigest =
|
|
72
|
+
const commandDigest = this.commandDigest(command);
|
|
73
|
+
if (!/^[0-9a-f]{64}$/u.test(commandDigest)) {
|
|
74
|
+
throw new Error(`${this.adapter.resourceLabel} command digest must be lowercase SHA-256`);
|
|
75
|
+
}
|
|
67
76
|
const existing = this.journal.get(command.operationId);
|
|
68
77
|
if (existing) {
|
|
69
78
|
if (existing.commandDigest !== commandDigest) {
|
|
@@ -154,8 +163,9 @@ var InteractionControllerCore = class {
|
|
|
154
163
|
);
|
|
155
164
|
}
|
|
156
165
|
try {
|
|
157
|
-
const
|
|
158
|
-
this.adapter.
|
|
166
|
+
const dispatchedObservation = await this.driver.dispatch(command);
|
|
167
|
+
const observation = dispatchedObservation === null ? null : this.adapter.parseObservation(dispatchedObservation);
|
|
168
|
+
if (observation) this.adapter.assertObservationAuthority(observation, command.targetId);
|
|
159
169
|
const completed = this.makeReceipt(
|
|
160
170
|
command,
|
|
161
171
|
"completed",
|
|
@@ -371,13 +381,15 @@ var BrowserInteractionController = class {
|
|
|
371
381
|
if (command.expectedTargetGeneration !== target.targetGeneration) {
|
|
372
382
|
throw new InteractionControllerError(
|
|
373
383
|
"target_stale",
|
|
374
|
-
"browser target changed before the command could dispatch"
|
|
384
|
+
"browser target changed before the command could dispatch",
|
|
385
|
+
true
|
|
375
386
|
);
|
|
376
387
|
}
|
|
377
388
|
if (command.expectedDocumentGeneration !== target.documentGeneration) {
|
|
378
389
|
throw new InteractionControllerError(
|
|
379
390
|
"document_stale",
|
|
380
|
-
"browser document changed before the command could dispatch"
|
|
391
|
+
"browser document changed before the command could dispatch",
|
|
392
|
+
true
|
|
381
393
|
);
|
|
382
394
|
}
|
|
383
395
|
},
|
|
@@ -447,6 +459,145 @@ function recoverBrowserOperationJournalRecord(record, settledAt) {
|
|
|
447
459
|
)
|
|
448
460
|
};
|
|
449
461
|
}
|
|
462
|
+
var BrowserProtectedAuthController = class {
|
|
463
|
+
core;
|
|
464
|
+
constructor(options) {
|
|
465
|
+
const { browserSessionId, controllerGeneration } = options;
|
|
466
|
+
this.core = new InteractionControllerCore({
|
|
467
|
+
driver: options.driver,
|
|
468
|
+
...options.authority ? { authority: options.authority } : {},
|
|
469
|
+
...options.maxJournalEntries !== void 0 ? { maxJournalEntries: options.maxJournalEntries } : {},
|
|
470
|
+
...options.now ? { now: options.now } : {},
|
|
471
|
+
...options.initialJournal ? { initialJournal: options.initialJournal } : {},
|
|
472
|
+
...options.onJournalRecord ? { onJournalRecord: options.onJournalRecord } : {},
|
|
473
|
+
commandDigest: protectedAuthCommandDigest,
|
|
474
|
+
adapter: {
|
|
475
|
+
resourceLabel: "browser protected fill",
|
|
476
|
+
parseCommand: (value) => BrowserProtectedAuthFillCommand.parse(value),
|
|
477
|
+
parseTarget: (value) => BrowserTarget.parse(value),
|
|
478
|
+
parseObservation: (value) => BrowserProtectedAuthObservation.parse(value),
|
|
479
|
+
parseReceipt: (value) => BrowserProtectedAuthFillReceipt.parse(value),
|
|
480
|
+
assertCommandAuthority(command) {
|
|
481
|
+
if (command.browserSessionId !== browserSessionId) {
|
|
482
|
+
throw new InteractionControllerError(
|
|
483
|
+
"resource_not_found",
|
|
484
|
+
"protected fill targets another browser session"
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
if (command.controllerGeneration !== controllerGeneration) {
|
|
488
|
+
throw new InteractionControllerError(
|
|
489
|
+
"controller_stale",
|
|
490
|
+
"protected fill targets a stale browser controller"
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
},
|
|
494
|
+
assertTargetAuthority(target) {
|
|
495
|
+
if (target.browserSessionId !== browserSessionId || target.controllerGeneration !== controllerGeneration) {
|
|
496
|
+
throw new InteractionControllerError(
|
|
497
|
+
"controller_stale",
|
|
498
|
+
"protected-fill target belongs to a stale browser controller"
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
assertExpectedGenerations(command, target) {
|
|
503
|
+
if (command.expectedTargetGeneration !== target.targetGeneration) {
|
|
504
|
+
throw new InteractionControllerError(
|
|
505
|
+
"target_stale",
|
|
506
|
+
"browser target changed before protected fill",
|
|
507
|
+
true
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
if (command.expectedDocumentGeneration !== target.documentGeneration) {
|
|
511
|
+
throw new InteractionControllerError(
|
|
512
|
+
"document_stale",
|
|
513
|
+
"browser document changed before protected fill",
|
|
514
|
+
true
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
assertObservationAuthority(observation, targetId) {
|
|
519
|
+
if (observation.target.id !== targetId || observation.target.browserSessionId !== browserSessionId || observation.target.controllerGeneration !== controllerGeneration) {
|
|
520
|
+
throw new InteractionControllerError(
|
|
521
|
+
"driver_failed",
|
|
522
|
+
"protected-fill driver returned a result outside controller authority"
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
makeReceipt(input) {
|
|
527
|
+
return BrowserProtectedAuthFillReceipt.parse({
|
|
528
|
+
protocolVersion: 1,
|
|
529
|
+
operationId: input.command.operationId,
|
|
530
|
+
browserSessionId,
|
|
531
|
+
controllerGeneration,
|
|
532
|
+
targetId: input.command.targetId,
|
|
533
|
+
state: input.state,
|
|
534
|
+
dispatchedAt: input.dispatchedAt,
|
|
535
|
+
settledAt: input.settledAt,
|
|
536
|
+
observation: input.observation,
|
|
537
|
+
error: input.error
|
|
538
|
+
});
|
|
539
|
+
},
|
|
540
|
+
recoverReceipt(receipt, settledAt) {
|
|
541
|
+
if (receipt.browserSessionId !== browserSessionId || receipt.controllerGeneration !== controllerGeneration) {
|
|
542
|
+
throw new Error(
|
|
543
|
+
`restored protected-fill operation ${receipt.operationId} is outside controller authority`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
return recoverInteractionReceipt(
|
|
547
|
+
receipt,
|
|
548
|
+
settledAt,
|
|
549
|
+
"browser protected fill",
|
|
550
|
+
(value) => BrowserProtectedAuthFillReceipt.parse(value)
|
|
551
|
+
);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
run(command) {
|
|
557
|
+
return this.core.run(command);
|
|
558
|
+
}
|
|
559
|
+
receipt(operationId) {
|
|
560
|
+
return this.core.receipt(operationId);
|
|
561
|
+
}
|
|
562
|
+
journalSnapshot() {
|
|
563
|
+
return this.core.journalSnapshot();
|
|
564
|
+
}
|
|
565
|
+
waitForIdle() {
|
|
566
|
+
return this.core.waitForIdle();
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
function recoverBrowserProtectedAuthOperationJournalRecord(record, settledAt) {
|
|
570
|
+
const receipt = BrowserProtectedAuthFillReceipt.parse(record.receipt);
|
|
571
|
+
return {
|
|
572
|
+
...record,
|
|
573
|
+
receipt: recoverInteractionReceipt(
|
|
574
|
+
receipt,
|
|
575
|
+
settledAt,
|
|
576
|
+
"browser protected fill",
|
|
577
|
+
(value) => BrowserProtectedAuthFillReceipt.parse(value)
|
|
578
|
+
)
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
function protectedAuthCommandDigest(command) {
|
|
582
|
+
const { fields, ...metadata } = command;
|
|
583
|
+
const secretFree = {
|
|
584
|
+
...metadata,
|
|
585
|
+
fields: fields.map(({ value: _value, ...field }) => field)
|
|
586
|
+
};
|
|
587
|
+
return createHash2("sha256").update(canonicalJson2(secretFree), "utf8").digest("hex");
|
|
588
|
+
}
|
|
589
|
+
function canonicalJson2(value) {
|
|
590
|
+
return JSON.stringify(canonicalJsonValue2(value));
|
|
591
|
+
}
|
|
592
|
+
function canonicalJsonValue2(value) {
|
|
593
|
+
if (Array.isArray(value)) return value.map(canonicalJsonValue2);
|
|
594
|
+
if (value !== null && typeof value === "object") {
|
|
595
|
+
return Object.fromEntries(
|
|
596
|
+
Object.entries(value).filter(([, entry]) => entry !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => [key, canonicalJsonValue2(entry)])
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
return value;
|
|
600
|
+
}
|
|
450
601
|
var ComputerInteractionController = class {
|
|
451
602
|
core;
|
|
452
603
|
constructor(options) {
|
|
@@ -490,7 +641,8 @@ var ComputerInteractionController = class {
|
|
|
490
641
|
if (command.expectedTargetGeneration !== target.targetGeneration) {
|
|
491
642
|
throw new InteractionControllerError(
|
|
492
643
|
"target_stale",
|
|
493
|
-
"computer target changed before the command could dispatch"
|
|
644
|
+
"computer target changed before the command could dispatch",
|
|
645
|
+
true
|
|
494
646
|
);
|
|
495
647
|
}
|
|
496
648
|
},
|
|
@@ -562,10 +714,12 @@ function recoverComputerOperationJournalRecord(record, settledAt) {
|
|
|
562
714
|
}
|
|
563
715
|
export {
|
|
564
716
|
BrowserInteractionController,
|
|
717
|
+
BrowserProtectedAuthController,
|
|
565
718
|
ComputerInteractionController,
|
|
566
719
|
InteractionControllerError,
|
|
567
720
|
InteractionDefiniteDriverError,
|
|
568
721
|
recoverBrowserOperationJournalRecord,
|
|
722
|
+
recoverBrowserProtectedAuthOperationJournalRecord,
|
|
569
723
|
recoverComputerOperationJournalRecord
|
|
570
724
|
};
|
|
571
725
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/controller-core.ts"],"sourcesContent":["import {\n BrowserActionCommand,\n BrowserActionReceipt,\n BrowserObservation,\n BrowserTarget,\n ComputerActionCommand,\n ComputerActionReceipt,\n ComputerObservation,\n ComputerTarget,\n type BrowserActionCommand as BrowserActionCommandValue,\n type BrowserActionReceipt as BrowserActionReceiptValue,\n type BrowserObservation as BrowserObservationValue,\n type BrowserTarget as BrowserTargetValue,\n type ComputerActionCommand as ComputerActionCommandValue,\n type ComputerActionReceipt as ComputerActionReceiptValue,\n type ComputerObservation as ComputerObservationValue,\n type ComputerTarget as ComputerTargetValue,\n} from \"@opengeni/contracts\";\nimport {\n InteractionControllerCore,\n InteractionControllerError,\n InteractionDefiniteDriverError,\n recoverInteractionReceipt,\n type InteractionControllerErrorCode,\n type InteractionOperationJournalRecord,\n} from \"./controller-core\";\n\nexport {\n InteractionControllerError,\n InteractionDefiniteDriverError,\n type InteractionControllerErrorCode,\n};\n\nexport type BrowserInteractionDriver = {\n target(targetId: string): Promise<BrowserTargetValue | null>;\n observe(targetId: string): Promise<BrowserObservationValue>;\n validate?(command: BrowserActionCommandValue, target: BrowserTargetValue): Promise<void> | void;\n dispatch(command: BrowserActionCommandValue): Promise<BrowserObservationValue>;\n};\n\nexport type BrowserInteractionAuthority = {\n authorizeDispatch(command: BrowserActionCommandValue): Promise<void> | void;\n};\n\nexport type BrowserOperationJournalRecord =\n InteractionOperationJournalRecord<BrowserActionReceiptValue>;\n\nexport type BrowserInteractionControllerOptions = {\n browserSessionId: string;\n controllerGeneration: string;\n driver: BrowserInteractionDriver;\n authority?: BrowserInteractionAuthority;\n maxJournalEntries?: number;\n now?: () => Date;\n initialJournal?: readonly BrowserOperationJournalRecord[];\n onJournalRecord?: (record: BrowserOperationJournalRecord) => Promise<void> | void;\n};\n\n/** Placement-resident BrowserSession mutation authority. */\nexport class BrowserInteractionController {\n private readonly core: InteractionControllerCore<\n BrowserActionCommandValue,\n BrowserTargetValue,\n BrowserObservationValue,\n BrowserActionReceiptValue\n >;\n\n constructor(options: BrowserInteractionControllerOptions) {\n const { browserSessionId, controllerGeneration } = options;\n this.core = new InteractionControllerCore({\n driver: options.driver,\n ...(options.authority ? { authority: options.authority } : {}),\n ...(options.maxJournalEntries !== undefined\n ? { maxJournalEntries: options.maxJournalEntries }\n : {}),\n ...(options.now ? { now: options.now } : {}),\n ...(options.initialJournal ? { initialJournal: options.initialJournal } : {}),\n ...(options.onJournalRecord ? { onJournalRecord: options.onJournalRecord } : {}),\n adapter: {\n resourceLabel: \"browser\",\n parseCommand: (value) => BrowserActionCommand.parse(value),\n parseTarget: (value) => BrowserTarget.parse(value),\n parseObservation: (value) => BrowserObservation.parse(value),\n parseReceipt: (value) => BrowserActionReceipt.parse(value),\n assertCommandAuthority(command) {\n if (command.browserSessionId !== browserSessionId) {\n throw new InteractionControllerError(\n \"resource_not_found\",\n \"browser command targets another browser session\",\n );\n }\n if (command.controllerGeneration !== controllerGeneration) {\n throw new InteractionControllerError(\n \"controller_stale\",\n \"browser command targets a stale controller generation\",\n );\n }\n },\n assertTargetAuthority(target) {\n if (\n target.browserSessionId !== browserSessionId ||\n target.controllerGeneration !== controllerGeneration\n ) {\n throw new InteractionControllerError(\n \"controller_stale\",\n \"browser target belongs to a stale controller generation\",\n );\n }\n },\n assertExpectedGenerations(command, target) {\n if (command.expectedTargetGeneration !== target.targetGeneration) {\n throw new InteractionControllerError(\n \"target_stale\",\n \"browser target changed before the command could dispatch\",\n );\n }\n if (command.expectedDocumentGeneration !== target.documentGeneration) {\n throw new InteractionControllerError(\n \"document_stale\",\n \"browser document changed before the command could dispatch\",\n );\n }\n },\n assertObservationAuthority(observation, targetId) {\n if (\n observation.browserSessionId !== browserSessionId ||\n observation.target.id !== targetId ||\n observation.target.browserSessionId !== browserSessionId ||\n observation.target.controllerGeneration !== controllerGeneration\n ) {\n throw new InteractionControllerError(\n \"driver_failed\",\n \"browser driver returned an observation outside controller authority\",\n );\n }\n },\n makeReceipt(input) {\n return BrowserActionReceipt.parse({\n protocolVersion: 1,\n operationId: input.command.operationId,\n browserSessionId,\n controllerGeneration,\n targetId: input.command.targetId,\n state: input.state,\n dispatchedAt: input.dispatchedAt,\n settledAt: input.settledAt,\n observation: input.observation,\n error: input.error,\n });\n },\n recoverReceipt(receipt, settledAt) {\n if (\n receipt.browserSessionId !== browserSessionId ||\n receipt.controllerGeneration !== controllerGeneration\n ) {\n throw new Error(\n `restored operation ${receipt.operationId} is outside controller authority`,\n );\n }\n return recoverInteractionReceipt(receipt, settledAt, \"browser\", (value) =>\n BrowserActionReceipt.parse(value),\n );\n },\n },\n });\n }\n\n observe(targetId: string): Promise<BrowserObservationValue> {\n return this.core.observe(targetId);\n }\n\n run(command: BrowserActionCommandValue): Promise<BrowserActionReceiptValue> {\n return this.core.run(command);\n }\n\n receipt(operationId: string): BrowserActionReceiptValue | null {\n return this.core.receipt(operationId);\n }\n\n journalSnapshot(): BrowserOperationJournalRecord[] {\n return this.core.journalSnapshot();\n }\n\n waitForIdle(): Promise<void> {\n return this.core.waitForIdle();\n }\n}\n\nexport function recoverBrowserOperationJournalRecord(\n record: BrowserOperationJournalRecord,\n settledAt: string,\n): BrowserOperationJournalRecord {\n const receipt = BrowserActionReceipt.parse(record.receipt);\n return {\n ...record,\n receipt: recoverInteractionReceipt(receipt, settledAt, \"browser\", (value) =>\n BrowserActionReceipt.parse(value),\n ),\n };\n}\n\nexport type ComputerInteractionDriver = {\n target(targetId: string): Promise<ComputerTargetValue | null>;\n observe(targetId: string): Promise<ComputerObservationValue>;\n validate?(command: ComputerActionCommandValue, target: ComputerTargetValue): Promise<void> | void;\n dispatch(command: ComputerActionCommandValue): Promise<ComputerObservationValue>;\n};\n\nexport type ComputerInteractionAuthority = {\n authorizeDispatch(command: ComputerActionCommandValue): Promise<void> | void;\n};\n\nexport type ComputerOperationJournalRecord =\n InteractionOperationJournalRecord<ComputerActionReceiptValue>;\n\nexport type ComputerInteractionControllerOptions = {\n computerSessionId: string;\n controllerGeneration: string;\n driver: ComputerInteractionDriver;\n authority?: ComputerInteractionAuthority;\n maxJournalEntries?: number;\n now?: () => Date;\n initialJournal?: readonly ComputerOperationJournalRecord[];\n onJournalRecord?: (record: ComputerOperationJournalRecord) => Promise<void> | void;\n};\n\n/** Placement-resident ComputerSession mutation authority. Linux AT-SPI/X11,\n * macOS AX/ScreenCaptureKit, and later UIA adapters share this exact core. */\nexport class ComputerInteractionController {\n private readonly core: InteractionControllerCore<\n ComputerActionCommandValue,\n ComputerTargetValue,\n ComputerObservationValue,\n ComputerActionReceiptValue\n >;\n\n constructor(options: ComputerInteractionControllerOptions) {\n const { computerSessionId, controllerGeneration } = options;\n this.core = new InteractionControllerCore({\n driver: options.driver,\n ...(options.authority ? { authority: options.authority } : {}),\n ...(options.maxJournalEntries !== undefined\n ? { maxJournalEntries: options.maxJournalEntries }\n : {}),\n ...(options.now ? { now: options.now } : {}),\n ...(options.initialJournal ? { initialJournal: options.initialJournal } : {}),\n ...(options.onJournalRecord ? { onJournalRecord: options.onJournalRecord } : {}),\n adapter: {\n resourceLabel: \"computer\",\n parseCommand: (value) => ComputerActionCommand.parse(value),\n parseTarget: (value) => ComputerTarget.parse(value),\n parseObservation: (value) => ComputerObservation.parse(value),\n parseReceipt: (value) => ComputerActionReceipt.parse(value),\n assertCommandAuthority(command) {\n if (command.computerSessionId !== computerSessionId) {\n throw new InteractionControllerError(\n \"resource_not_found\",\n \"computer command targets another computer session\",\n );\n }\n if (command.controllerGeneration !== controllerGeneration) {\n throw new InteractionControllerError(\n \"controller_stale\",\n \"computer command targets a stale controller generation\",\n );\n }\n },\n assertTargetAuthority(target) {\n if (\n target.computerSessionId !== computerSessionId ||\n target.controllerGeneration !== controllerGeneration\n ) {\n throw new InteractionControllerError(\n \"controller_stale\",\n \"computer target belongs to a stale controller generation\",\n );\n }\n },\n assertExpectedGenerations(command, target) {\n if (command.expectedTargetGeneration !== target.targetGeneration) {\n throw new InteractionControllerError(\n \"target_stale\",\n \"computer target changed before the command could dispatch\",\n );\n }\n },\n assertObservationAuthority(observation, targetId) {\n if (\n observation.computerSessionId !== computerSessionId ||\n observation.target.id !== targetId ||\n observation.target.computerSessionId !== computerSessionId ||\n observation.target.controllerGeneration !== controllerGeneration\n ) {\n throw new InteractionControllerError(\n \"driver_failed\",\n \"computer driver returned an observation outside controller authority\",\n );\n }\n },\n makeReceipt(input) {\n return ComputerActionReceipt.parse({\n protocolVersion: 1,\n operationId: input.command.operationId,\n computerSessionId,\n controllerGeneration,\n targetId: input.command.targetId,\n state: input.state,\n dispatchedAt: input.dispatchedAt,\n settledAt: input.settledAt,\n observation: input.observation,\n error: input.error,\n });\n },\n recoverReceipt(receipt, settledAt) {\n if (\n receipt.computerSessionId !== computerSessionId ||\n receipt.controllerGeneration !== controllerGeneration\n ) {\n throw new Error(\n `restored operation ${receipt.operationId} is outside controller authority`,\n );\n }\n return recoverInteractionReceipt(receipt, settledAt, \"computer\", (value) =>\n ComputerActionReceipt.parse(value),\n );\n },\n },\n });\n }\n\n observe(targetId: string): Promise<ComputerObservationValue> {\n return this.core.observe(targetId);\n }\n\n run(command: ComputerActionCommandValue): Promise<ComputerActionReceiptValue> {\n return this.core.run(command);\n }\n\n receipt(operationId: string): ComputerActionReceiptValue | null {\n return this.core.receipt(operationId);\n }\n\n journalSnapshot(): ComputerOperationJournalRecord[] {\n return this.core.journalSnapshot();\n }\n\n waitForIdle(): Promise<void> {\n return this.core.waitForIdle();\n }\n}\n\nexport function recoverComputerOperationJournalRecord(\n record: ComputerOperationJournalRecord,\n settledAt: string,\n): ComputerOperationJournalRecord {\n const receipt = ComputerActionReceipt.parse(record.receipt);\n return {\n ...record,\n receipt: recoverInteractionReceipt(receipt, settledAt, \"computer\", (value) =>\n ComputerActionReceipt.parse(value),\n ),\n };\n}\n","import { createHash } from \"node:crypto\";\nimport type { InteractionError, InteractionOperationState } from \"@opengeni/contracts\";\n\nexport type InteractionControllerErrorCode = InteractionError[\"code\"] | \"journal_full\";\n\n/** A safe, typed rejection known not to have dispatched a side effect. */\nexport class InteractionControllerError extends Error {\n constructor(\n readonly code: InteractionControllerErrorCode,\n message: string,\n readonly retryable = false,\n ) {\n super(message);\n this.name = \"InteractionControllerError\";\n }\n}\n\n/** Driver proof that a dispatched command definitively failed without an ambiguous outcome. */\nexport class InteractionDefiniteDriverError extends Error {\n constructor(\n readonly code: InteractionError[\"code\"],\n message: string,\n readonly retryable = false,\n ) {\n super(message);\n this.name = \"InteractionDefiniteDriverError\";\n }\n}\n\nexport type InteractionCoreCommand = {\n operationId: string;\n controllerGeneration: string;\n targetId: string;\n};\n\nexport type InteractionCoreTarget = {\n id: string;\n controllerGeneration: string;\n};\n\nexport type InteractionCoreObservation<TTarget extends InteractionCoreTarget> = {\n target: TTarget;\n};\n\nexport type InteractionCoreReceipt<TObservation> = {\n operationId: string;\n controllerGeneration: string;\n targetId: string;\n state: InteractionOperationState;\n dispatchedAt: string | null;\n settledAt: string | null;\n observation: TObservation | null;\n error: InteractionError | null;\n};\n\nexport type InteractionOperationJournalRecord<TReceipt> = {\n operationId: string;\n commandDigest: string;\n receipt: TReceipt;\n};\n\ntype InteractionDriver<\n TCommand extends InteractionCoreCommand,\n TTarget extends InteractionCoreTarget,\n TObservation extends InteractionCoreObservation<TTarget>,\n> = {\n target(targetId: string): Promise<TTarget | null>;\n observe(targetId: string): Promise<TObservation>;\n validate?(command: TCommand, target: TTarget): Promise<void> | void;\n dispatch(command: TCommand): Promise<TObservation>;\n};\n\ntype InteractionCoreAdapter<\n TCommand extends InteractionCoreCommand,\n TTarget extends InteractionCoreTarget,\n TObservation extends InteractionCoreObservation<TTarget>,\n TReceipt extends InteractionCoreReceipt<TObservation>,\n> = {\n resourceLabel: string;\n parseCommand(value: unknown): TCommand;\n parseTarget(value: unknown): TTarget;\n parseObservation(value: unknown): TObservation;\n parseReceipt(value: unknown): TReceipt;\n assertCommandAuthority(command: TCommand): void;\n assertTargetAuthority(target: TTarget): void;\n assertExpectedGenerations(command: TCommand, target: TTarget): void;\n assertObservationAuthority(observation: TObservation, targetId: string): void;\n makeReceipt(input: {\n command: TCommand;\n state: InteractionOperationState;\n dispatchedAt: string | null;\n settledAt: string | null;\n observation: TObservation | null;\n error: InteractionError | null;\n }): TReceipt;\n recoverReceipt(receipt: TReceipt, settledAt: string): TReceipt;\n};\n\ntype InteractionCoreAuthority<TCommand> = {\n authorizeDispatch(command: TCommand): Promise<void> | void;\n};\n\nexport type InteractionControllerCoreOptions<\n TCommand extends InteractionCoreCommand,\n TTarget extends InteractionCoreTarget,\n TObservation extends InteractionCoreObservation<TTarget>,\n TReceipt extends InteractionCoreReceipt<TObservation>,\n> = {\n driver: InteractionDriver<TCommand, TTarget, TObservation>;\n adapter: InteractionCoreAdapter<TCommand, TTarget, TObservation, TReceipt>;\n authority?: InteractionCoreAuthority<TCommand>;\n maxJournalEntries?: number;\n now?: () => Date;\n initialJournal?: readonly InteractionOperationJournalRecord<TReceipt>[];\n onJournalRecord?: (record: InteractionOperationJournalRecord<TReceipt>) => Promise<void> | void;\n};\n\ntype JournalEntry<TReceipt> = InteractionOperationJournalRecord<TReceipt> & {\n completion: Promise<TReceipt>;\n preparationPersisted: Promise<boolean>;\n};\n\nconst terminalStates = new Set<InteractionOperationState>([\n \"completed\",\n \"failed\",\n \"outcome_unknown\",\n]);\n\n/**\n * Resource-neutral placement mutation authority. Public Browser and Computer\n * controllers supply only their schemas and generation semantics; journaling,\n * idempotency, target-local serialization, and crash policy stay identical.\n */\nexport class InteractionControllerCore<\n TCommand extends InteractionCoreCommand,\n TTarget extends InteractionCoreTarget,\n TObservation extends InteractionCoreObservation<TTarget>,\n TReceipt extends InteractionCoreReceipt<TObservation>,\n> {\n private readonly driver: InteractionDriver<TCommand, TTarget, TObservation>;\n private readonly adapter: InteractionCoreAdapter<TCommand, TTarget, TObservation, TReceipt>;\n private readonly authority: InteractionCoreAuthority<TCommand> | undefined;\n private readonly maxJournalEntries: number;\n private readonly now: () => Date;\n private readonly onJournalRecord:\n | ((record: InteractionOperationJournalRecord<TReceipt>) => Promise<void> | void)\n | undefined;\n private readonly journal = new Map<string, JournalEntry<TReceipt>>();\n private readonly targetTails = new Map<string, Promise<void>>();\n\n constructor(\n options: InteractionControllerCoreOptions<TCommand, TTarget, TObservation, TReceipt>,\n ) {\n this.driver = options.driver;\n this.adapter = options.adapter;\n this.authority = options.authority;\n this.maxJournalEntries = options.maxJournalEntries ?? 10_000;\n this.now = options.now ?? (() => new Date());\n this.onJournalRecord = options.onJournalRecord;\n if (!Number.isSafeInteger(this.maxJournalEntries) || this.maxJournalEntries < 1) {\n throw new Error(\"maxJournalEntries must be a positive safe integer\");\n }\n for (const record of options.initialJournal ?? []) this.restoreJournalRecord(record);\n }\n\n async observe(targetId: string): Promise<TObservation> {\n const target = await this.requireCurrentTarget(targetId);\n const observed = this.adapter.parseObservation(await this.driver.observe(target.id));\n this.adapter.assertObservationAuthority(observed, target.id);\n return observed;\n }\n\n run(commandInput: TCommand): Promise<TReceipt> {\n const command = this.adapter.parseCommand(commandInput);\n this.adapter.assertCommandAuthority(command);\n const commandDigest = digestJson(command);\n const existing = this.journal.get(command.operationId);\n if (existing) {\n if (existing.commandDigest !== commandDigest) {\n throw new InteractionControllerError(\n \"operation_conflict\",\n `operation id is already bound to a different ${this.adapter.resourceLabel} command`,\n );\n }\n return existing.completion;\n }\n\n this.makeJournalSpace();\n const prepared = this.makeReceipt(command, \"prepared\", null, null, null, null);\n const entry: JournalEntry<TReceipt> = {\n operationId: command.operationId,\n commandDigest,\n receipt: prepared,\n completion: Promise.resolve(prepared),\n preparationPersisted: this.publish(command.operationId, commandDigest, prepared).then(\n () => true,\n () => false,\n ),\n };\n this.journal.set(command.operationId, entry);\n\n const previous = this.targetTails.get(command.targetId) ?? Promise.resolve();\n entry.completion = previous.then(async () => await this.execute(entry, command));\n const tail = entry.completion.then(\n () => undefined,\n () => undefined,\n );\n this.targetTails.set(command.targetId, tail);\n void tail.finally(() => {\n if (this.targetTails.get(command.targetId) === tail)\n this.targetTails.delete(command.targetId);\n });\n return entry.completion;\n }\n\n receipt(operationId: string): TReceipt | null {\n return this.journal.get(operationId)?.receipt ?? null;\n }\n\n journalSnapshot(): InteractionOperationJournalRecord<TReceipt>[] {\n return [...this.journal.values()].map(({ operationId, commandDigest, receipt }) => ({\n operationId,\n commandDigest,\n receipt,\n }));\n }\n\n /** Wait until every command already admitted to a target queue settles.\n * Callers must fence new dispatch through `authority` before awaiting this. */\n async waitForIdle(): Promise<void> {\n while (this.targetTails.size > 0) await Promise.all([...this.targetTails.values()]);\n }\n\n private async execute(entry: JournalEntry<TReceipt>, command: TCommand): Promise<TReceipt> {\n if (!(await entry.preparationPersisted)) {\n return await this.failBeforeDispatch(\n entry,\n command,\n `${this.adapter.resourceLabel} operation journal rejected the command before dispatch`,\n );\n }\n\n try {\n await this.authority?.authorizeDispatch(command);\n const target = await this.requireCurrentTarget(command.targetId);\n this.adapter.assertExpectedGenerations(command, target);\n await this.driver.validate?.(command, target);\n } catch (error) {\n return await this.settle(\n entry,\n this.makeReceipt(\n command,\n \"failed\",\n null,\n this.timestamp(),\n null,\n safePredispatchError(error, this.adapter.resourceLabel),\n ),\n );\n }\n\n const dispatchedAt = this.timestamp();\n const dispatched = this.makeReceipt(command, \"dispatched\", dispatchedAt, null, null, null);\n try {\n await this.publish(entry.operationId, entry.commandDigest, dispatched);\n entry.receipt = dispatched;\n } catch {\n return await this.failBeforeDispatch(\n entry,\n command,\n `${this.adapter.resourceLabel} operation journal became unavailable before dispatch`,\n );\n }\n\n try {\n const observation = this.adapter.parseObservation(await this.driver.dispatch(command));\n this.adapter.assertObservationAuthority(observation, command.targetId);\n const completed = this.makeReceipt(\n command,\n \"completed\",\n dispatchedAt,\n this.timestamp(),\n observation,\n null,\n );\n try {\n await this.publish(entry.operationId, entry.commandDigest, completed);\n entry.receipt = completed;\n return completed;\n } catch {\n return await this.settle(\n entry,\n this.makeReceipt(command, \"outcome_unknown\", dispatchedAt, this.timestamp(), null, {\n code: \"controller_lost\",\n message: `${this.adapter.resourceLabel} command completed but its durable outcome could not be recorded`,\n retryable: false,\n }),\n );\n }\n } catch (error) {\n if (error instanceof InteractionDefiniteDriverError) {\n return await this.settle(\n entry,\n this.makeReceipt(command, \"failed\", dispatchedAt, this.timestamp(), null, {\n code: error.code,\n message: error.message,\n retryable: error.retryable,\n }),\n );\n }\n return await this.settle(\n entry,\n this.makeReceipt(command, \"outcome_unknown\", dispatchedAt, this.timestamp(), null, {\n code: \"controller_lost\",\n message: `${this.adapter.resourceLabel} command outcome is unknown after dispatch`,\n retryable: false,\n }),\n );\n }\n }\n\n private async requireCurrentTarget(targetId: string): Promise<TTarget> {\n const target = await this.driver.target(targetId);\n if (!target) {\n throw new InteractionControllerError(\n \"target_not_found\",\n `${this.adapter.resourceLabel} target does not exist`,\n );\n }\n const parsed = this.adapter.parseTarget(target);\n this.adapter.assertTargetAuthority(parsed);\n return parsed;\n }\n\n private makeReceipt(\n command: TCommand,\n state: InteractionOperationState,\n dispatchedAt: string | null,\n settledAt: string | null,\n observation: TObservation | null,\n error: InteractionError | null,\n ): TReceipt {\n return this.adapter.makeReceipt({\n command,\n state,\n dispatchedAt,\n settledAt,\n observation,\n error,\n });\n }\n\n private async settle(entry: JournalEntry<TReceipt>, receipt: TReceipt): Promise<TReceipt> {\n entry.receipt = receipt;\n try {\n await this.publish(entry.operationId, entry.commandDigest, receipt);\n } catch {\n // Controller-lifetime terminal truth remains authoritative. A restored\n // dispatched receipt is always recovered as outcome_unknown, never replayed.\n }\n return receipt;\n }\n\n private async failBeforeDispatch(\n entry: JournalEntry<TReceipt>,\n command: TCommand,\n message: string,\n ): Promise<TReceipt> {\n return await this.settle(\n entry,\n this.makeReceipt(command, \"failed\", null, this.timestamp(), null, {\n code: \"driver_failed\",\n message,\n retryable: true,\n }),\n );\n }\n\n private async publish(\n operationId: string,\n commandDigest: string,\n receipt: TReceipt,\n ): Promise<void> {\n await this.onJournalRecord?.({ operationId, commandDigest, receipt });\n }\n\n private timestamp(): string {\n return this.now().toISOString();\n }\n\n private makeJournalSpace(): void {\n while (this.journal.size >= this.maxJournalEntries) {\n const terminal = [...this.journal.entries()].find(([, entry]) =>\n terminalStates.has(entry.receipt.state),\n );\n if (!terminal) {\n throw new InteractionControllerError(\n \"journal_full\",\n `${this.adapter.resourceLabel} operation journal has no safely evictable terminal entry`,\n true,\n );\n }\n this.journal.delete(terminal[0]);\n }\n }\n\n private restoreJournalRecord(record: InteractionOperationJournalRecord<TReceipt>): void {\n if (this.journal.has(record.operationId)) {\n throw new Error(`duplicate restored operation id: ${record.operationId}`);\n }\n const parsed = this.adapter.parseReceipt(record.receipt);\n if (parsed.operationId !== record.operationId) {\n throw new Error(`restored operation ${record.operationId} has another receipt id`);\n }\n const receipt = this.adapter.recoverReceipt(parsed, this.timestamp());\n const entry: JournalEntry<TReceipt> = {\n operationId: record.operationId,\n commandDigest: record.commandDigest,\n receipt,\n completion: Promise.resolve(receipt),\n preparationPersisted: Promise.resolve(true),\n };\n this.journal.set(record.operationId, entry);\n }\n}\n\nexport function recoverInteractionReceipt<TReceipt extends InteractionCoreReceipt<unknown>>(\n receipt: TReceipt,\n settledAt: string,\n resourceLabel: string,\n parse: (value: unknown) => TReceipt,\n): TReceipt {\n if (terminalStates.has(receipt.state)) return receipt;\n return parse({\n ...receipt,\n state: receipt.state === \"dispatched\" ? \"outcome_unknown\" : \"failed\",\n settledAt,\n observation: null,\n error:\n receipt.state === \"dispatched\"\n ? {\n code: \"controller_lost\",\n message: `${resourceLabel} controller restarted after command dispatch`,\n retryable: false,\n }\n : {\n code: \"controller_lost\",\n message: `${resourceLabel} controller restarted before command dispatch`,\n retryable: true,\n },\n });\n}\n\nfunction safePredispatchError(error: unknown, resourceLabel: string): InteractionError {\n if (error instanceof InteractionControllerError && error.code !== \"journal_full\") {\n return {\n code: error.code,\n message: error.message,\n retryable: error.retryable,\n };\n }\n return {\n code: \"driver_failed\",\n message: `${resourceLabel} command failed before dispatch`,\n retryable: false,\n };\n}\n\nfunction digestJson(value: unknown): string {\n return createHash(\"sha256\").update(canonicalJson(value), \"utf8\").digest(\"hex\");\n}\n\nfunction canonicalJson(value: unknown): string {\n return JSON.stringify(canonicalJsonValue(value));\n}\n\nfunction canonicalJsonValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonicalJsonValue);\n if (value !== null && typeof value === \"object\") {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, entry]) => [key, canonicalJsonValue(entry)]),\n );\n }\n return value;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/controller-core.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport {\n BrowserActionCommand,\n BrowserActionReceipt,\n BrowserObservation,\n BrowserProtectedAuthFillCommand,\n BrowserProtectedAuthFillReceipt,\n BrowserProtectedAuthObservation,\n BrowserTarget,\n ComputerActionCommand,\n ComputerActionReceipt,\n ComputerObservation,\n ComputerTarget,\n type BrowserActionCommand as BrowserActionCommandValue,\n type BrowserActionReceipt as BrowserActionReceiptValue,\n type BrowserObservation as BrowserObservationValue,\n type BrowserProtectedAuthFillCommand as BrowserProtectedAuthFillCommandValue,\n type BrowserProtectedAuthFillReceipt as BrowserProtectedAuthFillReceiptValue,\n type BrowserProtectedAuthObservation as BrowserProtectedAuthObservationValue,\n type BrowserTarget as BrowserTargetValue,\n type ComputerActionCommand as ComputerActionCommandValue,\n type ComputerActionReceipt as ComputerActionReceiptValue,\n type ComputerObservation as ComputerObservationValue,\n type ComputerTarget as ComputerTargetValue,\n} from \"@opengeni/contracts\";\nimport {\n InteractionControllerCore,\n InteractionControllerError,\n InteractionDefiniteDriverError,\n recoverInteractionReceipt,\n type InteractionControllerErrorCode,\n type InteractionOperationJournalRecord,\n} from \"./controller-core\";\n\nexport {\n InteractionControllerError,\n InteractionDefiniteDriverError,\n type InteractionControllerErrorCode,\n};\n\nexport type BrowserInteractionDriver = {\n target(targetId: string): Promise<BrowserTargetValue | null>;\n observe(targetId: string): Promise<BrowserObservationValue>;\n validate?(command: BrowserActionCommandValue, target: BrowserTargetValue): Promise<void> | void;\n dispatch(command: BrowserActionCommandValue): Promise<BrowserObservationValue | null>;\n};\n\nexport type BrowserInteractionAuthority = {\n authorizeDispatch(command: BrowserActionCommandValue): Promise<void> | void;\n};\n\nexport type BrowserOperationJournalRecord =\n InteractionOperationJournalRecord<BrowserActionReceiptValue>;\n\nexport type BrowserInteractionControllerOptions = {\n browserSessionId: string;\n controllerGeneration: string;\n driver: BrowserInteractionDriver;\n authority?: BrowserInteractionAuthority;\n maxJournalEntries?: number;\n now?: () => Date;\n initialJournal?: readonly BrowserOperationJournalRecord[];\n onJournalRecord?: (record: BrowserOperationJournalRecord) => Promise<void> | void;\n};\n\n/** Placement-resident BrowserSession mutation authority. */\nexport class BrowserInteractionController {\n private readonly core: InteractionControllerCore<\n BrowserActionCommandValue,\n BrowserTargetValue,\n BrowserObservationValue,\n BrowserActionReceiptValue\n >;\n\n constructor(options: BrowserInteractionControllerOptions) {\n const { browserSessionId, controllerGeneration } = options;\n this.core = new InteractionControllerCore({\n driver: options.driver,\n ...(options.authority ? { authority: options.authority } : {}),\n ...(options.maxJournalEntries !== undefined\n ? { maxJournalEntries: options.maxJournalEntries }\n : {}),\n ...(options.now ? { now: options.now } : {}),\n ...(options.initialJournal ? { initialJournal: options.initialJournal } : {}),\n ...(options.onJournalRecord ? { onJournalRecord: options.onJournalRecord } : {}),\n adapter: {\n resourceLabel: \"browser\",\n parseCommand: (value) => BrowserActionCommand.parse(value),\n parseTarget: (value) => BrowserTarget.parse(value),\n parseObservation: (value) => BrowserObservation.parse(value),\n parseReceipt: (value) => BrowserActionReceipt.parse(value),\n assertCommandAuthority(command) {\n if (command.browserSessionId !== browserSessionId) {\n throw new InteractionControllerError(\n \"resource_not_found\",\n \"browser command targets another browser session\",\n );\n }\n if (command.controllerGeneration !== controllerGeneration) {\n throw new InteractionControllerError(\n \"controller_stale\",\n \"browser command targets a stale controller generation\",\n );\n }\n },\n assertTargetAuthority(target) {\n if (\n target.browserSessionId !== browserSessionId ||\n target.controllerGeneration !== controllerGeneration\n ) {\n throw new InteractionControllerError(\n \"controller_stale\",\n \"browser target belongs to a stale controller generation\",\n );\n }\n },\n assertExpectedGenerations(command, target) {\n if (command.expectedTargetGeneration !== target.targetGeneration) {\n throw new InteractionControllerError(\n \"target_stale\",\n \"browser target changed before the command could dispatch\",\n true,\n );\n }\n if (command.expectedDocumentGeneration !== target.documentGeneration) {\n throw new InteractionControllerError(\n \"document_stale\",\n \"browser document changed before the command could dispatch\",\n true,\n );\n }\n },\n assertObservationAuthority(observation, targetId) {\n if (\n observation.browserSessionId !== browserSessionId ||\n observation.target.id !== targetId ||\n observation.target.browserSessionId !== browserSessionId ||\n observation.target.controllerGeneration !== controllerGeneration\n ) {\n throw new InteractionControllerError(\n \"driver_failed\",\n \"browser driver returned an observation outside controller authority\",\n );\n }\n },\n makeReceipt(input) {\n return BrowserActionReceipt.parse({\n protocolVersion: 1,\n operationId: input.command.operationId,\n browserSessionId,\n controllerGeneration,\n targetId: input.command.targetId,\n state: input.state,\n dispatchedAt: input.dispatchedAt,\n settledAt: input.settledAt,\n observation: input.observation,\n error: input.error,\n });\n },\n recoverReceipt(receipt, settledAt) {\n if (\n receipt.browserSessionId !== browserSessionId ||\n receipt.controllerGeneration !== controllerGeneration\n ) {\n throw new Error(\n `restored operation ${receipt.operationId} is outside controller authority`,\n );\n }\n return recoverInteractionReceipt(receipt, settledAt, \"browser\", (value) =>\n BrowserActionReceipt.parse(value),\n );\n },\n },\n });\n }\n\n observe(targetId: string): Promise<BrowserObservationValue> {\n return this.core.observe(targetId);\n }\n\n run(command: BrowserActionCommandValue): Promise<BrowserActionReceiptValue> {\n return this.core.run(command);\n }\n\n receipt(operationId: string): BrowserActionReceiptValue | null {\n return this.core.receipt(operationId);\n }\n\n journalSnapshot(): BrowserOperationJournalRecord[] {\n return this.core.journalSnapshot();\n }\n\n waitForIdle(): Promise<void> {\n return this.core.waitForIdle();\n }\n}\n\nexport function recoverBrowserOperationJournalRecord(\n record: BrowserOperationJournalRecord,\n settledAt: string,\n): BrowserOperationJournalRecord {\n const receipt = BrowserActionReceipt.parse(record.receipt);\n return {\n ...record,\n receipt: recoverInteractionReceipt(receipt, settledAt, \"browser\", (value) =>\n BrowserActionReceipt.parse(value),\n ),\n };\n}\n\nexport type BrowserProtectedAuthDriver = {\n target(targetId: string): Promise<BrowserTargetValue | null>;\n observe(targetId: string): Promise<BrowserProtectedAuthObservationValue>;\n validate?(\n command: BrowserProtectedAuthFillCommandValue,\n target: BrowserTargetValue,\n ): Promise<void> | void;\n dispatch(\n command: BrowserProtectedAuthFillCommandValue,\n ): Promise<BrowserProtectedAuthObservationValue>;\n};\n\nexport type BrowserProtectedAuthAuthority = {\n authorizeDispatch(command: BrowserProtectedAuthFillCommandValue): Promise<void> | void;\n};\n\nexport type BrowserProtectedAuthOperationJournalRecord =\n InteractionOperationJournalRecord<BrowserProtectedAuthFillReceiptValue>;\n\nexport type BrowserProtectedAuthControllerOptions = {\n browserSessionId: string;\n controllerGeneration: string;\n driver: BrowserProtectedAuthDriver;\n authority?: BrowserProtectedAuthAuthority;\n maxJournalEntries?: number;\n now?: () => Date;\n initialJournal?: readonly BrowserProtectedAuthOperationJournalRecord[];\n onJournalRecord?: (record: BrowserProtectedAuthOperationJournalRecord) => Promise<void> | void;\n};\n\n/** Secret-bearing commands are admitted only through this controller-private\n * authority. Its durable digest binds all semantics plus credential version,\n * but deliberately excludes ephemeral password/TOTP value bytes. */\nexport class BrowserProtectedAuthController {\n private readonly core: InteractionControllerCore<\n BrowserProtectedAuthFillCommandValue,\n BrowserTargetValue,\n BrowserProtectedAuthObservationValue,\n BrowserProtectedAuthFillReceiptValue\n >;\n\n constructor(options: BrowserProtectedAuthControllerOptions) {\n const { browserSessionId, controllerGeneration } = options;\n this.core = new InteractionControllerCore({\n driver: options.driver,\n ...(options.authority ? { authority: options.authority } : {}),\n ...(options.maxJournalEntries !== undefined\n ? { maxJournalEntries: options.maxJournalEntries }\n : {}),\n ...(options.now ? { now: options.now } : {}),\n ...(options.initialJournal ? { initialJournal: options.initialJournal } : {}),\n ...(options.onJournalRecord ? { onJournalRecord: options.onJournalRecord } : {}),\n commandDigest: protectedAuthCommandDigest,\n adapter: {\n resourceLabel: \"browser protected fill\",\n parseCommand: (value) => BrowserProtectedAuthFillCommand.parse(value),\n parseTarget: (value) => BrowserTarget.parse(value),\n parseObservation: (value) => BrowserProtectedAuthObservation.parse(value),\n parseReceipt: (value) => BrowserProtectedAuthFillReceipt.parse(value),\n assertCommandAuthority(command) {\n if (command.browserSessionId !== browserSessionId) {\n throw new InteractionControllerError(\n \"resource_not_found\",\n \"protected fill targets another browser session\",\n );\n }\n if (command.controllerGeneration !== controllerGeneration) {\n throw new InteractionControllerError(\n \"controller_stale\",\n \"protected fill targets a stale browser controller\",\n );\n }\n },\n assertTargetAuthority(target) {\n if (\n target.browserSessionId !== browserSessionId ||\n target.controllerGeneration !== controllerGeneration\n ) {\n throw new InteractionControllerError(\n \"controller_stale\",\n \"protected-fill target belongs to a stale browser controller\",\n );\n }\n },\n assertExpectedGenerations(command, target) {\n if (command.expectedTargetGeneration !== target.targetGeneration) {\n throw new InteractionControllerError(\n \"target_stale\",\n \"browser target changed before protected fill\",\n true,\n );\n }\n if (command.expectedDocumentGeneration !== target.documentGeneration) {\n throw new InteractionControllerError(\n \"document_stale\",\n \"browser document changed before protected fill\",\n true,\n );\n }\n },\n assertObservationAuthority(observation, targetId) {\n if (\n observation.target.id !== targetId ||\n observation.target.browserSessionId !== browserSessionId ||\n observation.target.controllerGeneration !== controllerGeneration\n ) {\n throw new InteractionControllerError(\n \"driver_failed\",\n \"protected-fill driver returned a result outside controller authority\",\n );\n }\n },\n makeReceipt(input) {\n return BrowserProtectedAuthFillReceipt.parse({\n protocolVersion: 1,\n operationId: input.command.operationId,\n browserSessionId,\n controllerGeneration,\n targetId: input.command.targetId,\n state: input.state,\n dispatchedAt: input.dispatchedAt,\n settledAt: input.settledAt,\n observation: input.observation,\n error: input.error,\n });\n },\n recoverReceipt(receipt, settledAt) {\n if (\n receipt.browserSessionId !== browserSessionId ||\n receipt.controllerGeneration !== controllerGeneration\n ) {\n throw new Error(\n `restored protected-fill operation ${receipt.operationId} is outside controller authority`,\n );\n }\n return recoverInteractionReceipt(receipt, settledAt, \"browser protected fill\", (value) =>\n BrowserProtectedAuthFillReceipt.parse(value),\n );\n },\n },\n });\n }\n\n run(\n command: BrowserProtectedAuthFillCommandValue,\n ): Promise<BrowserProtectedAuthFillReceiptValue> {\n return this.core.run(command);\n }\n\n receipt(operationId: string): BrowserProtectedAuthFillReceiptValue | null {\n return this.core.receipt(operationId);\n }\n\n journalSnapshot(): BrowserProtectedAuthOperationJournalRecord[] {\n return this.core.journalSnapshot();\n }\n\n waitForIdle(): Promise<void> {\n return this.core.waitForIdle();\n }\n}\n\nexport function recoverBrowserProtectedAuthOperationJournalRecord(\n record: BrowserProtectedAuthOperationJournalRecord,\n settledAt: string,\n): BrowserProtectedAuthOperationJournalRecord {\n const receipt = BrowserProtectedAuthFillReceipt.parse(record.receipt);\n return {\n ...record,\n receipt: recoverInteractionReceipt(receipt, settledAt, \"browser protected fill\", (value) =>\n BrowserProtectedAuthFillReceipt.parse(value),\n ),\n };\n}\n\nfunction protectedAuthCommandDigest(command: BrowserProtectedAuthFillCommandValue): string {\n const { fields, ...metadata } = command;\n const secretFree = {\n ...metadata,\n fields: fields.map(({ value: _value, ...field }) => field),\n };\n return createHash(\"sha256\").update(canonicalJson(secretFree), \"utf8\").digest(\"hex\");\n}\n\nfunction canonicalJson(value: unknown): string {\n return JSON.stringify(canonicalJsonValue(value));\n}\n\nfunction canonicalJsonValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonicalJsonValue);\n if (value !== null && typeof value === \"object\") {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>)\n .filter(([, entry]) => entry !== undefined)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, entry]) => [key, canonicalJsonValue(entry)]),\n );\n }\n return value;\n}\n\nexport type ComputerInteractionDriver = {\n target(targetId: string): Promise<ComputerTargetValue | null>;\n observe(targetId: string): Promise<ComputerObservationValue>;\n validate?(command: ComputerActionCommandValue, target: ComputerTargetValue): Promise<void> | void;\n dispatch(command: ComputerActionCommandValue): Promise<ComputerObservationValue | null>;\n};\n\nexport type ComputerInteractionAuthority = {\n authorizeDispatch(command: ComputerActionCommandValue): Promise<void> | void;\n};\n\nexport type ComputerOperationJournalRecord =\n InteractionOperationJournalRecord<ComputerActionReceiptValue>;\n\nexport type ComputerInteractionControllerOptions = {\n computerSessionId: string;\n controllerGeneration: string;\n driver: ComputerInteractionDriver;\n authority?: ComputerInteractionAuthority;\n maxJournalEntries?: number;\n now?: () => Date;\n initialJournal?: readonly ComputerOperationJournalRecord[];\n onJournalRecord?: (record: ComputerOperationJournalRecord) => Promise<void> | void;\n};\n\n/** Placement-resident ComputerSession mutation authority. Linux AT-SPI/X11,\n * macOS AX/ScreenCaptureKit, and later UIA adapters share this exact core. */\nexport class ComputerInteractionController {\n private readonly core: InteractionControllerCore<\n ComputerActionCommandValue,\n ComputerTargetValue,\n ComputerObservationValue,\n ComputerActionReceiptValue\n >;\n\n constructor(options: ComputerInteractionControllerOptions) {\n const { computerSessionId, controllerGeneration } = options;\n this.core = new InteractionControllerCore({\n driver: options.driver,\n ...(options.authority ? { authority: options.authority } : {}),\n ...(options.maxJournalEntries !== undefined\n ? { maxJournalEntries: options.maxJournalEntries }\n : {}),\n ...(options.now ? { now: options.now } : {}),\n ...(options.initialJournal ? { initialJournal: options.initialJournal } : {}),\n ...(options.onJournalRecord ? { onJournalRecord: options.onJournalRecord } : {}),\n adapter: {\n resourceLabel: \"computer\",\n parseCommand: (value) => ComputerActionCommand.parse(value),\n parseTarget: (value) => ComputerTarget.parse(value),\n parseObservation: (value) => ComputerObservation.parse(value),\n parseReceipt: (value) => ComputerActionReceipt.parse(value),\n assertCommandAuthority(command) {\n if (command.computerSessionId !== computerSessionId) {\n throw new InteractionControllerError(\n \"resource_not_found\",\n \"computer command targets another computer session\",\n );\n }\n if (command.controllerGeneration !== controllerGeneration) {\n throw new InteractionControllerError(\n \"controller_stale\",\n \"computer command targets a stale controller generation\",\n );\n }\n },\n assertTargetAuthority(target) {\n if (\n target.computerSessionId !== computerSessionId ||\n target.controllerGeneration !== controllerGeneration\n ) {\n throw new InteractionControllerError(\n \"controller_stale\",\n \"computer target belongs to a stale controller generation\",\n );\n }\n },\n assertExpectedGenerations(command, target) {\n if (command.expectedTargetGeneration !== target.targetGeneration) {\n throw new InteractionControllerError(\n \"target_stale\",\n \"computer target changed before the command could dispatch\",\n true,\n );\n }\n },\n assertObservationAuthority(observation, targetId) {\n if (\n observation.computerSessionId !== computerSessionId ||\n observation.target.id !== targetId ||\n observation.target.computerSessionId !== computerSessionId ||\n observation.target.controllerGeneration !== controllerGeneration\n ) {\n throw new InteractionControllerError(\n \"driver_failed\",\n \"computer driver returned an observation outside controller authority\",\n );\n }\n },\n makeReceipt(input) {\n return ComputerActionReceipt.parse({\n protocolVersion: 1,\n operationId: input.command.operationId,\n computerSessionId,\n controllerGeneration,\n targetId: input.command.targetId,\n state: input.state,\n dispatchedAt: input.dispatchedAt,\n settledAt: input.settledAt,\n observation: input.observation,\n error: input.error,\n });\n },\n recoverReceipt(receipt, settledAt) {\n if (\n receipt.computerSessionId !== computerSessionId ||\n receipt.controllerGeneration !== controllerGeneration\n ) {\n throw new Error(\n `restored operation ${receipt.operationId} is outside controller authority`,\n );\n }\n return recoverInteractionReceipt(receipt, settledAt, \"computer\", (value) =>\n ComputerActionReceipt.parse(value),\n );\n },\n },\n });\n }\n\n observe(targetId: string): Promise<ComputerObservationValue> {\n return this.core.observe(targetId);\n }\n\n run(command: ComputerActionCommandValue): Promise<ComputerActionReceiptValue> {\n return this.core.run(command);\n }\n\n receipt(operationId: string): ComputerActionReceiptValue | null {\n return this.core.receipt(operationId);\n }\n\n journalSnapshot(): ComputerOperationJournalRecord[] {\n return this.core.journalSnapshot();\n }\n\n waitForIdle(): Promise<void> {\n return this.core.waitForIdle();\n }\n}\n\nexport function recoverComputerOperationJournalRecord(\n record: ComputerOperationJournalRecord,\n settledAt: string,\n): ComputerOperationJournalRecord {\n const receipt = ComputerActionReceipt.parse(record.receipt);\n return {\n ...record,\n receipt: recoverInteractionReceipt(receipt, settledAt, \"computer\", (value) =>\n ComputerActionReceipt.parse(value),\n ),\n };\n}\n","import { createHash } from \"node:crypto\";\nimport type { InteractionError, InteractionOperationState } from \"@opengeni/contracts\";\n\nexport type InteractionControllerErrorCode = InteractionError[\"code\"] | \"journal_full\";\n\n/** A safe, typed rejection known not to have dispatched a side effect. */\nexport class InteractionControllerError extends Error {\n constructor(\n readonly code: InteractionControllerErrorCode,\n message: string,\n readonly retryable = false,\n ) {\n super(message);\n this.name = \"InteractionControllerError\";\n }\n}\n\n/** Driver proof that a dispatched command definitively failed without an ambiguous outcome. */\nexport class InteractionDefiniteDriverError extends Error {\n constructor(\n readonly code: InteractionError[\"code\"],\n message: string,\n readonly retryable = false,\n ) {\n super(message);\n this.name = \"InteractionDefiniteDriverError\";\n }\n}\n\nexport type InteractionCoreCommand = {\n operationId: string;\n controllerGeneration: string;\n targetId: string;\n};\n\nexport type InteractionCoreTarget = {\n id: string;\n controllerGeneration: string;\n};\n\nexport type InteractionCoreObservation<TTarget extends InteractionCoreTarget> = {\n target: TTarget;\n};\n\nexport type InteractionCoreReceipt<TObservation> = {\n operationId: string;\n controllerGeneration: string;\n targetId: string;\n state: InteractionOperationState;\n dispatchedAt: string | null;\n settledAt: string | null;\n observation: TObservation | null;\n error: InteractionError | null;\n};\n\nexport type InteractionOperationJournalRecord<TReceipt> = {\n operationId: string;\n commandDigest: string;\n receipt: TReceipt;\n};\n\ntype InteractionDriver<\n TCommand extends InteractionCoreCommand,\n TTarget extends InteractionCoreTarget,\n TObservation extends InteractionCoreObservation<TTarget>,\n> = {\n target(targetId: string): Promise<TTarget | null>;\n observe(targetId: string): Promise<TObservation>;\n validate?(command: TCommand, target: TTarget): Promise<void> | void;\n dispatch(command: TCommand): Promise<TObservation | null>;\n};\n\ntype InteractionCoreAdapter<\n TCommand extends InteractionCoreCommand,\n TTarget extends InteractionCoreTarget,\n TObservation extends InteractionCoreObservation<TTarget>,\n TReceipt extends InteractionCoreReceipt<TObservation>,\n> = {\n resourceLabel: string;\n parseCommand(value: unknown): TCommand;\n parseTarget(value: unknown): TTarget;\n parseObservation(value: unknown): TObservation;\n parseReceipt(value: unknown): TReceipt;\n assertCommandAuthority(command: TCommand): void;\n assertTargetAuthority(target: TTarget): void;\n assertExpectedGenerations(command: TCommand, target: TTarget): void;\n assertObservationAuthority(observation: TObservation, targetId: string): void;\n makeReceipt(input: {\n command: TCommand;\n state: InteractionOperationState;\n dispatchedAt: string | null;\n settledAt: string | null;\n observation: TObservation | null;\n error: InteractionError | null;\n }): TReceipt;\n recoverReceipt(receipt: TReceipt, settledAt: string): TReceipt;\n};\n\ntype InteractionCoreAuthority<TCommand> = {\n authorizeDispatch(command: TCommand): Promise<void> | void;\n};\n\nexport type InteractionControllerCoreOptions<\n TCommand extends InteractionCoreCommand,\n TTarget extends InteractionCoreTarget,\n TObservation extends InteractionCoreObservation<TTarget>,\n TReceipt extends InteractionCoreReceipt<TObservation>,\n> = {\n driver: InteractionDriver<TCommand, TTarget, TObservation>;\n adapter: InteractionCoreAdapter<TCommand, TTarget, TObservation, TReceipt>;\n authority?: InteractionCoreAuthority<TCommand>;\n maxJournalEntries?: number;\n now?: () => Date;\n initialJournal?: readonly InteractionOperationJournalRecord<TReceipt>[];\n onJournalRecord?: (record: InteractionOperationJournalRecord<TReceipt>) => Promise<void> | void;\n /** Override only when a private command contains ephemeral secret material.\n * The callback must bind every non-secret semantic input while excluding the\n * value bytes themselves and return lowercase SHA-256. */\n commandDigest?: (command: TCommand) => string;\n};\n\ntype JournalEntry<TReceipt> = InteractionOperationJournalRecord<TReceipt> & {\n completion: Promise<TReceipt>;\n preparationPersisted: Promise<boolean>;\n};\n\nconst terminalStates = new Set<InteractionOperationState>([\n \"completed\",\n \"failed\",\n \"outcome_unknown\",\n]);\n\n/**\n * Resource-neutral placement mutation authority. Public Browser and Computer\n * controllers supply only their schemas and generation semantics; journaling,\n * idempotency, target-local serialization, and crash policy stay identical.\n */\nexport class InteractionControllerCore<\n TCommand extends InteractionCoreCommand,\n TTarget extends InteractionCoreTarget,\n TObservation extends InteractionCoreObservation<TTarget>,\n TReceipt extends InteractionCoreReceipt<TObservation>,\n> {\n private readonly driver: InteractionDriver<TCommand, TTarget, TObservation>;\n private readonly adapter: InteractionCoreAdapter<TCommand, TTarget, TObservation, TReceipt>;\n private readonly authority: InteractionCoreAuthority<TCommand> | undefined;\n private readonly maxJournalEntries: number;\n private readonly now: () => Date;\n private readonly onJournalRecord:\n | ((record: InteractionOperationJournalRecord<TReceipt>) => Promise<void> | void)\n | undefined;\n private readonly commandDigest: (command: TCommand) => string;\n private readonly journal = new Map<string, JournalEntry<TReceipt>>();\n private readonly targetTails = new Map<string, Promise<void>>();\n\n constructor(\n options: InteractionControllerCoreOptions<TCommand, TTarget, TObservation, TReceipt>,\n ) {\n this.driver = options.driver;\n this.adapter = options.adapter;\n this.authority = options.authority;\n this.maxJournalEntries = options.maxJournalEntries ?? 10_000;\n this.now = options.now ?? (() => new Date());\n this.onJournalRecord = options.onJournalRecord;\n this.commandDigest = options.commandDigest ?? digestJson;\n if (!Number.isSafeInteger(this.maxJournalEntries) || this.maxJournalEntries < 1) {\n throw new Error(\"maxJournalEntries must be a positive safe integer\");\n }\n for (const record of options.initialJournal ?? []) this.restoreJournalRecord(record);\n }\n\n async observe(targetId: string): Promise<TObservation> {\n const target = await this.requireCurrentTarget(targetId);\n const observed = this.adapter.parseObservation(await this.driver.observe(target.id));\n this.adapter.assertObservationAuthority(observed, target.id);\n return observed;\n }\n\n run(commandInput: TCommand): Promise<TReceipt> {\n const command = this.adapter.parseCommand(commandInput);\n this.adapter.assertCommandAuthority(command);\n const commandDigest = this.commandDigest(command);\n if (!/^[0-9a-f]{64}$/u.test(commandDigest)) {\n throw new Error(`${this.adapter.resourceLabel} command digest must be lowercase SHA-256`);\n }\n const existing = this.journal.get(command.operationId);\n if (existing) {\n if (existing.commandDigest !== commandDigest) {\n throw new InteractionControllerError(\n \"operation_conflict\",\n `operation id is already bound to a different ${this.adapter.resourceLabel} command`,\n );\n }\n return existing.completion;\n }\n\n this.makeJournalSpace();\n const prepared = this.makeReceipt(command, \"prepared\", null, null, null, null);\n const entry: JournalEntry<TReceipt> = {\n operationId: command.operationId,\n commandDigest,\n receipt: prepared,\n completion: Promise.resolve(prepared),\n preparationPersisted: this.publish(command.operationId, commandDigest, prepared).then(\n () => true,\n () => false,\n ),\n };\n this.journal.set(command.operationId, entry);\n\n const previous = this.targetTails.get(command.targetId) ?? Promise.resolve();\n entry.completion = previous.then(async () => await this.execute(entry, command));\n const tail = entry.completion.then(\n () => undefined,\n () => undefined,\n );\n this.targetTails.set(command.targetId, tail);\n void tail.finally(() => {\n if (this.targetTails.get(command.targetId) === tail)\n this.targetTails.delete(command.targetId);\n });\n return entry.completion;\n }\n\n receipt(operationId: string): TReceipt | null {\n return this.journal.get(operationId)?.receipt ?? null;\n }\n\n journalSnapshot(): InteractionOperationJournalRecord<TReceipt>[] {\n return [...this.journal.values()].map(({ operationId, commandDigest, receipt }) => ({\n operationId,\n commandDigest,\n receipt,\n }));\n }\n\n /** Wait until every command already admitted to a target queue settles.\n * Callers must fence new dispatch through `authority` before awaiting this. */\n async waitForIdle(): Promise<void> {\n while (this.targetTails.size > 0) await Promise.all([...this.targetTails.values()]);\n }\n\n private async execute(entry: JournalEntry<TReceipt>, command: TCommand): Promise<TReceipt> {\n if (!(await entry.preparationPersisted)) {\n return await this.failBeforeDispatch(\n entry,\n command,\n `${this.adapter.resourceLabel} operation journal rejected the command before dispatch`,\n );\n }\n\n try {\n await this.authority?.authorizeDispatch(command);\n const target = await this.requireCurrentTarget(command.targetId);\n this.adapter.assertExpectedGenerations(command, target);\n await this.driver.validate?.(command, target);\n } catch (error) {\n return await this.settle(\n entry,\n this.makeReceipt(\n command,\n \"failed\",\n null,\n this.timestamp(),\n null,\n safePredispatchError(error, this.adapter.resourceLabel),\n ),\n );\n }\n\n const dispatchedAt = this.timestamp();\n const dispatched = this.makeReceipt(command, \"dispatched\", dispatchedAt, null, null, null);\n try {\n await this.publish(entry.operationId, entry.commandDigest, dispatched);\n entry.receipt = dispatched;\n } catch {\n return await this.failBeforeDispatch(\n entry,\n command,\n `${this.adapter.resourceLabel} operation journal became unavailable before dispatch`,\n );\n }\n\n try {\n const dispatchedObservation = await this.driver.dispatch(command);\n const observation =\n dispatchedObservation === null\n ? null\n : this.adapter.parseObservation(dispatchedObservation);\n if (observation) this.adapter.assertObservationAuthority(observation, command.targetId);\n const completed = this.makeReceipt(\n command,\n \"completed\",\n dispatchedAt,\n this.timestamp(),\n observation,\n null,\n );\n try {\n await this.publish(entry.operationId, entry.commandDigest, completed);\n entry.receipt = completed;\n return completed;\n } catch {\n return await this.settle(\n entry,\n this.makeReceipt(command, \"outcome_unknown\", dispatchedAt, this.timestamp(), null, {\n code: \"controller_lost\",\n message: `${this.adapter.resourceLabel} command completed but its durable outcome could not be recorded`,\n retryable: false,\n }),\n );\n }\n } catch (error) {\n if (error instanceof InteractionDefiniteDriverError) {\n return await this.settle(\n entry,\n this.makeReceipt(command, \"failed\", dispatchedAt, this.timestamp(), null, {\n code: error.code,\n message: error.message,\n retryable: error.retryable,\n }),\n );\n }\n return await this.settle(\n entry,\n this.makeReceipt(command, \"outcome_unknown\", dispatchedAt, this.timestamp(), null, {\n code: \"controller_lost\",\n message: `${this.adapter.resourceLabel} command outcome is unknown after dispatch`,\n retryable: false,\n }),\n );\n }\n }\n\n private async requireCurrentTarget(targetId: string): Promise<TTarget> {\n const target = await this.driver.target(targetId);\n if (!target) {\n throw new InteractionControllerError(\n \"target_not_found\",\n `${this.adapter.resourceLabel} target does not exist`,\n );\n }\n const parsed = this.adapter.parseTarget(target);\n this.adapter.assertTargetAuthority(parsed);\n return parsed;\n }\n\n private makeReceipt(\n command: TCommand,\n state: InteractionOperationState,\n dispatchedAt: string | null,\n settledAt: string | null,\n observation: TObservation | null,\n error: InteractionError | null,\n ): TReceipt {\n return this.adapter.makeReceipt({\n command,\n state,\n dispatchedAt,\n settledAt,\n observation,\n error,\n });\n }\n\n private async settle(entry: JournalEntry<TReceipt>, receipt: TReceipt): Promise<TReceipt> {\n entry.receipt = receipt;\n try {\n await this.publish(entry.operationId, entry.commandDigest, receipt);\n } catch {\n // Controller-lifetime terminal truth remains authoritative. A restored\n // dispatched receipt is always recovered as outcome_unknown, never replayed.\n }\n return receipt;\n }\n\n private async failBeforeDispatch(\n entry: JournalEntry<TReceipt>,\n command: TCommand,\n message: string,\n ): Promise<TReceipt> {\n return await this.settle(\n entry,\n this.makeReceipt(command, \"failed\", null, this.timestamp(), null, {\n code: \"driver_failed\",\n message,\n retryable: true,\n }),\n );\n }\n\n private async publish(\n operationId: string,\n commandDigest: string,\n receipt: TReceipt,\n ): Promise<void> {\n await this.onJournalRecord?.({ operationId, commandDigest, receipt });\n }\n\n private timestamp(): string {\n return this.now().toISOString();\n }\n\n private makeJournalSpace(): void {\n while (this.journal.size >= this.maxJournalEntries) {\n const terminal = [...this.journal.entries()].find(([, entry]) =>\n terminalStates.has(entry.receipt.state),\n );\n if (!terminal) {\n throw new InteractionControllerError(\n \"journal_full\",\n `${this.adapter.resourceLabel} operation journal has no safely evictable terminal entry`,\n true,\n );\n }\n this.journal.delete(terminal[0]);\n }\n }\n\n private restoreJournalRecord(record: InteractionOperationJournalRecord<TReceipt>): void {\n if (this.journal.has(record.operationId)) {\n throw new Error(`duplicate restored operation id: ${record.operationId}`);\n }\n const parsed = this.adapter.parseReceipt(record.receipt);\n if (parsed.operationId !== record.operationId) {\n throw new Error(`restored operation ${record.operationId} has another receipt id`);\n }\n const receipt = this.adapter.recoverReceipt(parsed, this.timestamp());\n const entry: JournalEntry<TReceipt> = {\n operationId: record.operationId,\n commandDigest: record.commandDigest,\n receipt,\n completion: Promise.resolve(receipt),\n preparationPersisted: Promise.resolve(true),\n };\n this.journal.set(record.operationId, entry);\n }\n}\n\nexport function recoverInteractionReceipt<TReceipt extends InteractionCoreReceipt<unknown>>(\n receipt: TReceipt,\n settledAt: string,\n resourceLabel: string,\n parse: (value: unknown) => TReceipt,\n): TReceipt {\n if (terminalStates.has(receipt.state)) return receipt;\n return parse({\n ...receipt,\n state: receipt.state === \"dispatched\" ? \"outcome_unknown\" : \"failed\",\n settledAt,\n observation: null,\n error:\n receipt.state === \"dispatched\"\n ? {\n code: \"controller_lost\",\n message: `${resourceLabel} controller restarted after command dispatch`,\n retryable: false,\n }\n : {\n code: \"controller_lost\",\n message: `${resourceLabel} controller restarted before command dispatch`,\n retryable: true,\n },\n });\n}\n\nfunction safePredispatchError(error: unknown, resourceLabel: string): InteractionError {\n if (error instanceof InteractionControllerError && error.code !== \"journal_full\") {\n return {\n code: error.code,\n message: error.message,\n retryable: error.retryable,\n };\n }\n return {\n code: \"driver_failed\",\n message: `${resourceLabel} command failed before dispatch`,\n retryable: false,\n };\n}\n\nfunction digestJson(value: unknown): string {\n return createHash(\"sha256\").update(canonicalJson(value), \"utf8\").digest(\"hex\");\n}\n\nfunction canonicalJson(value: unknown): string {\n return JSON.stringify(canonicalJsonValue(value));\n}\n\nfunction canonicalJsonValue(value: unknown): unknown {\n if (Array.isArray(value)) return value.map(canonicalJsonValue);\n if (value !== null && typeof value === \"object\") {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>)\n .sort(([left], [right]) => left.localeCompare(right))\n .map(([key, entry]) => [key, canonicalJsonValue(entry)]),\n );\n }\n return value;\n}\n"],"mappings":"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","names":["createHash","createHash","canonicalJson","canonicalJsonValue"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/interaction",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Provider-neutral browser and computer interaction controller for OpenGeni.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"prepublishOnly": "bash ../../scripts/prepublish-guard"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@opengeni/contracts": "^0.
|
|
37
|
+
"@opengeni/contracts": "^0.50.0"
|
|
38
38
|
},
|
|
39
39
|
"engines": {
|
|
40
40
|
"node": ">=18"
|
package/src/controller-core.ts
CHANGED
|
@@ -67,7 +67,7 @@ type InteractionDriver<
|
|
|
67
67
|
target(targetId: string): Promise<TTarget | null>;
|
|
68
68
|
observe(targetId: string): Promise<TObservation>;
|
|
69
69
|
validate?(command: TCommand, target: TTarget): Promise<void> | void;
|
|
70
|
-
dispatch(command: TCommand): Promise<TObservation>;
|
|
70
|
+
dispatch(command: TCommand): Promise<TObservation | null>;
|
|
71
71
|
};
|
|
72
72
|
|
|
73
73
|
type InteractionCoreAdapter<
|
|
@@ -113,6 +113,10 @@ export type InteractionControllerCoreOptions<
|
|
|
113
113
|
now?: () => Date;
|
|
114
114
|
initialJournal?: readonly InteractionOperationJournalRecord<TReceipt>[];
|
|
115
115
|
onJournalRecord?: (record: InteractionOperationJournalRecord<TReceipt>) => Promise<void> | void;
|
|
116
|
+
/** Override only when a private command contains ephemeral secret material.
|
|
117
|
+
* The callback must bind every non-secret semantic input while excluding the
|
|
118
|
+
* value bytes themselves and return lowercase SHA-256. */
|
|
119
|
+
commandDigest?: (command: TCommand) => string;
|
|
116
120
|
};
|
|
117
121
|
|
|
118
122
|
type JournalEntry<TReceipt> = InteractionOperationJournalRecord<TReceipt> & {
|
|
@@ -145,6 +149,7 @@ export class InteractionControllerCore<
|
|
|
145
149
|
private readonly onJournalRecord:
|
|
146
150
|
| ((record: InteractionOperationJournalRecord<TReceipt>) => Promise<void> | void)
|
|
147
151
|
| undefined;
|
|
152
|
+
private readonly commandDigest: (command: TCommand) => string;
|
|
148
153
|
private readonly journal = new Map<string, JournalEntry<TReceipt>>();
|
|
149
154
|
private readonly targetTails = new Map<string, Promise<void>>();
|
|
150
155
|
|
|
@@ -157,6 +162,7 @@ export class InteractionControllerCore<
|
|
|
157
162
|
this.maxJournalEntries = options.maxJournalEntries ?? 10_000;
|
|
158
163
|
this.now = options.now ?? (() => new Date());
|
|
159
164
|
this.onJournalRecord = options.onJournalRecord;
|
|
165
|
+
this.commandDigest = options.commandDigest ?? digestJson;
|
|
160
166
|
if (!Number.isSafeInteger(this.maxJournalEntries) || this.maxJournalEntries < 1) {
|
|
161
167
|
throw new Error("maxJournalEntries must be a positive safe integer");
|
|
162
168
|
}
|
|
@@ -173,7 +179,10 @@ export class InteractionControllerCore<
|
|
|
173
179
|
run(commandInput: TCommand): Promise<TReceipt> {
|
|
174
180
|
const command = this.adapter.parseCommand(commandInput);
|
|
175
181
|
this.adapter.assertCommandAuthority(command);
|
|
176
|
-
const commandDigest =
|
|
182
|
+
const commandDigest = this.commandDigest(command);
|
|
183
|
+
if (!/^[0-9a-f]{64}$/u.test(commandDigest)) {
|
|
184
|
+
throw new Error(`${this.adapter.resourceLabel} command digest must be lowercase SHA-256`);
|
|
185
|
+
}
|
|
177
186
|
const existing = this.journal.get(command.operationId);
|
|
178
187
|
if (existing) {
|
|
179
188
|
if (existing.commandDigest !== commandDigest) {
|
|
@@ -273,8 +282,12 @@ export class InteractionControllerCore<
|
|
|
273
282
|
}
|
|
274
283
|
|
|
275
284
|
try {
|
|
276
|
-
const
|
|
277
|
-
|
|
285
|
+
const dispatchedObservation = await this.driver.dispatch(command);
|
|
286
|
+
const observation =
|
|
287
|
+
dispatchedObservation === null
|
|
288
|
+
? null
|
|
289
|
+
: this.adapter.parseObservation(dispatchedObservation);
|
|
290
|
+
if (observation) this.adapter.assertObservationAuthority(observation, command.targetId);
|
|
278
291
|
const completed = this.makeReceipt(
|
|
279
292
|
command,
|
|
280
293
|
"completed",
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import {
|
|
2
3
|
BrowserActionCommand,
|
|
3
4
|
BrowserActionReceipt,
|
|
4
5
|
BrowserObservation,
|
|
6
|
+
BrowserProtectedAuthFillCommand,
|
|
7
|
+
BrowserProtectedAuthFillReceipt,
|
|
8
|
+
BrowserProtectedAuthObservation,
|
|
5
9
|
BrowserTarget,
|
|
6
10
|
ComputerActionCommand,
|
|
7
11
|
ComputerActionReceipt,
|
|
@@ -10,6 +14,9 @@ import {
|
|
|
10
14
|
type BrowserActionCommand as BrowserActionCommandValue,
|
|
11
15
|
type BrowserActionReceipt as BrowserActionReceiptValue,
|
|
12
16
|
type BrowserObservation as BrowserObservationValue,
|
|
17
|
+
type BrowserProtectedAuthFillCommand as BrowserProtectedAuthFillCommandValue,
|
|
18
|
+
type BrowserProtectedAuthFillReceipt as BrowserProtectedAuthFillReceiptValue,
|
|
19
|
+
type BrowserProtectedAuthObservation as BrowserProtectedAuthObservationValue,
|
|
13
20
|
type BrowserTarget as BrowserTargetValue,
|
|
14
21
|
type ComputerActionCommand as ComputerActionCommandValue,
|
|
15
22
|
type ComputerActionReceipt as ComputerActionReceiptValue,
|
|
@@ -35,7 +42,7 @@ export type BrowserInteractionDriver = {
|
|
|
35
42
|
target(targetId: string): Promise<BrowserTargetValue | null>;
|
|
36
43
|
observe(targetId: string): Promise<BrowserObservationValue>;
|
|
37
44
|
validate?(command: BrowserActionCommandValue, target: BrowserTargetValue): Promise<void> | void;
|
|
38
|
-
dispatch(command: BrowserActionCommandValue): Promise<BrowserObservationValue>;
|
|
45
|
+
dispatch(command: BrowserActionCommandValue): Promise<BrowserObservationValue | null>;
|
|
39
46
|
};
|
|
40
47
|
|
|
41
48
|
export type BrowserInteractionAuthority = {
|
|
@@ -112,12 +119,14 @@ export class BrowserInteractionController {
|
|
|
112
119
|
throw new InteractionControllerError(
|
|
113
120
|
"target_stale",
|
|
114
121
|
"browser target changed before the command could dispatch",
|
|
122
|
+
true,
|
|
115
123
|
);
|
|
116
124
|
}
|
|
117
125
|
if (command.expectedDocumentGeneration !== target.documentGeneration) {
|
|
118
126
|
throw new InteractionControllerError(
|
|
119
127
|
"document_stale",
|
|
120
128
|
"browser document changed before the command could dispatch",
|
|
129
|
+
true,
|
|
121
130
|
);
|
|
122
131
|
}
|
|
123
132
|
},
|
|
@@ -199,11 +208,212 @@ export function recoverBrowserOperationJournalRecord(
|
|
|
199
208
|
};
|
|
200
209
|
}
|
|
201
210
|
|
|
211
|
+
export type BrowserProtectedAuthDriver = {
|
|
212
|
+
target(targetId: string): Promise<BrowserTargetValue | null>;
|
|
213
|
+
observe(targetId: string): Promise<BrowserProtectedAuthObservationValue>;
|
|
214
|
+
validate?(
|
|
215
|
+
command: BrowserProtectedAuthFillCommandValue,
|
|
216
|
+
target: BrowserTargetValue,
|
|
217
|
+
): Promise<void> | void;
|
|
218
|
+
dispatch(
|
|
219
|
+
command: BrowserProtectedAuthFillCommandValue,
|
|
220
|
+
): Promise<BrowserProtectedAuthObservationValue>;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export type BrowserProtectedAuthAuthority = {
|
|
224
|
+
authorizeDispatch(command: BrowserProtectedAuthFillCommandValue): Promise<void> | void;
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export type BrowserProtectedAuthOperationJournalRecord =
|
|
228
|
+
InteractionOperationJournalRecord<BrowserProtectedAuthFillReceiptValue>;
|
|
229
|
+
|
|
230
|
+
export type BrowserProtectedAuthControllerOptions = {
|
|
231
|
+
browserSessionId: string;
|
|
232
|
+
controllerGeneration: string;
|
|
233
|
+
driver: BrowserProtectedAuthDriver;
|
|
234
|
+
authority?: BrowserProtectedAuthAuthority;
|
|
235
|
+
maxJournalEntries?: number;
|
|
236
|
+
now?: () => Date;
|
|
237
|
+
initialJournal?: readonly BrowserProtectedAuthOperationJournalRecord[];
|
|
238
|
+
onJournalRecord?: (record: BrowserProtectedAuthOperationJournalRecord) => Promise<void> | void;
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/** Secret-bearing commands are admitted only through this controller-private
|
|
242
|
+
* authority. Its durable digest binds all semantics plus credential version,
|
|
243
|
+
* but deliberately excludes ephemeral password/TOTP value bytes. */
|
|
244
|
+
export class BrowserProtectedAuthController {
|
|
245
|
+
private readonly core: InteractionControllerCore<
|
|
246
|
+
BrowserProtectedAuthFillCommandValue,
|
|
247
|
+
BrowserTargetValue,
|
|
248
|
+
BrowserProtectedAuthObservationValue,
|
|
249
|
+
BrowserProtectedAuthFillReceiptValue
|
|
250
|
+
>;
|
|
251
|
+
|
|
252
|
+
constructor(options: BrowserProtectedAuthControllerOptions) {
|
|
253
|
+
const { browserSessionId, controllerGeneration } = options;
|
|
254
|
+
this.core = new InteractionControllerCore({
|
|
255
|
+
driver: options.driver,
|
|
256
|
+
...(options.authority ? { authority: options.authority } : {}),
|
|
257
|
+
...(options.maxJournalEntries !== undefined
|
|
258
|
+
? { maxJournalEntries: options.maxJournalEntries }
|
|
259
|
+
: {}),
|
|
260
|
+
...(options.now ? { now: options.now } : {}),
|
|
261
|
+
...(options.initialJournal ? { initialJournal: options.initialJournal } : {}),
|
|
262
|
+
...(options.onJournalRecord ? { onJournalRecord: options.onJournalRecord } : {}),
|
|
263
|
+
commandDigest: protectedAuthCommandDigest,
|
|
264
|
+
adapter: {
|
|
265
|
+
resourceLabel: "browser protected fill",
|
|
266
|
+
parseCommand: (value) => BrowserProtectedAuthFillCommand.parse(value),
|
|
267
|
+
parseTarget: (value) => BrowserTarget.parse(value),
|
|
268
|
+
parseObservation: (value) => BrowserProtectedAuthObservation.parse(value),
|
|
269
|
+
parseReceipt: (value) => BrowserProtectedAuthFillReceipt.parse(value),
|
|
270
|
+
assertCommandAuthority(command) {
|
|
271
|
+
if (command.browserSessionId !== browserSessionId) {
|
|
272
|
+
throw new InteractionControllerError(
|
|
273
|
+
"resource_not_found",
|
|
274
|
+
"protected fill targets another browser session",
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (command.controllerGeneration !== controllerGeneration) {
|
|
278
|
+
throw new InteractionControllerError(
|
|
279
|
+
"controller_stale",
|
|
280
|
+
"protected fill targets a stale browser controller",
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
assertTargetAuthority(target) {
|
|
285
|
+
if (
|
|
286
|
+
target.browserSessionId !== browserSessionId ||
|
|
287
|
+
target.controllerGeneration !== controllerGeneration
|
|
288
|
+
) {
|
|
289
|
+
throw new InteractionControllerError(
|
|
290
|
+
"controller_stale",
|
|
291
|
+
"protected-fill target belongs to a stale browser controller",
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
assertExpectedGenerations(command, target) {
|
|
296
|
+
if (command.expectedTargetGeneration !== target.targetGeneration) {
|
|
297
|
+
throw new InteractionControllerError(
|
|
298
|
+
"target_stale",
|
|
299
|
+
"browser target changed before protected fill",
|
|
300
|
+
true,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
if (command.expectedDocumentGeneration !== target.documentGeneration) {
|
|
304
|
+
throw new InteractionControllerError(
|
|
305
|
+
"document_stale",
|
|
306
|
+
"browser document changed before protected fill",
|
|
307
|
+
true,
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
},
|
|
311
|
+
assertObservationAuthority(observation, targetId) {
|
|
312
|
+
if (
|
|
313
|
+
observation.target.id !== targetId ||
|
|
314
|
+
observation.target.browserSessionId !== browserSessionId ||
|
|
315
|
+
observation.target.controllerGeneration !== controllerGeneration
|
|
316
|
+
) {
|
|
317
|
+
throw new InteractionControllerError(
|
|
318
|
+
"driver_failed",
|
|
319
|
+
"protected-fill driver returned a result outside controller authority",
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
},
|
|
323
|
+
makeReceipt(input) {
|
|
324
|
+
return BrowserProtectedAuthFillReceipt.parse({
|
|
325
|
+
protocolVersion: 1,
|
|
326
|
+
operationId: input.command.operationId,
|
|
327
|
+
browserSessionId,
|
|
328
|
+
controllerGeneration,
|
|
329
|
+
targetId: input.command.targetId,
|
|
330
|
+
state: input.state,
|
|
331
|
+
dispatchedAt: input.dispatchedAt,
|
|
332
|
+
settledAt: input.settledAt,
|
|
333
|
+
observation: input.observation,
|
|
334
|
+
error: input.error,
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
recoverReceipt(receipt, settledAt) {
|
|
338
|
+
if (
|
|
339
|
+
receipt.browserSessionId !== browserSessionId ||
|
|
340
|
+
receipt.controllerGeneration !== controllerGeneration
|
|
341
|
+
) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`restored protected-fill operation ${receipt.operationId} is outside controller authority`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return recoverInteractionReceipt(receipt, settledAt, "browser protected fill", (value) =>
|
|
347
|
+
BrowserProtectedAuthFillReceipt.parse(value),
|
|
348
|
+
);
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
run(
|
|
355
|
+
command: BrowserProtectedAuthFillCommandValue,
|
|
356
|
+
): Promise<BrowserProtectedAuthFillReceiptValue> {
|
|
357
|
+
return this.core.run(command);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
receipt(operationId: string): BrowserProtectedAuthFillReceiptValue | null {
|
|
361
|
+
return this.core.receipt(operationId);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
journalSnapshot(): BrowserProtectedAuthOperationJournalRecord[] {
|
|
365
|
+
return this.core.journalSnapshot();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
waitForIdle(): Promise<void> {
|
|
369
|
+
return this.core.waitForIdle();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function recoverBrowserProtectedAuthOperationJournalRecord(
|
|
374
|
+
record: BrowserProtectedAuthOperationJournalRecord,
|
|
375
|
+
settledAt: string,
|
|
376
|
+
): BrowserProtectedAuthOperationJournalRecord {
|
|
377
|
+
const receipt = BrowserProtectedAuthFillReceipt.parse(record.receipt);
|
|
378
|
+
return {
|
|
379
|
+
...record,
|
|
380
|
+
receipt: recoverInteractionReceipt(receipt, settledAt, "browser protected fill", (value) =>
|
|
381
|
+
BrowserProtectedAuthFillReceipt.parse(value),
|
|
382
|
+
),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function protectedAuthCommandDigest(command: BrowserProtectedAuthFillCommandValue): string {
|
|
387
|
+
const { fields, ...metadata } = command;
|
|
388
|
+
const secretFree = {
|
|
389
|
+
...metadata,
|
|
390
|
+
fields: fields.map(({ value: _value, ...field }) => field),
|
|
391
|
+
};
|
|
392
|
+
return createHash("sha256").update(canonicalJson(secretFree), "utf8").digest("hex");
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function canonicalJson(value: unknown): string {
|
|
396
|
+
return JSON.stringify(canonicalJsonValue(value));
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function canonicalJsonValue(value: unknown): unknown {
|
|
400
|
+
if (Array.isArray(value)) return value.map(canonicalJsonValue);
|
|
401
|
+
if (value !== null && typeof value === "object") {
|
|
402
|
+
return Object.fromEntries(
|
|
403
|
+
Object.entries(value as Record<string, unknown>)
|
|
404
|
+
.filter(([, entry]) => entry !== undefined)
|
|
405
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
406
|
+
.map(([key, entry]) => [key, canonicalJsonValue(entry)]),
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
return value;
|
|
410
|
+
}
|
|
411
|
+
|
|
202
412
|
export type ComputerInteractionDriver = {
|
|
203
413
|
target(targetId: string): Promise<ComputerTargetValue | null>;
|
|
204
414
|
observe(targetId: string): Promise<ComputerObservationValue>;
|
|
205
415
|
validate?(command: ComputerActionCommandValue, target: ComputerTargetValue): Promise<void> | void;
|
|
206
|
-
dispatch(command: ComputerActionCommandValue): Promise<ComputerObservationValue>;
|
|
416
|
+
dispatch(command: ComputerActionCommandValue): Promise<ComputerObservationValue | null>;
|
|
207
417
|
};
|
|
208
418
|
|
|
209
419
|
export type ComputerInteractionAuthority = {
|
|
@@ -281,6 +491,7 @@ export class ComputerInteractionController {
|
|
|
281
491
|
throw new InteractionControllerError(
|
|
282
492
|
"target_stale",
|
|
283
493
|
"computer target changed before the command could dispatch",
|
|
494
|
+
true,
|
|
284
495
|
);
|
|
285
496
|
}
|
|
286
497
|
},
|