tina4-nodejs 3.13.142 → 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 (37) hide show
  1. package/CLAUDE.md +28 -2
  2. package/package.json +1 -1
  3. package/packages/cli/dist/bin.js +379 -85
  4. package/packages/cli/src/commands/generate.ts +7 -2
  5. package/packages/cli/src/commands/init.ts +18 -8
  6. package/packages/core/dist/index.js +377 -78
  7. package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +27 -11
  8. package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +20 -7
  9. package/packages/core/gallery/templates/src/templates/gallery_page.twig +34 -15
  10. package/packages/core/src/csp.ts +138 -0
  11. package/packages/core/src/errorOverlay.ts +80 -37
  12. package/packages/core/src/index.ts +10 -0
  13. package/packages/core/src/middleware.ts +20 -6
  14. package/packages/core/src/response.ts +14 -3
  15. package/packages/core/src/server.ts +52 -15
  16. package/packages/core/src/session.ts +154 -7
  17. package/packages/core/src/sso.ts +1 -1
  18. package/packages/core/src/types.ts +8 -0
  19. package/packages/core/templates/errors/302.twig +1 -1
  20. package/packages/core/templates/errors/401.twig +2 -2
  21. package/packages/core/templates/errors/403.twig +1 -1
  22. package/packages/core/templates/errors/404.twig +1 -1
  23. package/packages/core/templates/errors/500.twig +1 -1
  24. package/packages/core/templates/errors/502.twig +2 -2
  25. package/packages/core/templates/errors/503.twig +2 -2
  26. package/packages/core/templates/errors/base.twig +4 -1
  27. package/packages/frond/dist/index.js +2 -0
  28. package/packages/frond/src/engine.ts +23 -0
  29. package/packages/orm/dist/index.js +378 -87
  30. package/packages/swagger/dist/index.js +4 -4
  31. package/packages/swagger/src/ui.ts +7 -4
  32. package/types/core/src/csp.d.ts +42 -0
  33. package/types/core/src/index.d.ts +1 -0
  34. package/types/core/src/middleware.d.ts +6 -0
  35. package/types/core/src/session.d.ts +62 -2
  36. package/types/core/src/types.d.ts +8 -0
  37. package/types/frond/src/engine.d.ts +7 -0
