@cosmicdrift/kumiko-bundled-features 0.269.2 → 0.273.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.
Files changed (116) hide show
  1. package/package.json +9 -9
  2. package/src/__tests__/account-security-composition.boot.test.ts +25 -7
  3. package/src/__tests__/account-security-composition.integration.test.ts +23 -1
  4. package/src/__tests__/account-security-fixture.ts +3 -0
  5. package/src/agent-tools/__tests__/agent-doc-lint.test.ts +3 -1
  6. package/src/agent-tools/__tests__/feature.test.ts +10 -4
  7. package/src/audit/__tests__/escape-hatch-audit.integration.test.ts +96 -0
  8. package/src/audit/changes.json +12 -0
  9. package/src/audit/escape-hatch-audit-sink.ts +52 -0
  10. package/src/audit/feature.ts +5 -1
  11. package/src/audit/index.ts +1 -0
  12. package/src/auth-email-password/__tests__/query-as-member.integration.test.ts +303 -0
  13. package/src/auth-email-password/handlers/invite-accept.write.ts +8 -1
  14. package/src/auth-mfa/handlers/disable.write.ts +7 -1
  15. package/src/auth-mfa/handlers/enable-confirm.write.ts +7 -1
  16. package/src/auth-mfa/handlers/enable-start.write.ts +7 -1
  17. package/src/auth-mfa/handlers/regenerate-recovery.write.ts +7 -1
  18. package/src/auth-mfa/handlers/status.query.ts +7 -1
  19. package/src/auth-mfa/screens.ts +26 -3
  20. package/src/cap-counter/feature.ts +5 -3
  21. package/src/cap-overview/screens.ts +6 -0
  22. package/src/channel-in-app/handlers/inbox.query.ts +7 -1
  23. package/src/channel-in-app/handlers/mark-all-read.write.ts +7 -1
  24. package/src/channel-in-app/handlers/mark-read.write.ts +7 -1
  25. package/src/channel-in-app/handlers/unread-count.query.ts +7 -1
  26. package/src/compliance-profiles/handlers/for-tenant.query.ts +7 -1
  27. package/src/compliance-profiles/handlers/list-profiles.query.ts +7 -1
  28. package/src/config/__tests__/config.integration.test.ts +1 -1
  29. package/src/config/__tests__/reencrypt-job-tenant-checks.integration.test.ts +2 -1
  30. package/src/config/__tests__/tz-resolution.integration.test.ts +1 -1
  31. package/src/config/changes.json +6 -0
  32. package/src/config/db/queries/__tests__/resolver.test.ts +43 -38
  33. package/src/config/db/queries/resolver.ts +22 -22
  34. package/src/config/handlers/cascade.query.ts +13 -1
  35. package/src/config/handlers/readiness.query.ts +13 -1
  36. package/src/config/handlers/reencrypt.job.ts +21 -2
  37. package/src/config/handlers/reset.write.ts +7 -1
  38. package/src/config/handlers/schema.query.ts +13 -1
  39. package/src/config/handlers/set.write.ts +7 -1
  40. package/src/config/handlers/values.query.ts +13 -1
  41. package/src/data-retention/handlers/policy-for.query.ts +8 -1
  42. package/src/delivery/__tests__/delivery-pii-kms.integration.test.ts +2 -2
  43. package/src/delivery/__tests__/delivery-screens.boot.test.ts +2 -4
  44. package/src/delivery/__tests__/delivery.integration.test.ts +39 -5
  45. package/src/delivery/changes.json +6 -0
  46. package/src/delivery/delivery-service.ts +25 -3
  47. package/src/delivery/handlers/preferences.query.ts +13 -1
  48. package/src/delivery/handlers/set-preference.write.ts +7 -1
  49. package/src/delivery/testing.ts +8 -1
  50. package/src/derivatives-sharp/__tests__/derivatives-sharp.integration.test.ts +1 -1
  51. package/src/folders/__tests__/feature.test.ts +14 -5
  52. package/src/folders/__tests__/folders-parent-visibility.integration.test.ts +6 -1
  53. package/src/folders/__tests__/folders-read-gate.integration.test.ts +6 -1
  54. package/src/folders/__tests__/folders.integration.test.ts +6 -1
  55. package/src/folders/feature.ts +2 -2
  56. package/src/form-draft/constants.ts +7 -1
  57. package/src/jobs/handlers/list.query.ts +3 -1
  58. package/src/ledger/__tests__/ledger.integration.test.ts +57 -0
  59. package/src/managed-pages/screens/branding-screen.ts +3 -0
  60. package/src/managed-pages/screens/page-screens.ts +3 -0
  61. package/src/notes-history/__tests__/notes-read-gate.integration.test.ts +10 -2
  62. package/src/notes-history/__tests__/notes-section.test.tsx +23 -0
  63. package/src/notes-history/feature.ts +2 -2
  64. package/src/notes-history/web/i18n.ts +3 -0
  65. package/src/notes-history/web/notes-section.tsx +61 -51
  66. package/src/personal-access-tokens/handlers/available-scopes.query.ts +7 -1
  67. package/src/personal-access-tokens/handlers/create.write.ts +7 -1
  68. package/src/personal-access-tokens/handlers/list.query.ts +7 -1
  69. package/src/personal-access-tokens/handlers/revoke.write.ts +7 -1
  70. package/src/personal-access-tokens/screens.ts +15 -2
  71. package/src/secrets/__tests__/access-options-precedence.test.ts +11 -4
  72. package/src/secrets/__tests__/access-options.integration.test.ts +5 -1
  73. package/src/secrets/feature.ts +2 -2
  74. package/src/sessions/handlers/mine.query.ts +7 -1
  75. package/src/sessions/handlers/revoke-all-others.write.ts +7 -1
  76. package/src/sessions/handlers/revoke.write.ts +7 -1
  77. package/src/sessions/screens.ts +9 -1
  78. package/src/tags/__tests__/feature.test.ts +25 -8
  79. package/src/tags/__tests__/tags-parent-visibility.integration.test.ts +6 -1
  80. package/src/tags/__tests__/tags-read-gate.integration.test.ts +7 -2
  81. package/src/tags/__tests__/tags.integration.test.ts +6 -1
  82. package/src/tags/constants.ts +1 -1
  83. package/src/tags/feature.ts +2 -2
  84. package/src/tags/screens.ts +3 -0
  85. package/src/tenant/__tests__/members-facet-i18n.test.ts +5 -5
  86. package/src/tenant/__tests__/tenant-timezone-boot.integration.test.ts +2 -2
  87. package/src/tenant/__tests__/tenant.integration.test.ts +5 -1
  88. package/src/tenant/changes.json +6 -0
  89. package/src/tenant/handlers/me.query.ts +13 -1
  90. package/src/tenant/handlers/update-member-roles.write.ts +10 -1
  91. package/src/tenant/screens.ts +3 -0
  92. package/src/tenant/seeding.ts +25 -19
  93. package/src/tier-engine/feature.ts +3 -0
  94. package/src/user/changes.json +6 -0
  95. package/src/user/handlers/me.query.ts +13 -1
  96. package/src/user/handlers/update-roles.ts +8 -5
  97. package/src/user/screens.ts +3 -0
  98. package/src/user-data-rights/__tests__/boot-checks.test.ts +16 -0
  99. package/src/user-data-rights/__tests__/export-job-list-cross-tenant.integration.test.ts +1 -1
  100. package/src/user-data-rights/__tests__/inspector-screens.boot.test.ts +6 -1
  101. package/src/user-data-rights/feature.ts +11 -6
  102. package/src/user-data-rights/handlers/cancel-deletion.write.ts +7 -1
  103. package/src/user-data-rights/handlers/download-attempt-list.query.ts +4 -1
  104. package/src/user-data-rights/handlers/download-by-job.query.ts +7 -1
  105. package/src/user-data-rights/handlers/export-job-detail.query.ts +4 -1
  106. package/src/user-data-rights/handlers/export-job-list.query.ts +4 -1
  107. package/src/user-data-rights/handlers/export-status.query.ts +7 -1
  108. package/src/user-data-rights/handlers/my-audit-log.query.ts +8 -1
  109. package/src/user-data-rights/handlers/request-deletion.write.ts +7 -1
  110. package/src/user-data-rights/handlers/request-export.write.ts +7 -1
  111. package/src/user-data-rights/handlers/restrict-account.write.ts +8 -1
  112. package/src/user-data-rights/lib/is-admin-actor.ts +1 -1
  113. package/src/user-data-rights/screens.ts +3 -0
  114. package/src/user-profile/__tests__/profile-screen.boot.test.ts +6 -1
  115. package/src/user-profile/feature.ts +11 -1
  116. package/src/workflow-runner/__tests__/pending-projection.integration.test.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-bundled-features",
