@monster-orange/den 0.4.19 → 0.4.21
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 +122 -0
- package/README.md +7 -7
- package/docs/distributions.md +121 -13
- package/package.json +1 -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/owner-github.ts +100 -0
- package/src/shared/product.ts +45 -5
- package/src/shared/profile.ts +26 -2
- package/src/shared/secrets/index.ts +71 -3
- package/src/shared/secrets/onepassword.ts +87 -14
- package/src/shared/secrets/op-token.ts +270 -0
- package/src/shared/secrets/secret-env.ts +19 -6
- package/src/shared/secrets/strategies.ts +6 -0
- package/src/shared/secrets/types.ts +8 -0
- package/src/shared/web-session.ts +112 -6
- package/src/ui/cli/create.ts +92 -6
- package/src/ui/cli/doctor-command.ts +108 -17
- package/src/ui/cli/doctor-owner.ts +122 -0
- package/src/ui/cli/service-command.ts +16 -0
- package/src/ui/cli/web-command.ts +74 -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 +148 -13
- 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 +11 -1
- package/src/ui/server/public/i18n/ja.js +11 -1
- package/src/ui/server/public/settings-registry.js +23 -1
- package/src/ui/server/public/shell.js +11 -1
- package/src/ui/server/route-classes.ts +7 -0
package/src/ui/server/index.ts
CHANGED
|
@@ -173,6 +173,7 @@ import {
|
|
|
173
173
|
} from "../../shared/auth/sudo-vault.js";
|
|
174
174
|
import {
|
|
175
175
|
notConfiguredMessage,
|
|
176
|
+
opTokenSourceSetting,
|
|
176
177
|
secretsConfigWarnings,
|
|
177
178
|
} from "../../shared/secrets/index.js";
|
|
178
179
|
import { DEN_BANNER_LINES, DEN_SUBTITLE } from "../../shared/banner.js";
|
|
@@ -228,7 +229,7 @@ import {
|
|
|
228
229
|
ignoredConfigWarning,
|
|
229
230
|
localhostOrigin,
|
|
230
231
|
loginLink,
|
|
231
|
-
|
|
232
|
+
LOGIN_CODE_MAX_TTL_MS,
|
|
232
233
|
mintLoginCode,
|
|
233
234
|
readWebAuthConfig,
|
|
234
235
|
safeNext,
|
|
@@ -242,6 +243,14 @@ import {
|
|
|
242
243
|
import { serveUiAsset } from "./ui-assets.js";
|
|
243
244
|
import { PERSON_GRANT_ROUTES, personGrantAllowed } from "./approvals.js";
|
|
244
245
|
import { ClaimNonces, checkClaimCode } from "../../shared/claim-code.js";
|
|
246
|
+
import {
|
|
247
|
+
ATTEST_NONCES_FILE,
|
|
248
|
+
attestRedirect,
|
|
249
|
+
attestReturn,
|
|
250
|
+
checkAttestCode,
|
|
251
|
+
mintAttestCode,
|
|
252
|
+
} from "../../shared/attest-code.js";
|
|
253
|
+
import type { ClaimRules } from "./github-routes.js";
|
|
245
254
|
import {
|
|
246
255
|
expiringTokenWarning,
|
|
247
256
|
githubClientId,
|
|
@@ -251,6 +260,7 @@ import {
|
|
|
251
260
|
} from "../../shared/github-app.js";
|
|
252
261
|
import { githubDeviceClient } from "../../shared/github-device.js";
|
|
253
262
|
import { readWebOwner } from "../../shared/web-owner.js";
|
|
263
|
+
import { noRuleMessage, unclaimedSigninReason } from "./github-routes.js";
|
|
254
264
|
import { DeviceFlows } from "./device-flows.js";
|
|
255
265
|
import {
|
|
256
266
|
decideFlow,
|
|
@@ -896,7 +906,7 @@ import {
|
|
|
896
906
|
edgeExposedMessage,
|
|
897
907
|
edgeExposure,
|
|
898
908
|
edgeNetworkSubnets,
|
|
899
|
-
} from "
|
|
909
|
+
} from "../../shared/edge/exposure.js";
|
|
900
910
|
import { initLoginPath } from "../../shared/agents/runtime-search.js";
|
|
901
911
|
import {
|
|
902
912
|
linksFor,
|
|
@@ -1052,6 +1062,89 @@ const WEB_AUTH: WebAuthConfig = (() => {
|
|
|
1052
1062
|
}
|
|
1053
1063
|
})();
|
|
1054
1064
|
|
|
1065
|
+
/** web's sign-in config read now (Settings may change it), else at start. */
|
|
1066
|
+
function webAuthNow(): WebAuthConfig {
|
|
1067
|
+
try {
|
|
1068
|
+
return readWebAuthConfig(getDistRoot(), product().configFile);
|
|
1069
|
+
} catch {
|
|
1070
|
+
return WEB_AUTH;
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
/** D17: who may claim this Den while it has no owner (read now). */
|
|
1075
|
+
function claimRulesNow(): ClaimRules {
|
|
1076
|
+
const w = webAuthNow();
|
|
1077
|
+
return {
|
|
1078
|
+
...(w.ownerGithub ? { ownerGithub: w.ownerGithub } : {}),
|
|
1079
|
+
...(w.allowedGithubOrg ? { allowedGithubOrg: w.allowedGithubOrg } : {}),
|
|
1080
|
+
// Where it is, or would go: the distribution config.
|
|
1081
|
+
allowedGithubOrgFile: w.allowedGithubOrgFile ?? product().configFile,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/** D17: the spent "This is my computer" codes (attest-code.ts). */
|
|
1086
|
+
const ATTEST_NONCES = new ClaimNonces(getStateDir(), ATTEST_NONCES_FILE);
|
|
1087
|
+
|
|
1088
|
+
/** The request's Host without its port, lowercase. */
|
|
1089
|
+
function hostNameOf(req: http.IncomingMessage): string {
|
|
1090
|
+
return String(req.headers.host ?? "")
|
|
1091
|
+
.toLowerCase()
|
|
1092
|
+
.replace(/:\d+$/, "");
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
/**
|
|
1096
|
+
* D17: spend an attest code for this request's host: one of this server's
|
|
1097
|
+
* edge hostnames, the code minted for it, unused. True once.
|
|
1098
|
+
*/
|
|
1099
|
+
function spendAttest(req: http.IncomingMessage, code: string): boolean {
|
|
1100
|
+
const secret = webSecret.current();
|
|
1101
|
+
const host = hostNameOf(req);
|
|
1102
|
+
if (!secret || !denEdgeHosts().includes(host)) return false;
|
|
1103
|
+
const c = checkAttestCode(code, secret, host);
|
|
1104
|
+
return c.ok && ATTEST_NONCES.consume(c.nonce, c.expiresAt);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* `GET /attest?return=<edge URL>` (D17, fix A): only on localhost (only a
|
|
1109
|
+
* browser on this machine reaches it there). A 303 back to `return`, one of
|
|
1110
|
+
* this server's own edge hosts, with a short-lived code in the fragment.
|
|
1111
|
+
* Anything else: an error page, and no code.
|
|
1112
|
+
*/
|
|
1113
|
+
function handleAttest(
|
|
1114
|
+
req: http.IncomingMessage,
|
|
1115
|
+
res: http.ServerResponse,
|
|
1116
|
+
url: URL,
|
|
1117
|
+
guard: GuardResult,
|
|
1118
|
+
requestId: string,
|
|
1119
|
+
): void {
|
|
1120
|
+
res.setHeader("Cache-Control", "no-store");
|
|
1121
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
1122
|
+
const refuse = (status: 400 | 403, message: string) =>
|
|
1123
|
+
sendErrorPage(req, res, {
|
|
1124
|
+
status,
|
|
1125
|
+
path: "/attest",
|
|
1126
|
+
message,
|
|
1127
|
+
code: status === 403 ? "not_this_computer" : "bad_return",
|
|
1128
|
+
requestId,
|
|
1129
|
+
});
|
|
1130
|
+
if (!guard.ok || guard.via !== "loopback" || guard.beyond)
|
|
1131
|
+
return refuse(
|
|
1132
|
+
403,
|
|
1133
|
+
"This works only on the computer this Den runs on, at localhost.",
|
|
1134
|
+
);
|
|
1135
|
+
const ret = attestReturn(url.searchParams.get("return"), denEdgeHosts());
|
|
1136
|
+
const secret = webSecret.current();
|
|
1137
|
+
if (!ret || !secret)
|
|
1138
|
+
return refuse(
|
|
1139
|
+
400,
|
|
1140
|
+
"That address isn't one of this Den's own. Open this Den at its own address and try again.",
|
|
1141
|
+
);
|
|
1142
|
+
res.writeHead(303, {
|
|
1143
|
+
Location: attestRedirect(ret, mintAttestCode(secret, ret.host)),
|
|
1144
|
+
});
|
|
1145
|
+
res.end();
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1055
1148
|
/**
|
|
1056
1149
|
* The loopback names, with the port: localhost (canonical), 127.0.0.1 and
|
|
1057
1150
|
* [::1]; the product's own name when /etc/hosts maps it to 127.0.0.1 (the
|
|
@@ -1197,7 +1290,11 @@ const GITHUB = githubEndpoints();
|
|
|
1197
1290
|
const DEVICE_FLOWS = new DeviceFlows({
|
|
1198
1291
|
client: githubDeviceClient(GITHUB),
|
|
1199
1292
|
clientId: () => GITHUB_CLIENT_ID,
|
|
1200
|
-
decide: decideFlow(
|
|
1293
|
+
decide: decideFlow(
|
|
1294
|
+
getStateDir,
|
|
1295
|
+
(l) => serverLog(l, "lifecycle"),
|
|
1296
|
+
() => claimRulesNow(),
|
|
1297
|
+
),
|
|
1201
1298
|
// A test's fake GitHub may answer interval 0; the real one never does.
|
|
1202
1299
|
minIntervalMs: GITHUB.fake ? 50 : 1000,
|
|
1203
1300
|
onExpiringToken: noteExpiringToken,
|
|
@@ -1360,16 +1457,36 @@ function denEdgeRoutes(): { host: string; url: string }[] {
|
|
|
1360
1457
|
*/
|
|
1361
1458
|
function githubSignin():
|
|
1362
1459
|
| { state: "no_client_id" | "no_owner" }
|
|
1363
|
-
| { state: "ready"; ownerLogin: string }
|
|
1460
|
+
| { state: "ready"; ownerLogin: string }
|
|
1461
|
+
| {
|
|
1462
|
+
state: "claim";
|
|
1463
|
+
org?: string;
|
|
1464
|
+
ownerGithub?: string;
|
|
1465
|
+
/** "This is my computer"'s start, on localhost (fix A). */
|
|
1466
|
+
attest?: string;
|
|
1467
|
+
message: string;
|
|
1468
|
+
} {
|
|
1364
1469
|
if (!GITHUB_CLIENT_ID) return { state: "no_client_id" };
|
|
1365
1470
|
const owner = readWebOwner(getStateDir());
|
|
1366
|
-
return owner
|
|
1367
|
-
|
|
1368
|
-
|
|
1471
|
+
if (owner) return { state: "ready", ownerLogin: owner.login };
|
|
1472
|
+
// D17: no owner yet: who may claim it, and how.
|
|
1473
|
+
const r = claimRulesNow();
|
|
1474
|
+
if (!r.allowedGithubOrg && !r.ownerGithub) return { state: "no_owner" };
|
|
1475
|
+
return {
|
|
1476
|
+
state: "claim",
|
|
1477
|
+
...(r.allowedGithubOrg
|
|
1478
|
+
? {
|
|
1479
|
+
org: r.allowedGithubOrg,
|
|
1480
|
+
attest: `${localhostOrigin(PORT)}/attest`,
|
|
1481
|
+
}
|
|
1482
|
+
: {}),
|
|
1483
|
+
...(r.ownerGithub ? { ownerGithub: r.ownerGithub } : {}),
|
|
1484
|
+
message: unclaimedSigninReason(null, r),
|
|
1485
|
+
};
|
|
1369
1486
|
}
|
|
1370
1487
|
|
|
1371
1488
|
/**
|
|
1372
|
-
* 0.4.3: why the edge is reachable beyond this machine (edge
|
|
1489
|
+
* 0.4.3: why the edge is reachable beyond this machine (shared/edge/exposure.ts),
|
|
1373
1490
|
* once at start; null until known, or when Den can't tell.
|
|
1374
1491
|
*/
|
|
1375
1492
|
let EDGE_EXPOSED: string | null = null;
|
|
@@ -1486,7 +1603,16 @@ function sendSignedOut(
|
|
|
1486
1603
|
...(gh.state === "ready" ? { ownerLogin: gh.ownerLogin } : {}),
|
|
1487
1604
|
learnMore: GITHUB_SIGNIN_DOCS,
|
|
1488
1605
|
...(GITHUB_CLIENT_ID
|
|
1489
|
-
?
|
|
1606
|
+
? gh.state === "no_owner"
|
|
1607
|
+
? { message: noRuleMessage(claimRulesNow()) }
|
|
1608
|
+
: gh.state === "claim"
|
|
1609
|
+
? {
|
|
1610
|
+
message: gh.message,
|
|
1611
|
+
...(gh.org ? { org: gh.org } : {}),
|
|
1612
|
+
...(gh.ownerGithub ? { ownerGithub: gh.ownerGithub } : {}),
|
|
1613
|
+
...(gh.attest ? { attest: gh.attest } : {}),
|
|
1614
|
+
}
|
|
1615
|
+
: {}
|
|
1490
1616
|
: { message: noClientIdMessage(product().cli) }),
|
|
1491
1617
|
},
|
|
1492
1618
|
...(opts.expired ? { expired: true } : {}),
|
|
@@ -1921,6 +2047,9 @@ async function handleRequest(
|
|
|
1921
2047
|
(req.method === "GET" || req.method === "HEAD" || req.method === "POST")
|
|
1922
2048
|
)
|
|
1923
2049
|
return handleAuth(req, res, url, requestId);
|
|
2050
|
+
// D17: "This is my computer" (fix A), on localhost only.
|
|
2051
|
+
if (pathname === "/attest" && (req.method === "GET" || req.method === "HEAD"))
|
|
2052
|
+
return handleAttest(req, res, url, guard, requestId);
|
|
1924
2053
|
// SEC-2a: `web login` registers its link's wait, then polls it
|
|
1925
2054
|
// (shell-grade: the CLI's bearer).
|
|
1926
2055
|
if (pathname === "/api/web/logins" && req.method === "POST") {
|
|
@@ -1934,7 +2063,7 @@ async function handleRequest(
|
|
|
1934
2063
|
const id =
|
|
1935
2064
|
Number.isFinite(exp) &&
|
|
1936
2065
|
exp > Date.now() &&
|
|
1937
|
-
exp - Date.now() <=
|
|
2066
|
+
exp - Date.now() <= LOGIN_CODE_MAX_TTL_MS + 5_000
|
|
1938
2067
|
? LOGIN_WAITS.register(
|
|
1939
2068
|
String(body.nonce ?? ""),
|
|
1940
2069
|
String(body.code ?? ""),
|
|
@@ -2043,13 +2172,16 @@ async function handleRequest(
|
|
|
2043
2172
|
const secret = webSecret.current();
|
|
2044
2173
|
if (!secret) return null;
|
|
2045
2174
|
const now = Date.now();
|
|
2175
|
+
const ttl = webAuthNow().loginLinkTtlMs;
|
|
2046
2176
|
return {
|
|
2047
|
-
url: loginLink(origin, mintLoginCode(secret, now)),
|
|
2048
|
-
expiresAt: now +
|
|
2177
|
+
url: loginLink(origin, mintLoginCode(secret, now, ttl)),
|
|
2178
|
+
expiresAt: now + ttl,
|
|
2049
2179
|
};
|
|
2050
2180
|
},
|
|
2051
2181
|
log: (l) => serverLog(l, "lifecycle"),
|
|
2052
2182
|
stateDir: getStateDir,
|
|
2183
|
+
claimRules: claimRulesNow,
|
|
2184
|
+
spendAttest,
|
|
2053
2185
|
}))
|
|
2054
2186
|
)
|
|
2055
2187
|
return;
|
|
@@ -4860,7 +4992,8 @@ async function handleRequest(
|
|
|
4860
4992
|
// ── API: user settings ────────────────────────────────────────────────────
|
|
4861
4993
|
// GET /api/settings → { editor, editors, terminal, terminals, editor_overrides, languages,
|
|
4862
4994
|
// log_window_lines, graph, appearance, shell, distribution,
|
|
4863
|
-
// user_web_port, local_dev, cli_tabs, language, ui_languages
|
|
4995
|
+
// user_web_port, local_dev, cli_tabs, language, ui_languages,
|
|
4996
|
+
// op_token_source }
|
|
4864
4997
|
// POST /api/settings body: { editor?, terminal?, editor_overrides?, graph?, appearance?, shell?,
|
|
4865
4998
|
// language? }
|
|
4866
4999
|
// graph: { direction, group, density, panelWidth } (see graph-prefs.ts)
|
|
@@ -4929,6 +5062,8 @@ async function handleRequest(
|
|
|
4929
5062
|
// what it is when unset (the short hostname).
|
|
4930
5063
|
machine_name: machineNameSetting(user),
|
|
4931
5064
|
machine_name_default: machineLabel(null, os.hostname()),
|
|
5065
|
+
// Where the 1Password token comes from (the source, never the token).
|
|
5066
|
+
op_token_source: opTokenSourceSetting(),
|
|
4932
5067
|
});
|
|
4933
5068
|
}
|
|
4934
5069
|
if (req.method === "POST") {
|
|
@@ -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.",
|
|
@@ -402,6 +411,7 @@
|
|
|
402
411
|
"set.cli.web_port": "Tu puerto web",
|
|
403
412
|
"set.cli.signin": "Inicio de sesión del navegador",
|
|
404
413
|
"set.cli.sudo": "Contraseña de administrador",
|
|
414
|
+
"set.cli.op_token": "Origen del token de 1Password",
|
|
405
415
|
"set.access.owner": "Propietario",
|
|
406
416
|
"set.access.devices": "Dispositivos vinculados",
|
|
407
417
|
"set.access.waiting": "Dispositivos en espera",
|
|
@@ -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": "承認される前にコードの有効期限が切れました。",
|
|
@@ -391,6 +400,7 @@
|
|
|
391
400
|
"set.cli.web_port": "あなたの web ポート",
|
|
392
401
|
"set.cli.signin": "ブラウザのサインイン",
|
|
393
402
|
"set.cli.sudo": "管理者パスワード",
|
|
403
|
+
"set.cli.op_token": "1Password トークンの取得元",
|
|
394
404
|
"set.access.owner": "オーナー",
|
|
395
405
|
"set.access.devices": "リンク済みデバイス",
|
|
396
406
|
"set.access.waiting": "承認待ちのデバイス",
|