@yagni-app/code-staging 1.0.5-staging.1239.1 → 1.0.5-staging.1240.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js
CHANGED
|
@@ -516,6 +516,10 @@ export const HELP_TEXT = [
|
|
|
516
516
|
" yagni connect claude-code Route Claude Code through the YAGNI model proxy",
|
|
517
517
|
" (--project scopes to this repo; --off disconnects).",
|
|
518
518
|
" yagni connect codex Route Codex CLI through the YAGNI model proxy.",
|
|
519
|
+
" yagni mcp <subcommand> Manage MCP servers: add, remove, list, get,",
|
|
520
|
+
" add-from-claude, and more — run `yagni mcp` for",
|
|
521
|
+
" details. OAuth servers authenticate via the",
|
|
522
|
+
" /mcp panel in a session.",
|
|
519
523
|
" yagni token Output the active environment's API token (for helpers).",
|
|
520
524
|
" yagni use <name> Switch the active environment (sticky).",
|
|
521
525
|
" Presets: prod, local. Others need --base-url <url>.",
|
|
@@ -537,6 +541,7 @@ export const HELP_TEXT = [
|
|
|
537
541
|
" /mode plan|review|auto Hold writes for planning, or confirm each change.",
|
|
538
542
|
" /todos Show the agent's live task list.",
|
|
539
543
|
" /cost Session usage and credit headroom.",
|
|
544
|
+
" /mcp Manage MCP servers; authenticate OAuth servers.",
|
|
540
545
|
"",
|
|
541
546
|
"The active environment is sticky; `use` switches it (prod is the default).",
|
|
542
547
|
"Set YAGNI_BASE_URL to override the base URL for a single run.",
|
|
@@ -25,6 +25,7 @@ import { runningUnderTest } from "../crashReport.js";
|
|
|
25
25
|
import { getStoredOAuthEntry, updateStoredOAuthEntry } from "./authStore.js";
|
|
26
26
|
import { deleteStoredOAuthEntry as clearStoredOAuthEntry } from "./authStore.js";
|
|
27
27
|
import { logMcpEvent, redactSensitiveUrlParams } from "./log.js";
|
|
28
|
+
import { renderAuthErrorPage, renderAuthStateMismatchPage, renderAuthSuccessPage, } from "./callbackPage.js";
|
|
28
29
|
/** Cancellation via an AbortSignal (Ctrl-C / Esc in the /mcp panel). */
|
|
29
30
|
export class AuthenticationCancelledError extends Error {
|
|
30
31
|
constructor(message = "OAuth cancelled") {
|
|
@@ -424,20 +425,23 @@ function waitForCode(port, expectedState, opts = {}) {
|
|
|
424
425
|
// The provider-controlled values are HTML-escaped: any local process
|
|
425
426
|
// can hit the loopback with a crafted ?error=<script> payload, and the
|
|
426
427
|
// page must never reflect it (same spot Claude Code sanitizes).
|
|
427
|
-
res.end(
|
|
428
|
+
res.end(renderAuthErrorPage({
|
|
429
|
+
error: escapeHtml(error),
|
|
430
|
+
errorDescription: errorDescription ? escapeHtml(errorDescription) : undefined,
|
|
431
|
+
}));
|
|
428
432
|
finish(undefined);
|
|
429
433
|
return;
|
|
430
434
|
}
|
|
431
435
|
if (state !== expectedState) {
|
|
432
436
|
onEvent?.("oauth_callback_state_mismatch", {});
|
|
433
437
|
res.writeHead(400, { "Content-Type": "text/html" });
|
|
434
|
-
res.end(
|
|
438
|
+
res.end(renderAuthStateMismatchPage());
|
|
435
439
|
fail(new Error("OAuth state mismatch - possible CSRF attack"));
|
|
436
440
|
return;
|
|
437
441
|
}
|
|
438
442
|
onEvent?.("oauth_callback_code", {});
|
|
439
443
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
440
|
-
res.end(
|
|
444
|
+
res.end(renderAuthSuccessPage());
|
|
441
445
|
finish(code);
|
|
442
446
|
});
|
|
443
447
|
server.on("error", () => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The loopback OAuth callback page — the only surface a user ever sees served
|
|
3
|
+
* from the local `127.0.0.1:<port>/callback` listener. Rendered with the same
|
|
4
|
+
* Glass & Print language as the app: tinted blue field, floating glass card,
|
|
5
|
+
* one blue accent, three type voices (Instrument Sans interface / Newsreader
|
|
6
|
+
* authored / IBM Plex Mono fact).
|
|
7
|
+
*
|
|
8
|
+
* Self-contained: no external assets, fonts, or network calls — the page must
|
|
9
|
+
* render offline and never phone home. Tokens are inlined from
|
|
10
|
+
* packages/frontend/src/styles/tokens.css so this module can't drift a runtime
|
|
11
|
+
* dependency on the frontend build; token names are kept in comments so a
|
|
12
|
+
* re-derivation stays mechanical.
|
|
13
|
+
*
|
|
14
|
+
* `escapeHtml` is applied by the caller to every provider-controlled value
|
|
15
|
+
* before interpolation (a local process can hit the loopback with a crafted
|
|
16
|
+
* ?error=<script> payload — the page never reflects raw input).
|
|
17
|
+
*/
|
|
18
|
+
/** The success page shown after the provider redirects back with a code. */
|
|
19
|
+
export declare function renderAuthSuccessPage(): string;
|
|
20
|
+
/**
|
|
21
|
+
* The error page shown when the provider bounces the flow. `error` and
|
|
22
|
+
* `errorDescription` are provider-controlled and already HTML-escaped by the
|
|
23
|
+
* caller.
|
|
24
|
+
*/
|
|
25
|
+
export declare function renderAuthErrorPage(opts: {
|
|
26
|
+
error?: string;
|
|
27
|
+
errorDescription?: string;
|
|
28
|
+
}): string;
|
|
29
|
+
/** The state-mismatch page — a hard failure, shown before the flow aborts. */
|
|
30
|
+
export declare function renderAuthStateMismatchPage(): string;
|
|
31
|
+
//# sourceMappingURL=callbackPage.d.ts.map
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The loopback OAuth callback page — the only surface a user ever sees served
|
|
3
|
+
* from the local `127.0.0.1:<port>/callback` listener. Rendered with the same
|
|
4
|
+
* Glass & Print language as the app: tinted blue field, floating glass card,
|
|
5
|
+
* one blue accent, three type voices (Instrument Sans interface / Newsreader
|
|
6
|
+
* authored / IBM Plex Mono fact).
|
|
7
|
+
*
|
|
8
|
+
* Self-contained: no external assets, fonts, or network calls — the page must
|
|
9
|
+
* render offline and never phone home. Tokens are inlined from
|
|
10
|
+
* packages/frontend/src/styles/tokens.css so this module can't drift a runtime
|
|
11
|
+
* dependency on the frontend build; token names are kept in comments so a
|
|
12
|
+
* re-derivation stays mechanical.
|
|
13
|
+
*
|
|
14
|
+
* `escapeHtml` is applied by the caller to every provider-controlled value
|
|
15
|
+
* before interpolation (a local process can hit the loopback with a crafted
|
|
16
|
+
* ?error=<script> payload — the page never reflects raw input).
|
|
17
|
+
*/
|
|
18
|
+
const CSS = `
|
|
19
|
+
:root {
|
|
20
|
+
/* tokens.css — field, ink, accent, materials */
|
|
21
|
+
--field-base: #f4f6fc;
|
|
22
|
+
--field-tint-blue: #dfe8ff;
|
|
23
|
+
--field-tint-violet: #e8e4ff;
|
|
24
|
+
--field-tint-cyan: #e3f0f6;
|
|
25
|
+
--ink: #23252e;
|
|
26
|
+
--ink-strong: #181a22;
|
|
27
|
+
--text-secondary: #565b72;
|
|
28
|
+
--text-quiet: #8b90a5;
|
|
29
|
+
--text-label: #7b8098;
|
|
30
|
+
--accent: #2f56d3;
|
|
31
|
+
--accent-muted: rgba(47, 86, 211, 0.12);
|
|
32
|
+
--accent-border: rgba(47, 86, 211, 0.3);
|
|
33
|
+
--error: #b4452f;
|
|
34
|
+
--error-muted: rgba(180, 69, 47, 0.07);
|
|
35
|
+
--glass-bg: rgba(255, 255, 255, 0.55);
|
|
36
|
+
--glass-border: rgba(255, 255, 255, 0.85);
|
|
37
|
+
--glass-blur: 18px;
|
|
38
|
+
--radius-pane: 16px;
|
|
39
|
+
--shadow-staged: 0 1px 2px rgba(24, 34, 64, 0.07), 0 18px 40px -30px rgba(38, 52, 110, 0.35);
|
|
40
|
+
--shadow-staged-high: 0 1px 2px rgba(24, 34, 64, 0.08), 0 28px 60px -28px rgba(38, 52, 110, 0.42);
|
|
41
|
+
--body: "Instrument Sans Variable", "Instrument Sans", system-ui, sans-serif;
|
|
42
|
+
--prose: "Newsreader", ui-serif, Georgia, serif;
|
|
43
|
+
--mono: "IBM Plex Mono", ui-monospace, monospace;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
* { box-sizing: border-box; }
|
|
47
|
+
|
|
48
|
+
html, body { height: 100%; }
|
|
49
|
+
|
|
50
|
+
body {
|
|
51
|
+
margin: 0;
|
|
52
|
+
min-height: 100%;
|
|
53
|
+
display: grid;
|
|
54
|
+
place-items: center;
|
|
55
|
+
padding: 24px;
|
|
56
|
+
font-family: var(--body);
|
|
57
|
+
color: var(--ink);
|
|
58
|
+
background:
|
|
59
|
+
radial-gradient(1200px 700px at 8% -5%, var(--field-tint-blue) 0, rgba(223, 232, 255, 0) 60%),
|
|
60
|
+
radial-gradient(900px 600px at 95% 0%, var(--field-tint-violet) 0, rgba(232, 228, 255, 0) 55%),
|
|
61
|
+
radial-gradient(900px 700px at 60% 100%, var(--field-tint-cyan) 0, rgba(227, 240, 246, 0) 60%),
|
|
62
|
+
var(--field-base);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.card {
|
|
66
|
+
width: 100%;
|
|
67
|
+
max-width: 400px;
|
|
68
|
+
text-align: center;
|
|
69
|
+
padding: 40px 32px 32px;
|
|
70
|
+
background: var(--glass-bg);
|
|
71
|
+
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(1.15);
|
|
72
|
+
backdrop-filter: blur(var(--glass-blur)) saturate(1.15);
|
|
73
|
+
border: 1px solid var(--glass-border);
|
|
74
|
+
border-radius: var(--radius-pane);
|
|
75
|
+
box-shadow: var(--shadow-staged-high);
|
|
76
|
+
animation: rise 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@keyframes rise {
|
|
80
|
+
from { opacity: 0; transform: translateY(8px); }
|
|
81
|
+
to { opacity: 1; transform: none; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.wordmark {
|
|
85
|
+
font-family: var(--mono);
|
|
86
|
+
font-size: 10px;
|
|
87
|
+
font-weight: 600;
|
|
88
|
+
letter-spacing: 0.22em;
|
|
89
|
+
text-transform: uppercase;
|
|
90
|
+
color: var(--text-label);
|
|
91
|
+
margin: 0 0 28px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.badge {
|
|
95
|
+
width: 52px;
|
|
96
|
+
height: 52px;
|
|
97
|
+
margin: 0 auto 20px;
|
|
98
|
+
display: grid;
|
|
99
|
+
place-items: center;
|
|
100
|
+
border-radius: 14px;
|
|
101
|
+
background: var(--accent-muted);
|
|
102
|
+
border: 1px solid var(--accent-border);
|
|
103
|
+
color: var(--accent);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.card--error .badge {
|
|
107
|
+
background: var(--error-muted);
|
|
108
|
+
border-color: rgba(180, 69, 47, 0.28);
|
|
109
|
+
color: var(--error);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
h1 {
|
|
113
|
+
font-family: var(--prose);
|
|
114
|
+
font-size: 30px;
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
line-height: 1.15;
|
|
117
|
+
letter-spacing: -0.01em;
|
|
118
|
+
color: var(--ink-strong);
|
|
119
|
+
margin: 0 0 10px;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.lede {
|
|
123
|
+
font-size: 14px;
|
|
124
|
+
line-height: 1.55;
|
|
125
|
+
color: var(--text-secondary);
|
|
126
|
+
margin: 0 0 24px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.detail {
|
|
130
|
+
font-family: var(--mono);
|
|
131
|
+
font-size: 12px;
|
|
132
|
+
line-height: 1.5;
|
|
133
|
+
color: var(--error);
|
|
134
|
+
background: var(--error-muted);
|
|
135
|
+
border: 1px solid rgba(180, 69, 47, 0.2);
|
|
136
|
+
border-radius: 8px;
|
|
137
|
+
padding: 8px 10px;
|
|
138
|
+
margin: 0 0 24px;
|
|
139
|
+
word-break: break-word;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.divider {
|
|
143
|
+
height: 1px;
|
|
144
|
+
margin: 0 0 20px;
|
|
145
|
+
background: linear-gradient(90deg, transparent, var(--accent-border), transparent);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.hint {
|
|
149
|
+
font-size: 12.5px;
|
|
150
|
+
color: var(--text-quiet);
|
|
151
|
+
margin: 0;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@media (prefers-reduced-motion: reduce) {
|
|
155
|
+
.card { animation: none; }
|
|
156
|
+
}
|
|
157
|
+
`;
|
|
158
|
+
const CHECK_ICON = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg>`;
|
|
159
|
+
const ERROR_ICON = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><circle cx="12" cy="12" r="9"/><line x1="12" y1="8" x2="12" y2="13"/><line x1="12" y1="16.5" x2="12.01" y2="16.5"/></svg>`;
|
|
160
|
+
function shell(opts) {
|
|
161
|
+
return `<!DOCTYPE html>
|
|
162
|
+
<html lang="en">
|
|
163
|
+
<head>
|
|
164
|
+
<meta charset="utf-8">
|
|
165
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
166
|
+
<meta name="color-scheme" content="light">
|
|
167
|
+
<title>${opts.title} — YAGNI Code</title>
|
|
168
|
+
<style>${CSS}</style>
|
|
169
|
+
</head>
|
|
170
|
+
<body>
|
|
171
|
+
<main class="card${opts.error ? " card--error" : ""}">
|
|
172
|
+
<p class="wordmark">YAGNI Code</p>
|
|
173
|
+
<div class="badge">${opts.error ? ERROR_ICON : CHECK_ICON}</div>
|
|
174
|
+
<h1>${opts.heading}</h1>
|
|
175
|
+
<p class="lede">${opts.lede}</p>
|
|
176
|
+
${opts.detail ? `<p class="detail">${opts.detail}</p>` : ""}
|
|
177
|
+
<div class="divider"></div>
|
|
178
|
+
<p class="hint">${opts.hint}</p>
|
|
179
|
+
</main>
|
|
180
|
+
</body>
|
|
181
|
+
</html>`;
|
|
182
|
+
}
|
|
183
|
+
/** The success page shown after the provider redirects back with a code. */
|
|
184
|
+
export function renderAuthSuccessPage() {
|
|
185
|
+
return shell({
|
|
186
|
+
title: "Connected",
|
|
187
|
+
heading: "Connected to YAGNI Code",
|
|
188
|
+
lede: "Your MCP server is authorized and ready to use.",
|
|
189
|
+
hint: "You can close this window and return to your session.",
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The error page shown when the provider bounces the flow. `error` and
|
|
194
|
+
* `errorDescription` are provider-controlled and already HTML-escaped by the
|
|
195
|
+
* caller.
|
|
196
|
+
*/
|
|
197
|
+
export function renderAuthErrorPage(opts) {
|
|
198
|
+
const detail = opts.error
|
|
199
|
+
? opts.errorDescription
|
|
200
|
+
? `${opts.error}: ${opts.errorDescription}`
|
|
201
|
+
: opts.error
|
|
202
|
+
: undefined;
|
|
203
|
+
return shell({
|
|
204
|
+
title: "Connection failed",
|
|
205
|
+
error: true,
|
|
206
|
+
heading: "Authentication didn't complete",
|
|
207
|
+
lede: "The provider didn't finish the sign-in flow.",
|
|
208
|
+
detail,
|
|
209
|
+
hint: "You can close this window and try again from YAGNI Code.",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
/** The state-mismatch page — a hard failure, shown before the flow aborts. */
|
|
213
|
+
export function renderAuthStateMismatchPage() {
|
|
214
|
+
return shell({
|
|
215
|
+
title: "Connection failed",
|
|
216
|
+
error: true,
|
|
217
|
+
heading: "Authentication didn't complete",
|
|
218
|
+
lede: "The sign-in response didn't match the request, so it was rejected.",
|
|
219
|
+
hint: "You can close this window and try again from YAGNI Code.",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
//# sourceMappingURL=callbackPage.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yagni-app/code-staging",
|
|
3
|
-
"version": "1.0.5-staging.
|
|
3
|
+
"version": "1.0.5-staging.1240.1",
|
|
4
4
|
"description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
|
|
@@ -42,5 +42,5 @@
|
|
|
42
42
|
"turndown": "^7.2.4",
|
|
43
43
|
"typebox": "^1.3.15"
|
|
44
44
|
},
|
|
45
|
-
"yagniSourceSha": "
|
|
45
|
+
"yagniSourceSha": "24a0ebcde7f7df67fd0fcd230caa85c0c897742b"
|
|
46
46
|
}
|