@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-framework",
3
- "version": "0.166.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.18",
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.166.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";
@@ -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
- // Legacy path re-exported for callers still importing this module directly.
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
@@ -48,7 +48,6 @@ export {
48
48
  configureEntityFieldEncryption,
49
49
  decryptEntityFieldValues,
50
50
  encryptEntityFieldValues,
51
- resetEntityFieldEncryptionCacheForTests,
52
51
  } from "./entity-field-encryption";
53
52
  export type {
54
53
  BuildEntityTableMetaOptions,
@@ -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
- // Legacy path re-exported for callers still importing this module directly.
2
- export * from "@cosmicdrift/kumiko-types/event-store-errors";
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
  });
@@ -1,7 +1,16 @@
1
- // Test-Assertions, Domain-Test-Fixtures und Vitest-spezifische Helpers.
2
- // Production-Code (dev-server, bin/) darf NICHTS aus diesem Sub-Path importieren
3
- // die Stack-Builder leben in `@cosmicdrift/kumiko-framework/stack`, dieses Modul darf
4
- // vitest-Imports top-level enthalten (siehe expect-error.ts).
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";