@cosmicdrift/kumiko-framework 0.362.0 → 0.364.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 (259) hide show
  1. package/AGENTS.md +307 -0
  2. package/agents/examples/access-control.md +355 -0
  3. package/agents/examples/anonymous-access-multitenant.md +443 -0
  4. package/agents/examples/anonymous-access.md +300 -0
  5. package/agents/examples/apex-landing.md +528 -0
  6. package/agents/examples/apex-surface-auth.md +355 -0
  7. package/agents/examples/api-worker-split.md +434 -0
  8. package/agents/examples/auth-claims.md +351 -0
  9. package/agents/examples/auth-foundation-providers.md +155 -0
  10. package/agents/examples/auth-mfa.md +323 -0
  11. package/agents/examples/basic-entity.md +420 -0
  12. package/agents/examples/boot-check.md +122 -0
  13. package/agents/examples/chat-channels.md +254 -0
  14. package/agents/examples/compliance-profiles-demo.md +307 -0
  15. package/agents/examples/cross-feature-events.md +317 -0
  16. package/agents/examples/crypto-shredding-hr.md +314 -0
  17. package/agents/examples/currencies-global.md +609 -0
  18. package/agents/examples/currencies-per-tenant.md +453 -0
  19. package/agents/examples/custom-fields-basic.md +314 -0
  20. package/agents/examples/custom-handlers.md +397 -0
  21. package/agents/examples/delivery-notifications.md +432 -0
  22. package/agents/examples/designer-demo.md +86 -0
  23. package/agents/examples/e2e-generator.md +211 -0
  24. package/agents/examples/embedded-entity-form.md +598 -0
  25. package/agents/examples/embedded.md +649 -0
  26. package/agents/examples/encrypted-tenant-config.md +405 -0
  27. package/agents/examples/error-contract.md +539 -0
  28. package/agents/examples/event-sourcing.md +1287 -0
  29. package/agents/examples/feature-toggles.md +263 -0
  30. package/agents/examples/field-access.md +271 -0
  31. package/agents/examples/files-post-processing.md +433 -0
  32. package/agents/examples/i18n.md +166 -0
  33. package/agents/examples/idempotency.md +179 -0
  34. package/agents/examples/inbound-mail-triage.md +315 -0
  35. package/agents/examples/lane-routing.md +246 -0
  36. package/agents/examples/ledger-banking.md +214 -0
  37. package/agents/examples/ledger-invoicing.md +235 -0
  38. package/agents/examples/legal-pages.md +393 -0
  39. package/agents/examples/lifecycle-hooks.md +368 -0
  40. package/agents/examples/managed-config.md +428 -0
  41. package/agents/examples/managed-pages.md +353 -0
  42. package/agents/examples/notes-history-basic.md +345 -0
  43. package/agents/examples/ownership.md +396 -0
  44. package/agents/examples/pdf-audit-export.md +270 -0
  45. package/agents/examples/pipeline-basics.md +792 -0
  46. package/agents/examples/public-share-token.md +351 -0
  47. package/agents/examples/rate-limiting.md +211 -0
  48. package/agents/examples/realtime-sse.md +187 -0
  49. package/agents/examples/record-detail-layout.md +297 -0
  50. package/agents/examples/reference-data.md +151 -0
  51. package/agents/examples/relations.md +280 -0
  52. package/agents/examples/screens-nav.md +353 -0
  53. package/agents/examples/search.md +233 -0
  54. package/agents/examples/secrets-demo.md +417 -0
  55. package/agents/examples/security-baseline.md +187 -0
  56. package/agents/examples/seed-migration.md +99 -0
  57. package/agents/examples/session-revocation.md +259 -0
  58. package/agents/examples/state-machine.md +920 -0
  59. package/agents/examples/tags-basic.md +401 -0
  60. package/agents/examples/tenant-isolation.md +173 -0
  61. package/agents/examples/tenant-settings.md +275 -0
  62. package/agents/examples/tier-admin.md +347 -0
  63. package/agents/examples/timezones.md +366 -0
  64. package/agents/examples/user-data-rights.md +410 -0
  65. package/agents/examples/user-profile.md +226 -0
  66. package/agents/examples/webhook-step.md +597 -0
  67. package/agents/examples/wizard-form.md +379 -0
  68. package/agents/examples/workflow-engine.md +1731 -0
  69. package/agents/examples/writeform-section.md +300 -0
  70. package/agents/examples.md +74 -0
  71. package/agents/features.md +768 -0
  72. package/agents/widgets.md +181 -0
  73. package/dist/api/auth-middleware.d.ts +2 -0
  74. package/dist/api/auth-middleware.js +14 -5
  75. package/dist/api/auth-routes.js +5 -3
  76. package/dist/api/extra-route.d.ts +3 -0
  77. package/dist/api/index.d.ts +2 -2
  78. package/dist/api/index.js +1 -1
  79. package/dist/api/redis-sse-broker.js +10 -8
  80. package/dist/api/routes.js +48 -19
  81. package/dist/api/server.d.ts +2 -1
  82. package/dist/api/server.js +18 -6
  83. package/dist/api/session-cookies.d.ts +1 -0
  84. package/dist/api/session-cookies.js +20 -5
  85. package/dist/api/sse-route.js +16 -1
  86. package/dist/bun-db/index.d.ts +1 -1
  87. package/dist/bun-db/index.js +1 -1
  88. package/dist/bun-db/query.d.ts +12 -5
  89. package/dist/bun-db/query.js +88 -94
  90. package/dist/crypto/kek-source.js +5 -3
  91. package/dist/crypto/pii-field-encryption.d.ts +4 -1
  92. package/dist/crypto/pii-field-encryption.js +2 -2
  93. package/dist/db/apply-entity-event.js +2 -2
  94. package/dist/db/collect-table-metas.js +11 -7
  95. package/dist/db/cursor.js +2 -1
  96. package/dist/db/dialect.d.ts +1 -0
  97. package/dist/db/dialect.js +28 -13
  98. package/dist/db/eagerload.js +2 -2
  99. package/dist/db/entity-table-from-registry.js +1 -1
  100. package/dist/db/entity-table-meta.js +5 -13
  101. package/dist/db/event-store-executor-context.d.ts +5 -6
  102. package/dist/db/event-store-executor-context.js +29 -27
  103. package/dist/db/event-store-executor-read.js +40 -25
  104. package/dist/db/event-store-executor-write.js +18 -20
  105. package/dist/db/index.d.ts +1 -1
  106. package/dist/db/index.js +1 -1
  107. package/dist/db/lock-guard.d.ts +4 -0
  108. package/dist/db/lock-guard.js +14 -0
  109. package/dist/db/migrate-generator.js +6 -4
  110. package/dist/db/migrate-runner.js +32 -35
  111. package/dist/db/parent-ref-clause.js +2 -2
  112. package/dist/db/queries/backfill-pii.js +11 -9
  113. package/dist/db/queries/es-ops.js +1 -1
  114. package/dist/db/queries/event-consumer.js +17 -17
  115. package/dist/db/queries/event-store-admin.js +6 -6
  116. package/dist/db/queries/event-store-transfer.js +2 -2
  117. package/dist/db/queries/event-store.js +2 -2
  118. package/dist/db/queries/pii-backfill-state.js +2 -2
  119. package/dist/db/queries/raw-sql.js +1 -1
  120. package/dist/db/queries/test-stack.js +1 -1
  121. package/dist/db/reference-data.js +3 -5
  122. package/dist/db/table-builder.js +6 -4
  123. package/dist/db/table-introspection.d.ts +16 -0
  124. package/dist/db/table-introspection.js +76 -0
  125. package/dist/db/tenant-db-runner.d.ts +4 -0
  126. package/dist/db/tenant-db-runner.js +17 -0
  127. package/dist/db/tenant-db.d.ts +5 -2
  128. package/dist/db/tenant-db.js +34 -14
  129. package/dist/derivatives/derivatives-context.js +1 -1
  130. package/dist/derivatives/variant-key.js +2 -1
  131. package/dist/engine/boot-validator/access-declarations.js +2 -2
  132. package/dist/engine/boot-validator/api-ext.js +3 -3
  133. package/dist/engine/boot-validator/boot-check.js +1 -1
  134. package/dist/engine/boot-validator/config-deps.js +13 -13
  135. package/dist/engine/boot-validator/detail-screens.js +1 -1
  136. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  137. package/dist/engine/boot-validator/entity-handler.js +84 -35
  138. package/dist/engine/boot-validator/entity-list-screens.js +3 -3
  139. package/dist/engine/boot-validator/handler-field-references.js +19 -9
  140. package/dist/engine/boot-validator/i18n-keys.js +2 -2
  141. package/dist/engine/boot-validator/nav.js +1 -1
  142. package/dist/engine/boot-validator/pii-retention.js +9 -9
  143. package/dist/engine/boot-validator/projection-list-screens.js +5 -5
  144. package/dist/engine/boot-validator/query-output-columns.js +7 -7
  145. package/dist/engine/boot-validator/screens.d.ts +2 -1
  146. package/dist/engine/boot-validator/screens.js +155 -96
  147. package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
  148. package/dist/engine/changeset-changes.js +3 -3
  149. package/dist/engine/define-handler.d.ts +2 -2
  150. package/dist/engine/define-handler.js +11 -7
  151. package/dist/engine/embedded-derived.js +10 -6
  152. package/dist/engine/extensions/escape-hatch-usage.js +2 -1
  153. package/dist/engine/factories.js +5 -6
  154. package/dist/engine/feature-ast/extractors/events.js +43 -33
  155. package/dist/engine/feature-ast/extractors/handlers.js +2 -0
  156. package/dist/engine/feature-ast/patcher.d.ts +3 -3
  157. package/dist/engine/feature-ast/patcher.js +3 -3
  158. package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -8
  159. package/dist/engine/feature-ast/pattern-change-schema.js +4 -3
  160. package/dist/engine/feature-ast/patterns.d.ts +3 -3
  161. package/dist/engine/feature-ast/render.js +16 -5
  162. package/dist/engine/feature-changelog.d.ts +2 -1
  163. package/dist/engine/feature-changelog.js +8 -6
  164. package/dist/engine/feature-config-events-jobs.d.ts +17 -7
  165. package/dist/engine/feature-config-events-jobs.js +20 -5
  166. package/dist/engine/feature-entity-handlers.d.ts +2 -2
  167. package/dist/engine/feature-entity-handlers.js +12 -3
  168. package/dist/engine/feature-ui-extensions.js +2 -3
  169. package/dist/engine/handler-field-references.d.ts +21 -2
  170. package/dist/engine/handler-field-references.js +55 -17
  171. package/dist/engine/index.d.ts +3 -2
  172. package/dist/engine/index.js +1 -0
  173. package/dist/engine/ownership.js +7 -20
  174. package/dist/engine/pattern-library/mixed-schemas.js +0 -1
  175. package/dist/engine/registry-ingest.d.ts +2 -1
  176. package/dist/engine/registry-ingest.js +85 -2
  177. package/dist/engine/registry-validate.d.ts +1 -0
  178. package/dist/engine/registry-validate.js +38 -8
  179. package/dist/engine/registry.js +2 -1
  180. package/dist/engine/schema-builder.js +3 -4
  181. package/dist/engine/soft-delete-cleanup.js +12 -4
  182. package/dist/engine/steps/call-feature.js +1 -3
  183. package/dist/engine/steps/retry.js +2 -1
  184. package/dist/engine/steps/unsafe-projection-delete.js +2 -2
  185. package/dist/engine/steps/unsafe-projection-upsert.js +4 -15
  186. package/dist/engine/template-variables.d.ts +32 -0
  187. package/dist/engine/template-variables.js +80 -0
  188. package/dist/engine/tier-resolver-extension.js +2 -3
  189. package/dist/engine/types/index.d.ts +2 -2
  190. package/dist/engine/validate-projection-allowlist.js +12 -13
  191. package/dist/engine/where-rule-lint.js +8 -7
  192. package/dist/env/_zod-introspect.d.ts +1 -0
  193. package/dist/errors/classes.js +1 -0
  194. package/dist/errors/kumiko-error.d.ts +2 -0
  195. package/dist/errors/kumiko-error.js +2 -0
  196. package/dist/errors/reasons.d.ts +1 -0
  197. package/dist/errors/reasons.js +2 -0
  198. package/dist/errors/write-error-info.js +2 -0
  199. package/dist/event-store/tenant-db-append.js +2 -2
  200. package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
  201. package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
  202. package/dist/files/bind-pending-file-refs.d.ts +2 -0
  203. package/dist/files/bind-pending-file-refs.js +3 -3
  204. package/dist/files/bind-referenced-files.write.d.ts +1 -1
  205. package/dist/files/bind-referenced-files.write.js +22 -13
  206. package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
  207. package/dist/files/create-file-ref-from-bytes.js +67 -4
  208. package/dist/files/create-file-ref.d.ts +22 -0
  209. package/dist/files/create-file-ref.js +53 -25
  210. package/dist/files/file-handle.d.ts +1 -1
  211. package/dist/files/file-handle.js +8 -0
  212. package/dist/files/file-routes.js +2 -2
  213. package/dist/files/index.d.ts +2 -2
  214. package/dist/files/index.js +1 -1
  215. package/dist/files/load-save-file-refs.d.ts +10 -0
  216. package/dist/files/load-save-file-refs.js +47 -0
  217. package/dist/files/local-provider.js +2 -1
  218. package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
  219. package/dist/files/purge-expired-file-ref-bytes.js +45 -19
  220. package/dist/i18n/index.d.ts +1 -1
  221. package/dist/i18n/index.js +1 -1
  222. package/dist/i18n/mail-registry.d.ts +2 -0
  223. package/dist/i18n/mail-registry.js +7 -0
  224. package/dist/jobs/index.d.ts +1 -1
  225. package/dist/jobs/job-runner.d.ts +4 -1
  226. package/dist/jobs/job-runner.js +13 -9
  227. package/dist/observability/escape-hatch-report.d.ts +1 -1
  228. package/dist/observability/escape-hatch-report.js +17 -5
  229. package/dist/pipeline/cascade-handler.js +5 -2
  230. package/dist/pipeline/dispatch-batch.js +79 -31
  231. package/dist/pipeline/dispatch-query.js +2 -3
  232. package/dist/pipeline/dispatch-shared.d.ts +7 -2
  233. package/dist/pipeline/dispatch-shared.js +142 -34
  234. package/dist/pipeline/dispatch-write.js +34 -22
  235. package/dist/pipeline/event-dispatcher-delivery.js +2 -3
  236. package/dist/pipeline/index.d.ts +2 -0
  237. package/dist/pipeline/index.js +2 -0
  238. package/dist/pipeline/member-read-only-transaction.js +1 -1
  239. package/dist/pipeline/notify-scope.d.ts +13 -0
  240. package/dist/pipeline/notify-scope.js +32 -0
  241. package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
  242. package/dist/pipeline/pre-transaction-reservations.js +28 -9
  243. package/dist/pipeline/system-hooks.js +2 -1
  244. package/dist/pipeline/system-identity-switch.js +2 -2
  245. package/dist/rate-limit/bucket.d.ts +3 -3
  246. package/dist/rate-limit/bucket.js +14 -4
  247. package/dist/redis/cache-sync-bus.js +9 -3
  248. package/dist/search/meilisearch-adapter.js +39 -13
  249. package/dist/secrets/stored-envelope.js +7 -7
  250. package/dist/stack/push-entity-projection-tables.js +2 -3
  251. package/dist/stack/table-helpers.js +10 -26
  252. package/dist/stack/test-stack.js +2 -5
  253. package/dist/testing/multipart-helper.d.ts +0 -6
  254. package/dist/testing/multipart-helper.js +5 -5
  255. package/dist/testing/without-ambient-temporal.js +4 -4
  256. package/dist/utils/is-plain-object.d.ts +1 -2
  257. package/dist/utils/is-plain-object.js +1 -4
  258. package/package.json +7 -5
  259. package/src/changes.json +251 -8
