@cosmicdrift/kumiko-server-runtime 0.328.1 → 0.330.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.
Files changed (90) hide show
  1. package/README.md +15 -0
  2. package/dist/boot/apply-boot-seeds.d.ts +8 -0
  3. package/{src/boot/apply-boot-seeds.ts → dist/boot/apply-boot-seeds.js} +2 -10
  4. package/dist/boot/boot-crypto.d.ts +9 -0
  5. package/dist/boot/boot-crypto.js +39 -0
  6. package/dist/boot/job-run-logger.d.ts +19 -0
  7. package/dist/boot/job-run-logger.js +44 -0
  8. package/dist/boot/pii-event-backfill-on-boot.d.ts +13 -0
  9. package/dist/boot/pii-event-backfill-on-boot.js +25 -0
  10. package/dist/build-prod-bundle.d.ts +103 -0
  11. package/dist/build-prod-bundle.js +751 -0
  12. package/dist/bun-serve-options.d.ts +32 -0
  13. package/dist/bun-serve-options.js +49 -0
  14. package/dist/compose-features.d.ts +31 -0
  15. package/dist/compose-features.js +124 -0
  16. package/dist/extra-routes-deps.d.ts +30 -0
  17. package/dist/extra-routes-deps.js +4 -0
  18. package/dist/index.d.ts +6 -0
  19. package/dist/index.js +7 -0
  20. package/dist/pii-boot-gate.d.ts +13 -0
  21. package/dist/pii-boot-gate.js +46 -0
  22. package/dist/renderer-web-fonts.d.ts +9 -0
  23. package/{src/renderer-web-fonts.ts → dist/renderer-web-fonts.js} +14 -18
  24. package/dist/resolve-tailwind-cli.d.ts +14 -0
  25. package/dist/resolve-tailwind-cli.js +36 -0
  26. package/dist/run-prod-app-boot-context.d.ts +43 -0
  27. package/dist/run-prod-app-boot-context.js +198 -0
  28. package/dist/run-prod-app-static-files.d.ts +32 -0
  29. package/dist/run-prod-app-static-files.js +280 -0
  30. package/dist/run-prod-app.d.ts +471 -0
  31. package/dist/run-prod-app.js +741 -0
  32. package/dist/run-worker-app.d.ts +92 -0
  33. package/dist/run-worker-app.js +220 -0
  34. package/dist/security-headers.d.ts +20 -0
  35. package/dist/security-headers.js +49 -0
  36. package/dist/session-boot-gate.d.ts +11 -0
  37. package/dist/session-boot-gate.js +22 -0
  38. package/dist/session-wiring.d.ts +11 -0
  39. package/{src/session-wiring.ts → dist/session-wiring.js} +2 -6
  40. package/dist/try-hono-first.d.ts +24 -0
  41. package/{src/try-hono-first.ts → dist/try-hono-first.js} +8 -36
  42. package/package.json +102 -36
  43. package/src/changes.json +6 -0
  44. package/src/__tests__/boot-extra-context.test.ts +0 -163
  45. package/src/__tests__/boot-probe-fixture.ts +0 -64
  46. package/src/__tests__/build-prod-bundle.inprocess.test.ts +0 -233
  47. package/src/__tests__/build-prod-bundle.test.ts +0 -724
  48. package/src/__tests__/cache-headers.test.ts +0 -83
  49. package/src/__tests__/compose-features-mfa-wiring.integration.test.ts +0 -308
  50. package/src/__tests__/compose-features-wiring.integration.test.ts +0 -609
  51. package/src/__tests__/compose-features.test.ts +0 -206
  52. package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
  53. package/src/__tests__/pii-boot-gate.test.ts +0 -112
  54. package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
  55. package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
  56. package/src/__tests__/renderer-web-theme-plugin.integration.test.ts +0 -77
  57. package/src/__tests__/require-env.test.ts +0 -34
  58. package/src/__tests__/resolve-auth-mail.test.ts +0 -144
  59. package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
  60. package/src/__tests__/run-prod-app-dry-run.test.ts +0 -135
  61. package/src/__tests__/run-prod-app-env-source.test.ts +0 -118
  62. package/src/__tests__/run-prod-app-kms-slots.test.ts +0 -108
  63. package/src/__tests__/run-prod-app-spec.test.ts +0 -167
  64. package/src/__tests__/run-prod-app-static-files.test.ts +0 -298
  65. package/src/__tests__/run-prod-app.integration.test.ts +0 -1525
  66. package/src/__tests__/run-worker-app-boot.test.ts +0 -183
  67. package/src/__tests__/run-worker-app.integration.test.ts +0 -218
  68. package/src/__tests__/security-headers.test.ts +0 -88
  69. package/src/__tests__/session-boot-gate.test.ts +0 -70
  70. package/src/__tests__/session-wiring.test.ts +0 -16
  71. package/src/__tests__/try-hono-first.test.ts +0 -108
  72. package/src/__tests__/websocket-route.integration.test.ts +0 -580
  73. package/src/boot/__tests__/job-run-logger.integration.test.ts +0 -262
  74. package/src/boot/__tests__/job-run-logger.test.ts +0 -26
  75. package/src/boot/boot-crypto.ts +0 -67
  76. package/src/boot/job-run-logger.ts +0 -63
  77. package/src/boot/pii-event-backfill-on-boot.ts +0 -43
  78. package/src/build-prod-bundle.ts +0 -996
  79. package/src/bun-serve-options.ts +0 -110
  80. package/src/compose-features.ts +0 -197
  81. package/src/extra-routes-deps.ts +0 -38
  82. package/src/index.ts +0 -27
  83. package/src/pii-boot-gate.ts +0 -80
  84. package/src/resolve-tailwind-cli.ts +0 -45
  85. package/src/run-prod-app-boot-context.ts +0 -319
  86. package/src/run-prod-app-static-files.ts +0 -359
  87. package/src/run-prod-app.ts +0 -1420
  88. package/src/run-worker-app.ts +0 -366
  89. package/src/security-headers.ts +0 -67
  90. package/src/session-boot-gate.ts +0 -34
