@cosmicdrift/kumiko-framework 0.387.0 → 0.388.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 (240) hide show
  1. package/AGENTS.md +6 -1
  2. package/agents/examples/anonymous-access.md +1 -1
  3. package/agents/examples/apex-landing.md +204 -0
  4. package/agents/examples/api-worker-split.md +64 -0
  5. package/agents/examples/app-config.md +14 -0
  6. package/agents/examples/auth-mfa.md +115 -0
  7. package/agents/examples/env-composition.md +28 -1
  8. package/agents/examples/legal-pages.md +1 -1
  9. package/agents/examples/prod-boot-options.md +16 -0
  10. package/agents/examples/rate-limiting.md +48 -0
  11. package/agents/examples/record-detail-layout.md +692 -0
  12. package/agents/examples/seed-migration.md +212 -0
  13. package/agents/examples/wizard-form.md +941 -0
  14. package/agents/examples/writeform-section.md +548 -0
  15. package/agents/features.md +19 -2
  16. package/agents/widgets.md +1 -0
  17. package/dist/api/client-error-route.js +8 -2
  18. package/dist/api/index.d.ts +3 -1
  19. package/dist/api/index.js +3 -1
  20. package/dist/api/request-context.d.ts +1 -1
  21. package/dist/api/request-context.js +1 -1
  22. package/dist/api/server-boot-checks.d.ts +10 -0
  23. package/dist/api/server-boot-checks.js +157 -0
  24. package/dist/api/server-event-dispatcher.d.ts +26 -0
  25. package/dist/api/server-event-dispatcher.js +208 -0
  26. package/dist/api/server-extra-routes.d.ts +11 -0
  27. package/dist/api/server-extra-routes.js +203 -0
  28. package/dist/api/server-middleware.d.ts +20 -0
  29. package/dist/api/server-middleware.js +211 -0
  30. package/dist/api/server-routes.d.ts +19 -0
  31. package/dist/api/server-routes.js +293 -0
  32. package/dist/api/server-websocket-routes.d.ts +4 -0
  33. package/dist/api/server-websocket-routes.js +108 -0
  34. package/dist/api/server-wiring.d.ts +32 -0
  35. package/dist/api/server-wiring.js +236 -0
  36. package/dist/api/server.d.ts +10 -15
  37. package/dist/api/server.js +48 -1251
  38. package/dist/api/session-cookies.js +4 -4
  39. package/dist/api/sse-route.js +16 -12
  40. package/dist/crypto/owner-transition-rekey.d.ts +5 -1
  41. package/dist/crypto/owner-transition-rekey.js +1 -1
  42. package/dist/crypto/pii-field-encryption.d.ts +5 -1
  43. package/dist/crypto/pii-field-encryption.js +3 -1
  44. package/dist/db/aggregate-sql.d.ts +21 -0
  45. package/dist/db/aggregate-sql.js +155 -0
  46. package/dist/db/closed-connection.d.ts +9 -0
  47. package/dist/db/closed-connection.js +95 -0
  48. package/dist/db/dialect.js +1 -1
  49. package/dist/db/distinct-on.d.ts +1 -1
  50. package/dist/db/driver-row-coercion.d.ts +2 -0
  51. package/dist/db/driver-row-coercion.js +186 -0
  52. package/dist/db/driver-value-preparation.d.ts +16 -0
  53. package/dist/db/driver-value-preparation.js +99 -0
  54. package/dist/db/entity-field-encryption.d.ts +1 -0
  55. package/dist/db/entity-field-encryption.js +1 -1
  56. package/dist/db/entity-table-meta.js +103 -110
  57. package/dist/db/event-store-executor-context.d.ts +2 -1
  58. package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
  59. package/dist/db/event-store-executor-read-aggregate.js +112 -0
  60. package/dist/db/event-store-executor-read-filter.d.ts +31 -0
  61. package/dist/db/event-store-executor-read-filter.js +203 -0
  62. package/dist/db/event-store-executor-read-paging.d.ts +14 -0
  63. package/dist/db/event-store-executor-read-paging.js +99 -0
  64. package/dist/db/event-store-executor-read-rows.d.ts +6 -0
  65. package/dist/db/event-store-executor-read-rows.js +32 -0
  66. package/dist/db/event-store-executor-read-search.d.ts +28 -0
  67. package/dist/db/event-store-executor-read-search.js +221 -0
  68. package/dist/db/event-store-executor-read-sort.d.ts +41 -0
  69. package/dist/db/event-store-executor-read-sort.js +130 -0
  70. package/dist/db/event-store-executor-read-where.d.ts +24 -0
  71. package/dist/db/event-store-executor-read-where.js +81 -0
  72. package/dist/db/event-store-executor-read.d.ts +2 -9
  73. package/dist/db/event-store-executor-read.js +48 -701
  74. package/dist/db/event-store-executor-write-steps.d.ts +20 -0
  75. package/dist/db/event-store-executor-write-steps.js +100 -0
  76. package/dist/db/event-store-executor-write.js +395 -499
  77. package/dist/db/index.d.ts +6 -2
  78. package/dist/db/index.js +5 -1
  79. package/dist/db/list-aggregate-labels.d.ts +14 -0
  80. package/dist/db/list-aggregate-labels.js +26 -0
  81. package/dist/db/migrate-generator.js +71 -86
  82. package/dist/db/migrate-runner.js +4 -1
  83. package/dist/db/query.d.ts +2 -15
  84. package/dist/db/query.js +8 -761
  85. package/dist/db/reference-data.js +6 -1
  86. package/dist/db/table-builder.js +113 -166
  87. package/dist/db/table-info.d.ts +10 -0
  88. package/dist/db/table-info.js +45 -0
  89. package/dist/db/temporal-value-guards.d.ts +6 -0
  90. package/dist/db/temporal-value-guards.js +15 -0
  91. package/dist/db/tenant-db-runner.d.ts +1 -0
  92. package/dist/db/tenant-db-runner.js +3 -0
  93. package/dist/db/tenant-db.js +39 -9
  94. package/dist/db/where-clause.d.ts +6 -0
  95. package/dist/db/where-clause.js +236 -0
  96. package/dist/derivatives/delete-stored-file.js +2 -1
  97. package/dist/engine/boot-validator/boot-check.js +1 -1
  98. package/dist/engine/boot-validator/detail-screens.js +2 -2
  99. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  100. package/dist/engine/boot-validator/entity-handler.js +17 -0
  101. package/dist/engine/boot-validator/form-derivations.js +35 -1
  102. package/dist/engine/boot-validator/i18n-keys.js +5 -3
  103. package/dist/engine/boot-validator/index.js +2 -0
  104. package/dist/engine/boot-validator/pii-retention.js +233 -155
  105. package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
  106. package/dist/engine/boot-validator/screens-actions.js +8 -1
  107. package/dist/engine/boot-validator/screens-dashboard.js +2 -5
  108. package/dist/engine/boot-validator/screens-forms.js +2 -6
  109. package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
  110. package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
  111. package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
  112. package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
  113. package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
  114. package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
  115. package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
  116. package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
  117. package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
  118. package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
  119. package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
  120. package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
  121. package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
  122. package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
  123. package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
  124. package/dist/engine/boot-validator/screens-lists.js +12 -9
  125. package/dist/engine/boot-validator/screens.d.ts +0 -2
  126. package/dist/engine/boot-validator/screens.js +99 -860
  127. package/dist/engine/build-app-schema.js +56 -96
  128. package/dist/engine/define-feature.js +9 -2
  129. package/dist/engine/define-handler.js +1 -0
  130. package/dist/engine/entity-handlers.d.ts +13 -0
  131. package/dist/engine/entity-handlers.js +10 -21
  132. package/dist/engine/entity-list-runtime-options.d.ts +8 -0
  133. package/dist/engine/entity-list-runtime-options.js +18 -0
  134. package/dist/engine/feature-ast/extractors/handlers.js +1 -0
  135. package/dist/engine/feature-ast/extractors/round2.js +25 -6
  136. package/dist/engine/feature-ast/patch.js +4 -182
  137. package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
  138. package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
  139. package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
  140. package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
  141. package/dist/engine/feature-ast/patterns.d.ts +9 -0
  142. package/dist/engine/feature-ast/patterns.js +43 -51
  143. package/dist/engine/feature-ast/render.js +48 -87
  144. package/dist/engine/feature-entity-handlers.js +3 -0
  145. package/dist/engine/form-derivations.js +18 -7
  146. package/dist/engine/handler-field-references.js +1 -1
  147. package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
  148. package/dist/engine/project-app-schema-for-roles.js +6 -4
  149. package/dist/engine/registry-ingest.js +1 -1
  150. package/dist/engine/schema-builder.js +228 -212
  151. package/dist/engine/screen-helpers.js +1 -1
  152. package/dist/engine/screen-kind.d.ts +7 -0
  153. package/dist/engine/screen-kind.js +1 -0
  154. package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
  155. package/dist/engine/validate-projection-allowlist.js +2 -1
  156. package/dist/entrypoint/index.d.ts +4 -1
  157. package/dist/entrypoint/index.js +7 -1
  158. package/dist/errors/docs-url.d.ts +1 -1
  159. package/dist/errors/docs-url.js +22 -2
  160. package/dist/es-ops/context.js +1 -1
  161. package/dist/event-store/admin-api.js +1 -1
  162. package/dist/event-store/event-store.js +10 -4
  163. package/dist/files/bind-pending-file-refs.js +3 -3
  164. package/dist/files/create-file-ref-from-bytes.js +4 -1
  165. package/dist/files/file-handle.js +2 -3
  166. package/dist/files/file-routes.js +11 -8
  167. package/dist/files/resolve-content-type.js +43 -0
  168. package/dist/files/upload-ledger.js +7 -0
  169. package/dist/i18n/required-surface-keys.js +182 -206
  170. package/dist/internal/stack-support.d.ts +2 -1
  171. package/dist/internal/stack-support.js +1 -1
  172. package/dist/jobs/job-runner-context.d.ts +32 -0
  173. package/dist/jobs/job-runner-context.js +305 -0
  174. package/dist/jobs/job-runner-execution.d.ts +7 -0
  175. package/dist/jobs/job-runner-execution.js +160 -0
  176. package/dist/jobs/job-runner-gates.d.ts +19 -0
  177. package/dist/jobs/job-runner-gates.js +198 -0
  178. package/dist/jobs/job-runner-helpers.d.ts +17 -3
  179. package/dist/jobs/job-runner-helpers.js +61 -2
  180. package/dist/jobs/job-runner-scope.d.ts +44 -0
  181. package/dist/jobs/job-runner-scope.js +102 -0
  182. package/dist/jobs/job-runner-types.d.ts +12 -0
  183. package/dist/jobs/job-runner.d.ts +0 -1
  184. package/dist/jobs/job-runner.js +47 -657
  185. package/dist/observability/escape-hatch-report.d.ts +3 -0
  186. package/dist/observability/escape-hatch-report.js +12 -1
  187. package/dist/pipeline/cascade-handler.js +165 -138
  188. package/dist/pipeline/dispatch-query.js +10 -3
  189. package/dist/pipeline/dispatch-shared.d.ts +13 -3
  190. package/dist/pipeline/dispatch-shared.js +58 -710
  191. package/dist/pipeline/dispatch-write.js +296 -189
  192. package/dist/pipeline/event-dispatcher-admin.js +2 -1
  193. package/dist/pipeline/event-dispatcher-delivery.js +3 -5
  194. package/dist/pipeline/handler-context-auth.d.ts +9 -0
  195. package/dist/pipeline/handler-context-auth.js +54 -0
  196. package/dist/pipeline/handler-context-config.d.ts +14 -0
  197. package/dist/pipeline/handler-context-config.js +113 -0
  198. package/dist/pipeline/handler-context-db.d.ts +24 -0
  199. package/dist/pipeline/handler-context-db.js +149 -0
  200. package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
  201. package/dist/pipeline/handler-context-dispatch.js +89 -0
  202. package/dist/pipeline/handler-context-events.d.ts +43 -0
  203. package/dist/pipeline/handler-context-events.js +237 -0
  204. package/dist/pipeline/handler-context-notify.d.ts +12 -0
  205. package/dist/pipeline/handler-context-notify.js +55 -0
  206. package/dist/pipeline/handler-context-observability.d.ts +7 -0
  207. package/dist/pipeline/handler-context-observability.js +21 -0
  208. package/dist/pipeline/handler-context-scope.d.ts +48 -0
  209. package/dist/pipeline/handler-context-scope.js +88 -0
  210. package/dist/pipeline/notify-scope.js +12 -1
  211. package/dist/pipeline/pre-handler-gates.js +20 -4
  212. package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
  213. package/dist/pipeline/pre-transaction-reservations.js +9 -3
  214. package/dist/pipeline/projection-rebuild.js +2 -1
  215. package/dist/schema-cli/apply-command.d.ts +2 -0
  216. package/dist/schema-cli/apply-command.js +97 -0
  217. package/dist/schema-cli/baseline-command.d.ts +2 -0
  218. package/dist/schema-cli/baseline-command.js +33 -0
  219. package/dist/schema-cli/generate-command.d.ts +2 -0
  220. package/dist/schema-cli/generate-command.js +90 -0
  221. package/dist/schema-cli/shared.d.ts +39 -0
  222. package/dist/schema-cli/shared.js +95 -0
  223. package/dist/schema-cli/status-command.d.ts +2 -0
  224. package/dist/schema-cli/status-command.js +73 -0
  225. package/dist/schema-cli/validate-boot-input.d.ts +5 -0
  226. package/dist/schema-cli/validate-boot-input.js +130 -0
  227. package/dist/schema-cli/validate-command.d.ts +2 -0
  228. package/dist/schema-cli/validate-command.js +130 -0
  229. package/dist/schema-cli.d.ts +2 -18
  230. package/dist/schema-cli.js +47 -604
  231. package/dist/search/meilisearch-adapter.js +2 -1
  232. package/dist/upgrade-cli.js +149 -121
  233. package/package.json +4 -4
  234. package/src/changes.json +310 -0
  235. package/src/scripts/codemod/feature-layout.ts +10 -2
  236. package/src/scripts/codemod/object-form.ts +41 -4
  237. package/src/scripts/codemod/package-merge.ts +58 -8
  238. package/src/scripts/codemod/pii-personal-migration.ts +175 -145
  239. package/src/scripts/codemod/testing-injection.ts +122 -42
  240. package/src/scripts/codemod/testing-move.ts +29 -7
