@kindgi/runtime 0.0.0-bootstrap.0 → 0.1.1

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 (58) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +65 -1
  3. package/dist/bindings.d.ts +186 -0
  4. package/dist/bindings.d.ts.map +1 -0
  5. package/dist/bindings.js +4 -0
  6. package/dist/bindings.js.map +1 -0
  7. package/dist/derivation.d.ts +123 -0
  8. package/dist/derivation.d.ts.map +1 -0
  9. package/dist/derivation.js +249 -0
  10. package/dist/derivation.js.map +1 -0
  11. package/dist/errors.d.ts +78 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +4 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/event-bus.d.ts +41 -0
  16. package/dist/event-bus.d.ts.map +1 -0
  17. package/dist/event-bus.js +13 -0
  18. package/dist/event-bus.js.map +1 -0
  19. package/dist/index.d.ts +11 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +13 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/inputs.d.ts +160 -0
  24. package/dist/inputs.d.ts.map +1 -0
  25. package/dist/inputs.js +4 -0
  26. package/dist/inputs.js.map +1 -0
  27. package/dist/runs.d.ts +73 -0
  28. package/dist/runs.d.ts.map +1 -0
  29. package/dist/runs.js +4 -0
  30. package/dist/runs.js.map +1 -0
  31. package/dist/schedulers/registry.d.ts +142 -0
  32. package/dist/schedulers/registry.d.ts.map +1 -0
  33. package/dist/schedulers/registry.js +4 -0
  34. package/dist/schedulers/registry.js.map +1 -0
  35. package/dist/schedulers/types.d.ts +99 -0
  36. package/dist/schedulers/types.d.ts.map +1 -0
  37. package/dist/schedulers/types.js +4 -0
  38. package/dist/schedulers/types.js.map +1 -0
  39. package/dist/types.d.ts +317 -0
  40. package/dist/types.d.ts.map +1 -0
  41. package/dist/types.js +6 -0
  42. package/dist/types.js.map +1 -0
  43. package/dist/versioning.d.ts +59 -0
  44. package/dist/versioning.d.ts.map +1 -0
  45. package/dist/versioning.js +89 -0
  46. package/dist/versioning.js.map +1 -0
  47. package/package.json +50 -4
  48. package/src/bindings.ts +244 -0
  49. package/src/derivation.ts +354 -0
  50. package/src/errors.ts +92 -0
  51. package/src/event-bus.ts +51 -0
  52. package/src/index.ts +13 -0
  53. package/src/inputs.ts +169 -0
  54. package/src/runs.ts +84 -0
  55. package/src/schedulers/registry.ts +197 -0
  56. package/src/schedulers/types.ts +112 -0
  57. package/src/types.ts +378 -0
  58. package/src/versioning.ts +110 -0
