@cosmicdrift/kumiko-framework 0.383.1 → 0.385.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 (82) hide show
  1. package/agents/examples/custom-fields-basic.md +13 -3
  2. package/agents/examples/designer-demo.md +6 -2
  3. package/agents/examples/i18n.md +36 -1
  4. package/agents/features.md +4 -0
  5. package/dist/api/auth-rate-limiters.d.ts +8 -0
  6. package/dist/api/auth-rate-limiters.js +105 -0
  7. package/dist/api/auth-routes-context.d.ts +34 -0
  8. package/dist/api/auth-routes-context.js +192 -0
  9. package/dist/api/auth-routes-invite.d.ts +2 -0
  10. package/dist/api/auth-routes-invite.js +171 -0
  11. package/dist/api/auth-routes-login.d.ts +2 -0
  12. package/dist/api/auth-routes-login.js +103 -0
  13. package/dist/api/auth-routes-mfa.d.ts +4 -0
  14. package/dist/api/auth-routes-mfa.js +211 -0
  15. package/dist/api/auth-routes-session.d.ts +2 -0
  16. package/dist/api/auth-routes-session.js +144 -0
  17. package/dist/api/auth-routes-shared.d.ts +33 -0
  18. package/dist/api/auth-routes-shared.js +81 -0
  19. package/dist/api/auth-routes-signup.d.ts +2 -0
  20. package/dist/api/auth-routes-signup.js +75 -0
  21. package/dist/api/auth-routes-token-flows.d.ts +4 -0
  22. package/dist/api/auth-routes-token-flows.js +91 -0
  23. package/dist/api/auth-routes-types.d.ts +76 -0
  24. package/dist/api/auth-routes-types.js +1 -0
  25. package/dist/api/auth-routes.d.ts +5 -85
  26. package/dist/api/auth-routes.js +20 -1095
  27. package/dist/config/index.d.ts +1 -1
  28. package/dist/crypto/pii-ciphertext-format.js +1 -1
  29. package/dist/db/blind-index-cleanup.js +1 -1
  30. package/dist/db/dialect.js +1 -1
  31. package/dist/db/distinct-on.d.ts +9 -0
  32. package/dist/db/distinct-on.js +28 -0
  33. package/dist/db/event-store-executor-write.js +2 -2
  34. package/dist/db/migrate-runner.js +4 -4
  35. package/dist/db/pg-error.js +0 -12
  36. package/dist/db/queries/event-consumer.js +3 -3
  37. package/dist/db/queries/postgres-array-literal.js +2 -2
  38. package/dist/db/query.js +48 -20
  39. package/dist/db/tenant-db.js +1 -1
  40. package/dist/engine/boot-validator/i18n-keys.d.ts +2 -0
  41. package/dist/engine/boot-validator/i18n-keys.js +15 -1
  42. package/dist/engine/boot-validator/index.d.ts +4 -0
  43. package/dist/engine/boot-validator/index.js +3 -1
  44. package/dist/engine/boot-validator/pii-retention.js +6 -7
  45. package/dist/engine/boot-validator/screens-actions.d.ts +67 -0
  46. package/dist/engine/boot-validator/screens-actions.js +488 -0
  47. package/dist/engine/boot-validator/screens-dashboard.d.ts +15 -0
  48. package/dist/engine/boot-validator/screens-dashboard.js +467 -0
  49. package/dist/engine/boot-validator/screens-forms.d.ts +24 -0
  50. package/dist/engine/boot-validator/screens-forms.js +516 -0
  51. package/dist/engine/boot-validator/screens-lists.d.ts +47 -0
  52. package/dist/engine/boot-validator/screens-lists.js +319 -0
  53. package/dist/engine/boot-validator/screens.d.ts +4 -21
  54. package/dist/engine/boot-validator/screens.js +26 -1799
  55. package/dist/engine/build-app-schema.js +1 -1
  56. package/dist/engine/factories.d.ts +3 -3
  57. package/dist/engine/steps/read-find-one.js +1 -1
  58. package/dist/engine/types/index.d.ts +1 -1
  59. package/dist/event-store/provenance-append.js +1 -1
  60. package/dist/files/file-ref-entity.d.ts +6 -126
  61. package/dist/files/file-ref-table.d.ts +6 -126
  62. package/dist/files/file-routes.js +39 -16
  63. package/dist/i18n/required-surface-keys.d.ts +22 -2
  64. package/dist/i18n/required-surface-keys.js +91 -3
  65. package/dist/jobs/job-runner-helpers.d.ts +45 -0
  66. package/dist/jobs/job-runner-helpers.js +281 -0
  67. package/dist/jobs/job-runner-schedulers.d.ts +23 -0
  68. package/dist/jobs/job-runner-schedulers.js +106 -0
  69. package/dist/jobs/job-runner-types.d.ts +65 -0
  70. package/dist/jobs/job-runner-types.js +2 -0
  71. package/dist/jobs/job-runner.d.ts +4 -75
  72. package/dist/jobs/job-runner.js +7 -384
  73. package/dist/pipeline/dispatch-shared.js +2 -2
  74. package/dist/pipeline/event-dispatcher-admin.d.ts +15 -8
  75. package/dist/pipeline/event-dispatcher-admin.js +28 -7
  76. package/dist/pipeline/event-dispatcher.d.ts +2 -2
  77. package/dist/pipeline/event-dispatcher.js +3 -4
  78. package/dist/pipeline/index.d.ts +2 -2
  79. package/dist/pipeline/index.js +1 -1
  80. package/dist/schema-cli.js +5 -0
  81. package/package.json +3 -3
  82. package/src/changes.json +39 -0
