@cosmicdrift/kumiko-bundled-features 0.229.1 → 0.231.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.229.1",
3
+ "version": "0.231.0",
4
4
  "description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -130,12 +130,12 @@
130
130
  "./workflow-runner": "./src/workflow-runner/index.ts"
131
131
  },
132
132
  "dependencies": {
133
- "@cosmicdrift/kumiko-dispatcher-live": "0.229.1",
134
- "@cosmicdrift/kumiko-framework": "0.229.1",
135
- "@cosmicdrift/kumiko-headless": "0.229.1",
136
- "@cosmicdrift/kumiko-renderer": "0.229.1",
137
- "@cosmicdrift/kumiko-renderer-web": "0.229.1",
138
- "@cosmicdrift/kumiko-types": "0.229.1",
133
+ "@cosmicdrift/kumiko-dispatcher-live": "0.231.0",
134
+ "@cosmicdrift/kumiko-framework": "0.231.0",
135
+ "@cosmicdrift/kumiko-headless": "0.231.0",
136
+ "@cosmicdrift/kumiko-renderer": "0.231.0",
137
+ "@cosmicdrift/kumiko-renderer-web": "0.231.0",
138
+ "@cosmicdrift/kumiko-types": "0.231.0",
139
139
  "@mollie/api-client": "^4.5.0",
140
140
  "@node-rs/argon2": "^2.0.2",
141
141
  "@types/mailparser": "^3.4.6",
@@ -164,7 +164,7 @@
164
164
  "devDependencies": {
165
165
  "@testing-library/user-event": "^14.6.1",
166
166
  "@types/qrcode": "^1.5.5",
167
- "@cosmicdrift/kumiko-locale-de": "0.229.1",
168
- "@cosmicdrift/kumiko-locale-es": "0.229.1"
167
+ "@cosmicdrift/kumiko-locale-de": "0.231.0",
168
+ "@cosmicdrift/kumiko-locale-es": "0.231.0"
169
169
  }
170
170
  }
@@ -262,7 +262,7 @@ describe("compliance-profiles :: sub-processors (S1.4)", () => {
262
262
  const cloudflare = result.active.find((s) => s.name.includes("Cloudflare"));
263
263
  expect(cloudflare?.sccRequired).toBe(true);
264
264
 
265
- expect(result.planned.length).toBeGreaterThan(0);
265
+ expect(Array.isArray(result.planned)).toBe(true);
266
266
  expect(result.planned.every((p) => p.status === "planned")).toBe(true);
267
267
 
268
268
  expect(result.total).toBe(result.active.length + result.planned.length);
@@ -57,6 +57,8 @@ See [template-resolver/README.md](../template-resolver/README.md).
57
57
 
58
58
  ## Routes
59
59
 
60
+ **Default** (DACH: DE required, EN optional):
61
+
60
62
  | Path | Slug + locale | Title fallback (when block empty) |
61
63
  |---|---|---|
62
64
  | `GET /legal/impressum` | `imprint` / `de` | "Impressum" |
@@ -75,22 +77,57 @@ directly and render themselves.
75
77
 
76
78
  ---
77
79
 
80
+ ### Non-DACH apps: custom routes + required blocks
81
+
82
+ The default four routes and the two DE required blocks
83
+ (`LEGAL_ROUTES`/`LEGAL_REQUIRED_BLOCKS`) are DACH conventions, not a
84
+ hard constraint. An app with a different default language, or one
85
+ that needs an additional page (terms/AGB), passes its own lists:
86
+
87
+ ```typescript
88
+ createLegalPagesFeature({
89
+ routes: [
90
+ { path: "/legal/aviso-legal", slug: "imprint", lang: "es", titleFallback: "Aviso legal" },
91
+ { path: "/legal/privacidad", slug: "privacy", lang: "es", titleFallback: "Política de privacidad" },
92
+ { path: "/legal/terminos", slug: "terms", lang: "es", titleFallback: "Términos y condiciones" },
93
+ ],
94
+ requiredBlocks: [{ slug: "imprint", lang: "es" }, { slug: "privacy", lang: "es" }],
95
+ })
96
+ ```
97
+
98
+ `slug` is free-form — it only has to match the `slug` you seed the
99
+ text-block under via `template-resolver`. `requiredBlocks` drives the
100
+ boot check (see below); routes not listed there stay optional and
101
+ just 404 until seeded, same as the DACH default.
102
+
103
+ Validated once at feature-build time (fails app startup, not a
104
+ request): no two routes may share a `path`, every route needs a
105
+ non-empty `path`/`slug`/`lang`, and every `path` must start with `/`.
106
+ An empty `routes` array is allowed — the boot check alone, no public
107
+ routes.
108
+
109
+ ---
110
+
78
111
  ## Boot check
79
112
 
80
- `r.job` with `runOnBoot: true` checks at app start whether the DE
81
- required blocks exist in SYSTEM_TENANT:
113
+ `r.job` with `runOnBoot: true` checks at app start whether the
114
+ required blocks exist in SYSTEM_TENANT. **Default** (DACH):
82
115
 
83
116
  | Slug + locale | What happens when missing |
84
117
  |---|---|
85
118
  | `imprint` / `de` | **Production:** `throw new Error(...)` blocks app start. **Dev:** `ctx.log.warn(...)` |
86
119
  | `privacy` / `de` | as above |
87
120
 
88
- EN versions are **not** boot-fail-relevant (`LEGAL_OPTIONAL_BLOCKS`).
121
+ EN versions are **not** boot-fail-relevant by default (`LEGAL_OPTIONAL_BLOCKS`).
89
122
  Routes return `404` if an EN block is missing.
90
123
 
91
- → Apps that activate the feature must seed both DE blocks before a
92
- production deploy — either via a bootstrap script (`seedTextBlock`) or
93
- manually via the TenantAdmin API.
124
+ → Apps that activate the feature with the default config must seed
125
+ both DE blocks before a production deploy — either via a bootstrap
126
+ script (`seedTextBlock`) or manually via the TenantAdmin API.
127
+
128
+ → Apps with a `requiredBlocks` override (see above) get the same
129
+ production-hard-fail/dev-warn behavior for their own slug/lang list
130
+ instead — the DE blocks are not checked when `requiredBlocks` is set.
94
131
 
95
132
  ---
96
133
 
@@ -328,6 +328,88 @@ describe("legal-pages :: wrapLayout erhält route.slug (alt-lang-switch-regressi
328
328
  });
329
329
  });
