@cosmicdrift/kumiko-bundled-features 0.165.4 → 0.167.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 +7 -9
- package/src/auth-email-password/__tests__/invite-flow-kms.integration.test.ts +4 -2
- package/src/auth-mfa/__tests__/pii-subject-encryption.integration.test.ts +5 -6
- package/src/auth-mfa/__tests__/token-secrets.test.ts +40 -0
- package/src/auth-mfa/index.ts +5 -0
- package/src/auth-mfa/token-secrets.ts +39 -0
- package/src/billing-foundation/__tests__/billing-foundation.integration.test.ts +1 -1
- package/src/config/__tests__/pii-encrypted.integration.test.ts +1 -1
- package/src/crypto-shredding/__tests__/forget-subject.integration.test.ts +2 -6
- package/src/delivery/__tests__/delivery-pii-kms.integration.test.ts +1 -1
- package/src/document-ingest-foundation/__tests__/feature.integration.test.ts +10 -1
- package/src/document-ingest-foundation/__tests__/feature.test.ts +16 -2
- package/src/document-ingest-foundation/__tests__/pages-crypto-shredding.integration.test.ts +138 -0
- package/src/document-ingest-foundation/changes.json +9 -1
- package/src/document-ingest-foundation/entity.ts +13 -4
- package/src/document-ingest-foundation/feature.ts +13 -2
- package/src/document-ingest-foundation/index.ts +6 -1
- package/src/document-ingest-foundation/tenant-destroy-hook.ts +49 -0
- package/src/inbound-mail-foundation/__tests__/connect-routes.integration.test.ts +2 -5
- package/src/inbound-mail-foundation/__tests__/inbound-mail-foundation.integration.test.ts +1 -2
- package/src/inbound-mail-foundation/__tests__/retention.integration.test.ts +2 -5
- package/src/inbound-mail-foundation/__tests__/watch-supervisor.integration.test.ts +2 -6
- package/src/inbound-provider-imap/__tests__/imap-foundation.integration.test.ts +2 -5
- package/src/jobs/__tests__/jobs-pii-kms.integration.test.ts +1 -2
- package/src/personal-access-tokens/__tests__/pat.integration.test.ts +6 -3
- package/src/sessions/__tests__/sessions.integration.test.ts +2 -2
- package/src/tenant/__tests__/members-query-kms.integration.test.ts +1 -2
- package/src/tier-engine/compose-app.ts +14 -0
- package/src/user-data-rights/__tests__/anonymous-deletion-kms.integration.test.ts +5 -3
- package/src/user-data-rights/__tests__/export-encrypted-fields.integration.test.ts +5 -6
- package/src/user-data-rights/__tests__/run-user-export.integration.test.ts +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.167.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -119,11 +119,12 @@
|
|
|
119
119
|
"./step-dispatcher": "./src/step-dispatcher/index.ts"
|
|
120
120
|
},
|
|
121
121
|
"dependencies": {
|
|
122
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
123
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
124
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
125
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
126
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
122
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.167.0",
|
|
123
|
+
"@cosmicdrift/kumiko-framework": "0.167.0",
|
|
124
|
+
"@cosmicdrift/kumiko-headless": "0.167.0",
|
|
125
|
+
"@cosmicdrift/kumiko-renderer": "0.167.0",
|
|
126
|
+
"@cosmicdrift/kumiko-renderer-web": "0.167.0",
|
|
127
|
+
"@cosmicdrift/kumiko-types": "0.167.0",
|
|
127
128
|
"@mollie/api-client": "^4.5.0",
|
|
128
129
|
"imapflow": "^1.3.3",
|
|
129
130
|
"mailparser": "^3.9.8",
|
|
@@ -147,9 +148,6 @@
|
|
|
147
148
|
"README.md",
|
|
148
149
|
"LICENSE"
|
|
149
150
|
],
|
|
150
|
-
"peerDependencies": {
|
|
151
|
-
"@cosmicdrift/kumiko-types": "^0.165.4"
|
|
152
|
-
},
|
|
153
151
|
"devDependencies": {
|
|
154
152
|
"@testing-library/user-event": "^14.6.1",
|
|
155
153
|
"@types/mailparser": "^3.4.6",
|
|
@@ -13,8 +13,6 @@ import {
|
|
|
13
13
|
decryptPiiFieldValues,
|
|
14
14
|
InMemoryKmsAdapter,
|
|
15
15
|
isPiiCiphertext,
|
|
16
|
-
resetBlindIndexKeyForTests,
|
|
17
|
-
resetPiiSubjectKmsForTests,
|
|
18
16
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
19
17
|
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
20
18
|
import {
|
|
@@ -23,6 +21,10 @@ import {
|
|
|
23
21
|
unsafeCreateEntityTable,
|
|
24
22
|
unsafePushTables,
|
|
25
23
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
24
|
+
import {
|
|
25
|
+
resetBlindIndexKeyForTests,
|
|
26
|
+
resetPiiSubjectKmsForTests,
|
|
27
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
26
28
|
import { createChannelEmailFeature, createInMemoryTransport } from "../../channel-email";
|
|
27
29
|
import { createConfigFeature } from "../../config";
|
|
28
30
|
import { createConfigResolver } from "../../config/resolver";
|
|
@@ -10,11 +10,7 @@
|
|
|
10
10
|
// non-string userOwned field. enable-confirm failed for EVERY real deployment
|
|
11
11
|
// with crypto-shredding configured, while every test here stayed green.
|
|
12
12
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
13
|
-
import {
|
|
14
|
-
configurePiiSubjectKms,
|
|
15
|
-
InMemoryKmsAdapter,
|
|
16
|
-
resetPiiSubjectKmsForTests,
|
|
17
|
-
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
13
|
+
import { configurePiiSubjectKms, InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
18
14
|
import { configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
|
|
19
15
|
import {
|
|
20
16
|
createTestUser,
|
|
@@ -23,7 +19,10 @@ import {
|
|
|
23
19
|
unsafeCreateEntityTable,
|
|
24
20
|
unsafePushTables,
|
|
25
21
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
26
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
createTestEnvelopeCipher,
|
|
24
|
+
resetPiiSubjectKmsForTests,
|
|
25
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
27
26
|
import { createConfigFeature } from "../../config";
|
|
28
27
|
import { createConfigResolver } from "../../config/resolver";
|
|
29
28
|
import { configValuesTable } from "../../config/table";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { resolveMfaTokenSecrets } from "../token-secrets";
|
|
3
|
+
|
|
4
|
+
const JWT_SECRET = "app-jwt-secret-for-tests";
|
|
5
|
+
|
|
6
|
+
describe("resolveMfaTokenSecrets", () => {
|
|
7
|
+
// A setup token proves "enrolling a factor", a challenge token proves
|
|
8
|
+
// "passed step one of login". One shared key would let the first be
|
|
9
|
+
// replayed as the second.
|
|
10
|
+
test("derives two different secrets", () => {
|
|
11
|
+
const { setupTokenSecret, challengeTokenSecret } = resolveMfaTokenSecrets(JWT_SECRET);
|
|
12
|
+
|
|
13
|
+
expect(setupTokenSecret).not.toBe(challengeTokenSecret);
|
|
14
|
+
expect(setupTokenSecret).not.toBe(JWT_SECRET);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// The reason this helper exists: prod entrypoint and dev server each resolve
|
|
18
|
+
// the secrets, and a purpose string drifting between them would invalidate
|
|
19
|
+
// every token minted by the other.
|
|
20
|
+
test("is stable across calls, so two boot files agree", () => {
|
|
21
|
+
expect(resolveMfaTokenSecrets(JWT_SECRET)).toEqual(resolveMfaTokenSecrets(JWT_SECRET));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("an explicit override wins over derivation", () => {
|
|
25
|
+
const resolved = resolveMfaTokenSecrets(JWT_SECRET, { setupTokenSecret: "explicit-setup" });
|
|
26
|
+
|
|
27
|
+
expect(resolved.setupTokenSecret).toBe("explicit-setup");
|
|
28
|
+
expect(resolved.challengeTokenSecret).toBe(
|
|
29
|
+
resolveMfaTokenSecrets(JWT_SECRET).challengeTokenSecret,
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("rotating the master rotates both", () => {
|
|
34
|
+
const before = resolveMfaTokenSecrets(JWT_SECRET);
|
|
35
|
+
const after = resolveMfaTokenSecrets(`${JWT_SECRET}-rotated`);
|
|
36
|
+
|
|
37
|
+
expect(after.setupTokenSecret).not.toBe(before.setupTokenSecret);
|
|
38
|
+
expect(after.challengeTokenSecret).not.toBe(before.challengeTokenSecret);
|
|
39
|
+
});
|
|
40
|
+
});
|
package/src/auth-mfa/index.ts
CHANGED
|
@@ -17,3 +17,8 @@ export {
|
|
|
17
17
|
} from "./feature";
|
|
18
18
|
export type { MfaStatusChecker, MfaStatusCheckResult } from "./mfa-status-checker";
|
|
19
19
|
export { userMfaEntity, userMfaTable } from "./schema/user-mfa";
|
|
20
|
+
export {
|
|
21
|
+
type MfaTokenSecretOverrides,
|
|
22
|
+
type ResolvedMfaTokenSecrets,
|
|
23
|
+
resolveMfaTokenSecrets,
|
|
24
|
+
} from "./token-secrets";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { derivePurposeSecret } from "@cosmicdrift/kumiko-framework/secrets";
|
|
2
|
+
|
|
3
|
+
export type MfaTokenSecretOverrides = {
|
|
4
|
+
readonly setupTokenSecret?: string;
|
|
5
|
+
readonly challengeTokenSecret?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type ResolvedMfaTokenSecrets = {
|
|
9
|
+
readonly setupTokenSecret: string;
|
|
10
|
+
readonly challengeTokenSecret: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// The two HKDF purposes auth-mfa needs. They live here rather than at each
|
|
14
|
+
// call site because an app typically resolves them twice — once in the prod
|
|
15
|
+
// entrypoint against a validated JWT_SECRET, once in the dev server against
|
|
16
|
+
// its fallback — and a purpose string that drifts between those two files
|
|
17
|
+
// invalidates every token issued by the other.
|
|
18
|
+
//
|
|
19
|
+
// Setup and challenge stay separate on purpose: a setup token proves "this
|
|
20
|
+
// user is enrolling a factor", a challenge token proves "this user passed
|
|
21
|
+
// step one of login". Sharing one key would let the first be replayed as the
|
|
22
|
+
// second.
|
|
23
|
+
const SETUP_TOKEN_PURPOSE = "mfa-setup-token-v1";
|
|
24
|
+
const CHALLENGE_TOKEN_PURPOSE = "mfa-challenge-token-v1";
|
|
25
|
+
|
|
26
|
+
/** Derives both MFA token secrets from the app's master secret. Pass explicit
|
|
27
|
+
* overrides only when a deployment needs its own key for one of them —
|
|
28
|
+
* otherwise deriving keeps a single env var authoritative. */
|
|
29
|
+
export function resolveMfaTokenSecrets(
|
|
30
|
+
masterSecret: string,
|
|
31
|
+
overrides: MfaTokenSecretOverrides = {},
|
|
32
|
+
): ResolvedMfaTokenSecrets {
|
|
33
|
+
return {
|
|
34
|
+
setupTokenSecret:
|
|
35
|
+
overrides.setupTokenSecret ?? derivePurposeSecret(masterSecret, SETUP_TOKEN_PURPOSE),
|
|
36
|
+
challengeTokenSecret:
|
|
37
|
+
overrides.challengeTokenSecret ?? derivePurposeSecret(masterSecret, CHALLENGE_TOKEN_PURPOSE),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
InMemoryKmsAdapter,
|
|
21
21
|
isPiiCiphertext,
|
|
22
22
|
PII_ERASED_SENTINEL,
|
|
23
|
-
resetPiiSubjectKmsForTests,
|
|
24
23
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
25
24
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
26
25
|
import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -36,6 +35,7 @@ import {
|
|
|
36
35
|
testTenantId,
|
|
37
36
|
unsafeCreateEntityTable,
|
|
38
37
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
38
|
+
import { resetPiiSubjectKmsForTests } from "@cosmicdrift/kumiko-framework/testing";
|
|
39
39
|
import {
|
|
40
40
|
ComplianceProfileHandlers,
|
|
41
41
|
createComplianceProfilesFeature,
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
InMemoryKmsAdapter,
|
|
13
13
|
isPiiCiphertext,
|
|
14
14
|
PII_CIPHERTEXT_PREFIX,
|
|
15
|
-
resetPiiSubjectKmsForTests,
|
|
16
15
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
17
16
|
import {
|
|
18
17
|
access,
|
|
@@ -27,6 +26,7 @@ import {
|
|
|
27
26
|
TestUsers,
|
|
28
27
|
unsafePushTables,
|
|
29
28
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
29
|
+
import { resetPiiSubjectKmsForTests } from "@cosmicdrift/kumiko-framework/testing";
|
|
30
30
|
import { ConfigHandlers, ConfigQueries } from "../constants";
|
|
31
31
|
import { createConfigAccessorFactory, createConfigFeature } from "../feature";
|
|
32
32
|
import { createConfigResolver } from "../resolver";
|
|
@@ -9,15 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
11
11
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
12
|
-
import {
|
|
13
|
-
configurePiiSubjectKms,
|
|
14
|
-
InMemoryKmsAdapter,
|
|
15
|
-
resetPiiSubjectKmsForTests,
|
|
16
|
-
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
12
|
+
import { configurePiiSubjectKms, InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
17
13
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
18
14
|
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
19
15
|
import { setupTestStack, type TestStack, testTenantId } from "@cosmicdrift/kumiko-framework/stack";
|
|
20
|
-
import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
16
|
+
import { resetPiiSubjectKmsForTests, resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
21
17
|
import { SUBJECT_FORGOTTEN_EVENT_NAME } from "../constants";
|
|
22
18
|
import { createCryptoShreddingFeature } from "../feature";
|
|
23
19
|
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
InMemoryKmsAdapter,
|
|
13
13
|
isPiiCiphertext,
|
|
14
14
|
PII_ERASED_SENTINEL,
|
|
15
|
-
resetPiiSubjectKmsForTests,
|
|
16
15
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
17
16
|
import {
|
|
18
17
|
defineFeature,
|
|
@@ -28,6 +27,7 @@ import {
|
|
|
28
27
|
TestUsers,
|
|
29
28
|
unsafePushTables,
|
|
30
29
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
30
|
+
import { resetPiiSubjectKmsForTests } from "@cosmicdrift/kumiko-framework/testing";
|
|
31
31
|
import { z } from "zod";
|
|
32
32
|
import { createChannelEmailFeature } from "../../channel-email/feature";
|
|
33
33
|
import { createInMemoryTransport } from "../../channel-email/types";
|
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
type InMemoryFileProvider,
|
|
12
12
|
} from "@cosmicdrift/kumiko-framework/files";
|
|
13
13
|
import { setupTestStack, type TestStack, TestUsers } from "@cosmicdrift/kumiko-framework/stack";
|
|
14
|
+
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
14
15
|
import { createConfigFeature } from "../../config";
|
|
16
|
+
import { createTenantFeature } from "../../tenant/feature";
|
|
17
|
+
import { createTenantLifecycleFeature } from "../../tenant-lifecycle";
|
|
15
18
|
import { documentIngestFoundationFeature } from "../feature";
|
|
16
19
|
|
|
17
20
|
let stack: TestStack;
|
|
@@ -36,7 +39,13 @@ const textBytes = new TextEncoder().encode("plain text, not a supported mime typ
|
|
|
36
39
|
beforeAll(async () => {
|
|
37
40
|
provider = createInMemoryFileProvider();
|
|
38
41
|
stack = await setupTestStack({
|
|
39
|
-
features: [
|
|
42
|
+
features: [
|
|
43
|
+
createConfigFeature(),
|
|
44
|
+
createTenantFeature(),
|
|
45
|
+
createComplianceProfilesFeature(),
|
|
46
|
+
createTenantLifecycleFeature(),
|
|
47
|
+
documentIngestFoundationFeature,
|
|
48
|
+
],
|
|
40
49
|
files: { storageProvider: provider },
|
|
41
50
|
});
|
|
42
51
|
});
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// by feature.integration.test.ts.
|
|
6
6
|
|
|
7
7
|
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import { EXT_TENANT_DATA } from "@cosmicdrift/kumiko-framework/engine";
|
|
8
9
|
import { documentExtractEntity } from "../entity";
|
|
9
10
|
import { DOCUMENT_INGEST_REQUESTED_EVENT_QN } from "../events";
|
|
10
11
|
import { documentIngestFoundationFeature } from "../feature";
|
|
@@ -18,13 +19,24 @@ describe("documentIngestFoundationFeature — shape", () => {
|
|
|
18
19
|
expect(documentIngestFoundationFeature.requires).toContain("config");
|
|
19
20
|
});
|
|
20
21
|
|
|
22
|
+
test("declares tenant-lifecycle as a hard requirement — it hosts EXT_TENANT_DATA (#1621)", () => {
|
|
23
|
+
expect(documentIngestFoundationFeature.requires).toContain("tenant-lifecycle");
|
|
24
|
+
});
|
|
25
|
+
|
|
21
26
|
test("registers the documentExtract entity as an implicit projection", () => {
|
|
22
27
|
expect(Object.keys(documentIngestFoundationFeature.entities ?? {})).toEqual([
|
|
23
28
|
"documentExtract",
|
|
24
29
|
]);
|
|
25
30
|
});
|
|
26
31
|
|
|
27
|
-
test("
|
|
32
|
+
test("registers an entity-exact EXT_TENANT_DATA destroy hook (#1621)", () => {
|
|
33
|
+
const usage = documentIngestFoundationFeature.extensionUsages.find(
|
|
34
|
+
(u) => u.extensionName === EXT_TENANT_DATA && u.entityName === "documentExtract",
|
|
35
|
+
);
|
|
36
|
+
expect(typeof usage?.options?.["destroy"]).toBe("function");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("documentExtract entity pins table name, field set, and pages subject-encryption (#1621)", () => {
|
|
28
40
|
expect(documentExtractEntity.table).toBe("read_document_extracts");
|
|
29
41
|
expect(Object.keys(documentExtractEntity.fields)).toEqual([
|
|
30
42
|
"fileRefId",
|
|
@@ -32,10 +44,12 @@ describe("documentIngestFoundationFeature — shape", () => {
|
|
|
32
44
|
"pages",
|
|
33
45
|
"meta",
|
|
34
46
|
]);
|
|
47
|
+
// tenantOwned, not `encrypted: true` — the master-key path shreds nothing.
|
|
35
48
|
expect(documentExtractEntity.fields.pages).toMatchObject({
|
|
36
49
|
type: "longText",
|
|
37
|
-
|
|
50
|
+
tenantOwned: true,
|
|
38
51
|
});
|
|
52
|
+
expect(documentExtractEntity.fields.pages).not.toHaveProperty("encrypted");
|
|
39
53
|
expect(documentExtractEntity.fields.meta).toMatchObject({ type: "jsonb" });
|
|
40
54
|
});
|
|
41
55
|
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// The point of #1621: `documentExtract.pages` carries the full extracted
|
|
2
|
+
// text of invoices, IDs and contracts, and used to be `encrypted: true` —
|
|
3
|
+
// master-key ciphertext with no erasure subject, so no destroy path could
|
|
4
|
+
// ever make it unreadable. These tests pin the three properties the swap to
|
|
5
|
+
// `tenantOwned` buys: tenant-subject ciphertext at rest, a plaintext
|
|
6
|
+
// round-trip through the executor, and actual shredding when the tenant
|
|
7
|
+
// subject key dies.
|
|
8
|
+
|
|
9
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
+
import { asRawClient } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
11
|
+
import {
|
|
12
|
+
configurePiiSubjectKms,
|
|
13
|
+
InMemoryKmsAdapter,
|
|
14
|
+
PII_ERASED_SENTINEL,
|
|
15
|
+
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
16
|
+
import { createEventStoreExecutor, createTenantDb } from "@cosmicdrift/kumiko-framework/db";
|
|
17
|
+
import { createSystemUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
18
|
+
import {
|
|
19
|
+
setupTestStack,
|
|
20
|
+
type TestStack,
|
|
21
|
+
testTenantId,
|
|
22
|
+
unsafeCreateEntityTable,
|
|
23
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
24
|
+
import { resetPiiSubjectKmsForTests } from "@cosmicdrift/kumiko-framework/testing";
|
|
25
|
+
import { createConfigFeature } from "../../config";
|
|
26
|
+
import { documentExtractEntity, documentExtractsTable } from "../entity";
|
|
27
|
+
import { readIngestPages, writeIngestPages } from "../pages";
|
|
28
|
+
import { documentExtractTenantDestroyHook } from "../tenant-destroy-hook";
|
|
29
|
+
|
|
30
|
+
let stack: TestStack;
|
|
31
|
+
let kms: InMemoryKmsAdapter;
|
|
32
|
+
|
|
33
|
+
const tenantId = testTenantId(1);
|
|
34
|
+
const actor = createSystemUser(tenantId);
|
|
35
|
+
|
|
36
|
+
const executor = createEventStoreExecutor(documentExtractsTable, documentExtractEntity, {
|
|
37
|
+
entityName: "document-extract",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const pages = [
|
|
41
|
+
{ pageNumber: 1, text: "Rechnung Nr. 4711, Mieterin: Erika Mustermann" },
|
|
42
|
+
{ pageNumber: 2, text: "IBAN DE02120300000000202051" },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
beforeAll(async () => {
|
|
46
|
+
stack = await setupTestStack({ features: [createConfigFeature()] });
|
|
47
|
+
await unsafeCreateEntityTable(stack.db, documentExtractEntity);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterAll(async () => {
|
|
51
|
+
await stack.cleanup();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
beforeEach(async () => {
|
|
55
|
+
kms = new InMemoryKmsAdapter();
|
|
56
|
+
configurePiiSubjectKms(kms);
|
|
57
|
+
await asRawClient(stack.db).unsafe(`TRUNCATE read_document_extracts`);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
resetPiiSubjectKmsForTests();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
async function createExtract(fileRefId: string): Promise<string> {
|
|
65
|
+
const result = await executor.create(
|
|
66
|
+
{
|
|
67
|
+
fileRefId,
|
|
68
|
+
storageKey: `s3://bucket/${fileRefId}`,
|
|
69
|
+
pages: writeIngestPages(pages),
|
|
70
|
+
meta: { provider: "test", ms: 1, needsOcr: false, pagesParsed: 2, totalPages: 2 },
|
|
71
|
+
},
|
|
72
|
+
actor,
|
|
73
|
+
createTenantDb(stack.db, tenantId),
|
|
74
|
+
);
|
|
75
|
+
if (!result.isSuccess) throw new Error(`create failed: ${result.error.message}`);
|
|
76
|
+
return String(result.data.id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function rawPages(id: string): Promise<string> {
|
|
80
|
+
const rows = (await asRawClient(stack.db).unsafe(
|
|
81
|
+
`SELECT pages FROM read_document_extracts WHERE id = $1`,
|
|
82
|
+
[id],
|
|
83
|
+
)) as ReadonlyArray<{ pages: string }>;
|
|
84
|
+
return String(rows[0]?.pages);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
describe("documentExtract.pages — tenant-subject encryption (#1621)", () => {
|
|
88
|
+
test("stored as tenant-subject ciphertext, not master-key ciphertext", async () => {
|
|
89
|
+
const id = await createExtract("file-1");
|
|
90
|
+
|
|
91
|
+
const stored = await rawPages(id);
|
|
92
|
+
expect(stored.startsWith(`kumiko-pii:v2:tenant:${tenantId}:`)).toBe(true);
|
|
93
|
+
expect(stored).not.toContain("Erika Mustermann");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("round-trips back to the original pages through the executor", async () => {
|
|
97
|
+
const id = await createExtract("file-2");
|
|
98
|
+
|
|
99
|
+
const row = await executor.detail({ id }, actor, createTenantDb(stack.db, tenantId));
|
|
100
|
+
expect(readIngestPages(row?.["pages"])).toEqual(pages);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("erasing the tenant subject key makes the extracted text unreadable", async () => {
|
|
104
|
+
const id = await createExtract("file-3");
|
|
105
|
+
|
|
106
|
+
await kms.eraseKey({ kind: "tenant", tenantId });
|
|
107
|
+
|
|
108
|
+
const row = await executor.detail({ id }, actor, createTenantDb(stack.db, tenantId));
|
|
109
|
+
expect(row?.["pages"]).toBe(PII_ERASED_SENTINEL);
|
|
110
|
+
expect(readIngestPages(row?.["pages"])).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("documentExtractTenantDestroyHook (#1621)", () => {
|
|
115
|
+
test("drops the tenant's extract rows and leaves other tenants alone", async () => {
|
|
116
|
+
const mine = await createExtract("file-mine");
|
|
117
|
+
const otherTenantId = testTenantId(2);
|
|
118
|
+
const otherResult = await executor.create(
|
|
119
|
+
{
|
|
120
|
+
fileRefId: "file-theirs",
|
|
121
|
+
storageKey: "s3://bucket/file-theirs",
|
|
122
|
+
pages: writeIngestPages(pages),
|
|
123
|
+
meta: {},
|
|
124
|
+
},
|
|
125
|
+
createSystemUser(otherTenantId),
|
|
126
|
+
createTenantDb(stack.db, otherTenantId),
|
|
127
|
+
);
|
|
128
|
+
if (!otherResult.isSuccess) throw new Error("setup create failed");
|
|
129
|
+
|
|
130
|
+
await documentExtractTenantDestroyHook({ db: stack.db, tenantId });
|
|
131
|
+
|
|
132
|
+
const remaining = (await asRawClient(stack.db).unsafe(
|
|
133
|
+
`SELECT id FROM read_document_extracts`,
|
|
134
|
+
)) as ReadonlyArray<{ id: string }>;
|
|
135
|
+
expect(remaining.map((row) => row.id)).not.toContain(mine);
|
|
136
|
+
expect(remaining).toHaveLength(1);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -1 +1,9 @@
|
|
|
1
|
-
[
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.166.0",
|
|
4
|
+
"type": "breaking",
|
|
5
|
+
"title": "documentExtract.pages: tenantOwned instead of encrypted, and the feature now requires tenant-lifecycle",
|
|
6
|
+
"detail": "`pages` holds the full extracted text of ingested documents (invoices, IDs, contracts) and was `encrypted: true` — app-master-key ciphertext with no erasure subject, so no Art. 17 path could ever make it unreadable. It is now `tenantOwned: true`, which binds it to the tenant subject key that tenant-destroy's eraseSubjectKeys shreds. The feature registers an EXT_TENANT_DATA destroy hook for it, and since tenant-lifecycle hosts that extension point, `document-ingest-foundation` now declares it as a hard requirement — mounting the feature without tenant-lifecycle (plus its own tenant + compliance-profiles requires) makes createRegistry throw (#1621).",
|
|
7
|
+
"migration": "Mount createTenantFeature(), createComplianceProfilesFeature() and createTenantLifecycleFeature() alongside documentIngestFoundationFeature. Rows written before this version carry master-key envelope ciphertext that the subject-decrypt path does not recognise — readIngestPages returns [] for them. There is no reencrypt job; if you have existing extracts you care about, decrypt and rewrite them before upgrading."
|
|
8
|
+
}
|
|
9
|
+
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { buildEntityTable } from "@cosmicdrift/kumiko-framework/db";
|
|
1
2
|
import {
|
|
2
3
|
createEntity,
|
|
3
4
|
createJsonbField,
|
|
@@ -11,7 +12,8 @@ import {
|
|
|
11
12
|
// jsonb has no encryption support in the engine, and this column holds the
|
|
12
13
|
// full extracted text of ingested documents (invoices, IDs, contracts).
|
|
13
14
|
// Writers/readers MUST use writeIngestPages / readIngestPages — do not pass
|
|
14
|
-
// a raw IngestPage[] into executor.create (
|
|
15
|
+
// a raw IngestPage[] into executor.create (the encryption hook requires a
|
|
16
|
+
// string).
|
|
15
17
|
export type IngestPage = {
|
|
16
18
|
readonly pageNumber: number;
|
|
17
19
|
readonly text: string;
|
|
@@ -35,9 +37,16 @@ export const documentExtractEntity = createEntity({
|
|
|
35
37
|
fields: {
|
|
36
38
|
fileRefId: createTextField({ required: true }),
|
|
37
39
|
storageKey: createTextField({ required: true }),
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
|
|
40
|
+
// Holds the full extracted document text (PII). `tenantOwned`, NOT
|
|
41
|
+
// `encrypted: true` (#1621): the master-key path has no erasure subject,
|
|
42
|
+
// so nothing here would ever be shreddable. Tenant-subject ciphertext
|
|
43
|
+
// dies with eraseSubjectKeys on tenant-destroy (#800 pattern). Whose
|
|
44
|
+
// subject a third party named inside a document is stays open, same as
|
|
45
|
+
// inbound-mail-foundation (#957). meta is provider telemetry only and
|
|
46
|
+
// stays plaintext jsonb.
|
|
47
|
+
pages: createLongTextField({ tenantOwned: true }),
|
|
41
48
|
meta: createJsonbField(),
|
|
42
49
|
},
|
|
43
50
|
});
|
|
51
|
+
|
|
52
|
+
export const documentExtractsTable = buildEntityTable("documentExtract", documentExtractEntity);
|
|
@@ -12,7 +12,12 @@
|
|
|
12
12
|
// CosmicDriftGameStudio/kumiko-framework#1495 for the full phase breakdown.
|
|
13
13
|
|
|
14
14
|
import { entityEventName } from "@cosmicdrift/kumiko-framework/db";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
access,
|
|
17
|
+
createTenantConfig,
|
|
18
|
+
defineFeature,
|
|
19
|
+
EXT_TENANT_DATA,
|
|
20
|
+
} from "@cosmicdrift/kumiko-framework/engine";
|
|
16
21
|
import { z } from "zod";
|
|
17
22
|
import { documentExtractEntity } from "./entity";
|
|
18
23
|
import {
|
|
@@ -21,6 +26,7 @@ import {
|
|
|
21
26
|
DOCUMENT_INGEST_REQUESTED_EVENT_SHORT,
|
|
22
27
|
documentIngestRequestedPayloadSchema,
|
|
23
28
|
} from "./events";
|
|
29
|
+
import { documentExtractTenantDestroyHook } from "./tenant-destroy-hook";
|
|
24
30
|
|
|
25
31
|
const FEATURE_NAME = "document-ingest-foundation";
|
|
26
32
|
|
|
@@ -59,9 +65,14 @@ export const documentIngestFoundationFeature = defineFeature(FEATURE_NAME, (r) =
|
|
|
59
65
|
category: "storage",
|
|
60
66
|
recommended: false,
|
|
61
67
|
});
|
|
62
|
-
|
|
68
|
+
// tenant-lifecycle hosts EXT_TENANT_DATA — documentExtract.pages is
|
|
69
|
+
// tenant-subject ciphertext and needs the destroy hook below (#1621).
|
|
70
|
+
r.requires("config", "tenant-lifecycle");
|
|
63
71
|
|
|
64
72
|
r.entity("documentExtract", documentExtractEntity);
|
|
73
|
+
r.useExtension(EXT_TENANT_DATA, "documentExtract", {
|
|
74
|
+
destroy: documentExtractTenantDestroyHook,
|
|
75
|
+
});
|
|
65
76
|
|
|
66
77
|
const ocrLanguageConfigKey = r.config(
|
|
67
78
|
"ocrLanguage",
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
// Public API of the document-ingest-foundation bundled-feature.
|
|
2
2
|
|
|
3
|
-
export {
|
|
3
|
+
export {
|
|
4
|
+
type DocumentExtractMeta,
|
|
5
|
+
documentExtractEntity,
|
|
6
|
+
documentExtractsTable,
|
|
7
|
+
type IngestPage,
|
|
8
|
+
} from "./entity";
|
|
4
9
|
export {
|
|
5
10
|
DOCUMENT_INGEST_AGGREGATE_TYPE,
|
|
6
11
|
DOCUMENT_INGEST_REQUESTED_EVENT_QN,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Tenant-destroy hook for the documentExtract entity-projection (#1621).
|
|
2
|
+
//
|
|
3
|
+
// Per-row forget() through the executor, not a bulk deleteMany: documentExtract
|
|
4
|
+
// is an ES-managed implicit projection, so a store-table write here would be
|
|
5
|
+
// eventless and a later rebuild would replay the historical create events and
|
|
6
|
+
// resurrect every row this hook removed. forget() (Art. 17 hard-purge) is
|
|
7
|
+
// replayed by the implicit projection itself, which keeps the erasure
|
|
8
|
+
// rebuild-safe — the same reasoning as tenant-lifecycle's membership stage,
|
|
9
|
+
// and the reason the feature owns an r.entity instead of an r.projection
|
|
10
|
+
// (kumiko-framework#1495). The `pages` ciphertext dies separately when the
|
|
11
|
+
// pipeline's later `subject-keys` stage erases the tenant subject key.
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
createEventStoreExecutor,
|
|
15
|
+
createTenantDb,
|
|
16
|
+
type DbRunner,
|
|
17
|
+
selectMany,
|
|
18
|
+
} from "@cosmicdrift/kumiko-framework/db";
|
|
19
|
+
import { createSystemUser, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
20
|
+
import { documentExtractEntity, documentExtractsTable } from "./entity";
|
|
21
|
+
|
|
22
|
+
const executor = createEventStoreExecutor(documentExtractsTable, documentExtractEntity, {
|
|
23
|
+
entityName: "document-extract",
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
type DestroyCtx = {
|
|
27
|
+
readonly db: DbRunner;
|
|
28
|
+
readonly tenantId: TenantId;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export async function documentExtractTenantDestroyHook(ctx: DestroyCtx): Promise<void> {
|
|
32
|
+
const rows = await selectMany<{ id: string }>(ctx.db, documentExtractsTable, {
|
|
33
|
+
tenantId: ctx.tenantId,
|
|
34
|
+
});
|
|
35
|
+
const user = createSystemUser(ctx.tenantId);
|
|
36
|
+
const db = createTenantDb(ctx.db, ctx.tenantId, "system");
|
|
37
|
+
for (const row of rows) {
|
|
38
|
+
const result = await executor.forget({ id: row.id }, user, db);
|
|
39
|
+
// Executor writes return {isSuccess:false} instead of throwing — a
|
|
40
|
+
// discarded result would report this destroy stage "succeeded" while the
|
|
41
|
+
// extracted document text survives. Throw so the pipeline's retry/abandon
|
|
42
|
+
// handling sees it.
|
|
43
|
+
if (!result.isSuccess) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`document-ingest-foundation: failed to forget documentExtract ${row.id} for tenant ${ctx.tenantId}: ${result.error.message}`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -7,11 +7,7 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
9
9
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
10
|
-
import {
|
|
11
|
-
configurePiiSubjectKms,
|
|
12
|
-
InMemoryKmsAdapter,
|
|
13
|
-
resetPiiSubjectKmsForTests,
|
|
14
|
-
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
10
|
+
import { configurePiiSubjectKms, InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
15
11
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
16
12
|
import {
|
|
17
13
|
createSystemUser,
|
|
@@ -31,6 +27,7 @@ import {
|
|
|
31
27
|
import {
|
|
32
28
|
createMutableMasterKeyProvider,
|
|
33
29
|
type MutableMasterKeyProvider,
|
|
30
|
+
resetPiiSubjectKmsForTests,
|
|
34
31
|
} from "@cosmicdrift/kumiko-framework/testing";
|
|
35
32
|
import { Hono } from "hono";
|
|
36
33
|
import {
|
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
InMemoryKmsAdapter,
|
|
21
21
|
isPiiCiphertext,
|
|
22
22
|
PII_ERASED_SENTINEL,
|
|
23
|
-
resetPiiSubjectKmsForTests,
|
|
24
23
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
25
24
|
import { createTenantDb, type DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
26
25
|
import type { HandlerContext } from "@cosmicdrift/kumiko-framework/engine";
|
|
@@ -39,7 +38,7 @@ import {
|
|
|
39
38
|
testTenantId,
|
|
40
39
|
unsafeCreateEntityTable,
|
|
41
40
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
42
|
-
import { bridgeStub } from "@cosmicdrift/kumiko-framework/testing";
|
|
41
|
+
import { bridgeStub, resetPiiSubjectKmsForTests } from "@cosmicdrift/kumiko-framework/testing";
|
|
43
42
|
import {
|
|
44
43
|
createComplianceProfilesFeature,
|
|
45
44
|
tenantComplianceProfileEntity,
|
|
@@ -7,11 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
9
9
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
10
|
-
import {
|
|
11
|
-
configurePiiSubjectKms,
|
|
12
|
-
InMemoryKmsAdapter,
|
|
13
|
-
resetPiiSubjectKmsForTests,
|
|
14
|
-
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
10
|
+
import { configurePiiSubjectKms, InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
15
11
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
16
12
|
import { createEventsTable, loadAggregate } from "@cosmicdrift/kumiko-framework/event-store";
|
|
17
13
|
import { rebuildProjection } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
@@ -22,6 +18,7 @@ import {
|
|
|
22
18
|
testTenantId,
|
|
23
19
|
unsafeCreateEntityTable,
|
|
24
20
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
21
|
+
import { resetPiiSubjectKmsForTests } from "@cosmicdrift/kumiko-framework/testing";
|
|
25
22
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
26
23
|
import {
|
|
27
24
|
createComplianceProfilesFeature,
|
|
@@ -6,11 +6,7 @@
|
|
|
6
6
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
7
7
|
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
8
8
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
9
|
-
import {
|
|
10
|
-
configurePiiSubjectKms,
|
|
11
|
-
InMemoryKmsAdapter,
|
|
12
|
-
resetPiiSubjectKmsForTests,
|
|
13
|
-
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
9
|
+
import { configurePiiSubjectKms, InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
14
10
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
15
11
|
import { createSystemUser, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
16
12
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -21,7 +17,7 @@ import {
|
|
|
21
17
|
testTenantId,
|
|
22
18
|
unsafeCreateEntityTable,
|
|
23
19
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
24
|
-
import { waitFor } from "@cosmicdrift/kumiko-framework/testing";
|
|
20
|
+
import { resetPiiSubjectKmsForTests, waitFor } from "@cosmicdrift/kumiko-framework/testing";
|
|
25
21
|
import {
|
|
26
22
|
createComplianceProfilesFeature,
|
|
27
23
|
tenantComplianceProfileEntity,
|
|
@@ -16,11 +16,7 @@ import { randomBytes } from "node:crypto";
|
|
|
16
16
|
import { connect } from "node:net";
|
|
17
17
|
import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
18
18
|
import { selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
19
|
-
import {
|
|
20
|
-
configurePiiSubjectKms,
|
|
21
|
-
InMemoryKmsAdapter,
|
|
22
|
-
resetPiiSubjectKmsForTests,
|
|
23
|
-
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
19
|
+
import { configurePiiSubjectKms, InMemoryKmsAdapter } from "@cosmicdrift/kumiko-framework/crypto";
|
|
24
20
|
import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
|
|
25
21
|
import { createSystemUser, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
26
22
|
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -36,6 +32,7 @@ import {
|
|
|
36
32
|
import {
|
|
37
33
|
createMutableMasterKeyProvider,
|
|
38
34
|
type MutableMasterKeyProvider,
|
|
35
|
+
resetPiiSubjectKmsForTests,
|
|
39
36
|
waitFor,
|
|
40
37
|
} from "@cosmicdrift/kumiko-framework/testing";
|
|
41
38
|
import { createTransport } from "nodemailer";
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
InMemoryKmsAdapter,
|
|
14
14
|
isPiiCiphertext,
|
|
15
15
|
PII_ERASED_SENTINEL,
|
|
16
|
-
resetPiiSubjectKmsForTests,
|
|
17
16
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
18
17
|
import { createRegistry } from "@cosmicdrift/kumiko-framework/engine";
|
|
19
18
|
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -24,7 +23,7 @@ import {
|
|
|
24
23
|
type TestRedis,
|
|
25
24
|
unsafePushTables,
|
|
26
25
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
27
|
-
import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
26
|
+
import { resetPiiSubjectKmsForTests, resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
28
27
|
import { createJobsFeature } from "../feature";
|
|
29
28
|
import { createJobRunLogger, JOB_RUN_STARTED_EVENT } from "../job-run-logger";
|
|
30
29
|
import { jobRunLogsTable, jobRunsTable } from "../job-run-table";
|
|
@@ -7,8 +7,6 @@ import {
|
|
|
7
7
|
configurePiiSubjectKms,
|
|
8
8
|
InMemoryKmsAdapter,
|
|
9
9
|
isPiiCiphertext,
|
|
10
|
-
resetBlindIndexKeyForTests,
|
|
11
|
-
resetPiiSubjectKmsForTests,
|
|
12
10
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
13
11
|
import type { SessionUser, TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
12
|
import {
|
|
@@ -18,7 +16,12 @@ import {
|
|
|
18
16
|
unsafeCreateEntityTable,
|
|
19
17
|
unsafePushTables,
|
|
20
18
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
21
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
createTestEnvelopeCipher,
|
|
21
|
+
deleteRows,
|
|
22
|
+
resetBlindIndexKeyForTests,
|
|
23
|
+
resetPiiSubjectKmsForTests,
|
|
24
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
22
25
|
import { Temporal } from "temporal-polyfill";
|
|
23
26
|
import { AuthHandlers } from "../../auth-email-password/constants";
|
|
24
27
|
import { createAuthEmailPasswordFeature } from "../../auth-email-password/feature";
|
|
@@ -7,8 +7,6 @@ import {
|
|
|
7
7
|
configurePiiSubjectKms,
|
|
8
8
|
InMemoryKmsAdapter,
|
|
9
9
|
isPiiCiphertext,
|
|
10
|
-
resetBlindIndexKeyForTests,
|
|
11
|
-
resetPiiSubjectKmsForTests,
|
|
12
10
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
13
11
|
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
14
12
|
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
@@ -24,6 +22,8 @@ import {
|
|
|
24
22
|
createLateBoundHolder,
|
|
25
23
|
createTestEnvelopeCipher,
|
|
26
24
|
deleteRows,
|
|
25
|
+
resetBlindIndexKeyForTests,
|
|
26
|
+
resetPiiSubjectKmsForTests,
|
|
27
27
|
resetTestTables,
|
|
28
28
|
updateRows,
|
|
29
29
|
} from "@cosmicdrift/kumiko-framework/testing";
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
configurePiiSubjectKms,
|
|
12
12
|
InMemoryKmsAdapter,
|
|
13
13
|
isPiiCiphertext,
|
|
14
|
-
resetPiiSubjectKmsForTests,
|
|
15
14
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
16
15
|
import type { SessionUser } from "@cosmicdrift/kumiko-framework/engine";
|
|
17
16
|
import {
|
|
@@ -21,7 +20,7 @@ import {
|
|
|
21
20
|
unsafeCreateEntityTable,
|
|
22
21
|
unsafePushTables,
|
|
23
22
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
24
|
-
import { resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
23
|
+
import { resetPiiSubjectKmsForTests, resetTestTables } from "@cosmicdrift/kumiko-framework/testing";
|
|
25
24
|
import { createConfigFeature } from "../../config";
|
|
26
25
|
import { configValuesTable } from "../../config/table";
|
|
27
26
|
import { createUserFeature } from "../../user/feature";
|
|
@@ -84,6 +84,20 @@ export type ComposedApp<TCaps extends Readonly<Record<string, unknown>>> = {
|
|
|
84
84
|
* delivered something the platform doesn't understand. Failing loud at boot
|
|
85
85
|
* is safer than silently mounting the wrong feature-set and hoping nobody
|
|
86
86
|
* notices when "Pro" turns out to mean "Free".
|
|
87
|
+
*
|
|
88
|
+
* **No production callers, and that is not rot.** This is the BOOT-TIME half
|
|
89
|
+
* of tier composition — which features get mounted at all. The RUNTIME half,
|
|
90
|
+
* which features a given tenant may see out of everything mounted, is
|
|
91
|
+
* `createTierEngineFeature`'s `resolver(tenantId) => ReadonlySet<string>`
|
|
92
|
+
* (feature.ts). Both read the same tierMap at opposite ends. Apps use the
|
|
93
|
+
* resolver today; nobody composes per-tenant feature sets at boot yet, so an
|
|
94
|
+
* app that only needs caps merges them locally rather than constructing a
|
|
95
|
+
* featureRegistry it has no other use for (kumiko-studio's
|
|
96
|
+
* `resolvePlatformCaps`, studio#154/1 → PR #162 — a deliberate, typed copy of
|
|
97
|
+
* step 4, not a workaround). Whether the boot-time half ever gets a caller is
|
|
98
|
+
* a product question: kumiko-studio's BYO pivot (Sprint 8a Phase 3a)
|
|
99
|
+
* differentiates tiers by CAPS, not by feature set. Delete this only once no
|
|
100
|
+
* app plans to differentiate by feature set — the engine is app-agnostic.
|
|
87
101
|
*/
|
|
88
102
|
export function composeApp<TCaps extends Readonly<Record<string, unknown>>>(
|
|
89
103
|
input: ComposeAppInput<TCaps>,
|
|
@@ -11,8 +11,6 @@ import {
|
|
|
11
11
|
configurePiiSubjectKms,
|
|
12
12
|
InMemoryKmsAdapter,
|
|
13
13
|
isPiiCiphertext,
|
|
14
|
-
resetBlindIndexKeyForTests,
|
|
15
|
-
resetPiiSubjectKmsForTests,
|
|
16
14
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
17
15
|
import { createEventsTable, eventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
18
16
|
import {
|
|
@@ -21,7 +19,11 @@ import {
|
|
|
21
19
|
testTenantId,
|
|
22
20
|
unsafeCreateEntityTable,
|
|
23
21
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
24
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
resetBlindIndexKeyForTests,
|
|
24
|
+
resetPiiSubjectKmsForTests,
|
|
25
|
+
resetTestTables,
|
|
26
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
25
27
|
import {
|
|
26
28
|
createComplianceProfilesFeature,
|
|
27
29
|
tenantComplianceProfileEntity,
|
|
@@ -6,11 +6,7 @@
|
|
|
6
6
|
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
7
7
|
import { authFoundationFeature } from "@cosmicdrift/kumiko-bundled-features/auth-foundation";
|
|
8
8
|
import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
|
|
9
|
-
import {
|
|
10
|
-
buildEntityTable,
|
|
11
|
-
configureEntityFieldEncryption,
|
|
12
|
-
resetEntityFieldEncryptionCacheForTests,
|
|
13
|
-
} from "@cosmicdrift/kumiko-framework/db";
|
|
9
|
+
import { buildEntityTable, configureEntityFieldEncryption } from "@cosmicdrift/kumiko-framework/db";
|
|
14
10
|
import {
|
|
15
11
|
createEntity,
|
|
16
12
|
createRegistry,
|
|
@@ -20,7 +16,10 @@ import {
|
|
|
20
16
|
type UserDataExportHook,
|
|
21
17
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
22
18
|
import { setupTestStack, type TestStack } from "@cosmicdrift/kumiko-framework/stack";
|
|
23
|
-
import {
|
|
19
|
+
import {
|
|
20
|
+
createTestEnvelopeCipher,
|
|
21
|
+
resetEntityFieldEncryptionCacheForTests,
|
|
22
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
24
23
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
25
24
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
26
25
|
import { createDataRetentionFeature } from "../../data-retention";
|
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
configurePiiSubjectKms,
|
|
22
22
|
encryptPiiFieldValues,
|
|
23
23
|
InMemoryKmsAdapter,
|
|
24
|
-
resetPiiSubjectKmsForTests,
|
|
25
24
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
26
25
|
import { fileRefsTable } from "@cosmicdrift/kumiko-framework/files";
|
|
27
26
|
import {
|
|
@@ -30,7 +29,11 @@ import {
|
|
|
30
29
|
unsafeCreateEntityTable,
|
|
31
30
|
unsafePushTables,
|
|
32
31
|
} from "@cosmicdrift/kumiko-framework/stack";
|
|
33
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
resetPiiSubjectKmsForTests,
|
|
34
|
+
resetTestTables,
|
|
35
|
+
seedRow,
|
|
36
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
34
37
|
import { getTemporal } from "@cosmicdrift/kumiko-framework/time";
|
|
35
38
|
import { createComplianceProfilesFeature } from "../../compliance-profiles";
|
|
36
39
|
import { createDataRetentionFeature } from "../../data-retention";
|