@@ -0,0 +1,54 @@
1
+ import { INTERACTIVE_SIGN_IN_POLICY, resolveActiveMembershipFn } from "./active-membership.js";
2
+ import { resolveAuthClaimsFn } from "./dispatch-shared.js";
3
+ import { createMemberReaderFn } from "./member-reader.js";
4
+ import { createGatedMemberReader, systemIdentitySwitchDenied } from "./system-identity-switch.js";
5
+ export function buildAuthFacet(scope) {
6
+ const { ctx, type, user, tx, hasIdentitySwitchGrant, identitySwitchAudit } = scope;
7
+ const { effectiveFeatures } = ctx;
8
+ // Lazy — creates the reader (which fails closed on SYSTEM_TENANT_ID) only
9
+ // on first actual use, not on every HandlerContext build.
10
+ let ungatedMemberReader;
11
+ const queryAsMember = createGatedMemberReader(`handler "${type}"`, hasIdentitySwitchGrant, (userId, qn, payload) => {
12
+ ungatedMemberReader ??= createMemberReaderFn(ctx, user.tenantId, tx);
13
+ return ungatedMemberReader(userId, qn, payload);
14
+ }, { ...identitySwitchAudit, tenantId: user.tenantId });
15
+ return {
16
+ // Thin pass-through: one resolve impl lives on the dispatcher, the
17
+ // handler surface just forwards the call so both entry points (login
18
+ // handler via ctx.resolveAuthClaims, switch-tenant route via
19
+ // dispatcher.resolveAuthClaims) cannot drift.
20
+ resolveAuthClaims: (claimsUser) => resolveAuthClaimsFn(ctx, claimsUser), // @wrapper-known semantic-alias
21
+ // Thin pass-through, same reasoning as resolveAuthClaims above — one
22
+ // resolve impl lives on the dispatcher so callers can't drift apart.
23
+ // Internally queries memberships as SYSTEM, so it needs the same grant
24
+ // as a SYSTEM queryAs/writeAs (r.systemScope() or escapeHatch).
25
+ resolveActiveMembership: (userId, tenantId) => {
26
+ if (!hasIdentitySwitchGrant)
27
+ throw systemIdentitySwitchDenied(`handler "${type}"`);
28
+ return resolveActiveMembershipFn(ctx, userId, tenantId, INTERACTIVE_SIGN_IN_POLICY); // @wrapper-known semantic-alias
29
+ },
30
+ // Needs the same grant as a SYSTEM queryAs.
31
+ queryAsMember,
32
+ // Feature-effective check for in-handler opt-in logic. Scope:
33
+ // **current user's tenant** — for cross-tenant lookups (rare,
34
+ // SysAdmin operations) read effectiveFeatures(otherTenantId) directly.
35
+ // When the feature-toggles or tier-engine feature isn't wired (no
36
+ // effectiveFeatures callback), always returns true — apps without
37
+ // tier-cuts treat all features on.
38
+ //
39
+ // Falls back to the live trialGate when the sync set says the feature
40
+ // is off — the sync set never contains trial-tier features (time-
41
+ // derived, can't boot-cache), so without this a trial tenant checking
42
+ // a companion feature's toggle would silently read `false` even though
43
+ // the dispatch gate already lets trial-tier handlers run.
44
+ hasFeature: async (featureName) => {
45
+ if (!effectiveFeatures)
46
+ return true;
47
+ if (effectiveFeatures(user.tenantId).has(featureName))
48
+ return true;
49
+ if (!effectiveFeatures.trialGate)
50
+ return false;
51
+ return effectiveFeatures.trialGate(user.tenantId, featureName);
52
+ },
53
+ };
54
+ }
@@ -0,0 +1,14 @@
1
+ import { type TenantDb } from "../db/tenant-db.ts";
2
+ import type { TenantId } from "../engine/types/identifiers.ts";
3
+ import type { ConfigAccessor } from "../engine/types/index.ts";
4
+ import { createTzContext } from "../time/index.ts";
5
+ import type { HandlerContextScope } from "./handler-context-scope.ts";
6
+ export declare function buildConfigFacet(scope: HandlerContextScope, db: TenantDb | undefined): {
7
+ config: ConfigAccessor | undefined;
8
+ fragment: {
9
+ config?: ConfigAccessor | undefined;
10
+ configFor?: ((targetTenantId: TenantId) => ConfigAccessor) | undefined;
11
+ };
12
+ };
13
+ export declare function resolveHandlerTz(scope: HandlerContextScope, config: ConfigAccessor | undefined): Promise<ReturnType<typeof createTzContext>>;
14
+ export declare function resolveHandlerLocale(scope: HandlerContextScope): string;
@@ -0,0 +1,113 @@
1
+ import { createTenantDb } from "../db/tenant-db.js";
2
+ import { createSystemUser } from "../engine/system-user.js";
3
+ import { AccessDeniedError } from "../errors/index.js";
4
+ import { DEFAULT_LOCALE, isValidLocaleTag } from "../i18n/request-locale.js";
5
+ import { createTzContext, isValidIanaTimeZone } from "../time/index.js";
6
+ import { configAccessorFactoryOf } from "./config-accessor-factory.js";
7
+ import { isSystemIdentity } from "./system-identity-switch.js";
8
+ import { TENANT_TIMEZONE_CONFIG_KEY } from "./tenant-timezone-config-key.js";
9
+ // Cross-tenant accessor for SystemAdmin/system callers. The
10
+ // identity is the target tenant's system user so the caller's own user-scope
11
+ // values never resolve into another tenant's config; the db is tenant-mode
12
+ // scoped to the target.
13
+ function buildConfigFor(scope, configFactory, config) {
14
+ const { user, context, registry, dbSource } = scope;
15
+ return configFactory && dbSource
16
+ ? (targetTenantId) => {
17
+ if (targetTenantId === user.tenantId && config)
18
+ return config;
19
+ if (!isSystemIdentity(user) && !user.roles.includes("SystemAdmin")) {
20
+ throw new AccessDeniedError({
21
+ message: "ctx.configFor(otherTenant) requires the system role or SystemAdmin",
22
+ details: { reason: "config_for_requires_system_admin" },
23
+ });
24
+ }
25
+ const targetSystemUser = createSystemUser(targetTenantId);
26
+ return configFactory({
27
+ user: { id: targetSystemUser.id, tenantId: targetTenantId },
28
+ db: createTenantDb({
29
+ db: dbSource,
30
+ tenantId: targetTenantId,
31
+ mode: "tenant",
32
+ tracer: context.tracer,
33
+ meter: context.meter,
34
+ grants: { projectionRegistry: registry },
35
+ }),
36
+ secrets: context.secrets,
37
+ });
38
+ }
39
+ : undefined;
40
+ }
41
+ export function buildConfigFacet(scope, db) {
42
+ const { user, context } = scope;
43
+ // Mirror notify: only built when the config feature wired its factory.
44
+ const configFactory = configAccessorFactoryOf(context);
45
+ const config = configFactory && db
46
+ ? configFactory({
47
+ user: { id: user.id, tenantId: user.tenantId },
48
+ db,
49
+ secrets: context.secrets,
50
+ })
51
+ : undefined;
52
+ const configFor = buildConfigFor(scope, configFactory, config);
53
+ return { config, fragment: { ...(config && { config }), ...(configFor && { configFor }) } };
54
+ }
55
+ async function readTenantTimezone(scope, config) {
56
+ const { ctx, user, tx } = scope;
57
+ if (config === undefined)
58
+ return undefined;
59
+ const cachedTz = ctx.tenantTimezoneCache.get(user.tenantId);
60
+ if (cachedTz)
61
+ return cachedTz.value;
62
+ const generationAtRead = ctx.tenantTimezoneCache.generation();
63
+ const tenantTz = await config(TENANT_TIMEZONE_CONFIG_KEY);
64
+ if (tx === undefined) {
65
+ ctx.tenantTimezoneCache.setIfFresh(user.tenantId, typeof tenantTz === "string" ? tenantTz : undefined, generationAtRead);
66
+ }
67
+ return tenantTz;
68
+ }
69
+ // ctx.tz is always present. tenant reads tenant:config:timezone through
70
+ // the already-built `config` accessor (raw qualified-key — no import of
71
+ // the tenant feature, framework/pipeline stays bundled-features-free);
72
+ // falls back to "UTC" when config is missing or the key is unset. user
73
+ // comes from SessionUser.timezone (set at login), else falls back to
74
+ // tenant (createTzContext's own default). An app-injected GeoTzProvider
75
+ // (context.geoTzProvider) feeds ctx.tz.fromCoordinates / fromAddress.
76
+ //
77
+ // Resolving this via `config()` is a config-table SELECT on every single
78
+ // dispatch — ctx.tenantTimezoneCache memoizes it per tenant.
79
+ // dispatch-write.ts invalidates on config:write:set/reset for this key.
80
+ // Only populate from a tx-free read (tx === undefined): a write-path
81
+ // lookup runs inside the caller's own open transaction, and caching a
82
+ // value read there could memoize state from a write earlier in that same
83
+ // transaction before it's known to commit. Reads always consult the
84
+ // cache regardless of tx, since serving a slightly stale cached value on
85
+ // the write path is harmless (bounded by ttlMs) and avoids reintroducing
86
+ // the SELECT on the hot path this fix targets.
87
+ export async function resolveHandlerTz(scope, config) {
88
+ const { user, context } = scope;
89
+ const tenantTz = await readTenantTimezone(scope, config);
90
+ // Guarded against garbage: an unvalidated string here (free-form config
91
+ // key, legacy JWT claim predating validation) blows up every ctx.tz call
92
+ // for the whole tenant with a RangeError. Fall back to UTC/tenant instead
93
+ // of trusting the raw value.
94
+ const safeTenantTz = typeof tenantTz === "string" && isValidIanaTimeZone(tenantTz) ? tenantTz : "UTC";
95
+ const safeUserTz = user.timezone !== undefined && isValidIanaTimeZone(user.timezone) ? user.timezone : undefined;
96
+ return createTzContext({
97
+ ...(context.geoTzProvider !== undefined ? { geoTz: context.geoTzProvider } : {}),
98
+ ...(context.clock !== undefined ? { clock: context.clock } : {}),
99
+ tenant: safeTenantTz,
100
+ ...(safeUserTz !== undefined && { user: safeUserTz }),
101
+ });
102
+ }
103
+ // ctx.locale — request-layer signal (X-Locale header → Accept-Language,
104
+ // resolved once at the HTTP boundary by request-id-middleware.ts) wins;
105
+ // then SessionUser.locale (set at login — stale until re-login, same
106
+ // tradeoff as ctx.tz.user); falls back to the app's
107
+ // boot-configured defaultLocale, then DEFAULT_LOCALE. Mirrors ctx.tz's
108
+ // Request → Session → Boot-Default chain.
109
+ export function resolveHandlerLocale(scope) {
110
+ const { user, reqCtx, context } = scope;
111
+ const safeUserLocale = user.locale !== undefined && isValidLocaleTag(user.locale) ? user.locale : undefined;
112
+ return reqCtx?.locale ?? safeUserLocale ?? context.defaultLocale ?? DEFAULT_LOCALE;
113
+ }
@@ -0,0 +1,24 @@
1
+ import type { DbConnection, DbTx } from "../db/connection.ts";
2
+ import { type TenantDb } from "../db/tenant-db.ts";
3
+ import type { TenantId } from "../engine/types/identifiers.ts";
4
+ import type { TenantWriteTarget } from "../engine/types/index.ts";
5
+ import type { HandlerContextScope } from "./handler-context-scope.ts";
6
+ export declare function buildTenantScopedDb(scope: HandlerContextScope, source: DbConnection | DbTx, signal: AbortSignal | undefined, foreignTenantId?: TenantId, denyWrites?: boolean): TenantDb;
7
+ export declare function buildHandlerDbFacet(scope: HandlerContextScope): {
8
+ db: TenantDb | undefined;
9
+ exposedDb: TenantDb | undefined;
10
+ outsideTxSource: DbConnection | undefined;
11
+ fragment: {
12
+ db: TenantDb | undefined;
13
+ dbOutsideTransaction: TenantDb | undefined;
14
+ systemDb?: import("@cosmicdrift/kumiko-types/tenant-db-types").UncheckedSystemDb | undefined;
15
+ crossTenantReads?: import("@cosmicdrift/kumiko-types/tenant-db-types").CrossTenantReads | undefined;
16
+ };
17
+ };
18
+ export declare function buildForTenantFragment(scope: HandlerContextScope, exposedDb: TenantDb | undefined, readOnlyForTenant: boolean): {
19
+ forTenant?: ((targetTenantId: TenantId) => TenantWriteTarget) | undefined;
20
+ };
21
+ export declare function buildFileFragment(scope: HandlerContextScope, db: TenantDb | undefined): {
22
+ files?: import("@cosmicdrift/kumiko-types/file-handle-types").FileContext | undefined;
23
+ derivatives?: import("@cosmicdrift/kumiko-types/derivatives-types").DerivativesContext | undefined;
24
+ };
@@ -0,0 +1,149 @@
1
+ import { crossTenantReadsGrantFor, escapeHatchFor } from "@cosmicdrift/kumiko-types/handlers";
2
+ import { isAgentToolCallScope } from "../api/request-context.js";
3
+ import { createHandlerCrossTenantReads } from "../db/cross-tenant-reads.js";
4
+ import { createTenantDb, createUncheckedSystemDb } from "../db/tenant-db.js";
5
+ import { createDerivativesContext } from "../derivatives/derivatives-context.js";
6
+ import { mayOverrideTenant } from "../engine/cross-tenant.js";
7
+ import { AccessDeniedError, FrameworkReasons } from "../errors/index.js";
8
+ import { createTenantFileContext } from "../files/file-handle.js";
9
+ import { createSystemScopedDbGuard, isMemberResolutionPrincipal, resolveAppPoolDb, } from "./dispatch-shared.js";
10
+ import { buildPersonalDataGate } from "./write-origin.js";
11
+ export function buildTenantScopedDb(scope, source, signal, foreignTenantId, denyWrites = false) {
12
+ const { user, context, registry, origin, isSystem, writeEscapeHatch, handlerEscapeHatch } = scope;
13
+ return createTenantDb({
14
+ db: source,
15
+ tenantId: foreignTenantId ?? user.tenantId,
16
+ mode: isSystem && foreignTenantId === undefined ? "system" : "tenant",
17
+ tracer: context.tracer,
18
+ meter: context.meter,
19
+ signal,
20
+ grants: {
21
+ ...(foreignTenantId === undefined && {
22
+ globalWrites: escapeHatchFor(writeEscapeHatch, "globalWrites"),
23
+ unsafeRaw: escapeHatchFor(handlerEscapeHatch, "unsafeRaw"),
24
+ }),
25
+ report: scope.reportEscapeHatch,
26
+ memberReadOnly: isMemberResolutionPrincipal(user),
27
+ personalDataGate: buildPersonalDataGate(registry, origin),
28
+ projectionRegistry: registry,
29
+ ...(denyWrites && { writesDeniedReason: "without an open transaction" }),
30
+ },
31
+ });
32
+ }
33
+ function buildHandlerCrossTenantReads(scope) {
34
+ const { user, context, registry, origin, dbSource, reqCtx, handlerEscapeHatch } = scope;
35
+ // An agent tool call runs with the user's session but feeds the LLM context, so
36
+ // cross-tenant rows must never reach it, even when unsafeRaw implies the grant.
37
+ const crossTenantReadsReason = isAgentToolCallScope()
38
+ ? undefined
39
+ : crossTenantReadsGrantFor(handlerEscapeHatch)?.reason;
40
+ return dbSource && crossTenantReadsReason !== undefined
41
+ ? createHandlerCrossTenantReads({
42
+ db: dbSource,
43
+ tenantId: user.tenantId,
44
+ tracer: context.tracer,
45
+ meter: context.meter,
46
+ signal: reqCtx?.signal,
47
+ reason: crossTenantReadsReason,
48
+ report: scope.reportEscapeHatch,
49
+ grants: {
50
+ report: scope.reportEscapeHatch,
51
+ memberReadOnly: isMemberResolutionPrincipal(user),
52
+ personalDataGate: buildPersonalDataGate(registry, origin),
53
+ projectionRegistry: registry,
54
+ },
55
+ })
56
+ : undefined;
57
+ }
58
+ export function buildHandlerDbFacet(scope) {
59
+ const { ctx, type, isSystem, dbSource, reqCtx, denyDbWrites } = scope;
60
+ // Propagate the request's AbortSignal so every TenantDb query throws when
61
+ // the client has disconnected — handlers with many sequential queries skip
62
+ // the rest of the chain instead of burning DB-CPU for results no one reads.
63
+ const db = dbSource
64
+ ? buildTenantScopedDb(scope, dbSource, reqCtx?.signal, undefined, denyDbWrites)
65
+ : undefined;
66
+ // Unbound pool, tenant-scoped like `db` but never tx-bound — writes
67
+ // through it survive a rollback of the handler's own transaction. No
68
+ // AbortSignal here: a client disconnect must not abort a durability write
69
+ // that is meant to outlive the request. Computed before `systemDb` below
70
+ // so its guarded escape hatch (ctx.systemDb.outsideTransaction) can wrap
71
+ // this same TenantDb instead of a second, independently-built one.
72
+ const outsideTxSource = ctx.outsideTransactionDb ?? resolveAppPoolDb(ctx);
73
+ const rawDbOutsideTransaction = outsideTxSource
74
+ ? buildTenantScopedDb(scope, outsideTxSource, undefined)
75
+ : undefined;
76
+ const systemDb = isSystem && db
77
+ ? createUncheckedSystemDb(db, rawDbOutsideTransaction, scope.reportEscapeHatch)
78
+ : undefined;
79
+ const crossTenantReads = buildHandlerCrossTenantReads(scope);
80
+ // Exposed as ctx.db below — the internal `db` above stays the real,
81
+ // working TenantDb for config/derivatives accessors and systemDb
82
+ // construction.
83
+ const exposedDb = isSystem && db
84
+ ? createSystemScopedDbGuard(type, "db", "ctx.systemDb.assertTenantMatch(...) or ctx.systemDb.acknowledgeCrossTenant(...)")
85
+ : db;
86
+ // Same fail-closed treatment as `exposedDb` — a system handler reaching
87
+ // ctx.dbOutsideTransaction directly would be a second, unguarded door to
88
+ // the same cross-tenant access ctx.db closes off above.
89
+ const dbOutsideTransaction = isSystem && rawDbOutsideTransaction
90
+ ? createSystemScopedDbGuard(type, "dbOutsideTransaction", "ctx.systemDb.outsideTransaction.assertTenantMatch(...) or " +
91
+ "ctx.systemDb.outsideTransaction.acknowledgeCrossTenant(...)")
92
+ : rawDbOutsideTransaction;
93
+ return {
94
+ db,
95
+ exposedDb,
96
+ outsideTxSource,
97
+ fragment: {
98
+ db: exposedDb,
99
+ dbOutsideTransaction,
100
+ ...(systemDb && { systemDb }),
101
+ ...(crossTenantReads && { crossTenantReads }),
102
+ },
103
+ };
104
+ }
105
+ // Write handlers only, and not r.systemScope() ones (they have ctx.systemDb). The gate is the
106
+ // dispatcher's tenantIdOverride predicate, so no caller reaches more than it could via
107
+ // tenantIdOverride. The foreign db carries no unsafeRaw/globalWrites grant and is tenant mode
108
+ // bound to the target; the executor accepts streamTenantId only on a db bound to that tenant.
109
+ export function buildForTenantFragment(scope, exposedDb, readOnlyForTenant) {
110
+ const { user, isSystem, dbSource, isWriteHandler, reqCtx, denyDbWrites, isPerCommand } = scope;
111
+ const forTenant = !isSystem && dbSource && isWriteHandler
112
+ ? (targetTenantId) => {
113
+ if (targetTenantId === user.tenantId && exposedDb) {
114
+ // Only perCommand handlers lose forTenant writes; reservations keep their pre-transaction contract.
115
+ const ownTenantDb = denyDbWrites && !isPerCommand
116
+ ? buildTenantScopedDb(scope, dbSource, reqCtx?.signal, undefined, readOnlyForTenant)
117
+ : exposedDb;
118
+ return { db: ownTenantDb, streamTenantId: undefined };
119
+ }
120
+ if (!mayOverrideTenant(user)) {
121
+ throw new AccessDeniedError({
122
+ details: { reason: FrameworkReasons.tenantOverrideRequiresSystemAdmin },
123
+ });
124
+ }
125
+ return {
126
+ db: buildTenantScopedDb(scope, dbSource, reqCtx?.signal, targetTenantId, denyDbWrites && (isPerCommand || readOnlyForTenant)),
127
+ streamTenantId: targetTenantId,
128
+ };
129
+ }
130
+ : undefined;
131
+ return { ...(forTenant && { forTenant }) };
132
+ }
133
+ // ctx.files resolved per-tenant through file-foundation (lazy — the
134
+ // provider is only resolved when a handle actually does I/O). Boot wires
135
+ // _fileProviderResolver when a file-provider plugin is mounted; falls back
136
+ // to a statically-injected context.files (tests).
137
+ export function buildFileFragment(scope, db) {
138
+ const { context, registry, user } = scope;
139
+ const fileResolver = context._fileProviderResolver;
140
+ const files = fileResolver ? createTenantFileContext(fileResolver, user.tenantId) : context.files;
141
+ // ctx.derivatives builds on ctx.files (needs a FileContext to read the
142
+ // original + write the variant) plus db (to look up the FileRef row) — so
143
+ // it can only be constructed exactly when files+db both resolved; falls
144
+ // back to a statically-injected context.derivatives otherwise (tests).
145
+ const derivatives = files && db
146
+ ? createDerivativesContext({ files, registry, db, tenantId: user.tenantId })
147
+ : context.derivatives;
148
+ return { ...(files && { files }), ...(derivatives && { derivatives }) };
149
+ }
@@ -0,0 +1,21 @@
1
+ import type { WriteResult } from "../engine/types/index.ts";
2
+ import { type HandlerTransactionScope } from "./dispatch-shared.ts";
3
+ import { type AfterCommitHook } from "./dispatcher-utils.ts";
4
+ import type { HandlerContextScope } from "./handler-context-scope.ts";
5
+ export type CommitSinks = {
6
+ readonly bridgeSink: AfterCommitHook[];
7
+ readonly hasLiveCommitSink: boolean;
8
+ readonly notifyCommitSink: AfterCommitHook[] | undefined;
9
+ };
10
+ export declare function createCommitSinks(afterCommitHooks: AfterCommitHook[] | undefined, transactionScope: HandlerTransactionScope, outerNotifySink: AfterCommitHook[] | undefined): CommitSinks;
11
+ export declare function buildDispatchFacet(scope: HandlerContextScope, sinks: CommitSinks): {
12
+ bridge: {
13
+ query: (targetType: string, payload: unknown) => Promise<unknown>;
14
+ queryAs: import("./system-identity-switch.ts").QueryAsFn;
15
+ write: (targetType: string, payload: unknown) => Promise<WriteResult>;
16
+ writeAs: import("./system-identity-switch.ts").WriteAsFn;
17
+ };
18
+ scheduleFragment: {
19
+ scheduleAfterCommit?: ((hook: AfterCommitHook) => void) | undefined;
20
+ };
21
+ };
@@ -0,0 +1,89 @@
1
+ import { runInSavepoint } from "../db/query.js";
2
+ import { writeFailure } from "../errors/index.js";
3
+ import { runBatch, unwrapSingle } from "./dispatch-batch.js";
4
+ import { executeQuery } from "./dispatch-query.js";
5
+ import { perCommandNestedDenied } from "./dispatch-shared.js";
6
+ import { executeWrite } from "./dispatch-write.js";
7
+ import { isFailedWriteResult, NestedWriteRollback, } from "./dispatcher-utils.js";
8
+ import { createGatedIdentitySwitch } from "./system-identity-switch.js";
9
+ export function createCommitSinks(afterCommitHooks, transactionScope, outerNotifySink) {
10
+ // Cross-feature bridge: ctx.query/write share the current tx + afterCommitHooks sink.
11
+ // queryAs/writeAs to anyone but the caller itself needs r.systemScope() or { escapeHatch } (system-identity-switch.ts).
12
+ const bridgeSink = afterCommitHooks ?? [];
13
+ // Query bridges and post-commit contexts get a throwaway sink: hooks pushed there never run.
14
+ const hasLiveCommitSink = afterCommitHooks !== undefined && transactionScope !== "postCommit";
15
+ return {
16
+ bridgeSink,
17
+ hasLiveCommitSink,
18
+ notifyCommitSink: hasLiveCommitSink ? bridgeSink : outerNotifySink,
19
+ };
20
+ }
21
+ // A failed inner write has already appended its event and projections when a later
22
+ // step (e.g. an inTransaction hook) fails, and the outer handler may swallow the
23
+ // failure result. The savepoint makes the failure undo exactly the inner write; the
24
+ // inner afterCommit hooks only reach the outer sink if it succeeded.
25
+ async function runNestedWriteInSavepoint(scope, sinks, asUser, targetType, payload) {
26
+ const { ctx, registry, origin, tx } = scope;
27
+ const { bridgeSink } = sinks;
28
+ if (registry.getWriteHandler(targetType)?.transaction === "perCommand") {
29
+ return writeFailure(perCommandNestedDenied(targetType));
30
+ }
31
+ if (!tx)
32
+ return executeWrite(ctx, targetType, payload, asUser, origin, tx, bridgeSink);
33
+ const innerAfterCommitHooks = [];
34
+ try {
35
+ const result = await runInSavepoint(tx, async (savepointTx) => {
36
+ const innerResult = await executeWrite(ctx, targetType, payload, asUser, origin, savepointTx, // @cast-boundary db-runner savepoint handle is a transaction handle
37
+ innerAfterCommitHooks);
38
+ if (isFailedWriteResult(innerResult))
39
+ throw new NestedWriteRollback(innerResult);
40
+ return innerResult;
41
+ });
42
+ bridgeSink.push(...innerAfterCommitHooks);
43
+ return result;
44
+ }
45
+ catch (e) {
46
+ if (e instanceof NestedWriteRollback)
47
+ return e.failure;
48
+ throw e;
49
+ }
50
+ }
51
+ // Savepoints on one connection are not serialized by the driver: two parallel nested writes would
52
+ // interleave their statements and a ROLLBACK TO of the first would also discard the second's rows.
53
+ // Writes nested one level deeper run on their own savepoint handle and therefore their own chain.
54
+ function createSerializedNestedWriter(scope, sinks) {
55
+ let nestedWriteChain = Promise.resolve();
56
+ return (asUser, targetType, payload) => {
57
+ const run = nestedWriteChain.then(() => runNestedWriteInSavepoint(scope, sinks, asUser, targetType, payload));
58
+ nestedWriteChain = run.catch(() => undefined);
59
+ return run;
60
+ };
61
+ }
62
+ export function buildDispatchFacet(scope, sinks) {
63
+ const { ctx, type, user, origin, tx, transactionScope, hasIdentitySwitchGrant, identitySwitchAudit, } = scope;
64
+ const scheduleAfterCommit = (hook) => {
65
+ sinks.bridgeSink.push(hook);
66
+ };
67
+ const executeNestedWriteInSavepoint = createSerializedNestedWriter(scope, sinks);
68
+ // Without an open transaction (post-commit hooks, stream/query handlers, perCommand handlers) a bare
69
+ // executeWrite would auto-commit event append, projections and inTransaction hooks separately and its
70
+ // afterCommit hooks would land in a sink nobody flushes. A batch-of-one gives the write its own
71
+ // transaction and its own flush.
72
+ const writeAsUser = async (asUser, targetType, payload) => transactionScope === "inTransaction"
73
+ ? executeNestedWriteInSavepoint(asUser, targetType, payload)
74
+ : unwrapSingle(await runBatch(ctx, [{ type: targetType, payload }], asUser, undefined, origin));
75
+ const identitySwitch = createGatedIdentitySwitch(`handler "${type}"`, user, hasIdentitySwitchGrant, {
76
+ // Inherits the caller's origin, so switching to SYSTEM cannot shed an anonymous root.
77
+ queryAs: (asUser, targetType, payload) => executeQuery(ctx, targetType, payload, asUser, origin, tx, sinks.notifyCommitSink), // @wrapper-known semantic-alias
78
+ writeAs: writeAsUser,
79
+ }, identitySwitchAudit);
80
+ return {
81
+ bridge: {
82
+ query: (targetType, payload) => executeQuery(ctx, targetType, payload, user, origin, tx, sinks.notifyCommitSink), // @wrapper-known semantic-alias
83
+ queryAs: identitySwitch.queryAs,
84
+ write: (targetType, payload) => writeAsUser(user, targetType, payload),
85
+ writeAs: identitySwitch.writeAs,
86
+ },
87
+ scheduleFragment: { ...(sinks.hasLiveCommitSink && { scheduleAfterCommit }) },
88
+ };
89
+ }
@@ -0,0 +1,43 @@
1
+ import type { AggregateStreamHandle, AppendEventArgs, AppendEventFn, FetchForWritingArgs } from "../engine/types/index.ts";
2
+ import { IdempotentAppendConflictError as EventStoreIdempotentAppendConflictError, VersionConflictError as EventStoreVersionConflictError } from "../event-store/errors.ts";
3
+ import { type StoredEvent } from "../event-store/event-store.ts";
4
+ import { type LoadAggregateWithSnapshotOptions, type LoadAggregateWithSnapshotResult, type SnapshotReducer } from "../event-store/snapshot.ts";
5
+ import type { HandlerContextScope } from "./handler-context-scope.ts";
6
+ export declare function buildEventStoreFacet(scope: HandlerContextScope): {
7
+ appendEvent: AppendEventFn;
8
+ unsafeAppendEvent: (args: AppendEventArgs) => Promise<void>;
9
+ tryAppendEvent: (args: AppendEventArgs) => Promise<{
10
+ ok: true;
11
+ event: StoredEvent;
12
+ conflict?: undefined;
13
+ } | {
14
+ event?: undefined;
15
+ ok: false;
16
+ conflict: EventStoreVersionConflictError;
17
+ } | {
18
+ event?: undefined;
19
+ ok: false;
20
+ conflict: EventStoreIdempotentAppendConflictError;
21
+ }>;
22
+ fetchForWriting: (args: FetchForWritingArgs) => Promise<AggregateStreamHandle>;
23
+ loadAggregate: (aggregateId: string, loadOptions?: {
24
+ readonly asOf?: Temporal.Instant;
25
+ }) => Promise<readonly StoredEvent[]>;
26
+ archiveStream: (aggregateId: string, archiveArgs: {
27
+ readonly aggregateType: string;
28
+ readonly reason?: string;
29
+ }) => Promise<void>;
30
+ restoreStream: (aggregateId: string) => Promise<void>;
31
+ isStreamArchived: (aggregateId: string) => Promise<boolean>;
32
+ snapshotAggregate: (snapshotArgs: {
33
+ readonly aggregateId: string;
34
+ readonly aggregateType: string;
35
+ readonly version: number;
36
+ readonly state: Record<string, unknown>;
37
+ readonly snapshotVersion?: number;
38
+ }) => Promise<void>;
39
+ loadAggregateWithSnapshot: <TState extends Record<string, unknown>>(aggregateId: string, reducer: SnapshotReducer<TState>, initial: TState, loadOptions?: Omit<LoadAggregateWithSnapshotOptions, "upcastEvent" | "entityEventVersionOf">) => Promise<LoadAggregateWithSnapshotResult<TState>>;
40
+ queryProjection: <T = Record<string, unknown>>(qualifiedName: string, options?: {
41
+ readonly unsafeAllTenants?: boolean;
42
+ }) => Promise<readonly T[]>;
43
+ };