@crvouga/mockingbird-service-oauth 1.8.1 → 1.9.1

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 CHANGED
@@ -1,14 +1,24 @@
1
1
  # Changelog — @crvouga/mockingbird-service-oauth
2
2
 
3
- ## 1.8.1 (2026-09-30)
3
+ ## 1.9.1 (2026-09-30)
4
4
 
5
5
  ### Fixes and improvements
6
6
 
7
- - keep hosted pages inside narrow hosts and popups ([6512323](https://github.com/crvouga/mockingbird/commit/6512323198cb00b99005f3d2cfa5b9f1e6e321a7))
7
+ - keep admin buttons pressed until the action finishes ([0aabfcb](https://github.com/crvouga/mockingbird/commit/0aabfcb5f392aa51e193f9c3f817d5877d92bc6a))
8
+ - size admin select fields like the text inputs beside them ([0fb3ef2](https://github.com/crvouga/mockingbird/commit/0fb3ef2ddbf43376c93a2ae6d87c6c9bf890dafc))
9
+ - keep admin section tabs wired when boot is renamed ([4eda25e](https://github.com/crvouga/mockingbird/commit/4eda25eacd59da048d196f7cc8369d7a02b47bfc))
10
+
11
+ ## 1.9.0 (2026-09-29)
12
+
13
+ ### Features
8
14
 
9
- ### Dependencies
15
+ - boot Bolt over Socket Mode ([01322df](https://github.com/crvouga/mockingbird/commit/01322df315c66b1f2f5b53094cbbeeeef6d3ae6c))
10
16
 
11
- - `@crvouga/mockingbird-service-sqlite`
17
+ ## 1.8.1 (2026-09-29)
18
+
19
+ ### Fixes and improvements
20
+
21
+ - keep hosted pages inside narrow hosts and popups ([6512323](https://github.com/crvouga/mockingbird/commit/6512323198cb00b99005f3d2cfa5b9f1e6e321a7))
12
22
 
13
23
  ## 1.8.0 (2026-09-29)
14
24
 
@@ -2,7 +2,7 @@ import {
2
2
  OAUTH_SCENARIOS,
3
3
  createMultiRuntime,
4
4
  createRuntime
5
- } from "./chunk-Y3AP4ZFX.js";
5
+ } from "./chunk-XQNANV2C.js";
6
6
 
7
7
  // src/server.ts
8
8
  import { readFile as readFile2 } from "node:fs/promises";
@@ -240,6 +240,7 @@ var start = async (target, values, config) => {
240
240
  onLog: formatLog(config.log)
241
241
  });
242
242
  const listening = await listen(runtime, { port: config.port, host: config.host });
243
+ target.listening?.(listening);
243
244
  console.log(`${target.name} mock listening on ${listening.url}`);
244
245
  console.log(`${target.name} health: GET ${listening.url}/health`);
245
246
  console.log(`${target.name} admin: ${listening.url}/__admin (${config.adminKey ? "x-mockingbird-admin-key required" : "open \u2014 pass --admin-key to lock"})`);
@@ -378,4 +379,4 @@ export {
378
379
  createMultiServer,
379
380
  serveTarget
380
381
  };
381
- //# sourceMappingURL=chunk-CNCP7DHC.js.map
382
+ //# sourceMappingURL=chunk-FDLQK4NK.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/server.ts", "../../../adapters/node/src/cli.ts", "../../../adapters/node/src/serve.ts", "../../../adapters/node/src/listen.ts"],
4
+ "sourcesContent": ["/// <reference types=\"node\" />\nimport { readFile } from \"node:fs/promises\"\nimport { type Listening, listen, type ServeTarget } from \"@crvouga/mockingbird-adapter-node\"\nimport { OAUTH_SCENARIOS, type OAuthScenario } from \"./behavior.js\"\nimport {\n createMultiRuntime,\n type OAuthMount,\n type OAuthMultiRuntime,\n type OAuthMultiRuntimeOptions,\n} from \"./multi.js\"\nimport { createRuntime, type OAuthRuntime, type OAuthRuntimeOptions } from \"./runtime.js\"\nexport const DEFAULT_PORT = 8810\nexport type OAuthServerOptions = OAuthRuntimeOptions & { port?: number; host?: string }\nexport const createServer = async (\n options: OAuthServerOptions = {},\n): Promise<Listening & { runtime: OAuthRuntime }> => {\n const { port, host, ...rest } = options\n const runtime = createRuntime(rest)\n return { ...(await listen(runtime, { port: port ?? 0, ...(host ? { host } : {}) })), runtime }\n}\nexport type OAuthMultiServerOptions = OAuthMultiRuntimeOptions & { port?: number; host?: string }\nexport const createMultiServer = async (\n options: OAuthMultiServerOptions,\n): Promise<Listening & { runtime: OAuthMultiRuntime }> => {\n const { port, host, ...rest } = options\n const runtime = createMultiRuntime(rest)\n return { ...(await listen(runtime, { port: port ?? 0, ...(host ? { host } : {}) })), runtime }\n}\nexport const serveTarget: ServeTarget = {\n name: \"oauth\",\n defaultPort: DEFAULT_PORT,\n options: {\n provider: {\n type: \"string\",\n value: \"<google|apple|microsoft|github|oidc>\",\n description: \"Provider wire profile (default oidc)\",\n },\n scenario: {\n type: \"string\",\n value: \"<name>\",\n description: \"Initial behavior scenario (GET /__admin/scenarios lists all)\",\n },\n issuer: { type: \"string\", value: \"<url>\", description: \"Public issuer URL\" },\n mounts: {\n type: \"string\",\n value: \"<file>\",\n description: \"JSON file with a mounts array; serves every provider on one listener\",\n },\n },\n create: async (values, common) => {\n if (typeof values.mounts === \"string\") {\n const parsed = JSON.parse(await readFile(values.mounts, \"utf8\")) as { mounts?: OAuthMount[] }\n if (!Array.isArray(parsed.mounts))\n throw new Error(\"OAuth mounts file requires a mounts array\")\n return createMultiRuntime({\n mounts: parsed.mounts,\n ...(common.seed !== undefined ? { seed: common.seed } : {}),\n ...(common.adminKey !== undefined ? { adminKey: common.adminKey } : {}),\n }) as unknown as OAuthRuntime\n }\n const provider = values.provider ?? \"oidc\"\n if (\n provider !== \"google\" &&\n provider !== \"apple\" &&\n provider !== \"oidc\" &&\n provider !== \"microsoft\" &&\n provider !== \"github\"\n )\n throw new Error(\"provider must be google, apple, microsoft, github or oidc\")\n if (\n values.scenario !== undefined &&\n (typeof values.scenario !== \"string\" || !Object.hasOwn(OAUTH_SCENARIOS, values.scenario))\n )\n throw new Error(\"Unknown OAuth scenario\")\n return createRuntime({\n provider,\n ...(typeof values.scenario === \"string\"\n ? { behavior: { preset: values.scenario as OAuthScenario } }\n : {}),\n ...(common.seed !== undefined ? { seed: common.seed } : {}),\n ...(typeof values.issuer === \"string\" ? { issuer: values.issuer } : {}),\n ...(common.adminKey !== undefined ? { adminKey: common.adminKey } : {}),\n })\n },\n banner: () => [\n \"Register clients: POST /__admin/clients; seed identities: POST /__admin/accounts\",\n \"Discovery: /.well-known/openid-configuration\",\n ],\n}\n", "import { readFile } from \"node:fs/promises\"\nimport { type ParseArgsConfig, parseArgs } from \"node:util\"\nimport type { RequestLog, ServiceInstance, ServiceRuntime } from \"@crvouga/mockingbird-service\"\nimport { type Listening, listen } from \"./listen.js\"\n\nexport type CliOption = {\n type: \"string\" | \"boolean\"\n description: string\n /** Shown in help; the value placeholder, e.g. `<port>`. */\n value?: string\n default?: string | boolean\n}\n\nexport type CliValues = Record<string, string | boolean | undefined>\n\nexport type CliCommand = {\n summary: string\n usage?: string\n options?: Record<string, CliOption>\n /** Resolves to an exit code; a server command resolves only when it stops. */\n run(values: CliValues, positionals: string[]): Promise<number>\n}\n\nexport type CliSpec = {\n bin: string\n description: string\n commands: Record<string, CliCommand>\n}\n\nconst optionHelp = (options: Record<string, CliOption>): string[] =>\n Object.entries(options).map(([name, option]) => {\n const flag = `--${name}${option.type === \"string\" ? ` ${option.value ?? \"<value>\"}` : \"\"}`\n const fallback = option.default !== undefined ? ` (default: ${String(option.default)})` : \"\"\n return ` ${flag.padEnd(30)} ${option.description}${fallback}`\n })\n\nconst help = (spec: CliSpec): string =>\n [\n `${spec.bin} \u2014 ${spec.description}`,\n \"\",\n \"Usage:\",\n ` ${spec.bin} <command> [options]`,\n \"\",\n \"Commands:\",\n ...Object.entries(spec.commands).map(([name, c]) => ` ${name.padEnd(30)} ${c.summary}`),\n \"\",\n `Run \\`${spec.bin} <command> --help\\` for a command's options.`,\n ].join(\"\\n\")\n\nconst commandHelp = (spec: CliSpec, name: string, command: CliCommand): string =>\n [\n `${spec.bin} ${name} \u2014 ${command.summary}`,\n \"\",\n \"Usage:\",\n ` ${command.usage ?? `${spec.bin} ${name} [options]`}`,\n ...(command.options ? [\"\", \"Options:\", ...optionHelp(command.options)] : []),\n ].join(\"\\n\")\n\n/** Parse `argv` against `spec` and run the chosen command. Resolves to an exit code. */\nexport const runCli = async (spec: CliSpec, argv: string[]): Promise<number> => {\n // Two-word commands (`corpus pull`) win over one-word ones.\n const pair = argv.length >= 2 ? `${argv[0]} ${argv[1]}` : undefined\n const words = pair !== undefined && spec.commands[pair] ? 2 : 1\n const name = words === 2 ? pair : argv[0]\n const rest = argv.slice(words)\n if (name === undefined || name === \"--help\" || name === \"-h\" || name === \"help\") {\n console.log(help(spec))\n return 0\n }\n const command = spec.commands[name]\n if (!command) {\n console.error(`${spec.bin}: unknown command ${JSON.stringify(name)}\\n\\n${help(spec)}`)\n return 2\n }\n if (rest.includes(\"--help\") || rest.includes(\"-h\")) {\n console.log(commandHelp(spec, name, command))\n return 0\n }\n let parsed: { values: CliValues; positionals: string[] }\n try {\n parsed = parseArgs({\n args: rest,\n allowPositionals: true,\n strict: true,\n options: Object.fromEntries(\n Object.entries(command.options ?? {}).map(([key, option]) => [\n key,\n {\n type: option.type,\n ...(option.default !== undefined ? { default: option.default } : {}),\n },\n ]),\n ) as ParseArgsConfig[\"options\"],\n }) as { values: CliValues; positionals: string[] }\n } catch (error) {\n console.error(\n `${spec.bin} ${name}: ${error instanceof Error ? error.message : String(error)}\\n\\n${commandHelp(spec, name, command)}`,\n )\n return 2\n }\n return command.run(parsed.values, parsed.positionals)\n}\n\n// \u2500\u2500 serve \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport type LogFormat = \"pretty\" | \"json\" | \"off\"\n\nexport type CommonServeOptions = {\n adminKey: string | undefined\n seed: string | undefined\n onLog: ((entry: RequestLog) => void) | undefined\n}\n\n/**\n * What a service contributes to `serve`: how to build its runtime from CLI flags,\n * and what to say at startup. Every service's `./server` entry exports one as\n * `serveTarget`, which is also how `serve --config` finds services by name.\n */\nexport type ServeTarget = {\n name: string\n defaultPort: number\n /** Serve flags beyond the common ones. */\n options?: Record<string, CliOption>\n create(\n values: CliValues,\n common: CommonServeOptions,\n ): Promise<ServiceRuntime<ServiceInstance>> | ServiceRuntime<ServiceInstance>\n /** Startup lines after the listen address, e.g. the loaded corpus version. */\n banner?(runtime: ServiceRuntime<ServiceInstance>): string[]\n /** After the HTTP server is bound. A service that speaks more than HTTP attaches it here. */\n listening?(server: Listening): void\n}\n\nconst COMMON_SERVE_OPTIONS: Record<string, CliOption> = {\n port: { type: \"string\", value: \"<port>\", description: \"Port to listen on\" },\n host: { type: \"string\", value: \"<host>\", description: \"Interface to bind\", default: \"127.0.0.1\" },\n \"admin-key\": {\n type: \"string\",\n value: \"<key>\",\n description: \"Require x-mockingbird-admin-key on /__admin/* (env MOCKINGBIRD_ADMIN_KEY)\",\n },\n seed: { type: \"string\", value: \"<seed>\", description: \"Seed for every random choice\" },\n log: {\n type: \"string\",\n value: \"<pretty|json|off>\",\n description: \"Request log format\",\n default: \"pretty\",\n },\n \"log-requests\": {\n type: \"boolean\",\n description:\n \"One JSON line per request: namespace, operationId, status, ids touched (never bodies). Same as --log json\",\n },\n config: {\n type: \"string\",\n value: \"<file>\",\n description: \"Serve every service in a mockingbird.json config instead\",\n },\n}\n\nconst formatLog = (format: LogFormat) => {\n if (format === \"off\") return undefined\n if (format === \"json\") return (entry: RequestLog) => console.log(JSON.stringify(entry))\n return (entry: RequestLog) => {\n const op = entry.operationId ?? (entry.unmatched ? \"UNMATCHED\" : \"-\")\n const ns = entry.namespace === \"default\" ? \"\" : ` [${entry.namespace}]`\n const fault = entry.faultId ? ` fault=${entry.faultId}` : \"\"\n const adopted = entry.adopted ? \" adopted\" : \"\"\n // A rejection says what arrived (never the body) and why the service refused it.\n const received = entry.request\n ? ` received=${entry.request.bodyBytes === null ? \"?\" : `${entry.request.bodyBytes}B`} ct=${entry.request.contentType ?? \"-\"}${\n entry.request.transferEncoding ? ` te=${entry.request.transferEncoding}` : \"\"\n }`\n : \"\"\n const issues = entry.issues?.length\n ? ` issues=${entry.issues\n .slice(0, 3)\n .map((issue) => `${issue.path || \"$\"}: ${issue.message}`)\n .join(\"; \")}${entry.issues.length > 3 ? ` (+${entry.issues.length - 3})` : \"\"}`\n : \"\"\n console.log(\n `${entry.service} ${entry.method} ${entry.path} ${entry.status} ${op} ${entry.durationMs}ms${ns}${fault}${adopted}${received}${issues}`,\n )\n }\n}\n\nconst asString = (value: string | boolean | undefined): string | undefined =>\n typeof value === \"string\" ? value : undefined\n\n/** A service entry in `mockingbird.json`. */\nexport type ConfigService = {\n port?: number\n host?: string\n adminKey?: string\n seed?: string\n /** Service-specific serve flags, by long name: `{ \"webhook-url\": \"\u2026\" }`. */\n options?: Record<string, string | boolean>\n}\n\nexport type MockingbirdConfig = {\n /** Keyed by service name: `junction` loads `@crvouga/mockingbird-service-junction`. */\n services: Record<string, ConfigService>\n log?: LogFormat\n}\n\nconst loadTarget = async (name: string, own: ServeTarget): Promise<ServeTarget> => {\n if (name === own.name) return own\n const specifier = `@crvouga/mockingbird-service-${name}/server`\n try {\n const mod = (await import(specifier)) as { serveTarget?: ServeTarget }\n if (!mod.serveTarget) throw new Error(`${specifier} exports no serveTarget`)\n return mod.serveTarget\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error)\n throw new Error(\n `cannot load service \"${name}\": ${reason}. Install @crvouga/mockingbird-service-${name}.`,\n )\n }\n}\n\nconst start = async (\n target: ServeTarget,\n values: CliValues,\n config: { port: number; host: string; adminKey?: string; seed?: string; log: LogFormat },\n): Promise<Listening> => {\n const runtime = await target.create(values, {\n adminKey: config.adminKey,\n seed: config.seed,\n onLog: formatLog(config.log),\n })\n const listening = await listen(runtime, { port: config.port, host: config.host })\n target.listening?.(listening)\n console.log(`${target.name} mock listening on ${listening.url}`)\n console.log(`${target.name} health: GET ${listening.url}/health`)\n console.log(\n `${target.name} admin: ${listening.url}/__admin (${config.adminKey ? \"x-mockingbird-admin-key required\" : \"open \u2014 pass --admin-key to lock\"})`,\n )\n console.log(`${target.name} admin ui: ${listening.url}/__admin/ui`)\n for (const line of target.banner?.(runtime) ?? []) console.log(`${target.name} ${line}`)\n return listening\n}\n\nconst untilSignal = async (servers: Listening[]): Promise<number> =>\n new Promise((resolve) => {\n const stop = async () => {\n await Promise.allSettled(servers.map((s) => s.close()))\n resolve(0)\n }\n process.once(\"SIGINT\", stop)\n process.once(\"SIGTERM\", stop)\n })\n\n/** The standard `serve` command for a service, including multi-service `--config`. */\nexport const serveCommand = (target: ServeTarget): CliCommand => ({\n summary: `Serve the ${target.name} mock over HTTP`,\n options: { ...COMMON_SERVE_OPTIONS, ...target.options },\n async run(values) {\n const log = (\n values[\"log-requests\"] === true ? \"json\" : (asString(values.log) ?? \"pretty\")\n ) as LogFormat\n if (![\"pretty\", \"json\", \"off\"].includes(log)) {\n console.error(`--log must be pretty, json or off (got ${log})`)\n return 2\n }\n const configPath = asString(values.config)\n if (configPath !== undefined) {\n const config = JSON.parse(await readFile(configPath, \"utf8\")) as MockingbirdConfig\n const servers: Listening[] = []\n try {\n for (const [name, entry] of Object.entries(config.services ?? {})) {\n const each = await loadTarget(name, target)\n servers.push(\n await start(each, entry.options ?? {}, {\n port: entry.port ?? each.defaultPort,\n host: entry.host ?? \"127.0.0.1\",\n ...(entry.adminKey !== undefined ? { adminKey: entry.adminKey } : {}),\n ...(entry.seed !== undefined ? { seed: entry.seed } : {}),\n log: config.log ?? log,\n }),\n )\n }\n } catch (error) {\n await Promise.allSettled(servers.map((s) => s.close()))\n console.error(error instanceof Error ? error.message : String(error))\n return 1\n }\n return untilSignal(servers)\n }\n const port = asString(values.port)\n const adminKey = asString(values[\"admin-key\"]) ?? process.env.MOCKINGBIRD_ADMIN_KEY\n const seed = asString(values.seed)\n let listening: Listening\n try {\n listening = await start(target, values, {\n port: port === undefined ? target.defaultPort : Number.parseInt(port, 10),\n host: asString(values.host) ?? \"127.0.0.1\",\n ...(adminKey !== undefined ? { adminKey } : {}),\n ...(seed !== undefined ? { seed } : {}),\n log,\n })\n } catch (error) {\n console.error(error instanceof Error ? error.message : String(error))\n return 1\n }\n return untilSignal([listening])\n },\n})\n", "import { createServer } from \"node:http\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\n\n/** Options for {@link serve}. */\nexport type NodeServeOptions = {\n port?: number\n host?: string\n}\n\n/**\n * Serve any Mockingbird {@link FetchAPI} over `node:http`.\n * Port defaults to `0`, so the OS assigns an ephemeral port (read from `server.address()`).\n */\nexport const serve = async (api: FetchAPI, options: NodeServeOptions = {}) => {\n const server = createServer(async (req, res) => {\n const chunks: Buffer[] = []\n for await (const chunk of req) {\n chunks.push(Buffer.from(chunk))\n }\n const body = Buffer.concat(chunks)\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : undefined\n const base = `http://${req.headers.host ?? `localhost:${port ?? 80}`}`\n const raw = req.url ?? \"/\"\n const url = new URL(raw.replace(/^\\/+/, \"/\"), base)\n const method = req.method ?? \"GET\"\n const init: RequestInit = { method, headers: req.headers as Record<string, string> }\n if (method !== \"GET\" && method !== \"HEAD\" && body.length > 0) {\n init.body = body\n }\n const aborted = new AbortController()\n res.once(\"close\", () => {\n if (!res.writableFinished) aborted.abort()\n })\n init.signal = aborted.signal\n const request = new Request(url, init)\n let response: Response\n try {\n response = await api.fetch(request)\n } catch (error) {\n // A `drop` fault: destroy the socket so the client sees the connection die.\n if ((error as { code?: string }).code === \"MOCKINGBIRD_DROP\") {\n req.socket.destroy()\n return\n }\n res.writeHead(500, { \"content-type\": \"application/json\" })\n res.end(\n JSON.stringify({\n error: {\n type: \"mockingbird_internal\",\n message: error instanceof Error ? error.message : String(error),\n },\n }),\n )\n return\n }\n // Headers#entries() joins repeated headers; Set-Cookie must stay one header per cookie.\n const headers: Record<string, string | string[]> = Object.fromEntries(response.headers)\n const cookies = response.headers.getSetCookie()\n if (cookies.length > 0) headers[\"set-cookie\"] = cookies\n if (!response.body) {\n res.writeHead(response.status, headers)\n res.end()\n return\n }\n // Stream the body chunk by chunk: event streams and long-polls must not be buffered.\n res.writeHead(response.status, headers)\n res.flushHeaders()\n const reader = response.body.getReader()\n try {\n for (;;) {\n const { done, value } = await reader.read()\n if (done) break\n if (!res.write(value)) await new Promise<void>((resolve) => res.once(\"drain\", resolve))\n }\n res.end()\n } catch {\n res.destroy()\n } finally {\n reader.releaseLock()\n }\n })\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", reject)\n server.listen(options.port ?? 0, options.host, resolve)\n })\n return server\n}\n", "import type { Server } from \"node:http\"\nimport type { FetchAPI } from \"@crvouga/mockingbird-core\"\nimport { serve } from \"./serve.js\"\n\nexport type ListenOptions = {\n /** Default `0`: the OS picks a free port. */\n port?: number\n /** Default `127.0.0.1`: a mock should not be reachable off the machine by accident. */\n host?: string\n}\n\n/** A running server, with the address it actually bound. */\nexport type Listening = {\n url: string\n port: number\n host: string\n server: Server\n close(): Promise<void>\n}\n\nexport const listen = async (api: FetchAPI, options: ListenOptions = {}): Promise<Listening> => {\n const host = options.host ?? \"127.0.0.1\"\n const server = await serve(api, { port: options.port ?? 0, host })\n const address = server.address()\n const port = typeof address === \"object\" && address !== null ? address.port : (options.port ?? 0)\n const shown = host.includes(\":\") ? `[${host}]` : host\n return {\n url: `http://${shown}:${port}`,\n port,\n host,\n server,\n close: () =>\n new Promise<void>((resolve, reject) => {\n // Stop accepting first, then drop keep-alive sockets so close() can finish.\n server.close((error) => {\n if (error && (error as { code?: string }).code !== \"ERR_SERVER_NOT_RUNNING\") reject(error)\n else resolve()\n })\n server.closeAllConnections?.()\n }),\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["readFile", "createServer", "readFile"]
7
+ }
@@ -1887,6 +1887,50 @@ function bootAdmin(config) {
1887
1887
  if (render)
1888
1888
  await render();
1889
1889
  }
1890
+ const delay = (ms) => new Promise((resolve) => {
1891
+ window.setTimeout(resolve, ms);
1892
+ });
1893
+ const confirmAction = async (button, pending, done, work) => {
1894
+ if (button.dataset.pending === "1")
1895
+ return;
1896
+ const label = button.textContent ?? "";
1897
+ const width = button.offsetWidth;
1898
+ button.dataset.pending = "1";
1899
+ button.classList.add("pressed");
1900
+ button.setAttribute("aria-busy", "true");
1901
+ button.style.minWidth = `${width}px`;
1902
+ button.textContent = pending;
1903
+ try {
1904
+ await work();
1905
+ button.textContent = done;
1906
+ await delay(1e3);
1907
+ } catch (error) {
1908
+ showError(error);
1909
+ } finally {
1910
+ button.textContent = label;
1911
+ button.style.minWidth = "";
1912
+ button.classList.remove("pressed");
1913
+ button.removeAttribute("aria-busy");
1914
+ delete button.dataset.pending;
1915
+ }
1916
+ };
1917
+ document.addEventListener("click", (event) => {
1918
+ const target = event.target;
1919
+ if (!(target instanceof Element))
1920
+ return;
1921
+ const button = target.closest("button");
1922
+ if (!(button instanceof HTMLButtonElement) || button.dataset.pending === "1")
1923
+ return;
1924
+ button.classList.add("pressed");
1925
+ const token = String(Date.now());
1926
+ button.dataset.press = token;
1927
+ window.setTimeout(() => {
1928
+ if (button.dataset.press !== token || button.dataset.pending === "1")
1929
+ return;
1930
+ button.classList.remove("pressed");
1931
+ delete button.dataset.press;
1932
+ }, 450);
1933
+ });
1890
1934
  el("namespace", HTMLSelectElement).addEventListener("change", () => {
1891
1935
  state.namespace = el("namespace", HTMLSelectElement).value;
1892
1936
  refresh().catch(showError);
@@ -1897,7 +1941,7 @@ function bootAdmin(config) {
1897
1941
  refresh().catch(showError);
1898
1942
  });
1899
1943
  el("refresh", HTMLButtonElement).addEventListener("click", () => {
1900
- refresh().catch(showError);
1944
+ confirmAction(el("refresh", HTMLButtonElement), "Refreshing...", "Updated", () => refresh()).catch(showError);
1901
1945
  });
1902
1946
  el("collections", HTMLElement).addEventListener("click", (event) => {
1903
1947
  const target = event.target;
@@ -2020,10 +2064,10 @@ function bootAdmin(config) {
2020
2064
  }).then(() => renderFaults()).catch(showError);
2021
2065
  });
2022
2066
  el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
2023
- api("DELETE", "/faults").then(() => renderFaults()).catch(showError);
2067
+ confirmAction(el("fault-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/faults").then(() => renderFaults())).catch(showError);
2024
2068
  });
2025
2069
  el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
2026
- api("DELETE", "/requests").then(() => renderJournal()).catch(showError);
2070
+ confirmAction(el("journal-clear", HTMLButtonElement), "Clearing...", "Cleared", () => api("DELETE", "/requests").then(() => renderJournal())).catch(showError);
2027
2071
  });
2028
2072
  el("route-list", HTMLElement).addEventListener("click", (event) => {
2029
2073
  const target = event.target;
@@ -2138,9 +2182,13 @@ function bootAdmin(config) {
2138
2182
  var clientPrelude = [isRecord2, mountAdminBrand, startAdminBrand, faultPresetList, bootAdmin].map((fn) => fn.toString()).join("\n");
2139
2183
  var adminClientSource = (config) => {
2140
2184
  const payload = JSON.stringify(config).replace(/</g, "\\u003c");
2185
+ const entry = bootAdmin.name;
2186
+ if (!/^[A-Za-z_$][\w$]*$/.test(entry)) {
2187
+ throw new Error("admin client cannot call its boot function");
2188
+ }
2141
2189
  return `<script>
2142
2190
  ${clientPrelude}
2143
- bootAdmin(${payload});
2191
+ ${entry}(${payload});
2144
2192
  </script>`;
2145
2193
  };
2146
2194
 
@@ -2354,9 +2402,24 @@ var renderAdminDocument = (service) => {
2354
2402
  .controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
2355
2403
  label.field { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--faint); }
2356
2404
  label.field input, label.field select, .controls input[type="password"] {
2357
- background: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
2358
- padding: 7px 8px; min-height: 36px;
2359
- }
2405
+ background-color: var(--bg-inset); border: 1px solid var(--line); border-radius: 8px;
2406
+ padding: 7px 8px; min-height: 36px; line-height: 1.2;
2407
+ }
2408
+ /* A native menu ignores min-height, so Preset rendered about half as tall as
2409
+ the text fields in the same form. Paint it like those fields. */
2410
+ label.field select {
2411
+ -webkit-appearance: none; appearance: none;
2412
+ padding-right: 28px;
2413
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%2371717a' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M2.5 4.5 6 8l3.5-3.5'/%3E%3C/svg%3E");
2414
+ background-repeat: no-repeat; background-position: right 10px center; background-size: 12px 12px;
2415
+ }
2416
+ .stack > .field > input,
2417
+ .stack > .field > select,
2418
+ .stack > .field > textarea,
2419
+ dialog .field > input,
2420
+ dialog .field > select,
2421
+ dialog .field > textarea { width: 100%; }
2422
+ .icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger, nav.side button { cursor: pointer; }
2360
2423
  .icon-btn, .btn, .btn-primary, .btn-quiet, .btn-danger {
2361
2424
  border-radius: 9px; min-height: 36px; padding: 0 12px; border: 1px solid var(--line);
2362
2425
  background: var(--bg-elev);
@@ -2364,6 +2427,14 @@ var renderAdminDocument = (service) => {
2364
2427
  .btn-primary { background: var(--accent); color: var(--accent-ink); border-color: transparent; }
2365
2428
  .btn-quiet { background: transparent; }
2366
2429
  .btn-danger { color: var(--err); }
2430
+ /* Held after mouseup. Clearing an empty journal changes nothing on screen,
2431
+ so the press itself has to stay visible. */
2432
+ .icon-btn:active, .btn:active, .btn-quiet:active, .btn-danger:active, nav.side button:active,
2433
+ .icon-btn.pressed, .btn.pressed, .btn-quiet.pressed, .btn-danger.pressed, nav.side button.pressed {
2434
+ background: var(--bg-inset); border-color: var(--line-strong); color: var(--ink);
2435
+ }
2436
+ .btn-danger:active, .btn-danger.pressed { background: var(--err-soft); color: var(--err); }
2437
+ .btn-primary:active, .btn-primary.pressed { filter: brightness(0.88); color: var(--accent-ink); }
2367
2438
  /* minmax(0, 1fr): a 1fr column will not shrink below its content, so a long
2368
2439
  record or the full nav row would widen the page past the viewport. */
2369
2440
  .layout {
@@ -2452,8 +2523,16 @@ var renderAdminDocument = (service) => {
2452
2523
  input, select, textarea { font-size: 16px; }
2453
2524
  .brand, .vendor, .controls { width: 100%; }
2454
2525
  label.field, label.field select, label.field input { flex: 1; min-width: 0; }
2455
- .layout { grid-template-columns: minmax(0, 1fr); }
2526
+ /* The layout fills the screen. A single-column grid stretches every auto
2527
+ row, so the tab bar grew with the leftover height and the buttons floated
2528
+ in the middle of it. The bar stays one row; the page below takes the rest. */
2529
+ .layout {
2530
+ grid-template-columns: minmax(0, 1fr);
2531
+ grid-template-rows: auto minmax(0, 1fr);
2532
+ align-content: start;
2533
+ }
2456
2534
  nav.side {
2535
+ align-self: start; height: auto;
2457
2536
  flex-direction: row; align-items: center; overflow-x: auto; min-width: 0;
2458
2537
  border-right: 0; border-bottom: 1px solid var(--line);
2459
2538
  padding: 8px;
@@ -3551,7 +3630,7 @@ var stateAdminRoutes = (routes) => {
3551
3630
  };
3552
3631
 
3553
3632
  // ../core/dist/version.js
3554
- var PACKAGE_VERSION = true ? "1.8.1" : UNRELEASED_VERSION;
3633
+ var PACKAGE_VERSION = true ? "1.9.1" : UNRELEASED_VERSION;
3555
3634
 
3556
3635
  // ../core/dist/webhooks.js
3557
3636
  var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
@@ -6164,4 +6243,4 @@ export {
6164
6243
  createMultiRuntime,
6165
6244
  OAuthAPI
6166
6245
  };
6167
- //# sourceMappingURL=chunk-Y3AP4ZFX.js.map
6246
+ //# sourceMappingURL=chunk-XQNANV2C.js.map