@tagma/sdk 0.4.14 → 0.4.15
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 +21 -21
- package/README.md +569 -569
- package/dist/dag.d.ts.map +1 -1
- package/dist/dag.js +22 -56
- package/dist/dag.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +63 -37
- package/dist/engine.js.map +1 -1
- package/dist/middlewares/static-context.d.ts.map +1 -1
- package/dist/middlewares/static-context.js +7 -3
- package/dist/middlewares/static-context.js.map +1 -1
- package/dist/prompt-doc.d.ts +36 -0
- package/dist/prompt-doc.d.ts.map +1 -0
- package/dist/prompt-doc.js +44 -0
- package/dist/prompt-doc.js.map +1 -0
- package/dist/sdk.d.ts +3 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +4 -0
- package/dist/sdk.js.map +1 -1
- package/dist/task-ref.d.ts +55 -0
- package/dist/task-ref.d.ts.map +1 -0
- package/dist/task-ref.js +101 -0
- package/dist/task-ref.js.map +1 -0
- package/dist/templates.d.ts +20 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +93 -0
- package/dist/templates.js.map +1 -0
- package/dist/validate-raw.d.ts.map +1 -1
- package/dist/validate-raw.js +27 -53
- package/dist/validate-raw.js.map +1 -1
- package/package.json +2 -2
- package/scripts/preinstall.js +31 -31
- package/src/adapters/stdin-approval.ts +106 -106
- package/src/adapters/websocket-approval.ts +224 -224
- package/src/approval.ts +131 -131
- package/src/bootstrap.ts +37 -37
- package/src/completions/exit-code.ts +34 -34
- package/src/completions/file-exists.ts +66 -66
- package/src/completions/output-check.ts +86 -86
- package/src/config-ops.ts +307 -307
- package/src/dag.ts +24 -54
- package/src/drivers/claude-code.ts +250 -250
- package/src/engine.ts +1137 -1098
- package/src/hooks.ts +187 -187
- package/src/logger.ts +182 -182
- package/src/middlewares/static-context.ts +49 -45
- package/src/pipeline-runner.ts +156 -156
- package/src/prompt-doc.ts +49 -0
- package/src/registry.ts +242 -242
- package/src/runner.ts +395 -395
- package/src/schema.test.ts +101 -101
- package/src/schema.ts +338 -338
- package/src/sdk.ts +111 -92
- package/src/task-ref.ts +120 -0
- package/src/triggers/file.ts +164 -164
- package/src/triggers/manual.ts +86 -86
- package/src/types.ts +18 -18
- package/src/utils.ts +203 -203
- package/src/validate-raw.ts +412 -442
|
@@ -1,224 +1,224 @@
|
|
|
1
|
-
import type { ApprovalGateway, ApprovalEvent } from '../approval';
|
|
2
|
-
|
|
3
|
-
// ═══ WebSocket Approval Adapter ═══
|
|
4
|
-
//
|
|
5
|
-
// Bridges the ApprovalGateway to WebSocket clients (e.g. a frontend UI).
|
|
6
|
-
// Mirrors the stdin-approval adapter pattern: subscribe to gateway events,
|
|
7
|
-
// forward them as JSON to all connected clients, and call gateway.resolve()
|
|
8
|
-
// when a client sends a resolution message.
|
|
9
|
-
//
|
|
10
|
-
// Protocol — server → client:
|
|
11
|
-
// { type: 'pending', requests: ApprovalRequest[] } ← sent on connect
|
|
12
|
-
// { type: 'approval_requested', request: ApprovalRequest }
|
|
13
|
-
// { type: 'approval_resolved', request: ApprovalRequest, decision: ApprovalDecision }
|
|
14
|
-
// { type: 'approval_expired', request: ApprovalRequest }
|
|
15
|
-
// { type: 'approval_aborted', request: ApprovalRequest, reason: string }
|
|
16
|
-
//
|
|
17
|
-
// Protocol — client → server:
|
|
18
|
-
// { type: 'resolve', approvalId: string, outcome: 'approved'|'rejected',
|
|
19
|
-
// actor?: string, reason?: string }
|
|
20
|
-
|
|
21
|
-
export interface WebSocketApprovalAdapterOptions {
|
|
22
|
-
port?: number; // default: 3000
|
|
23
|
-
hostname?: string; // default: 'localhost'
|
|
24
|
-
/**
|
|
25
|
-
* M11: shared secret required from the client during the WebSocket
|
|
26
|
-
* upgrade. The token can be supplied either as the `?token=` query
|
|
27
|
-
* parameter or in the `x-tagma-token` request header. When set, any
|
|
28
|
-
* upgrade request that fails the check is rejected with HTTP 401 and
|
|
29
|
-
* never reaches the WebSocket layer (so a misconfigured client cannot
|
|
30
|
-
* exhaust rate-limit slots either). Leave undefined for backward
|
|
31
|
-
* compatibility with localhost-only deployments.
|
|
32
|
-
*/
|
|
33
|
-
token?: string;
|
|
34
|
-
/**
|
|
35
|
-
* M11: opt-out of origin checking. Defaults to false, meaning Origin
|
|
36
|
-
* headers are restricted to loopback hosts (localhost / 127.0.0.1 / ::1).
|
|
37
|
-
* Requests without an Origin header are still allowed so non-browser local
|
|
38
|
-
* clients can connect. Set true only for trusted reverse-proxy setups.
|
|
39
|
-
*/
|
|
40
|
-
allowAnyOrigin?: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface WebSocketApprovalAdapter {
|
|
44
|
-
readonly port: number;
|
|
45
|
-
readonly detach: () => void;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Maximum allowed message payload (bytes) to prevent DoS via oversized messages.
|
|
49
|
-
const MAX_PAYLOAD_BYTES = 4_096;
|
|
50
|
-
// Per-client rate limit: at most this many messages per window.
|
|
51
|
-
const RATE_LIMIT_MAX = 10;
|
|
52
|
-
const RATE_LIMIT_WINDOW_MS = 1_000;
|
|
53
|
-
|
|
54
|
-
export function attachWebSocketApprovalAdapter(
|
|
55
|
-
gateway: ApprovalGateway,
|
|
56
|
-
options: WebSocketApprovalAdapterOptions = {},
|
|
57
|
-
): WebSocketApprovalAdapter {
|
|
58
|
-
const port = options.port ?? 3000;
|
|
59
|
-
const hostname = options.hostname ?? 'localhost';
|
|
60
|
-
const requiredToken = options.token ?? null;
|
|
61
|
-
const enforceOriginCheck = options.allowAnyOrigin !== true;
|
|
62
|
-
|
|
63
|
-
function isLoopbackOrigin(origin: string): boolean {
|
|
64
|
-
try {
|
|
65
|
-
const host = new URL(origin).hostname.toLowerCase();
|
|
66
|
-
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
|
|
67
|
-
} catch {
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
type WS = import('bun').ServerWebSocket<unknown>;
|
|
73
|
-
const clients = new Set<WS>();
|
|
74
|
-
const clientRates = new Map<WS, { count: number; resetAt: number }>();
|
|
75
|
-
|
|
76
|
-
function broadcast(msg: unknown): void {
|
|
77
|
-
const text = JSON.stringify(msg);
|
|
78
|
-
for (const ws of clients) {
|
|
79
|
-
ws.send(text);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const unsubscribe = gateway.subscribe((event: ApprovalEvent) => {
|
|
84
|
-
switch (event.type) {
|
|
85
|
-
case 'requested':
|
|
86
|
-
broadcast({ type: 'approval_requested', request: event.request });
|
|
87
|
-
break;
|
|
88
|
-
case 'resolved':
|
|
89
|
-
broadcast({ type: 'approval_resolved', request: event.request, decision: event.decision });
|
|
90
|
-
break;
|
|
91
|
-
case 'expired':
|
|
92
|
-
broadcast({ type: 'approval_expired', request: event.request });
|
|
93
|
-
break;
|
|
94
|
-
case 'aborted':
|
|
95
|
-
broadcast({ type: 'approval_aborted', request: event.request, reason: event.reason });
|
|
96
|
-
break;
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const server = Bun.serve({
|
|
101
|
-
port,
|
|
102
|
-
hostname,
|
|
103
|
-
|
|
104
|
-
fetch(req, server) {
|
|
105
|
-
if (enforceOriginCheck) {
|
|
106
|
-
const origin = req.headers.get('origin');
|
|
107
|
-
if (origin && !isLoopbackOrigin(origin)) {
|
|
108
|
-
return new Response('forbidden origin', { status: 403 });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
// M11: enforce token before any upgrade so an unauthenticated client
|
|
112
|
-
// can't even open a socket. Tokens may arrive via header or query.
|
|
113
|
-
if (requiredToken !== null) {
|
|
114
|
-
const headerToken = req.headers.get('x-tagma-token') ?? '';
|
|
115
|
-
let queryToken = '';
|
|
116
|
-
try {
|
|
117
|
-
queryToken = new URL(req.url).searchParams.get('token') ?? '';
|
|
118
|
-
} catch {
|
|
119
|
-
/* malformed URL — leave queryToken empty */
|
|
120
|
-
}
|
|
121
|
-
const presented = headerToken || queryToken;
|
|
122
|
-
if (presented !== requiredToken) {
|
|
123
|
-
return new Response('unauthorized', { status: 401 });
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
if (server.upgrade(req)) return undefined;
|
|
127
|
-
return new Response('tagma-sdk WebSocket approval endpoint', { status: 426 });
|
|
128
|
-
},
|
|
129
|
-
|
|
130
|
-
websocket: {
|
|
131
|
-
open(ws) {
|
|
132
|
-
clients.add(ws);
|
|
133
|
-
// Sync current pending approvals to newly connected client.
|
|
134
|
-
ws.send(JSON.stringify({ type: 'pending', requests: gateway.pending() }));
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
message(ws, raw) {
|
|
138
|
-
const rawStr = typeof raw === 'string' ? raw : raw.toString();
|
|
139
|
-
|
|
140
|
-
// Payload size guard — reject oversized messages before parsing.
|
|
141
|
-
if (rawStr.length > MAX_PAYLOAD_BYTES) {
|
|
142
|
-
ws.send(JSON.stringify({ type: 'error', message: 'message too large' }));
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Per-client rate limit.
|
|
147
|
-
const now = Date.now();
|
|
148
|
-
const rate = clientRates.get(ws) ?? { count: 0, resetAt: now + RATE_LIMIT_WINDOW_MS };
|
|
149
|
-
if (now >= rate.resetAt) {
|
|
150
|
-
rate.count = 0;
|
|
151
|
-
rate.resetAt = now + RATE_LIMIT_WINDOW_MS;
|
|
152
|
-
}
|
|
153
|
-
rate.count++;
|
|
154
|
-
clientRates.set(ws, rate);
|
|
155
|
-
if (rate.count > RATE_LIMIT_MAX) {
|
|
156
|
-
ws.send(JSON.stringify({ type: 'error', message: 'rate limit exceeded' }));
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
let msg: unknown;
|
|
161
|
-
try {
|
|
162
|
-
msg = JSON.parse(rawStr);
|
|
163
|
-
} catch {
|
|
164
|
-
ws.send(JSON.stringify({ type: 'error', message: 'invalid JSON' }));
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (!isResolveMessage(msg)) {
|
|
169
|
-
ws.send(JSON.stringify({ type: 'error', message: 'unknown message type' }));
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const ok = gateway.resolve(msg.approvalId, {
|
|
174
|
-
outcome: msg.outcome,
|
|
175
|
-
actor: msg.actor ?? 'websocket',
|
|
176
|
-
reason: msg.reason,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
if (!ok) {
|
|
180
|
-
ws.send(
|
|
181
|
-
JSON.stringify({
|
|
182
|
-
type: 'error',
|
|
183
|
-
message: `approval ${msg.approvalId} not found or already resolved`,
|
|
184
|
-
}),
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
},
|
|
188
|
-
|
|
189
|
-
close(ws) {
|
|
190
|
-
clients.delete(ws);
|
|
191
|
-
clientRates.delete(ws);
|
|
192
|
-
},
|
|
193
|
-
},
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
return {
|
|
197
|
-
port: server.port!,
|
|
198
|
-
detach() {
|
|
199
|
-
unsubscribe();
|
|
200
|
-
clients.clear();
|
|
201
|
-
server.stop(true);
|
|
202
|
-
},
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// ── Type guard ──
|
|
207
|
-
|
|
208
|
-
interface ResolveMessage {
|
|
209
|
-
type: 'resolve';
|
|
210
|
-
approvalId: string;
|
|
211
|
-
outcome: 'approved' | 'rejected';
|
|
212
|
-
actor?: string;
|
|
213
|
-
reason?: string;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function isResolveMessage(v: unknown): v is ResolveMessage {
|
|
217
|
-
if (typeof v !== 'object' || v === null) return false;
|
|
218
|
-
const m = v as Record<string, unknown>;
|
|
219
|
-
return (
|
|
220
|
-
m['type'] === 'resolve' &&
|
|
221
|
-
typeof m['approvalId'] === 'string' &&
|
|
222
|
-
(m['outcome'] === 'approved' || m['outcome'] === 'rejected')
|
|
223
|
-
);
|
|
224
|
-
}
|
|
1
|
+
import type { ApprovalGateway, ApprovalEvent } from '../approval';
|
|
2
|
+
|
|
3
|
+
// ═══ WebSocket Approval Adapter ═══
|
|
4
|
+
//
|
|
5
|
+
// Bridges the ApprovalGateway to WebSocket clients (e.g. a frontend UI).
|
|
6
|
+
// Mirrors the stdin-approval adapter pattern: subscribe to gateway events,
|
|
7
|
+
// forward them as JSON to all connected clients, and call gateway.resolve()
|
|
8
|
+
// when a client sends a resolution message.
|
|
9
|
+
//
|
|
10
|
+
// Protocol — server → client:
|
|
11
|
+
// { type: 'pending', requests: ApprovalRequest[] } ← sent on connect
|
|
12
|
+
// { type: 'approval_requested', request: ApprovalRequest }
|
|
13
|
+
// { type: 'approval_resolved', request: ApprovalRequest, decision: ApprovalDecision }
|
|
14
|
+
// { type: 'approval_expired', request: ApprovalRequest }
|
|
15
|
+
// { type: 'approval_aborted', request: ApprovalRequest, reason: string }
|
|
16
|
+
//
|
|
17
|
+
// Protocol — client → server:
|
|
18
|
+
// { type: 'resolve', approvalId: string, outcome: 'approved'|'rejected',
|
|
19
|
+
// actor?: string, reason?: string }
|
|
20
|
+
|
|
21
|
+
export interface WebSocketApprovalAdapterOptions {
|
|
22
|
+
port?: number; // default: 3000
|
|
23
|
+
hostname?: string; // default: 'localhost'
|
|
24
|
+
/**
|
|
25
|
+
* M11: shared secret required from the client during the WebSocket
|
|
26
|
+
* upgrade. The token can be supplied either as the `?token=` query
|
|
27
|
+
* parameter or in the `x-tagma-token` request header. When set, any
|
|
28
|
+
* upgrade request that fails the check is rejected with HTTP 401 and
|
|
29
|
+
* never reaches the WebSocket layer (so a misconfigured client cannot
|
|
30
|
+
* exhaust rate-limit slots either). Leave undefined for backward
|
|
31
|
+
* compatibility with localhost-only deployments.
|
|
32
|
+
*/
|
|
33
|
+
token?: string;
|
|
34
|
+
/**
|
|
35
|
+
* M11: opt-out of origin checking. Defaults to false, meaning Origin
|
|
36
|
+
* headers are restricted to loopback hosts (localhost / 127.0.0.1 / ::1).
|
|
37
|
+
* Requests without an Origin header are still allowed so non-browser local
|
|
38
|
+
* clients can connect. Set true only for trusted reverse-proxy setups.
|
|
39
|
+
*/
|
|
40
|
+
allowAnyOrigin?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface WebSocketApprovalAdapter {
|
|
44
|
+
readonly port: number;
|
|
45
|
+
readonly detach: () => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Maximum allowed message payload (bytes) to prevent DoS via oversized messages.
|
|
49
|
+
const MAX_PAYLOAD_BYTES = 4_096;
|
|
50
|
+
// Per-client rate limit: at most this many messages per window.
|
|
51
|
+
const RATE_LIMIT_MAX = 10;
|
|
52
|
+
const RATE_LIMIT_WINDOW_MS = 1_000;
|
|
53
|
+
|
|
54
|
+
export function attachWebSocketApprovalAdapter(
|
|
55
|
+
gateway: ApprovalGateway,
|
|
56
|
+
options: WebSocketApprovalAdapterOptions = {},
|
|
57
|
+
): WebSocketApprovalAdapter {
|
|
58
|
+
const port = options.port ?? 3000;
|
|
59
|
+
const hostname = options.hostname ?? 'localhost';
|
|
60
|
+
const requiredToken = options.token ?? null;
|
|
61
|
+
const enforceOriginCheck = options.allowAnyOrigin !== true;
|
|
62
|
+
|
|
63
|
+
function isLoopbackOrigin(origin: string): boolean {
|
|
64
|
+
try {
|
|
65
|
+
const host = new URL(origin).hostname.toLowerCase();
|
|
66
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
|
|
67
|
+
} catch {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
type WS = import('bun').ServerWebSocket<unknown>;
|
|
73
|
+
const clients = new Set<WS>();
|
|
74
|
+
const clientRates = new Map<WS, { count: number; resetAt: number }>();
|
|
75
|
+
|
|
76
|
+
function broadcast(msg: unknown): void {
|
|
77
|
+
const text = JSON.stringify(msg);
|
|
78
|
+
for (const ws of clients) {
|
|
79
|
+
ws.send(text);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const unsubscribe = gateway.subscribe((event: ApprovalEvent) => {
|
|
84
|
+
switch (event.type) {
|
|
85
|
+
case 'requested':
|
|
86
|
+
broadcast({ type: 'approval_requested', request: event.request });
|
|
87
|
+
break;
|
|
88
|
+
case 'resolved':
|
|
89
|
+
broadcast({ type: 'approval_resolved', request: event.request, decision: event.decision });
|
|
90
|
+
break;
|
|
91
|
+
case 'expired':
|
|
92
|
+
broadcast({ type: 'approval_expired', request: event.request });
|
|
93
|
+
break;
|
|
94
|
+
case 'aborted':
|
|
95
|
+
broadcast({ type: 'approval_aborted', request: event.request, reason: event.reason });
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const server = Bun.serve({
|
|
101
|
+
port,
|
|
102
|
+
hostname,
|
|
103
|
+
|
|
104
|
+
fetch(req, server) {
|
|
105
|
+
if (enforceOriginCheck) {
|
|
106
|
+
const origin = req.headers.get('origin');
|
|
107
|
+
if (origin && !isLoopbackOrigin(origin)) {
|
|
108
|
+
return new Response('forbidden origin', { status: 403 });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// M11: enforce token before any upgrade so an unauthenticated client
|
|
112
|
+
// can't even open a socket. Tokens may arrive via header or query.
|
|
113
|
+
if (requiredToken !== null) {
|
|
114
|
+
const headerToken = req.headers.get('x-tagma-token') ?? '';
|
|
115
|
+
let queryToken = '';
|
|
116
|
+
try {
|
|
117
|
+
queryToken = new URL(req.url).searchParams.get('token') ?? '';
|
|
118
|
+
} catch {
|
|
119
|
+
/* malformed URL — leave queryToken empty */
|
|
120
|
+
}
|
|
121
|
+
const presented = headerToken || queryToken;
|
|
122
|
+
if (presented !== requiredToken) {
|
|
123
|
+
return new Response('unauthorized', { status: 401 });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (server.upgrade(req)) return undefined;
|
|
127
|
+
return new Response('tagma-sdk WebSocket approval endpoint', { status: 426 });
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
websocket: {
|
|
131
|
+
open(ws) {
|
|
132
|
+
clients.add(ws);
|
|
133
|
+
// Sync current pending approvals to newly connected client.
|
|
134
|
+
ws.send(JSON.stringify({ type: 'pending', requests: gateway.pending() }));
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
message(ws, raw) {
|
|
138
|
+
const rawStr = typeof raw === 'string' ? raw : raw.toString();
|
|
139
|
+
|
|
140
|
+
// Payload size guard — reject oversized messages before parsing.
|
|
141
|
+
if (rawStr.length > MAX_PAYLOAD_BYTES) {
|
|
142
|
+
ws.send(JSON.stringify({ type: 'error', message: 'message too large' }));
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Per-client rate limit.
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
const rate = clientRates.get(ws) ?? { count: 0, resetAt: now + RATE_LIMIT_WINDOW_MS };
|
|
149
|
+
if (now >= rate.resetAt) {
|
|
150
|
+
rate.count = 0;
|
|
151
|
+
rate.resetAt = now + RATE_LIMIT_WINDOW_MS;
|
|
152
|
+
}
|
|
153
|
+
rate.count++;
|
|
154
|
+
clientRates.set(ws, rate);
|
|
155
|
+
if (rate.count > RATE_LIMIT_MAX) {
|
|
156
|
+
ws.send(JSON.stringify({ type: 'error', message: 'rate limit exceeded' }));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let msg: unknown;
|
|
161
|
+
try {
|
|
162
|
+
msg = JSON.parse(rawStr);
|
|
163
|
+
} catch {
|
|
164
|
+
ws.send(JSON.stringify({ type: 'error', message: 'invalid JSON' }));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!isResolveMessage(msg)) {
|
|
169
|
+
ws.send(JSON.stringify({ type: 'error', message: 'unknown message type' }));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const ok = gateway.resolve(msg.approvalId, {
|
|
174
|
+
outcome: msg.outcome,
|
|
175
|
+
actor: msg.actor ?? 'websocket',
|
|
176
|
+
reason: msg.reason,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
if (!ok) {
|
|
180
|
+
ws.send(
|
|
181
|
+
JSON.stringify({
|
|
182
|
+
type: 'error',
|
|
183
|
+
message: `approval ${msg.approvalId} not found or already resolved`,
|
|
184
|
+
}),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
close(ws) {
|
|
190
|
+
clients.delete(ws);
|
|
191
|
+
clientRates.delete(ws);
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
port: server.port!,
|
|
198
|
+
detach() {
|
|
199
|
+
unsubscribe();
|
|
200
|
+
clients.clear();
|
|
201
|
+
server.stop(true);
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ── Type guard ──
|
|
207
|
+
|
|
208
|
+
interface ResolveMessage {
|
|
209
|
+
type: 'resolve';
|
|
210
|
+
approvalId: string;
|
|
211
|
+
outcome: 'approved' | 'rejected';
|
|
212
|
+
actor?: string;
|
|
213
|
+
reason?: string;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isResolveMessage(v: unknown): v is ResolveMessage {
|
|
217
|
+
if (typeof v !== 'object' || v === null) return false;
|
|
218
|
+
const m = v as Record<string, unknown>;
|
|
219
|
+
return (
|
|
220
|
+
m['type'] === 'resolve' &&
|
|
221
|
+
typeof m['approvalId'] === 'string' &&
|
|
222
|
+
(m['outcome'] === 'approved' || m['outcome'] === 'rejected')
|
|
223
|
+
);
|
|
224
|
+
}
|