@cosmicdrift/kumiko-bundled-features 0.109.0 → 0.111.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 (46) 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__/feature-options.test.ts +33 -0
  8. package/src/auth-email-password/__tests__/identity-v3-login.integration.test.ts +3 -3
  9. package/src/auth-email-password/__tests__/password-reset.integration.test.ts +3 -4
  10. package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.test.ts +3 -3
  11. package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +3 -3
  12. package/src/auth-email-password/constants.ts +4 -0
  13. package/src/auth-email-password/feature.ts +8 -4
  14. package/src/config/__tests__/config.integration.test.ts +7 -8
  15. package/src/config/__tests__/encrypted-legacy-migration.integration.test.ts +179 -0
  16. package/src/config/__tests__/inherited-redaction.integration.test.ts +4 -3
  17. package/src/config/feature.ts +14 -8
  18. package/src/config/handlers/reencrypt.job.ts +197 -0
  19. package/src/config/handlers/set.write.ts +2 -2
  20. package/src/config/index.ts +4 -3
  21. package/src/config/resolver.ts +34 -10
  22. package/src/file-foundation/__tests__/file-foundation.integration.test.ts +4 -3
  23. package/src/jobs/__tests__/job-system-user.integration.test.ts +4 -8
  24. package/src/mail-foundation/__tests__/mail-foundation.integration.test.ts +4 -3
  25. package/src/personal-access-tokens/__tests__/pat.integration.test.ts +3 -4
  26. package/src/readiness/__tests__/readiness.integration.test.ts +4 -3
  27. package/src/secrets/handlers/rotate.job.ts +77 -98
  28. package/src/secrets/secrets-context.ts +7 -38
  29. package/src/secrets/table.ts +6 -7
  30. package/src/sessions/__tests__/auto-revoke-binding.test.ts +71 -0
  31. package/src/sessions/__tests__/password-auto-revoke.integration.test.ts +6 -4
  32. package/src/sessions/__tests__/sessions.integration.test.ts +3 -3
  33. package/src/sessions/feature.ts +51 -18
  34. package/src/sessions/index.ts +2 -2
  35. package/src/shared/chunked-entity-migration.ts +89 -0
  36. package/src/shared/index.ts +7 -0
  37. package/src/subscription-stripe/__tests__/stripe-foundation.integration.test.ts +6 -5
  38. package/src/tenant/__tests__/tenant.integration.test.ts +8 -4
  39. package/src/tier-engine/__tests__/tier-engine.integration.test.ts +7 -4
  40. package/src/user/__tests__/user.integration.test.ts +26 -0
  41. package/src/user/handlers/find-for-auth.query.ts +7 -5
  42. package/src/user-data-rights/__tests__/download.integration.test.ts +7 -4
  43. package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +152 -0
  44. package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +3 -3
  45. package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +3 -3
  46. package/src/user-data-rights/run-user-export.ts +42 -4
@@ -1,4 +1,4 @@
1
- import type { DbConnection, EncryptionProvider, TenantDb } from "@cosmicdrift/kumiko-framework/db";
1
+ import type { DbConnection, TenantDb } from "@cosmicdrift/kumiko-framework/db";
2
2
  import {
3
3
  type ConfigAccessor,
4
4
  type ConfigAccessorFactory,
@@ -13,8 +13,10 @@ import {
13
13
  type TenantId,
14
14
  } from "@cosmicdrift/kumiko-framework/engine";
15
15
  import { InternalError } from "@cosmicdrift/kumiko-framework/errors";
16
+ import type { EnvelopeCipher } from "@cosmicdrift/kumiko-framework/secrets";
16
17
  import { cascadeQuery } from "./handlers/cascade.query";
17
18
  import { readinessQuery } from "./handlers/readiness.query";
19
+ import { reencryptJob } from "./handlers/reencrypt.job";
18
20
  import { resetWrite } from "./handlers/reset.write";
19
21
  import { schemaQuery } from "./handlers/schema.query";
20
22
  import { setWrite } from "./handlers/set.write";
@@ -27,7 +29,7 @@ export type ConfigContext = { readonly config: ConfigAccessor };
27
29
  export function createConfigFeature(): FeatureDefinition {
28
30
  return defineFeature("config", (r) => {
29
31
  r.describe(
30
- "Stores per-tenant (and optionally per-user) configuration values with a multi-layer cascade: user-row \u2192 tenant-row \u2192 system-row \u2192 app-override (deploy-time `AppConfigOverrides`) \u2192 computed \u2192 feature default. Access a value in handlers via `ctx.config(handle)`, declare keys with `r.config({ keys: { ... } })` inside a feature's registry callback, and optionally mark them `encrypted: true` to route storage through an `EncryptionProvider`. Use this feature whenever a tenant admin needs to customise behaviour at runtime without a code deploy.",
32
+ "Stores per-tenant (and optionally per-user) configuration values with a multi-layer cascade: user-row \u2192 tenant-row \u2192 system-row \u2192 app-override (deploy-time `AppConfigOverrides`) \u2192 computed \u2192 feature default. Access a value in handlers via `ctx.config(handle)`, declare keys with `r.config({ keys: { ... } })` inside a feature's registry callback, and optionally mark them `encrypted: true` to route storage through the envelope cipher (versioned master key). Use this feature whenever a tenant admin needs to customise behaviour at runtime without a code deploy.",
31
33
  );
32
34
  r.uiHints({
33
35
  displayLabel: "Tenant Config Store",
@@ -55,6 +57,12 @@ export function createConfigFeature(): FeatureDefinition {
55
57
  readiness: r.queryHandler(readinessQuery),
56
58
  };
57
59
 
60
+ // Migration + KEK-rotation for encrypted values (legacy-format →
61
+ // envelope, old kekVersion → current). Manual trigger — ops runs it
62
+ // once after adding a new master key version or before retiring the
63
+ // legacy CONFIG_ENCRYPTION_KEY.
64
+ r.job("reencrypt", { trigger: { manual: true } }, reencryptJob);
65
+
58
66
  return { handlers, queries };
59
67
  });
60
68
  }
@@ -116,16 +124,14 @@ export function requireConfigResolver(ctx: HandlerContext, handlerName: string):
116
124
  // lazily so apps that never wire encryption still boot (and only crash
117
125
  // if a handler tries to write an encrypted key without the provider in
118
126
  // place, pointing at the exact wiring gap).
119
- export function requireConfigEncryption(
120
- ctx: HandlerContext,
121
- handlerName: string,
122
- ): EncryptionProvider {
127
+ export function requireConfigEncryption(ctx: HandlerContext, handlerName: string): EnvelopeCipher {
123
128
  if (!ctx.configEncryption) {
124
129
  throw new InternalError({
125
130
  message:
126
131
  `[${handlerName}] ctx.configEncryption missing — at least one config key declares ` +
127
- `encrypted: true, so the boot wiring must pass an EncryptionProvider via ` +
128
- `extraContext.configEncryption (same instance the resolver was built with).`,
132
+ `encrypted: true, so the boot wiring must pass an EnvelopeCipher via ` +
133
+ `extraContext.configEncryption (run{Prod,Dev}App wire it automatically from ` +
134
+ `KUMIKO_SECRETS_MASTER_KEY_V<n>; same instance the resolver was built with).`,
129
135
  });
130
136
  }
131
137
  return ctx.configEncryption;
@@ -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: {
@@ -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"),