@cosmicdrift/kumiko-framework 0.293.0 → 0.294.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/src/__tests__/schema-cli-rebuild.integration.test.ts +0 -2
- package/src/changes.json +12 -0
- package/src/crypto/__tests__/kek-source.test.ts +82 -0
- package/src/crypto/kek-source.ts +19 -19
- package/src/db/__tests__/apply-entity-event-tenant.integration.test.ts +0 -2
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +1 -2
- package/src/db/__tests__/blind-index.integration.test.ts +0 -2
- package/src/db/__tests__/entity-field-encryption.test.ts +29 -0
- package/src/db/__tests__/event-store-executor-list.integration.test.ts +0 -2
- package/src/db/__tests__/event-store-executor-money-rehydrate.integration.test.ts +0 -2
- package/src/db/__tests__/event-store-executor-reference-search.integration.test.ts +0 -2
- package/src/db/__tests__/event-store-executor-reference-sort.integration.test.ts +0 -2
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +0 -2
- package/src/db/__tests__/event-store-executor.integration.test.ts +1 -2
- package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +0 -2
- package/src/db/__tests__/unique-violation-mapping.integration.test.ts +0 -2
- package/src/db/entity-field-encryption.ts +5 -3
- package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +0 -2
- package/src/engine/boot-validator/index.ts +5 -1
- package/src/entrypoint/__tests__/entrypoint-attach-dispatcher.integration.test.ts +1 -2
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +1 -2
- package/src/entrypoint/__tests__/split-deploy.integration.test.ts +1 -2
- package/src/env/__tests__/compose-env-schema.test.ts +114 -1
- package/src/env/index.ts +64 -3
- package/src/es-ops/__tests__/context.integration.test.ts +0 -2
- package/src/es-ops/__tests__/runner.integration.test.ts +0 -2
- package/src/event-store/__tests__/admin-api.integration.test.ts +1 -2
- package/src/event-store/__tests__/backfill-pii.integration.test.ts +0 -2
- package/src/event-store/__tests__/event-store.integration.test.ts +0 -2
- package/src/event-store/__tests__/get-stream-version-perf.integration.test.ts +1 -2
- package/src/event-store/__tests__/perf.integration.test.ts +1 -8
- package/src/event-store/__tests__/provenance-append.integration.test.ts +1 -2
- package/src/event-store/__tests__/snapshot.integration.test.ts +0 -2
- package/src/event-store/__tests__/upcaster-dead-letter.integration.test.ts +1 -2
- package/src/event-store/__tests__/upcaster.integration.test.ts +1 -2
- package/src/files/__tests__/files.integration.test.ts +1 -2
- package/src/jobs/__tests__/job-write-as.integration.test.ts +1 -2
- package/src/migrations/__tests__/pending-rebuilds.integration.test.ts +0 -2
- package/src/pipeline/__tests__/blind-index-rebuild-guard.integration.test.ts +0 -2
- package/src/pipeline/__tests__/cascade-handler.integration.test.ts +0 -2
- package/src/pipeline/__tests__/perf-rebuild.integration.test.ts +0 -2
- package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +1 -2
- package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +1 -2
- package/src/search/__tests__/reindex-entity.integration.test.ts +0 -2
- package/src/search/__tests__/search-pii-derived-index.integration.test.ts +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.294.1",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -198,8 +198,8 @@
|
|
|
198
198
|
"./package.json": "./package.json"
|
|
199
199
|
},
|
|
200
200
|
"dependencies": {
|
|
201
|
-
"@cosmicdrift/kumiko-http": "0.
|
|
202
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
201
|
+
"@cosmicdrift/kumiko-http": "0.294.1",
|
|
202
|
+
"@cosmicdrift/kumiko-types": "0.294.1",
|
|
203
203
|
"bullmq": "^5.76.7",
|
|
204
204
|
"bun-types": "^1.3.13",
|
|
205
205
|
"hono": "^4.13.1",
|
|
@@ -215,7 +215,7 @@
|
|
|
215
215
|
"zod": "^4.4.3"
|
|
216
216
|
},
|
|
217
217
|
"devDependencies": {
|
|
218
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
218
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.294.1",
|
|
219
219
|
"bun-types": "^1.3.13",
|
|
220
220
|
"pino-pretty": "^13.1.3"
|
|
221
221
|
},
|
|
@@ -18,7 +18,6 @@ import { buildEntityTable } from "../db/table-builder";
|
|
|
18
18
|
import { createTenantDb, type TenantDb } from "../db/tenant-db";
|
|
19
19
|
import { createEntity, createTextField, defineApply, defineFeature } from "../engine";
|
|
20
20
|
import type { ProjectionDefinition } from "../engine/types";
|
|
21
|
-
import { createEventsTable } from "../event-store";
|
|
22
21
|
import { createProjectionStateTable } from "../pipeline";
|
|
23
22
|
import { runSchemaCli, type SchemaCliOut } from "../schema-cli";
|
|
24
23
|
import {
|
|
@@ -101,7 +100,6 @@ beforeAll(async () => {
|
|
|
101
100
|
await ensureTemporalPolyfill();
|
|
102
101
|
testDb = await createTestDb();
|
|
103
102
|
await unsafeCreateEntityTable(testDb.db, itemEntity, "apply-item");
|
|
104
|
-
await createEventsTable(testDb.db);
|
|
105
103
|
await createProjectionStateTable(testDb.db);
|
|
106
104
|
await unsafePushTables(testDb.db, { applyItemsPerGroup: counterTable });
|
|
107
105
|
tdb = createTenantDb(testDb.db, admin.tenantId);
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.294.1",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "Fix parseEnv for refined env schemas with ciphertext-only Key Manager slots",
|
|
6
|
+
"detail": "Relaxing a `kms` slot that is present only as `<NAME>_CIPHERTEXT` used `schema.extend`, which Zod 4 rejects for object schemas carrying refinements (\"Cannot overwrite keys on object schemas containing refinements\"). An app whose composed env schema ends in `.superRefine(...)` therefore failed at boot as soon as its slots were delivered as ciphertext. The relaxation now uses `safeExtend`, which keeps the refinements running."
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"version": "0.294.0",
|
|
10
|
+
"type": "improvement",
|
|
11
|
+
"title": "Declare Key Manager ciphertext slots in the env schema",
|
|
12
|
+
"detail": "An env field can carry `.meta({ kumiko: { kms: true } })` to be deliverable as `<NAME>_CIPHERTEXT`. `runProdApp` now decrypts the schema-declared slots once at boot (`kmsSlotsOf(schema)` → `resolvePlatformKeks({ slots })`) and hands the resolved env to the boot-time master-key probe (`ValidateBootOptions.env`) and to the master-key keyring, so a ciphertext-only `KUMIKO_SECRETS_MASTER_KEY_V<n>` boots. `parseEnv` treats a `kms` slot as satisfied by its ciphertext twin. `composeEnvSchema` declares the `<NAME>_CIPHERTEXT` twin of every `kms` field itself (an already declared twin is kept), so a versioned family needs one declaration per version. `KUMIKO_SECRETS_MASTER_KEY_V1` of the secrets feature is such a slot. `runWorkerApp` takes the same slots as `kmsSlots` (it parses no schema). A deployment missing `KUMIKO_SECRETS_MASTER_KEY_V1` but carrying its `_CIPHERTEXT` twin now passes `parseEnv` and fails at the master-key probe if the decrypt yields nothing, instead of failing in `parseEnv`. `resolvePlatformKeks` without `slots` keeps resolving the three platform slots until consumers declare theirs."
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"version": "0.293.0",
|
|
4
16
|
"type": "improvement",
|
|
@@ -356,4 +356,86 @@ describe("resolvePlatformKeks", () => {
|
|
|
356
356
|
expect(result.KUMIKO_BLIND_INDEX_KEY).toBe("blind-index-plaintext");
|
|
357
357
|
expect(calls.length).toBe(2);
|
|
358
358
|
});
|
|
359
|
+
|
|
360
|
+
describe("with schema-declared slots", () => {
|
|
361
|
+
const KEY_MANAGER = { PLATFORM_KEK_KMS_KEY_ID: "key-1", PLATFORM_KEK_KMS_TOKEN: TOKEN };
|
|
362
|
+
|
|
363
|
+
test("resolves a versioned master-key family without the framework knowing the versions", async () => {
|
|
364
|
+
const { fetch, calls } = trackedFetch([
|
|
365
|
+
jsonResponse(200, { plaintext: "v1-plaintext" }),
|
|
366
|
+
jsonResponse(200, { plaintext: "v2-plaintext" }),
|
|
367
|
+
]);
|
|
368
|
+
const env: KekSourceEnv = {
|
|
369
|
+
KUMIKO_SECRETS_MASTER_KEY_V1_CIPHERTEXT: CIPHERTEXT_A,
|
|
370
|
+
KUMIKO_SECRETS_MASTER_KEY_V2_CIPHERTEXT: CIPHERTEXT_B,
|
|
371
|
+
...KEY_MANAGER,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const result = await resolvePlatformKeks(env, {
|
|
375
|
+
fetch,
|
|
376
|
+
slots: ["KUMIKO_SECRETS_MASTER_KEY_V1", "KUMIKO_SECRETS_MASTER_KEY_V2"],
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
expect(result["KUMIKO_SECRETS_MASTER_KEY_V1"]).toBe("v1-plaintext");
|
|
380
|
+
expect(result["KUMIKO_SECRETS_MASTER_KEY_V2"]).toBe("v2-plaintext");
|
|
381
|
+
expect(calls.length).toBe(2);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("a plaintext beats its ciphertext per slot, the other slot still resolves", async () => {
|
|
385
|
+
const { fetch, calls } = trackedFetch([jsonResponse(200, { plaintext: "v2-plaintext" })]);
|
|
386
|
+
const env: KekSourceEnv = {
|
|
387
|
+
KUMIKO_SECRETS_MASTER_KEY_V1: "v1-plaintext",
|
|
388
|
+
KUMIKO_SECRETS_MASTER_KEY_V1_CIPHERTEXT: CIPHERTEXT_A,
|
|
389
|
+
KUMIKO_SECRETS_MASTER_KEY_V2_CIPHERTEXT: CIPHERTEXT_B,
|
|
390
|
+
...KEY_MANAGER,
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const result = await resolvePlatformKeks(env, {
|
|
394
|
+
fetch,
|
|
395
|
+
slots: ["KUMIKO_SECRETS_MASTER_KEY_V1", "KUMIKO_SECRETS_MASTER_KEY_V2"],
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(result["KUMIKO_SECRETS_MASTER_KEY_V1"]).toBe("v1-plaintext");
|
|
399
|
+
expect(result["KUMIKO_SECRETS_MASTER_KEY_V2"]).toBe("v2-plaintext");
|
|
400
|
+
expect(calls.length).toBe(1);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("does not touch the platform slots that the caller did not name", async () => {
|
|
404
|
+
const { fetch, calls } = trackedFetch([]);
|
|
405
|
+
const env: KekSourceEnv = { PLATFORM_KEK_CIPHERTEXT: CIPHERTEXT_A, ...KEY_MANAGER };
|
|
406
|
+
|
|
407
|
+
const result = await resolvePlatformKeks(env, { fetch, slots: [] });
|
|
408
|
+
|
|
409
|
+
expect(result).toBe(env);
|
|
410
|
+
expect(calls.length).toBe(0);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("logs the source line for a declared slot", async () => {
|
|
414
|
+
const { fetch } = trackedFetch([jsonResponse(200, { plaintext: "v1-plaintext" })]);
|
|
415
|
+
const lines: string[] = [];
|
|
416
|
+
const env: KekSourceEnv = {
|
|
417
|
+
KUMIKO_SECRETS_MASTER_KEY_V1_CIPHERTEXT: CIPHERTEXT_A,
|
|
418
|
+
...KEY_MANAGER,
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
await resolvePlatformKeks(env, {
|
|
422
|
+
fetch,
|
|
423
|
+
slots: ["KUMIKO_SECRETS_MASTER_KEY_V1"],
|
|
424
|
+
log: (line) => lines.push(line),
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
expect(lines).toEqual([
|
|
428
|
+
"KUMIKO_SECRETS_MASTER_KEY_V1 source=key-manager keyId=key-1 region=fr-par",
|
|
429
|
+
]);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("still rejects a previous KEK without its version when that slot is declared", async () => {
|
|
433
|
+
const { fetch } = trackedFetch([jsonResponse(200, { plaintext: "previous-plaintext" })]);
|
|
434
|
+
const env: KekSourceEnv = { PLATFORM_KEK_PREVIOUS_CIPHERTEXT: CIPHERTEXT_B, ...KEY_MANAGER };
|
|
435
|
+
|
|
436
|
+
await expect(
|
|
437
|
+
resolvePlatformKeks(env, { fetch, slots: ["PLATFORM_KEK_PREVIOUS"] }),
|
|
438
|
+
).rejects.toThrow(/PLATFORM_KEK_PREVIOUS_VERSION must be set/);
|
|
439
|
+
});
|
|
440
|
+
});
|
|
359
441
|
});
|
package/src/crypto/kek-source.ts
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
// Resolves
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// fail boot.
|
|
1
|
+
// Resolves a caller-named set of secret slots from a Key Manager ciphertext
|
|
2
|
+
// when no plaintext is set, so they need not sit in the pod env in the clear.
|
|
3
|
+
// A slot's plaintext always wins over its ciphertext sibling, with no request
|
|
4
|
+
// made at all. Any `*_CIPHERTEXT` in the env that names no requested slot is
|
|
5
|
+
// ignored — a foreign ciphertext must never fail boot.
|
|
7
6
|
|
|
8
7
|
const SCALEWAY_KEY_MANAGER_API_VERSION = "v1alpha1";
|
|
9
8
|
const DEFAULT_REGION = "fr-par";
|
|
10
9
|
const DECRYPT_TIMEOUT_MS = 5_000;
|
|
11
10
|
const RETRY_DELAYS_MS = [200, 800];
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"KUMIKO_BLIND_INDEX_KEY",
|
|
16
|
-
] as const;
|
|
17
|
-
type ResolvableSlot = (typeof RESOLVABLE_SLOTS)[number];
|
|
11
|
+
// Used when the caller names no slots. Apps that predate `kms` schema meta
|
|
12
|
+
// rely on it; drop once every consumer declares its slots in the env schema.
|
|
13
|
+
const LEGACY_SLOTS = ["PLATFORM_KEK", "PLATFORM_KEK_PREVIOUS", "KUMIKO_BLIND_INDEX_KEY"] as const;
|
|
18
14
|
|
|
19
15
|
export type KekSourceEnv = {
|
|
20
16
|
readonly PLATFORM_KEK?: string | undefined;
|
|
@@ -35,6 +31,9 @@ export type KekSourceOptions = {
|
|
|
35
31
|
readonly logPrefix?: string;
|
|
36
32
|
/** Where the boot line naming the KEK source goes. Defaults to `console.info`. */
|
|
37
33
|
readonly log?: (message: string) => void;
|
|
34
|
+
/** Env names to resolve, e.g. `kmsSlotsOf(schema)`. Defaults to the three
|
|
35
|
+
* platform slots. */
|
|
36
|
+
readonly slots?: readonly string[];
|
|
38
37
|
};
|
|
39
38
|
|
|
40
39
|
function isRetryableStatus(status: number): boolean {
|
|
@@ -107,7 +106,7 @@ async function decryptCiphertext(
|
|
|
107
106
|
}
|
|
108
107
|
|
|
109
108
|
async function resolveSlot(
|
|
110
|
-
name:
|
|
109
|
+
name: string,
|
|
111
110
|
env: KekSourceEnv,
|
|
112
111
|
options: KekSourceOptions,
|
|
113
112
|
fetchImpl: typeof globalThis.fetch,
|
|
@@ -132,7 +131,7 @@ async function resolveSlot(
|
|
|
132
131
|
// A leftover plaintext beside a ciphertext boots green while nothing was
|
|
133
132
|
// migrated, which is indistinguishable from a finished cutover unless the
|
|
134
133
|
// boot says which source won. Never carries a key value, only its origin.
|
|
135
|
-
function describeKekSource(name:
|
|
134
|
+
function describeKekSource(name: string, env: KekSourceEnv): string | undefined {
|
|
136
135
|
const plaintext = env[name];
|
|
137
136
|
const ciphertext = env[`${name}_CIPHERTEXT`];
|
|
138
137
|
if (plaintext) {
|
|
@@ -154,26 +153,27 @@ export async function resolvePlatformKeks(
|
|
|
154
153
|
): Promise<KekSourceEnv> {
|
|
155
154
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
156
155
|
|
|
157
|
-
const
|
|
158
|
-
|
|
156
|
+
const slots = options.slots ?? LEGACY_SLOTS;
|
|
157
|
+
const resolved: Record<string, string | undefined> = {};
|
|
158
|
+
for (const name of slots) {
|
|
159
159
|
resolved[name] = await resolveSlot(name, env, options, fetchImpl);
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
const prefix = options.logPrefix ? `${options.logPrefix} ` : "";
|
|
163
163
|
// biome-ignore lint/suspicious/noConsole: ops-visible fallback when no logger is wired
|
|
164
164
|
const log = options.log ?? console.info;
|
|
165
|
-
for (const name of
|
|
165
|
+
for (const name of slots) {
|
|
166
166
|
const line = describeKekSource(name, env);
|
|
167
167
|
if (line) log(`${prefix}${line}`);
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
if (resolved
|
|
170
|
+
if (resolved["PLATFORM_KEK_PREVIOUS"] && !env.PLATFORM_KEK_PREVIOUS_VERSION) {
|
|
171
171
|
throw new Error(
|
|
172
172
|
`${prefix}PLATFORM_KEK_PREVIOUS_VERSION must be set when PLATFORM_KEK_PREVIOUS is set.`,
|
|
173
173
|
);
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
const changed =
|
|
176
|
+
const changed = slots.some((name) => resolved[name] !== env[name]);
|
|
177
177
|
if (!changed) return env;
|
|
178
178
|
|
|
179
179
|
return { ...env, ...resolved };
|
|
@@ -21,7 +21,6 @@ import { asRawClient, selectMany } from "../../db/query";
|
|
|
21
21
|
import { createEntity, createTextField } from "../../engine/factories";
|
|
22
22
|
import type { TenantId } from "../../engine/types";
|
|
23
23
|
import type { StoredEvent } from "../../event-store";
|
|
24
|
-
import { createEventsTable } from "../../event-store";
|
|
25
24
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
26
25
|
import { applyEntityEvent } from "../apply-entity-event";
|
|
27
26
|
import { buildEntityTable } from "../table-builder";
|
|
@@ -39,7 +38,6 @@ let testDb: BunTestDb;
|
|
|
39
38
|
beforeAll(async () => {
|
|
40
39
|
await ensureTemporalPolyfill();
|
|
41
40
|
testDb = await createTestDb();
|
|
42
|
-
await createEventsTable(testDb.db);
|
|
43
41
|
await asRawClient(testDb.db).unsafe(`
|
|
44
42
|
CREATE TABLE read_apply_tenant_check (
|
|
45
43
|
id uuid PRIMARY KEY,
|
|
@@ -14,7 +14,7 @@ import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db
|
|
|
14
14
|
import { asRawClient, insertOne, selectMany } from "../../db/query";
|
|
15
15
|
import { createBooleanField, createEntity, createTextField, defineFeature } from "../../engine";
|
|
16
16
|
import { createRegistry } from "../../engine/registry";
|
|
17
|
-
import { archiveStream, createArchivedStreamsTable
|
|
17
|
+
import { archiveStream, createArchivedStreamsTable } from "../../event-store";
|
|
18
18
|
import { rebuildProjection } from "../../pipeline";
|
|
19
19
|
import { createProjectionStateTable } from "../../pipeline/projection-state";
|
|
20
20
|
import { TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
@@ -49,7 +49,6 @@ beforeAll(async () => {
|
|
|
49
49
|
await ensureTemporalPolyfill();
|
|
50
50
|
testDb = await createTestDb();
|
|
51
51
|
await unsafeCreateEntityTable(testDb.db, userEntity, "user");
|
|
52
|
-
await createEventsTable(testDb.db);
|
|
53
52
|
await createArchivedStreamsTable(testDb.db);
|
|
54
53
|
await createProjectionStateTable(testDb.db);
|
|
55
54
|
tdb = createTenantDb(testDb.db, adminUser.tenantId);
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
import { defineFeature } from "../../engine/define-feature";
|
|
21
21
|
import { createEntity, createTextField } from "../../engine/factories";
|
|
22
22
|
import { createRegistry } from "../../engine/registry";
|
|
23
|
-
import { createEventsTable } from "../../event-store";
|
|
24
23
|
import { rebuildProjection } from "../../pipeline";
|
|
25
24
|
import { createProjectionStateTable } from "../../pipeline/projection-state";
|
|
26
25
|
import { createTestDb, type TestDb, TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
@@ -65,7 +64,6 @@ const adminUser = TestUsers.admin;
|
|
|
65
64
|
beforeAll(async () => {
|
|
66
65
|
testDb = await createTestDb();
|
|
67
66
|
await unsafeCreateEntityTable(testDb.db, personEntity, "person");
|
|
68
|
-
await createEventsTable(testDb.db);
|
|
69
67
|
await createProjectionStateTable(testDb.db);
|
|
70
68
|
tdb = createTenantDb(testDb.db, adminUser.tenantId);
|
|
71
69
|
});
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
collectEncryptedFieldNames,
|
|
6
6
|
decryptEntityFieldValues,
|
|
7
7
|
encryptEntityFieldValues,
|
|
8
|
+
validateEntityFieldEncryptionAvailable,
|
|
8
9
|
} from "../entity-field-encryption";
|
|
9
10
|
|
|
10
11
|
const TEST_KEY = Buffer.from("a]bJm#kP9xQ2@wN!vL$hR5yT8eU0iO3f").toString("base64");
|
|
@@ -43,3 +44,31 @@ describe("entity-field-encryption", () => {
|
|
|
43
44
|
expect(stored["secretNote"]).not.toBe("note");
|
|
44
45
|
});
|
|
45
46
|
});
|
|
47
|
+
|
|
48
|
+
describe("validateEntityFieldEncryptionAvailable", () => {
|
|
49
|
+
test("accepts a keyring made only of the env it is handed", () => {
|
|
50
|
+
expect(() =>
|
|
51
|
+
validateEntityFieldEncryptionAvailable({ KUMIKO_SECRETS_MASTER_KEY_V1: TEST_KEY }),
|
|
52
|
+
).not.toThrow();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("rejects a handed env without any master key, whatever process.env holds", () => {
|
|
56
|
+
const previous = process.env["KUMIKO_SECRETS_MASTER_KEY_V1"];
|
|
57
|
+
process.env["KUMIKO_SECRETS_MASTER_KEY_V1"] = TEST_KEY;
|
|
58
|
+
try {
|
|
59
|
+
expect(() => validateEntityFieldEncryptionAvailable({})).toThrow(/no usable master key/);
|
|
60
|
+
} finally {
|
|
61
|
+
if (previous === undefined) delete process.env["KUMIKO_SECRETS_MASTER_KEY_V1"];
|
|
62
|
+
else process.env["KUMIKO_SECRETS_MASTER_KEY_V1"] = previous;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("a pinned CURRENT_VERSION without that version's key still fails", () => {
|
|
67
|
+
expect(() =>
|
|
68
|
+
validateEntityFieldEncryptionAvailable({
|
|
69
|
+
KUMIKO_SECRETS_MASTER_KEY_V1: TEST_KEY,
|
|
70
|
+
KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION: "2",
|
|
71
|
+
}),
|
|
72
|
+
).toThrow(/no usable master key/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -15,7 +15,6 @@ import {
|
|
|
15
15
|
SYSTEM_TENANT_ID,
|
|
16
16
|
} from "../../engine";
|
|
17
17
|
import { UnprocessableError } from "../../errors";
|
|
18
|
-
import { createEventsTable } from "../../event-store";
|
|
19
18
|
import { TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
20
19
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
21
20
|
import { encodeCursor } from "../cursor";
|
|
@@ -46,7 +45,6 @@ beforeAll(async () => {
|
|
|
46
45
|
await ensureTemporalPolyfill();
|
|
47
46
|
testDb = await createTestDb();
|
|
48
47
|
await unsafeCreateEntityTable(testDb.db, entity, "pagerItem");
|
|
49
|
-
await createEventsTable(testDb.db);
|
|
50
48
|
tdb = createTenantDb(testDb.db, admin.tenantId);
|
|
51
49
|
});
|
|
52
50
|
|
|
@@ -9,7 +9,6 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:tes
|
|
|
9
9
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
10
10
|
import { asRawClient } from "../../db/query";
|
|
11
11
|
import { createEntity, createMoneyField, createTextField, from } from "../../engine";
|
|
12
|
-
import { createEventsTable } from "../../event-store";
|
|
13
12
|
import { TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
14
13
|
import { createTestEnvelopeCipher } from "../../testing";
|
|
15
14
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
@@ -59,7 +58,6 @@ beforeAll(async () => {
|
|
|
59
58
|
await ensureTemporalPolyfill();
|
|
60
59
|
testDb = await createTestDb();
|
|
61
60
|
await unsafeCreateEntityTable(testDb.db, entity, "moneyOrder");
|
|
62
|
-
await createEventsTable(testDb.db);
|
|
63
61
|
tdb = createTenantDb(testDb.db, admin.tenantId);
|
|
64
62
|
configureEntityFieldEncryption(cipher);
|
|
65
63
|
});
|
|
@@ -11,7 +11,6 @@ import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db
|
|
|
11
11
|
import { asRawClient } from "../../db/query";
|
|
12
12
|
import { createEntity, createTextField, from } from "../../engine";
|
|
13
13
|
import type { EntityDefinition } from "../../engine/types";
|
|
14
|
-
import { createEventsTable } from "../../event-store";
|
|
15
14
|
import { createInMemorySearchAdapter } from "../../search";
|
|
16
15
|
import { TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
17
16
|
import { seedRows } from "../../testing";
|
|
@@ -156,7 +155,6 @@ beforeAll(async () => {
|
|
|
156
155
|
await unsafeCreateEntityTable(testDb.db, piiCustomerEntity, "refSearchPiiCustomer");
|
|
157
156
|
await unsafeCreateEntityTable(testDb.db, piiOrderEntity, "refSearchPiiOrder");
|
|
158
157
|
await unsafeCreateEntityTable(testDb.db, tenantMetaEntity, "tenant");
|
|
159
|
-
await createEventsTable(testDb.db);
|
|
160
158
|
tdbA = createTenantDb(testDb.db, admin.tenantId);
|
|
161
159
|
tdbB = createTenantDb(testDb.db, otherTenantAdmin.tenantId);
|
|
162
160
|
});
|
|
@@ -9,7 +9,6 @@ import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db
|
|
|
9
9
|
import { asRawClient } from "../../db/query";
|
|
10
10
|
import { createEntity, createTextField } from "../../engine";
|
|
11
11
|
import type { EntityDefinition } from "../../engine/types";
|
|
12
|
-
import { createEventsTable } from "../../event-store";
|
|
13
12
|
import { TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
14
13
|
import { seedRows } from "../../testing";
|
|
15
14
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
@@ -107,7 +106,6 @@ beforeAll(async () => {
|
|
|
107
106
|
await unsafeCreateEntityTable(testDb.db, customerEntity, "refSortCustomer");
|
|
108
107
|
await unsafeCreateEntityTable(testDb.db, orderEntity, "refSortOrder");
|
|
109
108
|
await unsafeCreateEntityTable(testDb.db, restrictedOrderEntity, "refSortRestrictedOrder");
|
|
110
|
-
await createEventsTable(testDb.db);
|
|
111
109
|
tdbA = createTenantDb(testDb.db, admin.tenantId);
|
|
112
110
|
});
|
|
113
111
|
|
|
@@ -9,7 +9,6 @@ import { asRawClient, transaction } from "../../db/query";
|
|
|
9
9
|
import { createEntity, createTextField } from "../../engine";
|
|
10
10
|
import { from } from "../../engine/ownership";
|
|
11
11
|
import { createSystemUser } from "../../engine/system-user";
|
|
12
|
-
import { createEventsTable } from "../../event-store";
|
|
13
12
|
import type { EntityCache } from "../../pipeline/entity-cache";
|
|
14
13
|
import {
|
|
15
14
|
createTestDb,
|
|
@@ -29,7 +28,6 @@ const nonAdmin = TestUsers.user;
|
|
|
29
28
|
|
|
30
29
|
beforeAll(async () => {
|
|
31
30
|
testDb = await createTestDb();
|
|
32
|
-
await createEventsTable(testDb.db);
|
|
33
31
|
tdb = createTenantDb(testDb.db, admin.tenantId);
|
|
34
32
|
});
|
|
35
33
|
|
|
@@ -2,7 +2,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:tes
|
|
|
2
2
|
import { InMemoryKmsAdapter, PII_CIPHERTEXT_PREFIX, PII_ERASED_SENTINEL } from "../../crypto";
|
|
3
3
|
import { asRawClient } from "../../db/query";
|
|
4
4
|
import { createBooleanField, createEntity, createTextField } from "../../engine";
|
|
5
|
-
import { append,
|
|
5
|
+
import { append, loadEventsAfterVersion } from "../../event-store";
|
|
6
6
|
import type { EntityCache } from "../../pipeline/entity-cache";
|
|
7
7
|
import {
|
|
8
8
|
createTestDb,
|
|
@@ -41,7 +41,6 @@ const adminUser = TestUsers.admin;
|
|
|
41
41
|
beforeAll(async () => {
|
|
42
42
|
testDb = await createTestDb();
|
|
43
43
|
await unsafeCreateEntityTable(testDb.db, entity, "esExecUser");
|
|
44
|
-
await createEventsTable(testDb.db);
|
|
45
44
|
tdb = createTenantDb(testDb.db, adminUser.tenantId);
|
|
46
45
|
});
|
|
47
46
|
|
|
@@ -19,7 +19,6 @@ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } fr
|
|
|
19
19
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
20
20
|
import { createBooleanField, createEntity, createTextField, defineFeature } from "../../engine";
|
|
21
21
|
import { createRegistry } from "../../engine/registry";
|
|
22
|
-
import { createEventsTable } from "../../event-store";
|
|
23
22
|
import { rebuildProjection } from "../../pipeline";
|
|
24
23
|
import { createProjectionStateTable } from "../../pipeline/projection-state";
|
|
25
24
|
import { TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
@@ -52,7 +51,6 @@ beforeAll(async () => {
|
|
|
52
51
|
await ensureTemporalPolyfill();
|
|
53
52
|
testDb = await createTestDb();
|
|
54
53
|
await unsafeCreateEntityTable(testDb.db, userEntity, "user");
|
|
55
|
-
await createEventsTable(testDb.db);
|
|
56
54
|
await createProjectionStateTable(testDb.db);
|
|
57
55
|
tdb = createTenantDb(testDb.db, adminUser.tenantId);
|
|
58
56
|
});
|
|
@@ -17,7 +17,6 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:tes
|
|
|
17
17
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
18
18
|
import { asRawClient, selectMany, transaction } from "../../db/query";
|
|
19
19
|
import { createEntity, createTextField } from "../../engine";
|
|
20
|
-
import { createEventsTable } from "../../event-store";
|
|
21
20
|
import { TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
22
21
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
23
22
|
import { createEventStoreExecutor } from "../event-store-executor";
|
|
@@ -52,7 +51,6 @@ beforeAll(async () => {
|
|
|
52
51
|
await ensureTemporalPolyfill();
|
|
53
52
|
testDb = await createTestDb();
|
|
54
53
|
await unsafeCreateEntityTable(testDb.db, userEntity, "unique-user");
|
|
55
|
-
await createEventsTable(testDb.db);
|
|
56
54
|
tdb = createTenantDb(testDb.db, admin.tenantId);
|
|
57
55
|
});
|
|
58
56
|
|
|
@@ -134,15 +134,17 @@ export function resetEntityFieldEncryptionCacheForTests(): void {
|
|
|
134
134
|
// surfaces missing AND malformed keys (wrong length, bad base64) at boot
|
|
135
135
|
// instead of on the first encrypted read in prod. validateBoot runs before
|
|
136
136
|
// configureEntityFieldEncryption, so the env probe is the common path.
|
|
137
|
-
export function validateEntityFieldEncryptionAvailable(
|
|
137
|
+
export function validateEntityFieldEncryptionAvailable(
|
|
138
|
+
env: Readonly<Record<string, string | undefined>> = process.env,
|
|
139
|
+
): void {
|
|
138
140
|
// skip: an injected cipher (test seam / custom KMS) satisfies availability
|
|
139
141
|
if (injectedCipher) return;
|
|
140
142
|
try {
|
|
141
143
|
createEnvMasterKeyProvider({
|
|
142
144
|
env: {
|
|
143
|
-
...
|
|
145
|
+
...env,
|
|
144
146
|
KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION:
|
|
145
|
-
|
|
147
|
+
env["KUMIKO_SECRETS_MASTER_KEY_CURRENT_VERSION"] ?? "1",
|
|
146
148
|
},
|
|
147
149
|
});
|
|
148
150
|
} catch (err) {
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { type BunTestDb, createTestDb } from "../../../bun-db/__tests__/bun-test-db";
|
|
3
|
-
import { createEventsTable } from "../../../event-store/events-schema";
|
|
4
3
|
import { asRawClient } from "../../query";
|
|
5
4
|
import { ensureIdempotencyKeyIndex } from "../event-store";
|
|
6
5
|
|
|
@@ -8,7 +7,6 @@ let testDb: BunTestDb;
|
|
|
8
7
|
|
|
9
8
|
beforeAll(async () => {
|
|
10
9
|
testDb = await createTestDb();
|
|
11
|
-
await createEventsTable(testDb.db);
|
|
12
10
|
});
|
|
13
11
|
|
|
14
12
|
afterAll(async () => {
|
|
@@ -100,6 +100,10 @@ export type ValidateBootOptions = {
|
|
|
100
100
|
* screen is reachable another way (e.g. as a sub-page of a generated
|
|
101
101
|
* settings hub). Suppresses the navAllowlist warning for these QNs. */
|
|
102
102
|
readonly navAllowlistExempt?: ReadonlySet<string>;
|
|
103
|
+
/** Env the encrypted-field master-key probe reads. Defaults to
|
|
104
|
+
* `process.env`; runners pass their resolved env so a ciphertext-only
|
|
105
|
+
* key (already decrypted at boot) is visible to the probe. */
|
|
106
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
103
107
|
};
|
|
104
108
|
|
|
105
109
|
/**
|
|
@@ -276,7 +280,7 @@ export function validateBoot(
|
|
|
276
280
|
// catches malformed keys (wrong length, bad base64) at boot instead of
|
|
277
281
|
// on the first encrypted read in prod. An injected cipher (test seam,
|
|
278
282
|
// custom KMS provider) satisfies the requirement without env keys.
|
|
279
|
-
validateEntityFieldEncryptionAvailable();
|
|
283
|
+
validateEntityFieldEncryptionAvailable(options?.env);
|
|
280
284
|
}
|
|
281
285
|
|
|
282
286
|
if (hasFileFields && !process.env[FILE_STORAGE_PROVIDER_ENV]) {
|
|
@@ -14,7 +14,7 @@ import { z } from "zod";
|
|
|
14
14
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
15
15
|
import { asRawClient } from "../../db/query";
|
|
16
16
|
import { createRegistry, defineFeature } from "../../engine";
|
|
17
|
-
import { createArchivedStreamsTable
|
|
17
|
+
import { createArchivedStreamsTable } from "../../event-store";
|
|
18
18
|
import { createJobRunner } from "../../jobs/job-runner";
|
|
19
19
|
import { createEventConsumerStateTable } from "../../pipeline";
|
|
20
20
|
import { createTestRedis, type TestRedis } from "../../stack";
|
|
@@ -63,7 +63,6 @@ let testRedis: TestRedis;
|
|
|
63
63
|
|
|
64
64
|
beforeAll(async () => {
|
|
65
65
|
[testDb, testRedis] = await Promise.all([createTestDb(), createTestRedis()]);
|
|
66
|
-
await createEventsTable(testDb.db);
|
|
67
66
|
await createArchivedStreamsTable(testDb.db);
|
|
68
67
|
await createEventConsumerStateTable(testDb.db);
|
|
69
68
|
});
|
|
@@ -16,7 +16,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
|
16
16
|
import { z } from "zod";
|
|
17
17
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
18
18
|
import { createRegistry, defineFeature } from "../../engine";
|
|
19
|
-
import { createArchivedStreamsTable
|
|
19
|
+
import { createArchivedStreamsTable } from "../../event-store";
|
|
20
20
|
import { createNoopProvider, createPrometheusMeter } from "../../observability";
|
|
21
21
|
import { createEventConsumerStateTable } from "../../pipeline";
|
|
22
22
|
import { createTestRedis, type TestRedis, TestUsers } from "../../stack";
|
|
@@ -79,7 +79,6 @@ let testRedis: TestRedis;
|
|
|
79
79
|
|
|
80
80
|
beforeAll(async () => {
|
|
81
81
|
[testDb, testRedis] = await Promise.all([createTestDb(), createTestRedis()]);
|
|
82
|
-
await createEventsTable(testDb.db);
|
|
83
82
|
await createArchivedStreamsTable(testDb.db);
|
|
84
83
|
await createEventConsumerStateTable(testDb.db);
|
|
85
84
|
});
|
|
@@ -17,7 +17,7 @@ import { z } from "zod";
|
|
|
17
17
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
18
18
|
import { asRawClient } from "../../db/query";
|
|
19
19
|
import { createRegistry, defineFeature } from "../../engine";
|
|
20
|
-
import { createArchivedStreamsTable
|
|
20
|
+
import { createArchivedStreamsTable } from "../../event-store";
|
|
21
21
|
import { createEventConsumerStateTable } from "../../pipeline";
|
|
22
22
|
import { createTestRedis, type TestRedis, TestUsers } from "../../stack";
|
|
23
23
|
import { createAllInOneEntrypoint, createApiEntrypoint, createWorkerEntrypoint } from "../index";
|
|
@@ -125,7 +125,6 @@ let testRedis: TestRedis;
|
|
|
125
125
|
|
|
126
126
|
beforeAll(async () => {
|
|
127
127
|
[testDb, testRedis] = await Promise.all([createTestDb(), createTestRedis()]);
|
|
128
|
-
await createEventsTable(testDb.db);
|
|
129
128
|
await createArchivedStreamsTable(testDb.db);
|
|
130
129
|
await createEventConsumerStateTable(testDb.db);
|
|
131
130
|
});
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { defineFeature } from "../../engine/define-feature";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
camelCase,
|
|
6
|
+
composeEnvSchema,
|
|
7
|
+
KumikoBootError,
|
|
8
|
+
kmsSlotsOf,
|
|
9
|
+
parseEnv,
|
|
10
|
+
pulumiConfigKey,
|
|
11
|
+
readKumikoMeta,
|
|
12
|
+
} from "../index";
|
|
5
13
|
|
|
6
14
|
describe("composeEnvSchema", () => {
|
|
7
15
|
it("merges per-feature schemas and tags sources", () => {
|
|
@@ -295,3 +303,108 @@ describe("pulumiConfigKey + camelCase", () => {
|
|
|
295
303
|
expect(pulumiConfigKey("JWT_SECRET", undefined, undefined)).toBe("jwtSecret");
|
|
296
304
|
});
|
|
297
305
|
});
|
|
306
|
+
|
|
307
|
+
describe("kms env slots", () => {
|
|
308
|
+
const schema = z.object({
|
|
309
|
+
MASTER_KEY: z
|
|
310
|
+
.string()
|
|
311
|
+
.min(1)
|
|
312
|
+
.meta({ kumiko: { kms: true } }),
|
|
313
|
+
MASTER_KEY_CIPHERTEXT: z.string().min(1).optional(),
|
|
314
|
+
PLAIN: z.string().min(1).optional(),
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("kmsSlotsOf lists exactly the fields declaring kms", () => {
|
|
318
|
+
expect(kmsSlotsOf(schema)).toEqual(["MASTER_KEY"]);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it("rejects a non-true kms value in the meta", () => {
|
|
322
|
+
const bad = z.string().meta({ kumiko: { kms: "yes" } });
|
|
323
|
+
expect(readKumikoMeta(bad)).toEqual({});
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it("parseEnv accepts a required kms slot that is present only as ciphertext", () => {
|
|
327
|
+
expect(() => parseEnv(schema, { MASTER_KEY_CIPHERTEXT: "abc" })).not.toThrow();
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("parseEnv still requires a kms slot when no ciphertext twin is set", () => {
|
|
331
|
+
expect(() => parseEnv(schema, {})).toThrow(KumikoBootError);
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("parseEnv still validates a kms slot that is set", () => {
|
|
335
|
+
expect(() => parseEnv(schema, { MASTER_KEY: "" })).toThrow(KumikoBootError);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
describe("schema carrying a refinement", () => {
|
|
339
|
+
const refined = schema.superRefine((value, ctx) => {
|
|
340
|
+
if (value.PLAIN === "forbidden") {
|
|
341
|
+
ctx.addIssue({ code: "custom", path: ["PLAIN"], message: "PLAIN is forbidden" });
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("parseEnv relaxes a ciphertext-only slot without dropping the refinement", () => {
|
|
346
|
+
expect(parseEnv(refined, { MASTER_KEY_CIPHERTEXT: "abc" })["MASTER_KEY_CIPHERTEXT"]).toBe(
|
|
347
|
+
"abc",
|
|
348
|
+
);
|
|
349
|
+
expect(() => parseEnv(refined, { MASTER_KEY_CIPHERTEXT: "abc", PLAIN: "forbidden" })).toThrow(
|
|
350
|
+
KumikoBootError,
|
|
351
|
+
);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe("composeEnvSchema kms twins", () => {
|
|
357
|
+
const kmsField = z
|
|
358
|
+
.string()
|
|
359
|
+
.min(1)
|
|
360
|
+
.meta({ kumiko: { kms: true } });
|
|
361
|
+
|
|
362
|
+
it("declares the ciphertext twin of every kms field, owned by the same source", () => {
|
|
363
|
+
const secrets = defineFeature("secrets", (r) => {
|
|
364
|
+
r.envSchema(z.object({ KEY_V1: kmsField }));
|
|
365
|
+
});
|
|
366
|
+
const { schema, sources } = composeEnvSchema({
|
|
367
|
+
features: [secrets],
|
|
368
|
+
extend: z.object({ KEY_V2: kmsField }),
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
expect(Object.keys(schema.shape).sort()).toEqual([
|
|
372
|
+
"KEY_V1",
|
|
373
|
+
"KEY_V1_CIPHERTEXT",
|
|
374
|
+
"KEY_V2",
|
|
375
|
+
"KEY_V2_CIPHERTEXT",
|
|
376
|
+
]);
|
|
377
|
+
expect(sources["KEY_V1_CIPHERTEXT"]).toBe("secrets");
|
|
378
|
+
expect(sources["KEY_V2_CIPHERTEXT"]).toBe("app");
|
|
379
|
+
expect(() => parseEnv(schema, {})).toThrow(KumikoBootError);
|
|
380
|
+
expect(() =>
|
|
381
|
+
parseEnv(schema, { KEY_V1_CIPHERTEXT: "a", KEY_V2_CIPHERTEXT: "b" }),
|
|
382
|
+
).not.toThrow();
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
it("keeps a twin the app already declared instead of conflicting", () => {
|
|
386
|
+
const declared = z.string().min(1).optional().describe("hand-written twin");
|
|
387
|
+
const { schema } = composeEnvSchema({
|
|
388
|
+
features: [],
|
|
389
|
+
extend: z.object({ KEY: kmsField, KEY_CIPHERTEXT: declared }),
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
expect(schema.shape["KEY_CIPHERTEXT"]).toBe(declared);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("adds the twin for a kms field of an optional feature too", () => {
|
|
396
|
+
const secrets = defineFeature("secrets", (r) => {
|
|
397
|
+
r.envSchema(z.object({ KEY: kmsField }));
|
|
398
|
+
});
|
|
399
|
+
const { schema } = composeEnvSchema({ features: [secrets], optionalFeatures: ["secrets"] });
|
|
400
|
+
|
|
401
|
+
expect(Object.keys(schema.shape).sort()).toEqual(["KEY", "KEY_CIPHERTEXT"]);
|
|
402
|
+
expect(kmsSlotsOf(schema)).toEqual(["KEY"]);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it("adds nothing for a schema without kms fields", () => {
|
|
406
|
+
const { schema } = composeEnvSchema({ features: [], extend: z.object({ PLAIN: z.string() }) });
|
|
407
|
+
|
|
408
|
+
expect(Object.keys(schema.shape)).toEqual(["PLAIN"]);
|
|
409
|
+
});
|
|
410
|
+
});
|
package/src/env/index.ts
CHANGED
|
@@ -34,12 +34,18 @@ export type KumikoEnvMeta = {
|
|
|
34
34
|
/** Force the `--secret` flag in `pulumi config set`. Default false. */
|
|
35
35
|
readonly secret?: boolean;
|
|
36
36
|
};
|
|
37
|
+
/** The value may be delivered as a Scaleway Key Manager ciphertext in
|
|
38
|
+
* `<NAME>_CIPHERTEXT`. `runProdApp` decrypts it at boot when `<NAME>` itself
|
|
39
|
+
* is unset; a set plaintext always wins. A flag, not an object: every slot
|
|
40
|
+
* uses the same `_CIPHERTEXT` twin and one shared key (`PLATFORM_KEK_KMS_*`). */
|
|
41
|
+
readonly kms?: true;
|
|
37
42
|
};
|
|
38
43
|
|
|
39
44
|
function isKumikoMeta(value: unknown): value is KumikoEnvMeta {
|
|
40
45
|
if (value === null || typeof value !== "object") return false;
|
|
41
46
|
// @cast-boundary schema-walk — runtime-shape narrowing of zod-meta payload
|
|
42
|
-
const v = value as { pulumi?: unknown };
|
|
47
|
+
const v = value as { pulumi?: unknown; kms?: unknown };
|
|
48
|
+
if (v.kms !== undefined && v.kms !== true) return false;
|
|
43
49
|
if (v.pulumi === undefined) return true;
|
|
44
50
|
if (v.pulumi === null || typeof v.pulumi !== "object") return false;
|
|
45
51
|
// @cast-boundary schema-walk
|
|
@@ -60,6 +66,32 @@ export function readKumikoMeta(field: z.ZodType): KumikoEnvMeta {
|
|
|
60
66
|
return {};
|
|
61
67
|
}
|
|
62
68
|
|
|
69
|
+
/** Env names whose schema field declares `kms: true` — the slots
|
|
70
|
+
* `resolvePlatformKeks` resolves from a ciphertext. */
|
|
71
|
+
export function kmsSlotsOf(schema: z.ZodObject<z.ZodRawShape>): readonly string[] {
|
|
72
|
+
return Object.entries(zodShape(schema))
|
|
73
|
+
.filter(([, field]) => readKumikoMeta(field).kms === true)
|
|
74
|
+
.map(([name]) => name);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// A ciphertext-only slot is satisfied by its `_CIPHERTEXT` twin: the plaintext
|
|
78
|
+
// only exists after the boot-time decrypt, so requiring it here would reject
|
|
79
|
+
// exactly the deployment this meta enables. safeExtend, not extend: Zod 4 throws
|
|
80
|
+
// on extend for schemas carrying refinements (app-level superRefine).
|
|
81
|
+
function relaxCiphertextOnlySlots<S extends z.ZodObject<z.ZodRawShape>>(
|
|
82
|
+
schema: S,
|
|
83
|
+
env: Readonly<Record<string, string>>,
|
|
84
|
+
): z.ZodObject<z.ZodRawShape> {
|
|
85
|
+
const shape = zodShape(schema);
|
|
86
|
+
const relaxed: Record<string, z.ZodType> = {};
|
|
87
|
+
for (const name of kmsSlotsOf(schema)) {
|
|
88
|
+
const field = shape[name];
|
|
89
|
+
if (field && env[name] === undefined && env[`${name}_CIPHERTEXT`])
|
|
90
|
+
relaxed[name] = field.optional();
|
|
91
|
+
}
|
|
92
|
+
return Object.keys(relaxed).length === 0 ? schema : schema.safeExtend(relaxed);
|
|
93
|
+
}
|
|
94
|
+
|
|
63
95
|
// --- Field-classification helpers (Zod v4 introspection) ---
|
|
64
96
|
|
|
65
97
|
export type EnvFieldClass = "required" | "optional" | "withDefault";
|
|
@@ -144,10 +176,23 @@ export type ComposedEnvSchema = {
|
|
|
144
176
|
readonly sources: Readonly<Record<string, string>>;
|
|
145
177
|
};
|
|
146
178
|
|
|
179
|
+
// `.optional()` returns a new Zod instance that drops `.meta()`, which would
|
|
180
|
+
// hide the slot from `kmsSlotsOf` on the composed schema.
|
|
181
|
+
function optionalKeepingKmsMeta(field: z.ZodType): z.ZodType {
|
|
182
|
+
const optional = field.optional();
|
|
183
|
+
return readKumikoMeta(field).kms === true
|
|
184
|
+
? optional.meta({ kumiko: readKumikoMeta(field) })
|
|
185
|
+
: optional;
|
|
186
|
+
}
|
|
187
|
+
|
|
147
188
|
export function composeEnvSchema(options: ComposeEnvSchemaOptions): ComposedEnvSchema {
|
|
148
189
|
const optionalSet = new Set(options.optionalFeatures ?? []);
|
|
149
190
|
const merged: Record<string, z.ZodType> = {};
|
|
150
191
|
const sources: Record<string, string> = {};
|
|
192
|
+
const kmsFields: { readonly name: string; readonly source: string }[] = [];
|
|
193
|
+
const noteKms = (name: string, field: z.ZodType, source: string): void => {
|
|
194
|
+
if (readKumikoMeta(field).kms === true) kmsFields.push({ name, source });
|
|
195
|
+
};
|
|
151
196
|
|
|
152
197
|
// Framework-core first so a feature that accidentally declares the same
|
|
153
198
|
// var (e.g. PORT) gets a clear conflict error citing "framework-core"
|
|
@@ -159,6 +204,7 @@ export function composeEnvSchema(options: ComposeEnvSchemaOptions): ComposedEnvS
|
|
|
159
204
|
for (const [key, field] of Object.entries(zodShape(options.core))) {
|
|
160
205
|
merged[key] = field;
|
|
161
206
|
sources[key] = "framework-core";
|
|
207
|
+
noteKms(key, field, "framework-core");
|
|
162
208
|
}
|
|
163
209
|
}
|
|
164
210
|
|
|
@@ -178,8 +224,9 @@ export function composeEnvSchema(options: ComposeEnvSchemaOptions): ComposedEnvS
|
|
|
178
224
|
},
|
|
179
225
|
]);
|
|
180
226
|
}
|
|
181
|
-
merged[key] = wrap ? field
|
|
227
|
+
merged[key] = wrap ? optionalKeepingKmsMeta(field) : field;
|
|
182
228
|
sources[key] = feature.name;
|
|
229
|
+
noteKms(key, field, feature.name);
|
|
183
230
|
}
|
|
184
231
|
}
|
|
185
232
|
|
|
@@ -198,9 +245,23 @@ export function composeEnvSchema(options: ComposeEnvSchemaOptions): ComposedEnvS
|
|
|
198
245
|
}
|
|
199
246
|
merged[key] = field;
|
|
200
247
|
sources[key] = "app";
|
|
248
|
+
noteKms(key, field, "app");
|
|
201
249
|
}
|
|
202
250
|
}
|
|
203
251
|
|
|
252
|
+
// The twin follows from the slot, so a versioned family (`…_V2`) needs no
|
|
253
|
+
// second declaration. An already declared twin (apps that predate this) stays.
|
|
254
|
+
for (const { name, source } of kmsFields) {
|
|
255
|
+
const twin = `${name}_CIPHERTEXT`;
|
|
256
|
+
if (merged[twin] !== undefined) continue;
|
|
257
|
+
merged[twin] = z
|
|
258
|
+
.string()
|
|
259
|
+
.min(1)
|
|
260
|
+
.optional()
|
|
261
|
+
.describe(`Key-Manager ciphertext of ${name}; used when ${name} is unset.`);
|
|
262
|
+
sources[twin] = source;
|
|
263
|
+
}
|
|
264
|
+
|
|
204
265
|
return {
|
|
205
266
|
schema: z.object(merged),
|
|
206
267
|
sources,
|
|
@@ -282,7 +343,7 @@ export function parseEnv<S extends z.ZodObject<z.ZodRawShape>>(
|
|
|
282
343
|
if (v !== undefined) cleaned[k] = v;
|
|
283
344
|
}
|
|
284
345
|
|
|
285
|
-
const result = schema.safeParse(cleaned);
|
|
346
|
+
const result = relaxCiphertextOnlySlots(schema, cleaned).safeParse(cleaned);
|
|
286
347
|
if (result.success) {
|
|
287
348
|
// @cast-boundary schema-walk — z.infer<S> erasure across safeParse result
|
|
288
349
|
return result.data as z.infer<S>;
|
|
@@ -21,7 +21,6 @@ import { join } from "node:path";
|
|
|
21
21
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
22
22
|
import { asRawClient, selectMany } from "../../db/query";
|
|
23
23
|
import { createRegistry } from "../../engine";
|
|
24
|
-
import { createEventsTable } from "../../event-store";
|
|
25
24
|
import { createDispatcher, type Dispatcher } from "../../pipeline";
|
|
26
25
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
27
26
|
import { createSeedMigrationContext } from "../context";
|
|
@@ -38,7 +37,6 @@ let dispatcher: Dispatcher;
|
|
|
38
37
|
beforeAll(async () => {
|
|
39
38
|
await ensureTemporalPolyfill();
|
|
40
39
|
testDb = await createTestDb();
|
|
41
|
-
await createEventsTable(testDb.db);
|
|
42
40
|
await createEsOperationsTable(testDb.db);
|
|
43
41
|
|
|
44
42
|
const registry = createRegistry([]);
|
|
@@ -29,7 +29,6 @@ import {
|
|
|
29
29
|
SYSTEM_TENANT_ID,
|
|
30
30
|
} from "../../engine";
|
|
31
31
|
import { VersionConflictError } from "../../errors";
|
|
32
|
-
import { createEventsTable } from "../../event-store";
|
|
33
32
|
import { createDispatcher, type Dispatcher } from "../../pipeline";
|
|
34
33
|
import { createTestDb, type TestDb, unsafeCreateEntityTable } from "../../stack";
|
|
35
34
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
@@ -89,7 +88,6 @@ let registry: ReturnType<typeof createRegistry>;
|
|
|
89
88
|
beforeAll(async () => {
|
|
90
89
|
await ensureTemporalPolyfill();
|
|
91
90
|
testDb = await createTestDb();
|
|
92
|
-
await createEventsTable(testDb.db);
|
|
93
91
|
await createEsOperationsTable(testDb.db);
|
|
94
92
|
await unsafeCreateEntityTable(testDb.db, probeEntity, "esops-probe");
|
|
95
93
|
registry = createRegistry([seedTestFeature]);
|
|
@@ -17,7 +17,7 @@ import { generateId as uuid } from "../../utils";
|
|
|
17
17
|
import { appendRaw, appendRawBatch, type RawEventToAppend } from "../admin-api";
|
|
18
18
|
import { IdempotentAppendConflictError, VersionConflictError } from "../errors";
|
|
19
19
|
import { append, loadAggregate } from "../event-store";
|
|
20
|
-
import {
|
|
20
|
+
import { eventsTable } from "../events-schema";
|
|
21
21
|
|
|
22
22
|
// Test-only spy: wrap a DbConnection's `.unsafe()` to capture the SQL
|
|
23
23
|
// string of every query the framework runs. Used to assert batching
|
|
@@ -47,7 +47,6 @@ const legacyUser = "legacy-user-42";
|
|
|
47
47
|
|
|
48
48
|
beforeAll(async () => {
|
|
49
49
|
testDb = await createTestDb();
|
|
50
|
-
await createEventsTable(testDb.db);
|
|
51
50
|
});
|
|
52
51
|
|
|
53
52
|
afterAll(async () => {
|
|
@@ -34,7 +34,6 @@ import type { Registry, TenantId } from "../../engine/types";
|
|
|
34
34
|
import { createTestDb, type TestDb, unsafeCreateEntityTable } from "../../stack";
|
|
35
35
|
import { generateId } from "../../utils";
|
|
36
36
|
import { append, loadAggregate } from "../event-store";
|
|
37
|
-
import { createEventsTable } from "../events-schema";
|
|
38
37
|
|
|
39
38
|
const TENANT = "00000000-0000-4000-8000-000000000001" as TenantId;
|
|
40
39
|
const BIDX_KEY = Buffer.alloc(32, 5).toString("base64");
|
|
@@ -145,7 +144,6 @@ beforeAll(async () => {
|
|
|
145
144
|
surveyFeature,
|
|
146
145
|
signalsFeature,
|
|
147
146
|
]);
|
|
148
|
-
await createEventsTable(testDb.db);
|
|
149
147
|
await unsafeCreateEntityTable(testDb.db, contactEntity, "contact");
|
|
150
148
|
await unsafeCreateEntityTable(testDb.db, noteEntity, "note");
|
|
151
149
|
await unsafeCreateEntityTable(testDb.db, surveyEntity, "survey");
|
|
@@ -5,7 +5,6 @@ import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
|
5
5
|
import { generateId as uuid } from "../../utils";
|
|
6
6
|
import {
|
|
7
7
|
append,
|
|
8
|
-
createEventsTable,
|
|
9
8
|
IdempotentAppendConflictError,
|
|
10
9
|
loadAggregate,
|
|
11
10
|
loadAggregateAsOf,
|
|
@@ -25,7 +24,6 @@ const userA = uuid();
|
|
|
25
24
|
beforeAll(async () => {
|
|
26
25
|
await ensureTemporalPolyfill();
|
|
27
26
|
testDb = await createTestDb();
|
|
28
|
-
await createEventsTable(testDb.db);
|
|
29
27
|
});
|
|
30
28
|
|
|
31
29
|
afterAll(async () => {
|
|
@@ -15,7 +15,7 @@ import { insertMany } from "../../bun-db/query";
|
|
|
15
15
|
import type { TenantId } from "../../engine/types";
|
|
16
16
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
17
17
|
import { generateId as uuid } from "../../utils";
|
|
18
|
-
import {
|
|
18
|
+
import { eventsTable, getStreamVersion } from "../index";
|
|
19
19
|
|
|
20
20
|
let testDb: BunTestDb;
|
|
21
21
|
const tenantId: TenantId = uuid();
|
|
@@ -24,7 +24,6 @@ const userId = uuid();
|
|
|
24
24
|
beforeAll(async () => {
|
|
25
25
|
await ensureTemporalPolyfill();
|
|
26
26
|
testDb = await createTestDb();
|
|
27
|
-
await createEventsTable(testDb.db);
|
|
28
27
|
});
|
|
29
28
|
|
|
30
29
|
afterAll(async () => {
|
|
@@ -24,13 +24,7 @@ import { asRawClient } from "../../db/query";
|
|
|
24
24
|
import type { TenantId } from "../../engine/types";
|
|
25
25
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
26
26
|
import { generateId as uuid } from "../../utils";
|
|
27
|
-
import {
|
|
28
|
-
append,
|
|
29
|
-
createEventsTable,
|
|
30
|
-
loadAggregate,
|
|
31
|
-
loadAggregateWithSnapshot,
|
|
32
|
-
saveSnapshot,
|
|
33
|
-
} from "../index";
|
|
27
|
+
import { append, loadAggregate, loadAggregateWithSnapshot, saveSnapshot } from "../index";
|
|
34
28
|
|
|
35
29
|
let testDb: BunTestDb;
|
|
36
30
|
const tenantId = uuid() as TenantId;
|
|
@@ -39,7 +33,6 @@ const userId = uuid();
|
|
|
39
33
|
beforeAll(async () => {
|
|
40
34
|
await ensureTemporalPolyfill();
|
|
41
35
|
testDb = await createTestDb();
|
|
42
|
-
await createEventsTable(testDb.db);
|
|
43
36
|
});
|
|
44
37
|
|
|
45
38
|
afterAll(async () => {
|
|
@@ -5,7 +5,7 @@ import { createTenantDb, type TenantDb } from "../../db/tenant-db";
|
|
|
5
5
|
import { AccessDeniedError, InternalError } from "../../errors";
|
|
6
6
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
7
7
|
import { generateId as uuid } from "../../utils";
|
|
8
|
-
import {
|
|
8
|
+
import { loadAggregate, VersionConflictError } from "../index";
|
|
9
9
|
import { appendProvenanceEvent, type ProvenanceEventInput } from "../provenance-append";
|
|
10
10
|
|
|
11
11
|
let testDb: BunTestDb;
|
|
@@ -37,7 +37,6 @@ function provenanceEvent(overrides: Partial<ProvenanceEventInput>): ProvenanceEv
|
|
|
37
37
|
beforeAll(async () => {
|
|
38
38
|
await ensureTemporalPolyfill();
|
|
39
39
|
testDb = await createTestDb();
|
|
40
|
-
await createEventsTable(testDb.db);
|
|
41
40
|
tdb = createTenantDb(testDb.db, tenantA);
|
|
42
41
|
});
|
|
43
42
|
|
|
@@ -22,7 +22,6 @@ import { generateId as uuid } from "../../utils";
|
|
|
22
22
|
import {
|
|
23
23
|
append,
|
|
24
24
|
archiveStream,
|
|
25
|
-
createEventsTable,
|
|
26
25
|
loadAggregate,
|
|
27
26
|
loadAggregateWithSnapshot,
|
|
28
27
|
loadLatestSnapshot,
|
|
@@ -56,7 +55,6 @@ const reducer: SnapshotReducer<CounterState> = (state, event) => {
|
|
|
56
55
|
beforeAll(async () => {
|
|
57
56
|
await ensureTemporalPolyfill();
|
|
58
57
|
bun = await createTestDb();
|
|
59
|
-
await createEventsTable(bun.db);
|
|
60
58
|
});
|
|
61
59
|
|
|
62
60
|
afterAll(async () => {
|
|
@@ -11,7 +11,7 @@ import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db
|
|
|
11
11
|
import { asRawClient, insertOne, selectMany } from "../../db/query";
|
|
12
12
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
13
13
|
import type { StoredEvent } from "../event-store";
|
|
14
|
-
import {
|
|
14
|
+
import { eventsTable } from "../events-schema";
|
|
15
15
|
import { type EventUpcasters, makeUpcastCtx, upcastStoredEvents } from "../upcaster";
|
|
16
16
|
import {
|
|
17
17
|
createUpcasterDeadLetterTable,
|
|
@@ -81,7 +81,6 @@ const passthroughUpcasters: EventUpcasters = new Map([
|
|
|
81
81
|
beforeAll(async () => {
|
|
82
82
|
await ensureTemporalPolyfill();
|
|
83
83
|
testDb = await createTestDb();
|
|
84
|
-
await createEventsTable(testDb.db);
|
|
85
84
|
await createUpcasterDeadLetterTable(testDb.db);
|
|
86
85
|
});
|
|
87
86
|
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
unsafeCreateEntityTable,
|
|
26
26
|
unsafePushTables,
|
|
27
27
|
} from "../../stack";
|
|
28
|
-
import { append
|
|
28
|
+
import { append } from "../index";
|
|
29
29
|
import { upcastStoredEvent } from "../upcaster";
|
|
30
30
|
|
|
31
31
|
// --- Fixture entity + projection table ---
|
|
@@ -113,7 +113,6 @@ const orderExecutor = createEventStoreExecutor(orderTable, orderEntity, {
|
|
|
113
113
|
beforeAll(async () => {
|
|
114
114
|
testDb = await createTestDb();
|
|
115
115
|
await unsafeCreateEntityTable(testDb.db, orderEntity, "upcast-order");
|
|
116
|
-
await createEventsTable(testDb.db);
|
|
117
116
|
const { createProjectionStateTable } = await import("../../pipeline");
|
|
118
117
|
await createProjectionStateTable(testDb.db);
|
|
119
118
|
await unsafePushTables(testDb.db, { upcastOrderSummary: orderSummaryTable });
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
defineFeature,
|
|
15
15
|
type SessionUser,
|
|
16
16
|
} from "../../engine";
|
|
17
|
-
import {
|
|
17
|
+
import { loadAggregate } from "../../event-store";
|
|
18
18
|
import {
|
|
19
19
|
createTestDb,
|
|
20
20
|
createTestUser,
|
|
@@ -79,7 +79,6 @@ beforeAll(async () => {
|
|
|
79
79
|
await unsafeCreateEntityTable(testDb.db, testTenantEntity);
|
|
80
80
|
// Event-store table: the upload route appends files:event:uploaded in the
|
|
81
81
|
// same tx as the FileRef insert. Without events, upload would 500.
|
|
82
|
-
await createEventsTable(testDb.db);
|
|
83
82
|
|
|
84
83
|
const registry = createRegistry([tenantFeature]);
|
|
85
84
|
const storageProvider = createLocalProvider(storagePath);
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
type WriteResult,
|
|
22
22
|
} from "../../engine";
|
|
23
23
|
import { createWorkerEntrypoint } from "../../entrypoint";
|
|
24
|
-
import { createArchivedStreamsTable
|
|
24
|
+
import { createArchivedStreamsTable } from "../../event-store";
|
|
25
25
|
import { createEventConsumerStateTable } from "../../pipeline";
|
|
26
26
|
import { createTestRedis, type TestRedis } from "../../stack";
|
|
27
27
|
import { waitFor } from "../../testing";
|
|
@@ -110,7 +110,6 @@ let testRedis: TestRedis;
|
|
|
110
110
|
|
|
111
111
|
beforeAll(async () => {
|
|
112
112
|
[testDb, testRedis] = await Promise.all([createTestDb(), createTestRedis()]);
|
|
113
|
-
await createEventsTable(testDb.db);
|
|
114
113
|
await createArchivedStreamsTable(testDb.db);
|
|
115
114
|
await createEventConsumerStateTable(testDb.db);
|
|
116
115
|
});
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
defineFeature,
|
|
21
21
|
type ProjectionDefinition,
|
|
22
22
|
} from "../../engine";
|
|
23
|
-
import { createEventsTable } from "../../event-store";
|
|
24
23
|
import type { JobRunner } from "../../jobs/job-runner";
|
|
25
24
|
import { createProjectionStateTable } from "../../pipeline";
|
|
26
25
|
import {
|
|
@@ -97,7 +96,6 @@ let markerDir: string;
|
|
|
97
96
|
beforeAll(async () => {
|
|
98
97
|
testDb = await createTestDb();
|
|
99
98
|
await unsafeCreateEntityTable(testDb.db, itemEntity, "pending-item");
|
|
100
|
-
await createEventsTable(testDb.db);
|
|
101
99
|
await createProjectionStateTable(testDb.db);
|
|
102
100
|
await unsafePushTables(testDb.db, {
|
|
103
101
|
readPendingCounts: countsTable,
|
|
@@ -10,7 +10,6 @@ import { asRawClient } from "../../db/query";
|
|
|
10
10
|
import { buildEntityTable } from "../../db/table-builder";
|
|
11
11
|
import { createTenantDb, type TenantDb } from "../../db/tenant-db";
|
|
12
12
|
import { createEntity, createRegistry, createTextField, defineFeature } from "../../engine";
|
|
13
|
-
import { createEventsTable } from "../../event-store";
|
|
14
13
|
import { createProjectionStateTable, rebuildProjection } from "../../pipeline";
|
|
15
14
|
import { createTestDb, type TestDb, TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
16
15
|
|
|
@@ -38,7 +37,6 @@ const crud = createEventStoreExecutor(personTable, personEntity, { entityName: "
|
|
|
38
37
|
beforeAll(async () => {
|
|
39
38
|
testDb = await createTestDb();
|
|
40
39
|
await unsafeCreateEntityTable(testDb.db, personEntity, "person");
|
|
41
|
-
await createEventsTable(testDb.db);
|
|
42
40
|
await createProjectionStateTable(testDb.db);
|
|
43
41
|
tdb = createTenantDb(testDb.db, admin.tenantId);
|
|
44
42
|
});
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
defineFeature,
|
|
12
12
|
type Registry,
|
|
13
13
|
} from "../../engine";
|
|
14
|
-
import { createEventsTable } from "../../event-store";
|
|
15
14
|
import { TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
16
15
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
17
16
|
import { createCascadeDeleteHook } from "../cascade-handler";
|
|
@@ -96,7 +95,6 @@ const memberEntity = createEntity({
|
|
|
96
95
|
beforeAll(async () => {
|
|
97
96
|
await ensureTemporalPolyfill();
|
|
98
97
|
testDb = await createTestDb();
|
|
99
|
-
await createEventsTable(testDb.db);
|
|
100
98
|
tdb = createTenantDb(testDb.db, admin.tenantId);
|
|
101
99
|
|
|
102
100
|
await unsafeCreateEntityTable(testDb.db, departmentEntity);
|
|
@@ -16,7 +16,6 @@ import { integer, table as pgTable, uuid as pgUuid } from "../../db/dialect";
|
|
|
16
16
|
import { asRawClient } from "../../db/query";
|
|
17
17
|
import { createEntity, createRegistry, createTextField, defineFeature } from "../../engine";
|
|
18
18
|
import type { ProjectionDefinition } from "../../engine/types";
|
|
19
|
-
import { createEventsTable } from "../../event-store";
|
|
20
19
|
import { createProjectionStateTable, rebuildProjection } from "../../pipeline";
|
|
21
20
|
import { TestUsers, unsafePushTables } from "../../stack";
|
|
22
21
|
import { ensureTemporalPolyfill } from "../../time/polyfill";
|
|
@@ -68,7 +67,6 @@ const qualifiedProjectionName = "perfrebuild:projection:task-count";
|
|
|
68
67
|
beforeAll(async () => {
|
|
69
68
|
await ensureTemporalPolyfill();
|
|
70
69
|
testDb = await createTestDb();
|
|
71
|
-
await createEventsTable(testDb.db);
|
|
72
70
|
await createProjectionStateTable(testDb.db);
|
|
73
71
|
await unsafePushTables(testDb.db, { perf_rebuild_task_count: taskCountTable });
|
|
74
72
|
});
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
defineFeature,
|
|
27
27
|
} from "../../engine";
|
|
28
28
|
import type { ProjectionDefinition } from "../../engine/types";
|
|
29
|
-
import { archiveStream
|
|
29
|
+
import { archiveStream } from "../../event-store";
|
|
30
30
|
import {
|
|
31
31
|
createProjectionStateTable,
|
|
32
32
|
getAllProjectionProgress,
|
|
@@ -104,7 +104,6 @@ const executor = createEventStoreExecutor(itemTable, itemEntity, { entityName: "
|
|
|
104
104
|
beforeAll(async () => {
|
|
105
105
|
testDb = await createTestDb();
|
|
106
106
|
await unsafeCreateEntityTable(testDb.db, itemEntity, "rebuild-item");
|
|
107
|
-
await createEventsTable(testDb.db);
|
|
108
107
|
await createProjectionStateTable(testDb.db);
|
|
109
108
|
await unsafePushTables(testDb.db, { rebuildItemsPerGroup: itemsPerGroupTable });
|
|
110
109
|
tdb = createTenantDb(testDb.db, admin.tenantId);
|
|
@@ -23,7 +23,7 @@ import { buildEntityTable } from "../../db/table-builder";
|
|
|
23
23
|
import { createTenantDb, type TenantDb } from "../../db/tenant-db";
|
|
24
24
|
import { createEntity, createTextField, defineApply, defineFeature } from "../../engine";
|
|
25
25
|
import type { ProjectionDefinition } from "../../engine/types";
|
|
26
|
-
import { append
|
|
26
|
+
import { append } from "../../event-store";
|
|
27
27
|
import { listRebuildDeadLetters } from "../../event-store/rebuild-dead-letter";
|
|
28
28
|
import {
|
|
29
29
|
createProjectionStateTable,
|
|
@@ -140,7 +140,6 @@ const executor = createEventStoreExecutor(itemTable, itemEntity, { entityName: "
|
|
|
140
140
|
beforeAll(async () => {
|
|
141
141
|
stack = await setupTestStack({ features: [feature] });
|
|
142
142
|
await unsafeCreateEntityTable(stack.db, itemEntity, "poison-item");
|
|
143
|
-
await createEventsTable(stack.db);
|
|
144
143
|
await createProjectionStateTable(stack.db);
|
|
145
144
|
await unsafePushTables(stack.db, {
|
|
146
145
|
poisonCounter: counterTable,
|
|
@@ -17,7 +17,6 @@ import {
|
|
|
17
17
|
createTenantDb,
|
|
18
18
|
} from "@cosmicdrift/kumiko-framework/db";
|
|
19
19
|
import { createEntity, createTextField, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
20
|
-
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
21
20
|
import { reindexEntity } from "@cosmicdrift/kumiko-framework/search";
|
|
22
21
|
import {
|
|
23
22
|
setupTestStack,
|
|
@@ -67,7 +66,6 @@ beforeAll(async () => {
|
|
|
67
66
|
stack = await setupTestStack({ features: [widgetFeature, contactFeature] });
|
|
68
67
|
await unsafeCreateEntityTable(stack.db, widgetEntity);
|
|
69
68
|
await unsafeCreateEntityTable(stack.db, contactEntity, "contact");
|
|
70
|
-
await createEventsTable(stack.db);
|
|
71
69
|
// Shared across both pii tests below (not per-test) — reindexEntity scans
|
|
72
70
|
// the whole tenant table regardless of which test created which row, so a
|
|
73
71
|
// fresh KMS instance per test would make earlier rows' subject keys
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
} from "../../crypto";
|
|
12
12
|
import { asRawClient, buildEntityTable, createEventStoreExecutor, createTenantDb } from "../../db";
|
|
13
13
|
import { createEntity, createRegistry, createTextField, defineFeature } from "../../engine";
|
|
14
|
-
import { createEventsTable } from "../../event-store";
|
|
15
14
|
import { setupTestStack, type TestStack, TestUsers, unsafeCreateEntityTable } from "../../stack";
|
|
16
15
|
import { purgeSearchDocumentsForSubject } from "../purge-subject";
|
|
17
16
|
|
|
@@ -53,7 +52,6 @@ const admin = TestUsers.admin;
|
|
|
53
52
|
beforeAll(async () => {
|
|
54
53
|
stack = await setupTestStack({ features: [contactFeature] });
|
|
55
54
|
await unsafeCreateEntityTable(stack.db, contactEntity, "contact");
|
|
56
|
-
await createEventsTable(stack.db);
|
|
57
55
|
});
|
|
58
56
|
|
|
59
57
|
afterAll(async () => {
|