@@ -0,0 +1,300 @@
1
+ <!-- Generated by `bun run gen:agent-docs` from samples/recipes/anonymous-access. Do not edit. -->
2
+
3
+ # anonymous-access
4
+
5
+ Public endpoints (anonymous role) for shop / CMS-style apps. Tenant resolution via header, cookie, or default.
6
+
7
+ ## README
8
+
9
+ # Anonymous Access
10
+
11
+ Public endpoints without a logged-in user.
12
+
13
+ ## What it shows
14
+
15
+ - `access: "anonymous"` on handlers that must stay public
16
+ - Rate limits that still bind anonymous callers (`per: "ip+handler"`)
17
+
18
+ ## Source
19
+
20
+ Feature entry point: `src/feature.ts`.
21
+
22
+ ## Tests
23
+
24
+ Needs a running Postgres and `TEST_DATABASE_URL` set (e.g. `postgres://postgres:postgres@127.0.0.1:5432/postgres`, see `demo/.env.example`).
25
+
26
+ ```bash
27
+ cd samples/recipes/anonymous-access
28
+ bun test
29
+ ```
30
+
31
+ ## Source files
32
+
33
+ ### src/__tests__/feature.integration.test.ts
34
+
35
+ ```ts
36
+ // Single-tenant shop wiring: defaultTenantId is the simplest setup. No
37
+ // resolver, no header, no cookie — anonymous requests just work. Suitable
38
+ // for WordPress-clones, SaaS landing pages, and standalone shops.
39
+ //
40
+ // For multi-tenant deployments, swap defaultTenantId for a tenantResolver
41
+ // that parses the host (acme.shop.com → tenantId) plus a tenantExists
42
+ // callback that confirms the tenant is real (against DB or cache).
43
+
44
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
45
+ import { asRawClient, selectMany } from "@cosmicdrift/kumiko-framework/bun-db";
46
+ import { ANONYMOUS_USER_ID, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
47
+ import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
48
+ import {
49
+ setupTestStack,
50
+ type TestStack,
51
+ TestUsers,
52
+ unsafeCreateEntityTable,
53
+ } from "@cosmicdrift/kumiko-framework/stack";
54
+ import {
55
+ anonymousAccessFeature,
56
+ guestOrderEntity,
57
+ guestOrderTable,
58
+ productEntity,
59
+ productTable,
60
+ } from "../feature";
61
+
62
+ const TENANT_ID = "00000000-0000-4000-8000-000000000001" as TenantId;
63
+
64
+ let stack: TestStack;
65
+
66
+ beforeAll(async () => {
67
+ stack = await setupTestStack({
68
+ features: [anonymousAccessFeature],
69
+ // The single-tenant shortcut: every anonymous request lands on this
70
+ // tenant. No header, cookie, or resolver needed.
71
+ anonymousAccess: { defaultTenantId: TENANT_ID },
72
+ });
73
+ await unsafeCreateEntityTable(stack.db, productEntity);
74
+ await unsafeCreateEntityTable(stack.db, guestOrderEntity);
75
+ await createEventsTable(stack.db);
76
+ });
77
+
78
+ afterAll(async () => {
79
+ await stack?.cleanup();
80
+ });
81
+
82
+ beforeEach(async () => {
83
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${productTable.tableName}"`);
84
+ await asRawClient(stack.db).unsafe(`DELETE FROM "${guestOrderTable.tableName}"`);
85
+ });
86
+
87
+ describe("public read: anonymous + authenticated share the listing", () => {
88
+ test("anonymous caller sees products without any auth headers", async () => {
89
+ await stack.http.writeOk(
90
+ "shop:write:product:create",
91
+ { name: "Espresso Beans", priceCents: "1500" },
92
+ TestUsers.admin,
93
+ );
94
+
95
+ const res = await stack.http.raw("POST", "/api/query", {
96
+ type: "shop:query:product:list",
97
+ payload: {},
98
+ });
99
+
100
+ expect(res.status).toBe(200);
101
+ const body = (await res.json()) as { data: Array<{ name: string }> };
102
+ expect(body.data).toHaveLength(1);
103
+ expect(body.data[0]?.name).toBe("Espresso Beans");
104
+ });
105
+
106
+ test("authenticated user reaches the same handler via JWT", async () => {
107
+ await stack.http.writeOk(
108
+ "shop:write:product:create",
109
+ { name: "Espresso Beans", priceCents: "1500" },
110
+ TestUsers.admin,
111
+ );
112
+
113
+ const res = await stack.http.query("shop:query:product:list", {}, TestUsers.user);
114
+ expect(res.status).toBe(200);
115
+ });
116
+ });
117
+
118
+ describe("guest checkout: anonymous write lands with placedBy=anonymous", () => {
119
+ test("anonymous order is stored and tagged with the anonymous user-id", async () => {
120
+ await stack.http.writeOk(
121
+ "shop:write:product:create",
122
+ { name: "Espresso Beans", priceCents: "1500" },
123
+ TestUsers.admin,
124
+ );
125
+
126
+ const res = await stack.http.raw("POST", "/api/write", {
127
+ type: "shop:write:guest-order:place",
128
+ payload: {
129
+ productId: "00000000-0000-4000-8000-000000000099",
130
+ email: "guest@example.com",
131
+ },
132
+ });
133
+
134
+ expect(res.status).toBe(200);
135
+
136
+ const orders = await selectMany(stack.db, guestOrderTable);
137
+ expect(orders).toHaveLength(1);
138
+ expect(orders[0]?.placedBy).toBe(ANONYMOUS_USER_ID);
139
+ expect(orders[0]?.email).toBe("guest@example.com");
140
+ });
141
+ });
142
+
143
+ describe("regression guards", () => {
144
+ test("openToAll handler rejects anonymous (was the silent-public bug)", async () => {
145
+ // Enabling anonymousAccess must NOT turn every existing openToAll endpoint
146
+ // public. hasAccess rejects anonymous on openToAll, and the dispatcher
147
+ // answers 401: the caller has to sign in, not ask for another role.
148
+ const res = await stack.http.raw("POST", "/api/query", {
149
+ type: "shop:query:product:authenticated-only",
150
+ payload: {},
151
+ });
152
+ expect(res.status).toBe(401);
153
+ });
154
+
155
+ test("admin-only handler rejects anonymous", async () => {
156
+ const res = await stack.http.raw("POST", "/api/write", {
157
+ type: "shop:write:product:create",
158
+ payload: { name: "Tea Set", priceCents: "1000" },
159
+ });
160
+ expect(res.status).toBe(401);
161
+ });
162
+ });
163
+ ```
164
+
165
+ ### src/feature.ts
166
+
167
+ ```ts
168
+ // Anonymous-Access Sample
169
+ //
170
+ // Most Kumiko apps put every endpoint behind a JWT — fine for CRMs, ERPs,
171
+ // and back-office tools where every caller is a known user. Shops, CMSs,
172
+ // marketing-microsites have a different shape: 50%+ of traffic is anonymous
173
+ // (visitors, scrapers, search engines) and the public surface is a feature,
174
+ // not a hack.
175
+ //
176
+ // This recipe wires that public surface in two ways:
177
+ //
178
+ // 1. `roles: ["anonymous"]` on a handler → unauthenticated callers reach it.
179
+ // The framework synthesises a SessionUser with id="anonymous" + that
180
+ // single role. `access.anonymous` is the typed shorthand for the role.
181
+ //
182
+ // 2. Combined with authenticated roles → the same handler serves both
183
+ // audiences, with the handler-body branching on `event.user.id` if
184
+ // personalisation is needed.
185
+ //
186
+ // `openToAll` is intentionally NOT public — it still means "any
187
+ // authenticated user", and the framework rejects anonymous callers there.
188
+ // Without that guard, enabling anonymousAccess on the server would silently
189
+ // expose every existing openToAll handler. The `product:authenticated-only`
190
+ // query below proves this.
191
+
192
+ import { buildEntityTable, createEventStoreExecutor } from "@cosmicdrift/kumiko-framework/db";
193
+ import {
194
+ access,
195
+ createEntity,
196
+ createTextField,
197
+ defineFeature,
198
+ } from "@cosmicdrift/kumiko-framework/engine";
199
+ import { z } from "zod";
200
+
201
+ export const productEntity = createEntity({
202
+ table: "shop_products",
203
+ fields: {
204
+ name: createTextField({ personal: false, reason: "is_business_data", required: true }),
205
+ priceCents: createTextField({ personal: false, reason: "is_business_data", default: "0" }),
206
+ },
207
+ });
208
+
209
+ export const guestOrderEntity = createEntity({
210
+ table: "shop_guest_orders",
211
+ fields: {
212
+ productId: createTextField({ personal: false, reason: "is_system_identifier", required: true }),
213
+ email: createTextField({ personal: { of: "id" }, find: "none", required: true }),
214
+ placedBy: createTextField({ personal: "ref", default: "" }),
215
+ },
216
+ });
217
+
218
+ export const productTable = buildEntityTable("product", productEntity);
219
+ export const guestOrderTable = buildEntityTable("guest-order", guestOrderEntity);
220
+
221
+ export const anonymousAccessFeature = defineFeature("shop", (r) => {
222
+ r.entity("product", productEntity);
223
+ r.entity("guest-order", guestOrderEntity);
224
+
225
+ // Public listing: anonymous + authenticated customers see it. Combining
226
+ // the roles in one handler is preferred over duplicating the read path —
227
+ // the handler body can branch on `event.user.id === "anonymous"` if it
228
+ // needs to personalise (e.g. show a sign-in CTA, hide a wishlist button).
229
+ r.queryHandler(
230
+ "product:list",
231
+ z.object({}),
232
+ async (_event, ctx) => ctx.db.selectMany(productTable),
233
+ {
234
+ access: { roles: [...access.anonymous, "User", "Admin"] },
235
+ rateLimit: { per: "ip+handler", limit: 60, windowSeconds: 60 },
236
+ },
237
+ );
238
+
239
+ // Authenticated-only listing — same data, different access rule. The
240
+ // openToAll declaration here is the regression-guard: even with
241
+ // anonymousAccess enabled on the server, this endpoint stays gated to
242
+ // logged-in users.
243
+ r.queryHandler(
244
+ "product:authenticated-only",
245
+ z.object({}),
246
+ async (_event, ctx) => ctx.db.selectMany(productTable),
247
+ {
248
+ access: {
249
+ openToAll: {
250
+ reason:
251
+ "any signed-in (non-anonymous) user may list products here; this handler is " +
252
+ "the regression-guard proving anonymousAccess never silently exposes an authenticated-only endpoint",
253
+ },
254
+ },
255
+ },
256
+ );
257
+
258
+ // Anonymous + authenticated guest checkout. Stores the synthesised user-id
259
+ // ("anonymous") in `placedBy` so audit-trail / support queries can tell
260
+ // an authenticated order from a walk-in. Rate-limited per IP because every
261
+ // anonymous caller shares user.id="anonymous" — a per-user rate-limit
262
+ // would be a single global tap any caller could drain (the boot-validator
263
+ // refuses the misconfiguration).
264
+ const guestOrderExecutor = createEventStoreExecutor(guestOrderTable, guestOrderEntity, {
265
+ entityName: "guest-order",
266
+ });
267
+ r.writeHandler(
268
+ "guest-order:place",
269
+ z.object({
270
+ productId: z.uuid(),
271
+ email: z.string().email(),
272
+ }),
273
+ async (event, ctx) =>
274
+ guestOrderExecutor.create({ ...event.payload, placedBy: event.user.id }, event.user, ctx.db),
275
+ {
276
+ // `email` is recordOwned PII (personal: { of: "id" }) written by an
277
+ // anonymous root handler — the runtime personal-data gate
278
+ // (write-origin.ts) requires this explicit opt-in, the same way the
279
+ // boot validator would reject an openToAll declaration missing it.
280
+ access: { roles: [...access.anonymous, "User"], personalData: "public-intake" },
281
+ rateLimit: { per: "ip+handler", limit: 30, windowSeconds: 60 },
282
+ },
283
+ );
284
+
285
+ // Admin-only product CRUD — proves role-gated handlers still reject
286
+ // anonymous callers. Same shape as any other handler in the framework:
287
+ // anonymous is just a regular role, not a special case.
288
+ r.writeHandler(
289
+ "product:create",
290
+ z.object({ name: z.string().min(1), priceCents: z.string() }),
291
+ async (event, ctx) => {
292
+ const exec = createEventStoreExecutor(productTable, productEntity, {
293
+ entityName: "product",
294
+ });
295
+ return exec.create(event.payload, event.user, ctx.db);
296
+ },
297
+ { access: { roles: ["Admin"] } },
298
+ );
299
+ });
300
+ ```