@warlock.js/core 5.34.0 → 5.34.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 +10 -0
- package/esm/generations/features/web.feature.mjs +7 -4
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/generations/stubs.mjs +18 -35
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/index.d.mts +2 -2
- package/esm/secrets/file-backend.mjs +1 -0
- package/esm/secrets/file-backend.mjs.map +1 -1
- package/esm/secrets/index.d.mts +2 -2
- package/esm/secrets/keyring-backend.mjs +1 -0
- package/esm/secrets/keyring-backend.mjs.map +1 -1
- package/esm/secrets/memory-backend.d.mts +1 -0
- package/esm/secrets/memory-backend.d.mts.map +1 -1
- package/esm/secrets/memory-backend.mjs +1 -0
- package/esm/secrets/memory-backend.mjs.map +1 -1
- package/esm/secrets/secret-store.d.mts +4 -2
- package/esm/secrets/secret-store.d.mts.map +1 -1
- package/esm/secrets/secret-store.mjs +19 -8
- package/esm/secrets/secret-store.mjs.map +1 -1
- package/esm/secrets/secrets.d.mts +2 -0
- package/esm/secrets/secrets.d.mts.map +1 -1
- package/esm/secrets/secrets.mjs +3 -0
- package/esm/secrets/secrets.mjs.map +1 -1
- package/esm/secrets/types.d.mts +4 -0
- package/esm/secrets/types.d.mts.map +1 -1
- package/llms-full.txt +26 -3
- package/llms.txt +2 -2
- package/package.json +13 -13
- package/skills/store-secrets/SKILL.md +20 -1
- package/skills/write-cli-command/SKILL.md +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
> ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an _Upgrading_ section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
|
|
8
8
|
|
|
9
|
+
## 5.34.1 - 2026-10-19
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Async `secrets.backend()` and `SecretStore.backend()` report `"keychain"`, `"file"` or `"memory"` without credentials, sharing lazy selection and its memoized keychain probe. Resolved driver configuration, environment lookups and injected backend metadata have documented reporting semantics; memory fakes report `"memory"` without probing.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Generate `root.setup.ts` and `index.setup.ts` with Web starter policy and universal registration, keeping the UI files focused on components.
|
|
18
|
+
|
|
9
19
|
## 5.34.0 - 2026-10-19
|
|
10
20
|
|
|
11
21
|
### Added
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { rootPath, srcPath } from "../../utils/paths.mjs";
|
|
2
2
|
import "../../utils/index.mjs";
|
|
3
3
|
import { ensureDirectoryAsync, fileExistsAsync, getFileAsync, putFileAsync } from "../generation-fs.mjs";
|
|
4
|
-
import { webContactControllerStub, webContactRoutesStub, webHomePageStub,
|
|
4
|
+
import { webContactControllerStub, webContactRoutesStub, webHomePageStub, webHomeSetupStub, webRootSetupStub, webRootStub } from "../stubs.mjs";
|
|
5
5
|
import { INSTALLED_WARLOCK_VERSION } from "./types.mjs";
|
|
6
6
|
import { insertConnectorEntry } from "./shared/insert-connector-entry.mjs";
|
|
7
7
|
import { relocateConflictingHomeRoute } from "./shared/relocate-conflicting-home-route.mjs";
|
|
@@ -65,17 +65,20 @@ async function completeWebInstallation(_options) {
|
|
|
65
65
|
else {
|
|
66
66
|
await ensureDirectoryAsync(srcPath("web"));
|
|
67
67
|
await putFileAsync(rootFile, webRootStub);
|
|
68
|
+
const rootSetupFile = srcPath("web/root.setup.ts");
|
|
69
|
+
if (!await fileExistsAsync(rootSetupFile)) await putFileAsync(rootSetupFile, webRootSetupStub);
|
|
68
70
|
console.log(`${colors.green("✓")} Created src/web/root.tsx`);
|
|
69
71
|
const collision = await relocateConflictingHomeRoute();
|
|
70
72
|
if (collision.outcome === "relocated") console.log(`${colors.green("✓")} Moved the existing ${colors.yellowBright("GET \"/\"")} route to ${colors.yellowBright("\"/welcome\"")} in ${colors.yellowBright(`src/${collision.relativePath}`)} — the new page owns \`/\` now, and the JSON welcome route still answers at /welcome.`);
|
|
71
73
|
if (collision.outcome === "conflict" || collision.outcome === "failed") {
|
|
72
74
|
const verb = collision.outcome === "failed" ? colors.redBright("✗") : colors.yellowBright("!");
|
|
73
|
-
console.log(`${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\n The page
|
|
74
|
-
Free up ${colors.yellowBright("GET \"/\"")} under src/app — move it to a path of its own, or remove it — then create src/web/index.page.tsx yourself. Giving the
|
|
75
|
+
console.log(`${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\n The page setup declares ${colors.yellowBright("route.path = \"/\"")}, and two handlers on one path is a 500 at request time, not a startup error.
|
|
76
|
+
Free up ${colors.yellowBright("GET \"/\"")} under src/app — move it to a path of its own, or remove it — then create src/web/index.page.tsx yourself. Giving the setup file a \`config.route\` other than \`/\` works too.`);
|
|
75
77
|
process.exitCode = 1;
|
|
76
78
|
} else {
|
|
77
79
|
await putFileAsync(srcPath("web/index.page.tsx"), webHomePageStub);
|
|
78
|
-
|
|
80
|
+
const homeSetupFile = srcPath("web/index.setup.ts");
|
|
81
|
+
if (!await fileExistsAsync(homeSetupFile)) await putFileAsync(homeSetupFile, webHomeSetupStub);
|
|
79
82
|
const contactPlan = resolveContactScaffold({
|
|
80
83
|
controllerExists: await fileExistsAsync(srcPath("app/contact/controllers/contact.controller.ts")),
|
|
81
84
|
routesExists: await fileExistsAsync(srcPath("app/contact/routes.ts"))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"web.feature.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/web.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport {\n ensureDirectoryAsync,\n fileExistsAsync,\n getFileAsync,\n putFileAsync,\n} from \"../generation-fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport { insertConnectorEntry } from \"./shared/insert-connector-entry\";\nimport { relocateConflictingHomeRoute } from \"./shared/relocate-conflicting-home-route\";\nimport { resolveContactScaffold } from \"./shared/resolve-contact-scaffold\";\nimport {\n webContactControllerStub,\n webContactRoutesStub,\n webHomePageStub,\n webHomeRegisterStub,\n webRootStub,\n} from \"../stubs\";\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\n/** The one vite range shared by every feature (web's peer range is `>=7.3.5 <8`). */\nexport const VITE_RANGE = \"^7.3.5\";\n\n/**\n * Register the WebConnector in `warlock.config.ts`, and ONLY there.\n *\n * It belongs to the config array or to app code, never both. Both halves are\n * registered before app code loads — the CLI preloader in dev, the generated\n * entry in production — so also calling `connectorsManager.register(...)` in\n * `src/app/main.ts` boots the connector twice and installs every page route\n * twice. That surfaces at PRODUCTION boot as `Route name \"...\" is already\n * taken`, because pages and API routes share one route-name namespace.\n *\n * The config array is the half to prefer: `warlock build` reads the same array\n * to drain each connector's build contribution, so \"built for\" and \"boots with\"\n * cannot drift.\n *\n * String surgery rather than a TypeScript parse: `warlock.config.ts` is an\n * app-owned file that may carry any formatting, and a parse-and-print would\n * reformat the parts we did not come to change.\n */\nasync function registerWebConnector(): Promise<void> {\n const configPath = rootPath(\"warlock.config.ts\");\n\n if (!(await fileExistsAsync(configPath))) {\n console.log(\n `${colors.yellowBright(\"warlock.config.ts\")} not found — add this yourself:\\n` +\n ` import { webConnector } from \"@warlock.js/web/connector\";\\n` +\n ` export default defineConfig({ connectors: [webConnector()] });`,\n );\n\n return;\n }\n\n const current = await getFileAsync(configPath);\n\n if (current.includes(\"webConnector\")) {\n console.log(`${colors.yellowBright(\"webConnector\")} already registered, skipping...`);\n\n return;\n }\n\n const importLine = 'import { webConnector } from \"@warlock.js/web/connector\";';\n let next = current.includes(importLine) ? current : `${importLine}\\n${current}`;\n\n // An existing `connectors: [` gains one entry; otherwise the key is added to\n // the object `defineConfig` receives.\n const insertion = insertConnectorEntry(next, \"webConnector()\");\n\n if (insertion.status === \"already-present\") {\n return;\n }\n\n if (insertion.status === \"added\") {\n next = insertion.next;\n } else if (next.includes(\"defineConfig({\")) {\n next = next.replace(\"defineConfig({\", \"defineConfig({\\n connectors: [webConnector()],\");\n } else {\n console.log(\n `${colors.yellowBright(\"warlock.config.ts\")} has no recognisable defineConfig({...}) — ` +\n \"add `connectors: [webConnector()]` yourself.\",\n );\n\n return;\n }\n\n await putFileAsync(configPath, next);\n console.log(`${colors.green(\"✓\")} Registered webConnector in warlock.config.ts`);\n}\n\n/**\n * Scaffold the smallest page layer that renders, and register the connector.\n *\n * `src/web/root.tsx` is the sentinel for \"already scaffolded\" — the framework\n * ships a default root, so its presence means a human has been here.\n */\nasync function completeWebInstallation(_options: CommandActionData) {\n const rootFile = srcPath(\"web/root.tsx\");\n\n if (await fileExistsAsync(rootFile)) {\n console.log(`${colors.yellowBright(\"src/web\")} already scaffolded, skipping...`);\n } else {\n await ensureDirectoryAsync(srcPath(\"web\"));\n await putFileAsync(rootFile, webRootStub);\n console.log(`${colors.green(\"✓\")} Created src/web/root.tsx`);\n\n const collision = await relocateConflictingHomeRoute();\n\n if (collision.outcome === \"relocated\") {\n console.log(\n `${colors.green(\"✓\")} Moved the existing ${colors.yellowBright('GET \"/\"')} route to ` +\n `${colors.yellowBright('\"/welcome\"')} in ${colors.yellowBright(`src/${collision.relativePath}`)} — ` +\n \"the new page owns `/` now, and the JSON welcome route still answers at /welcome.\",\n );\n }\n\n // The page is written ONLY when `/` is provably free. Writing it while\n // another handler holds `/` produces a homepage that 500s on first request,\n // which is precisely the outcome a scaffolder must never hand back.\n if (collision.outcome === \"conflict\" || collision.outcome === \"failed\") {\n const verb =\n collision.outcome === \"failed\" ? colors.redBright(\"✗\") : colors.yellowBright(\"!\");\n\n console.log(\n `${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\\n` +\n ` The page stub declares ${colors.yellowBright('route.path = \"/\"')}, and two handlers on one ` +\n \"path is a 500 at request time, not a startup error.\\n\" +\n ` Free up ${colors.yellowBright('GET \"/\"')} under src/app — move it to a path of its own, ` +\n \"or remove it — then create src/web/index.page.tsx yourself. Giving the page a `route` other \" +\n \"than `/` works too.\",\n );\n\n // Non-zero on BOTH branches. The page layer this command exists to\n // scaffold was not scaffolded, and a 0 here is the exact \"looked like it\n // worked\" signal that put `/` in this state to begin with — a conflict we\n // declined to guess at is still an incomplete install, not a success.\n //\n // `exitCode` rather than `exit(1)`: the connector below still has to be\n // registered, and any other feature in the same `warlock add` invocation\n // still has to install, or the project is left half-wired on top of this.\n process.exitCode = 1;\n } else {\n await putFileAsync(srcPath(\"web/index.page.tsx\"), webHomePageStub);\n await putFileAsync(srcPath(\"web/index.register.ts\"), webHomeRegisterStub);\n\n // Unlike root.tsx above, the contact controller and its routes file are\n // ordinary application files a project can already have — independently\n // of ever having run `warlock add web`. Each is guarded on its OWN\n // existence, not on the (already-consumed) root.tsx sentinel, so an\n // existing `contact` module is skipped rather than clobbered. See\n // resolveContactScaffold's doc comment for why.\n const contactPlan = resolveContactScaffold({\n controllerExists: await fileExistsAsync(\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\n ),\n routesExists: await fileExistsAsync(srcPath(\"app/contact/routes.ts\")),\n });\n\n if (contactPlan.writeController) {\n await ensureDirectoryAsync(srcPath(\"app/contact/controllers\"));\n await putFileAsync(\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\n webContactControllerStub,\n );\n }\n\n if (contactPlan.writeRoutes) {\n await ensureDirectoryAsync(srcPath(\"app/contact\"));\n await putFileAsync(srcPath(\"app/contact/routes.ts\"), webContactRoutesStub);\n }\n\n console.log(`${colors.green(\"✓\")} Created src/web/index.page.tsx`);\n\n for (const message of contactPlan.messages) {\n console.log(message);\n }\n }\n }\n\n await registerWebConnector();\n}\n\nexport const webFeature: FeatureDefinition = {\n description:\n \"Installs @warlock.js/web — SSR React pages served by the Warlock HTTP server. Scaffolds src/web (root.tsx + a home page) and registers the WebConnector in warlock.config.ts. Pages are opt-in: a Warlock app is an API until you add this.\",\n dependencies: {\n \"@warlock.js/web\": INSTALLED_WARLOCK_VERSION,\n \"@mongez/http\": \"^3.5.0\",\n \"@mongez/react-form\": \"^4.0.0\",\n \"@mongez/react-localization\": \"^3.4.7\",\n react: \"^19.2.3\",\n \"react-dom\": \"^19.2.3\",\n },\n devDependencies: {\n \"@types/react\": \"^19.2.7\",\n \"@types/react-dom\": \"^19.2.3\",\n // Loaded through `await import()` by the dev server only, so both are\n // optional peers of `web` rather than hard dependencies.\n vite: VITE_RANGE,\n \"@vitejs/plugin-react\": \"^5.2.0\",\n },\n onExecuting: completeWebInstallation,\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"web.feature.mjs","names":[],"sources":["../../../../../../../../../packages/core/src/generations/features/web.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport {\n ensureDirectoryAsync,\n fileExistsAsync,\n getFileAsync,\n putFileAsync,\n} from \"../generation-fs\";\nimport type { CommandActionData } from \"../../commands/types\";\nimport { rootPath, srcPath } from \"../../utils\";\nimport { insertConnectorEntry } from \"./shared/insert-connector-entry\";\nimport { relocateConflictingHomeRoute } from \"./shared/relocate-conflicting-home-route\";\nimport { resolveContactScaffold } from \"./shared/resolve-contact-scaffold\";\nimport {\n webContactControllerStub,\n webContactRoutesStub,\n webHomePageStub,\n webHomeSetupStub,\n webRootStub,\n webRootSetupStub,\n} from \"../stubs\";\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\n/** The one vite range shared by every feature (web's peer range is `>=7.3.5 <8`). */\nexport const VITE_RANGE = \"^7.3.5\";\n\n/**\n * Register the WebConnector in `warlock.config.ts`, and ONLY there.\n *\n * It belongs to the config array or to app code, never both. Both halves are\n * registered before app code loads — the CLI preloader in dev, the generated\n * entry in production — so also calling `connectorsManager.register(...)` in\n * `src/app/main.ts` boots the connector twice and installs every page route\n * twice. That surfaces at PRODUCTION boot as `Route name \"...\" is already\n * taken`, because pages and API routes share one route-name namespace.\n *\n * The config array is the half to prefer: `warlock build` reads the same array\n * to drain each connector's build contribution, so \"built for\" and \"boots with\"\n * cannot drift.\n *\n * String surgery rather than a TypeScript parse: `warlock.config.ts` is an\n * app-owned file that may carry any formatting, and a parse-and-print would\n * reformat the parts we did not come to change.\n */\nasync function registerWebConnector(): Promise<void> {\n const configPath = rootPath(\"warlock.config.ts\");\n\n if (!(await fileExistsAsync(configPath))) {\n console.log(\n `${colors.yellowBright(\"warlock.config.ts\")} not found — add this yourself:\\n` +\n ` import { webConnector } from \"@warlock.js/web/connector\";\\n` +\n ` export default defineConfig({ connectors: [webConnector()] });`,\n );\n\n return;\n }\n\n const current = await getFileAsync(configPath);\n\n if (current.includes(\"webConnector\")) {\n console.log(`${colors.yellowBright(\"webConnector\")} already registered, skipping...`);\n\n return;\n }\n\n const importLine = 'import { webConnector } from \"@warlock.js/web/connector\";';\n let next = current.includes(importLine) ? current : `${importLine}\\n${current}`;\n\n // An existing `connectors: [` gains one entry; otherwise the key is added to\n // the object `defineConfig` receives.\n const insertion = insertConnectorEntry(next, \"webConnector()\");\n\n if (insertion.status === \"already-present\") {\n return;\n }\n\n if (insertion.status === \"added\") {\n next = insertion.next;\n } else if (next.includes(\"defineConfig({\")) {\n next = next.replace(\"defineConfig({\", \"defineConfig({\\n connectors: [webConnector()],\");\n } else {\n console.log(\n `${colors.yellowBright(\"warlock.config.ts\")} has no recognisable defineConfig({...}) — ` +\n \"add `connectors: [webConnector()]` yourself.\",\n );\n\n return;\n }\n\n await putFileAsync(configPath, next);\n console.log(`${colors.green(\"✓\")} Registered webConnector in warlock.config.ts`);\n}\n\n/**\n * Scaffold the smallest page layer that renders, and register the connector.\n *\n * `src/web/root.tsx` is the sentinel for \"already scaffolded\" — the framework\n * ships a default root, so its presence means a human has been here.\n */\nasync function completeWebInstallation(_options: CommandActionData) {\n const rootFile = srcPath(\"web/root.tsx\");\n\n if (await fileExistsAsync(rootFile)) {\n console.log(`${colors.yellowBright(\"src/web\")} already scaffolded, skipping...`);\n } else {\n await ensureDirectoryAsync(srcPath(\"web\"));\n await putFileAsync(rootFile, webRootStub);\n const rootSetupFile = srcPath(\"web/root.setup.ts\");\n if (!(await fileExistsAsync(rootSetupFile))) {\n await putFileAsync(rootSetupFile, webRootSetupStub);\n }\n console.log(`${colors.green(\"✓\")} Created src/web/root.tsx`);\n\n const collision = await relocateConflictingHomeRoute();\n\n if (collision.outcome === \"relocated\") {\n console.log(\n `${colors.green(\"✓\")} Moved the existing ${colors.yellowBright('GET \"/\"')} route to ` +\n `${colors.yellowBright('\"/welcome\"')} in ${colors.yellowBright(`src/${collision.relativePath}`)} — ` +\n \"the new page owns `/` now, and the JSON welcome route still answers at /welcome.\",\n );\n }\n\n // The page is written ONLY when `/` is provably free. Writing it while\n // another handler holds `/` produces a homepage that 500s on first request,\n // which is precisely the outcome a scaffolder must never hand back.\n if (collision.outcome === \"conflict\" || collision.outcome === \"failed\") {\n const verb =\n collision.outcome === \"failed\" ? colors.redBright(\"✗\") : colors.yellowBright(\"!\");\n\n console.log(\n `${verb} Did not create src/web/index.page.tsx: ${collision.reason}.\\n` +\n ` The page setup declares ${colors.yellowBright('route.path = \"/\"')}, and two handlers on one ` +\n \"path is a 500 at request time, not a startup error.\\n\" +\n ` Free up ${colors.yellowBright('GET \"/\"')} under src/app — move it to a path of its own, ` +\n \"or remove it — then create src/web/index.page.tsx yourself. Giving the setup file a `config.route` other \" +\n \"than `/` works too.\",\n );\n\n // Non-zero on BOTH branches. The page layer this command exists to\n // scaffold was not scaffolded, and a 0 here is the exact \"looked like it\n // worked\" signal that put `/` in this state to begin with — a conflict we\n // declined to guess at is still an incomplete install, not a success.\n //\n // `exitCode` rather than `exit(1)`: the connector below still has to be\n // registered, and any other feature in the same `warlock add` invocation\n // still has to install, or the project is left half-wired on top of this.\n process.exitCode = 1;\n } else {\n await putFileAsync(srcPath(\"web/index.page.tsx\"), webHomePageStub);\n const homeSetupFile = srcPath(\"web/index.setup.ts\");\n if (!(await fileExistsAsync(homeSetupFile))) {\n await putFileAsync(homeSetupFile, webHomeSetupStub);\n }\n\n // Unlike root.tsx above, the contact controller and its routes file are\n // ordinary application files a project can already have — independently\n // of ever having run `warlock add web`. Each is guarded on its OWN\n // existence, not on the (already-consumed) root.tsx sentinel, so an\n // existing `contact` module is skipped rather than clobbered. See\n // resolveContactScaffold's doc comment for why.\n const contactPlan = resolveContactScaffold({\n controllerExists: await fileExistsAsync(\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\n ),\n routesExists: await fileExistsAsync(srcPath(\"app/contact/routes.ts\")),\n });\n\n if (contactPlan.writeController) {\n await ensureDirectoryAsync(srcPath(\"app/contact/controllers\"));\n await putFileAsync(\n srcPath(\"app/contact/controllers/contact.controller.ts\"),\n webContactControllerStub,\n );\n }\n\n if (contactPlan.writeRoutes) {\n await ensureDirectoryAsync(srcPath(\"app/contact\"));\n await putFileAsync(srcPath(\"app/contact/routes.ts\"), webContactRoutesStub);\n }\n\n console.log(`${colors.green(\"✓\")} Created src/web/index.page.tsx`);\n\n for (const message of contactPlan.messages) {\n console.log(message);\n }\n }\n }\n\n await registerWebConnector();\n}\n\nexport const webFeature: FeatureDefinition = {\n description:\n \"Installs @warlock.js/web — SSR React pages served by the Warlock HTTP server. Scaffolds src/web (root.tsx + a home page) and registers the WebConnector in warlock.config.ts. Pages are opt-in: a Warlock app is an API until you add this.\",\n dependencies: {\n \"@warlock.js/web\": INSTALLED_WARLOCK_VERSION,\n \"@mongez/http\": \"^3.5.0\",\n \"@mongez/react-form\": \"^4.0.0\",\n \"@mongez/react-localization\": \"^3.4.7\",\n react: \"^19.2.3\",\n \"react-dom\": \"^19.2.3\",\n },\n devDependencies: {\n \"@types/react\": \"^19.2.7\",\n \"@types/react-dom\": \"^19.2.3\",\n // Loaded through `await import()` by the dev server only, so both are\n // optional peers of `web` rather than hard dependencies.\n vite: VITE_RANGE,\n \"@vitejs/plugin-react\": \"^5.2.0\",\n },\n onExecuting: completeWebInstallation,\n};\n"],"mappings":"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"}
|
|
@@ -825,13 +825,18 @@ ${signedInCases.replaceAll("fixture.", "fixture!.")}
|
|
|
825
825
|
});
|
|
826
826
|
`;
|
|
827
827
|
}
|
|
828
|
+
/** `src/web/root.setup.ts` — document policy. */
|
|
829
|
+
const webRootSetupStub = `import type { RootConfig } from "@warlock.js/web";
|
|
830
|
+
|
|
831
|
+
export const config: RootConfig = { strictMode: true };
|
|
832
|
+
`;
|
|
828
833
|
/**
|
|
829
834
|
* `src/web/root.tsx` — the application root for the SSR page layer.
|
|
830
835
|
*
|
|
831
836
|
* Deliberately minimal. The framework ships a default root, so this exists to
|
|
832
837
|
* give you a place to start rather than because anything requires it. The
|
|
833
|
-
*
|
|
834
|
-
*
|
|
838
|
+
* document component stays here; request policy and an app-level loader
|
|
839
|
+
* belong in `root.setup.ts`. Named ErrorBoundary components stay in the UI.
|
|
835
840
|
*/
|
|
836
841
|
const webRootStub = `import type { AppProps } from "@warlock.js/web";
|
|
837
842
|
import { Head, Scripts } from "@warlock.js/web";
|
|
@@ -925,14 +930,14 @@ import { contactController } from "./controllers/contact.controller";
|
|
|
925
930
|
|
|
926
931
|
router.post("/api/contact", contactController, { public: true });
|
|
927
932
|
`;
|
|
928
|
-
/**
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
933
|
+
/** `src/web/index.setup.ts` — page policy and universal registration. */
|
|
934
|
+
const webHomeSetupStub = `import { extend } from "@mongez/localization";
|
|
935
|
+
import type { PageConfig } from "@warlock.js/web";
|
|
936
|
+
|
|
937
|
+
export const config: PageConfig = {
|
|
938
|
+
route: { path: "/", name: "index" },
|
|
939
|
+
metadata: { title: "Home" },
|
|
940
|
+
};
|
|
936
941
|
|
|
937
942
|
export function register() {
|
|
938
943
|
extend("en", {
|
|
@@ -972,25 +977,9 @@ import { setCurrentLocaleCode } from "@mongez/localization";
|
|
|
972
977
|
import { Form, useFormControl, type FormControlProps } from "@mongez/react-form";
|
|
973
978
|
import { transX } from "@mongez/react-localization";
|
|
974
979
|
import { v } from "@warlock.js/seal";
|
|
975
|
-
import { Link, type
|
|
980
|
+
import { Link, type PageProps } from "@warlock.js/web";
|
|
976
981
|
import { useState } from "react";
|
|
977
982
|
|
|
978
|
-
export { register } from "./index.register";
|
|
979
|
-
|
|
980
|
-
/**
|
|
981
|
-
* A page route is an ordinary Warlock route whose handler renders React
|
|
982
|
-
* instead of returning JSON.
|
|
983
|
-
*
|
|
984
|
-
* The URL and stable hydration name are the ones this file DECLARES below.
|
|
985
|
-
* This page answers \`GET "/"\` because \`config.route.path = "/"\`, not because of
|
|
986
|
-
* where the file lives. A page file with
|
|
987
|
-
* no \`config.route\` declaration derives its URL from the filesystem.
|
|
988
|
-
*/
|
|
989
|
-
export const config: PageConfig = {
|
|
990
|
-
route: { path: "/", name: "index" },
|
|
991
|
-
metadata: { title: "Home" },
|
|
992
|
-
};
|
|
993
|
-
|
|
994
983
|
const contactSchema = v.object({
|
|
995
984
|
name: v.string().min(2),
|
|
996
985
|
email: v.email(),
|
|
@@ -1009,13 +998,7 @@ function TextInput({ label, ...controlProps }: FormControlProps & { label: strin
|
|
|
1009
998
|
);
|
|
1010
999
|
}
|
|
1011
1000
|
|
|
1012
|
-
/**
|
|
1013
|
-
* Add a \`loader\` export to fetch data on the server, and it arrives here as
|
|
1014
|
-
* \`data\`, typed:
|
|
1015
|
-
*
|
|
1016
|
-
* export const loader = (async () => ({ items: await itemsRepository.all() }));
|
|
1017
|
-
* export default function HomePage({ data }: PageProps<typeof loader>) { ... }
|
|
1018
|
-
*/
|
|
1001
|
+
/** Fetch data in index.setup.ts; use a type-only loader import for PageProps. */
|
|
1019
1002
|
export default function HomePage(_props: PageProps) {
|
|
1020
1003
|
// Live state. If the button below does nothing, the page rendered on the
|
|
1021
1004
|
// server but never hydrated — the runtime never mounted at \`#vessel\`. This is
|
|
@@ -1189,5 +1172,5 @@ function ContactMessage() {
|
|
|
1189
1172
|
`;
|
|
1190
1173
|
|
|
1191
1174
|
//#endregion
|
|
1192
|
-
export { accessConfigStub, accessResolverStub, accessRoleMigrationStub, accessRoleModelIndexStub, accessRoleModelStub, accessUserRoleMigrationStub, accessUserRoleModelIndexStub, accessUserRoleModelStub, aiConfigStub, communicatorsConfigStub, listNotificationsControllerStub, listNotificationsSchemaStub, markAllNotificationsReadControllerStub, markNotificationReadControllerStub, markNotificationReadServiceStub, notificationControllerSpecStub, notificationLocalesStub, notificationMigrationStub, notificationModelStub, notificationResourceStub, notificationRoutesStub, notificationTestFixtureStub, notificationsConfigStub, notificationsRequestTypeStub, socketConfigStub, unreadNotificationsCountControllerStub, webContactControllerStub, webContactRoutesStub, webHomePageStub,
|
|
1175
|
+
export { accessConfigStub, accessResolverStub, accessRoleMigrationStub, accessRoleModelIndexStub, accessRoleModelStub, accessUserRoleMigrationStub, accessUserRoleModelIndexStub, accessUserRoleModelStub, aiConfigStub, communicatorsConfigStub, listNotificationsControllerStub, listNotificationsSchemaStub, markAllNotificationsReadControllerStub, markNotificationReadControllerStub, markNotificationReadServiceStub, notificationControllerSpecStub, notificationLocalesStub, notificationMigrationStub, notificationModelStub, notificationResourceStub, notificationRoutesStub, notificationTestFixtureStub, notificationsConfigStub, notificationsRequestTypeStub, socketConfigStub, unreadNotificationsCountControllerStub, webContactControllerStub, webContactRoutesStub, webHomePageStub, webHomeSetupStub, webRootSetupStub, webRootStub };
|
|
1193
1176
|
//# sourceMappingURL=stubs.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stubs.mjs","names":[],"sources":["../../../../../../../../packages/core/src/generations/stubs.ts"],"sourcesContent":["export const accessConfigStub = `import { type AccessConfigurations } from \"@warlock.js/access\";\nimport { DatabaseAccessResolver } from \"app/access/services/access-resolver\";\n\n/**\n * Authorization configuration — read by @warlock.js/access on boot.\n *\n * The resolver is the one required piece: it tells the engine how to read a\n * user's roles + permissions. The ejected DatabaseAccessResolver reads roles\n * from the user_roles table and maps them through the roles catalog table (so\n * roles + their permissions are managed at runtime, in the DB).\n *\n * For a fixed, code-defined catalog with no tables, swap in DefaultAccessResolver:\n * import { DefaultAccessResolver } from \"@warlock.js/access\";\n * resolver: new DefaultAccessResolver({ admin: [\"*\"], editor: [\"orders.*\"] }),\n *\n * Multi-tenant? Add a \\`resolveTenant()\\` to the resolver to read the active\n * tenant from the request; checks then scope to it automatically.\n */\nconst access: AccessConfigurations = {\n resolver: new DatabaseAccessResolver(),\n\n // Cache resolved permission sets (default \"10m\").\n // cache: { ttl: \"10m\" },\n};\n\nexport default access;\n`;\n\nexport const aiConfigStub = `import type { AIConfig } from \"@warlock.js/ai\";\n\n// >>> warlock:ai-packages (auto-managed) >>>\n// Satellite packages augment the \"ai\" object on import — e.g. ai.workspace,\n// ai.tools / ai.mcp, and panoptic's ai.config({ panoptic }) wiring. The command\n// \"warlock add ai-workspace | ai-tools | ai-panoptic\" adds the matching\n// side-effect import below; keep them so the augmentation + runtime registration\n// load before the ai connector applies this config.\n// <<< warlock:ai-packages <<<\n\n/**\n * AI configuration — applied on boot by the ai connector, which calls\n * ai.config(...) with the object below. Cross-cutting defaults live here\n * (shared cache / snapshot stores, observability); per-call options always win.\n *\n * Wire a default model from a provider you installed, e.g.:\n * import { OpenAISDK } from \"@warlock.js/ai-openai\";\n * const openai = OpenAISDK({ apiKey: env(\"OPENAI_API_KEY\") });\n * // then pass openai.model({ name: \"gpt-4o-mini\" }) into your agents.\n */\nconst ai: Partial<AIConfig> = {\n // Default cache driver for cache-backed AI features (semantic cache, rag / memory vector stores).\n // defaultStore: cache.driver(\"redis\", { client }),\n\n // Observability — requires \"warlock add ai-panoptic\". Exporters + the local dashboard.\n // panoptic: { exporters: [], dashboard: false, observeAll: false },\n};\n\nexport default ai;\n`;\n\nexport const accessRoleModelStub = `import { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for the roles catalog — mirrors the migration columns\n * (snake_case). Each row is a role name plus the permission strings it grants;\n * wildcards work (\"orders.*\", \"*\"). The DatabaseAccessResolver maps a user's\n * assigned role names through this table to their effective permissions.\n */\nexport const roleSchema = v.object({\n name: v.string(),\n permissions: v.array(v.string()).default([]),\n});\n\nexport type RoleSchema = Infer<typeof roleSchema>;\n\n/**\n * The roles catalog — role name → the permissions it grants. Managed at runtime\n * (admins add roles + edit their permissions), unlike a fixed code map. Read by\n * DatabaseAccessResolver.resolvePermissions to expand a user's roles to permissions.\n */\n@RegisterModel()\nexport class Role extends Model<RoleSchema> {\n public static table = \"roles\";\n\n public static schema = roleSchema;\n\n /** The permission strings this role grants. */\n public get permissions(): string[] {\n return this.get<string[]>(\"permissions\", []);\n }\n}\n`;\n\nexport const accessRoleModelIndexStub = `export * from \"./role.model\";\n`;\n\nexport const accessRoleMigrationStub = `import { arrayText, Migration, text } from \"@warlock.js/cascade\";\nimport { Role } from \"../role.model\";\n\n/**\n * Roles catalog table. \\`name\\` is unique (one row per role); \\`permissions\\` is a\n * text array of the permission strings the role grants.\n */\nexport default Migration.create(Role, {\n name: text().notNullable().unique(),\n permissions: arrayText().nullable(),\n});\n`;\n\nexport const accessUserRoleModelStub = `import { access } from \"@warlock.js/access\";\nimport type { Auth } from \"@warlock.js/auth\";\nimport { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for a role assignment — mirrors the migration columns\n * (snake_case). \\`tenant\\` is nullable: a null tenant is a GLOBAL assignment.\n */\nexport const userRoleSchema = v.object({\n user_id: v.string(),\n user_type: v.string(),\n role: v.string(),\n tenant: v.string().optional(),\n});\n\nexport type UserRoleSchema = Infer<typeof userRoleSchema>;\n\n/**\n * The role-assignment table — which roles a user holds, optionally per tenant.\n * Read by DatabaseAccessResolver.resolveRoles; mutated via the statics below.\n * \\`assign\\` / \\`revoke\\` flush the cached permission set automatically, so callers\n * never need to call \\`access.flush(user, tenant)\\` themselves.\n */\n@RegisterModel()\nexport class UserRole extends Model<UserRoleSchema> {\n public static table = \"user_roles\";\n\n public static schema = userRoleSchema;\n\n /**\n * Role names assigned to the user in the given tenant.\n *\n * An unresolved tenant (\\`undefined\\`) scopes to GLOBAL roles only — the rows\n * stored with no tenant (\\`null\\`) — never the union across every tenant. The\n * union would be a privilege-escalation: a user who is \\`owner\\` in one tenant\n * must not be treated as \\`owner\\` everywhere just because a check didn't carry\n * a tenant. This mirrors how \\`assign(user, role)\\` stores a global row.\n */\n public static async rolesFor(user: Auth, tenant?: string): Promise<string[]> {\n const rows = await this.query()\n .where({\n user_id: user.id,\n user_type: user.userType,\n tenant: tenant ?? null,\n })\n .get();\n\n // De-dupe so a duplicate row (a concurrent assign that slipped past the\n // existence check) can't distort the resolved set.\n return [...new Set(rows.map((row) => row.get(\"role\") as string))];\n }\n\n /**\n * Assign a role to the user. No-op if the assignment already exists.\n * Flushes the user's cached permission set automatically.\n */\n public static async assign(user: Auth, role: string, tenant?: string): Promise<void> {\n const existing = await this.first({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant: tenant ?? null,\n });\n\n if (existing) return;\n\n await this.create({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant,\n });\n\n await access.flush(user, tenant);\n }\n\n /**\n * Remove a role assignment from the user.\n * Flushes the user's cached permission set automatically.\n */\n public static async revoke(user: Auth, role: string, tenant?: string): Promise<void> {\n await this.delete({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant: tenant ?? null,\n });\n\n await access.flush(user, tenant);\n }\n}\n`;\n\nexport const accessUserRoleModelIndexStub = `export * from \"./user-role.model\";\n`;\n\nexport const accessUserRoleMigrationStub = `import { Migration, text, uuid } from \"@warlock.js/cascade\";\nimport { UserRole } from \"../user-role.model\";\n\n/**\n * Role-assignment table. \\`user_id\\` is a UUID — override this migration if your\n * user ids are integers. The composite index powers the per-user (per-tenant)\n * lookup the resolver runs on every check.\n */\nexport default Migration.create(\n UserRole,\n {\n user_id: uuid().notNullable().index(),\n user_type: text().notNullable(),\n role: text().notNullable().index(),\n tenant: text().nullable().index(),\n },\n {\n index: [{ columns: [\"user_id\", \"user_type\", \"tenant\"] }],\n },\n);\n`;\n\nexport const accessResolverStub = `import type { AccessResolver } from \"@warlock.js/access\";\nimport type { Auth } from \"@warlock.js/auth\";\nimport { Role } from \"app/access/models/role\";\nimport { UserRole } from \"app/access/models/user-role\";\n\n/**\n * The app's access adapter — connects @warlock.js/access to the ejected role\n * tables. Roles come from the user_roles assignment table; permissions are\n * expanded by mapping those role names through the roles catalog table. Both\n * are managed at runtime (in the DB), so admins can add roles + edit their\n * permissions without a deploy.\n *\n * The engine owns the hard parts (wildcard matching, caching, fail-closed); this\n * resolver only fetches — keep it dumb, never cache inside it.\n */\nexport class DatabaseAccessResolver implements AccessResolver {\n /** The role names this user holds (powers \\`hasRole\\` / \\`hasAnyRole\\`). */\n public async resolveRoles(user: Auth, tenant?: string): Promise<string[]> {\n return UserRole.rolesFor(user, tenant);\n }\n\n /** The effective permission strings this user has (powers \\`can\\` / \\`authorize\\`). */\n public async resolvePermissions(user: Auth, tenant?: string): Promise<string[]> {\n const names = await this.resolveRoles(user, tenant);\n\n if (names.length === 0) return [];\n\n const roles = await Role.query().whereIn(\"name\", names).get();\n\n // Flatten + de-dupe so two roles granting the same permission yield one entry.\n return [...new Set(roles.flatMap((role) => role.permissions))];\n }\n\n /**\n * Optional. Resolve the ambient tenant when a check doesn't pass one\n * explicitly — derive it from the authenticated user (safer than reading\n * client request input, which a caller could spoof). Uncomment + adapt for a\n * multi-tenant app (single-tenant apps leave this off and return undefined).\n */\n // public resolveTenant(user: Auth): string | undefined {\n // return user.get(\"organization_id\");\n // }\n}\n`;\n\nexport const socketConfigStub = `import type { SocketOptions } from \"@warlock.js/core\";\n\n/**\n * Socket.IO configuration — read by the framework's socket connector\n * on boot. When the HTTP server is running the socket server attaches\n * to it; otherwise it listens on its own configured port.\n *\n * Remove this file to disable the socket server entirely.\n */\nexport default {\n options: {\n cors: {\n origin: \"*\",\n },\n },\n} as SocketOptions;\n`;\n\nexport const communicatorsConfigStub = `import { env } from \"@warlock.js/core\";\nimport type { BrokerConfigurations, RabbitMQClientOptions } from \"@warlock.js/herald\";\n\nconst heraldConfigurations: BrokerConfigurations<RabbitMQClientOptions> = {\n driver: \"rabbitmq\",\n name: \"default\",\n isDefault: true,\n\n // ============================================================================\n // Connection Settings\n // ============================================================================\n\n host: env(\"RABBITMQ_HOST\", \"localhost\"),\n port: env(\"RABBITMQ_PORT\", 5672),\n username: env(\"RABBITMQ_USERNAME\", \"guest\"),\n password: env(\"RABBITMQ_PASSWORD\", \"guest\"),\n vhost: env(\"RABBITMQ_VHOST\", \"/\"),\n\n // Or use connection URI (takes precedence over host/port)\n // uri: env(\"RABBITMQ_URL\"),\n\n // ============================================================================\n // Connection Options\n // ============================================================================\n\n /** Heartbeat interval in seconds */\n heartbeat: 60,\n\n /** Connection timeout in milliseconds */\n connectionTimeout: 10000,\n\n /** Enable automatic reconnection on disconnect */\n reconnect: true,\n\n /** Delay between reconnection attempts in milliseconds */\n reconnectDelay: 5_000,\n\n // ============================================================================\n // Consumer Options\n // ============================================================================\n\n /** Default prefetch count (number of unacknowledged messages per consumer) */\n prefetch: 10,\n\n // ============================================================================\n // Client Options (Native amqplib options)\n // ============================================================================\n // These options are passed directly to amqplib.connect()\n // for low-level configuration like frame size, TLS, socket options, etc.\n // ============================================================================\n clientOptions: {\n // Frame max size in bytes (0 = no limit)\n // frameMax: 0,\n\n // Channel max (0 = unlimited)\n // channelMax: 0,\n\n // Socket options\n socket: {\n // Enable TCP keep-alive\n keepAlive: true,\n\n // Disable Nagle's algorithm for lower latency\n noDelay: true,\n\n // Socket timeout (in addition to heartbeat)\n // timeout: 30000,\n },\n\n // TLS/SSL options (uncomment for secure connections)\n // socket: {\n // ca: fs.readFileSync('/path/to/ca.pem'),\n // cert: fs.readFileSync('/path/to/cert.pem'),\n // key: fs.readFileSync('/path/to/key.pem'),\n // rejectUnauthorized: true,\n // },\n },\n};\n\nexport default heraldConfigurations;\n`;\n\nexport const notificationsConfigStub = `import { type NotificationConfig, inApp, mailChannel } from \"@warlock.js/notifications\";\nimport { Notification } from \"app/notifications/models/notification/notification.model\";\n\n/**\n * Notifications configuration. Auto-loaded from src/config on boot — the\n * framework's notifications connector reads this default export and hands it to\n * setNotificationConfig, so this file stays declarative (no side-effect call).\n *\n * Each channel is payload-typed, so notify.mail(...) / notify.database(...)\n * and defineNotification are type-checked against the registry.\n *\n * Channels enabled here:\n * - mail wraps @warlock.js/core sendMail; route is notifiable.email.\n * The \"from\" address defaults to config/mail.ts; override per\n * channel with mailChannel({ from: \"no-reply@yourapp.com\" }).\n * - database in-app store backed by the Notification model. The \"inApp\"\n * facade exposes the recipient-scoped read API: listUnread,\n * countUnread, markAsRead, dismiss, ...\n *\n * Async delivery (.queue()) is OPTIONAL: run \"npx warlock add herald\",\n * import { heraldQueue } from \"@warlock.js/notifications\", and uncomment the\n * queue line below.\n */\nconst config: NotificationConfig = {\n channels: {\n mail: mailChannel(),\n database: inApp.configure({ model: Notification }),\n },\n\n // Async queue — requires @warlock.js/herald (npx warlock add herald):\n // queue: heraldQueue(),\n};\n\nexport default config;\n`;\n\n/** `src/app/notifications/models/notification/notification.model.ts`. */\nexport const notificationModelStub = `import { RegisterModel } from \"@warlock.js/cascade\";\nimport { DatabaseNotification, type NotificationColumnMap } from \"@warlock.js/notifications\";\nimport { v } from \"@warlock.js/seal\";\nimport { NotificationResource } from \"../../resources/notification.resource\";\n\n/**\n * Validation schema for the notifications table — mirrors the migration\n * columns (snake_case). Cascade validates + casts every write against it:\n * nullable columns use .nullish() (may be absent or null), and payload is\n * free-form JSON. Keep this in sync with the migration + columnMap when you\n * add or rename columns.\n *\n * user_id accepts a string or an integer so it matches whichever id type your\n * users use (and the idType you pass to notificationColumns in the migration).\n * Narrow it to that one type.\n */\nconst notificationSchema = v.object({\n user_id: v.union([v.string(), v.int()]),\n type: v.string(),\n title: v.string(),\n body: v.string().nullish(),\n payload: v.record(v.any()).nullish(),\n read_at: v.date().nullish(),\n idempotency_key: v.string().nullish(),\n});\n\n/**\n * In-app notification model.\n *\n * Extends the package's DatabaseNotification base, which provides the stable\n * accessors (recipientId, tenantId, isRead, readAt, markRead) — all derived\n * from the columnMap below. The read/write API lives on the inApp facade\n * (configured in config/notifications.ts); you rarely touch this class directly.\n */\n@RegisterModel()\nexport class Notification extends DatabaseNotification {\n public static table = \"notifications\";\n public static schema = notificationSchema;\n\n /** Every response that returns a notification is shaped by this resource. */\n public static resource = NotificationResource;\n\n /**\n * Maps the in-app store's roles to your columns. This default is\n * single-tenant + read_at-only. Add tenant: \"organization_id\" for\n * multi-tenant; use isRead: \"is_read\" (instead of, or alongside, readAt) to\n * track a boolean read flag. The migration, the accessors and the resource\n * all follow this map.\n */\n public static columnMap: NotificationColumnMap = { readAt: \"read_at\" };\n}\n`;\n\n/** The notifications migration, under `models/notification/migrations/`. */\nexport const notificationMigrationStub = `import { Migration } from \"@warlock.js/cascade\";\nimport { notificationColumns } from \"@warlock.js/notifications\";\nimport { Notification } from \"../notification.model\";\n\n/**\n * Notifications table.\n *\n * Columns come from notificationColumns(Notification) — the recipient / tenant\n * / read-state names follow the model's columnMap; type / title / body /\n * payload / idempotency_key are fixed. The recipient column is a UUID unless\n * you pass { idType: \"integer\" } (or \"bigint\" / \"string\") to match your users.\n * Spread it to add your own columns (and mirror them in the model schema):\n *\n * import { uuid } from \"@warlock.js/cascade\";\n *\n * export default Migration.create(Notification, {\n * ...notificationColumns(Notification, { idType: \"integer\" }),\n * // category_id: uuid().index().nullable(),\n * });\n */\nexport default Migration.create(Notification, notificationColumns(Notification));\n`;\n\n/** `src/app/notifications/resources/notification.resource.ts` — the output shape. */\nexport const notificationResourceStub = `import { defineResource } from \"@warlock.js/core\";\n\n/**\n * What every notification endpoint returns, whatever columns the model holds.\n * readAt reads the default read_at column; change it if you change the model's\n * columnMap.\n */\nexport const NotificationResource = defineResource({\n schema: {\n // Passed through as stored: a UUID string or an integer, per the migration.\n id: (id: string | number) => id,\n type: \"string\",\n title: \"string\",\n body: \"string?\",\n payload: \"object?\",\n readAt: [\"read_at\", \"date?\"],\n createdAt: \"date?\",\n },\n});\n`;\n\n/** `src/app/notifications/schema/list-notifications.schema.ts` — the validated list query. */\nexport const listNotificationsSchemaStub = `import { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Query for GET /notifications. Only these keys reach the store (others, such\n * as a cache-busting \\`_\\`, are dropped rather than rejected), and limit is\n * capped so one request cannot read every row. Query values arrive as strings,\n * so the numbers and the flag are coerced.\n */\nexport const listNotificationsSchema = v.object({\n page: v.int().coerce().min(1).optional(),\n limit: v.int().coerce().min(1).max(50).optional(),\n type: v.string().optional(),\n unread: v.boolean().coerce().optional(),\n}).stripUnknown();\n\nexport type ListNotificationsSchema = Infer<typeof listNotificationsSchema>;\n`;\n\n/** `src/app/notifications/types/notifications-request.type.ts` — the guarded request. */\nexport const notificationsRequestTypeStub = `import type { Request, RequestHandler, RequestLocals } from \"@warlock.js/core\";\nimport type { Id } from \"@warlock.js/notifications\";\n\n/**\n * A request that passed authMiddleware, so locals.user is always set. Only the\n * user's id is declared because that is all the inApp store needs. If your\n * app has its own guarded request type, use it instead.\n */\nexport type NotificationsRequest<Payload = unknown> = Request<Payload> & {\n locals: RequestLocals & { user: { id: Id } };\n};\n\nexport type NotificationsRequestHandler<Payload = unknown> = RequestHandler<\n NotificationsRequest<Payload>\n>;\n`;\n\n/** `src/app/notifications/services/mark-notification-read.service.ts`. */\nexport const markNotificationReadServiceStub = `import { ResourceNotFoundError, t } from \"@warlock.js/core\";\nimport { type Id, inApp } from \"@warlock.js/notifications\";\n\n/**\n * Mark one of the user's notifications read and return it. inApp is scoped to\n * the recipient, so another user's id changes nothing and answers 404 here.\n */\nexport async function markNotificationReadService(userId: Id, notificationId: Id) {\n await inApp.markAsRead(userId, notificationId);\n\n const notification = await inApp.find(userId, notificationId);\n\n if (!notification) {\n throw new ResourceNotFoundError(t(\"notifications.notFound\"));\n }\n\n return notification;\n}\n`;\n\n/** `src/app/notifications/controllers/list-notifications.controller.ts`. */\nexport const listNotificationsControllerStub = `import { inApp } from \"@warlock.js/notifications\";\nimport {\n type ListNotificationsSchema,\n listNotificationsSchema,\n} from \"../schema/list-notifications.schema\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** GET /notifications — the user's notifications, most recent first. */\nexport const listNotificationsController: NotificationsRequestHandler<\n ListNotificationsSchema\n> = async ({ request, response }) => {\n const { data, pagination } = await inApp.list(request.locals.user.id, request.validated());\n\n return response.success({ notifications: data, pagination });\n};\n\nlistNotificationsController.description = \"List notifications\";\n\nlistNotificationsController.validation = {\n schema: listNotificationsSchema,\n validating: [\"query\"],\n};\n`;\n\n/** `src/app/notifications/controllers/unread-notifications-count.controller.ts`. */\nexport const unreadNotificationsCountControllerStub = `import { inApp } from \"@warlock.js/notifications\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** GET /notifications/unread-count — drives the bell badge. */\nexport const unreadNotificationsCountController: NotificationsRequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.countUnread(request.locals.user.id);\n\n return response.success({ count });\n};\n\nunreadNotificationsCountController.description = \"Unread notifications count\";\n`;\n\n/** `src/app/notifications/controllers/mark-notification-read.controller.ts`. */\nexport const markNotificationReadControllerStub = `import { markNotificationReadService } from \"../services/mark-notification-read.service\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** PATCH /notifications/:id/read — mark one read and return it. */\nexport const markNotificationReadController: NotificationsRequestHandler = async ({\n request,\n response,\n}) => {\n // Route params are strings. A numeric id is an integer id (the default\n // MongoDB stack); anything else, such as a UUID, stays a string.\n const rawId = request.string(\"id\");\n const id = /^\\\\d+$/.test(rawId) ? Number(rawId) : rawId;\n\n const notification = await markNotificationReadService(request.locals.user.id, id);\n\n return response.success({ notification });\n};\n\nmarkNotificationReadController.description = \"Mark notification read\";\n`;\n\n/** `src/app/notifications/controllers/mark-all-notifications-read.controller.ts`. */\nexport const markAllNotificationsReadControllerStub = `import { inApp } from \"@warlock.js/notifications\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** PATCH /notifications/read-all — mark every unread one read. */\nexport const markAllNotificationsReadController: NotificationsRequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.markAsRead(request.locals.user.id);\n\n return response.success({ count });\n};\n\nmarkAllNotificationsReadController.description = \"Mark all notifications read\";\n`;\n\n/** `src/app/notifications/routes.ts` — the four read endpoints behind `authMiddleware`. */\nexport const notificationRoutesStub = `import { authMiddleware } from \"@warlock.js/auth\";\nimport { router } from \"@warlock.js/core\";\nimport { listNotificationsController } from \"./controllers/list-notifications.controller\";\nimport { markAllNotificationsReadController } from \"./controllers/mark-all-notifications-read.controller\";\nimport { markNotificationReadController } from \"./controllers/mark-notification-read.controller\";\nimport { unreadNotificationsCountController } from \"./controllers/unread-notifications-count.controller\";\n\n/**\n * The signed-in user's own notifications.\n *\n * Notifications come from domain events, so there is no create. Dismissing is\n * left out on purpose; add it when the product needs it (inApp.dismiss).\n *\n * authMiddleware([]) admits any signed-in user. To limit who may read\n * notifications, add a permission gate to the group:\n *\n * import { gate } from \"@warlock.js/access\";\n *\n * router.group({ middleware: [authMiddleware([]), gate(\"notifications.read\")] }, ...);\n */\nrouter.group({ middleware: [authMiddleware([])] }, () => {\n const notifications = router.route(\"/notifications\");\n\n notifications.list(listNotificationsController);\n notifications.nest(\"/unread-count\").get(unreadNotificationsCountController);\n notifications.nest(\"/read-all\").patch(markAllNotificationsReadController);\n notifications.nest(\"/:id/read\").patch(markNotificationReadController);\n});\n`;\n\n/** `src/app/notifications/utils/locales.json` — the module's translations. */\nexport const notificationLocalesStub = `{\n \"notFound\": { \"en\": \"Notification not found\" }\n}\n`;\n\n/** Shared fixtures for the generated signed-in notification controller specs. */\nexport const notificationTestFixtureStub = `import type { Auth } from \"@warlock.js/auth\";\nimport type { ChildModel } from \"@warlock.js/cascade\";\nimport { config } from \"@warlock.js/core\";\nimport { actingAs, type AuthenticatedTestClient } from \"@warlock.js/core/tests\";\nimport { randomUUID } from \"node:crypto\";\nimport { Notification } from \"../models/notification/notification.model\";\n\n/** Isolated recipients and rows; adjust user fields if your auth model requires more. */\nexport async function createNotificationTestFixture() {\n const User = config.key<ChildModel<Auth>>(\"auth.userType.user\");\n if (!User) throw new Error(\"Notification specs need the configured auth.userType.user model.\");\n const users: Auth[] = [];\n const clients: AuthenticatedTestClient[] = [];\n const rows: Notification[] = [];\n\n async function cleanup() {\n const credentials = await Promise.allSettled(\n clients.splice(0).map((client) => client.dispose()),\n );\n const notifications = await Promise.allSettled(\n rows.splice(0).map((row) => row.destroy({ strategy: \"permanent\" })),\n );\n const recipients = await Promise.allSettled(\n users.splice(0).map((user) => user.destroy({ strategy: \"permanent\" })),\n );\n const errors = [...credentials, ...notifications, ...recipients]\n .filter((result): result is PromiseRejectedResult => result.status === \"rejected\")\n .map((result) => result.reason);\n if (errors.length) throw new AggregateError(errors, \"Notification fixture cleanup failed.\");\n }\n\n const createUser = async () => {\n const user = await User.create({\n name: \"Notification test user\",\n email: \"notifications-\" + randomUUID() + \"@example.test\",\n password: \"test-password\",\n });\n users.push(user);\n return user;\n };\n const signIn = async (user: Auth) => {\n const client = await actingAs(user);\n clients.push(client);\n return client;\n };\n const createNotification = async (recipientId: string | number, title: string, readAt?: Date) => {\n const row = await Notification.create({\n user_id: recipientId,\n type: \"test\",\n title,\n read_at: readAt,\n });\n rows.push(row);\n return row;\n };\n\n try {\n const user = await createUser();\n const otherUser = await createUser();\n const client = await signIn(user);\n const otherClient = await signIn(otherUser);\n const own = await createNotification(user.id, \"Own unread notification\");\n const alreadyRead = await createNotification(user.id, \"Own read notification\", new Date());\n const foreign = await createNotification(otherUser.id, \"Other user's notification\");\n await createNotification(otherUser.id, \"Other user's second notification\");\n return { user, otherUser, client, otherClient, own, alreadyRead, foreign, cleanup };\n } catch (error) {\n // Preserve the setup failure after attempting every owned resource's cleanup.\n await cleanup().catch(() => undefined);\n throw error;\n }\n}\n`;\n\n/** A spec for each generated controller, including authenticated recipient isolation. */\nexport function notificationControllerSpecStub(method: \"GET\" | \"PATCH\", path: string): string {\n const helper = method === \"GET\" ? \"testGet\" : \"testPatch\";\n let signedInCases: string;\n if (path === \"/notifications\") {\n signedInCases = ` it(\"lists only the signed-in recipient's rows, ignoring a forged recipient query\", async () => {\n const response = await fixture.client.get(\n \"/notifications?recipientId=\" + fixture.otherUser.id,\n );\n const body = await expectJson<{ notifications: { id: string | number }[] }>(response);\n expect(body.notifications.map((row) => row.id)).toEqual(\n expect.arrayContaining([fixture.own.id, fixture.alreadyRead.id]),\n );\n expect(body.notifications).toHaveLength(2);\n });\n\n it(\"applies unread and paging query filters\", async () => {\n const response = await fixture.client.get(\"/notifications?unread=true&limit=1\");\n const body = await expectJson<{ notifications: { id: string | number }[] }>(response);\n expect(body.notifications.map((row) => row.id)).toEqual([fixture.own.id]);\n });\n\n it(\"rejects a list limit above the schema's cap\", async () => {\n expect((await fixture.client.get(\"/notifications?limit=999\")).status).toBe(422);\n });`;\n } else if (path === \"/notifications/unread-count\") {\n signedInCases = ` it(\"counts only this recipient's unread notifications\", async () => {\n const own = await expectJson<{ count: number }>(\n await fixture.client.get(\"/notifications/unread-count\"),\n );\n const other = await expectJson<{ count: number }>(\n await fixture.otherClient.get(\"/notifications/unread-count\"),\n );\n expect(own.count).toBe(1);\n expect(other.count).toBe(2);\n });`;\n } else if (path === \"/notifications/read-all\") {\n signedInCases = ` it(\"marks only this recipient's unread rows and leaves another recipient unchanged\", async () => {\n const body = await expectJson<{ count: number }>(\n await fixture.client.patch(\"/notifications/read-all\"),\n );\n expect(body.count).toBe(1);\n expect((await Notification.find(fixture.own.id))?.isRead).toBe(true);\n expect((await Notification.find(fixture.foreign.id))?.isRead).toBe(false);\n const other = await expectJson<{ count: number }>(\n await fixture.otherClient.get(\"/notifications/unread-count\"),\n );\n expect(other.count).toBe(2);\n const again = await expectJson<{ count: number }>(\n await fixture.client.patch(\"/notifications/read-all\"),\n );\n expect(again.count).toBe(0);\n });`;\n } else {\n signedInCases = ` it(\"marks its own notification read and returns it\", async () => {\n const response = await fixture.client.patch(\"/notifications/\" + fixture.own.id + \"/read\");\n const body = await expectJson<{\n notification: { id: string | number; readAt?: string | null };\n }>(response);\n expect(body.notification.id).toBe(fixture.own.id);\n expect(body.notification.readAt).toBeTruthy();\n expect((await Notification.find(fixture.own.id))?.isRead).toBe(true);\n });\n\n it(\"answers 404 for another recipient's notification without changing it\", async () => {\n const response = await fixture.client.patch(\"/notifications/\" + fixture.foreign.id + \"/read\");\n expect(response.status).toBe(404);\n expect((await Notification.find(fixture.foreign.id))?.isRead).toBe(false);\n });`;\n }\n const modelImport =\n method === \"PATCH\"\n ? 'import { Notification } from \"../models/notification/notification.model\";\\n'\n : \"\";\n return `import { expectJson, ${helper} } from \"@warlock.js/core/tests\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\n${modelImport}import { createNotificationTestFixture } from \"../tests/notifications.fixture\";\n\ndescribe(\"${method} ${path}\", () => {\n it(\"answers 401 without a signed-in user\", async () => {\n const response = await ${helper}(\"${path}\");\n expect(response.status).toBe(401);\n });\n\n describe(\"signed in\", () => {\n let fixture: Awaited<ReturnType<typeof createNotificationTestFixture>> | undefined;\n beforeEach(async () => {\n fixture = await createNotificationTestFixture();\n });\n afterEach(async () => {\n const current = fixture;\n fixture = undefined;\n await current?.cleanup();\n });\n\n${signedInCases.replaceAll(\"fixture.\", \"fixture!.\")}\n });\n});\n`;\n}\n\n/**\n * `src/web/root.tsx` — the application root for the SSR page layer.\n *\n * Deliberately minimal. The framework ships a default root, so this exists to\n * give you a place to start rather than because anything requires it. The\n * reference app (`v5/app/src/web/root.tsx`) is where to look for the fuller\n * shape: middleware, an app-level loader, locales, an ErrorBoundary.\n */\nexport const webRootStub = `import type { AppProps } from \"@warlock.js/web\";\nimport { Head, Scripts } from \"@warlock.js/web\";\n\n/**\n * The application root.\n *\n * NOT async, and it receives no request/response: it renders on the server and\n * again in the browser during hydration, where neither exists.\n */\nexport default function App({ children }: AppProps) {\n return (\n <html lang=\"en\">\n <head>\n {/*\n <Head /> also renders the viewport tag (width=device-width,\n initial-scale=1). Change it with \\`metadata.viewport\\`,\n or set it to false to leave it out; do not add a second one here.\n */}\n {/*\n Placement only. The framework injects the page's \\`metadata\\`, the\n stylesheet and preload tags for this route, and the canonical links\n into <head> by default — <Head /> just says WHERE they land.\n\n Do not add a <title> here: the page's \\`metadata\\` owns it, and a root\n that emits one too produces two.\n */}\n <Head />\n <link rel=\"icon\" href=\"data:,\" />\n </head>\n <body>\n {/*\n REQUIRED — this is the hydration mount point, not a styling wrapper.\n\n The browser runtime looks up \\`#vessel\\` and hydrates that element only.\n Remove this div, or rename the id, and the page still renders from the\n server but never becomes interactive: the runtime throws in the console\n and nothing on screen changes.\n\n Wrap it in your own markup freely, and put anything that must live\n outside the hydrated tree (a static footer, a portal target) outside\n it — just keep an element with \\`id=\"vessel\"\\` around {children}.\n */}\n <div id=\"vessel\">{children}</div>\n {/*\n The hydration payload and module tags. Written explicitly because\n placement occasionally matters — a CSP nonce, or ordering against\n your own scripts.\n */}\n <Scripts />\n </body>\n </html>\n );\n}\n`;\n\n/**\n * `src/app/contact/controllers/contact.controller.ts` — a real API endpoint\n * for the Web starter's contact form. It intentionally has no persistence\n * dependency: replace the acknowledgement with a mail/job/database action.\n */\nexport const webContactControllerStub = `import { type Request, type RequestHandler } from \"@warlock.js/core\";\nimport { v, type Infer } from \"@warlock.js/seal\";\n\nexport const contactSchema = v.object({\n name: v.string().min(2),\n email: v.email(),\n message: v.string().min(10),\n});\n\nexport type ContactSchema = Infer.Output<typeof contactSchema>;\n\n/** POST /api/contact — validates the starter contact form. */\nexport const contactController: RequestHandler<Request<ContactSchema>> = async ({\n request,\n response,\n}) => {\n const contact = request.validated();\n\n // Replace this with delivery/persistence for your app. Keeping the accepted\n // payload visible makes the endpoint useful while remaining side-effect free.\n return response.success({\n message: \"Thanks, \" + contact.name + \". Your message has been received.\",\n });\n};\n\ncontactController.validation = { schema: contactSchema };\n`;\n\n/** `src/app/contact/routes.ts` — discovered by the standard app route loader. */\nexport const webContactRoutesStub = `import { router } from \"@warlock.js/core\";\nimport { contactController } from \"./controllers/contact.controller\";\n\nrouter.post(\"/api/contact\", contactController, { public: true });\n`;\n\n/**\n * `src/web/index.register.ts` — universal static setup for the starter page.\n *\n * The page re-exports this stable binding so Warlock's `register()` lifecycle\n * still sees it in both realms without making React Fast Refresh treat every\n * JSX edit as an incompatible function-export replacement.\n */\nexport const webHomeRegisterStub = `import { extend } from \"@mongez/localization\";\n\nexport function register() {\n extend(\"en\", {\n starter: {\n title: \"Your Warlock app is running.\",\n introduction: \"This page is rendered on the server and hydrated in the browser.\",\n language: \"العربية\",\n contact: \"Send a message\",\n name: \"Name\",\n email: \"Email\",\n message: \"Message\",\n submit: \"Send message\",\n sent: \"Thanks — your message has been received.\",\n },\n });\n extend(\"ar\", {\n starter: {\n title: \"تطبيق Warlock يعمل الآن.\",\n introduction: \"تُعرض هذه الصفحة على الخادم ثم تُفعَّل في المتصفح.\",\n language: \"English\",\n contact: \"أرسل رسالة\",\n name: \"الاسم\",\n email: \"البريد الإلكتروني\",\n message: \"الرسالة\",\n submit: \"إرسال الرسالة\",\n sent: \"شكرًا — تم استلام رسالتك.\",\n },\n });\n}\n`;\n\n/**\n * `src/web/index.page.tsx` — one page, so \\`warlock dev\\` has something to serve\n * the moment this finishes.\n */\nexport const webHomePageStub = `import { http } from \"@mongez/http\";\nimport { setCurrentLocaleCode } from \"@mongez/localization\";\nimport { Form, useFormControl, type FormControlProps } from \"@mongez/react-form\";\nimport { transX } from \"@mongez/react-localization\";\nimport { v } from \"@warlock.js/seal\";\nimport { Link, type PageConfig, type PageProps } from \"@warlock.js/web\";\nimport { useState } from \"react\";\n\nexport { register } from \"./index.register\";\n\n/**\n * A page route is an ordinary Warlock route whose handler renders React\n * instead of returning JSON.\n *\n * The URL and stable hydration name are the ones this file DECLARES below.\n * This page answers \\`GET \"/\"\\` because \\`config.route.path = \"/\"\\`, not because of\n * where the file lives. A page file with\n * no \\`config.route\\` declaration derives its URL from the filesystem.\n */\nexport const config: PageConfig = {\n route: { path: \"/\", name: \"index\" },\n metadata: { title: \"Home\" },\n};\n\nconst contactSchema = v.object({\n name: v.string().min(2),\n email: v.email(),\n message: v.string().min(10),\n});\n\nfunction TextInput({ label, ...controlProps }: FormControlProps & { label: string }) {\n const { error, getErrorProps, getInputProps } = useFormControl(controlProps);\n\n return (\n <div className=\"wk-field\">\n <label htmlFor={controlProps.name}>{label}</label>\n <input {...getInputProps()} />\n {error && <p {...getErrorProps()}>{error}</p>}\n </div>\n );\n}\n\n/**\n * Add a \\`loader\\` export to fetch data on the server, and it arrives here as\n * \\`data\\`, typed:\n *\n * export const loader = (async () => ({ items: await itemsRepository.all() }));\n * export default function HomePage({ data }: PageProps<typeof loader>) { ... }\n */\nexport default function HomePage(_props: PageProps) {\n // Live state. If the button below does nothing, the page rendered on the\n // server but never hydrated — the runtime never mounted at \\`#vessel\\`. This is\n // deliberately here so that failure is impossible to miss.\n const [count, setCount] = useState(0);\n const [locale, setLocale] = useState<\"en\" | \"ar\">(\"en\");\n const [submitted, setSubmitted] = useState(false);\n const [submitError, setSubmitError] = useState<string | null>(null);\n\n const toggleLocale = () => {\n const nextLocale = locale === \"en\" ? \"ar\" : \"en\";\n setCurrentLocaleCode(nextLocale);\n setLocale(nextLocale);\n };\n\n return (\n <>\n {/*\n Self-contained, dependency-free styling: plain CSS, system fonts, and\n CSS custom properties, scoped to this page. No CSS framework, no utility\n classes, no external stylesheet — this page looks the same whether or\n not \\`warlock add tailwind\\` has ever been run.\n */}\n <style>{\\`\n .wk-home {\n --wk-fg: #0f172a;\n --wk-muted: #64748b;\n --wk-accent: #4f46e5;\n --wk-border: #e2e8f0;\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n color: var(--wk-fg);\n max-width: 42rem;\n margin: 4rem auto;\n padding: 0 1.5rem;\n line-height: 1.6;\n }\n .wk-home h1 { font-size: 2.25rem; margin: 0 0 0.5rem; }\n .wk-home p { color: var(--wk-muted); margin: 0 0 1.5rem; }\n .wk-home code {\n font-family: ui-monospace, \"SFMono-Regular\", Menlo, monospace;\n background: #f1f5f9;\n padding: 0.1rem 0.35rem;\n border-radius: 0.25rem;\n }\n .wk-check {\n border: 1px solid var(--wk-border);\n border-radius: 0.75rem;\n padding: 1.25rem 1.5rem;\n margin: 2rem 0;\n }\n .wk-check strong { display: block; font-size: 1.5rem; }\n .wk-check button {\n font: inherit;\n cursor: pointer;\n background: var(--wk-accent);\n color: #fff;\n border: 0;\n border-radius: 0.5rem;\n padding: 0.5rem 1rem;\n margin-top: 0.75rem;\n }\n .wk-links { display: flex; gap: 1.25rem; font-size: 0.95rem; }\n .wk-links a { color: var(--wk-accent); text-decoration: none; }\n .wk-links a:hover { text-decoration: underline; }\n .wk-language { margin-left: auto; }\n .wk-contact { margin-top: 2rem; }\n .wk-field { display: grid; gap: 0.35rem; margin: 0.8rem 0; }\n .wk-field input, .wk-field textarea { font: inherit; padding: 0.55rem; }\n .wk-field p, .wk-submit-error { color: #b91c1c; margin: 0; }\n .wk-success { color: #047857; }\n \\`}</style>\n\n <main className=\"wk-home\" dir={locale === \"ar\" ? \"rtl\" : \"ltr\"}>\n <nav className=\"wk-links\" aria-label=\"Starter links\">\n <a href=\"https://warlock.js.org\" target=\"_blank\" rel=\"noreferrer\">\n Docs\n </a>\n <Link href=\"/\" aria-current=\"page\">\n Home\n </Link>\n <button\n className=\"wk-language\"\n type=\"button\"\n aria-pressed={locale === \"ar\"}\n onClick={toggleLocale}\n >\n {transX(\"starter.language\")}\n </button>\n </nav>\n\n <h1>{transX(\"starter.title\")}</h1>\n <p>{transX(\"starter.introduction\")}</p>\n\n <section className=\"wk-check\">\n <label>If this number goes up when you click, React is hydrated:</label>\n <strong>{count}</strong>\n <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\n Count up\n </button>\n </section>\n\n <section className=\"wk-contact\" aria-labelledby=\"contact-heading\">\n <h2 id=\"contact-heading\">{transX(\"starter.contact\")}</h2>\n <Form<typeof contactSchema>\n id=\"contact-form\"\n schema={contactSchema}\n onSubmit={async ({ form, values }) => {\n setSubmitted(false);\n setSubmitError(null);\n const result = await http.post<{ message: string }>(\"/api/contact\", values);\n\n if (result.error) {\n if (result.error.isValidationError) {\n const body = result.error.body as {\n errors?: Array<{ input: string; error: string }>;\n message?: string;\n };\n form.setErrors(\n Object.fromEntries(\n (body.errors ?? []).map(({ input, error }) => [input, error]),\n ),\n );\n setSubmitError(body.message ?? \"Please correct the highlighted fields.\");\n } else {\n setSubmitError(\"Your message could not be sent. Please try again.\");\n }\n return;\n }\n\n setSubmitted(true);\n form.reset();\n }}\n >\n <TextInput name=\"name\" label={transX(\"starter.name\")} autoComplete=\"name\" />\n <TextInput\n name=\"email\"\n label={transX(\"starter.email\")}\n type=\"email\"\n autoComplete=\"email\"\n />\n <ContactMessage />\n <button type=\"submit\">{transX(\"starter.submit\")}</button>\n {submitError && (\n <p className=\"wk-submit-error\" role=\"alert\">\n {submitError}\n </p>\n )}\n {submitted && (\n <p className=\"wk-success\" role=\"status\">\n {transX(\"starter.sent\")}\n </p>\n )}\n </Form>\n </section>\n </main>\n </>\n );\n}\n\nfunction ContactMessage() {\n const { error, getErrorProps, getInputProps } = useFormControl({ name: \"message\" });\n\n return (\n <div className=\"wk-field\">\n <label htmlFor=\"message\">{transX(\"starter.message\")}</label>\n <textarea {...getInputProps()} rows={5} />\n {error && <p {...getErrorProps()}>{error}</p>}\n </div>\n );\n}\n`;\n"],"mappings":";AAAA,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BhC,MAAa,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+B5B,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCnC,MAAa,2BAA2B;;AAGxC,MAAa,0BAA0B;;;;;;;;;;;;AAavC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FvC,MAAa,+BAA+B;;AAG5C,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;AAsB3C,MAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6ClC,MAAa,mBAAmB;;;;;;;;;;;;;;;;;AAkBhC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFvC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCvC,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDrC,MAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;AAwBzC,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;AAsBxC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;AAmB3C,MAAa,+BAA+B;;;;;;;;;;;;;;;;;AAkB5C,MAAa,kCAAkC;;;;;;;;;;;;;;;;;;;;AAqB/C,MAAa,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;;AAyB/C,MAAa,yCAAyC;;;;;;;;;;;;;;;;AAiBtD,MAAa,qCAAqC;;;;;;;;;;;;;;;;;;;;;AAsBlD,MAAa,yCAAyC;;;;;;;;;;;;;;;;AAiBtD,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BtC,MAAa,0BAA0B;;;;;AAMvC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2E3C,SAAgB,+BAA+B,QAAyB,MAAsB;CAC5F,MAAM,SAAS,WAAW,QAAQ,YAAY;CAC9C,IAAI;CACJ,IAAI,SAAS,kBACX,gBAAgB;;;;;;;;;;;;;;;;;;;;MAoBX,IAAI,SAAS,+BAClB,gBAAgB;;;;;;;;;;MAUX,IAAI,SAAS,2BAClB,gBAAgB;;;;;;;;;;;;;;;;MAiBhB,gBAAgB;;;;;;;;;;;;;;;CAoBlB,OAAO,wBAAwB,OAAO;;EAHpC,WAAW,UACP,kFACA,GAGM;;YAEF,OAAO,GAAG,KAAK;;6BAEE,OAAO,IAAI,KAAK;;;;;;;;;;;;;;;EAe3C,cAAc,WAAW,YAAY,WAAW,EAAE;;;;AAIpD;;;;;;;;;AAUA,MAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4D3B,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BxC,MAAa,uBAAuB;;;;;;;;;;;;AAapC,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCnC,MAAa,kBAAkB"}
|
|
1
|
+
{"version":3,"file":"stubs.mjs","names":[],"sources":["../../../../../../../../packages/core/src/generations/stubs.ts"],"sourcesContent":["export const accessConfigStub = `import { type AccessConfigurations } from \"@warlock.js/access\";\nimport { DatabaseAccessResolver } from \"app/access/services/access-resolver\";\n\n/**\n * Authorization configuration — read by @warlock.js/access on boot.\n *\n * The resolver is the one required piece: it tells the engine how to read a\n * user's roles + permissions. The ejected DatabaseAccessResolver reads roles\n * from the user_roles table and maps them through the roles catalog table (so\n * roles + their permissions are managed at runtime, in the DB).\n *\n * For a fixed, code-defined catalog with no tables, swap in DefaultAccessResolver:\n * import { DefaultAccessResolver } from \"@warlock.js/access\";\n * resolver: new DefaultAccessResolver({ admin: [\"*\"], editor: [\"orders.*\"] }),\n *\n * Multi-tenant? Add a \\`resolveTenant()\\` to the resolver to read the active\n * tenant from the request; checks then scope to it automatically.\n */\nconst access: AccessConfigurations = {\n resolver: new DatabaseAccessResolver(),\n\n // Cache resolved permission sets (default \"10m\").\n // cache: { ttl: \"10m\" },\n};\n\nexport default access;\n`;\n\nexport const aiConfigStub = `import type { AIConfig } from \"@warlock.js/ai\";\n\n// >>> warlock:ai-packages (auto-managed) >>>\n// Satellite packages augment the \"ai\" object on import — e.g. ai.workspace,\n// ai.tools / ai.mcp, and panoptic's ai.config({ panoptic }) wiring. The command\n// \"warlock add ai-workspace | ai-tools | ai-panoptic\" adds the matching\n// side-effect import below; keep them so the augmentation + runtime registration\n// load before the ai connector applies this config.\n// <<< warlock:ai-packages <<<\n\n/**\n * AI configuration — applied on boot by the ai connector, which calls\n * ai.config(...) with the object below. Cross-cutting defaults live here\n * (shared cache / snapshot stores, observability); per-call options always win.\n *\n * Wire a default model from a provider you installed, e.g.:\n * import { OpenAISDK } from \"@warlock.js/ai-openai\";\n * const openai = OpenAISDK({ apiKey: env(\"OPENAI_API_KEY\") });\n * // then pass openai.model({ name: \"gpt-4o-mini\" }) into your agents.\n */\nconst ai: Partial<AIConfig> = {\n // Default cache driver for cache-backed AI features (semantic cache, rag / memory vector stores).\n // defaultStore: cache.driver(\"redis\", { client }),\n\n // Observability — requires \"warlock add ai-panoptic\". Exporters + the local dashboard.\n // panoptic: { exporters: [], dashboard: false, observeAll: false },\n};\n\nexport default ai;\n`;\n\nexport const accessRoleModelStub = `import { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for the roles catalog — mirrors the migration columns\n * (snake_case). Each row is a role name plus the permission strings it grants;\n * wildcards work (\"orders.*\", \"*\"). The DatabaseAccessResolver maps a user's\n * assigned role names through this table to their effective permissions.\n */\nexport const roleSchema = v.object({\n name: v.string(),\n permissions: v.array(v.string()).default([]),\n});\n\nexport type RoleSchema = Infer<typeof roleSchema>;\n\n/**\n * The roles catalog — role name → the permissions it grants. Managed at runtime\n * (admins add roles + edit their permissions), unlike a fixed code map. Read by\n * DatabaseAccessResolver.resolvePermissions to expand a user's roles to permissions.\n */\n@RegisterModel()\nexport class Role extends Model<RoleSchema> {\n public static table = \"roles\";\n\n public static schema = roleSchema;\n\n /** The permission strings this role grants. */\n public get permissions(): string[] {\n return this.get<string[]>(\"permissions\", []);\n }\n}\n`;\n\nexport const accessRoleModelIndexStub = `export * from \"./role.model\";\n`;\n\nexport const accessRoleMigrationStub = `import { arrayText, Migration, text } from \"@warlock.js/cascade\";\nimport { Role } from \"../role.model\";\n\n/**\n * Roles catalog table. \\`name\\` is unique (one row per role); \\`permissions\\` is a\n * text array of the permission strings the role grants.\n */\nexport default Migration.create(Role, {\n name: text().notNullable().unique(),\n permissions: arrayText().nullable(),\n});\n`;\n\nexport const accessUserRoleModelStub = `import { access } from \"@warlock.js/access\";\nimport type { Auth } from \"@warlock.js/auth\";\nimport { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for a role assignment — mirrors the migration columns\n * (snake_case). \\`tenant\\` is nullable: a null tenant is a GLOBAL assignment.\n */\nexport const userRoleSchema = v.object({\n user_id: v.string(),\n user_type: v.string(),\n role: v.string(),\n tenant: v.string().optional(),\n});\n\nexport type UserRoleSchema = Infer<typeof userRoleSchema>;\n\n/**\n * The role-assignment table — which roles a user holds, optionally per tenant.\n * Read by DatabaseAccessResolver.resolveRoles; mutated via the statics below.\n * \\`assign\\` / \\`revoke\\` flush the cached permission set automatically, so callers\n * never need to call \\`access.flush(user, tenant)\\` themselves.\n */\n@RegisterModel()\nexport class UserRole extends Model<UserRoleSchema> {\n public static table = \"user_roles\";\n\n public static schema = userRoleSchema;\n\n /**\n * Role names assigned to the user in the given tenant.\n *\n * An unresolved tenant (\\`undefined\\`) scopes to GLOBAL roles only — the rows\n * stored with no tenant (\\`null\\`) — never the union across every tenant. The\n * union would be a privilege-escalation: a user who is \\`owner\\` in one tenant\n * must not be treated as \\`owner\\` everywhere just because a check didn't carry\n * a tenant. This mirrors how \\`assign(user, role)\\` stores a global row.\n */\n public static async rolesFor(user: Auth, tenant?: string): Promise<string[]> {\n const rows = await this.query()\n .where({\n user_id: user.id,\n user_type: user.userType,\n tenant: tenant ?? null,\n })\n .get();\n\n // De-dupe so a duplicate row (a concurrent assign that slipped past the\n // existence check) can't distort the resolved set.\n return [...new Set(rows.map((row) => row.get(\"role\") as string))];\n }\n\n /**\n * Assign a role to the user. No-op if the assignment already exists.\n * Flushes the user's cached permission set automatically.\n */\n public static async assign(user: Auth, role: string, tenant?: string): Promise<void> {\n const existing = await this.first({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant: tenant ?? null,\n });\n\n if (existing) return;\n\n await this.create({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant,\n });\n\n await access.flush(user, tenant);\n }\n\n /**\n * Remove a role assignment from the user.\n * Flushes the user's cached permission set automatically.\n */\n public static async revoke(user: Auth, role: string, tenant?: string): Promise<void> {\n await this.delete({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant: tenant ?? null,\n });\n\n await access.flush(user, tenant);\n }\n}\n`;\n\nexport const accessUserRoleModelIndexStub = `export * from \"./user-role.model\";\n`;\n\nexport const accessUserRoleMigrationStub = `import { Migration, text, uuid } from \"@warlock.js/cascade\";\nimport { UserRole } from \"../user-role.model\";\n\n/**\n * Role-assignment table. \\`user_id\\` is a UUID — override this migration if your\n * user ids are integers. The composite index powers the per-user (per-tenant)\n * lookup the resolver runs on every check.\n */\nexport default Migration.create(\n UserRole,\n {\n user_id: uuid().notNullable().index(),\n user_type: text().notNullable(),\n role: text().notNullable().index(),\n tenant: text().nullable().index(),\n },\n {\n index: [{ columns: [\"user_id\", \"user_type\", \"tenant\"] }],\n },\n);\n`;\n\nexport const accessResolverStub = `import type { AccessResolver } from \"@warlock.js/access\";\nimport type { Auth } from \"@warlock.js/auth\";\nimport { Role } from \"app/access/models/role\";\nimport { UserRole } from \"app/access/models/user-role\";\n\n/**\n * The app's access adapter — connects @warlock.js/access to the ejected role\n * tables. Roles come from the user_roles assignment table; permissions are\n * expanded by mapping those role names through the roles catalog table. Both\n * are managed at runtime (in the DB), so admins can add roles + edit their\n * permissions without a deploy.\n *\n * The engine owns the hard parts (wildcard matching, caching, fail-closed); this\n * resolver only fetches — keep it dumb, never cache inside it.\n */\nexport class DatabaseAccessResolver implements AccessResolver {\n /** The role names this user holds (powers \\`hasRole\\` / \\`hasAnyRole\\`). */\n public async resolveRoles(user: Auth, tenant?: string): Promise<string[]> {\n return UserRole.rolesFor(user, tenant);\n }\n\n /** The effective permission strings this user has (powers \\`can\\` / \\`authorize\\`). */\n public async resolvePermissions(user: Auth, tenant?: string): Promise<string[]> {\n const names = await this.resolveRoles(user, tenant);\n\n if (names.length === 0) return [];\n\n const roles = await Role.query().whereIn(\"name\", names).get();\n\n // Flatten + de-dupe so two roles granting the same permission yield one entry.\n return [...new Set(roles.flatMap((role) => role.permissions))];\n }\n\n /**\n * Optional. Resolve the ambient tenant when a check doesn't pass one\n * explicitly — derive it from the authenticated user (safer than reading\n * client request input, which a caller could spoof). Uncomment + adapt for a\n * multi-tenant app (single-tenant apps leave this off and return undefined).\n */\n // public resolveTenant(user: Auth): string | undefined {\n // return user.get(\"organization_id\");\n // }\n}\n`;\n\nexport const socketConfigStub = `import type { SocketOptions } from \"@warlock.js/core\";\n\n/**\n * Socket.IO configuration — read by the framework's socket connector\n * on boot. When the HTTP server is running the socket server attaches\n * to it; otherwise it listens on its own configured port.\n *\n * Remove this file to disable the socket server entirely.\n */\nexport default {\n options: {\n cors: {\n origin: \"*\",\n },\n },\n} as SocketOptions;\n`;\n\nexport const communicatorsConfigStub = `import { env } from \"@warlock.js/core\";\nimport type { BrokerConfigurations, RabbitMQClientOptions } from \"@warlock.js/herald\";\n\nconst heraldConfigurations: BrokerConfigurations<RabbitMQClientOptions> = {\n driver: \"rabbitmq\",\n name: \"default\",\n isDefault: true,\n\n // ============================================================================\n // Connection Settings\n // ============================================================================\n\n host: env(\"RABBITMQ_HOST\", \"localhost\"),\n port: env(\"RABBITMQ_PORT\", 5672),\n username: env(\"RABBITMQ_USERNAME\", \"guest\"),\n password: env(\"RABBITMQ_PASSWORD\", \"guest\"),\n vhost: env(\"RABBITMQ_VHOST\", \"/\"),\n\n // Or use connection URI (takes precedence over host/port)\n // uri: env(\"RABBITMQ_URL\"),\n\n // ============================================================================\n // Connection Options\n // ============================================================================\n\n /** Heartbeat interval in seconds */\n heartbeat: 60,\n\n /** Connection timeout in milliseconds */\n connectionTimeout: 10000,\n\n /** Enable automatic reconnection on disconnect */\n reconnect: true,\n\n /** Delay between reconnection attempts in milliseconds */\n reconnectDelay: 5_000,\n\n // ============================================================================\n // Consumer Options\n // ============================================================================\n\n /** Default prefetch count (number of unacknowledged messages per consumer) */\n prefetch: 10,\n\n // ============================================================================\n // Client Options (Native amqplib options)\n // ============================================================================\n // These options are passed directly to amqplib.connect()\n // for low-level configuration like frame size, TLS, socket options, etc.\n // ============================================================================\n clientOptions: {\n // Frame max size in bytes (0 = no limit)\n // frameMax: 0,\n\n // Channel max (0 = unlimited)\n // channelMax: 0,\n\n // Socket options\n socket: {\n // Enable TCP keep-alive\n keepAlive: true,\n\n // Disable Nagle's algorithm for lower latency\n noDelay: true,\n\n // Socket timeout (in addition to heartbeat)\n // timeout: 30000,\n },\n\n // TLS/SSL options (uncomment for secure connections)\n // socket: {\n // ca: fs.readFileSync('/path/to/ca.pem'),\n // cert: fs.readFileSync('/path/to/cert.pem'),\n // key: fs.readFileSync('/path/to/key.pem'),\n // rejectUnauthorized: true,\n // },\n },\n};\n\nexport default heraldConfigurations;\n`;\n\nexport const notificationsConfigStub = `import { type NotificationConfig, inApp, mailChannel } from \"@warlock.js/notifications\";\nimport { Notification } from \"app/notifications/models/notification/notification.model\";\n\n/**\n * Notifications configuration. Auto-loaded from src/config on boot — the\n * framework's notifications connector reads this default export and hands it to\n * setNotificationConfig, so this file stays declarative (no side-effect call).\n *\n * Each channel is payload-typed, so notify.mail(...) / notify.database(...)\n * and defineNotification are type-checked against the registry.\n *\n * Channels enabled here:\n * - mail wraps @warlock.js/core sendMail; route is notifiable.email.\n * The \"from\" address defaults to config/mail.ts; override per\n * channel with mailChannel({ from: \"no-reply@yourapp.com\" }).\n * - database in-app store backed by the Notification model. The \"inApp\"\n * facade exposes the recipient-scoped read API: listUnread,\n * countUnread, markAsRead, dismiss, ...\n *\n * Async delivery (.queue()) is OPTIONAL: run \"npx warlock add herald\",\n * import { heraldQueue } from \"@warlock.js/notifications\", and uncomment the\n * queue line below.\n */\nconst config: NotificationConfig = {\n channels: {\n mail: mailChannel(),\n database: inApp.configure({ model: Notification }),\n },\n\n // Async queue — requires @warlock.js/herald (npx warlock add herald):\n // queue: heraldQueue(),\n};\n\nexport default config;\n`;\n\n/** `src/app/notifications/models/notification/notification.model.ts`. */\nexport const notificationModelStub = `import { RegisterModel } from \"@warlock.js/cascade\";\nimport { DatabaseNotification, type NotificationColumnMap } from \"@warlock.js/notifications\";\nimport { v } from \"@warlock.js/seal\";\nimport { NotificationResource } from \"../../resources/notification.resource\";\n\n/**\n * Validation schema for the notifications table — mirrors the migration\n * columns (snake_case). Cascade validates + casts every write against it:\n * nullable columns use .nullish() (may be absent or null), and payload is\n * free-form JSON. Keep this in sync with the migration + columnMap when you\n * add or rename columns.\n *\n * user_id accepts a string or an integer so it matches whichever id type your\n * users use (and the idType you pass to notificationColumns in the migration).\n * Narrow it to that one type.\n */\nconst notificationSchema = v.object({\n user_id: v.union([v.string(), v.int()]),\n type: v.string(),\n title: v.string(),\n body: v.string().nullish(),\n payload: v.record(v.any()).nullish(),\n read_at: v.date().nullish(),\n idempotency_key: v.string().nullish(),\n});\n\n/**\n * In-app notification model.\n *\n * Extends the package's DatabaseNotification base, which provides the stable\n * accessors (recipientId, tenantId, isRead, readAt, markRead) — all derived\n * from the columnMap below. The read/write API lives on the inApp facade\n * (configured in config/notifications.ts); you rarely touch this class directly.\n */\n@RegisterModel()\nexport class Notification extends DatabaseNotification {\n public static table = \"notifications\";\n public static schema = notificationSchema;\n\n /** Every response that returns a notification is shaped by this resource. */\n public static resource = NotificationResource;\n\n /**\n * Maps the in-app store's roles to your columns. This default is\n * single-tenant + read_at-only. Add tenant: \"organization_id\" for\n * multi-tenant; use isRead: \"is_read\" (instead of, or alongside, readAt) to\n * track a boolean read flag. The migration, the accessors and the resource\n * all follow this map.\n */\n public static columnMap: NotificationColumnMap = { readAt: \"read_at\" };\n}\n`;\n\n/** The notifications migration, under `models/notification/migrations/`. */\nexport const notificationMigrationStub = `import { Migration } from \"@warlock.js/cascade\";\nimport { notificationColumns } from \"@warlock.js/notifications\";\nimport { Notification } from \"../notification.model\";\n\n/**\n * Notifications table.\n *\n * Columns come from notificationColumns(Notification) — the recipient / tenant\n * / read-state names follow the model's columnMap; type / title / body /\n * payload / idempotency_key are fixed. The recipient column is a UUID unless\n * you pass { idType: \"integer\" } (or \"bigint\" / \"string\") to match your users.\n * Spread it to add your own columns (and mirror them in the model schema):\n *\n * import { uuid } from \"@warlock.js/cascade\";\n *\n * export default Migration.create(Notification, {\n * ...notificationColumns(Notification, { idType: \"integer\" }),\n * // category_id: uuid().index().nullable(),\n * });\n */\nexport default Migration.create(Notification, notificationColumns(Notification));\n`;\n\n/** `src/app/notifications/resources/notification.resource.ts` — the output shape. */\nexport const notificationResourceStub = `import { defineResource } from \"@warlock.js/core\";\n\n/**\n * What every notification endpoint returns, whatever columns the model holds.\n * readAt reads the default read_at column; change it if you change the model's\n * columnMap.\n */\nexport const NotificationResource = defineResource({\n schema: {\n // Passed through as stored: a UUID string or an integer, per the migration.\n id: (id: string | number) => id,\n type: \"string\",\n title: \"string\",\n body: \"string?\",\n payload: \"object?\",\n readAt: [\"read_at\", \"date?\"],\n createdAt: \"date?\",\n },\n});\n`;\n\n/** `src/app/notifications/schema/list-notifications.schema.ts` — the validated list query. */\nexport const listNotificationsSchemaStub = `import { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Query for GET /notifications. Only these keys reach the store (others, such\n * as a cache-busting \\`_\\`, are dropped rather than rejected), and limit is\n * capped so one request cannot read every row. Query values arrive as strings,\n * so the numbers and the flag are coerced.\n */\nexport const listNotificationsSchema = v.object({\n page: v.int().coerce().min(1).optional(),\n limit: v.int().coerce().min(1).max(50).optional(),\n type: v.string().optional(),\n unread: v.boolean().coerce().optional(),\n}).stripUnknown();\n\nexport type ListNotificationsSchema = Infer<typeof listNotificationsSchema>;\n`;\n\n/** `src/app/notifications/types/notifications-request.type.ts` — the guarded request. */\nexport const notificationsRequestTypeStub = `import type { Request, RequestHandler, RequestLocals } from \"@warlock.js/core\";\nimport type { Id } from \"@warlock.js/notifications\";\n\n/**\n * A request that passed authMiddleware, so locals.user is always set. Only the\n * user's id is declared because that is all the inApp store needs. If your\n * app has its own guarded request type, use it instead.\n */\nexport type NotificationsRequest<Payload = unknown> = Request<Payload> & {\n locals: RequestLocals & { user: { id: Id } };\n};\n\nexport type NotificationsRequestHandler<Payload = unknown> = RequestHandler<\n NotificationsRequest<Payload>\n>;\n`;\n\n/** `src/app/notifications/services/mark-notification-read.service.ts`. */\nexport const markNotificationReadServiceStub = `import { ResourceNotFoundError, t } from \"@warlock.js/core\";\nimport { type Id, inApp } from \"@warlock.js/notifications\";\n\n/**\n * Mark one of the user's notifications read and return it. inApp is scoped to\n * the recipient, so another user's id changes nothing and answers 404 here.\n */\nexport async function markNotificationReadService(userId: Id, notificationId: Id) {\n await inApp.markAsRead(userId, notificationId);\n\n const notification = await inApp.find(userId, notificationId);\n\n if (!notification) {\n throw new ResourceNotFoundError(t(\"notifications.notFound\"));\n }\n\n return notification;\n}\n`;\n\n/** `src/app/notifications/controllers/list-notifications.controller.ts`. */\nexport const listNotificationsControllerStub = `import { inApp } from \"@warlock.js/notifications\";\nimport {\n type ListNotificationsSchema,\n listNotificationsSchema,\n} from \"../schema/list-notifications.schema\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** GET /notifications — the user's notifications, most recent first. */\nexport const listNotificationsController: NotificationsRequestHandler<\n ListNotificationsSchema\n> = async ({ request, response }) => {\n const { data, pagination } = await inApp.list(request.locals.user.id, request.validated());\n\n return response.success({ notifications: data, pagination });\n};\n\nlistNotificationsController.description = \"List notifications\";\n\nlistNotificationsController.validation = {\n schema: listNotificationsSchema,\n validating: [\"query\"],\n};\n`;\n\n/** `src/app/notifications/controllers/unread-notifications-count.controller.ts`. */\nexport const unreadNotificationsCountControllerStub = `import { inApp } from \"@warlock.js/notifications\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** GET /notifications/unread-count — drives the bell badge. */\nexport const unreadNotificationsCountController: NotificationsRequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.countUnread(request.locals.user.id);\n\n return response.success({ count });\n};\n\nunreadNotificationsCountController.description = \"Unread notifications count\";\n`;\n\n/** `src/app/notifications/controllers/mark-notification-read.controller.ts`. */\nexport const markNotificationReadControllerStub = `import { markNotificationReadService } from \"../services/mark-notification-read.service\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** PATCH /notifications/:id/read — mark one read and return it. */\nexport const markNotificationReadController: NotificationsRequestHandler = async ({\n request,\n response,\n}) => {\n // Route params are strings. A numeric id is an integer id (the default\n // MongoDB stack); anything else, such as a UUID, stays a string.\n const rawId = request.string(\"id\");\n const id = /^\\\\d+$/.test(rawId) ? Number(rawId) : rawId;\n\n const notification = await markNotificationReadService(request.locals.user.id, id);\n\n return response.success({ notification });\n};\n\nmarkNotificationReadController.description = \"Mark notification read\";\n`;\n\n/** `src/app/notifications/controllers/mark-all-notifications-read.controller.ts`. */\nexport const markAllNotificationsReadControllerStub = `import { inApp } from \"@warlock.js/notifications\";\nimport type { NotificationsRequestHandler } from \"../types/notifications-request.type\";\n\n/** PATCH /notifications/read-all — mark every unread one read. */\nexport const markAllNotificationsReadController: NotificationsRequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.markAsRead(request.locals.user.id);\n\n return response.success({ count });\n};\n\nmarkAllNotificationsReadController.description = \"Mark all notifications read\";\n`;\n\n/** `src/app/notifications/routes.ts` — the four read endpoints behind `authMiddleware`. */\nexport const notificationRoutesStub = `import { authMiddleware } from \"@warlock.js/auth\";\nimport { router } from \"@warlock.js/core\";\nimport { listNotificationsController } from \"./controllers/list-notifications.controller\";\nimport { markAllNotificationsReadController } from \"./controllers/mark-all-notifications-read.controller\";\nimport { markNotificationReadController } from \"./controllers/mark-notification-read.controller\";\nimport { unreadNotificationsCountController } from \"./controllers/unread-notifications-count.controller\";\n\n/**\n * The signed-in user's own notifications.\n *\n * Notifications come from domain events, so there is no create. Dismissing is\n * left out on purpose; add it when the product needs it (inApp.dismiss).\n *\n * authMiddleware([]) admits any signed-in user. To limit who may read\n * notifications, add a permission gate to the group:\n *\n * import { gate } from \"@warlock.js/access\";\n *\n * router.group({ middleware: [authMiddleware([]), gate(\"notifications.read\")] }, ...);\n */\nrouter.group({ middleware: [authMiddleware([])] }, () => {\n const notifications = router.route(\"/notifications\");\n\n notifications.list(listNotificationsController);\n notifications.nest(\"/unread-count\").get(unreadNotificationsCountController);\n notifications.nest(\"/read-all\").patch(markAllNotificationsReadController);\n notifications.nest(\"/:id/read\").patch(markNotificationReadController);\n});\n`;\n\n/** `src/app/notifications/utils/locales.json` — the module's translations. */\nexport const notificationLocalesStub = `{\n \"notFound\": { \"en\": \"Notification not found\" }\n}\n`;\n\n/** Shared fixtures for the generated signed-in notification controller specs. */\nexport const notificationTestFixtureStub = `import type { Auth } from \"@warlock.js/auth\";\nimport type { ChildModel } from \"@warlock.js/cascade\";\nimport { config } from \"@warlock.js/core\";\nimport { actingAs, type AuthenticatedTestClient } from \"@warlock.js/core/tests\";\nimport { randomUUID } from \"node:crypto\";\nimport { Notification } from \"../models/notification/notification.model\";\n\n/** Isolated recipients and rows; adjust user fields if your auth model requires more. */\nexport async function createNotificationTestFixture() {\n const User = config.key<ChildModel<Auth>>(\"auth.userType.user\");\n if (!User) throw new Error(\"Notification specs need the configured auth.userType.user model.\");\n const users: Auth[] = [];\n const clients: AuthenticatedTestClient[] = [];\n const rows: Notification[] = [];\n\n async function cleanup() {\n const credentials = await Promise.allSettled(\n clients.splice(0).map((client) => client.dispose()),\n );\n const notifications = await Promise.allSettled(\n rows.splice(0).map((row) => row.destroy({ strategy: \"permanent\" })),\n );\n const recipients = await Promise.allSettled(\n users.splice(0).map((user) => user.destroy({ strategy: \"permanent\" })),\n );\n const errors = [...credentials, ...notifications, ...recipients]\n .filter((result): result is PromiseRejectedResult => result.status === \"rejected\")\n .map((result) => result.reason);\n if (errors.length) throw new AggregateError(errors, \"Notification fixture cleanup failed.\");\n }\n\n const createUser = async () => {\n const user = await User.create({\n name: \"Notification test user\",\n email: \"notifications-\" + randomUUID() + \"@example.test\",\n password: \"test-password\",\n });\n users.push(user);\n return user;\n };\n const signIn = async (user: Auth) => {\n const client = await actingAs(user);\n clients.push(client);\n return client;\n };\n const createNotification = async (recipientId: string | number, title: string, readAt?: Date) => {\n const row = await Notification.create({\n user_id: recipientId,\n type: \"test\",\n title,\n read_at: readAt,\n });\n rows.push(row);\n return row;\n };\n\n try {\n const user = await createUser();\n const otherUser = await createUser();\n const client = await signIn(user);\n const otherClient = await signIn(otherUser);\n const own = await createNotification(user.id, \"Own unread notification\");\n const alreadyRead = await createNotification(user.id, \"Own read notification\", new Date());\n const foreign = await createNotification(otherUser.id, \"Other user's notification\");\n await createNotification(otherUser.id, \"Other user's second notification\");\n return { user, otherUser, client, otherClient, own, alreadyRead, foreign, cleanup };\n } catch (error) {\n // Preserve the setup failure after attempting every owned resource's cleanup.\n await cleanup().catch(() => undefined);\n throw error;\n }\n}\n`;\n\n/** A spec for each generated controller, including authenticated recipient isolation. */\nexport function notificationControllerSpecStub(method: \"GET\" | \"PATCH\", path: string): string {\n const helper = method === \"GET\" ? \"testGet\" : \"testPatch\";\n let signedInCases: string;\n if (path === \"/notifications\") {\n signedInCases = ` it(\"lists only the signed-in recipient's rows, ignoring a forged recipient query\", async () => {\n const response = await fixture.client.get(\n \"/notifications?recipientId=\" + fixture.otherUser.id,\n );\n const body = await expectJson<{ notifications: { id: string | number }[] }>(response);\n expect(body.notifications.map((row) => row.id)).toEqual(\n expect.arrayContaining([fixture.own.id, fixture.alreadyRead.id]),\n );\n expect(body.notifications).toHaveLength(2);\n });\n\n it(\"applies unread and paging query filters\", async () => {\n const response = await fixture.client.get(\"/notifications?unread=true&limit=1\");\n const body = await expectJson<{ notifications: { id: string | number }[] }>(response);\n expect(body.notifications.map((row) => row.id)).toEqual([fixture.own.id]);\n });\n\n it(\"rejects a list limit above the schema's cap\", async () => {\n expect((await fixture.client.get(\"/notifications?limit=999\")).status).toBe(422);\n });`;\n } else if (path === \"/notifications/unread-count\") {\n signedInCases = ` it(\"counts only this recipient's unread notifications\", async () => {\n const own = await expectJson<{ count: number }>(\n await fixture.client.get(\"/notifications/unread-count\"),\n );\n const other = await expectJson<{ count: number }>(\n await fixture.otherClient.get(\"/notifications/unread-count\"),\n );\n expect(own.count).toBe(1);\n expect(other.count).toBe(2);\n });`;\n } else if (path === \"/notifications/read-all\") {\n signedInCases = ` it(\"marks only this recipient's unread rows and leaves another recipient unchanged\", async () => {\n const body = await expectJson<{ count: number }>(\n await fixture.client.patch(\"/notifications/read-all\"),\n );\n expect(body.count).toBe(1);\n expect((await Notification.find(fixture.own.id))?.isRead).toBe(true);\n expect((await Notification.find(fixture.foreign.id))?.isRead).toBe(false);\n const other = await expectJson<{ count: number }>(\n await fixture.otherClient.get(\"/notifications/unread-count\"),\n );\n expect(other.count).toBe(2);\n const again = await expectJson<{ count: number }>(\n await fixture.client.patch(\"/notifications/read-all\"),\n );\n expect(again.count).toBe(0);\n });`;\n } else {\n signedInCases = ` it(\"marks its own notification read and returns it\", async () => {\n const response = await fixture.client.patch(\"/notifications/\" + fixture.own.id + \"/read\");\n const body = await expectJson<{\n notification: { id: string | number; readAt?: string | null };\n }>(response);\n expect(body.notification.id).toBe(fixture.own.id);\n expect(body.notification.readAt).toBeTruthy();\n expect((await Notification.find(fixture.own.id))?.isRead).toBe(true);\n });\n\n it(\"answers 404 for another recipient's notification without changing it\", async () => {\n const response = await fixture.client.patch(\"/notifications/\" + fixture.foreign.id + \"/read\");\n expect(response.status).toBe(404);\n expect((await Notification.find(fixture.foreign.id))?.isRead).toBe(false);\n });`;\n }\n const modelImport =\n method === \"PATCH\"\n ? 'import { Notification } from \"../models/notification/notification.model\";\\n'\n : \"\";\n return `import { expectJson, ${helper} } from \"@warlock.js/core/tests\";\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\n${modelImport}import { createNotificationTestFixture } from \"../tests/notifications.fixture\";\n\ndescribe(\"${method} ${path}\", () => {\n it(\"answers 401 without a signed-in user\", async () => {\n const response = await ${helper}(\"${path}\");\n expect(response.status).toBe(401);\n });\n\n describe(\"signed in\", () => {\n let fixture: Awaited<ReturnType<typeof createNotificationTestFixture>> | undefined;\n beforeEach(async () => {\n fixture = await createNotificationTestFixture();\n });\n afterEach(async () => {\n const current = fixture;\n fixture = undefined;\n await current?.cleanup();\n });\n\n${signedInCases.replaceAll(\"fixture.\", \"fixture!.\")}\n });\n});\n`;\n}\n\n/** `src/web/root.setup.ts` — document policy. */\nexport const webRootSetupStub = `import type { RootConfig } from \"@warlock.js/web\";\n\nexport const config: RootConfig = { strictMode: true };\n`;\n\n/**\n * `src/web/root.tsx` — the application root for the SSR page layer.\n *\n * Deliberately minimal. The framework ships a default root, so this exists to\n * give you a place to start rather than because anything requires it. The\n * document component stays here; request policy and an app-level loader\n * belong in `root.setup.ts`. Named ErrorBoundary components stay in the UI.\n */\nexport const webRootStub = `import type { AppProps } from \"@warlock.js/web\";\nimport { Head, Scripts } from \"@warlock.js/web\";\n\n/**\n * The application root.\n *\n * NOT async, and it receives no request/response: it renders on the server and\n * again in the browser during hydration, where neither exists.\n */\nexport default function App({ children }: AppProps) {\n return (\n <html lang=\"en\">\n <head>\n {/*\n <Head /> also renders the viewport tag (width=device-width,\n initial-scale=1). Change it with \\`metadata.viewport\\`,\n or set it to false to leave it out; do not add a second one here.\n */}\n {/*\n Placement only. The framework injects the page's \\`metadata\\`, the\n stylesheet and preload tags for this route, and the canonical links\n into <head> by default — <Head /> just says WHERE they land.\n\n Do not add a <title> here: the page's \\`metadata\\` owns it, and a root\n that emits one too produces two.\n */}\n <Head />\n <link rel=\"icon\" href=\"data:,\" />\n </head>\n <body>\n {/*\n REQUIRED — this is the hydration mount point, not a styling wrapper.\n\n The browser runtime looks up \\`#vessel\\` and hydrates that element only.\n Remove this div, or rename the id, and the page still renders from the\n server but never becomes interactive: the runtime throws in the console\n and nothing on screen changes.\n\n Wrap it in your own markup freely, and put anything that must live\n outside the hydrated tree (a static footer, a portal target) outside\n it — just keep an element with \\`id=\"vessel\"\\` around {children}.\n */}\n <div id=\"vessel\">{children}</div>\n {/*\n The hydration payload and module tags. Written explicitly because\n placement occasionally matters — a CSP nonce, or ordering against\n your own scripts.\n */}\n <Scripts />\n </body>\n </html>\n );\n}\n`;\n\n/**\n * `src/app/contact/controllers/contact.controller.ts` — a real API endpoint\n * for the Web starter's contact form. It intentionally has no persistence\n * dependency: replace the acknowledgement with a mail/job/database action.\n */\nexport const webContactControllerStub = `import { type Request, type RequestHandler } from \"@warlock.js/core\";\nimport { v, type Infer } from \"@warlock.js/seal\";\n\nexport const contactSchema = v.object({\n name: v.string().min(2),\n email: v.email(),\n message: v.string().min(10),\n});\n\nexport type ContactSchema = Infer.Output<typeof contactSchema>;\n\n/** POST /api/contact — validates the starter contact form. */\nexport const contactController: RequestHandler<Request<ContactSchema>> = async ({\n request,\n response,\n}) => {\n const contact = request.validated();\n\n // Replace this with delivery/persistence for your app. Keeping the accepted\n // payload visible makes the endpoint useful while remaining side-effect free.\n return response.success({\n message: \"Thanks, \" + contact.name + \". Your message has been received.\",\n });\n};\n\ncontactController.validation = { schema: contactSchema };\n`;\n\n/** `src/app/contact/routes.ts` — discovered by the standard app route loader. */\nexport const webContactRoutesStub = `import { router } from \"@warlock.js/core\";\nimport { contactController } from \"./controllers/contact.controller\";\n\nrouter.post(\"/api/contact\", contactController, { public: true });\n`;\n\n/** `src/web/index.setup.ts` — page policy and universal registration. */\nexport const webHomeSetupStub = `import { extend } from \"@mongez/localization\";\nimport type { PageConfig } from \"@warlock.js/web\";\n\nexport const config: PageConfig = {\n route: { path: \"/\", name: \"index\" },\n metadata: { title: \"Home\" },\n};\n\nexport function register() {\n extend(\"en\", {\n starter: {\n title: \"Your Warlock app is running.\",\n introduction: \"This page is rendered on the server and hydrated in the browser.\",\n language: \"العربية\",\n contact: \"Send a message\",\n name: \"Name\",\n email: \"Email\",\n message: \"Message\",\n submit: \"Send message\",\n sent: \"Thanks — your message has been received.\",\n },\n });\n extend(\"ar\", {\n starter: {\n title: \"تطبيق Warlock يعمل الآن.\",\n introduction: \"تُعرض هذه الصفحة على الخادم ثم تُفعَّل في المتصفح.\",\n language: \"English\",\n contact: \"أرسل رسالة\",\n name: \"الاسم\",\n email: \"البريد الإلكتروني\",\n message: \"الرسالة\",\n submit: \"إرسال الرسالة\",\n sent: \"شكرًا — تم استلام رسالتك.\",\n },\n });\n}\n`;\n\n/**\n * `src/web/index.page.tsx` — one page, so \\`warlock dev\\` has something to serve\n * the moment this finishes.\n */\nexport const webHomePageStub = `import { http } from \"@mongez/http\";\nimport { setCurrentLocaleCode } from \"@mongez/localization\";\nimport { Form, useFormControl, type FormControlProps } from \"@mongez/react-form\";\nimport { transX } from \"@mongez/react-localization\";\nimport { v } from \"@warlock.js/seal\";\nimport { Link, type PageProps } from \"@warlock.js/web\";\nimport { useState } from \"react\";\n\nconst contactSchema = v.object({\n name: v.string().min(2),\n email: v.email(),\n message: v.string().min(10),\n});\n\nfunction TextInput({ label, ...controlProps }: FormControlProps & { label: string }) {\n const { error, getErrorProps, getInputProps } = useFormControl(controlProps);\n\n return (\n <div className=\"wk-field\">\n <label htmlFor={controlProps.name}>{label}</label>\n <input {...getInputProps()} />\n {error && <p {...getErrorProps()}>{error}</p>}\n </div>\n );\n}\n\n/** Fetch data in index.setup.ts; use a type-only loader import for PageProps. */\nexport default function HomePage(_props: PageProps) {\n // Live state. If the button below does nothing, the page rendered on the\n // server but never hydrated — the runtime never mounted at \\`#vessel\\`. This is\n // deliberately here so that failure is impossible to miss.\n const [count, setCount] = useState(0);\n const [locale, setLocale] = useState<\"en\" | \"ar\">(\"en\");\n const [submitted, setSubmitted] = useState(false);\n const [submitError, setSubmitError] = useState<string | null>(null);\n\n const toggleLocale = () => {\n const nextLocale = locale === \"en\" ? \"ar\" : \"en\";\n setCurrentLocaleCode(nextLocale);\n setLocale(nextLocale);\n };\n\n return (\n <>\n {/*\n Self-contained, dependency-free styling: plain CSS, system fonts, and\n CSS custom properties, scoped to this page. No CSS framework, no utility\n classes, no external stylesheet — this page looks the same whether or\n not \\`warlock add tailwind\\` has ever been run.\n */}\n <style>{\\`\n .wk-home {\n --wk-fg: #0f172a;\n --wk-muted: #64748b;\n --wk-accent: #4f46e5;\n --wk-border: #e2e8f0;\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n color: var(--wk-fg);\n max-width: 42rem;\n margin: 4rem auto;\n padding: 0 1.5rem;\n line-height: 1.6;\n }\n .wk-home h1 { font-size: 2.25rem; margin: 0 0 0.5rem; }\n .wk-home p { color: var(--wk-muted); margin: 0 0 1.5rem; }\n .wk-home code {\n font-family: ui-monospace, \"SFMono-Regular\", Menlo, monospace;\n background: #f1f5f9;\n padding: 0.1rem 0.35rem;\n border-radius: 0.25rem;\n }\n .wk-check {\n border: 1px solid var(--wk-border);\n border-radius: 0.75rem;\n padding: 1.25rem 1.5rem;\n margin: 2rem 0;\n }\n .wk-check strong { display: block; font-size: 1.5rem; }\n .wk-check button {\n font: inherit;\n cursor: pointer;\n background: var(--wk-accent);\n color: #fff;\n border: 0;\n border-radius: 0.5rem;\n padding: 0.5rem 1rem;\n margin-top: 0.75rem;\n }\n .wk-links { display: flex; gap: 1.25rem; font-size: 0.95rem; }\n .wk-links a { color: var(--wk-accent); text-decoration: none; }\n .wk-links a:hover { text-decoration: underline; }\n .wk-language { margin-left: auto; }\n .wk-contact { margin-top: 2rem; }\n .wk-field { display: grid; gap: 0.35rem; margin: 0.8rem 0; }\n .wk-field input, .wk-field textarea { font: inherit; padding: 0.55rem; }\n .wk-field p, .wk-submit-error { color: #b91c1c; margin: 0; }\n .wk-success { color: #047857; }\n \\`}</style>\n\n <main className=\"wk-home\" dir={locale === \"ar\" ? \"rtl\" : \"ltr\"}>\n <nav className=\"wk-links\" aria-label=\"Starter links\">\n <a href=\"https://warlock.js.org\" target=\"_blank\" rel=\"noreferrer\">\n Docs\n </a>\n <Link href=\"/\" aria-current=\"page\">\n Home\n </Link>\n <button\n className=\"wk-language\"\n type=\"button\"\n aria-pressed={locale === \"ar\"}\n onClick={toggleLocale}\n >\n {transX(\"starter.language\")}\n </button>\n </nav>\n\n <h1>{transX(\"starter.title\")}</h1>\n <p>{transX(\"starter.introduction\")}</p>\n\n <section className=\"wk-check\">\n <label>If this number goes up when you click, React is hydrated:</label>\n <strong>{count}</strong>\n <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\n Count up\n </button>\n </section>\n\n <section className=\"wk-contact\" aria-labelledby=\"contact-heading\">\n <h2 id=\"contact-heading\">{transX(\"starter.contact\")}</h2>\n <Form<typeof contactSchema>\n id=\"contact-form\"\n schema={contactSchema}\n onSubmit={async ({ form, values }) => {\n setSubmitted(false);\n setSubmitError(null);\n const result = await http.post<{ message: string }>(\"/api/contact\", values);\n\n if (result.error) {\n if (result.error.isValidationError) {\n const body = result.error.body as {\n errors?: Array<{ input: string; error: string }>;\n message?: string;\n };\n form.setErrors(\n Object.fromEntries(\n (body.errors ?? []).map(({ input, error }) => [input, error]),\n ),\n );\n setSubmitError(body.message ?? \"Please correct the highlighted fields.\");\n } else {\n setSubmitError(\"Your message could not be sent. Please try again.\");\n }\n return;\n }\n\n setSubmitted(true);\n form.reset();\n }}\n >\n <TextInput name=\"name\" label={transX(\"starter.name\")} autoComplete=\"name\" />\n <TextInput\n name=\"email\"\n label={transX(\"starter.email\")}\n type=\"email\"\n autoComplete=\"email\"\n />\n <ContactMessage />\n <button type=\"submit\">{transX(\"starter.submit\")}</button>\n {submitError && (\n <p className=\"wk-submit-error\" role=\"alert\">\n {submitError}\n </p>\n )}\n {submitted && (\n <p className=\"wk-success\" role=\"status\">\n {transX(\"starter.sent\")}\n </p>\n )}\n </Form>\n </section>\n </main>\n </>\n );\n}\n\nfunction ContactMessage() {\n const { error, getErrorProps, getInputProps } = useFormControl({ name: \"message\" });\n\n return (\n <div className=\"wk-field\">\n <label htmlFor=\"message\">{transX(\"starter.message\")}</label>\n <textarea {...getInputProps()} rows={5} />\n {error && <p {...getErrorProps()}>{error}</p>}\n </div>\n );\n}\n`;\n"],"mappings":";AAAA,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BhC,MAAa,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+B5B,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCnC,MAAa,2BAA2B;;AAGxC,MAAa,0BAA0B;;;;;;;;;;;;AAavC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FvC,MAAa,+BAA+B;;AAG5C,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;AAsB3C,MAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6ClC,MAAa,mBAAmB;;;;;;;;;;;;;;;;;AAkBhC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFvC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCvC,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDrC,MAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;;;AAwBzC,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;AAsBxC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;AAmB3C,MAAa,+BAA+B;;;;;;;;;;;;;;;;;AAkB5C,MAAa,kCAAkC;;;;;;;;;;;;;;;;;;;;AAqB/C,MAAa,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;;AAyB/C,MAAa,yCAAyC;;;;;;;;;;;;;;;;AAiBtD,MAAa,qCAAqC;;;;;;;;;;;;;;;;;;;;;AAsBlD,MAAa,yCAAyC;;;;;;;;;;;;;;;;AAiBtD,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BtC,MAAa,0BAA0B;;;;;AAMvC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2E3C,SAAgB,+BAA+B,QAAyB,MAAsB;CAC5F,MAAM,SAAS,WAAW,QAAQ,YAAY;CAC9C,IAAI;CACJ,IAAI,SAAS,kBACX,gBAAgB;;;;;;;;;;;;;;;;;;;;MAoBX,IAAI,SAAS,+BAClB,gBAAgB;;;;;;;;;;MAUX,IAAI,SAAS,2BAClB,gBAAgB;;;;;;;;;;;;;;;;MAiBhB,gBAAgB;;;;;;;;;;;;;;;CAoBlB,OAAO,wBAAwB,OAAO;;EAHpC,WAAW,UACP,kFACA,GAGM;;YAEF,OAAO,GAAG,KAAK;;6BAEE,OAAO,IAAI,KAAK;;;;;;;;;;;;;;;EAe3C,cAAc,WAAW,YAAY,WAAW,EAAE;;;;AAIpD;;AAGA,MAAa,mBAAmB;;;;;;;;;;;;AAahC,MAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4D3B,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BxC,MAAa,uBAAuB;;;;;;AAOpC,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0ChC,MAAa,kBAAkB"}
|
package/esm/index.d.mts
CHANGED
|
@@ -151,7 +151,7 @@ import { hmacHash } from "./encryption/hash.mjs";
|
|
|
151
151
|
import { hashPassword, verifyPassword } from "./encryption/password.mjs";
|
|
152
152
|
import { EncryptionConfigurations, EncryptionPasswordConfigurations } from "./encryption/types.mjs";
|
|
153
153
|
import { SecretStoreError, SecretStoreErrorCode } from "./secrets/errors.mjs";
|
|
154
|
-
import { SecretBackend, SecretStoreOptions, SecretsConfigurations } from "./secrets/types.mjs";
|
|
154
|
+
import { SecretBackend, SecretBackendType, SecretStoreOptions, SecretsConfigurations } from "./secrets/types.mjs";
|
|
155
155
|
import { MemorySecretBackend } from "./secrets/memory-backend.mjs";
|
|
156
156
|
import { SecretStore } from "./secrets/secret-store.mjs";
|
|
157
157
|
import { secrets } from "./secrets/secrets.mjs";
|
|
@@ -232,7 +232,7 @@ import "./warlock-config/index.mjs";
|
|
|
232
232
|
import { env } from "@mongez/dotenv";
|
|
233
233
|
import { colors } from "@mongez/copper";
|
|
234
234
|
export * from "@mongez/localization";
|
|
235
|
-
export { $registerUseCase, $unregisterUseCase, APP_ROLES, AUTH_GUARD_MARKER, AccessConnector, AggregateExpressionInput, AggregateExpressions, AiConnector, AllRepositoryOptions, AppConfigurations, AppRole, Application, AuthDescriptor, BOOT_SIGNAL_ENV_KEY, BOOT_SIGNAL_VERSION, BadRequestError, BadSchemaUseCaseError, BaseCastOutput, BaseConnector, type BenchmarkChannel, type BenchmarkConfigurations, type BenchmarkErrorResult, type BenchmarkOptions, BenchmarkProfiler, type BenchmarkProfilerOptions, type BenchmarkResult, BenchmarkSnapshots, type BenchmarkSnapshotsOptions, type BenchmarkStats, type BenchmarkSuccessResult, BootContext, BootListener, BootSignal, BootSignalType, BootValidator, CLICommand, type CLICommandAction, type CLICommandOption, type CLICommandOptions, type CLICommandPreload, type CLICommandSource, CacheConnector, type CacheMiddlewareOptions, CachedRepositoryOptions, type CapturedMail, CascadeAdapter, CascadeQueryBuilder, CastOutput, ChunkCallback, ClockSource, ClosableServer, CloudDriver, CloudStorageDriverContract, CloudStorageDriverOptions, CloudStorageFileData, type CommandActionData, type ConcurrencyLimitOptions, ConditionalRequestInput, ConditionalRequestOutcome, ConfigIssue, ConfigKey, ConfigKeyRegistry, ConfigModuleEntry, ConfigName, ConfigOf, ConfigRegistry, ConfigSchema, ConfigSchemaOptions, ConfigSpecialHandlers, ConflictError, Connector, ConnectorBuildContext, ConnectorBuildContribution, ConnectorBuildGenerateResult, ConnectorEsbuildPatch, ConnectorLifecyclePhase, ConnectorName, ConnectorPriority, ConnectorsManager, ConsoleChannel, ContainerTypes, CookieJarUnavailableError, CookieOptions, CspConfig, CsrfOriginCheckReason, CsrfOriginVerdict, CursorPaginationOptions, CursorPaginationResult, DEFAULT_CSP_DIRECTIVES, DEFAULT_POSTMAN_BASE_URL, DOSpacesDriver, DatabaseCacheDriver, type DatabaseCacheOptions, DatabaseConnector, DatabaseLog, DatabaseLogModel, DatabaseLogOptions, DecodedAccessToken, DefineResourceOptions, DeleteManyResult, DetectedImageFormat, type DevelopmentAppModuleEntry, type DevelopmentAppModules, type DevelopmentModelModuleEntry, type DevelopmentModelModuleState, type DevelopmentModelModules, DevelopmentPostmanResult, ERROR_CODES, ERROR_CODES_DOCS_URL, EncryptionConfigurations, EncryptionPasswordConfigurations, EntityTag, Environment, ErrorCodeArea, ErrorCodeCategory, ErrorCodeDefinition, EventSubscription, ExistsExceptCurrentIdRuleOptions, ExistsExceptCurrentUserRuleOptions, FastifyInstance, FieldOutput, FileNamingStrategy, FileValidationOptions, FileValidator, FileVisibility, FilterFunction, FilterOperator, FilterOptions, FilterRule, FilterRules, ForbiddenError, GenerateImageVariantsOptions, GeneratedImageDescriptor, GeneratedImageVariant, GroupByFields, GroupedRoutesOptions, HealthCheck, HealthStatus, HeraldConnector, HttpConfigurations, HttpConnector, HttpContext, HttpError, HttpErrorCodes, type HttpErrorMapper, type HttpErrorMapping, HttpLoggingConfig, HttpReadyReport, HttpServerOptions, HttpServerRebuiltListener, HttpTracingConfig, type IdempotencyOptions, IdsSource, Image, ImageFormat, ImageInput, ImageTransformCallback, ImageTransformConfig, ImageTransformOptions, ImageVariantDefinition, ImageVariantFit, ImageVariantOutputFormat, ImageVariantsConfigError, InvalidConfigError, InvalidCspDirectiveError, type IpFilterOptions, JavaScriptRuntime, JavaScriptRuntimeName, ListOptions, LocalDriver, LocalStorageDriverOptions, LocaleDictionaryEntries, LocaleDictionaryError, LocalizedObject, LogConfigurations, LoggerConnector, MAIL_EVENTS, Mail, type MailAddress, type MailAttachment, type MailConfigurations, MailError, type MailErrorCode, type MailEvents, type MailMode, type MailOptions, type MailPriority, type MailResult, MailerConnector, type MailersConfig, type MaintenanceOptions, MemorySecretBackend, Middleware, MiddlewareResponse, MimeTypes, ModelResourceRegistry, NULLABLE_RESOURCE, NamedApiRoute, NamedApiRoutesOptions, NoopChannel, type NormalizedMail, NotAcceptableError, NotAllowedError, NotificationsConnector, NullableResource, NullableResourceTarget, OPENAPI_JSON_SCHEMA_DIALECT, OpenApiBuildResult, OpenApiComponents, OpenApiContext, OpenApiDocument, OpenApiOperation, OpenApiParameter, OpenApiRequestBody, OpenApiResponse, OpenApiRouteInput, OpenApiSchema, OpenApiSecurityRequirement, OpenApiSecurityScheme, OpenApiValidationResponseConfig, POSTMAN_COLLECTION_SCHEMA, PaginationMode, PaginationResult, ParsedLocaleDictionary, PartialMiddleware, PartialPick, Path, PipeableReactStream, PipelineOptions, PortInUseError, PositionalHandlerSuspect, PostmanAuth, PostmanAuthEntry, PostmanBody, PostmanCollection, PostmanCollectionOptions, PostmanFolder, PostmanFormParam, PostmanHeader, PostmanItem, PostmanOriginalRequest, PostmanQueryParam, PostmanRequest, PostmanRequestItem, PostmanResponse, PostmanUrl, PostmanVariable, PrefixConfig, PrefixOptions, PresignedOptions, PresignedUploadOptions, PrivateRequestLoggingRule, PurgeImageVariantsOptions, PutDirectoryOptions, PutDirectoryResult, PutFromUrlOptions, PutOptions, QueryBuilderContract, Queue, R2Driver, R2StorageDriverOptions, type RateLimitOptions, RegisterConfiguredConnectorsOptions, RegisterResource, RegisteredUseCase, RepositoryAdapterContract, RepositoryConfigurations, RepositoryEvent, RepositoryManager, RepositoryOptions, RepositoryOptionsWithCursor, RepositoryOptionsWithPages, Request, RequestContextStore, RequestController, RequestControllerContract, RequestEvent, RequestHandler, RequestHandlerType, RequestHandlerValidation, RequestLocals, RequestLog, RequestLogSnapshot, RequestLogSource, RequestLoggingOptions, RequestMethod, RequestTranslator, ResolveImageVariantInput, type ResolvedCLICommandOption, ResolvedImageVariant, ResolvedImageVariantsConfig, Resource, ResourceArraySchema, ResourceCastType, ResourceConstructor, ResourceContract, ResourceFieldBuilder, ResourceFieldBuilderDateOutputOptions, ResourceFieldConfig, ResourceMethod, ResourceNotFoundError, ResourceOutput, ResourceOutputOf, ResourceOutputValueCastType, ResourceSchema, ResourceSelfReference, Response, ResponseBodyOutput, ResponseBodyValue, ResponseEvent, ResponseSSEController, ResponseSSEMessageOptions, ResponseSSEOptions, ResponseSchema, ResponseSchemaOutput, ResponseStatus, ResponseStreamController, ResponseStreamOptions, Restful, RestfulMiddleware, ReturnedResponse, Route, RouteNameTakenError, RouteOptions, RouteRateLimitOptions, RouteRegistry, RouteResource, Router, RouterGroupCallback, RouterStacks, RuntimeStrategy, S3Driver, type SESConfigurations, type SMTPConfigurations, SUPPORTED_RUNTIMES_DOCS_URL, SafeFetchOptions, SafeFetchResult, SafeRedirectOptions, SaveAsOptions, SaveMode, SaveOptions, ScopedStorage, ScopedStorageContract, type SecretBackend, SecretStore, SecretStoreError, SecretStoreErrorCode, type SecretStoreOptions, type SecretsConfigurations, SeedClock, SeedContext, SeedRecordRef, SeedResult, Seeder, SeederDependencyCycleError, SeederMetadata, SendBufferOptions, SendFileOptions, Serialized, SerializedWire, ServerError, ShutdownListener, SocketAdapterFactory, SocketConnector, SocketOptions, StaticMountOptions, Storage, StorageCapabilityError, StorageConfigurations, StorageConnector, StorageCopyEventPayload, StorageDriverConfig, StorageDriverContextStore, StorageDriverContract, StorageDriverName, StorageDriverRegistry, StorageDriverType, StorageError, StorageErrorOptions, StorageEventHandler, StorageEventPayload, StorageEventType, StorageFile, StorageFileData, StorageFileInfo, StorageManagerContract, StorageNotInitializedError, StoragePutEventPayload, StreamReactResponseOptions, TemporaryTokenError, TemporaryTokenPayload, TemporaryTokenValidation, TracingContext, TracingContextSource, TracingHooks, TracingPhaseInfo, TracingRequestEndInfo, Track, TrackableModel, TypedAllRepositoryOptions, TypedRepositoryOptions, TypedRepositoryOptionsWithCursor, TypedRepositoryOptionsWithPages, UPDATE_MANY_BATCH_SIZE, UPLOADS_DEFAULTS, URL_CREDENTIAL_PARAMS, UnAuthorizedError, UniqueExceptCurrentIdRuleOptions, UniqueExceptCurrentUserRuleOptions, UnknownSeederDependencyError, UploadedFile, UploadedFileControllerOptions, UploadedFileImageOptions, UploadedFileJson, UploadedFileQuery, UploadsConfigurations, UploadsImagesConfigurations, UseCase, UseCaseAfterMiddleware, UseCaseBeforeMiddleware, UseCaseBroadcastChannel, UseCaseBroadcastEvent, UseCaseBroadcastOption, UseCaseConfigurations, UseCaseContext, UseCaseErrorResult, UseCaseEventsCallbacksMap, UseCaseGuard, UseCaseHandler, UseCaseOnExecutingContext, UseCaseResult, UseCaseRuntimeOptions, UseCaseWithSchema, ValidationConfiguration, ValidationFailureContext, ValidationFailureHandler, ValidationFailureResponse, ValidationResponseConfiguration, VariantOutputFormat, VariantSourceFormat, WarlockConfig, WarlockConfigManager, WarlockError, WarlockErrorCode, WarningSink, WatermarkConfig, WhereOperator, XMLable, addUseCaseHistory, anyMatch, app, appLog, appPath, applyCspHeader, assertConfiguredHttpPortIsFree, assertMailCount, assertMailSent, assertNoReservedConnectorNames, assertNodeOnlyCommand, assertPortIsAvailable, assertSitesRequireWebRole, assertUniqueConnectorNames, assertValidConfigModules, assetsUrl, beginHttpDrain, bootstrap, broadcastSocketEvent, broadcastUseCaseResult, buildCspHeaderValue, buildIdempotencyCacheKey, buildOpenApiDocument, buildRequestLogSnapshot, buildTracingContext, cachePath, captureMail, clearPositionalHandlerSuspects, clearTestMailbox, clock, closeAllMailers, closeMailer, closeServerWithTimeout, colors, command, config, configPath, configSpecialHandlers, connectorsManager, container, createHttpApplication, createRequestStore, createUploadedFileController, decrypt, defaultHttpConfigurations, defineConfig, defineConfigSchema, defineResource, deriveTraceId, describeJavaScriptRuntime, describePositionalHandlerSuspect, detectJavaScriptRuntime, dispatchPhase, dispatchRequestEnd, dispatchRequestStart, displayEnvironmentMode, encrypt, ensureFatalIsVisible, env, environment, errorCodeDocsUrl, errorCodeOf, escapeHeaderValue, evaluateConditionalRequest, exampleFromSchema, existsExceptCurrentIdRule, existsExceptCurrentUserRule, explainErrorCode, fetchLatestVersion, fileExtensionRule, fileRule, fileTypeRule, findMailsBySubject, findMailsTo, fireLifecycleEvent, forgetPositionalHandlerSuspects, formatConfigIssues, generateImageVariants, generateMailId, getDefaultMailConfig, getDevelopmentOpenApiDocument, getDevelopmentPostmanCollection, getExplicitMailMode, getHttpReadyReport, getHttpServer, getLastMail, getLocalized, getMailEventName, getMailMode, getMailboxSize, getMailer, getMailerConfig, getMimeType, getPoolStats, getPublicUrl, getRoles, getSites, getSocketServer, getTestMailbox, getUseCase, getUseCaseHistory, getUseCases, globalEventsCallbacksMap, globalUseCasesEvents, hasRole, hashBody, hashPassword, health, hmacHash, httpConfig, ids, imageRule, increaseUseCaseFailedCalls, increaseUseCaseSuccessCalls, inspectHandlerSignature, ipMatches, isAllowedCsrfOrigin, isAuthGuardMiddleware, isBootSignal, isConfigSchema, isDevelopmentMode, isNewerVersion, isNullableResource, isPortAvailable, isPrivateOrReservedIp, isProductionMode, isSafeRedirect, isTestMode, isTracingEnabled, isUnguardedMutatingRoute, isUnknownTsExtensionError, isUnresolvedConfigImportError, isUnsupportedTypeStrippingError, isValidIdempotencyKey, isWarlockErrorCode, listPositionalHandlerSuspects, loadS3, logResponse, logsPath, looksLikePositionalHandler, mailEvents, markAuthGuard, matchesDerivedRouteName, matchesIfMatch, matchesIfNoneMatch, measure, mergeCspDirectives, middleware, nodeOnlyCommandError, normalizeOrigin, normalizeRequestPath, normalizeRoutePath, notifyHttpServerRebuilt, nullable, onHttpServerRebuilt, openApiToPostmanCollection, originOf, ownOrigin, parseEntityTag, parseEntityTagList, parseLocaleDictionary, parseRoles, parseSites, parseSize, parseTraceparentTraceId, paths, preflightConfiguredHttpPort, promiseAllObject, publicPath, publicUrl, purgeImageVariants, redactQueryCredentials, redactUrlCredentials, registerAppConfig, registerConfiguredConnectors, registerDevelopmentOpenApiProvider, registerGroupedTranslationKeys, registerHttpPlugins, registerLocaleDictionary, registerTracingHooks, remedyLines, renderReact, renderReactMail, reportPositionalHandlerSuspects, reportUnguardedMutatingRoutes, requestContext, requestMemo, requiresDefaultCsrfGuard, resetHttpReadyReport, resetMailConfig, resetMailMode, resetRolesCacheForTests, resetTracingConfigForTests, resetUnguardedRouteReports, resolveCspConfig, resolveCsrfOriginVerdict, resolveImageVariant, resolveMailConfig, resolvePublicRoot, resolveRequestLoggingOptions, resolveTracingConfig, resolveWithinRoot, rootPath, routeNameMethodSuffix, router, runDefaultCsrfGuard, runPipeline, safeFetchToBuffer, safeRedirect, sanitizePath, secrets, seeder, sendBootSignal, sendMail, serializeCspDirectives, servesHttp, setBaseUrl, setClockSource, setConfig, setEnvironment, setHttpReadyReport, setIdsSource, setLogConfigurations, setMailConfigurations, setMailMode, shouldPreflightHttpPort, sleep, sluggable, srcPath, startHttpServer, stopHttpApplication, storage, storageConfig, storageConfigurations, storageDriverContext, storagePath, streamReactResponse, strongEntityTagMatch, t, tempPath, toJson, toSingleLineSseField, trackConnectionsForDrain, uniqueExceptCurrentIdRule, uniqueExceptCurrentUserRule, unregisterLocaleDictionary, unverifiedBuildRuntimeNotice, uploadedFileController, uploadedFileMetadataSchema, uploadsConfig, uploadsPath, uploadsUrl, url, useCase, useComputedModel, useComputedSlug, useCurrentUser, useHashedPassword, useRequest, useRequestStore, validateConfigValue, validateCspConfigAtBoot, validateCspDirectives, verifyMailer, verifyPassword, warlockConfigManager, warlockPath, wasMailSentTo, wasMailSentWithSubject, weakEntityTagMatch, wrapResponseInDataKey };
|
|
235
|
+
export { $registerUseCase, $unregisterUseCase, APP_ROLES, AUTH_GUARD_MARKER, AccessConnector, AggregateExpressionInput, AggregateExpressions, AiConnector, AllRepositoryOptions, AppConfigurations, AppRole, Application, AuthDescriptor, BOOT_SIGNAL_ENV_KEY, BOOT_SIGNAL_VERSION, BadRequestError, BadSchemaUseCaseError, BaseCastOutput, BaseConnector, type BenchmarkChannel, type BenchmarkConfigurations, type BenchmarkErrorResult, type BenchmarkOptions, BenchmarkProfiler, type BenchmarkProfilerOptions, type BenchmarkResult, BenchmarkSnapshots, type BenchmarkSnapshotsOptions, type BenchmarkStats, type BenchmarkSuccessResult, BootContext, BootListener, BootSignal, BootSignalType, BootValidator, CLICommand, type CLICommandAction, type CLICommandOption, type CLICommandOptions, type CLICommandPreload, type CLICommandSource, CacheConnector, type CacheMiddlewareOptions, CachedRepositoryOptions, type CapturedMail, CascadeAdapter, CascadeQueryBuilder, CastOutput, ChunkCallback, ClockSource, ClosableServer, CloudDriver, CloudStorageDriverContract, CloudStorageDriverOptions, CloudStorageFileData, type CommandActionData, type ConcurrencyLimitOptions, ConditionalRequestInput, ConditionalRequestOutcome, ConfigIssue, ConfigKey, ConfigKeyRegistry, ConfigModuleEntry, ConfigName, ConfigOf, ConfigRegistry, ConfigSchema, ConfigSchemaOptions, ConfigSpecialHandlers, ConflictError, Connector, ConnectorBuildContext, ConnectorBuildContribution, ConnectorBuildGenerateResult, ConnectorEsbuildPatch, ConnectorLifecyclePhase, ConnectorName, ConnectorPriority, ConnectorsManager, ConsoleChannel, ContainerTypes, CookieJarUnavailableError, CookieOptions, CspConfig, CsrfOriginCheckReason, CsrfOriginVerdict, CursorPaginationOptions, CursorPaginationResult, DEFAULT_CSP_DIRECTIVES, DEFAULT_POSTMAN_BASE_URL, DOSpacesDriver, DatabaseCacheDriver, type DatabaseCacheOptions, DatabaseConnector, DatabaseLog, DatabaseLogModel, DatabaseLogOptions, DecodedAccessToken, DefineResourceOptions, DeleteManyResult, DetectedImageFormat, type DevelopmentAppModuleEntry, type DevelopmentAppModules, type DevelopmentModelModuleEntry, type DevelopmentModelModuleState, type DevelopmentModelModules, DevelopmentPostmanResult, ERROR_CODES, ERROR_CODES_DOCS_URL, EncryptionConfigurations, EncryptionPasswordConfigurations, EntityTag, Environment, ErrorCodeArea, ErrorCodeCategory, ErrorCodeDefinition, EventSubscription, ExistsExceptCurrentIdRuleOptions, ExistsExceptCurrentUserRuleOptions, FastifyInstance, FieldOutput, FileNamingStrategy, FileValidationOptions, FileValidator, FileVisibility, FilterFunction, FilterOperator, FilterOptions, FilterRule, FilterRules, ForbiddenError, GenerateImageVariantsOptions, GeneratedImageDescriptor, GeneratedImageVariant, GroupByFields, GroupedRoutesOptions, HealthCheck, HealthStatus, HeraldConnector, HttpConfigurations, HttpConnector, HttpContext, HttpError, HttpErrorCodes, type HttpErrorMapper, type HttpErrorMapping, HttpLoggingConfig, HttpReadyReport, HttpServerOptions, HttpServerRebuiltListener, HttpTracingConfig, type IdempotencyOptions, IdsSource, Image, ImageFormat, ImageInput, ImageTransformCallback, ImageTransformConfig, ImageTransformOptions, ImageVariantDefinition, ImageVariantFit, ImageVariantOutputFormat, ImageVariantsConfigError, InvalidConfigError, InvalidCspDirectiveError, type IpFilterOptions, JavaScriptRuntime, JavaScriptRuntimeName, ListOptions, LocalDriver, LocalStorageDriverOptions, LocaleDictionaryEntries, LocaleDictionaryError, LocalizedObject, LogConfigurations, LoggerConnector, MAIL_EVENTS, Mail, type MailAddress, type MailAttachment, type MailConfigurations, MailError, type MailErrorCode, type MailEvents, type MailMode, type MailOptions, type MailPriority, type MailResult, MailerConnector, type MailersConfig, type MaintenanceOptions, MemorySecretBackend, Middleware, MiddlewareResponse, MimeTypes, ModelResourceRegistry, NULLABLE_RESOURCE, NamedApiRoute, NamedApiRoutesOptions, NoopChannel, type NormalizedMail, NotAcceptableError, NotAllowedError, NotificationsConnector, NullableResource, NullableResourceTarget, OPENAPI_JSON_SCHEMA_DIALECT, OpenApiBuildResult, OpenApiComponents, OpenApiContext, OpenApiDocument, OpenApiOperation, OpenApiParameter, OpenApiRequestBody, OpenApiResponse, OpenApiRouteInput, OpenApiSchema, OpenApiSecurityRequirement, OpenApiSecurityScheme, OpenApiValidationResponseConfig, POSTMAN_COLLECTION_SCHEMA, PaginationMode, PaginationResult, ParsedLocaleDictionary, PartialMiddleware, PartialPick, Path, PipeableReactStream, PipelineOptions, PortInUseError, PositionalHandlerSuspect, PostmanAuth, PostmanAuthEntry, PostmanBody, PostmanCollection, PostmanCollectionOptions, PostmanFolder, PostmanFormParam, PostmanHeader, PostmanItem, PostmanOriginalRequest, PostmanQueryParam, PostmanRequest, PostmanRequestItem, PostmanResponse, PostmanUrl, PostmanVariable, PrefixConfig, PrefixOptions, PresignedOptions, PresignedUploadOptions, PrivateRequestLoggingRule, PurgeImageVariantsOptions, PutDirectoryOptions, PutDirectoryResult, PutFromUrlOptions, PutOptions, QueryBuilderContract, Queue, R2Driver, R2StorageDriverOptions, type RateLimitOptions, RegisterConfiguredConnectorsOptions, RegisterResource, RegisteredUseCase, RepositoryAdapterContract, RepositoryConfigurations, RepositoryEvent, RepositoryManager, RepositoryOptions, RepositoryOptionsWithCursor, RepositoryOptionsWithPages, Request, RequestContextStore, RequestController, RequestControllerContract, RequestEvent, RequestHandler, RequestHandlerType, RequestHandlerValidation, RequestLocals, RequestLog, RequestLogSnapshot, RequestLogSource, RequestLoggingOptions, RequestMethod, RequestTranslator, ResolveImageVariantInput, type ResolvedCLICommandOption, ResolvedImageVariant, ResolvedImageVariantsConfig, Resource, ResourceArraySchema, ResourceCastType, ResourceConstructor, ResourceContract, ResourceFieldBuilder, ResourceFieldBuilderDateOutputOptions, ResourceFieldConfig, ResourceMethod, ResourceNotFoundError, ResourceOutput, ResourceOutputOf, ResourceOutputValueCastType, ResourceSchema, ResourceSelfReference, Response, ResponseBodyOutput, ResponseBodyValue, ResponseEvent, ResponseSSEController, ResponseSSEMessageOptions, ResponseSSEOptions, ResponseSchema, ResponseSchemaOutput, ResponseStatus, ResponseStreamController, ResponseStreamOptions, Restful, RestfulMiddleware, ReturnedResponse, Route, RouteNameTakenError, RouteOptions, RouteRateLimitOptions, RouteRegistry, RouteResource, Router, RouterGroupCallback, RouterStacks, RuntimeStrategy, S3Driver, type SESConfigurations, type SMTPConfigurations, SUPPORTED_RUNTIMES_DOCS_URL, SafeFetchOptions, SafeFetchResult, SafeRedirectOptions, SaveAsOptions, SaveMode, SaveOptions, ScopedStorage, ScopedStorageContract, type SecretBackend, type SecretBackendType, SecretStore, SecretStoreError, SecretStoreErrorCode, type SecretStoreOptions, type SecretsConfigurations, SeedClock, SeedContext, SeedRecordRef, SeedResult, Seeder, SeederDependencyCycleError, SeederMetadata, SendBufferOptions, SendFileOptions, Serialized, SerializedWire, ServerError, ShutdownListener, SocketAdapterFactory, SocketConnector, SocketOptions, StaticMountOptions, Storage, StorageCapabilityError, StorageConfigurations, StorageConnector, StorageCopyEventPayload, StorageDriverConfig, StorageDriverContextStore, StorageDriverContract, StorageDriverName, StorageDriverRegistry, StorageDriverType, StorageError, StorageErrorOptions, StorageEventHandler, StorageEventPayload, StorageEventType, StorageFile, StorageFileData, StorageFileInfo, StorageManagerContract, StorageNotInitializedError, StoragePutEventPayload, StreamReactResponseOptions, TemporaryTokenError, TemporaryTokenPayload, TemporaryTokenValidation, TracingContext, TracingContextSource, TracingHooks, TracingPhaseInfo, TracingRequestEndInfo, Track, TrackableModel, TypedAllRepositoryOptions, TypedRepositoryOptions, TypedRepositoryOptionsWithCursor, TypedRepositoryOptionsWithPages, UPDATE_MANY_BATCH_SIZE, UPLOADS_DEFAULTS, URL_CREDENTIAL_PARAMS, UnAuthorizedError, UniqueExceptCurrentIdRuleOptions, UniqueExceptCurrentUserRuleOptions, UnknownSeederDependencyError, UploadedFile, UploadedFileControllerOptions, UploadedFileImageOptions, UploadedFileJson, UploadedFileQuery, UploadsConfigurations, UploadsImagesConfigurations, UseCase, UseCaseAfterMiddleware, UseCaseBeforeMiddleware, UseCaseBroadcastChannel, UseCaseBroadcastEvent, UseCaseBroadcastOption, UseCaseConfigurations, UseCaseContext, UseCaseErrorResult, UseCaseEventsCallbacksMap, UseCaseGuard, UseCaseHandler, UseCaseOnExecutingContext, UseCaseResult, UseCaseRuntimeOptions, UseCaseWithSchema, ValidationConfiguration, ValidationFailureContext, ValidationFailureHandler, ValidationFailureResponse, ValidationResponseConfiguration, VariantOutputFormat, VariantSourceFormat, WarlockConfig, WarlockConfigManager, WarlockError, WarlockErrorCode, WarningSink, WatermarkConfig, WhereOperator, XMLable, addUseCaseHistory, anyMatch, app, appLog, appPath, applyCspHeader, assertConfiguredHttpPortIsFree, assertMailCount, assertMailSent, assertNoReservedConnectorNames, assertNodeOnlyCommand, assertPortIsAvailable, assertSitesRequireWebRole, assertUniqueConnectorNames, assertValidConfigModules, assetsUrl, beginHttpDrain, bootstrap, broadcastSocketEvent, broadcastUseCaseResult, buildCspHeaderValue, buildIdempotencyCacheKey, buildOpenApiDocument, buildRequestLogSnapshot, buildTracingContext, cachePath, captureMail, clearPositionalHandlerSuspects, clearTestMailbox, clock, closeAllMailers, closeMailer, closeServerWithTimeout, colors, command, config, configPath, configSpecialHandlers, connectorsManager, container, createHttpApplication, createRequestStore, createUploadedFileController, decrypt, defaultHttpConfigurations, defineConfig, defineConfigSchema, defineResource, deriveTraceId, describeJavaScriptRuntime, describePositionalHandlerSuspect, detectJavaScriptRuntime, dispatchPhase, dispatchRequestEnd, dispatchRequestStart, displayEnvironmentMode, encrypt, ensureFatalIsVisible, env, environment, errorCodeDocsUrl, errorCodeOf, escapeHeaderValue, evaluateConditionalRequest, exampleFromSchema, existsExceptCurrentIdRule, existsExceptCurrentUserRule, explainErrorCode, fetchLatestVersion, fileExtensionRule, fileRule, fileTypeRule, findMailsBySubject, findMailsTo, fireLifecycleEvent, forgetPositionalHandlerSuspects, formatConfigIssues, generateImageVariants, generateMailId, getDefaultMailConfig, getDevelopmentOpenApiDocument, getDevelopmentPostmanCollection, getExplicitMailMode, getHttpReadyReport, getHttpServer, getLastMail, getLocalized, getMailEventName, getMailMode, getMailboxSize, getMailer, getMailerConfig, getMimeType, getPoolStats, getPublicUrl, getRoles, getSites, getSocketServer, getTestMailbox, getUseCase, getUseCaseHistory, getUseCases, globalEventsCallbacksMap, globalUseCasesEvents, hasRole, hashBody, hashPassword, health, hmacHash, httpConfig, ids, imageRule, increaseUseCaseFailedCalls, increaseUseCaseSuccessCalls, inspectHandlerSignature, ipMatches, isAllowedCsrfOrigin, isAuthGuardMiddleware, isBootSignal, isConfigSchema, isDevelopmentMode, isNewerVersion, isNullableResource, isPortAvailable, isPrivateOrReservedIp, isProductionMode, isSafeRedirect, isTestMode, isTracingEnabled, isUnguardedMutatingRoute, isUnknownTsExtensionError, isUnresolvedConfigImportError, isUnsupportedTypeStrippingError, isValidIdempotencyKey, isWarlockErrorCode, listPositionalHandlerSuspects, loadS3, logResponse, logsPath, looksLikePositionalHandler, mailEvents, markAuthGuard, matchesDerivedRouteName, matchesIfMatch, matchesIfNoneMatch, measure, mergeCspDirectives, middleware, nodeOnlyCommandError, normalizeOrigin, normalizeRequestPath, normalizeRoutePath, notifyHttpServerRebuilt, nullable, onHttpServerRebuilt, openApiToPostmanCollection, originOf, ownOrigin, parseEntityTag, parseEntityTagList, parseLocaleDictionary, parseRoles, parseSites, parseSize, parseTraceparentTraceId, paths, preflightConfiguredHttpPort, promiseAllObject, publicPath, publicUrl, purgeImageVariants, redactQueryCredentials, redactUrlCredentials, registerAppConfig, registerConfiguredConnectors, registerDevelopmentOpenApiProvider, registerGroupedTranslationKeys, registerHttpPlugins, registerLocaleDictionary, registerTracingHooks, remedyLines, renderReact, renderReactMail, reportPositionalHandlerSuspects, reportUnguardedMutatingRoutes, requestContext, requestMemo, requiresDefaultCsrfGuard, resetHttpReadyReport, resetMailConfig, resetMailMode, resetRolesCacheForTests, resetTracingConfigForTests, resetUnguardedRouteReports, resolveCspConfig, resolveCsrfOriginVerdict, resolveImageVariant, resolveMailConfig, resolvePublicRoot, resolveRequestLoggingOptions, resolveTracingConfig, resolveWithinRoot, rootPath, routeNameMethodSuffix, router, runDefaultCsrfGuard, runPipeline, safeFetchToBuffer, safeRedirect, sanitizePath, secrets, seeder, sendBootSignal, sendMail, serializeCspDirectives, servesHttp, setBaseUrl, setClockSource, setConfig, setEnvironment, setHttpReadyReport, setIdsSource, setLogConfigurations, setMailConfigurations, setMailMode, shouldPreflightHttpPort, sleep, sluggable, srcPath, startHttpServer, stopHttpApplication, storage, storageConfig, storageConfigurations, storageDriverContext, storagePath, streamReactResponse, strongEntityTagMatch, t, tempPath, toJson, toSingleLineSseField, trackConnectionsForDrain, uniqueExceptCurrentIdRule, uniqueExceptCurrentUserRule, unregisterLocaleDictionary, unverifiedBuildRuntimeNotice, uploadedFileController, uploadedFileMetadataSchema, uploadsConfig, uploadsPath, uploadsUrl, url, useCase, useComputedModel, useComputedSlug, useCurrentUser, useHashedPassword, useRequest, useRequestStore, validateConfigValue, validateCspConfigAtBoot, validateCspDirectives, verifyMailer, verifyPassword, warlockConfigManager, warlockPath, wasMailSentTo, wasMailSentWithSubject, weakEntityTagMatch, wrapResponseInDataKey };
|
|
236
236
|
import "./config/types.mjs";
|
|
237
237
|
import "./http/request.mjs";
|
|
238
238
|
import "./http/serialized.mjs";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"file-backend.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/file-backend.ts"],"sourcesContent":["import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes } from \"node:crypto\";\nimport { registerLogSecret } from \"@warlock.js/logger\";\nimport { constants, type Stats } from \"node:fs\";\nimport { lstat, mkdir, open, rename, unlink, type FileHandle } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { SecretStoreError } from \"./errors\";\nimport type { SecretBackend } from \"./types\";\n\nfunction digest(value: string): string {\n return createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nfunction isMissing(error: unknown): boolean {\n return error instanceof Error && \"code\" in error && error.code === \"ENOENT\";\n}\n\n/** One authenticated file per secret: concurrent writes to different keys cannot clobber each other. */\nexport class FileSecretBackend implements SecretBackend {\n public constructor(\n private readonly directory: string,\n private readonly encryptionKey: () => string | undefined,\n ) {}\n\n private key(namespace: string): Buffer {\n const secret = this.encryptionKey();\n if (typeof secret !== \"string\" || !/^[a-f\\d]{64}$/i.test(secret)) {\n throw new SecretStoreError(\"KEY_REQUIRED\");\n }\n registerLogSecret(secret, [\"encryptionKey\"]);\n // Core's encrypt/decrypt helpers use global config directly. Derive a\n // separate AES-256-GCM key without changing that global config.\n // https://nodejs.org/docs/latest-v22.x/api/crypto.html#cryptohkdfsyncdigest-ikm-salt-info-keylen\n return Buffer.from(\n hkdfSync(\"sha256\", Buffer.from(secret, \"hex\"), namespace, \"warlock:secrets:v1\", 32),\n );\n }\n\n private validateOwner(stats: Stats, directory: boolean): void {\n if (\n (directory ? !stats.isDirectory() : !stats.isFile() || stats.nlink !== 1) ||\n (process.getuid && stats.uid !== process.getuid())\n ) {\n throw new SecretStoreError(\"UNSAFE_FILE\");\n }\n }\n\n private async folder(namespace: string, create: boolean): Promise<string | undefined> {\n // Node's Windows modes cannot enforce owner-only ACLs. Fail closed there.\n // https://nodejs.org/docs/latest-v22.x/api/fs.html#file-modes\n const root = path.resolve(this.directory);\n const folder = path.join(root, digest(namespace));\n for (const directory of [root, folder]) {\n if (!create) {\n try {\n await lstat(directory);\n } catch (error) {\n if (isMissing(error)) return undefined;\n throw error;\n }\n }\n if (process.platform === \"win32\") throw new SecretStoreError(\"UNSAFE_FILE\");\n if (create) await mkdir(directory, { recursive: true, mode: 0o700 });\n this.validateOwner(await lstat(directory), true);\n const handle = await open(directory, constants.O_RDONLY | constants.O_NOFOLLOW);\n try {\n this.validateOwner(await handle.stat(), true);\n await handle.chmod(0o700);\n if (((await handle.stat()).mode & 0o777) !== 0o700) {\n throw new SecretStoreError(\"UNSAFE_FILE\");\n }\n } finally {\n await handle.close();\n }\n }\n return folder;\n }\n\n private async location(\n namespace: string,\n name: string,\n create: boolean,\n ): Promise<string | undefined> {\n const folder = await this.folder(namespace, create);\n return folder ? path.join(folder, `${digest(name)}.secret`) : undefined;\n }\n\n private aad(namespace: string, name: string): Buffer {\n return Buffer.from(JSON.stringify([\"warlock:secrets:v1\", namespace, name]));\n }\n\n private async safe<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation();\n } catch (error) {\n if (error instanceof SecretStoreError) throw error;\n if (error instanceof Error && \"code\" in error && error.code === \"ELOOP\") {\n throw new SecretStoreError(\"UNSAFE_FILE\");\n }\n throw new SecretStoreError(\"UNAVAILABLE\");\n }\n }\n\n public async get(namespace: string, name: string): Promise<string | undefined> {\n return this.safe(async () => {\n const file = await this.location(namespace, name, false);\n if (!file) return undefined;\n let handle: FileHandle;\n try {\n handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);\n } catch (error) {\n if (isMissing(error)) return undefined;\n throw error;\n }\n let payload: Buffer;\n try {\n this.validateOwner(await handle.stat(), false);\n await handle.chmod(0o600);\n if (((await handle.stat()).mode & 0o777) !== 0o600) {\n throw new SecretStoreError(\"UNSAFE_FILE\");\n }\n payload = await handle.readFile();\n } finally {\n await handle.close();\n }\n const key = this.key(namespace);\n try {\n // v1 (one byte), 12-byte IV, 16-byte tag, then ciphertext (possibly empty).\n if (payload.length < 29 || payload[0] !== 1) throw new Error();\n const decipher = createDecipheriv(\"aes-256-gcm\", key, payload.subarray(1, 13));\n decipher.setAAD(this.aad(namespace, name));\n decipher.setAuthTag(payload.subarray(13, 29));\n return Buffer.concat([decipher.update(payload.subarray(29)), decipher.final()]).toString(\n \"utf8\",\n );\n } catch {\n throw new SecretStoreError(\"CORRUPT_FILE\");\n }\n });\n }\n\n public async set(namespace: string, name: string, value: string): Promise<void> {\n return this.safe(async () => {\n const key = this.key(namespace);\n const file = await this.location(namespace, name, true);\n if (!file) throw new SecretStoreError(\"UNAVAILABLE\");\n try {\n this.validateOwner(await lstat(file), false);\n } catch (error) {\n if (!isMissing(error)) throw error;\n }\n const iv = randomBytes(12);\n const cipher = createCipheriv(\"aes-256-gcm\", key, iv);\n cipher.setAAD(this.aad(namespace, name));\n const encrypted = Buffer.concat([cipher.update(value, \"utf8\"), cipher.final()]);\n const payload = Buffer.concat([Buffer.from([1]), iv, cipher.getAuthTag(), encrypted]);\n const temporary = `${file}.${randomBytes(16).toString(\"hex\")}.tmp`;\n const handle = await open(temporary, \"wx\", 0o600);\n try {\n try {\n await handle.chmod(0o600);\n this.validateOwner(await handle.stat(), false);\n if (((await handle.stat()).mode & 0o777) !== 0o600) {\n throw new SecretStoreError(\"UNSAFE_FILE\");\n }\n await handle.writeFile(payload);\n await handle.sync();\n } finally {\n await handle.close();\n }\n await rename(temporary, file);\n } finally {\n await unlink(temporary).catch((error: unknown) => {\n if (!isMissing(error)) throw error;\n });\n }\n });\n }\n\n public async remove(namespace: string, name: string): Promise<boolean> {\n return this.safe(async () => {\n const file = await this.location(namespace, name, false);\n if (!file) return false;\n try {\n this.validateOwner(await lstat(file), false);\n await unlink(file);\n return true;\n } catch (error) {\n if (isMissing(error)) return false;\n throw error;\n }\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"file-backend.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/file-backend.ts"],"sourcesContent":["import { createCipheriv, createDecipheriv, createHash, hkdfSync, randomBytes } from \"node:crypto\";\nimport { registerLogSecret } from \"@warlock.js/logger\";\nimport { constants, type Stats } from \"node:fs\";\nimport { lstat, mkdir, open, rename, unlink, type FileHandle } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { SecretStoreError } from \"./errors\";\nimport type { SecretBackend } from \"./types\";\n\nfunction digest(value: string): string {\n return createHash(\"sha256\").update(value).digest(\"hex\");\n}\n\nfunction isMissing(error: unknown): boolean {\n return error instanceof Error && \"code\" in error && error.code === \"ENOENT\";\n}\n\n/** One authenticated file per secret: concurrent writes to different keys cannot clobber each other. */\nexport class FileSecretBackend implements SecretBackend {\n public readonly type = \"file\";\n public constructor(\n private readonly directory: string,\n private readonly encryptionKey: () => string | undefined,\n ) {}\n\n private key(namespace: string): Buffer {\n const secret = this.encryptionKey();\n if (typeof secret !== \"string\" || !/^[a-f\\d]{64}$/i.test(secret)) {\n throw new SecretStoreError(\"KEY_REQUIRED\");\n }\n registerLogSecret(secret, [\"encryptionKey\"]);\n // Core's encrypt/decrypt helpers use global config directly. Derive a\n // separate AES-256-GCM key without changing that global config.\n // https://nodejs.org/docs/latest-v22.x/api/crypto.html#cryptohkdfsyncdigest-ikm-salt-info-keylen\n return Buffer.from(\n hkdfSync(\"sha256\", Buffer.from(secret, \"hex\"), namespace, \"warlock:secrets:v1\", 32),\n );\n }\n\n private validateOwner(stats: Stats, directory: boolean): void {\n if (\n (directory ? !stats.isDirectory() : !stats.isFile() || stats.nlink !== 1) ||\n (process.getuid && stats.uid !== process.getuid())\n ) {\n throw new SecretStoreError(\"UNSAFE_FILE\");\n }\n }\n\n private async folder(namespace: string, create: boolean): Promise<string | undefined> {\n // Node's Windows modes cannot enforce owner-only ACLs. Fail closed there.\n // https://nodejs.org/docs/latest-v22.x/api/fs.html#file-modes\n const root = path.resolve(this.directory);\n const folder = path.join(root, digest(namespace));\n for (const directory of [root, folder]) {\n if (!create) {\n try {\n await lstat(directory);\n } catch (error) {\n if (isMissing(error)) return undefined;\n throw error;\n }\n }\n if (process.platform === \"win32\") throw new SecretStoreError(\"UNSAFE_FILE\");\n if (create) await mkdir(directory, { recursive: true, mode: 0o700 });\n this.validateOwner(await lstat(directory), true);\n const handle = await open(directory, constants.O_RDONLY | constants.O_NOFOLLOW);\n try {\n this.validateOwner(await handle.stat(), true);\n await handle.chmod(0o700);\n if (((await handle.stat()).mode & 0o777) !== 0o700) {\n throw new SecretStoreError(\"UNSAFE_FILE\");\n }\n } finally {\n await handle.close();\n }\n }\n return folder;\n }\n\n private async location(\n namespace: string,\n name: string,\n create: boolean,\n ): Promise<string | undefined> {\n const folder = await this.folder(namespace, create);\n return folder ? path.join(folder, `${digest(name)}.secret`) : undefined;\n }\n\n private aad(namespace: string, name: string): Buffer {\n return Buffer.from(JSON.stringify([\"warlock:secrets:v1\", namespace, name]));\n }\n\n private async safe<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation();\n } catch (error) {\n if (error instanceof SecretStoreError) throw error;\n if (error instanceof Error && \"code\" in error && error.code === \"ELOOP\") {\n throw new SecretStoreError(\"UNSAFE_FILE\");\n }\n throw new SecretStoreError(\"UNAVAILABLE\");\n }\n }\n\n public async get(namespace: string, name: string): Promise<string | undefined> {\n return this.safe(async () => {\n const file = await this.location(namespace, name, false);\n if (!file) return undefined;\n let handle: FileHandle;\n try {\n handle = await open(file, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);\n } catch (error) {\n if (isMissing(error)) return undefined;\n throw error;\n }\n let payload: Buffer;\n try {\n this.validateOwner(await handle.stat(), false);\n await handle.chmod(0o600);\n if (((await handle.stat()).mode & 0o777) !== 0o600) {\n throw new SecretStoreError(\"UNSAFE_FILE\");\n }\n payload = await handle.readFile();\n } finally {\n await handle.close();\n }\n const key = this.key(namespace);\n try {\n // v1 (one byte), 12-byte IV, 16-byte tag, then ciphertext (possibly empty).\n if (payload.length < 29 || payload[0] !== 1) throw new Error();\n const decipher = createDecipheriv(\"aes-256-gcm\", key, payload.subarray(1, 13));\n decipher.setAAD(this.aad(namespace, name));\n decipher.setAuthTag(payload.subarray(13, 29));\n return Buffer.concat([decipher.update(payload.subarray(29)), decipher.final()]).toString(\n \"utf8\",\n );\n } catch {\n throw new SecretStoreError(\"CORRUPT_FILE\");\n }\n });\n }\n\n public async set(namespace: string, name: string, value: string): Promise<void> {\n return this.safe(async () => {\n const key = this.key(namespace);\n const file = await this.location(namespace, name, true);\n if (!file) throw new SecretStoreError(\"UNAVAILABLE\");\n try {\n this.validateOwner(await lstat(file), false);\n } catch (error) {\n if (!isMissing(error)) throw error;\n }\n const iv = randomBytes(12);\n const cipher = createCipheriv(\"aes-256-gcm\", key, iv);\n cipher.setAAD(this.aad(namespace, name));\n const encrypted = Buffer.concat([cipher.update(value, \"utf8\"), cipher.final()]);\n const payload = Buffer.concat([Buffer.from([1]), iv, cipher.getAuthTag(), encrypted]);\n const temporary = `${file}.${randomBytes(16).toString(\"hex\")}.tmp`;\n const handle = await open(temporary, \"wx\", 0o600);\n try {\n try {\n await handle.chmod(0o600);\n this.validateOwner(await handle.stat(), false);\n if (((await handle.stat()).mode & 0o777) !== 0o600) {\n throw new SecretStoreError(\"UNSAFE_FILE\");\n }\n await handle.writeFile(payload);\n await handle.sync();\n } finally {\n await handle.close();\n }\n await rename(temporary, file);\n } finally {\n await unlink(temporary).catch((error: unknown) => {\n if (!isMissing(error)) throw error;\n });\n }\n });\n }\n\n public async remove(namespace: string, name: string): Promise<boolean> {\n return this.safe(async () => {\n const file = await this.location(namespace, name, false);\n if (!file) return false;\n try {\n this.validateOwner(await lstat(file), false);\n await unlink(file);\n return true;\n } catch (error) {\n if (isMissing(error)) return false;\n throw error;\n }\n });\n }\n}\n"],"mappings":"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"}
|
package/esm/secrets/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { SecretStoreError, SecretStoreErrorCode } from "./errors.mjs";
|
|
2
|
-
import { SecretBackend, SecretStoreOptions, SecretsConfigurations } from "./types.mjs";
|
|
2
|
+
import { SecretBackend, SecretBackendType, SecretStoreOptions, SecretsConfigurations } from "./types.mjs";
|
|
3
3
|
import { MemorySecretBackend } from "./memory-backend.mjs";
|
|
4
4
|
import { SecretStore } from "./secret-store.mjs";
|
|
5
5
|
import { secrets } from "./secrets.mjs";
|
|
6
|
-
export { MemorySecretBackend, type SecretBackend, SecretStore, SecretStoreError, SecretStoreErrorCode, type SecretStoreOptions, type SecretsConfigurations, secrets };
|
|
6
|
+
export { MemorySecretBackend, type SecretBackend, type SecretBackendType, SecretStore, SecretStoreError, SecretStoreErrorCode, type SecretStoreOptions, type SecretsConfigurations, secrets };
|
|
@@ -4,6 +4,7 @@ import { SecretStoreError } from "./errors.mjs";
|
|
|
4
4
|
var KeyringSecretBackend = class KeyringSecretBackend {
|
|
5
5
|
constructor(keyring) {
|
|
6
6
|
this.keyring = keyring;
|
|
7
|
+
this.type = "keychain";
|
|
7
8
|
}
|
|
8
9
|
/** Read-only availability probe; never writes a setup credential. */
|
|
9
10
|
static async load(namespace, importer = importKeyring) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keyring-backend.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/keyring-backend.ts"],"sourcesContent":["import { SecretStoreError } from \"./errors\";\nimport type { SecretBackend } from \"./types\";\n\n// Verified against @napi-rs/keyring 2.1.1's published index.d.ts and README:\n// https://github.com/Brooooooklyn/keyring-node (2026-10-19).\n// Structural types keep the optional peer unnecessary for TypeScript consumers.\ninterface KeyringEntry {\n getPassword(): Promise<string | null>;\n setPassword(value: string): Promise<void>;\n deletePassword(): Promise<boolean>;\n}\n\ntype KeyringModule = {\n AsyncEntry: new (\n service: string,\n username: string,\n options: { linux: { store: \"secret-service\" } },\n ) => KeyringEntry;\n};\n\nexport class KeyringSecretBackend implements SecretBackend {\n private constructor(private readonly keyring: KeyringModule) {}\n\n /** Read-only availability probe; never writes a setup credential. */\n public static async load(\n namespace: string,\n importer: () => Promise<unknown> = importKeyring,\n ): Promise<KeyringSecretBackend | undefined> {\n try {\n const keyring = (await importer()) as KeyringModule;\n const backend = new KeyringSecretBackend(keyring);\n await backend.entry(namespace, \"\").getPassword();\n return backend;\n } catch {\n return undefined;\n }\n }\n\n private entry(namespace: string, name: string): KeyringEntry {\n // Require persistence on Linux: the library's automatic keyutils fallback\n // is memory-only and loses credentials on reboot. Ignored on other OSes.\n return new this.keyring.AsyncEntry(`warlock:${namespace}`, name || \"__availability__\", {\n linux: { store: \"secret-service\" },\n });\n }\n\n public async get(namespace: string, name: string): Promise<string | undefined> {\n try {\n return (await this.entry(namespace, name).getPassword()) ?? undefined;\n } catch {\n throw new SecretStoreError(\"UNAVAILABLE\");\n }\n }\n\n public async set(namespace: string, name: string, value: string): Promise<void> {\n try {\n await this.entry(namespace, name).setPassword(value);\n } catch {\n throw new SecretStoreError(\"UNAVAILABLE\");\n }\n }\n\n public async remove(namespace: string, name: string): Promise<boolean> {\n try {\n return await this.entry(namespace, name).deletePassword();\n } catch {\n throw new SecretStoreError(\"UNAVAILABLE\");\n }\n }\n}\n\nasync function importKeyring(): Promise<unknown> {\n const peer = \"@napi-rs/keyring\";\n return import(peer);\n}\n"],"mappings":";;;AAoBA,IAAa,uBAAb,MAAa,qBAA8C;
|
|
1
|
+
{"version":3,"file":"keyring-backend.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/keyring-backend.ts"],"sourcesContent":["import { SecretStoreError } from \"./errors\";\nimport type { SecretBackend } from \"./types\";\n\n// Verified against @napi-rs/keyring 2.1.1's published index.d.ts and README:\n// https://github.com/Brooooooklyn/keyring-node (2026-10-19).\n// Structural types keep the optional peer unnecessary for TypeScript consumers.\ninterface KeyringEntry {\n getPassword(): Promise<string | null>;\n setPassword(value: string): Promise<void>;\n deletePassword(): Promise<boolean>;\n}\n\ntype KeyringModule = {\n AsyncEntry: new (\n service: string,\n username: string,\n options: { linux: { store: \"secret-service\" } },\n ) => KeyringEntry;\n};\n\nexport class KeyringSecretBackend implements SecretBackend {\n public readonly type = \"keychain\";\n private constructor(private readonly keyring: KeyringModule) {}\n\n /** Read-only availability probe; never writes a setup credential. */\n public static async load(\n namespace: string,\n importer: () => Promise<unknown> = importKeyring,\n ): Promise<KeyringSecretBackend | undefined> {\n try {\n const keyring = (await importer()) as KeyringModule;\n const backend = new KeyringSecretBackend(keyring);\n await backend.entry(namespace, \"\").getPassword();\n return backend;\n } catch {\n return undefined;\n }\n }\n\n private entry(namespace: string, name: string): KeyringEntry {\n // Require persistence on Linux: the library's automatic keyutils fallback\n // is memory-only and loses credentials on reboot. Ignored on other OSes.\n return new this.keyring.AsyncEntry(`warlock:${namespace}`, name || \"__availability__\", {\n linux: { store: \"secret-service\" },\n });\n }\n\n public async get(namespace: string, name: string): Promise<string | undefined> {\n try {\n return (await this.entry(namespace, name).getPassword()) ?? undefined;\n } catch {\n throw new SecretStoreError(\"UNAVAILABLE\");\n }\n }\n\n public async set(namespace: string, name: string, value: string): Promise<void> {\n try {\n await this.entry(namespace, name).setPassword(value);\n } catch {\n throw new SecretStoreError(\"UNAVAILABLE\");\n }\n }\n\n public async remove(namespace: string, name: string): Promise<boolean> {\n try {\n return await this.entry(namespace, name).deletePassword();\n } catch {\n throw new SecretStoreError(\"UNAVAILABLE\");\n }\n }\n}\n\nasync function importKeyring(): Promise<unknown> {\n const peer = \"@napi-rs/keyring\";\n return import(peer);\n}\n"],"mappings":";;;AAoBA,IAAa,uBAAb,MAAa,qBAA8C;CAEzD,AAAQ,YAAY,AAAiB,SAAwB;EAAxB;cADd;CACuC;;CAG9D,aAAoB,KAClB,WACA,WAAmC,eACQ;EAC3C,IAAI;GACF,MAAM,UAAW,MAAM,SAAS;GAChC,MAAM,UAAU,IAAI,qBAAqB,OAAO;GAChD,MAAM,QAAQ,MAAM,WAAW,EAAE,CAAC,CAAC,YAAY;GAC/C,OAAO;EACT,QAAQ;GACN;EACF;CACF;CAEA,AAAQ,MAAM,WAAmB,MAA4B;EAG3D,OAAO,IAAI,KAAK,QAAQ,WAAW,WAAW,aAAa,QAAQ,oBAAoB,EACrF,OAAO,EAAE,OAAO,iBAAiB,EACnC,CAAC;CACH;CAEA,MAAa,IAAI,WAAmB,MAA2C;EAC7E,IAAI;GACF,OAAQ,MAAM,KAAK,MAAM,WAAW,IAAI,CAAC,CAAC,YAAY,KAAM;EAC9D,QAAQ;GACN,MAAM,IAAI,iBAAiB,aAAa;EAC1C;CACF;CAEA,MAAa,IAAI,WAAmB,MAAc,OAA8B;EAC9E,IAAI;GACF,MAAM,KAAK,MAAM,WAAW,IAAI,CAAC,CAAC,YAAY,KAAK;EACrD,QAAQ;GACN,MAAM,IAAI,iBAAiB,aAAa;EAC1C;CACF;CAEA,MAAa,OAAO,WAAmB,MAAgC;EACrE,IAAI;GACF,OAAO,MAAM,KAAK,MAAM,WAAW,IAAI,CAAC,CAAC,eAAe;EAC1D,QAAQ;GACN,MAAM,IAAI,iBAAiB,aAAa;EAC1C;CACF;AACF;AAEA,eAAe,gBAAkC;CAE/C,OAAO,OAAO;AAChB"}
|
|
@@ -2,6 +2,7 @@ import { SecretBackend } from "./types.mjs";
|
|
|
2
2
|
//#region ../../packages/core/src/secrets/memory-backend.d.ts
|
|
3
3
|
/** Isolated, namespace-aware storage. No OS, environment or filesystem access. */
|
|
4
4
|
export declare class MemorySecretBackend implements SecretBackend {
|
|
5
|
+
readonly type = "memory";
|
|
5
6
|
private readonly values;
|
|
6
7
|
get(namespace: string, name: string): Promise<string | undefined>;
|
|
7
8
|
set(namespace: string, name: string, value: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-backend.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/secrets/memory-backend.ts"],"mappings":";;;qBAGa,+BAA+B;
|
|
1
|
+
{"version":3,"file":"memory-backend.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/secrets/memory-backend.ts"],"mappings":";;;qBAGa,+BAA+B;WAC1B;mBACC;EAEJ,IAAI,mBAAmB,eAAe;EAItC,IAAI,mBAAmB,cAAc,gBAAgB;EASrD,OAAO,mBAAmB,eAAe;EAI/C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-backend.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/memory-backend.ts"],"sourcesContent":["import type { SecretBackend } from \"./types\";\n\n/** Isolated, namespace-aware storage. No OS, environment or filesystem access. */\nexport class MemorySecretBackend implements SecretBackend {\n private readonly values = new Map<string, Map<string, string>>();\n\n public async get(namespace: string, name: string): Promise<string | undefined> {\n return this.values.get(namespace)?.get(name);\n }\n\n public async set(namespace: string, name: string, value: string): Promise<void> {\n let entries = this.values.get(namespace);\n if (!entries) {\n entries = new Map();\n this.values.set(namespace, entries);\n }\n entries.set(name, value);\n }\n\n public async remove(namespace: string, name: string): Promise<boolean> {\n return this.values.get(namespace)?.delete(name) ?? false;\n }\n\n public clear(): void {\n this.values.clear();\n }\n}\n"],"mappings":";;AAGA,IAAa,sBAAb,MAA0D;;
|
|
1
|
+
{"version":3,"file":"memory-backend.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/memory-backend.ts"],"sourcesContent":["import type { SecretBackend } from \"./types\";\n\n/** Isolated, namespace-aware storage. No OS, environment or filesystem access. */\nexport class MemorySecretBackend implements SecretBackend {\n public readonly type = \"memory\";\n private readonly values = new Map<string, Map<string, string>>();\n\n public async get(namespace: string, name: string): Promise<string | undefined> {\n return this.values.get(namespace)?.get(name);\n }\n\n public async set(namespace: string, name: string, value: string): Promise<void> {\n let entries = this.values.get(namespace);\n if (!entries) {\n entries = new Map();\n this.values.set(namespace, entries);\n }\n entries.set(name, value);\n }\n\n public async remove(namespace: string, name: string): Promise<boolean> {\n return this.values.get(namespace)?.delete(name) ?? false;\n }\n\n public clear(): void {\n this.values.clear();\n }\n}\n"],"mappings":";;AAGA,IAAa,sBAAb,MAA0D;;cACjC;gCACG,IAAI,IAAiC;;CAE/D,MAAa,IAAI,WAAmB,MAA2C;EAC7E,OAAO,KAAK,OAAO,IAAI,SAAS,CAAC,EAAE,IAAI,IAAI;CAC7C;CAEA,MAAa,IAAI,WAAmB,MAAc,OAA8B;EAC9E,IAAI,UAAU,KAAK,OAAO,IAAI,SAAS;EACvC,IAAI,CAAC,SAAS;GACZ,0BAAU,IAAI,IAAI;GAClB,KAAK,OAAO,IAAI,WAAW,OAAO;EACpC;EACA,QAAQ,IAAI,MAAM,KAAK;CACzB;CAEA,MAAa,OAAO,WAAmB,MAAgC;EACrE,OAAO,KAAK,OAAO,IAAI,SAAS,CAAC,EAAE,OAAO,IAAI,KAAK;CACrD;CAEA,AAAO,QAAc;EACnB,KAAK,OAAO,MAAM;CACpB;AACF"}
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
-
import { SecretStoreOptions } from "./types.mjs";
|
|
1
|
+
import { SecretBackendType, SecretStoreOptions } from "./types.mjs";
|
|
2
2
|
//#region ../../packages/core/src/secrets/secret-store.d.ts
|
|
3
3
|
/** Lazily prepared, application-scoped credentials. All operations are asynchronous. */
|
|
4
4
|
export declare class SecretStore {
|
|
5
5
|
private primary;
|
|
6
6
|
private readonly file;
|
|
7
|
-
private readonly
|
|
7
|
+
private readonly selectedBackend;
|
|
8
8
|
readonly namespace: string;
|
|
9
9
|
constructor(options: SecretStoreOptions);
|
|
10
10
|
private validate;
|
|
11
11
|
/** Exact, collision-free environment variable name (UTF-8 encoded as uppercase hex). */
|
|
12
12
|
environmentName(name: string): string;
|
|
13
13
|
private keyring;
|
|
14
|
+
/** Resolve the active store using the same memoized probe as credential operations. */
|
|
15
|
+
backend(): Promise<SecretBackendType>;
|
|
14
16
|
private register;
|
|
15
17
|
private environment;
|
|
16
18
|
private assertWritable;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-store.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secret-store.ts"],"mappings":";;;qBAWa;UACH;mBACS;mBACA;WACD;EAEhB,YAAmB,SAAS;
|
|
1
|
+
{"version":3,"file":"secret-store.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secret-store.ts"],"mappings":";;;qBAWa;UACH;mBACS;mBACA;WACD;EAEhB,YAAmB,SAAS;UAqBpB;;EAYD,gBAAgB;UAMf;;EAMK,WAAW,QAAQ;UAWxB;UAIA;UAIA;UAQM;EAUD,IAAI,eAAe;EAkBnB,IAAI,eAAe;EAInB,IAAI,cAAc,gBAAgB;;EA2BlC,OAAO,eAAe"}
|
|
@@ -14,7 +14,7 @@ var SecretStore = class {
|
|
|
14
14
|
if (typeof options.namespace !== "string" || !options.namespace.trim() || Buffer.from(options.namespace).toString("utf8") !== options.namespace) throw new SecretStoreError("INVALID_NAMESPACE");
|
|
15
15
|
this.namespace = options.namespace;
|
|
16
16
|
const driver = options.backend ? "auto" : resolveSecretsDriver(options);
|
|
17
|
-
this.
|
|
17
|
+
this.selectedBackend = options.backend ?? (driver === "memory" ? new MemorySecretBackend() : void 0);
|
|
18
18
|
const encryptionKey = options.encryptionKey;
|
|
19
19
|
this.file = new FileSecretBackend(options.directory ?? storagePath("secrets"), typeof encryptionKey === "function" ? encryptionKey : () => encryptionKey ?? config.key("encryption.key"));
|
|
20
20
|
}
|
|
@@ -31,6 +31,17 @@ var SecretStore = class {
|
|
|
31
31
|
this.primary ??= KeyringSecretBackend.load(this.namespace);
|
|
32
32
|
return this.primary;
|
|
33
33
|
}
|
|
34
|
+
/** Resolve the active store using the same memoized probe as credential operations. */
|
|
35
|
+
async backend() {
|
|
36
|
+
return this.safe(async () => {
|
|
37
|
+
if (this.selectedBackend) {
|
|
38
|
+
const type = this.selectedBackend.type ?? "memory";
|
|
39
|
+
if (type === "keychain" || type === "file" || type === "memory") return type;
|
|
40
|
+
throw new SecretStoreError("UNAVAILABLE");
|
|
41
|
+
}
|
|
42
|
+
return await this.keyring() ? "keychain" : "file";
|
|
43
|
+
});
|
|
44
|
+
}
|
|
34
45
|
register(name, value) {
|
|
35
46
|
registerLogSecret(value ?? "", [name, this.environmentName(name)]);
|
|
36
47
|
}
|
|
@@ -57,7 +68,7 @@ var SecretStore = class {
|
|
|
57
68
|
this.register(name);
|
|
58
69
|
return this.safe(async () => {
|
|
59
70
|
let value;
|
|
60
|
-
if (this.
|
|
71
|
+
if (this.selectedBackend) value = await this.selectedBackend.get(this.namespace, name);
|
|
61
72
|
else {
|
|
62
73
|
value = await (await this.keyring())?.get(this.namespace, name);
|
|
63
74
|
value ??= this.environment(name);
|
|
@@ -75,10 +86,10 @@ var SecretStore = class {
|
|
|
75
86
|
if (typeof value !== "string") throw new SecretStoreError("INVALID_VALUE");
|
|
76
87
|
this.register(name, value);
|
|
77
88
|
return this.safe(async () => {
|
|
78
|
-
if (this.
|
|
79
|
-
const previous = await this.
|
|
89
|
+
if (this.selectedBackend) {
|
|
90
|
+
const previous = await this.selectedBackend.get(this.namespace, name);
|
|
80
91
|
if (previous !== void 0) this.register(name, previous);
|
|
81
|
-
return this.
|
|
92
|
+
return this.selectedBackend.set(this.namespace, name, value);
|
|
82
93
|
}
|
|
83
94
|
this.assertWritable(name);
|
|
84
95
|
const primary = await this.keyring();
|
|
@@ -98,10 +109,10 @@ var SecretStore = class {
|
|
|
98
109
|
this.validate(name);
|
|
99
110
|
this.register(name);
|
|
100
111
|
return this.safe(async () => {
|
|
101
|
-
if (this.
|
|
102
|
-
const previous = await this.
|
|
112
|
+
if (this.selectedBackend) {
|
|
113
|
+
const previous = await this.selectedBackend.get(this.namespace, name);
|
|
103
114
|
if (previous !== void 0) this.register(name, previous);
|
|
104
|
-
return this.
|
|
115
|
+
return this.selectedBackend.remove(this.namespace, name);
|
|
105
116
|
}
|
|
106
117
|
this.assertWritable(name);
|
|
107
118
|
const primary = await this.keyring();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secret-store.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secret-store.ts"],"sourcesContent":["import { registerLogSecret } from \"@warlock.js/logger\";\nimport { config } from \"../config/config-getter\";\nimport { storagePath } from \"../utils/paths\";\nimport { SecretStoreError } from \"./errors\";\nimport { FileSecretBackend } from \"./file-backend\";\nimport { MemorySecretBackend } from \"./memory-backend\";\nimport { resolveSecretsDriver } from \"./resolve-driver\";\nimport { KeyringSecretBackend } from \"./keyring-backend\";\nimport type { SecretBackend, SecretStoreOptions } from \"./types\";\n\n/** Lazily prepared, application-scoped credentials. All operations are asynchronous. */\nexport class SecretStore {\n private primary: Promise<KeyringSecretBackend | undefined> | undefined;\n private readonly file: FileSecretBackend;\n private readonly backend: SecretBackend | undefined;\n public readonly namespace: string;\n\n public constructor(options: SecretStoreOptions) {\n if (\n typeof options.namespace !== \"string\" ||\n !options.namespace.trim() ||\n Buffer.from(options.namespace).toString(\"utf8\") !== options.namespace\n ) {\n throw new SecretStoreError(\"INVALID_NAMESPACE\");\n }\n this.namespace = options.namespace;\n const driver = options.backend ? \"auto\" : resolveSecretsDriver(options);\n this.backend = options.backend ?? (driver === \"memory\" ? new MemorySecretBackend() : undefined);\n const encryptionKey = options.encryptionKey;\n this.file = new FileSecretBackend(\n options.directory ?? storagePath(\"secrets\"),\n typeof encryptionKey === \"function\"\n ? encryptionKey\n : () => encryptionKey ?? config.key<string | undefined>(\"encryption.key\"),\n );\n }\n\n private validate(name: string): void {\n if (\n typeof name !== \"string\" ||\n !name.length ||\n name.length > 256 ||\n Buffer.from(name).toString(\"utf8\") !== name\n ) {\n throw new SecretStoreError(\"INVALID_NAME\");\n }\n }\n\n /** Exact, collision-free environment variable name (UTF-8 encoded as uppercase hex). */\n public environmentName(name: string): string {\n this.validate(name);\n const hex = (value: string) => Buffer.from(value).toString(\"hex\").toUpperCase();\n return `WARLOCK_SECRET_${hex(this.namespace)}_${hex(name)}`;\n }\n\n private keyring(): Promise<KeyringSecretBackend | undefined> {\n this.primary ??= KeyringSecretBackend.load(this.namespace);\n return this.primary;\n }\n\n private register(name: string, value?: string): void {\n registerLogSecret(value ?? \"\", [name, this.environmentName(name)]);\n }\n\n private environment(name: string): string | undefined {\n return process.env[this.environmentName(name)];\n }\n\n private assertWritable(name: string): void {\n const value = this.environment(name);\n if (value !== undefined) {\n this.register(name, value);\n throw new SecretStoreError(\"READ_ONLY\");\n }\n }\n\n private async safe<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation();\n } catch (error) {\n if (error instanceof SecretStoreError) throw error;\n // Custom backends may throw messages containing values. Do not propagate them.\n throw new SecretStoreError(\"UNAVAILABLE\");\n }\n }\n\n public async get(name: string): Promise<string | undefined> {\n this.validate(name);\n this.register(name);\n return this.safe(async () => {\n let value: string | undefined;\n if (this.backend) {\n value = await this.backend.get(this.namespace, name);\n } else {\n const primary = await this.keyring();\n value = await primary?.get(this.namespace, name);\n value ??= this.environment(name);\n value ??= await this.file.get(this.namespace, name);\n }\n if (value !== undefined) this.register(name, value);\n return value;\n });\n }\n\n public async has(name: string): Promise<boolean> {\n return (await this.get(name)) !== undefined;\n }\n\n public async set(name: string, value: string): Promise<void> {\n this.validate(name);\n if (typeof value !== \"string\") throw new SecretStoreError(\"INVALID_VALUE\");\n // Register before any backend call, including failed writes.\n this.register(name, value);\n return this.safe(async () => {\n if (this.backend) {\n const previous = await this.backend.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n return this.backend.set(this.namespace, name, value);\n }\n this.assertWritable(name);\n const primary = await this.keyring();\n const backend = primary ?? this.file;\n if (primary) {\n const fallback = await this.file.get(this.namespace, name);\n if (fallback !== undefined) this.register(name, fallback);\n }\n // Retain masking for the old value after replacement as well.\n const previous = await backend.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n await backend.set(this.namespace, name, value);\n if (primary) await this.file.remove(this.namespace, name);\n });\n }\n\n /** Remove every writable layer so a fallback value cannot reappear. */\n public async remove(name: string): Promise<boolean> {\n this.validate(name);\n this.register(name);\n return this.safe(async () => {\n if (this.backend) {\n const previous = await this.backend.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n return this.backend.remove(this.namespace, name);\n }\n this.assertWritable(name);\n const primary = await this.keyring();\n const previous = await primary?.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n const fromFile = await this.file.get(this.namespace, name);\n if (fromFile !== undefined) this.register(name, fromFile);\n const removed = (await primary?.remove(this.namespace, name)) ?? false;\n return (await this.file.remove(this.namespace, name)) || removed;\n });\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"secret-store.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secret-store.ts"],"sourcesContent":["import { registerLogSecret } from \"@warlock.js/logger\";\nimport { config } from \"../config/config-getter\";\nimport { storagePath } from \"../utils/paths\";\nimport { SecretStoreError } from \"./errors\";\nimport { FileSecretBackend } from \"./file-backend\";\nimport { MemorySecretBackend } from \"./memory-backend\";\nimport { resolveSecretsDriver } from \"./resolve-driver\";\nimport { KeyringSecretBackend } from \"./keyring-backend\";\nimport type { SecretBackend, SecretBackendType, SecretStoreOptions } from \"./types\";\n\n/** Lazily prepared, application-scoped credentials. All operations are asynchronous. */\nexport class SecretStore {\n private primary: Promise<KeyringSecretBackend | undefined> | undefined;\n private readonly file: FileSecretBackend;\n private readonly selectedBackend: SecretBackend | undefined;\n public readonly namespace: string;\n\n public constructor(options: SecretStoreOptions) {\n if (\n typeof options.namespace !== \"string\" ||\n !options.namespace.trim() ||\n Buffer.from(options.namespace).toString(\"utf8\") !== options.namespace\n ) {\n throw new SecretStoreError(\"INVALID_NAMESPACE\");\n }\n this.namespace = options.namespace;\n const driver = options.backend ? \"auto\" : resolveSecretsDriver(options);\n this.selectedBackend =\n options.backend ?? (driver === \"memory\" ? new MemorySecretBackend() : undefined);\n const encryptionKey = options.encryptionKey;\n this.file = new FileSecretBackend(\n options.directory ?? storagePath(\"secrets\"),\n typeof encryptionKey === \"function\"\n ? encryptionKey\n : () => encryptionKey ?? config.key<string | undefined>(\"encryption.key\"),\n );\n }\n\n private validate(name: string): void {\n if (\n typeof name !== \"string\" ||\n !name.length ||\n name.length > 256 ||\n Buffer.from(name).toString(\"utf8\") !== name\n ) {\n throw new SecretStoreError(\"INVALID_NAME\");\n }\n }\n\n /** Exact, collision-free environment variable name (UTF-8 encoded as uppercase hex). */\n public environmentName(name: string): string {\n this.validate(name);\n const hex = (value: string) => Buffer.from(value).toString(\"hex\").toUpperCase();\n return `WARLOCK_SECRET_${hex(this.namespace)}_${hex(name)}`;\n }\n\n private keyring(): Promise<KeyringSecretBackend | undefined> {\n this.primary ??= KeyringSecretBackend.load(this.namespace);\n return this.primary;\n }\n\n /** Resolve the active store using the same memoized probe as credential operations. */\n public async backend(): Promise<SecretBackendType> {\n return this.safe(async () => {\n if (this.selectedBackend) {\n const type = this.selectedBackend.type ?? \"memory\";\n if (type === \"keychain\" || type === \"file\" || type === \"memory\") return type;\n throw new SecretStoreError(\"UNAVAILABLE\");\n }\n return (await this.keyring()) ? \"keychain\" : \"file\";\n });\n }\n\n private register(name: string, value?: string): void {\n registerLogSecret(value ?? \"\", [name, this.environmentName(name)]);\n }\n\n private environment(name: string): string | undefined {\n return process.env[this.environmentName(name)];\n }\n\n private assertWritable(name: string): void {\n const value = this.environment(name);\n if (value !== undefined) {\n this.register(name, value);\n throw new SecretStoreError(\"READ_ONLY\");\n }\n }\n\n private async safe<T>(operation: () => Promise<T>): Promise<T> {\n try {\n return await operation();\n } catch (error) {\n if (error instanceof SecretStoreError) throw error;\n // Custom backends may throw messages containing values. Do not propagate them.\n throw new SecretStoreError(\"UNAVAILABLE\");\n }\n }\n\n public async get(name: string): Promise<string | undefined> {\n this.validate(name);\n this.register(name);\n return this.safe(async () => {\n let value: string | undefined;\n if (this.selectedBackend) {\n value = await this.selectedBackend.get(this.namespace, name);\n } else {\n const primary = await this.keyring();\n value = await primary?.get(this.namespace, name);\n value ??= this.environment(name);\n value ??= await this.file.get(this.namespace, name);\n }\n if (value !== undefined) this.register(name, value);\n return value;\n });\n }\n\n public async has(name: string): Promise<boolean> {\n return (await this.get(name)) !== undefined;\n }\n\n public async set(name: string, value: string): Promise<void> {\n this.validate(name);\n if (typeof value !== \"string\") throw new SecretStoreError(\"INVALID_VALUE\");\n // Register before any backend call, including failed writes.\n this.register(name, value);\n return this.safe(async () => {\n if (this.selectedBackend) {\n const previous = await this.selectedBackend.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n return this.selectedBackend.set(this.namespace, name, value);\n }\n this.assertWritable(name);\n const primary = await this.keyring();\n const backend = primary ?? this.file;\n if (primary) {\n const fallback = await this.file.get(this.namespace, name);\n if (fallback !== undefined) this.register(name, fallback);\n }\n // Retain masking for the old value after replacement as well.\n const previous = await backend.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n await backend.set(this.namespace, name, value);\n if (primary) await this.file.remove(this.namespace, name);\n });\n }\n\n /** Remove every writable layer so a fallback value cannot reappear. */\n public async remove(name: string): Promise<boolean> {\n this.validate(name);\n this.register(name);\n return this.safe(async () => {\n if (this.selectedBackend) {\n const previous = await this.selectedBackend.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n return this.selectedBackend.remove(this.namespace, name);\n }\n this.assertWritable(name);\n const primary = await this.keyring();\n const previous = await primary?.get(this.namespace, name);\n if (previous !== undefined) this.register(name, previous);\n const fromFile = await this.file.get(this.namespace, name);\n if (fromFile !== undefined) this.register(name, fromFile);\n const removed = (await primary?.remove(this.namespace, name)) ?? false;\n return (await this.file.remove(this.namespace, name)) || removed;\n });\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { SecretBackendType } from "./types.mjs";
|
|
1
2
|
//#region ../../packages/core/src/secrets/secrets.d.ts
|
|
2
3
|
/** App identity and configuration are resolved on first use, after config has loaded. */
|
|
3
4
|
export declare const secrets: {
|
|
5
|
+
backend(): Promise<SecretBackendType>;
|
|
4
6
|
get(name: string): Promise<string | undefined>;
|
|
5
7
|
set(name: string, value: string): Promise<void>;
|
|
6
8
|
remove(name: string): Promise<boolean>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secrets.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secrets.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"secrets.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secrets.ts"],"mappings":";;;qBA6Ca;EACL,WAAW,QAAQ;EAGnB,IAAG,eAAgB;EAGnB,IAAG,cAAa,gBAAkB;EAGlC,OAAM,eAAgB;EAGtB,IAAG,eAAgB;EAGzB,gBAAe"}
|
package/esm/secrets/secrets.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"secrets.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secrets.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport path from \"node:path\";\nimport { config } from \"../config/config-getter\";\nimport { rootPath, storagePath } from \"../utils/paths\";\nimport { resolveSecretsDriver } from \"./resolve-driver\";\nimport { SecretStore } from \"./secret-store\";\nimport type { SecretBackend, SecretsConfigurations } from \"./types\";\n\nlet testBackend: SecretBackend | undefined;\nlet stores = new Map<string, SecretStore>();\n\n/** @internal The test kit swaps only the facade, never explicitly constructed stores. */\nexport function replaceSecretsBackend(backend: SecretBackend): () => void {\n const previous = testBackend;\n const previousStores = stores;\n testBackend = backend;\n stores = new Map();\n return () => {\n testBackend = previous;\n stores = previousStores;\n };\n}\n\nfunction current(): SecretStore {\n const root = rootPath();\n const appName = config.key<string | undefined>(\"app.appName\") || path.basename(root);\n const namespace =\n config.key<string | undefined>(\"secrets.namespace\") ??\n `${appName}:${createHash(\"sha256\").update(root).digest(\"hex\")}`;\n const directory = config.key<string | undefined>(\"secrets.directory\") ?? storagePath(\"secrets\");\n const options: SecretsConfigurations = {\n driver: config.key<SecretsConfigurations[\"driver\"]>(\"secrets.driver\"),\n allowMemoryInProduction: config.key<boolean | undefined>(\"secrets.allowMemoryInProduction\"),\n };\n const driver = testBackend ? \"auto\" : resolveSecretsDriver(options);\n const identity = JSON.stringify([root, namespace, directory, driver]);\n let store = stores.get(identity);\n if (!store) {\n store = new SecretStore({ ...options, driver, namespace, directory, backend: testBackend });\n stores.set(identity, store);\n }\n return store;\n}\n\n/** App identity and configuration are resolved on first use, after config has loaded. */\nexport const secrets = {\n async get(name: string): Promise<string | undefined> {\n return current().get(name);\n },\n async set(name: string, value: string): Promise<void> {\n return current().set(name, value);\n },\n async remove(name: string): Promise<boolean> {\n return current().remove(name);\n },\n async has(name: string): Promise<boolean> {\n return current().has(name);\n },\n environmentName(name: string): string {\n return current().environmentName(name);\n },\n};\n"],"mappings":";;;;;;;;AAQA,IAAI;AACJ,IAAI,yBAAS,IAAI,IAAyB;;AAG1C,SAAgB,sBAAsB,SAAoC;CACxE,MAAM,WAAW;CACjB,MAAM,iBAAiB;CACvB,cAAc;CACd,yBAAS,IAAI,IAAI;CACjB,aAAa;EACX,cAAc;EACd,SAAS;CACX;AACF;AAEA,SAAS,UAAuB;CAC9B,MAAM,OAAO,SAAS;CACtB,MAAM,UAAU,OAAO,IAAwB,aAAa,KAAK,KAAK,SAAS,IAAI;CACnF,MAAM,YACJ,OAAO,IAAwB,mBAAmB,KAClD,GAAG,QAAQ,GAAG,WAAW,QAAQ,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,KAAK;CAC9D,MAAM,YAAY,OAAO,IAAwB,mBAAmB,KAAK,YAAY,SAAS;CAC9F,MAAM,UAAiC;EACrC,QAAQ,OAAO,IAAqC,gBAAgB;EACpE,yBAAyB,OAAO,IAAyB,iCAAiC;CAC5F;CACA,MAAM,SAAS,cAAc,SAAS,qBAAqB,OAAO;CAClE,MAAM,WAAW,KAAK,UAAU;EAAC;EAAM;EAAW;EAAW;CAAM,CAAC;CACpE,IAAI,QAAQ,OAAO,IAAI,QAAQ;CAC/B,IAAI,CAAC,OAAO;EACV,QAAQ,IAAI,YAAY;GAAE,GAAG;GAAS;GAAQ;GAAW;GAAW,SAAS;EAAY,CAAC;EAC1F,OAAO,IAAI,UAAU,KAAK;CAC5B;CACA,OAAO;AACT;;AAGA,MAAa,UAAU;CACrB,MAAM,IAAI,MAA2C;EACnD,OAAO,QAAQ,CAAC,CAAC,IAAI,IAAI;CAC3B;CACA,MAAM,IAAI,MAAc,OAA8B;EACpD,OAAO,QAAQ,CAAC,CAAC,IAAI,MAAM,KAAK;CAClC;CACA,MAAM,OAAO,MAAgC;EAC3C,OAAO,QAAQ,CAAC,CAAC,OAAO,IAAI;CAC9B;CACA,MAAM,IAAI,MAAgC;EACxC,OAAO,QAAQ,CAAC,CAAC,IAAI,IAAI;CAC3B;CACA,gBAAgB,MAAsB;EACpC,OAAO,QAAQ,CAAC,CAAC,gBAAgB,IAAI;CACvC;AACF"}
|
|
1
|
+
{"version":3,"file":"secrets.mjs","names":[],"sources":["../../../../../../../../packages/core/src/secrets/secrets.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport path from \"node:path\";\nimport { config } from \"../config/config-getter\";\nimport { rootPath, storagePath } from \"../utils/paths\";\nimport { resolveSecretsDriver } from \"./resolve-driver\";\nimport { SecretStore } from \"./secret-store\";\nimport type { SecretBackend, SecretBackendType, SecretsConfigurations } from \"./types\";\n\nlet testBackend: SecretBackend | undefined;\nlet stores = new Map<string, SecretStore>();\n\n/** @internal The test kit swaps only the facade, never explicitly constructed stores. */\nexport function replaceSecretsBackend(backend: SecretBackend): () => void {\n const previous = testBackend;\n const previousStores = stores;\n testBackend = backend;\n stores = new Map();\n return () => {\n testBackend = previous;\n stores = previousStores;\n };\n}\n\nfunction current(): SecretStore {\n const root = rootPath();\n const appName = config.key<string | undefined>(\"app.appName\") || path.basename(root);\n const namespace =\n config.key<string | undefined>(\"secrets.namespace\") ??\n `${appName}:${createHash(\"sha256\").update(root).digest(\"hex\")}`;\n const directory = config.key<string | undefined>(\"secrets.directory\") ?? storagePath(\"secrets\");\n const options: SecretsConfigurations = {\n driver: config.key<SecretsConfigurations[\"driver\"]>(\"secrets.driver\"),\n allowMemoryInProduction: config.key<boolean | undefined>(\"secrets.allowMemoryInProduction\"),\n };\n const driver = testBackend ? \"auto\" : resolveSecretsDriver(options);\n const identity = JSON.stringify([root, namespace, directory, driver]);\n let store = stores.get(identity);\n if (!store) {\n store = new SecretStore({ ...options, driver, namespace, directory, backend: testBackend });\n stores.set(identity, store);\n }\n return store;\n}\n\n/** App identity and configuration are resolved on first use, after config has loaded. */\nexport const secrets = {\n async backend(): Promise<SecretBackendType> {\n return current().backend();\n },\n async get(name: string): Promise<string | undefined> {\n return current().get(name);\n },\n async set(name: string, value: string): Promise<void> {\n return current().set(name, value);\n },\n async remove(name: string): Promise<boolean> {\n return current().remove(name);\n },\n async has(name: string): Promise<boolean> {\n return current().has(name);\n },\n environmentName(name: string): string {\n return current().environmentName(name);\n },\n};\n"],"mappings":";;;;;;;;AAQA,IAAI;AACJ,IAAI,yBAAS,IAAI,IAAyB;;AAG1C,SAAgB,sBAAsB,SAAoC;CACxE,MAAM,WAAW;CACjB,MAAM,iBAAiB;CACvB,cAAc;CACd,yBAAS,IAAI,IAAI;CACjB,aAAa;EACX,cAAc;EACd,SAAS;CACX;AACF;AAEA,SAAS,UAAuB;CAC9B,MAAM,OAAO,SAAS;CACtB,MAAM,UAAU,OAAO,IAAwB,aAAa,KAAK,KAAK,SAAS,IAAI;CACnF,MAAM,YACJ,OAAO,IAAwB,mBAAmB,KAClD,GAAG,QAAQ,GAAG,WAAW,QAAQ,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,KAAK;CAC9D,MAAM,YAAY,OAAO,IAAwB,mBAAmB,KAAK,YAAY,SAAS;CAC9F,MAAM,UAAiC;EACrC,QAAQ,OAAO,IAAqC,gBAAgB;EACpE,yBAAyB,OAAO,IAAyB,iCAAiC;CAC5F;CACA,MAAM,SAAS,cAAc,SAAS,qBAAqB,OAAO;CAClE,MAAM,WAAW,KAAK,UAAU;EAAC;EAAM;EAAW;EAAW;CAAM,CAAC;CACpE,IAAI,QAAQ,OAAO,IAAI,QAAQ;CAC/B,IAAI,CAAC,OAAO;EACV,QAAQ,IAAI,YAAY;GAAE,GAAG;GAAS;GAAQ;GAAW;GAAW,SAAS;EAAY,CAAC;EAC1F,OAAO,IAAI,UAAU,KAAK;CAC5B;CACA,OAAO;AACT;;AAGA,MAAa,UAAU;CACrB,MAAM,UAAsC;EAC1C,OAAO,QAAQ,CAAC,CAAC,QAAQ;CAC3B;CACA,MAAM,IAAI,MAA2C;EACnD,OAAO,QAAQ,CAAC,CAAC,IAAI,IAAI;CAC3B;CACA,MAAM,IAAI,MAAc,OAA8B;EACpD,OAAO,QAAQ,CAAC,CAAC,IAAI,MAAM,KAAK;CAClC;CACA,MAAM,OAAO,MAAgC;EAC3C,OAAO,QAAQ,CAAC,CAAC,OAAO,IAAI;CAC9B;CACA,MAAM,IAAI,MAAgC;EACxC,OAAO,QAAQ,CAAC,CAAC,IAAI,IAAI;CAC3B;CACA,gBAAgB,MAAsB;EACpC,OAAO,QAAQ,CAAC,CAAC,gBAAgB,IAAI;CACvC;AACF"}
|
package/esm/secrets/types.d.mts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
//#region ../../packages/core/src/secrets/types.d.ts
|
|
2
|
+
/** Active storage selection, independent of the source of an individual secret. */
|
|
3
|
+
export type SecretBackendType = "keychain" | "file" | "memory";
|
|
2
4
|
/** Backend implementations must never log credentials or attach them to errors. */
|
|
3
5
|
export interface SecretBackend {
|
|
6
|
+
/** Report injected storage; omitted metadata defaults to memory for test overrides. */
|
|
7
|
+
readonly type?: SecretBackendType;
|
|
4
8
|
get(namespace: string, name: string): Promise<string | undefined>;
|
|
5
9
|
set(namespace: string, name: string, value: string): Promise<void>;
|
|
6
10
|
remove(namespace: string, name: string): Promise<boolean>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/secrets/types.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../../packages/core/src/secrets/types.ts"],"mappings":";;YACY;;iBAGK;;WAEN,OAAO;EAChB,IAAI,mBAAmB,eAAe;EACtC,IAAI,mBAAmB,cAAc,gBAAgB;EACrD,OAAO,mBAAmB,eAAe;;YAG/B;;EAEV;;EAEA;;EAEA;;EAEA;;YAGU,qBAAqB;EAC/B;;EAEA,UAAU;;EAEV"}
|
package/llms-full.txt
CHANGED
|
@@ -6491,7 +6491,7 @@ Client `PUT`s the bytes straight to R2 — your server never sees them.
|
|
|
6491
6491
|
|
|
6492
6492
|
---
|
|
6493
6493
|
name: store-secrets
|
|
6494
|
-
description: "Store app credentials with secrets.get/set/remove/has and environmentName; lazy optional OS keychain, read-only scoped environment variables, encrypted file fallback, typed SecretStoreError and memory-backed secretsFake. Use for provider keys and OAuth credentials the app must read back. Skip user passwords (hash-password) and encrypted database columns (encrypt-data)."
|
|
6494
|
+
description: "Store app credentials with secrets.get/set/remove/has, backend() and environmentName; lazy optional OS keychain, read-only scoped environment variables, encrypted file fallback, typed SecretStoreError and memory-backed secretsFake. Use for provider keys and OAuth credentials the app must read back. Skip user passwords (hash-password) and encrypted database columns (encrypt-data)."
|
|
6495
6495
|
---
|
|
6496
6496
|
|
|
6497
6497
|
# Application secret store
|
|
@@ -6507,6 +6507,7 @@ const token = await secrets.get("vendor-token"); // string | undefined
|
|
|
6507
6507
|
const exists = await secrets.has("vendor-token"); // boolean; true for ""
|
|
6508
6508
|
const removed = await secrets.remove("vendor-token"); // boolean
|
|
6509
6509
|
const variable = secrets.environmentName("vendor-token"); // variable name, not value
|
|
6510
|
+
const backend = await secrets.backend(); // "keychain" | "file" | "memory"; no credentials
|
|
6510
6511
|
|
|
6511
6512
|
try {
|
|
6512
6513
|
await secrets.remove("vendor-token");
|
|
@@ -6523,6 +6524,23 @@ Names are case-sensitive, nonempty strings of at most 256 UTF-16 code units. Val
|
|
|
6523
6524
|
|
|
6524
6525
|
## Configuration and backend decisions
|
|
6525
6526
|
|
|
6527
|
+
Use `await secrets.backend()` or `await store.backend()` to inspect active storage.
|
|
6528
|
+
The exported `SecretBackendType` is `"keychain" | "file" | "memory"`. Inspection
|
|
6529
|
+
triggers core's own lazy selection and shares the memoized keychain probe with
|
|
6530
|
+
credential operations, including concurrent calls; never run an independent probe.
|
|
6531
|
+
It returns the selected backend, not the source of an individual secret. Scoped
|
|
6532
|
+
environment reads do not change it. File selection does not create files or verify
|
|
6533
|
+
the encryption key/filesystem. A later locked keychain remains `"keychain"` and
|
|
6534
|
+
credential operations fail safely instead of silently changing selection.
|
|
6535
|
+
|
|
6536
|
+
Resolved `secrets.driver` overrides `SECRETS_DRIVER`: memory reports `"memory"`;
|
|
6537
|
+
auto reports `"keychain"` after a successful initial probe or `"file"` after failure.
|
|
6538
|
+
`secretsFake` and explicit `MemorySecretBackend` report `"memory"` without probing.
|
|
6539
|
+
Explicit backends bypass driver configuration. Custom `SecretBackend` implementations
|
|
6540
|
+
may declare readonly `type: SecretBackendType`; omitted metadata reports `"memory"`
|
|
6541
|
+
for compatibility with isolated injected test backends. Core file/keychain instances
|
|
6542
|
+
declare their own type.
|
|
6543
|
+
|
|
6526
6544
|
- Default namespace: app name plus the SHA-256 hash of the absolute project root. For changing release directories, configure a stable unique namespace including the deployment environment.
|
|
6527
6545
|
- Reads: OS keychain → exact scoped environment variable → encrypted file.
|
|
6528
6546
|
- Optional peer: `npm install @napi-rs/keyring`. Core lazily uses its 2.1.1 `AsyncEntry` API. Linux requires `secret-service` for persistence; it does not use the memory-only keyutils fallback. Sources: [official README](https://github.com/Brooooooklyn/keyring-node#readme), [published declarations](https://www.npmjs.com/package/@napi-rs/keyring?activeTab=code), verified 2026-10-19.
|
|
@@ -6575,6 +6593,7 @@ import type { SecretBackend, SecretStoreOptions } from "@warlock.js/core/secrets
|
|
|
6575
6593
|
const backend: SecretBackend = new MemorySecretBackend();
|
|
6576
6594
|
const options: SecretStoreOptions = { namespace: "test-app", backend };
|
|
6577
6595
|
const store = new SecretStore(options);
|
|
6596
|
+
await store.backend(); // "memory"
|
|
6578
6597
|
await store.set("vendor-token", "test-credential");
|
|
6579
6598
|
await store.get("vendor-token");
|
|
6580
6599
|
await store.has("vendor-token");
|
|
@@ -12551,7 +12570,7 @@ CLI commands, scheduled jobs, queue workers — anything running outside an HTTP
|
|
|
12551
12570
|
|
|
12552
12571
|
---
|
|
12553
12572
|
name: write-cli-command
|
|
12554
|
-
description: 'Author a custom `warlock <my-cmd>` command via the `command()` factory — name, description, action, options, preload, then register in `warlock.config.ts > cli.commands` or drop in `src/app/<module>/commands/`. Also covers built-in `warlock add` feature scaffolding, including the Web starter and `index.
|
|
12573
|
+
description: 'Author a custom `warlock <my-cmd>` command via the `command()` factory — name, description, action, options, preload, then register in `warlock.config.ts > cli.commands` or drop in `src/app/<module>/commands/`. Also covers built-in `warlock add` feature scaffolding, including the Web starter and `index.setup.ts`. Triggers: `command`, `CLICommand`, `CLICommandPreload`, `CLICommandOption`, `preload`, `preAction`, `persistent`, `colors`, `warlock add`, `warlock add bull-board`, `index.setup.ts`; "write a custom warlock command", "one-off maintenance task", "ship a CLI from a package", "framework built-in commands"; typical import `import { command } from "@warlock.js/core"`. Skip: framework dev/build/start — the `run-app` topic; warlock.config.ts wiring — the `configure-app` topic; competing libs `commander`, `yargs`, `oclif`.'
|
|
12555
12574
|
---
|
|
12556
12575
|
|
|
12557
12576
|
# Warlock — write a CLI command
|
|
@@ -12781,7 +12800,11 @@ Run `warlock add --list` to see what's currently registered. Pass `--packageMana
|
|
|
12781
12800
|
is loaded automatically by `warlock dev` when installed, and is omitted from a
|
|
12782
12801
|
production install with dev dependencies excluded.
|
|
12783
12802
|
|
|
12784
|
-
|
|
12803
|
+
`warlock add web` creates `root.tsx` + `root.setup.ts` and `index.page.tsx` +
|
|
12804
|
+
`index.setup.ts`. Setup files own configuration and universal `register()`
|
|
12805
|
+
logic; UI files own components. Warlock composes the pairs on the server and
|
|
12806
|
+
projects browser-safe registration to the client, preserving the component
|
|
12807
|
+
Fast Refresh boundary.
|
|
12785
12808
|
|
|
12786
12809
|
### AI input measurement
|
|
12787
12810
|
|
package/llms.txt
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
- [send-mail](@warlock.js/core/send-mail/SKILL.md): Send transactional email — `Mail` fluent builder, `sendMail()` direct call, React Email components. Test mode auto-captures into an in-memory mailbox; dev mode logs. Triggers: `Mail.to`, `sendMail`, `setMailMode`, `mailEvents`, `assertMailSent`, `getTestMailbox`, `wasMailSentTo`, `closeAllMailers`; "send a transactional email", "build a React Email template", "configure SMTP or SES", "assert an email was sent in tests"; typical import `import { Mail, sendMail } from "@warlock.js/core"`. Skip: per-config wiring — the `configure-app` topic; layered service patterns — the `warlock-conventions` topic; competing libs `nodemailer` direct, `@sendgrid/mail`, `resend`, `mailgun.js`.
|
|
32
32
|
- [send-response](@warlock.js/core/send-response/SKILL.md): Send HTTP responses via @warlock.js/core's Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `retainEvents`, `retainChunks`, `backpressure`, `@warlock.js/core/errors`, `ResourceNotFoundError`, `ForbiddenError`, `request.cookie`, `request.hasCookie`, `CookieJarUnavailableError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — the `create-controller` topic; route registration — the `register-route` topic; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.
|
|
33
33
|
- [store-file](@warlock.js/core/store-file/SKILL.md): Read/write/delete files via the `storage` singleton — disks, drivers (local/S3/R2/DO Spaces), `storage.use(name)`, `StorageFile` handles, presigned URLs. Triggers: `storage.put`, `storage.get`, `storage.use`, `StorageFile`, `storageConfigurations`, `getPresignedUrl`, `getPresignedUploadUrl`; "save an uploaded file", "switch between local and S3", "generate a presigned URL", "read file metadata"; typical import `import { storage } from "@warlock.js/core"`. Skip: multipart parsing + image chain — the `upload-file` topic; image transforms — the `process-image` topic; storage config shape — the `configure-app` topic; competing libs `@aws-sdk/client-s3`, `multer`, `formidable`.
|
|
34
|
-
- [store-secrets](@warlock.js/core/store-secrets/SKILL.md): Store app credentials with secrets.get/set/remove/has and environmentName; lazy optional OS keychain, read-only scoped environment variables, encrypted file fallback, typed SecretStoreError and memory-backed secretsFake. Use for provider keys and OAuth credentials the app must read back. Skip user passwords (hash-password) and encrypted database columns (encrypt-data).
|
|
34
|
+
- [store-secrets](@warlock.js/core/store-secrets/SKILL.md): Store app credentials with secrets.get/set/remove/has, backend() and environmentName; lazy optional OS keychain, read-only scoped environment variables, encrypted file fallback, typed SecretStoreError and memory-backed secretsFake. Use for provider keys and OAuth credentials the app must read back. Skip user passwords (hash-password) and encrypted database columns (encrypt-data).
|
|
35
35
|
- [test-browser](@warlock.js/core/test-browser/SKILL.md): Write or repair browser e2e specs using warlock e2e and @warlock.js/core/e2e, with owned app fixtures, direct CDP interaction, responsive screenshots and run evidence. Use for real browser flows, click races or viewport overflow; use test-http for API-only tests.
|
|
36
36
|
- [test-fakes](@warlock.js/core/test-fakes/SKILL.md): The test fakes Warlock installs by default in the test environment — `mailFake`, `queueFake`, `cacheFake`, `storageFake`, `clockFake`, `idsFake`, `secretsFake` — and their assertions (`assertSent`, `assertPushed`, `assertHas`, `assertExists`, `travel`), plus how to opt out (`tests.fakes`, `setupTest({ fakes })`). Triggers: `mailFake`, `queueFake`, `cacheFake`, `storageFake`, `clockFake`, `idsFake`, `secretsFake`, `installTestFakes`, `restoreTestFakes`, `tests.fakes`, `FakeAssertionError`, `clock.now`, `ids.uuid`, `setDispatchInterceptor`; "assert an email was sent", "assert a job was dispatched", "freeze time in a test", "deterministic ids", "tests without Redis", "keep the real cache driver in tests". Typical import `import { mailFake, queueFake } from "@warlock.js/core/tests"`. Skip: HTTP request helpers — the `test-http` topic; fixtures — the `model-factories` topic.
|
|
37
37
|
- [test-http](@warlock.js/core/test-http/SKILL.md): Integration tests against a real HTTP server — `startHttpTestServer()` boots one shared server in globalSetup, then `testGet` / `testPost` / `expectJson` make typed requests against it. Triggers: `startHttpTestServer`, `startHttpTestServer({ port })`, `startHttpTestServer({ port: 0 })`, `stopHttpTestServer`, `createTestHttpServer`, `TestHttpServer`, `createTestRequestClient`, `snapshotResponse`, `actingAs`, `AuthenticatedTestClient`, `testGet`, `testPost`, `testPut`, `testPatch`, `testDelete`, `expectJson`, `getTestServerUrl`, `testRequest`, `PortInUseError`, `assertPortIsAvailable`, `isPortAvailable`; "integration-test a controller", "end-to-end HTTP test", "globalSetup HTTP server", "assert status and body shape", "test server port already in use", "EADDRINUSE while running tests", "run tests while the dev server is up", "ephemeral test port", "test with the real Fastify plugins", "assert mail sent by a request", "response snapshot"; typical import `import { testGet, testPost, expectJson } from "@warlock.js/core/tests"`. Skip: pure unit tests — the `test-service` topic; controller shape — the `create-controller` topic; competing libs `supertest`, `light-my-request`, `nock`.
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
- [warlock-routes](@warlock.js/core/warlock-routes/SKILL.md): Run `warlock routes` — a read-only command that lists the registered HTTP routes as a verb-colored table (method / path / name / action / middleware-count / source), a sibling of `warlock doctor`. Filter with `--method` / `--path` / `--name`, or emit normalized rows as JSON with `--json`. Also covers `warlock routes:diff`, which compares live page routes against the last `warlock build`'s route snapshot and exits non-zero on drift. Triggers: `warlock routes`, `routesCommand`, `warlock routes:diff`, `routesDiffCommand`, "list my routes", "show all routes", "route table", "what endpoints does my app expose", "dump routes as JSON", "which routes have middleware", "route map for CI", "did my page routes drift from the last build"; run as `npx warlock routes` / `npx warlock routes:diff`. Skip: read-only health/preflight checks — the `warlock-doctor` topic; defining/naming/grouping routes — the `register-route` topic; authoring a general CLI command — the `write-cli-command` topic; competing tools `nest`/`express` route listers, `php artisan route:list`.
|
|
57
57
|
- [warlock-trace](@warlock.js/core/warlock-trace/SKILL.md): Verify runtime behaviour without a browser: `warlock trace <method> <url> [--body json|@file] [--header k:v] [--json]` sends one request to the running `warlock dev` server and prints what devtools recorded for it — middleware chain with timings, validation result and errors, handler time, queries with N+1 flags, logs, cache operations, captured mails, the response and timings — as a versioned report (`schemaVersion: 1`); `traceRequest()` returns the same object. Triggers: `warlock trace`, `traceRequest`, `TraceReport`, `buildTraceReport`, `x-request-id`, `.warlock/dev-server.json`, `WL8008`, `WL8009`, `WL5004`, `--id-header`; "did validation pass for this request", "which queries does this endpoint run", "is there an N+1 on this route", "what middleware runs on this route", "check the endpoint after my change", "request timeline as JSON". Skip: the devtools dashboard in a browser — the `devtools-overview` skill of @warlock.js/devtools; static route facts (guard, rate limit, schemas) — the `warlock-inspect` topic; typecheck/lint/tests — the `warlock-check` topic.
|
|
58
58
|
- [wire-socket](@warlock.js/core/wire-socket/SKILL.md): Configure Socket.IO via `src/config/socket.ts`, reach the live server through `getSocketServer()` (or `app.socket` post-bootstrap), register `connection` handlers once the late-phase socket connector has booted, emit from controllers/services, use rooms and namespaces. Triggers: `app.socket`, `getSocketServer`, `SocketOptions`, `socket.io` `Server`, `socket.join`, `socket.to`, `io.of`, `io.use`; "add realtime chat", "emit socket events from a service", "use rooms and namespaces", "per-socket JWT auth". Skip: connector lifecycle — the `add-connector` topic; app context accessors — the `use-app-context` topic; competing libs `ws`, `socket.io` direct without Warlock connector, `uWebSockets.js`.
|
|
59
|
-
- [write-cli-command](@warlock.js/core/write-cli-command/SKILL.md): Author a custom `warlock <my-cmd>` command via the `command()` factory — name, description, action, options, preload, then register in `warlock.config.ts > cli.commands` or drop in `src/app/<module>/commands/`. Also covers built-in `warlock add` feature scaffolding, including the Web starter and `index.
|
|
59
|
+
- [write-cli-command](@warlock.js/core/write-cli-command/SKILL.md): Author a custom `warlock <my-cmd>` command via the `command()` factory — name, description, action, options, preload, then register in `warlock.config.ts > cli.commands` or drop in `src/app/<module>/commands/`. Also covers built-in `warlock add` feature scaffolding, including the Web starter and `index.setup.ts`. Triggers: `command`, `CLICommand`, `CLICommandPreload`, `CLICommandOption`, `preload`, `preAction`, `persistent`, `colors`, `warlock add`, `warlock add bull-board`, `index.setup.ts`; "write a custom warlock command", "one-off maintenance task", "ship a CLI from a package", "framework built-in commands"; typical import `import { command } from "@warlock.js/core"`. Skip: framework dev/build/start — the `run-app` topic; warlock.config.ts wiring — the `configure-app` topic; competing libs `commander`, `yargs`, `oclif`.
|
|
60
60
|
- [write-middleware](@warlock.js/core/write-middleware/SKILL.md): Author HTTP middleware for @warlock.js/core — the `({ request, response })` signature, short-circuit by returning a response, enrich the request with extra fields, register per-route, per-group, or app-wide. Triggers: `Middleware`, `MiddlewareResponse`, `router.group`, `guarded`, `request.detectIp`, `authMiddleware`; "write a custom middleware", "short-circuit a request", "enrich the request with extra fields", "per-route vs per-group middleware"; typical import `import type { Middleware } from "@warlock.js/core"`. Skip: built-in middleware catalog — the `use-middleware` topic; route attachment — the `register-route` topic; response helpers — the `send-response` topic; competing patterns: `express` `(req, res, next)` middleware, Fastify `preHandler` hooks.
|
|
61
61
|
- [write-seeder](@warlock.js/core/write-seeder/SKILL.md): Author a seed file under `src/app/<module>/seeds/<name>.ts` using the `seeder()` factory — `name`, `dependsOn`, `once`, `order`, `batchSize`, `run({ track, now, batchSize })`. Auto-discovered by `warlock seed`; tracked in a `seeds` table; per-record refs in `seed_records` so `warlock seed --drop` can undo a seed. Triggers: `seeder`, `Seeder`, `SeedResult`, `SeedContext`, `SeedClock`, `track`, `now`, `batchSize`, `SeedersManager`, `warlock seed`, `--fresh`, `--drop`, `--list`, `--path`; "seed default roles", "undo a seed", "one-time data migration", "auto-discovered seeds", "order seeds by dependency", "deterministic seed timestamps", "inject a seed clock"; typical import `import { seeder } from "@warlock.js/core"`. Skip: module folder layout — the `create-module` topic; repository CRUD — the `use-repository` topic; CLI flags — the `write-cli-command` topic; competing patterns: hand-rolled `node scripts/seed.js`, `typeorm-seeding`.
|
|
62
62
|
- [write-use-case](@warlock.js/core/write-use-case/SKILL.md): Author `useCase()` pipelines for business logic — guards, schema, before/after middleware, retry, benchmark, broadcast, lifecycle callbacks; transport-agnostic and observable by default. Input is inferred from the `schema`. Triggers: `useCase`, `UseCaseContext`, `UseCaseResult`, `retry`, `benchmark`, `broadcast`, `description`, `globalUseCasesEvents`, `UseCaseBroadcastChannel`; "encapsulate a business operation", "share logic between HTTP and CLI", "add guards and lifecycle hooks", "broadcast a use case result", "transport-agnostic pipeline"; typical import `import { useCase } from "@warlock.js/core"`. Skip: thin handler shape — the `create-controller` topic; schema details — the `validate-input` topic; the standalone retry util — the `retry-operation` topic; competing libs `@nestjs/cqrs`, `inversify`, hand-rolled service classes.
|
package/package.json
CHANGED
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"@mongez/slug": "^1.0.7",
|
|
26
26
|
"@mongez/supportive-is": "^3.0.0",
|
|
27
27
|
"@mongez/time-wizard": "^1.0.6",
|
|
28
|
-
"@warlock.js/cache": "5.34.
|
|
29
|
-
"@warlock.js/cascade": "5.34.
|
|
30
|
-
"@warlock.js/context": "5.34.
|
|
31
|
-
"@warlock.js/fs": "5.34.
|
|
32
|
-
"@warlock.js/logger": "5.34.
|
|
33
|
-
"@warlock.js/seal": "5.34.
|
|
28
|
+
"@warlock.js/cache": "5.34.1",
|
|
29
|
+
"@warlock.js/cascade": "5.34.1",
|
|
30
|
+
"@warlock.js/context": "5.34.1",
|
|
31
|
+
"@warlock.js/fs": "5.34.1",
|
|
32
|
+
"@warlock.js/logger": "5.34.1",
|
|
33
|
+
"@warlock.js/seal": "5.34.1",
|
|
34
34
|
"bcryptjs": "^3.0.3",
|
|
35
35
|
"chokidar": "^5.0.0",
|
|
36
36
|
"dayjs": "^1.11.19",
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
"@aws-sdk/lib-storage": "^3.955.0",
|
|
56
56
|
"@aws-sdk/s3-request-presigner": "^3.955.0",
|
|
57
57
|
"@react-email/render": "^2.0.5",
|
|
58
|
-
"@warlock.js/access": "5.34.
|
|
59
|
-
"@warlock.js/ai": "5.34.
|
|
60
|
-
"@warlock.js/ai-tools": "5.34.
|
|
61
|
-
"@warlock.js/devtools": "5.34.
|
|
62
|
-
"@warlock.js/herald": "5.34.
|
|
63
|
-
"@warlock.js/notifications": "5.34.
|
|
58
|
+
"@warlock.js/access": "5.34.1",
|
|
59
|
+
"@warlock.js/ai": "5.34.1",
|
|
60
|
+
"@warlock.js/ai-tools": "5.34.1",
|
|
61
|
+
"@warlock.js/devtools": "5.34.1",
|
|
62
|
+
"@warlock.js/herald": "5.34.1",
|
|
63
|
+
"@warlock.js/notifications": "5.34.1",
|
|
64
64
|
"nodemailer": "^8.0.5",
|
|
65
65
|
"react": "^19.2.3",
|
|
66
66
|
"react-dom": "^19.2.3",
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
"engines": {
|
|
146
146
|
"node": ">=22.15"
|
|
147
147
|
},
|
|
148
|
-
"version": "5.34.
|
|
148
|
+
"version": "5.34.1",
|
|
149
149
|
"type": "module",
|
|
150
150
|
"main": "./esm/index.mjs",
|
|
151
151
|
"module": "./esm/index.mjs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: store-secrets
|
|
3
|
-
description: "Store app credentials with secrets.get/set/remove/has and environmentName; lazy optional OS keychain, read-only scoped environment variables, encrypted file fallback, typed SecretStoreError and memory-backed secretsFake. Use for provider keys and OAuth credentials the app must read back. Skip user passwords (hash-password) and encrypted database columns (encrypt-data)."
|
|
3
|
+
description: "Store app credentials with secrets.get/set/remove/has, backend() and environmentName; lazy optional OS keychain, read-only scoped environment variables, encrypted file fallback, typed SecretStoreError and memory-backed secretsFake. Use for provider keys and OAuth credentials the app must read back. Skip user passwords (hash-password) and encrypted database columns (encrypt-data)."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Application secret store
|
|
@@ -16,6 +16,7 @@ const token = await secrets.get("vendor-token"); // string | undefined
|
|
|
16
16
|
const exists = await secrets.has("vendor-token"); // boolean; true for ""
|
|
17
17
|
const removed = await secrets.remove("vendor-token"); // boolean
|
|
18
18
|
const variable = secrets.environmentName("vendor-token"); // variable name, not value
|
|
19
|
+
const backend = await secrets.backend(); // "keychain" | "file" | "memory"; no credentials
|
|
19
20
|
|
|
20
21
|
try {
|
|
21
22
|
await secrets.remove("vendor-token");
|
|
@@ -32,6 +33,23 @@ Names are case-sensitive, nonempty strings of at most 256 UTF-16 code units. Val
|
|
|
32
33
|
|
|
33
34
|
## Configuration and backend decisions
|
|
34
35
|
|
|
36
|
+
Use `await secrets.backend()` or `await store.backend()` to inspect active storage.
|
|
37
|
+
The exported `SecretBackendType` is `"keychain" | "file" | "memory"`. Inspection
|
|
38
|
+
triggers core's own lazy selection and shares the memoized keychain probe with
|
|
39
|
+
credential operations, including concurrent calls; never run an independent probe.
|
|
40
|
+
It returns the selected backend, not the source of an individual secret. Scoped
|
|
41
|
+
environment reads do not change it. File selection does not create files or verify
|
|
42
|
+
the encryption key/filesystem. A later locked keychain remains `"keychain"` and
|
|
43
|
+
credential operations fail safely instead of silently changing selection.
|
|
44
|
+
|
|
45
|
+
Resolved `secrets.driver` overrides `SECRETS_DRIVER`: memory reports `"memory"`;
|
|
46
|
+
auto reports `"keychain"` after a successful initial probe or `"file"` after failure.
|
|
47
|
+
`secretsFake` and explicit `MemorySecretBackend` report `"memory"` without probing.
|
|
48
|
+
Explicit backends bypass driver configuration. Custom `SecretBackend` implementations
|
|
49
|
+
may declare readonly `type: SecretBackendType`; omitted metadata reports `"memory"`
|
|
50
|
+
for compatibility with isolated injected test backends. Core file/keychain instances
|
|
51
|
+
declare their own type.
|
|
52
|
+
|
|
35
53
|
- Default namespace: app name plus the SHA-256 hash of the absolute project root. For changing release directories, configure a stable unique namespace including the deployment environment.
|
|
36
54
|
- Reads: OS keychain → exact scoped environment variable → encrypted file.
|
|
37
55
|
- Optional peer: `npm install @napi-rs/keyring`. Core lazily uses its 2.1.1 `AsyncEntry` API. Linux requires `secret-service` for persistence; it does not use the memory-only keyutils fallback. Sources: [official README](https://github.com/Brooooooklyn/keyring-node#readme), [published declarations](https://www.npmjs.com/package/@napi-rs/keyring?activeTab=code), verified 2026-10-19.
|
|
@@ -84,6 +102,7 @@ import type { SecretBackend, SecretStoreOptions } from "@warlock.js/core/secrets
|
|
|
84
102
|
const backend: SecretBackend = new MemorySecretBackend();
|
|
85
103
|
const options: SecretStoreOptions = { namespace: "test-app", backend };
|
|
86
104
|
const store = new SecretStore(options);
|
|
105
|
+
await store.backend(); // "memory"
|
|
87
106
|
await store.set("vendor-token", "test-credential");
|
|
88
107
|
await store.get("vendor-token");
|
|
89
108
|
await store.has("vendor-token");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: write-cli-command
|
|
3
|
-
description: 'Author a custom `warlock <my-cmd>` command via the `command()` factory — name, description, action, options, preload, then register in `warlock.config.ts > cli.commands` or drop in `src/app/<module>/commands/`. Also covers built-in `warlock add` feature scaffolding, including the Web starter and `index.
|
|
3
|
+
description: 'Author a custom `warlock <my-cmd>` command via the `command()` factory — name, description, action, options, preload, then register in `warlock.config.ts > cli.commands` or drop in `src/app/<module>/commands/`. Also covers built-in `warlock add` feature scaffolding, including the Web starter and `index.setup.ts`. Triggers: `command`, `CLICommand`, `CLICommandPreload`, `CLICommandOption`, `preload`, `preAction`, `persistent`, `colors`, `warlock add`, `warlock add bull-board`, `index.setup.ts`; "write a custom warlock command", "one-off maintenance task", "ship a CLI from a package", "framework built-in commands"; typical import `import { command } from "@warlock.js/core"`. Skip: framework dev/build/start — the `run-app` topic; warlock.config.ts wiring — the `configure-app` topic; competing libs `commander`, `yargs`, `oclif`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Warlock — write a CLI command
|
|
@@ -230,7 +230,11 @@ Run `warlock add --list` to see what's currently registered. Pass `--packageMana
|
|
|
230
230
|
is loaded automatically by `warlock dev` when installed, and is omitted from a
|
|
231
231
|
production install with dev dependencies excluded.
|
|
232
232
|
|
|
233
|
-
|
|
233
|
+
`warlock add web` creates `root.tsx` + `root.setup.ts` and `index.page.tsx` +
|
|
234
|
+
`index.setup.ts`. Setup files own configuration and universal `register()`
|
|
235
|
+
logic; UI files own components. Warlock composes the pairs on the server and
|
|
236
|
+
projects browser-safe registration to the client, preserving the component
|
|
237
|
+
Fast Refresh boundary.
|
|
234
238
|
|
|
235
239
|
### AI input measurement
|
|
236
240
|
|