3
- "version": "0.269.2",
3
+ "version": "0.273.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>",
@@ -129,12 +129,12 @@
129
129
  "./workflow-runner": "./src/workflow-runner/index.ts"
130
130
  },
131
131
  "dependencies": {
132
- "@cosmicdrift/kumiko-dispatcher-live": "0.269.2",
133
- "@cosmicdrift/kumiko-framework": "0.269.2",
134
- "@cosmicdrift/kumiko-headless": "0.269.2",
135
- "@cosmicdrift/kumiko-renderer": "0.269.2",
136
- "@cosmicdrift/kumiko-renderer-web": "0.269.2",
137
- "@cosmicdrift/kumiko-types": "0.269.2",
132
+ "@cosmicdrift/kumiko-dispatcher-live": "0.273.0",
133
+ "@cosmicdrift/kumiko-framework": "0.273.0",
134
+ "@cosmicdrift/kumiko-headless": "0.273.0",
135
+ "@cosmicdrift/kumiko-renderer": "0.273.0",
136
+ "@cosmicdrift/kumiko-renderer-web": "0.273.0",
137
+ "@cosmicdrift/kumiko-types": "0.273.0",
138
138
  "@mollie/api-client": "^4.5.0",
139
139
  "@node-rs/argon2": "^2.0.2",
140
140
  "@types/mailparser": "^3.4.6",
@@ -163,7 +163,7 @@
163
163
  ],