@@ -456,7 +456,7 @@ function assertValidScreenMultilineRows(featureName, shortId, screen) {
456
456
  // `derived`, `totals`, `totalsMatch`) — a top-level `Array.isArray`/plain-
457
457
  // object check alone would let a function survive one level deep.
458
458
  //
459
- // Cast at the exit `as FieldDefinition`: type-system-wise our out-map only
459
+ // Cast at the exit `as EntityFieldDefinition`: type-system-wise our out-map only
460
460
  // satisfies the discriminated union with an unnarrowed `type` string — the
461
461
  // cast bridges the variant inference TS can't recompute from a generic
462
462
  // Record.
@@ -1,8 +1,8 @@
1
1
  import type { EntityTenancy } from "@cosmicdrift/kumiko-types/tenancy-brand";
2
- import type { BigIntFieldDef, BooleanFieldDef, DateFieldDef, DecimalFieldDef, DerivedFieldDef, EmbeddedFieldDef, EntityDefinition, EntityIndexDef, FieldsMap, FileFieldDef, FilesFieldDef, ImageFieldDef, ImagesFieldDef, JsonbFieldDef, LocatedTimestampFieldDef, LongTextFieldDef, MoneyFieldDef, MultiSelectFieldDef, NumberFieldDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, ResolvedPiiFlags, RetentionDef, SelectFieldDef, TextFieldDef, TimestampFieldDef, TzFieldDef } from "./types/index.ts";
2
+ import type { BigIntFieldDef, BooleanFieldDef, DateFieldDef, DecimalFieldDef, DerivedFieldDef, EmbeddedFieldDef, EntityDefinition, EntityIndexDef, EntityLongTextFieldDef, EntityTextFieldDef, FieldsMap, FileFieldDef, FilesFieldDef, ImageFieldDef, ImagesFieldDef, JsonbFieldDef, LocatedTimestampFieldDef, LongTextFieldDef, MoneyFieldDef, MultiSelectFieldDef, NumberFieldDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, ResolvedPiiFlags, RetentionDef, SelectFieldDef, TextFieldDef, TimestampFieldDef, TzFieldDef } from "./types/index.ts";
3
3
  export declare function createTextField<R extends true | false = false>(overrides: Partial<Omit<TextFieldDef, "type" | "required" | keyof ResolvedPiiFlags>> & PersonalAnnotations & {
4
4
  required?: R;
5
- }): TextFieldDef & {
5
+ }): EntityTextFieldDef & {
6
6
  required: R;
7
7
  };
