@cosmicdrift/kumiko-framework 0.166.0 → 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 +4 -6
- package/src/api/__tests__/pii-leak-guard.integration.test.ts +2 -5
- package/src/crypto/index.ts +0 -3
- package/src/crypto/kms-adapter.ts +26 -1
- package/src/db/__tests__/blind-index.integration.test.ts +4 -2
- package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +4 -2
- package/src/db/index.ts +0 -1
- package/src/event-store/__tests__/backfill-pii.integration.test.ts +4 -2
- package/src/event-store/errors.ts +58 -2
- package/src/search/__tests__/search-pii-derived-index.integration.test.ts +1 -1
- package/src/secrets/__tests__/leak-guard.test.ts +12 -0
- package/src/testing/index.ts +13 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.167.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -182,9 +182,10 @@
|
|
|
182
182
|
"./package.json": "./package.json"
|
|
183
183
|
},
|
|
184
184
|
"dependencies": {
|
|
185
|
+
"@cosmicdrift/kumiko-types": "0.167.0",
|
|
185
186
|
"bullmq": "^5.76.7",
|
|
186
187
|
"bun-types": "^1.3.13",
|
|
187
|
-
"hono": "^4.12.
|
|
188
|
+
"hono": "^4.12.27",
|
|
188
189
|
"i18next": "^26.1.0",
|
|
189
190
|
"ioredis": "^5.10.1",
|
|
190
191
|
"jose": "^6.2.3",
|
|
@@ -196,11 +197,8 @@
|
|
|
196
197
|
"uuid": "^14.0.0",
|
|
197
198
|
"zod": "^4.4.3"
|
|
198
199
|
},
|
|
199
|
-
"peerDependencies": {
|
|
200
|
-
"@cosmicdrift/kumiko-types": "^0.166.0"
|
|
201
|
-
},
|
|
202
200
|
"devDependencies": {
|
|
203
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
201
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.167.0",
|
|
204
202
|
"bun-types": "^1.3.13",
|
|
205
203
|
"pino-pretty": "^13.1.3"
|
|
206
204
|
},
|
|
@@ -3,12 +3,9 @@
|
|
|
3
3
|
// rot), Prod → redact + Error-Log, ohne KMS → kein Scan (pass-through).
|
|
4
4
|
|
|
5
5
|
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
6
|
+
import { resetPiiSubjectKmsForTests } from "@cosmicdrift/kumiko-framework/testing";
|
|
6
7
|
import { z } from "zod";
|
|
7
|
-
import {
|
|
8
|
-
configurePiiSubjectKms,
|
|
9
|
-
InMemoryKmsAdapter,
|
|
10
|
-
resetPiiSubjectKmsForTests,
|
|
11
|
-
} from "../../crypto";
|
|
8
|
+
import { configurePiiSubjectKms, InMemoryKmsAdapter } from "../../crypto";
|
|
12
9
|
import { defineFeature } from "../../engine/define-feature";
|
|
13
10
|
import { defineQueryHandler } from "../../engine/define-handler";
|
|
14
11
|
import { setupTestStack, type TestStack, TestUsers } from "../../stack";
|
package/src/crypto/index.ts
CHANGED
|
@@ -6,14 +6,12 @@ export {
|
|
|
6
6
|
configureBlindIndexKey,
|
|
7
7
|
configuredBlindIndexKey,
|
|
8
8
|
decodeBlindIndexKey,
|
|
9
|
-
resetBlindIndexKeyForTests,
|
|
10
9
|
} from "./blind-index";
|
|
11
10
|
export {
|
|
12
11
|
configuredEventPiiCatalog,
|
|
13
12
|
configureEventPiiCatalog,
|
|
14
13
|
type EventPiiCatalog,
|
|
15
14
|
encryptEventPayloadPii,
|
|
16
|
-
resetEventPiiCatalogForTests,
|
|
17
15
|
} from "./event-pii";
|
|
18
16
|
export { InMemoryKmsAdapter } from "./in-memory-kms-adapter";
|
|
19
17
|
export {
|
|
@@ -64,7 +62,6 @@ export {
|
|
|
64
62
|
isPiiCiphertext,
|
|
65
63
|
PII_CIPHERTEXT_PREFIX,
|
|
66
64
|
PII_ERASED_SENTINEL,
|
|
67
|
-
resetPiiSubjectKmsForTests,
|
|
68
65
|
} from "./pii-field-encryption";
|
|
69
66
|
export {
|
|
70
67
|
createRequestKmsCache,
|
|
@@ -1,2 +1,27 @@
|
|
|
1
|
-
|
|
1
|
+
import { type SubjectId, subjectIdToKey } from "@cosmicdrift/kumiko-types/kms-adapter-types";
|
|
2
|
+
|
|
2
3
|
export * from "@cosmicdrift/kumiko-types/kms-adapter-types";
|
|
4
|
+
|
|
5
|
+
// The KMS error classes live here and not in kumiko-types (#1629): callers
|
|
6
|
+
// branch on them with `instanceof`, which needs a single copy of the class.
|
|
7
|
+
|
|
8
|
+
export class KeyErasedError extends Error {
|
|
9
|
+
constructor(public readonly subject: SubjectId) {
|
|
10
|
+
super(`Subject key erased: ${subjectIdToKey(subject)}`);
|
|
11
|
+
this.name = "KeyErasedError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class KeyNotFoundError extends Error {
|
|
16
|
+
constructor(public readonly subject: SubjectId) {
|
|
17
|
+
super(`Subject key not found: ${subjectIdToKey(subject)}`);
|
|
18
|
+
this.name = "KeyNotFoundError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class KeyAlreadyExistsError extends Error {
|
|
23
|
+
constructor(public readonly subject: SubjectId) {
|
|
24
|
+
super(`Subject key already exists: ${subjectIdToKey(subject)}`);
|
|
25
|
+
this.name = "KeyAlreadyExistsError";
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
// dem Ciphertext (erased → NULL), und der Forget-Sweep nullt sofort.
|
|
5
5
|
|
|
6
6
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
7
|
+
import {
|
|
8
|
+
resetBlindIndexKeyForTests,
|
|
9
|
+
resetPiiSubjectKmsForTests,
|
|
10
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
7
11
|
import {
|
|
8
12
|
computeBlindIndex,
|
|
9
13
|
configureBlindIndexKey,
|
|
@@ -11,8 +15,6 @@ import {
|
|
|
11
15
|
decodeBlindIndexKey,
|
|
12
16
|
InMemoryKmsAdapter,
|
|
13
17
|
isPiiCiphertext,
|
|
14
|
-
resetBlindIndexKeyForTests,
|
|
15
|
-
resetPiiSubjectKmsForTests,
|
|
16
18
|
subjectIdToKey,
|
|
17
19
|
} from "../../crypto";
|
|
18
20
|
import { defineFeature } from "../../engine/define-feature";
|
|
@@ -227,6 +227,10 @@ describe("implicit-projection / Live==Rebuild equivalence", () => {
|
|
|
227
227
|
// damit auch für sensitive Spalten + Blind-Index. Einzige legitime Divergenz
|
|
228
228
|
// bleibt Crypto-Shredding: DEK erased → bidx NULL, Wert unlesbar.
|
|
229
229
|
|
|
230
|
+
import {
|
|
231
|
+
resetBlindIndexKeyForTests,
|
|
232
|
+
resetPiiSubjectKmsForTests,
|
|
233
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
230
234
|
import {
|
|
231
235
|
computeBlindIndex,
|
|
232
236
|
configureBlindIndexKey,
|
|
@@ -235,8 +239,6 @@ import {
|
|
|
235
239
|
decryptPiiFieldValues,
|
|
236
240
|
InMemoryKmsAdapter,
|
|
237
241
|
isPiiCiphertext,
|
|
238
|
-
resetBlindIndexKeyForTests,
|
|
239
|
-
resetPiiSubjectKmsForTests,
|
|
240
242
|
} from "../../crypto";
|
|
241
243
|
import { asRawClient, selectMany } from "../../db/query";
|
|
242
244
|
|
package/src/db/index.ts
CHANGED
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
// blind-index column, so equality lookups keep working.
|
|
8
8
|
|
|
9
9
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
10
|
+
import {
|
|
11
|
+
resetBlindIndexKeyForTests,
|
|
12
|
+
resetPiiSubjectKmsForTests,
|
|
13
|
+
} from "@cosmicdrift/kumiko-framework/testing";
|
|
10
14
|
import { z } from "zod";
|
|
11
15
|
import {
|
|
12
16
|
configureBlindIndexKey,
|
|
@@ -14,8 +18,6 @@ import {
|
|
|
14
18
|
InMemoryKmsAdapter,
|
|
15
19
|
isPiiCiphertext,
|
|
16
20
|
PII_ERASED_SENTINEL,
|
|
17
|
-
resetBlindIndexKeyForTests,
|
|
18
|
-
resetPiiSubjectKmsForTests,
|
|
19
21
|
} from "../../crypto";
|
|
20
22
|
import { applyEntityEvent } from "../../db/apply-entity-event";
|
|
21
23
|
import { backfillEventPiiEncryption } from "../../db/queries/backfill-pii";
|
|
@@ -1,2 +1,58 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// Failure modes of the event-store's append() path. Surfaced as typed
|
|
2
|
+
// errors so the executor layer can map them to the framework's
|
|
3
|
+
// WriteResult error contract (version_conflict).
|
|
4
|
+
//
|
|
5
|
+
// These live here and not in kumiko-types (#1629): `instanceof` needs a
|
|
6
|
+
// single copy of the class, which forced kumiko-types to be a peerDependency
|
|
7
|
+
// and made every minor changeset resolve to a major bump.
|
|
8
|
+
|
|
9
|
+
export class VersionConflictError extends Error {
|
|
10
|
+
public readonly aggregateId: string;
|
|
11
|
+
public readonly expectedVersion: number;
|
|
12
|
+
constructor(aggregateId: string, expectedVersion: number) {
|
|
13
|
+
super(
|
|
14
|
+
`Version conflict on aggregate ${aggregateId}: expected predecessor version ${expectedVersion}`,
|
|
15
|
+
);
|
|
16
|
+
this.name = "VersionConflictError";
|
|
17
|
+
this.aggregateId = aggregateId;
|
|
18
|
+
this.expectedVersion = expectedVersion;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Thrown when append() collides on the partial unique index over
|
|
23
|
+
// metadata.idempotencyKey (tenant-scoped). Distinct from VersionConflictError:
|
|
24
|
+
// a version conflict means two writers raced the same predecessor; this
|
|
25
|
+
// means the same idempotency key was used twice, which is a caller-side
|
|
26
|
+
// retry that must have already appended once. Callers that set
|
|
27
|
+
// idempotencyKey should treat this as "already applied" rather than retry.
|
|
28
|
+
export class IdempotentAppendConflictError extends Error {
|
|
29
|
+
public readonly tenantId: string;
|
|
30
|
+
public readonly idempotencyKey: string;
|
|
31
|
+
constructor(tenantId: string, idempotencyKey: string) {
|
|
32
|
+
super(
|
|
33
|
+
`Idempotency conflict on tenant ${tenantId}: an event with idempotencyKey "${idempotencyKey}" was already appended.`,
|
|
34
|
+
);
|
|
35
|
+
this.name = "IdempotentAppendConflictError";
|
|
36
|
+
this.tenantId = tenantId;
|
|
37
|
+
this.idempotencyKey = idempotencyKey;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Thrown when ctx.appendEvent targets an archived stream. Archived aggregates
|
|
42
|
+
// are read-only — restoreStream() makes them writable again. The archive
|
|
43
|
+
// state is not carried on the events themselves; it lives on the sparse
|
|
44
|
+
// kumiko_archived_streams table. Handlers that need to branch on archive
|
|
45
|
+
// state should call ctx.isStreamArchived(id) first.
|
|
46
|
+
export class ArchivedStreamError extends Error {
|
|
47
|
+
public readonly tenantId: string;
|
|
48
|
+
public readonly aggregateId: string;
|
|
49
|
+
constructor(tenantId: string, aggregateId: string) {
|
|
50
|
+
super(
|
|
51
|
+
`Aggregate ${aggregateId} on tenant ${tenantId} is archived — appendEvent is blocked. ` +
|
|
52
|
+
`Call restoreStream() to re-open the stream before writing.`,
|
|
53
|
+
);
|
|
54
|
+
this.name = "ArchivedStreamError";
|
|
55
|
+
this.tenantId = tenantId;
|
|
56
|
+
this.aggregateId = aggregateId;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
// plaintext in derived search index, purged on subject erase.
|
|
3
3
|
|
|
4
4
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
5
|
+
import { resetPiiSubjectKmsForTests } from "@cosmicdrift/kumiko-framework/testing";
|
|
5
6
|
import {
|
|
6
7
|
configurePiiSubjectKms,
|
|
7
8
|
InMemoryKmsAdapter,
|
|
8
9
|
isPiiCiphertext,
|
|
9
|
-
resetPiiSubjectKmsForTests,
|
|
10
10
|
subjectIdToKey,
|
|
11
11
|
} from "../../crypto";
|
|
12
12
|
import { asRawClient, buildEntityTable, createEventStoreExecutor, createTenantDb } from "../../db";
|
|
@@ -89,4 +89,16 @@ describe("assertNoSecretLeak — walks the response tree for branded values", ()
|
|
|
89
89
|
expect(() => assertNoSecretLeak(undefined)).not.toThrow();
|
|
90
90
|
expect(() => assertNoSecretLeak(null)).not.toThrow();
|
|
91
91
|
});
|
|
92
|
+
|
|
93
|
+
// Dual-package hazard: a second resolved copy of @cosmicdrift/kumiko-types
|
|
94
|
+
// brands with ITS OWN symbol. Constructing the brand from the global registry
|
|
95
|
+
// here stands in for that copy — with a plain Symbol() the guard walks past
|
|
96
|
+
// this value and serializes the plaintext (#1438-adjacent).
|
|
97
|
+
test("catches a Secret branded by another copy of the package", () => {
|
|
98
|
+
const foreign = {
|
|
99
|
+
[Symbol.for("kumiko.secret")]: true as const,
|
|
100
|
+
reveal: () => "plaintext-from-another-copy",
|
|
101
|
+
};
|
|
102
|
+
expect(() => assertNoSecretLeak({ payload: foreign })).toThrow(/leaked.*payload/);
|
|
103
|
+
});
|
|
92
104
|
});
|
package/src/testing/index.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
// Test
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
1
|
+
// Test assertions and domain test fixtures. Production code (dev-server, bin/)
|
|
2
|
+
// must import nothing from this subpath — the stack builders live in
|
|
3
|
+
// `@cosmicdrift/kumiko-framework/stack`.
|
|
4
|
+
|
|
5
|
+
// The four cache/injection resets stay in their own modules (they close over
|
|
6
|
+
// module-private state) and are only re-exported here — they are out of /crypto
|
|
7
|
+
// and /db as of #1631. A production call to resetPiiSubjectKmsForTests() silently
|
|
8
|
+
// switches the PII layer off, and subject-annotated fields are written in
|
|
9
|
+
// plaintext from then on: no error, no log.
|
|
10
|
+
export { resetBlindIndexKeyForTests } from "../crypto/blind-index";
|
|
11
|
+
export { resetEventPiiCatalogForTests } from "../crypto/event-pii";
|
|
12
|
+
export { resetPiiSubjectKmsForTests } from "../crypto/pii-field-encryption";
|
|
13
|
+
export { resetEntityFieldEncryptionCacheForTests } from "../db/entity-field-encryption";
|
|
5
14
|
|
|
6
15
|
export { rolesOf } from "./access-assertions";
|
|
7
16
|
export { expectError, expectSuccess } from "./assertions";
|