330
330
 
331
+ describe("legal-pages :: configurable routes/requiredBlocks (non-DACH apps)", () => {
332
+ test("custom route (es) serves the seeded Spanish text-block", async () => {
333
+ const esStack = await setupTestStack({
334
+ features: [
335
+ createTemplateResolverFeature(),
336
+ createLegalPagesFeature({
337
+ routes: [
338
+ {
339
+ path: "/legal/aviso-legal",
340
+ slug: "imprint",
341
+ lang: "es",
342
+ titleFallback: "Aviso legal",
343
+ },
344
+ ],
345
+ requiredBlocks: [{ slug: "imprint", lang: "es" }],
346
+ }),
347
+ ],
348
+ anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID },
349
+ extraContext: ({ db }) => ({
350
+ templateResolver: createTemplateResolverApi(db),
351
+ }),
352
+ });
353
+ try {
354
+ await unsafeCreateEntityTable(esStack.db, templateResourceEntity);
355
+ await createEventsTable(esStack.db);
356
+ await seedTextBlock(esStack.db, {
357
+ tenantId: SYSTEM_TENANT_ID,
358
+ slug: "imprint",
359
+ locale: "es",
360
+ title: "Aviso legal",
361
+ content: "## Datos del titular\n\n**Marc Frost**",
362
+ });
363
+
364
+ const res = await esStack.app.request("/legal/aviso-legal");
365
+ expect(res.status).toBe(200);
366
+ const body = await res.text();
367
+ expect(body).toContain('lang="es"');
368
+ expect(body).toContain("<title>Aviso legal</title>");
369
+ expect(body).toContain("Datos del titular");
370
+
371
+ // Default DACH routes are gone once `routes` is overridden.
372
+ const dachRes = await esStack.app.request("/legal/impressum");
373
+ expect(dachRes.status).toBe(404);
374
+ } finally {
375
+ await esStack.cleanup();
376
+ }
377
+ });
378
+
379
+ test("createLegalPagesFeature throws on duplicate route path", () => {
380
+ expect(() =>
381
+ createLegalPagesFeature({
382
+ routes: [
383
+ { path: "/legal/aviso-legal", slug: "imprint", lang: "es", titleFallback: "Aviso" },
384
+ { path: "/legal/aviso-legal", slug: "privacy", lang: "es", titleFallback: "Privacidad" },
385
+ ],
386
+ }),
387
+ ).toThrow(/duplicate route path/);
388
+ });
389
+
390
+ test("createLegalPagesFeature throws on empty path/slug/lang", () => {
391
+ expect(() =>
392
+ createLegalPagesFeature({
393
+ routes: [{ path: "", slug: "imprint", lang: "es", titleFallback: "Aviso" }],
394
+ }),
395
+ ).toThrow(/missing a required field/);
396
+ });
397
+
398
+ test("createLegalPagesFeature throws when path doesn't start with /", () => {
399
+ expect(() =>
400
+ createLegalPagesFeature({
401
+ routes: [
402
+ { path: "legal/aviso-legal", slug: "imprint", lang: "es", titleFallback: "Aviso" },
403
+ ],
404
+ }),
405
+ ).toThrow(/must start with/);
406
+ });
407
+
408
+ test("empty routes array is allowed (boot-check only, no public routes)", () => {
409
+ expect(() => createLegalPagesFeature({ routes: [] })).not.toThrow();
410
+ });
411
+ });
412
+
331
413
  describe("legal-pages :: runLegalPagesBootCheck (direct unit-tests)", () => {
332
414
  // Direkter Test der Boot-Check-Logik mit constructed ctx-Objects —
333
415
  // keine JobRunner-Coupling, keine Test-Stacks. Das ist die echte
@@ -471,4 +553,98 @@ describe("legal-pages :: runLegalPagesBootCheck (direct unit-tests)", () => {
471
553
  expect(call.tenantId).toBe(SYSTEM_TENANT_ID);
472
554
  }
473
555
  });
556
+
557
+ test("custom requiredBlock (es privacy) fehlt → throws in production", async () => {
558
+ const { api } = fakeTemplateResolver([
559
+ { slug: "imprint", lang: "es", title: "I", content: "x" },
560
+ ]);
561
+ const originalEnv = process.env["NODE_ENV"];
562
+ process.env["NODE_ENV"] = "production";
563
+ try {
564
+ await expect(
565
+ runLegalPagesBootCheck({ templateResolver: api }, [
566
+ { slug: "imprint", lang: "es" },
567
+ { slug: "privacy", lang: "es" },
568
+ ]),
569
+ ).rejects.toThrow(/Boot-Validation failed.*privacy\/es/s);
570
+ } finally {
571
+ if (originalEnv === undefined) delete process.env["NODE_ENV"];
572
+ else process.env["NODE_ENV"] = originalEnv;
573
+ }
574
+ });
575
+
576
+ test("custom requiredBlock (es privacy) vorhanden → kein throw", async () => {
577
+ const { api } = fakeTemplateResolver([
578
+ { slug: "imprint", lang: "es", title: "I", content: "x" },
579
+ { slug: "privacy", lang: "es", title: "P", content: "x" },
580
+ ]);
581
+ const originalEnv = process.env["NODE_ENV"];
582
+ process.env["NODE_ENV"] = "production";
583
+ try {
584
+ await expect(
585
+ runLegalPagesBootCheck({ templateResolver: api }, [
586
+ { slug: "imprint", lang: "es" },
587
+ { slug: "privacy", lang: "es" },
588
+ ]),
589
+ ).resolves.toBeUndefined();
590
+ } finally {
591
+ if (originalEnv === undefined) delete process.env["NODE_ENV"];
592
+ else process.env["NODE_ENV"] = originalEnv;
593
+ }
594
+ });
595
+
596
+ test("DE-Blocks fehlen, custom requiredBlocks (es) konfiguriert + geseedet → kein Fail", async () => {
597
+ // Proves the boot check only looks at the configured requiredBlocks,
598
+ // not hardcoded LEGAL_REQUIRED_BLOCKS (DE) — required for apps with a
599
+ // default language other than DE.
600
+ const { api } = fakeTemplateResolver([
601
+ { slug: "imprint", lang: "es", title: "I", content: "x" },
602
+ { slug: "privacy", lang: "es", title: "P", content: "x" },
603
+ ]);
604
+ const originalEnv = process.env["NODE_ENV"];
605
+ process.env["NODE_ENV"] = "production";
606
+ try {
607
+ await expect(
608
+ runLegalPagesBootCheck({ templateResolver: api }, [
609
+ { slug: "imprint", lang: "es" },
610
+ { slug: "privacy", lang: "es" },
611
+ ]),
612
+ ).resolves.toBeUndefined();
613
+ } finally {
614
+ if (originalEnv === undefined) delete process.env["NODE_ENV"];
615
+ else process.env["NODE_ENV"] = originalEnv;
616
+ }
617
+ });
618
+
619
+ test("createLegalPagesFeature threads requiredBlocks into the boot-check job, not the DACH default", async () => {
620
+ // Goes through the actual job registered by createLegalPagesFeature
621
+ // (not a direct runLegalPagesBootCheck call, unlike the tests above) —
622
+ // proves the `requiredBlocks` option reaches the job closure. DE blocks
623
+ // are seeded but the feature is built with an es-only requiredBlocks
624
+ // list: if the job still checked the hardcoded LEGAL_REQUIRED_BLOCKS
625
+ // (DE), this would resolve instead of rejecting on the missing es block.
626
+ const { api } = fakeTemplateResolver([
627
+ { slug: "imprint", lang: "de", title: "I", content: "x" },
628
+ { slug: "privacy", lang: "de", title: "P", content: "x" },
629
+ ]);
630
+ const feature = createLegalPagesFeature({
631
+ requiredBlocks: [{ slug: "privacy", lang: "es" }],
632
+ });
633
+ const job = feature.jobs["legal-pages-boot-check"];
634
+ if (!job) throw new Error("legal-pages-boot-check job not registered");
635
+ // @cast-boundary test-only — job.handler is typed against the full
636
+ // JobContext; runLegalPagesBootCheck only reads templateResolver off it.
637
+ const handler = job.handler as unknown as (
638
+ payload: Record<string, unknown>,
639
+ ctx: { templateResolver: TemplateResolverApi },
640
+ ) => Promise<void>;
641
+ const originalEnv = process.env["NODE_ENV"];
642
+ process.env["NODE_ENV"] = "production";
643
+ try {
644
+ await expect(handler({}, { templateResolver: api })).rejects.toThrow(/privacy\/es/);
645
+ } finally {
646
+ if (originalEnv === undefined) delete process.env["NODE_ENV"];
647
+ else process.env["NODE_ENV"] = originalEnv;
648
+ }
649
+ });
474
650
  });
