@cosmicdrift/kumiko-dev-server 0.152.0 → 0.153.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 (48) hide show
  1. package/package.json +5 -8
  2. package/src/__tests__/build-prod-bundle.integration.test.ts +1 -1
  3. package/src/__tests__/compose-stacks.test.ts +1 -1
  4. package/src/__tests__/discover-format.test.ts +1 -1
  5. package/src/__tests__/env-schema.integration.test.ts +1 -1
  6. package/src/__tests__/walkthrough.integration.test.ts +1 -1
  7. package/src/build.ts +1 -1
  8. package/src/create-kumiko-server.ts +12 -6
  9. package/src/index.ts +4 -12
  10. package/src/run-dev-app.ts +12 -8
  11. package/src/scaffold-app.ts +9 -4
  12. package/src/schema-apply.ts +4 -1
  13. package/src/schema-check-core.ts +1 -1
  14. package/src/setup-test-stack-from-features.ts +4 -1
  15. package/src/__tests__/boot-extra-context.test.ts +0 -140
  16. package/src/__tests__/build-prod-bundle.test.ts +0 -278
  17. package/src/__tests__/cache-headers.test.ts +0 -83
  18. package/src/__tests__/compose-features-mfa-wiring.integration.test.ts +0 -308
  19. package/src/__tests__/compose-features-wiring.integration.test.ts +0 -382
  20. package/src/__tests__/compose-features.test.ts +0 -128
  21. package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
  22. package/src/__tests__/inject-schema.test.ts +0 -62
  23. package/src/__tests__/pii-boot-gate.test.ts +0 -68
  24. package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
  25. package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
  26. package/src/__tests__/require-env.test.ts +0 -29
  27. package/src/__tests__/resolve-auth-mail.test.ts +0 -69
  28. package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
  29. package/src/__tests__/run-prod-app-env-source.test.ts +0 -157
  30. package/src/__tests__/run-prod-app-spec.test.ts +0 -57
  31. package/src/__tests__/run-prod-app.integration.test.ts +0 -915
  32. package/src/__tests__/session-wiring.test.ts +0 -51
  33. package/src/__tests__/try-hono-first.test.ts +0 -63
  34. package/src/boot/__tests__/job-run-logger.test.ts +0 -26
  35. package/src/boot/apply-boot-seeds.ts +0 -19
  36. package/src/boot/boot-crypto.ts +0 -82
  37. package/src/boot/job-run-logger.ts +0 -51
  38. package/src/build-prod-bundle.ts +0 -692
  39. package/src/compose-features.ts +0 -164
  40. package/src/extra-routes-deps.ts +0 -47
  41. package/src/inject-schema.ts +0 -30
  42. package/src/pii-boot-gate.ts +0 -62
  43. package/src/resolve-tailwind-cli.ts +0 -45
  44. package/src/run-prod-app-boot-context.ts +0 -241
  45. package/src/run-prod-app-static-files.ts +0 -273
  46. package/src/run-prod-app.ts +0 -1158
  47. package/src/session-wiring.ts +0 -29
  48. package/src/try-hono-first.ts +0 -46
