@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.
- package/dist/api/api-constants.d.ts +1 -0
- package/dist/api/api-constants.js +9 -0
- package/dist/api/auth-middleware.js +45 -21
- package/dist/api/auth-routes.d.ts +1 -0
- package/dist/api/auth-routes.js +20 -0
- package/dist/api/redis-sse-broker.d.ts +1 -0
- package/dist/api/redis-sse-broker.js +4 -5
- package/dist/api/server.js +14 -4
- package/dist/api/sse-route.js +14 -1
- package/dist/bun-db/query.js +6 -4
- package/dist/compliance/sub-processors.js +10 -1
- package/dist/crypto/kek-source.js +1 -0
- package/dist/db/blind-index-cleanup.d.ts +1 -0
- package/dist/db/blind-index-cleanup.js +12 -1
- package/dist/db/event-store-executor-read.js +30 -4
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/queries/event-consumer.d.ts +2 -2
- package/dist/db/queries/event-consumer.js +5 -4
- package/dist/db/queries/event-store.js +6 -6
- package/dist/db/queries/projection-rebuild.js +4 -4
- package/dist/db/queries/seed-context.js +6 -6
- package/dist/db/tenant-db.js +16 -8
- package/dist/derivatives/derivatives-context.js +5 -2
- package/dist/engine/boot-validator/access-declarations.js +13 -7
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +26 -0
- package/dist/engine/boot-validator/index.js +2 -1
- package/dist/engine/boot-validator/pii-retention.js +11 -1
- package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
- package/dist/engine/boot-validator/projection-list-screens.js +13 -0
- package/dist/engine/boot-validator/screens.js +12 -0
- package/dist/engine/build-config-feature-schema.js +4 -0
- package/dist/engine/config-helpers.d.ts +2 -1
- package/dist/engine/config-helpers.js +1 -0
- package/dist/engine/define-feature.js +1 -0
- package/dist/engine/extensions/escape-hatch-usage.js +3 -1
- package/dist/engine/feature-ast/extractors/hooks.js +7 -1
- package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
- package/dist/engine/feature-ast/extractors/index.js +1 -1
- package/dist/engine/feature-ast/extractors/jobs-routes.js +6 -2
- package/dist/engine/feature-ast/extractors/round3.d.ts +2 -1
- package/dist/engine/feature-ast/extractors/round3.js +11 -0
- package/dist/engine/feature-ast/extractors/shared.js +1 -3
- package/dist/engine/feature-ast/index.d.ts +1 -1
- package/dist/engine/feature-ast/parse.js +3 -1
- package/dist/engine/feature-ast/patch.d.ts +3 -0
- package/dist/engine/feature-ast/patch.js +8 -11
- package/dist/engine/feature-ast/patcher.d.ts +6 -1
- package/dist/engine/feature-ast/patcher.js +3 -0
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +630 -1
- package/dist/engine/feature-ast/pattern-change-schema.js +19 -2
- package/dist/engine/feature-ast/patterns.d.ts +8 -2
- package/dist/engine/feature-ast/patterns.js +1 -0
- package/dist/engine/feature-ast/render.js +9 -0
- package/dist/engine/feature-builder-state.d.ts +2 -1
- package/dist/engine/feature-builder-state.js +1 -0
- package/dist/engine/feature-config-events-jobs.d.ts +4 -1
- package/dist/engine/feature-config-events-jobs.js +25 -1
- package/dist/engine/feature-manifest.d.ts +1 -0
- package/dist/engine/feature-manifest.js +1 -0
- package/dist/engine/feature-ui-extensions.js +24 -21
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/pattern-library/library.js +2 -1
- package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
- package/dist/engine/pattern-library/static-schemas.js +47 -4
- package/dist/engine/registry-facade.js +7 -0
- package/dist/engine/registry-ingest.js +7 -0
- package/dist/engine/registry-state.d.ts +2 -1
- package/dist/engine/registry-state.js +1 -0
- package/dist/engine/registry-validate.d.ts +1 -1
- package/dist/engine/registry-validate.js +39 -17
- package/dist/engine/registry.js +2 -2
- package/dist/engine/types/index.d.ts +3 -3
- package/dist/engine/types/index.js +1 -1
- package/dist/engine/types/screen.d.ts +1 -1
- package/dist/engine/types/screen.js +1 -1
- package/dist/errors/classes.d.ts +5 -0
- package/dist/errors/classes.js +15 -0
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/i18n/required-surface-keys.js +3 -5
- package/dist/jobs/job-runner.js +2 -1
- package/dist/migrations/pending-rebuilds.d.ts +2 -0
- package/dist/migrations/pending-rebuilds.js +3 -0
- package/dist/pipeline/dispatch-query.js +3 -5
- package/dist/pipeline/dispatch-shared.js +32 -5
- package/dist/pipeline/dispatch-stream.js +2 -4
- package/dist/pipeline/dispatch-write.js +2 -4
- package/dist/pipeline/event-dispatcher-delivery.d.ts +0 -1
- package/dist/pipeline/event-dispatcher-delivery.js +1 -8
- package/dist/pipeline/event-dispatcher.js +16 -25
- package/dist/pipeline/handler-access-error.d.ts +3 -0
- package/dist/pipeline/handler-access-error.js +17 -0
- package/dist/pipeline/irreversible-operation-gate.js +9 -3
- package/dist/pipeline/member-read-only-transaction.js +8 -3
- package/dist/pipeline/system-hooks.js +7 -1
- package/dist/pipeline/system-identity-switch.js +11 -8
- package/dist/search/meilisearch-adapter.js +0 -2
- package/dist/stack/push-entity-projection-tables.js +5 -3
- package/dist/stack/redis.d.ts +1 -0
- package/dist/stack/redis.js +11 -2
- package/dist/stack/table-helpers.d.ts +3 -1
- package/dist/stack/table-helpers.js +9 -7
- package/dist/stack/test-stack.js +1 -7
- package/dist/testing/handler-context.js +3 -6
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.js +29 -2
- package/package.json +4 -4
- package/src/changes.json +153 -0
- package/src/scripts/codemod/collect-source-files.ts +48 -0
- package/src/scripts/codemod/migrate-cross-tenant.ts +3 -23
- package/src/scripts/codemod/migrate-db-raw.ts +6 -23
- package/src/scripts/codemod/migrate-open-to-all.ts +4 -24
- 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 =
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/db/tenant-db.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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":
|
|
@@ -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
|
|
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
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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
|
|
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
|
|
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;
|