@@ -487,13 +487,13 @@ function uniqueOperationId(method, openApiPath, seen) {
487
487
  function swaggerUiCdn() {
488
488
  return (process.env.TINA4_SWAGGER_UI_CDN ?? "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5").replace(/\/+$/, "");
489
489
  }
490
- var SWAGGER_UI_HTML = (specUrl) => `<!DOCTYPE html>
490
+ var SWAGGER_UI_HTML = (specUrl, nonce) => `<!DOCTYPE html>
491
491
  <html lang="en">
492
492
  <head>
493
493
  <meta charset="UTF-8">
494
494
  <title>Tina4 API Documentation</title>
495
495
  <link rel="stylesheet" href="${swaggerUiCdn()}/swagger-ui.css">
496
- <style>
496
+ <style nonce="${nonce}">
497
497
  body { margin: 0; background: #fafafa; }
498
498
  .topbar { display: none !important; }
499
499
  </style>
@@ -501,7 +501,7 @@ var SWAGGER_UI_HTML = (specUrl) => `<!DOCTYPE html>
501
501
  <body>
502
502
  <div id="swagger-ui"></div>
503
503
  <script src="${swaggerUiCdn()}/swagger-ui-bundle.js"></script>
504
- <script>
504
+ <script nonce="${nonce}">
505
505
  SwaggerUIBundle({
506
506
  url: "${specUrl}",
507
507
  dom_id: '#swagger-ui',
@@ -522,7 +522,7 @@ function swaggerEnabled() {
522
522
  }
523
523
  function createSwaggerRoutes(getSpec) {
524
524
  const serveUi = async (_req, res) => {
525
- res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
525
+ res.html(SWAGGER_UI_HTML("/swagger/openapi.json", res.cspNonce));
526
526
  };
527
527
  return [
528
528
  {
@@ -18,13 +18,16 @@ function swaggerUiCdn(): string {
18
18
  return (process.env.TINA4_SWAGGER_UI_CDN ?? "https://cdn.jsdelivr.net/npm/swagger-ui-dist@5").replace(/\/+$/, "");
19
19
  }
20
20
 
21
- const SWAGGER_UI_HTML = (specUrl: string) => `<!DOCTYPE html>
21
+ // The inline <style> and the inline SwaggerUIBundle(...) <script> carry this
22
+ // response's CSP nonce (ADR-0088) so they run under the strict default
23
+ // Content-Security-Policy; the external <script src>/<link> need none.
24
+ const SWAGGER_UI_HTML = (specUrl: string, nonce: string) => `<!DOCTYPE html>
22
25
  <html lang="en">
23
26
  <head>
24
27
  <meta charset="UTF-8">
25
28
  <title>Tina4 API Documentation</title>
26
29
  <link rel="stylesheet" href="${swaggerUiCdn()}/swagger-ui.css">
27
- <style>
30
+ <style nonce="${nonce}">
28
31
  body { margin: 0; background: #fafafa; }
29
32
  .topbar { display: none !important; }
30
33
  </style>
@@ -32,7 +35,7 @@ const SWAGGER_UI_HTML = (specUrl: string) => `<!DOCTYPE html>
32
35
  <body>
33
36
  <div id="swagger-ui"></div>
34
37
  <script src="${swaggerUiCdn()}/swagger-ui-bundle.js"></script>
35
- <script>
38
+ <script nonce="${nonce}">
36
39
  SwaggerUIBundle({
37
40
  url: "${specUrl}",
38
41
  dom_id: '#swagger-ui',
@@ -65,7 +68,7 @@ export function createSwaggerRoutes(
65
68
  getSpec: () => unknown
66
69
  ): RouteDefinition[] {
67
70
  const serveUi = async (_req: Tina4Request, res: Tina4Response): Promise<void> => {
68
- res.html(SWAGGER_UI_HTML("/swagger/openapi.json"));
71
+ res.html(SWAGGER_UI_HTML("/swagger/openapi.json", res.cspNonce));
69
72
  };
70
73
 
71
74
  return [
@@ -0,0 +1,42 @@
1
+ /** Return a fresh cryptographically-random nonce (128 bits, base64). */
2
+ export declare function generateNonce(): string;
3
+ /** Run `fn` with `nonce` established as the request-scoped CSP nonce. */
4
+ export declare function runWithCspNonce<T>(nonce: string, fn: () => T): T;
5
+ /** Set the nonce for the current request context (mirrors Log.setRequestId). */
6
+ export declare function setCurrentNonce(nonce: string | undefined): void;
7
+ /** Clear the nonce (mirrors Log.clearRequestId); the scope ends on its own. */
8
+ export declare function clearCurrentNonce(): void;
9
+ /**
10
+ * Return the current request's nonce, minting one on first access.
11
+ *
12
+ * Generate-on-first-access makes the value independent of ordering: whether the
13
+ * inline body or the CSP header is built first, both read the same nonce.
14
+ */
15
+ export declare function currentCspNonce(): string;
16
+ /**
17
+ * Frond global `csp_nonce()`: the current response's CSP nonce.
18
+ *
19
+ * Use it on any inline element a template emits:
20
+ *
21
+ * <style nonce="{{ csp_nonce() }}"> ... </style>
22
+ * <script nonce="{{ csp_nonce() }}"> ... </script>
23
+ */
24
+ export declare function cspNonce(): string;
25
+ /**
26
+ * Return `csp` with `'nonce-<nonce>'` present in style-src AND script-src.
27
+ *
28
+ * For the default `default-src 'self'` this yields
29
+ * `default-src 'self'; style-src 'self' 'nonce-X'; script-src 'self' 'nonce-X'`.
30
+ * When a directive is absent it is derived from `default-src` (falling back to
31
+ * `'self'`) so the framework's own nonce'd content always works; when present
32
+ * the nonce is appended (idempotently). Every other directive is kept, in order.
33
+ */
34
+ export declare function injectNonceIntoCsp(csp: string, nonce: string): string;
35
+ /**
36
+ * Build the CSP header value for `nonce` from the environment.
37
+ *
38
+ * Honours `TINA4_CSP` (default `default-src 'self'`) and always injects the
39
+ * nonce into style-src and script-src. An empty `TINA4_CSP` is treated as the
40
+ * default policy, matching how the warn-once gate reads "unset".
41
+ */
42
+ export declare function resolveCspHeader(nonce: string): string;
@@ -54,6 +54,7 @@ export { WSDLService, WSDLOperation } from "./wsdl.js";
54
54
  export type { WSDLOperationMeta } from "./wsdl.js";
55
55
  export { HtmlElement, htmlElement, addHtmlHelpers, Raw, SafeString } from "./htmlElement.js";
56
56
  export { renderErrorOverlay, isDebugMode } from "./errorOverlay.js";
57
+ export { cspNonce, currentCspNonce, generateNonce, injectNonceIntoCsp, resolveCspHeader, runWithCspNonce, setCurrentNonce, clearCurrentNonce, } from "./csp.js";
57
58
  export { AI_TOOLS, isInstalled, showMenu, installSelected, installAll, generateContext } from "./ai.js";
58
59
  export type { AiTool } from "./ai.js";
59
60
  export { Sso, SSO, SsoError } from "./sso.js";
@@ -344,6 +344,12 @@ export declare class SecurityHeadersMiddleware {
344
344
  * overrides, WITHOUT Strict-Transport-Security, which depends on the
345
345
  * request's scheme. Also what a transport rejection carries (ADR-0068
346
346
  * section 4), written before the scheme is known.
347
+ *
348
+ * Content-Security-Policy always carries a per-response nonce in style-src
349
+ * AND script-src (ADR-0088), read from the request async-context (minted on
350
+ * first access), so the framework's own inline content runs under the strict
351
+ * default without 'unsafe-inline'. The nonce is random per response, so a
352
+ * caller comparing this header must assert its STRUCTURE, never a byte string.
347
353
  */
348
354
  static canonicalHeaders(): Record<string, string>;
349
355
  static beforeSecurity(req: Tina4Request, res: Tina4Response): [Tina4Request, Tina4Response];
@@ -123,6 +123,29 @@ export declare class Session {
123
123
  * Lets `start()` tell "no such session" from "the store is unreachable".
124
124
  */
125
125
  private lastReadFailed;
126
+ /**
127
+ * What this request last saw of the stored record, one fingerprint per key:
128
+ * taken by start() and again by every save. save() compares the live data
129
+ * against it to find the keys THIS request changed.
130
+ */
131
+ private loaded;
132
+ /**
133
+ * True when this request is working on a record the store holds: start()
134
+ * adopted one, or a save has written one since. A save never re-creates such
135
+ * a record once another request has removed it.
136
+ */
137
+ private stored;
138
+ /**
139
+ * True when clear() ran since the last save: the save then replaces the
140
+ * stored record instead of merging into it.
141
+ */
142
+ private cleared;
143
+ /**
144
+ * True once this request found its session ended by another request (the
145
+ * record it loaded is gone). It stays ended for the rest of the request:
146
+ * regenerate() mints nothing either.
147
+ */
148
+ private ended;
126
149
  constructor(backend?: string, config?: SessionConfig);
127
150
  /**
128
151
  * Use a custom session handler (for advanced use cases).
@@ -209,8 +232,14 @@ export declare class Session {
209
232
  * session fixation — the pre-auth ID is destroyed and the data is carried
210
233
  * onto a fresh, unguessable ID. A backend destroy/write failure is logged
211
234
  * (never silent) but does not throw under the default policy.
235
+ *
236
+ * What is carried is the session as it is stored now with this request's
237
+ * own changes applied, the same merge save() does. If another request ended
238
+ * the session after this one loaded it (a logout, or a regenerate of its
239
+ * own), it stays ended: nothing is carried, no id is minted and null is
240
+ * returned, so no cookie goes out to replace the one that request sent.
212
241
  */
213
- regenerate(): string;
242
+ regenerate(): string | null;
214
243
  /**
215
244
  * Dual-mode flash: set with value, get+remove without.
216
245
  *
@@ -256,9 +285,40 @@ export declare class Session {
256
285
  *
257
286
  * Returns true on a successful persist, false if the backend was unreachable
258
287
  * (logged). The dirty flag is cleared only on success so a later save()
259
- * retries once the backend recovers. A nothing-to-persist call returns true.
288
+ * retries once the backend recovers. A call with no session returns true.
289
+ *
290
+ * Other requests on the same session run while this one does, so the save
291
+ * writes only THIS request's changes, onto the record as it is stored now.
292
+ * Writing back the whole snapshot start() loaded undid whatever another
293
+ * request did in between: a logout (destroy, clear, regenerate) or a
294
+ * privilege change made with set(). A record removed since this request
295
+ * loaded it is never re-created: the session has ended, for this request
296
+ * too. A request that has not loaded a stored record (a new session, the new
297
+ * id regenerate() mints) writes its data whole, as before.
298
+ *
299
+ * A session the store already holds is re-written even when this request
300
+ * changed nothing: that write re-stamps the backend deadline to
301
+ * now + TINA4_SESSION_TTL, so a session expires after that span of INACTIVITY
302
+ * rather than that long after its last CHANGE (ADR-0087 — a session's expiry
303
+ * slides on activity). The re-write is the re-read, merged record, so sliding
304
+ * the deadline is concurrent-safe: it never clobbers another request's change,
305
+ * never re-creates an ended record, and TINA4_SESSION_TTL=0 still re-stamps a
306
+ * never-expires deadline (ADR-0027). A request with no stored session and no
307
+ * change writes nothing — there is no deadline to move.
260
308
  */
261
309
  save(): boolean;
310
+ /**
311
+ * The stored record with this request's changes applied: every key whose
312
+ * value differs from what this request loaded or last saved, and the
313
+ * removal of every key it deleted. Keys it did not touch keep whatever is
314
+ * stored now.
315
+ */
316
+ private merged;
317
+ /**
318
+ * End the session for this request without touching the store: no id and
319
+ * no data, so nothing more is written and no cookie goes out for it.
320
+ */
321
+ private forget;
262
322
  }
263
323
  /**
264
324
  * Is the client's scheme HTTPS? Proxy-aware.
@@ -116,6 +116,14 @@ export interface Tina4ResponseMethods {
116
116
  stream(source: AsyncIterable<string | Buffer>, contentType?: string): Promise<Tina4Response>;
117
117
  /** The underlying ServerResponse for advanced use */
118
118
  raw: ServerResponse;
119
+ /**
120
+ * This response's Content-Security-Policy nonce (ADR-0088). `runDispatch()`
121
+ * mints one per request and publishes it on the async-context (csp.ts) so the
122
+ * framework's inline `<style>`/`<script>`, the Frond `csp_nonce()` global, and
123
+ * the value the security middleware names in the header all agree. Stamp it on
124
+ * any inline block a route emits: `<script nonce="${res.cspNonce}">`.
125
+ */
126
+ cspNonce: string;
119
127
  }
120
128
  /**
121
129
  * Tina4 Response — callable AND has methods.
@@ -53,6 +53,13 @@ export declare const expressionFormCache: Map<string, number>;
53
53
  * Set the session ID used by formToken() / form_token() for CSRF session binding.
54
54
  */
55
55
  export declare function setFormTokenSessionId(sessionId: string): void;
56
+ /**
57
+ * Register the function that backs the `{{ csp_nonce() }}` template global.
58
+ * @tina4/core calls this once with its request-scoped nonce source so every
59
+ * Frond instance (framework templates AND the app's) resolves the same nonce
60
+ * the Content-Security-Policy header names.
61
+ */
62
+ export declare function setCspNonceProvider(fn: () => string): void;
56
63
  export declare class Frond {
57
64
  private static classFilters;
58
65
  private static classGlobals;