dsh-realtime-audio-ws 0.2.2 → 0.2.4
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/README.md +64 -12
- package/lib/bridge.d.ts +24 -0
- package/lib/bridge.d.ts.map +1 -1
- package/lib/bridge.js +47 -3
- package/lib/bridge.js.map +1 -1
- package/lib/client/bundle.js +347 -6
- package/lib/client/index.js +347 -6
- package/lib/control.d.ts +173 -0
- package/lib/control.d.ts.map +1 -0
- package/lib/control.js +286 -0
- package/lib/control.js.map +1 -0
- package/lib/index.d.ts +6 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +72 -3
- package/lib/index.js.map +1 -1
- package/lib/injection.d.ts +18 -0
- package/lib/injection.d.ts.map +1 -1
- package/lib/injection.js.map +1 -1
- package/lib/journal-file.d.ts +39 -0
- package/lib/journal-file.d.ts.map +1 -0
- package/lib/journal-file.js +45 -0
- package/lib/journal-file.js.map +1 -0
- package/lib/strip.d.ts +71 -0
- package/lib/strip.d.ts.map +1 -0
- package/lib/strip.js +93 -0
- package/lib/strip.js.map +1 -0
- package/lib/types.d.ts +9 -1
- package/lib/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/src/bridge.ts +59 -3
- package/src/client/index.ts +441 -6
- package/src/control.ts +402 -0
- package/src/index.ts +91 -5
- package/src/injection.ts +14 -0
- package/src/journal-file.ts +47 -0
- package/src/strip.ts +98 -0
- package/src/types.ts +9 -1
package/lib/control.js
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The control channel: verbs, sent as text frames on the socket that already carries audio.
|
|
3
|
+
*
|
|
4
|
+
* Until this module there was no way to drive the plugin without a console. The socket was duplex and
|
|
5
|
+
* carried PCM16 in both directions, and a text frame was explicitly *not* part of its contract; the
|
|
6
|
+
* client half published itself on a `globalThis` key and that global was the on-switch. Two restarts
|
|
7
|
+
* and a false lead went into changing which session the voice steered, because the config was read at
|
|
8
|
+
* boot and only at boot.
|
|
9
|
+
*
|
|
10
|
+
* So this is a deliberate widening of a documented contract, and it is written as a **channel** rather
|
|
11
|
+
* than as a second API: one text frame in, exactly one text frame out, on the socket the client already
|
|
12
|
+
* holds.
|
|
13
|
+
*
|
|
14
|
+
* Five properties are load-bearing.
|
|
15
|
+
*
|
|
16
|
+
* - **Every frame is answered.** A control frame the channel cannot parse is answered with the reason,
|
|
17
|
+
* not dropped — the same rule `set` follows one layer down. A channel that reports nothing is
|
|
18
|
+
* indistinguishable from one that never received the frame.
|
|
19
|
+
* - **The reply is JSON, and small.** It carries the values a status surface needs and no prose to
|
|
20
|
+
* parse: `ok`, the verb as it was sent, and the fields for that verb.
|
|
21
|
+
* - **`steer` is a resolution, not a special case.** The verb addresses whichever plugin declares a live
|
|
22
|
+
* `sessionId`; it refuses with a reason rather than guessing when none or several do.
|
|
23
|
+
* - **`set` goes through the settings surface and nowhere else**, so a change on this channel takes the
|
|
24
|
+
* identical path — same parse, same refusal, same journal entry — as one from any other control plane.
|
|
25
|
+
* - **The verb vocabulary is closed.** A typo answers with the verbs that exist, because a silent
|
|
26
|
+
* fall-through is how a user concludes the feature is missing.
|
|
27
|
+
*
|
|
28
|
+
* @module dsh-realtime-audio-ws/control
|
|
29
|
+
*/
|
|
30
|
+
import { redact } from 'dsh-realtime';
|
|
31
|
+
/** The verbs, as a value, so an unknown one can be answered with the list. */
|
|
32
|
+
const VERBS = Object.freeze(['status', 'start', 'stop', 'steer', 'set']);
|
|
33
|
+
/**
|
|
34
|
+
* Read one text frame as a control command.
|
|
35
|
+
*
|
|
36
|
+
* A control frame is a **line**, so its line ending is stripped and nothing else is: `set x= 5` and
|
|
37
|
+
* `set x=5` are two different requests, and a channel that normalised one into the other would apply a
|
|
38
|
+
* change the caller did not ask for. For the same reason the verbs are matched exactly — an `INVALID_CONTROL`
|
|
39
|
+
* for `STATUS` is a better answer than a guess about intent, because it can be read and fixed.
|
|
40
|
+
*
|
|
41
|
+
* @param frame - the frame as it arrived, without its line ending.
|
|
42
|
+
* @returns the command, or a refusal naming the frame's fault.
|
|
43
|
+
*/
|
|
44
|
+
export function parseControlFrame(frame) {
|
|
45
|
+
// The line ending and leading whitespace are the only things stripped: a control frame is a line, and a
|
|
46
|
+
// shell or a console may well hand over a leading space. Everything after the first `=` is left
|
|
47
|
+
// untouched, because `set x= 5` and `set x=5` are two different requests and a channel that normalised
|
|
48
|
+
// one into the other would apply a change the caller did not ask for.
|
|
49
|
+
const line = frame.replace(/[\r\n]+$/u, '').trimStart();
|
|
50
|
+
const split = line.search(/\s/u);
|
|
51
|
+
const verb = split < 0 ? line : line.slice(0, split);
|
|
52
|
+
const tail = split < 0 ? '' : line.slice(split);
|
|
53
|
+
const known = VERBS.find(candidate => candidate === verb);
|
|
54
|
+
if (known === undefined) {
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
verb,
|
|
58
|
+
code: 'UNKNOWN_VERB',
|
|
59
|
+
reason: `"${verb}" is not a control verb — this channel answers ${VERBS.join(', ')}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (known === 'status' || known === 'start' || known === 'stop') {
|
|
63
|
+
if (tail.trim() !== '') {
|
|
64
|
+
return { ok: false, verb: known, code: 'INVALID_CONTROL', reason: `"${known}" takes no argument, received ${quote(tail.trim())}` };
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, verb: known };
|
|
67
|
+
}
|
|
68
|
+
if (known === 'steer') {
|
|
69
|
+
const sessionId = tail.trim();
|
|
70
|
+
if (sessionId === '') {
|
|
71
|
+
return { ok: false, verb: known, code: 'INVALID_CONTROL', reason: '"steer" needs the session id to steer at' };
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, verb: known, sessionId };
|
|
74
|
+
}
|
|
75
|
+
// `set`: the key ends at the **first** `=`, and the value is everything after it. A value is allowed to
|
|
76
|
+
// contain `=` — and a split on the last one would silently cut a session id in half.
|
|
77
|
+
const argument = tail.trimStart();
|
|
78
|
+
const equals = argument.indexOf('=');
|
|
79
|
+
const key = equals < 0 ? '' : argument.slice(0, equals).trim();
|
|
80
|
+
if (equals < 0 || key === '') {
|
|
81
|
+
return { ok: false, verb: known, code: 'INVALID_CONTROL', reason: '"set" needs <key>=<value>' };
|
|
82
|
+
}
|
|
83
|
+
return { ok: true, verb: known, key, value: argument.slice(equals + 1) };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Build the channel's handler.
|
|
87
|
+
*
|
|
88
|
+
* The returned function is **total**: every frame it is given produces exactly one reply string, and it
|
|
89
|
+
* never rejects. That is a contract, not a hope — the caller writes the reply into a socket and has no
|
|
90
|
+
* one to catch a rejection, and a control frame that produces nothing at all is the silence this
|
|
91
|
+
* project keeps having to pay for.
|
|
92
|
+
*
|
|
93
|
+
* @param deps - the settings surface, the request edges, and this route's own facts.
|
|
94
|
+
* @returns a handler taking one frame and answering with the reply to send.
|
|
95
|
+
*/
|
|
96
|
+
export function createControlHandler(deps) {
|
|
97
|
+
return async (frame) => {
|
|
98
|
+
try {
|
|
99
|
+
return encode(await dispatch(deps, frame));
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
// Reachable through any of the deps, not just the obvious one: a status query that throws, a
|
|
103
|
+
// count that throws. Refused rather than swallowed, because the alternative is a client waiting
|
|
104
|
+
// for an answer that will never come.
|
|
105
|
+
return encode({
|
|
106
|
+
ok: false,
|
|
107
|
+
verb: '',
|
|
108
|
+
code: 'CONTROL_FAILED',
|
|
109
|
+
reason: `the control handler failed: ${classOf(error)}`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Carry out one frame's command.
|
|
116
|
+
* @param deps - the handler's dependencies.
|
|
117
|
+
* @param frame - the frame as it arrived.
|
|
118
|
+
* @returns the reply to send.
|
|
119
|
+
*/
|
|
120
|
+
async function dispatch(deps, frame) {
|
|
121
|
+
const command = parseControlFrame(frame);
|
|
122
|
+
if (!command.ok)
|
|
123
|
+
return command;
|
|
124
|
+
switch (command.verb) {
|
|
125
|
+
case 'status': return await statusReply(deps);
|
|
126
|
+
case 'start':
|
|
127
|
+
case 'stop': return await sessionReply(deps, command.verb);
|
|
128
|
+
case 'steer': return steer(deps, command.sessionId);
|
|
129
|
+
case 'set': return change(deps, command.key, command.value, 'set');
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Answer `status`.
|
|
134
|
+
*
|
|
135
|
+
* A status that quietly omitted whether voice is live would be the same class of report this project
|
|
136
|
+
* exists to replace, so a query that *fails* refuses the whole verb: a partial status is indistinguishable
|
|
137
|
+
* from a healthy one to whoever is reading it in a bug report.
|
|
138
|
+
* @param deps - the handler's dependencies.
|
|
139
|
+
* @returns the status, or the refusal explaining why there is none.
|
|
140
|
+
*/
|
|
141
|
+
async function statusReply(deps) {
|
|
142
|
+
let voice;
|
|
143
|
+
try {
|
|
144
|
+
voice = await deps.voice() ?? null;
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
verb: 'status',
|
|
150
|
+
code: 'CONTROL_FAILED',
|
|
151
|
+
reason: `the voice status query failed: ${classOf(error)}`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const entries = deps.journal.snapshot();
|
|
155
|
+
return {
|
|
156
|
+
ok: true,
|
|
157
|
+
verb: 'status',
|
|
158
|
+
audio: { path: deps.path, clients: deps.clients() },
|
|
159
|
+
voice,
|
|
160
|
+
settings: deps.settings.list(),
|
|
161
|
+
journal: {
|
|
162
|
+
size: entries.length,
|
|
163
|
+
oldestSeq: entries[0]?.seq,
|
|
164
|
+
last: entries.at(-1) ?? null,
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Answer `start` or `stop`.
|
|
170
|
+
*
|
|
171
|
+
* These produce an **outcome** rather than an acknowledgement, which is why the dispatch is `serial` and
|
|
172
|
+
* not `emit`: "asked" and "it worked" are the two things this project has already collapsed once, in the
|
|
173
|
+
* delegation path, at the cost of an evening.
|
|
174
|
+
* @param deps - the handler's dependencies.
|
|
175
|
+
* @param verb - which request to make.
|
|
176
|
+
* @returns what the request produced.
|
|
177
|
+
*/
|
|
178
|
+
async function sessionReply(deps, verb) {
|
|
179
|
+
let outcome;
|
|
180
|
+
try {
|
|
181
|
+
outcome = await deps.request(verb);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
verb,
|
|
187
|
+
code: 'CONTROL_FAILED',
|
|
188
|
+
reason: `the ${verb} request failed: ${classOf(error)}`,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (outcome === undefined) {
|
|
192
|
+
// No listener: the agent row is absent or has not loaded. Saying so is the difference between a
|
|
193
|
+
// profile that is missing a row and a plugin that is broken.
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
verb,
|
|
197
|
+
code: 'CONTROL_FAILED',
|
|
198
|
+
reason: `nothing answered the ${verb} request — is a realtime agent mounted in this profile?`,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
ok: outcome.ok,
|
|
203
|
+
verb,
|
|
204
|
+
voice: outcome.voice,
|
|
205
|
+
...outcome.refusal === undefined ? {} : { refusal: outcome.refusal },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Resolve `steer <sessionId>` to a setting and change it.
|
|
210
|
+
*
|
|
211
|
+
* The verb names a *purpose* rather than a key, because the purpose is the thing a user has: "make the
|
|
212
|
+
* voice talk to that session". Which key that is belongs to whichever plugin declares it, so this
|
|
213
|
+
* resolves at the moment of use and refuses when the answer is not unique.
|
|
214
|
+
* @param deps - the handler's dependencies.
|
|
215
|
+
* @param sessionId - the session to steer at.
|
|
216
|
+
* @returns the change's outcome, or the refusal explaining why there is no single target.
|
|
217
|
+
*/
|
|
218
|
+
function steer(deps, sessionId) {
|
|
219
|
+
const targets = deps.settings
|
|
220
|
+
.list()
|
|
221
|
+
.filter(entry => entry.field === 'sessionId' && entry.scope === 'live' && entry.kind === 'string');
|
|
222
|
+
const only = targets[0];
|
|
223
|
+
if (only === undefined) {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
verb: 'steer',
|
|
227
|
+
code: 'NO_STEER_TARGET',
|
|
228
|
+
reason: 'no plugin declares a live sessionId to steer — use set <key>=<value>',
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (targets.length > 1) {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
verb: 'steer',
|
|
235
|
+
code: 'AMBIGUOUS_STEER_TARGET',
|
|
236
|
+
reason: `${String(targets.length)} plugins declare a sessionId (${targets.map(entry => entry.key).join(', ')}) — use set <key>=<value>`,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
return change(deps, only.key, sessionId, 'steer');
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Apply one change through the settings surface.
|
|
243
|
+
* @param deps - the handler's dependencies.
|
|
244
|
+
* @param key - the `<owner>.<field>` key.
|
|
245
|
+
* @param value - the value as text.
|
|
246
|
+
* @param verb - which verb asked, so the reply can be paired with it.
|
|
247
|
+
* @returns the change that landed, or the refusal with its reason.
|
|
248
|
+
*/
|
|
249
|
+
function change(deps, key, value, verb) {
|
|
250
|
+
const outcome = deps.settings.apply(key, value);
|
|
251
|
+
if (!outcome.ok) {
|
|
252
|
+
return { ok: false, verb, code: outcome.code, key: outcome.key, reason: outcome.reason };
|
|
253
|
+
}
|
|
254
|
+
return { ok: true, verb, key: outcome.key, value: outcome.value };
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Serialise one reply.
|
|
258
|
+
* @param reply - the reply to send.
|
|
259
|
+
* @returns the text frame.
|
|
260
|
+
*/
|
|
261
|
+
function encode(reply) {
|
|
262
|
+
return JSON.stringify(reply);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Name a failure without repeating it.
|
|
266
|
+
*
|
|
267
|
+
* A class, never a message: the message is where a provider puts the key it refused, and this module
|
|
268
|
+
* holds no credential to redact against.
|
|
269
|
+
* @param error - whatever was thrown.
|
|
270
|
+
* @returns the failure's class, or its type for a non-Error throw.
|
|
271
|
+
*/
|
|
272
|
+
function classOf(error) {
|
|
273
|
+
return error instanceof Error ? error.name : typeof error;
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Quote part of a frame back, redacted and bounded.
|
|
277
|
+
*
|
|
278
|
+
* Redacted because the echo travels to whoever sent the frame and into whatever they render it in; a
|
|
279
|
+
* caller that pasted a credential to the wrong verb should not have it handed back for display.
|
|
280
|
+
* @param text - the text to quote.
|
|
281
|
+
* @returns a short, safe quotation.
|
|
282
|
+
*/
|
|
283
|
+
function quote(text) {
|
|
284
|
+
return JSON.stringify(redact(text).slice(0, 40));
|
|
285
|
+
}
|
|
286
|
+
//# sourceMappingURL=control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"control.js","sourceRoot":"","sources":["../src/control.ts"],"names":[],"mappings":"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"}
|
package/lib/index.d.ts
CHANGED
|
@@ -26,9 +26,12 @@ export * from './types.ts';
|
|
|
26
26
|
* stays where it is.
|
|
27
27
|
*/
|
|
28
28
|
export { TOKEN_PARAM } from './injection.ts';
|
|
29
|
-
export { attachAudioSocket, toBytes, type AudioSocketBridgeDeps } from './bridge.ts';
|
|
29
|
+
export { attachAudioSocket, toBytes, toText, type AudioSocketBridgeDeps } from './bridge.ts';
|
|
30
|
+
export { createControlHandler, parseControlFrame, type ControlCommand, type ControlDeps, type ControlRefusedReply, type ControlRefusalCode, type ControlReply, type ControlSessionReply, type ControlSettingReply, type ControlStatusReply, type ControlVerb, } from './control.ts';
|
|
30
31
|
export { createUpgradeAcceptor, rejectUpgrade, type UpgradeAcceptor } from './upgrade.ts';
|
|
31
32
|
export { diagnosticsRoute, type DiagnosticsDeps, type DiagnosticsJournal, type DiagnosticsRoute } from './diagnostics.ts';
|
|
33
|
+
export { STRIP_ELEMENT_ID, stripBootstrap, stripMarkup, stripRows } from './strip.ts';
|
|
34
|
+
export type { InjectedGlobalRow, InjectedRow } from './injection.ts';
|
|
32
35
|
/** Plugin name. */
|
|
33
36
|
export declare const name = "realtime-audio-ws";
|
|
34
37
|
export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
@@ -37,12 +40,14 @@ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
|
|
|
37
40
|
maxConnections: Schema<number, number, "defined">;
|
|
38
41
|
openSessionOnConnect: Schema<boolean, boolean, "defined">;
|
|
39
42
|
diagnosticsPath: Schema<string, string, "defined">;
|
|
43
|
+
journalPath: Schema<string, string, "defined">;
|
|
40
44
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
41
45
|
path: Schema<string, string, "defined">;
|
|
42
46
|
maxFrameBytes: Schema<number, number, "defined">;
|
|
43
47
|
maxConnections: Schema<number, number, "defined">;
|
|
44
48
|
openSessionOnConnect: Schema<boolean, boolean, "defined">;
|
|
45
49
|
diagnosticsPath: Schema<string, string, "defined">;
|
|
50
|
+
journalPath: Schema<string, string, "defined">;
|
|
46
51
|
}>>, "plain">;
|
|
47
52
|
export declare function apply(ctx: Context, config: RealtimeAudioWsConfig): void;
|
|
48
53
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAkBlD,OAAO,EAOL,KAAK,qBAAqB,EAC3B,MAAM,YAAY,CAAA;AAEnB,cAAc,YAAY,CAAA;AAC1B;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,qBAAqB,EAAE,MAAM,aAAa,CAAA;AAC5F,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,WAAW,GACjB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AACzF,OAAO,EAAE,gBAAgB,EAAE,KAAK,eAAe,EAAE,KAAK,kBAAkB,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACzH,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACrF,YAAY,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAIpE,mBAAmB;AACnB,eAAO,MAAM,IAAI,sBAAsB,CAAA;AAEvC,eAAO,MAAM,MAAM;;;;;;;;;;;;;;aAajB,CAAA;AAqCF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,qBAAqB,GAAG,IAAI,CA4KvE"}
|
package/lib/index.js
CHANGED
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import Schema from '@deepseek-ai/schemastery';
|
|
18
18
|
import { attachAudioSocket } from './bridge.js';
|
|
19
|
+
import { createControlHandler } from './control.js';
|
|
19
20
|
import { diagnosticsRoute } from './diagnostics.js';
|
|
20
21
|
import { createRouteToken, routeAuthority, routeInjectionRow, tokenFromUrl, verdictFor, } from './injection.js';
|
|
22
|
+
import { stripRows } from './strip.js';
|
|
21
23
|
import { createUpgradeAcceptor, rejectUpgrade } from './upgrade.js';
|
|
22
24
|
import { DEFAULT_DIAGNOSTICS_PATH, DEFAULT_MAX_CONNECTIONS, DEFAULT_MAX_FRAME_BYTES, DEFAULT_OPEN_SESSION_ON_CONNECT, DEFAULT_PATH, } from './types.js';
|
|
23
25
|
export * from './types.js';
|
|
@@ -29,9 +31,12 @@ export * from './types.js';
|
|
|
29
31
|
* stays where it is.
|
|
30
32
|
*/
|
|
31
33
|
export { TOKEN_PARAM } from './injection.js';
|
|
32
|
-
export { attachAudioSocket, toBytes } from './bridge.js';
|
|
34
|
+
export { attachAudioSocket, toBytes, toText } from './bridge.js';
|
|
35
|
+
export { createControlHandler, parseControlFrame, } from './control.js';
|
|
33
36
|
export { createUpgradeAcceptor, rejectUpgrade } from './upgrade.js';
|
|
34
37
|
export { diagnosticsRoute } from './diagnostics.js';
|
|
38
|
+
export { STRIP_ELEMENT_ID, stripBootstrap, stripMarkup, stripRows } from './strip.js';
|
|
39
|
+
import { createJournalFileSink } from './journal-file.js';
|
|
35
40
|
/** Plugin name. */
|
|
36
41
|
export const name = 'realtime-audio-ws';
|
|
37
42
|
export const Config = Schema.object({
|
|
@@ -43,6 +48,10 @@ export const Config = Schema.object({
|
|
|
43
48
|
// microphone: a connection takes an explicit action, an authenticated one, and its absence is silence.
|
|
44
49
|
openSessionOnConnect: Schema.boolean().default(DEFAULT_OPEN_SESSION_ON_CONNECT),
|
|
45
50
|
diagnosticsPath: Schema.string().default(DEFAULT_DIAGNOSTICS_PATH),
|
|
51
|
+
// Empty means no file, and empty is the default: a plugin that writes files because it was installed is a
|
|
52
|
+
// plugin writing files in somebody else's directory. The deployment that wants a recording is the
|
|
53
|
+
// deployment that names a path.
|
|
54
|
+
journalPath: Schema.string().default(''),
|
|
46
55
|
});
|
|
47
56
|
export function apply(ctx, config) {
|
|
48
57
|
ctx.inject(['connection', 'webServer', 'realtime'], (injected) => {
|
|
@@ -51,21 +60,75 @@ export function apply(ctx, config) {
|
|
|
51
60
|
const clients = new Set();
|
|
52
61
|
const token = createRouteToken();
|
|
53
62
|
const journal = injected.realtime.journal;
|
|
63
|
+
// The one field the gate classifies as live for this route. Read at the moment each decision is
|
|
64
|
+
// made — at a connect, and at the last client leaving — rather than captured here, so turning it
|
|
65
|
+
// off does not need a restart and turning it on applies to the next connection.
|
|
66
|
+
const live = { openSessionOnConnect: config.openSessionOnConnect };
|
|
54
67
|
// The token travels in a URL query, and this route records request targets, so the journal has to be
|
|
55
68
|
// able to redact it before it records one. This is exactly why `addSecrets` is additive instead of a
|
|
56
69
|
// constructor argument: the plugin that mints the token is the only one that can name it, and it
|
|
57
70
|
// mints it here, after the journal already exists.
|
|
58
71
|
journal.addSecrets([token]);
|
|
72
|
+
// The record leaves the process when a path is named. Registered *after* the token, so that every entry
|
|
73
|
+
// the file receives has already been through the redaction that knows about it — a sink is handed
|
|
74
|
+
// entries after `record` has redacted them, and that ordering is the reason this is a sink rather than a
|
|
75
|
+
// second writer with its own idea of what a secret looks like.
|
|
76
|
+
if (config.journalPath.length > 0) {
|
|
77
|
+
journal.onEntry(createJournalFileSink(config.journalPath));
|
|
78
|
+
}
|
|
59
79
|
// The injection event is declared by `@deepseek-ai/dsh-host-webserver`, which this package deliberately
|
|
60
80
|
// does not import: importing it would augment `Context` with a second declaration of `webServer` and
|
|
61
81
|
// turn the structural dependency above into a compile error. The name is cast at this one boundary.
|
|
62
82
|
const onInjection = ctx.on;
|
|
83
|
+
/**
|
|
84
|
+
* The control channel: one text frame in, one text frame out, on the socket that carries audio.
|
|
85
|
+
*
|
|
86
|
+
* Every edge it needs is injected rather than reached for, and the two session requests go through the
|
|
87
|
+
* same events the transport already emits — one start path, not a second one beside it. The difference
|
|
88
|
+
* is that this caller *waits*: `serial` returns the agent's outcome, so `start` can answer with the
|
|
89
|
+
* session that opened rather than with an acknowledgement that it asked.
|
|
90
|
+
*/
|
|
91
|
+
const control = createControlHandler({
|
|
92
|
+
settings: injected.realtime.settings,
|
|
93
|
+
journal,
|
|
94
|
+
path: config.path,
|
|
95
|
+
clients: () => clients.size,
|
|
96
|
+
voice: () => ctx.serial('realtime-agent/status'),
|
|
97
|
+
request: (verb) => verb === 'start'
|
|
98
|
+
? ctx.serial('realtime-agent/start')
|
|
99
|
+
: ctx.serial('realtime-agent/stop'),
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* The tail of the control-reply queue.
|
|
103
|
+
*
|
|
104
|
+
* The channel answers in **arrival order**, which is a property of this queue rather than of the socket:
|
|
105
|
+
* a handler is asynchronous, so without it a slow `status` could be overtaken by the `set` behind it and
|
|
106
|
+
* a client pairing replies with frames would be guessing. One handler at a time costs a control frame
|
|
107
|
+
* the latency of the one before it, which is the right trade for a channel a person drives by hand.
|
|
108
|
+
*/
|
|
109
|
+
let replies = Promise.resolve();
|
|
110
|
+
ctx.effect(function* () {
|
|
111
|
+
const release = injected.realtime.settings.register(name, [
|
|
112
|
+
{
|
|
113
|
+
field: 'openSessionOnConnect',
|
|
114
|
+
kind: 'boolean',
|
|
115
|
+
scope: 'live',
|
|
116
|
+
describe: 'Ask the agent to open the session when an authenticated client connects',
|
|
117
|
+
get: () => live.openSessionOnConnect,
|
|
118
|
+
set: (value) => { live.openSessionOnConnect = value; },
|
|
119
|
+
},
|
|
120
|
+
]);
|
|
121
|
+
yield () => { release(); };
|
|
122
|
+
}, 'realtime-audio-ws.settings');
|
|
63
123
|
ctx.effect(() => {
|
|
64
124
|
// The page is told where the route is and what to present. The web server gathers this table on every
|
|
65
125
|
// index render and every worker boot-payload request, so the row is built at emit time — the only
|
|
66
126
|
// moment the listening port is known for certain, since an index render follows the listen.
|
|
67
127
|
onInjection('webserver/index-inject', (table) => {
|
|
68
128
|
table.push(routeInjectionRow(config.path, routeAuthority(services.webServer.config?.host, services.webServer.listenedPort), token));
|
|
129
|
+
// The panel's own two rows, after the settings it reads. Markup first, then the bootstrap that
|
|
130
|
+
// mounts the panel on it — the table's own ordering guarantee is what makes that safe.
|
|
131
|
+
table.push(...stripRows());
|
|
69
132
|
});
|
|
70
133
|
const unregisterDiagnostics = services.webServer.register(diagnosticsRoute({
|
|
71
134
|
path: config.diagnosticsPath,
|
|
@@ -116,18 +179,24 @@ export function apply(ctx, config) {
|
|
|
116
179
|
journal.record('socket.accepted', { clients: String(clients.size), url: target });
|
|
117
180
|
// Ask for the session before the bridge goes in, so the first frames are written into a session
|
|
118
181
|
// that is being opened rather than dropped by the mic seam's no-session rule.
|
|
119
|
-
if (
|
|
182
|
+
if (live.openSessionOnConnect)
|
|
120
183
|
ctx.emit('realtime-agent/start');
|
|
121
184
|
attachAudioSocket(client, {
|
|
122
185
|
emitMic: (pcm16) => { ctx.emit('realtime-agent/mic', pcm16); },
|
|
123
186
|
subscribeAudio: (listener) => ctx.on('realtime-agent/audio', listener),
|
|
124
187
|
maxFrameBytes: config.maxFrameBytes,
|
|
188
|
+
// The handler is total — every frame produces exactly one reply, and it refuses rather than
|
|
189
|
+
// rejecting — so there is nothing here to catch and nothing here may invent a second reply.
|
|
190
|
+
// `reply` drops the frame if the socket has gone in the meantime, and is total itself.
|
|
191
|
+
onControl: (frame, reply) => {
|
|
192
|
+
replies = replies.then(() => control(frame)).then(reply);
|
|
193
|
+
},
|
|
125
194
|
onDetach: () => {
|
|
126
195
|
clients.delete(client);
|
|
127
196
|
journal.record('socket.closed', { clients: String(clients.size) });
|
|
128
197
|
// The last client leaving ends the session. This also covers the transport's own disposal,
|
|
129
198
|
// which terminates its clients — so a profile reload does not leave a session nobody holds.
|
|
130
|
-
if (
|
|
199
|
+
if (live.openSessionOnConnect && clients.size === 0)
|
|
131
200
|
ctx.emit('realtime-agent/stop');
|
|
132
201
|
},
|
|
133
202
|
});
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAM7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,UAAU,GAEX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AACnE,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,+BAA+B,EAC/B,YAAY,GAGb,MAAM,YAAY,CAAA;AAEnB,cAAc,YAAY,CAAA;AAC1B;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAA8B,MAAM,aAAa,CAAA;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAM7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAC/C,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAA;AACnD,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,YAAY,EACZ,UAAU,GAEX,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACtC,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AACnE,OAAO,EACL,wBAAwB,EACxB,uBAAuB,EACvB,uBAAuB,EACvB,+BAA+B,EAC/B,YAAY,GAGb,MAAM,YAAY,CAAA;AAEnB,cAAc,YAAY,CAAA;AAC1B;;;;;;GAMG;AACH,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAC5C,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,MAAM,EAA8B,MAAM,aAAa,CAAA;AAC5F,OAAO,EACL,oBAAoB,EACpB,iBAAiB,GAUlB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,qBAAqB,EAAE,aAAa,EAAwB,MAAM,cAAc,CAAA;AACzF,OAAO,EAAE,gBAAgB,EAAwE,MAAM,kBAAkB,CAAA;AACzH,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AAGrF,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAA;AAEzD,mBAAmB;AACnB,MAAM,CAAC,MAAM,IAAI,GAAG,mBAAmB,CAAA;AAEvC,MAAM,CAAC,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAClC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC;IAC3C,aAAa,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,uBAAuB,CAAC;IAChE,cAAc,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,uBAAuB,CAAC;IACjE,uGAAuG;IACvG,0GAA0G;IAC1G,uGAAuG;IACvG,oBAAoB,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,+BAA+B,CAAC;IAC/E,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,wBAAwB,CAAC;IAClE,0GAA0G;IAC1G,kGAAkG;IAClG,gCAAgC;IAChC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;CACzC,CAAC,CAAA;AAqCF,MAAM,UAAU,KAAK,CAAC,GAAY,EAAE,MAA6B;IAC/D,GAAG,CAAC,MAAM,CAAC,CAAC,YAAY,EAAE,WAAW,EAAE,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE;QAC/D,MAAM,QAAQ,GAAG,QAA+E,CAAA;QAChG,MAAM,QAAQ,GAAG,qBAAqB,CAAC,MAAM,CAAC,aAAa,CAAC,CAAA;QAC5D,MAAM,OAAO,GAAG,IAAI,GAAG,EAAe,CAAA;QACtC,MAAM,KAAK,GAAG,gBAAgB,EAAE,CAAA;QAChC,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAA;QACzC,gGAAgG;QAChG,iGAAiG;QACjG,gFAAgF;QAChF,MAAM,IAAI,GAAG,EAAE,oBAAoB,EAAE,MAAM,CAAC,oBAAoB,EAAE,CAAA;QAClE,qGAAqG;QACrG,qGAAqG;QACrG,iGAAiG;QACjG,mDAAmD;QACnD,OAAO,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,CAAC,CAAA;QAC3B,wGAAwG;QACxG,kGAAkG;QAClG,yGAAyG;QACzG,+DAA+D;QAC/D,IAAI,MAAM,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClC,OAAO,CAAC,OAAO,CAAC,qBAAqB,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAA;QAC5D,CAAC;QACD,wGAAwG;QACxG,qGAAqG;QACrG,oGAAoG;QACpG,MAAM,WAAW,GAAG,GAAG,CAAC,EAGZ,CAAA;QAEZ;;;;;;;WAOG;QACH,MAAM,OAAO,GAAG,oBAAoB,CAAC;YACnC,QAAQ,EAAE,QAAQ,CAAC,QAAQ,CAAC,QAAQ;YACpC,OAAO;YACP,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI;YAC3B,KAAK,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,uBAAuB,CAAC;YAChD,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,OAAO;gBACjC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,sBAAsB,CAAC;gBACpC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,qBAAqB,CAAC;SACtC,CAAC,CAAA;QAEF;;;;;;;WAOG;QACH,IAAI,OAAO,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAA;QAEjD,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC;YAClB,MAAM,OAAO,GAAG,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,EAAE;gBACxD;oBACE,KAAK,EAAE,sBAAsB;oBAC7B,IAAI,EAAE,SAAS;oBACf,KAAK,EAAE,MAAM;oBACb,QAAQ,EAAE,yEAAyE;oBACnF,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,oBAAoB;oBACpC,GAAG,EAAE,CAAC,KAAc,EAAE,EAAE,GAAG,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAA,CAAC,CAAC;iBAC/D;aACF,CAAC,CAAA;YACF,MAAM,GAAG,EAAE,GAAG,OAAO,EAAE,CAAA,CAAC,CAAC,CAAA;QAC3B,CAAC,EAAE,4BAA4B,CAAC,CAAA;QAEhC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE;YACd,sGAAsG;YACtG,kGAAkG;YAClG,4FAA4F;YAC5F,WAAW,CAAC,wBAAwB,EAAE,CAAC,KAAK,EAAE,EAAE;gBAC9C,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAC1B,MAAM,CAAC,IAAI,EACX,cAAc,CAAC,QAAQ,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,SAAS,CAAC,YAAY,CAAC,EAChF,KAAK,CACN,CAAC,CAAA;gBACF,+FAA+F;gBAC/F,uFAAuF;gBACvF,KAAK,CAAC,IAAI,CAAC,GAAG,SAAS,EAAE,CAAC,CAAA;YAC5B,CAAC,CAAC,CAAA;YACF,MAAM,qBAAqB,GAAG,QAAQ,CAAC,SAAS,CAAC,QAAQ,CAAC,gBAAgB,CAAC;gBACzE,IAAI,EAAE,MAAM,CAAC,eAAe;gBAC5B,KAAK;gBACL,YAAY,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,gBAAgB,CAAC,OAAO,CAAC;gBACxE,OAAO;aACR,CAAC,CAAC,CAAA;YACH,MAAM,UAAU,GAAG,QAAQ,CAAC,SAAS,CAAC,eAAe,CAAC;gBACpD,IAAI,EAAE,MAAM,CAAC,IAAI;gBACjB,OAAO,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE;oBAC7B,iGAAiG;oBACjG,4FAA4F;oBAC5F,2FAA2F;oBAC3F,iGAAiG;oBACjG,0CAA0C;oBAC1C,EAAE;oBACF,iGAAiG;oBACjG,qFAAqF;oBACrF,kGAAkG;oBAClG,0CAA0C;oBAC1C,2FAA2F;oBAC3F,gGAAgG;oBAChG,iCAAiC;oBACjC,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,IAAI,EAAE,CAAA;oBAC5B,MAAM,SAAS,GAAG,UAAU,CAC1B,QAAQ,CAAC,UAAU,CAAC,gBAAgB,CAAC,GAAG,CAAC,EACzC,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,EACrB,KAAK,CACN,CAAA;oBACD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;wBAC5B,sFAAsF;wBACtF,wFAAwF;wBACxF,6FAA6F;wBAC7F,qCAAqC;wBACrC,OAAO,CAAC,MAAM,CAAC,iBAAiB,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC,CAAA;wBACjE,aAAa,CAAC,MAAM,EAAE,SAAS,CAAC,CAAA;wBAChC,OAAM;oBACR,CAAC;oBACD,QAAQ,CAAC,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE;wBACnD,IAAI,OAAO,CAAC,IAAI,IAAI,MAAM,CAAC,cAAc,EAAE,CAAC;4BAC1C,oFAAoF;4BACpF,OAAO,CAAC,MAAM,CAAC,iBAAiB,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;4BACtE,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;4BAC1B,OAAM;wBACR,CAAC;wBACD,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;wBACnB,yFAAyF;wBACzF,uFAAuF;wBACvF,+FAA+F;wBAC/F,0FAA0F;wBAC1F,4DAA4D;wBAC5D,OAAO,CAAC,MAAM,CAAC,iBAAiB,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,CAAA;wBACjF,gGAAgG;wBAChG,8EAA8E;wBAC9E,IAAI,IAAI,CAAC,oBAAoB;4BAAE,GAAG,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAA;wBAC/D,iBAAiB,CAAC,MAAM,EAAE;4BACxB,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,oBAAoB,EAAE,KAAK,CAAC,CAAA,CAAC,CAAC;4BAC7D,cAAc,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,sBAAsB,EAAE,QAAQ,CAAC;4BACtE,aAAa,EAAE,MAAM,CAAC,aAAa;4BACnC,4FAA4F;4BAC5F,4FAA4F;4BAC5F,uFAAuF;4BACvF,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;gCAC1B,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;4BAC1D,CAAC;4BACD,QAAQ,EAAE,GAAG,EAAE;gCACb,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;gCACtB,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;gCAClE,2FAA2F;gCAC3F,4FAA4F;gCAC5F,IAAI,IAAI,CAAC,oBAAoB,IAAI,OAAO,CAAC,IAAI,KAAK,CAAC;oCAAE,GAAG,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAA;4BACtF,CAAC;yBACF,CAAC,CAAA;oBACJ,CAAC,CAAC,CAAA;gBACJ,CAAC;aACF,CAAC,CAAA;YACF,OAAO,KAAK,IAAI,EAAE;gBAChB,UAAU,EAAE,CAAA;gBACZ,qBAAqB,EAAE,CAAA;gBACvB,KAAK,MAAM,MAAM,IAAI,OAAO;oBAAE,MAAM,CAAC,SAAS,EAAE,CAAA;gBAChD,OAAO,CAAC,KAAK,EAAE,CAAA;gBACf,MAAM,QAAQ,CAAC,KAAK,EAAE,CAAA;YACxB,CAAC,CAAA;QACH,CAAC,EAAE,sBAAsB,MAAM,CAAC,IAAI,EAAE,CAAC,CAAA;IACzC,CAAC,CAAC,CAAA;AACJ,CAAC"}
|
package/lib/injection.d.ts
CHANGED
|
@@ -30,6 +30,24 @@ export interface InjectedGlobalRow {
|
|
|
30
30
|
readonly name: string;
|
|
31
31
|
readonly value: unknown;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Any row this package contributes.
|
|
35
|
+
*
|
|
36
|
+
* The host's table takes six kinds; this package uses three, and describes only those — `html` for the
|
|
37
|
+
* panel's markup and `script` for the bootstrap that mounts it, beside the `global` row carrying the
|
|
38
|
+
* route's own settings. The payload contract is the host's and is stated where it is enforced: a `script`
|
|
39
|
+
* row's text or a `style` row's text must not contain the closing tag of its own element, because the row
|
|
40
|
+
* is spliced into the document's text.
|
|
41
|
+
*/
|
|
42
|
+
export type InjectedRow = InjectedGlobalRow | {
|
|
43
|
+
readonly kind: 'html';
|
|
44
|
+
readonly placement: 'head' | 'body';
|
|
45
|
+
readonly html: string;
|
|
46
|
+
} | {
|
|
47
|
+
readonly kind: 'script';
|
|
48
|
+
readonly placement: 'head' | 'body';
|
|
49
|
+
readonly text: string;
|
|
50
|
+
};
|
|
33
51
|
/**
|
|
34
52
|
* Where the client should open its socket, or undefined when the host is not listening yet.
|
|
35
53
|
*
|
package/lib/injection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"injection.d.ts","sourceRoot":"","sources":["../src/injection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,4GAA4G;AAC5G,eAAO,MAAM,YAAY,2BAA2B,CAAA;AAEpD,2EAA2E;AAC3E,eAAO,MAAM,WAAW,MAAM,CAAA;AAE9B,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CACxB;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIrG;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMxE;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMpF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CACxB,SAAS,EAAE,GAAG,GAAG,GAAG,GAAG,SAAS,EAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,QAAQ,EAAE,MAAM,GACf,GAAG,GAAG,GAAG,GAAG,SAAS,CAGvB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,KAAK,EAAE,MAAM,GACZ,iBAAiB,CAEnB"}
|
|
1
|
+
{"version":3,"file":"injection.d.ts","sourceRoot":"","sources":["../src/injection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,4GAA4G;AAC5G,eAAO,MAAM,YAAY,2BAA2B,CAAA;AAEpD,2EAA2E;AAC3E,eAAO,MAAM,WAAW,MAAM,CAAA;AAE9B,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAA;CACxB;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GACnB,iBAAiB,GACjB;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GACrF;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAA;AAE3F;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAIrG;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAEzC;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMxE;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAMpF;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CACxB,SAAS,EAAE,GAAG,GAAG,GAAG,GAAG,SAAS,EAChC,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,QAAQ,EAAE,MAAM,GACf,GAAG,GAAG,GAAG,GAAG,SAAS,CAGvB;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,KAAK,EAAE,MAAM,GACZ,iBAAiB,CAEnB"}
|
package/lib/injection.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"injection.js","sourceRoot":"","sources":["../src/injection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE1D,4GAA4G;AAC5G,MAAM,CAAC,MAAM,YAAY,GAAG,wBAAwB,CAAA;AAEpD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,CAAA;
|
|
1
|
+
{"version":3,"file":"injection.js","sourceRoot":"","sources":["../src/injection.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAE1D,4GAA4G;AAC5G,MAAM,CAAC,MAAM,YAAY,GAAG,wBAAwB,CAAA;AAEpD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,CAAA;AAuB9B;;;;;;;;;;GAUG;AACH,MAAM,UAAU,cAAc,CAAC,IAAwB,EAAE,IAAwB;IAC/E,IAAI,IAAI,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACxC,MAAM,IAAI,GAAG,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAA;IAC1G,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAA;AACrF,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB;IAC9B,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAA;AAC9C,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,GAAuB;IAClD,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IACvC,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAA;IAC9B,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,SAAS,CAAA;IAC/B,MAAM,KAAK,GAAG,IAAI,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;IACxE,OAAO,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAA;AAC3D,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,QAA4B,EAAE,QAAgB;IACzE,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,KAAK,CAAA;IACxC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACrC,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACjC,IAAI,OAAO,CAAC,MAAM,KAAK,GAAG,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IAC/C,OAAO,eAAe,CAAC,OAAO,EAAE,GAAG,CAAC,CAAA;AACtC,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,UAAU,CACxB,SAAgC,EAChC,QAA4B,EAC5B,QAAgB;IAEhB,IAAI,SAAS,KAAK,SAAS;QAAE,OAAO,SAAS,CAAA;IAC7C,OAAO,YAAY,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;AACjE,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAC/B,IAAY,EACZ,SAA6B,EAC7B,KAAa;IAEb,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,CAAA;AAClF,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A journal sink that appends to a file, so the record outlives the process that made it.
|
|
3
|
+
*
|
|
4
|
+
* The journal has always *held* everything worth knowing and has always been unable to hand it over: it is
|
|
5
|
+
* served over a route that wants a per-boot capability token, which means the record can be read by the page
|
|
6
|
+
* that holds the token and by nobody else, and it cannot be read after the fact at all. Every failure this
|
|
7
|
+
* plugin has had was diagnosed by reading somebody else's log — a harness session file off disk, or a
|
|
8
|
+
* `curl` at a route that answered 401. This is the missing half: the same entries, on disk, readable with
|
|
9
|
+
* `cat` by whoever is handed the file.
|
|
10
|
+
*
|
|
11
|
+
* ## Why `appendFileSync`, deliberately
|
|
12
|
+
*
|
|
13
|
+
* The ordering of a diagnostic record is the record. An asynchronous append can interleave two entries or
|
|
14
|
+
* lose one to a process that exits during the write, and the failure this exists to explain is usually the
|
|
15
|
+
* last thing that happened. So each line is written synchronously, as one line, and a reader can trust that
|
|
16
|
+
* the file is ordered and complete up to the last entry it contains.
|
|
17
|
+
*
|
|
18
|
+
* ## Redaction already happened
|
|
19
|
+
*
|
|
20
|
+
* A sink is handed entries *after* `Journal.record` has redacted them, so the file carries exactly what the
|
|
21
|
+
* route would have served and never the values named in `redactSecrets`. That ordering is the whole reason
|
|
22
|
+
* this is a sink and not a second writer with its own copy of the redaction rules — two code paths that must
|
|
23
|
+
* agree about what a secret looks like are two code paths that eventually disagree.
|
|
24
|
+
*
|
|
25
|
+
* @module dsh-realtime-audio-ws/journal-file
|
|
26
|
+
*/
|
|
27
|
+
import type { JournalEntry } from 'dsh-realtime';
|
|
28
|
+
/**
|
|
29
|
+
* Build a sink that appends each entry to `path` as one JSON object per line.
|
|
30
|
+
*
|
|
31
|
+
* The parent directory is created here, at construction, rather than on the first write. That is the one
|
|
32
|
+
* failure this can have that is worth being loud about — a path that cannot be written — and at construction
|
|
33
|
+
* it is a startup error a caller can refuse to launch on, instead of a silent nothing that looks exactly
|
|
34
|
+
* like a quiet session. A *later* write that fails is swallowed by `Journal.onEntry`'s guard, by design.
|
|
35
|
+
* @param path - absolute or relative file path; the file is appended to, never truncated.
|
|
36
|
+
* @returns the sink, ready for `Journal.onEntry`.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createJournalFileSink(path: string): (entry: JournalEntry) => void;
|
|
39
|
+
//# sourceMappingURL=journal-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"journal-file.d.ts","sourceRoot":"","sources":["../src/journal-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAIH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAKjF"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A journal sink that appends to a file, so the record outlives the process that made it.
|
|
3
|
+
*
|
|
4
|
+
* The journal has always *held* everything worth knowing and has always been unable to hand it over: it is
|
|
5
|
+
* served over a route that wants a per-boot capability token, which means the record can be read by the page
|
|
6
|
+
* that holds the token and by nobody else, and it cannot be read after the fact at all. Every failure this
|
|
7
|
+
* plugin has had was diagnosed by reading somebody else's log — a harness session file off disk, or a
|
|
8
|
+
* `curl` at a route that answered 401. This is the missing half: the same entries, on disk, readable with
|
|
9
|
+
* `cat` by whoever is handed the file.
|
|
10
|
+
*
|
|
11
|
+
* ## Why `appendFileSync`, deliberately
|
|
12
|
+
*
|
|
13
|
+
* The ordering of a diagnostic record is the record. An asynchronous append can interleave two entries or
|
|
14
|
+
* lose one to a process that exits during the write, and the failure this exists to explain is usually the
|
|
15
|
+
* last thing that happened. So each line is written synchronously, as one line, and a reader can trust that
|
|
16
|
+
* the file is ordered and complete up to the last entry it contains.
|
|
17
|
+
*
|
|
18
|
+
* ## Redaction already happened
|
|
19
|
+
*
|
|
20
|
+
* A sink is handed entries *after* `Journal.record` has redacted them, so the file carries exactly what the
|
|
21
|
+
* route would have served and never the values named in `redactSecrets`. That ordering is the whole reason
|
|
22
|
+
* this is a sink and not a second writer with its own copy of the redaction rules — two code paths that must
|
|
23
|
+
* agree about what a secret looks like are two code paths that eventually disagree.
|
|
24
|
+
*
|
|
25
|
+
* @module dsh-realtime-audio-ws/journal-file
|
|
26
|
+
*/
|
|
27
|
+
import { appendFileSync, mkdirSync } from 'node:fs';
|
|
28
|
+
import { dirname } from 'node:path';
|
|
29
|
+
/**
|
|
30
|
+
* Build a sink that appends each entry to `path` as one JSON object per line.
|
|
31
|
+
*
|
|
32
|
+
* The parent directory is created here, at construction, rather than on the first write. That is the one
|
|
33
|
+
* failure this can have that is worth being loud about — a path that cannot be written — and at construction
|
|
34
|
+
* it is a startup error a caller can refuse to launch on, instead of a silent nothing that looks exactly
|
|
35
|
+
* like a quiet session. A *later* write that fails is swallowed by `Journal.onEntry`'s guard, by design.
|
|
36
|
+
* @param path - absolute or relative file path; the file is appended to, never truncated.
|
|
37
|
+
* @returns the sink, ready for `Journal.onEntry`.
|
|
38
|
+
*/
|
|
39
|
+
export function createJournalFileSink(path) {
|
|
40
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
41
|
+
return (entry) => {
|
|
42
|
+
appendFileSync(path, `${JSON.stringify(entry)}\n`);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=journal-file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"journal-file.js","sourceRoot":"","sources":["../src/journal-file.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAGnC;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CAAC,IAAY;IAChD,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAA;IAC7C,OAAO,CAAC,KAAmB,EAAQ,EAAE;QACnC,cAAc,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACpD,CAAC,CAAA;AACH,CAAC"}
|