@cosmicdrift/kumiko-types 0.413.1 → 0.415.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 (38) hide show
  1. package/dist/access-predicates.d.ts +1 -0
  2. package/dist/access-predicates.js +3 -3
  3. package/dist/config.d.ts +4 -2
  4. package/dist/config.js +10 -1
  5. package/dist/db-connection.d.ts +0 -3
  6. package/dist/define-handler.d.ts +1 -1
  7. package/dist/delivery-attempt-types.d.ts +22 -0
  8. package/dist/delivery-attempt-types.js +31 -0
  9. package/dist/entity-table-meta-types.d.ts +2 -0
  10. package/dist/event-store-executor-types.d.ts +4 -1
  11. package/dist/feature.d.ts +1 -0
  12. package/dist/fields.d.ts +2 -0
  13. package/dist/handler-context-facets.d.ts +5 -2
  14. package/dist/handler-context-services.d.ts +2 -1
  15. package/dist/handlers.d.ts +17 -15
  16. package/dist/handlers.js +4 -32
  17. package/dist/hooks.d.ts +1 -0
  18. package/dist/identifiers.d.ts +7 -0
  19. package/dist/identifiers.js +20 -0
  20. package/dist/minor-units.d.ts +3 -0
  21. package/dist/minor-units.js +11 -0
  22. package/dist/multi-stream-apply-context-types.d.ts +3 -1
  23. package/dist/named-list-filters.d.ts +39 -0
  24. package/dist/nav-icon.js +1 -1
  25. package/dist/observability/provider.d.ts +0 -2
  26. package/dist/projection.d.ts +2 -1
  27. package/dist/screen-dashboard.d.ts +139 -22
  28. package/dist/screen-entity-edit.d.ts +2 -0
  29. package/dist/screen-entity-list.d.ts +83 -19
  30. package/dist/screen-list-table.d.ts +49 -3
  31. package/dist/screen-list-table.js +35 -1
  32. package/dist/screen-projection.d.ts +58 -4
  33. package/dist/screen-record-guidance.d.ts +96 -0
  34. package/dist/screen-record-guidance.js +1 -0
  35. package/dist/screen.d.ts +9 -5
  36. package/dist/screen.js +1 -1
  37. package/package.json +9 -1
  38. package/src/changes.json +6 -0
@@ -1,4 +1,5 @@
1
1
  import type { AccessRule } from "./handlers.ts";
2
2
  export declare function isOpenToAllGranted(rule: AccessRule): boolean;
3
+ export declare const ANONYMOUS_ROLE: "anonymous";
3
4
  export declare function isUiAccessGranted(access: AccessRule | undefined, userRoles: readonly string[] | undefined): boolean;
4
5
  export declare function isAnySignedInGranted(rule: AccessRule): boolean;
@@ -11,7 +11,7 @@ export function isOpenToAllGranted(rule) {
11
11
  return false;
12
12
  return openToAll.reason.trim().length > 0;
13
13
  }
14
- const ANONYMOUS_ACCESS_ROLE = "anonymous";
14
+ export const ANONYMOUS_ROLE = "anonymous";
15
15
  // UI-visibility predicate — DEFAULT-VISIBLE, the opposite stance from
16
16
  // `hasAccess` and the default-deny doc on AccessRule (handlers.ts): an unset `access`
17
17
  // means every signed-in user may see the screen/nav/workspace, not nobody.
@@ -28,7 +28,7 @@ export function isUiAccessGranted(access, userRoles) {
28
28
  return false;
29
29
  if (!("roles" in access))
30
30
  return false;
31
- if (isAnySignedInGranted(access) && !userRoles.includes(ANONYMOUS_ACCESS_ROLE))
31
+ if (isAnySignedInGranted(access) && !userRoles.includes(ANONYMOUS_ROLE))
32
32
  return true;
33
33
  return access.roles.some((role) => userRoles.includes(role));
34
34
  }
@@ -38,5 +38,5 @@ export function isAnySignedInGranted(rule) {
38
38
  if (!("roles" in rule) || !("anySignedIn" in rule))
39
39
  return false;
40
40
  const anySignedIn = rule.anySignedIn;
41
- return (anySignedIn === true && Array.isArray(rule.roles) && rule.roles.includes(ANONYMOUS_ACCESS_ROLE));
41
+ return anySignedIn === true && Array.isArray(rule.roles) && rule.roles.includes(ANONYMOUS_ROLE);
42
42
  }
