@lanes-sh/link 0.6.8 → 0.6.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/.gcloudignore +50 -0
  2. package/README.md +5 -0
  3. package/package.json +4 -3
  4. package/src/auth/oidc.ts +65 -12
  5. package/src/cli/brand.ts +16 -9
  6. package/src/cli/commands/operate/auth.ts +333 -0
  7. package/src/cli/commands/operate/desktop.ts +220 -0
  8. package/src/cli/commands/operate/findings.ts +9 -38
  9. package/src/cli/commands/operate/inspect.ts +59 -40
  10. package/src/cli/commands/operate/serve.ts +0 -3
  11. package/src/cli/commands/operate.ts +5 -3
  12. package/src/cli/main.ts +19 -4
  13. package/src/cli/selection.ts +15 -4
  14. package/src/cli/usage.ts +5 -1
  15. package/src/connectivity/auth/index.ts +1 -0
  16. package/src/connectivity/auth/oauth-authcode/provider.ts +17 -1
  17. package/src/connectivity/auth/oauth-authcode/refresh.ts +43 -11
  18. package/src/connectivity/auth/oauth-jwt/index.ts +12 -1
  19. package/src/connectivity/auth/reauth.ts +48 -0
  20. package/src/deployments/gcp/Dockerfile +27 -2
  21. package/src/deployments/gcp/bucket.ts +82 -9
  22. package/src/deployments/gcp/driver.ts +43 -5
  23. package/src/deployments/gcp/lifecycle.json +12 -0
  24. package/src/deployments/gcp/survey.ts +12 -1
  25. package/src/policy/limits.ts +76 -3
  26. package/src/profile/index.ts +1 -0
  27. package/src/profile/legacy.ts +8 -3
  28. package/src/profile/schema.ts +65 -0
  29. package/src/server/cors.ts +3 -3
  30. package/src/server/edge.ts +188 -1
  31. package/src/server/endpoint.ts +0 -17
  32. package/src/server/harness.ts +10 -7
  33. package/src/server/index.ts +62 -90
  34. package/src/server/mcp/index.ts +0 -1
  35. package/src/server/mcp/visibility.ts +0 -33
  36. package/src/server/oauth.ts +21 -2
  37. package/src/cli/commands/operate/dashboard.ts +0 -107
  38. package/src/cli/dashboard-page.ts +0 -293
  39. package/src/cli/dashboard-shell.ts +0 -125
  40. package/src/cli/provider-marks.ts +0 -45
  41. package/src/server/dashboard.ts +0 -212
