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.
Files changed (116) hide show
  1. package/dist/api/audit.d.ts +57 -0
  2. package/dist/api/audit.d.ts.map +1 -0
  3. package/dist/api/audit.js +115 -0
  4. package/dist/api/audit.js.map +1 -0
  5. package/dist/api/auth.d.ts +192 -0
  6. package/dist/api/auth.d.ts.map +1 -0
  7. package/dist/api/auth.js +515 -0
  8. package/dist/api/auth.js.map +1 -0
  9. package/dist/api/gate-cache.d.ts +8 -0
  10. package/dist/api/gate-cache.d.ts.map +1 -1
  11. package/dist/api/gate-cache.js +10 -0
  12. package/dist/api/gate-cache.js.map +1 -1
  13. package/dist/api/open-path.d.ts +18 -0
  14. package/dist/api/open-path.d.ts.map +1 -0
  15. package/dist/api/open-path.js +79 -0
  16. package/dist/api/open-path.js.map +1 -0
  17. package/dist/api/routes.d.ts +112 -1
  18. package/dist/api/routes.d.ts.map +1 -1
  19. package/dist/api/routes.js +435 -16
  20. package/dist/api/routes.js.map +1 -1
  21. package/dist/api/seat-health.d.ts +55 -0
  22. package/dist/api/seat-health.d.ts.map +1 -0
  23. package/dist/api/seat-health.js +273 -0
  24. package/dist/api/seat-health.js.map +1 -0
  25. package/dist/api/seat-signin.d.ts +27 -0
  26. package/dist/api/seat-signin.d.ts.map +1 -0
  27. package/dist/api/seat-signin.js +143 -0
  28. package/dist/api/seat-signin.js.map +1 -0
  29. package/dist/api/server.d.ts +111 -3
  30. package/dist/api/server.d.ts.map +1 -1
  31. package/dist/api/server.js +236 -11
  32. package/dist/api/server.js.map +1 -1
  33. package/dist/cli/index.js +97 -3
  34. package/dist/cli/index.js.map +1 -1
  35. package/dist/cli/mcp.d.ts +14 -0
  36. package/dist/cli/mcp.d.ts.map +1 -0
  37. package/dist/cli/mcp.js +119 -0
  38. package/dist/cli/mcp.js.map +1 -0
  39. package/dist/core/adapter.d.ts +64 -1
  40. package/dist/core/adapter.d.ts.map +1 -1
  41. package/dist/core/adapter.js +162 -4
  42. package/dist/core/adapter.js.map +1 -1
  43. package/dist/core/types.d.ts +23 -361
  44. package/dist/core/types.d.ts.map +1 -1
  45. package/dist/core/types.js +11 -4
  46. package/dist/core/types.js.map +1 -1
  47. package/dist/interactive/bridge-pool.d.ts +71 -0
  48. package/dist/interactive/bridge-pool.d.ts.map +1 -0
  49. package/dist/interactive/bridge-pool.js +187 -0
  50. package/dist/interactive/bridge-pool.js.map +1 -0
  51. package/dist/interactive/bridge-root.d.ts +36 -0
  52. package/dist/interactive/bridge-root.d.ts.map +1 -0
  53. package/dist/interactive/bridge-root.js +48 -0
  54. package/dist/interactive/bridge-root.js.map +1 -0
  55. package/dist/interactive/draft-events.d.ts +144 -0
  56. package/dist/interactive/draft-events.d.ts.map +1 -0
  57. package/dist/interactive/draft-events.js +490 -0
  58. package/dist/interactive/draft-events.js.map +1 -0
  59. package/dist/interactive/edit-events.d.ts +172 -0
  60. package/dist/interactive/edit-events.d.ts.map +1 -0
  61. package/dist/interactive/edit-events.js +530 -0
  62. package/dist/interactive/edit-events.js.map +1 -0
  63. package/dist/interactive/ledger.d.ts +39 -0
  64. package/dist/interactive/ledger.d.ts.map +1 -0
  65. package/dist/interactive/ledger.js +93 -0
  66. package/dist/interactive/ledger.js.map +1 -0
  67. package/dist/interactive/proxy-routes.d.ts +39 -0
  68. package/dist/interactive/proxy-routes.d.ts.map +1 -0
  69. package/dist/interactive/proxy-routes.js +189 -0
  70. package/dist/interactive/proxy-routes.js.map +1 -0
  71. package/dist/interactive/ws-relay.d.ts +85 -0
  72. package/dist/interactive/ws-relay.d.ts.map +1 -0
  73. package/dist/interactive/ws-relay.js +191 -0
  74. package/dist/interactive/ws-relay.js.map +1 -0
  75. package/dist/projects/activity.d.ts +29 -0
  76. package/dist/projects/activity.d.ts.map +1 -0
  77. package/dist/projects/activity.js +172 -0
  78. package/dist/projects/activity.js.map +1 -0
  79. package/dist/projects/charter.d.ts +28 -0
  80. package/dist/projects/charter.d.ts.map +1 -0
  81. package/dist/projects/charter.js +53 -0
  82. package/dist/projects/charter.js.map +1 -0
  83. package/dist/projects/events.d.ts +55 -0
  84. package/dist/projects/events.d.ts.map +1 -0
  85. package/dist/projects/events.js +141 -0
  86. package/dist/projects/events.js.map +1 -0
  87. package/dist/projects/membership-index.d.ts +25 -0
  88. package/dist/projects/membership-index.d.ts.map +1 -0
  89. package/dist/projects/membership-index.js +46 -0
  90. package/dist/projects/membership-index.js.map +1 -0
  91. package/dist/projects/routes.d.ts +90 -0
  92. package/dist/projects/routes.d.ts.map +1 -0
  93. package/dist/projects/routes.js +388 -0
  94. package/dist/projects/routes.js.map +1 -0
  95. package/dist/projects/settings.d.ts +32 -0
  96. package/dist/projects/settings.d.ts.map +1 -0
  97. package/dist/projects/settings.js +64 -0
  98. package/dist/projects/settings.js.map +1 -0
  99. package/dist/qe/acceptance.d.ts +137 -0
  100. package/dist/qe/acceptance.d.ts.map +1 -0
  101. package/dist/qe/acceptance.js +249 -0
  102. package/dist/qe/acceptance.js.map +1 -0
  103. package/dist/qe/gate-events.d.ts +111 -0
  104. package/dist/qe/gate-events.d.ts.map +1 -0
  105. package/dist/qe/gate-events.js +168 -0
  106. package/dist/qe/gate-events.js.map +1 -0
  107. package/dist/qe/ledger.d.ts +100 -0
  108. package/dist/qe/ledger.d.ts.map +1 -0
  109. package/dist/qe/ledger.js +154 -0
  110. package/dist/qe/ledger.js.map +1 -0
  111. package/dist/studio/assets/index-CCwXa1cn.js +428 -0
  112. package/dist/studio/assets/index-HWxo0h41.css +32 -0
  113. package/dist/studio/index.html +2 -2
  114. package/package.json +10 -4
  115. package/dist/studio/assets/index-DaaUU8Ep.css +0 -32
  116. 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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"}