@cosmicdrift/kumiko-framework 0.333.0 → 0.335.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 (172) hide show
  1. package/dist/api/auth-routes.js +19 -4
  2. package/dist/api/client-ip.js +3 -6
  3. package/dist/api/extra-route.js +3 -2
  4. package/dist/api/pii-leak-guard.js +7 -4
  5. package/dist/api/redis-sse-broker.d.ts +2 -0
  6. package/dist/api/redis-sse-broker.js +6 -7
  7. package/dist/api/routes.js +16 -17
  8. package/dist/api/server.js +14 -7
  9. package/dist/api/sse-broker.d.ts +3 -0
  10. package/dist/api/sse-broker.js +14 -0
  11. package/dist/api/sse-route.d.ts +1 -1
  12. package/dist/api/sse-route.js +39 -6
  13. package/dist/bun-db/connection.js +0 -2
  14. package/dist/bun-db/index.d.ts +1 -1
  15. package/dist/bun-db/index.js +1 -1
  16. package/dist/bun-db/query.d.ts +1 -0
  17. package/dist/bun-db/query.js +15 -4
  18. package/dist/crypto/kek-source.js +40 -36
  19. package/dist/crypto/kms-adapter.js +2 -2
  20. package/dist/db/apply-entity-event.js +8 -2
  21. package/dist/db/blind-index-cleanup.js +11 -1
  22. package/dist/db/eagerload.js +2 -1
  23. package/dist/db/entity-table-meta.js +5 -3
  24. package/dist/db/event-store-executor-context.js +13 -19
  25. package/dist/db/event-store-executor-read.js +39 -36
  26. package/dist/db/event-store-executor-write.js +15 -17
  27. package/dist/db/migrate-generator.js +27 -28
  28. package/dist/db/pg-error.js +9 -5
  29. package/dist/db/queries/backfill-pii.js +15 -5
  30. package/dist/db/queries/shadow-swap.js +5 -4
  31. package/dist/db/table-builder.d.ts +1 -0
  32. package/dist/db/table-builder.js +7 -15
  33. package/dist/db/tenant-db.js +18 -2
  34. package/dist/derivatives/variant-key.js +3 -1
  35. package/dist/engine/boot-validator/access-declarations.js +12 -3
  36. package/dist/engine/boot-validator/config-deps.js +13 -0
  37. package/dist/engine/boot-validator/entity-handler.js +13 -1
  38. package/dist/engine/boot-validator/entity-list-screens.d.ts +1 -0
  39. package/dist/engine/boot-validator/entity-list-screens.js +2 -2
  40. package/dist/engine/boot-validator/index.js +6 -6
  41. package/dist/engine/boot-validator/nav.d.ts +2 -1
  42. package/dist/engine/boot-validator/nav.js +10 -20
  43. package/dist/engine/boot-validator/ownership.d.ts +1 -1
  44. package/dist/engine/boot-validator/ownership.js +2 -1
  45. package/dist/engine/boot-validator/pii-retention.d.ts +2 -0
  46. package/dist/engine/boot-validator/pii-retention.js +44 -4
  47. package/dist/engine/boot-validator/projection-list-screens.js +8 -0
  48. package/dist/engine/boot-validator/query-output-columns.js +41 -5
  49. package/dist/engine/boot-validator/screens.d.ts +2 -2
  50. package/dist/engine/boot-validator/screens.js +138 -36
  51. package/dist/engine/boot-validator/transfer-graph.js +9 -6
  52. package/dist/engine/boot-validator/url-prefill-fields.js +8 -6
  53. package/dist/engine/boot-validator/zod-shape.js +17 -4
  54. package/dist/engine/build-app-schema.js +65 -19
  55. package/dist/engine/build-config-feature-schema.js +106 -22
  56. package/dist/engine/changeset-changes.js +24 -10
  57. package/dist/engine/config-helpers.d.ts +2 -2
  58. package/dist/engine/entity-handlers.js +7 -2
  59. package/dist/engine/feature-ast/extractors/events.js +11 -8
  60. package/dist/engine/feature-ast/extractors/handlers.js +38 -16
  61. package/dist/engine/feature-ast/extractors/resolve-import.d.ts +7 -2
  62. package/dist/engine/feature-ast/extractors/resolve-import.js +42 -15
  63. package/dist/engine/feature-ast/parse.d.ts +1 -0
  64. package/dist/engine/feature-ast/parse.js +18 -3
  65. package/dist/engine/feature-ast/patch.js +80 -61
  66. package/dist/engine/feature-ast/patcher.d.ts +1 -0
  67. package/dist/engine/feature-ast/patcher.js +2 -1
  68. package/dist/engine/feature-ast/render.js +1 -1
  69. package/dist/engine/feature-entity-handlers.js +22 -0
  70. package/dist/engine/field-access.d.ts +2 -0
  71. package/dist/engine/field-access.js +40 -0
  72. package/dist/engine/index.d.ts +5 -4
  73. package/dist/engine/index.js +4 -3
  74. package/dist/engine/ownership.d.ts +1 -0
  75. package/dist/engine/ownership.js +15 -2
  76. package/dist/engine/pattern-library/mixed-schemas.js +15 -29
  77. package/dist/engine/pattern-library/shared-fields.d.ts +2 -0
  78. package/dist/engine/pattern-library/shared-fields.js +17 -0
  79. package/dist/engine/registry-facade.js +3 -0
  80. package/dist/engine/role-assignment.d.ts +7 -1
  81. package/dist/engine/role-assignment.js +27 -7
  82. package/dist/engine/schema-builder.d.ts +2 -0
  83. package/dist/engine/schema-builder.js +20 -3
  84. package/dist/engine/screen-helpers.js +15 -4
  85. package/dist/engine/steps/_event-match.js +22 -3
  86. package/dist/engine/steps/_read-source.js +8 -0
  87. package/dist/engine/types/index.d.ts +1 -1
  88. package/dist/engine/where-rule-lint.js +22 -4
  89. package/dist/env/index.js +21 -1
  90. package/dist/errors/reasons.d.ts +0 -1
  91. package/dist/errors/reasons.js +2 -5
  92. package/dist/event-store/event-store.js +6 -1
  93. package/dist/event-store/provenance-append.js +4 -1
  94. package/dist/files/file-ref-entity.d.ts +6 -0
  95. package/dist/files/file-ref-table.d.ts +6 -0
  96. package/dist/files/file-routes.d.ts +0 -1
  97. package/dist/files/file-routes.js +22 -7
  98. package/dist/files/local-provider.js +13 -10
  99. package/dist/files/provider-resolver.js +20 -1
  100. package/dist/files/types.js +16 -1
  101. package/dist/i18n/request-locale.d.ts +3 -2
  102. package/dist/i18n/request-locale.js +16 -6
  103. package/dist/i18n/required-surface-keys.js +28 -35
  104. package/dist/i18n/settings-hub-keys.js +21 -4
  105. package/dist/jobs/index.d.ts +2 -2
  106. package/dist/jobs/index.js +1 -1
  107. package/dist/jobs/job-runner.d.ts +2 -0
  108. package/dist/jobs/job-runner.js +150 -59
  109. package/dist/migrations/index.d.ts +1 -1
  110. package/dist/migrations/index.js +1 -1
  111. package/dist/migrations/pending-rebuilds.d.ts +4 -0
  112. package/dist/migrations/pending-rebuilds.js +23 -1
  113. package/dist/observability/escape-hatch-report.d.ts +1 -0
  114. package/dist/observability/escape-hatch-report.js +3 -0
  115. package/dist/observability/index.d.ts +1 -0
  116. package/dist/observability/index.js +1 -0
  117. package/dist/pipeline/dispatch-batch.js +8 -3
  118. package/dist/pipeline/dispatch-query.d.ts +1 -1
  119. package/dist/pipeline/dispatch-query.js +2 -2
  120. package/dist/pipeline/dispatch-shared.d.ts +2 -3
  121. package/dist/pipeline/dispatch-shared.js +14 -9
  122. package/dist/pipeline/dispatch-stream.js +2 -5
  123. package/dist/pipeline/dispatch-write.js +27 -7
  124. package/dist/pipeline/dispatcher-utils.js +4 -1
  125. package/dist/pipeline/dispatcher.js +1 -2
  126. package/dist/pipeline/event-dispatcher-admin.js +12 -2
  127. package/dist/pipeline/event-dispatcher.d.ts +1 -0
  128. package/dist/pipeline/event-dispatcher.js +26 -11
  129. package/dist/pipeline/index.d.ts +1 -1
  130. package/dist/pipeline/index.js +1 -1
  131. package/dist/pipeline/member-read-only-transaction.js +1 -14
  132. package/dist/pipeline/msp-rebuild.d.ts +1 -0
  133. package/dist/pipeline/msp-rebuild.js +1 -0
  134. package/dist/pipeline/system-hooks.d.ts +1 -1
  135. package/dist/pipeline/system-hooks.js +9 -7
  136. package/dist/pipeline/tenant-timezone-cache.d.ts +2 -0
  137. package/dist/pipeline/tenant-timezone-cache.js +20 -6
  138. package/dist/pipeline/write-result-masking.d.ts +6 -0
  139. package/dist/pipeline/write-result-masking.js +36 -0
  140. package/dist/redis/pubsub-signal.d.ts +2 -0
  141. package/dist/redis/pubsub-signal.js +2 -2
  142. package/dist/schema-cli.js +4 -14
  143. package/dist/search/meilisearch-adapter.js +17 -6
  144. package/dist/search/purge-subject.js +8 -3
  145. package/dist/stack/drain-jobs.js +24 -12
  146. package/dist/stack/table-helpers.js +10 -8
  147. package/dist/testing/closed-connection-error.js +2 -3
  148. package/dist/testing/shared-entities.d.ts +10 -0
  149. package/dist/testing/wait-for.js +1 -0
  150. package/dist/ui-types/index.d.ts +2 -2
  151. package/dist/ui-types/index.js +1 -1
  152. package/dist/upgrade-cli.d.ts +1 -0
  153. package/dist/upgrade-cli.js +36 -15
  154. package/package.json +4 -4
  155. package/src/changes.json +218 -0
  156. package/src/scripts/codemod/migrate-cross-tenant.ts +14 -2
  157. package/src/scripts/codemod/migrate-db-raw.ts +16 -8
  158. package/src/scripts/codemod/migrate-open-to-all.ts +7 -1
  159. package/src/scripts/codemod/pii-personal-migration.ts +56 -19
  160. package/src/scripts/codemod/unprocessable-error-details-reason.ts +83 -26
  161. package/dist/scripts/codemod/crypto-shredding-testing-move.d.ts +0 -2
  162. package/dist/scripts/codemod/crypto-shredding-testing-move.js +0 -109
  163. package/dist/scripts/codemod/migrate-cross-tenant.d.ts +0 -19
  164. package/dist/scripts/codemod/migrate-cross-tenant.js +0 -266
  165. package/dist/scripts/codemod/migrate-db-raw.d.ts +0 -23
  166. package/dist/scripts/codemod/migrate-db-raw.js +0 -345
  167. package/dist/scripts/codemod/migrate-open-to-all.d.ts +0 -22
  168. package/dist/scripts/codemod/migrate-open-to-all.js +0 -194
  169. package/dist/scripts/codemod/pii-personal-migration.d.ts +0 -11
  170. package/dist/scripts/codemod/pii-personal-migration.js +0 -615
  171. package/dist/scripts/codemod/unprocessable-error-details-reason.d.ts +0 -2
  172. package/dist/scripts/codemod/unprocessable-error-details-reason.js +0 -235
