@cosmicdrift/kumiko-framework 0.336.1 → 0.338.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 (116) hide show
  1. package/dist/api/api-constants.d.ts +1 -0
  2. package/dist/api/api-constants.js +9 -0
  3. package/dist/api/auth-middleware.js +45 -21
  4. package/dist/api/auth-routes.d.ts +1 -0
  5. package/dist/api/auth-routes.js +20 -0
  6. package/dist/api/redis-sse-broker.d.ts +1 -0
  7. package/dist/api/redis-sse-broker.js +4 -5
  8. package/dist/api/server.js +14 -4
  9. package/dist/api/sse-route.js +14 -1
  10. package/dist/bun-db/query.js +6 -4
  11. package/dist/compliance/sub-processors.js +10 -1
  12. package/dist/crypto/kek-source.js +1 -0
  13. package/dist/db/blind-index-cleanup.d.ts +1 -0
  14. package/dist/db/blind-index-cleanup.js +12 -1
  15. package/dist/db/event-store-executor-read.js +30 -4
  16. package/dist/db/index.d.ts +1 -1
  17. package/dist/db/index.js +1 -1
  18. package/dist/db/queries/event-consumer.d.ts +2 -2
  19. package/dist/db/queries/event-consumer.js +5 -4
  20. package/dist/db/queries/event-store.js +6 -6
  21. package/dist/db/queries/projection-rebuild.js +4 -4
  22. package/dist/db/queries/seed-context.js +6 -6
  23. package/dist/db/tenant-db.js +16 -8
  24. package/dist/derivatives/derivatives-context.js +5 -2
  25. package/dist/engine/boot-validator/access-declarations.js +13 -7
  26. package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
  27. package/dist/engine/boot-validator/entity-handler.js +26 -0
  28. package/dist/engine/boot-validator/index.js +2 -1
  29. package/dist/engine/boot-validator/pii-retention.js +11 -1
  30. package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
  31. package/dist/engine/boot-validator/projection-list-screens.js +13 -0
  32. package/dist/engine/boot-validator/screens.js +12 -0
  33. package/dist/engine/build-config-feature-schema.js +4 -0
  34. package/dist/engine/config-helpers.d.ts +2 -1
  35. package/dist/engine/config-helpers.js +1 -0
  36. package/dist/engine/define-feature.js +1 -0
  37. package/dist/engine/extensions/escape-hatch-usage.js +3 -1
  38. package/dist/engine/feature-ast/extractors/hooks.js +7 -1
  39. package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
  40. package/dist/engine/feature-ast/extractors/index.js +1 -1
  41. package/dist/engine/feature-ast/extractors/jobs-routes.js +6 -2
  42. package/dist/engine/feature-ast/extractors/round3.d.ts +2 -1
  43. package/dist/engine/feature-ast/extractors/round3.js +11 -0
  44. package/dist/engine/feature-ast/extractors/shared.js +1 -3
  45. package/dist/engine/feature-ast/index.d.ts +1 -1
  46. package/dist/engine/feature-ast/parse.js +3 -1
  47. package/dist/engine/feature-ast/patch.d.ts +3 -0
  48. package/dist/engine/feature-ast/patch.js +8 -11
  49. package/dist/engine/feature-ast/patcher.d.ts +6 -1
  50. package/dist/engine/feature-ast/patcher.js +3 -0
  51. package/dist/engine/feature-ast/pattern-change-schema.d.ts +630 -1
  52. package/dist/engine/feature-ast/pattern-change-schema.js +19 -2
  53. package/dist/engine/feature-ast/patterns.d.ts +8 -2
  54. package/dist/engine/feature-ast/patterns.js +1 -0
  55. package/dist/engine/feature-ast/render.js +9 -0
  56. package/dist/engine/feature-builder-state.d.ts +2 -1
  57. package/dist/engine/feature-builder-state.js +1 -0
  58. package/dist/engine/feature-config-events-jobs.d.ts +4 -1
  59. package/dist/engine/feature-config-events-jobs.js +25 -1
  60. package/dist/engine/feature-manifest.d.ts +1 -0
  61. package/dist/engine/feature-manifest.js +1 -0
  62. package/dist/engine/feature-ui-extensions.js +24 -21
  63. package/dist/engine/index.d.ts +1 -1
  64. package/dist/engine/pattern-library/library.js +2 -1
  65. package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
  66. package/dist/engine/pattern-library/static-schemas.js +47 -4
  67. package/dist/engine/registry-facade.js +7 -0
  68. package/dist/engine/registry-ingest.js +7 -0
  69. package/dist/engine/registry-state.d.ts +2 -1
  70. package/dist/engine/registry-state.js +1 -0
  71. package/dist/engine/registry-validate.d.ts +1 -1
  72. package/dist/engine/registry-validate.js +39 -17
  73. package/dist/engine/registry.js +2 -2
  74. package/dist/engine/types/index.d.ts +3 -3
  75. package/dist/engine/types/index.js +1 -1
  76. package/dist/engine/types/screen.d.ts +1 -1
  77. package/dist/engine/types/screen.js +1 -1
  78. package/dist/errors/classes.d.ts +5 -0
  79. package/dist/errors/classes.js +15 -0
  80. package/dist/errors/index.d.ts +1 -1
  81. package/dist/errors/index.js +1 -1
  82. package/dist/i18n/required-surface-keys.js +3 -5
  83. package/dist/jobs/job-runner.js +2 -1
  84. package/dist/migrations/pending-rebuilds.d.ts +2 -0
  85. package/dist/migrations/pending-rebuilds.js +3 -0
  86. package/dist/pipeline/dispatch-query.js +3 -5
  87. package/dist/pipeline/dispatch-shared.js +32 -5
  88. package/dist/pipeline/dispatch-stream.js +2 -4
  89. package/dist/pipeline/dispatch-write.js +2 -4
  90. package/dist/pipeline/event-dispatcher-delivery.d.ts +0 -1
  91. package/dist/pipeline/event-dispatcher-delivery.js +1 -8
  92. package/dist/pipeline/event-dispatcher.js +16 -25
  93. package/dist/pipeline/handler-access-error.d.ts +3 -0
  94. package/dist/pipeline/handler-access-error.js +17 -0
  95. package/dist/pipeline/irreversible-operation-gate.js +9 -3
  96. package/dist/pipeline/member-read-only-transaction.js +8 -3
  97. package/dist/pipeline/system-hooks.js +7 -1
  98. package/dist/pipeline/system-identity-switch.js +11 -8
  99. package/dist/search/meilisearch-adapter.js +0 -2
  100. package/dist/stack/push-entity-projection-tables.js +5 -3
  101. package/dist/stack/redis.d.ts +1 -0
  102. package/dist/stack/redis.js +11 -2
  103. package/dist/stack/table-helpers.d.ts +3 -1
  104. package/dist/stack/table-helpers.js +9 -7
  105. package/dist/stack/test-stack.js +1 -7
  106. package/dist/testing/handler-context.js +3 -6
  107. package/dist/ui-types/index.d.ts +1 -1
  108. package/dist/ui-types/index.js +1 -1
  109. package/dist/upgrade-cli.js +29 -2
  110. package/package.json +4 -4
  111. package/src/changes.json +153 -0
  112. package/src/scripts/codemod/collect-source-files.ts +48 -0
  113. package/src/scripts/codemod/migrate-cross-tenant.ts +3 -23
  114. package/src/scripts/codemod/migrate-db-raw.ts +6 -23
  115. package/src/scripts/codemod/migrate-open-to-all.ts +4 -24
  116. package/src/scripts/codemod/unprocessable-error-details-reason.ts +47 -3