@@ -2,6 +2,23 @@
2
2
  // Feature name
3
3
  export const LEGAL_PAGES_FEATURE = "legal-pages" as const;
4
4
 
5
+ // Shape of one public route entry — the type behind LEGAL_ROUTES, exported
6
+ // so non-DACH apps can type their own `routes` option (e.g. a Spanish-
7
+ // default app with an additional AGB/terms page).
8
+ export type LegalPageRoute = {
9
+ readonly path: string;
10
+ readonly slug: string;
11
+ readonly lang: string;
12
+ readonly titleFallback: string;
13
+ };
14
+
15
+ // Shape of one boot-check-required block — the type behind
16
+ // LEGAL_REQUIRED_BLOCKS, exported for the same reason as LegalPageRoute.
17
+ export type LegalRequiredBlock = {
18
+ readonly slug: string;
19
+ readonly lang: string;
20
+ };
21
+
5
22
  // Required slugs that must exist as text-blocks for production-boot.
6
23
  // Pro Sprache + Slug eine Pflicht-Kombo. Wer mehr Sprachen will, ergänzt
7
24
  // die Liste — Boot-Check wird dynamisch aus der Liste generiert.
@@ -10,7 +10,12 @@ import {
10
10
  SYSTEM_TENANT_ID,
11
11
  } from "@cosmicdrift/kumiko-framework/engine";
