@cosmicdrift/kumiko-framework 0.361.0 → 0.363.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 (126) hide show
  1. package/dist/api/auth-middleware.js +7 -5
  2. package/dist/api/redis-sse-broker.js +10 -8
  3. package/dist/api/routes.js +2 -2
  4. package/dist/api/server.js +3 -0
  5. package/dist/bun-db/query.d.ts +6 -4
  6. package/dist/bun-db/query.js +66 -92
  7. package/dist/crypto/kek-source.js +5 -3
  8. package/dist/crypto/pii-field-encryption.d.ts +4 -1
  9. package/dist/crypto/pii-field-encryption.js +2 -2
  10. package/dist/db/apply-entity-event.js +2 -2
  11. package/dist/db/cursor.js +2 -1
  12. package/dist/db/dialect.d.ts +1 -0
  13. package/dist/db/dialect.js +28 -13
  14. package/dist/db/entity-table-from-registry.js +1 -1
  15. package/dist/db/entity-table-meta.js +5 -13
  16. package/dist/db/event-store-executor-context.js +6 -6
  17. package/dist/db/event-store-executor-read.js +9 -9
  18. package/dist/db/migrate-generator.js +5 -3
  19. package/dist/db/migrate-runner.js +31 -35
  20. package/dist/db/parent-ref-clause.js +2 -2
  21. package/dist/db/queries/backfill-pii.js +11 -9
  22. package/dist/db/queries/es-ops.js +1 -1
  23. package/dist/db/queries/event-consumer.d.ts +6 -6
  24. package/dist/db/queries/event-consumer.js +17 -17
  25. package/dist/db/queries/event-store-admin.js +6 -6
  26. package/dist/db/queries/event-store-transfer.js +2 -2
  27. package/dist/db/queries/event-store.d.ts +1 -1
  28. package/dist/db/queries/event-store.js +2 -2
  29. package/dist/db/queries/pii-backfill-state.js +2 -2
  30. package/dist/db/queries/projection-rebuild.d.ts +1 -1
  31. package/dist/db/queries/raw-sql.js +1 -1
  32. package/dist/db/queries/test-stack.js +1 -1
  33. package/dist/db/reference-data.js +3 -5
  34. package/dist/db/table-builder.js +6 -4
  35. package/dist/db/table-introspection.d.ts +16 -0
  36. package/dist/db/table-introspection.js +76 -0
  37. package/dist/db/tenant-db.d.ts +3 -2
  38. package/dist/db/tenant-db.js +4 -10
  39. package/dist/derivatives/variant-key.js +2 -1
  40. package/dist/engine/boot-validator/access-declarations.js +29 -18
  41. package/dist/engine/boot-validator/card-meta-overflow.js +5 -2
  42. package/dist/engine/boot-validator/entity-list-screens.js +1 -1
  43. package/dist/engine/boot-validator/pii-retention.js +1 -1
  44. package/dist/engine/boot-validator/screens.js +2 -1
  45. package/dist/engine/build-app-schema.js +1 -0
  46. package/dist/engine/changeset-changes.js +3 -3
  47. package/dist/engine/define-handler.js +3 -6
  48. package/dist/engine/embedded-derived.js +10 -6
  49. package/dist/engine/extensions/escape-hatch-usage.js +2 -1
  50. package/dist/engine/factories.js +5 -6
  51. package/dist/engine/feature-ast/extractors/events.js +43 -33
  52. package/dist/engine/feature-ast/extractors/round3.d.ts +2 -2
  53. package/dist/engine/feature-ast/extractors/round3.js +4 -4
  54. package/dist/engine/feature-ast/patcher.d.ts +3 -3
  55. package/dist/engine/feature-ast/patcher.js +3 -3
  56. package/dist/engine/feature-ast/pattern-change-schema.d.ts +8 -8
  57. package/dist/engine/feature-ast/pattern-change-schema.js +3 -3
  58. package/dist/engine/feature-ast/patterns.d.ts +3 -3
  59. package/dist/engine/feature-ast/render.js +6 -3
  60. package/dist/engine/feature-changelog.d.ts +2 -1
  61. package/dist/engine/feature-changelog.js +8 -6
  62. package/dist/engine/feature-config-events-jobs.d.ts +17 -7
  63. package/dist/engine/feature-config-events-jobs.js +20 -5
  64. package/dist/engine/feature-ui-extensions.js +2 -3
  65. package/dist/engine/index.d.ts +2 -1
  66. package/dist/engine/index.js +1 -0
  67. package/dist/engine/ownership.js +7 -20
  68. package/dist/engine/pattern-library/mixed-schemas.js +0 -1
  69. package/dist/engine/registry-ingest.d.ts +2 -1
  70. package/dist/engine/registry-ingest.js +85 -2
  71. package/dist/engine/registry-validate.d.ts +1 -0
  72. package/dist/engine/registry-validate.js +38 -8
  73. package/dist/engine/registry.js +2 -1
  74. package/dist/engine/schema-builder.js +1 -3
  75. package/dist/engine/soft-delete-cleanup.js +2 -1
  76. package/dist/engine/steps/call-feature.js +1 -3
  77. package/dist/engine/steps/unsafe-projection-upsert.js +2 -13
  78. package/dist/engine/template-variables.d.ts +29 -0
  79. package/dist/engine/template-variables.js +77 -0
  80. package/dist/engine/tier-resolver-extension.js +2 -3
  81. package/dist/engine/types/index.d.ts +2 -2
  82. package/dist/engine/validate-projection-allowlist.js +12 -13
  83. package/dist/engine/where-rule-lint.js +8 -7
  84. package/dist/errors/reasons.d.ts +1 -0
  85. package/dist/errors/reasons.js +2 -0
  86. package/dist/files/bind-referenced-files.write.d.ts +16 -0
  87. package/dist/files/bind-referenced-files.write.js +109 -0
  88. package/dist/files/feature.d.ts +1 -0
  89. package/dist/files/feature.js +2 -0
  90. package/dist/files/file-routes.d.ts +2 -1
  91. package/dist/files/file-routes.js +10 -3
  92. package/dist/files/local-provider.js +2 -1
  93. package/dist/files/record-write-check.d.ts +1 -0
  94. package/dist/files/record-write-check.js +9 -9
  95. package/dist/i18n/index.d.ts +1 -1
  96. package/dist/i18n/index.js +1 -1
  97. package/dist/i18n/mail-registry.d.ts +2 -0
  98. package/dist/i18n/mail-registry.js +7 -0
  99. package/dist/jobs/job-runner.js +5 -5
  100. package/dist/pipeline/dispatch-query.js +2 -3
  101. package/dist/pipeline/dispatch-shared.js +45 -14
  102. package/dist/pipeline/event-dispatcher-delivery.js +2 -3
  103. package/dist/pipeline/index.d.ts +1 -0
  104. package/dist/pipeline/index.js +1 -0
  105. package/dist/pipeline/member-read-only-transaction.js +1 -1
  106. package/dist/pipeline/notify-scope.d.ts +13 -0
  107. package/dist/pipeline/notify-scope.js +32 -0
  108. package/dist/pipeline/system-hooks.js +2 -1
  109. package/dist/pipeline/system-identity-switch.js +2 -2
  110. package/dist/pipeline/write-origin.js +2 -2
  111. package/dist/search/meilisearch-adapter.js +3 -4
  112. package/dist/secrets/stored-envelope.js +7 -7
  113. package/dist/stack/push-entity-projection-tables.js +2 -3
  114. package/dist/stack/table-helpers.js +10 -26
  115. package/dist/stack/test-stack.js +2 -5
  116. package/dist/testing/multipart-helper.d.ts +0 -6
  117. package/dist/testing/multipart-helper.js +5 -5
  118. package/dist/testing/without-ambient-temporal.js +4 -4
  119. package/dist/ui-types/card-column-roles.d.ts +5 -0
  120. package/dist/ui-types/card-column-roles.js +22 -4
  121. package/dist/ui-types/index.d.ts +1 -1
  122. package/dist/ui-types/index.js +1 -1
  123. package/dist/utils/is-plain-object.d.ts +1 -2
  124. package/dist/utils/is-plain-object.js +1 -4
  125. package/package.json +4 -4
  126. package/src/changes.json +88 -0