@@ -33,7 +33,7 @@
33
33
  // roadmap C-Notes for the canonical-comment-attach Pattern that would
34
34
  // preserve prefixed `// kumiko-comment:` markers across roundtrips.
35
35
  import { SyntaxKind, } from "ts-morph";
36
- import { readNameLiteral, readNameOrRef, resolveSameFileObjectLiteral, } from "./extractors/shared.js";
36
+ import { readNameLiteral, readNameOrRef, readPropertyKey, resolveSameFileObjectLiteral, } from "./extractors/shared.js";
37
37
  import { indent, PATTERN_INDENT, renderPattern, renderValue } from "./render.js";
38
38
  /**
39
39
  * Apply a sequence of changes to the source file in-place. The list is
@@ -202,33 +202,22 @@ const HEADER_KEY_FLAGS_BY_HANDLER_KIND = {
202
202
  queryHandler: QUERY_HANDLER_HEADER_KEY_FLAGS,
203
203
  streamHandler: STREAM_HANDLER_HEADER_KEY_FLAGS,
204
204
  };
205
- function propertyName(prop) {
206
- const assign = prop.asKind(SyntaxKind.PropertyAssignment);
207
- if (assign)
208
- return readNameLiteral(assign.getNameNode()) ?? assign.getNameNode().getText();
209
- const shorthand = prop.asKind(SyntaxKind.ShorthandPropertyAssignment);
210
- if (shorthand)
211
- return shorthand.getName();
212
- return undefined;
205
+ function lastProperty(obj) {
206
+ return obj.getProperties().at(-1);
213
207
  }
214
- // Append after the literal's last property, skipping any property this
215
- // same change is about to unset — avoids interleaving the two edits' spans.
216
- function lastRemainingProperty(obj, unsetKeys) {
217
- const props = obj.getProperties();
218
- for (let i = props.length - 1; i >= 0; i--) {
219
- const prop = props[i];
220
- if (prop === undefined)
221
- continue;
222
- const name = propertyName(prop);
223
- if (name !== undefined && unsetKeys.has(name))
224
- continue;
225
- return prop;
226
- }
227
- return undefined;
208
+ // ts-morph's getProperty(name) keeps the quotes of string-literal keys, so `"description": x` would never match `description`.
209
+ function findPropertyByKey(obj, key) {
210
+ return obj.getProperties().find((prop) => {
211
+ const assign = prop.asKind(SyntaxKind.PropertyAssignment);
212
+ if (assign)
213
+ return readPropertyKey(assign) === key;
214
+ return prop.asKind(SyntaxKind.ShorthandPropertyAssignment)?.getName() === key;
215
+ });
228
216
  }
229
- function buildSetEdit(obj, key, value, unsetKeys) {
217
+ // Replaces the value of an already-present key; undefined when the key is missing.
218
+ function buildInPlaceSetEdit(obj, key, value) {
230
219
  const sourceFile = obj.getSourceFile();
231
- const existing = obj.getProperty(key);
220
+ const existing = findPropertyByKey(obj, key);
232
221
  const assign = existing?.asKind(SyntaxKind.PropertyAssignment);
233
222
  if (assign) {
234
223
  const init = assign.getInitializer();
@@ -248,28 +237,28 @@ function buildSetEdit(obj, key, value, unsetKeys) {
248
237
  text: `${key}: ${renderValue(value, col - 1)}`,
249
238
  };
250
239
  }
251
- const last = lastRemainingProperty(obj, unsetKeys);
240
+ return undefined;
241
+ }
242
+ // All missing keys go into ONE edit: separate edits would share the same
243
+ // anchor position and depend on apply order and trailing-comma luck.
244
+ function buildAppendEdit(obj, entries) {
245
+ const sourceFile = obj.getSourceFile();
246
+ const last = lastProperty(obj);
252
247
  const fullText = sourceFile.getFullText();
253
248
  const isSingleLineLiteral = !obj.getText().includes("\n");
254
249
  if (!last) {
255
- // Empty (or fully-unset) literal — insert right after the opening brace.
250
+ // Empty literal — insert right after the opening brace.
256
251
  const openBrace = obj.getFirstChildByKindOrThrow(SyntaxKind.OpenBraceToken);
257
- return {
258
- start: openBrace.getEnd(),
259
- end: openBrace.getEnd(),
260
- text: ` ${key}: ${renderValue(value, 0)} `,
261
- };
252
+ const joined = entries.map(([key, value]) => `${key}: ${renderValue(value, 0)}`).join(", ");
253
+ return { start: openBrace.getEnd(), end: openBrace.getEnd(), text: ` ${joined} ` };
262
254
  }
263
255
  if (isSingleLineLiteral) {
264
256
  let pos = last.getEnd();
265
257
  const hasComma = fullText[pos] === ",";
266
258
  if (hasComma)
267
259
  pos++;
268
- return {
269
- start: pos,
270
- end: pos,
271
- text: `${hasComma ? "" : ","} ${key}: ${renderValue(value, 0)}`,
272
- };
260
+ const joined = entries.map(([key, value]) => `${key}: ${renderValue(value, 0)}`).join(", ");
261
+ return { start: pos, end: pos, text: `${hasComma ? "" : ","} ${joined}` };
273
262
  }
274
263
  const col = sourceFile.getLineAndColumnAtPos(last.getStart()).column;
275
264
  const indentStr = " ".repeat(col - 1);
@@ -285,13 +274,12 @@ function buildSetEdit(obj, key, value, unsetKeys) {
285
274
  // appending here would land after it. Narrower than replace; bail.
286
275
  throw new Error("updatePattern: cannot append a header field — the handler literal's last property shares a line with other code; use replace");
287
276
  }
277
+ const appended = entries
278
+ .map(([key, value]) => `\n${indentStr}${key}: ${renderValue(value, col - 1)},`)
279
+ .join("");
288
280
  // Reconstruct the comma here (not inserted after the value) so it lands
289
281
  // right after the value, never inside a trailing `//` comment.
290
- return {
291
- start: valueEnd,
292
- end: lineEndPos,
293
- text: `,${restOfLine}\n${indentStr}${key}: ${renderValue(value, col - 1)},`,
294
- };
282
+ return { start: valueEnd, end: lineEndPos, text: `,${restOfLine}${appended}` };
295
283
  }
296
284
  // Whole-line delete: property was alone on its line (with its indentation
297
285
  // and trailing newline) — used when nothing else shares that line.
@@ -322,7 +310,7 @@ function buildInlineUnsetEdit(text, propStart, afterPos, hasTrailingComma) {
322
310
  return { start, end, text: "" };
323
311
  }
324
312
  function buildUnsetEdit(obj, key) {
325
- const prop = obj.getProperty(key);
313
+ const prop = findPropertyByKey(obj, key);
326
314
  if (!prop)
327
315
  return undefined; // no-op: nothing to remove, unset is idempotent
328
316
  const sourceFile = obj.getSourceFile();
@@ -346,12 +334,7 @@ function buildUnsetEdit(obj, key) {
346
334
  return buildWholeLineUnsetEdit(sourceFile, lineStartPos, afterPos);
347
335
  return buildInlineUnsetEdit(text, propStart, afterPos, hasTrailingComma);
348
336
  }
349
- /**
350
- * Apply a `HandlerHeaderUpdate` to the header fields named in `set`/`unset`
351
- * only, via targeted text edits — `schemaSource`, `handlerBody`, comments,
352
- * and every unnamed field stay byte-identical (narrower than replacePattern).
353
- */
354
- export function updatePattern(sourceFile, change) {
337
+ function resolveUpdatableHandlerLiteral(sourceFile, change) {
355
338
  const call = findCallForId(sourceFile, change.id);
356
339
  if (!call) {
357
340
  throw new Error(`updatePattern: no call found for ${describeId(change.id)}`);
@@ -371,6 +354,15 @@ export function updatePattern(sourceFile, change) {
371
354
  if (obj.getProperties().some((prop) => prop.isKind(SyntaxKind.SpreadAssignment))) {
372
355
  throw new Error("updatePattern: cannot update a handler literal containing a spread; use replace");
373
356
  }
357
+ return obj;
358
+ }
359
+ /**
360
+ * Apply a `HandlerHeaderUpdate` to the header fields named in `set`/`unset`
361
+ * only, via targeted text edits — `schemaSource`, `handlerBody`, comments,
362
+ * and every unnamed field stay byte-identical (narrower than replacePattern).
363
+ */
364
+ export function updatePattern(sourceFile, change) {
365
+ const obj = resolveUpdatableHandlerLiteral(sourceFile, change);
374
366
  const headerFlags = HEADER_KEY_FLAGS_BY_HANDLER_KIND[change.id.kind];
375
367
  const setEntries = Object.entries(change.set).filter(([, v]) => v !== undefined);
376
368
  for (const [key] of setEntries) {
@@ -396,20 +388,47 @@ export function updatePattern(sourceFile, change) {
396
388
  // Dedupe: two identical `unset` entries would otherwise produce two
397
389
  // overlapping deletes, the second eating text past the first's shrink.
398
390
  const unsetKeys = new Set(unsetKeyList);
399
- const edits = [
400
- ...setEntries.map(([key, value]) => buildSetEdit(obj, key, value, unsetKeys)),
401
- ...[...unsetKeys]
391
+ const originalText = sourceFile.getFullText();
392
+ try {
393
+ // Unsets run first and the set edits are built against the re-parsed
394
+ // result: an append anchor and an inline unset can otherwise share the
395
+ // same comma and overlap.
396
+ applyEditsDescending(sourceFile, [...unsetKeys]
402
397
  .map((key) => buildUnsetEdit(obj, key))
403
- .filter((edit) => edit !== undefined),
404
- ];
405
- // Apply from the highest offset down so an earlier edit's positions,
406
- // captured above against the pre-edit text, stay valid.
407
- edits
408
- .slice()
409
- .sort((a, b) => b.start - a.start)
410
- .forEach((edit) => {
398
+ .filter((edit) => edit !== undefined));
399
+ const settable = unsetKeys.size > 0 ? resolveUpdatableHandlerLiteral(sourceFile, change) : obj;
400
+ const inPlaceEdits = [];
401
+ const missingEntries = [];
402
+ for (const [key, value] of setEntries) {
403
+ const edit = buildInPlaceSetEdit(settable, key, value);
404
+ if (edit)
405
+ inPlaceEdits.push(edit);
406
+ else
407
+ missingEntries.push([key, value]);
408
+ }
409
+ applyEditsDescending(sourceFile, [
410
+ ...inPlaceEdits,
411
+ ...(missingEntries.length > 0 ? [buildAppendEdit(settable, missingEntries)] : []),
412
+ ]);
413
+ }
414
+ catch (error) {
415
+ sourceFile.replaceWithText(originalText);
416
+ throw error;
417
+ }
418
+ }
419
+ // Apply from the highest offset down so an earlier edit's positions,
420
+ // captured against the pre-edit text, stay valid.
421
+ function applyEditsDescending(sourceFile, edits) {
422
+ const sorted = edits.slice().sort((a, b) => b.start - a.start);
423
+ for (let i = 1; i < sorted.length; i++) {
424
+ const previous = sorted[i - 1];
425
+ const current = sorted[i];
426
+ if (previous && current && current.end > previous.start) {
427
+ throw new Error("updatePattern: internal error, text edits overlap");
428
+ }
429
+ }
430
+ for (const edit of sorted)
411
431
  sourceFile.replaceText([edit.start, edit.end], edit.text);
412
- });
413
432
  }
414
433
  // =============================================================================
415
434
  // Lookup
@@ -50,6 +50,7 @@ export type AddStreamHandlerArgs = {
50
50
  readonly handlerSource: string;
51
51
  readonly access?: AccessRule;
52
52
  readonly rateLimit?: RateLimitOption;
53
+ readonly escapeHatch?: EscapeHatchDeclaration;
53
54
  };
54
55
  export type AddHookArgs = {
55
56
  readonly type: LifecycleHookType | "validation";
@@ -162,7 +162,7 @@ export function createFeaturePatcher(sourceFile) {
162
162
  ...(escapeHatch !== undefined && { escapeHatch }),
163
163
  });
164
164
  },
165
- addStreamHandler({ name, schemaSource, handlerSource, access, rateLimit }) {
165
+ addStreamHandler({ name, schemaSource, handlerSource, access, rateLimit, escapeHatch }) {
166
166
  add({
167
167
  kind: "streamHandler",
168
168
  source: SYNTHETIC_LOC,
@@ -171,6 +171,7 @@ export function createFeaturePatcher(sourceFile) {
171
171
  handlerBody: rawLoc(handlerSource),
172
172
  ...(access !== undefined && { access }),
173
173
  ...(rateLimit !== undefined && { rateLimit }),
174
+ ...(escapeHatch !== undefined && { escapeHatch }),
174
175
  });
175
176
  },
176
177
  addHook({ type, target, handlerSource, phase, escapeHatch }) {
@@ -640,7 +640,7 @@ function renderValueWithRawSlots(value, indent) {
640
640
  }
641
641
  const DEFAULT_IMPORTS = [
642
642
  'import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";',
643
- 'import { z } from "zod";',
643
+ 'import * as z from "zod";',
644
644
  ];
645
645
  /**
646
646
  * Render a complete feature-file: schema-version header, imports, the
@@ -5,6 +5,24 @@ import { registerEntityCrud } from "./entity-handlers.js";
5
5
  import { resolveName } from "./handler-helpers.js";
6
6
  import { splitNamedDefinition } from "./object-form.js";
7
7
  const CRUD_VERBS = new Set(["create", "update", "delete"]);
8
+ const CRUD_REGISTERED_VERBS = new Set([...CRUD_VERBS, "restore", "list", "detail"]);
9
+ // A silent overwrite would drop framework behaviour that rides on the r.crud
10
+ // handlers (e.g. writeOnly masking), so a same-name registration fails loudly.
11
+ function assertHandlerNameFree(state, featureName, kind, handlerName) {
12
+ const registered = kind === "write" ? state.writeHandlers : state.queryHandlers;
13
+ // skip: a free name is the normal case
14
+ if (!Object.hasOwn(registered, handlerName))
15
+ return;
16
+ const [entityName, verb] = handlerName.split(":");
17
+ const crudShaped = entityName !== undefined &&
18
+ verb !== undefined &&
19
+ state.entities[entityName] !== undefined &&
20
+ CRUD_REGISTERED_VERBS.has(verb);
21
+ throw new Error(`Feature "${featureName}" registers the ${kind} handler "${handlerName}" twice.` +
22
+ (crudShaped
23
+ ? ` r.crud("${entityName}", ...) already registers that name; use a distinct name or drop the "${verb}" verb from r.crud.`
24
+ : " Use a distinct name."));
25
+ }
8
26
  // Map handler name to entity via colon convention.
9
27
  // "task:create" → entity "task". Bare CRUD verbs (create/update/delete) map
10
28
  // when feature name matches an entity or the feature owns exactly one entity.
@@ -70,6 +88,7 @@ export function buildEntityHandlerMethods(state, name) {
70
88
  writeHandler(nameOrDef, schema, handler, options) {
71
89
  if (typeof nameOrDef === "object") {
72
90
  const def = nameOrDef;
91
+ assertHandlerNameFree(state, name, "write", def.name);
73
92
  state.writeHandlers[def.name] = {
74
93
  name: def.name,
75
94
  schema: def.schema,
@@ -101,6 +120,7 @@ export function buildEntityHandlerMethods(state, name) {
101
120
  return { name: def.name };
102
121
  }
103
122
  const inline = requireInlineHandlerArgs("writeHandler", schema, handler, options);
123
+ assertHandlerNameFree(state, name, "write", nameOrDef);
104
124
  state.writeHandlers[nameOrDef] = {
105
125
  name: nameOrDef,
106
126
  schema: inline.schema,
@@ -116,6 +136,7 @@ export function buildEntityHandlerMethods(state, name) {
116
136
  queryHandler(nameOrDef, schema, handler, options) {
117
137
  if (typeof nameOrDef === "object") {
118
138
  const def = nameOrDef;
139
+ assertHandlerNameFree(state, name, "query", def.name);
119
140
  state.queryHandlers[def.name] = {
120
141
  name: def.name,
121
142
  schema: def.schema,
@@ -137,6 +158,7 @@ export function buildEntityHandlerMethods(state, name) {
137
158
  return { name: def.name };
138
159
  }
139
160
  const inline = requireInlineHandlerArgs("queryHandler", schema, handler, options);
161
+ assertHandlerNameFree(state, name, "query", nameOrDef);
140
162
  state.queryHandlers[nameOrDef] = {
141
163
  name: nameOrDef,
142
164
  schema: inline.schema,
@@ -1,4 +1,6 @@
1
1
  import type { EntityDefinition, SessionUser } from "./types/index.js";
2
+ export declare function maskWriteOnlyFields<T extends Readonly<Record<string, unknown>>>(entity: EntityDefinition, row: T): T;
3
+ export declare function dropEmptyWriteOnlyValues<T extends Readonly<Record<string, unknown>>>(entity: EntityDefinition, values: T): T;
2
4
  export declare function filterReadFields(entity: EntityDefinition, data: Readonly<Record<string, unknown>>, user: SessionUser): Record<string, unknown>;
3
5
  export declare function checkWriteFieldRoles(entity: EntityDefinition, changes: Readonly<Record<string, unknown>>, user: SessionUser): string | null;
4
6
  export declare function checkWriteFieldOwnership(entity: EntityDefinition, submittedChanges: Readonly<Record<string, unknown>>, user: SessionUser, oldRow?: Readonly<Record<string, unknown>>, rowContext?: Readonly<Record<string, unknown>>): string | null;
@@ -1,4 +1,40 @@
1
1
  import { normalizeAccessEntry, userCanReadFieldRow, userCanWriteFieldRow } from "./ownership.js";
2
+ function writeOnlyFieldNames(entity) {
3
+ const names = [];
4
+ for (const [name, field] of Object.entries(entity.fields)) {
5
+ if (field.type === "text" && field.writeOnly === true)
6
+ names.push(name);
7
+ }
8
+ return names;
9
+ }
10
+ function maskWriteOnlyValue(value) {
11
+ return value === null || value === undefined || value === "" ? null : true;
12
+ }
13
+ // writeOnly text fields never leave the server as plaintext: set -> true,
14
+ // empty -> null. Only keys present in the row are touched (a partial `changes`
15
+ // stays partial). Returns the same object when the entity has no such field.
16
+ export function maskWriteOnlyFields(entity, row) {
17
+ const names = writeOnlyFieldNames(entity);
18
+ if (names.length === 0)
19
+ return row;
20
+ const masked = { ...row };
21
+ for (const name of names) {
22
+ if (name in masked)
23
+ masked[name] = maskWriteOnlyValue(masked[name]);
24
+ }
25
+ return masked; // @cast-boundary engine-payload
26
+ }
27
+ // Write payloads: "" on a writeOnly field means "unchanged" (an untouched
28
+ // masked input submits it), so the key is dropped before the executor sees it.
29
+ export function dropEmptyWriteOnlyValues(entity, values) {
30
+ const names = writeOnlyFieldNames(entity).filter((name) => values[name] === "");
31
+ if (names.length === 0)
32
+ return values;
33
+ const kept = { ...values };
34
+ for (const name of names)
35
+ delete kept[name];
36
+ return kept; // @cast-boundary engine-payload
37
+ }
2
38
  // Field-level read filtering. Returns a copy of `data` with fields stripped
3
39
  // if the user's roles don't grant read access OR the ownership-rule for the
4
40
  // matching role doesn't accept this concrete row. Fields without access
@@ -22,6 +58,10 @@ export function filterReadFields(entity, data, user) {
22
58
  if (!userCanReadFieldRow(user, accessMap, data)) {
23
59
  continue; // entire field stripped
24
60
  }
61
+ if (field.type === "text" && field.writeOnly === true) {
62
+ result[key] = maskWriteOnlyValue(value);
63
+ continue;
64
+ }
25
65
  // For embedded fields: filter sub-fields with access restrictions.
26
66
  // A list-embedded value is an array of rows — each row is filtered on its
27
67
  // own, so a sub-field rule can reference the row it sits in. Filtering the
@@ -1,6 +1,7 @@
1
1
  export { hasAccess } from "./access.js";
2
2
  export { isPrincipalStatusPlugin, isTenantLifecycleStatusPlugin, type PrincipalProfile, type PrincipalStatus, type PrincipalStatusPlugin, TENANT_TEARDOWN_STATUSES, type TenantLifecycleStatusPlugin, } from "./active-membership.js";
3
3
  export { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./boot-validator/entity-handler.js";
4
+ export { entityHasAnonymizableSubjectField } from "./boot-validator/pii-retention.js";
4
5
  export { collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, type ValidateBootOptions, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
5
6
  export { type BuildAppSchemaOptions, buildAppSchema } from "./build-app-schema.js";
6
7
  export type { ConfigFeatureSchema } from "./build-config-feature-schema.js";
@@ -40,7 +41,7 @@ export type { AddEntityArgs, AddHookArgs, AddRelationArgs, AddWriteHandlerArgs,
40
41
  export { addPattern, applyChanges, createFeaturePatcher, getPatternSchema, groupByCategory, PATTERN_LIBRARY, parseFeatureFile, parsePatternChanges, parseSourceFile, removePattern, renderFeatureFile, renderPattern, replacePattern, updatePattern, VERSION_HEADER, } from "./feature-ast/index.js";
41
42
  export { type ChangelogEntry, type ChangelogType, compareVersions, type FeatureChangelog, filterEntriesAfter, parseFeatureChangelog, sortEntries, validateChangelog, } from "./feature-changelog.js";
42
43
  export { type BuildManifestOptions, buildManifestFromRegistry, type FeatureManifest, type ManifestConfigKey, type ManifestExtension, type ManifestFeature, type ManifestSecret, serializeManifest, } from "./feature-manifest.js";
43
- export { checkWriteFieldOwnership, checkWriteFieldRoles, filterReadFields, } from "./field-access.js";
44
+ export { checkWriteFieldOwnership, checkWriteFieldRoles, filterReadFields, maskWriteOnlyFields, } from "./field-access.js";
44
45
  export { resolveName, withResponseData } from "./handler-helpers.js";
45
46
  export { i18nKey } from "./i18n-key.js";
46
47
  export { buildSessionRoles, findForbiddenMembershipRole, isForbiddenMembershipRole, stripForbiddenMembershipRoles, } from "./membership-roles.js";
@@ -58,11 +59,11 @@ export { createRegistry } from "./registry.js";
58
59
  export type { ClampInfo, ResolveOptions } from "./resolve-config-or-param.js";
59
60
  export { resolveConfigOrParam } from "./resolve-config-or-param.js";
60
61
  export type { AssignableAppRoles, AssignableFromRole } from "./role-assignment.js";
61
- export { assignableAppRolesFromUsages, findForbiddenRoleAssignment, isAssignableByRole, } from "./role-assignment.js";
62
+ export { assignableAppRolesFromUsages, assignableAppRolesOf, canActorAssignRole, findForbiddenRoleAssignment, isAssignableByRole, mergeAssignedRoles, } from "./role-assignment.js";
62
63
  export { runsInLane } from "./run-in.js";
63
64
  export type { StepListOutcome } from "./run-pipeline.js";
64
65
  export { runPipeline, runStepList } from "./run-pipeline.js";
65
- export { buildInsertSchema, buildUpdateSchema, fieldToZod } from "./schema-builder.js";
66
+ export { buildInsertSchema, buildUpdateSchema, fieldToZod, SYSTEM_ONLY_JSON_SCHEMA_KEY, } from "./schema-builder.js";
66
67
  export { isExtensionEditSection, isFieldsEditSection, isWriteFormEditSection, normalizeEditField, normalizeListColumn, sectionFieldSpecs, } from "./screen-helpers.js";
67
68
  export type { TransitionGraph } from "./state-machine.js";
68
69
  export { defineTransitions, guardTransition } from "./state-machine.js";
@@ -72,7 +73,7 @@ export { describeWorkflowStepError } from "./steps/describe-workflow-step-error.
72
73
  export { ANONYMOUS_ROLE, ANONYMOUS_USER_ID, createAnonymousUser, createSystemUser, SYSTEM_ROLE, SYSTEM_USER_ID, } from "./system-user.js";
73
74
  export { type EffectiveFeaturesResolver, findTierResolverUsage, isTierResolverPlugin, TENANT_TIER_RESOLVER_EXT, type TierResolverPlugin, type TrialGate, } from "./tier-resolver-extension.js";
74
75
  export { isSystemTenant, isUuid, parseTenantId, SYSTEM_TENANT_ID } from "./types/identifiers.js";
75
- export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DateFieldDef, DecimalFieldDef, DeleteContext, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EntityDefinition, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, NotifyFactory, NotifyFn, NotifyOptions, NotifyPriority, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TenantId, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
76
+ export type { AccessRule, ActionFormScreenDefinition, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BelongsToRelation, BigIntFieldDef, BooleanFieldDef, CamelToKebab, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, ConcurrencyMode, ConfigAccessor, ConfigAccessorFactory, ConfigBacking, ConfigCascade, ConfigCascadeLevel, ConfigDefinition, ConfigEditScreenDefinition, ConfigKeyAccess, ConfigKeyDefinition, ConfigKeyHandle, ConfigKeyType, ConfigMask, ConfigResolver, ConfigScope, ConfigSecretsReader, ConfigSeedDef, ConfigStoredRow, ConfigStoredRowWithSource, ConfigValue, ConfigValueSource, ConfigValueWithSource, ContentCollectionDefinition, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, CustomScreenDefinition, CustomScreenRoute, DashboardChartPanel, DashboardCustomPanel, DashboardFeedPanel, DashboardFilterDefinition, DashboardListPanel, DashboardPanelDefinition, DashboardPanelVisibility, DashboardProgressListPanel, DashboardScreenDefinition, DashboardScreenPanel, DashboardStatGroupPanel, DashboardStatPanel, DateFieldDef, DecimalFieldDef, DeleteContext, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EditExtensionSection, EditFieldSpec, EditFieldsSection, EditLayout, EditRelatedListSection, EditSectionSpec, EntityDefinition, EntityEditScreenDefinition, EntityId, EntityListExpandableRow, EntityListScreenDefinition, EntityRef, EntityRelations, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, ExtensionOptionsArgs, ExtensionOptionsFor, FeatureDefinition, FeatureRegistrar, FieldAccess, FieldCondition, FieldDefinition, FieldFormatRegistry, FieldIconKey, FieldRenderer, FileFieldDef, FilesFieldDef, Findability, FormatSpec, FormWidth, HandlerContext, HasManyRelation, HookMap, ImageFieldDef, ImagesFieldDef, JobBackoff, JobBackoffStrategy, JobContext, JobDefinition, JobHandlerFn, JobTrigger, JobTriggerEvent, JsonbFieldDef, KumikoEntityTypeMap, KumikoEventTypeMap, KumikoExtensionOptionsMap, KumikoHandlerPayloadMap, KumikoHandlerResultMap, LifecycleHookType, ListColumnSpec, ListFacetSpec, ListPaginationMode, ListSortDir, ListSortSpec, LongTextFindability, ManyToManyRelation, MemberReader, MetricNavigate, MetricSpec, MspErrorMode, MspErrorPolicy, MultiSelectFieldDef, MultiStreamApplyFn, MultiStreamProjectionDefinition, NameOrRef, NavDefinition, NavIconKey, NotificationDataFn, NotificationDefinition, NotificationRecipientFn, NotificationTemplateFn, NotifyFactory, NotifyFn, NotifyOptions, NotifyPriority, NumberFieldDef, OnDeleteStrategy, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, PlatformComponent, PluralForms, PostDeleteHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, ProjectionDefinition, ProjectionDetailScreenDefinition, ProjectionListScreenDefinition, ProjectionTable, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitPer, RecordHeaderSpec, RecordHeaderSubtitlePart, ReferenceDataDef, Registry, RelationDefinition, ResolvedPiiFlags, RetentionDef, RoleAccessPersonalData, RoleAccessRule, RowAction, RowActionNavigate, RowActionNavigateBase, RowActionWriteHandler, RowFieldExtractor, SaveContext, ScreenDefinition, ScreenFilter, ScreenFilterOp, ScreenNavSugar, ScreenSlots, SecretKeyHandle, SecretMintConfirmStep, SecretMintScreenDefinition, SecretReveal, SecretRevealField, SecretsEditScreenDefinition, SecretsEditSection, SelectFieldDef, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, Subscribe, TargetRef, TenantId, TextFieldDef, ToolbarAction, TranslationEntry, TranslationKeys, TranslationsDef, TranslationValue, TreeAction, TreeActionDef, TreeActionsHandle, TreeChildrenSubscribe, TreeNode, TreeNodeState, UnitKey, UnsafeAppendEventFn, ValidationError, ValidationHookFn, WorkspaceDefinition, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "./types/index.js";
76
77
  export { DEFAULT_CURRENCIES, DEFAULT_LOCALES, HookPhases, isAgentVisibleScreen, isOpenToAllGranted, resolveAgentExposure, } from "./types/index.js";
77
78
  export type { AwaitedEventType, PipelineBuildCtx, PipelineCtx, PipelineDef, StepBuilder, StepDef, StepFailureStrategy, StepInstance, StepKind, StepNamespace, StepResolver, } from "./types/step.js";
78
79
  export { runValidation } from "./validation.js";
@@ -2,6 +2,7 @@
2
2
  export { hasAccess } from "./access.js";
3
3
  export { isPrincipalStatusPlugin, isTenantLifecycleStatusPlugin, TENANT_TEARDOWN_STATUSES, } from "./active-membership.js";
4
4
  export { PII_DIRECT_NAME_HINTS, PII_USER_OWNED_NAME_HINTS, PII_USER_REFERENCE_NAME_HINTS, } from "./boot-validator/entity-handler.js";
5
+ export { entityHasAnonymizableSubjectField } from "./boot-validator/pii-retention.js";
5
6
  export { collectWriteHandlerQns, MAX_TRANSFER_DEPTH, SECURITY_BASELINE_FEATURE_NAMES, validateAppCustomScreenWriteQns, validateBoot, } from "./boot-validator.js";
6
7
  export { buildAppSchema } from "./build-app-schema.js";
7
8
  export { buildConfigFeatureSchema, SETTINGS_HUB_FEATURE, SETTINGS_HUB_WORKSPACE, } from "./build-config-feature-schema.js";
@@ -35,7 +36,7 @@ export { createBigIntField, createBooleanField, createDateField, createDecimalFi
35
36
  export { addPattern, applyChanges, createFeaturePatcher, getPatternSchema, groupByCategory, PATTERN_LIBRARY, parseFeatureFile, parsePatternChanges, parseSourceFile, removePattern, renderFeatureFile, renderPattern, replacePattern, updatePattern, VERSION_HEADER, } from "./feature-ast/index.js";
36
37
  export { compareVersions, filterEntriesAfter, parseFeatureChangelog, sortEntries, validateChangelog, } from "./feature-changelog.js";
37
38
  export { buildManifestFromRegistry, serializeManifest, } from "./feature-manifest.js";
38
- export { checkWriteFieldOwnership, checkWriteFieldRoles, filterReadFields, } from "./field-access.js";
39
+ export { checkWriteFieldOwnership, checkWriteFieldRoles, filterReadFields, maskWriteOnlyFields, } from "./field-access.js";
39
40
  export { resolveName, withResponseData } from "./handler-helpers.js";
40
41
  export { i18nKey } from "./i18n-key.js";
41
42
  // findForbiddenMembershipRole/isForbiddenMembershipRole/
@@ -54,10 +55,10 @@ export { isValidQn, parseQn, QnTypes, qn, toKebab } from "./qualified-name.js";
54
55
  export { readClaim } from "./read-claim.js";
55
56
  export { createRegistry } from "./registry.js";
56
57
  export { resolveConfigOrParam } from "./resolve-config-or-param.js";
57
- export { assignableAppRolesFromUsages, findForbiddenRoleAssignment, isAssignableByRole, } from "./role-assignment.js";
58
+ export { assignableAppRolesFromUsages, assignableAppRolesOf, canActorAssignRole, findForbiddenRoleAssignment, isAssignableByRole, mergeAssignedRoles, } from "./role-assignment.js";
58
59
  export { runsInLane } from "./run-in.js";
59
60
  export { runPipeline, runStepList } from "./run-pipeline.js";
60
- export { buildInsertSchema, buildUpdateSchema, fieldToZod } from "./schema-builder.js";
61
+ export { buildInsertSchema, buildUpdateSchema, fieldToZod, SYSTEM_ONLY_JSON_SCHEMA_KEY, } from "./schema-builder.js";
61
62
  export { isExtensionEditSection, isFieldsEditSection, isWriteFormEditSection, normalizeEditField, normalizeListColumn, sectionFieldSpecs, } from "./screen-helpers.js";
62
63
  export { defineTransitions, guardTransition } from "./state-machine.js";
63
64
  export { evaluateEventMatch } from "./steps/_event-match.js";
@@ -7,6 +7,7 @@ export declare function userCanReadFieldRow(user: SessionUser, accessMap: Owners
7
7
  export declare function userCanWriteFieldRow(user: SessionUser, accessMap: OwnershipMap | undefined, oldRow: Readonly<Record<string, unknown>>, newRow: Readonly<Record<string, unknown>>): boolean;
8
8
  export declare function normalizeAccessEntry(entry: OwnershipMap | readonly string[] | undefined): OwnershipMap | undefined;
9
9
  export declare function userCanCreateFieldRow(user: SessionUser, accessMap: OwnershipMap | undefined, newRow: Readonly<Record<string, unknown>>): boolean;
10
+ export declare function tableNameOf(table: unknown): string;
10
11
  export declare function shiftParams(fragment: SqlFragment, shift: number): SqlFragment;
11
12
  export declare function buildOwnershipClause(user: SessionUser, accessMap: OwnershipMap | undefined, table: unknown, paramStart?: number): OwnershipClause;
12
13
  export declare function combineClauses(a: OwnershipClause, b: OwnershipClause): OwnershipClause;
@@ -185,7 +185,7 @@ export function userCanCreateFieldRow(user, accessMap, newRow) {
185
185
  }
186
186
  const PASS_CLAUSE = { kind: "pass" };
187
187
  const EMPTY_CLAUSE = { kind: "empty" };
188
- function tableNameOf(table) {
188
+ export function tableNameOf(table) {
189
189
  if (table !== null && typeof table === "object") {
190
190
  const sym = table[KUMIKO_NAME_SYMBOL];
191
191
  if (typeof sym === "string")
@@ -298,6 +298,19 @@ export function combineClauses(a, b) {
298
298
  params: [...a.params, ...shifted.params],
299
299
  };
300
300
  }
301
+ // The column set is constant per table object, but ruleToFragment runs on every
302
+ // read with a where-rule.
303
+ const columnSqlNamesByTable = new WeakMap();
304
+ function cachedTableColumnSqlNames(table) {
305
+ if (table === null || typeof table !== "object")
306
+ return tableColumnSqlNames(table);
307
+ let names = columnSqlNamesByTable.get(table);
308
+ if (!names) {
309
+ names = tableColumnSqlNames(table);
310
+ columnSqlNamesByTable.set(table, names);
311
+ }
312
+ return names;
313
+ }
301
314
  function ruleToFragment(rule, user, table, paramStart) {
302
315
  if (rule === "all") {
303
316
  return { kind: "sql", sqlText: "TRUE", params: [] };
@@ -311,7 +324,7 @@ function ruleToFragment(rule, user, table, paramStart) {
311
324
  // Runtime backstop for the boot-time probe (boot-validator/ownership.ts):
312
325
  // an unqualified column in a correlated subquery silently turns into a
313
326
  // fail-open tautology (fw#2639) — throw instead of shipping it.
314
- assertQualifiedWhereFragment(frag.sqlText, tableColumnSqlNames(table), `ownership where-rule on "${tableNameOf(table)}"`);
327
+ assertQualifiedWhereFragment(frag.sqlText, cachedTableColumnSqlNames(table), `ownership where-rule on "${tableNameOf(table)}"`);
315
328
  return { kind: "sql", sqlText: frag.sqlText, params: frag.params };
316
329
  }
317
330
  // FromRule
@@ -1,5 +1,5 @@
1
1
  // Mixed pattern schemas (header form + opaque body source).
2
- import { accessRuleField, HOOK_TYPE_OPTIONS, HTTP_METHOD_OPTIONS } from "./shared-fields.js";
2
+ import { accessRuleField, agentDescriptionField, agentHintsField, HOOK_TYPE_OPTIONS, HTTP_METHOD_OPTIONS, } from "./shared-fields.js";
3
3
  // --- Mixed patterns (header form + opaque body source) --------------------
4
4
  export const screenSchema = {
5
5
  kind: "screen",
@@ -71,20 +71,8 @@ export const writeHandlerSchema = {
71
71
  readOnly: true,
72
72
  },
73
73
  accessRuleField,
74
- {
75
- path: "description",
76
- label: { en: "Description", de: "Beschreibung" },
77
- hint: {
78
- en: "Prose the AI agent sees as the tool description. Without it the handler stays invisible to the agent.",
79
- },
80
- input: "textarea",
81
- },
82
- {
83
- path: "agent",
84
- label: { en: "Agent hints", de: "Agent-Hinweise" },
85
- hint: { en: "expose override + risk level for the AI-agent manifest." },
86
- input: "json-readonly",
87
- },
74
+ agentDescriptionField,
75
+ agentHintsField,
88
76
  {
89
77
  path: "rateLimit",
90
78
  label: { en: "Rate limit", de: "Rate-Limit" },
@@ -126,20 +114,8 @@ export const queryHandlerSchema = {
126
114
  readOnly: true,
127
115
  },
128
116
  accessRuleField,
129
- {
130
- path: "description",
131
- label: { en: "Description", de: "Beschreibung" },
132
- hint: {
133
- en: "Prose the AI agent sees as the tool description. Without it the handler stays invisible to the agent.",
134
- },
135
- input: "textarea",
136
- },
137
- {
138
- path: "agent",
139
- label: { en: "Agent hints", de: "Agent-Hinweise" },
140
- hint: { en: "expose override + risk level for the AI-agent manifest." },
141
- input: "json-readonly",
142
- },
117
+ agentDescriptionField,
118
+ agentHintsField,
143
119
  {
144
120
  path: "rateLimit",
145
121
  label: { en: "Rate limit", de: "Rate-Limit" },
@@ -549,6 +525,16 @@ export const defineEventSchema = {
549
525
  language: "zod",
550
526
  readOnly: true,
551
527
  },
528
+ {
529
+ path: "piiFields",
530
+ label: { en: "PII fields (source)", de: "PII-Felder (Source)" },
531
+ hint: {
532
+ en: 'Which payload fields are encrypted, or "none" when the event carries no PII.',
533
+ },
534
+ input: "code-block",
535
+ language: "typescript",
536
+ readOnly: true,
537
+ },
552
538
  {
553
539
  path: "version",
554
540
  label: { en: "Version", de: "Version" },
@@ -117,4 +117,6 @@ export declare const ID_TYPE_OPTIONS: readonly [{
117
117
  readonly de: "Serial Integer";
118
118
  };
119
119
  }];
120
+ export declare const agentDescriptionField: FormFieldSpec;
121
+ export declare const agentHintsField: FormFieldSpec;
120
122
  export declare const accessRuleField: FormFieldSpec;
@@ -30,6 +30,23 @@ export const ID_TYPE_OPTIONS = [
30
30
  { value: "uuid", label: { en: "UUID (default)", de: "UUID (Standard)" } },
31
31
  { value: "serial", label: { en: "Serial integer", de: "Serial Integer" } },
32
32
  ];
33
+ export const agentDescriptionField = {
34
+ path: "description",
35
+ label: { en: "Description", de: "Beschreibung" },
36
+ hint: {
37
+ en: "Prose the AI agent sees as the tool description. Without it the handler stays invisible to the agent.",
38
+ },
39
+ input: "textarea",
40
+ };
41
+ export const agentHintsField = {
42
+ path: "agent",
43
+ label: { en: "Agent hints", de: "Agent-Hinweise" },
44
+ hint: {
45
+ en: "expose override + risk level for the AI-agent manifest.",
46
+ de: "Expose-Override und Risikostufe für das KI-Agent-Manifest.",
47
+ },
48
+ input: "json-readonly",
49
+ };
33
50
  export const accessRuleField = {
34
51
  path: "access",
35
52
  label: { en: "Access", de: "Zugriff" },
@@ -113,6 +113,9 @@ export function buildRegistryFacade(state) {
113
113
  getHandlerFeature(qualifiedHandler) {
114
114
  return state.handlerFeatureMap.get(qualifiedHandler);
115
115
  },
116
+ getJobFeature(qualifiedJobName) {
117
+ return state.jobFeatureMap.get(qualifiedJobName);
118
+ },
116
119
  getAllMetrics() {
117
120
  return state.metricMap;
118
121
  },
@@ -1,4 +1,6 @@
1
- export type AssignableFromRole = "User" | "Member" | "Editor" | "Admin" | "TenantAdmin" | "SystemAdmin";
1
+ import type { Registry } from "./types/feature.js";
2
+ declare const ASSIGNABLE_FROM_ROLE_LIST: readonly ["User", "Member", "Editor", "Admin", "TenantAdmin", "SystemAdmin"];
3
+ export type AssignableFromRole = (typeof ASSIGNABLE_FROM_ROLE_LIST)[number];
2
4
  /** App role → lowest built-in role allowed to assign it. */
3
5
  export type AssignableAppRoles = ReadonlyMap<string, AssignableFromRole>;
4
6
  export declare const DEFAULT_APP_ROLE_ASSIGNABLE_FROM: AssignableFromRole;
@@ -8,4 +10,8 @@ export declare function assignableAppRolesFromUsages(usages: readonly {
8
10
  }[]): AssignableAppRoles;
9
11
  /** True when `actorRole` ranks at least as high as the role an app role is assignable from. */
10
12
  export declare function isAssignableByRole(assignableFrom: AssignableFromRole, actorRole: AssignableFromRole): boolean;
13
+ export declare function canActorAssignRole(actorRoles: readonly string[], role: string, assignableAppRoles?: AssignableAppRoles): boolean;
14
+ export declare function mergeAssignedRoles(actorRoles: readonly string[], assignedRoles: readonly string[], targetCurrentRoles: readonly string[], assignableAppRoles: AssignableAppRoles): readonly string[];
15
+ export declare function assignableAppRolesOf(registry: Pick<Registry, "getExtensionUsages">): AssignableAppRoles;
11
16
  export declare function findForbiddenRoleAssignment(actorRoles: readonly string[], assignedRoles: readonly string[], targetCurrentRoles: readonly string[], assignableAppRoles?: AssignableAppRoles): string | undefined;
17
+ export {};