8
8
  /**
@@ -25,7 +25,7 @@ export declare function createTextField<R extends true | false = false>(override
25
25
  export declare function createDerivedField(spec: DerivedFieldDef): DerivedFieldDef;
26
26
  export declare function createLongTextField<R extends true | false = false>(overrides: Partial<Omit<LongTextFieldDef, "type" | "required" | keyof ResolvedPiiFlags>> & PersonalAnnotationsLongText & {
27
27
  required?: R;
28
- }): LongTextFieldDef & {
28
+ }): EntityLongTextFieldDef & {
29
29
  required: R;
30
30
  };
31
31
  export declare function createBooleanField<R extends true | false = false>(overrides?: Partial<Omit<BooleanFieldDef, "type" | "required">> & {
@@ -1,6 +1,6 @@
1
1
  // r.step.read.findOne — load a single row from a projection table.
2
2
  //
3
- // Thin wrapper on selectMany(db, table, where, { limit: 1 }) (bun-db).
3
+ // Thin wrapper on selectMany(db, table, where, { limit: 1 }) (db/query.ts).
4
4
  // Resolves to the first row or null. Tenant-filtered like ctx.db
5
5
  // method-form reads (own tenant + SYSTEM_TENANT_ID reference rows); a
6
6
  // foreign `where.tenantId` is narrowed to the caller's own scope.
@@ -6,7 +6,7 @@ export type { KumikoEntityTypeMap, KumikoEventTypeMap, KumikoHandlerPayloadMap,
6
6
  export type { ExtensionOptionsArgs, ExtensionOptionsFor, KumikoExtensionOptionsMap, } from "@cosmicdrift/kumiko-types/extension-options-map";
7
7
  export type { BootCheckContext, BootCheckFn, BootOptionName, EntityObjectDefinition, FeatureDefinition, FeatureMetricDef, FeatureMetricType, FeatureRegistrar, HookDefinition, MetricOptions, Registry, SecretKeyDefinition, SecretKeyHandle, SecretNamespaceDefinition, SecretNamespaceHandle, SecretNamespaceOptions, SecretOptions, StackBootRequirement, StoreTableDef, StoreTableEntry, StoreTableOptions, UiHintOption, UiHints, } from "@cosmicdrift/kumiko-types/feature";
8
8
  export type { FieldIconKey } from "@cosmicdrift/kumiko-types/field-icon";
9
- export type { AnyFileFieldDef, BigIntFieldDef, BooleanFieldDef, ClientDerivedFieldDef, DateFieldDef, DecimalFieldDef, DefaultCurrency, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EmbeddedDerivedCellDef, EmbeddedFieldDef, EmbeddedSubFieldDef, EntityDefinition, EntityEventMigration, EntityIndexDef, FieldAccess, FieldDefinition, FieldsMap, FileFieldDef, FilesFieldDef, Findability, ImageFieldDef, ImagesFieldDef, JsonbFieldDef, LocatedTimestampFieldDef, LongTextFieldDef, LongTextFindability, MoneyCurrencySource, MoneyFieldDef, MultiSelectFieldDef, NumberFieldDef, OptionsQueryPayload, OptionsQueryPayloadValue, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, ReferenceFieldDef, ResolvedPiiFlags, RetentionDef, SelectFieldDef, SelectOptionTone, TextFieldDef, TimestampFieldDef, TransitionMap, TzFieldDef, } from "@cosmicdrift/kumiko-types/fields";
9
+ export type { AnyFileFieldDef, BigIntFieldDef, BooleanFieldDef, ClientDerivedFieldDef, DateFieldDef, DecimalFieldDef, DefaultCurrency, DeriveContext, DerivedFieldDef, DerivedFieldsMap, DerivedValueType, EmbeddedDerivedCellDef, EmbeddedFieldDef, EmbeddedSubFieldDef, EntityDefinition, EntityEventMigration, EntityFieldDefinition, EntityIndexDef, EntityLongTextFieldDef, EntityTextFieldDef, FieldAccess, FieldDefinition, FieldsMap, FileFieldDef, FilesFieldDef, Findability, ImageFieldDef, ImagesFieldDef, JsonbFieldDef, LocatedTimestampFieldDef, LongTextFieldDef, LongTextFindability, MoneyCurrencySource, MoneyFieldDef, MultiSelectFieldDef, NumberFieldDef, OptionsQueryPayload, OptionsQueryPayloadValue, ParentRefDef, PersonalAnnotations, PersonalAnnotationsLongText, PersonalAnnotationsNoFind, PersonalSubject, ReferenceFieldDef, ResolvedPiiFlags, ResolvedPiiStance, RetentionDef, SelectFieldDef, SelectOptionTone, TextFieldDef, TimestampFieldDef, TransitionMap, TzFieldDef, } from "@cosmicdrift/kumiko-types/fields";
10
10
  export type { AccessRule, ActiveMembership, ActiveMembershipRejection, ActiveMembershipResult, AgentExposure, AgentHandlerHints, AgentRisk, AggregateStreamHandle, AppContext, AppendEventArgs, AppendEventFn, AuthClaimsContext, AuthClaimsFn, AuthClaimsHookDef, BeforeTransactionPhase, BeforeTransactionResult, CamelToKebab, ChatSendFailureCode, ClaimKeyDefinition, ClaimKeyHandle, ClaimKeyJsType, ClaimKeyType, DeclarativeEventMigration, DeliveryErrorCode, DeliveryFailureCode, DeliverySkipReason, DispatchWriteRef, EntityRef, EscapeHatchAuditSink, EscapeHatchDeclaration, EscapeHatchKind, EscapeHatchReporter, EscapeHatchTarget, EscapeHatchUseEvent, EventDef, EventMigrationDef, EventPiiFields, EventPiiStance, EventUpcastCtx, EventUpcastFn, FetchForWritingArgs, HandlerContext, HandlerRef, JobContext, JobDeferral, JobRunnerRef, JobTriggerEvent, MemberReader, NameOrRef, NotifyDeferred, NotifyDelivery, NotifyDeliveryStatus, NotifyDispatched, NotifyFactory, NotifyFn, NotifyJobDispatcher, NotifyOptions, NotifyPriority, NotifyResult, OpenToAllAccessRule, OpenToAllDeclaration, OpenToAllPersonalData, PayloadBucketNormalization, PayloadRateLimitOption, PreparedWriteEvent, ProviderPluginContext, QualifiedEventName, QueryEvent, QueryHandlerDef, QueryHandlerFn, RateLimitDeclaration, RateLimitDisabled, RateLimitOption, RateLimitOptions, RateLimitPer, RoleAccessPersonalData, RoleAccessRule, SessionUser, SessionUserOrigin, StreamHandlerDef, StreamHandlerFn, TenantWriteTarget, TryAppendEventFn, TryAppendEventResult, UnsafeAppendEventFn, WriteEvent, WriteHandlerDef, WriteHandlerFn, WriteResult, } from "@cosmicdrift/kumiko-types/handlers";
11
11
  export { DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, declaredRateLimitOptions, HTTP_ERROR_CODE_PATTERN, isDeliveryErrorCode, isOpenToAllGranted, isRateLimitDisabled, isRateLimitOption, isUiAccessGranted, } from "@cosmicdrift/kumiko-types/handlers";
12
12
  export type { DeleteContext, EntityHookMap, HookMap, HookOwner, HookPhase, LifecycleHookFn, LifecycleResult, OwnedFn, PhasedHook, PostDeleteBatchHookFn, PostDeleteHookFn, PostQueryHookFn, PostSaveBatchHookFn, PostSaveHookFn, PreDeleteHookFn, PreQueryHookFn, PreSaveHookFn, SaveContext, SearchPayloadContributorFn, ValidationError, ValidationHookFn, } from "@cosmicdrift/kumiko-types/hooks";
@@ -32,7 +32,7 @@ export async function appendProvenanceEvent(db, event) {
32
32
  message: `appendProvenanceEvent: event tenant "${event.tenantId}" does not match the TenantDb's tenant "${db.tenantId}".`,
33
33
  });
34
34
  }
35
- // Bun.SQL/postgres.js abort the whole surrounding begin() on a statement
35
+ // postgres-js aborts the whole surrounding begin() on a statement
36
36
  // error (25P02) even when the caller's catch swallows it — the savepoint
37
37
  // confines that to this scope. Pool connections have no ambient tx to poison.
38
38
  await runInSavepointIfSupported(runner, async (sp) => {
@@ -1,72 +1,12 @@
1
1
  export { FILE_REF_ENTITY_NAME } from "./types.ts";
2
2
  export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
3
- readonly storageKey: {
4
- readonly type: "text";
5
- readonly description?: string;
6
- readonly maxLength?: number;
7
- readonly minLength?: number;
8
- readonly autoComplete?: string;
9
- readonly required?: boolean;
10
- readonly searchable?: boolean;
11
- readonly sortable?: boolean;
12
- readonly filterable?: boolean;
13
- readonly encrypted?: boolean;
14
- readonly sensitive?: boolean;
15
- readonly writeOnly?: boolean;
16
- readonly readAsInstruction?: boolean;
17
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
18
- readonly default?: string;
19
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
20
- readonly multiline?: boolean | {
21
- readonly rows?: number;
22
- };
23
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
3
+ readonly storageKey: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
24
4
  required: true;
25
5
  };
26
- readonly fileName: {
27
- readonly type: "text";
28
- readonly description?: string;
29
- readonly maxLength?: number;
30
- readonly minLength?: number;
31
- readonly autoComplete?: string;
32
- readonly required?: boolean;
33
- readonly searchable?: boolean;
34
- readonly sortable?: boolean;
35
- readonly filterable?: boolean;
36
- readonly encrypted?: boolean;
37
- readonly sensitive?: boolean;
38
- readonly writeOnly?: boolean;
39
- readonly readAsInstruction?: boolean;
40
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
41
- readonly default?: string;
42
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
43
- readonly multiline?: boolean | {
44
- readonly rows?: number;
45
- };
46
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
6
+ readonly fileName: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
47
7
  required: true;
48
8
  };
49
- readonly mimeType: {
50
- readonly type: "text";
51
- readonly description?: string;
52
- readonly maxLength?: number;
53
- readonly minLength?: number;
54
- readonly autoComplete?: string;
55
- readonly required?: boolean;
56
- readonly searchable?: boolean;
57
- readonly sortable?: boolean;
58
- readonly filterable?: boolean;
59
- readonly encrypted?: boolean;
60
- readonly sensitive?: boolean;
61
- readonly writeOnly?: boolean;
62
- readonly readAsInstruction?: boolean;
63
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
64
- readonly default?: string;
65
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
66
- readonly multiline?: boolean | {
67
- readonly rows?: number;
68
- };
69
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
9
+ readonly mimeType: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
70
10
  required: true;
71
11
  };
72
12
  readonly size: {
@@ -81,73 +21,13 @@ export declare const fileRefEntity: import("@cosmicdrift/kumiko-types/fields").E
81
21
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
82
22
  required: true;
83
23
  };
84
- readonly entityType: {
85
- readonly type: "text";
86
- readonly description?: string;
87
- readonly maxLength?: number;
88
- readonly minLength?: number;
89
- readonly autoComplete?: string;
90
- readonly required?: boolean;
91
- readonly searchable?: boolean;
92
- readonly sortable?: boolean;
93
- readonly filterable?: boolean;
94
- readonly encrypted?: boolean;
95
- readonly sensitive?: boolean;
96
- readonly writeOnly?: boolean;
97
- readonly readAsInstruction?: boolean;
98
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
99
- readonly default?: string;
100
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
101
- readonly multiline?: boolean | {
102
- readonly rows?: number;
103
- };
104
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
24
+ readonly entityType: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
105
25
  required: false;
106
26
  };
107
- readonly entityId: {
108
- readonly type: "text";
109
- readonly description?: string;
110
- readonly maxLength?: number;
111
- readonly minLength?: number;
112
- readonly autoComplete?: string;
113
- readonly required?: boolean;
114
- readonly searchable?: boolean;
115
- readonly sortable?: boolean;
116
- readonly filterable?: boolean;
117
- readonly encrypted?: boolean;
118
- readonly sensitive?: boolean;
119
- readonly writeOnly?: boolean;
120
- readonly readAsInstruction?: boolean;
121
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
122
- readonly default?: string;
123
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
124
- readonly multiline?: boolean | {
125
- readonly rows?: number;
126
- };
127
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
27
+ readonly entityId: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
128
28
  required: false;
129
29
  };
130
- readonly fieldName: {
131
- readonly type: "text";
132
- readonly description?: string;
133
- readonly maxLength?: number;
134
- readonly minLength?: number;
135
- readonly autoComplete?: string;
136
- readonly required?: boolean;
137
- readonly searchable?: boolean;
138
- readonly sortable?: boolean;
139
- readonly filterable?: boolean;
140
- readonly encrypted?: boolean;
141
- readonly sensitive?: boolean;
142
- readonly writeOnly?: boolean;
143
- readonly readAsInstruction?: boolean;
144
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
145
- readonly default?: string;
146
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
147
- readonly multiline?: boolean | {
148
- readonly rows?: number;
149
- };
150
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
30
+ readonly fieldName: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
151
31
  required: false;
152
32
  };
153
33
  }, "tenant">;
@@ -1,71 +1,11 @@
1
1
  export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable<import("@cosmicdrift/kumiko-types/fields").EntityDefinition<{
2
- readonly storageKey: {
3
- readonly type: "text";
4
- readonly description?: string;
5
- readonly maxLength?: number;
6
- readonly minLength?: number;
7
- readonly autoComplete?: string;
8
- readonly required?: boolean;
9
- readonly searchable?: boolean;
10
- readonly sortable?: boolean;
11
- readonly filterable?: boolean;
12
- readonly encrypted?: boolean;
13
- readonly sensitive?: boolean;
14
- readonly writeOnly?: boolean;
15
- readonly readAsInstruction?: boolean;
16
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
17
- readonly default?: string;
18
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
19
- readonly multiline?: boolean | {
20
- readonly rows?: number;
21
- };
22
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
2
+ readonly storageKey: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
23
3
  required: true;
24
4
  };
25
- readonly fileName: {
26
- readonly type: "text";
27
- readonly description?: string;
28
- readonly maxLength?: number;
29
- readonly minLength?: number;
30
- readonly autoComplete?: string;
31
- readonly required?: boolean;
32
- readonly searchable?: boolean;
33
- readonly sortable?: boolean;
34
- readonly filterable?: boolean;
35
- readonly encrypted?: boolean;
36
- readonly sensitive?: boolean;
37
- readonly writeOnly?: boolean;
38
- readonly readAsInstruction?: boolean;
39
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
40
- readonly default?: string;
41
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
42
- readonly multiline?: boolean | {
43
- readonly rows?: number;
44
- };
45
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
5
+ readonly fileName: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
46
6
  required: true;
47
7
  };
48
- readonly mimeType: {
49
- readonly type: "text";
50
- readonly description?: string;
51
- readonly maxLength?: number;
52
- readonly minLength?: number;
53
- readonly autoComplete?: string;
54
- readonly required?: boolean;
55
- readonly searchable?: boolean;
56
- readonly sortable?: boolean;
57
- readonly filterable?: boolean;
58
- readonly encrypted?: boolean;
59
- readonly sensitive?: boolean;
60
- readonly writeOnly?: boolean;
61
- readonly readAsInstruction?: boolean;
62
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
63
- readonly default?: string;
64
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
65
- readonly multiline?: boolean | {
66
- readonly rows?: number;
67
- };
68
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
8
+ readonly mimeType: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
69
9
  required: true;
70
10
  };
71
11
  readonly size: {
@@ -80,73 +20,13 @@ export declare const fileRefsTable: import("../db/table-builder.ts").EntityTable
80
20
  } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
81
21
  required: true;
82
22
  };
83
- readonly entityType: {
84
- readonly type: "text";
85
- readonly description?: string;
86
- readonly maxLength?: number;
87
- readonly minLength?: number;
88
- readonly autoComplete?: string;
89
- readonly required?: boolean;
90
- readonly searchable?: boolean;
91
- readonly sortable?: boolean;
92
- readonly filterable?: boolean;
93
- readonly encrypted?: boolean;
94
- readonly sensitive?: boolean;
95
- readonly writeOnly?: boolean;
96
- readonly readAsInstruction?: boolean;
97
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
98
- readonly default?: string;
99
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
100
- readonly multiline?: boolean | {
101
- readonly rows?: number;
102
- };
103
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
23
+ readonly entityType: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
104
24
  required: false;
105
25
  };
106
- readonly entityId: {
107
- readonly type: "text";
108
- readonly description?: string;
109
- readonly maxLength?: number;
110
- readonly minLength?: number;
111
- readonly autoComplete?: string;
112
- readonly required?: boolean;
113
- readonly searchable?: boolean;
114
- readonly sortable?: boolean;
115
- readonly filterable?: boolean;
116
- readonly encrypted?: boolean;
117
- readonly sensitive?: boolean;
118
- readonly writeOnly?: boolean;
119
- readonly readAsInstruction?: boolean;
120
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
121
- readonly default?: string;
122
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
123
- readonly multiline?: boolean | {
124
- readonly rows?: number;
125
- };
126
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
26
+ readonly entityId: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
127
27
  required: false;
128
28
  };
129
- readonly fieldName: {
130
- readonly type: "text";
131
- readonly description?: string;
132
- readonly maxLength?: number;
133
- readonly minLength?: number;
134
- readonly autoComplete?: string;
135
- readonly required?: boolean;
136
- readonly searchable?: boolean;
137
- readonly sortable?: boolean;
138
- readonly filterable?: boolean;
139
- readonly encrypted?: boolean;
140
- readonly sensitive?: boolean;
141
- readonly writeOnly?: boolean;
142
- readonly readAsInstruction?: boolean;
143
- readonly format?: "email" | "url" | "phone" | "password" | "month" | "iban";
144
- readonly default?: string;
145
- readonly access?: import("@cosmicdrift/kumiko-types/fields").FieldAccess;
146
- readonly multiline?: boolean | {
147
- readonly rows?: number;
148
- };
149
- } & import("@cosmicdrift/kumiko-types/fields").ResolvedPiiFlags & {
29
+ readonly fieldName: import("@cosmicdrift/kumiko-types/fields").EntityTextFieldDef & {
150
30
  required: false;
151
31
  };
152
32
  }, "tenant">>;
@@ -23,6 +23,9 @@ const DEFAULT_PRIVILEGED_ROLES = ["Admin", "SystemAdmin"];
23
23
  // leaked URL (e.g. from a browser history screenshot) isn't a long-lived
24
24
  // credential (see "Signed-URL default expiry" in core-files.md).
25
25
  const SIGNED_URL_DEFAULT_EXPIRY_SECONDS = 15 * 60;
26
+ const MAX_BATCH_META_IDS = 100;
27
+ // Each detail() read decrypts through the KMS pool; a bounded chunk keeps one batch from hogging it.
28
+ const BATCH_META_CONCURRENCY = 10;
26
29
  // Default guard: on attached files, allow the uploader or a privileged role,
27
30
  // and for reads also everyone who may read the record. Delete stays with the
28
31
  // uploader and privileged roles: reading a record is no right to remove its
@@ -218,6 +221,25 @@ export function createFileRoutes(options) {
218
221
  return c.json({ error: "upload_failed" }, 500);
219
222
  return c.json(outcome.fileRef, 201);
220
223
  });
224
+ // GET /files/meta?ids=a,b,c — meta of many files in one request (list/card
225
+ // cells otherwise fire one request per row). Unreadable and unknown ids are
226
+ // left out of the answer, same as /files/:id/meta answering 404 for them.
227
+ // Registered before /files/:id so "meta" is never taken for an id.
228
+ api.get("/files/meta", async (c) => {
229
+ const user = getUser(c);
230
+ const ids = [...new Set((c.req.query("ids") ?? "").split(",").filter((id) => isUuid(id)))];
231
+ if (ids.length > MAX_BATCH_META_IDS)
232
+ return c.json({ error: "too_many_ids" }, 400);
233
+ const files = [];
234
+ for (let start = 0; start < ids.length; start += BATCH_META_CONCURRENCY) {
235
+ const chunk = ids.slice(start, start + BATCH_META_CONCURRENCY);
236
+ const metas = await Promise.all(chunk.map((id) => loadReadableFileMeta(id, user)));
237
+ for (const meta of metas)
238
+ if (meta)
239
+ files.push(meta);
240
+ }
241
+ return c.json({ files });
242
+ });
221
243
  // GET /files/:id — download.
222
244
  //
223
245
  // Authorization stack:
@@ -388,31 +410,28 @@ export function createFileRoutes(options) {
388
410
  const expiresAt = new Date(Date.now() + expiresInSeconds * 1000).toISOString();
389
411
  return c.json({ url, expiresAt });
390
412
  });
391
- // GET /files/:id/meta — metadata without the bytes. Unlike the byte-serving
392
- // routes above, this returns fileName as JSON — and fileName is
393
- // `personal: "self"` (fw#2423), so it must go through the entity executor's
394
- // detail() to decrypt instead of loadFileForTenant's raw, still-encrypted read.
395
- api.get("/files/:id/meta", async (c) => {
396
- const user = getUser(c);
397
- const id = c.req.param("id");
413
+ // Metadata without the bytes. Unlike the byte-serving routes, this returns
414
+ // fileName as JSON — and fileName is `personal: "self"` (fw#2423), so it must
415
+ // go through the entity executor's detail() to decrypt instead of
416
+ // loadFileForTenant's raw, still-encrypted read.
417
+ async function loadReadableFileMeta(id, user) {
398
418
  // Same 22P02-avoidance as loadFileForTenant — detail() doesn't validate
399
419
  // id shape before it hits the UUID column.
400
420
  if (!isUuid(id))
401
- return c.json({ error: "not_found" }, 404);
421
+ return null;
402
422
  const row = await executor.detail({ id }, user, tenantDbFor(user.tenantId));
403
423
  if (!row)
404
- return c.json({ error: "not_found" }, 404);
424
+ return null;
405
425
  const fileRef = row; // @cast-boundary db-row (decrypted via executor.detail)
406
426
  // detail()'s "pass"-ownership read widens to tenantId IN (self, SYSTEM),
407
427
  // narrower in loadFileForTenant's selectMany. Restore that here so meta
408
428
  // doesn't leak reference-tenant rows.
409
- if (fileRef.tenantId !== user.tenantId) {
410
- return c.json({ error: "not_found" }, 404);
411
- }
429
+ if (fileRef.tenantId !== user.tenantId)
430
+ return null;
412
431
  const decision = await guard({ fileRef, user, operation: "read" });
413
432
  if (decision === "deny")
414
- return c.json({ error: "not_found" }, 404);
415
- return c.json({
433
+ return null;
434
+ return {
416
435
  id: fileRef.id,
417
436
  fileName: fileRef.fileName,
418
437
  mimeType: fileRef.mimeType,
@@ -420,11 +439,15 @@ export function createFileRoutes(options) {
420
439
  entityType: fileRef.entityType,
421
440
  entityId: fileRef.entityId,
422
441
  fieldName: fileRef.fieldName,
423
- });
442
+ };
443
+ }
444
+ api.get("/files/:id/meta", async (c) => {
445
+ const meta = await loadReadableFileMeta(c.req.param("id"), getUser(c));
446
+ return meta ? c.json(meta) : c.json({ error: "not_found" }, 404);
424
447
  });
425
448
  async function loadFileForTenant(id, tenantId) {
426
449
  // fileRefsTable.id is a UUID column — a malformed id must fail here, not
427
- // at Postgres (22P02 poisons the pooled Bun.SQL connection). 404, not
450
+ // at Postgres (22P02 poisons the pooled connection). 404, not
428
451
  // 400: an authenticated caller sending a bad id must see the same
429
452
  // response as a nonexistent one, no existence-oracle.
430
453
  if (!isUuid(id))
@@ -31,5 +31,25 @@ export type TranslationLocaleGap = {
31
31
  readonly key: string;
32
32
  readonly missingLocales: readonly string[];
33
33
  };
34
- /** Locale completeness is opt-in via @cosmicdrift/kumiko-locale-* packages. */
35
- export declare function findTranslationLocaleGaps(_features: readonly FeatureDefinition[]): readonly TranslationLocaleGap[];
34
+ /**
35
+ * Per feature, every translation key must carry every locale that any key of the
36
+ * same feature carries. A feature with a single locale across all keys is
37
+ * consistent by definition (locale packages, en-only framework features).
38
+ */
39
+ export declare function findTranslationLocaleGaps(features: readonly FeatureDefinition[]): readonly TranslationLocaleGap[];
40
+ export type RequiredLocaleGap = {
41
+ readonly featureName: string;
42
+ readonly key: string;
43
+ readonly missingLocales: readonly string[];
44
+ };
45
+ /**
46
+ * Per translation key, the union of locales over ALL features must cover
47
+ * `requiredLocales`. Locale packages translate other features' keys under the
48
+ * full `feature:key` name, so the lookup unions by effective name. Raw names
49
+ * only come from locale packages and already-prefixed keys. Entries that only translate another
50
+ * mounted feature's key are not reported separately; the defining feature is.
51
+ * Locale packages (`locale-*`) only contribute locales: their keys often belong to
52
+ * client bundles (e.g. `auth.errors.*`) that no feature defines, so they are never
53
+ * reported as gap owners.
54
+ */
55
+ export declare function findRequiredLocaleGaps(features: readonly FeatureDefinition[], requiredLocales: readonly string[]): readonly RequiredLocaleGap[];
@@ -519,7 +519,95 @@ export function buildEffectiveTranslationKeys(features) {
519
519
  }
