tina4-nodejs 3.13.141 → 3.13.143

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.
Files changed (67) hide show
  1. package/CLAUDE.md +28 -2
  2. package/package.json +1 -1
  3. package/packages/cli/dist/bin.js +817 -538
  4. package/packages/cli/src/bin.ts +4 -14
  5. package/packages/cli/src/commands/build.ts +2 -18
  6. package/packages/cli/src/commands/generate.ts +9 -24
  7. package/packages/cli/src/commands/init.ts +18 -8
  8. package/packages/cli/src/commands/lint.ts +3 -13
  9. package/packages/cli/src/commands/migrate.ts +2 -6
  10. package/packages/cli/src/commands/migrateRollback.ts +2 -6
  11. package/packages/cli/src/commands/migrateStatus.ts +2 -6
  12. package/packages/cli/src/commands/queue.ts +2 -30
  13. package/packages/cli/src/util.ts +90 -0
  14. package/packages/core/dist/index.js +592 -262
  15. package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +27 -11
  16. package/packages/core/gallery/queue/src/lib/queueDb.ts +11 -0
  17. package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +2 -5
  18. package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +2 -5
  19. package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +20 -7
  20. package/packages/core/gallery/templates/src/templates/gallery_page.twig +34 -15
  21. package/packages/core/src/csp.ts +138 -0
  22. package/packages/core/src/devAdmin.ts +3 -12
  23. package/packages/core/src/dispatchPipeline.ts +2 -18
  24. package/packages/core/src/errorOverlay.ts +80 -37
  25. package/packages/core/src/etag.ts +24 -0
  26. package/packages/core/src/feedback.ts +2 -11
  27. package/packages/core/src/index.ts +10 -0
  28. package/packages/core/src/middleware.ts +20 -6
  29. package/packages/core/src/response.ts +14 -3
  30. package/packages/core/src/server.ts +52 -15
  31. package/packages/core/src/session.ts +174 -71
  32. package/packages/core/src/sessionHandlers/respSessionHandler.ts +94 -0
  33. package/packages/core/src/sessionHandlers/valkeyHandler.ts +22 -73
  34. package/packages/core/src/sso.ts +1 -1
  35. package/packages/core/src/static.ts +1 -13
  36. package/packages/core/src/types.ts +8 -0
  37. package/packages/core/src/upstreamResponse.ts +31 -0
  38. package/packages/core/src/websocket.ts +48 -7
  39. package/packages/core/templates/errors/302.twig +1 -1
  40. package/packages/core/templates/errors/401.twig +2 -2
  41. package/packages/core/templates/errors/403.twig +1 -1
  42. package/packages/core/templates/errors/404.twig +1 -1
  43. package/packages/core/templates/errors/500.twig +1 -1
  44. package/packages/core/templates/errors/502.twig +2 -2
  45. package/packages/core/templates/errors/503.twig +2 -2
  46. package/packages/core/templates/errors/base.twig +4 -1
  47. package/packages/frond/dist/index.js +2 -0
  48. package/packages/frond/src/engine.ts +23 -0
  49. package/packages/orm/dist/index.js +593 -271
  50. package/packages/orm/src/graph/adapters/bolt.ts +8 -29
  51. package/packages/orm/src/graph/adapters/rowMapping.ts +57 -0
  52. package/packages/orm/src/graph/adapters/ultipa.ts +8 -30
  53. package/packages/swagger/dist/index.js +4 -4
  54. package/packages/swagger/src/ui.ts +7 -4
  55. package/types/cli/src/util.d.ts +28 -0
  56. package/types/core/src/csp.d.ts +42 -0
  57. package/types/core/src/etag.d.ts +9 -0
  58. package/types/core/src/index.d.ts +1 -0
  59. package/types/core/src/middleware.d.ts +6 -0
  60. package/types/core/src/session.d.ts +64 -21
  61. package/types/core/src/sessionHandlers/respSessionHandler.d.ts +52 -0
  62. package/types/core/src/sessionHandlers/valkeyHandler.d.ts +2 -26
  63. package/types/core/src/types.d.ts +8 -0
  64. package/types/core/src/upstreamResponse.d.ts +10 -0
  65. package/types/core/src/websocket.d.ts +21 -0
  66. package/types/frond/src/engine.d.ts +7 -0
  67. package/types/orm/src/graph/adapters/rowMapping.d.ts +26 -0