@@ -1,107 +0,0 @@
1
- import { deploymentIdentity, endpointHealth, localUrl } from '../../endpoint-url.ts';
2
- import { defaultOpenBrowser } from '../../oauth.ts';
3
- import { announce, ok, print, style, warn } from '../../output.ts';
4
- import {
5
- ensureProfileToken,
6
- openRuntime,
7
- resolveProfile,
8
- type GlobalFlags,
9
- } from '../../runtime.ts';
10
-
11
- /**
12
- * `lanes link dashboard` — open the page this endpoint serves.
13
- *
14
- * The command exists because the page cannot be reached without it. A browser
15
- * navigating to `/dashboard` carries no `Authorization` header, so the token has
16
- * to arrive some other way, and the only place it already exists is the
17
- * credential store this command can read. It puts it in the URL once; the
18
- * endpoint exchanges it for a session cookie and redirects to a URL without it.
19
- *
20
- * Local only, deliberately. See `#server/dashboard.ts` — ADR-018 leaves a
21
- * deployed instance with no door a person at a browser can come through, and a
22
- * command that opened a loopback URL for a target that is not there would be
23
- * reporting success for a page nobody can load.
24
- */
25
-
26
- export interface DashboardFlags extends GlobalFlags {
27
- /** Print the URL instead of opening it. */
28
- readonly print?: boolean | undefined;
29
- }
30
-
31
- export async function dashboard(flags: DashboardFlags): Promise<void> {
32
- // `resolveProfile` rather than `openRuntime`, and the order is the point.
33
- // Opening a runtime opens that target's adapters, so a deployed target sends
34
- // this command to Secret Manager and a bucket before it can say the one thing
35
- // it needs to — that the dashboard is not there. The refusal below would
36
- // still be correct and nobody would ever read it, because a storage 403
37
- // arrives first. This is the seam `runtime.ts` documents `deploy` and
38
- // `secrets push` stopping at, for the same reason.
39
- const { resolution, config, target, resolved } = await resolveProfile(flags);
40
- announce(resolution);
41
-
42
- const deployed = deploymentIdentity(resolved?.declared.deploy);
43
- if (deployed) {
44
- throw new Error(
45
- `Target "${target}" is deployed to ${deployed.platform}, and the dashboard is served only ` +
46
- 'by a local endpoint — a browser carries no bearer token, and Cloud Run\'s own gate ' +
47
- 'admits only a Google-signed identity token it cannot mint either (ADR-018).\n' +
48
- `What a deployed target can answer: ` +
49
- `lanes link status --profile ${resolution.profile} --target ${target}`,
50
- );
51
- }
52
-
53
- const runtime = await openRuntime(flags);
54
-
55
- try {
56
- const { token } = await ensureProfileToken(runtime.credentials, runtime.config.auth.token_ref);
57
-
58
- const url = new URL(localUrl(runtime.config));
59
- url.pathname = '/dashboard';
60
- url.searchParams.set('k', token);
61
- // Named rather than left to the endpoint's primary, so the page opens on
62
- // the profile this command resolved and printed above.
63
- url.searchParams.set('profile', runtime.resolution.profile);
64
-
65
- // The page is served by a running endpoint or by nothing. Saying so here
66
- // beats opening a browser at a connection error.
67
- const live = await endpointHealth(localUrl(runtime.config), token);
68
- if (!live) {
69
- // Both flags on both lines. They read as correct without them only
70
- // because they were written while a missing one still resolved to the
71
- // workspace default; with nothing to fall back on (ADR-037) each is a
72
- // paste that refuses, and this one is printed at the moment somebody is
73
- // least able to guess what it wanted.
74
- const where = `--profile ${runtime.resolution.profile} --target ${target}`;
75
-
76
- print(warn(`nothing is serving this port — run: lanes link start ${where}`));
77
- print(style.dim(` then: lanes link dashboard ${where}`));
78
- return;
79
- }
80
-
81
- if (!live.profiles.includes(runtime.resolution.profile)) {
82
- // `start --only` serves one profile. The URL would 404 on the rest, which
83
- // is honest but unhelpful without being told why.
84
- print(
85
- warn(
86
- `the endpoint on this port serves ${live.profiles.join(', ')} — ` +
87
- `not "${runtime.resolution.profile}"`,
88
- ),
89
- );
90
- return;
91
- }
92
-
93
- if (flags.print) {
94
- // Printed, not opened: for a terminal on a machine with no browser, and
95
- // for anyone who would rather see what they are about to open. It carries
96
- // the token, which is why it is behind a flag rather than always shown.
97
- print(url.href);
98
- return;
99
- }
100
-
101
- defaultOpenBrowser(url.href);
102
- print(ok(`opened ${style.bold(`${url.origin}/dashboard`)}`));
103
- print(style.dim(' the link carries a one-time key; the page swaps it for a session cookie.'));
104
- } finally {
105
- await runtime.close();
106
- }
107
- }
@@ -1,293 +0,0 @@
1
- import { PROVIDER_MANIFESTS } from '#providers/index.ts';
2
- import { planAll, type ProviderPlan } from '#providers/setup/plan.ts';
3
- import { PROVIDER_MARKS } from './provider-marks.ts';
4
- import { escapeHtml } from './brand.ts';
5
- import { shell } from './dashboard-shell.ts';
6
-
7
- /**
8
- * The dashboard — one page answering what `status`, `setup plan` and
9
- * `target list` answer between them.
10
- *
11
- * It renders and it does not act. Every state-changing thing on it is a command
12
- * to paste, for two reasons that are not the same. Connecting is ADR-005: the
13
- * consent belongs to whoever owns the browser, and the loopback listener that
14
- * receives the code is the CLI's. Everything else is ADR-007: config is written
15
- * from the control plane, and a page served by the endpoint is not it.
16
- *
17
- * So this is a reader, and the honest shape of a reader is a page that tells you
18
- * the line to run. What it saves is not typing — it is knowing which line.
19
- *
20
- * The design is `callback-page.ts`'s, because that is the product's page and
21
- * this is the same product: nothing painted (`color-scheme: light dark` over a
22
- * transparent background, correct in either mode with no theme switch), Lora on
23
- * the heading, and colour reserved for status rather than spent on emphasis.
24
- * What differs is the width — a card centred at 460px is right for one sentence
25
- * and wrong for four sections.
26
- */
27
-
28
- export interface DashboardConnection {
29
- /** `provider.id`, the key everything else addresses this by. */
30
- readonly key: string;
31
- readonly provider: string;
32
- readonly account: string;
33
- /** What the operator calls it, where that is not just the account. */
34
- readonly label?: string | undefined;
35
- /** `active`, `unauthorized`, `disabled`, or `not reconciled`. */
36
- readonly state: string;
37
- }
38
-
39
- export interface DashboardView {
40
- /** The profile being rendered, which `?profile=` may have chosen. */
41
- readonly profile: string;
42
- /** Every profile this endpoint serves, for the switcher. */
43
- readonly profiles: readonly string[];
44
- /** The target whose adapters are open. */
45
- readonly target: string;
46
- /** Every target the profile declares, live or not. */
47
- readonly targets: readonly string[];
48
- readonly connections: readonly DashboardConnection[];
49
- /** `oauth_apps` keys, so a profile with its own client is described as having one. */
50
- readonly ownClients: readonly string[];
51
- }
52
-
53
- /**
54
- * Every command this page renders, built in one place.
55
- *
56
- * `--profile` and `--target` on all of them, unconditionally. The shell a line
57
- * is pasted into resolves both for itself — from `LANES_LINK_PROFILE` and
58
- * `LANES_LINK_TARGET`, or from the workspace default — and a page that shows
59
- * you one profile while handing you a command that silently acts on another is
60
- * worse than one that shows nothing. It is also the rule `resolveSelection`
61
- * already follows by refusing to guess.
62
- */
63
- function command(view: DashboardView, rest: string): string {
64
- return `lanes link ${rest} --profile ${view.profile} --target ${view.target}`;
65
- }
66
-
67
- /**
68
- * A command, shown in full.
69
- *
70
- * Kept for the two places where the command *is* the label — the sign-in page
71
- * and the footer — because there is no provider name to carry it. Everywhere
72
- * else the line is on the button, not on the page; see `copyButton`.
73
- */
74
- function commandLine(line: string): string {
75
- const text = escapeHtml(line);
76
- return (
77
- `<div class="cmd"><code>${text}</code>` +
78
- `<button class="btn copy" type="button" data-copy="${text}" aria-label="Copy command">copy</button></div>`
79
- );
80
- }
81
-
82
- /**
83
- * The command, on the button rather than beside it.
84
- *
85
- * What this trades away is worth naming: the line is no longer selectable by
86
- * hand, so a browser with no clipboard API leaves no way to get it. That is
87
- * narrower than it sounds — `navigator.clipboard` needs a secure context, and
88
- * `http://127.0.0.1` is one by definition, which is the only address this page
89
- * is ever served on. The `title` carries the text for anyone who wants to read
90
- * before pasting, and `lanes link setup plan` prints the same lines.
91
- */
92
- function copyButton(line: string, label: string): string {
93
- const text = escapeHtml(line);
94
- return (
95
- `<button class="btn copy" type="button" data-copy="${text}" title="${text}" ` +
96
- `aria-label="Copy the command that connects ${escapeHtml(label)}">copy</button>`
97
- );
98
- }
99
-
100
- /**
101
- * The provider's mark, or letters standing in for one.
102
- *
103
- * `aria-hidden` on both: every caller puts the name beside it, and a screen
104
- * reader announcing "GitHub, GitHub" is worse than one announcing it once.
105
- *
106
- * The fallback takes the first letter of the family and two of the member,
107
- * because the plain two collapse a catalogue holding four `icloud_*` entries
108
- * and two `*_mcp` ones into a handful of identical marks — `ICA` and `ICO`
109
- * rather than `IC` twice.
110
- */
111
- function mark(id: string): string {
112
- const path = PROVIDER_MARKS[id];
113
- if (path) {
114
- return (
115
- '<svg class="glyph" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">' +
116
- `<path d="${path}"/></svg>`
117
- );
118
- }
119
-
120
- const [family, member] = id.split('_');
121
- const letters = member ? `${family![0]}${member.slice(0, 2)}` : id.slice(0, 2);
122
- return `<span class="glyph letters" aria-hidden="true">${escapeHtml(letters.toUpperCase())}</span>`;
123
- }
124
-
125
- /**
126
- * The state, as one of the design system's badge variants.
127
- *
128
- * "Gold for positive, neutral tokens otherwise" — so `active` is the only gold
129
- * one, and `unauthorized` is *neutral* rather than red. It is not an error: the
130
- * credential is absent from this target's store, which is a thing to do, and
131
- * `--destructive` is reserved for something having gone wrong. `disabled` is
132
- * quieter still, being a row that config no longer declares.
133
- */
134
- function statusPill(state: string): string {
135
- const kind = state === 'active' ? 'positive' : state === 'disabled' ? 'quiet' : 'neutral';
136
- return `<span class="pill ${kind}">${escapeHtml(state)}</span>`;
137
- }
138
-
139
- function connectionsSection(view: DashboardView): string {
140
- if (view.connections.length === 0) {
141
- return '<p class="empty">Nothing connected in this profile yet. Pick one below.</p>';
142
- }
143
-
144
- const rows = view.connections
145
- .map((connection) => {
146
- // A connection that is not active is one the reader can act on, and the
147
- // action is the same `connect` that made it — re-running against an
148
- // existing key repairs it rather than adding a second account. An active
149
- // one has nothing to do, so it carries no button.
150
- const repair =
151
- connection.state === 'active'
152
- ? ''
153
- : copyButton(command(view, `connect ${connection.key}`), connection.key);
154
-
155
- return (
156
- '<div class="row">' +
157
- mark(connection.provider) +
158
- `<code class="key">${escapeHtml(connection.key)}</code>` +
159
- statusPill(connection.state) +
160
- // The label reads, the account hovers: the row is one line wide and the
161
- // address is the longer of the two, so putting both in it wraps every
162
- // row to make one of them legible.
163
- `<span class="account"${
164
- connection.label && connection.label !== connection.account
165
- ? ` title="${escapeHtml(connection.account)}"`
166
- : ''
167
- }>${escapeHtml(connection.label ?? connection.account)}</span>` +
168
- repair +
169
- '</div>'
170
- );
171
- })
172
- .join('\n');
173
-
174
- return `<div class="rows">${rows}</div>`;
175
- }
176
-
177
- /**
178
- * One provider, one line: what it is called and the line that connects it.
179
- *
180
- * Everything else a card used to carry is gone — the description, whether it
181
- * opens a browser, who operates its OAuth client. None of it is lost: it is all
182
- * in `lanes link setup plan` and in the `setup_provider` capability, both of
183
- * which exist to answer "what does connecting this involve" at the length that
184
- * question deserves. This page answers a different one — what is there, and
185
- * what would I paste — and a paragraph per provider buried it.
186
- *
187
- * `plan.command` is built by the same `planFor` those two render, so the line
188
- * copied here and the line an agent suggests cannot drift.
189
- */
190
- function providerCard(plan: ProviderPlan): string {
191
- return (
192
- '<div class="row">' +
193
- mark(plan.id) +
194
- `<span class="name">${escapeHtml(plan.name)}</span>` +
195
- copyButton(plan.command, plan.name) +
196
- '</div>'
197
- );
198
- }
199
-
200
- function catalogue(view: DashboardView): { available: string; another: string } {
201
- const plans = planAll(PROVIDER_MANIFESTS, {
202
- profile: view.profile,
203
- connections: view.connections.map((connection) => connection.key),
204
- ownClients: view.ownClients,
205
- target: view.target,
206
- });
207
-
208
- // The same split `setup_overview` makes: never connected is an invitation,
209
- // and already connected is only an invitation when a second account means
210
- // something (ADR — `multiAccount` is the credential test, not a preference).
211
- const available = plans.filter((plan) => plan.connected.length === 0);
212
- const more = plans.filter((plan) => plan.connected.length > 0 && plan.multiAccount);
213
-
214
- return {
215
- available: available.map(providerCard).join('\n'),
216
- another: more.map(providerCard).join('\n'),
217
- };
218
- }
219
-
220
- function switcher(view: DashboardView): string {
221
- // Links, not a form: choosing a profile here changes what this page shows and
222
- // nothing else. The workspace default is config, and config is written from
223
- // the CLI.
224
- const profiles = view.profiles
225
- .map((name) => {
226
- const current = name === view.profile;
227
- const label = escapeHtml(name);
228
- return current
229
- ? `<span class="chip on">${label}</span>`
230
- : `<a class="chip" href="?profile=${encodeURIComponent(name)}">${label}</a>`;
231
- })
232
- .join('');
233
-
234
- // Every declared target, with the one whose adapters are actually open marked.
235
- // The others are real — they are where `--target` would write — but nothing on
236
- // this page is reading from them.
237
- const targets = view.targets
238
- .map((name) =>
239
- name === view.target
240
- ? `<span class="chip on">${escapeHtml(name)}</span>`
241
- : `<span class="chip off" title="declared, but not the one this endpoint opened">${escapeHtml(name)}</span>`,
242
- )
243
- .join('');
244
-
245
- return (
246
- '<div class="switch">' +
247
- `<div class="group"><span class="eyebrow">profile</span>${profiles}</div>` +
248
- `<div class="group"><span class="eyebrow">target</span>${targets}</div>` +
249
- '</div>'
250
- );
251
- }
252
-
253
- export function dashboardPage(view: DashboardView): Response {
254
- const { available, another } = catalogue(view);
255
-
256
- const body =
257
- `<h1>Lanes Link</h1>${switcher(view)}` +
258
- `<h2 class="eyebrow">Connections</h2>${connectionsSection(view)}` +
259
- (available ? `<h2 class="eyebrow">Available</h2><div class="rows">${available}</div>` : '') +
260
- (another ? `<h2 class="eyebrow">Connect another account</h2><div class="rows">${another}</div>` : '') +
261
- '<h2 class="eyebrow">Elsewhere</h2>' +
262
- '<p class="empty">What is reachable, and what is wrong with it:</p>' +
263
- commandLine(command(view, 'status')) +
264
- commandLine(command(view, 'doctor'));
265
-
266
- return shell(body, 'Lanes Link', 200);
267
- }
268
-
269
- /**
270
- * What an unauthenticated browser gets.
271
- *
272
- * The narrow card rather than the wide page, because it is one sentence — and
273
- * it names the command instead of asking for the token, since the command is
274
- * what puts the token in the URL in the first place. A password field here
275
- * would be a second way in to guard for no benefit: whoever can run the command
276
- * is already whoever the token would prove them to be.
277
- *
278
- * The one command on this page carries neither `--profile` nor `--target`,
279
- * against the rule every other command here follows. Both are names, and this
280
- * page answers before authentication — ADR-018 stopped `/health` naming
281
- * profiles to an anonymous caller for exactly that reason, and a 401 that
282
- * recites the workspace's profile list would put it back.
283
- */
284
- export function dashboardSignInPage(status: number): Response {
285
- return shell(
286
- '<div class="narrow"><h1>Not signed in</h1>' +
287
- '<p class="desc">This page opens from the terminal that is serving it.</p>' +
288
- commandLine('lanes link dashboard') +
289
- '</div>',
290
- 'Lanes Link',
291
- status,
292
- );
293
- }
@@ -1,125 +0,0 @@
1
- import { escapeHtml, FONTS, FOOTER, PAGE_CSP, PAGE_HEADERS, TOKENS } from './brand.ts';
2
-
3
- /**
4
- * The dashboard's chrome: its layout, its one listener, and the document they
5
- * hang in.
6
- *
7
- * Split from `dashboard-page.ts` because the two answer different questions —
8
- * that file decides what a row says, this one decides what a row looks like and
9
- * what the response is allowed to load. Everything shared with the two
10
- * authorization pages, which is every colour and every face, is in `brand.ts`;
11
- * what is left here is the one thing this surface does not share with them, a
12
- * wide list rather than a centred card.
13
- */
14
-
15
- const STYLE = `
16
- body { padding: 40px 24px; }
17
- .wrap { width: 100%; max-width: 620px; margin: 0 auto; }
18
- .narrow { text-align: center; padding: 32px 0; }
19
- h1 { margin: 0 0 20px; }
20
- h2 { margin: 30px 0 8px; }
21
- .desc { margin: 0 0 12px; font-size: 14px; line-height: 1.6; color: var(--muted-foreground); }
22
- .empty { margin: 0 0 12px; font-size: 14px; line-height: 1.6; color: var(--muted-foreground);
23
- opacity: 0.8; }
24
-
25
- /* Profile and target selectors. The current one is not gold: gold says a thing
26
- is good, and which profile you are looking at is not a verdict. */
27
- .switch { display: flex; flex-wrap: wrap; gap: 20px; padding-bottom: 4px; }
28
- .group { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; }
29
- .chip { display: inline-block; padding: 3px 10px; font-size: 13px; border-radius: 999px;
30
- border: 1px solid var(--border); color: var(--muted-foreground); text-decoration: none; }
31
- .chip:hover { color: var(--foreground); background: var(--muted); }
32
- .chip.on { color: var(--foreground); background: var(--muted); font-weight: 500; }
33
- .chip.off { border-style: dashed; opacity: 0.5; }
34
-
35
- /* One line per item: mark, name, whatever qualifies it, then the button hard
36
- right. \`min-width: 0\` on the row lets the name ellipsize rather than push
37
- the button off the end. */
38
- .rows { display: flex; flex-direction: column; }
39
- .row { display: flex; align-items: center; gap: 10px; min-width: 0;
40
- padding: 7px 10px 7px 4px; border-radius: 6px; }
41
- .row:hover { background: var(--muted); }
42
- .glyph { flex: none; display: inline-flex; align-items: center; justify-content: center;
43
- width: 17px; height: 17px; margin: 0 3px; opacity: 0.85; }
44
- /* The stand-in wants the box the real marks do not: bare letters read as a mark
45
- that failed to load, where a brand mark reads as itself. */
46
- .glyph.letters { width: 23px; height: 23px; margin: 0; border-radius: 6px; font-size: 8.5px;
47
- font-weight: 500; letter-spacing: 0.02em; color: var(--muted-foreground);
48
- border: 1px solid var(--border); opacity: 1; }
49
- .name, .key { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
50
- .name { font-size: 14px; }
51
- .key { font-family: var(--mono); font-size: 13px; }
52
- .account { font-size: 12.5px; color: var(--muted-foreground); opacity: 0.8; min-width: 0;
53
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
54
-
55
- /* Always visible, never revealed on hover: the button is the only way to get the
56
- command now that the line itself is not on the page, and an affordance you
57
- have to find by sweeping the mouse is one most readers never find. */
58
- .copy { margin-left: auto; opacity: 0.6; }
59
- .row:hover .copy { opacity: 1; }
60
- .copy:hover, .copy:focus-visible { opacity: 1; }
61
-
62
- /* The two places the command is still the label, so it stays visible. */
63
- .cmd { display: flex; align-items: center; gap: 8px; margin-top: 8px; padding: 8px 10px;
64
- background: var(--card); border: 1px dashed var(--border); border-radius: 10px; }
65
- .cmd code { flex: 1; font-size: 12.5px; line-height: 1.5; word-break: break-all; }
66
- .cmd .copy { opacity: 0.8; margin-left: 0; }
67
- `.trim();
68
-
69
- /**
70
- * The copy button, and nothing else.
71
- *
72
- * Inline because a separate file would be the first static asset this
73
- * repository serves, and one listener is not worth that. It reads the command
74
- * from a data attribute rather than from the DOM text so that what is copied is
75
- * exactly what was rendered, whitespace included.
76
- */
77
- const SCRIPT = `
78
- document.addEventListener('click', function (event) {
79
- var button = event.target.closest('.copy');
80
- if (!button || !navigator.clipboard) return;
81
- navigator.clipboard.writeText(button.dataset.copy).then(function () {
82
- button.textContent = 'copied';
83
- setTimeout(function () { button.textContent = 'copy'; }, 1200);
84
- });
85
- });
86
- `.trim();
87
-
88
- export function shell(inner: string, title: string, status: number): Response {
89
- return new Response(
90
- `<!doctype html>
91
- <html lang="en">
92
- <head>
93
- <meta charset="utf-8">
94
- <meta name="viewport" content="width=device-width, initial-scale=1">
95
- ${FONTS}
96
- <title>${escapeHtml(title)}</title>
97
- <style>
98
- ${TOKENS}
99
- ${STYLE}
100
- </style>
101
- </head>
102
- <body>
103
- <div class="wrap">
104
- ${inner}
105
- ${FOOTER}
106
- </div>
107
- <script>
108
- ${SCRIPT}
109
- </script>
110
- </body>
111
- </html>`,
112
- {
113
- status,
114
- headers: {
115
- ...PAGE_HEADERS,
116
- // The shared policy plus the one thing this page has that the others do
117
- // not: an inline listener, for the copy button.
118
- 'content-security-policy': `${PAGE_CSP}; script-src 'unsafe-inline'`,
119
- // Connection keys and account names are not something to leave in a
120
- // shared browser's back-forward cache.
121
- 'cache-control': 'no-store',
122
- },
123
- },
124
- );
125
- }
@@ -1,45 +0,0 @@
1
- /**
2
- * A brand mark per provider, drawn rather than fetched.
3
- *
4
- * Vendored from Simple Icons 16.28.0, which dedicates its paths to the public
5
- * domain under CC0-1.0. **The trademarks themselves remain their owners'** —
6
- * these identify the service a connection reaches, which is what nominative use
7
- * is for, and nothing here implies that any of them endorses this.
8
- *
9
- * Inlined because there is nowhere else to put them. The page is served under a
10
- * strict `default-src 'none'`, so it fetches nothing; the alternative — a CDN —
11
- * would tell a third party which dashboard the owner is looking at, every time
12
- * they look. There is also no static-asset pipeline to serve them from.
13
- *
14
- * Every path is one shape on a `0 0 24 24` canvas, filled with `currentColor`,
15
- * so a mark inherits the page's text colour and is correct in either theme
16
- * without a second copy. That is also why these are monochrome rather than the
17
- * full-colour originals: `callback-page.ts` reserves colour for status, and a
18
- * row of brand hues would spend it on decoration.
19
- *
20
- * Ids sharing a mark do so deliberately — `gmail` and `gmail_mcp` reach the same
21
- * mailbox by different transports, and the four `icloud_*` providers are one
22
- * service. The mark names the vendor; the name beside it names the product.
23
- *
24
- * Absent here, `dashboard-page.ts` falls back to a monogram. That covers every
25
- * provider an operator writes themselves, and the two shipped ones Simple Icons
26
- * does not carry: Slack, whose mark was withdrawn at the brand's request, and
27
- * Google Contacts, which it never had.
28
- */
29
- export const PROVIDER_MARKS: Readonly<Record<string, string>> = {
30
- calendar: 'M18.316 5.684H24v12.632h-5.684V5.684zM5.684 24h12.632v-5.684H5.684V24zM18.316 5.684V0H1.895A1.894 1.894 0 0 0 0 1.895v16.421h5.684V5.684h12.632zm-7.207 6.25v-.065c.272-.144.5-.349.687-.617s.279-.595.279-.982c0-.379-.099-.72-.3-1.025a2.05 2.05 0 0 0-.832-.714 2.703 2.703 0 0 0-1.197-.257c-.6 0-1.094.156-1.481.467-.386.311-.65.671-.793 1.078l1.085.452c.086-.249.224-.461.413-.633.189-.172.445-.257.767-.257.33 0 .602.088.816.264a.86.86 0 0 1 .322.703c0 .33-.12.589-.36.778-.24.19-.535.284-.886.284h-.567v1.085h.633c.407 0 .748.109 1.02.327.272.218.407.499.407.843 0 .336-.129.614-.387.832s-.565.327-.924.327c-.351 0-.651-.103-.897-.311-.248-.208-.422-.502-.521-.881l-1.096.452c.178.616.505 1.082.977 1.401.472.319.984.478 1.538.477a2.84 2.84 0 0 0 1.293-.291c.382-.193.684-.458.902-.794.218-.336.327-.72.327-1.149 0-.429-.115-.797-.344-1.105a2.067 2.067 0 0 0-.881-.689zm2.093-1.931l.602.913L15 10.045v5.744h1.187V8.446h-.827l-2.158 1.557zM22.105 0h-3.289v5.184H24V1.895A1.894 1.894 0 0 0 22.105 0zm-3.289 23.5l4.684-4.684h-4.684V23.5zM0 22.105C0 23.152.848 24 1.895 24h3.289v-5.184H0v3.289z',
31
- docs: 'M14.727 6.727H14V0H4.91c-.905 0-1.637.732-1.637 1.636v20.728c0 .904.732 1.636 1.636 1.636h14.182c.904 0 1.636-.732 1.636-1.636V6.727h-6zm-.545 10.455H7.09v-1.364h7.09v1.364zm2.727-3.273H7.091v-1.364h9.818v1.364zm0-3.273H7.091V9.273h9.818v1.363zM14.727 6h6l-6-6v6z',
32
- drive: 'M12.01 1.485c-2.082 0-3.754.02-3.743.047.01.02 1.708 3.001 3.774 6.62l3.76 6.574h3.76c2.081 0 3.753-.02 3.742-.047-.005-.02-1.708-3.001-3.775-6.62l-3.76-6.574zm-4.76 1.73a789.828 789.861 0 0 0-3.63 6.319L0 15.868l1.89 3.298 1.885 3.297 3.62-6.335 3.618-6.33-1.88-3.287C8.1 4.704 7.255 3.22 7.25 3.214zm2.259 12.653-.203.348c-.114.198-.96 1.672-1.88 3.287a423.93 423.948 0 0 1-1.698 2.97c-.01.026 3.24.042 7.222.042h7.244l1.796-3.157c.992-1.734 1.85-3.23 1.906-3.323l.104-.167h-7.249z',
33
- drive_mcp: 'M12.01 1.485c-2.082 0-3.754.02-3.743.047.01.02 1.708 3.001 3.774 6.62l3.76 6.574h3.76c2.081 0 3.753-.02 3.742-.047-.005-.02-1.708-3.001-3.775-6.62l-3.76-6.574zm-4.76 1.73a789.828 789.861 0 0 0-3.63 6.319L0 15.868l1.89 3.298 1.885 3.297 3.62-6.335 3.618-6.33-1.88-3.287C8.1 4.704 7.255 3.22 7.25 3.214zm2.259 12.653-.203.348c-.114.198-.96 1.672-1.88 3.287a423.93 423.948 0 0 1-1.698 2.97c-.01.026 3.24.042 7.222.042h7.244l1.796-3.157c.992-1.734 1.85-3.23 1.906-3.323l.104-.167h-7.249z',
34
- github: 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',
35
- gmail: 'M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z',
36
- gmail_mcp: 'M24 5.457v13.909c0 .904-.732 1.636-1.636 1.636h-3.819V11.73L12 16.64l-6.545-4.91v9.273H1.636A1.636 1.636 0 0 1 0 19.366V5.457c0-2.023 2.309-3.178 3.927-1.964L5.455 4.64 12 9.548l6.545-4.91 1.528-1.145C21.69 2.28 24 3.434 24 5.457z',
37
- icloud_calendar: 'M13.762 4.29a6.51 6.51 0 0 0-5.669 3.332 3.571 3.571 0 0 0-1.558-.36 3.571 3.571 0 0 0-3.516 3A4.918 4.918 0 0 0 0 14.796a4.918 4.918 0 0 0 4.92 4.914 4.93 4.93 0 0 0 .617-.045h14.42c2.305-.272 4.041-2.258 4.043-4.589v-.009a4.594 4.594 0 0 0-3.727-4.508 6.51 6.51 0 0 0-6.511-6.27z',
38
- icloud_contacts: 'M13.762 4.29a6.51 6.51 0 0 0-5.669 3.332 3.571 3.571 0 0 0-1.558-.36 3.571 3.571 0 0 0-3.516 3A4.918 4.918 0 0 0 0 14.796a4.918 4.918 0 0 0 4.92 4.914 4.93 4.93 0 0 0 .617-.045h14.42c2.305-.272 4.041-2.258 4.043-4.589v-.009a4.594 4.594 0 0 0-3.727-4.508 6.51 6.51 0 0 0-6.511-6.27z',
39
- icloud_drive: 'M13.762 4.29a6.51 6.51 0 0 0-5.669 3.332 3.571 3.571 0 0 0-1.558-.36 3.571 3.571 0 0 0-3.516 3A4.918 4.918 0 0 0 0 14.796a4.918 4.918 0 0 0 4.92 4.914 4.93 4.93 0 0 0 .617-.045h14.42c2.305-.272 4.041-2.258 4.043-4.589v-.009a4.594 4.594 0 0 0-3.727-4.508 6.51 6.51 0 0 0-6.511-6.27z',
40
- icloud_mail: 'M13.762 4.29a6.51 6.51 0 0 0-5.669 3.332 3.571 3.571 0 0 0-1.558-.36 3.571 3.571 0 0 0-3.516 3A4.918 4.918 0 0 0 0 14.796a4.918 4.918 0 0 0 4.92 4.914 4.93 4.93 0 0 0 .617-.045h14.42c2.305-.272 4.041-2.258 4.043-4.589v-.009a4.594 4.594 0 0 0-3.727-4.508 6.51 6.51 0 0 0-6.511-6.27z',
41
- linear: 'M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z',
42
- notion: 'M4.459 4.208c.746.606 1.026.56 2.428.466l13.215-.793c.28 0 .047-.28-.046-.326L17.86 1.968c-.42-.326-.981-.7-2.055-.607L3.01 2.295c-.466.046-.56.28-.374.466zm.793 3.08v13.904c0 .747.373 1.027 1.214.98l14.523-.84c.841-.046.935-.56.935-1.167V6.354c0-.606-.233-.933-.748-.887l-15.177.887c-.56.047-.747.327-.747.933zm14.337.745c.093.42 0 .84-.42.888l-.7.14v10.264c-.608.327-1.168.514-1.635.514-.748 0-.935-.234-1.495-.933l-4.577-7.186v6.952L12.21 19s0 .84-1.168.84l-3.222.186c-.093-.186 0-.653.327-.746l.84-.233V9.854L7.822 9.76c-.094-.42.14-1.026.793-1.073l3.456-.233 4.764 7.279v-6.44l-1.215-.139c-.093-.514.28-.887.747-.933zM1.936 1.035l13.31-.98c1.634-.14 2.055-.047 3.082.7l4.249 2.986c.7.513.934.653.934 1.213v16.378c0 1.026-.373 1.634-1.68 1.726l-15.458.934c-.98.047-1.448-.093-1.962-.747l-3.129-4.06c-.56-.747-.793-1.306-.793-1.96V2.667c0-.839.374-1.54 1.447-1.632z',
43
- sheets: 'M11.318 12.545H7.91v-1.909h3.41v1.91zM14.728 0v6h6l-6-6zm1.363 10.636h-3.41v1.91h3.41v-1.91zm0 3.273h-3.41v1.91h3.41v-1.91zM20.727 6.5v15.864c0 .904-.732 1.636-1.636 1.636H4.909a1.636 1.636 0 0 1-1.636-1.636V1.636C3.273.732 4.005 0 4.909 0h9.318v6.5h6.5zm-3.273 2.773H6.545v7.909h10.91v-7.91zm-6.136 4.636H7.91v1.91h3.41v-1.91z',
44
- google_tasks: 'M11.383.617C5.097.617 0 5.714 0 12c0 6.286 5.097 11.383 11.383 11.383 6.286 0 11.38-5.097 11.38-11.383a11.34 11.34 0 0 0-.878-4.389l-3.203 3.203c.062.387.1.782.1 1.186a7.398 7.398 0 1 1-7.4-7.398c1.499 0 2.889.448 4.054 1.214l2.857-2.857a11.325 11.325 0 0 0-6.91-2.342zm9.674.756c-.292 0-.583.112-.805.334-2.97 2.965-5.934 5.934-8.9 8.902L9.596 8.854a1.139 1.139 0 0 0-1.61 0l-1.775 1.773a1.139 1.139 0 0 0 0 1.61l4.166 4.163a1.421 1.421 0 0 0 2.012 0L23.666 5.121a1.136 1.136 0 0 0 0-1.61l-1.805-1.804a1.136 1.136 0 0 0-.804-.334z',
45
- };