@@ -1,25 +1,25 @@
1
1
  import { unsafeReadRetrying } from "../query.js";
2
2
  export async function selectUserByEmail(db, email) {
3
- const rows = (await unsafeReadRetrying(db, `SELECT id::text AS id, email, tenant_id::text AS tenant_id
3
+ const rows = await unsafeReadRetrying(db, `SELECT id::text AS id, email, tenant_id::text AS tenant_id
4
4
  FROM read_users
5
5
  WHERE email = $1
6
- LIMIT 1`, [email]));
6
+ LIMIT 1`, [email]);
7
7
  const row = rows[0];
8
8
  if (!row)
9
9
  return null;
10
10
  return { id: row.id, email: row.email, tenantId: row.tenant_id };
11
11
  }
12
12
  export async function selectMembershipsOfUser(db, userId) {
13
- return (await unsafeReadRetrying(db, `SELECT m.user_id::text AS user_id,
13
+ return unsafeReadRetrying(db, `SELECT m.user_id::text AS user_id,
14
14
  m.tenant_id::text AS tenant_id,
15
15
  e.tenant_id::text AS stream_tenant_id,
16
16
  m.roles
17
17
  FROM read_tenant_memberships m
18
18
  JOIN kumiko_events e ON e.aggregate_id = m.id AND e.version = 1
19
- WHERE m.user_id = $1`, [userId]));
19
+ WHERE m.user_id = $1`, [userId]);
20
20
  }
21
21
  export async function selectAllTenants(db) {
22
- return (await unsafeReadRetrying(db, `SELECT id::text AS id, name, key AS tenant_key
22
+ return unsafeReadRetrying(db, `SELECT id::text AS id, name, key AS tenant_key
23
23
  FROM read_tenants
24
- ORDER BY inserted_at`, []));
24
+ ORDER BY inserted_at`, []);
25
25
  }
@@ -61,7 +61,7 @@ export function unsafeRawForDeclaredStep(holder, reason) {
61
61
  "createUncheckedSystemDb, or createSystemDbView — no declared unsafeRaw runner bound.",
62
62
  });
63
63
  }
64
- return runner(reason);
64
+ return runner(reason, true);
65
65
  }
66
66
  const systemDbRebinders = new WeakMap();
67
67
  // Rebinds a hook's own escapeHatch onto ctx.systemDb, mirroring withUnsafeRawGrant: always
@@ -96,9 +96,15 @@ function buildUncheckedSystemDb(db, dbOutsideTransaction, report, gate) {
96
96
  }
97
97
  return dbOutsideTransaction;
98
98
  }
