@cosmicdrift/kumiko-server-runtime 0.329.0 → 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 -695
  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,262 +0,0 @@
1
- // Regression guard for startDevJobRunners' lane default: a job registered
2
- // WITHOUT an explicit runIn used to get no consumer at all in the dev server
3
- // (the old `lanes` filter dropped `undefined` lanes), so it sat in the queue
4
- // forever. `runners.length` alone can't catch that — a lane with no consumer
5
- // still produces a runner object. This dispatches a real job and waits for
6
- // its side effect to prove the lane is actually being consumed.
7
- import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
8
- import {
9
- createJobsFeature,
10
- jobRunLogsTable,
11
- jobRunsTable,
12
- } from "@cosmicdrift/kumiko-bundled-features/jobs";
13
- import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
14
- import {
15
- buildEntityTable,
16
- createEventStoreExecutor,
17
- selectMany,
18
- } from "@cosmicdrift/kumiko-framework/db";
19
- import {
20
- createEntity,
21
- createRegistry,
22
- createTextField,
23
- defineFeature,
24
- } from "@cosmicdrift/kumiko-framework/engine";
25
- import { createDispatcher } from "@cosmicdrift/kumiko-framework/pipeline";
26
- import {
27
- createTestDb,
28
- createTestRedis,
29
- setupTestStack,
30
- type TestDb,
31
- type TestRedis,
32
- TestUsers,
33
- unsafeCreateEntityTable,
34
- unsafePushTables,
35
- } from "@cosmicdrift/kumiko-framework/stack";
36
- import { waitFor } from "@cosmicdrift/kumiko-framework/testing";
37
- import * as z from "zod";
38
- import { startDevJobRunners } from "../job-run-logger";
39
-
40
- let testDb: TestDb;
41
- let testRedis: TestRedis;
42
- let db: DbConnection;
43
-
44
- const jobRuns: Array<{ readonly note: string }> = [];
45
-
46
- // No `runIn` — the exact case the lane-default regression hits.
47
- const probeFeature = defineFeature("probe", (r) => {
48
- r.job("noteIt", { trigger: { manual: true } }, async (payload) => {
49
- jobRuns.push({ note: (payload as { note: string }).note });
50
- });
51
- });
52
-
53
- beforeAll(async () => {
54
- testDb = await createTestDb();
55
- testRedis = await createTestRedis();
56
- db = testDb.db;
57
- await unsafePushTables(db, { jobRunsTable, jobRunLogsTable });
58
- });
59
-
60
- afterAll(async () => {
61
- await testDb.cleanup();
62
- await testRedis.cleanup();
63
- });
64
-
65
- describe("startDevJobRunners", () => {
66
- test("job without explicit runIn gets a consumer and actually executes", async () => {
67
- const registry = createRegistry([probeFeature, createJobsFeature()]);
68
-
69
- const { runners, stop } = await startDevJobRunners({
70
- registry,
71
- db,
72
- context: {},
73
- redisUrl: testRedis.redisUrl,
74
- queueNamePrefix: `test-lane-default-${crypto.randomUUID()}`,
75
- // Construction-only — this job never calls ctx.write/ctx.queryAs, so a
76
- // dispatcher built against a bare `{}` context is enough to satisfy
77
- // the (now required) opts.dispatcher without wiring a full stack.
78
- dispatcher: createDispatcher(registry, {}),
79
- });
80
-
81
- try {
82
- expect(runners.length).toBe(1);
83
- const runner = runners[0];
84
- if (runner === undefined) throw new Error("expected a runner");
85
-
86
- await runner.dispatch("probe:job:note-it", { note: "hello-from-dev-runner" });
87
-
88
- await waitFor(() => {
89
- expect(jobRuns.some((r) => r.note === "hello-from-dev-runner")).toBe(true);
90
- });
91
- } finally {
92
- await stop();
93
- }
94
- });
95
- });
96
-
97
- // kumiko-framework#2553: startDevJobRunners built its per-lane JobRunners but
98
- // never called attachDispatcher() on them (unlike createApiEntrypoint /
99
- // createWorkerEntrypoint / createAllInOneEntrypoint, which all do this
100
- // automatically) — so ctx.write/ctx.queryAs inside a dev-run job always hit
101
- // the "dispatcher attached — call attachDispatcher() first" stub. This
102
- // mirrors create-kumiko-server.ts's real wiring (setupTestStack + a separate
103
- // startDevJobRunners as the lane consumer) and drives a job that calls both
104
- // ctx.write and ctx.queryAs end-to-end.
105
- const attachNoteEntity = createEntity({
106
- table: "job_attach_notes",
107
- fields: { text: createTextField({ personal: false, reason: "test_fixture", required: true }) },
108
- });
109
- const attachNoteTable = buildEntityTable("note", attachNoteEntity);
110
-
111
- const attachResults: Array<{ writeOk: boolean; rowCount: number }> = [];
112
- const attachFailures: string[] = [];
113
-
114
- const attachDispatcherFeature = defineFeature("jobattach", (r) => {
115
- r.entity("note", attachNoteEntity);
116
-
117
- r.writeHandler(
118
- "create",
119
- z.object({ text: z.string() }),
120
- async (event, ctx) => {
121
- const crud = createEventStoreExecutor(attachNoteTable, attachNoteEntity, {
122
- entityName: "note",
123
- });
124
- return crud.create(event.payload, event.user, ctx.db);
125
- },
126
- { access: { openToAll: { reason: "test handler callable by any signed-in test user" } } },
127
- );
128
-
129
- r.queryHandler("list", z.object({}), async (_query, ctx) => selectMany(ctx.db, attachNoteTable), {
130
- access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
131
- });
132
-
133
- r.job("record", { trigger: { manual: true }, retries: 0 }, async (payload, ctx) => {
134
- try {
135
- const text = (payload as { text: string }).text;
136
- const writeResult = await ctx.write("jobattach:write:create", { text });
137
- const rows = await ctx.queryAs(TestUsers.admin, "jobattach:query:list", {});
138
- attachResults.push({
139
- writeOk: writeResult.isSuccess,
140
- rowCount: (rows as unknown[]).length,
141
- });
142
- } catch (error) {
143
- attachFailures.push(error instanceof Error ? error.message : String(error));
144
- throw error;
145
- }
146
- });
147
- });
148
-
149
- describe("startDevJobRunners attaches the dispatcher (kumiko-framework#2553)", () => {
150
- afterEach(() => {
151
- attachResults.length = 0;
152
- attachFailures.length = 0;
153
- });
154
-
155
- test("a dev-run job's ctx.write and ctx.queryAs both succeed", async () => {
156
- const attachQueueNamePrefix = `test-attach-dispatcher-${crypto.randomUUID()}`;
157
- const stack = await setupTestStack({
158
- features: [attachDispatcherFeature, createJobsFeature()],
159
- // Enqueuer-only — startDevJobRunners below is the sole consumer,
160
- // mirroring create-kumiko-server.ts.
161
- jobs: { queueNamePrefix: attachQueueNamePrefix },
162
- });
163
- await unsafeCreateEntityTable(stack.db, attachNoteEntity);
164
-
165
- try {
166
- const { runners, stop } = await startDevJobRunners({
167
- registry: stack.registry,
168
- db: stack.db,
169
- context: stack.context,
170
- redisUrl: stack.redis.redisUrl,
171
- dispatcher: stack.dispatcher,
172
- queueNamePrefix: attachQueueNamePrefix,
173
- });
174
-
175
- try {
176
- const runner = runners[0];
177
- if (runner === undefined) throw new Error("expected a runner");
178
-
179
- await runner.dispatch("jobattach:job:record", { text: "hello-from-dev-runner" });
180
-
181
- await waitFor(() => {
182
- expect(attachResults.length + attachFailures.length).toBeGreaterThan(0);
183
- });
184
- if (attachFailures.length > 0) {
185
- throw new Error(`job failed: ${attachFailures.join(", ")}`);
186
- }
187
- expect(attachResults[0]).toEqual({ writeOk: true, rowCount: 1 });
188
- } finally {
189
- await stop();
190
- }
191
- } finally {
192
- await stack.cleanup();
193
- }
194
- }, 15000);
195
- });
196
-
197
- // Regression guard for the shared-Redis job-queue-collision bug: two boots
198
- // (e.g. two parallel dev/e2e servers) that don't set distinct
199
- // queueNamePrefix values consume from the same BullMQ queue names and steal
200
- // each other's jobs ("Unknown job: ..."). Both registries below register the
201
- // SAME feature/job name on purpose — if the queues were shared, runner B
202
- // would recognize and execute jobs meant for runner A (proving the
203
- // collision), not just fail to run them. Distinct prefixes must keep every
204
- // dispatched job on runner A's side.
205
- describe("startDevJobRunners isolates queues by queueNamePrefix", () => {
206
- test("two runners with distinct prefixes on the same Redis don't cross-consume", async () => {
207
- const runsA: string[] = [];
208
- const runsB: string[] = [];
209
-
210
- const isoFeatureA = defineFeature("iso", (r) => {
211
- r.job("mark", { trigger: { manual: true } }, async (payload) => {
212
- runsA.push((payload as { note: string }).note);
213
- });
214
- });
215
- const isoFeatureB = defineFeature("iso", (r) => {
216
- r.job("mark", { trigger: { manual: true } }, async (payload) => {
217
- runsB.push((payload as { note: string }).note);
218
- });
219
- });
220
-
221
- const registryA = createRegistry([isoFeatureA, createJobsFeature()]);
222
- const registryB = createRegistry([isoFeatureB, createJobsFeature()]);
223
-
224
- const runnerA = await startDevJobRunners({
225
- registry: registryA,
226
- db,
227
- context: {},
228
- redisUrl: testRedis.redisUrl,
229
- queueNamePrefix: `test-iso-a-${crypto.randomUUID()}`,
230
- dispatcher: createDispatcher(registryA, {}),
231
- });
232
- const runnerB = await startDevJobRunners({
233
- registry: registryB,
234
- db,
235
- context: {},
236
- redisUrl: testRedis.redisUrl,
237
- queueNamePrefix: `test-iso-b-${crypto.randomUUID()}`,
238
- dispatcher: createDispatcher(registryB, {}),
239
- });
240
-
241
- try {
242
- const dispatchA = runnerA.runners[0];
243
- if (dispatchA === undefined) throw new Error("expected a runner for registry A");
244
-
245
- const notes = Array.from({ length: 10 }, (_, i) => `from-a-${i}`);
246
- for (const note of notes) {
247
- await dispatchA.dispatch("iso:job:mark", { note });
248
- }
249
-
250
- // If the queues were shared, some of these 10 jobs would land on
251
- // runner B instead — runsA would never reach length 10 and this
252
- // would time out rather than pass.
253
- await waitFor(() => {
254
- expect(runsA).toHaveLength(10);
255
- });
256
- expect(runsB).toEqual([]);
257
- } finally {
258
- await runnerA.stop();
259
- await runnerB.stop();
260
- }
261
- });
262
- });
@@ -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,67 +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 {
9
- createDekCache,
10
- createEnvelopeCipher,
11
- createEnvMasterKeyProvider,
12
- type DekCache,
13
- type EnvelopeCipher,
14
- type MasterKeyProvider,
15
- } from "@cosmicdrift/kumiko-framework/secrets";
16
-
17
- // Keyring detection without throwing: only build the env provider when at
18
- // least one versioned KEK is actually set. Covers (a) apps without any
19
- // encryption — no KEK requirement, and (b) dev with an app-supplied
20
- // provider in options.masterKey — env stays irrelevant.
21
- const MASTER_KEK_VAR = /^KUMIKO_SECRETS_MASTER_KEY_V\d+$/;
22
- export function envHasMasterKek(env: Record<string, string | undefined>): boolean {
23
- return Object.entries(env).some(([k, v]) => MASTER_KEK_VAR.test(k) && !!v);
24
- }
25
-
26
- export type BootCrypto = {
27
- readonly masterKeyProvider?: MasterKeyProvider;
28
- // Cipher for encrypted config keys. Present exactly when a master key is
29
- // available.
30
- readonly configCipher?: EnvelopeCipher;
31
- // Cipher for `encrypted: true` entity fields — same master key and
32
- // instance as configCipher (kept as a separate field for API stability).
33
- readonly entityFieldCipher?: EnvelopeCipher;
34
- readonly dekCache: DekCache;
35
- };
36
-
37
- export function resolveBootCrypto(
38
- envSource: Record<string, string | undefined>,
39
- masterKeyOverride?: MasterKeyProvider,
40
- ): BootCrypto {
41
- const masterKeyProvider =
42
- masterKeyOverride ??
43
- (envHasMasterKek(envSource)
44
- ? createEnvMasterKeyProvider({
45
- // CURRENT_VERSION default "1" spiegelt secretsEnvSchema — ohne
46
- // ihn wirft der raw-env-Provider, obwohl V1 gesetzt ist.
47
- env: {
48
- ...envSource,
49
- KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION:
50
- envSource["KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION"] ?? "1",
51
- },
52
- })
53
- : undefined);
54
-
55
- const dekCache = createDekCache();
56
- const configCipher = masterKeyProvider
57
- ? createEnvelopeCipher(masterKeyProvider, { dekCache })
58
- : undefined;
59
- const entityFieldCipher = configCipher;
60
-
61
- return {
62
- ...(masterKeyProvider && { masterKeyProvider }),
63
- ...(configCipher && { configCipher }),
64
- ...(entityFieldCipher && { entityFieldCipher }),
65
- dekCache,
66
- };
67
- }
@@ -1,63 +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 { AppContext, JobRunIn } from "@cosmicdrift/kumiko-framework/engine/types";
5
- import { createJobRunner, type JobRunner } from "@cosmicdrift/kumiko-framework/jobs";
6
- import { type Dispatcher, dispatcherToWriteRef } from "@cosmicdrift/kumiko-framework/pipeline";
7
-
8
- export function jobRunLoggerCallbacks(
9
- registry: Registry,
10
- db: DbConnection,
11
- ): ReturnType<typeof createJobRunLogger> | undefined {
12
- if (registry.getFeature("jobs") === undefined) return undefined;
13
- return createJobRunLogger({ db, registry });
14
- }
15
-
16
- /** Dev-server parity: consume api + worker lanes when jobs are registered. */
17
- export async function startDevJobRunners(opts: {
18
- readonly registry: Registry;
19
- readonly db: DbConnection;
20
- readonly context: AppContext;
21
- readonly redisUrl: string;
22
- readonly dispatcher: Dispatcher;
23
- // Redis is shared across boots and apps (unlike the ephemeral per-boot
24
- // test DB); no silent default here so every caller is forced to pick a
25
- // prefix that isolates its own queues.
26
- readonly queueNamePrefix: string;
27
- }): Promise<{ readonly runners: readonly JobRunner[]; readonly stop: () => Promise<void> }> {
28
- const jobs = [...opts.registry.getAllJobs().values()];
29
- if (opts.registry.getFeature("jobs") === undefined || jobs.length === 0) {
30
- return { runners: [], stop: async () => {} };
31
- }
32
-
33
- const logger = createJobRunLogger({ db: opts.db, registry: opts.registry });
34
- const runners: JobRunner[] = [];
35
- // `?? "worker"` mirrors job-runner's laneForJob: a job without an explicit
36
- // runIn is enqueued onto the worker lane, so filtering those out would leave
37
- // its queue without a consumer whenever no other job named the lane.
38
- const lanes = new Set<JobRunIn>(jobs.map((j) => j.runIn ?? "worker"));
39
-
40
- for (const lane of lanes) {
41
- const jr = createJobRunner({
42
- registry: opts.registry,
43
- context: { ...opts.context, db: opts.db },
44
- redisUrl: opts.redisUrl,
45
- queueNamePrefix: opts.queueNamePrefix,
46
- consumerLane: lane,
47
- ...logger,
48
- });
49
- // Without this, ctx.write/ctx.queryAs inside a dev-run job throw
50
- // "dispatcher attached — call attachDispatcher() first" on their first
51
- // call — the prod entrypoints attach automatically, dev boot must too.
52
- jr.attachDispatcher(dispatcherToWriteRef(opts.dispatcher));
53
- await jr.start();
54
- runners.push(jr);
55
- }
56
-
57
- return {
58
- runners,
59
- stop: async () => {
60
- for (const runner of runners) await runner.stop();
61
- },
62
- };
63
- }
@@ -1,43 +0,0 @@
1
- import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
- import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
3
- import { createFallbackLogger } from "@cosmicdrift/kumiko-framework/logging";
4
- import { runPiiEventBackfill } from "@cosmicdrift/kumiko-framework/migrations";
5
-
6
- export const SKIP_PII_BACKFILL_ENV = "KUMIKO_SKIP_PII_BACKFILL";
7
-
8
- type ShutdownRegistry = {
9
- registerShutdownHook(name: string, fn: () => Promise<void>): void;
10
- };
11
-
12
- // Runs in the background instead of blocking boot: the first run after a bump
13
- // scans the whole store and would delay readiness in proportion to its size.
14
- // Every batch is its own transaction, so an abort at any point is safe.
15
- export function startPiiEventBackfillOnBoot(opts: {
16
- readonly db: DbConnection;
17
- readonly registry: Registry;
18
- readonly lifecycle: ShutdownRegistry;
19
- readonly envSource: Readonly<Record<string, string | undefined>>;
20
- }): void {
21
- if (opts.envSource[SKIP_PII_BACKFILL_ENV] === "1") {
22
- // skip: operator opted out via KUMIKO_SKIP_PII_BACKFILL
23
- return;
24
- }
25
-
26
- const log = createFallbackLogger("pii-event-backfill");
27
- const abortController = new AbortController();
28
- const run = runPiiEventBackfill(opts.db, opts.registry, {
29
- signal: abortController.signal,
30
- }).then(
31
- () => undefined,
32
- (e: unknown) => {
33
- log.error("PII event backfill crashed; it will be retried on the next boot", {
34
- error: e instanceof Error ? e.message : String(e),
35
- });
36
- },
37
- );
38
-
39
- opts.lifecycle.registerShutdownHook("piiEventBackfill", async () => {
40
- abortController.abort();
41
- await run;
42
- });
43
- }