@nano133/nanoterra 0.8.0 → 0.9.0
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 +59 -0
- package/LICENSE +3 -0
- package/README.md +363 -25
- package/ai/SPEC.md +65 -3
- package/dist/admin/panel/dom.d.ts +2 -0
- package/dist/admin/panel/dom.js +6 -0
- package/dist/admin/panel/mount.d.ts +3 -0
- package/dist/admin/panel/mount.js +4 -2
- package/dist/emotes/panel/mount.d.ts +5 -0
- package/dist/emotes/panel/mount.js +7 -3
- package/dist/friends/panel/mount.d.ts +5 -0
- package/dist/friends/panel/mount.js +5 -2
- package/dist/options/panel/mount.d.ts +5 -0
- package/dist/options/panel/mount.js +5 -3
- package/dist/player-signin/adapters/d1.d.ts +10 -0
- package/dist/player-signin/adapters/d1.js +108 -0
- package/dist/player-signin/adapters/firestore.d.ts +45 -0
- package/dist/player-signin/adapters/firestore.js +168 -0
- package/dist/player-signin/conformance.d.ts +11 -0
- package/dist/player-signin/conformance.js +160 -0
- package/dist/player-signin/handler.d.ts +64 -0
- package/dist/player-signin/handler.js +364 -0
- package/dist/player-signin/host.d.ts +4 -0
- package/dist/player-signin/host.js +10 -0
- package/dist/player-signin/index.d.ts +9 -0
- package/dist/player-signin/index.js +15 -0
- package/dist/player-signin/memory.d.ts +15 -0
- package/dist/player-signin/memory.js +101 -0
- package/dist/player-signin/panel/index.d.ts +9 -0
- package/dist/player-signin/panel/index.js +7 -0
- package/dist/player-signin/panel/memo.d.ts +12 -0
- package/dist/player-signin/panel/memo.js +41 -0
- package/dist/player-signin/panel/mount.d.ts +21 -0
- package/dist/player-signin/panel/mount.js +105 -0
- package/dist/player-signin/panel/view.d.ts +45 -0
- package/dist/player-signin/panel/view.js +360 -0
- package/dist/player-signin/panel/words.d.ts +8 -0
- package/dist/player-signin/panel/words.js +26 -0
- package/dist/player-signin/scheme-check.d.ts +3 -0
- package/dist/player-signin/scheme-check.js +14 -0
- package/dist/player-signin/scheme.d.ts +13 -0
- package/dist/player-signin/scheme.js +28 -0
- package/dist/player-signin/types.d.ts +116 -0
- package/dist/player-signin/types.js +33 -0
- package/dist/shop/adapters/d1.d.ts +25 -0
- package/dist/shop/adapters/d1.js +200 -0
- package/dist/shop/adapters/firestore.d.ts +53 -0
- package/dist/shop/adapters/firestore.js +245 -0
- package/dist/shop/address.d.ts +14 -0
- package/dist/shop/address.js +104 -0
- package/dist/shop/amount.d.ts +26 -0
- package/dist/shop/amount.js +59 -0
- package/dist/shop/conformance.d.ts +13 -0
- package/dist/shop/conformance.js +203 -0
- package/dist/shop/handler.d.ts +76 -0
- package/dist/shop/handler.js +156 -0
- package/dist/shop/index.d.ts +19 -0
- package/dist/shop/index.js +21 -0
- package/dist/shop/match.d.ts +23 -0
- package/dist/shop/match.js +58 -0
- package/dist/shop/memory.d.ts +13 -0
- package/dist/shop/memory.js +106 -0
- package/dist/shop/nodes.d.ts +47 -0
- package/dist/shop/nodes.js +106 -0
- package/dist/shop/orders.d.ts +47 -0
- package/dist/shop/orders.js +82 -0
- package/dist/shop/panel/index.d.ts +7 -0
- package/dist/shop/panel/index.js +11 -0
- package/dist/shop/panel/looks.d.ts +30 -0
- package/dist/shop/panel/looks.js +61 -0
- package/dist/shop/panel/mount.d.ts +52 -0
- package/dist/shop/panel/mount.js +194 -0
- package/dist/shop/panel/pay.d.ts +24 -0
- package/dist/shop/panel/pay.js +115 -0
- package/dist/shop/panel/qr.d.ts +9 -0
- package/dist/shop/panel/qr.js +281 -0
- package/dist/shop/price.d.ts +23 -0
- package/dist/shop/price.js +43 -0
- package/dist/shop/scan.d.ts +24 -0
- package/dist/shop/scan.js +80 -0
- package/dist/shop/security.d.ts +10 -0
- package/dist/shop/security.js +56 -0
- package/dist/shop/store.d.ts +101 -0
- package/dist/shop/store.js +4 -0
- package/dist/shop/worker.d.ts +43 -0
- package/dist/shop/worker.js +76 -0
- package/package.json +21 -6
- package/src/admin/panel/dom.ts +6 -0
- package/src/admin/panel/mount.ts +7 -2
- package/src/emotes/panel/mount.ts +9 -2
- package/src/friends/panel/mount.ts +8 -2
- package/src/options/panel/mount.ts +8 -3
- package/src/player-signin/adapters/d1.sql +45 -0
- package/src/player-signin/adapters/d1.ts +120 -0
- package/src/player-signin/adapters/firestore.ts +194 -0
- package/src/player-signin/conformance.ts +184 -0
- package/src/player-signin/handler.ts +404 -0
- package/src/player-signin/host.ts +12 -0
- package/src/player-signin/index.ts +20 -0
- package/src/player-signin/memory.ts +94 -0
- package/src/player-signin/panel/index.ts +12 -0
- package/src/player-signin/panel/memo.ts +49 -0
- package/src/player-signin/panel/mount.ts +127 -0
- package/src/player-signin/panel/style.css +473 -0
- package/src/player-signin/panel/view.ts +428 -0
- package/src/player-signin/panel/words.ts +32 -0
- package/src/player-signin/scheme-check.ts +14 -0
- package/src/player-signin/scheme.ts +32 -0
- package/src/player-signin/types.ts +103 -0
- package/src/shop/adapters/d1.sql +52 -0
- package/src/shop/adapters/d1.ts +245 -0
- package/src/shop/adapters/firestore.ts +281 -0
- package/src/shop/address.ts +102 -0
- package/src/shop/amount.ts +66 -0
- package/src/shop/conformance.ts +232 -0
- package/src/shop/handler.ts +202 -0
- package/src/shop/index.ts +30 -0
- package/src/shop/match.ts +63 -0
- package/src/shop/memory.ts +102 -0
- package/src/shop/nodes.ts +115 -0
- package/src/shop/orders.ts +93 -0
- package/src/shop/panel/index.ts +15 -0
- package/src/shop/panel/looks.ts +106 -0
- package/src/shop/panel/mount.ts +241 -0
- package/src/shop/panel/pay.ts +171 -0
- package/src/shop/panel/qr.ts +256 -0
- package/src/shop/panel/style.css +534 -0
- package/src/shop/price.ts +52 -0
- package/src/shop/scan.ts +90 -0
- package/src/shop/security.ts +50 -0
- package/src/shop/store.ts +99 -0
- package/src/shop/worker.ts +107 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type PlayerSigninStore, type SessionRecord, type SigninRequest, type Way } from "./types.js";
|
|
2
|
+
type RequestRow = SigninRequest & {
|
|
3
|
+
way: Way;
|
|
4
|
+
delivered: boolean;
|
|
5
|
+
};
|
|
6
|
+
export type MemoryPlayerSignin = PlayerSigninStore & {
|
|
7
|
+
readonly requestRows: Map<string, RequestRow>;
|
|
8
|
+
readonly sessionRows: Map<string, SessionRecord>;
|
|
9
|
+
readonly onceRows: Map<string, {
|
|
10
|
+
address: string;
|
|
11
|
+
until: number;
|
|
12
|
+
}>;
|
|
13
|
+
};
|
|
14
|
+
export declare function createMemoryPlayerSignin(): MemoryPlayerSignin;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// The player sign-in's store in this process's memory: for tests and demos. A game uses a database (./adapters/d1.ts, ./adapters/firestore.ts, or
|
|
2
|
+
// its own store that passes playerSigninStoreConformance).
|
|
3
|
+
import { WAYS } from "./types.js";
|
|
4
|
+
const copy = (v) => structuredClone(v);
|
|
5
|
+
export function createMemoryPlayerSignin() {
|
|
6
|
+
const requestRows = new Map();
|
|
7
|
+
const sessionRows = new Map();
|
|
8
|
+
const onceRows = new Map();
|
|
9
|
+
const strip = ({ way: _way, delivered: _delivered, ...r }) => copy(r);
|
|
10
|
+
const stores = new Map();
|
|
11
|
+
for (const way of WAYS) {
|
|
12
|
+
stores.set(way, {
|
|
13
|
+
async create(key, r) {
|
|
14
|
+
if (requestRows.has(key))
|
|
15
|
+
throw new Error("a request with this key exists");
|
|
16
|
+
requestRows.set(key, { ...copy(r), way, delivered: false });
|
|
17
|
+
},
|
|
18
|
+
async get(key) {
|
|
19
|
+
const r = requestRows.get(key);
|
|
20
|
+
return r && r.way === way ? strip(r) : null;
|
|
21
|
+
},
|
|
22
|
+
async find(id) {
|
|
23
|
+
for (const [key, r] of requestRows)
|
|
24
|
+
if (r.way === way && r.id === id)
|
|
25
|
+
return { key, request: strip(r) };
|
|
26
|
+
return null;
|
|
27
|
+
},
|
|
28
|
+
async finish(key, account) {
|
|
29
|
+
const r = requestRows.get(key);
|
|
30
|
+
if (!r || r.way !== way || r.status !== "waiting")
|
|
31
|
+
return false;
|
|
32
|
+
r.status = "signedin";
|
|
33
|
+
r.account = account;
|
|
34
|
+
return true;
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
requestRows,
|
|
40
|
+
sessionRows,
|
|
41
|
+
onceRows,
|
|
42
|
+
requests: (way) => {
|
|
43
|
+
const s = stores.get(way);
|
|
44
|
+
if (!s)
|
|
45
|
+
throw new Error(`no such way: ${String(way)}`);
|
|
46
|
+
return s;
|
|
47
|
+
},
|
|
48
|
+
async deliver(way, key, session) {
|
|
49
|
+
const r = requestRows.get(key);
|
|
50
|
+
if (!r || r.way !== way || r.status !== "signedin" || r.delivered)
|
|
51
|
+
return false;
|
|
52
|
+
r.delivered = true;
|
|
53
|
+
sessionRows.set(session.idHash, copy(session));
|
|
54
|
+
return true;
|
|
55
|
+
},
|
|
56
|
+
async createSession(session) {
|
|
57
|
+
if (sessionRows.has(session.idHash))
|
|
58
|
+
throw new Error("a session with this id exists");
|
|
59
|
+
sessionRows.set(session.idHash, copy(session));
|
|
60
|
+
},
|
|
61
|
+
async session(idHash, now) {
|
|
62
|
+
const s = sessionRows.get(idHash);
|
|
63
|
+
return s && s.expiresAt > now ? copy(s) : null;
|
|
64
|
+
},
|
|
65
|
+
async endSession(idHash) {
|
|
66
|
+
sessionRows.delete(idHash);
|
|
67
|
+
},
|
|
68
|
+
async useOnce(key, ttlMs, address) {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
const had = onceRows.get(key);
|
|
71
|
+
if (had && had.until > now)
|
|
72
|
+
return false;
|
|
73
|
+
onceRows.set(key, { address, until: now + ttlMs });
|
|
74
|
+
return true;
|
|
75
|
+
},
|
|
76
|
+
async forget(address) {
|
|
77
|
+
let n = 0;
|
|
78
|
+
for (const [k, s] of sessionRows)
|
|
79
|
+
if (s.address === address && sessionRows.delete(k))
|
|
80
|
+
n++;
|
|
81
|
+
for (const [k, r] of requestRows)
|
|
82
|
+
if (r.account === address && requestRows.delete(k))
|
|
83
|
+
n++;
|
|
84
|
+
for (const [k, o] of onceRows)
|
|
85
|
+
if (o.address === address && onceRows.delete(k))
|
|
86
|
+
n++;
|
|
87
|
+
return n;
|
|
88
|
+
},
|
|
89
|
+
async clean(now) {
|
|
90
|
+
for (const [k, r] of requestRows)
|
|
91
|
+
if (r.expiresAt < now - 2 * 3_600_000)
|
|
92
|
+
requestRows.delete(k);
|
|
93
|
+
for (const [k, s] of sessionRows)
|
|
94
|
+
if (s.expiresAt <= now)
|
|
95
|
+
sessionRows.delete(k);
|
|
96
|
+
for (const [k, o] of onceRows)
|
|
97
|
+
if (o.until <= now)
|
|
98
|
+
onceRows.delete(k);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { mountPlayerSignin } from "./mount.js";
|
|
2
|
+
export type { MountedPlayerSignin, PlayerSigninMountOptions } from "./mount.js";
|
|
3
|
+
export { hostFits, passkeyName, signInView } from "./view.js";
|
|
4
|
+
export { PASSKEY_PREVIEW_NOTE, previewHost } from "../host.js";
|
|
5
|
+
export type { PlayerSigninView, PlayerSigninViewOptions, SignedIn } from "./view.js";
|
|
6
|
+
export { faultWords } from "./words.js";
|
|
7
|
+
export type { FaultWords } from "./words.js";
|
|
8
|
+
export { readMemo } from "./memo.js";
|
|
9
|
+
export type { SigninMemo } from "./memo.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// "@nano133/nanoterra/player-signin/panel": the player's sign-in sheet in the browser. `mountPlayerSignin` is the sheet (call `open()`);
|
|
2
|
+
// `signInView` is its content, for a game that has a sheet of its own. Add "@nano133/nanoterra/player-signin/style.css" to the page.
|
|
3
|
+
export { mountPlayerSignin } from "./mount.js";
|
|
4
|
+
export { hostFits, passkeyName, signInView } from "./view.js";
|
|
5
|
+
export { PASSKEY_PREVIEW_NOTE, previewHost } from "../host.js";
|
|
6
|
+
export { faultWords } from "./words.js";
|
|
7
|
+
export { readMemo } from "./memo.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Way } from "../types.js";
|
|
2
|
+
export type SigninMemo = {
|
|
3
|
+
ids: string[];
|
|
4
|
+
unopened: string[];
|
|
5
|
+
last: Way | "passkey" | null;
|
|
6
|
+
};
|
|
7
|
+
export declare function readMemo(rpId: string): SigninMemo;
|
|
8
|
+
/** A passkey was just MADE and not used yet: remember it at once (a closed sheet or a reload between the two steps must not forget it). */
|
|
9
|
+
export declare function rememberMade(rpId: string, id: string): void;
|
|
10
|
+
/** A passkey signed the player in. Returns true when it was made here and never opened before: a new account. */
|
|
11
|
+
export declare function rememberOpened(rpId: string, id: string): boolean;
|
|
12
|
+
export declare function rememberLast(rpId: string, last: Way | "passkey"): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// What this browser remembers of the player's sign-in, in localStorage (no secret, no address): the ids of the passkeys it made or used (so the device
|
|
2
|
+
// refuses a second passkey here, and "Use my passkey" is the main key on the next visit), the ids made here and never opened (their first open is a
|
|
3
|
+
// NEW account: the spare key shows then), and the way chosen last. A browser that refuses storage remembers nothing, and nothing depends on it.
|
|
4
|
+
const KEY = (rpId) => `n133.player-signin.v1:${rpId}`;
|
|
5
|
+
const isId = (x) => typeof x === "string" && /^[A-Za-z0-9_-]{8,700}$/.test(x);
|
|
6
|
+
const LAST = new Set(["passkey", "berry", "nanchat", "nautilus"]);
|
|
7
|
+
export function readMemo(rpId) {
|
|
8
|
+
try {
|
|
9
|
+
const m = JSON.parse(globalThis.localStorage?.getItem(KEY(rpId)) ?? "null");
|
|
10
|
+
const ids = Array.isArray(m?.ids) ? m.ids.filter(isId).slice(-8) : [];
|
|
11
|
+
const unopened = Array.isArray(m?.unopened) ? m.unopened.filter((x) => isId(x) && ids.includes(x)) : [];
|
|
12
|
+
const last = typeof m?.last === "string" && LAST.has(m.last) ? m.last : null;
|
|
13
|
+
return { ids, unopened, last };
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return { ids: [], unopened: [], last: null };
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function write(rpId, m) {
|
|
20
|
+
try {
|
|
21
|
+
globalThis.localStorage?.setItem(KEY(rpId), JSON.stringify(m));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// Storage refused (a private window): nothing is remembered.
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/** A passkey was just MADE and not used yet: remember it at once (a closed sheet or a reload between the two steps must not forget it). */
|
|
28
|
+
export function rememberMade(rpId, id) {
|
|
29
|
+
const m = readMemo(rpId);
|
|
30
|
+
write(rpId, { ...m, ids: [...m.ids.filter((x) => x !== id), id].slice(-8), unopened: [...m.unopened.filter((x) => x !== id), id] });
|
|
31
|
+
}
|
|
32
|
+
/** A passkey signed the player in. Returns true when it was made here and never opened before: a new account. */
|
|
33
|
+
export function rememberOpened(rpId, id) {
|
|
34
|
+
const m = readMemo(rpId);
|
|
35
|
+
const fresh = m.unopened.includes(id);
|
|
36
|
+
write(rpId, { ids: [...m.ids.filter((x) => x !== id), id].slice(-8), unopened: m.unopened.filter((x) => x !== id), last: "passkey" });
|
|
37
|
+
return fresh;
|
|
38
|
+
}
|
|
39
|
+
export function rememberLast(rpId, last) {
|
|
40
|
+
write(rpId, { ...readMemo(rpId), last });
|
|
41
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type PlayerSigninViewOptions, type SignedIn } from "./view.js";
|
|
2
|
+
export type PlayerSigninMountOptions = PlayerSigninViewOptions & {
|
|
3
|
+
/** The sheet's title. Default "Sign in". */
|
|
4
|
+
title?: string;
|
|
5
|
+
/** How long "Signed in as …" shows before the sheet closes, in ms. Default 900. */
|
|
6
|
+
closeAfterMs?: number;
|
|
7
|
+
/**
|
|
8
|
+
* Where the keyboard focus goes when the sheet closes. "game" (the default): out of the sheet, to the page, so the game's keys reach the game. "back":
|
|
9
|
+
* to the element that had it when the sheet opened (a page with keys of its own).
|
|
10
|
+
*/
|
|
11
|
+
focusAfterClose?: "game" | "back";
|
|
12
|
+
};
|
|
13
|
+
export type MountedPlayerSignin = {
|
|
14
|
+
/** Opens the sheet. Resolves with the player when they sign in, or null when the sheet closes first. A second call while it is open gets the same promise. */
|
|
15
|
+
open(): Promise<SignedIn | null>;
|
|
16
|
+
close(): void;
|
|
17
|
+
readonly isOpen: boolean;
|
|
18
|
+
/** Removes the sheet and stops every wait. */
|
|
19
|
+
destroy(): void;
|
|
20
|
+
};
|
|
21
|
+
export declare function mountPlayerSignin(root: HTMLElement, options: PlayerSigninMountOptions): MountedPlayerSignin;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// The sign-in sheet of a game: `mountPlayerSignin(element, options)`. It draws no key of its own: the game's account key, the shop's "sign in
|
|
2
|
+
// first" and anything else call `open()`, which resolves with the player once they are signed in, or null when the sheet closes. Plain DOM; a
|
|
3
|
+
// modal sheet with a scrim: from the bottom on a phone held upright, in the middle on a wide or sideways screen. Escape, the close key and a press
|
|
4
|
+
// on the scrim close it. Every key is 44 px or more.
|
|
5
|
+
import { h, releaseFocus } from "../../admin/panel/dom.js";
|
|
6
|
+
import { icon } from "../../admin/panel/icons.js";
|
|
7
|
+
import { signInView } from "./view.js";
|
|
8
|
+
let mounts = 0;
|
|
9
|
+
export function mountPlayerSignin(root, options) {
|
|
10
|
+
const n = ++mounts;
|
|
11
|
+
let isOpen = false;
|
|
12
|
+
let pending = null;
|
|
13
|
+
let before = null;
|
|
14
|
+
let closing;
|
|
15
|
+
const settle = (p) => {
|
|
16
|
+
const was = pending;
|
|
17
|
+
pending = null;
|
|
18
|
+
was?.resolve(p);
|
|
19
|
+
};
|
|
20
|
+
const view = signInView({
|
|
21
|
+
...options,
|
|
22
|
+
onSignedIn(player) {
|
|
23
|
+
options.onSignedIn?.(player);
|
|
24
|
+
settle(player);
|
|
25
|
+
closing = setTimeout(close, Math.max(0, options.closeAfterMs ?? 900));
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const titleEl = h("h2", { class: "n133s-title", id: `n133s-title-${n}` }, options.title ?? "Sign in");
|
|
29
|
+
const closeKey = h("button", { class: "n133s-close", type: "button", "aria-label": "Close" }, icon("close", 20));
|
|
30
|
+
const scrim = h("div", { class: "n133s-scrim" });
|
|
31
|
+
const panel = h("section", { class: "n133s-panel", role: "dialog", "aria-modal": "true", "aria-labelledby": `n133s-title-${n}` }, h("header", { class: "n133s-head" }, titleEl, closeKey), h("div", { class: "n133s-body" }, view.node));
|
|
32
|
+
const wrap = h("div", { class: "n133-signin n133s-sheet", "data-open": "false" }, scrim, panel);
|
|
33
|
+
wrap.hidden = true;
|
|
34
|
+
root.append(wrap);
|
|
35
|
+
function close() {
|
|
36
|
+
if (closing)
|
|
37
|
+
clearTimeout(closing);
|
|
38
|
+
closing = undefined;
|
|
39
|
+
if (!isOpen)
|
|
40
|
+
return;
|
|
41
|
+
isOpen = false;
|
|
42
|
+
view.stop();
|
|
43
|
+
wrap.dataset.open = "false";
|
|
44
|
+
wrap.hidden = true;
|
|
45
|
+
settle(null);
|
|
46
|
+
if (options.focusAfterClose === "back" && before instanceof HTMLElement)
|
|
47
|
+
before.focus();
|
|
48
|
+
else
|
|
49
|
+
releaseFocus(wrap);
|
|
50
|
+
}
|
|
51
|
+
function open() {
|
|
52
|
+
if (isOpen && pending)
|
|
53
|
+
return pending.promise;
|
|
54
|
+
if (closing)
|
|
55
|
+
clearTimeout(closing);
|
|
56
|
+
let resolve;
|
|
57
|
+
const promise = new Promise((r) => (resolve = r));
|
|
58
|
+
pending = { promise, resolve };
|
|
59
|
+
before = document.activeElement;
|
|
60
|
+
isOpen = true;
|
|
61
|
+
view.reset();
|
|
62
|
+
wrap.hidden = false;
|
|
63
|
+
wrap.dataset.open = "true";
|
|
64
|
+
(panel.querySelector(".n133s-area button") ?? closeKey).focus({ preventScroll: true });
|
|
65
|
+
return promise;
|
|
66
|
+
}
|
|
67
|
+
closeKey.addEventListener("click", close);
|
|
68
|
+
scrim.addEventListener("click", close);
|
|
69
|
+
wrap.addEventListener("keydown", (e) => {
|
|
70
|
+
if (e.key === "Escape") {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
e.stopPropagation();
|
|
73
|
+
close();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (e.key !== "Tab")
|
|
77
|
+
return;
|
|
78
|
+
// The focus stays in the sheet while it is open.
|
|
79
|
+
const keys = Array.from(panel.querySelectorAll("button:not([disabled]), a[href], [tabindex]:not([tabindex='-1'])")).filter((el) => !el.closest("[hidden]"));
|
|
80
|
+
if (!keys.length)
|
|
81
|
+
return;
|
|
82
|
+
const first = keys[0];
|
|
83
|
+
const last = keys[keys.length - 1];
|
|
84
|
+
if (e.shiftKey && document.activeElement === first) {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
last.focus();
|
|
87
|
+
}
|
|
88
|
+
else if (!e.shiftKey && document.activeElement === last) {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
first.focus();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
return {
|
|
94
|
+
open,
|
|
95
|
+
close,
|
|
96
|
+
get isOpen() {
|
|
97
|
+
return isOpen;
|
|
98
|
+
},
|
|
99
|
+
destroy() {
|
|
100
|
+
close();
|
|
101
|
+
view.stop();
|
|
102
|
+
wrap.remove();
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type PasskeyScheme } from "@nano133/signin/passkey";
|
|
2
|
+
import { type Way } from "../types.js";
|
|
3
|
+
export type SignedIn = {
|
|
4
|
+
address: string;
|
|
5
|
+
via: "wallet" | "passkey";
|
|
6
|
+
};
|
|
7
|
+
export type PlayerSigninViewOptions = {
|
|
8
|
+
/** The game's name: the device shows it next to the passkey ("Coin Bees · 1 Nov 2026"). */
|
|
9
|
+
name: string;
|
|
10
|
+
/** The game's FROZEN passkey scheme (see "/player-signin": checkScheme, schemeVector). Needed while the passkey is offered. */
|
|
11
|
+
scheme?: PasskeyScheme;
|
|
12
|
+
/** Offer the passkey. Default: on, except on a preview address (`*.workers.dev`, see `previewHost`): a passkey made there is lost when the game
|
|
13
|
+
* moves to its own domain. `true` offers it there too. */
|
|
14
|
+
passkey?: boolean;
|
|
15
|
+
/** The passkeys' relying party id: the game's domain. Default: the page's host name. The passkey is offered on this host and its subdomains only. */
|
|
16
|
+
rpId?: string;
|
|
17
|
+
/** The wallet apps, in the sheet's order. Default: Berry Wallet, NanChat, Nautilus. */
|
|
18
|
+
ways?: readonly Way[];
|
|
19
|
+
/** Where the handler's routes are. Default "/api/signin". */
|
|
20
|
+
base?: string;
|
|
21
|
+
/** Show "Keep a spare key" after a new passkey. Default true. */
|
|
22
|
+
spareKey?: boolean;
|
|
23
|
+
/** The person check (Turnstile): fills `box` and gives its token, before a wallet start and a passkey sign-in. No hook: no check. */
|
|
24
|
+
personCheck?: (box: HTMLElement) => Promise<string | null | undefined>;
|
|
25
|
+
/** Draws a QR code of `text` (a wallet's link), for a screen with a mouse. No hook: the link is shown as a link. */
|
|
26
|
+
qr?: (text: string) => HTMLElement;
|
|
27
|
+
/** The player signed in. */
|
|
28
|
+
onSignedIn?: (player: SignedIn) => void;
|
|
29
|
+
/** The `fetch` to use (default: the page's). */
|
|
30
|
+
fetch?: typeof fetch;
|
|
31
|
+
/** A touch screen (one key that opens the app) or not (a QR code). Default: `(pointer: coarse)`. */
|
|
32
|
+
touch?: boolean;
|
|
33
|
+
};
|
|
34
|
+
export type PlayerSigninView = {
|
|
35
|
+
node: HTMLElement;
|
|
36
|
+
/** Ends the waiting and any device prompt (the sheet closed). */
|
|
37
|
+
stop(): void;
|
|
38
|
+
/** Starts again at the ways (the sheet opens again). */
|
|
39
|
+
reset(): void;
|
|
40
|
+
};
|
|
41
|
+
/** True when the page's host may ask for a passkey of `rpId`: the same host, or a subdomain of it. An IP address never may. */
|
|
42
|
+
export declare function hostFits(hostname: string, rpId: string): boolean;
|
|
43
|
+
/** The passkey's name in the device's list: the game and the day it was made (the account does not exist yet). */
|
|
44
|
+
export declare const passkeyName: (game: string, now?: Date) => string;
|
|
45
|
+
export declare function signInView(options: PlayerSigninViewOptions): PlayerSigninView;
|