99
- function grantedUnsafeRawRunner(reason) {
99
+ function grantedUnsafeRawRunner(reason, forwardedDeclaredStep) {
100
100
  if (gate?.kind === "source-tenant-db") {
101
- return unsafeRawForDeclaredStep(db, reason);
101
+ const sourceRunner = declaredUnsafeRawRunners.get(db);
102
+ if (!sourceRunner) {
103
+ throw new InternalError({
104
+ message: "createSystemDbView received a TenantDb with no declared unsafeRaw runner bound.",
105
+ });
106
+ }
107
+ return sourceRunner(reason, forwardedDeclaredStep);
102
108
  }
103
109
  if (reason.trim().length === 0) {
104
110
  throw new Error("unsafeRaw requires a non-empty reason");
@@ -109,7 +115,8 @@ function buildUncheckedSystemDb(db, dbOutsideTransaction, report, gate) {
109
115
  `${gate.deniedCallerLabel} to allow unsafeRaw.`,
110
116
  });
111
117
  }
112
- report("unsafe-raw", reason);
118
+ // Engine-forwarded steps carry their own declared reason; a caller-supplied one never reaches the audit trail.
119
+ report("unsafe-raw", forwardedDeclaredStep ? reason : (gate?.grant?.reason ?? reason));
113
120
  const runner = tenantDbRunner(db);
114
121
  const personalDataGate = personalDataGates.get(db);
115
122
  return personalDataGate ? gatedRunner(runner, personalDataGate) : runner;
@@ -143,7 +150,7 @@ function buildUncheckedSystemDb(db, dbOutsideTransaction, report, gate) {
143
150
  report("acknowledge-cross-tenant", reason);
144
151
  return db;
145
152
  },
146
- unsafeRaw: grantedUnsafeRawRunner,
153
+ unsafeRaw: (reason) => grantedUnsafeRawRunner(reason, false),
147
154
  outsideTransaction: {
148
155
  assertTenantMatch(tenantId) {
149
156
  if (tenantId !== db.tenantId) {
@@ -419,7 +426,7 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
419
426
  // @cast-boundary type-brand — GlobalWrites is only present in the type when TTable is not ExecutorOnly; the object above always carries the methods.
420
427
  };
421
428
  }
422
- function grantedUnsafeRawRunner(reason) {
429
+ function grantedUnsafeRawRunner(reason, forwardedDeclaredStep) {
423
430
  if (reason.trim().length === 0) {
424
431
  throw new Error("unsafeRaw requires a non-empty reason");
425
432
  }
@@ -433,14 +440,15 @@ export function createTenantDb(db, tenantId, mode = "tenant", tracer, meter, sig
433
440
  "the handler or hook to allow unsafeRaw.",
434
441
  });
435
442
  }
436
- report("unsafe-raw", reason);
443
+ // Engine-forwarded steps carry their own declared reason; a caller-supplied one never reaches the audit trail.
444
+ report("unsafe-raw", forwardedDeclaredStep ? reason : (grants?.unsafeRaw?.reason ?? reason));
437
445
  return personalDataGate ? gatedRunner(db, personalDataGate) : db;
438
446
  }
439
447
  const tenantDb = {
440
448
  tenantId,
441
449
  mode,
442
450
  global: globalTable,
443
- unsafeRaw: grantedUnsafeRawRunner,
451
+ unsafeRaw: (reason) => grantedUnsafeRawRunner(reason, false),
444
452
  selectMany(table, where, options) {
445
453
  const filter = readWhere(table, where);
446
454
  return withDbSpan("select", table, async () => bunSelectMany(db, table, filter, options));
@@ -97,8 +97,11 @@ function resolveOverlayLayer(layer, qrContext, tenantId) {
97
97
  // the spec, so a resolver whose target changes (e.g. a base-URL rotation)
98
98
  // invalidates the cache instead of serving a stale target under the old key.
99
99
  async function resolveOverlaySpec(spec, row, registry, tenantId) {
100
- if (!spec.overlays || spec.overlays.length === 0)
101
- return spec;
100
+ // `resolvedOverlays` is framework-internal; a caller-supplied value must
101
+ // never reach the renderer past the token resolution below.
102
+ if (!spec.overlays || spec.overlays.length === 0) {
103
+ return spec.resolvedOverlays === undefined ? spec : { ...spec, resolvedOverlays: undefined };
104
+ }
102
105
  const qrContext = spec.overlays.some((layer) => layer.kind === "qr")
103
106
  ? resolveQrOverlayContext(row, registry)
104
107
  : undefined;
@@ -1,3 +1,4 @@
1
+ import { escapeHatchGrantsProblem } from "@cosmicdrift/kumiko-types/handlers";
1
2
  import { accessAllowsAnonymous, declaredPersonalData, personalFieldNames, } from "../personal-data-fields.js";
2
3
  import { ANONYMOUS_ROLE } from "../system-user.js";
3
4
  import { collectZodObjectKeys } from "./zod-shape.js";
@@ -48,13 +49,18 @@ function validateOpenToAllReason(feature, kind, handlerName, access) {
48
49
  "authenticated user may call this handler. `{ openToAll: true }` is no longer accepted.");
49
50
  }
50
51
  function validateEscapeHatchReason(feature, kind, handlerName, escapeHatch) {
51
- // skip: no escapeHatch declared, or its reason is already non-empty
52
- if (!escapeHatch || escapeHatch.reason.trim().length > 0)
53
- return;
54
- throw new Error(`[Feature ${feature.name}] ${kind} handler "${handlerName}" declares ` +
55
- `{ escapeHatch: { reason: "" } } — the reason must be a non-empty string ` +
56
- "explaining why this handler needs db.global() write access, a SYSTEM identity switch " +
57
- "or a cross-tenant ctx.queryProjection({ unsafeAllTenants: true }) read.");
52
+ if (escapeHatch) {
53
+ const grantsProblem = escapeHatchGrantsProblem(escapeHatch);
54
+ if (grantsProblem !== undefined) {
55
+ throw new Error(`[Feature ${feature.name}] ${kind} handler "${handlerName}" declares an invalid escapeHatch — ${grantsProblem}`);
56
+ }
57
+ if (escapeHatch.reason.trim().length === 0) {
58
+ throw new Error(`[Feature ${feature.name}] ${kind} handler "${handlerName}" declares ` +
59
+ `{ escapeHatch: { reason: "" } } — the reason must be a non-empty string ` +
60
+ "explaining why this handler needs db.global() write access, a SYSTEM identity switch " +
61
+ "or a cross-tenant ctx.queryProjection({ unsafeAllTenants: true }) read.");
62
+ }
63
+ }
58
64
  }
59
65
  function validatePersonalDataOnlyOnWrite(feature, kind, handlerName, access) {
60
66
  // skip: write handlers may declare personalData, or it wasn't declared here
@@ -22,3 +22,4 @@ export declare function validateImageVariants(feature: FeatureDefinition): void;
22
22
  export declare function validateTransitions(feature: FeatureDefinition): void;
23
23
  export declare function validateExtendSchemaCollisions(feature: FeatureDefinition): void;
24
24
  export declare function validateDerivedFieldCollisions(feature: FeatureDefinition): void;
25
+ export declare function validateSearchableFieldReadAccess(feature: FeatureDefinition): void;
@@ -784,3 +784,29 @@ export function validateDerivedFieldCollisions(feature) {
784
784
  }
785
785
  }
786
786
  }
787
+ // --- Searchable fields with restricted read access ---
788
+ // The search index is tenant-wide, not per-role: `search` matches every
789
+ // searchable field and `filterReadFields` only strips the response afterwards.
790
+ // A role that cannot read the field could still guess its value from match
791
+ // counts, so the combination is rejected until the index can filter per role.
792
+ export function validateSearchableFieldReadAccess(feature) {
793
+ for (const [entityName, entity] of Object.entries(feature.entities ?? {})) {
794
+ for (const [fieldName, field] of Object.entries(entity.fields)) {
795
+ if (field.type !== "text")
796
+ continue;
797
+ if (!field.searchable)
798
+ continue;
799
+ const readAccess = field.access?.read;
800
+ if (readAccess === undefined)
801
+ continue;
802
+ const isUnrestricted = Array.isArray(readAccess)
803
+ ? readAccess.length === 0
804
+ : Object.keys(readAccess).length === 0;
805
+ if (isUnrestricted)
806
+ continue;
807
+ throw new Error(`Field "${fieldName}" on entity "${entityName}" cannot be searchable with a restricted ` +
808
+ "access.read — the search index is not filtered per role, so search matches would leak " +
809
+ 'the value to roles that cannot read it. Drop `searchable`/`find: "fuzzy"` or `access.read`.');
810
+ }
811
+ }
812
+ }
@@ -9,7 +9,7 @@ import { validateApiExposureMatching, validateExtensionUsages } from "./api-ext.
9
9
  import { validateFeatureBootChecks } from "./boot-check.js";
10
10
  import { validateCircularDeps, validateConfigKeyAllowPerRequest, validateConfigKeyBacking, validateConfigKeyBounds, validateConfigKeyComputed, validateConfigKeyOptionsQuery, validateConfigKeyPiiEncrypted, validateConfigKeyRequired, validateConfigReads, warnOnToggleableDependencies, } from "./config-deps.js";
11
11
  import { collectDetailForScreens } from "./detail-screens.js";
12
- import { validateDerivedFieldCollisions, validateEmbeddedFields, validateEncryptedFields, validateEntityIndexes, validateEntitySelectOptionsQuery, validateExtendSchemaCollisions, validateExtensionPreSaveWiring, validateFileFields, validateHandlerAccess, validateImageVariants, validateLocatedTimestamps, validateMultiSelectFields, validateMultiStreamProjections, validateReferenceFields, validateTransitions, } from "./entity-handler.js";
12
+ import { validateDerivedFieldCollisions, validateEmbeddedFields, validateEncryptedFields, validateEntityIndexes, validateEntitySelectOptionsQuery, validateExtendSchemaCollisions, validateExtensionPreSaveWiring, validateFileFields, validateHandlerAccess, validateImageVariants, validateLocatedTimestamps, validateMultiSelectFields, validateMultiStreamProjections, validateReferenceFields, validateSearchableFieldReadAccess, validateTransitions, } from "./entity-handler.js";
13
13
  import { validateEntityListScreens } from "./entity-list-screens.js";
14
14
  import { validateGdprStoragePersistence } from "./gdpr-storage.js";
15
15
  import { validateGlobalTenancyEntities } from "./global-tenancy.js";
@@ -118,6 +118,7 @@ export function validateBoot(rawFeatures, options) {
118
118
  validateCircularDeps(feature.name, featureMap);
119
119
  if (validateEncryptedFields(feature))
120
120
  hasEncryptedFields = true;
121
+ validateSearchableFieldReadAccess(feature);
121
122
  if (validateFileFields(feature))
122
123
  hasFileFields = true;
123
124
  validatePiiAndRetention(feature);
@@ -12,6 +12,12 @@ const FRAMEWORK_TIMESTAMP_FIELDS = new Set([
12
12
  // macht nur den Sanity-Check damit Tippfehler ("30days") frueh sichtbar
13
13
  // werden statt erst beim ersten Cleanup-Run.
14
14
  const KEEP_FOR_PATTERN = /^\d+[hdwmy]$/;
15
+ const FILE_FIELD_TYPES = new Set([
16
+ "file",
17
+ "image",
18
+ "files",
19
+ "images",
20
+ ]);
15
21
  // Excludes subjectRef: its `personal: "ref"` union member structurally
16
22
  // forbids `anonymize` (packages/types/src/fields.ts) — #2336.
17
23
  function hasAnonymizableSubjectField(annot) {
@@ -184,7 +190,11 @@ export function validatePiiAndRetention(feature) {
184
190
  const noAnnotation = annotCount === 0 && !annot.allowPlaintext;
185
191
  if (noAnnotation) {
186
192
  const lower = fieldName.toLowerCase();
187
- if (PII_DIRECT_NAME_HINTS.has(lower)) {
193
+ if (FILE_FIELD_TYPES.has(field.type)) {
194
+ // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
195
+ console.warn(`[kumiko:boot] [Feature ${feature.name}] File field "${fieldName}" on entity "${entityName}" has no personal annotation. A forget with strategy "delete" then only severs the uploader link and keeps the binary. If the file holds personal data, mark it { personal: "self" } or { personal: { of: "<ownerField>" } }. If it is business data, set { personal: false, reason: "is_business_data" } to silence.`);
196
+ }
197
+ else if (PII_DIRECT_NAME_HINTS.has(lower)) {
188
198
  // biome-ignore lint/suspicious/noConsole: boot-time dev hint, no logger available yet
189
199
  console.warn(`[kumiko:boot] [Feature ${feature.name}] Field "${fieldName}" on entity "${entityName}" has a PII-typical name but no personal annotation. If this is PII, mark it { personal: "self", find: ... }. If business data, set { personal: false, reason: "is_business_data" } to silence.`);
190
200
  }
@@ -1,4 +1,4 @@
1
- import type { FeatureDefinition, QueryHandlerDef } from "../types/index.js";
1
+ import { type FeatureDefinition, type QueryHandlerDef } from "../types/index.js";
2
2
  export declare function buildQueryHandlerMap(features: readonly FeatureDefinition[]): ReadonlyMap<string, QueryHandlerDef>;
3
3
  export declare function validateProjectionListScreens(features: readonly FeatureDefinition[]): void;
4
4
  export declare function validateRelatedListSectionQueries(features: readonly FeatureDefinition[]): void;
@@ -1,6 +1,7 @@
1
1
  import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
2
2
  import { parseQn, QnTypes, qualifyEntityName } from "../qualified-name.js";
3
3
  import { isEditLayoutScreen } from "../screen-helpers.js";
4
+ import { relatedListGroupHeaderLabel, relatedListGroupKey, } from "../types/index.js";
4
5
  import { SEARCHABLE_FALSE_WHITELIST } from "./entity-list-screens.js";
5
6
  import { findEntity } from "./parent-ref.js";
6
7
  import { getZodObjectShape } from "./zod-shape.js";
@@ -198,6 +199,17 @@ function validateRelatedListParentFilter(prefix, section, schema, handler, featu
198
199
  `entity behind query "${section.query}". Known fields: ${Object.keys(entity.fields).sort().join(", ")}.`);
199
200
  }
200
201
  }
202
+ // A collapsed group without a header has no toggle, so its rows could never be opened.
203
+ function validateRelatedListGroupBy(prefix, section) {
204
+ const groupBy = section.groupBy;
205
+ const collapsedKey = groupBy?.collapsedWhen === undefined ? undefined : relatedListGroupKey(groupBy.collapsedWhen);
206
+ if (groupBy !== undefined &&
207
+ collapsedKey !== undefined &&
208
+ relatedListGroupHeaderLabel(groupBy, collapsedKey) === undefined) {
209
+ throw new Error(`${prefix}: groupBy.collapsedWhen "${collapsedKey}" names a group without a header; ` +
210
+ `set groupBy.label or groupBy.labels["${collapsedKey}"], or drop collapsedWhen.`);
211
+ }
212
+ }
201
213
  // relatedList sections declare search/facets the same way a projectionList
202
214
  // screen does, and hit the same 422 footgun: definePagedQueryHandler doesn't
203
215
  // auto-merge params into the handler's own Zod schema (fw#2165/#2224), so an
@@ -214,6 +226,7 @@ export function validateRelatedListSectionQueries(features) {
214
226
  validateRelatedListSearchable(prefix, section, schema);
215
227
  validateRelatedListFacets(prefix, section, schema);
216
228
  validateRelatedListParentFilter(prefix, section, schema, handler, featureMap);
229
+ validateRelatedListGroupBy(prefix, section);
217
230
  };
218
231
  for (const feature of features) {
219
232
  for (const screen of Object.values(feature.screens)) {
@@ -620,6 +620,15 @@ function validateRedirectTarget(feature, screenId, screenKind, redirect, allScre
620
620
  }
621
621
  validateScreenNavTarget(feature.name, screenId, screenKind, "redirect", redirectTarget, allScreenQns, feature.screens);
622
622
  }
623
+ // redirect is only honored on record actions; on list/related-list row
624
+ // actions it would be silently dropped, so the author would believe it works.
625
+ function rejectRedirectOnListRowAction(featureName, screenId, screenKind, action) {
626
+ if (action.kind !== "navigate" && action.kind !== "drawer" && action.redirect !== undefined) {
627
+ throw new Error(`[Feature ${featureName}] Screen "${screenId}" (${screenKind}) rowAction "${action.id}" sets redirect — ` +
628
+ `redirect is only honored on record actions (projectionDetail/entityEdit header and section ` +
629
+ `actions) and is ignored on list row actions. Remove it.`);
630
+ }
631
+ }
623
632
  // A writeHandler record action's redirect (projectionDetail/entityEdit
624
633
  // header and section actions) follows the entityEdit.redirect rule.
625
634
  function validateRecordActionRedirect(feature, screenId, screenKind, action, allScreenQns) {
@@ -780,6 +789,7 @@ function validateRelatedListSection(args) {
780
789
  function validateRelatedListRowActions(args, rowActions) {
781
790
  const { featureName, screenId, screenType, where, section, screens, allWriteHandlerQns, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens, } = args;
782
791
  for (const action of rowActions) {
792
+ rejectRedirectOnListRowAction(featureName, screenId, screenType, action);
783
793
  if (action.kind === "navigate") {
784
794
  const target = resolveRowActionNavigateTarget(featureName, screenId, screenType, "rowAction", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
785
795
  validateRowActionNavigateParams(featureName, screenId, screenType, undefined, action, target);
@@ -899,6 +909,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
899
909
  }
900
910
  if (screen.rowActions !== undefined) {
901
911
  for (const action of screen.rowActions) {
912
+ rejectRedirectOnListRowAction(feature.name, screenId, "projectionList", action);
902
913
  if (action.kind === "navigate") {
903
914
  const target = resolveRowActionNavigateTarget(feature.name, screenId, "projectionList", "rowAction", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
904
915
  validateRowActionNavigateParams(feature.name, screenId, "projectionList", undefined, action, target);
@@ -1414,6 +1425,7 @@ export function validateScreens(feature, featureMap, allWriteHandlerQns, allScre
1414
1425
  // erst beim ersten Klick als "Screen not found" / 404 auf.
1415
1426
  if (screen.rowActions !== undefined) {
1416
1427
  for (const action of screen.rowActions) {
1428
+ rejectRedirectOnListRowAction(feature.name, screenId, "entityList", action);
1417
1429
  if (action.kind === "navigate") {
1418
1430
  const target = resolveRowActionNavigateTarget(feature.name, screenId, "entityList", "rowAction", action, allScreenQns, navTargetShortIds, screensByShortId, detailForScreens);
1419
1431
  // The renderer's default-entityId fallback (row["id"]) only fires
@@ -409,6 +409,7 @@ function buildSecretsEditScreen(screenId, secrets, access, declaredTranslationKe
409
409
  const fieldLabels = {};
410
410
  const fieldHints = {};
411
411
  const requiredFields = [];
412
+ const fieldAccess = {};
412
413
  // Mutable outer record — TranslationKeys' Readonly<Record<...>> index
413
414
  // signature only permits reading, so the top-level assignments below
414
415
  // need a writable local type; each entry is still a fresh, never-mutated object.
@@ -430,6 +431,8 @@ function buildSecretsEditScreen(screenId, secrets, access, declaredTranslationKe
430
431
  }
431
432
  if (s.def.required === true)
432
433
  requiredFields.push(fieldId);
434
+ if (s.def.writeRoles !== undefined)
435
+ fieldAccess[fieldId] = { roles: [...s.def.writeRoles] };
433
436
  }
434
437
  const titleKey = `${feature}.settings`;
435
438
  sections.push({
@@ -444,6 +447,7 @@ function buildSecretsEditScreen(screenId, secrets, access, declaredTranslationKe
444
447
  fieldLabels,
445
448
  ...(Object.keys(fieldHints).length > 0 && { fieldHints }),
446
449
  ...(requiredFields.length > 0 && { requiredFields }),
450
+ ...(Object.keys(fieldAccess).length > 0 && { fieldAccess }),
447
451
  sections,
448
452
  ...(access !== undefined && { access }),
449
453
  };
@@ -1,4 +1,4 @@
1
- import type { ConfigBacking, ConfigBounds, ConfigComputedFn, ConfigKeyDefinition, ConfigKeyType, ConfigMask, ConfigSeedDef, ConfigValue, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, OptionsQueryPayload } from "./types/index.js";
1
+ import type { ConfigBacking, ConfigBounds, ConfigComputedFn, ConfigKeyDefinition, ConfigKeyType, ConfigMask, ConfigSeedDef, ConfigValidateFn, ConfigValue, CreateSeedOptions, CreateTenantSeedOptions, CreateUserSeedOptions, OptionsQueryPayload } from "./types/index.js";
2
2
  export declare function isEncryptedAtRest(def: Pick<ConfigKeyDefinition, "encrypted" | "backing">): boolean;
3
3
  export declare const access: {
4
4
  readonly all: readonly string[];
@@ -26,6 +26,7 @@ type ConfigKeyOptions<T extends ConfigKeyType> = {
26
26
  flags?: string;
27
27
  } : never;
28
28
  computed?: ConfigComputedFn<T>;
29
+ validate?: ConfigValidateFn<T>;
29
30
  allowPerRequest?: T extends "text" ? never : boolean;
30
31
  required?: boolean;
31
32
  env?: string;
@@ -60,6 +60,7 @@ function createConfigKey(scope, type, opts = {}) {
60
60
  bounds: opts.bounds, // @cast-boundary schema-walk
61
61
  ...(opts.pattern ? { pattern: opts.pattern } : {}),
62
62
  computed: opts.computed,
63
+ validate: opts.validate,
63
64
  ...(opts.allowPerRequest === true ? { allowPerRequest: true } : {}),
64
65
  ...(opts.required === true ? { required: true } : {}),
65
66
  ...(opts.env ? { env: opts.env } : {}),
@@ -124,6 +124,7 @@ export function defineFeature(name, setup, settings) {
124
124
  handlerEntityMappings: state.handlerEntityMappings,
125
125
  metrics: state.metrics,
126
126
  secretKeys: state.secretKeys,
127
+ secretNamespaces: state.secretNamespaces,
127
128
  projections: state.projections,
128
129
  entityProjectionExtensions: state.entityProjectionExtensions,
129
130
  multiStreamProjections: state.multiStreamProjections,
@@ -7,6 +7,8 @@ export function extensionUsageEscapeHatchReason(usage) {
7
7
  const escapeHatch = usage.options?.["escapeHatch"];
8
8
  if (typeof escapeHatch !== "object" || escapeHatch === null)
9
9
  return undefined;
10
- const reason = escapeHatch.reason;
10
+ const { reason, grants } = escapeHatch;
11
+ if (Array.isArray(grants) && !grants.includes("unsafeRaw"))
12
+ return undefined;
11
13
  return typeof reason === "string" && reason.trim().length > 0 ? reason : undefined;
12
14
  }
@@ -1,3 +1,4 @@
1
+ import { isEscapeHatchGrant } from "@cosmicdrift/kumiko-types/handlers";
1
2
  import { SyntaxKind } from "ts-morph";
2
3
  import { sourceLocationFromNode } from "../source-location.js";
3
4
  import { containsRawRefSentinel, fail, findFunctionLiteral, isPlainObject, ok, readDataLiteralNode, readNameLiteral, readNameOrRef, readNameOrRefOrList, readObjectPropertyInitializer, } from "./shared.js";
@@ -75,7 +76,12 @@ export function readOptionalEscapeHatch(value) {
75
76
  return undefined;
76
77
  if (typeof value["reason"] !== "string")
77
78
  return undefined;
78
- return { reason: value["reason"] };
79
+ const grants = value["grants"];
80
+ if (grants === undefined)
81
+ return { reason: value["reason"] };
82
+ if (!Array.isArray(grants) || !grants.every(isEscapeHatchGrant))
83
+ return undefined;
84
+ return { reason: value["reason"], grants };
79
85
  }
80
86
  // Reads the `escapeHatch` sub-property node first, not via readDataLiteralNode
81
87
  // on the whole object: a sibling property like `handler` (a closure) isn't
@@ -7,7 +7,7 @@ export { collectScreenOpaqueProps, extractMultiStreamProjection, extractProjecti
7
7
  export { findImportBindingForLocalName, resolveModuleFile, } from "./resolve-import.js";
8
8
  export { extractDescribe, extractOptionalRequires, extractReadsConfig, extractRequires, extractSystemScope, extractToggleable, extractUiHints, } from "./round1.js";
9
9
  export { extractEntity, extractNav, extractRelation, extractWorkspace, } from "./round2.js";
10
- export { extractClaimKey, extractConfig, extractMetric, extractReferenceData, extractSecret, extractTranslations, extractUseExtension, isClaimKeyType, type NamedOptionsResult, readNamedOptions, } from "./round3.js";
10
+ export { extractClaimKey, extractConfig, extractMetric, extractReferenceData, extractSecret, extractSecretNamespace, extractTranslations, extractUseExtension, isClaimKeyType, type NamedOptionsResult, readNamedOptions, } from "./round3.js";
11
11
  export { extractBootCheck, extractEnvSchema, extractExposesApi, extractExtendsRegistrar, extractStoreTable, extractUsesApi, } from "./round5.js";
12
12
  export { extractTreeActions } from "./round6.js";
13
13
  export type { ExtractOutput } from "./shared.js";
@@ -7,7 +7,7 @@ export { collectScreenOpaqueProps, extractMultiStreamProjection, extractProjecti
7
7
  export { findImportBindingForLocalName, resolveModuleFile, } from "./resolve-import.js";
8
8
  export { extractDescribe, extractOptionalRequires, extractReadsConfig, extractRequires, extractSystemScope, extractToggleable, extractUiHints, } from "./round1.js";
9
9
  export { extractEntity, extractNav, extractRelation, extractWorkspace, } from "./round2.js";
10
- export { extractClaimKey, extractConfig, extractMetric, extractReferenceData, extractSecret, extractTranslations, extractUseExtension, isClaimKeyType, readNamedOptions, } from "./round3.js";
10
+ export { extractClaimKey, extractConfig, extractMetric, extractReferenceData, extractSecret, extractSecretNamespace, extractTranslations, extractUseExtension, isClaimKeyType, readNamedOptions, } from "./round3.js";
11
11
  export { extractBootCheck, extractEnvSchema, extractExposesApi, extractExtendsRegistrar, extractStoreTable, extractUsesApi, } from "./round5.js";
12
12
  export { extractTreeActions } from "./round6.js";
13
13
  export { fail, findFunctionLiteral, isPlainObject, ok, readBooleanProperty, readDataLiteralNode, readNameOrRef, readNameOrRefOrList, readObjectPropertyInitializer, readPropertyKey, readStringLiteralArgs, readVarargsOrArrayProp, resolveSameFileObjectLiteral, } from "./shared.js";
@@ -126,10 +126,14 @@ export function extractHttpRoute(call, sourceFile) {
126
126
  if (!fn) {
127
127
  return fail("httpRoute", sourceLocationFromNode(call, sourceFile), "handler must be an inline arrow function or function expression");
128
128
  }
129
- // Missing/non-boolean `anonymous` reads as false (the safe side — mounted
129
+ // Missing `anonymous` reads as false (the safe side — mounted
130
130
  // behind the session auth chain, not public) so a source file that predates
131
131
  // the required field still parses instead of erroring here.
132
- const anonymous = readBooleanProperty(arg, "anonymous") === true;
132
+ const anonymousValue = readBooleanProperty(arg, "anonymous");
133
+ if (anonymousValue === undefined && arg.getProperty("anonymous") !== undefined) {
134
+ return fail("httpRoute", sourceLocationFromNode(call, sourceFile), "anonymous must be a boolean literal — a non-literal value would be rewritten to a literal on round-trip");
135
+ }
136
+ const anonymous = anonymousValue === true;
133
137
  return ok({
134
138
  kind: "httpRoute",
135
139
  source: sourceLocationFromNode(call, sourceFile),
@@ -1,7 +1,7 @@
1
1
  import type { CallExpression, SourceFile } from "ts-morph";
2
2
  import type { ClaimKeyType } from "../../types/handlers.js";
3
3
  import type { ParseError } from "../parse.js";
4
- import type { ClaimKeyPattern, ConfigPattern, MetricPattern, ReferenceDataPattern, SecretPattern, TranslationsPattern, UseExtensionPattern } from "../patterns.js";
4
+ import type { ClaimKeyPattern, ConfigPattern, MetricPattern, ReferenceDataPattern, SecretNamespacePattern, SecretPattern, TranslationsPattern, UseExtensionPattern } from "../patterns.js";
5
5
  import { type ExtractOutput } from "./shared.js";
6
6
  export type NamedOptionsResult = {
7
7
  readonly kind: "ok";
@@ -16,6 +16,7 @@ export declare function extractConfig(call: CallExpression, sourceFile: SourceFi
16
16
  export declare function extractTranslations(call: CallExpression, sourceFile: SourceFile): ExtractOutput<TranslationsPattern>;
17
17
  export declare function extractMetric(call: CallExpression, sourceFile: SourceFile): ExtractOutput<MetricPattern>;
18
18
  export declare function extractSecret(call: CallExpression, sourceFile: SourceFile): ExtractOutput<SecretPattern>;
19
+ export declare function extractSecretNamespace(call: CallExpression, sourceFile: SourceFile): ExtractOutput<SecretNamespacePattern>;
19
20
  export declare function isClaimKeyType(value: unknown): value is ClaimKeyType;
20
21
  export declare function extractClaimKey(call: CallExpression, sourceFile: SourceFile): ExtractOutput<ClaimKeyPattern>;
21
22
  export declare function extractReferenceData(call: CallExpression, sourceFile: SourceFile): ExtractOutput<ReferenceDataPattern>;
@@ -98,6 +98,17 @@ export function extractSecret(call, sourceFile) {
98
98
  options: parsed.options,
99
99
  });
100
100
  }
101
+ export function extractSecretNamespace(call, sourceFile) {
102
+ const parsed = readNamedOptions(call, sourceFile, "secretNamespace");
103
+ if (parsed.kind === "error")
104
+ return parsed;
105
+ return ok({
106
+ kind: "secretNamespace",
107
+ source: sourceLocationFromNode(call, sourceFile),
108
+ shortName: parsed.name,
109
+ options: parsed.options,
110
+ });
111
+ }
101
112
  export function isClaimKeyType(value) {
102
113
  return (value === "string" ||
103
114
  value === "number" ||
@@ -195,9 +195,7 @@ function resolveIdentifierToStringLiteral(identifier) {
195
195
  const scoped = findScopedVariableDeclaration(id, name);
196
196
  if (scoped) {
197
197
  const init = scoped.getInitializer();
198
- const value = init ? unwrapLiteralInitializer(init) : undefined;
199
- if (value !== undefined)
200
- return value;
198
+ return init ? unwrapLiteralInitializer(init) : undefined;
201
199
  }
202
200
  const binding = findImportBindingForLocalName(id.getSourceFile(), name);
203
201
  if (!binding)
@@ -8,7 +8,7 @@ export type { AddAuthClaimsArgs, AddClaimKeyArgs, AddConfigArgs, AddDefineEventA
8
8
  export { createFeaturePatcher } from "./patcher.js";
9
9
  export type { PatternChangeIssue, PatternChangesParseResult } from "./pattern-change-schema.js";
10
10
  export { parsePatternChanges } from "./pattern-change-schema.js";
11
- export type { AuthClaimsPattern, ClaimKeyPattern, ConfigPattern, DefineEventPattern, Editability, EntityPattern, ExtendsRegistrarPattern, FeaturePattern, FeaturePatternKind, HookPattern, HttpRoutePattern, JobPattern, MetricPattern, MultiStreamProjectionPattern, NavPattern, NotificationPattern, OptionalRequiresPattern, ProjectionPattern, QueryHandlerPattern, ReadsConfigPattern, ReferenceDataPattern, RelationPattern, RequiresPattern, ScreenPattern, SecretPattern, StreamHandlerPattern, SystemScopePattern, ToggleablePattern, TranslationsPattern, UnknownPattern, UseExtensionPattern, WorkspacePattern, WriteHandlerPattern, } from "./patterns.js";
11
+ export type { AuthClaimsPattern, ClaimKeyPattern, ConfigPattern, DefineEventPattern, Editability, EntityPattern, ExtendsRegistrarPattern, FeaturePattern, FeaturePatternKind, HookPattern, HttpRoutePattern, JobPattern, MetricPattern, MultiStreamProjectionPattern, NavPattern, NotificationPattern, OptionalRequiresPattern, ProjectionPattern, QueryHandlerPattern, ReadsConfigPattern, ReferenceDataPattern, RelationPattern, RequiresPattern, ScreenPattern, SecretNamespacePattern, SecretPattern, StreamHandlerPattern, SystemScopePattern, ToggleablePattern, TranslationsPattern, UnknownPattern, UseExtensionPattern, WorkspacePattern, WriteHandlerPattern, } from "./patterns.js";
12
12
  export { getEditability } from "./patterns.js";
13
13
  export type { RenderFeatureFileInput } from "./render.js";
14
14
  export { FEATURE_FILE_VERSION, renderFeatureFile, renderPattern, renderValue, VERSION_HEADER, } from "./render.js";
@@ -21,7 +21,7 @@
21
21
  // Per-pattern extractors fill in iteratively (C1.5) — each round adds
22
22
  // one extractor + a focused test.
23
23
  import { Project, SyntaxKind } from "ts-morph";
24
- import { extractAiClassify, extractAiExtract, extractAiGenerate, extractAuthClaims, extractBootCheck, extractClaimKey, extractConfig, extractDefineEvent, extractDescribe, extractEntity, extractEnvSchema, extractExposesApi, extractExtendsRegistrar, extractHook, extractHttpRoute, extractJob, extractMetric, extractMultiStreamProjection, extractNav, extractNotification, extractOptionalRequires, extractProjection, extractQueryHandler, extractReadsConfig, extractReferenceData, extractRelation, extractRequires, extractScreen, extractSecret, extractStoreTable, extractStreamHandler, extractSystemScope, extractToggleable, extractTranslations, extractTreeActions, extractUiHints, extractUseExtension, extractUsesApi, extractWorkspace, extractWriteHandler, findFunctionLiteral, findImportBindingForLocalName, readObjectPropertyInitializer, resolveModuleFile, resolveSameFileObjectLiteral, } from "./extractors/index.js";
24
+ import { extractAiClassify, extractAiExtract, extractAiGenerate, extractAuthClaims, extractBootCheck, extractClaimKey, extractConfig, extractDefineEvent, extractDescribe, extractEntity, extractEnvSchema, extractExposesApi, extractExtendsRegistrar, extractHook, extractHttpRoute, extractJob, extractMetric, extractMultiStreamProjection, extractNav, extractNotification, extractOptionalRequires, extractProjection, extractQueryHandler, extractReadsConfig, extractReferenceData, extractRelation, extractRequires, extractScreen, extractSecret, extractSecretNamespace, extractStoreTable, extractStreamHandler, extractSystemScope, extractToggleable, extractTranslations, extractTreeActions, extractUiHints, extractUseExtension, extractUsesApi, extractWorkspace, extractWriteHandler, findFunctionLiteral, findImportBindingForLocalName, readObjectPropertyInitializer, resolveModuleFile, resolveSameFileObjectLiteral, } from "./extractors/index.js";
25
25
  import { sourceLocationFromNode } from "./source-location.js";
26
26
  /**
27
27
  * Parse the given feature file and return all recognised r.* calls
@@ -415,6 +415,8 @@ function dispatchExtractor(methodName, call, sourceFile) {
415
415
  return extractMetric(call, sourceFile);
416
416
  case "secret":
417
417
  return extractSecret(call, sourceFile);
418
+ case "secretNamespace":
419
+ return extractSecretNamespace(call, sourceFile);
418
420
  case "claimKey":
419
421
  return extractClaimKey(call, sourceFile);
420
422
  case "referenceData":
@@ -44,6 +44,9 @@ export type PatternId = {
44
44
  } | {
45
45
  readonly kind: "secret";
46
46
  readonly shortName: string;
47
+ } | {
48
+ readonly kind: "secretNamespace";
49
+ readonly shortName: string;
47
50
  } | {
48
51
  readonly kind: "claimKey";
49
52
  readonly shortName: string;
@@ -85,9 +85,7 @@ export function applyChanges(sourceFile, changes) {
85
85
  * biome-stable formatting that matches the renderFeatureFile output.
86
86
  */
87
87
  export function addPattern(sourceFile, pattern) {
88
- if (pattern.kind === "ai.generate" ||
89
- pattern.kind === "ai.extract" ||
90
- pattern.kind === "ai.classify") {
88
+ if (isAiStepId(pattern)) {
91
89
  throw new Error(`addPattern: ${pattern.kind} steps must be inserted into a defineWorkflow steps array, not the setup body`);
92
90
  }
93
91
  const setup = findSetupCallback(sourceFile);
@@ -430,11 +428,10 @@ function applyEditsDescending(sourceFile, edits) {
430
428
  for (const edit of sorted)
431
429
  sourceFile.replaceText([edit.start, edit.end], edit.text);
432
430
  }
433
- // =============================================================================
434
- // Lookup
435
- // =============================================================================
436
- function isAiStepId(id) {
437
- return id.kind === "ai.generate" || id.kind === "ai.extract" || id.kind === "ai.classify";
431
+ function isAiStepId(idOrPattern) {
432
+ return (idOrPattern.kind === "ai.generate" ||
433
+ idOrPattern.kind === "ai.extract" ||
434
+ idOrPattern.kind === "ai.classify");
438
435
  }
439
436
  function aiStepPatchSpan(sourceFile, call) {
440
437
  const start = call.getStart();
@@ -503,7 +500,7 @@ export const SINGLETON_KINDS = new Set([
503
500
  * feature can fix it explicitly.
504
501
  */
505
502
  function findCallForId(sourceFile, id) {
506
- if (id.kind === "ai.generate" || id.kind === "ai.extract" || id.kind === "ai.classify") {
503
+ if (isAiStepId(id)) {
507
504
  return findAiStepCall(sourceFile, id);
508
505
  }
509
506
  const setup = findSetupCallback(sourceFile);
@@ -561,9 +558,8 @@ function readAiStepKey(call) {
561
558
  return undefined;
562
559
  }
563
560
  function findAiStepCall(sourceFile, id) {
564
- if (id.kind !== "ai.generate" && id.kind !== "ai.extract" && id.kind !== "ai.classify") {
561
+ if (!isAiStepId(id))
565
562
  return undefined;
566
- }
567
563
  const factory = AI_STEP_FACTORY[id.kind];
568
564
  const matches = [];
569
565
  for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
@@ -629,6 +625,7 @@ function callMatchesId(call, id) {
629
625
  matchObjectProperty(call, "target", id.target));
630
626
  case "metric":
631
627
  case "secret":
628
+ case "secretNamespace":
632
629
  case "claimKey":
633
630
  return (matchFirstArgString(call, id.shortName) || matchObjectProperty(call, "name", id.shortName));
634
631
  case "referenceData":
@@ -1,7 +1,7 @@
1
1
  import type { SourceFile } from "ts-morph";
2
2
  import type { LifecycleHookType } from "../constants.js";
3
3
  import type { ConfigKeyDefinition, ConfigKeyType, JobDefinition, RunIn, TranslationKeys } from "../types/config.js";
4
- import type { MetricOptions, SecretOptions } from "../types/feature.js";
4
+ import type { MetricOptions, SecretNamespaceOptions, SecretOptions } from "../types/feature.js";
5
5
  import type { EntityDefinition } from "../types/fields.js";
6
6
  import type { AccessRule, ClaimKeyType, EscapeHatchDeclaration, RateLimitOption } from "../types/handlers.js";
7
7
  import type { HookPhase } from "../types/hooks.js";
@@ -115,6 +115,10 @@ export type AddSecretArgs = {
115
115
  readonly name: string;
116
116
  readonly options: SecretOptions;
117
117
  };
118
+ export type AddSecretNamespaceArgs = {
119
+ readonly name: string;
120
+ readonly options: SecretNamespaceOptions;
121
+ };
118
122
  export type AddClaimKeyArgs = {
119
123
  readonly name: string;
120
124
  readonly type: ClaimKeyType;
@@ -177,6 +181,7 @@ export type FeaturePatcher = {
177
181
  readonly addTranslations: (args: AddTranslationsArgs) => void;
178
182
  readonly addMetric: (args: AddMetricArgs) => void;
179
183
  readonly addSecret: (args: AddSecretArgs) => void;
184
+ readonly addSecretNamespace: (args: AddSecretNamespaceArgs) => void;
180
185
  readonly addClaimKey: (args: AddClaimKeyArgs) => void;
181
186
  readonly addReferenceData: (args: AddReferenceDataArgs) => void;
182
187
  readonly addUseExtension: (args: AddUseExtensionArgs) => void;