@@ -0,0 +1,51 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { Result, TenantId } from '@kindgi/types';
5
+
6
+ /**
7
+ * Minimal structural interface the kernel calls into after each
8
+ * successful journal write. Deliberately narrower than
9
+ * `@kindgi/api`'s `EventBusBinding` so the kernel can accept
10
+ * either the API-side binding OR a plain publish-only shim without
11
+ * a hard dependency on `@kindgi/api`.
12
+ *
13
+ * The full binding (with `subscribe`) is defined in
14
+ * `packages/api/src/event-bus-binding.ts`. That type is structurally
15
+ * assignable to this one — a caller wiring the same instance into
16
+ * both sides just works.
17
+ *
18
+ * `publish` MUST NOT throw. Errors are returned in the `Result` so
19
+ * the kernel can log-and-continue: a publish failure MUST NOT roll
20
+ * back the journal append (that would be a correctness bug — the
21
+ * journal is the source of truth).
22
+ */
23
+ export interface KernelEventBusBinding {
24
+ publish(
25
+ tenantId: TenantId,
26
+ channel: string,
27
+ doc: unknown,
28
+ ): Promise<Result<void, { readonly message: string }>>;
29
+ /**
30
+ * Publish `docs` to the channel, in order, as one write. The kernel
31
+ * writes a run's journal entries in batches and publishes each batch
32
+ * with this when the binding has it, and entry by entry with
33
+ * `publish` otherwise. Same contract as `publish`: never throws.
34
+ */
35
+ publishMany?(
36
+ tenantId: TenantId,
37
+ channel: string,
38
+ docs: readonly unknown[],
39
+ ): Promise<Result<void, { readonly message: string }>>;
40
+ }
41
+
42
+ /**
43
+ * Derived per-run channel name. Every kernel journal write publishes
44
+ * to this channel; SSE consumers subscribe with the same shape.
45
+ *
46
+ * Deliberately colon-namespaced so other channels (e.g.
47
+ * `kernel:trigger:<id>`) cannot collide with it.
48
+ */
49
+ export function kernelRunChannel(runId: string): string {
50
+ return `kernel:run:${runId}`;
51
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ export * from './types.js';
5
+ export * from './errors.js';
6
+ export * from './derivation.js';
7
+ export * from './versioning.js';
8
+ export * from './event-bus.js';
9
+ export * from './schedulers/types.js';
10
+ export * from './schedulers/registry.js';
11
+ export * from './inputs.js';
12
+ export * from './runs.js';
13
+ export * from './bindings.js';
package/src/inputs.ts ADDED
@@ -0,0 +1,169 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { Principal } from '@kindgi/authz';
5
+ import type { Flow } from '@kindgi/flow';
6
+ import type { HandlerRegistry } from '@kindgi/handler';
7
+ import type { NodeId, ProjectId, Result, RunId, TenantId } from '@kindgi/types';
8
+
9
+ import type { HandlerMissingError } from './errors.js';
10
+ import type { KernelEventBusBinding } from './event-bus.js';
11
+ import type { RunOptions } from './types.js';
12
+
13
+ /**
14
+ * Resolver contract for `subgraph` node dispatch. Structurally
15
+ * satisfied by `FlowRegistryBinding.getVersion(...)` in `@kindgi/api`.
16
+ * The runtime invokes the resolver with the PARENT run's tenantId; a
17
+ * flow registered under a different tenant returns `null`, which is
18
+ * the cross-tenant guarantee (rejection at dispatch with
19
+ * `subgraph-flow-not-found` attribution).
20
+ */
21
+ export interface FlowResolver {
22
+ getVersion(input: {
23
+ readonly tenantId: TenantId;
24
+ readonly flowId: string;
25
+ readonly version: string;
26
+ }): Promise<Flow | null>;
27
+ }
28
+
29
+ /**
30
+ * The node in a parent run that started this run as its child — a
31
+ * subgraph node's sub-run, or the agent turn an agent step launches.
32
+ * Stored on the child's row, so a parent (or a caller) can find the
33
+ * children of a run, and a resumed child can wake the parent that waits
34
+ * on it.
35
+ */
36
+ export interface ParentRunRef {
37
+ readonly runId: RunId;
38
+ readonly nodeId: NodeId;
39
+ /**
40
+ * Distinguishes children of the same node — one per loop iteration.
41
+ * The enclosing loop stack, serialized; `''` for a node outside any
42
+ * loop.
43
+ */
44
+ readonly scope: string;
45
+ }
46
+
47
+ /**
48
+ * Binds the handlers for a flow the runtime is about to run or resume —
49
+ * the root flow and every child flow a subgraph node starts. Lets a
50
+ * child flow get handlers for its own nodes instead of sharing the
51
+ * parent's registry.
52
+ */
53
+ export interface HandlerResolver {
54
+ resolve(input: {
55
+ readonly tenantId: TenantId;
56
+ readonly projectId: ProjectId;
57
+ readonly flow: Flow;
58
+ readonly dryRun: boolean;
59
+ }): Promise<Result<HandlerRegistry, HandlerMissingError>>;
60
+ }
61
+
62
+ export interface RunFlowInput {
63
+ readonly tenantId: TenantId;
64
+ /**
65
+ * Content-scope anchor. REQUIRED — every kernel run is bound to
66
+ * exactly one project inside the tenant. Callers without a natural
67
+ * project id resolve to the tenant's Default via
68
+ * `projectBinding.getDefault(tenantId)` at the caller layer.
69
+ */
70
+ readonly projectId: ProjectId;
71
+ readonly flow: Flow;
72
+ readonly handlers: HandlerRegistry;
73
+ readonly input: unknown;
74
+ readonly options?: RunOptions;
75
+ /**
76
+ * Optional flow resolver — required for flows that contain
77
+ * `subgraph` nodes. Absent resolver → subgraph nodes fail
78
+ * immediately with reason `resolver-missing`.
79
+ */
80
+ readonly flowResolver?: FlowResolver;
81
+ /**
82
+ * Internal: subgraph depth of THIS run. Populated by the runtime
83
+ * when a parent's `subgraph` node dispatches a child. External
84
+ * callers do NOT set this — it always starts at `0` for a
85
+ * top-level `RunBinding.runGraph` call.
86
+ */
87
+ readonly subgraphDepth?: number;
88
+ /** The parent node that started this run, when it is a child run. */
89
+ readonly parent?: ParentRunRef;
90
+ /**
91
+ * Run an existing `pending` row (created by `startRun`) instead of
92
+ * inserting a new one — how a caller hands back a run id before the
93
+ * run finishes.
94
+ */
95
+ readonly runId?: RunId;
96
+ /** Handlers for child flows; see `HandlerResolver`. */
97
+ readonly handlerResolver?: HandlerResolver;
98
+ /**
99
+ * Optional push-based event bus. When set, the runtime publishes
100
+ * a `JournalEntry`-shaped `doc` to `kernel:run:<runId>` after each
101
+ * successful journal write. When absent, journal writes still land
102
+ * durably.
103
+ */
104
+ readonly eventBus?: KernelEventBusBinding;
105
+ /**
106
+ * Authorization — the Principal on whose authority this run
107
+ * executes. When set together with `authz`, every `ctx.authorize` /
108
+ * `can` / `check` inside handlers is decided against this principal.
109
+ */
110
+ readonly principal?: Principal;
111
+ /**
112
+ * Authorization config — enables authorization checks inside the
113
+ * runtime. When absent, `ctx.authorize` is a no-op and tool
114
+ * invocations skip their pre-flight check.
115
+ */
116
+ readonly authz?: {
117
+ readonly fgaApiUrl: string;
118
+ };
119
+ }
120
+
121
+ export interface ResumeRunInput {
122
+ readonly tenantId: TenantId;
123
+ readonly runId: RunId;
124
+ readonly flow: Flow;
125
+ readonly handlers: HandlerRegistry;
126
+ readonly options?: RunOptions;
127
+ /** Same shape as `RunFlowInput.flowResolver`. */
128
+ readonly flowResolver?: FlowResolver;
129
+ /**
130
+ * Internal: subgraph depth of THIS run when it was originally
131
+ * started. Callers on the top level do NOT set this.
132
+ */
133
+ readonly subgraphDepth?: number;
134
+ /** Same shape + semantics as `RunFlowInput.handlerResolver`. */
135
+ readonly handlerResolver?: HandlerResolver;
136
+ /** Same shape + semantics as `RunFlowInput.eventBus`. */
137
+ readonly eventBus?: KernelEventBusBinding;
138
+ /**
139
+ * Authorization — carried through on resume so the resumed run
140
+ * keeps enforcing per-tool + per-subgraph checks.
141
+ */
142
+ readonly principal?: Principal;
143
+ readonly authz?: {
144
+ readonly fgaApiUrl: string;
145
+ };
146
+ }
147
+
148
+ export interface StartRunParams {
149
+ readonly tenantId: TenantId;
150
+ readonly projectId: ProjectId;
151
+ readonly flowId: string;
152
+ readonly flowVersion: string;
153
+ readonly input: unknown;
154
+ readonly dryRun?: boolean;
155
+ /** The parent node that starts this run, when it is a child run. */
156
+ readonly parent?: ParentRunRef;
157
+ }
158
+
159
+ export type StartRunError = { readonly code: 'insert-failed'; readonly message: string };
160
+
161
+ export interface DeleteRunParams {
162
+ readonly tenantId: TenantId;
163
+ readonly runId: RunId;
164
+ }
165
+
166
+ export type DeleteRunError = {
167
+ readonly code: 'not-found' | 'delete-failed';
168
+ readonly message?: string;
169
+ };
package/src/runs.ts ADDED
@@ -0,0 +1,84 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ //
5
+ // KernelRunRecord + list/get shapes — the public projection of a run
6
+ // that RunBinding exposes. Deliberately narrower than what an
7
+ // implementation stores: bookkeeping such as retention or cache markers
8
+ // is not part of this shape.
9
+ //
10
+
11
+ import type { Cursor, NodeId, OrgId, ProjectId, RunId, TenantId, Timestamp } from '@kindgi/types';
12
+
13
+ import type { RunStatus } from './types.js';
14
+
15
+ /**
16
+ * Public run shape exposed by `RunBinding.getRun` / `.listRuns`.
17
+ * Matches the fields @kindgi/api's `/v1/runs` routes serialize to
18
+ * the wire.
19
+ */
20
+ export interface KernelRunRecord {
21
+ readonly runId: RunId;
22
+ readonly tenantId: TenantId;
23
+ readonly projectId: ProjectId;
24
+ readonly flowId: string;
25
+ readonly flowVersion: string;
26
+ readonly status: RunStatus;
27
+ readonly input: unknown;
28
+ /** The run's output once it completed — the value itself, not a storage envelope. */
29
+ readonly output?: unknown;
30
+ readonly failureMessage?: string | null;
31
+ readonly dryRun: boolean;
32
+ readonly createdAt: Timestamp;
33
+ readonly updatedAt: Timestamp;
34
+ readonly completedAt?: Timestamp | null;
35
+ /** Set on a child run: the parent run and the node that started it (`ParentRunRef`). */
36
+ readonly parentRunId?: RunId | null;
37
+ readonly parentNodeId?: NodeId | null;
38
+ readonly parentScope?: string | null;
39
+ }
40
+
41
+ /**
42
+ * Content-scope filter for `RunBinding.listRuns`. Present-with-value
43
+ * narrows to a specific project or org; absent = tenant-wide (admin
44
+ * default). An org scope includes the runs of every project in that
45
+ * org.
46
+ */
47
+ export type RunListScope =
48
+ | { readonly kind: 'project'; readonly projectId: ProjectId }
49
+ | { readonly kind: 'org'; readonly orgId: OrgId };
50
+
51
+ /**
52
+ * Cursor position for `RunBinding.listRuns`. Sort key is
53
+ * `(createdAt desc, id desc)` — same discipline as every other
54
+ * kernel-facing list. Callers pass the opaque `Cursor` string
55
+ * decoded from the wire; the impl decodes it back to
56
+ * `{ createdAt, id }` internally.
57
+ */
58
+ export interface RunListCursor {
59
+ readonly createdAt: Timestamp;
60
+ readonly id: RunId;
61
+ }
62
+
63
+ export interface ListRunsInput {
64
+ readonly tenantId: TenantId;
65
+ readonly scope?: RunListScope;
66
+ readonly limit?: number;
67
+ readonly cursor?: RunListCursor;
68
+ /**
69
+ * Only the children of this run — optionally only those a given node
70
+ * (and loop scope) started.
71
+ */
72
+ readonly parent?: {
73
+ readonly runId: RunId;
74
+ readonly nodeId?: NodeId;
75
+ readonly scope?: string;
76
+ };
77
+ /** Only runs that are not a child of another run. */
78
+ readonly topLevelOnly?: boolean;
79
+ }
80
+
81
+ export interface ListRunsPage {
82
+ readonly data: readonly KernelRunRecord[];
83
+ readonly nextCursor?: Cursor;
84
+ }
@@ -0,0 +1,197 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ // Public trigger-registry shape: binding interface, register/update/list
5
+ // input variants, record shapes, error shapes, TRIGGER_KINDS constant.
6
+ // The implementation is supplied by the Kindgi runtime.
7
+
8
+ import type { Cursor, Result, TenantId, TriggerId } from '@kindgi/types';
9
+
10
+ import type { CronTriggerConfig, EventTriggerConfig, WebhookTriggerConfig } from './types.js';
11
+
12
+ export interface TriggerRegistryBinding {
13
+ register(input: RegisterTriggerInput): Promise<Result<TriggerRecord, RegisterTriggerError>>;
14
+
15
+ update(input: UpdateTriggerInput): Promise<Result<TriggerRecord, UpdateTriggerError>>;
16
+
17
+ list(input: ListTriggersInput): Promise<TriggerListPage>;
18
+
19
+ get(input: GetTriggerInput): Promise<TriggerRecord | null>;
20
+
21
+ pause(input: TriggerLifecycleInput): Promise<Result<TriggerRecord, TriggerLifecycleError>>;
22
+
23
+ resume(input: TriggerLifecycleInput): Promise<Result<TriggerRecord, TriggerLifecycleError>>;
24
+
25
+ unregister(
26
+ input: TriggerLifecycleInput,
27
+ ): Promise<{ readonly triggerId: TriggerId; readonly unregistered: boolean }>;
28
+
29
+ /**
30
+ * Webhook-receiver hot path. The receiver resolves the HMAC secret
31
+ * named by `hmacSecretName` from the tenant's secrets (`SecretBinding`
32
+ * in `@kindgi/api`) and verifies the request before spawning. Returns
33
+ * null when webhookId is unknown, the trigger is tombstoned, or
34
+ * `status !== 'active'`.
35
+ */
36
+ fetchActiveByWebhookId(input: {
37
+ readonly tenantId: TenantId;
38
+ readonly webhookId: string;
39
+ }): Promise<WebhookTriggerRecord | null>;
40
+ }
41
+
42
+ // ---------- register (discriminated on kind) ----------
43
+
44
+ export type RegisterTriggerInput =
45
+ | RegisterCronTriggerInput
46
+ | RegisterEventTriggerInput
47
+ | RegisterWebhookTriggerInput;
48
+
49
+ export interface RegisterCronTriggerInput {
50
+ readonly kind: 'cron';
51
+ readonly tenantId: TenantId;
52
+ readonly flowId: string;
53
+ readonly flowVersion: string;
54
+ readonly config: CronTriggerConfig;
55
+ readonly label?: string;
56
+ }
57
+
58
+ export interface RegisterEventTriggerInput {
59
+ readonly kind: 'event';
60
+ readonly tenantId: TenantId;
61
+ readonly flowId: string;
62
+ readonly flowVersion: string;
63
+ readonly config: EventTriggerConfig;
64
+ readonly label?: string;
65
+ }
66
+
67
+ export interface RegisterWebhookTriggerInput {
68
+ readonly kind: 'webhook';
69
+ readonly tenantId: TenantId;
70
+ readonly flowId: string;
71
+ readonly flowVersion: string;
72
+ readonly config: WebhookTriggerConfig;
73
+ /** Caller-supplied (uuid). The routable id a webhook receiver looks the trigger up by. */
74
+ readonly webhookId: string;
75
+ /**
76
+ * Name of the HMAC secret in the tenant's secrets store. The caller
77
+ * writes the secret first (`POST /v1/secrets`); this row never holds it.
78
+ */
79
+ readonly hmacSecretName: string;
80
+ readonly label?: string;
81
+ }
82
+
83
+ // ---------- update (discriminated on kind) ----------
84
+
85
+ export type UpdateTriggerInput =
86
+ | UpdateCronTriggerInput
87
+ | UpdateEventTriggerInput
88
+ | UpdateWebhookTriggerInput;
89
+
90
+ interface UpdateBase {
91
+ readonly tenantId: TenantId;
92
+ readonly triggerId: TriggerId;
93
+ /** `null` clears the label; omitted leaves it unchanged. */
94
+ readonly label?: string | null;
95
+ /** Pin the trigger to a different flow version. */
96
+ readonly flowVersion?: string;
97
+ }
98
+
99
+ export interface UpdateCronTriggerInput extends UpdateBase {
100
+ readonly kind: 'cron';
101
+ readonly config?: Partial<CronTriggerConfig>;
102
+ }
103
+
104
+ export interface UpdateEventTriggerInput extends UpdateBase {
105
+ readonly kind: 'event';
106
+ readonly config?: Partial<EventTriggerConfig>;
107
+ }
108
+
109
+ export interface UpdateWebhookTriggerInput extends UpdateBase {
110
+ readonly kind: 'webhook';
111
+ readonly config?: Partial<WebhookTriggerConfig>;
112
+ // HMAC secret rotation flows through /v1/secrets — not here.
113
+ }
114
+
115
+ // ---------- records (discriminated on kind) ----------
116
+
117
+ interface TriggerRecordBase {
118
+ readonly triggerId: TriggerId;
119
+ readonly tenantId: TenantId;
120
+ readonly flowId: string;
121
+ readonly flowVersion: string;
122
+ readonly status: 'active' | 'paused';
123
+ readonly label: string | null;
124
+ readonly lastFiredAt: string | null;
125
+ readonly createdAt: string;
126
+ readonly updatedAt: string;
127
+ }
128
+
129
+ export interface CronTriggerRecord extends TriggerRecordBase {
130
+ readonly kind: 'cron';
131
+ readonly config: CronTriggerConfig;
132
+ readonly nextFireAt: string | null;
133
+ }
134
+
135
+ export interface EventTriggerRecord extends TriggerRecordBase {
136
+ readonly kind: 'event';
137
+ readonly config: EventTriggerConfig;
138
+ }
139
+
140
+ export interface WebhookTriggerRecord extends TriggerRecordBase {
141
+ readonly kind: 'webhook';
142
+ readonly config: WebhookTriggerConfig;
143
+ readonly webhookId: string;
144
+ readonly hmacSecretName: string;
145
+ }
146
+
147
+ export type TriggerRecord = CronTriggerRecord | EventTriggerRecord | WebhookTriggerRecord;
148
+
149
+ // ---------- list + get + lifecycle ----------
150
+
151
+ export interface ListTriggersInput {
152
+ readonly tenantId: TenantId;
153
+ readonly kind?: TriggerKind;
154
+ readonly status?: 'active' | 'paused';
155
+ readonly limit?: number;
156
+ readonly cursor?: Cursor;
157
+ }
158
+
159
+ export interface TriggerListPage {
160
+ readonly data: readonly TriggerRecord[];
161
+ readonly nextCursor?: Cursor;
162
+ }
163
+
164
+ export interface GetTriggerInput {
165
+ readonly tenantId: TenantId;
166
+ readonly triggerId: TriggerId;
167
+ }
168
+
169
+ export interface TriggerLifecycleInput {
170
+ readonly tenantId: TenantId;
171
+ readonly triggerId: TriggerId;
172
+ }
173
+
174
+ // ---------- errors ----------
175
+
176
+ export interface RegisterTriggerError {
177
+ readonly code:
178
+ | 'trigger-invalid-config'
179
+ | 'trigger-webhook-id-conflict'
180
+ | 'trigger-register-failed';
181
+ readonly message: string;
182
+ }
183
+
184
+ export interface UpdateTriggerError {
185
+ readonly code: 'trigger-not-found' | 'trigger-invalid-config' | 'trigger-update-failed';
186
+ readonly message: string;
187
+ readonly triggerId: TriggerId;
188
+ }
189
+
190
+ export interface TriggerLifecycleError {
191
+ readonly code: 'trigger-not-found' | 'trigger-already-in-state' | 'trigger-lifecycle-failed';
192
+ readonly message: string;
193
+ readonly triggerId: TriggerId;
194
+ }
195
+
196
+ export const TRIGGER_KINDS = ['cron', 'event', 'webhook'] as const;
197
+ export type TriggerKind = (typeof TRIGGER_KINDS)[number];
@@ -0,0 +1,112 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (C) 2026 Kindgi Inc.
3
+
4
+ import type { EventId, Result, RunId, TenantId, TriggerId } from '@kindgi/types';
5
+
6
+ /**
7
+ * What the trigger runtime hands to a caller-supplied `RunFlowBinding`.
8
+ *
9
+ * The kernel doesn't own a flow registry — resolving `flowId + flowVersion`
10
+ * to a concrete `Flow + HandlerRegistry` is deployment-specific (packs
11
+ * register at boot, dev consumers pass in inline maps, etc.). The binding
12
+ * shape lets any of those wire in without the kernel choosing for them.
13
+ */
14
+ export interface TriggerFireContext {
15
+ readonly tenantId: TenantId;
16
+ readonly flowId: string;
17
+ readonly flowVersion: string;
18
+ /**
19
+ * Input to hand to `runGraph`. Shape is trigger-kind-specific:
20
+ * - cron: `config.input ?? {}` (the caller pre-shapes at register time).
21
+ * - event: the entire `Event` object — handler decides which fields
22
+ * it cares about.
23
+ * - webhook: parsed request body — the receiver route decides whether
24
+ * to JSON.parse or hand raw bytes to the binding.
25
+ */
26
+ readonly input: unknown;
27
+ readonly source: TriggerSource;
28
+ }
29
+
30
+ /** Discriminated origin of the run spawn, useful for provenance / audit. */
31
+ export type TriggerSource =
32
+ | { readonly kind: 'cron'; readonly triggerId: TriggerId }
33
+ | {
34
+ readonly kind: 'event';
35
+ readonly triggerId: TriggerId;
36
+ readonly eventId: EventId;
37
+ readonly eventKind: string;
38
+ }
39
+ | { readonly kind: 'webhook'; readonly triggerId: TriggerId; readonly webhookId: string };
40
+
41
+ /**
42
+ * The caller-supplied hook that turns a trigger fire into a real run. The
43
+ * kernel calls this for every due tick, every matching event, every valid
44
+ * webhook receive. Deployments wire it to their own flow+handler lookup
45
+ * (packs manifest, in-process registry, etc.) and then call `runGraph`.
46
+ */
47
+ export type RunFlowBinding = (
48
+ ctx: TriggerFireContext,
49
+ ) => Promise<Result<{ readonly runId: RunId }, TriggerBindingError>>;
50
+
51
+ /** Errors the caller-supplied binding is allowed to surface. */
52
+ export interface TriggerBindingError {
53
+ readonly code: 'flow-not-found' | 'handler-missing' | 'run-spawn-failed';
54
+ readonly message: string;
55
+ readonly cause?: unknown;
56
+ }
57
+
58
+ /**
59
+ * Union of every error the trigger runtime primitives can return. The
60
+ * kind-specific shapes are declared below.
61
+ */
62
+ export type TriggerError = WebhookTriggerError | CronTriggerError | EventTriggerError;
63
+
64
+ export interface WebhookTriggerError {
65
+ readonly code:
66
+ | 'webhook-not-found'
67
+ | 'webhook-signature-invalid'
68
+ | 'webhook-inactive'
69
+ | 'webhook-flow-not-found';
70
+ readonly message: string;
71
+ readonly webhookId: string;
72
+ }
73
+
74
+ export interface CronTriggerError {
75
+ readonly code: 'cron-config-invalid' | 'cron-flow-not-found';
76
+ readonly message: string;
77
+ readonly triggerId: TriggerId;
78
+ }
79
+
80
+ export interface EventTriggerError {
81
+ readonly code: 'event-config-invalid' | 'event-flow-not-found';
82
+ readonly message: string;
83
+ readonly triggerId: TriggerId;
84
+ }
85
+
86
+ /** Trigger-kind-specific config shapes, stored as each trigger's `config`. */
87
+ export interface CronTriggerConfig {
88
+ /** 5- or 6-field cron expression (croner-compatible). 6-field enables second precision. */
89
+ readonly cronExpression: string;
90
+ /** IANA timezone (e.g. 'UTC', 'America/New_York'). Defaults to 'UTC'. */
91
+ readonly timezone?: string;
92
+ /** Static input handed to the flow on every fire. Absent → `{}`. */
93
+ readonly input?: unknown;
94
+ }
95
+
96
+ export interface EventTriggerConfig {
97
+ /** Event type filter (matched against `event.type`). */
98
+ readonly eventKind: string;
99
+ /**
100
+ * Optional input override. When absent, the entire `Event` object is
101
+ * passed as the flow input.
102
+ */
103
+ readonly input?: unknown;
104
+ }
105
+
106
+ export interface WebhookTriggerConfig {
107
+ /**
108
+ * Optional input override. When absent, the parsed request body (as
109
+ * passed to `fireByWebhookId`) is used as the flow input.
110
+ */
111
+ readonly input?: unknown;
112
+ }