@@ -16,8 +16,9 @@
16
16
  //
17
17
  // Construction: use the `from(ref, column?)` helper. It returns a FromRule
18
18
  // ready to drop into an access map.
19
- import { KUMIKO_COLUMNS_SYMBOL, KUMIKO_NAME_SYMBOL, } from "@cosmicdrift/kumiko-types/schema-table-types";
19
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
20
20
  import { toSnakeCase } from "../db/table-builder.js";
21
+ import { columnSqlNameOf, schemaTableName } from "../db/table-introspection.js";
21
22
  import { assertQualifiedWhereFragment, tableColumnSqlNames } from "./where-rule-lint.js";
22
23
  // Parse an OwnershipRef into kind + resolved path + default column.
23
24
  // Throws on malformed input so the error surfaces at `from()`-call-site (in
@@ -185,32 +186,18 @@ export function userCanCreateFieldRow(user, accessMap, newRow) {
185
186
  const PASS_CLAUSE = { kind: "pass" };
186
187
  const EMPTY_CLAUSE = { kind: "empty" };
187
188
  export function tableNameOf(table) {
188
- if (table !== null && typeof table === "object") {
189
- const sym = table[KUMIKO_NAME_SYMBOL];
190
- if (typeof sym === "string")
191
- return sym;
192
- }
193
- return "<unknown>";
189
+ return schemaTableName(table) ?? "<unknown>";
194
190
  }
195
191
  // Resolve a JS-field name on the table to its underlying SQL column name.
196
- // Drizzle tables carry the mapping under Symbol.for("kumiko:schema:Columns");
197
- // we read it without importing drizzle-orm at runtime.
198
192
  function columnSqlName(table, field) {
199
193
  if (table === null || typeof table !== "object")
200
194
  return null;
201
- const cols = table[KUMIKO_COLUMNS_SYMBOL];
202
- if (cols && typeof cols === "object") {
203
- const col = cols[field];
204
- if (col && typeof col === "object") {
205
- const nameVal = col["name"];
206
- if (typeof nameVal === "string")
207
- return nameVal;
208
- }
209
- }
195
+ const handleName = columnSqlNameOf(table, field);
196
+ if (handleName !== undefined)
197
+ return handleName;
210
198
  // Field may already be the SQL column name on plain objects (tests, etc.).
211
- if (table[field] !== undefined) {
199
+ if (isPlainObject(table) && table[field] !== undefined)
212
200
  return toSnakeCase(field);
213
- }
214
201
  return null;
215
202
  }
216
203
  function quoteIdent(name) {
@@ -351,7 +351,6 @@ export const notificationSchema = {
351
351
  path: "trigger.on",
352
352
  label: { en: "Trigger on entity", de: "Trigger-Entität" },
353
353
  input: "entity-ref",
354
- required: true,
355
354
  },
356
355
  {
357
356
  path: "recipientBody",
@@ -1,8 +1,9 @@
1
1
  import type { RegistryState } from "./registry-state.js";
2
- import type { FeatureDefinition } from "./types/index.js";
2
+ import type { FeatureDefinition, NotificationDefinition } from "./types/index.js";
3
3
  export declare function populateFeatureCore(state: RegistryState, feature: FeatureDefinition): void;
4
4
  export declare function populateHandlers(state: RegistryState, feature: FeatureDefinition): void;
5
5
  export declare function populateConfigKeys(state: RegistryState, feature: FeatureDefinition): void;
6
+ export declare function validateNotificationTemplate(featureName: string, notificationName: string, notifDef: NotificationDefinition): void;
6
7
  export declare function populateJobsAndNotifications(state: RegistryState, feature: FeatureDefinition): void;
7
8
  export declare function populateEvents(state: RegistryState, feature: FeatureDefinition): void;
8
9
  export declare function populateSystemEvents(state: RegistryState): void;
@@ -90,6 +90,86 @@ export function populateConfigKeys(state, feature) {
90
90
  state.configKeyMap.set(qualifiedKey, keyDef);
91
91
  }
92
92
  }
93
+ // Mirrors isDocumentReferenceName in bundled-features/template-resolver: the
94
+ // framework cannot import bundled-features, and both sides must accept the
95
+ // same placeholder names.
96
+ const TEMPLATE_VARIABLE_NAME = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
97
+ const FORBIDDEN_TEMPLATE_VARIABLE_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]);
98
+ // The first four mirror SYSTEM_PLACEHOLDERS in bundled-features/template-resolver: the
99
+ // renderer fills these itself, so a declared variable of the same name would
100
+ // be shadowed or shadow them.
101
+ const RESERVED_TEMPLATE_VARIABLE_NAMES = new Set([
102
+ "tenantName",
103
+ "signature",
104
+ "imprint",
105
+ "unsubscribeUrl",
106
+ // Handlebars helpers of the mail renderer win over context values of the
107
+ // same name, so a variable called like a helper would never render.
108
+ "link",
109
+ "money",
110
+ "date",
111
+ ]);
112
+ const KEBAB_NOTIFICATION_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
113
+ function isTemplateVariableName(value) {
114
+ return (TEMPLATE_VARIABLE_NAME.test(value) &&
115
+ !value.split(".").some((segment) => FORBIDDEN_TEMPLATE_VARIABLE_SEGMENTS.has(segment)));
116
+ }
117
+ function notificationLabel(featureName, notificationName) {
118
+ return `Notification "${featureName}:notify:${notificationName}"`;
119
+ }
120
+ function assertTriggerGroupComplete(label, notifDef) {
121
+ if (notifDef.trigger !== undefined) {
122
+ const missing = [
123
+ notifDef.recipient === undefined ? "recipient" : undefined,
124
+ notifDef.data === undefined ? "data" : undefined,
125
+ ].filter((name) => name !== undefined);
126
+ if (missing.length > 0) {
127
+ throw new Error(`${label} declares a trigger but no ${missing.join(" and ")}: trigger, recipient and data belong together`);
128
+ }
129
+ }
130
+ }
131
+ // localizedTemplate exists because translations are only complete once every
132
+ // locale feature is built; the registry resolves it into the static template
133
+ // + meta that all downstream consumers (seeding, validation) already read.
134
+ function resolveLocalizedTemplate(featureName, notificationName, notifDef) {
135
+ const { localizedTemplate, ...rest } = notifDef;
136
+ if (localizedTemplate === undefined)
137
+ return rest;
138
+ if (rest.template !== undefined || rest.meta !== undefined) {
139
+ throw new Error(`${notificationLabel(featureName, notificationName)} declares localizedTemplate together with template/meta: pick one`);
140
+ }
141
+ const byLocale = Object.entries(localizedTemplate());
142
+ return {
143
+ ...rest,
144
+ template: Object.fromEntries(byLocale.map(([locale, entry]) => [locale, { body: entry.body }])),
145
+ meta: Object.fromEntries(byLocale.map(([locale, entry]) => [locale, entry.meta])),
146
+ };
147
+ }
148
+ export function validateNotificationTemplate(featureName, notificationName, notifDef) {
149
+ // skip: a notification without a template has nothing to validate
150
+ if (notifDef.template === undefined)
151
+ return;
152
+ const where = notificationLabel(featureName, notificationName);
153
+ if (!KEBAB_NOTIFICATION_NAME.test(notificationName)) {
154
+ throw new Error(`${where} declares a template, so its name must be kebab-case (lowercase, digits, dashes): the name becomes the template slug`);
155
+ }
156
+ for (const locale of Object.keys(notifDef.template)) {
157
+ if (notifDef.meta?.[locale]?.subject === undefined) {
158
+ throw new Error(`${where} has a template for locale "${locale}" but no meta.${locale}.subject`);
159
+ }
160
+ }
161
+ for (const [name, def] of Object.entries(notifDef.variables ?? {})) {
162
+ if (RESERVED_TEMPLATE_VARIABLE_NAMES.has(name)) {
163
+ throw new Error(`${where} declares variable "${name}" which collides with a system placeholder (${[...RESERVED_TEMPLATE_VARIABLE_NAMES].join(", ")})`);
164
+ }
165
+ const names = def.type === "list" ? [name, ...Object.keys(def.item)] : [name];
166
+ for (const candidate of names) {
167
+ if (!isTemplateVariableName(candidate)) {
168
+ throw new Error(`${where} declares variable "${candidate}" which is not a valid placeholder name (letters, digits, underscore; no prototype segments)`);
169
+ }
170
+ }
171
+ }
172
+ }
93
173
  // Jobs (runIn-pinned, duplicate-guarded) + notifications (trigger resolved later).
