@monster-orange/den 0.4.20 → 0.4.22
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/CHANGELOG.md +164 -0
- package/README.md +7 -7
- package/docs/distributions.md +167 -19
- package/package.json +1 -1
- package/service/install.sh +213 -63
- package/src/shared/agents/den-claude.ts +13 -1
- package/src/shared/agents/records.ts +6 -0
- package/src/shared/agents/runtime.ts +6 -0
- package/src/shared/agents/session-name.ts +21 -1
- package/src/shared/attest-code.ts +125 -0
- package/src/shared/claim-code.ts +6 -2
- package/src/{ui/server/edge-exposure.ts → shared/edge/exposure.ts} +7 -1
- package/src/shared/github-app.ts +4 -0
- package/src/shared/github-device.ts +38 -1
- package/src/shared/launchd-session.ts +92 -0
- package/src/shared/owner-github.ts +100 -0
- package/src/shared/product.ts +45 -5
- package/src/shared/profile.ts +26 -2
- package/src/shared/sessions/client.ts +24 -0
- package/src/shared/sessions/host.ts +114 -5
- package/src/shared/sessions/types.ts +5 -0
- package/src/shared/web-session.ts +112 -6
- package/src/ui/cli/agent-ops.ts +20 -2
- package/src/ui/cli/agents-command.ts +32 -1
- package/src/ui/cli/create.ts +92 -6
- package/src/ui/cli/doctor-command.ts +126 -15
- package/src/ui/cli/doctor-owner.ts +122 -0
- package/src/ui/cli/doctor-where.ts +203 -0
- package/src/ui/cli/index.ts +1 -1
- package/src/ui/cli/service-command.ts +233 -11
- package/src/ui/cli/sessions-command.ts +46 -12
- package/src/ui/cli/web-command.ts +78 -12
- package/src/ui/server/device-flows.ts +45 -10
- package/src/ui/server/github-routes.ts +169 -16
- package/src/ui/server/index.ts +147 -12
- package/src/ui/server/public/agents-page.js +103 -6
- package/src/ui/server/public/agents.html +30 -0
- package/src/ui/server/public/error-page.js +115 -33
- package/src/ui/server/public/error.html +16 -0
- package/src/ui/server/public/header.js +5 -7
- package/src/ui/server/public/i18n/es.js +24 -1
- package/src/ui/server/public/i18n/ja.js +24 -1
- package/src/ui/server/public/settings-registry.js +1 -1
- package/src/ui/server/public/shell.js +24 -1
- package/src/ui/server/route-classes.ts +7 -0
- package/src/ui/server/session-routes.ts +4 -0
|
@@ -347,6 +347,25 @@ function wsLinkHtml(ws) {
|
|
|
347
347
|
return `<a class="wslink" href="${esc(wsHref(ws))}" title="${esc(t("ag.goWs", { ws }))}">${esc(ws)}</a>`;
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
/**
|
|
351
|
+
* D20: where its session runs, as a tag: "Stops at logout" (the desktop
|
|
352
|
+
* session) or "No GUI" (outside it: kept across logout). `button`: a
|
|
353
|
+
* button that says why when tapped (the pane bar); else a span (a row,
|
|
354
|
+
* itself a button). "" when the host didn't say (off macOS, older host).
|
|
355
|
+
*/
|
|
356
|
+
function whereTag(r, button = false) {
|
|
357
|
+
const s = r?.launchd;
|
|
358
|
+
const logout = s === "Aqua" || s === "LoginWindow";
|
|
359
|
+
if (!logout && s !== "Background" && s !== "System") return "";
|
|
360
|
+
const key = logout ? "ag.where.logout" : "ag.where.bg";
|
|
361
|
+
const tip = t(`${key}Tip`, { cli: CLI });
|
|
362
|
+
const cls = `tagx${logout ? " warn" : ""}`;
|
|
363
|
+
const where = logout ? "logout" : "bg";
|
|
364
|
+
return button
|
|
365
|
+
? `<button type="button" class="${cls}" data-act="where" data-where="${where}" title="${esc(tip)}" aria-label="${esc(`${t(key)}: ${tip}`)}">${logout ? "⚠ " : ""}${esc(t(key))}</button>`
|
|
366
|
+
: `<span class="${cls}" data-where="${where}" title="${esc(tip)}">${logout ? "⚠ " : ""}${esc(t(key))}</span>`;
|
|
367
|
+
}
|
|
368
|
+
|
|
350
369
|
/** Its rotation, when one is due: "ready" or "due" (den#4). */
|
|
351
370
|
function rotationOf(r) {
|
|
352
371
|
if (!r.rotation?.due) return "";
|
|
@@ -370,6 +389,8 @@ function stoppedHtml(r) {
|
|
|
370
389
|
: "";
|
|
371
390
|
const fresh = `<button type="button" class="den-btn sm" data-act="fresh" aria-label="${esc(t("ag.freshName", { id: r.agent }))}">${esc(t("ag.fresh"))}</button>`;
|
|
372
391
|
const remove = `<button type="button" class="den-btn sm" data-act="remove" aria-label="${esc(t("ag.removeName", { id: r.agent }))}">${esc(t("ag.remove"))}</button>`;
|
|
392
|
+
// D25: an agent with no pane open can be renamed too.
|
|
393
|
+
const rename = `<button type="button" class="den-btn sm" data-act="rename" aria-label="${esc(t("ag.renameName", { id: r.agent }))}">${esc(t("ag.rn.act"))}</button>`;
|
|
373
394
|
// A rotation due without a handoff that passes: say so (den#4).
|
|
374
395
|
const handoff =
|
|
375
396
|
rot === "due"
|
|
@@ -400,7 +421,7 @@ function stoppedHtml(r) {
|
|
|
400
421
|
(meta ? `<span class="meta">${esc(meta)}</span>` : "") +
|
|
401
422
|
`<span class="why">${esc(stopReason(r))}</span>` +
|
|
402
423
|
`</button>` +
|
|
403
|
-
`<div class="agacts">${handoff}${cant}${relaunch}${resume}${fresh}${remove}</div>` +
|
|
424
|
+
`<div class="agacts">${handoff}${cant}${relaunch}${resume}${fresh}${rename}${remove}</div>` +
|
|
404
425
|
`</div>`
|
|
405
426
|
);
|
|
406
427
|
}
|
|
@@ -428,9 +449,10 @@ function rowHtml(r) {
|
|
|
428
449
|
? `<span class="tagx warn" title="${esc(t("ag.revokedTip"))}">${esc(t("ag.revoked"))}</span>`
|
|
429
450
|
: "";
|
|
430
451
|
const ovTags = r.kind === "agent" ? overrideTagsHtml(r) : "";
|
|
452
|
+
const wTag = whereTag(r);
|
|
431
453
|
const tags =
|
|
432
|
-
rotTag || revokedTag || ovTags
|
|
433
|
-
? `<span class="agtags">${revokedTag}${rotTag}${ovTags}</span>`
|
|
454
|
+
rotTag || revokedTag || ovTags || wTag
|
|
455
|
+
? `<span class="agtags">${revokedTag}${rotTag}${wTag}${ovTags}</span>`
|
|
434
456
|
: "";
|
|
435
457
|
return (
|
|
436
458
|
`<button type="button" class="agr" data-id="${esc(r.id)}" data-kind="${esc(r.kind)}" data-state="${esc(r.state)}"` +
|
|
@@ -976,9 +998,10 @@ function paintBar(p) {
|
|
|
976
998
|
const reconnect = p.lost
|
|
977
999
|
? `<button type="button" class="den-btn sm" data-act="reconnect">${esc(t("ag.reconnect"))}</button>`
|
|
978
1000
|
: "";
|
|
979
|
-
// AG-18: ⋯, its menu: Change persona… (a
|
|
1001
|
+
// AG-18: ⋯, its menu: Rename… (D25, every agent), Change persona… (a
|
|
1002
|
+
// runtime that can resume).
|
|
980
1003
|
const more =
|
|
981
|
-
r?.kind === "agent"
|
|
1004
|
+
r?.kind === "agent"
|
|
982
1005
|
? `<button type="button" class="den-btn sm" data-act="more" aria-haspopup="menu" aria-expanded="false" aria-label="${esc(t("ag.more", { id: r.agent }))}" title="${esc(t("ag.more", { id: r.agent }))}">⋯</button>`
|
|
983
1006
|
: "";
|
|
984
1007
|
bar.innerHTML =
|
|
@@ -987,6 +1010,7 @@ function paintBar(p) {
|
|
|
987
1010
|
(r?.tokenRevoked
|
|
988
1011
|
? `<span class="tagx warn" title="${esc(t("ag.revokedTip"))}">${esc(t("ag.revoked"))}</span>`
|
|
989
1012
|
: "") +
|
|
1013
|
+
whereTag(r, true) +
|
|
990
1014
|
`<span class="x">${reconnect}${stop}${resume}${more}<button type="button" class="den-btn sm" data-act="close" aria-label="${esc(t("ag.detachName", { id: name }))}" title="${esc(t("ag.detach"))}">×</button></span>`;
|
|
991
1015
|
// Tabs: the tab's × closes it; the pane's bar has none.
|
|
992
1016
|
const x = bar.querySelector('[data-act="close"]');
|
|
@@ -2126,6 +2150,63 @@ function goLabel() {
|
|
|
2126
2150
|
return t(sd.change ? "ag.pc.go" : sd.bring ? "ag.bring.go" : "ag.go");
|
|
2127
2151
|
}
|
|
2128
2152
|
|
|
2153
|
+
/**
|
|
2154
|
+
* D25: the Rename dialog for agent row `r`: its name (empty: back to its
|
|
2155
|
+
* id), and the name other sessions will see it by (D24).
|
|
2156
|
+
*/
|
|
2157
|
+
function openRename(r, opener) {
|
|
2158
|
+
const dlg = $d("#ag-rn");
|
|
2159
|
+
if (!dlg || dlg.open) return;
|
|
2160
|
+
const input = $d("#ag-rn-name");
|
|
2161
|
+
const help = $d("#ag-rn-help");
|
|
2162
|
+
const err = $d("#ag-rn-err");
|
|
2163
|
+
$d("#ag-rn-h").textContent = t("ag.rn.title", { id: r.agent });
|
|
2164
|
+
$d("#ag-rn-t").textContent = t("ag.rn.name");
|
|
2165
|
+
$d("#ag-rn-go").textContent = t("ag.rn.go");
|
|
2166
|
+
$d("#ag-rn-cancel").textContent = t("ag.rn.cancel");
|
|
2167
|
+
input.value = r.displayName || "";
|
|
2168
|
+
input.placeholder = r.agent;
|
|
2169
|
+
const paintHelp = () => {
|
|
2170
|
+
const name = input.value.trim() || r.agent;
|
|
2171
|
+
help.textContent = t("ag.rn.help", {
|
|
2172
|
+
id: r.agent,
|
|
2173
|
+
peer: `${CLI}-${r.workspace}:${name}`,
|
|
2174
|
+
});
|
|
2175
|
+
};
|
|
2176
|
+
paintHelp();
|
|
2177
|
+
err.hidden = true;
|
|
2178
|
+
input.oninput = paintHelp;
|
|
2179
|
+
const close = () => {
|
|
2180
|
+
dlg.close?.();
|
|
2181
|
+
dlg.removeAttribute("open");
|
|
2182
|
+
opener?.focus?.();
|
|
2183
|
+
};
|
|
2184
|
+
$d("#ag-rn-cancel").onclick = close;
|
|
2185
|
+
$d("#ag-rn-form").onsubmit = async (e) => {
|
|
2186
|
+
e.preventDefault();
|
|
2187
|
+
const name = input.value.trim();
|
|
2188
|
+
const go = $d("#ag-rn-go");
|
|
2189
|
+
go.disabled = true;
|
|
2190
|
+
try {
|
|
2191
|
+
await api("POST", agentPath(r, "name"), {
|
|
2192
|
+
name: name && name !== r.agent ? name : null,
|
|
2193
|
+
});
|
|
2194
|
+
close();
|
|
2195
|
+
await refresh();
|
|
2196
|
+
paintLayout();
|
|
2197
|
+
} catch (x) {
|
|
2198
|
+
err.textContent = x?.message || String(x);
|
|
2199
|
+
err.hidden = false;
|
|
2200
|
+
} finally {
|
|
2201
|
+
go.disabled = false;
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
if (typeof dlg.showModal === "function") dlg.showModal();
|
|
2205
|
+
else dlg.setAttribute("open", "");
|
|
2206
|
+
input.focus();
|
|
2207
|
+
input.select();
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2129
2210
|
/** AG-18: Change persona… for agent row `r` (its persona, model, name). */
|
|
2130
2211
|
function openPersonaChange(r) {
|
|
2131
2212
|
return openStart(false, {
|
|
@@ -2708,6 +2789,7 @@ export async function mountAgentsPage(root) {
|
|
|
2708
2789
|
if (which === "resume" || which === "fresh" || which === "relaunch")
|
|
2709
2790
|
void restartRow(r, which, act);
|
|
2710
2791
|
else if (which === "remove") void removeRow(r, act);
|
|
2792
|
+
else if (which === "rename") openRename(r, act);
|
|
2711
2793
|
return;
|
|
2712
2794
|
}
|
|
2713
2795
|
const b = e.target.closest(".agr");
|
|
@@ -2738,6 +2820,11 @@ export async function mountAgentsPage(root) {
|
|
|
2738
2820
|
const id = b.closest(".pane")?.dataset.id ?? b.dataset.id;
|
|
2739
2821
|
const p = id && st.panes.get(id);
|
|
2740
2822
|
if (!p) return;
|
|
2823
|
+
if (b.dataset.act === "where") {
|
|
2824
|
+
// D20: why it stops at logout (or what it lacks), for touch too.
|
|
2825
|
+
window.denShell?.toast?.(b.title);
|
|
2826
|
+
return;
|
|
2827
|
+
}
|
|
2741
2828
|
if (b.dataset.act === "close") closePane(id);
|
|
2742
2829
|
else if (b.dataset.act === "stop") void stopAgent(p, b);
|
|
2743
2830
|
else if (b.dataset.act === "resume") {
|
|
@@ -2750,7 +2837,17 @@ export async function mountAgentsPage(root) {
|
|
|
2750
2837
|
window.denShell?.openMenu?.(b, {
|
|
2751
2838
|
label: t("ag.more", { id: r.agent }),
|
|
2752
2839
|
items: [
|
|
2753
|
-
|
|
2840
|
+
// D25: a visible Rename, for touch and keyboard (not only a
|
|
2841
|
+
// double-click on a tab).
|
|
2842
|
+
{ label: t("ag.rn.item"), onSelect: () => openRename(r, b) },
|
|
2843
|
+
...(r.personaChange
|
|
2844
|
+
? [
|
|
2845
|
+
{
|
|
2846
|
+
label: t("ag.pc.item"),
|
|
2847
|
+
onSelect: () => void openPersonaChange(r),
|
|
2848
|
+
},
|
|
2849
|
+
]
|
|
2850
|
+
: []),
|
|
2754
2851
|
],
|
|
2755
2852
|
});
|
|
2756
2853
|
}
|
|
@@ -412,6 +412,16 @@
|
|
|
412
412
|
flex: none;
|
|
413
413
|
white-space: nowrap;
|
|
414
414
|
}
|
|
415
|
+
/* D20: the launchd tag is a button (tap: why); it looks like a tag. */
|
|
416
|
+
button.tagx {
|
|
417
|
+
background: none;
|
|
418
|
+
cursor: pointer;
|
|
419
|
+
line-height: inherit;
|
|
420
|
+
}
|
|
421
|
+
button.tagx:focus-visible {
|
|
422
|
+
outline: 2px solid var(--accent);
|
|
423
|
+
outline-offset: 1px;
|
|
424
|
+
}
|
|
415
425
|
.wslink {
|
|
416
426
|
color: inherit;
|
|
417
427
|
text-decoration: none;
|
|
@@ -771,6 +781,26 @@
|
|
|
771
781
|
</div>
|
|
772
782
|
</form>
|
|
773
783
|
</dialog>
|
|
784
|
+
<!-- D25: Rename, from a pane's ⋯ or a stopped agent's row. -->
|
|
785
|
+
<dialog class="ag-dlg" id="ag-rn" aria-labelledby="ag-rn-h">
|
|
786
|
+
<form method="dialog" id="ag-rn-form" novalidate>
|
|
787
|
+
<h2 id="ag-rn-h">Rename</h2>
|
|
788
|
+
<label class="fld"
|
|
789
|
+
><span id="ag-rn-t">Name</span>
|
|
790
|
+
<input id="ag-rn-name" maxlength="40" autocomplete="off" />
|
|
791
|
+
</label>
|
|
792
|
+
<p class="help" id="ag-rn-help"></p>
|
|
793
|
+
<p class="err" id="ag-rn-err" role="alert" hidden></p>
|
|
794
|
+
<div class="btns">
|
|
795
|
+
<button type="button" class="den-btn" id="ag-rn-cancel">
|
|
796
|
+
Cancel
|
|
797
|
+
</button>
|
|
798
|
+
<button type="submit" class="den-btn primary" id="ag-rn-go">
|
|
799
|
+
Rename
|
|
800
|
+
</button>
|
|
801
|
+
</div>
|
|
802
|
+
</form>
|
|
803
|
+
</dialog>
|
|
774
804
|
|
|
775
805
|
<script type="module">
|
|
776
806
|
import {
|
|
@@ -268,7 +268,8 @@ export function landingAfterSignIn(loc) {
|
|
|
268
268
|
*/
|
|
269
269
|
function githubBlock(s, t) {
|
|
270
270
|
const g = s.github;
|
|
271
|
-
if (!g || s.localhost || g.state === "no_client_id"
|
|
271
|
+
if (!g || s.localhost || g.state === "no_client_id" || g.state === "claim")
|
|
272
|
+
return "";
|
|
272
273
|
const host = s.machine || "";
|
|
273
274
|
const ready = g.state === "ready";
|
|
274
275
|
const step1 = withHost(t, "err.signin.ghStep1", {
|
|
@@ -291,6 +292,9 @@ function githubBlock(s, t) {
|
|
|
291
292
|
`<button type="button" class="btn primary" id="den-gh-start">${esc(t("err.signin.github"))}</button>` +
|
|
292
293
|
`</div><div id="den-gh-flow" aria-live="polite"></div>`
|
|
293
294
|
: "") +
|
|
295
|
+
(!ready && g.message
|
|
296
|
+
? `<p class="den-gh-note" id="den-gh-norule">${esc(g.message)}</p>`
|
|
297
|
+
: "") +
|
|
294
298
|
`<p class="den-gh-note" id="den-gh-only">${esc(t("err.signin.ghOnly"))}` +
|
|
295
299
|
(g.learnMore
|
|
296
300
|
? ` <a id="den-gh-learn" href="${esc(g.learnMore)}" target="_blank" rel="noopener noreferrer">${esc(t("err.signin.ghLearn"))}</a>`
|
|
@@ -299,6 +303,57 @@ function githubBlock(s, t) {
|
|
|
299
303
|
);
|
|
300
304
|
}
|
|
301
305
|
|
|
306
|
+
/**
|
|
307
|
+
* D17: a Den with no owner yet that a rule lets someone claim. It leads the
|
|
308
|
+
* page: Connect your GitHub account. "This is my computer" (with
|
|
309
|
+
* web.allowed_github_org) goes to localhost and back, which only works in a
|
|
310
|
+
* browser on the machine Den runs on, then starts the GitHub sign-in with
|
|
311
|
+
* that proof; "Sign in as @login" (web.owner_github) works from anywhere.
|
|
312
|
+
*/
|
|
313
|
+
function claimBlock(s, t) {
|
|
314
|
+
const g = s.github;
|
|
315
|
+
if (!g || g.state !== "claim" || s.localhost) return "";
|
|
316
|
+
const host = s.machine || "";
|
|
317
|
+
const back = location.origin + location.pathname + location.search;
|
|
318
|
+
const here = g.attest ? `${g.attest}?return=${encodeURIComponent(back)}` : "";
|
|
319
|
+
return (
|
|
320
|
+
`<div id="den-gh" class="den-claim">` +
|
|
321
|
+
`<h2 class="den-h" id="den-claim-title">${esc(t("err.signin.claimTitle"))}</h2>` +
|
|
322
|
+
`<p class="lead" id="den-claim-lead">${esc(t("err.signin.claimLead", { name: s.name || "Den" }))}</p>` +
|
|
323
|
+
(g.org
|
|
324
|
+
? `<p id="den-claim-org">${withHost(t, "err.signin.claimOrg", { org: g.org, host })}</p>`
|
|
325
|
+
: "") +
|
|
326
|
+
`<div class="acts" style="margin-top:0">` +
|
|
327
|
+
(here
|
|
328
|
+
? `<a class="btn primary" id="den-claim-here" href="${esc(here)}">${esc(t("err.signin.claimHere"))}</a>`
|
|
329
|
+
: "") +
|
|
330
|
+
(g.ownerGithub
|
|
331
|
+
? `<button type="button" class="btn${here ? "" : " primary"}" id="den-gh-start">${esc(t("err.signin.claimAs", { login: g.ownerGithub }))}</button>`
|
|
332
|
+
: "") +
|
|
333
|
+
`</div><div id="den-gh-flow" aria-live="polite"></div>` +
|
|
334
|
+
(here
|
|
335
|
+
? `<p class="den-gh-note" id="den-claim-note">${withHost(t, "err.signin.claimHereNote", { host })}</p>`
|
|
336
|
+
: "") +
|
|
337
|
+
`</div>`
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** D17: the "This is my computer" code /attest put in the fragment, taken once. */
|
|
342
|
+
export function takeAttestCode(loc = location, hist = history) {
|
|
343
|
+
const m = /(?:^#|&)den-attest=([^&]+)/.exec(loc.hash || "");
|
|
344
|
+
if (!m) return "";
|
|
345
|
+
try {
|
|
346
|
+
hist.replaceState(null, "", loc.pathname + loc.search);
|
|
347
|
+
} catch {
|
|
348
|
+
/* the address keeps it; it is single use anyway */
|
|
349
|
+
}
|
|
350
|
+
try {
|
|
351
|
+
return decodeURIComponent(m[1]);
|
|
352
|
+
} catch {
|
|
353
|
+
return "";
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
302
357
|
/** How often, and how long, the page watches for an owner (SEC-2a-UX). */
|
|
303
358
|
export const OWNER_POLL_MS = 5000;
|
|
304
359
|
const OWNER_WATCH_MS = 30 * 60 * 1000;
|
|
@@ -332,17 +387,23 @@ function watchOwner(root, t, s, pollMs = OWNER_POLL_MS) {
|
|
|
332
387
|
setTimeout(tick, pollMs);
|
|
333
388
|
}
|
|
334
389
|
|
|
335
|
-
/**
|
|
336
|
-
|
|
390
|
+
/**
|
|
391
|
+
* The device flow on the page: start, show the code, poll, go on. D17: with
|
|
392
|
+
* `attest` (the code "This is my computer" brought back), it starts at once
|
|
393
|
+
* with that proof.
|
|
394
|
+
*/
|
|
395
|
+
function wireGithub(root, t, s, attest = "") {
|
|
337
396
|
const start = root.querySelector("#den-gh-start");
|
|
338
397
|
const box = root.querySelector("#den-gh-flow");
|
|
339
|
-
if (!
|
|
398
|
+
if (!box || (!start && !attest)) return;
|
|
340
399
|
let timer = null;
|
|
341
400
|
const show = (html) => (box.innerHTML = html);
|
|
342
401
|
const fail = (text) => {
|
|
343
402
|
clearTimeout(timer);
|
|
344
|
-
start
|
|
345
|
-
|
|
403
|
+
if (start) {
|
|
404
|
+
start.hidden = false;
|
|
405
|
+
start.textContent = t("err.signin.githubAgain");
|
|
406
|
+
}
|
|
346
407
|
show(
|
|
347
408
|
`<p class="den-gh-err" role="alert" style="margin-top:12px">${esc(text)}</p>`,
|
|
348
409
|
);
|
|
@@ -367,29 +428,31 @@ function wireGithub(root, t, s) {
|
|
|
367
428
|
return;
|
|
368
429
|
}
|
|
369
430
|
fail(
|
|
370
|
-
b.state === "refused"
|
|
371
|
-
?
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
: b.state === "
|
|
378
|
-
? t("err.signin.
|
|
379
|
-
:
|
|
431
|
+
b.state === "refused" && b.error
|
|
432
|
+
? b.error
|
|
433
|
+
: b.state === "refused"
|
|
434
|
+
? t("err.signin.githubRefused", {
|
|
435
|
+
login: b.login ?? "",
|
|
436
|
+
name: s.name,
|
|
437
|
+
})
|
|
438
|
+
: b.state === "expired"
|
|
439
|
+
? t("err.signin.githubExpired")
|
|
440
|
+
: b.state === "denied"
|
|
441
|
+
? t("err.signin.githubDenied")
|
|
442
|
+
: t("err.signin.githubFailed"),
|
|
380
443
|
);
|
|
381
444
|
})
|
|
382
445
|
.catch(() => fail(t("err.signin.githubFailed")));
|
|
383
446
|
};
|
|
384
|
-
|
|
447
|
+
const begin = (body) => {
|
|
385
448
|
clearTimeout(timer);
|
|
386
|
-
start.disabled = true;
|
|
449
|
+
if (start) start.disabled = true;
|
|
387
450
|
fetch("/api/github/signin", {
|
|
388
451
|
method: "POST",
|
|
389
452
|
cache: "no-store",
|
|
390
453
|
credentials: "same-origin",
|
|
391
454
|
headers: { "Content-Type": "application/json" },
|
|
392
|
-
body:
|
|
455
|
+
body: JSON.stringify(body),
|
|
393
456
|
})
|
|
394
457
|
.then((r) =>
|
|
395
458
|
r
|
|
@@ -398,7 +461,7 @@ function wireGithub(root, t, s) {
|
|
|
398
461
|
.then((b) => [r.status, b]),
|
|
399
462
|
)
|
|
400
463
|
.then(([status, b]) => {
|
|
401
|
-
start.disabled = false;
|
|
464
|
+
if (start) start.disabled = false;
|
|
402
465
|
if (!b.ok || !b.userCode) {
|
|
403
466
|
fail(
|
|
404
467
|
status === 429
|
|
@@ -407,7 +470,11 @@ function wireGithub(root, t, s) {
|
|
|
407
470
|
);
|
|
408
471
|
return;
|
|
409
472
|
}
|
|
410
|
-
start.hidden = true;
|
|
473
|
+
if (start) start.hidden = true;
|
|
474
|
+
const here = root.querySelector("#den-claim-here");
|
|
475
|
+
if (here) here.style.display = "none";
|
|
476
|
+
const note = root.querySelector("#den-claim-note");
|
|
477
|
+
if (note) note.style.display = "none";
|
|
411
478
|
const url = b.verificationUri || "https://github.com/login/device";
|
|
412
479
|
const shown = url.replace(/^https:\/\//, "");
|
|
413
480
|
show(
|
|
@@ -417,21 +484,30 @@ function wireGithub(root, t, s) {
|
|
|
417
484
|
url: `<a href="${esc(url)}" target="_blank" rel="noopener noreferrer">${esc(shown)}</a>`,
|
|
418
485
|
},
|
|
419
486
|
)}</p>` +
|
|
420
|
-
|
|
487
|
+
// The code and its copy button on one line, as every command.
|
|
488
|
+
cmdLine("den-gh-code", b.userCode, t).replace(
|
|
489
|
+
'class="den-cmd"',
|
|
490
|
+
'class="den-cmd den-gh-coderow"',
|
|
491
|
+
) +
|
|
421
492
|
`<div class="acts" style="margin-top:0">` +
|
|
422
|
-
`<a class="btn primary" id="den-gh-open" href="${esc(url)}" target="_blank" rel="noopener noreferrer">${esc(t("err.signin.githubOpen"))}</a>` +
|
|
423
|
-
`<button type="button" class="btn" id="den-gh-copy">${esc(t("err.signin.githubCopy"))}</button></div>` +
|
|
493
|
+
`<a class="btn primary" id="den-gh-open" href="${esc(url)}" target="_blank" rel="noopener noreferrer">${esc(t("err.signin.githubOpen"))}</a></div>` +
|
|
424
494
|
`<p class="den-gh-wait" style="margin-top:12px">${esc(t("err.signin.githubWaiting"))}</p>`,
|
|
425
495
|
);
|
|
426
|
-
|
|
427
|
-
cp?.addEventListener("click", () => copy(b.userCode, cp, t));
|
|
496
|
+
wireCopy(box, t);
|
|
428
497
|
timer = setTimeout(() => poll(b.id), 1500);
|
|
429
498
|
})
|
|
430
499
|
.catch(() => {
|
|
431
|
-
start.disabled = false;
|
|
500
|
+
if (start) start.disabled = false;
|
|
432
501
|
fail(t("err.signin.githubFailed"));
|
|
433
502
|
});
|
|
434
|
-
}
|
|
503
|
+
};
|
|
504
|
+
start?.addEventListener("click", () => begin({}));
|
|
505
|
+
if (attest) {
|
|
506
|
+
show(
|
|
507
|
+
`<p class="den-gh-wait" style="margin-top:12px">${esc(t("err.signin.claimChecking", { host: s.machine || "" }))}</p>`,
|
|
508
|
+
);
|
|
509
|
+
begin({ attest });
|
|
510
|
+
}
|
|
435
511
|
}
|
|
436
512
|
|
|
437
513
|
/**
|
|
@@ -604,6 +680,8 @@ function mountSignedOut(root, sh, d) {
|
|
|
604
680
|
`<div class="sub">${esc(t("err.signin.sub"))}</div></div></div>`;
|
|
605
681
|
if (s.confirm) {
|
|
606
682
|
// SEC-2a: a link `web login` waits on: the code it printed, first.
|
|
683
|
+
// D17: a plain sign-in page, no shell frame and no error wording.
|
|
684
|
+
document.body.classList.add("den-plain");
|
|
607
685
|
root.innerHTML =
|
|
608
686
|
`<div class="ph"><div><h1>${esc(t("err.signin.title", { name }))}</h1>` +
|
|
609
687
|
`<div class="sub">${esc(t("err.signin.sub"))}</div></div></div>` +
|
|
@@ -617,7 +695,6 @@ function mountSignedOut(root, sh, d) {
|
|
|
617
695
|
`<p class="den-confirm-err" id="den-confirm-err" role="alert" style="margin-top:12px"></p>` +
|
|
618
696
|
`</form>` +
|
|
619
697
|
`<p class="den-signin-why">${withHost(t, "err.signin.confirmWhy", { host })}</p>` +
|
|
620
|
-
facts +
|
|
621
698
|
`</div>`;
|
|
622
699
|
const form = root.querySelector("#den-confirm");
|
|
623
700
|
const input = root.querySelector("#den-confirm-code");
|
|
@@ -672,15 +749,20 @@ function mountSignedOut(root, sh, d) {
|
|
|
672
749
|
`<div class="acts"><a class="btn primary" id="den-signin-local" href="${esc(s.localhost)}">${esc(t("err.421.open"))}</a></div></div>`;
|
|
673
750
|
return;
|
|
674
751
|
}
|
|
675
|
-
const
|
|
752
|
+
const claim = claimBlock(s, t);
|
|
753
|
+
const gh = claim || githubBlock(s, t);
|
|
676
754
|
root.innerHTML =
|
|
677
755
|
head +
|
|
678
756
|
`<div class="empty" id="den-signin">` +
|
|
679
757
|
(s.expired
|
|
680
758
|
? `<p class="lead" role="alert">${esc(t("err.signin.expiredLead"))}</p>`
|
|
681
759
|
: "") +
|
|
682
|
-
|
|
683
|
-
|
|
760
|
+
claim +
|
|
761
|
+
// D17: with the claim first, "from another computer" may not be true.
|
|
762
|
+
(claim
|
|
763
|
+
? ""
|
|
764
|
+
: `<p class="lead" id="den-signin-where">${withHost(t, "err.signin.where", { name, host })}</p>`) +
|
|
765
|
+
(claim ? "" : gh) +
|
|
684
766
|
`<h2 class="den-h" id="den-pair-title">${esc(t(gh ? "err.signin.approveOr" : "err.signin.approve"))}</h2>` +
|
|
685
767
|
`<div id="den-pair" aria-live="polite"></div>` +
|
|
686
768
|
`<p class="den-signin-why"><b>${esc(t("err.signin.whyTitle"))}</b> ${withHost(t, "err.signin.whyText", { name, host })}</p>` +
|
|
@@ -695,7 +777,7 @@ function mountSignedOut(root, sh, d) {
|
|
|
695
777
|
facts +
|
|
696
778
|
`</div>`;
|
|
697
779
|
wireCopy(root, t);
|
|
698
|
-
wireGithub(root, t, s);
|
|
780
|
+
wireGithub(root, t, s, claim ? takeAttestCode() : "");
|
|
699
781
|
watchOwner(root, t, s, window.__denOwnerPollMs || OWNER_POLL_MS);
|
|
700
782
|
wirePair(root, t, s);
|
|
701
783
|
if (s.expired) return;
|
|
@@ -48,6 +48,16 @@
|
|
|
48
48
|
flex-direction: column;
|
|
49
49
|
overflow: hidden;
|
|
50
50
|
}
|
|
51
|
+
/* D17: the /auth code page is a plain sign-in page: no shell frame
|
|
52
|
+
around it, the form in the middle. */
|
|
53
|
+
html body.den-plain > :not(#den-content):not(script):not(style) {
|
|
54
|
+
display: none !important;
|
|
55
|
+
}
|
|
56
|
+
html body.den-plain > #den-content {
|
|
57
|
+
max-width: 560px;
|
|
58
|
+
margin: 10vh auto 0;
|
|
59
|
+
width: 100%;
|
|
60
|
+
}
|
|
51
61
|
/* .main */
|
|
52
62
|
html body.den-has-shell > #den-content {
|
|
53
63
|
overflow: auto;
|
|
@@ -234,6 +244,12 @@
|
|
|
234
244
|
white-space: pre-wrap;
|
|
235
245
|
overflow-wrap: anywhere;
|
|
236
246
|
}
|
|
247
|
+
/* The GitHub code (D17): big, centred, its copy button beside it. */
|
|
248
|
+
.den-gh-coderow pre.code {
|
|
249
|
+
font-size: 22px;
|
|
250
|
+
letter-spacing: 0.12em;
|
|
251
|
+
text-align: center;
|
|
252
|
+
}
|
|
237
253
|
.btn.den-copy {
|
|
238
254
|
padding: 6px 8px;
|
|
239
255
|
align-self: stretch;
|
|
@@ -686,7 +686,7 @@ export async function initChangelog() {
|
|
|
686
686
|
|
|
687
687
|
// ── __DEN_TITLE__ config ──────────────────────────────────────────────────────────────
|
|
688
688
|
|
|
689
|
-
let
|
|
689
|
+
let _cliConfig = null;
|
|
690
690
|
|
|
691
691
|
/**
|
|
692
692
|
* 0.4.1: true once the shell knows the config fails validation (shell.js
|
|
@@ -705,18 +705,16 @@ function apiOff() {
|
|
|
705
705
|
}
|
|
706
706
|
|
|
707
707
|
async function getDenConfig() {
|
|
708
|
-
if (
|
|
708
|
+
if (_cliConfig) return _cliConfig;
|
|
709
709
|
if (configInvalid()) return { environments: ["local"] };
|
|
710
710
|
try {
|
|
711
|
-
|
|
712
|
-
r.json(),
|
|
713
|
-
);
|
|
711
|
+
_cliConfig = await fetch("/api/__DEN_CLI__/config").then((r) => r.json());
|
|
714
712
|
} catch {
|
|
715
|
-
|
|
713
|
+
_cliConfig = {
|
|
716
714
|
environments: ["local"],
|
|
717
715
|
};
|
|
718
716
|
}
|
|
719
|
-
return
|
|
717
|
+
return _cliConfig;
|
|
720
718
|
}
|
|
721
719
|
|
|
722
720
|
/**
|
|
@@ -305,6 +305,16 @@
|
|
|
305
305
|
"err.signin.ghOnly":
|
|
306
306
|
"Solo la cuenta de GitHub del propietario puede iniciar sesión así. Para cualquier otra persona, usa «Aprueba este ordenador».",
|
|
307
307
|
"err.signin.ghLearn": "Más información",
|
|
308
|
+
"err.signin.claimTitle": "Conecta tu cuenta de GitHub",
|
|
309
|
+
"err.signin.claimLead":
|
|
310
|
+
"{name} aún no tiene propietario. Conecta tu cuenta de GitHub para que sea tuyo.",
|
|
311
|
+
"err.signin.claimOrg":
|
|
312
|
+
"Los miembros de la organización {org} de GitHub pueden reclamarlo, en un navegador en {host}.",
|
|
313
|
+
"err.signin.claimHere": "Este es mi ordenador: conectar GitHub",
|
|
314
|
+
"err.signin.claimAs": "Iniciar sesión como @{login}",
|
|
315
|
+
"err.signin.claimHereNote":
|
|
316
|
+
"«Este es mi ordenador» solo funciona en un navegador en {host}. En otro sitio no abre nada.",
|
|
317
|
+
"err.signin.claimChecking": "Comprobando que esto es {host}…",
|
|
308
318
|
"err.signin.githubRefused":
|
|
309
319
|
"@{login} no es el propietario de este {name}. Solo la cuenta del propietario puede usar Iniciar sesión con GitHub. Usa «Aprueba este ordenador» en su lugar.",
|
|
310
320
|
"err.signin.approveOr": "O aprueba este ordenador",
|
|
@@ -354,7 +364,6 @@
|
|
|
354
364
|
"err.signin.github": "Iniciar sesión con GitHub",
|
|
355
365
|
"err.signin.githubEnter": "Introduce este código en {url}:",
|
|
356
366
|
"err.signin.githubOpen": "Abrir GitHub",
|
|
357
|
-
"err.signin.githubCopy": "Copiar código",
|
|
358
367
|
"err.signin.githubWaiting": "Esperando a que lo apruebes en GitHub…",
|
|
359
368
|
"err.signin.githubDone": "Sesión iniciada. Cargando…",
|
|
360
369
|
"err.signin.githubExpired": "El código caducó antes de aprobarse.",
|
|
@@ -781,6 +790,20 @@
|
|
|
781
790
|
"ag.hint.tile": "Un clic añade una sesión al mosaico, hasta 4.",
|
|
782
791
|
"ag.renameTip": "Doble clic para cambiar el nombre",
|
|
783
792
|
"ag.renameName": "Cambiar el nombre de {id}",
|
|
793
|
+
"ag.rn.item": "Cambiar el nombre…",
|
|
794
|
+
"ag.rn.title": "Cambiar el nombre de {id}",
|
|
795
|
+
"ag.rn.name": "Nombre",
|
|
796
|
+
"ag.rn.help":
|
|
797
|
+
"Lo muestran su pestaña, su fila y la cabecera del panel; el id {id} no cambia. Las demás sesiones lo ven como {peer} desde su próximo inicio o reanudación. Vacío: vuelve a {id}.",
|
|
798
|
+
"ag.rn.go": "Cambiar el nombre",
|
|
799
|
+
"ag.rn.cancel": "Cancelar",
|
|
800
|
+
"ag.rn.act": "Renombrar",
|
|
801
|
+
"ag.where.logout": "Se detiene al cerrar sesión",
|
|
802
|
+
"ag.where.logoutTip":
|
|
803
|
+
"Se ejecuta en la sesión de escritorio, así que cerrar sesión lo termina. Para que los agentes sigan: {cli} service install --mode always (opcional, aún no probado tras cerrar sesión)",
|
|
804
|
+
"ag.where.bg": "Sin GUI",
|
|
805
|
+
"ag.where.bgTip":
|
|
806
|
+
"Sigue ejecutándose tras cerrar sesión, fuera de la sesión de escritorio: sin el llavero de inicio de sesión ni apps con GUI. 1Password usa el token de la cuenta de servicio, los navegadores van sin interfaz y las carpetas protegidas necesitan Acceso total al disco ({cli} doctor).",
|
|
784
807
|
"ag.rt.ready": "Inicia {name} {version} desde",
|
|
785
808
|
"ag.rt.readyNoVer": "Inicia {name} desde",
|
|
786
809
|
"ag.rt.notFound": "No se encontró {name}.",
|
|
@@ -293,6 +293,16 @@
|
|
|
293
293
|
"err.signin.ghOnly":
|
|
294
294
|
"この方法でサインインできるのはオーナーの GitHub アカウントだけです。それ以外の人は「このコンピューターを承認」を使ってください。",
|
|
295
295
|
"err.signin.ghLearn": "詳しく見る",
|
|
296
|
+
"err.signin.claimTitle": "GitHub アカウントを接続",
|
|
297
|
+
"err.signin.claimLead":
|
|
298
|
+
"{name} にはまだオーナーがいません。GitHub アカウントを接続して、自分のものにします。",
|
|
299
|
+
"err.signin.claimOrg":
|
|
300
|
+
"GitHub の {org} 組織のメンバーは、{host} 上のブラウザーでこれを登録できます。",
|
|
301
|
+
"err.signin.claimHere": "これは自分のコンピューター: GitHub を接続",
|
|
302
|
+
"err.signin.claimAs": "@{login} としてサインイン",
|
|
303
|
+
"err.signin.claimHereNote":
|
|
304
|
+
"「これは自分のコンピューター」は {host} 上のブラウザーでだけ使えます。ほかの場所では何も開きません。",
|
|
305
|
+
"err.signin.claimChecking": "ここが {host} か確認しています…",
|
|
296
306
|
"err.signin.githubRefused":
|
|
297
307
|
"@{login} はこの {name} のオーナーではありません。GitHub でサインインを使えるのはオーナーのアカウントだけです。代わりに「このコンピューターを承認」を使ってください。",
|
|
298
308
|
"err.signin.approveOr": "または、このコンピューターを承認",
|
|
@@ -343,7 +353,6 @@
|
|
|
343
353
|
"err.signin.github": "GitHub でサインイン",
|
|
344
354
|
"err.signin.githubEnter": "{url} でこのコードを入力してください:",
|
|
345
355
|
"err.signin.githubOpen": "GitHub を開く",
|
|
346
|
-
"err.signin.githubCopy": "コードをコピー",
|
|
347
356
|
"err.signin.githubWaiting": "GitHub での承認を待っています…",
|
|
348
357
|
"err.signin.githubDone": "サインインしました。読み込み中…",
|
|
349
358
|
"err.signin.githubExpired": "承認される前にコードの有効期限が切れました。",
|
|
@@ -766,6 +775,20 @@
|
|
|
766
775
|
"ag.hint.tile": "クリックでセッションをタイルに追加します(最大 4)。",
|
|
767
776
|
"ag.renameTip": "ダブルクリックで名前を変更",
|
|
768
777
|
"ag.renameName": "{id} の名前を変更",
|
|
778
|
+
"ag.rn.item": "名前を変更…",
|
|
779
|
+
"ag.rn.title": "{id} の名前を変更",
|
|
780
|
+
"ag.rn.name": "名前",
|
|
781
|
+
"ag.rn.help":
|
|
782
|
+
"タブ、行、ペインの見出しに表示されます。ID {id} は変わりません。ほかのセッションからは、次の開始または再開から {peer} として見えます。空にすると {id} に戻ります。",
|
|
783
|
+
"ag.rn.go": "名前を変更",
|
|
784
|
+
"ag.rn.cancel": "キャンセル",
|
|
785
|
+
"ag.rn.act": "名前を変更",
|
|
786
|
+
"ag.where.logout": "ログアウトで停止",
|
|
787
|
+
"ag.where.logoutTip":
|
|
788
|
+
"デスクトップのログインセッションで動いているため、ログアウトすると終了します。エージェントを動かし続けるには: {cli} service install --mode always (任意。ログアウトをまたいだ動作はまだ未検証)",
|
|
789
|
+
"ag.where.bg": "GUI なし",
|
|
790
|
+
"ag.where.bgTip":
|
|
791
|
+
"デスクトップセッションの外で、ログアウト後も動き続けます。ログインキーチェーンと GUI アプリは使えません。1Password はサービスアカウントのトークン、ブラウザはヘッドレス、保護されたフォルダにはフルディスクアクセスが必要です ({cli} doctor)。",
|
|
769
792
|
"ag.rt.ready": "{name} {version} を起動:",
|
|
770
793
|
"ag.rt.readyNoVer": "{name} を起動:",
|
|
771
794
|
"ag.rt.notFound": "{name} が見つかりません。",
|
|
@@ -425,7 +425,7 @@ function drawPair(host, ctx) {
|
|
|
425
425
|
out.innerHTML =
|
|
426
426
|
`<div style="display:flex;gap:16px;flex-wrap:wrap;align-items:flex-start;margin-top:8px">` +
|
|
427
427
|
`<div id="set-pair-qr" style="line-height:0">${qrSvg(p.url, { scale: 4, margin: 4 })}</div>` +
|
|
428
|
-
`<div style="flex:1 1 220px;min-width:0"><p class="set-note" style="margin:0 0 8px">Scan it with the other device, or open the link there. It works once,
|
|
428
|
+
`<div style="flex:1 1 220px;min-width:0"><p class="set-note" style="margin:0 0 8px">Scan it with the other device, or open the link there. It works once, until it expires (below), and signs that browser in for good.</p>` +
|
|
429
429
|
`<a id="set-pair-link" href="${escHtml(p.url)}" style="font:12px var(--mono);overflow-wrap:anywhere">${escHtml(p.url)}</a>` +
|
|
430
430
|
`<p class="set-note" id="set-pair-left" style="margin:8px 0 0"></p></div></div>`;
|
|
431
431
|
const until = Date.parse(p.expiresAt);
|