package/dist/config.d.ts CHANGED
@@ -56,7 +56,7 @@ export type ConfigKeyDefinition<T extends ConfigKeyType = ConfigKeyType> = {
56
56
  readonly options?: readonly string[];
57
57
  /** Only on `type: "select"`: loads the options from this query (QN), contract
58
58
  * `{ rows: { value, label, description?, group? }[] }`, labels shown verbatim. Mutually exclusive with
59
- * `options` and `allowPerRequest`. The write side does not check the value against
59
+ * `options`. The write side does not check the value against
60
60
  * the query result; the consuming feature validates it where it uses it. */
61
61
  readonly optionsQuery?: string;
62
62
  /** Payload sent with every `optionsQuery` call. Requires `optionsQuery`. `{ field }`
@@ -70,7 +70,6 @@ export type ConfigKeyDefinition<T extends ConfigKeyType = ConfigKeyType> = {
70
70
  };
71
71
  readonly computed?: ConfigComputedFn<T>;
72
72
  readonly validate?: ConfigValidateFn<T>;
73
- readonly allowPerRequest?: boolean;
74
73
  readonly required?: boolean;
75
74
  readonly env?: string;
76
75
  readonly inheritedToTenant?: boolean;
@@ -165,6 +164,8 @@ export type MaxPerTenantOptions = {
165
164
  readonly onLimit: "skip" | "defer";
166
165
  readonly deferMs?: number | undefined;
167
166
  };
167
+ export declare const IRREVERSIBLE_WITHOUT_ENTRY_HANDLER_CASES: readonly ["rebuildable-derived-data", "high-risk-or-cron-pipeline", "per-dispatch-record-key"];
168
+ export type IrreversibleWithoutEntryHandler = (typeof IRREVERSIBLE_WITHOUT_ENTRY_HANDLER_CASES)[number];
168
169
  export type JobDefinition = {
169
170
  readonly name: string;
170
171
  readonly handler: JobHandlerFn;
@@ -190,6 +191,7 @@ export type JobDefinition = {
190
191
  readonly tenantVisibleRun?: {
191
192
  readonly subjectFields?: readonly string[] | undefined;
192
193
  } | undefined;
194
+ readonly irreversibleWithoutEntryHandler?: IrreversibleWithoutEntryHandler | undefined;
193
195
  };
194
196
  export type NotificationRecipientFn = (result: import("./hooks.ts").SaveContext) => string | readonly string[] | {
195
197
  readonly tenant: string;
package/dist/config.js CHANGED
@@ -1 +1,10 @@
1
- export {};
1
+ // The only pipelines allowed to call an irreversible primitive (forget, hard delete, kms.eraseKey,
2
+ // readAsInstruction write) without a directly dispatched entry handler. A new case is a framework change.
3
+ // rebuildable-derived-data deletes derived data that is re-created from a reversible source
4
+ // high-risk-or-cron-pipeline runs behind a risk "high" handler or as a cron scan of expired data
5
+ // per-dispatch-record-key the step-dispatcher erasing its per-dispatch record key
6
+ export const IRREVERSIBLE_WITHOUT_ENTRY_HANDLER_CASES = [
7
+ "rebuildable-derived-data",
8
+ "high-risk-or-cron-pipeline",
9
+ "per-dispatch-record-key",
10
+ ];
@@ -2,9 +2,6 @@ import type { SQL, TransactionSQL } from "bun";
2
2
  export type RawDbClient = {
3
3
  unsafe: (sql: string, params?: readonly unknown[]) => PromiseLike<readonly unknown[]>;
4
4
  begin: <T>(fn: (tx: DbTx) => Promise<T>) => Promise<T>;
5
- end?: (options?: {
6
- timeout?: number;
7
- }) => Promise<void>;
8
5
  };
9
6
  export type DbConnection = SQL | RawDbClient;
10
7
  export type DbTx = TransactionSQL | RawDbClient;
@@ -9,7 +9,7 @@ export type WriteHandlerDefinition<TName extends string = string, TSchema extend
9
9
  readonly access: AccessRule;
10
10
  readonly description?: string;
11
11
  readonly agent?: AgentHandlerHints;
12
- /** Set by the entity convention create/update handlers; see WriteHandlerDef.excludedFields. */
12
+ /** Set by the entity convention create/update handlers; a hand-written handler may declare it for its form, its own schema still decides what it accepts. See WriteHandlerDef.excludedFields. */
13
13
  readonly excludedFields?: readonly string[];
14
14
  readonly unsafeSkipTransitionGuard?: boolean;
15
15
  readonly rateLimit?: RateLimitDeclaration;
@@ -0,0 +1,22 @@
1
+ import type { DbConnection } from "./db-connection.ts";
2
+ import type { NotifyPriority, SessionUser } from "./handlers.ts";
3
+ import type { TenantId } from "./identifiers.ts";
4
+ export type NotifyDeliveryStatus = "queued" | "sent" | "failed" | "skipped";
5
+ export declare const DELIVERY_FAILURE_CODES: readonly ["timeout", "network_error", "redirect_blocked", "host_not_allowed", "missing_credentials", "invalid_address", "unexpected_response", "render_failed", "send_failed", "channel_error"];
6
+ export declare const DELIVERY_SKIP_REASONS: readonly ["channel_disabled", "preference_disabled", "rate_limited", "no_address", "unsubscribed", "duplicate_idempotency_key"];
7
+ export type DeliveryFailureCode = (typeof DELIVERY_FAILURE_CODES)[number];
8
+ export type DeliverySkipReason = (typeof DELIVERY_SKIP_REASONS)[number];
9
+ export type DeliveryErrorCode = DeliveryFailureCode | DeliverySkipReason | `http_${number}`;
10
+ export type ChatSendFailureCode = `http_${number}` | Exclude<DeliveryFailureCode, "render_failed" | "send_failed" | "channel_error">;
11
+ export declare const HTTP_ERROR_CODE_PATTERN: RegExp;
12
+ export declare function isDeliveryErrorCode(value: unknown): value is DeliveryErrorCode;
13
+ export type DeliveryAttemptLogEntry = {
14
+ readonly notificationType: string;
15
+ readonly channel: string;
16
+ readonly recipientAddress: string | null;
17
+ readonly status: NotifyDeliveryStatus;
18
+ readonly error: DeliveryErrorCode | null;
19
+ readonly priority: NotifyPriority;
20
+ };
21
+ export type DeliveryAttemptLogFn = (entry: DeliveryAttemptLogEntry) => Promise<void>;
22
+ export type DeliveryAttemptLogFactory = (user: SessionUser, tenantId: TenantId, deliveryDb?: DbConnection) => DeliveryAttemptLogFn;
@@ -0,0 +1,31 @@
1
+ // Closed vocabulary for delivery failures and skips. Only these values (plus
2
+ // `http_<status>`) are ever stored or returned as a delivery error, never a raw
3
+ // error message: provider errors can carry URLs, tokens and addresses.
4
+ export const DELIVERY_FAILURE_CODES = [
5
+ "timeout",
6
+ "network_error",
7
+ "redirect_blocked",
8
+ "host_not_allowed",
9
+ "missing_credentials",
10
+ "invalid_address",
11
+ "unexpected_response",
12
+ "render_failed",
13
+ "send_failed",
14
+ "channel_error",
15
+ ];
16
+ export const DELIVERY_SKIP_REASONS = [
17
+ "channel_disabled",
18
+ "preference_disabled",
19
+ "rate_limited",
20
+ "no_address",
21
+ "unsubscribed",
22
+ "duplicate_idempotency_key",
23
+ ];
24
+ export const HTTP_ERROR_CODE_PATTERN = /^http_(\d{3})$/;
25
+ export function isDeliveryErrorCode(value) {
26
+ if (typeof value !== "string")
27
+ return false;
28
+ return (HTTP_ERROR_CODE_PATTERN.test(value) ||
29
+ DELIVERY_FAILURE_CODES.some((code) => code === value) ||
30
+ DELIVERY_SKIP_REASONS.some((reason) => reason === value));
31
+ }
@@ -30,6 +30,7 @@ export type EntityTableMeta = {
30
30
  readonly source: "managed" | "unmanaged";
31
31
  readonly piiSubjectFields?: readonly string[];
32
32
  readonly tenancy?: "global";
33
+ readonly systemRowsReadable?: true;
33
34
  };
34
35
  export type BuildEntityTableMetaOptions = {
35
36
  readonly featureName?: string;
@@ -42,4 +43,5 @@ export type UnmanagedTableInput = {
42
43
  readonly indexes?: readonly IndexMeta[];
43
44
  readonly compositePrimaryKey?: CompositePrimaryKeyMeta;
44
45
  readonly tenancy?: EntityTenancy;
46
+ readonly systemRowsReadable?: true;
45
47
  };
@@ -15,6 +15,7 @@ export type ParentVisibilityOption = {
15
15
  }>;
16
16
  readonly canReadEntity?: (entityName: string) => boolean;
17
17
  readonly resolveTable?: (entityName: string) => SchemaTable;
18
+ readonly globalHostIds?: ReadonlyMap<string, readonly string[]>;
18
19
  };
19
20
  export type ParentVisibilityGateOption = {
20
21
  readonly parentVisibility?: ParentVisibilityOption;
@@ -91,7 +92,9 @@ export type EventStoreExecutor<Row extends Record<string, unknown> = Record<stri
91
92
  }) => Promise<WriteResult<SaveContext>>;