@@ -20,8 +20,9 @@ import { runSeeds } from "./commands/seed.js";
20
20
  import { queueCommand, QUEUE_SUBCOMMAND_NAMES } from "./commands/queue.js";
21
21
  import { buildImage } from "./commands/build.js";
22
22
  import { spawnSync } from "node:child_process";
23
- import { existsSync, readFileSync, statSync } from "node:fs";
24
- import { delimiter, dirname, join } from "node:path";
23
+ import { existsSync, readFileSync } from "node:fs";
24
+ import { dirname, join } from "node:path";
25
+ import { findOnPath } from "./util.js";
25
26
  import { fileURLToPath, pathToFileURL } from "node:url";
26
27
 
27
28
  // ── Version (cheap, side-effect-free) ───────────────────────────────
@@ -496,18 +497,7 @@ function findClient(): string | null {
496
497
  const names = windows
497
498
  ? [`${CLIENT_BINARY}.exe`, `${CLIENT_BINARY}.cmd`, `${CLIENT_BINARY}.bat`]
498
499
  : [CLIENT_BINARY];
499
- for (const dir of (process.env.PATH ?? "").split(delimiter)) {
500
- if (!dir) continue;
501
- for (const name of names) {
502
- const candidate = join(dir, name);
503
- try {
504
- if (statSync(candidate).isFile()) return candidate;
505
- } catch {
506
- // not there — keep looking
507
- }
508
- }
509
- }
510
- return null;
500
+ return findOnPath(names);
511
501
  }
512
502
 
513
503
  /**
@@ -24,27 +24,11 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
24
24
  import { accessSync, constants, existsSync, statSync } from "node:fs";
25
25
  import { basename, delimiter, join } from "node:path";
26
26
  import { spawnSync } from "node:child_process";
27
+ import { parseFlags as parseFlagsShared } from "../util.js";
27
28
 
28
29
  /** Parse --key value / bare --flag (build only needs --tag and --file values). */
29
30
  function parseFlags(args: string[]): Record<string, string | boolean> {
30
- const flags: Record<string, string | boolean> = {};
31
- let i = 0;
32
- while (i < args.length) {
33
- const arg = args[i];
34
- if (arg.startsWith("--")) {
35
- const key = arg.slice(2);
36
- if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
37
- flags[key] = args[i + 1];
38
- i += 2;
39
- } else {
40
- flags[key] = true;
41
- i += 1;
42
- }
43
- } else {
44
- i += 1;
45
- }
46
- }
47
- return flags;
31
+ return parseFlagsShared(args).flags;
48
32
  }
49
33
 
50
34
  /**
@@ -42,6 +42,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
42
42
  */
43
43
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
44
44
  import { join, relative, resolve, sep } from "node:path";
45
+ import { parseFlags } from "../util.js";
45
46
 
46
47
  // ── Field type mapping ──────────────────────────────────────────────
47
48
  const FIELD_TYPE_MAP: Record<string, { orm: string; sql: string; defaultVal: string }> = {
@@ -558,28 +559,7 @@ export function parseCliArgs(args: string[]): { flags: Record<string, string | b
558
559
  "json", "dry-run",
559
560
  ]);
560
561
 
561
- const flags: Record<string, string | boolean> = {};
562
- const positional: string[] = [];
563
- let i = 0;
564
- while (i < args.length) {
565
- if (args[i].startsWith("--")) {
566
- const key = args[i].slice(2);
567
- if (booleanFlags.has(key)) {
568
- flags[key] = true;
569
- i += 1;
570
- } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
571
- flags[key] = args[i + 1];
572
- i += 2;
573
- } else {
574
- flags[key] = true;
575
- i += 1;
576
- }
577
- } else {
578
- positional.push(args[i]);
579
- i += 1;
580
- }
581
- }
582
- return { flags, positional };
562
+ return parseFlags(args, booleanFlags);
583
563
  }
584
564
 
