@cosmicdrift/kumiko-bundled-features 0.254.0 → 0.255.1
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-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.255.1",
|
|
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>",
|
|
@@ -130,12 +130,12 @@
|
|
|
130
130
|
"./workflow-runner": "./src/workflow-runner/index.ts"
|
|
131
131
|
},
|
|
132
132
|
"dependencies": {
|
|
133
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
134
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
135
|
-
"@cosmicdrift/kumiko-headless": "0.
|
|
136
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
137
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
138
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
133
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.255.1",
|
|
134
|
+
"@cosmicdrift/kumiko-framework": "0.255.1",
|
|
135
|
+
"@cosmicdrift/kumiko-headless": "0.255.1",
|
|
136
|
+
"@cosmicdrift/kumiko-renderer": "0.255.1",
|
|
137
|
+
"@cosmicdrift/kumiko-renderer-web": "0.255.1",
|
|
138
|
+
"@cosmicdrift/kumiko-types": "0.255.1",
|
|
139
139
|
"@mollie/api-client": "^4.5.0",
|
|
140
140
|
"@node-rs/argon2": "^2.0.2",
|
|
141
141
|
"@types/mailparser": "^3.4.6",
|
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"devDependencies": {
|
|
165
165
|
"@testing-library/user-event": "^14.6.1",
|
|
166
166
|
"@types/qrcode": "^1.5.5",
|
|
167
|
-
"@cosmicdrift/kumiko-locale-de": "0.
|
|
168
|
-
"@cosmicdrift/kumiko-locale-es": "0.
|
|
167
|
+
"@cosmicdrift/kumiko-locale-de": "0.255.1",
|
|
168
|
+
"@cosmicdrift/kumiko-locale-es": "0.255.1"
|
|
169
169
|
}
|
|
170
170
|
}
|
|
@@ -855,12 +855,13 @@ describe("scenario 10: PII is encrypted at rest, not just erasable on destroy",
|
|
|
855
855
|
// is written through the encrypted path above. If that changes (billing-
|
|
856
856
|
// foundation goes live before a KMS is configured somewhere), note for
|
|
857
857
|
// whoever builds the backfill: the existing backfillEventPiiEncryption
|
|
858
|
-
// (#799, db/queries/backfill-pii.ts) does NOT cover this feature yet —
|
|
859
|
-
//
|
|
860
|
-
//
|
|
861
|
-
//
|
|
862
|
-
// (
|
|
863
|
-
//
|
|
858
|
+
// (#799, db/queries/backfill-pii.ts) does NOT cover this feature yet — its
|
|
859
|
+
// lifecycle-event matcher expects `<aggregateType>.<verb>` event names,
|
|
860
|
+
// but billing's are `billing-foundation:event:subscription-created`. The
|
|
861
|
+
// custom-event catalog branch itself already resolves user/tenant/self
|
|
862
|
+
// subjects (fw#2801/#2819) — no framework gap to close here, only the
|
|
863
|
+
// lifecycle-event matcher would need widening if this feature ever needs
|
|
864
|
+
// a backfill.
|
|
864
865
|
});
|
|
865
866
|
|
|
866
867
|
// =============================================================================
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Mint/shred parity (fw#2801 security review): a record subject key is
|
|
2
|
+
// minted via subjectKeyForRecord (framework crypto) but shredded via a
|
|
3
|
+
// forget-subject request validated against subjectIdSchema (this feature).
|
|
4
|
+
// If the two ever disagreed on which entity names are valid, a field could
|
|
5
|
+
// encrypt cleanly and then never be reachable by forgetSubject — the
|
|
6
|
+
// "encrypted but not shreddable" state fw#2809 was built to prevent. This
|
|
7
|
+
// pins the invariant down without needing a DB.
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import { subjectKeyForRecord } from "@cosmicdrift/kumiko-framework/crypto";
|
|
11
|
+
import { subjectIdSchema } from "../handlers/forget-subject.write";
|
|
12
|
+
|
|
13
|
+
const UUID_A = "6b2f4a0e-1c9d-4f3a-9d2e-00000000000a";
|
|
14
|
+
|
|
15
|
+
const VALID_ENTITY_NAMES = [
|
|
16
|
+
"mail-account",
|
|
17
|
+
"note-entry",
|
|
18
|
+
"documentExtract",
|
|
19
|
+
"a",
|
|
20
|
+
"A1",
|
|
21
|
+
"tenant_membership",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const INVALID_ENTITY_NAMES = ["v2.digest", "_internal", "3rd", "bad:name", ""];
|
|
25
|
+
|
|
26
|
+
describe("record subject mint/shred parity", () => {
|
|
27
|
+
test("every entity name subjectKeyForRecord accepts, subjectIdSchema also accepts", () => {
|
|
28
|
+
for (const entity of VALID_ENTITY_NAMES) {
|
|
29
|
+
expect(() => subjectKeyForRecord(entity, UUID_A)).not.toThrow();
|
|
30
|
+
const result = subjectIdSchema.safeParse({ kind: "record", entity, id: UUID_A });
|
|
31
|
+
expect(result.success).toBe(true);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("every entity name subjectKeyForRecord rejects, subjectIdSchema also rejects", () => {
|
|
36
|
+
for (const entity of INVALID_ENTITY_NAMES) {
|
|
37
|
+
expect(() => subjectKeyForRecord(entity, UUID_A)).toThrow();
|
|
38
|
+
const result = subjectIdSchema.safeParse({ kind: "record", entity, id: UUID_A });
|
|
39
|
+
expect(result.success).toBe(false);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -4,6 +4,7 @@ import { ROLES } from "@cosmicdrift/kumiko-framework/auth";
|
|
|
4
4
|
import {
|
|
5
5
|
configuredPiiSubjectKms,
|
|
6
6
|
type SubjectId,
|
|
7
|
+
subjectIdSchema,
|
|
7
8
|
subjectIdToKey,
|
|
8
9
|
} from "@cosmicdrift/kumiko-framework/crypto";
|
|
9
10
|
import {
|
|
@@ -18,7 +19,6 @@ import {
|
|
|
18
19
|
type FeatureDefinition,
|
|
19
20
|
type HandlerContext,
|
|
20
21
|
type SessionUser,
|
|
21
|
-
type TenantId,
|
|
22
22
|
type WriteEvent,
|
|
23
23
|
} from "@cosmicdrift/kumiko-framework/engine";
|
|
24
24
|
import {
|
|
@@ -48,18 +48,7 @@ import {
|
|
|
48
48
|
TARGET_TENANT_NOT_ADMIN_TENANT,
|
|
49
49
|
} from "../constants";
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
const RECORD_ENTITY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
|
|
53
|
-
|
|
54
|
-
export const subjectIdSchema = z.discriminatedUnion("kind", [
|
|
55
|
-
z.object({ kind: z.literal("user"), userId: z.uuid() }),
|
|
56
|
-
z.object({ kind: z.literal("tenant"), tenantId: z.uuid() }),
|
|
57
|
-
z.object({
|
|
58
|
-
kind: z.literal("record"),
|
|
59
|
-
entity: z.string().regex(RECORD_ENTITY_PATTERN),
|
|
60
|
-
id: z.uuid(),
|
|
61
|
-
}),
|
|
62
|
-
]);
|
|
51
|
+
export { subjectIdSchema };
|
|
63
52
|
|
|
64
53
|
export const forgetSubjectSchema = z.object({
|
|
65
54
|
subject: subjectIdSchema,
|
|
@@ -298,7 +287,7 @@ export const forgetSubjectWrite = defineWriteHandler({
|
|
|
298
287
|
raw.kind === "user"
|
|
299
288
|
? { kind: "user", userId: raw.userId }
|
|
300
289
|
: raw.kind === "tenant"
|
|
301
|
-
? { kind: "tenant", tenantId: raw.tenantId
|
|
290
|
+
? { kind: "tenant", tenantId: raw.tenantId }
|
|
302
291
|
: { kind: "record", entity: raw.entity, id: raw.id };
|
|
303
292
|
const subjectKey = subjectIdToKey(subject);
|
|
304
293
|
|
package/src/delivery/feature.ts
CHANGED
|
@@ -64,7 +64,7 @@ export function createDeliveryFeature(options?: DeliveryFeatureOptions): Feature
|
|
|
64
64
|
// recipientAddress is the real PII (email address); recipientId stays
|
|
65
65
|
// plaintext — pseudonymous fk, same line as config.userId (#821).
|
|
66
66
|
r.defineEvent("attempt", deliveryAttemptSchema, {
|
|
67
|
-
piiFields: { recipientAddress: {
|
|
67
|
+
piiFields: { recipientAddress: { personal: { of: "recipientId" } } },
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
// Inline projection that materialises every delivery attempt into
|