@cosmicdrift/kumiko-bundled-features 0.110.0 → 0.112.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 (43) hide show
  1. package/package.json +6 -6
  2. package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.test.ts +3 -3
  3. package/src/auth-email-password/__tests__/account-lockout.integration.test.ts +3 -3
  4. package/src/auth-email-password/__tests__/auth-claims.integration.test.ts +9 -10
  5. package/src/auth-email-password/__tests__/auth.integration.test.ts +5 -5
  6. package/src/auth-email-password/__tests__/email-verification.integration.test.ts +7 -4
  7. package/src/auth-email-password/__tests__/identity-v3-login.integration.test.ts +3 -3
  8. package/src/auth-email-password/__tests__/password-reset.integration.test.ts +3 -4
  9. package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.test.ts +3 -3
  10. package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +3 -3
  11. package/src/config/__tests__/config.integration.test.ts +7 -8
  12. package/src/config/__tests__/encrypted-legacy-migration.integration.test.ts +179 -0
  13. package/src/config/__tests__/inherited-redaction.integration.test.ts +4 -3
  14. package/src/config/feature.ts +14 -8
  15. package/src/config/handlers/reencrypt.job.ts +197 -0
  16. package/src/config/handlers/set.write.ts +2 -2
  17. package/src/config/index.ts +4 -3
  18. package/src/config/resolver.ts +34 -10
  19. package/src/file-foundation/__tests__/file-foundation.integration.test.ts +4 -3
  20. package/src/folders/__tests__/folders.integration.test.ts +3 -0
  21. package/src/folders-user-data/__tests__/hooks.integration.test.ts +1 -0
  22. package/src/jobs/__tests__/job-system-user.integration.test.ts +4 -8
  23. package/src/mail-foundation/__tests__/mail-foundation.integration.test.ts +4 -3
  24. package/src/personal-access-tokens/__tests__/pat.integration.test.ts +3 -4
  25. package/src/readiness/__tests__/readiness.integration.test.ts +4 -3
  26. package/src/secrets/handlers/rotate.job.ts +77 -98
  27. package/src/secrets/secrets-context.ts +7 -38
  28. package/src/secrets/table.ts +6 -7
  29. package/src/sessions/__tests__/password-auto-revoke.integration.test.ts +6 -4
  30. package/src/sessions/__tests__/sessions.integration.test.ts +3 -3
  31. package/src/shared/chunked-entity-migration.ts +89 -0
  32. package/src/shared/index.ts +7 -0
  33. package/src/subscription-stripe/__tests__/stripe-foundation.integration.test.ts +6 -5
  34. package/src/tenant/__tests__/tenant.integration.test.ts +8 -4
  35. package/src/tier-engine/__tests__/tier-engine.integration.test.ts +7 -4
  36. package/src/user-data-rights/__tests__/download.integration.test.ts +7 -4
  37. package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +152 -0
  38. package/src/user-data-rights/__tests__/forget-hook-registry.integration.test.ts +108 -0
  39. package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +3 -3
  40. package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +3 -3
  41. package/src/user-data-rights/run-forget-cleanup.ts +1 -0
  42. package/src/user-data-rights/run-user-export.ts +43 -4
  43. package/src/user-data-rights-defaults/__tests__/user-data-rights-defaults.integration.test.ts +31 -9
