@thestreamcode/dsh-muse-code 0.1.0 → 0.2.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/lib/index.d.ts CHANGED
@@ -1,31 +1,176 @@
1
- type CredentialStore = {
2
- set(reference: string, value: string): Promise<void>;
3
- modifyRecord(key: string, update: () => Promise<unknown>): Promise<unknown>;
4
- };
5
- type AuthorizationSession = {
6
- signal?: AbortSignal;
7
- notify(input: {
8
- message: string;
9
- url?: string;
10
- }): void;
11
- };
12
- type AuthorizationFlow = {
1
+ import type { IncomingMessage, ServerResponse } from "node:http";
2
+ export declare const name = "dsh-muse-code";
3
+ export declare const inject: string[];
4
+ /** Web route the Settings control calls to start the sign-in. */
5
+ export declare const LOGIN_ROUTE = "/muse-code/login";
6
+ type Logger = {
7
+ info?(message: string): void;
8
+ warn?(message: string, ...rest: unknown[]): void;
9
+ debug?(message: string): void;
10
+ };
11
+ type CommandResult = {
12
+ kind: "success";
13
+ text?: string;
14
+ } | {
15
+ kind: "error";
16
+ text: string;
17
+ };
18
+ type AuthorizationMethods = Array<{
19
+ id: string;
20
+ label: string;
21
+ }>;
22
+ type AuthorizationEntry = {
13
23
  key: string;
14
24
  label: string;
15
- methods: Array<{
16
- id: string;
17
- label: string;
25
+ methods: AuthorizationMethods;
26
+ inFlight: boolean;
27
+ };
28
+ /**
29
+ * One notice from the authorization seam. The seam's vocabulary is
30
+ * `{ message, url?, code? }`; the raw pi-ai fields are accepted too so the
31
+ * reader also covers a direct adapter notice.
32
+ */
33
+ type AuthorizationNotice = {
34
+ type?: string;
35
+ message?: string;
36
+ instructions?: string;
37
+ url?: string;
38
+ code?: string;
39
+ userCode?: string;
40
+ verificationUri?: string;
41
+ expiresInSeconds?: number;
42
+ };
43
+ type AuthorizationInteraction = {
44
+ notify(notice: AuthorizationNotice): void;
45
+ prompt(prompt: unknown): Promise<unknown>;
46
+ };
47
+ type AuthorizationFace = {
48
+ describe(key: string): AuthorizationEntry | undefined;
49
+ begin(request: {
50
+ key: string;
51
+ method?: string;
52
+ interaction: AuthorizationInteraction;
53
+ }): Promise<{
54
+ status: string;
18
55
  }>;
19
- run(session: AuthorizationSession): Promise<void>;
56
+ cancel?(key: string): void;
57
+ };
58
+ type CommandFace = {
59
+ register(definition: {
60
+ name: string;
61
+ description: string;
62
+ handler(context: {
63
+ rawInput?: string;
64
+ }): Promise<CommandResult>;
65
+ }): () => void;
66
+ };
67
+ type SettingsRow = {
68
+ ns: string;
69
+ revision: number;
70
+ value: Record<string, unknown>;
71
+ };
72
+ type SettingsFace = {
73
+ describe(): SettingsRow[];
74
+ update(ns: string, patch: Record<string, unknown>, expectedRevision: number): Promise<void>;
75
+ };
76
+ type WebRoute = {
77
+ kind: "exact" | "prefix";
78
+ path: string;
79
+ handler(request: IncomingMessage, response: ServerResponse): Promise<void>;
80
+ };
81
+ type WebServerFace = {
82
+ register(route: WebRoute): () => void;
83
+ };
84
+ type ChildContext = HostContext & {
85
+ settings?: SettingsFace;
86
+ webServer?: WebServerFace;
20
87
  };
21
88
  type HostContext = {
22
- credentials: CredentialStore;
23
- authorization: {
24
- registerFlow(flow: AuthorizationFlow): () => void;
25
- };
26
- effect?(callback: () => (() => void) | void): () => void;
89
+ authorization: AuthorizationFace;
90
+ commands: CommandFace;
91
+ logger?: Logger;
92
+ effect(callback: () => () => void, label?: string): () => void;
93
+ inject?(dependencies: string[], callback: (child: ChildContext) => void): void;
27
94
  };
28
- export declare const name = "dsh-muse-code";
29
- export declare const inject: string[];
95
+ /** Injected side effects, overridable in tests. */
96
+ export type SignInOptions = {
97
+ openBrowser?: (url: string) => boolean;
98
+ timeoutMs?: number;
99
+ };
100
+ /** Actionable instructions extracted from one authorization notice. */
101
+ export type SignInInstruction = {
102
+ /** Verification page as the flow reported it. */
103
+ url: string;
104
+ /** Verification page with the user code attached when the URL accepts it. */
105
+ completeUrl: string;
106
+ code?: string;
107
+ message?: string;
108
+ };
109
+ type SignInOutcome = {
110
+ ok: true;
111
+ text: string;
112
+ url: string;
113
+ code?: string;
114
+ } | {
115
+ ok: false;
116
+ reason: "unavailable" | "busy" | "timeout" | "failed" | "ended";
117
+ text: string;
118
+ };
119
+ /**
120
+ * Register the sign-in command, the Settings > Models sign-in route, and the
121
+ * settings write that makes the Muse Code provider visible.
122
+ * @param ctx - Host plugin context.
123
+ */
30
124
  export declare function apply(ctx: HostContext): void;
125
+ /** Command definition for the sign-in; exported for tests. */
126
+ export declare function museLoginCommand(ctx: HostContext, options?: SignInOptions): Parameters<CommandFace["register"]>[0];
127
+ /**
128
+ * Build the exact Web route that starts the same sign-in from the Settings
129
+ * page. The route answers JSON so the Client control can render the code and
130
+ * the failure reason next to its button.
131
+ * @param ctx - Host plugin context.
132
+ * @param options - injected side effects, overridable in tests.
133
+ * @returns an HTTP handler for {@link LOGIN_ROUTE}.
134
+ */
135
+ export declare function createLoginRouteHandler(ctx: HostContext, options?: SignInOptions): (request: IncomingMessage, response: ServerResponse) => Promise<void>;
136
+ /**
137
+ * Start one sign-in attempt and wait for the actionable device-code notice.
138
+ * The attempt itself keeps running after the notice, so Meta's approval is
139
+ * picked up and the credential is stored by the seam.
140
+ * @param ctx - Host plugin context.
141
+ * @param options - injected side effects, overridable in tests.
142
+ * @returns the user-facing outcome.
143
+ */
144
+ export declare function startSignIn(ctx: HostContext, options?: SignInOptions): Promise<SignInOutcome>;
145
+ /**
146
+ * Read the actionable instruction out of one authorization notice.
147
+ * @param notice - a seam notice (`message`/`url`/`code`) or a raw adapter one.
148
+ * @returns the instruction, or undefined for progress notices with no page.
149
+ */
150
+ export declare function readSignInNotice(notice: AuthorizationNotice): SignInInstruction | undefined;
151
+ /**
152
+ * Render the instruction text shared by the command result and the Settings
153
+ * control.
154
+ * @param instruction - actionable notice.
155
+ * @param opened - whether the browser hand-off was started.
156
+ * @returns one line the user can act on.
157
+ */
158
+ export declare function formatSignInText(instruction: SignInInstruction, opened: boolean): string;
159
+ /**
160
+ * Declare the shipped `meta` route in the `llm-pi-ai` settings section so the
161
+ * provider appears in Settings > Models and its models in the picker.
162
+ *
163
+ * The write is a merge into the entry's editable config, exactly what the
164
+ * Models page does when a user adds this provider by hand; an already declared
165
+ * `meta` profile is never touched.
166
+ *
167
+ * @param settings - the Settings form service.
168
+ * @param logger - optional plugin logger.
169
+ * @param options - retry window for late-activating profile entries.
170
+ * @returns whether this call added the route.
171
+ */
172
+ export declare function ensureMuseRoute(settings: SettingsFace, logger?: Logger, options?: {
173
+ attempts?: number;
174
+ delayMs?: number;
175
+ }): Promise<boolean>;
31
176
  export {};
package/lib/index.js CHANGED
@@ -1,40 +1,387 @@
1
- // DeepSeek Harness bundle: Muse Code subscription route + login flow.
1
+ // Muse Code (Meta subscription) integration for DeepSeek Harness.
2
2
  //
3
- // The route is declared by cordis.patch.yml through a second pi-ai adapter
4
- // instance. This plugin owns the human sign-in: it runs Meta's device flow,
5
- // mints the account-bound inference key, stores it under the
6
- // MUSE_CODE_SUB_TOKEN credential reference, and commits the authorization
7
- // record the seam requires.
8
- import { deviceAuthorize, mintKey, pollToken } from "./auth.js";
3
+ // DeepSeek Harness already serves this subscription: the shipped pi-ai catalog
4
+ // exposes the `meta` route (api.meta.ai/v1) and registers the `llm-pi-ai/meta`
5
+ // authorization flow, which runs Meta's device-code login and stores the
6
+ // renewable credential record. The Web/Desktop Settings surface has no generic
7
+ // button for provider OAuth flows, so this bundle adds the pieces a user needs:
8
+ //
9
+ // - it declares the `meta` route in the `llm-pi-ai` settings section, so the
10
+ // provider appears under Settings > Models and its models in the model picker;
11
+ // - it exposes the shipped sign-in as the `/muse-login` command;
12
+ // - it registers an exact Web route (`/muse-code/login`) and a Sign in control
13
+ // under Settings > Models that calls it, because the Settings page cannot
14
+ // reach the Host authorization seam through the Remote API.
15
+ //
16
+ // The Host opens the verification page in the user's default browser itself; the
17
+ // route and the command also return the code and the URL, so a blocked browser
18
+ // never strands the sign-in.
19
+ //
20
+ // A second `@deepseek-ai/dsh-llm-pi-ai` instance is not an option: every
21
+ // instance declares the whole installed provider catalog, so the extra instance
22
+ // fails to activate with a duplicate configurable-provider error.
23
+ import { openExternal } from "./open-browser.js";
9
24
  export const name = "dsh-muse-code";
10
- export const inject = ["credentials", "authorization"];
11
- const FLOW_KEY = "dsh-muse-code/subscription";
12
- const CREDENTIAL_REF = "MUSE_CODE_SUB_TOKEN";
25
+ export const inject = ["authorization", "commands"];
26
+ /** Settings namespace whose provider profiles configure the pi-ai routes. */
27
+ const SETTINGS_NS = "llm-pi-ai";
28
+ /** Route served by the shipped pi-ai Meta provider. */
29
+ const PROVIDER = "meta";
30
+ /** Credential record / authorization flow the shipped pi-ai adapter registers. */
31
+ const AUTH_KEY = "llm-pi-ai/meta";
32
+ /** Preferred sign-in method; the shipped flow offers `oauth` first. */
33
+ const AUTH_METHOD = "oauth";
34
+ /** Display name for the route this bundle declares. */
35
+ const ROUTE_LABEL = "Muse Code (Meta subscription)";
36
+ /** Web route the Settings control calls to start the sign-in. */
37
+ export const LOGIN_ROUTE = "/muse-code/login";
38
+ /** How long the sign-in waits for the device-code notice before giving up. */
39
+ const NOTICE_TIMEOUT_MS = 20_000;
40
+ /**
41
+ * Register the sign-in command, the Settings > Models sign-in route, and the
42
+ * settings write that makes the Muse Code provider visible.
43
+ * @param ctx - Host plugin context.
44
+ */
13
45
  export function apply(ctx) {
14
- const flow = {
15
- key: FLOW_KEY,
16
- label: "Muse Code (subscription)",
17
- methods: [{ id: "device", label: "Sign in with Meta device code" }],
18
- async run(session) {
19
- const device = await deviceAuthorize(session.signal);
20
- const url = device.verification_uri_complete || device.verification_uri;
21
- session.notify({ message: `Open ${url} and enter code ${device.user_code}`, url });
22
- const accessToken = await pollToken(device.device_code, device.interval, device.expires_in, session.signal);
23
- const minted = await mintKey(accessToken, session.signal);
24
- await ctx.credentials.set(CREDENTIAL_REF, minted.apiKey);
25
- await ctx.credentials.modifyRecord(FLOW_KEY, async () => ({
26
- kind: "grant",
27
- payload: {
28
- accountId: minted.accountId,
29
- ...(minted.email ? { email: minted.email } : {}),
30
- },
31
- }));
32
- session.notify({ message: `Signed in as ${minted.email || minted.accountId}` });
46
+ ctx.effect(() => ctx.commands.register(museLoginCommand(ctx)), "dsh-muse-code: /muse-login");
47
+ if (typeof ctx.inject !== "function")
48
+ return;
49
+ ctx.inject(["webServer"], (child) => {
50
+ const webServer = child.webServer;
51
+ if (webServer === undefined)
52
+ return;
53
+ child.effect(() => webServer.register({
54
+ kind: "exact",
55
+ path: LOGIN_ROUTE,
56
+ handler: createLoginRouteHandler(ctx),
57
+ }), "dsh-muse-code: login route");
58
+ });
59
+ ctx.inject(["settings"], (child) => {
60
+ child.effect(() => {
61
+ const settings = child.settings;
62
+ if (settings === undefined)
63
+ return () => { };
64
+ void ensureMuseRoute(settings, ctx.logger).catch((error) => {
65
+ warn(ctx.logger, "dsh-muse-code: could not declare the Muse Code route", error);
66
+ });
67
+ return () => { };
68
+ }, "dsh-muse-code: declare route");
69
+ });
70
+ }
71
+ /** Command definition for the sign-in; exported for tests. */
72
+ export function museLoginCommand(ctx, options = {}) {
73
+ return {
74
+ name: "muse-login",
75
+ description: "Sign in to the Muse Code (Meta) subscription and enable its models",
76
+ async handler() {
77
+ const outcome = await startSignIn(ctx, options);
78
+ if (outcome.ok)
79
+ return { kind: "success", text: outcome.text };
80
+ return { kind: "error", text: outcome.text };
81
+ },
82
+ };
83
+ }
84
+ /**
85
+ * Build the exact Web route that starts the same sign-in from the Settings
86
+ * page. The route answers JSON so the Client control can render the code and
87
+ * the failure reason next to its button.
88
+ * @param ctx - Host plugin context.
89
+ * @param options - injected side effects, overridable in tests.
90
+ * @returns an HTTP handler for {@link LOGIN_ROUTE}.
91
+ */
92
+ export function createLoginRouteHandler(ctx, options = {}) {
93
+ return async (request, response) => {
94
+ setCorsHeaders(response);
95
+ if (request.method === "OPTIONS") {
96
+ response.writeHead(204);
97
+ response.end();
98
+ return;
99
+ }
100
+ if (request.method === "GET") {
101
+ // A plain navigation: this tab is the browser, so the flow need not open
102
+ // another one. The page shows the code and moves on to Meta's page.
103
+ const outcome = await startSignIn(ctx, { ...options, openBrowser: () => false });
104
+ sendHtml(response, signInPage(outcome));
105
+ return;
106
+ }
107
+ if (request.method !== "POST") {
108
+ sendJson(response, 405, { ok: false, reason: "method", text: "Use GET or POST to start the sign-in." });
109
+ return;
110
+ }
111
+ const outcome = await startSignIn(ctx, options);
112
+ if (outcome.ok) {
113
+ sendJson(response, 200, { ok: true, text: outcome.text, url: outcome.url, code: outcome.code });
114
+ return;
115
+ }
116
+ const status = outcome.reason === "busy" ? 409 : outcome.reason === "timeout" ? 504 : 500;
117
+ sendJson(response, status, { ok: false, reason: outcome.reason, text: outcome.text });
118
+ };
119
+ }
120
+ /**
121
+ * Start one sign-in attempt and wait for the actionable device-code notice.
122
+ * The attempt itself keeps running after the notice, so Meta's approval is
123
+ * picked up and the credential is stored by the seam.
124
+ * @param ctx - Host plugin context.
125
+ * @param options - injected side effects, overridable in tests.
126
+ * @returns the user-facing outcome.
127
+ */
128
+ export async function startSignIn(ctx, options = {}) {
129
+ const entry = ctx.authorization.describe(AUTH_KEY);
130
+ if (entry === undefined) {
131
+ return {
132
+ ok: false,
133
+ reason: "unavailable",
134
+ text: "Muse Code is unavailable: this DeepSeek Harness build has no Meta sign-in flow " +
135
+ `(${AUTH_KEY}). Update DeepSeek Harness or add the Meta provider under Settings > Models first.`,
136
+ };
137
+ }
138
+ if (entry.inFlight) {
139
+ return {
140
+ ok: false,
141
+ reason: "busy",
142
+ text: "A Muse Code sign-in is already running. Approve it in the browser, then retry.",
143
+ };
144
+ }
145
+ const method = entry.methods.find((candidate) => candidate.id === AUTH_METHOD)?.id ?? entry.methods[0]?.id;
146
+ if (method === undefined) {
147
+ return {
148
+ ok: false,
149
+ reason: "unavailable",
150
+ text: `The Muse Code sign-in flow offers no method to run (${AUTH_KEY}).`,
151
+ };
152
+ }
153
+ const openBrowser = options.openBrowser ?? openExternal;
154
+ const timeoutMs = options.timeoutMs ?? NOTICE_TIMEOUT_MS;
155
+ let announced;
156
+ let announce = () => { };
157
+ const notice = new Promise((resolve) => {
158
+ announce = resolve;
159
+ });
160
+ const attempt = ctx.authorization.begin({
161
+ key: AUTH_KEY,
162
+ method,
163
+ interaction: {
164
+ notify(value) {
165
+ const instruction = readSignInNotice(value);
166
+ if (instruction === undefined)
167
+ return;
168
+ announced = instruction;
169
+ announce(instruction);
170
+ },
171
+ prompt() {
172
+ return Promise.reject(new Error("Muse Code sign-in does not accept interactive prompts"));
173
+ },
33
174
  },
175
+ });
176
+ // The attempt outlives the request that started it: Meta approval arrives
177
+ // later and the seam stores the credential then.
178
+ attempt.then((outcome) => ctx.logger?.info?.(`dsh-muse-code: sign-in settled as ${outcome.status}`), (failure) => warn(ctx.logger, "dsh-muse-code: sign-in failed", failure));
179
+ const announcedNotice = notice.then((instruction) => ({ kind: "notice", instruction }));
180
+ const settled = attempt.then((outcome) => ({ kind: "settled", outcome }), (failure) => ({ kind: "failed", failure }));
181
+ const timer = {};
182
+ const timeout = new Promise((resolve) => {
183
+ timer.handle = setTimeout(() => resolve({ kind: "timeout" }), timeoutMs);
184
+ timer.handle.unref?.();
185
+ });
186
+ try {
187
+ const outcome = await Promise.race([announcedNotice, settled, timeout]);
188
+ if (outcome.kind === "notice") {
189
+ return successOutcome(outcome.instruction, openBrowser);
190
+ }
191
+ if (outcome.kind === "timeout") {
192
+ ctx.authorization.cancel?.(AUTH_KEY);
193
+ return {
194
+ ok: false,
195
+ reason: "timeout",
196
+ text: "Muse Code sign-in did not answer in time. Check the connection and run /muse-login again.",
197
+ };
198
+ }
199
+ if (outcome.kind === "failed") {
200
+ const text = messageOf(outcome.failure);
201
+ if (/already running/i.test(text)) {
202
+ return { ok: false, reason: "busy", text: "A Muse Code sign-in is already running. Approve it in the browser, then retry." };
203
+ }
204
+ return { ok: false, reason: "failed", text: `Muse Code sign-in failed: ${text}` };
205
+ }
206
+ if (announced !== undefined)
207
+ return successOutcome(announced, openBrowser);
208
+ if (outcome.outcome.status === "authorized") {
209
+ return {
210
+ ok: false,
211
+ reason: "ended",
212
+ text: "Muse Code sign-in finished without showing a code. Run /muse-login again if the models stay unavailable.",
213
+ };
214
+ }
215
+ return { ok: false, reason: "ended", text: `Muse Code sign-in ended (${outcome.outcome.status}).` };
216
+ }
217
+ finally {
218
+ if (timer.handle !== undefined)
219
+ clearTimeout(timer.handle);
220
+ }
221
+ }
222
+ function successOutcome(instruction, openBrowser) {
223
+ // The browser hand-off is a courtesy for a purely local effect: the user must
224
+ // still confirm the code on Meta's page, and both surfaces expose the URL.
225
+ const opened = openBrowser(instruction.completeUrl);
226
+ return {
227
+ ok: true,
228
+ url: instruction.completeUrl,
229
+ ...(instruction.code === undefined ? {} : { code: instruction.code }),
230
+ text: formatSignInText(instruction, opened),
231
+ };
232
+ }
233
+ /**
234
+ * Read the actionable instruction out of one authorization notice.
235
+ * @param notice - a seam notice (`message`/`url`/`code`) or a raw adapter one.
236
+ * @returns the instruction, or undefined for progress notices with no page.
237
+ */
238
+ export function readSignInNotice(notice) {
239
+ const rawUrl = firstNonEmptyString(notice.url, notice.verificationUri);
240
+ if (rawUrl === undefined)
241
+ return undefined;
242
+ let url;
243
+ try {
244
+ url = new URL(rawUrl);
245
+ }
246
+ catch {
247
+ return undefined;
248
+ }
249
+ if (url.protocol !== "http:" && url.protocol !== "https:")
250
+ return undefined;
251
+ const code = firstNonEmptyString(notice.code, notice.userCode);
252
+ const message = firstNonEmptyString(notice.message, notice.instructions);
253
+ if (code !== undefined && !url.searchParams.has("code"))
254
+ url.searchParams.set("code", code);
255
+ return {
256
+ url: rawUrl,
257
+ completeUrl: url.toString(),
258
+ ...(code === undefined ? {} : { code }),
259
+ ...(message === undefined ? {} : { message }),
34
260
  };
35
- const register = () => ctx.authorization.registerFlow(flow);
36
- if (ctx.effect)
37
- ctx.effect(register);
38
- else
39
- register();
261
+ }
262
+ /**
263
+ * Render the instruction text shared by the command result and the Settings
264
+ * control.
265
+ * @param instruction - actionable notice.
266
+ * @param opened - whether the browser hand-off was started.
267
+ * @returns one line the user can act on.
268
+ */
269
+ export function formatSignInText(instruction, opened) {
270
+ const openPart = opened
271
+ ? "A browser tab should have opened; if not, open it yourself."
272
+ : "Open this page in your browser.";
273
+ const codePart = instruction.code === undefined
274
+ ? "Confirm the sign-in on the Meta page."
275
+ : `Confirm the code ${instruction.code} there.`;
276
+ return `Muse Code sign-in started. ${openPart} ${codePart} Page: ${instruction.completeUrl} Muse Code models become available as soon as Meta confirms.`;
277
+ }
278
+ /**
279
+ * Declare the shipped `meta` route in the `llm-pi-ai` settings section so the
280
+ * provider appears in Settings > Models and its models in the picker.
281
+ *
282
+ * The write is a merge into the entry's editable config, exactly what the
283
+ * Models page does when a user adds this provider by hand; an already declared
284
+ * `meta` profile is never touched.
285
+ *
286
+ * @param settings - the Settings form service.
287
+ * @param logger - optional plugin logger.
288
+ * @param options - retry window for late-activating profile entries.
289
+ * @returns whether this call added the route.
290
+ */
291
+ export async function ensureMuseRoute(settings, logger, options = {}) {
292
+ const attempts = options.attempts ?? 12;
293
+ const delayMs = options.delayMs ?? 250;
294
+ for (let attempt = 0; attempt < attempts; attempt += 1) {
295
+ const row = settings.describe().find((candidate) => candidate.ns === SETTINGS_NS);
296
+ if (row !== undefined) {
297
+ const providers = row.value["providers"];
298
+ const declared = isRecord(providers) && Object.hasOwn(providers, PROVIDER);
299
+ if (declared)
300
+ return false;
301
+ await settings.update(SETTINGS_NS, { providers: { [PROVIDER]: { displayName: ROUTE_LABEL } } }, row.revision);
302
+ logger?.info?.(`dsh-muse-code: declared the ${PROVIDER} route in Settings > Models`);
303
+ return true;
304
+ }
305
+ if (attempt < attempts - 1 && delayMs > 0)
306
+ await sleep(delayMs);
307
+ }
308
+ logger?.debug?.(`dsh-muse-code: no configurable "${SETTINGS_NS}" entry; leaving the route to Settings > Models`);
309
+ return false;
310
+ }
311
+ function setCorsHeaders(response) {
312
+ response.setHeader("Access-Control-Allow-Origin", "*");
313
+ response.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
314
+ response.setHeader("Access-Control-Allow-Headers", "content-type");
315
+ response.setHeader("Cache-Control", "no-store");
316
+ }
317
+ function sendJson(response, status, body) {
318
+ const text = JSON.stringify(body);
319
+ response.writeHead(status, { "content-type": "application/json; charset=utf-8" });
320
+ response.end(text);
321
+ }
322
+ function sendHtml(response, html) {
323
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
324
+ response.end(html);
325
+ }
326
+ /**
327
+ * The page a plain navigation to {@link LOGIN_ROUTE} receives: the code, the
328
+ * fallback link, and an immediate move to Meta's verification page.
329
+ * @param outcome - result of the sign-in attempt.
330
+ * @returns a self-contained HTML document.
331
+ */
332
+ function signInPage(outcome) {
333
+ const title = "Muse Code sign-in";
334
+ if (!outcome.ok) {
335
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title></head>` +
336
+ `<body style="font-family:system-ui,sans-serif;padding:2rem;line-height:1.5">` +
337
+ `<h1 style="font-size:1.1rem">${escapeHtml(title)}</h1>` +
338
+ `<p>${escapeHtml(outcome.text)}</p>` +
339
+ `<p><a href="/muse-code/login">Try again</a></p></body></html>`;
340
+ }
341
+ const codePart = outcome.code === undefined
342
+ ? ""
343
+ : `<p style="font-size:1.4rem;letter-spacing:0.1em"><strong>${escapeHtml(outcome.code)}</strong></p>`;
344
+ return `<!doctype html><html><head><meta charset="utf-8"><title>${title}</title>` +
345
+ `<meta http-equiv="refresh" content="1; url=${escapeHtml(outcome.url)}"></head>` +
346
+ `<body style="font-family:system-ui,sans-serif;padding:2rem;line-height:1.5">` +
347
+ `<h1 style="font-size:1.1rem">${escapeHtml(title)}</h1>` +
348
+ `<p>Confirm this code on Meta's page:</p>${codePart}` +
349
+ `<p>If the page does not open, <a href="${escapeHtml(outcome.url)}">continue on Meta</a>.</p>` +
350
+ `</body></html>`;
351
+ }
352
+ function escapeHtml(value) {
353
+ return value
354
+ .replaceAll("&", "&amp;")
355
+ .replaceAll("<", "&lt;")
356
+ .replaceAll(">", "&gt;")
357
+ .replaceAll('"', "&quot;");
358
+ }
359
+ function firstNonEmptyString(...values) {
360
+ for (const value of values) {
361
+ if (typeof value === "string" && value.trim().length > 0)
362
+ return value.trim();
363
+ }
364
+ return undefined;
365
+ }
366
+ function isRecord(value) {
367
+ return typeof value === "object" && value !== null && !Array.isArray(value);
368
+ }
369
+ function messageOf(failure) {
370
+ if (failure instanceof Error && failure.message.length > 0)
371
+ return failure.message;
372
+ const text = String(failure);
373
+ return text.length > 0 ? text : "unknown error";
374
+ }
375
+ function warn(logger, message, error) {
376
+ const text = `${message}: ${messageOf(error)}`;
377
+ if (logger?.warn !== undefined)
378
+ logger.warn(text);
379
+ else if (logger?.debug !== undefined)
380
+ logger.debug(text);
381
+ }
382
+ function sleep(ms) {
383
+ return new Promise((resolve) => {
384
+ const handle = setTimeout(resolve, ms);
385
+ handle.unref?.();
386
+ });
40
387
  }
@@ -0,0 +1,10 @@
1
+ import { spawn } from "node:child_process";
2
+ type SpawnLike = typeof spawn;
3
+ /**
4
+ * Hand one http(s) URL to the platform's default browser.
5
+ * @param url - absolute URL to open.
6
+ * @param spawnImpl - injectable process spawner, for tests.
7
+ * @returns whether the hand-off was started; it does not prove a page opened.
8
+ */
9
+ export declare function openExternal(url: string, spawnImpl?: SpawnLike): boolean;
10
+ export {};
@@ -0,0 +1,38 @@
1
+ // Best-effort "open this URL in the user's default browser" for the host side.
2
+ //
3
+ // DeepSeek Harness has no provider-neutral host service for opening an external
4
+ // page; its account flow delegates that to the Desktop shell. The sign-in needs
5
+ // the Meta verification page in a browser, so this module hands the URL to the
6
+ // platform's own URI handler without a shell: `rundll32 url.dll,FileProtocolHandler`
7
+ // on Windows, `open` on macOS, `xdg-open` elsewhere.
8
+ //
9
+ // Failure is never fatal: the command and the login route both return the URL
10
+ // as text so the user can open it manually.
11
+ import { spawn } from "node:child_process";
12
+ /**
13
+ * Hand one http(s) URL to the platform's default browser.
14
+ * @param url - absolute URL to open.
15
+ * @param spawnImpl - injectable process spawner, for tests.
16
+ * @returns whether the hand-off was started; it does not prove a page opened.
17
+ */
18
+ export function openExternal(url, spawnImpl = spawn) {
19
+ if (!/^https?:\/\//i.test(url))
20
+ return false;
21
+ const [command, args] = openerCommand(url);
22
+ try {
23
+ const child = spawnImpl(command, args, { detached: true, stdio: "ignore", windowsHide: true });
24
+ child.on("error", () => { });
25
+ child.unref();
26
+ return true;
27
+ }
28
+ catch {
29
+ return false;
30
+ }
31
+ }
32
+ function openerCommand(url) {
33
+ if (process.platform === "win32")
34
+ return ["rundll32.exe", ["url.dll,FileProtocolHandler", url]];
35
+ if (process.platform === "darwin")
36
+ return ["open", [url]];
37
+ return ["xdg-open", [url]];
38
+ }