@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.
- package/dist/api/auth-routes.js +19 -4
- package/dist/api/client-ip.js +3 -6
- package/dist/api/extra-route.js +3 -2
- package/dist/api/pii-leak-guard.js +7 -4
- package/dist/api/redis-sse-broker.d.ts +2 -0
- package/dist/api/redis-sse-broker.js +6 -7
- package/dist/api/routes.js +16 -17
- package/dist/api/server.js +14 -7
- package/dist/api/sse-broker.d.ts +3 -0
- package/dist/api/sse-broker.js +14 -0
- package/dist/api/sse-route.d.ts +1 -1
- package/dist/api/sse-route.js +39 -6
- package/dist/bun-db/connection.js +0 -2
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +1 -0
- package/dist/bun-db/query.js +15 -4
- package/dist/crypto/kek-source.js +40 -36
- package/dist/crypto/kms-adapter.js +2 -2
- package/dist/db/apply-entity-event.js +8 -2
- package/dist/db/blind-index-cleanup.js +11 -1
- package/dist/db/eagerload.js +2 -1
- package/dist/db/entity-table-meta.js +5 -3
- package/dist/db/event-store-executor-context.js +13 -19
- package/dist/db/event-store-executor-read.js +39 -36
- package/dist/db/event-store-executor-write.js +15 -17
- package/dist/db/migrate-generator.js +27 -28
- package/dist/db/pg-error.js +9 -5
- package/dist/db/queries/backfill-pii.js +15 -5
- package/dist/db/queries/shadow-swap.js +5 -4
- package/dist/db/table-builder.d.ts +1 -0
- package/dist/db/table-builder.js +7 -15
- package/dist/db/tenant-db.js +18 -2
- package/dist/derivatives/variant-key.js +3 -1
- package/dist/engine/boot-validator/access-declarations.js +12 -3
- package/dist/engine/boot-validator/config-deps.js +13 -0
- package/dist/engine/boot-validator/entity-handler.js +13 -1
- package/dist/engine/boot-validator/entity-list-screens.d.ts +1 -0
- package/dist/engine/boot-validator/entity-list-screens.js +2 -2
- package/dist/engine/boot-validator/index.js +6 -6
- package/dist/engine/boot-validator/nav.d.ts +2 -1
- package/dist/engine/boot-validator/nav.js +10 -20
- package/dist/engine/boot-validator/ownership.d.ts +1 -1
- package/dist/engine/boot-validator/ownership.js +2 -1
- package/dist/engine/boot-validator/pii-retention.d.ts +2 -0
- package/dist/engine/boot-validator/pii-retention.js +44 -4
- package/dist/engine/boot-validator/projection-list-screens.js +8 -0
- package/dist/engine/boot-validator/query-output-columns.js +41 -5
- package/dist/engine/boot-validator/screens.d.ts +2 -2
- package/dist/engine/boot-validator/screens.js +138 -36
- package/dist/engine/boot-validator/transfer-graph.js +9 -6
- package/dist/engine/boot-validator/url-prefill-fields.js +8 -6
- package/dist/engine/boot-validator/zod-shape.js +17 -4
- package/dist/engine/build-app-schema.js +65 -19
- package/dist/engine/build-config-feature-schema.js +106 -22
- package/dist/engine/changeset-changes.js +24 -10
- package/dist/engine/config-helpers.d.ts +2 -2
- package/dist/engine/entity-handlers.js +7 -2
- package/dist/engine/feature-ast/extractors/events.js +11 -8
- package/dist/engine/feature-ast/extractors/handlers.js +38 -16
- package/dist/engine/feature-ast/extractors/resolve-import.d.ts +7 -2
- package/dist/engine/feature-ast/extractors/resolve-import.js +42 -15
- package/dist/engine/feature-ast/parse.d.ts +1 -0
- package/dist/engine/feature-ast/parse.js +18 -3
- package/dist/engine/feature-ast/patch.js +80 -61
- package/dist/engine/feature-ast/patcher.d.ts +1 -0
- package/dist/engine/feature-ast/patcher.js +2 -1
- package/dist/engine/feature-ast/render.js +1 -1
- package/dist/engine/feature-entity-handlers.js +22 -0
- package/dist/engine/field-access.d.ts +2 -0
- package/dist/engine/field-access.js +40 -0
- package/dist/engine/index.d.ts +5 -4
- package/dist/engine/index.js +4 -3
- package/dist/engine/ownership.d.ts +1 -0
- package/dist/engine/ownership.js +15 -2
- package/dist/engine/pattern-library/mixed-schemas.js +15 -29
- package/dist/engine/pattern-library/shared-fields.d.ts +2 -0
- package/dist/engine/pattern-library/shared-fields.js +17 -0
- package/dist/engine/registry-facade.js +3 -0
- package/dist/engine/role-assignment.d.ts +7 -1
- package/dist/engine/role-assignment.js +27 -7
- package/dist/engine/schema-builder.d.ts +2 -0
- package/dist/engine/schema-builder.js +20 -3
- package/dist/engine/screen-helpers.js +15 -4
- package/dist/engine/steps/_event-match.js +22 -3
- package/dist/engine/steps/_read-source.js +8 -0
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/engine/where-rule-lint.js +22 -4
- package/dist/env/index.js +21 -1
- package/dist/errors/reasons.d.ts +0 -1
- package/dist/errors/reasons.js +2 -5
- package/dist/event-store/event-store.js +6 -1
- package/dist/event-store/provenance-append.js +4 -1
- package/dist/files/file-ref-entity.d.ts +6 -0
- package/dist/files/file-ref-table.d.ts +6 -0
- package/dist/files/file-routes.d.ts +0 -1
- package/dist/files/file-routes.js +22 -7
- package/dist/files/local-provider.js +13 -10
- package/dist/files/provider-resolver.js +20 -1
- package/dist/files/types.js +16 -1
- package/dist/i18n/request-locale.d.ts +3 -2
- package/dist/i18n/request-locale.js +16 -6
- package/dist/i18n/required-surface-keys.js +28 -35
- package/dist/i18n/settings-hub-keys.js +21 -4
- package/dist/jobs/index.d.ts +2 -2
- package/dist/jobs/index.js +1 -1
- package/dist/jobs/job-runner.d.ts +2 -0
- package/dist/jobs/job-runner.js +150 -59
- package/dist/migrations/index.d.ts +1 -1
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/pending-rebuilds.d.ts +4 -0
- package/dist/migrations/pending-rebuilds.js +23 -1
- package/dist/observability/escape-hatch-report.d.ts +1 -0
- package/dist/observability/escape-hatch-report.js +3 -0
- package/dist/observability/index.d.ts +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/pipeline/dispatch-batch.js +8 -3
- package/dist/pipeline/dispatch-query.d.ts +1 -1
- package/dist/pipeline/dispatch-query.js +2 -2
- package/dist/pipeline/dispatch-shared.d.ts +2 -3
- package/dist/pipeline/dispatch-shared.js +14 -9
- package/dist/pipeline/dispatch-stream.js +2 -5
- package/dist/pipeline/dispatch-write.js +27 -7
- package/dist/pipeline/dispatcher-utils.js +4 -1
- package/dist/pipeline/dispatcher.js +1 -2
- package/dist/pipeline/event-dispatcher-admin.js +12 -2
- package/dist/pipeline/event-dispatcher.d.ts +1 -0
- package/dist/pipeline/event-dispatcher.js +26 -11
- package/dist/pipeline/index.d.ts +1 -1
- package/dist/pipeline/index.js +1 -1
- package/dist/pipeline/member-read-only-transaction.js +1 -14
- package/dist/pipeline/msp-rebuild.d.ts +1 -0
- package/dist/pipeline/msp-rebuild.js +1 -0
- package/dist/pipeline/system-hooks.d.ts +1 -1
- package/dist/pipeline/system-hooks.js +9 -7
- package/dist/pipeline/tenant-timezone-cache.d.ts +2 -0
- package/dist/pipeline/tenant-timezone-cache.js +20 -6
- package/dist/pipeline/write-result-masking.d.ts +6 -0
- package/dist/pipeline/write-result-masking.js +36 -0
- package/dist/redis/pubsub-signal.d.ts +2 -0
- package/dist/redis/pubsub-signal.js +2 -2
- package/dist/schema-cli.js +4 -14
- package/dist/search/meilisearch-adapter.js +17 -6
- package/dist/search/purge-subject.js +8 -3
- package/dist/stack/drain-jobs.js +24 -12
- package/dist/stack/table-helpers.js +10 -8
- package/dist/testing/closed-connection-error.js +2 -3
- package/dist/testing/shared-entities.d.ts +10 -0
- package/dist/testing/wait-for.js +1 -0
- package/dist/ui-types/index.d.ts +2 -2
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.d.ts +1 -0
- package/dist/upgrade-cli.js +36 -15
- package/package.json +4 -4
- package/src/changes.json +218 -0
- package/src/scripts/codemod/migrate-cross-tenant.ts +14 -2
- package/src/scripts/codemod/migrate-db-raw.ts +16 -8
- package/src/scripts/codemod/migrate-open-to-all.ts +7 -1
- package/src/scripts/codemod/pii-personal-migration.ts +56 -19
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +83 -26
- package/dist/scripts/codemod/crypto-shredding-testing-move.d.ts +0 -2
- package/dist/scripts/codemod/crypto-shredding-testing-move.js +0 -109
- package/dist/scripts/codemod/migrate-cross-tenant.d.ts +0 -19
- package/dist/scripts/codemod/migrate-cross-tenant.js +0 -266
- package/dist/scripts/codemod/migrate-db-raw.d.ts +0 -23
- package/dist/scripts/codemod/migrate-db-raw.js +0 -345
- package/dist/scripts/codemod/migrate-open-to-all.d.ts +0 -22
- package/dist/scripts/codemod/migrate-open-to-all.js +0 -194
- package/dist/scripts/codemod/pii-personal-migration.d.ts +0 -11
- package/dist/scripts/codemod/pii-personal-migration.js +0 -615
- package/dist/scripts/codemod/unprocessable-error-details-reason.d.ts +0 -2
- 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
|
|
206
|
-
|
|
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
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
250
|
+
// Empty literal — insert right after the opening brace.
|
|
256
251
|
const openBrace = obj.getFirstChildByKindOrThrow(SyntaxKind.OpenBraceToken);
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
|
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
|
|
400
|
-
|
|
401
|
-
|
|
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
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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
|
|
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
|
package/dist/engine/index.d.ts
CHANGED
|
@@ -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";
|
package/dist/engine/index.js
CHANGED
|
@@ -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;
|
package/dist/engine/ownership.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {};
|