520
520
  return out;
521
521
  }
522
- /** Locale completeness is opt-in via @cosmicdrift/kumiko-locale-* packages. */
523
- export function findTranslationLocaleGaps(_features) {
524
- return [];
522
+ function hasLocaleValue(value) {
523
+ if (value === undefined)
524
+ return false;
525
+ return typeof value === "string" ? value !== "" : true;
526
+ }
527
+ /**
528
+ * Per feature, every translation key must carry every locale that any key of the
529
+ * same feature carries. A feature with a single locale across all keys is
530
+ * consistent by definition (locale packages, en-only framework features).
531
+ */
532
+ export function findTranslationLocaleGaps(features) {
533
+ const gaps = [];
534
+ for (const feature of features) {
535
+ const entries = Object.entries(feature.translations ?? {});
536
+ const featureLocales = new Set();
537
+ for (const [, byLocale] of entries) {
538
+ for (const locale of Object.keys(byLocale))
539
+ featureLocales.add(locale);
540
+ }
541
+ if (featureLocales.size < 2)
542
+ continue;
543
+ const sortedLocales = [...featureLocales].sort();
544
+ for (const [key, byLocale] of entries) {
545
+ const missingLocales = sortedLocales.filter((locale) => !hasLocaleValue(byLocale[locale]));
546
+ if (missingLocales.length > 0)
547
+ gaps.push({ featureName: feature.name, key, missingLocales });
548
+ }
549
+ }
550
+ return gaps;
551
+ }
552
+ const LOCALE_PACKAGE_PREFIX = "locale-";
553
+ function collectLocalesByName(features) {
554
+ // A plain key of a regular feature is only its own; registering it raw would let two
555
+ // features with the same plain key cover each other's missing locales.
556
+ const namesOf = (featureName, key) => featureName.startsWith(LOCALE_PACKAGE_PREFIX) || key.includes(":")
557
+ ? [`${featureName}:${key}`, key]
558
+ : [`${featureName}:${key}`];
559
+ const localesByName = new Map();
560
+ for (const feature of features) {
561
+ for (const [key, byLocale] of Object.entries(feature.translations ?? {})) {
562
+ const present = Object.entries(byLocale)
563
+ .filter(([, value]) => hasLocaleValue(value))
564
+ .map(([locale]) => locale);
565
+ for (const name of namesOf(feature.name, key)) {
566
+ const locales = localesByName.get(name) ?? new Set();
567
+ for (const locale of present)
568
+ locales.add(locale);
569
+ localesByName.set(name, locales);
570
+ }
571
+ }
572
+ }
573
+ return localesByName;
574
+ }
575
+ /**
576
+ * Per translation key, the union of locales over ALL features must cover
577
+ * `requiredLocales`. Locale packages translate other features' keys under the
578
+ * full `feature:key` name, so the lookup unions by effective name. Raw names
579
+ * only come from locale packages and already-prefixed keys. Entries that only translate another
580
+ * mounted feature's key are not reported separately; the defining feature is.
581
+ * Locale packages (`locale-*`) only contribute locales: their keys often belong to
582
+ * client bundles (e.g. `auth.errors.*`) that no feature defines, so they are never
583
+ * reported as gap owners.
584
+ */
585
+ export function findRequiredLocaleGaps(features, requiredLocales) {
586
+ const featureNames = new Set(features.map((feature) => feature.name));
587
+ const localesByName = collectLocalesByName(features);
588
+ const gaps = [];
589
+ const reported = new Set();
590
+ for (const feature of features) {
591
+ if (feature.name.startsWith(LOCALE_PACKAGE_PREFIX))
592
+ continue;
593
+ for (const key of Object.keys(feature.translations ?? {})) {
594
+ const prefix = key.includes(":") ? key.slice(0, key.indexOf(":")) : undefined;
595
+ const translatesOtherFeature = prefix !== undefined && prefix !== feature.name && featureNames.has(prefix);
596
+ if (translatesOtherFeature)
597
+ continue;
598
+ const effective = key.includes(":") ? key : `${feature.name}:${key}`;
599
+ if (reported.has(effective))
600
+ continue;
601
+ // Locale packages spell plurals as flat `.one`/`.other` keys; `other` is the form every locale has.
602
+ const present = new Set([effective, key, `${effective}.other`, `${key}.other`].flatMap((name) => [
603
+ ...(localesByName.get(name) ?? []),
604
+ ]));
605
+ const missingLocales = requiredLocales.filter((locale) => !present.has(locale));
606
+ if (missingLocales.length > 0) {
607
+ reported.add(effective);
608
+ gaps.push({ featureName: feature.name, key, missingLocales });
609
+ }
610
+ }
611
+ }
612
+ return gaps;
525
613
  }
@@ -0,0 +1,45 @@
1
+ /// <reference types="temporal-polyfill/global" preserve="true" />
2
+ import type { ConfigAccessor } from "@cosmicdrift/kumiko-types/config";
3
+ import type { TzContext } from "@cosmicdrift/kumiko-types/tz-context";
4
+ import type { JobsOptions, Queue } from "bullmq";
5
+ import type { TenantDb } from "../db/tenant-db.ts";
6
+ import type { AppContext, EscapeHatchReporter, JobContext, JobDefinition, JobRunIn, JobTriggerEvent } from "../engine/types/index.ts";
7
+ import type { Logger } from "../logging/types.ts";
8
+ import type { Meter, SerializedTraceContext, Tracer } from "../observability/index.ts";
9
+ import type { JobLogEntry, JobSubjectValue } from "./job-runner-types.ts";
10
+ export declare function isJobTriggerEvent(value: unknown): value is JobTriggerEvent;
11
+ export declare function stampGatedWriteOrigin(data: Record<string, unknown>): void;
12
+ export declare function createJobLogger(logs: JobLogEntry[], baseContext?: Record<string, unknown>): Logger;
13
+ export declare function serializeJobSubject(subject: Readonly<Record<string, JobSubjectValue>>): string;
14
+ export declare function jobSubjectKey(jobName: string, payload: Record<string, unknown>, fields: readonly string[] | undefined, declaration: "tenantVisibleFailure" | "tenantVisibleRun"): string | null;
15
+ export declare function resolveJobTz(config: ConfigAccessor | undefined, context: Pick<AppContext, "geoTzProvider" | "clock">): Promise<TzContext>;
16
+ export declare function missingDbProxy(jobName: string): TenantDb;
17
+ export declare function createCrossTenantReads(systemModeDb: TenantDb, reason: string, report: EscapeHatchReporter): NonNullable<JobContext["crossTenantReads"]>;
18
+ export declare function priorAttemptsOf(data: Record<string, unknown>): number;
19
+ export declare function tenantFailureMessageKey(err: unknown, declaration: JobDefinition["tenantVisibleFailure"]): string | null;
20
+ export declare const TRACE_CONTEXT_KEY = "_traceContext";
21
+ export declare const DEFER_COUNT_KEY = "_deferCount";
22
+ export declare const HANDLER_DEFER_COUNT_KEY = "_handlerDeferCount";
23
+ export declare const DEFAULT_MAX_DEFERS = 100;
24
+ export declare const DEFER_DELAY_CAP_MS = 30000;
25
+ export declare const DEFER_BACKOFF_MAX_DOUBLINGS = 5;
26
+ export declare function readTraceContext(data: Record<string, unknown>): SerializedTraceContext | undefined;
27
+ export declare function captureTraceContext(tracer: Tracer): SerializedTraceContext | undefined;
28
+ export declare function startQueueDepthPolling(queue: Queue, lane: JobRunIn, meter: Meter): Promise<ReturnType<typeof setInterval>>;
29
+ export declare function parseRedisOpts(url: string): {
30
+ host: string;
31
+ port: number;
32
+ db?: number | undefined;
33
+ };
34
+ export declare const BOOT_REDIS_TIMEOUT_MS = 10000;
35
+ export declare const STOP_QUEUE_READY_TIMEOUT_MS = 1000;
36
+ export declare const BOOT_GATE_SLOW_WARN_MS = 60000;
37
+ export declare function timeoutReject(ms: number, message: string): {
38
+ promise: Promise<never>;
39
+ cancel: () => void;
40
+ };
41
+ export declare function buildRetryBullOpts(jobDef: JobDefinition, priorAttempts?: number): Pick<JobsOptions, "attempts" | "backoff">;
42
+ export declare function payloadMatchesTriggerFilter(where: Readonly<Record<string, string | number | boolean>> | undefined, payload: Record<string, unknown>): boolean;
43
+ export declare const COMPLETED_JOB_RETENTION_AGE_SEC = 86400;
44
+ export declare const FAILED_JOB_RETENTION_AGE_SEC = 604800;
45
+ export declare function maxRetryWindowMs(jobDef: JobDefinition): number;