@@ -1,206 +0,0 @@
1
- // Unit-Tests für composeFeatures.authOptions — pinst dass die
2
- // passwordReset / emailVerification options an
3
- // createAuthEmailPasswordFeature durchgereicht werden, sodass die
4
- // request- und confirm-Handler im resultierenden Feature registriert
5
- // sind. Bug-Pattern: ohne diesen Wiring würde runProdApp.options.auth.
6
- // passwordReset = {sendResetEmail, appResetUrl} die routes mounten,
7
- // aber die Handler fehlen → POST /api/auth/request-password-reset
8
- // dispatched ins Leere → 500.
9
-
10
- import { describe, expect, spyOn, test } from "bun:test";
11
- import { securityBaselineFeatures } from "@cosmicdrift/kumiko-bundled-features/presets";
12
- import { createSessionsFeature } from "@cosmicdrift/kumiko-bundled-features/sessions";
13
- import { defineFeature, validateBoot } from "@cosmicdrift/kumiko-framework/engine";
14
- import { composeFeatures } from "../compose-features";
15
-
16
- const noopFeature = defineFeature("noop-app", () => {});
17
-
18
- // Mirrors what the create-kumiko-app picker hands back when the user
19
- // ticks an auto-mounted feature: a stub with the same name as a bundled
20
- // one. The dedupe path drops it and warns.
21
- const pickerAuthDupe = defineFeature("auth-email-password", () => {});
22
-
23
- const HMAC_SECRET = "test-secret-with-at-least-32-bytes-aaa";
24
-
25
- describe("composeFeatures", () => {
26
- test("includeBundled=false → nur App-Features", () => {
27
- const features = composeFeatures([noopFeature], { includeBundled: false });
28
- expect(features.map((f) => f.name)).toEqual(["noop-app"]);
29
- });
30
-
31
- test("includeBundled=true → 4 bundled Features davor", () => {
32
- const features = composeFeatures([noopFeature], { includeBundled: true });
33
- expect(features.map((f) => f.name)).toEqual([
34
- "config",
35
- "user",
36
- "tenant",
37
- "auth-email-password",
38
- "noop-app",
39
- ]);
40
- });
41
-
42
- test("authOptions.passwordReset → request-password-reset + reset-password handlers registriert", () => {
43
- const features = composeFeatures([noopFeature], {
44
- includeBundled: true,
45
- authOptions: { passwordReset: { hmacSecret: HMAC_SECRET, appUrl: "https://app/reset" } },
46
- });
47
- const auth = features.find((f) => f.name === "auth-email-password");
48
- expect(auth).toBeDefined();
49
- if (!auth) return;
50
- const handlerNames = Array.from(Object.keys(auth.writeHandlers));
51
- expect(handlerNames).toContain("request-password-reset");
52
- expect(handlerNames).toContain("reset-password");
53
- });
54
-
55
- test("authOptions.emailVerification → request-email-verification + verify-email handlers registriert", () => {
56
- const features = composeFeatures([noopFeature], {
57
- includeBundled: true,
58
- authOptions: { emailVerification: { hmacSecret: HMAC_SECRET, appUrl: "https://app/verify" } },
59
- });
60
- const auth = features.find((f) => f.name === "auth-email-password");
61
- expect(auth).toBeDefined();
62
- if (!auth) return;
63
- const handlerNames = Array.from(Object.keys(auth.writeHandlers));
64
- expect(handlerNames).toContain("request-email-verification");
65
- expect(handlerNames).toContain("verify-email");
66
- });
67
-
68
- test("authOptions.signup → signup-request + signup-confirm handlers registriert", () => {
69
- const features = composeFeatures([noopFeature], {
70
- includeBundled: true,
71
- authOptions: { signup: { appUrl: "https://app/signup/complete" } },
72
- });
73
- const auth = features.find((f) => f.name === "auth-email-password");
74
- expect(auth).toBeDefined();
75
- if (!auth) return;
76
- const handlerNames = Array.from(Object.keys(auth.writeHandlers));
77
- expect(handlerNames).toContain("signup-request");
78
- expect(handlerNames).toContain("signup-confirm");
79
- });
80
-
81
- // Regression: signup-request no-ops silently (always-200 anti-enumeration
82
- // contract) unless "auth-self-registration" is mounted. composeFeatures
83
- // must bundle it alongside authOptions.signup, not leave apps using the
84
- // includeBundled convenience path to mount it by hand.
85
- test("authOptions.signup → auth-self-registration mounted, default ON", () => {
86
- const features = composeFeatures([noopFeature], {
87
- includeBundled: true,
88
- authOptions: { signup: { appUrl: "https://app/signup/complete" } },
89
- });
90
- const toggle = features.find((f) => f.name === "auth-self-registration");
91
- expect(toggle).toBeDefined();
92
- // The security-relevant part isn't that the feature is mounted, it's
93
- // that self-signup defaults ON — a flip to `default: false` upstream
94
- // would silently no-op signup (always-200 anti-enumeration masks it)
95
- // while this assertion alone (toBeDefined) stayed green.
96
- expect(toggle?.toggleableDefault).toBe(true);
97
- });
98
-
99
- test("no authOptions.signup → auth-self-registration NOT mounted", () => {
100
- const features = composeFeatures([noopFeature], { includeBundled: true });
101
- expect(features.map((f) => f.name)).not.toContain("auth-self-registration");
102
- });
103
-
104
- test("authOptions.signup + app also mounts its own auth-self-registration stub → deduped to exactly one", () => {
105
- const pickerSelfRegDupe = defineFeature("auth-self-registration", () => {});
106
- const features = composeFeatures([noopFeature, pickerSelfRegDupe], {
107
- includeBundled: true,
108
- authOptions: { signup: { appUrl: "https://app/signup/complete" } },
109
- });
110
- const names = features.map((f) => f.name).filter((n) => n === "auth-self-registration");
111
- expect(names).toEqual(["auth-self-registration"]);
112
- });
113
-
114
- test("OHNE authOptions → KEINE reset/verify-handlers (anti-default-deploy-bug)", () => {
115
- // Genau der Bug der vom Review-Agent gefangen wurde: composeFeatures
116
- // ohne authOptions registriert die handler nicht. Wenn jemand das
117
- // versehentlich vergisst und nur die routes (auth-routes-config)
118
- // wired, schlagen die requests auf prod fehl. Der Test pinst dass
119
- // dieser default-deny-Pfad bewusst leer ist.
120
- const features = composeFeatures([noopFeature], { includeBundled: true });
121
- const auth = features.find((f) => f.name === "auth-email-password");
122
- expect(auth).toBeDefined();
123
- if (!auth) return;
124
- const handlerNames = Array.from(Object.keys(auth.writeHandlers));
125
- expect(handlerNames).not.toContain("request-password-reset");
126
- expect(handlerNames).not.toContain("reset-password");
127
- expect(handlerNames).not.toContain("request-email-verification");
128
- expect(handlerNames).not.toContain("verify-email");
129
- // Die regulären auth-handlers (login/logout/change-password) MÜSSEN
130
- // aber immer da sein — das ist der core-flow.
131
- expect(handlerNames).toContain("login");
132
- expect(handlerNames).toContain("logout");
133
- });
134
-
135
- // fw#2223 regression: without this wiring, includeBundled apps that DO
136
- // configure authOptions.invite still got /members with no invite button —
137
- // the screen is bound to auth-email-password's invite-create handler,
138
- // which only exists when opts.invite is set (feature.ts:238-239).
139
- test("authOptions.invite → tenant bekommt invite-Screen + Toolbar-Action; ohne invite bootet sauber ohne beides", () => {
140
- const withInvite = composeFeatures([noopFeature], {
141
- includeBundled: true,
142
- authOptions: { invite: { appUrl: "https://app/invite" } },
143
- });
144
- const tenantWith = withInvite.find((f) => f.name === "tenant");
145
- expect(tenantWith).toBeDefined();
146
- if (!tenantWith) return;
147
- const membersScreenWith = tenantWith.screens["members"];
148
- if (membersScreenWith?.type !== "projectionList") {
149
- throw new Error("expected members screen to be projectionList");
150
- }
151
- expect(membersScreenWith.toolbarActions?.some((a) => a.id === "invite")).toBe(true);
152
- expect(tenantWith.screens["invite-create"]).toBeDefined();
153
-
154
- const withoutInvite = composeFeatures([noopFeature], { includeBundled: true });
155
- const tenantWithout = withoutInvite.find((f) => f.name === "tenant");
156
- expect(tenantWithout).toBeDefined();
157
- if (!tenantWithout) return;
158
- const membersScreenWithout = tenantWithout.screens["members"];
159
- if (membersScreenWithout?.type !== "projectionList") {
160
- throw new Error("expected members screen to be projectionList");
161
- }
162
- expect(membersScreenWithout.toolbarActions ?? []).toHaveLength(0);
163
- expect(tenantWithout.screens["invite-create"]).toBeUndefined();
164
- expect(() => validateBoot(withoutInvite)).not.toThrow();
165
- });
166
-
167
- test("app feature duplicating a bundled name is dropped (no createRegistry crash)", () => {
168
- // create-kumiko-app's picker hands back createAuthEmailPasswordFeature()
169
- // because the user ticked it in the recommended set; runDevApp then adds
170
- // its OWN bundled copy via includeBundled:true, and createRegistry throws
171
- // "Duplicate feature: auth-email-password". The dedupe path keeps the
172
- // bundled instance (it carries authOptions wiring) and drops the app stub.
173
- const warnSpy = spyOn(console, "warn").mockImplementation(() => {});
174
- const features = composeFeatures([pickerAuthDupe, noopFeature], {
175
- includeBundled: true,
176
- });
177
- // The warn is part of the fix contract (changeset: "warn so the user can
178
- // remove the line") — without this assertion the warn is removable with no RED.
179
- expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("auth-email-password"));
180
- warnSpy.mockRestore();
181
- expect(features.map((f) => f.name)).toEqual([
182
- "config",
183
- "user",
184
- "tenant",
185
- "auth-email-password",
186
- "noop-app",
187
- ]);
188
- // The kept instance is the bundled one — confirmed by the presence of
189
- // login/logout handlers (the picker stub has none).
190
- const auth = features.find((f) => f.name === "auth-email-password");
191
- expect(auth).toBeDefined();
192
- if (!auth) return;
193
- expect(Object.keys(auth.writeHandlers)).toContain("login");
194
- });
195
-
196
- test("includeBundled=false → sessions mounted via preset + standalone collapses to one entry", () => {
197
- const sessionsInstances = securityBaselineFeatures();
198
- const standaloneSessions = createSessionsFeature();
199
- const features = composeFeatures([...sessionsInstances, standaloneSessions], {
200
- includeBundled: false,
201
- });
202
- const sessionsEntries = features.filter((f) => f.name === "sessions");
203
- expect(sessionsEntries).toHaveLength(1);
204
- expect(sessionsEntries[0]).toBe(sessionsInstances[0]);
205
- });
206
- });
@@ -1,158 +0,0 @@
1
- // Pins the boot-wiring contract for config-seeds. runDevApp's onAfterSetup
2
- // and runProdApp's seed-block both call `applyBootSeeds(...)` — this test
3
- // calls the SAME helper, so if someone removes the call site from
4
- // runDevApp / runProdApp the helper still has at least one caller (this
5
- // test). Code review then sees an orphaned helper, not a silently broken
6
- // boot. For a stricter end-to-end pin you'd start an actual runDevApp;
7
- // that's heavy and not done here.
8
- //
9
- // Tests:
10
- // 1. seed rows land in the projection after applyBootSeeds runs,
11
- // 2. a re-boot is a no-op (idempotent),
12
- // 3. an admin set on top of a seed wins the resolver cascade; coexistence
13
- // vs. override semantics depend on the admin user's tenantId.
14
-
15
- import { afterAll, beforeAll, describe, expect, test } from "bun:test";
16
- import {
17
- configValuesTable,
18
- createConfigAccessorFactory,
19
- createConfigFeature,
20
- createConfigResolver,
21
- } from "@cosmicdrift/kumiko-bundled-features/config";
22
- import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
23
- import {
24
- access,
25
- createSystemConfig,
26
- createSystemSeed,
27
- createTenantConfig,
28
- createTenantSeed,
29
- defineFeature,
30
- } from "@cosmicdrift/kumiko-framework/engine";
31
- import {
32
- setupTestStack,
33
- type TestStack,
34
- TestUsers,
35
- unsafePushTables,
36
- } from "@cosmicdrift/kumiko-framework/stack";
37
- import { applyBootSeeds } from "../boot/apply-boot-seeds";
38
-
39
- const bootSeedsFeature = defineFeature("boot-seeds-test", (r) => {
40
- r.requires("config");
41
- r.config({
42
- keys: {
43
- siteName: createTenantConfig("text", {
44
- default: "DEFAULT_SITE",
45
- read: access.all,
46
- write: access.all,
47
- }),
48
- maintenance: createSystemConfig("boolean", {
49
- default: false,
50
- read: access.all,
51
- write: access.systemAdmin,
52
- }),
53
- },
54
- seeds: {
55
- siteName: createTenantSeed({ value: "from-seed" }),
56
- maintenance: createSystemSeed({ value: true }),
57
- },
58
- });
59
- });
60
-
61
- const SITE_KEY = "boot-seeds-test:config:site-name";
62
- const MAINT_KEY = "boot-seeds-test:config:maintenance";
63
-
64
- let stack: TestStack;
65
- const resolver = createConfigResolver();
66
-
67
- beforeAll(async () => {
68
- stack = await setupTestStack({
69
- features: [createConfigFeature(), bootSeedsFeature],
70
- extraContext: ({ registry }) => ({
71
- configResolver: resolver,
72
- _configAccessorFactory: createConfigAccessorFactory(registry, resolver),
73
- }),
74
- });
75
- await unsafePushTables(stack.db, { configValuesTable });
76
- });
77
-
78
- afterAll(async () => {
79
- await stack.cleanup();
80
- });
81
-
82
- describe("config-seed boot wiring", () => {
83
- test("first boot: applyBootSeeds writes one row per seed", async () => {
84
- await applyBootSeeds({ registry: stack.registry, db: stack.db });
85
-
86
- const rows = await selectMany(stack.db, configValuesTable);
87
- expect(rows.length).toBe(2);
88
-
89
- const siteKeyDef = stack.registry.getConfigKey(SITE_KEY);
90
- expect(siteKeyDef).toBeDefined();
91
- const sitePeek = await resolver.get(
92
- SITE_KEY,
93
- siteKeyDef!,
94
- TestUsers.systemAdmin.tenantId,
95
- TestUsers.systemAdmin.id,
96
- stack.db,
97
- );
98
- expect(sitePeek).toBe("from-seed");
99
-
100
- const maintKeyDef = stack.registry.getConfigKey(MAINT_KEY);
101
- expect(maintKeyDef).toBeDefined();
102
- const maintPeek = await resolver.get(
103
- MAINT_KEY,
104
- maintKeyDef!,
105
- TestUsers.systemAdmin.tenantId,
106
- TestUsers.systemAdmin.id,
107
- stack.db,
108
- );
109
- expect(maintPeek).toBe(true);
110
- });
111
-
112
- test("re-boot: idempotent — every seed already on disk → no extra rows", async () => {
113
- await applyBootSeeds({ registry: stack.registry, db: stack.db });
114
-
115
- const rows = await selectMany(stack.db, configValuesTable);
116
- expect(rows.length).toBe(2);
117
- });
118
-
119
- test("admin set on top of seed wins resolver — Re-Boot preserves admin", async () => {
120
- // siteName is a TENANT-scope key. The seed writes a row under
121
- // SYSTEM_TENANT_ID (= "for all tenants"). An admin on a real tenant
122
- // writes a row under THAT tenantId — higher specificity. Both rows
123
- // coexist; the resolver returns the more specific one.
124
- //
125
- // If the admin happens to write as SYSTEM_TENANT_ID (e.g. test user
126
- // is the system-admin on the system-tenant), the admin write hits
127
- // the seed-row directly and updates the same aggregate stream. Both
128
- // paths end up with the admin value winning — the row-count
129
- // assertion makes the path explicit.
130
- await stack.http.writeOk(
131
- "config:write:set",
132
- { key: SITE_KEY, value: "admin-override", scope: "tenant" },
133
- TestUsers.systemAdmin,
134
- );
135
-
136
- await applyBootSeeds({ registry: stack.registry, db: stack.db });
137
-
138
- const siteKeyDef = stack.registry.getConfigKey(SITE_KEY);
139
- expect(siteKeyDef).toBeDefined();
140
- const peek = await resolver.get(
141
- SITE_KEY,
142
- siteKeyDef!,
143
- TestUsers.systemAdmin.tenantId,
144
- TestUsers.systemAdmin.id,
145
- stack.db,
146
- );
147
- expect(peek).toBe("admin-override");
148
-
149
- // Row-count tells us which path was hit:
150
- // - 2 rows = override path (admin tenantId === SYSTEM_TENANT_ID,
151
- // updated the seed-stream in place).
152
- // - 3 rows = coexistence path (admin tenantId !== SYSTEM_TENANT_ID,
153
- // new specific-tenant row sits next to the seed system-row).
154
- // Either is correct as long as the resolver picks the admin value.
155
- const rows = await selectMany(stack.db, configValuesTable);
156
- expect([2, 3]).toContain(rows.length);
157
- });
158
- });
@@ -1,112 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
3
- import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
4
- import * as z from "zod";
5
- import { assertPiiBootInvariants } from "../pii-boot-gate";
6
-
7
- const piiFeature = defineFeature("gate-pii", (r) => {
8
- r.entity(
9
- "person",
10
- createEntity({
11
- table: "read_gate_persons",
12
- fields: { email: createTextField({ required: true, personal: "self", find: "exact" }) },
13
- }),
14
- );
15
- });
16
- const plainFeature = defineFeature("gate-plain", (r) => {
17
- r.entity(
18
- "thing",
19
- createEntity({
20
- table: "read_gate_things",
21
- fields: { name: createTextField({ personal: false, reason: "test_fixture" }) },
22
- }),
23
- );
24
- });
25
-
26
- // Entity-free features: the gate must react to the event stance on its own,
27
- // not ride along on an entity annotation (fw#2776).
28
- const piiEventFeature = defineFeature("gate-pii-event", (r) => {
29
- r.defineEvent("attempt", z.object({ userId: z.string(), email: z.string() }), {
30
- piiFields: { email: { personal: { of: "userId" } } },
31
- });
32
- });
33
- const noneEventFeature = defineFeature("gate-none-event", (r) => {
34
- r.defineEvent("ping", z.object({ at: z.string() }), { piiFields: "none" });
35
- });
36
-
37
- const kms = new InMemoryKmsAdapter();
38
- const KEY = Buffer.alloc(32, 7).toString("base64");
39
-
40
- describe("assertPiiBootInvariants — prod", () => {
41
- test("PII without a KMS aborts boot", () => {
42
- expect(() => assertPiiBootInvariants([piiFeature], { mode: "prod" })).toThrow(
43
- /BOOT ABORTED.*PLAINTEXT.*allowPlaintextPii/s,
44
- );
45
- });
46
-
47
- test("explicit allowPlaintextPii downgrades to a warning", () => {
48
- expect(() =>
49
- assertPiiBootInvariants([piiFeature], {
50
- mode: "prod",
51
- allowPlaintextPii: "kms rollout pending, infra#188",
52
- }),
53
- ).not.toThrow();
54
- });
55
-
56
- test("KMS + blindIndexKey boots", () => {
57
- expect(() =>
58
- assertPiiBootInvariants([piiFeature], { mode: "prod", kms, blindIndexKey: KEY }),
59
- ).not.toThrow();
60
- });
61
-
62
- test("KMS without blindIndexKey aborts when lookupable fields exist", () => {
63
- expect(() => assertPiiBootInvariants([piiFeature], { mode: "prod", kms })).toThrow(
64
- /blindIndexKey/,
65
- );
66
- });
67
-
68
- test("no PII entities → nothing to gate", () => {
69
- expect(() => assertPiiBootInvariants([plainFeature], { mode: "prod" })).not.toThrow();
70
- });
71
-
72
- test('an entity-free feature with a non-"none" event stance aborts boot without a KMS', () => {
73
- expect(() => assertPiiBootInvariants([piiEventFeature], { mode: "prod" })).toThrow(
74
- /BOOT ABORTED.*gate-pii-event:event:attempt.*PLAINTEXT/s,
75
- );
76
- });
77
-
78
- test('events declaring piiFields: "none" are not gated', () => {
79
- expect(() => assertPiiBootInvariants([noneEventFeature], { mode: "prod" })).not.toThrow();
80
- });
81
-
82
- test("a KMS satisfies the event gate", () => {
83
- expect(() =>
84
- assertPiiBootInvariants([piiEventFeature], { mode: "prod", kms, blindIndexKey: KEY }),
85
- ).not.toThrow();
86
- });
87
-
88
- test("allowPlaintextPii acknowledges the event gate too", () => {
89
- expect(() =>
90
- assertPiiBootInvariants([piiEventFeature], {
91
- mode: "prod",
92
- allowPlaintextPii: "kms rollout pending",
93
- }),
94
- ).not.toThrow();
95
- });
96
- });
97
-
98
- describe("assertPiiBootInvariants — dev", () => {
99
- test("PII without a KMS only warns in dev", () => {
100
- expect(() => assertPiiBootInvariants([piiFeature], { mode: "dev" })).not.toThrow();
101
- });
102
-
103
- test("KMS without blindIndexKey aborts in dev too (lookups broken in any mode)", () => {
104
- expect(() => assertPiiBootInvariants([piiFeature], { mode: "dev", kms })).toThrow(
105
- /blindIndexKey/,
106
- );
107
- });
108
-
109
- test("a PII event without a KMS only warns in dev", () => {
110
- expect(() => assertPiiBootInvariants([piiEventFeature], { mode: "dev" })).not.toThrow();
111
- });
112
- });
@@ -1,85 +0,0 @@
1
- // Relocation-Test für den @source-Layout-Fix (#359).
2
- //
3
- // Der Bug: renderer-web/src/styles.css scannte seine eigenen Shell-Klassen
4
- // über einen MONOREPO-relativen @source (`../../renderer-web/src`). Im
5
- // Workspace löst der via Symlink auf (grün), in einem Standalone-Consumer
6
- // (echtes node_modules ohne Monorepo-Geschwister) findet er nichts → unstyled
7
- // prod (15KB statt 48KB). Der Fix macht die Zeile self-relativ (`./`), weil
8
- // das Paket `src` shippt.
9
- //
10
- // Am REALEN Ort der styles.css sind `./` und `../../renderer-web/src`
11
- // identisch — ein Test dort wäre grün mit Bug UND Fix. Der Diskriminator ist
12
- // RELOCATION: wir kopieren die echte styles.css an einen Ort, an dem der alte
13
- // Pfad tot ist und nur `./` greift. Der Ort liegt unter dem Repo-Root, damit
14
- // `@import "tailwindcss"` / `react-day-picker` weiter über node_modules
15
- // auflösen. Braucht Bun (Bun.spawn im Tailwind-One-Shot) — sonst silent skip.
16
-
17
- import { describe, expect, test } from "bun:test";
18
- import { execFileSync } from "node:child_process";
19
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
20
- import { dirname, join, resolve } from "node:path";
21
- import { fileURLToPath } from "node:url";
22
- import { runTailwindOnce } from "../build-prod-bundle";
23
-
24
- const __dirname = dirname(fileURLToPath(import.meta.url));
25
- const REPO_ROOT = resolve(__dirname, "../../../..");
26
- const RENDERER_WEB_STYLES = resolve(__dirname, "../../../renderer-web/src/styles.css");
27
- const SELF_SOURCE = '@source "./**/*.{ts,tsx}";';
28
- const MONOREPO_SOURCE = '@source "../../renderer-web/src/**/*.{ts,tsx}";';
29
-
30
- function bunAvailable(): boolean {
31
- try {
32
- execFileSync("bun", ["--version"], { stdio: "ignore" });
33
- return true;
34
- } catch {
35
- return false;
36
- }
37
- }
38
-
39
- describe("renderer-web styles.css @source relocation (#359)", () => {
40
- // skipIf statt `if (!bunAvailable()) return;` — sonst meldet der Runner den
41
- // Test grün, obwohl er nie lief (silent-pass verschleiert fehlende Coverage).
42
- test.skipIf(!bunAvailable())(
43
- "self-relative @source scans the shell standalone; monorepo path would not",
44
- async () => {
45
- // Tailwind v4 auto-scannt nur das cwd-Verzeichnis (empirisch bestätigt:
46
- // das Verzeichnis der Input-CSS wird NICHT automatisch gescannt). Im echten
47
- // Standalone-Consumer ist renderer-web zudem in node_modules (gitignored) →
48
- // ebenfalls nicht auto-gescannt. Die Shell-Klassen erreicht also NUR der
49
- // explizite @source der styles.css. Hier mirrorn wir das robust, ohne
50
- // gitignore-/node_modules-Semantik: das Paket-`src` (mit der Shell-Probe)
51
- // liegt AUSSERHALB des Build-cwd — nur der self-relative `@source "./**"`
52
- // der relozierten styles.css erreicht es, der monorepo-Pfad ist tot.
53
- // Temp unter REPO_ROOT, damit @import "tailwindcss"/react-day-picker via
54
- // node_modules auflösen.
55
- const dir = await mkdtemp(join(REPO_ROOT, ".reloc-rendererweb-"));
56
- const buildCwd = join(dir, "app");
57
- const pkgSrc = join(dir, "pkg");
58
- try {
59
- const realCss = await readFile(RENDERER_WEB_STYLES, "utf8");
60
- // Guard: der Fix MUSS in der Quelle stehen, sonst testet die Relocation nichts.
61
- expect(realCss).toContain(SELF_SOURCE);
62
-
63
- await mkdir(buildCwd, { recursive: true });
64
- await mkdir(join(pkgSrc, "layout"), { recursive: true });
65
- await writeFile(
66
- join(pkgSrc, "layout/probe.tsx"),
67
- `export const P = () => <div className="min-h-screen" />;\n`,
68
- );
69
-
70
- await writeFile(join(pkgSrc, "styles.css"), realCss);
71
- const fixed = await runTailwindOnce(join(pkgSrc, "styles.css"), buildCwd);
72
- expect(fixed).toContain("min-h-screen");
73
-
74
- // Diskriminator: mit dem ALTEN monorepo-relativen @source ist die Probe
75
- // an diesem relozierten Ort unerreichbar → Sentinel fehlt. Beweist, dass
76
- // der Fix kein No-op ist (der Test würde bei einem Revert rot).
77
- await writeFile(join(pkgSrc, "styles.css"), realCss.replace(SELF_SOURCE, MONOREPO_SOURCE));
78
- const buggy = await runTailwindOnce(join(pkgSrc, "styles.css"), buildCwd);
79
- expect(buggy).not.toContain("min-h-screen");
80
- } finally {
81
- await rm(dir, { recursive: true, force: true });
82
- }
83
- },
84
- );
85
- });
@@ -1,35 +0,0 @@
1
- // Unit-Test für den CSS-Completeness-Guard (#359): nach dem Tailwind-One-Shot
2
- // prüft der Build, ob das Output-CSS die renderer-web-Shell-Sentinel-Klasse
3
- // enthält — aber NUR wenn auf das gepackte renderer-web-styles.css
4
- // zurückgefallen wurde. Fehlt sie dort, würde prod unstyled rendern → laut
5
- // failen mit Hinweis auf src/styles.css.
6
-
7
- import { describe, expect, test } from "bun:test";
8
- import { assertRendererWebShellPresent } from "../build-prod-bundle";
9
-
10
- const fallback = {
11
- path: "/app/node_modules/renderer-web/src/styles.css",
12
- isRendererWebFallback: true,
13
- };
14
- const appOwned = { path: "/app/src/styles.css", isRendererWebFallback: false };
15
-
16
- describe("assertRendererWebShellPresent (#359 CSS-completeness guard)", () => {
17
- test("fallback + fehlende Shell-Sentinel-Klasse → wirft mit src/styles.css-Hinweis", () => {
18
- expect(() => assertRendererWebShellPresent(".some-other{display:flex}", fallback)).toThrow(
19
- /min-h-screen/,
20
- );
21
- expect(() => assertRendererWebShellPresent(".some-other{display:flex}", fallback)).toThrow(
22
- /src\/styles\.css/,
23
- );
24
- });
25
-
26
- test("fallback + Shell-Sentinel vorhanden → kein Throw", () => {
27
- expect(() =>
28
- assertRendererWebShellPresent(".min-h-screen{min-height:100vh}", fallback),
29
- ).not.toThrow();
30
- });
31
-
32
- test("app-eigenes styles.css (kein Fallback) → nie asserten, auch ohne Sentinel", () => {
33
- expect(() => assertRendererWebShellPresent("", appOwned)).not.toThrow();
34
- });
35
- });
@@ -1,77 +0,0 @@
1
- // The app theme plugin only works if its base-layer output lands AFTER the framework's light
2
- // palette (`@layer base :root:not(.dark)`); same selector, same layer, so source order decides.
3
- // This runs the real Tailwind one-shot over an app stylesheet that imports the framework CSS and
4
- // loads a theme module through the published `theme-plugin` subpath.
5
-
6
- import { describe, expect, test } from "bun:test";
7
- import { execFileSync } from "node:child_process";
8
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
9
- import { dirname, join, resolve } from "node:path";
10
- import { fileURLToPath } from "node:url";
11
- import { runTailwindOnce } from "../build-prod-bundle";
12
-
13
- const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../../..");
14
- const FRAMEWORK_LIGHT_PRIMARY = "--color-primary:#1a1c1e";
15
-
16
- function bunAvailable(): boolean {
17
- try {
18
- execFileSync("bun", ["--version"], { stdio: "ignore" });
19
- return true;
20
- } catch {
21
- return false;
22
- }
23
- }
24
-
25
- describe("renderer-web theme plugin in a real Tailwind build", () => {
26
- test.skipIf(!bunAvailable())(
27
- "app theme overrides the framework light palette, sets dark values and adds utilities for new colors",
28
- async () => {
29
- const dir = await mkdtemp(join(REPO_ROOT, ".theme-plugin-"));
30
- try {
31
- await writeFile(
32
- join(dir, "theme.ts"),
33
- `import { createThemePlugin, defineAppTheme } from "@cosmicdrift/kumiko-renderer-web/theme-plugin";
34
- export default createThemePlugin(defineAppTheme({
35
- colors: {
36
- primary: { light: "#0a7d5a", dark: "#34d399" },
37
- "brand-soft": "#e0f5ee",
38
- },
39
- shadows: { card: "none" },
40
- fonts: { sans: "Manrope, sans-serif" },
41
- }));
42
- `,
43
- );
44
- await writeFile(
45
- join(dir, "probe.tsx"),
46
- `export const Probe = () => <div className="bg-brand-soft text-primary" />;\n`,
47
- );
48
- await writeFile(
49
- join(dir, "styles.css"),
50
- `@import "@cosmicdrift/kumiko-renderer-web/styles.css";\n@plugin "./theme.ts";\n@source "./probe.tsx";\n`,
51
- );
52
-
53
- const css = await runTailwindOnce(join(dir, "styles.css"), dir);
54
-
55
- const frameworkLight = css.indexOf(FRAMEWORK_LIGHT_PRIMARY);
56
- const appLight = css.indexOf("--color-primary:#0a7d5a");
57
- expect(frameworkLight).toBeGreaterThan(-1);
58
- expect(appLight).toBeGreaterThan(frameworkLight);
59
-
60
- expect(css).toMatch(/\.dark\{[^}]*--color-primary:#34d399/);
61
- expect(css).toMatch(/\.bg-brand-soft\{background-color:var\(--color-brand-soft\)\}/);
62
-
63
- const frameworkCardShadow = css.indexOf("--card-shadow:0 1px 3px");
64
- const appCardShadow = css.indexOf("--card-shadow:none");
65
- expect(frameworkCardShadow).toBeGreaterThan(-1);
66
- expect(appCardShadow).toBeGreaterThan(frameworkCardShadow);
67
- // An unlayered framework :root would beat the plugin's base layer despite the order.
68
- expect(css).toMatch(/:root\{[^}]*--kumiko-grid-row-h/);
69
- expect(css).not.toMatch(/:root\{[^}]*--kumiko-grid-row-h[^}]*--card-/);
70
- expect(css).not.toMatch(/:root\{[^}]*--card-[^}]*--kumiko-grid-row-h/);
71
- expect(css).toMatch(/body\{font-family:var\(--font-sans\)\}/);
72
- } finally {
73
- await rm(dir, { recursive: true, force: true });
74
- }
75
- },
76
- );
77
- });