@flowdot.ai/guardian-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +40 -0
- package/README.md +281 -0
- package/ROADMAP.md +109 -0
- package/dist/audit/attestor.d.ts +102 -0
- package/dist/audit/attestor.d.ts.map +1 -0
- package/dist/audit/attestor.js +103 -0
- package/dist/audit/attestor.js.map +1 -0
- package/dist/audit/chain.d.ts +30 -0
- package/dist/audit/chain.d.ts.map +1 -0
- package/dist/audit/chain.js +65 -0
- package/dist/audit/chain.js.map +1 -0
- package/dist/audit/correlation.d.ts +114 -0
- package/dist/audit/correlation.d.ts.map +1 -0
- package/dist/audit/correlation.js +259 -0
- package/dist/audit/correlation.js.map +1 -0
- package/dist/audit/index.d.ts +13 -0
- package/dist/audit/index.d.ts.map +1 -0
- package/dist/audit/index.js +8 -0
- package/dist/audit/index.js.map +1 -0
- package/dist/audit/reader.d.ts +30 -0
- package/dist/audit/reader.d.ts.map +1 -0
- package/dist/audit/reader.js +85 -0
- package/dist/audit/reader.js.map +1 -0
- package/dist/audit/signature.d.ts +39 -0
- package/dist/audit/signature.d.ts.map +1 -0
- package/dist/audit/signature.js +73 -0
- package/dist/audit/signature.js.map +1 -0
- package/dist/audit/stats.d.ts +106 -0
- package/dist/audit/stats.d.ts.map +1 -0
- package/dist/audit/stats.js +196 -0
- package/dist/audit/stats.js.map +1 -0
- package/dist/audit/writer.d.ts +96 -0
- package/dist/audit/writer.d.ts.map +1 -0
- package/dist/audit/writer.js +263 -0
- package/dist/audit/writer.js.map +1 -0
- package/dist/cli/guardian-baseline.d.ts +42 -0
- package/dist/cli/guardian-baseline.d.ts.map +1 -0
- package/dist/cli/guardian-baseline.js +265 -0
- package/dist/cli/guardian-baseline.js.map +1 -0
- package/dist/cli/guardian-correlator.d.ts +47 -0
- package/dist/cli/guardian-correlator.d.ts.map +1 -0
- package/dist/cli/guardian-correlator.js +217 -0
- package/dist/cli/guardian-correlator.js.map +1 -0
- package/dist/cli/guardian-verify.d.ts +30 -0
- package/dist/cli/guardian-verify.d.ts.map +1 -0
- package/dist/cli/guardian-verify.js +149 -0
- package/dist/cli/guardian-verify.js.map +1 -0
- package/dist/errors.d.ts +28 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +40 -0
- package/dist/errors.js.map +1 -0
- package/dist/estop/heartbeat.d.ts +94 -0
- package/dist/estop/heartbeat.d.ts.map +1 -0
- package/dist/estop/heartbeat.js +135 -0
- package/dist/estop/heartbeat.js.map +1 -0
- package/dist/estop/hub.d.ts +76 -0
- package/dist/estop/hub.d.ts.map +1 -0
- package/dist/estop/hub.js +167 -0
- package/dist/estop/hub.js.map +1 -0
- package/dist/estop/index.d.ts +12 -0
- package/dist/estop/index.d.ts.map +1 -0
- package/dist/estop/index.js +6 -0
- package/dist/estop/index.js.map +1 -0
- package/dist/estop/local.d.ts +31 -0
- package/dist/estop/local.d.ts.map +1 -0
- package/dist/estop/local.js +101 -0
- package/dist/estop/local.js.map +1 -0
- package/dist/estop/middleware.d.ts +36 -0
- package/dist/estop/middleware.d.ts.map +1 -0
- package/dist/estop/middleware.js +40 -0
- package/dist/estop/middleware.js.map +1 -0
- package/dist/estop/poller.d.ts +36 -0
- package/dist/estop/poller.d.ts.map +1 -0
- package/dist/estop/poller.js +85 -0
- package/dist/estop/poller.js.map +1 -0
- package/dist/estop/types.d.ts +31 -0
- package/dist/estop/types.d.ts.map +1 -0
- package/dist/estop/types.js +5 -0
- package/dist/estop/types.js.map +1 -0
- package/dist/gate/async-callback.d.ts +27 -0
- package/dist/gate/async-callback.d.ts.map +1 -0
- package/dist/gate/async-callback.js +79 -0
- package/dist/gate/async-callback.js.map +1 -0
- package/dist/gate/cli.d.ts +29 -0
- package/dist/gate/cli.d.ts.map +1 -0
- package/dist/gate/cli.js +83 -0
- package/dist/gate/cli.js.map +1 -0
- package/dist/gate/data-channel.d.ts +41 -0
- package/dist/gate/data-channel.d.ts.map +1 -0
- package/dist/gate/data-channel.js +132 -0
- package/dist/gate/data-channel.js.map +1 -0
- package/dist/gate/index.d.ts +13 -0
- package/dist/gate/index.d.ts.map +1 -0
- package/dist/gate/index.js +7 -0
- package/dist/gate/index.js.map +1 -0
- package/dist/gate/options.d.ts +90 -0
- package/dist/gate/options.d.ts.map +1 -0
- package/dist/gate/options.js +131 -0
- package/dist/gate/options.js.map +1 -0
- package/dist/gate/programmatic.d.ts +9 -0
- package/dist/gate/programmatic.d.ts.map +1 -0
- package/dist/gate/programmatic.js +20 -0
- package/dist/gate/programmatic.js.map +1 -0
- package/dist/gate/two-key.d.ts +90 -0
- package/dist/gate/two-key.d.ts.map +1 -0
- package/dist/gate/two-key.js +78 -0
- package/dist/gate/two-key.js.map +1 -0
- package/dist/gate/types.d.ts +25 -0
- package/dist/gate/types.d.ts.map +1 -0
- package/dist/gate/types.js +5 -0
- package/dist/gate/types.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/notify/console.d.ts +13 -0
- package/dist/notify/console.d.ts.map +1 -0
- package/dist/notify/console.js +27 -0
- package/dist/notify/console.js.map +1 -0
- package/dist/notify/index.d.ts +8 -0
- package/dist/notify/index.d.ts.map +1 -0
- package/dist/notify/index.js +4 -0
- package/dist/notify/index.js.map +1 -0
- package/dist/notify/multi.d.ts +14 -0
- package/dist/notify/multi.d.ts.map +1 -0
- package/dist/notify/multi.js +22 -0
- package/dist/notify/multi.js.map +1 -0
- package/dist/notify/types.d.ts +21 -0
- package/dist/notify/types.d.ts.map +1 -0
- package/dist/notify/types.js +5 -0
- package/dist/notify/types.js.map +1 -0
- package/dist/notify/webhook.d.ts +21 -0
- package/dist/notify/webhook.d.ts.map +1 -0
- package/dist/notify/webhook.js +37 -0
- package/dist/notify/webhook.js.map +1 -0
- package/dist/policy/attribution.d.ts +61 -0
- package/dist/policy/attribution.d.ts.map +1 -0
- package/dist/policy/attribution.js +116 -0
- package/dist/policy/attribution.js.map +1 -0
- package/dist/policy/evaluator.d.ts +36 -0
- package/dist/policy/evaluator.d.ts.map +1 -0
- package/dist/policy/evaluator.js +211 -0
- package/dist/policy/evaluator.js.map +1 -0
- package/dist/policy/index.d.ts +11 -0
- package/dist/policy/index.d.ts.map +1 -0
- package/dist/policy/index.js +7 -0
- package/dist/policy/index.js.map +1 -0
- package/dist/policy/integrity.d.ts +17 -0
- package/dist/policy/integrity.d.ts.map +1 -0
- package/dist/policy/integrity.js +31 -0
- package/dist/policy/integrity.js.map +1 -0
- package/dist/policy/loader.d.ts +9 -0
- package/dist/policy/loader.d.ts.map +1 -0
- package/dist/policy/loader.js +124 -0
- package/dist/policy/loader.js.map +1 -0
- package/dist/policy/site-key.d.ts +22 -0
- package/dist/policy/site-key.d.ts.map +1 -0
- package/dist/policy/site-key.js +48 -0
- package/dist/policy/site-key.js.map +1 -0
- package/dist/policy/store.d.ts +45 -0
- package/dist/policy/store.d.ts.map +1 -0
- package/dist/policy/store.js +223 -0
- package/dist/policy/store.js.map +1 -0
- package/dist/policy/types.d.ts +72 -0
- package/dist/policy/types.d.ts.map +1 -0
- package/dist/policy/types.js +5 -0
- package/dist/policy/types.js.map +1 -0
- package/dist/runtime/capability.d.ts +125 -0
- package/dist/runtime/capability.d.ts.map +1 -0
- package/dist/runtime/capability.js +121 -0
- package/dist/runtime/capability.js.map +1 -0
- package/dist/runtime/honeytokens.d.ts +104 -0
- package/dist/runtime/honeytokens.d.ts.map +1 -0
- package/dist/runtime/honeytokens.js +115 -0
- package/dist/runtime/honeytokens.js.map +1 -0
- package/dist/runtime/multi-rate-limiter.d.ts +90 -0
- package/dist/runtime/multi-rate-limiter.d.ts.map +1 -0
- package/dist/runtime/multi-rate-limiter.js +133 -0
- package/dist/runtime/multi-rate-limiter.js.map +1 -0
- package/dist/runtime/runtime.d.ts +94 -0
- package/dist/runtime/runtime.d.ts.map +1 -0
- package/dist/runtime/runtime.js +276 -0
- package/dist/runtime/runtime.js.map +1 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dead-man's-switch heartbeat. SPEC §6 (v0.4.0+).
|
|
3
|
+
*
|
|
4
|
+
* For long-running surfaces (CLI daemon, MCP server's persistent session,
|
|
5
|
+
* Native main loop) the agent (or its harness) must call
|
|
6
|
+
* `monitor.heartbeat()` every N seconds. If no heartbeat arrives within
|
|
7
|
+
* `softMs`, an `x_heartbeat_warning` audit row is written (no behavior
|
|
8
|
+
* change). If still no heartbeat by `hardMs`, the configured `EStopLocal`
|
|
9
|
+
* is pressed with `reason: 'heartbeat_missed'`.
|
|
10
|
+
*
|
|
11
|
+
* Pure mechanism: setInterval + timestamp arithmetic. The supervisor does
|
|
12
|
+
* not decide whether the agent should be alive — only whether it is
|
|
13
|
+
* claiming to be.
|
|
14
|
+
*
|
|
15
|
+
* OPT-IN PER SURFACE. Default OFF. A surface that hasn't wired
|
|
16
|
+
* `heartbeat()` calls into its main loop must not enable this — that's a
|
|
17
|
+
* day-1 false E-stop.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* State machine: idle → softMissed → hardMissed. Each transition writes
|
|
21
|
+
* an audit row (warning for softMissed, estop_press for hardMissed). The
|
|
22
|
+
* monitor stops checking once hardMissed fires; restart by constructing a
|
|
23
|
+
* new monitor (a new session).
|
|
24
|
+
*/
|
|
25
|
+
export class HeartbeatMonitor {
|
|
26
|
+
audit;
|
|
27
|
+
estop;
|
|
28
|
+
softMs;
|
|
29
|
+
hardMs;
|
|
30
|
+
checkIntervalMs;
|
|
31
|
+
now;
|
|
32
|
+
setIntervalFn;
|
|
33
|
+
clearIntervalFn;
|
|
34
|
+
lastBeat;
|
|
35
|
+
state = 'idle';
|
|
36
|
+
interval = null;
|
|
37
|
+
stopped = false;
|
|
38
|
+
constructor(options) {
|
|
39
|
+
if (options.softMs <= 0)
|
|
40
|
+
throw new Error('softMs must be > 0');
|
|
41
|
+
if (options.hardMs <= options.softMs) {
|
|
42
|
+
throw new Error('hardMs must be > softMs');
|
|
43
|
+
}
|
|
44
|
+
this.audit = options.audit;
|
|
45
|
+
this.estop = options.estop;
|
|
46
|
+
this.softMs = options.softMs;
|
|
47
|
+
this.hardMs = options.hardMs;
|
|
48
|
+
this.now = options.now ?? Date.now;
|
|
49
|
+
this.setIntervalFn =
|
|
50
|
+
options.setIntervalFn ?? ((cb, ms) => setInterval(cb, ms));
|
|
51
|
+
this.clearIntervalFn =
|
|
52
|
+
options.clearIntervalFn ?? ((h) => clearInterval(h));
|
|
53
|
+
this.checkIntervalMs =
|
|
54
|
+
options.checkIntervalMs ?? Math.max(50, Math.min(5000, Math.floor(this.softMs / 4)));
|
|
55
|
+
this.lastBeat = this.now();
|
|
56
|
+
}
|
|
57
|
+
/** Start the watchdog. Idempotent. */
|
|
58
|
+
start() {
|
|
59
|
+
if (this.interval !== null || this.stopped)
|
|
60
|
+
return;
|
|
61
|
+
this.lastBeat = this.now();
|
|
62
|
+
this.interval = this.setIntervalFn(() => {
|
|
63
|
+
void this.tick();
|
|
64
|
+
}, this.checkIntervalMs);
|
|
65
|
+
}
|
|
66
|
+
/** Stop the watchdog. Idempotent. Call from supervisor close(). */
|
|
67
|
+
stop() {
|
|
68
|
+
this.stopped = true;
|
|
69
|
+
if (this.interval !== null) {
|
|
70
|
+
this.clearIntervalFn(this.interval);
|
|
71
|
+
this.interval = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Record a heartbeat. Resets the state machine to `idle` if a soft
|
|
76
|
+
* warning had fired but no hard miss yet.
|
|
77
|
+
*/
|
|
78
|
+
heartbeat() {
|
|
79
|
+
if (this.state === 'hardMissed') {
|
|
80
|
+
// Cannot recover from a hard miss. Caller should restart the session.
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
this.lastBeat = this.now();
|
|
84
|
+
this.state = 'idle';
|
|
85
|
+
}
|
|
86
|
+
/** Current state (for tests + introspection). */
|
|
87
|
+
getState() {
|
|
88
|
+
return { state: this.state, lastBeatMs: this.lastBeat };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Public tick. Called automatically by the interval; exposed so tests can
|
|
92
|
+
* drive deterministically without real timers.
|
|
93
|
+
*/
|
|
94
|
+
async tick() {
|
|
95
|
+
if (this.stopped)
|
|
96
|
+
return;
|
|
97
|
+
const elapsed = this.now() - this.lastBeat;
|
|
98
|
+
if (elapsed >= this.hardMs && this.state !== 'hardMissed') {
|
|
99
|
+
this.state = 'hardMissed';
|
|
100
|
+
if (this.audit) {
|
|
101
|
+
// Audit the miss explicitly even though estop.press will also write
|
|
102
|
+
// an estop_press row. The two are useful together: the warning
|
|
103
|
+
// captures the elapsed time + threshold, the estop_press carries
|
|
104
|
+
// the eventual halt.
|
|
105
|
+
await this.audit.append({
|
|
106
|
+
kind: 'x_heartbeat_warning',
|
|
107
|
+
status: 'halted',
|
|
108
|
+
initiator: 'system',
|
|
109
|
+
detail: { elapsed_ms: elapsed, hard_ms: this.hardMs, level: 'hard' },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (this.estop) {
|
|
113
|
+
await this.estop.press({
|
|
114
|
+
reason: 'heartbeat_missed',
|
|
115
|
+
initiator: 'system',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
// Once hard-missed, stop checking — the session is halted.
|
|
119
|
+
this.stop();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (elapsed >= this.softMs && this.state === 'idle') {
|
|
123
|
+
this.state = 'softMissed';
|
|
124
|
+
if (this.audit) {
|
|
125
|
+
await this.audit.append({
|
|
126
|
+
kind: 'x_heartbeat_warning',
|
|
127
|
+
status: 'approved',
|
|
128
|
+
initiator: 'system',
|
|
129
|
+
detail: { elapsed_ms: elapsed, soft_ms: this.softMs, level: 'soft' },
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=heartbeat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heartbeat.js","sourceRoot":"","sources":["../../src/estop/heartbeat.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EStopHub — hub-coordinated emergency-stop adapter. SPEC §5.4.
|
|
3
|
+
*
|
|
4
|
+
* Pluggable state store (in-memory by default; FlowDot supplies a SQL adapter).
|
|
5
|
+
* Per-user scoping. 1-second cache TTL on isPressed reads (matches FlowDot).
|
|
6
|
+
* Notifier fan-out on every press AND clear.
|
|
7
|
+
*/
|
|
8
|
+
import type { AuditLogWriter } from '../audit/writer.js';
|
|
9
|
+
import type { Notifier } from '../notify/types.js';
|
|
10
|
+
import type { EStopClearOptions, EStopClearResult, EStopPressOptions, EStopPressResult, EStopState } from './types.js';
|
|
11
|
+
/**
|
|
12
|
+
* Backing store for hub state. Implementations: in-memory (default),
|
|
13
|
+
* SQL-backed (host-supplied).
|
|
14
|
+
*/
|
|
15
|
+
export interface EStopStateStore {
|
|
16
|
+
get(userId: string): Promise<EStopState | null>;
|
|
17
|
+
set(userId: string, state: EStopState): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Channel that fans out press notifications to live daemons (push side).
|
|
21
|
+
* Library-internal; FlowDot supplies the `comms_daemon_commands` adapter.
|
|
22
|
+
*/
|
|
23
|
+
export interface EStopBroadcastChannel {
|
|
24
|
+
broadcastPress(userId: string, state: EStopState): Promise<void>;
|
|
25
|
+
broadcastClear(userId: string, state: EStopState): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export interface EStopHubOptions {
|
|
28
|
+
state: EStopStateStore;
|
|
29
|
+
audit: AuditLogWriter;
|
|
30
|
+
notifier?: Notifier;
|
|
31
|
+
broadcast?: EStopBroadcastChannel;
|
|
32
|
+
/** Cache TTL for isPressed in ms. SPEC §5.4 default = 1000. */
|
|
33
|
+
cacheTtlMs?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Required recent-auth check for clear() — host supplies it. Return true if
|
|
36
|
+
* the operator has confirmed authentication recently; false to force a
|
|
37
|
+
* second factor (password.confirm flow on the host).
|
|
38
|
+
*/
|
|
39
|
+
recentAuthCheck?: (userId: string, options: EStopClearOptions) => Promise<boolean>;
|
|
40
|
+
/** Canonical clear URL included in notifications. */
|
|
41
|
+
canonicalClearUrl?: string;
|
|
42
|
+
}
|
|
43
|
+
/** Source surface tag included in audit + notification. */
|
|
44
|
+
export interface EStopActorContext {
|
|
45
|
+
source: string;
|
|
46
|
+
ip?: string;
|
|
47
|
+
userAgent?: string;
|
|
48
|
+
}
|
|
49
|
+
export declare class EStopHub {
|
|
50
|
+
private readonly state;
|
|
51
|
+
private readonly audit;
|
|
52
|
+
private readonly notifier;
|
|
53
|
+
private readonly broadcast;
|
|
54
|
+
private readonly cacheTtlMs;
|
|
55
|
+
private readonly recentAuthCheck;
|
|
56
|
+
private readonly canonicalClearUrl;
|
|
57
|
+
private cache;
|
|
58
|
+
constructor(options: EStopHubOptions);
|
|
59
|
+
/** Cached, hot-path check used by the middleware. */
|
|
60
|
+
isPressed(userId: string): Promise<boolean>;
|
|
61
|
+
status(userId: string): Promise<EStopState>;
|
|
62
|
+
press(userId: string, options: EStopPressOptions, actor?: EStopActorContext): Promise<EStopPressResult>;
|
|
63
|
+
clear(userId: string, options: EStopClearOptions, actor?: EStopActorContext): Promise<EStopClearResult>;
|
|
64
|
+
/** Force-invalidate a single user's cache entry. Idempotent. */
|
|
65
|
+
invalidateCache(userId: string): void;
|
|
66
|
+
/** Clear the entire cache. Useful for tests + Redis-down scenarios. */
|
|
67
|
+
invalidateAllCache(): void;
|
|
68
|
+
private fireNotification;
|
|
69
|
+
}
|
|
70
|
+
/** Reference in-memory state store. */
|
|
71
|
+
export declare class InMemoryEStopStateStore implements EStopStateStore {
|
|
72
|
+
private states;
|
|
73
|
+
get(userId: string): Promise<EStopState | null>;
|
|
74
|
+
set(userId: string, state: EStopState): Promise<void>;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=hub.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub.d.ts","sourceRoot":"","sources":["../../src/estop/hub.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EStopHub — hub-coordinated emergency-stop adapter. SPEC §5.4.
|
|
3
|
+
*
|
|
4
|
+
* Pluggable state store (in-memory by default; FlowDot supplies a SQL adapter).
|
|
5
|
+
* Per-user scoping. 1-second cache TTL on isPressed reads (matches FlowDot).
|
|
6
|
+
* Notifier fan-out on every press AND clear.
|
|
7
|
+
*/
|
|
8
|
+
const DEFAULT_CACHE_TTL_MS = 1000;
|
|
9
|
+
export class EStopHub {
|
|
10
|
+
state;
|
|
11
|
+
audit;
|
|
12
|
+
notifier;
|
|
13
|
+
broadcast;
|
|
14
|
+
cacheTtlMs;
|
|
15
|
+
recentAuthCheck;
|
|
16
|
+
canonicalClearUrl;
|
|
17
|
+
cache = new Map();
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.state = options.state;
|
|
20
|
+
this.audit = options.audit;
|
|
21
|
+
this.notifier = options.notifier;
|
|
22
|
+
this.broadcast = options.broadcast;
|
|
23
|
+
this.cacheTtlMs = options.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
|
|
24
|
+
this.recentAuthCheck = options.recentAuthCheck;
|
|
25
|
+
this.canonicalClearUrl = options.canonicalClearUrl;
|
|
26
|
+
}
|
|
27
|
+
/** Cached, hot-path check used by the middleware. */
|
|
28
|
+
async isPressed(userId) {
|
|
29
|
+
const now = Date.now();
|
|
30
|
+
const entry = this.cache.get(userId);
|
|
31
|
+
if (entry && entry.expiresAt > now)
|
|
32
|
+
return entry.value;
|
|
33
|
+
const state = await this.state.get(userId);
|
|
34
|
+
const pressed = state?.pressed ?? false;
|
|
35
|
+
this.cache.set(userId, { value: pressed, expiresAt: now + this.cacheTtlMs });
|
|
36
|
+
return pressed;
|
|
37
|
+
}
|
|
38
|
+
async status(userId) {
|
|
39
|
+
return (await this.state.get(userId)) ?? { pressed: false };
|
|
40
|
+
}
|
|
41
|
+
async press(userId, options, actor = { source: 'hub' }) {
|
|
42
|
+
// SPEC §7: reject agent-initiated press only if deployment policy demands
|
|
43
|
+
// it. The library default allows agent-initiated press (they SHOULD be
|
|
44
|
+
// able to halt themselves on anomaly). The caller controls via initiator.
|
|
45
|
+
const initiator = options.initiator ?? 'operator';
|
|
46
|
+
const existing = (await this.state.get(userId)) ?? { pressed: false };
|
|
47
|
+
const newState = existing.pressed
|
|
48
|
+
? existing
|
|
49
|
+
: {
|
|
50
|
+
pressed: true,
|
|
51
|
+
pressedAt: new Date().toISOString(),
|
|
52
|
+
pressedReason: options.reason,
|
|
53
|
+
...(options.operatorId === undefined ? {} : { pressedOperatorId: options.operatorId }),
|
|
54
|
+
};
|
|
55
|
+
if (!existing.pressed) {
|
|
56
|
+
await this.state.set(userId, newState);
|
|
57
|
+
this.invalidateCache(userId);
|
|
58
|
+
}
|
|
59
|
+
await this.audit.append({
|
|
60
|
+
kind: 'estop_press',
|
|
61
|
+
status: 'halted',
|
|
62
|
+
initiator,
|
|
63
|
+
detail: {
|
|
64
|
+
user_id: userId,
|
|
65
|
+
source: actor.source,
|
|
66
|
+
reason: options.reason,
|
|
67
|
+
...(options.operatorId === undefined ? {} : { operator_id: options.operatorId }),
|
|
68
|
+
...(actor.ip === undefined ? {} : { ip: actor.ip }),
|
|
69
|
+
...(actor.userAgent === undefined ? {} : { user_agent: actor.userAgent }),
|
|
70
|
+
...(options.detail ?? {}),
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
if (this.broadcast && !existing.pressed) {
|
|
74
|
+
await this.broadcast.broadcastPress(userId, newState);
|
|
75
|
+
}
|
|
76
|
+
await this.fireNotification('estop_press', userId, actor, {
|
|
77
|
+
reason: options.reason,
|
|
78
|
+
...(options.operatorId === undefined ? {} : { operator_id: options.operatorId }),
|
|
79
|
+
...(options.detail ?? {}),
|
|
80
|
+
});
|
|
81
|
+
return { state: { ...newState } };
|
|
82
|
+
}
|
|
83
|
+
async clear(userId, options, actor = { source: 'hub' }) {
|
|
84
|
+
// SPEC §7: agent-initiated clear MUST be rejected.
|
|
85
|
+
const initiator = options.initiator ?? 'operator';
|
|
86
|
+
if (initiator === 'agent') {
|
|
87
|
+
return {
|
|
88
|
+
state: (await this.state.get(userId)) ?? { pressed: false },
|
|
89
|
+
authRequired: false,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
if (this.recentAuthCheck) {
|
|
93
|
+
const ok = await this.recentAuthCheck(userId, options);
|
|
94
|
+
if (!ok) {
|
|
95
|
+
return {
|
|
96
|
+
state: (await this.state.get(userId)) ?? { pressed: false },
|
|
97
|
+
authRequired: true,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const existing = (await this.state.get(userId)) ?? { pressed: false };
|
|
102
|
+
if (!existing.pressed) {
|
|
103
|
+
return { state: existing };
|
|
104
|
+
}
|
|
105
|
+
const cleared = {
|
|
106
|
+
pressed: false,
|
|
107
|
+
clearedAt: new Date().toISOString(),
|
|
108
|
+
};
|
|
109
|
+
await this.state.set(userId, cleared);
|
|
110
|
+
this.invalidateCache(userId);
|
|
111
|
+
await this.audit.append({
|
|
112
|
+
kind: 'estop_clear',
|
|
113
|
+
status: 'approved',
|
|
114
|
+
initiator,
|
|
115
|
+
detail: {
|
|
116
|
+
user_id: userId,
|
|
117
|
+
source: actor.source,
|
|
118
|
+
...(options.operatorId === undefined ? {} : { operator_id: options.operatorId }),
|
|
119
|
+
...(actor.ip === undefined ? {} : { ip: actor.ip }),
|
|
120
|
+
...(actor.userAgent === undefined ? {} : { user_agent: actor.userAgent }),
|
|
121
|
+
...(options.detail ?? {}),
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
if (this.broadcast) {
|
|
125
|
+
await this.broadcast.broadcastClear(userId, cleared);
|
|
126
|
+
}
|
|
127
|
+
await this.fireNotification('estop_clear', userId, actor, {
|
|
128
|
+
...(options.operatorId === undefined ? {} : { operator_id: options.operatorId }),
|
|
129
|
+
...(options.detail ?? {}),
|
|
130
|
+
});
|
|
131
|
+
return { state: cleared };
|
|
132
|
+
}
|
|
133
|
+
/** Force-invalidate a single user's cache entry. Idempotent. */
|
|
134
|
+
invalidateCache(userId) {
|
|
135
|
+
this.cache.delete(userId);
|
|
136
|
+
}
|
|
137
|
+
/** Clear the entire cache. Useful for tests + Redis-down scenarios. */
|
|
138
|
+
invalidateAllCache() {
|
|
139
|
+
this.cache.clear();
|
|
140
|
+
}
|
|
141
|
+
async fireNotification(kind, userId, actor, summary) {
|
|
142
|
+
if (!this.notifier)
|
|
143
|
+
return;
|
|
144
|
+
await this.notifier.notify({
|
|
145
|
+
kind,
|
|
146
|
+
userId,
|
|
147
|
+
agentId: '',
|
|
148
|
+
ts: new Date().toISOString(),
|
|
149
|
+
source: actor.source,
|
|
150
|
+
summary,
|
|
151
|
+
...(this.canonicalClearUrl === undefined
|
|
152
|
+
? {}
|
|
153
|
+
: { canonicalClearUrl: this.canonicalClearUrl }),
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/** Reference in-memory state store. */
|
|
158
|
+
export class InMemoryEStopStateStore {
|
|
159
|
+
states = new Map();
|
|
160
|
+
async get(userId) {
|
|
161
|
+
return this.states.get(userId) ?? null;
|
|
162
|
+
}
|
|
163
|
+
async set(userId, state) {
|
|
164
|
+
this.states.set(userId, state);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=hub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub.js","sourceRoot":"","sources":["../../src/estop/hub.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { EStopLocal } from './local.js';
|
|
2
|
+
export type { EStopLocalOptions } from './local.js';
|
|
3
|
+
export { EStopHub, InMemoryEStopStateStore, } from './hub.js';
|
|
4
|
+
export type { EStopHubOptions, EStopStateStore, EStopBroadcastChannel, EStopActorContext, } from './hub.js';
|
|
5
|
+
export { createEStopMiddleware } from './middleware.js';
|
|
6
|
+
export type { EStopMiddlewareOptions, MiddlewareRequest, MiddlewareResponse, MiddlewareNext, } from './middleware.js';
|
|
7
|
+
export { createEStopPoller, EStopPoller } from './poller.js';
|
|
8
|
+
export type { EStopPollerOptions } from './poller.js';
|
|
9
|
+
export { HeartbeatMonitor } from './heartbeat.js';
|
|
10
|
+
export type { HeartbeatMonitorOptions } from './heartbeat.js';
|
|
11
|
+
export type { EStopState, EStopPressOptions, EStopClearOptions, EStopPressResult, EStopClearResult, } from './types.js';
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/estop/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EACL,QAAQ,EACR,uBAAuB,GACxB,MAAM,UAAU,CAAC;AAClB,YAAY,EACV,eAAe,EACf,eAAe,EACf,qBAAqB,EACrB,iBAAiB,GAClB,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AACxD,YAAY,EACV,sBAAsB,EACtB,iBAAiB,EACjB,kBAAkB,EAClB,cAAc,GACf,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC7D,YAAY,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,YAAY,EAAE,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAC9D,YAAY,EACV,UAAU,EACV,iBAAiB,EACjB,iBAAiB,EACjB,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,YAAY,CAAC"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { EStopLocal } from './local.js';
|
|
2
|
+
export { EStopHub, InMemoryEStopStateStore, } from './hub.js';
|
|
3
|
+
export { createEStopMiddleware } from './middleware.js';
|
|
4
|
+
export { createEStopPoller, EStopPoller } from './poller.js';
|
|
5
|
+
export { HeartbeatMonitor } from './heartbeat.js';
|
|
6
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/estop/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,OAAO,EACL,QAAQ,EACR,uBAAuB,GACxB,MAAM,UAAU,CAAC;AAOlB,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAOxD,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EStopLocal — in-process emergency-stop primitive. SPEC §5.3.
|
|
3
|
+
*
|
|
4
|
+
* Single-process deployment: halt flag is an AbortController; clear semantics
|
|
5
|
+
* are terminal (session must be reconstructed).
|
|
6
|
+
*/
|
|
7
|
+
import type { AuditLogWriter } from '../audit/writer.js';
|
|
8
|
+
import type { Notifier } from '../notify/types.js';
|
|
9
|
+
import type { EStopClearOptions, EStopClearResult, EStopPressOptions, EStopPressResult, EStopState } from './types.js';
|
|
10
|
+
export interface EStopLocalOptions {
|
|
11
|
+
audit?: AuditLogWriter;
|
|
12
|
+
notifier?: Notifier;
|
|
13
|
+
/** If true, construct already-pressed. Useful for tests; rare in production. */
|
|
14
|
+
initiallyPressed?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export declare class EStopLocal {
|
|
17
|
+
private state;
|
|
18
|
+
private controller;
|
|
19
|
+
private readonly audit;
|
|
20
|
+
private readonly notifier;
|
|
21
|
+
constructor(options?: EStopLocalOptions);
|
|
22
|
+
/** AbortSignal callers can listen on. Aborts when pressed. */
|
|
23
|
+
get abortSignal(): AbortSignal;
|
|
24
|
+
isPressed(): boolean;
|
|
25
|
+
getState(): EStopState;
|
|
26
|
+
press(options: EStopPressOptions): Promise<EStopPressResult>;
|
|
27
|
+
clear(options: EStopClearOptions): Promise<EStopClearResult>;
|
|
28
|
+
private recordEvent;
|
|
29
|
+
private fireNotification;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=local.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.d.ts","sourceRoot":"","sources":["../../src/estop/local.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EACV,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,gBAAgB,EAChB,UAAU,EACX,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,gFAAgF;IAChF,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,KAAK,CAAa;IAC1B,OAAO,CAAC,UAAU,CAAkB;IACpC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;IACnD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;gBAEpC,OAAO,GAAE,iBAAsB;IAgB3C,8DAA8D;IAC9D,IAAI,WAAW,IAAI,WAAW,CAE7B;IAED,SAAS,IAAI,OAAO;IAIpB,QAAQ,IAAI,UAAU;IAIhB,KAAK,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAqB5D,KAAK,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,gBAAgB,CAAC;YAoBpD,WAAW;YAmBX,gBAAgB;CAiB/B"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EStopLocal — in-process emergency-stop primitive. SPEC §5.3.
|
|
3
|
+
*
|
|
4
|
+
* Single-process deployment: halt flag is an AbortController; clear semantics
|
|
5
|
+
* are terminal (session must be reconstructed).
|
|
6
|
+
*/
|
|
7
|
+
export class EStopLocal {
|
|
8
|
+
state;
|
|
9
|
+
controller;
|
|
10
|
+
audit;
|
|
11
|
+
notifier;
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.audit = options.audit;
|
|
14
|
+
this.notifier = options.notifier;
|
|
15
|
+
this.controller = new AbortController();
|
|
16
|
+
this.state = { pressed: false };
|
|
17
|
+
if (options.initiallyPressed) {
|
|
18
|
+
this.state = {
|
|
19
|
+
pressed: true,
|
|
20
|
+
pressedAt: new Date().toISOString(),
|
|
21
|
+
pressedReason: 'initially_pressed',
|
|
22
|
+
};
|
|
23
|
+
this.controller.abort();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** AbortSignal callers can listen on. Aborts when pressed. */
|
|
27
|
+
get abortSignal() {
|
|
28
|
+
return this.controller.signal;
|
|
29
|
+
}
|
|
30
|
+
isPressed() {
|
|
31
|
+
return this.state.pressed;
|
|
32
|
+
}
|
|
33
|
+
getState() {
|
|
34
|
+
return { ...this.state };
|
|
35
|
+
}
|
|
36
|
+
async press(options) {
|
|
37
|
+
if (this.state.pressed) {
|
|
38
|
+
// Idempotent: re-pressing doesn't change state but does record an audit event.
|
|
39
|
+
await this.recordEvent('estop_press', options);
|
|
40
|
+
return { state: this.getState() };
|
|
41
|
+
}
|
|
42
|
+
this.state = {
|
|
43
|
+
pressed: true,
|
|
44
|
+
pressedAt: new Date().toISOString(),
|
|
45
|
+
pressedReason: options.reason,
|
|
46
|
+
...(options.operatorId === undefined ? {} : { pressedOperatorId: options.operatorId }),
|
|
47
|
+
};
|
|
48
|
+
this.controller.abort();
|
|
49
|
+
await this.recordEvent('estop_press', options);
|
|
50
|
+
await this.fireNotification('estop_press', options);
|
|
51
|
+
return { state: this.getState() };
|
|
52
|
+
}
|
|
53
|
+
async clear(options) {
|
|
54
|
+
if (!this.state.pressed) {
|
|
55
|
+
// No-op clear: don't audit, don't notify.
|
|
56
|
+
return { state: this.getState() };
|
|
57
|
+
}
|
|
58
|
+
this.state = {
|
|
59
|
+
pressed: false,
|
|
60
|
+
clearedAt: new Date().toISOString(),
|
|
61
|
+
};
|
|
62
|
+
// EStopLocal does NOT reset the AbortController: the existing signal stays
|
|
63
|
+
// aborted forever. Recovery requires a new EStopLocal instance.
|
|
64
|
+
await this.recordEvent('estop_clear', options);
|
|
65
|
+
await this.fireNotification('estop_clear', options);
|
|
66
|
+
return { state: this.getState() };
|
|
67
|
+
}
|
|
68
|
+
async recordEvent(kind, options) {
|
|
69
|
+
if (!this.audit)
|
|
70
|
+
return;
|
|
71
|
+
const detail = {
|
|
72
|
+
...(options.detail ?? {}),
|
|
73
|
+
};
|
|
74
|
+
if ('reason' in options)
|
|
75
|
+
detail.reason = options.reason;
|
|
76
|
+
if (options.operatorId !== undefined)
|
|
77
|
+
detail.operator_id = options.operatorId;
|
|
78
|
+
await this.audit.append({
|
|
79
|
+
kind,
|
|
80
|
+
status: kind === 'estop_press' ? 'halted' : 'approved',
|
|
81
|
+
initiator: options.initiator ?? 'operator',
|
|
82
|
+
detail,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
async fireNotification(kind, options) {
|
|
86
|
+
if (!this.notifier)
|
|
87
|
+
return;
|
|
88
|
+
await this.notifier.notify({
|
|
89
|
+
kind,
|
|
90
|
+
agentId: '',
|
|
91
|
+
ts: new Date().toISOString(),
|
|
92
|
+
source: 'local',
|
|
93
|
+
summary: {
|
|
94
|
+
...(options.detail ?? {}),
|
|
95
|
+
...('reason' in options ? { reason: options.reason } : {}),
|
|
96
|
+
...(options.operatorId !== undefined ? { operator_id: options.operatorId } : {}),
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local.js","sourceRoot":"","sources":["../../src/estop/local.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* createEStopMiddleware — Express/Connect/Fastify-compatible middleware that
|
|
3
|
+
* returns HTTP 423 Locked when the resolved user is currently pressed.
|
|
4
|
+
* SPEC §5.4.
|
|
5
|
+
*
|
|
6
|
+
* Framework-agnostic: the middleware signature is `(req, res, next)` which
|
|
7
|
+
* Express, Connect, and Fastify (with middleware mode) all accept.
|
|
8
|
+
*/
|
|
9
|
+
import type { EStopHub } from './hub.js';
|
|
10
|
+
/** Minimal subset of Node's IncomingMessage we read. */
|
|
11
|
+
export interface MiddlewareRequest {
|
|
12
|
+
headers: Record<string, string | string[] | undefined>;
|
|
13
|
+
url?: string;
|
|
14
|
+
method?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Minimal subset of Node's ServerResponse we write. */
|
|
17
|
+
export interface MiddlewareResponse {
|
|
18
|
+
statusCode: number;
|
|
19
|
+
setHeader(name: string, value: string): void;
|
|
20
|
+
end(body?: string): void;
|
|
21
|
+
}
|
|
22
|
+
export type MiddlewareNext = (err?: unknown) => void;
|
|
23
|
+
export interface EStopMiddlewareOptions {
|
|
24
|
+
/** Host extracts the user id from the request. Return null to skip the gate. */
|
|
25
|
+
resolveUserId: (req: MiddlewareRequest) => string | null;
|
|
26
|
+
/** Bypass predicate — return true to skip the gate (e.g., for /estop/clear). */
|
|
27
|
+
exclude?: (req: MiddlewareRequest) => boolean;
|
|
28
|
+
/** Override the JSON body returned on 423. */
|
|
29
|
+
lockedResponseBody?: (state: {
|
|
30
|
+
pressedAt?: string;
|
|
31
|
+
}, userId: string) => unknown;
|
|
32
|
+
/** Header name carrying the request originator (operator | agent | system). */
|
|
33
|
+
initiatorHeader?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function createEStopMiddleware(hub: EStopHub, options: EStopMiddlewareOptions): (req: MiddlewareRequest, res: MiddlewareResponse, next: MiddlewareNext) => Promise<void>;
|
|
36
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../src/estop/middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEzC,wDAAwD;AACxD,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wDAAwD;AACxD,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7C,GAAG,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;AAErD,MAAM,WAAW,sBAAsB;IACrC,gFAAgF;IAChF,aAAa,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,MAAM,GAAG,IAAI,CAAC;IACzD,gFAAgF;IAChF,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,OAAO,CAAC;IAC9C,8CAA8C;IAC9C,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,EAAE,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC;IAChF,+EAA+E;IAC/E,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,QAAQ,EACb,OAAO,EAAE,sBAAsB,GAC9B,CAAC,GAAG,EAAE,iBAAiB,EAAE,GAAG,EAAE,kBAAkB,EAAE,IAAI,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAqB1F"}
|