12
12
  import { cachedSecurePageResponse } from "../page-render";
13
- import { LEGAL_REQUIRED_BLOCKS, LEGAL_ROUTES } from "./constants";
13
+ import {
14
+ LEGAL_REQUIRED_BLOCKS,
15
+ LEGAL_ROUTES,
16
+ type LegalPageRoute,
17
+ type LegalRequiredBlock,
18
+ } from "./constants";
14
19
  import { renderMarkdownToHtml, wrapInLayout } from "./markdown";
15
20
 
16
21
  // QN-Konstante als dokumentierter Public-Contract des template-resolver-
@@ -32,11 +37,13 @@ type TextBlockQueryResult = {
32
37
  // 60s-shared-cache saves the origin-revalidate roundtrip; legal-content edits are live within 60s.
33
38
  const PUBLIC_PAGE_CACHE = { kind: "revalidate", maxAgeSeconds: 60 } as const;
34
39
 
35
- // legal-pages — Opt-in-Wrapper um template-resolver (kind text-block) für DACH-Compliance.
36
- // Liefert vier feste Public-HTML-Routes (/legal/impressum,
37
- // /legal/datenschutz, /legal/imprint, /legal/privacy) mit
38
- // Markdown→HTML-Rendering und einen Boot-Check der in Production hart
39
- // fehlt wenn die DE-Pflicht-Blocks nicht geseedet sind.
40
+ // legal-pages — opt-in wrapper around template-resolver (kind text-block).
41
+ // Default config ships four DACH public HTML routes (/legal/impressum,
42
+ // /legal/datenschutz, /legal/imprint, /legal/privacy) with Markdown-to-HTML
43
+ // rendering and a boot check that hard-fails in production when the DE
44
+ // required blocks aren't seeded. `routes`/`requiredBlocks` are configurable
45
+ // (see LegalPagesOptions) for apps with a different default language or
46
+ // additional pages (e.g. terms/AGB).
40
47
  //
41
48
  // Cross-Feature-Decoupling:
42
49
  // • Routes nutzen app.fetch zu "/api/query" mit dem QN-string
@@ -55,7 +62,7 @@ export type LegalPagesWrapLayout = (opts: {
55
62
  readonly title: string;
56
63
  readonly bodyHtml: string;
57
64
  readonly lang: string;
58
- readonly slug?: (typeof LEGAL_ROUTES)[number]["slug"];
65
+ readonly slug?: string;
59
66
  }) => string;
60
67
 
61
68
  export type LegalPagesOptions = {
@@ -64,13 +71,48 @@ export type LegalPagesOptions = {
64
71
  * Marketing-Layout (Header/Footer/Theme) auch um Legal-Body legen
65
72
  * wollen, übergeben hier ihre Render-Function. */
66
73
  readonly wrapLayout?: LegalPagesWrapLayout;
74
+ /** Public HTML routes to register. Default: LEGAL_ROUTES (DACH: DE+EN
75
+ * imprint/privacy). Non-DACH apps (other default language, additional
76
+ * pages like terms/AGB) pass their own list instead. Validated at
77
+ * feature-build time: no duplicate `path`, no empty field, every `path`
78
+ * starts with "/". An empty array is allowed (boot-check only, no
79
+ * public routes). */
80
+ readonly routes?: readonly LegalPageRoute[];
81
+ /** Slug+lang combos the boot check requires to be seeded in
82
+ * SYSTEM_TENANT. Default: LEGAL_REQUIRED_BLOCKS (`imprint/de`,
83
+ * `privacy/de`). Apps whose routes use a different slug/lang set
84
+ * (e.g. Spanish-default) pass their own list here. */
85
+ readonly requiredBlocks?: readonly LegalRequiredBlock[];
67
86
  };
68
87
 
88
+ // Validated once at feature-build time (not per-request) so a config typo
89
+ // fails app startup instead of surfacing as a confusing 404 later.
90
+ function validateRoutes(routes: readonly LegalPageRoute[]): void {
91
+ const seenPaths = new Set<string>();
92
+ for (const route of routes) {
93
+ if (!route.path || !route.slug || !route.lang) {
94
+ throw new Error(
95
+ `legal-pages: route is missing a required field (path/slug/lang): ${JSON.stringify(route)}`,
96
+ );
97
+ }
98
+ if (!route.path.startsWith("/")) {
99
+ throw new Error(`legal-pages: route path must start with "/", got "${route.path}"`);
100
+ }
101
+ if (seenPaths.has(route.path)) {
102
+ throw new Error(`legal-pages: duplicate route path "${route.path}"`);
103
+ }
104
+ seenPaths.add(route.path);
105
+ }
106
+ }
107
+
69
108
  export function createLegalPagesFeature(opts: LegalPagesOptions = {}): FeatureDefinition {
70
109
  const wrapLayout = opts.wrapLayout ?? wrapInLayout;
110
+ const routes = opts.routes ?? LEGAL_ROUTES;
111
+ const requiredBlocks = opts.requiredBlocks ?? LEGAL_REQUIRED_BLOCKS;
112
+ validateRoutes(routes);
71
113
  return defineFeature("legal-pages", (r) => {
72
114
  r.describe(
73
- "Opt-in wrapper around `template-resolver` text-blocks that registers four public HTML routes (`/legal/impressum`, `/legal/datenschutz`, `/legal/imprint`, `/legal/privacy`) with Markdown-to-HTML rendering and a boot-time job that hard-fails in production when the required DE blocks (`imprint/de`, `privacy/de`) are not seeded in `SYSTEM_TENANT`. Requires `anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID }` and `extraContext.templateResolver` to be wired at app bootstrap; for per-tenant imprints or a custom layout call `template-resolver:query:by-slug` directly.",
115
+ "Opt-in wrapper around `template-resolver` text-blocks that registers public HTML routes (default: `/legal/impressum`, `/legal/datenschutz`, `/legal/imprint`, `/legal/privacy`) with Markdown-to-HTML rendering and a boot-time job that hard-fails in production when the required blocks (default: `imprint/de`, `privacy/de`) are not seeded in `SYSTEM_TENANT`. Both are configurable via `routes`/`requiredBlocks` for apps with a different default language or additional pages. Requires `anonymousAccess: { defaultTenantId: SYSTEM_TENANT_ID }` and `extraContext.templateResolver` to be wired at app bootstrap; for per-tenant imprints or a custom layout call `template-resolver:query:by-slug` directly.",
74
116
  );
75
117
  r.uiHints({
76
118
  displayLabel: "Legal Pages",
@@ -79,8 +121,8 @@ export function createLegalPagesFeature(opts: LegalPagesOptions = {}): FeatureDe
79
121
  });
80
122
  r.requires("template-resolver");
81
123
 
82
- // 4 Public-HTML-Routes
83
- for (const route of LEGAL_ROUTES) {
124
+ // Public HTML routes (default: 4 DACH routes, configurable via opts.routes)
125
+ for (const route of routes) {
84
126
  r.httpRoute({
85
127
  method: "GET",
86
128
  path: route.path,
@@ -169,7 +211,7 @@ export function createLegalPagesFeature(opts: LegalPagesOptions = {}): FeatureDe
169
211
  runOnBoot: true,
170
212
  runIn: "api",
171
213
  },
172
- async (_payload, ctx) => runLegalPagesBootCheck(ctx),
214
+ async (_payload, ctx) => runLegalPagesBootCheck(ctx, requiredBlocks),
173
215
  );
174
216
 
175
217
  return {};
@@ -192,14 +234,17 @@ export type LegalPagesBootCheckCtx = {
192
234
  // nicht gewired ist (Hinweis auf fehlenden extraContext). Wirft Error
193
235
  // in NODE_ENV=production wenn Pflicht-Blocks fehlen, sonst log.warn.
194
236
  // Logged log.info wenn alles vorhanden ist (kein silent-skip).
195
- export async function runLegalPagesBootCheck(ctx: LegalPagesBootCheckCtx): Promise<void> {
237
+ export async function runLegalPagesBootCheck(
238
+ ctx: LegalPagesBootCheckCtx,
239
+ requiredBlocks: readonly LegalRequiredBlock[] = LEGAL_REQUIRED_BLOCKS,
240
+ ): Promise<void> {
196
241
  const templateResolver: TemplateResolverApi = requireTemplateResolver(
197
242
  ctx,
198
243
  "legal-pages-boot-check",
199
244
  );
200
245
  const missing: { slug: string; lang: string }[] = [];
201
246
 
202
- for (const required of LEGAL_REQUIRED_BLOCKS) {
247
+ for (const required of requiredBlocks) {
203
248
  const block = await templateResolver.findExact({
204
249
  tenantId: SYSTEM_TENANT_ID,
205
250
  slug: required.slug,
@@ -3,7 +3,9 @@ export {
3
3
  LEGAL_PAGES_FEATURE,
4
4
  LEGAL_REQUIRED_BLOCKS,
5
5
  LEGAL_ROUTES,
6
+ type LegalPageRoute,
6
7
  LegalPagesErrors,
8
+ type LegalRequiredBlock,
7
9
  } from "./constants";
8
10
  export {
9
11
  createLegalPagesFeature,