585
565
  /**
@@ -1870,7 +1850,11 @@ export default async function (req: Tina4Request, res: Tina4Response) {
1870
1850
  `{% extends "base.twig" %}
1871
1851
  {% block title %}Login{% endblock %}
1872
1852
  {% block content %}
1873
- <div class="container mt-4" style="max-width:400px">
1853
+ {# No inline style attribute: a nonce lets this <style> block run under the
1854
+ strict default Content-Security-Policy. csp_nonce() is a built-in Frond global
1855
+ that returns this response's nonce (ADR-0088). #}
1856
+ <style nonce="{{ csp_nonce() }}">.auth-form{max-width:400px;margin:0 auto}</style>
1857
+ <div class="container mt-4 auth-form">
1874
1858
  <h1>Login</h1>
1875
1859
  <form method="post" action="/api/auth/login">
1876
1860
  {{ form_token() }}
@@ -1896,7 +1880,8 @@ export default async function (req: Tina4Request, res: Tina4Response) {
1896
1880
  `{% extends "base.twig" %}
1897
1881
  {% block title %}Register{% endblock %}
1898
1882
  {% block content %}
1899
- <div class="container mt-4" style="max-width:400px">
1883
+ <style nonce="{{ csp_nonce() }}">.auth-form{max-width:400px;margin:0 auto}</style>
1884
+ <div class="container mt-4 auth-form">
1900
1885
  <h1>Register</h1>
1901
1886
  <form method="post" action="/api/auth/register">
1902
1887
  {{ form_token() }}
@@ -211,20 +211,30 @@ export default async function (req: Tina4Request, res: Tina4Response): Promise<v
211
211
  `
212
212
  );
213
213
 
214
- // Static index page
214
+ // Static index stylesheet. A static file cannot carry a per-response CSP
215
+ // nonce (ADR-0088), so its styles live in an external same-origin stylesheet
216
+ // the strict default policy ('self') already allows — not an inline <style>,
217
+ // which the browser would refuse, leaving a fresh project unstyled.
218
+ writeFileSync(
219
+ join(targetDir, "public/style.css"),
220
+ `body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 80px auto; padding: 0 20px; color: #333; }
221
+ h1 { font-size: 2.5em; margin-bottom: 0.2em; }
222
+ p { color: #666; font-size: 1.1em; }
223
+ code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
224
+ a { color: #2563eb; }
225
+ `
226
+ );
227
+
228
+ // Static index page. CSP-clean: an external stylesheet, no inline <style>.
229
+ // For a page that needs an inline block, render it through a Frond template
230
+ // and stamp the nonce: <style nonce="{{ csp_nonce() }}">...</style>.
215
231
  writeFileSync(
216
232
  join(targetDir, "public/index.html"),
217
233
  `<!DOCTYPE html>
218
234
  <html>
219
235
  <head>
220
236
  <title>Tina4</title>
221
- <style>
222
- body { font-family: -apple-system, sans-serif; max-width: 600px; margin: 80px auto; padding: 0 20px; color: #333; }
223
- h1 { font-size: 2.5em; margin-bottom: 0.2em; }
224
- p { color: #666; font-size: 1.1em; }
225
- code { background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
226
- a { color: #2563eb; }
227
- </style>
237
+ <link rel="stylesheet" href="/style.css">
228
238
  </head>
229
239
  <body>
230
240
  <h1>tina4</h1>
@@ -43,7 +43,8 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
43
43
  */
44
44
  import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
45
45
  import { createRequire } from "node:module";
46
- import { delimiter, dirname, join, relative } from "node:path";
46
+ import { dirname, join, relative } from "node:path";
47
+ import { findOnPath } from "../util.js";
47
48
  import { spawnSync } from "node:child_process";
48
49
 
49
50
  // Source extensions we count as lintable app code. `.d.ts` declaration files are
