diff --git a/README.md b/README.md index 1f20f27..076486f 100644 --- a/README.md +++ b/README.md @@ -713,8 +713,8 @@ included, since the plugin folder *is* the repo. A baked image needs no extra st contract, turning a sign-in redirect into a 500, so discovery refuses one there at boot. - **The host never upgrades or dedupes your dependencies.** Two plugins depending on the same package each get their own copy at their own version, so neither can break the other by upgrading — and - keeping yours current, and audited, is yours to own. Renovate here watches the host's manifests - only. + keeping yours current, and audited, is yours to own. Renovate here watches every manifest in this + repo, the example plugins included — a plugin in its own repo needs its own. - **Depend on packages that ship JavaScript.** Node refuses to strip types under `node_modules`, so a dependency whose entry is `.ts` fails at import with `ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING`. @@ -759,8 +759,7 @@ The menu is **driven entirely by config** and assembled from two sources: in or bind-mounting your own dir onto `/app/config` (a commented example sits in `compose.override.yml`). The file imports its typed builder from **`#menu-config`** (the subpath import mapped to `src/ui/menu-config.ts`), so it resolves wherever it's mounted - (keep the mounted `config/` a plain dir — no `package.json` of its own — or `#menu-config` - resolves against that instead and boot fails loud): + (keep the mounted `config/` a plain dir — no `package.json` of its own): ```ts import { defineMenu } from "#menu-config"; export default defineMenu({ branding: { name: "Acme Ops" }, override: { hide: ["teams"] } }); diff --git a/renovate.json b/renovate.json index b6327ea..e3366e4 100644 --- a/renovate.json +++ b/renovate.json @@ -1,6 +1,8 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": ["config:recommended"], + "description": "ignorePaths overrides config:recommended's :ignoreModulesAndTests, which ignores **/examples/** — an example plugin's dependencies get update PRs like any other manifest here", + "ignorePaths": ["**/node_modules/**"], "automerge": true, "commitBody": "Release-Bump: {{{updateType}}}", "packageRules": [ diff --git a/src/auth/flow-view.test.ts b/src/auth/flow-view.test.ts index 0aa0ec2..7e9aef9 100644 --- a/src/auth/flow-view.test.ts +++ b/src/auth/flow-view.test.ts @@ -113,6 +113,7 @@ test("the code field guards a pasted space: one-time-code autofill + numeric inp ); assert.deepEqual(view.fields.find((f) => f.name === "code"), { autocomplete: "one-time-code", // Kratos sends none for the OTP node — enable OS/email autofill + hint: "Digits only — no spaces.", // the pattern refusal alone reads as a bare "match the requested format" icon: "i-shield", id: "field-code", inputmode: "numeric", diff --git a/src/auth/flow-view.ts b/src/auth/flow-view.ts index 04826cb..8a8d677 100644 --- a/src/auth/flow-view.ts +++ b/src/auth/flow-view.ts @@ -11,6 +11,7 @@ import type { Flow, FlowType, UiNode } from "./kratos-public.ts"; export interface FlowField { autocomplete?: string; error?: { text: string }; + hint?: string; // muted helper text under the input icon?: string; // Lucide sprite id for the input id: string; inputmode?: string; // virtual-keyboard hint (e.g. "numeric" for the OTP code) @@ -139,7 +140,7 @@ function toField(node: UiNode, name: string, type: string, t: Translate): FlowFi ...(autocomplete ? { autocomplete } : {}), ...(errorMsg ? { error: { text: kratosText(t, errorMsg.text, idKey(errorMsg.id)) } } : {}), ...(icon ? { icon } : {}), - ...(isCode ? { inputmode: "numeric", pattern: "[0-9]*" } : {}), + ...(isCode ? { hint: t("auth.field.code.hint"), inputmode: "numeric", pattern: "[0-9]*" } : {}), ...(node.attributes["required"] === true ? { required: true } : {}), ...(value ? { value } : {}), }; diff --git a/src/i18n/locales/en-US.ts b/src/i18n/locales/en-US.ts index 1c4bbc7..a1e095b 100644 --- a/src/i18n/locales/en-US.ts +++ b/src/i18n/locales/en-US.ts @@ -9,6 +9,7 @@ const messages = { "auth.continue": "Continue", // Kratos labels its own form fields; these translate the ones the built-in identity schema uses, // keyed on the input name. A deployment's extra traits keep Kratos' label until a plugin covers them. + "auth.field.code.hint": "Digits only — no spaces.", "auth.field.email": "Email", "auth.field.identifier": "Email", "auth.field.password": "Password", diff --git a/src/i18n/locales/sv-SE.ts b/src/i18n/locales/sv-SE.ts index 0fcd4d3..ff6cdbe 100644 --- a/src/i18n/locales/sv-SE.ts +++ b/src/i18n/locales/sv-SE.ts @@ -2,6 +2,7 @@ import type { CoreMessages } from "./en-US.ts"; const messages: CoreMessages = { "auth.continue": "Fortsätt", + "auth.field.code.hint": "Endast siffror — inga mellanslag.", "auth.field.email": "E-postadress", "auth.field.identifier": "E-postadress", "auth.field.password": "Lösenord", diff --git a/src/ui/menu-config.test.ts b/src/ui/menu-config.test.ts index a5657bb..d53f19f 100644 --- a/src/ui/menu-config.test.ts +++ b/src/ui/menu-config.test.ts @@ -1,16 +1,20 @@ import assert from "node:assert/strict"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test, type TestContext } from "node:test"; import { DEFAULT_MENU, loadMenuConfig } from "./menu-config.ts"; // Write a throwaway menu.ts (a plain object — defineMenu is identity) and clean it up after. -function scaffold(t: TestContext, source: string): string { +function scaffold(t: TestContext, source: string, strays: string[] = []): string { const dir = mkdtempSync(join(tmpdir(), "pp-menu-")); t.after(() => rmSync(dir, { force: true, recursive: true })); const file = join(dir, "menu.ts"); writeFileSync(file, source); + for (const stray of strays) { + if (stray.endsWith(".json")) writeFileSync(join(dir, stray), "{}"); + else mkdirSync(join(dir, stray), { recursive: true }); + } return file; } @@ -37,3 +41,14 @@ test("loadMenuConfig fails loud on a malformed config", async (t) => { await assert.rejects(loadMenuConfig({ file: scaffold(t, `export default { branding: { theme: "neon" } };`) }), /theme/); await assert.rejects(loadMenuConfig({ file: scaffold(t, `export default { override: { hide: "teams" } };`) }), /hide.*array/s); }); + +test("loadMenuConfig refuses a stray package.json or node_modules beside the config", async (t) => { + const valid = `export default { branding: { name: "Acme Ops" } };`; + + for (const stray of ["node_modules", "package.json"]) { + await assert.rejects( + loadMenuConfig({ file: scaffold(t, valid, [stray]) }), + new RegExp(`config/${stray.replace(".", "\\.")} must not exist.*delete`, "s"), + ); + } +}); diff --git a/src/ui/menu-config.ts b/src/ui/menu-config.ts index 7c24373..d897c67 100644 --- a/src/ui/menu-config.ts +++ b/src/ui/menu-config.ts @@ -50,6 +50,14 @@ export async function loadMenuConfig(options: LoadMenuOptions = {}): Promise