92
93
  delete: (payload: {
93
94
  id: EntityId;
94
- }, user: SessionUser, db: TenantDb, options?: StreamTenantOption & LockGuardOption & ParentVisibilityGateOption) => Promise<WriteResult<DeleteContext>>;
95
+ }, user: SessionUser, db: TenantDb, options?: StreamTenantOption & LockGuardOption & ParentVisibilityGateOption & {
96
+ expect?: Readonly<Record<string, string | number | boolean | null>>;
97
+ }) => Promise<WriteResult<DeleteContext>>;
95
98
  forget: (payload: {
96
99
  id: EntityId;
97
100
  }, user: SessionUser, db: TenantDb, options?: StreamTenantOption & ParentVisibilityGateOption) => Promise<WriteResult<DeleteContext>>;
package/dist/feature.d.ts CHANGED
@@ -37,6 +37,7 @@ export type SecretKeyDefinition = {
37
37
  };
38
38
  readonly scope: "tenant";
39
39
  readonly required?: boolean;
40
+ readonly requiredWhenConfigSet?: string;
40
41
  readonly writeRoles?: readonly string[];
41
42
  readonly valueSchema?: ZodType<string>;
42
43
  };
package/dist/fields.d.ts CHANGED
@@ -728,6 +728,8 @@ export type EntityDefinition<F extends FieldsMap = FieldsMap, T extends EntityTe
728
728
  * only for genuinely tenant-independent aggregates like `user`. The first