@@ -1,51 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import {
3
- createSessionsFeature,
4
- SESSIONS_FEATURE,
5
- } from "@cosmicdrift/kumiko-bundled-features/sessions";
6
- import { resolveProdSessionsConfig, shouldWireProdSessions } from "../session-wiring";
7
-
8
- describe("shouldWireProdSessions — secure-by-default with opt-out (KF-1)", () => {
9
- it("wires sessions when the feature is mounted, even without an explicit config", () => {
10
- // The publicstatus bug: sessions feature mounted + auth set, but no auth.sessions —
11
- // previously left stateless (no revocation). Now it wires automatically.
12
- expect(shouldWireProdSessions(true, true, undefined)).toBe(true);
13
- });
14
-
15
- it("wires sessions when a config object is given", () => {
16
- expect(shouldWireProdSessions(true, true, { expiresInMs: 1000 })).toBe(true);
17
- });
18
-
19
- it("does not wire when sessions: false (explicit opt-out)", () => {
20
- expect(shouldWireProdSessions(true, true, false)).toBe(false);
21
- });
22
-
23
- it("does not wire when the sessions feature is not mounted", () => {
24
- expect(shouldWireProdSessions(true, false, undefined)).toBe(false);
25
- });
26
-
27
- it("does not wire when the app has no auth at all", () => {
28
- expect(shouldWireProdSessions(false, true, undefined)).toBe(false);
29
- });
30
- });
31
-
32
- describe("SESSIONS_FEATURE constant matches the real feature name", () => {
33
- it("createSessionsFeature()'s name equals SESSIONS_FEATURE", () => {
34
- // shouldWireProdSessions's own arm only tests the pure boolean helper —
35
- // the actual run-prod-app.ts integration seam
36
- // (`features.some((f) => f.name === SESSIONS_FEATURE)`) drifts silently
37
- // if the feature is ever renamed without updating this constant.
38
- expect(createSessionsFeature().name).toBe(SESSIONS_FEATURE);
39
- });
40
- });
41
-
42
- describe("resolveProdSessionsConfig", () => {
43
- it("passes a config object through", () => {
44
- expect(resolveProdSessionsConfig({ expiresInMs: 5000 })).toEqual({ expiresInMs: 5000 });
45
- });
46
-
47
- it("collapses false / undefined to defaults", () => {
48
- expect(resolveProdSessionsConfig(undefined)).toEqual({});
49
- expect(resolveProdSessionsConfig(false)).toEqual({});
50
- });
51
- });
@@ -1,63 +0,0 @@
1
- // Pure-function pin für tryHonoFirst. Trivial aber load-bearing:
2
- // Drift zwischen dev (createKumikoServer) und prod (runProdApp) hat
3
- // schon einen Bug verursacht (legal-pages funktionierten in prod aber
4
- // nicht in dev). Beide nutzen jetzt diesen helper — wenn die Semantik
5
- // sich ändert (z.B. "matched" auch für 4xx anders als 404), MÜSSEN
6
- // beide Pfade synchron updaten.
7
-
8
- import { describe, expect, test } from "bun:test";
9
- import { type HonoLikeApp, tryHonoFirst } from "../try-hono-first";
10
-
11
- function makeApp(response: Response): HonoLikeApp {
12
- return { fetch: () => response };
13
- }
14
-
15
- function makeAsyncApp(response: Response): HonoLikeApp {
16
- return { fetch: async () => response };
17
- }
18
-
19
- describe("tryHonoFirst", () => {
20
- test("matched=true bei 200 (Hono-route greift)", async () => {
21
- const app = makeApp(new Response("ok", { status: 200 }));
22
- const res = await tryHonoFirst(app, new Request("http://test/foo"));
23
- expect(res.matched).toBe(true);
24
- expect(res.response.status).toBe(200);
25
- });
26
-
27
- test("matched=false bei 404 (keine Route — caller fällt auf SPA-fallback)", async () => {
28
- const app = makeApp(new Response("not found", { status: 404 }));
29
- const res = await tryHonoFirst(app, new Request("http://test/unknown"));
30
- expect(res.matched).toBe(false);
31
- // response wird trotzdem zurückgegeben — caller kann den 404 als
32
- // letztes Sicherheitsnetz nutzen wenn auch SPA-fallback nichts liefert.
33
- expect(res.response.status).toBe(404);
34
- });
35
-
36
- test("matched=true bei 401/403/500 (Hono hat geantwortet — kein SPA-fallback)", async () => {
37
- // Bug-Pin: matched darf NUR bei status=404 false sein. Wenn Hono
38
- // 401 (auth required) returnt, war die Route klar gefunden + hat
39
- // bewusst rejected — SPA-fallback würde das überschreiben und den
40
- // User auf eine SPA leiten statt die 401-message zu zeigen.
41
- for (const status of [401, 403, 422, 500] as const) {
42
- const app = makeApp(new Response(null, { status }));
43
- const res = await tryHonoFirst(app, new Request("http://test/x"));
44
- expect(res.matched, `status ${status} should be matched`).toBe(true);
45
- }
46
- });
47
-
48
- test("akzeptiert sowohl sync als auch async fetch (Hono-Variation)", async () => {
49
- // Hono.app.fetch returnt Response | Promise<Response> abhängig vom
50
- // handler-mix. createApiEntrypoint's apiHandler dasselbe. Helper
51
- // muss beide schluckable.
52
- const sync = await tryHonoFirst(
53
- makeApp(new Response("s", { status: 200 })),
54
- new Request("http://t/"),
55
- );
56
- const asyncRes = await tryHonoFirst(
57
- makeAsyncApp(new Response("a", { status: 200 })),
58
- new Request("http://t/"),
59
- );
60
- expect(sync.matched).toBe(true);
61
- expect(asyncRes.matched).toBe(true);
62
- });
63
- });
@@ -1,26 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { createJobsFeature } from "@cosmicdrift/kumiko-bundled-features/jobs";
3
- import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
4
- import { createRegistry, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
5
- import { jobRunLoggerCallbacks } from "../job-run-logger";
6
-
7
- // ponytail: callbacks shape only — no DB I/O; createTestDb needs TEST_DATABASE_URL (CI unit job has none).
8
- const mockDb = {} as DbConnection;
9
-
10
- describe("jobRunLoggerCallbacks", () => {
11
- test("returns undefined when jobs feature is not registered", () => {
12
- const registry = createRegistry([
13
- defineFeature("empty", () => ({ handlers: {}, queries: {} })),
14
- ]);
15
- expect(jobRunLoggerCallbacks(registry, mockDb)).toBeUndefined();
16
- });
17
-
18
- test("returns logger callbacks when jobs feature is registered", () => {
19
- const registry = createRegistry([createJobsFeature()]);
20
- const callbacks = jobRunLoggerCallbacks(registry, mockDb);
21
- expect(callbacks).toBeDefined();
22
- expect(typeof callbacks?.onJobStart).toBe("function");
23
- expect(typeof callbacks?.onJobComplete).toBe("function");
24
- expect(typeof callbacks?.onJobFailed).toBe("function");
25
- });
26
- });
@@ -1,19 +0,0 @@
1
- import { seedAllConfigValues } from "@cosmicdrift/kumiko-bundled-features/config";
2
- import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
- import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
4
- import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
5
-
6
- // Single boot-seed entry-point. runDevApp + runProdApp both call this
7
- // from their post-stack hook, so the wiring lives in exactly one place
8
- // — config-seed-boot.integration.ts pins this helper, which means a
9
- // missing call site (e.g. someone deletes the line from runDevApp)
10
- // surfaces as a missing-helper-use in code review rather than silently
11
- // shipping a server that never seeds.
12
- // @wrapper-known semantic-alias
13
- export async function applyBootSeeds(deps: {
14
- registry: Registry;
15
- db: DbConnection;
16
- cipher?: EnvelopeCipher;
17
- }): Promise<void> {
18
- await seedAllConfigValues(deps.registry, deps.db, deps.cipher);
19
- }
@@ -1,82 +0,0 @@
1
- // One place that turns env/options into the app-wide crypto objects:
2
- // the MasterKeyProvider (KUMIKO_SECRETS_MASTER_KEY_V<n> keyring) and the
3
- // EnvelopeCipher for `encrypted: true` config keys. runProdApp and
4
- // runDevApp both resolve this once and thread it into
5
- // buildBootExtraContext + applyBootSeeds so resolver, set-handler and
6
- // seeds share the same cipher instance (and DEK cache).
7
-
8
- import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
9
- import {
10
- createDekCache,
11
- createEnvelopeCipher,
12
- createEnvMasterKeyProvider,
13
- type DekCache,
14
- type EnvelopeCipher,
15
- type MasterKeyProvider,
16
- } from "@cosmicdrift/kumiko-framework/secrets";
17
-
18
- // Keyring detection without throwing: only build the env provider when at
19
- // least one versioned KEK is actually set. Covers (a) apps without any
20
- // encryption — no KEK requirement, and (b) dev with an app-supplied
21
- // provider in options.masterKey — env stays irrelevant.
22
- const MASTER_KEK_VAR = /^KUMIKO_SECRETS_MASTER_KEY_V\d+$/;
23
- export function envHasMasterKek(env: Record<string, string | undefined>): boolean {
24
- return Object.entries(env).some(([k, v]) => MASTER_KEK_VAR.test(k) && !!v);
25
- }
26
-
27
- export type BootCrypto = {
28
- readonly masterKeyProvider?: MasterKeyProvider;
29
- // Cipher for encrypted config keys. Present exactly when a master key is
30
- // available. Decrypts legacy CONFIG_ENCRYPTION_KEY values as fallback
31
- // until the config re-encrypt job migrated them.
32
- readonly configCipher?: EnvelopeCipher;
33
- // Cipher for `encrypted: true` entity fields — same master key, but the
34
- // legacy fallback reads the pre-envelope ENCRYPTION_KEY format. Identical
35
- // to configCipher when no ENCRYPTION_KEY is set.
36
- readonly entityFieldCipher?: EnvelopeCipher;
37
- readonly dekCache: DekCache;
38
- };
39
-
40
- export function resolveBootCrypto(
41
- envSource: Record<string, string | undefined>,
42
- masterKeyOverride?: MasterKeyProvider,
43
- ): BootCrypto {
44
- const masterKeyProvider =
45
- masterKeyOverride ??
46
- (envHasMasterKek(envSource)
47
- ? createEnvMasterKeyProvider({
48
- // CURRENT_VERSION default "1" spiegelt secretsEnvSchema — ohne
49
- // ihn wirft der raw-env-Provider, obwohl V1 gesetzt ist.
50
- env: {
51
- ...envSource,
52
- KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION:
53
- envSource["KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION"] ?? "1",
54
- },
55
- })
56
- : undefined);
57
-
58
- const dekCache = createDekCache();
59
- const legacyConfigKey = envSource["CONFIG_ENCRYPTION_KEY"];
60
- const configCipher = masterKeyProvider
61
- ? createEnvelopeCipher(masterKeyProvider, {
62
- dekCache,
63
- ...(legacyConfigKey ? { legacy: createEncryptionProvider(legacyConfigKey) } : {}),
64
- })
65
- : undefined;
66
-
67
- const legacyEntityKey = envSource["ENCRYPTION_KEY"];
68
- const entityFieldCipher =
69
- masterKeyProvider && legacyEntityKey
70
- ? createEnvelopeCipher(masterKeyProvider, {
71
- dekCache,
72
- legacy: createEncryptionProvider(legacyEntityKey),
73
- })
74
- : configCipher;
75
-
76
- return {
77
- ...(masterKeyProvider && { masterKeyProvider }),
78
- ...(configCipher && { configCipher }),
79
- ...(entityFieldCipher && { entityFieldCipher }),
80
- dekCache,
81
- };
82
- }
@@ -1,51 +0,0 @@
1
- import { createJobRunLogger } from "@cosmicdrift/kumiko-bundled-features/jobs";
2
- import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
3
- import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
4
- import type { JobRunIn } from "@cosmicdrift/kumiko-framework/engine/types";
5
- import { createJobRunner, type JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
6
-
7
- export function jobRunLoggerCallbacks(
8
- registry: Registry,
9
- db: DbConnection,
10
- ): ReturnType<typeof createJobRunLogger> | undefined {
11
- if (registry.getFeature("jobs") === undefined) return undefined;
12
- return createJobRunLogger({ db, registry });
13
- }
14
-
15
- /** Dev-server parity: consume api + worker lanes when jobs are registered. */
16
- export async function startDevJobRunners(opts: {
17
- readonly registry: Registry;
18
- readonly db: DbConnection;
19
- readonly context: Record<string, unknown>;
20
- readonly redisUrl: string;
21
- }): Promise<{ readonly runners: readonly JobRunner[]; readonly stop: () => Promise<void> }> {
22
- const jobs = [...opts.registry.getAllJobs().values()];
23
- if (opts.registry.getFeature("jobs") === undefined || jobs.length === 0) {
24
- return { runners: [], stop: async () => {} };
25
- }
26
-
27
- const logger = createJobRunLogger({ db: opts.db, registry: opts.registry });
28
- const runners: JobRunner[] = [];
29
- const lanes = new Set(
30
- jobs.map((j) => j.runIn).filter((lane): lane is JobRunIn => lane !== undefined),
31
- );
32
-
33
- for (const lane of lanes) {
34
- const jr = createJobRunner({
35
- registry: opts.registry,
36
- context: { ...opts.context, db: opts.db },
37
- redisUrl: opts.redisUrl,
38
- consumerLane: lane,
39
- ...logger,
40
- });
41
- await jr.start();
42
- runners.push(jr);
43
- }
44
-
45
- return {
46
- runners,
47
- stop: async () => {
48
- for (const runner of runners) await runner.stop();
49
- },
50
- };
51
- }