@@ -0,0 +1,197 @@
1
+ // Re-encrypt job for `encrypted: true` config values. Two jobs in one,
2
+ // because format detection makes them the same loop:
3
+ // - MIGRATION: legacy CONFIG_ENCRYPTION_KEY values (base64 blob, no key
4
+ // id) → envelope format under the current master key. After a clean
5
+ // run the legacy key can be dropped from the environment.
6
+ // - KEK-ROTATION: envelope values wrapped under an older kekVersion →
7
+ // re-encrypted under provider.currentVersion(). Config has no
8
+ // kek_version column (values live in a TEXT column), so unlike the
9
+ // secrets rotate job the version check parses the stored JSON.
10
+ //
11
+ // Idempotent: a re-run skips rows already on the current version. Every
12
+ // write goes through the event-store executor (config values are
13
+ // entity-backed — raw UPDATEs would be wiped by a projection rebuild),
14
+ // so each migration appends a normal `.updated` event whose payload
15
+ // carries the NEW envelope: after a full run even a from-scratch rebuild
16
+ // no longer needs the legacy key for the final state.
17
+
18
+ import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
19
+ import {
20
+ createEventStoreExecutor,
21
+ createTenantDb,
22
+ type DbConnection,
23
+ type TenantDb,
24
+ } from "@cosmicdrift/kumiko-framework/db";
25
+ import type { JobHandlerFn, SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
26
+ import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
27
+ import { type EnvelopeCipher, isStoredEnvelope } from "@cosmicdrift/kumiko-framework/secrets";
28
+ import { type ChunkedMigrationStopReason, runChunkedMigration } from "../../shared";
29
+ import { configValueEntity, configValuesTable } from "../table";
30
+
31
+ const DEFAULT_MAX_FAILURES = 10;
32
+ const SCAN_SLICE_SIZE = 100;
33
+ const SYSTEM_ROLES = ["system"] as const;
34
+
35
+ const executor = createEventStoreExecutor(configValuesTable, configValueEntity, {
36
+ entityName: "config-value",
37
+ });
38
+
39
+ export type ReencryptJobPayload = {
40
+ readonly maxDurationMs?: number;
41
+ readonly maxFailures?: number;
42
+ };
43
+
44
+ export type ReencryptJobResult = {
45
+ readonly migrated: number;
46
+ readonly failed: number;
47
+ readonly alreadyCurrent: number;
48
+ readonly stoppedReason: ChunkedMigrationStopReason;
49
+ };
50
+
51
+ function needsReencrypt(value: string, targetVersion: number): boolean {
52
+ // legacy single-key format (base64 — can never start with "{")
53
+ if (!value.startsWith("{")) return true;
54
+ try {
55
+ const parsed: unknown = JSON.parse(value);
56
+ if (!isStoredEnvelope(parsed)) return true;
57
+ return parsed.kekVersion !== targetVersion;
58
+ } catch {
59
+ // malformed JSON — let the decrypt attempt surface the real error
60
+ return true;
61
+ }
62
+ }
63
+
64
+ export const reencryptJob: JobHandlerFn = async (rawPayload, ctx): Promise<void> => {
65
+ const payload = rawPayload as ReencryptJobPayload; // @cast-boundary engine-payload
66
+ const maybeCipher = ctx.configEncryption;
67
+ if (!maybeCipher) {
68
+ throw new InternalError({
69
+ message:
70
+ "[config:reencrypt] ctx.configEncryption missing — provide a master key " +
71
+ "(KUMIKO_SECRETS_MASTER_KEY_V<n>) so the boot wires the envelope cipher.",
72
+ });
73
+ }
74
+ // hoisted function declarations below capture these — pin the narrowed
75
+ // type explicitly so TS keeps it inside the closures
76
+ const cipher: EnvelopeCipher = maybeCipher;
77
+ const provider = ctx.masterKeyProvider;
78
+ if (!provider) {
79
+ throw new InternalError({
80
+ message:
81
+ "[config:reencrypt] ctx.masterKeyProvider missing — wire it via extraContext.masterKeyProvider at boot.",
82
+ });
83
+ }
84
+ if (!ctx.db) {
85
+ throw new InternalError({
86
+ message: "[config:reencrypt] ctx.db missing — job context requires a database connection.",
87
+ });
88
+ }
89
+ if (!ctx.registry) {
90
+ throw new InternalError({
91
+ message: "[config:reencrypt] ctx.registry missing — job context requires the registry.",
92
+ });
93
+ }
94
+ const db = ctx.db as DbConnection; // @cast-boundary db-operator
95
+
96
+ const encryptedKeys = [...ctx.registry.getAllConfigKeys()]
97
+ .filter(([, def]) => def.encrypted === true)
98
+ .map(([key]) => key);
99
+
100
+ const maxFailures = payload.maxFailures ?? DEFAULT_MAX_FAILURES;
101
+ const deadline = payload.maxDurationMs
102
+ ? Date.now() + payload.maxDurationMs
103
+ : Number.POSITIVE_INFINITY;
104
+
105
+ const tdbCache = new Map<TenantId, TenantDb>();
106
+ function tdbFor(tenantId: TenantId): TenantDb {
107
+ let existing = tdbCache.get(tenantId);
108
+ if (!existing) {
109
+ existing = createTenantDb(db, tenantId, "system");
110
+ tdbCache.set(tenantId, existing);
111
+ }
112
+ return existing;
113
+ }
114
+
115
+ type ConfigRow = {
116
+ id: string;
117
+ key: string;
118
+ value: string | null;
119
+ tenantId: string;
120
+ version: number;
121
+ };
122
+
123
+ let alreadyCurrent = 0;
124
+ const targetVersion = provider.currentVersion();
125
+
126
+ // ponytail: one full candidate scan — config rows are operator-scale
127
+ // (tenants × encrypted keys), cursor pagination when that ever changes.
128
+ // Served to the shared loop in slices so its deadline/signal/failure
129
+ // checks run between chunks, not only once.
130
+ let pending: ConfigRow[] | undefined;
131
+ async function nextBatch(): Promise<readonly ConfigRow[]> {
132
+ if (pending === undefined) {
133
+ pending =
134
+ encryptedKeys.length === 0
135
+ ? []
136
+ : [
137
+ ...(await selectMany<ConfigRow>(db, configValuesTable, {
138
+ key: { in: encryptedKeys },
139
+ })),
140
+ ];
141
+ }
142
+ const slice = pending;
143
+ return slice.splice(0, SCAN_SLICE_SIZE);
144
+ }
145
+
146
+ async function migrateRow(row: ConfigRow): Promise<"migrated" | "skipped" | "failed"> {
147
+ if (row.value === null || row.value === undefined) return "skipped";
148
+ if (!needsReencrypt(row.value, targetVersion)) {
149
+ alreadyCurrent++;
150
+ return "skipped";
151
+ }
152
+
153
+ // decrypt failure (missing legacy key, unknown kekVersion, tamper)
154
+ // throws → counted as failed via onRowError; the row stays untouched —
155
+ // never write anything we couldn't read.
156
+ const tenantId = row.tenantId as TenantId; // @cast-boundary db-row
157
+ const plaintext = await cipher.decrypt(row.value, { tenantId });
158
+ const reencrypted = await cipher.encrypt(plaintext, { tenantId });
159
+
160
+ const actor: SessionUser = { id: "system", tenantId, roles: SYSTEM_ROLES };
161
+ const result = await executor.update(
162
+ { id: row.id, version: row.version, changes: { value: reencrypted } },
163
+ actor,
164
+ tdbFor(tenantId),
165
+ );
166
+
167
+ // version_conflict == a concurrent config:set beat us; the row now
168
+ // holds a fresh envelope written by the set handler — already fine.
169
+ if (!result.isSuccess) {
170
+ if (result.error.code === "version_conflict") return "skipped";
171
+ ctx.log?.warn?.(`[config:reencrypt] executor rejected row ${row.id}`, {
172
+ code: result.error.code,
173
+ });
174
+ return "failed";
175
+ }
176
+ return "migrated";
177
+ }
178
+
179
+ const outcome = await runChunkedMigration<ConfigRow>({
180
+ nextBatch,
181
+ migrateRow,
182
+ maxFailures,
183
+ deadlineAt: deadline,
184
+ signal: ctx.signal,
185
+ onRowError: (row, err) => {
186
+ ctx.log?.warn?.(`[config:reencrypt] failed to re-encrypt row ${row.id}`, { err });
187
+ },
188
+ });
189
+
190
+ const result: ReencryptJobResult = {
191
+ migrated: outcome.migrated,
192
+ failed: outcome.failed,
193
+ alreadyCurrent,
194
+ stoppedReason: outcome.stoppedReason,
195
+ };
196
+ ctx.log?.info?.(`[config:reencrypt] complete: ${JSON.stringify(result)}`);
197
+ };
@@ -88,8 +88,8 @@ export const setWrite = defineWriteHandler({
88
88
 
89
89
  let serialized = JSON.stringify(event.payload.value);
90
90
  if (keyDef.encrypted) {
91
- const encryption = requireConfigEncryption(ctx, "config:write:set");
92
- serialized = encryption.encrypt(serialized);
91
+ const cipher = requireConfigEncryption(ctx, "config:write:set");
92
+ serialized = await cipher.encrypt(serialized, { tenantId });
93
93
  }
94
94
 
95
95
  const existing = await findConfigRow(db, event.payload.key, tenantId, userId);
@@ -1,6 +1,7 @@
1
- import type { DbConnection, EncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
1
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
2
  import { seedConfigValues } from "@cosmicdrift/kumiko-framework/db";
3
3
  import type { Registry } from "@cosmicdrift/kumiko-framework/engine";
4
+ import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
4
5
  import { configValueEntity, configValuesTable } from "./table";
5
6
 
6
7
  export {
@@ -30,8 +31,8 @@ export { configValuesTable } from "./table";
30
31
  export function seedAllConfigValues(
31
32
  registry: Registry,
32
33
  db: DbConnection,
33
- encryption?: EncryptionProvider,
34
+ cipher?: EnvelopeCipher,
34
35
  ): Promise<{ created: number; skipped: number }> {
35
36
  const seeds = registry.getAllConfigSeeds();
36
- return seedConfigValues(seeds, configValuesTable, configValueEntity, registry, db, encryption);
37
+ return seedConfigValues(seeds, configValuesTable, configValueEntity, registry, db, cipher);
37
38
  }
@@ -1,5 +1,5 @@
1
1
  import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
2
- import type { DbConnection, EncryptionProvider, TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
3
3
  import type {
4
4
  ConfigCascade,
5
5
  ConfigCascadeLevel,
@@ -12,6 +12,7 @@ import type {
12
12
  } from "@cosmicdrift/kumiko-framework/engine";
13
13
  import { SYSTEM_TENANT_ID } from "@cosmicdrift/kumiko-framework/engine";
14
14
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
15
+ import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
15
16
  import { assertUnreachable, parseJsonOrThrow } from "@cosmicdrift/kumiko-framework/utils";
16
17
  import { selectConfigRowsForKeys, selectConfigRowsForScope } from "./db/queries/resolver";
17
18
  import { configValuesTable } from "./table";
@@ -63,10 +64,33 @@ export function deserializeValue(
63
64
  export type AppConfigOverrides = ReadonlyMap<string, string | number | boolean>;
64
65
 
65
66
  export type ConfigResolverOptions = {
66
- encryption?: EncryptionProvider;
67
+ // Envelope cipher for `encrypted: true` keys. run{Prod,Dev}App wire it
68
+ // automatically from the secrets master key — apps only pass their own
69
+ // to override (tests, custom providers).
70
+ cipher?: EnvelopeCipher;
67
71
  appOverrides?: AppConfigOverrides;
68
72
  };
69
73
 
74
+ // Decrypt gate for encrypted keys: an encrypted row without a cipher MUST
75
+ // throw — passing the ciphertext through as the "value" (the pre-envelope
76
+ // behavior) silently feeds base64 garbage to whatever consumes the key
77
+ // (e.g. an SMTP password) and is a debugging nightmare.
78
+ async function decryptEncrypted(
79
+ cipher: EnvelopeCipher | undefined,
80
+ raw: string,
81
+ qualifiedKey: string,
82
+ ): Promise<string> {
83
+ if (!cipher) {
84
+ throw new InternalError({
85
+ message:
86
+ `[config] key "${qualifiedKey}" is encrypted but no cipher is wired — the boot must ` +
87
+ `provide a master key (KUMIKO_SECRETS_MASTER_KEY_V<n>) or pass ConfigResolverOptions.cipher.`,
88
+ i18nKey: "config.errors.cipher_missing",
89
+ });
90
+ }
91
+ return cipher.decrypt(raw);
92
+ }
93
+
70
94
  // backing="secrets" keys store their value in the secrets store (flat per
71
95
  // (tenant,key) at SYSTEM_TENANT_ID), not in config_values. Both read paths
72
96
  // (getWithSource + buildCascade) route the system rung here. Missing reader =
@@ -101,7 +125,7 @@ async function buildCascade(
101
125
  userId: string | null,
102
126
  ) => Promise<ConfigRow | null> | ConfigRow | null,
103
127
  appOverrides: AppConfigOverrides | undefined,
104
- encryption: EncryptionProvider | undefined,
128
+ cipher: EnvelopeCipher | undefined,
105
129
  secretsReader: ConfigSecretsReader | undefined,
106
130
  ): Promise<ConfigCascade> {
107
131
  type Lookup = {
@@ -179,8 +203,8 @@ async function buildCascade(
179
203
  const row = await fetchRow(lookup.tenantId, lookup.userId);
180
204
  if (row?.value !== null && row?.value !== undefined) {
181
205
  let raw = row.value;
182
- if (keyDef.encrypted && encryption) {
183
- raw = encryption.decrypt(raw);
206
+ if (keyDef.encrypted) {
207
+ raw = await decryptEncrypted(cipher, raw, qualifiedKey);
184
208
  }
185
209
  if (activeIndex === -1) activeIndex = levels.length;
186
210
  levels.push({
@@ -265,7 +289,7 @@ async function buildCascade(
265
289
  }
266
290
 
267
291
  export function createConfigResolver(options: ConfigResolverOptions = {}): ConfigResolver {
268
- const { encryption, appOverrides } = options;
292
+ const { cipher, appOverrides } = options;
269
293
  async function findRow(
270
294
  key: string,
271
295
  tenantId: string,
@@ -356,8 +380,8 @@ export function createConfigResolver(options: ConfigResolverOptions = {}): Confi
356
380
  const row = await findRow(qualifiedKey, lookup.tenantId, lookup.userId, db);
357
381
  if (row?.value !== null && row?.value !== undefined) {
358
382
  let raw = row.value;
359
- if (keyDef.encrypted && encryption) {
360
- raw = encryption.decrypt(raw);
383
+ if (keyDef.encrypted) {
384
+ raw = await decryptEncrypted(cipher, raw, qualifiedKey);
361
385
  }
362
386
  return { value: deserializeValue(raw, keyDef.type), source: lookup.source };
363
387
  }
@@ -470,7 +494,7 @@ export function createConfigResolver(options: ConfigResolverOptions = {}): Confi
470
494
  db,
471
495
  (tid, uid) => findRow(qualifiedKey, tid, uid, db),
472
496
  appOverrides,
473
- encryption,
497
+ cipher,
474
498
  secretsReader,
475
499
  );
476
500
  },
@@ -512,7 +536,7 @@ export function createConfigResolver(options: ConfigResolverOptions = {}): Confi
512
536
  (tid, uid) =>
513
537
  keyRows.find((r) => r.tenantId === tid && (r.userId ?? null) === uid) ?? null,
514
538
  appOverrides,
515
- encryption,
539
+ cipher,
516
540
  secretsReader,
517
541
  );
518
542
  result.set(key, cascade);
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
6
6
  import { randomBytes } from "node:crypto";
7
- import { createEncryptionProvider, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
7
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
8
8
  import { defineFeature, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
9
9
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
10
10
  import { createEnvMasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
@@ -18,6 +18,7 @@ import {
18
18
  } from "@cosmicdrift/kumiko-framework/stack";
19
19
  import {
20
20
  createMutableMasterKeyProvider,
21
+ createTestEnvelopeCipher,
21
22
  type MutableMasterKeyProvider,
22
23
  } from "@cosmicdrift/kumiko-framework/testing";
23
24
  import { z } from "zod";
@@ -73,8 +74,8 @@ let providerRef: MutableMasterKeyProvider;
73
74
  const testEncryptionKey = randomBytes(32).toString("base64");
74
75
 
75
76
  beforeAll(async () => {
76
- const encryption = createEncryptionProvider(testEncryptionKey);
77
- resolver = createConfigResolver({ encryption });
77
+ const encryption = createTestEnvelopeCipher(testEncryptionKey);
78
+ resolver = createConfigResolver({ cipher: encryption });
78
79
 
79
80
  const initialKp = createEnvMasterKeyProvider({
80
81
  env: {
@@ -331,6 +331,7 @@ describe("folders-user-data — tenantScopedDelete hooks", () => {
331
331
  await seedOneFolderWithAssignment();
332
332
  const ctx = {
333
333
  db: stack.db,
334
+ registry: stack.registry,
334
335
  tenantId: admin.tenantId,
335
336
  userId: admin.id,
336
337
  tenantModel: "multi-user" as const,
@@ -345,6 +346,7 @@ describe("folders-user-data — tenantScopedDelete hooks", () => {
345
346
  await seedOneFolderWithAssignment();
346
347
  const ctx = {
347
348
  db: stack.db,
349
+ registry: stack.registry,
348
350
  tenantId: admin.tenantId,
349
351
  userId: admin.id,
350
352
  tenantModel: "single-user" as const,
@@ -359,6 +361,7 @@ describe("folders-user-data — tenantScopedDelete hooks", () => {
359
361
  await seedOneFolderWithAssignment();
360
362
  const ctx = {
361
363
  db: stack.db,
364
+ registry: stack.registry,
362
365
  tenantId: admin.tenantId,
363
366
  userId: admin.id,
364
367
  tenantModel: "single-user" as const,
@@ -57,6 +57,7 @@ describe("folderAssignmentExportHook", () => {
57
57
 
58
58
  const snippet = await folderAssignmentExportHook({
59
59
  db: stack.db,
60
+ registry: stack.registry,
60
61
  tenantId: admin.tenantId,
61
62
  userId: admin.id,
62
63
  });
@@ -1,10 +1,6 @@
1
1
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { randomBytes } from "node:crypto";
3
- import {
4
- createEncryptionProvider,
5
- createTenantDb,
6
- type DbConnection,
7
- } from "@cosmicdrift/kumiko-framework/db";
3
+ import { createTenantDb, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
8
4
  import {
9
5
  access,
10
6
  createRegistry,
@@ -26,7 +22,7 @@ import {
26
22
  TestUsers,
27
23
  unsafePushTables,
28
24
  } from "@cosmicdrift/kumiko-framework/stack";
29
- import { bridgeStub, sleep } from "@cosmicdrift/kumiko-framework/testing";
25
+ import { bridgeStub, createTestEnvelopeCipher, sleep } from "@cosmicdrift/kumiko-framework/testing";
30
26
  import { ConfigHandlers } from "../../config/constants";
31
27
  import { createConfigAccessor, createConfigFeature } from "../../config/feature";
32
28
  import { type ConfigResolver, createConfigResolver } from "../../config/resolver";
@@ -107,8 +103,8 @@ beforeAll(async () => {
107
103
  await createEventsTable(db);
108
104
  await createArchivedStreamsTable(db);
109
105
 
110
- const encryption = createEncryptionProvider(testEncryptionKey);
111
- resolver = createConfigResolver({ encryption });
106
+ const encryption = createTestEnvelopeCipher(testEncryptionKey);
107
+ resolver = createConfigResolver({ cipher: encryption });
112
108
 
113
109
  registry = createRegistry([configFeature, billingFeature]);
114
110
 
@@ -10,7 +10,7 @@
10
10
 
11
11
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
12
12
  import { randomBytes } from "node:crypto";
13
- import { createEncryptionProvider, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
13
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
14
14
  import { defineFeature, defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
15
15
  import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
16
16
  import { createEnvMasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
@@ -24,6 +24,7 @@ import {
24
24
  } from "@cosmicdrift/kumiko-framework/stack";
25
25
  import {
26
26
  createMutableMasterKeyProvider,
27
+ createTestEnvelopeCipher,
27
28
  type MutableMasterKeyProvider,
28
29
  } from "@cosmicdrift/kumiko-framework/testing";
29
30
  import { z } from "zod";
@@ -87,8 +88,8 @@ let providerRef: MutableMasterKeyProvider;
87
88
  const testEncryptionKey = randomBytes(32).toString("base64");
88
89
 
89
90
  beforeAll(async () => {
90
- const encryption = createEncryptionProvider(testEncryptionKey);
91
- resolver = createConfigResolver({ encryption });
91
+ const encryption = createTestEnvelopeCipher(testEncryptionKey);
92
+ resolver = createConfigResolver({ cipher: encryption });
92
93
 
93
94
  // Master-key for the secrets-feature. Production env shape:
94
95
  // KUMIKO_SECRETS_MASTER_KEY_V1=<base64 32 bytes>
@@ -5,7 +5,6 @@ import {
5
5
  type PatResolver,
6
6
  } from "@cosmicdrift/kumiko-framework/api";
7
7
  import { updateMany } from "@cosmicdrift/kumiko-framework/bun-db";
8
- import { createEncryptionProvider } from "@cosmicdrift/kumiko-framework/db";
9
8
  import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
10
9
  import {
11
10
  setupTestStack,
@@ -14,7 +13,7 @@ import {
14
13
  unsafeCreateEntityTable,
15
14
  unsafePushTables,
16
15
  } from "@cosmicdrift/kumiko-framework/stack";
17
- import { deleteRows } from "@cosmicdrift/kumiko-framework/testing";
16
+ import { createTestEnvelopeCipher, deleteRows } from "@cosmicdrift/kumiko-framework/testing";
18
17
  import { Temporal } from "temporal-polyfill";
19
18
  import { AuthHandlers } from "../../auth-email-password/constants";
20
19
  import { createAuthEmailPasswordFeature } from "../../auth-email-password/feature";
@@ -73,8 +72,8 @@ async function mintToken(
73
72
  }
74
73
 
75
74
  beforeAll(async () => {
76
- const encryption = createEncryptionProvider(encryptionKey);
77
- const resolver = createConfigResolver({ encryption });
75
+ const encryption = createTestEnvelopeCipher(encryptionKey);
76
+ const resolver = createConfigResolver({ cipher: encryption });
78
77
 
79
78
  stack = await setupTestStack({
80
79
  features: [
@@ -6,7 +6,7 @@
6
6
  import { afterAll, beforeAll, describe, expect, test } from "bun:test";
7
7
  import { randomBytes } from "node:crypto";
8
8
  import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
9
- import { createEncryptionProvider, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
9
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
10
10
  import { access, createTenantConfig, defineFeature } from "@cosmicdrift/kumiko-framework/engine";
11
11
  import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
12
12
  import { createEnvMasterKeyProvider } from "@cosmicdrift/kumiko-framework/secrets";
@@ -18,6 +18,7 @@ import {
18
18
  unsafeCreateEntityTable,
19
19
  unsafePushTables,
20
20
  } from "@cosmicdrift/kumiko-framework/stack";
21
+ import { createTestEnvelopeCipher } from "@cosmicdrift/kumiko-framework/testing";
21
22
  import { createConfigFeature } from "../../config";
22
23
  import { createConfigAccessorFactory } from "../../config/feature";
23
24
  import { createConfigResolver } from "../../config/resolver";
@@ -133,8 +134,8 @@ let stack: TestStack;
133
134
  let db: DbConnection;
134
135
 
135
136
  beforeAll(async () => {
136
- const encryption = createEncryptionProvider(randomBytes(32).toString("base64"));
137
- const resolver = createConfigResolver({ encryption });
137
+ const encryption = createTestEnvelopeCipher(randomBytes(32).toString("base64"));
138
+ const resolver = createConfigResolver({ cipher: encryption });
138
139
  const masterKeyProvider = createEnvMasterKeyProvider({
139
140
  env: {
140
141
  KUMIKO_SECRETS_MASTER_KEY_V1: randomBytes(32).toString("base64"),