729
729
  * event (create) is what's routed; updates resolve the stream tenant upstream. */
730
730
  readonly systemStream?: boolean;
731
+ /** Tenant sessions may read this entity's SYSTEM_TENANT_ID rows (shared templates, config defaults, catalogs). Without it the table's row level security hides them. Never set it on credential, session, secret or PII entities. */
732
+ readonly systemRowsReadable?: boolean;
731
733
  /** Lets token scopes release `<entity>.created/.updated/.deleted` as webhook events. Off by default: bundle entities can hold secrets. */
732
734
  readonly webhookEvents?: boolean;
733
735
  /** Schema version stamped on every lifecycle event (default 1, integer 1..999). Bump it together with a step in `eventMigrations`. */
@@ -40,9 +40,11 @@ export type HandlerEvents<TMap extends object = KumikoEventTypeMap> = {
40
40
  readonly load: (aggregateId: string, options?: {
41
41
  readonly asOf?: Temporal.Instant;
42
42
  readonly atVersion?: never;
43
+ readonly includeArchived?: boolean;
43
44
  } | {
44
45
  readonly atVersion?: number;
45
46
  readonly asOf?: never;
47
+ readonly includeArchived?: boolean;
46
48
  }) => Promise<readonly import("./event-store-types.ts").StoredEvent[]>;
47
49
  readonly archive: (aggregateId: string, args: {
48
50
  readonly aggregateType: string;
@@ -85,7 +87,7 @@ export type HandlerObservability = {
85
87
  readonly metricsFor: (featureName: string) => MetricsHandle;
86
88
  readonly meter?: Meter;
87
89
  };
88
- export type JobEvents = Pick<HandlerEvents, "append">;
90
+ export type JobEvents = Pick<HandlerEvents, "append" | "load">;
89
91
  export type JobDispatch = Pick<HandlerDispatch, "write" | "writeAs" | "queryAs" | "queryAsMember">;
90
92
  export type JobObservability = Pick<HandlerObservability, "log" | "metricsFor" | "meter">;
91
93
  type PhaseWithoutTransaction = "events" | "notify" | "dispatch" | "lifecycle";
@@ -97,7 +99,8 @@ export type ReservationContext<TMap extends object = KumikoEventTypeMap> = Omit<
97
99
  readonly dispatch: Omit<HandlerDispatch, "write" | "writeAs">;
98
100
  readonly lifecycle: Omit<HandlerLifecycle, "afterCommit" | "afterRollback">;
99
101
  };
100
- /** ctx of `beforeTransaction`: external I/O and reads only, so the outside-transaction handles are gone too. */
102
+ /** ctx of `beforeTransaction`: external I/O and reads only, so the outside-transaction handles are gone too.
103
+ * A long external call that must also write (metering, provenance) belongs in `transaction: "perCommand"`. */
101
104
  export type BeforeTransactionContext<TMap extends object = KumikoEventTypeMap> = Omit<ReservationContext<TMap>, "db" | "systemDb"> & {
102
105
  readonly db: TenantDb & {
103
106
  readonly outsideTransaction: undefined;
@@ -6,7 +6,7 @@ import type { EntityCache } from "./entity-cache.ts";
6
6
  import type { EnvelopeCipher } from "./envelope-cipher-types.ts";
7
7
  import type { FileContext } from "./file-handle-types.ts";
8
8
  import type { FileProviderResolver } from "./file-provider-resolver-types.ts";
9
- import type { EscapeHatchAuditSink, JobRunnerRef, NotifyFactory, NotifyFn, SessionUser, TenantWriteTarget } from "./handlers.ts";
9
+ import type { DeliveryAttemptLogFn, EscapeHatchAuditSink, JobRunnerRef, NotifyFactory, NotifyFn, SessionUser, TenantWriteTarget } from "./handlers.ts";
10
10
  import type { TenantId } from "./identifiers.ts";
11
11
  import type { KmsAdapter } from "./kms-adapter-types.ts";
12
12
  import type { RateLimitResolver } from "./rate-limit-types.ts";
@@ -44,6 +44,7 @@ export type ContextServices = {
44
44
  readonly _notifyFactory?: NotifyFactory;
45
45
  readonly _notifyInTransactionFactory?: (user: SessionUser, tenantId: TenantId) => NotifyFn;
46
46
  readonly _notifyAfterCommitFactory?: (user: SessionUser, tenantId: TenantId) => NotifyFn;
47
+ readonly _logOwnDeliveryAttempt?: DeliveryAttemptLogFn;
47
48
  readonly _escapeHatchAuditSink?: EscapeHatchAuditSink;
48
49
  readonly extensions: Readonly<Record<string, unknown>>;
49
50
  };
@@ -4,6 +4,7 @@ import type { ZodType } from "zod";
4
4
  import type { CacheSyncBus } from "./cache-sync-types.ts";
5
5
  import type { ConfigAccessor, ConfigAccessorFactory, ConfigResolver } from "./config.ts";
6
6
  import type { DbConnection, DbTx } from "./db-connection.ts";
7
+ import type { DeliveryAttemptLogFactory, DeliveryAttemptLogFn, DeliveryErrorCode, NotifyDeliveryStatus } from "./delivery-attempt-types.ts";
7
8
  import type { DerivativesContext } from "./derivatives-types.ts";
8
9
  import type { EntityCache } from "./entity-cache.ts";
9
10
  import type { EventMetadata, WriteOrigin } from "./event-store-types.ts";
@@ -49,7 +50,7 @@ export declare function crossTenantReadsGrantFor(declaration: EscapeHatchDeclara
49
50
  export declare function escapeHatchGrantsProblem(declaration: {
50
51
  readonly grants?: unknown;
51
52
  }): string | undefined;
52
- export { isAnySignedInGranted, isOpenToAllGranted, isUiAccessGranted, } from "./access-predicates.ts";
53
+ export { ANONYMOUS_ROLE, isAnySignedInGranted, isOpenToAllGranted, isUiAccessGranted, } from "./access-predicates.ts";
53
54
  export type SessionUserOrigin = "member-resolution";
54
55
  export type SessionUser = {
55
56
  readonly id: string;
@@ -142,6 +143,7 @@ export type JobRunnerRef = {
142
143
  readonly priority?: number;
143
144
  readonly jobId?: string;
144
145
  }): Promise<unknown>;
146
+ isJobPending?(jobName: string, jobId: string): Promise<boolean>;
145
147
  };
146
148
  export type DispatchWriteRef = {
147
149
  readonly write: (user: SessionUser, qn: string, payload: unknown, inheritedOrigin?: WriteOrigin) => Promise<WriteResult>;
@@ -162,15 +164,7 @@ export type NotifyOptions = {
162
164
  readonly immediate?: boolean;
163
165
  readonly locale?: string;
164
166
  };
165
- export type NotifyDeliveryStatus = "queued" | "sent" | "failed" | "skipped";
166
- export declare const DELIVERY_FAILURE_CODES: readonly ["timeout", "network_error", "redirect_blocked", "host_not_allowed", "missing_credentials", "invalid_address", "unexpected_response", "render_failed", "send_failed", "channel_error"];
167
- export declare const DELIVERY_SKIP_REASONS: readonly ["channel_disabled", "preference_disabled", "rate_limited", "no_address", "unsubscribed", "duplicate_idempotency_key"];
168
- export type DeliveryFailureCode = (typeof DELIVERY_FAILURE_CODES)[number];
169
- export type DeliverySkipReason = (typeof DELIVERY_SKIP_REASONS)[number];
170
- export type DeliveryErrorCode = DeliveryFailureCode | DeliverySkipReason | `http_${number}`;
171
- export type ChatSendFailureCode = `http_${number}` | Exclude<DeliveryFailureCode, "render_failed" | "send_failed" | "channel_error">;
172
- export declare const HTTP_ERROR_CODE_PATTERN: RegExp;
173
- export declare function isDeliveryErrorCode(value: unknown): value is DeliveryErrorCode;
167
+ export { type ChatSendFailureCode, DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, type DeliveryAttemptLogEntry, type DeliveryAttemptLogFactory, type DeliveryAttemptLogFn, type DeliveryErrorCode, type DeliveryFailureCode, type DeliverySkipReason, HTTP_ERROR_CODE_PATTERN, isDeliveryErrorCode, type NotifyDeliveryStatus, } from "./delivery-attempt-types.ts";
174
168
  export type NotifyDelivery = {
175
169
  readonly channel: string;
176
170
  readonly recipientId: string | null;
@@ -244,6 +238,8 @@ type SharedContextFields = {
244
238
  readonly _notifyFactory?: NotifyFactory;
245
239
  readonly _notifyInTransactionFactory?: (user: SessionUser, tenantId: TenantId) => NotifyFn;
246
240
  readonly _notifyAfterCommitFactory?: (user: SessionUser, tenantId: TenantId) => NotifyFn;
241
+ readonly _deliveryAttemptLogFactory?: DeliveryAttemptLogFactory;
242
+ readonly _logOwnDeliveryAttempt?: DeliveryAttemptLogFn;
247
243
  readonly _escapeHatchAuditSink?: EscapeHatchAuditSink;
248
244
  readonly secrets?: import("./secrets-types.ts").SecretsContext;
249
245
  readonly masterKeyProvider?: import("./secrets-types.ts").MasterKeyProvider;
@@ -268,9 +264,10 @@ export type AppContext = SharedContextFields & {
268
264
  readonly id: string;
269
265
  readonly tenantId: TenantId;
270
266
  } | null;
271
- /** For job-handler calls triggered by an event
272
- * (`r.job` with `trigger: { on: ... }`): the qualified name of the event
273
- * that fired the trigger, e.g. `inbound-mail-foundation:event:inbound-message-received`.
267
+ /** For job-handler calls triggered by an `on` entry
268
+ * (`r.job` with `trigger: { on: ... }`): the qualified name the entry matched,
269
+ * either a write/query-handler QN (e.g. `multi:write:order:open`) or an
270
+ * `r.defineEvent` QN (e.g. `inbound-mail-foundation:event:inbound-message-received`).
274
271
  * For multi-trigger jobs (`on: [...]`), this is the only way for the
275
272
  * handler to know which trigger fired. Cron and manual jobs leave the
276
273
  * field undefined. */
@@ -348,7 +345,6 @@ export type JobContext = {
348
345
  readonly dispatch: JobDispatch;
349
346
  readonly triggerName?: string;
350
347
  readonly triggerEvent?: JobTriggerEvent;
351
- readonly _tenantId?: TenantId;
352
348
  readonly config?: ConfigAccessor;
353
349
  readonly notify?: NotifyFn;
354
350
  readonly services: ContextServices;
@@ -546,7 +542,9 @@ export type BeforeTransactionResult<TPrepared = unknown> = {
546
542
  readonly release?: ReservationRelease;
547
543
  } | WriteFailure;
548
544
  export type BeforeTransactionPhase<TPayload = unknown, TPrepared = unknown> = (event: WriteEvent<TPayload>, ctx: BeforeTransactionContext) => Promise<BeforeTransactionResult<TPrepared>>;
549
- /** `perCommand`: the handler runs without a transaction of its own. */
545
+ /** `perCommand`: the handler runs without a transaction of its own. Use it for long external calls
546
+ * (minutes) that also need writes: no transaction or connection is held while the call runs, and
547
+ * `ctx.db.outsideTransaction` stays available for metering/provenance bookings. */
550
548
  export type WriteHandlerTransactionMode = "perCommand";
551
549
  /** `skip`: run as a nested write without the `beforeTransaction` phase. */
552
550
  export type BeforeTransactionWhenNested = "skip";
@@ -558,6 +556,9 @@ export type WriteHandlerDef = {
558
556
  readonly name: string;
559
557
  readonly schema: ZodType;
560
558
  readonly handler: WriteHandlerFn;
559
+ /** Set only by defineEntityWriteHandler, which wires the parent-ref visibility gate.
560
+ * The boot-validator requires it on a `parentRef` entity's update/delete/restore handler. */
561
+ readonly [ENTITY_CONVENTION_WRITE_BRAND]?: true;
561
562
  readonly access: AccessRule;
562
563
  readonly description?: string;
563
564
  readonly agent?: AgentHandlerHints;
@@ -600,6 +601,7 @@ export type WriteHandlerDef = {
600
601
  readonly perform?: import("./step.ts").PipelineDef;
601
602
  };
602
603
  export declare const ENTITY_CONVENTION_QUERY_BRAND = "__kumikoEntityConventionQuery";
604
+ export declare const ENTITY_CONVENTION_WRITE_BRAND = "__kumikoEntityConventionWrite";
603
605
  export type QueryHandlerDef = {
604
606
  readonly name: string;
605
607
  readonly schema: ZodType;
package/dist/handlers.js CHANGED
@@ -39,38 +39,8 @@ export function escapeHatchGrantsProblem(declaration) {
39
39
  }
40
40
  return undefined;
41
41
  }
42
- export { isAnySignedInGranted, isOpenToAllGranted, isUiAccessGranted, } from "./access-predicates.js";
43
- // Closed vocabulary for delivery failures and skips. Only these values (plus
44
- // `http_<status>`) are ever stored or returned as a delivery error, never a raw
45
- // error message: provider errors can carry URLs, tokens and addresses.
46
- export const DELIVERY_FAILURE_CODES = [
47
- "timeout",
48
- "network_error",
49
- "redirect_blocked",
50
- "host_not_allowed",
51
- "missing_credentials",
52
- "invalid_address",
53
- "unexpected_response",
54
- "render_failed",
55
- "send_failed",
56
- "channel_error",
57
- ];
58
- export const DELIVERY_SKIP_REASONS = [
59
- "channel_disabled",
60
- "preference_disabled",
61
- "rate_limited",
62
- "no_address",
63
- "unsubscribed",
64
- "duplicate_idempotency_key",
65
- ];
66
- export const HTTP_ERROR_CODE_PATTERN = /^http_(\d{3})$/;
67
- export function isDeliveryErrorCode(value) {
68
- if (typeof value !== "string")
69
- return false;
70
- return (HTTP_ERROR_CODE_PATTERN.test(value) ||
71
- DELIVERY_FAILURE_CODES.some((code) => code === value) ||
72
- DELIVERY_SKIP_REASONS.some((reason) => reason === value));
73
- }
42
+ export { ANONYMOUS_ROLE, isAnySignedInGranted, isOpenToAllGranted, isUiAccessGranted, } from "./access-predicates.js";
43
+ export { DELIVERY_FAILURE_CODES, DELIVERY_SKIP_REASONS, HTTP_ERROR_CODE_PATTERN, isDeliveryErrorCode, } from "./delivery-attempt-types.js";
74
44
  export function normalizeEventPiiSubject(spec) {
75
45
  if ("subjectField" in spec) {
76
46
  return { kind: "user", ownerField: spec.subjectField, whenAbsent: undefined };
@@ -122,3 +92,5 @@ export function isRateLimitDisabled(v) {
122
92
  // workspace can evaluate a module twice, and two Symbol() calls would not
123
93
  // compare equal across those instances.
124
94
  export const ENTITY_CONVENTION_QUERY_BRAND = "__kumikoEntityConventionQuery";
95
+ // Same brand for the update/delete/restore handlers built by defineEntityWriteHandler.
96
+ export const ENTITY_CONVENTION_WRITE_BRAND = "__kumikoEntityConventionWrite";
package/dist/hooks.d.ts CHANGED
@@ -16,6 +16,7 @@ export type SaveContext = {
16
16
  readonly isNew: boolean;
17
17
  readonly entityName?: string | undefined;
18
18
  readonly event: StoredEvent;
19
+ readonly claimedFileRefIds?: readonly string[] | undefined;
19
20
  };
20
21
  export type DeleteContext = {
21
22
  readonly kind: "delete";
@@ -1,6 +1,13 @@
1
1
  export type TenantId = string;
2
2
  export declare const UUID_SHAPE_PATTERN = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$";
3
3
  export declare function isUuid(value: unknown): value is string;
4
+ export type FileHandleParts = {
5
+ readonly id: string;
6
+ readonly claim: string;
7
+ };
8
+ export declare function parseFileHandle(value: unknown): FileHandleParts | null;
9
+ export declare function isFileHandle(value: unknown): value is string;
10
+ export declare function formatFileHandle(parts: FileHandleParts): string;
4
11
  export declare function parseTenantId(value: unknown): TenantId | null;
5
12
  export declare const SYSTEM_TENANT_ID: TenantId;
6
13
  export declare function isSystemTenant(tenantId: TenantId | null | undefined): boolean;
@@ -20,6 +20,26 @@ const TENANT_ID_REGEX = new RegExp(UUID_SHAPE_PATTERN);
20
20
  export function isUuid(value) {
21
21
  return typeof value === "string" && TENANT_ID_REGEX.test(value);
22
22
  }
23
+ // Opaque reference to a pending anonymous upload: `<fileRef uuid>.<claim>`. The claim is 32 random
24
+ // bytes, base64url; the server stores only its hash. Field values accept this form and persist
25
+ // the bare uuid.
26
+ const FILE_HANDLE_CLAIM_PATTERN = /^[A-Za-z0-9_-]{43}$/;
27
+ export function parseFileHandle(value) {
28
+ if (typeof value !== "string")
29
+ return null;
30
+ const separator = value.indexOf(".");
31
+ if (separator === -1)
32
+ return null;
33
+ const id = value.slice(0, separator);
34
+ const claim = value.slice(separator + 1);
35
+ return isUuid(id) && FILE_HANDLE_CLAIM_PATTERN.test(claim) ? { id, claim } : null;
36
+ }
37
+ export function isFileHandle(value) {
38
+ return parseFileHandle(value) !== null;
39
+ }
40
+ export function formatFileHandle(parts) {
41
+ return `${parts.id}.${parts.claim}`;
42
+ }
23
43
  // Validates a candidate string against the tenantId format and returns it
24
44
  // as a TenantId, or `null` when it doesn't match. Use at every system
25
45
  // boundary that admits untrusted input (HTTP headers, cookies, query
@@ -0,0 +1,3 @@
1
+ export declare const MINOR_UNIT_SCALE = 100;
2
+ export declare function toMajorUnits(minorAmount: number): number;
3
+ export declare function toMinorUnits(amount: number): number;
@@ -0,0 +1,11 @@
1
+ // 2 decimal places (cents) — covers every currently-supported currency
2
+ // (EUR/USD/GBP/...). No ISO-4217 minor-unit table yet (JPY=0, BHD=3) —
3
+ // upgrade path once a currency needing a different scale actually lands.
4
+ // Client-safe on purpose: db/money.ts pulls in engine runtime values.
5
+ export const MINOR_UNIT_SCALE = 100;
6
+ export function toMajorUnits(minorAmount) {
7
+ return minorAmount / MINOR_UNIT_SCALE;
8
+ }
9
+ export function toMinorUnits(amount) {
10
+ return Math.round(amount * MINOR_UNIT_SCALE);
11
+ }
@@ -8,7 +8,9 @@ import type { AppendEventFn, UnsafeAppendEventFn } from "./handlers.ts";
8
8
  import type { SecretsContext } from "./secrets-types.ts";
9
9
  export type MultiStreamApplyContext<TMap extends object = KumikoEventTypeMap> = {
10
10
  readonly appendEvent: AppendEventFn<TMap>;
11
- readonly unsafeAppendEvent: UnsafeAppendEventFn;
11
+ readonly unsafeAppendEvent: (args: Parameters<UnsafeAppendEventFn>[0] & {
12
+ readonly expectedVersion?: number;
13
+ }) => Promise<void>;
12
14
  readonly loadAggregate: (aggregateId: string, options?: {
13
15
  readonly asOf?: Temporal.Instant;
14
16
  }) => Promise<readonly StoredEvent[]>;
@@ -16,8 +16,47 @@ export type NamedListFilterDef = {
16
16
  readonly access?: AccessRule;
17
17
  readonly filters: (scope: NamedListFilterScope) => readonly NamedListFilterCondition[] | Promise<readonly NamedListFilterCondition[]>;
18
18
  };
19
+ export type ListSortDir = "asc" | "desc";
20
+ export type ListSortSpec = {
21
+ readonly field: string;
22
+ readonly dir: ListSortDir;
23
+ };
24
+ /** One entry of a list's "Sorted by" select; `label` is an i18n key. */
25
+ export type ListSortOption = ListSortSpec & {
26
+ readonly label: string;
27
+ };
19
28
  /** Screen-side reference to an entity-defined named filter; `label` is an i18n key. */
20
29
  export type NamedListFilterRef = {
21
30
  readonly id: string;
22
31
  readonly label: string;
32
+ /** Order while this filter is active and the URL carries no sort of its own; wins over `defaultSort`. */
33
+ readonly sort?: ListSortSpec;
34
+ /** Select display: draws a divider line below this entry in the dropdown. */
35
+ readonly separatorAfter?: boolean;
36
+ /**
37
+ * Shown instead of the generic "no matches" text when this filter yields an empty list and no
38
+ * search term is set; `title` and `description` are i18n keys. `paramsQuery` (a registered query
39
+ * QN, called without payload) runs only while the empty state is on screen; its result object
40
+ * becomes the translation params of `description`. Without data (loading, error, no result) the
41
+ * description is not rendered.
42
+ */
43
+ readonly emptyState?: {
44
+ readonly title: string;
45
+ readonly description?: string;
46
+ readonly paramsQuery?: string;
47
+ };
48
+ /** Set by the framework from the entity filter's access rule; the client offers the chip only where `isUiAccessGranted` allows it. */
49
+ readonly access?: AccessRule;
50
+ };
51
+ /** How the named filters appear in the toolbar: toggle chips (default) or one single-choice select. */
52
+ export type NamedFilterFacetOptions = {
53
+ readonly display?: "chips" | "select";
54
+ /** Ask the list query for each filter's hit count and show it next to the label. */
55
+ readonly showCounts?: boolean;
56
+ /** Select entries with zero hits are left out (the active one and "all" stay). Asks for the counts like `showCounts` does, without showing the number unless `showCounts` is set. */
57
+ readonly hideEmpty?: boolean;
58
+ /** "behind-filter": the select opens behind the filter icon next to the search field instead of standing as a pill. Only below `sm`; otherwise it stays a pill. */
59
+ readonly narrowPlacement?: "behind-filter";
60
+ /** i18n key for the select's label, e.g. "To do". */
61
+ readonly label?: string;
23
62
  };
package/dist/nav-icon.js CHANGED
@@ -10,7 +10,7 @@
10
10
  // versa.
11
11
  //
12
12
  // Declared as a runtime tuple (not just a type) so callers that sit outside
13
- // this repo's `tsc --build` project graph (e.g. samples, external consumers
13
+ // this repo's typecheck project graph (e.g. samples, external consumers
14
14
  // resolving from a stale published type) still get a runtime-checkable
15
15
  // vocabulary — the boot-validator's validateActionHasIcon checks a declared
16
16
  // icon against NAV_ICON_KEYS instead of trusting "not undefined".
@@ -8,8 +8,6 @@ export type SamplingConfig = {
8
8
  };
9
9
  };
10
10
  export type SensitiveFilterConfig = {
11
- readonly redactedHeaders: readonly string[];
12
- readonly redactedQueryParams: readonly string[];
13
11
  readonly redactedAttributeKeyPatterns: readonly RegExp[];
14
12
  };
15
13
  export type ObservabilityOptions = {
@@ -1,4 +1,4 @@
1
- import type { RunIn } from "./config.ts";
1
+ import type { IrreversibleWithoutEntryHandler, RunIn } from "./config.ts";
2
2
  import type { StoredEvent } from "./event-store-types.ts";
3
3
  import type { ExecutorManagedRunner } from "./executor-brand.ts";
4
4
  import type { EntityDefinition } from "./fields.ts";
@@ -38,4 +38,5 @@ export type MultiStreamProjectionDefinition = {
38
38
  readonly runIn?: RunIn;
39
39
  readonly delivery?: "shared" | "per-instance";
40
40
  readonly startFrom?: "beginning" | "now";
41
+ readonly irreversibleWithoutEntryHandler?: IrreversibleWithoutEntryHandler;
41
42
  };