@@ -217,18 +218,7 @@ function hasPackage(cwd: string, pkg: string): boolean {
217
218
  function resolveNpm(): string | null {
218
219
  const windows = process.platform === "win32";
219
220
  const names = windows ? ["npm.cmd", "npm.exe", "npm"] : ["npm"];
220
- for (const dir of (process.env.PATH ?? "").split(delimiter)) {
221
- if (!dir) continue;
222
- for (const name of names) {
223
- const candidate = join(dir, name);
224
- try {
225
- if (statSync(candidate).isFile()) return candidate;
226
- } catch {
227
- // not there — keep looking
228
- }
229
- }
230
- }
231
- return null;
221
+ return findOnPath(names);
232
222
  }
233
223
 
234
224
  interface EslintSetup {
@@ -29,6 +29,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
29
29
  import { existsSync } from "node:fs";
30
30
  import { resolve } from "node:path";
31
31
  import { loadEnv } from "../../../core/src/dotenv.js";
32
+ import { initDatabaseOrExit } from "../util.js";
32
33
 
33
34
  export async function runMigrations(migrationDir?: string): Promise<void> {
34
35
  // Load .env before initialising the DB so DATABASE_URL/TINA4_DATABASE_URL
@@ -59,12 +60,7 @@ export async function runMigrations(migrationDir?: string): Promise<void> {
59
60
  // async — MUST be awaited, otherwise setAdapter() has not run by the time
60
61
  // migrate() asks for the adapter and the whole CLI crashes with
61
62
  // "No database adapter configured."
62
- try {
63
- await initDatabase();
64
- } catch (err) {
65
- console.error(` Error initialising database: ${err instanceof Error ? err.message : String(err)}`);
66
- process.exit(1);
67
- }
63
+ await initDatabaseOrExit(initDatabase);
68
64
 
69
65
  const result = await migrate(undefined, { migrationsDir: dir });
70
66
 
@@ -18,6 +18,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
18
18
  */
19
19
  import { resolve } from "node:path";
20
20
  import { loadEnv } from "../../../core/src/dotenv.js";
21
+ import { initDatabaseOrExit } from "../util.js";
21
22
 
22
23
  export async function migrateRollback(migrationDir?: string): Promise<void> {
23
24
  // .env must load before initDatabase() — otherwise DATABASE_URL from the
@@ -45,12 +46,7 @@ export async function migrateRollback(migrationDir?: string): Promise<void> {
45
46
  // Ensure database is initialised — MUST await; initDatabase() is async
46
47
  // and calls setAdapter() inside the promise. Without await, the next call
47
48
  // to getAdapter() throws "No database adapter configured."
48
- try {
49
- await initDatabase();
50
- } catch (err) {
51
- console.error(` Error initialising database: ${err instanceof Error ? err.message : String(err)}`);
52
- process.exit(1);
53
- }
49
+ await initDatabaseOrExit(initDatabase);
54
50
 
55
51
  await ensureMigrationTable();
56
52
 
@@ -15,6 +15,7 @@ file, You can obtain one at https://mozilla.org/MPL/2.0/.
15
15
  */
16
16
  import { resolve } from "node:path";
17
17
  import { loadEnv } from "../../../core/src/dotenv.js";
18
+ import { initDatabaseOrExit } from "../util.js";
18
19
 
19
20
  export async function migrateStatus(migrationDir?: string): Promise<void> {
20
21
  // .env must load before initDatabase() so the project's DATABASE_URL is seen.
@@ -38,12 +39,7 @@ export async function migrateStatus(migrationDir?: string): Promise<void> {
38
39
 
39
40
  // Ensure database is initialised — MUST await; initDatabase() is async
40
41
  // and calls setAdapter() inside. Without await, getAdapter() throws.
41
- try {
42
- await initDatabase();
43
- } catch (err) {
44
- console.error(` Error initialising database: ${err instanceof Error ? err.message : String(err)}`);
45
- process.exit(1);
46
- }
42
+ await initDatabaseOrExit(initDatabase);
47
43
 
48
44
  ensureMigrationTable();
49
45
 
@@ -28,6 +28,7 @@ import { pathToFileURL } from "node:url";
28
28
  import { loadEnv } from "../../../core/src/dotenv.js";
29
29
  import { Queue } from "../../../core/src/queue.js";
30
30
  import type { QueueJob } from "../../../core/src/job.js";
31
+ import { parseFlags as parseFlagsShared } from "../util.js";
31
32
 
32
33
  // ── Flag parsing ─────────────────────────────────────────────────────
33
34
  //
@@ -36,36 +37,7 @@ import type { QueueJob } from "../../../core/src/job.js";
36
37
 
37
38
  const BOOLEAN_FLAGS = new Set(["once", "json"]);
38
39
 
39
- interface ParsedFlags {
40
- flags: Record<string, string | boolean>;
41
- positional: string[];
42
- }
43
-
44
- function parseFlags(args: string[]): ParsedFlags {
45
- const flags: Record<string, string | boolean> = {};
46
- const positional: string[] = [];
47
- let i = 0;
48
- while (i < args.length) {
49
- const arg = args[i];
50
- if (arg.startsWith("--")) {
51
- const key = arg.slice(2);
52
- if (BOOLEAN_FLAGS.has(key)) {
53
- flags[key] = true;
54
- i += 1;
55
- } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
56
- flags[key] = args[i + 1];
57
- i += 2;
58
- } else {
59
- flags[key] = true;
60
- i += 1;
61
- }
62
- } else {
63
- positional.push(arg);
64
- i += 1;
65
- }
66
- }
67
- return { flags, positional };
68
- }
40
+ const parseFlags = (args: string[]) => parseFlagsShared(args, BOOLEAN_FLAGS);
69
41
 
70
42
  /** A per-job handler declared by a consumer module (receives the job payload). */
71
43
  export type QueueHandler = (payload: unknown) => unknown | Promise<unknown>;
@@ -0,0 +1,90 @@
1
+ /*
2
+ Copyright (c) 2026 Code Infinity
3
+ SPDX-License-Identifier: MPL-2.0
4
+ This Source Code Form is subject to the terms of the Mozilla Public
5
+ License, v. 2.0. If a copy of the MPL was not distributed with this
6
+ file, You can obtain one at https://mozilla.org/MPL/2.0/.
7
+ */
8
+
9
+ /**
10
+ * Shared CLI helpers — argument parsing, PATH lookup, and database bootstrap.
11
+ * Kept zero-dependency (node stdlib only) so the CLI entry path stays light.
12
+ */
13
+ import { delimiter, join } from "node:path";
14
+ import { statSync } from "node:fs";
15
+
16
+ /** Flags and positional arguments split out of a command's argv slice. */
17
+ export interface ParsedFlags {
18
+ flags: Record<string, string | boolean>;
19
+ positional: string[];
20
+ }
21
+
22
+ /**
23
+ * Parse `--key value` and bare `--flag` arguments into flags plus positionals.
24
+ *
25
+ * A name listed in `booleanFlags` never consumes the next token; every other
26
+ * `--key` takes the following token as its value unless that token is itself a
27
+ * `--flag` (or the argv ends), in which case the key is a bare boolean.
28
+ */
29
+ export function parseFlags(args: string[], booleanFlags?: ReadonlySet<string>): ParsedFlags {
30
+ const flags: Record<string, string | boolean> = {};
31
+ const positional: string[] = [];
32
+ let i = 0;
33
+ while (i < args.length) {
34
+ const arg = args[i];
35
+ if (arg.startsWith("--")) {
36
+ const key = arg.slice(2);
37
+ if (booleanFlags?.has(key)) {
38
+ flags[key] = true;
39
+ i += 1;
40
+ } else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
41
+ flags[key] = args[i + 1];
42
+ i += 2;
43
+ } else {
44
+ flags[key] = true;
45
+ i += 1;
46
+ }
47
+ } else {
48
+ positional.push(arg);
49
+ i += 1;
50
+ }
51
+ }
52
+ return { flags, positional };
53
+ }
54
+
55
+ /**
56
+ * Absolute path of the first of `names` found on PATH, or null.
57
+ *
58
+ * Scans PATH directories directly (no `which` shell-out) so it behaves the same
59
+ * on every platform and honours a deliberately emptied PATH. Callers pass the
60
+ * platform-specific candidate names (e.g. `["npm.cmd", "npm.exe", "npm"]`).
61
+ */
62
+ export function findOnPath(names: string[]): string | null {
63
+ for (const dir of (process.env.PATH ?? "").split(delimiter)) {
64
+ if (!dir) continue;
65
+ for (const name of names) {
66
+ const candidate = join(dir, name);
67
+ try {
68
+ if (statSync(candidate).isFile()) return candidate;
69
+ } catch {
70
+ // not there — keep looking
71
+ }
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+
77
+ /**
78
+ * Await the ORM's async `initDatabase()` and exit(1) with a clear message on
79
+ * failure. The await matters: initDatabase() calls setAdapter() inside the
80
+ * promise, so without it the next getAdapter() throws "No database adapter
81
+ * configured." Shared by the migrate, rollback, and status commands.
82
+ */
83
+ export async function initDatabaseOrExit(initDatabase: () => Promise<unknown>): Promise<void> {
84
+ try {
85
+ await initDatabase();
86
+ } catch (err) {
87
+ console.error(` Error initialising database: ${err instanceof Error ? err.message : String(err)}`);
88
+ process.exit(1);
89
+ }
90
+ }