wicked-crew 0.5.0 → 0.6.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/dist/api/audit.d.ts +57 -0
- package/dist/api/audit.d.ts.map +1 -0
- package/dist/api/audit.js +115 -0
- package/dist/api/audit.js.map +1 -0
- package/dist/api/auth.d.ts +192 -0
- package/dist/api/auth.d.ts.map +1 -0
- package/dist/api/auth.js +515 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/gate-cache.d.ts +8 -0
- package/dist/api/gate-cache.d.ts.map +1 -1
- package/dist/api/gate-cache.js +10 -0
- package/dist/api/gate-cache.js.map +1 -1
- package/dist/api/open-path.d.ts +18 -0
- package/dist/api/open-path.d.ts.map +1 -0
- package/dist/api/open-path.js +79 -0
- package/dist/api/open-path.js.map +1 -0
- package/dist/api/routes.d.ts +112 -1
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +435 -16
- package/dist/api/routes.js.map +1 -1
- package/dist/api/seat-health.d.ts +55 -0
- package/dist/api/seat-health.d.ts.map +1 -0
- package/dist/api/seat-health.js +273 -0
- package/dist/api/seat-health.js.map +1 -0
- package/dist/api/seat-signin.d.ts +27 -0
- package/dist/api/seat-signin.d.ts.map +1 -0
- package/dist/api/seat-signin.js +143 -0
- package/dist/api/seat-signin.js.map +1 -0
- package/dist/api/server.d.ts +111 -3
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +236 -11
- package/dist/api/server.js.map +1 -1
- package/dist/cli/index.js +97 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp.d.ts +14 -0
- package/dist/cli/mcp.d.ts.map +1 -0
- package/dist/cli/mcp.js +119 -0
- package/dist/cli/mcp.js.map +1 -0
- package/dist/core/adapter.d.ts +64 -1
- package/dist/core/adapter.d.ts.map +1 -1
- package/dist/core/adapter.js +162 -4
- package/dist/core/adapter.js.map +1 -1
- package/dist/core/types.d.ts +23 -361
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +11 -4
- package/dist/core/types.js.map +1 -1
- package/dist/interactive/bridge-pool.d.ts +71 -0
- package/dist/interactive/bridge-pool.d.ts.map +1 -0
- package/dist/interactive/bridge-pool.js +187 -0
- package/dist/interactive/bridge-pool.js.map +1 -0
- package/dist/interactive/bridge-root.d.ts +36 -0
- package/dist/interactive/bridge-root.d.ts.map +1 -0
- package/dist/interactive/bridge-root.js +48 -0
- package/dist/interactive/bridge-root.js.map +1 -0
- package/dist/interactive/draft-events.d.ts +144 -0
- package/dist/interactive/draft-events.d.ts.map +1 -0
- package/dist/interactive/draft-events.js +490 -0
- package/dist/interactive/draft-events.js.map +1 -0
- package/dist/interactive/edit-events.d.ts +172 -0
- package/dist/interactive/edit-events.d.ts.map +1 -0
- package/dist/interactive/edit-events.js +530 -0
- package/dist/interactive/edit-events.js.map +1 -0
- package/dist/interactive/ledger.d.ts +39 -0
- package/dist/interactive/ledger.d.ts.map +1 -0
- package/dist/interactive/ledger.js +93 -0
- package/dist/interactive/ledger.js.map +1 -0
- package/dist/interactive/proxy-routes.d.ts +39 -0
- package/dist/interactive/proxy-routes.d.ts.map +1 -0
- package/dist/interactive/proxy-routes.js +189 -0
- package/dist/interactive/proxy-routes.js.map +1 -0
- package/dist/interactive/ws-relay.d.ts +85 -0
- package/dist/interactive/ws-relay.d.ts.map +1 -0
- package/dist/interactive/ws-relay.js +191 -0
- package/dist/interactive/ws-relay.js.map +1 -0
- package/dist/projects/activity.d.ts +29 -0
- package/dist/projects/activity.d.ts.map +1 -0
- package/dist/projects/activity.js +172 -0
- package/dist/projects/activity.js.map +1 -0
- package/dist/projects/charter.d.ts +28 -0
- package/dist/projects/charter.d.ts.map +1 -0
- package/dist/projects/charter.js +53 -0
- package/dist/projects/charter.js.map +1 -0
- package/dist/projects/events.d.ts +55 -0
- package/dist/projects/events.d.ts.map +1 -0
- package/dist/projects/events.js +141 -0
- package/dist/projects/events.js.map +1 -0
- package/dist/projects/membership-index.d.ts +25 -0
- package/dist/projects/membership-index.d.ts.map +1 -0
- package/dist/projects/membership-index.js +46 -0
- package/dist/projects/membership-index.js.map +1 -0
- package/dist/projects/routes.d.ts +90 -0
- package/dist/projects/routes.d.ts.map +1 -0
- package/dist/projects/routes.js +388 -0
- package/dist/projects/routes.js.map +1 -0
- package/dist/projects/settings.d.ts +32 -0
- package/dist/projects/settings.d.ts.map +1 -0
- package/dist/projects/settings.js +64 -0
- package/dist/projects/settings.js.map +1 -0
- package/dist/qe/acceptance.d.ts +137 -0
- package/dist/qe/acceptance.d.ts.map +1 -0
- package/dist/qe/acceptance.js +249 -0
- package/dist/qe/acceptance.js.map +1 -0
- package/dist/qe/gate-events.d.ts +111 -0
- package/dist/qe/gate-events.d.ts.map +1 -0
- package/dist/qe/gate-events.js +168 -0
- package/dist/qe/gate-events.js.map +1 -0
- package/dist/qe/ledger.d.ts +100 -0
- package/dist/qe/ledger.d.ts.map +1 -0
- package/dist/qe/ledger.js +154 -0
- package/dist/qe/ledger.js.map +1 -0
- package/dist/studio/assets/index-CCwXa1cn.js +428 -0
- package/dist/studio/assets/index-HWxo0h41.css +32 -0
- package/dist/studio/index.html +2 -2
- package/package.json +10 -4
- package/dist/studio/assets/index-DaaUU8Ep.css +0 -32
- package/dist/studio/assets/index-Fu5DRC00.js +0 -423
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable handoff ledger shared by crew's wicked-interactive seams (task #86, Phase 7c).
|
|
3
|
+
*
|
|
4
|
+
* Extracted from the draft leg (draft-events.ts, PR #241) when the structural-edit leg arrived,
|
|
5
|
+
* because both seams need the same replay-dedup discipline with DIFFERENT key grammars:
|
|
6
|
+
*
|
|
7
|
+
* - the DRAFT leg keys by document id — one first draft per document lifetime;
|
|
8
|
+
* - the EDIT leg keys by `<doc>:v<version>` — one governed edit per HANDOFF, because the same
|
|
9
|
+
* document legitimately produces many structural handoffs over its life (versioned targeting).
|
|
10
|
+
*
|
|
11
|
+
* The ledger itself is key-agnostic: a durable map `key → HandoffLedgerEntry`, JSON on disk,
|
|
12
|
+
* written atomically (tmp + rename). The bus cursor alone cannot carry this: at-least-once
|
|
13
|
+
* delivery means the same trigger can arrive again after a crash between handling and cursor
|
|
14
|
+
* advance, and in-memory state dies with the process. The ledger is the system of record for
|
|
15
|
+
* "this handoff was answered"; each seam's in-flight map is just the live half.
|
|
16
|
+
*/
|
|
17
|
+
import { mkdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
|
|
18
|
+
import { dirname } from 'node:path';
|
|
19
|
+
export class InteractiveHandoffLedger {
|
|
20
|
+
path;
|
|
21
|
+
entries;
|
|
22
|
+
constructor(path) {
|
|
23
|
+
this.path = path;
|
|
24
|
+
this.entries = {};
|
|
25
|
+
try {
|
|
26
|
+
// The persisted top-level field stays `docs` for wire-compat with ledgers the draft leg
|
|
27
|
+
// already wrote to operator machines (~/.wicked-crew/interactive-draft-ledger.json).
|
|
28
|
+
const parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
29
|
+
if (parsed && typeof parsed === 'object' && parsed.docs && typeof parsed.docs === 'object') {
|
|
30
|
+
for (const [key, entry] of Object.entries(parsed.docs)) {
|
|
31
|
+
// Per-row validation: one malformed row (null, primitive, missing/empty fields) costs
|
|
32
|
+
// that row alone — letting it throw into the outer catch would blank the WHOLE ledger
|
|
33
|
+
// and defeat replay-dedup for every handoff already answered. A kept row must satisfy
|
|
34
|
+
// the full HandoffLedgerEntry contract (non-empty runId + launchedAt strings).
|
|
35
|
+
if (typeof entry !== 'object' ||
|
|
36
|
+
entry === null ||
|
|
37
|
+
typeof entry.runId !== 'string' ||
|
|
38
|
+
entry.runId.length === 0 ||
|
|
39
|
+
typeof entry.launchedAt !== 'string') {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const { draftEmittedAt, ...rest } = entry;
|
|
43
|
+
this.entries[key] = {
|
|
44
|
+
...rest,
|
|
45
|
+
// Migrate the draft leg's original field name in place.
|
|
46
|
+
...(rest.emittedAt === undefined && draftEmittedAt !== undefined
|
|
47
|
+
? { emittedAt: draftEmittedAt }
|
|
48
|
+
: {}),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Missing or malformed ledger — start empty. Malformed is deliberately NOT fatal: a
|
|
55
|
+
// corrupt ledger must not stop the daemon, and the worst case is one duplicate closing
|
|
56
|
+
// emit, which the deterministic bus idempotency key still dedupes.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
has(key) {
|
|
60
|
+
return Object.prototype.hasOwnProperty.call(this.entries, key);
|
|
61
|
+
}
|
|
62
|
+
get(key) {
|
|
63
|
+
return this.entries[key];
|
|
64
|
+
}
|
|
65
|
+
recordLaunch(key, runId) {
|
|
66
|
+
this.entries[key] = { runId, launchedAt: new Date().toISOString() };
|
|
67
|
+
this.persist();
|
|
68
|
+
}
|
|
69
|
+
recordEmitted(key) {
|
|
70
|
+
const entry = this.entries[key];
|
|
71
|
+
if (entry) {
|
|
72
|
+
entry.emittedAt = new Date().toISOString();
|
|
73
|
+
this.persist();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
recordFailure(key) {
|
|
77
|
+
const entry = this.entries[key];
|
|
78
|
+
if (entry) {
|
|
79
|
+
entry.failedAt = new Date().toISOString();
|
|
80
|
+
this.persist();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
size() {
|
|
84
|
+
return Object.keys(this.entries).length;
|
|
85
|
+
}
|
|
86
|
+
persist() {
|
|
87
|
+
mkdirSync(dirname(this.path), { recursive: true });
|
|
88
|
+
const tmp = `${this.path}.tmp-${process.pid}`;
|
|
89
|
+
writeFileSync(tmp, JSON.stringify({ docs: this.entries }, null, 2), 'utf8');
|
|
90
|
+
renameSync(tmp, this.path);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=ledger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ledger.js","sourceRoot":"","sources":["../../src/interactive/ledger.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/api/v1/projects/:projectId/interactive/*` — crew reverse-proxies the wicked-interactive
|
|
3
|
+
* bridge (DES-MERGE-001 §5.3, §7.2; slice 1).
|
|
4
|
+
*
|
|
5
|
+
* PURE TRANSPORT. Nothing user-visible moves in this slice: what changes is that interactive's
|
|
6
|
+
* HTTP surface becomes reachable through crew's OWN origin, so the studio client never learns a
|
|
7
|
+
* second origin, never needs CORS, and never carries a bridge port literal — ADR-0022's dynamic
|
|
8
|
+
* port is honored rather than worked around, and the bridge stops being browser-reachable at all.
|
|
9
|
+
*
|
|
10
|
+
* The path encodes the project (§7.2) because the ROOT is a per-project setting: the same proxy
|
|
11
|
+
* mount serves N interactive instances, one bridge per resolved root.
|
|
12
|
+
*
|
|
13
|
+
* Two things are deliberately hand-rolled over `node:http` rather than delegated to `fetch`:
|
|
14
|
+
*
|
|
15
|
+
* 1. UNBUFFERED STREAMING BOTH WAYS. Interactive's generation surface is SSE; a proxy that
|
|
16
|
+
* buffers turns a live narration into a single blob delivered at stream close. Piping raw
|
|
17
|
+
* sockets keeps every chunk flowing as it arrives, in both directions.
|
|
18
|
+
* 2. VERBATIM FORWARDING. The path remainder and query string are taken from `req.raw.url`,
|
|
19
|
+
* not from Fastify's decoded wildcard param, so percent-encoding survives the hop intact.
|
|
20
|
+
*/
|
|
21
|
+
import type { FastifyInstance } from 'fastify';
|
|
22
|
+
import type { CoreAdapter } from '../core/adapter.js';
|
|
23
|
+
import type { ProjectSettingsStore } from '../projects/settings.js';
|
|
24
|
+
import { InteractiveBridgePool, type LiveBridge } from './bridge-pool.js';
|
|
25
|
+
/**
|
|
26
|
+
* Point a `Location` back at the proxy. The bridge answers on its own origin and knows nothing
|
|
27
|
+
* about the mount prefix, so both spellings it can emit have to be re-anchored: an absolute URL
|
|
28
|
+
* on the bridge origin, and a root-relative path. Anything else (a foreign origin, a relative
|
|
29
|
+
* path) is left exactly as-is — rewriting those would be inventing a redirect target.
|
|
30
|
+
*/
|
|
31
|
+
export declare function rewriteLocation(location: string, bridge: LiveBridge, prefix: string): string;
|
|
32
|
+
export interface InteractiveProxyDeps {
|
|
33
|
+
settings: ProjectSettingsStore;
|
|
34
|
+
pool: InteractiveBridgePool;
|
|
35
|
+
env?: NodeJS.ProcessEnv;
|
|
36
|
+
log?: (msg: string) => void;
|
|
37
|
+
}
|
|
38
|
+
export declare function registerInteractiveProxy(app: FastifyInstance, adapter: CoreAdapter, deps: InteractiveProxyDeps): void;
|
|
39
|
+
//# sourceMappingURL=proxy-routes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-routes.d.ts","sourceRoot":"","sources":["../../src/interactive/proxy-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAgC,MAAM,SAAS,CAAC;AAG7E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAGtD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAEpE,OAAO,EAA0B,qBAAqB,EAAE,KAAK,UAAU,EAAE,MAAM,kBAAkB,CAAC;AA4BlG;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAO5F;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,IAAI,EAAE,qBAAqB,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,oBAAoB,GAAG,IAAI,CAiErH"}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/api/v1/projects/:projectId/interactive/*` — crew reverse-proxies the wicked-interactive
|
|
3
|
+
* bridge (DES-MERGE-001 §5.3, §7.2; slice 1).
|
|
4
|
+
*
|
|
5
|
+
* PURE TRANSPORT. Nothing user-visible moves in this slice: what changes is that interactive's
|
|
6
|
+
* HTTP surface becomes reachable through crew's OWN origin, so the studio client never learns a
|
|
7
|
+
* second origin, never needs CORS, and never carries a bridge port literal — ADR-0022's dynamic
|
|
8
|
+
* port is honored rather than worked around, and the bridge stops being browser-reachable at all.
|
|
9
|
+
*
|
|
10
|
+
* The path encodes the project (§7.2) because the ROOT is a per-project setting: the same proxy
|
|
11
|
+
* mount serves N interactive instances, one bridge per resolved root.
|
|
12
|
+
*
|
|
13
|
+
* Two things are deliberately hand-rolled over `node:http` rather than delegated to `fetch`:
|
|
14
|
+
*
|
|
15
|
+
* 1. UNBUFFERED STREAMING BOTH WAYS. Interactive's generation surface is SSE; a proxy that
|
|
16
|
+
* buffers turns a live narration into a single blob delivered at stream close. Piping raw
|
|
17
|
+
* sockets keeps every chunk flowing as it arrives, in both directions.
|
|
18
|
+
* 2. VERBATIM FORWARDING. The path remainder and query string are taken from `req.raw.url`,
|
|
19
|
+
* not from Fastify's decoded wildcard param, so percent-encoding survives the hop intact.
|
|
20
|
+
*/
|
|
21
|
+
import { request as httpRequest } from 'node:http';
|
|
22
|
+
import { API_PREFIX } from '../api/api-prefix.js';
|
|
23
|
+
import { ProjectsUnsupportedError } from '../core/adapter.js';
|
|
24
|
+
import { DEFAULT_PROJECT_ID } from '../projects/routes.js';
|
|
25
|
+
import { resolveInteractiveRoot } from './bridge-root.js';
|
|
26
|
+
import { BridgeUnavailableError } from './bridge-pool.js';
|
|
27
|
+
/** Per-hop headers that must never be forwarded across a proxy (RFC 9110 §7.6.1). */
|
|
28
|
+
const HOP_BY_HOP = new Set([
|
|
29
|
+
'connection',
|
|
30
|
+
'keep-alive',
|
|
31
|
+
'proxy-authenticate',
|
|
32
|
+
'proxy-authorization',
|
|
33
|
+
'te',
|
|
34
|
+
'trailer',
|
|
35
|
+
'transfer-encoding',
|
|
36
|
+
'upgrade',
|
|
37
|
+
]);
|
|
38
|
+
function forwardableRequestHeaders(headers, bridge) {
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
41
|
+
if (HOP_BY_HOP.has(name) || value === undefined)
|
|
42
|
+
continue;
|
|
43
|
+
// Crew's bearer token is CREW's credential. The bridge is a local process with no auth of
|
|
44
|
+
// its own and no use for it; forwarding it would hand a third-party child crew's ambient
|
|
45
|
+
// authority for free. One auth path (§5.3) means crew terminates auth, it does not relay it.
|
|
46
|
+
if (name === 'authorization')
|
|
47
|
+
continue;
|
|
48
|
+
out[name] = value;
|
|
49
|
+
}
|
|
50
|
+
out['host'] = `${bridge.host}:${bridge.port}`;
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Point a `Location` back at the proxy. The bridge answers on its own origin and knows nothing
|
|
55
|
+
* about the mount prefix, so both spellings it can emit have to be re-anchored: an absolute URL
|
|
56
|
+
* on the bridge origin, and a root-relative path. Anything else (a foreign origin, a relative
|
|
57
|
+
* path) is left exactly as-is — rewriting those would be inventing a redirect target.
|
|
58
|
+
*/
|
|
59
|
+
export function rewriteLocation(location, bridge, prefix) {
|
|
60
|
+
for (const host of new Set([bridge.host, 'localhost', '127.0.0.1'])) {
|
|
61
|
+
const origin = `http://${host}:${bridge.port}`;
|
|
62
|
+
if (location.startsWith(origin))
|
|
63
|
+
return `${prefix}${location.slice(origin.length)}`;
|
|
64
|
+
}
|
|
65
|
+
if (location.startsWith('/'))
|
|
66
|
+
return `${prefix}${location}`;
|
|
67
|
+
return location;
|
|
68
|
+
}
|
|
69
|
+
export function registerInteractiveProxy(app, adapter, deps) {
|
|
70
|
+
const { settings, pool } = deps;
|
|
71
|
+
const env = deps.env ?? process.env;
|
|
72
|
+
// Strips exactly the mount prefix off the RAW url, leaving the remainder + query untouched.
|
|
73
|
+
const prefixRe = new RegExp(`^${API_PREFIX}/projects/[^/?#]+/interactive`);
|
|
74
|
+
/** The resolved docs root for a project, or null when no such project exists. */
|
|
75
|
+
async function rootFor(projectId) {
|
|
76
|
+
// `default` is SYNTHESIZED by the route layer (DES-PROJECT-001 §7) — the engine has no row
|
|
77
|
+
// for it, so an existence check there would 404 the one project every operator starts with.
|
|
78
|
+
if (projectId !== DEFAULT_PROJECT_ID) {
|
|
79
|
+
try {
|
|
80
|
+
if ((await adapter.projectGet(projectId)) === null)
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
// A pre-0.6.0 engine has no project surface at all; the shared default root is still
|
|
85
|
+
// a truthful answer for the only project such a deployment can have.
|
|
86
|
+
if (!(err instanceof ProjectsUnsupportedError))
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return resolveInteractiveRoot(settings.get(projectId), env);
|
|
91
|
+
}
|
|
92
|
+
// Encapsulated so the raw-body parser below applies to the PROXY ONLY. The root instance
|
|
93
|
+
// installs a JSON parser that buffers and parses the body — correct for every other route,
|
|
94
|
+
// fatal here: a parsed body is a consumed stream, and an SSE POST would arrive at the bridge
|
|
95
|
+
// only once the client finished sending.
|
|
96
|
+
void app.register(async (scope) => {
|
|
97
|
+
scope.removeAllContentTypeParsers();
|
|
98
|
+
scope.addContentTypeParser('*', (_req, payload, done) => {
|
|
99
|
+
done(null, payload);
|
|
100
|
+
});
|
|
101
|
+
scope.all(`${API_PREFIX}/projects/:projectId/interactive/*`, async (req, reply) => {
|
|
102
|
+
const { projectId } = req.params;
|
|
103
|
+
const prefix = `${API_PREFIX}/projects/${encodeURIComponent(projectId)}/interactive`;
|
|
104
|
+
const root = await rootFor(projectId);
|
|
105
|
+
if (root === null)
|
|
106
|
+
return reply.code(404).send({ error: `Project ${projectId} not found` });
|
|
107
|
+
let bridge;
|
|
108
|
+
try {
|
|
109
|
+
bridge = await pool.ensure(root);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
return unavailable(reply, err, deps.log);
|
|
113
|
+
}
|
|
114
|
+
const target = (req.raw.url ?? '').replace(prefixRe, '') || '/';
|
|
115
|
+
try {
|
|
116
|
+
await forward(req, reply, bridge, target, prefix);
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
// The cached bridge died between the pid check and the connect (an operator killed it,
|
|
120
|
+
// a crash). Invalidate and let `ensure` restart it — ONE retry, so a genuinely broken
|
|
121
|
+
// bridge fails fast to a 503 instead of looping.
|
|
122
|
+
if (!isConnectionRefused(err) || reply.raw.headersSent)
|
|
123
|
+
throw err;
|
|
124
|
+
pool.invalidate(root);
|
|
125
|
+
try {
|
|
126
|
+
bridge = await pool.ensure(root);
|
|
127
|
+
}
|
|
128
|
+
catch (startErr) {
|
|
129
|
+
return unavailable(reply, startErr, deps.log);
|
|
130
|
+
}
|
|
131
|
+
await forward(req, reply, bridge, target, prefix);
|
|
132
|
+
}
|
|
133
|
+
return reply;
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
function isConnectionRefused(err) {
|
|
138
|
+
const code = err?.code;
|
|
139
|
+
return code === 'ECONNREFUSED' || code === 'ECONNRESET' || code === 'EHOSTUNREACH';
|
|
140
|
+
}
|
|
141
|
+
/** §5.6's failure shape: a machine-readable code plus a command the operator can actually run. */
|
|
142
|
+
function unavailable(reply, err, log) {
|
|
143
|
+
if (!(err instanceof BridgeUnavailableError))
|
|
144
|
+
throw err;
|
|
145
|
+
log?.(`interactive bridge unavailable: ${err.message}`);
|
|
146
|
+
return reply.code(503).send({ code: 'bridge_unavailable', hint: err.hint });
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* One proxied exchange, socket to socket. `reply.hijack()` hands us the raw response before
|
|
150
|
+
* Fastify can serialize or buffer anything, which is what lets an SSE chunk reach the browser
|
|
151
|
+
* the moment the bridge emits it rather than at stream close.
|
|
152
|
+
*/
|
|
153
|
+
function forward(req, reply, bridge, target, prefix) {
|
|
154
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
155
|
+
const upstream = httpRequest({
|
|
156
|
+
host: bridge.host,
|
|
157
|
+
port: bridge.port,
|
|
158
|
+
method: req.method,
|
|
159
|
+
path: target,
|
|
160
|
+
headers: forwardableRequestHeaders(req.headers, bridge),
|
|
161
|
+
}, (res) => {
|
|
162
|
+
reply.hijack();
|
|
163
|
+
const headers = {};
|
|
164
|
+
for (const [name, value] of Object.entries(res.headers)) {
|
|
165
|
+
if (HOP_BY_HOP.has(name) || value === undefined)
|
|
166
|
+
continue;
|
|
167
|
+
headers[name] = name === 'location' && typeof value === 'string' ? rewriteLocation(value, bridge, prefix) : value;
|
|
168
|
+
}
|
|
169
|
+
reply.raw.writeHead(res.statusCode ?? 502, headers);
|
|
170
|
+
// No `pipeline` and no buffering layer: `pipe` forwards each chunk as it lands, and
|
|
171
|
+
// Node flushes it because we never set a highWaterMark barrier in between.
|
|
172
|
+
res.pipe(reply.raw);
|
|
173
|
+
res.on('end', () => resolvePromise());
|
|
174
|
+
res.on('error', (err) => {
|
|
175
|
+
reply.raw.destroy();
|
|
176
|
+
rejectPromise(err);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
upstream.on('error', (err) => {
|
|
180
|
+
if (reply.raw.headersSent)
|
|
181
|
+
reply.raw.destroy();
|
|
182
|
+
rejectPromise(err);
|
|
183
|
+
});
|
|
184
|
+
// If the client hangs up mid-stream (closing an SSE tab), stop pulling from the bridge.
|
|
185
|
+
reply.raw.on('close', () => upstream.destroy());
|
|
186
|
+
req.raw.pipe(upstream);
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
//# sourceMappingURL=proxy-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"proxy-routes.js","sourceRoot":"","sources":["../../src/interactive/proxy-routes.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DES-MERGE-001 §5.4 / §6.1 (slice 3) — the `wicked.interactive.*` ↔ `/ws` relay.
|
|
3
|
+
*
|
|
4
|
+
* wicked-studio is absorbing wicked-interactive. The SPA already holds crew's `/ws` socket for
|
|
5
|
+
* CoreEvent frames; interactive's own progress travels on the wicked-bus. This module bridges the
|
|
6
|
+
* two so the skin needs exactly ONE socket — no second websocket, no new port.
|
|
7
|
+
*
|
|
8
|
+
* Two halves, one bus handle:
|
|
9
|
+
*
|
|
10
|
+
* 1. RELAY (bus → /ws). Subscribe to `wicked.interactive.**` and rebroadcast each event as
|
|
11
|
+
* `{ type: 'interactiveEvent', event: <the full bus event> }` — ONE envelope type, the original
|
|
12
|
+
* event nested whole, no field renaming, no project filter. Ordering is as received (the bus
|
|
13
|
+
* hands a batch to the handler in order and `broadcast` is synchronous), and there is no
|
|
14
|
+
* buffering beyond what the existing broadcast path already does.
|
|
15
|
+
*
|
|
16
|
+
* 2. EMIT (/ws client → bus). `POST /projects/:projectId/interactive-events` validates the body
|
|
17
|
+
* against a SERVER-SIDE whitelist ({@link EMITTABLE_TYPES}) and puts the event on the bus with
|
|
18
|
+
* interactive's own envelope conventions (domain `wicked-interactive`, producer `wi-crew`,
|
|
19
|
+
* `ts`-stamped payload — draft-events.ts is the precedent), so interactive's own subscribers
|
|
20
|
+
* see a UI-originated event exactly as they see a service-originated one.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately DISTINCT from the `projectActivity` bridge in projects/events.ts, which reads the
|
|
23
|
+
* same filter: that one is project-FILTERED and reshapes the event into an activity frame for a
|
|
24
|
+
* project view. This one is unfiltered and verbatim, because the studio's document canvas must see
|
|
25
|
+
* the events of a doc that was never filed under a project. Separate plugin identity → separate
|
|
26
|
+
* durable cursor, so neither seam can strand the other.
|
|
27
|
+
*
|
|
28
|
+
* Posture mirrors every other bus seam here: LOUD-non-fatal. No wicked-bus, or a broken db → the
|
|
29
|
+
* factory logs once and returns null, the daemon boots without the relay, and the POST route
|
|
30
|
+
* answers 503 instead of pretending the emit landed.
|
|
31
|
+
*/
|
|
32
|
+
import type { FastifyInstance } from 'fastify';
|
|
33
|
+
import { z } from 'zod';
|
|
34
|
+
/** The ONE envelope type this relay puts on `/ws`. Consumers that don't know it ignore it — the
|
|
35
|
+
* CoreEvent contract is additive-safe (DES-STUDIO-001 §2.1). */
|
|
36
|
+
export declare const INTERACTIVE_EVENT_FRAME = "interactiveEvent";
|
|
37
|
+
/**
|
|
38
|
+
* Server-side whitelist of the event types a `/ws` client may emit through
|
|
39
|
+
* `POST /projects/:projectId/interactive-events`. A non-whitelisted type is a 400 that NAMES this
|
|
40
|
+
* array — the UI never has to guess. Extend here; nothing else needs to change.
|
|
41
|
+
*/
|
|
42
|
+
export declare const EMITTABLE_TYPES: readonly string[];
|
|
43
|
+
/** The request body: a whitelisted type plus that type's own payload. Strict — an unknown key is
|
|
44
|
+
* a 400, not a silently-dropped field. */
|
|
45
|
+
export declare const EmitInteractiveEventSchema: z.ZodObject<{
|
|
46
|
+
type: z.ZodString;
|
|
47
|
+
payload: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
48
|
+
}, "strict", z.ZodTypeAny, {
|
|
49
|
+
type: string;
|
|
50
|
+
payload?: Record<string, unknown> | undefined;
|
|
51
|
+
}, {
|
|
52
|
+
type: string;
|
|
53
|
+
payload?: Record<string, unknown> | undefined;
|
|
54
|
+
}>;
|
|
55
|
+
export interface InteractiveRelay {
|
|
56
|
+
/**
|
|
57
|
+
* Emit one interactive event onto the bus. Returns true on success (or on WB-002, a duplicate
|
|
58
|
+
* key — the emit already happened); logs and returns false otherwise. Never throws.
|
|
59
|
+
*/
|
|
60
|
+
emitInteractive(type: string, payload: Record<string, unknown>, idempotencyKey: string): boolean;
|
|
61
|
+
stop(): Promise<void>;
|
|
62
|
+
}
|
|
63
|
+
export interface InteractiveRelayOptions {
|
|
64
|
+
/** Bus db path; omit for wicked-bus's own resolution (honors WICKED_BUS_DATA_DIR). */
|
|
65
|
+
dbPath?: string;
|
|
66
|
+
/** Poll cadence for the relay subscriber, ms (tests shorten it). */
|
|
67
|
+
pollIntervalMs?: number;
|
|
68
|
+
log?: (msg: string) => void;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Open the bus and arm both halves. Returns `null` (logged) when wicked-bus is not importable or
|
|
72
|
+
* the db cannot open — the caller degrades to a daemon whose `/ws` simply carries no interactive
|
|
73
|
+
* frames, exactly as before this slice.
|
|
74
|
+
*/
|
|
75
|
+
export declare function startInteractiveWsRelay(opts?: InteractiveRelayOptions): Promise<InteractiveRelay | null>;
|
|
76
|
+
/** Occurrence-unique, type-inclusive (DES-EXEC-001 rev0.2 #4a): two types sharing a key would
|
|
77
|
+
* silently drop one on the bus's UNIQUE constraint, and every UI emission is a new fact. */
|
|
78
|
+
export declare function interactiveEmitKey(projectId: string, type: string): string;
|
|
79
|
+
/**
|
|
80
|
+
* `POST /api/v1/projects/:projectId/interactive-events` — the UI-emittable direction (§6.1).
|
|
81
|
+
* Registered even when the relay is null, so the failure is an honest 503 rather than a 404 that
|
|
82
|
+
* reads like "this daemon doesn't have the feature".
|
|
83
|
+
*/
|
|
84
|
+
export declare function registerInteractiveEventRoutes(app: FastifyInstance, relay: InteractiveRelay | null): void;
|
|
85
|
+
//# sourceMappingURL=ws-relay.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-relay.d.ts","sourceRoot":"","sources":["../../src/interactive/ws-relay.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAiBxB;iEACiE;AACjE,eAAO,MAAM,uBAAuB,qBAAqB,CAAC;AAE1D;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,SAAS,MAAM,EAG5C,CAAC;AAEF;2CAC2C;AAC3C,eAAO,MAAM,0BAA0B;;;;;;;;;EAK5B,CAAC;AAEZ,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC;IACjG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,oEAAoE;IACpE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CAC7B;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAC3C,IAAI,GAAE,uBAA4B,GACjC,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAgGlC;AAED;6FAC6F;AAC7F,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1E;AAED;;;;GAIG;AACH,wBAAgB,8BAA8B,CAC5C,GAAG,EAAE,eAAe,EACpB,KAAK,EAAE,gBAAgB,GAAG,IAAI,GAC7B,IAAI,CAmCN"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DES-MERGE-001 §5.4 / §6.1 (slice 3) — the `wicked.interactive.*` ↔ `/ws` relay.
|
|
3
|
+
*
|
|
4
|
+
* wicked-studio is absorbing wicked-interactive. The SPA already holds crew's `/ws` socket for
|
|
5
|
+
* CoreEvent frames; interactive's own progress travels on the wicked-bus. This module bridges the
|
|
6
|
+
* two so the skin needs exactly ONE socket — no second websocket, no new port.
|
|
7
|
+
*
|
|
8
|
+
* Two halves, one bus handle:
|
|
9
|
+
*
|
|
10
|
+
* 1. RELAY (bus → /ws). Subscribe to `wicked.interactive.**` and rebroadcast each event as
|
|
11
|
+
* `{ type: 'interactiveEvent', event: <the full bus event> }` — ONE envelope type, the original
|
|
12
|
+
* event nested whole, no field renaming, no project filter. Ordering is as received (the bus
|
|
13
|
+
* hands a batch to the handler in order and `broadcast` is synchronous), and there is no
|
|
14
|
+
* buffering beyond what the existing broadcast path already does.
|
|
15
|
+
*
|
|
16
|
+
* 2. EMIT (/ws client → bus). `POST /projects/:projectId/interactive-events` validates the body
|
|
17
|
+
* against a SERVER-SIDE whitelist ({@link EMITTABLE_TYPES}) and puts the event on the bus with
|
|
18
|
+
* interactive's own envelope conventions (domain `wicked-interactive`, producer `wi-crew`,
|
|
19
|
+
* `ts`-stamped payload — draft-events.ts is the precedent), so interactive's own subscribers
|
|
20
|
+
* see a UI-originated event exactly as they see a service-originated one.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately DISTINCT from the `projectActivity` bridge in projects/events.ts, which reads the
|
|
23
|
+
* same filter: that one is project-FILTERED and reshapes the event into an activity frame for a
|
|
24
|
+
* project view. This one is unfiltered and verbatim, because the studio's document canvas must see
|
|
25
|
+
* the events of a doc that was never filed under a project. Separate plugin identity → separate
|
|
26
|
+
* durable cursor, so neither seam can strand the other.
|
|
27
|
+
*
|
|
28
|
+
* Posture mirrors every other bus seam here: LOUD-non-fatal. No wicked-bus, or a broken db → the
|
|
29
|
+
* factory logs once and returns null, the daemon boots without the relay, and the POST route
|
|
30
|
+
* answers 503 instead of pretending the emit landed.
|
|
31
|
+
*/
|
|
32
|
+
import { randomUUID } from 'node:crypto';
|
|
33
|
+
import { z } from 'zod';
|
|
34
|
+
import { broadcast } from '../events/bus.js';
|
|
35
|
+
import { API_PREFIX } from '../api/api-prefix.js';
|
|
36
|
+
import { LOCAL_ACTOR } from '../api/auth.js';
|
|
37
|
+
import { INTERACTIVE_DOMAIN, INTERACTIVE_PRODUCER } from './draft-events.js';
|
|
38
|
+
const V = API_PREFIX;
|
|
39
|
+
/** Dedicated durable-cursor identity — NOT the projects bridge's, so the two seams that read
|
|
40
|
+
* `wicked.interactive.**` advance independent cursors. */
|
|
41
|
+
const RELAY_PLUGIN = 'wicked-crew-interactive-relay';
|
|
42
|
+
const RELAY_FILTER = 'wicked.interactive.**';
|
|
43
|
+
/** The scope guard, restated locally: the property "only interactive events are relayed" is this
|
|
44
|
+
* module's, not the bus glob's. */
|
|
45
|
+
const RELAY_TYPE_PREFIX = 'wicked.interactive.';
|
|
46
|
+
/** The ONE envelope type this relay puts on `/ws`. Consumers that don't know it ignore it — the
|
|
47
|
+
* CoreEvent contract is additive-safe (DES-STUDIO-001 §2.1). */
|
|
48
|
+
export const INTERACTIVE_EVENT_FRAME = 'interactiveEvent';
|
|
49
|
+
/**
|
|
50
|
+
* Server-side whitelist of the event types a `/ws` client may emit through
|
|
51
|
+
* `POST /projects/:projectId/interactive-events`. A non-whitelisted type is a 400 that NAMES this
|
|
52
|
+
* array — the UI never has to guess. Extend here; nothing else needs to change.
|
|
53
|
+
*/
|
|
54
|
+
export const EMITTABLE_TYPES = [
|
|
55
|
+
'wicked.interactive.feedback.submitted',
|
|
56
|
+
'wicked.interactive.status.requested',
|
|
57
|
+
];
|
|
58
|
+
/** The request body: a whitelisted type plus that type's own payload. Strict — an unknown key is
|
|
59
|
+
* a 400, not a silently-dropped field. */
|
|
60
|
+
export const EmitInteractiveEventSchema = z
|
|
61
|
+
.object({
|
|
62
|
+
type: z.string().min(1).max(128),
|
|
63
|
+
payload: z.record(z.string(), z.unknown()).optional(),
|
|
64
|
+
})
|
|
65
|
+
.strict();
|
|
66
|
+
/**
|
|
67
|
+
* Open the bus and arm both halves. Returns `null` (logged) when wicked-bus is not importable or
|
|
68
|
+
* the db cannot open — the caller degrades to a daemon whose `/ws` simply carries no interactive
|
|
69
|
+
* frames, exactly as before this slice.
|
|
70
|
+
*/
|
|
71
|
+
export async function startInteractiveWsRelay(opts = {}) {
|
|
72
|
+
const log = opts.log ?? (() => undefined);
|
|
73
|
+
let bus;
|
|
74
|
+
try {
|
|
75
|
+
bus = await import('wicked-bus');
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
log(`[interactive-relay] wicked-bus is not importable — the interactive /ws relay is disabled: ${err instanceof Error ? err.message : String(err)}`);
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
let db;
|
|
82
|
+
let config;
|
|
83
|
+
try {
|
|
84
|
+
const override = opts.dbPath !== undefined ? { db_path: opts.dbPath } : {};
|
|
85
|
+
config = bus.loadConfig(override);
|
|
86
|
+
db = bus.openDb(override);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
log(`[interactive-relay] could not open the bus db${opts.dbPath !== undefined ? ` at ${opts.dbPath}` : ''} — the interactive /ws relay is disabled: ${err instanceof Error ? err.message : String(err)}`);
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
// Half 1 — bus → /ws. maxRetries: 0 for the same reason the projects bridge uses it: a retry
|
|
93
|
+
// loop or a DLQ entry for a lost liveness frame is noise, and the durable feed is the record.
|
|
94
|
+
//
|
|
95
|
+
// This subscription also sees the events emitted by half 2 (they match the same filter). That
|
|
96
|
+
// is not a loop — the relay only broadcasts, never re-emits — and the echo is the UI's
|
|
97
|
+
// confirmation that its own emission actually landed on the bus.
|
|
98
|
+
let subscription = null;
|
|
99
|
+
try {
|
|
100
|
+
subscription = bus.subscribe({
|
|
101
|
+
db,
|
|
102
|
+
plugin: RELAY_PLUGIN,
|
|
103
|
+
filter: RELAY_FILTER,
|
|
104
|
+
cursor_init: 'latest',
|
|
105
|
+
pollIntervalMs: opts.pollIntervalMs ?? 2000,
|
|
106
|
+
maxRetries: 0,
|
|
107
|
+
handler: (event) => {
|
|
108
|
+
if (!event.event_type.startsWith(RELAY_TYPE_PREFIX))
|
|
109
|
+
return;
|
|
110
|
+
broadcast({ type: INTERACTIVE_EVENT_FRAME, event });
|
|
111
|
+
},
|
|
112
|
+
onError: (err, event) => {
|
|
113
|
+
log(`[interactive-relay] relay error on event ${String(event?.event_id ?? '?')}: ${err.message}`);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
log(`[interactive-relay] could not arm the /ws relay subscriber (the emit direction still works): ${err instanceof Error ? err.message : String(err)}`);
|
|
119
|
+
}
|
|
120
|
+
// Half 2 — the emit surface the POST route calls after the whitelist check. The subdomain is
|
|
121
|
+
// the third segment of interactive's own type grammar (`wicked.interactive.<subdomain>.<verb>`).
|
|
122
|
+
function emitInteractive(type, payload, idempotencyKey) {
|
|
123
|
+
try {
|
|
124
|
+
bus.emit(db, config, {
|
|
125
|
+
event_type: type,
|
|
126
|
+
domain: INTERACTIVE_DOMAIN,
|
|
127
|
+
subdomain: type.split('.')[2] ?? 'status',
|
|
128
|
+
payload: { ts: new Date().toISOString(), ...payload },
|
|
129
|
+
producer_id: INTERACTIVE_PRODUCER,
|
|
130
|
+
idempotency_key: idempotencyKey,
|
|
131
|
+
});
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const code = err.error;
|
|
136
|
+
if (code === 'WB-002')
|
|
137
|
+
return true; // duplicate key — the emit already happened
|
|
138
|
+
log(`[interactive-relay] emit ${type} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
emitInteractive,
|
|
144
|
+
stop: async () => {
|
|
145
|
+
if (subscription !== null)
|
|
146
|
+
await subscription.stop();
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** Occurrence-unique, type-inclusive (DES-EXEC-001 rev0.2 #4a): two types sharing a key would
|
|
151
|
+
* silently drop one on the bus's UNIQUE constraint, and every UI emission is a new fact. */
|
|
152
|
+
export function interactiveEmitKey(projectId, type) {
|
|
153
|
+
return `crew:interactive.ui:${projectId}:${type}:${randomUUID()}`;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* `POST /api/v1/projects/:projectId/interactive-events` — the UI-emittable direction (§6.1).
|
|
157
|
+
* Registered even when the relay is null, so the failure is an honest 503 rather than a 404 that
|
|
158
|
+
* reads like "this daemon doesn't have the feature".
|
|
159
|
+
*/
|
|
160
|
+
export function registerInteractiveEventRoutes(app, relay) {
|
|
161
|
+
const actorOf = (req) => req.actor ?? LOCAL_ACTOR;
|
|
162
|
+
app.post(`${V}/projects/:projectId/interactive-events`, async (req, reply) => {
|
|
163
|
+
const { projectId } = req.params;
|
|
164
|
+
const parsed = EmitInteractiveEventSchema.safeParse(req.body);
|
|
165
|
+
if (!parsed.success) {
|
|
166
|
+
return reply.code(400).send({ error: 'Invalid request body', details: parsed.error.issues });
|
|
167
|
+
}
|
|
168
|
+
const { type } = parsed.data;
|
|
169
|
+
if (!EMITTABLE_TYPES.includes(type)) {
|
|
170
|
+
return reply.code(400).send({
|
|
171
|
+
error: `Event type '${type}' is not emittable from a client`,
|
|
172
|
+
allowed: EMITTABLE_TYPES,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (relay === null) {
|
|
176
|
+
return reply
|
|
177
|
+
.code(503)
|
|
178
|
+
.send({ error: 'the interactive bus seam is not armed on this daemon' });
|
|
179
|
+
}
|
|
180
|
+
// Server-side fields go LAST: `project_id` and `actor` are the daemon's to state, never the
|
|
181
|
+
// caller's (task #88 — event actors are not caller-supplied), so a payload that names either
|
|
182
|
+
// one cannot override them.
|
|
183
|
+
const idempotencyKey = interactiveEmitKey(projectId, type);
|
|
184
|
+
const emitted = relay.emitInteractive(type, { ...(parsed.data.payload ?? {}), project_id: projectId, actor: actorOf(req).id }, idempotencyKey);
|
|
185
|
+
if (!emitted) {
|
|
186
|
+
return reply.code(502).send({ error: `the bus refused the ${type} emit` });
|
|
187
|
+
}
|
|
188
|
+
return reply.code(202).send({ emitted: true, type, projectId, idempotencyKey });
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=ws-relay.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-relay.js","sourceRoot":"","sources":["../../src/interactive/ws-relay.ts"],"names":[],"mappings":"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"}
|