94
174
  export function populateJobsAndNotifications(state, feature) {
95
175
  // Jobs: scope:job:name
@@ -115,10 +195,13 @@ export function populateJobsAndNotifications(state, feature) {
115
195
  // Notifications: scope:notify:name
116
196
  for (const [name, notifDef] of Object.entries(feature.notifications ?? {})) {
117
197
  const qualifiedName = qualify(feature.name, "notify", name);
198
+ assertTriggerGroupComplete(notificationLabel(feature.name, name), notifDef);
199
+ const resolvedDef = resolveLocalizedTemplate(feature.name, name, notifDef);
200
+ validateNotificationTemplate(feature.name, name, resolvedDef);
118
201
  state.notificationMap.set(qualifiedName, {
119
- ...notifDef,
202
+ ...resolvedDef,
120
203
  name: qualifiedName,
121
- trigger: { on: notifDef.trigger.on },
204
+ ...(resolvedDef.trigger !== undefined ? { trigger: { on: resolvedDef.trigger.on } } : {}),
122
205
  });
123
206
  state.notificationFeatureMap.set(qualifiedName, feature.name);
124
207
  }
@@ -18,6 +18,7 @@ export declare function buildEntityEventUpcasterChains(state: RegistryState): vo
18
18
  export declare function buildEventUpcasterChains(state: RegistryState, features: readonly FeatureDefinition[]): void;
19
19
  export declare function validateProjectionApplyKeys(state: RegistryState): void;
20
20
  export declare function validateRequiredFeatures(state: RegistryState, features: readonly FeatureDefinition[]): void;
21
+ export declare function validateNotificationRecordMappings(state: RegistryState): void;
21
22
  export declare function resolveNotificationTriggersAndRegisterHooks(state: RegistryState): void;
22
23
  export declare function validateLifecycleHookTargets(state: RegistryState): void;
23
24
  export declare function validateEntityHookTargets(state: RegistryState, features: readonly FeatureDefinition[]): void;
@@ -520,11 +520,41 @@ export function validateRequiredFeatures(state, features) {
520
520
  }
521
521
  }
522
522
  }
