@cosmicdrift/kumiko-server-runtime 0.329.0 → 0.330.2
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/README.md +15 -0
- package/dist/boot/apply-boot-seeds.d.ts +8 -0
- package/{src/boot/apply-boot-seeds.ts → dist/boot/apply-boot-seeds.js} +2 -10
- package/dist/boot/boot-crypto.d.ts +9 -0
- package/dist/boot/boot-crypto.js +39 -0
- package/dist/boot/job-run-logger.d.ts +19 -0
- package/dist/boot/job-run-logger.js +44 -0
- package/dist/boot/pii-event-backfill-on-boot.d.ts +13 -0
- package/dist/boot/pii-event-backfill-on-boot.js +25 -0
- package/dist/build-prod-bundle.d.ts +103 -0
- package/dist/build-prod-bundle.js +751 -0
- package/dist/bun-serve-options.d.ts +32 -0
- package/dist/bun-serve-options.js +49 -0
- package/dist/compose-features.d.ts +31 -0
- package/dist/compose-features.js +124 -0
- package/dist/extra-routes-deps.d.ts +30 -0
- package/dist/extra-routes-deps.js +4 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +7 -0
- package/dist/pii-boot-gate.d.ts +13 -0
- package/dist/pii-boot-gate.js +46 -0
- package/dist/renderer-web-fonts.d.ts +9 -0
- package/{src/renderer-web-fonts.ts → dist/renderer-web-fonts.js} +14 -18
- package/dist/resolve-tailwind-cli.d.ts +14 -0
- package/dist/resolve-tailwind-cli.js +36 -0
- package/dist/run-prod-app-boot-context.d.ts +43 -0
- package/dist/run-prod-app-boot-context.js +198 -0
- package/dist/run-prod-app-static-files.d.ts +32 -0
- package/dist/run-prod-app-static-files.js +280 -0
- package/dist/run-prod-app.d.ts +471 -0
- package/dist/run-prod-app.js +741 -0
- package/dist/run-worker-app.d.ts +92 -0
- package/dist/run-worker-app.js +220 -0
- package/dist/security-headers.d.ts +20 -0
- package/dist/security-headers.js +49 -0
- package/dist/session-boot-gate.d.ts +11 -0
- package/dist/session-boot-gate.js +22 -0
- package/dist/session-wiring.d.ts +11 -0
- package/{src/session-wiring.ts → dist/session-wiring.js} +2 -6
- package/dist/try-hono-first.d.ts +24 -0
- package/{src/try-hono-first.ts → dist/try-hono-first.js} +8 -36
- package/package.json +102 -36
- package/src/changes.json +6 -0
- package/src/__tests__/boot-extra-context.test.ts +0 -163
- package/src/__tests__/boot-probe-fixture.ts +0 -64
- package/src/__tests__/build-prod-bundle.inprocess.test.ts +0 -233
- package/src/__tests__/build-prod-bundle.test.ts +0 -724
- package/src/__tests__/cache-headers.test.ts +0 -83
- package/src/__tests__/compose-features-mfa-wiring.integration.test.ts +0 -308
- package/src/__tests__/compose-features-wiring.integration.test.ts +0 -609
- package/src/__tests__/compose-features.test.ts +0 -206
- package/src/__tests__/config-seed-boot.integration.test.ts +0 -158
- package/src/__tests__/pii-boot-gate.test.ts +0 -112
- package/src/__tests__/renderer-web-css-relocation.integration.test.ts +0 -85
- package/src/__tests__/renderer-web-shell-sentinel.test.ts +0 -35
- package/src/__tests__/renderer-web-theme-plugin.integration.test.ts +0 -77
- package/src/__tests__/require-env.test.ts +0 -34
- package/src/__tests__/resolve-auth-mail.test.ts +0 -144
- package/src/__tests__/resolve-tailwind-cli.test.ts +0 -81
- package/src/__tests__/run-prod-app-dry-run.test.ts +0 -135
- package/src/__tests__/run-prod-app-env-source.test.ts +0 -118
- package/src/__tests__/run-prod-app-kms-slots.test.ts +0 -108
- package/src/__tests__/run-prod-app-spec.test.ts +0 -167
- package/src/__tests__/run-prod-app-static-files.test.ts +0 -298
- package/src/__tests__/run-prod-app.integration.test.ts +0 -1525
- package/src/__tests__/run-worker-app-boot.test.ts +0 -183
- package/src/__tests__/run-worker-app.integration.test.ts +0 -218
- package/src/__tests__/security-headers.test.ts +0 -88
- package/src/__tests__/session-boot-gate.test.ts +0 -70
- package/src/__tests__/session-wiring.test.ts +0 -16
- package/src/__tests__/try-hono-first.test.ts +0 -108
- package/src/__tests__/websocket-route.integration.test.ts +0 -695
- package/src/boot/__tests__/job-run-logger.integration.test.ts +0 -262
- package/src/boot/__tests__/job-run-logger.test.ts +0 -26
- package/src/boot/boot-crypto.ts +0 -67
- package/src/boot/job-run-logger.ts +0 -63
- package/src/boot/pii-event-backfill-on-boot.ts +0 -43
- package/src/build-prod-bundle.ts +0 -996
- package/src/bun-serve-options.ts +0 -110
- package/src/compose-features.ts +0 -197
- package/src/extra-routes-deps.ts +0 -38
- package/src/index.ts +0 -27
- package/src/pii-boot-gate.ts +0 -80
- package/src/resolve-tailwind-cli.ts +0 -45
- package/src/run-prod-app-boot-context.ts +0 -319
- package/src/run-prod-app-static-files.ts +0 -359
- package/src/run-prod-app.ts +0 -1420
- package/src/run-worker-app.ts +0 -366
- package/src/security-headers.ts +0 -67
- 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
|
-
});
|
package/src/boot/boot-crypto.ts
DELETED
|
@@ -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
|
-
}
|