@monster-orange/den 0.4.20 → 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.
@@ -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") return "";
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
- /** The device flow on the page: start, show the code, poll, go on. */
336
- function wireGithub(root, t, s) {
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 (!start || !box) return;
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.hidden = false;
345
- start.textContent = t("err.signin.githubAgain");
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
- ? t("err.signin.githubRefused", {
372
- login: b.login ?? "",
373
- name: s.name,
374
- })
375
- : b.state === "expired"
376
- ? t("err.signin.githubExpired")
377
- : b.state === "denied"
378
- ? t("err.signin.githubDenied")
379
- : t("err.signin.githubFailed"),
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
- start.addEventListener("click", () => {
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
- `<pre class="code" id="den-gh-code" style="font-size:22px;letter-spacing:0.12em;text-align:center">${esc(b.userCode)}</pre>` +
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
- const cp = box.querySelector("#den-gh-copy");
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 gh = githubBlock(s, t);
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
- `<p class="lead" id="den-signin-where">${withHost(t, "err.signin.where", { name, host })}</p>` +
683
- gh +
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 ___DEN_CLI__Config = null;
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 (___DEN_CLI__Config) return ___DEN_CLI__Config;
708
+ if (_cliConfig) return _cliConfig;
709
709
  if (configInvalid()) return { environments: ["local"] };
710
710
  try {
711
- ___DEN_CLI__Config = await fetch("/api/__DEN_CLI__/config").then((r) =>
712
- r.json(),
713
- );
711
+ _cliConfig = await fetch("/api/__DEN_CLI__/config").then((r) => r.json());
714
712
  } catch {
715
- ___DEN_CLI__Config = {
713
+ _cliConfig = {
716
714
  environments: ["local"],
717
715
  };
718
716
  }
719
- return ___DEN_CLI__Config;
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.",
@@ -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": "承認される前にコードの有効期限が切れました。",
@@ -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, within 60 seconds, and signs that browser in for good.</p>` +
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);
@@ -420,6 +420,16 @@
420
420
  "err.signin.ghOnly":
421
421
  "Only the owner's GitHub account can sign in this way. For anyone else, use Approve this computer.",
422
422
  "err.signin.ghLearn": "Learn more",
423
+ "err.signin.claimTitle": "Connect your GitHub account",
424
+ "err.signin.claimLead":
425
+ "{name} has no owner yet. Connect your GitHub account to make it yours.",
426
+ "err.signin.claimOrg":
427
+ "Members of the {org} GitHub org can claim it, in a browser on {host}.",
428
+ "err.signin.claimHere": "This is my computer: connect GitHub",
429
+ "err.signin.claimAs": "Sign in as @{login}",
430
+ "err.signin.claimHereNote":
431
+ "This is my computer works only in a browser on {host}. Anywhere else it opens nothing.",
432
+ "err.signin.claimChecking": "Checking this is {host}…",
423
433
  "err.signin.githubRefused":
424
434
  "@{login} isn't the owner of this {name}. Only the owner's account can use Sign in with GitHub. Use Approve this computer instead.",
425
435
  "err.signin.approveOr": "Or approve this computer",
@@ -467,7 +477,6 @@
467
477
  "err.signin.github": "Sign in with GitHub",
468
478
  "err.signin.githubEnter": "Enter this code at {url}:",
469
479
  "err.signin.githubOpen": "Open GitHub",
470
- "err.signin.githubCopy": "Copy code",
471
480
  "err.signin.githubWaiting": "Waiting for you to approve it on GitHub…",
472
481
  "err.signin.githubDone": "Signed in. Loading…",
473
482
  "err.signin.githubExpired": "The code expired before it was approved.",
@@ -243,6 +243,10 @@ export const ROUTES: readonly RouteEntry[] = [
243
243
  "/signin",
244
244
  "the signed-out landing page (§6b.3): it renders signed out; signed in, it goes on to `next`",
245
245
  ),
246
+ R(
247
+ "/attest",
248
+ 'D17: "This is my computer", on localhost only: a 303 back to one of this server\'s edge hosts with a short-lived code (it never makes a session)',
249
+ ),
246
250
  M(["POST"], "/api/session/logout", "ends this browser's session"),
247
251
  S(
248
252
  ["GET", "HEAD"],
@@ -452,6 +456,9 @@ export const SIGNIN_FREE: readonly string[] = [
452
456
  "/auth",
453
457
  "/signin",
454
458
  "/api/session",
459
+ // D17: "This is my computer" answers for itself: on localhost a 303,
460
+ // anywhere else its own "only on this computer" page (never a code).
461
+ "/attest",
455
462
  // SEC-2a: Sign in with GitHub, from the signed-out page.
456
463
  "/api/github/signin",
457
464
  "/api/github/signin/:id",