164
164
  "devDependencies": {
165
165
  "@testing-library/user-event": "^14.6.1",
166
- "@cosmicdrift/kumiko-locale-de": "0.269.2",
167
- "@cosmicdrift/kumiko-locale-es": "0.269.2"
166
+ "@cosmicdrift/kumiko-locale-de": "0.273.0",
167
+ "@cosmicdrift/kumiko-locale-es": "0.273.0"
168
168
  }
169
169
  }
@@ -40,14 +40,32 @@ describe("account-security composed from bundled screens (fw#2841)", () => {
40
40
 
41
41
  test("the self-service screens are open to every signed-in user", () => {
42
42
  const [, , , , , sessions, mfa] = bootFeatures();
43
- const screens = [
44
- sessions?.screens[SESSION_MINE_SCREEN_ID],
45
- mfa?.screens[MFA_ENABLE_SCREEN_ID],
46
- mfa?.screens[MFA_DISABLE_SCREEN_ID],
47
- mfa?.screens[MFA_REGENERATE_RECOVERY_SCREEN_ID],
43
+ const screensWithReason: ReadonlyArray<{
44
+ readonly screen: { readonly access?: unknown } | undefined;
45
+ readonly reason: string;
46
+ }> = [
47
+ {
48
+ screen: sessions?.screens[SESSION_MINE_SCREEN_ID],
49
+ reason:
50
+ "each signed-in user views and revokes only their own sessions, mirroring the mine/revoke handlers",
51
+ },
52
+ {
53
+ screen: mfa?.screens[MFA_ENABLE_SCREEN_ID],
54
+ reason:
55
+ "each signed-in user may enroll their own account in MFA, mirroring the enable-start handler",
56
+ },
57
+ {
58
+ screen: mfa?.screens[MFA_DISABLE_SCREEN_ID],
59
+ reason: "each signed-in user may turn off their own MFA, mirroring the disable handler",
60
+ },
61
+ {
62
+ screen: mfa?.screens[MFA_REGENERATE_RECOVERY_SCREEN_ID],
63
+ reason:
64
+ "each signed-in user may regenerate their own recovery codes, mirroring the regenerate-recovery handler",
65
+ },
48
66
  ];
49
- for (const screen of screens) {
50
- expect(screen?.access).toEqual({ openToAll: true });
67
+ for (const { screen, reason } of screensWithReason) {
68
+ expect(screen?.access).toEqual({ openToAll: { reason } });
51
69
  }
52
70
  });
53
71
 
@@ -114,7 +114,29 @@ describe("account-security dashboard on a real stack", () => {
114
114
  "auth-mfa:screen:auth-mfa-disable",
115
115
  "sessions:screen:my-sessions",
116
116
  ]);
117
- for (const qn of targets) expect(embeddedScreen(qn).access).toEqual({ openToAll: true });
117
+ const expectedReasons: ReadonlyArray<{ readonly qn: string; readonly reason: string }> = [
118
+ {
119
+ qn: "auth-mfa:screen:auth-mfa-enable",
120
+ reason:
121
+ "each signed-in user may enroll their own account in MFA, mirroring the enable-start handler",
122
+ },
123
+ {
124
+ qn: "auth-mfa:screen:auth-mfa-regenerate-recovery",
125
+ reason:
126
+ "each signed-in user may regenerate their own recovery codes, mirroring the regenerate-recovery handler",
127
+ },
128
+ {
129
+ qn: "auth-mfa:screen:auth-mfa-disable",
130
+ reason: "each signed-in user may turn off their own MFA, mirroring the disable handler",
131
+ },
132
+ {
133
+ qn: "sessions:screen:my-sessions",
134
+ reason:
135
+ "each signed-in user views and revokes only their own sessions, mirroring the mine/revoke handlers",
136
+ },
137
+ ];
138
+ for (const { qn, reason } of expectedReasons)
139
+ expect(embeddedScreen(qn).access).toEqual({ openToAll: { reason } });
118
140
  });
119
141
 
120
142
  test("a plain User gets the flat visibleWhen record and their own sessions over HTTP", async () => {
@@ -24,6 +24,9 @@ export const accountSecurityFeature = defineFeature("account-security", (r) => {
24
24
  r.screen({
25
25
  id: ACCOUNT_SECURITY_SCREEN_ID,
26
26
  type: "dashboard",
27
+ // Test fixture demonstrating composition — a real app would nav this,
28
+ // but that's out of scope here.
29
+ dormant: true,
27
30
  panels: [
28
31
  {
29
32
  kind: "screen",
@@ -7,7 +7,9 @@ import {
7
7
  import { z } from "zod";
8
8
  import { AgentDocGapKinds, findAgentDocGaps, formatAgentDocGap } from "../agent-doc-lint";
9
9
 
10
- const OPEN_ACCESS = { openToAll: true } as const;
10
+ const OPEN_ACCESS = {
11
+ openToAll: { reason: "test handler callable by any signed-in test user" },
12
+ } as const;
11
13
 
12
14
  async function noopWriteHandler() {
13
15
  return { isSuccess: true as const, data: {} };
@@ -9,9 +9,15 @@ async function noopWriteHandler() {
9
9
 
10
10
  function undocumentedHandlersFeature() {
11
11
  return defineFeature("doc-gap-demo", (r) => {
12
- r.writeHandler("do-a", z.object({}), noopWriteHandler, { access: { openToAll: true } });
13
- r.writeHandler("do-b", z.object({}), noopWriteHandler, { access: { openToAll: true } });
14
- r.writeHandler("do-c", z.object({}), noopWriteHandler, { access: { openToAll: true } });
12
+ r.writeHandler("do-a", z.object({}), noopWriteHandler, {
13
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
14
+ });
15
+ r.writeHandler("do-b", z.object({}), noopWriteHandler, {
16
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
17
+ });
18
+ r.writeHandler("do-c", z.object({}), noopWriteHandler, {
19
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
20
+ });
15
21
  });
16
22
  }
17
23
 
@@ -41,7 +47,7 @@ describe("createAgentToolsFeature", () => {
41
47
  test("boot check stays silent for a fully-described registry", () => {
42
48
  const describedFeature = defineFeature("doc-clean-demo", (r) => {
43
49
  r.writeHandler("do-a", z.object({}), noopWriteHandler, {
44
- access: { openToAll: true },
50
+ access: { openToAll: { reason: "test handler callable by any signed-in test user" } },
45
51
  description: "Does A.",
46
52
  });
47
53
  });
@@ -0,0 +1,96 @@
1
+ // fw#2861 — createEscapeHatchAuditSink persists an escape-hatch-use event as
2
+ // audit:event:escape-hatch-used, visible through the existing audit:query:list
3
+ // handler. Real HTTP calls + setupTestStack — never createTestDispatcher.
4
+ // Modelled on audit.integration.test.ts for users/tenant setup + resetEventStore.
5
+
6
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
7
+ import { defineFeature, type SessionUser } from "@cosmicdrift/kumiko-framework/engine";
8
+ import {
9
+ createTestUser,
10
+ resetEventStore,
11
+ setupTestStack,
12
+ type TestStack,
13
+ testTenantId,
14
+ } from "@cosmicdrift/kumiko-framework/stack";
15
+ import { z } from "zod";
16
+ import { createConfigFeature } from "../../config";
17
+ import { createTenantFeature } from "../../tenant";
18
+ import { AuditQueries } from "../constants";
19
+ import { createEscapeHatchAuditSink, ESCAPE_HATCH_USED_EVENT } from "../escape-hatch-audit-sink";
20
+ import { createAuditFeature } from "../feature";
21
+
22
+ const UNSAFE_RAW_REASON = "fw#2861 bundled-features integration test — declared unsafeRaw write";
23
+
24
+ const probeFeature = defineFeature("escape-hatch-audit-sink-probe", (r) => {
25
+ r.writeHandler(
26
+ "unsafe-raw-write",
27
+ z.object({}),
28
+ async (_event, ctx) => {
29
+ ctx.db.unsafeRaw(UNSAFE_RAW_REASON);
30
+ return { isSuccess: true as const, data: { ok: true } };
31
+ },
32
+ { access: { roles: ["User"] }, escapeHatch: { reason: UNSAFE_RAW_REASON } },
33
+ );
34
+ });
35
+
36
+ let stack: TestStack;
37
+
38
+ const tenantId = testTenantId(1);
39
+ const user: SessionUser = createTestUser({ id: 9, tenantId, roles: ["User"] });
40
+ const adminOfSameTenant: SessionUser = createTestUser({ id: 10, tenantId, roles: ["Admin"] });
41
+
42
+ beforeAll(async () => {
43
+ stack = await setupTestStack({
44
+ features: [probeFeature, createConfigFeature(), createTenantFeature(), createAuditFeature()],
45
+ extraContext: ({ db }) => ({ _escapeHatchAuditSink: createEscapeHatchAuditSink({ db }) }),
46
+ });
47
+ });
48
+
49
+ afterAll(async () => {
50
+ await stack.cleanup();
51
+ });
52
+
53
+ beforeEach(async () => {
54
+ await resetEventStore(stack);
55
+ });
56
+
57
+ type AuditRow = {
58
+ id: string;
59
+ type: string;
60
+ createdBy: string;
61
+ payload: Record<string, unknown>;
62
+ };
63
+
64
+ type AuditResponse = { rows: AuditRow[]; nextBefore: string | null };
65
+
66
+ async function pollForEscapeHatchRow(): Promise<AuditRow> {
67
+ const deadline = Date.now() + 2000;
68
+ while (Date.now() < deadline) {
69
+ const res = await stack.http.queryOk<AuditResponse>(
70
+ AuditQueries.list,
71
+ { eventType: ESCAPE_HATCH_USED_EVENT },
72
+ adminOfSameTenant,
73
+ );
74
+ const row = res.rows[0];
75
+ if (row) return row;
76
+ await new Promise((resolve) => setTimeout(resolve, 50));
77
+ }
78
+ throw new Error("escape-hatch-used audit row did not appear within 2s");
79
+ }
80
+
81
+ describe("createEscapeHatchAuditSink — persists audit:event:escape-hatch-used", () => {
82
+ test("an unsafeRaw use is queryable via audit:query:list", async () => {
83
+ await stack.http.writeOk("escape-hatch-audit-sink-probe:write:unsafe-raw-write", {}, user);
84
+
85
+ const row = await pollForEscapeHatchRow();
86
+
87
+ expect(row.type).toBe(ESCAPE_HATCH_USED_EVENT);
88
+ expect(row.createdBy).toBe(user.id);
89
+ expect(row.payload).toEqual({
90
+ handler: "escape-hatch-audit-sink-probe:write:unsafe-raw-write",
91
+ kind: "unsafe-raw",
92
+ reason: UNSAFE_RAW_REASON,
93
+ actor: user.id,
94
+ });
95
+ });
96
+ });
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "version": "0.271.0",
4
+ "type": "improvement",
5
+ "title": "escapeHatchUsedSchema.kind accepts the new unsafe-all-tenants EscapeHatchKind (fw#2913).",
6
+ "detail": "`escapeHatchUsedSchema.kind` (`audit/escape-hatch-audit-sink.ts`) now also accepts `\"unsafe-all-tenants\"`, keeping the `satisfies readonly EscapeHatchKind[]` tuple in sync with the framework's new `EscapeHatchKind` member for a granted `ctx.queryProjection(qn, { unsafeAllTenants: true })` call — see the framework changelog for fw#2913. No consumer action needed; existing sinks already accept any `EscapeHatchKind`."
7
+ },
8
+ {
9
+ "version": "0.271.0",
10
+ "type": "improvement",
11
+ "title": "New escape-hatch-used audit sink: createEscapeHatchAuditSink records security:escape-hatch-used as audit:event:escape-hatch-used (fw#2861).",
12
+ "detail": "New `createEscapeHatchAuditSink({ db })` (`@cosmicdrift/kumiko-bundled-features/audit`) implements the framework's `EscapeHatchAuditSink` — it parses each escape-hatch-use event against the new `escapeHatchUsedSchema` and appends it as an `audit:event:escape-hatch-used` event (`r.defineEvent(\"escape-hatch-used\", ...)`, `piiFields: \"none\"`) on the unbound pool, so the entry survives a rollback of the handler that triggered it. `server-runtime`'s `buildBootExtraContext` wires it as `AppContext._escapeHatchAuditSink` automatically whenever the `audit` feature is mounted; without the feature, escape-hatch uses fall back to a structured warn log. Events show up in the existing `audit:query:list`/`audit-log` screen like any other event."
13
+ },
2
14
  {
3
15
  "version": "0.241.0",
4
16
  "type": "breaking",
@@ -0,0 +1,52 @@
1
+ import type { DbConnection } from "@cosmicdrift/kumiko-framework/db";
2
+ import type { EscapeHatchAuditSink, EscapeHatchKind } from "@cosmicdrift/kumiko-framework/engine";
3
+ import { append } from "@cosmicdrift/kumiko-framework/event-store";
4
+ import { generateId } from "@cosmicdrift/kumiko-framework/utils";
5
+ import { z } from "zod";
6
+
7
+ export const ESCAPE_HATCH_USED_EVENT = "audit:event:escape-hatch-used";
8
+ export const ESCAPE_HATCH_USE_AGGREGATE_TYPE = "escapeHatchUse";
9
+
10
+ // Kept as a tuple so `satisfies` catches a kind added/removed/renamed on
11
+ // EscapeHatchKind without this schema following along.
12
+ const escapeHatchKinds = [
13
+ "unsafe-raw",
14
+ "acknowledge-cross-tenant",
15
+ "global-write",
16
+ "identity-switch",
17
+ "unsafe-all-tenants",
18
+ ] as const satisfies readonly EscapeHatchKind[];
19
+
20
+ export const escapeHatchUsedSchema = z.object({
21
+ handler: z.string(),
22
+ kind: z.enum(escapeHatchKinds),
23
+ reason: z.string(),
24
+ actor: z.string(),
25
+ targetUserId: z.string().optional(),
26
+ targetTenantId: z.string().optional(),
27
+ });
28
+
29
+ export function createEscapeHatchAuditSink(opts: {
30
+ readonly db: DbConnection;
31
+ }): EscapeHatchAuditSink {
32
+ return async (event) => {
33
+ const payload = escapeHatchUsedSchema.parse({
34
+ handler: event.handler,
35
+ kind: event.kind,
36
+ reason: event.reason,
37
+ actor: event.actor,
38
+ ...(event.target && { targetUserId: event.target.id, targetTenantId: event.target.tenantId }),
39
+ });
40
+ // Appends on the unbound pool, never the handler tx — an audit entry must
41
+ // survive a rollback of the handler that used the hatch.
42
+ await append(opts.db, {
43
+ aggregateId: generateId(),
44
+ aggregateType: ESCAPE_HATCH_USE_AGGREGATE_TYPE,
45
+ tenantId: event.tenantId,
46
+ expectedVersion: 0,
47
+ type: ESCAPE_HATCH_USED_EVENT,
48
+ payload,
49
+ metadata: { userId: event.actor },
50
+ });
51
+ };
52
+ }
@@ -5,6 +5,7 @@ import {
5
5
  i18nKey,
6
6
  } from "@cosmicdrift/kumiko-framework/engine";
7
7
  import { AUDIT_LOG_DETAIL_SCREEN_ID, AUDIT_LOG_SCREEN_ID, AuditQueries } from "./constants";
8
+ import { escapeHatchUsedSchema } from "./escape-hatch-audit-sink";
8
9
  import { detailsQuery } from "./handlers/details.query";
9
10
  import { listQuery } from "./handlers/list.query";
10
11
  import { AUDIT_I18N } from "./i18n";
@@ -24,7 +25,7 @@ import { AUDIT_I18N } from "./i18n";
24
25
  export function createAuditFeature(): FeatureDefinition {
25
26
  return defineFeature("audit", (r) => {
26
27
  r.describe(
27
- "Exposes the framework's event store as a paginated, filterable audit log via the `audit:query:list` handler (accessible to `Admin` and `SystemAdmin` roles). No separate table or projection \u2014 the event store is the audit trail by construction: every entity write already records who, when, what entity, and the event payload with PII stripped. Filter by `aggregateType`, `aggregateId`, `eventType`, `userId`, or time range.",
28
+ "Exposes the framework's event store as a paginated, filterable audit log via the `audit:query:list` handler (accessible to `Admin` and `SystemAdmin` roles). No separate table or projection \u2014 the event store is the audit trail by construction: every entity write already records who, when, what entity, and the event payload with PII stripped. Filter by `aggregateType`, `aggregateId`, `eventType`, `userId`, or time range. Also records `audit:event:escape-hatch-used` whenever a handler uses one of the framework's escape hatches (unsafeRaw, acknowledgeCrossTenant, db.global() writes, or a granted identity switch).",
28
29
  );
29
30
  r.uiHints({
30
31
  displayLabel: "Audit Log",
@@ -35,6 +36,9 @@ export function createAuditFeature(): FeatureDefinition {
35
36
  // Screens resolve actor names via tenant:query:members.
36
37
  r.requires("tenant");
37
38
 
39
+ // Registered so ops tools/MSPs can discover the type — escape-hatch-audit-sink.ts appends it via the low-level event-store API.
40
+ r.defineEvent("escape-hatch-used", escapeHatchUsedSchema, { piiFields: "none" });
41
+
38
42
  const queries = {
39
43
  list: r.queryHandler(listQuery),
40
44
  details: r.queryHandler(detailsQuery),
@@ -1,2 +1,3 @@
1
1
  export { AUDIT_FEATURE, AuditQueries } from "./constants";
2
+ export { createEscapeHatchAuditSink, ESCAPE_HATCH_USED_EVENT } from "./escape-hatch-audit-sink";
2
3
  export { createAuditFeature } from "./feature";