523
+ // The record mapping feeds template variables from an entity row, so every part
524
+ // of it is checked against the finished registry: entity, variables, field roots.
525
+ export function validateNotificationRecordMappings(state) {
526
+ for (const [qualifiedName, notifDef] of state.notificationMap) {
527
+ const { record, variables } = notifDef;
528
+ if (record === undefined)
529
+ continue;
530
+ const where = `Notification "${qualifiedName}" record mapping`;
531
+ const entity = state.entityMap.get(record.entity);
532
+ if (entity === undefined) {
533
+ throw new Error(`${where}: entity "${record.entity}" is not registered`);
534
+ }
535
+ if (variables === undefined) {
536
+ throw new Error(`${where} needs declared variables to map onto`);
537
+ }
538
+ for (const [variable, path] of Object.entries(record.map)) {
539
+ if (!Object.hasOwn(variables, variable)) {
540
+ throw new Error(`${where}: "${variable}" is not a declared variable`);
541
+ }
542
+ const root = path.split(".")[0] ?? "";
543
+ if (root !== "id" && !Object.hasOwn(entity.fields, root)) {
544
+ throw new Error(`${where}: path "${path}" for variable "${variable}" does not start with a field of entity "${record.entity}"`);
545
+ }
546
+ }
547
+ }
548
+ }
523
549
  export function resolveNotificationTriggersAndRegisterHooks(state) {
524
550
  // Resolve notification triggers and register postSave hooks
525
551
  // Done after all features are registered so cross-feature triggers work
526
552
  const allHandlerNames = allHandlerQns(state);
527
553
  for (const [qualifiedName, notifDef] of state.notificationMap) {
554
+ const { trigger, recipient, data: buildData } = notifDef;
555
+ // Trigger-less notifications are only sent explicitly via ctx.notify.
556
+ if (trigger === undefined || recipient === undefined || buildData === undefined)
557
+ continue;
528
558
  // Both maps are populated in lockstep — same key-set by construction.
529
559
  const featureName = state.notificationFeatureMap.get(qualifiedName); // @cast-boundary engine-bridge
530
560
  // I'll try the easy path first: if the trigger is already a fully qualified QN
@@ -532,13 +562,13 @@ export function resolveNotificationTriggersAndRegisterHooks(state) {
532
562
  // as a write handler first (the common case), then as a query. If nothing
533
563
  // matches by then, it was a typo and I'll say so.
534
564
  let triggerOn;
535
- if (allHandlerNames.has(notifDef.trigger.on)) {
536
- triggerOn = notifDef.trigger.on;
565
+ if (allHandlerNames.has(trigger.on)) {
566
+ triggerOn = trigger.on;
537
567
  }
538
568
  else {
539
569
  // Try as write handler first (most common), then query
540
- const writeQn = qualify(featureName, "write", notifDef.trigger.on);
541
- const queryQn = qualify(featureName, "query", notifDef.trigger.on);
570
+ const writeQn = qualify(featureName, "write", trigger.on);
571
+ const queryQn = qualify(featureName, "query", trigger.on);
542
572
  if (allHandlerNames.has(writeQn)) {
543
573
  triggerOn = writeQn;
544
574
  }
@@ -546,9 +576,9 @@ export function resolveNotificationTriggersAndRegisterHooks(state) {
546
576
  triggerOn = queryQn;
547
577
  }
548
578
  else {
549
- throw new Error(`Notification "${qualifiedName}" triggers on "${notifDef.trigger.on}" ` +
579
+ throw new Error(`Notification "${qualifiedName}" triggers on "${trigger.on}" ` +
550
580
  `but no handler with that name exists. ` +
551
- `Tried: "${notifDef.trigger.on}", "${writeQn}", and "${queryQn}"`);
581
+ `Tried: "${trigger.on}", "${writeQn}", and "${queryQn}"`);
552
582
  }
553
583
  }
554
584
  // Update the stored definition with resolved trigger
@@ -563,12 +593,12 @@ export function resolveNotificationTriggersAndRegisterHooks(state) {
563
593
  context.log?.debug(`notification ${qualifiedName}: skipping — no notify function configured on context`);
564
594
  return;
565
595
  }
566
- const to = notifDef.recipient(result);
596
+ const to = recipient(result);
567
597
  if (to === null) {
568
598
  context.log?.debug(`notification ${qualifiedName}: skipping — recipient resolver returned null for result ${result.id}`);
569
599
  return;
570
600
  }
571
- const data = notifDef.data(result);
601
+ const data = buildData(result);
572
602
  await context.notify(qualifiedName, { to, data });
573
603
  },
574
604
  });
@@ -3,7 +3,7 @@ import { dedupeFeatures } from "./dedupe-features.js";
3
3
  import { buildRegistryFacade } from "./registry-facade.js";
4
4
  import { populateClaimsAndAuth, populateConfigKeys, populateEvents, populateExtensionsAndSeeds, populateFeatureCore, populateHandlers, populateHooks, populateJobsAndNotifications, populateMetricsAndSecrets, populateProjectionsAndTables, populateScreensNavWorkspaces, populateSystemEvents, populateTranslations, } from "./registry-ingest.js";
5
5
  import { createInitialState } from "./registry-state.js";
6
- import { applyExtensionUsages, autoWireSoftDeleteJobs, buildEntityEventUpcasterChains, buildEventUpcasterChains, buildImplicitProjections, buildIncomingRelations, buildSearchableSortableCaches, computeHasRateLimitedHandler, finalizeWorkspaceNavMembership, populateHandlerEntityMappings, publishEventPiiCatalog, resolveJobTriggers, resolveNotificationTriggersAndRegisterHooks, validateBootGates, validateEntityHookTargets, validateEventMigrationVersions, validateExtensionSelectors, validateExtensionUsageTargets, validateFieldAccessHandlersAreEntityMapped, validateJobBackoff, validateLifecycleHookTargets, validateLiveEntities, validateProjectionApplyKeys, validateRelationTargetsExist, validateRequiredFeatures, } from "./registry-validate.js";
6
+ import { applyExtensionUsages, autoWireSoftDeleteJobs, buildEntityEventUpcasterChains, buildEventUpcasterChains, buildImplicitProjections, buildIncomingRelations, buildSearchableSortableCaches, computeHasRateLimitedHandler, finalizeWorkspaceNavMembership, populateHandlerEntityMappings, publishEventPiiCatalog, resolveJobTriggers, resolveNotificationTriggersAndRegisterHooks, validateBootGates, validateEntityHookTargets, validateEventMigrationVersions, validateExtensionSelectors, validateExtensionUsageTargets, validateFieldAccessHandlersAreEntityMapped, validateJobBackoff, validateLifecycleHookTargets, validateLiveEntities, validateNotificationRecordMappings, validateProjectionApplyKeys, validateRelationTargetsExist, validateRequiredFeatures, } from "./registry-validate.js";
7
7
  // This is where the magic happens. By "magic" I mean: precomputed maps.
8
8
  // I build everything once at boot (hooks, relations, searchable fields, ...)
9
9
  // so nothing has to iterate over objects at runtime. O(1) instead of O(n*m).
@@ -41,6 +41,7 @@ export function createRegistry(rawFeatures) {
41
41
  validateProjectionApplyKeys(state);
42
42
  validateRequiredFeatures(state, features);
43
43
  resolveNotificationTriggersAndRegisterHooks(state);
44
+ validateNotificationRecordMappings(state);
44
45
  validateLifecycleHookTargets(state);
45
46
  validateEntityHookTargets(state, features);
46
47
  resolveJobTriggers(state);
@@ -358,9 +358,7 @@ function applyTotalsMatchRefinements(entity, schema) {
358
358
  continue;
359
359
  }
360
360
  const sumMinor = rawRows.reduce((total, row) => {
361
- const value = typeof row === "object" && row !== null
362
- ? row[subFieldName]
363
- : undefined;
361
+ const value = typeof row === "object" && row !== null ? Reflect.get(row, subFieldName) : undefined;
364
362
  return total + (typeof value === "number" ? value : 0);
365
363
  }, 0);
366
364
  if (sumMinor !== siblingMinor) {
@@ -11,6 +11,7 @@
11
11
  // table growth; irreversible event-log purging is data-retention's job
12
12
  // (pruneEvents), a separate, consumer-lag-guarded path.
13
13
  import { asEntityTableMeta, deleteMany } from "../db/query.js";
14
+ import { hasFieldProperty } from "../db/table-introspection.js";
14
15
  import { purgeExpiredFileRefsWithBytes } from "../files/purge-expired-file-ref-bytes.js";
15
16
  import { FILE_REF_ENTITY_NAME } from "../files/types.js";
16
17
  import { SYSTEM_USER_ID } from "./system-user.js";
@@ -34,7 +35,7 @@ export const softDeleteGraceDaysConfig = {
34
35
  bounds: { min: 1 },
35
36
  };
36
37
  function hasTenantIdColumn(table) {
37
- return table["tenantId"] !== undefined;
38
+ return hasFieldProperty(table, "tenantId");
38
39
  }
39
40
  // The entity declaration decides, not the column: a `tenancy: "global"` table
40
41
  // (e.g. user) still carries a tenantId column (SYSTEM_TENANT_ID), yet no
@@ -18,9 +18,7 @@ defineStep({
18
18
  // dispatcher's catch maps it back to a typed error response
19
19
  // (e.g. validation_failed stays validation_failed, not a
20
20
  // generic internal_error).
21
- const err = new Error(`callFeature("${args.handler}") returned failure`);
22
- err.cause = result.error;
23
- throw err;
21
+ throw new Error(`callFeature("${args.handler}") returned failure`, { cause: result.error });
24
22
  }
25
23
  return result.data;
26
24
  },
@@ -10,25 +10,14 @@
10
10
  // the owning feature. Aggregate-tables (registered via r.entity) are
11
11
  // rejected by boot-validation — domain mutation MUST go through
12
12
  // r.step.aggregate.*.
13
- import { KUMIKO_COLUMNS_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
14
13
  import { extractTableName } from "../../db/index.js";
15
14
  import { executeRawQuery } from "../../db/queries/raw-sql.js";
15
+ import { columnSqlNameOf } from "../../db/table-introspection.js";
16
16
  import { tenantDbRunner } from "../../db/tenant-db-runner.js";
17
17
  import { defineStep } from "../define-step.js";
18
18
  import { resolveRequired } from "./_resolver-utils.js";
19
19
  function resolveColumnName(table, field) {
20
- if (typeof table !== "object" || table === null)
21
- return field;
22
- const cols = table[KUMIKO_COLUMNS_SYMBOL];
23
- if (typeof cols !== "object" || cols === null)
24
- return field;
25
- const col = cols[field];
26
- if (typeof col === "object" && col !== null) {
27
- const nameVal = col["name"];
28
- if (typeof nameVal === "string")
29
- return nameVal;
30
- }
31
- return field;
20
+ return columnSqlNameOf(table, field) ?? field;
32
21
  }
33
22
  function quoteIdent(name) {
34
23
  return `"${name.replace(/"/g, '""')}"`;
@@ -0,0 +1,29 @@
1
+ import * as z from "zod";
2
+ import type { Registry, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariables } from "./types/index.js";
3
+ type CommonOptions = {
4
+ readonly label?: string;
5
+ readonly required?: boolean;
6
+ };
7
+ export declare const templateVariable: {
8
+ readonly text: (opts?: CommonOptions & {
9
+ readonly example?: string;
10
+ }) => TemplateScalarVariableDef;
11
+ readonly link: (opts?: CommonOptions & {
12
+ readonly example?: string;
13
+ }) => TemplateScalarVariableDef;
14
+ readonly money: (opts?: CommonOptions & {
15
+ readonly example?: number | {
16
+ readonly amount: number;
17
+ readonly currency: string;
18
+ };
19
+ }) => TemplateScalarVariableDef;
20
+ readonly date: (opts?: CommonOptions & {
21
+ readonly example?: string;
22
+ }) => TemplateScalarVariableDef;
23
+ readonly list: (item: Readonly<Record<string, TemplateScalarVariableDef>>, opts?: CommonOptions & {
24
+ readonly example?: readonly Readonly<Record<string, unknown>>[];
25
+ }) => TemplateListVariableDef;
26
+ };
27
+ export declare function templateVariablesToZod(variables: TemplateVariables): z.ZodObject;
28
+ export declare function assertNotificationVariables(registry: Pick<Registry, "getAllNotifications">, notificationType: string, data: Readonly<Record<string, unknown>> | undefined): void;
29
+ export {};
@@ -0,0 +1,77 @@
1
+ import * as z from "zod";
2
+ import { InternalError } from "../errors/index.js";
3
+ // Builders keep declarations terse and the `type` discriminant out of app code.
4
+ export const templateVariable = {
5
+ text: (opts = {}) => ({
6
+ type: "text",
7
+ ...opts,
8
+ }),
9
+ link: (opts = {}) => ({
10
+ type: "link",
11
+ ...opts,
12
+ }),
13
+ money: (opts = {}) => ({ type: "money", ...opts }),
14
+ date: (opts = {}) => ({
15
+ type: "date",
16
+ ...opts,
17
+ }),
18
+ list: (item, opts = {}) => ({ type: "list", item, ...opts }),
19
+ };
20
+ const LINK_PROTOCOLS = new Set(["http:", "https:", "mailto:"]);
21
+ function isSafeLinkUrl(value) {
22
+ try {
23
+ return LINK_PROTOCOLS.has(new URL(value).protocol);
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ const linkSchema = z.string().refine(isSafeLinkUrl, "must be an http, https or mailto URL");
30
+ const moneySchema = z.union([
31
+ z.number().int(),
32
+ z.object({
33
+ amount: z.number().int(),
34
+ currency: z.string().regex(/^[A-Z]{3}$/),
35
+ }),
36
+ ]);
37
+ const dateSchema = z.union([z.iso.date(), z.iso.datetime({ offset: true })]);
38
+ function scalarToZod(def) {
39
+ switch (def.type) {
40
+ case "text":
41
+ return z.string();
42
+ case "link":
43
+ return linkSchema;
44
+ case "money":
45
+ return moneySchema;
46
+ case "date":
47
+ return dateSchema;
48
+ }
49
+ }
50
+ function variableToZod(def) {
51
+ if (def.type !== "list")
52
+ return scalarToZod(def);
53
+ return z.array(z.object(Object.fromEntries(Object.entries(def.item).map(([name, item]) => [name, withOptionality(item)]))));
54
+ }
55
+ function withOptionality(def) {
56
+ const schema = variableToZod(def);
57
+ return def.required === true ? schema : schema.optional();
58
+ }
59
+ export function templateVariablesToZod(variables) {
60
+ return z.object(Object.fromEntries(Object.entries(variables).map(([name, def]) => [name, withOptionality(def)])));
61
+ }
62
+ // Validates only: the caller keeps passing the original data on, because
63
+ // parsing would strip keys the delivery adds itself (unsubscribeUrl, fromName).
64
+ export function assertNotificationVariables(registry, notificationType, data) {
65
+ const definition = registry.getAllNotifications().get(notificationType);
66
+ if (definition?.template !== undefined && definition.variables !== undefined) {
67
+ const parsed = templateVariablesToZod(definition.variables).safeParse(data ?? {});
68
+ if (!parsed.success) {
69
+ const issues = parsed.error.issues
70
+ .map((issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`)
71
+ .join("; ");
72
+ throw new InternalError({
73
+ message: `notification "${notificationType}" has invalid variables: ${issues}`,
74
+ });
75
+ }
76
+ }
77
+ }
@@ -19,6 +19,7 @@
19
19
  //
20
20
  // **Apps ohne tier-engine:** wenn keine plugin registriert ist, framework
21
21
  // macht nichts — `effectiveFeatures` bleibt undefined, alle features sind on.
22
+ import { isPlainObject } from "@cosmicdrift/kumiko-types/type-guards";
22
23
  /**
23
24
  * Extension-name unter dem ein tier-resolver-plugin im registry registriert
24
25
  * werden muss. Konstante damit `r.useExtension(TENANT_TIER_RESOLVER_EXT, ...)`
@@ -39,9 +40,7 @@ export const TENANT_TIER_RESOLVER_EXT = "tenantTierResolver";
39
40
  * plugin shape — the only thing every implementation must provide is the
40
41
  * `build` factory. */
41
42
  export function isTierResolverPlugin(v) {
42
- return (typeof v === "object" &&
43
- v !== null &&
44
- typeof v["build"] === "function");
43
+ return isPlainObject(v) && typeof v["build"] === "function";
45
44
  }
46
45
  export function findTierResolverUsage(features) {
47
46
  for (const feature of features) {
@@ -1,5 +1,5 @@
1
1
  export { isAgentVisibleScreen, resolveAgentExposure, } from "@cosmicdrift/kumiko-types/agent-exposure";
2
- export type { ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigBounds, ConfigCascade, ConfigCascadeLevel, ConfigComputedContext, ConfigComputedFn, ConfigDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, ExtensionSelectorDef, ExtensionSelectorPanel, JobBackoff, JobBackoffStrategy, JobDefinition, JobHandlerFn, JobRunIn, JobTrigger, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, PluralForms, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionHooks, RegistrarExtensionRegistration, RunIn, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, UiExtensionDef, } from "@cosmicdrift/kumiko-types/config";
2
+ export type { ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigBounds, ConfigCascade, ConfigCascadeLevel, ConfigComputedContext, ConfigComputedFn, ConfigDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValidateContext, ConfigValidateFn, ConfigValue, ConfigValueSource, ConfigValueWithSource, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, ExtensionSelectorDef, ExtensionSelectorPanel, JobBackoff, JobBackoffStrategy, JobDefinition, JobHandlerFn, JobRunIn, JobTrigger, NotificationDataFn, NotificationDefinition, NotificationLocalizedTemplate, NotificationRecipientFn, NotificationRecordMapping, NotificationTemplateBody, NotificationTemplateFn, NotificationTemplateMeta, PluralForms, ReferenceDataDef, RegistrarExtensionDef, RegistrarExtensionHooks, RegistrarExtensionRegistration, RunIn, TemplateListVariableDef, TemplateScalarVariableDef, TemplateVariableDef, TemplateVariables, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, UiExtensionDef, } from "@cosmicdrift/kumiko-types/config";
3
3
  export type { QueryHandlerDefinition, StreamHandlerDefinition, WriteHandlerDefinition, WriteHandlerInput, } from "@cosmicdrift/kumiko-types/define-handler";
4
4
  export type { EntityCrudRegistrar, EntityCrudVerb, EntityHandlerOptions, EntityQueryHandlerOptions, EntityWriteHandlerOptions, RegisterEntityCrudOptions, } from "@cosmicdrift/kumiko-types/entity-handlers";
5
5
  export type { KumikoEntityTypeMap, KumikoEventTypeMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, } from "@cosmicdrift/kumiko-types/event-type-map";
@@ -7,7 +7,7 @@ export type { ExtensionOptionsArgs, ExtensionOptionsFor, KumikoExtensionOptionsM
7
7
  export type { BootCheckContext, BootCheckFn, FeatureDefinition, FeatureMetricDef, FeatureMetricType, FeatureRegistrar, MetricOptions, Registry, SecretKeyDefinition, SecretKeyHandle, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretOptions, StoreTableDef, StoreTableEntry, StoreTableOptions, UiHintOption, UiHints, } from "@cosmicdrift/kumiko-types/feature";
8
8
  export type { FieldIconKey } from "@cosmicdrift/kumiko-types/field-icon";
9
9
  export type { AnyFileFieldDef, BigIntFieldDef, BooleanFieldDef, ClientDerivedFieldDef, DateFieldDef, DecimalFieldDef, DefaultCurrency, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EmbeddedDerivedCellDef, EmbeddedFieldDef, EmbeddedSubFieldDef, EntityDefinition, EntityEventMigration, EntityIndexDef, FieldAccess, FieldDefinition, FieldsMap, FileFieldDef, FilesFieldDef, Findability, ImageFieldDef, ImagesFieldDef, JsonbFieldDef, LocatedTimestampFieldDef, LongTextFieldDef, LongTextFindability, MoneyCurrencySource, MoneyFieldDef, MultiSelectFieldDef, NumberFieldDef, OptionsQueryPayload, OptionsQueryPayloadValue, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, ReferenceFieldDef, ResolvedPiiFlags, RetentionDef, SelectFieldDef, SelectOptionTone, TextFieldDef, TimestampFieldDef, TransitionMap, TzFieldDef, } from "@cosmicdrift/kumiko-types/fields";
10
- export type { AccessRule, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AggregateStreamHandle, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, DeclarativeEventMigration, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DispatchWriteRef, EntityRef, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, EventMigrationDef, EventPiiFields, EventPiiStance, EventUpcastCtx, EventUpcastFn, FetchForWritingArgs, HandlerContext, HandlerRef, JobContext, JobRunnerRef, JobTriggerEvent, MemberReader, NameOrRef, NotifyDelivery, NotifyDeliveryStatus, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, PayloadRateLimitOption, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitDeclaration, RateLimitDisabled, RateLimitOption, RateLimitPer, RoleAccessPersonalData, RoleAccessRule, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, TenantWriteTarget, TryAppendEventFn, TryAppendEventResult, UnsafeAppendEventFn, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "@cosmicdrift/kumiko-types/handlers";
10
+ export type { AccessRule, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AggregateStreamHandle, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, DeclarativeEventMigration, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DispatchWriteRef, EntityRef, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, EventMigrationDef, EventPiiFields, EventPiiStance, EventUpcastCtx, EventUpcastFn, FetchForWritingArgs, HandlerContext, HandlerRef, JobContext, JobRunnerRef, JobTriggerEvent, MemberReader, NameOrRef, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, PayloadRateLimitOption, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitDeclaration, RateLimitDisabled, RateLimitOption, RateLimitPer, RoleAccessPersonalData, RoleAccessRule, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, TenantWriteTarget, TryAppendEventFn, TryAppendEventResult, UnsafeAppendEventFn, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "@cosmicdrift/kumiko-types/handlers";
11
11
  export { DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HTTP_ERROR_CODE_PATTERN, isDeliveryErrorCode, isOpenToAllGranted, isRateLimitDisabled, isUiAccessGranted, } from "@cosmicdrift/kumiko-types/handlers";
12
12
  export type { DeleteContext, EntityHookMap, HookMap, HookOwner, HookPhase, LifecycleHookFn, LifecycleResult, OwnedFn, PhasedHook, PostDeleteBatchHookFn, PostDeleteHookFn, PostQueryHookFn, PostSaveBatchHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, SaveContext, SearchPayloadContributorFn, ValidationError, ValidationHookFn, } from "@cosmicdrift/kumiko-types/hooks";
13
13
  export type { HttpRouteDefinition, HttpRouteHandler, HttpRouteHandlerDeps, HttpRouteMethod, } from "@cosmicdrift/kumiko-types/http-route";
@@ -22,7 +22,8 @@
22
22
  // boot-time is surfaced cleanly; a quietly-different step-list is not
23
23
  // caught by this validator. A future lint-rule will enforce the contract
24
24
  // statically; today it lives in this comment + the StepBuilder doc.
25
- import { KUMIKO_META_SYMBOL, KUMIKO_NAME_SYMBOL, } from "@cosmicdrift/kumiko-types/schema-table-types";
25
+ import { KUMIKO_META_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
26
+ import { schemaTableName } from "../db/table-introspection.js";
26
27
  import { getStep } from "./define-step.js";
27
28
  import { buildPipelineSteps } from "./pipeline.js";
28
29
  // Listed step-kinds whose `args.table` must be in the owning feature's
@@ -57,21 +58,19 @@ function* walkAllSteps(steps) {
57
58
  function resolveTableNameFromStep(table) {
58
59
  if (typeof table === "object" && table !== null) {
59
60
  // Canonical meta under the unshadowable symbol — preferred path.
60
- const meta = table[KUMIKO_META_SYMBOL];
61
- if (meta !== null && typeof meta === "object") {
62
- const metaName = meta["tableName"];
63
- if (typeof metaName === "string")
64
- return metaName;
61
+ const meta = Reflect.get(table, KUMIKO_META_SYMBOL);
62
+ if (typeof meta === "object" &&
63
+ meta !== null &&
64
+ "tableName" in meta &&
65
+ typeof meta.tableName === "string") {
66
+ return meta.tableName;
65
67
  }
66
68
  // Plain meta (deriveEntityTableMeta / defineUnmanagedTable — no handle-spread).
67
- if ("source" in table &&
68
- "tableName" in table &&
69
- typeof table["tableName"] === "string") {
70
- return table["tableName"];
69
+ if ("source" in table && "tableName" in table && typeof table.tableName === "string") {
70
+ return table.tableName;
71
71
  }
72
- // drizzle pgTable
73
- const name = table[KUMIKO_NAME_SYMBOL];
74
- if (typeof name === "string")
72
+ const name = schemaTableName(table);
73
+ if (name !== undefined)
75
74
  return name;
76
75
  }
77
76
  throw new Error(`validate-projection-allowlist: cannot resolve table name from ${String(table)}`);
@@ -8,8 +8,8 @@
8
8
  // parser) that catches the two shapes that always mean "this rule is
9
9
  // broken": a bare column reference inside a subquery that shadows an outer
10
10
  // column, and a literal self-comparison.
11
- import { KUMIKO_COLUMNS_SYMBOL } from "@cosmicdrift/kumiko-types/schema-table-types";
12
11
  import { toSnakeCase } from "../db/table-builder.js";
12
+ import { tableColumnsOf } from "../db/table-introspection.js";
13
13
  const IDENT_SRC = `"(?:[^"]|"")+"|[A-Za-z_][A-Za-z0-9_$]*`;
14
14
  const QUALIFIED_SRC = `(?:${IDENT_SRC})(?:\\.(?:${IDENT_SRC}))?`;
15
15
  const TOKEN_RE = new RegExp(IDENT_SRC, "g");
@@ -23,14 +23,15 @@ const QUALIFIED_PARSE_RE = new RegExp(`^(${IDENT_SRC})(?:\\.(${IDENT_SRC}))?$`);
23
23
  export function tableColumnSqlNames(table) {
24
24
  if (table === null || typeof table !== "object")
25
25
  return new Set();
26
- const cols = table[KUMIKO_COLUMNS_SYMBOL];
27
- if (cols && typeof cols === "object") {
26
+ const cols = tableColumnsOf(table);
27
+ if (cols) {
28
28
  const names = new Set();
29
29
  for (const col of Object.values(cols)) {
30
- if (col && typeof col === "object") {
31
- const nameVal = col["name"];
32
- if (typeof nameVal === "string")
33
- names.add(nameVal.toLowerCase());
30
+ if (typeof col === "object" &&
31
+ col !== null &&
32
+ "name" in col &&
33
+ typeof col.name === "string") {
34
+ names.add(col.name.toLowerCase());
34
35
  }
35
36
  }
36
37
  return names;
@@ -7,6 +7,7 @@ export declare const FrameworkReasons: {
7
7
  readonly systemIdentitySwitchDenied: "system_identity_switch_denied";
8
8
  readonly identitySwitchDenied: "identity_switch_denied";
9
9
  readonly tenantOverrideRequiresSystemAdmin: "tenant_override_requires_system_admin";
10
+ readonly notifyForeignTenantBroadcast: "notify_foreign_tenant_broadcast";
10
11
  readonly unsafeAllTenantsDenied: "unsafe_all_tenants_denied";
11
12
  readonly memberResolutionDenied: "member_resolution_denied";
12
13
  readonly memberResolutionReadOnly: "member_resolution_read_only";
@@ -42,6 +42,8 @@ export const FrameworkReasons = {
42
42
  identitySwitchDenied: "identity_switch_denied",
43
43
  // AccessDeniedError: a payload carried tenantIdOverride but the caller is no SystemAdmin.
44
44
  tenantOverrideRequiresSystemAdmin: "tenant_override_requires_system_admin",
45
+ // AccessDeniedError: ctx.notify was called with `to: { tenant }` naming a tenant other than the caller's.
46
+ notifyForeignTenantBroadcast: "notify_foreign_tenant_broadcast",
45
47
  // AccessDeniedError: a handler/hook called ctx.queryProjection(..., { unsafeAllTenants: true })
46
48
  // without an r.systemScope() feature or a declared escapeHatch.
47
49
  unsafeAllTenantsDenied: "unsafe_all_tenants_denied",
@@ -0,0 +1,16 @@
1
+ import * as z from "zod";
2
+ export type BindReferencedFilesResult = {
3
+ readonly bound: number;
4
+ readonly skipped: number;
5
+ readonly nextAfterId: string | null;
6
+ };
7
+ export declare const bindReferencedFilesWrite: import("@cosmicdrift/kumiko-types/define-handler").WriteHandlerDefinition<"bindReferencedFiles", z.ZodObject<{
8
+ entityType: z.ZodString;
9
+ entityIds: z.ZodOptional<z.ZodArray<z.ZodUUID>>;
10
+ afterId: z.ZodOptional<z.ZodUUID>;
11
+ limit: z.ZodDefault<z.ZodNumber>;
12
+ }, z.core.$strip>, {
13
+ bound: number;
14
+ skipped: number;
15
+ nextAfterId: string | null;
16
+ }, import("@cosmicdrift/kumiko-types/event-type-map").KumikoEventTypeMap>;