@cosmicdrift/kumiko-bundled-features 0.110.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.
- package/package.json +6 -6
- package/src/auth-email-password/__tests__/account-lockout-no-redis.integration.test.ts +3 -3
- package/src/auth-email-password/__tests__/account-lockout.integration.test.ts +3 -3
- package/src/auth-email-password/__tests__/auth-claims.integration.test.ts +9 -10
- package/src/auth-email-password/__tests__/auth.integration.test.ts +5 -5
- package/src/auth-email-password/__tests__/email-verification.integration.test.ts +7 -4
- package/src/auth-email-password/__tests__/identity-v3-login.integration.test.ts +3 -3
- package/src/auth-email-password/__tests__/password-reset.integration.test.ts +3 -4
- package/src/auth-email-password/__tests__/public-routes-rate-limit.integration.test.ts +3 -3
- package/src/auth-email-password/__tests__/session-callbacks.integration.test.ts +3 -3
- package/src/config/__tests__/config.integration.test.ts +7 -8
- package/src/config/__tests__/encrypted-legacy-migration.integration.test.ts +179 -0
- package/src/config/__tests__/inherited-redaction.integration.test.ts +4 -3
- package/src/config/feature.ts +14 -8
- package/src/config/handlers/reencrypt.job.ts +197 -0
- package/src/config/handlers/set.write.ts +2 -2
- package/src/config/index.ts +4 -3
- package/src/config/resolver.ts +34 -10
- package/src/file-foundation/__tests__/file-foundation.integration.test.ts +4 -3
- package/src/jobs/__tests__/job-system-user.integration.test.ts +4 -8
- package/src/mail-foundation/__tests__/mail-foundation.integration.test.ts +4 -3
- package/src/personal-access-tokens/__tests__/pat.integration.test.ts +3 -4
- package/src/readiness/__tests__/readiness.integration.test.ts +4 -3
- package/src/secrets/handlers/rotate.job.ts +77 -98
- package/src/secrets/secrets-context.ts +7 -38
- package/src/secrets/table.ts +6 -7
- package/src/sessions/__tests__/password-auto-revoke.integration.test.ts +6 -4
- package/src/sessions/__tests__/sessions.integration.test.ts +3 -3
- package/src/shared/chunked-entity-migration.ts +89 -0
- package/src/shared/index.ts +7 -0
- package/src/subscription-stripe/__tests__/stripe-foundation.integration.test.ts +6 -5
- package/src/tenant/__tests__/tenant.integration.test.ts +8 -4
- package/src/tier-engine/__tests__/tier-engine.integration.test.ts +7 -4
- package/src/user-data-rights/__tests__/download.integration.test.ts +7 -4
- package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +152 -0
- package/src/user-data-rights/__tests__/read-users-rebuild-survives-lifecycle.integration.test.ts +3 -3
- package/src/user-data-rights/__tests__/restriction-flow.integration.test.ts +3 -3
- package/src/user-data-rights/run-user-export.ts +42 -4
|
@@ -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
|
|
92
|
-
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);
|
package/src/config/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { DbConnection
|
|
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
|
-
|
|
34
|
+
cipher?: EnvelopeCipher,
|
|
34
35
|
): Promise<{ created: number; skipped: number }> {
|
|
35
36
|
const seeds = registry.getAllConfigSeeds();
|
|
36
|
-
return seedConfigValues(seeds, configValuesTable, configValueEntity, registry, db,
|
|
37
|
+
return seedConfigValues(seeds, configValuesTable, configValueEntity, registry, db, cipher);
|
|
37
38
|
}
|
package/src/config/resolver.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fetchOne } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
2
|
-
import type { DbConnection,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
183
|
-
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 {
|
|
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
|
|
360
|
-
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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 =
|
|
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"),
|