@frontera-sdk/functions 1.49.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/types.ts ADDED
@@ -0,0 +1,600 @@
1
+ // `import type`, so this is erased at compile time and adds no runtime import —
2
+ // but `@frontera-sdk/blueprint` is still a real `dependencies` entry, because
3
+ // `WhereNode` is part of this package's PUBLIC type surface: anyone consuming
4
+ // `AutomationContext` needs it to resolve. See the packaging note in
5
+ // docs/superpowers/specs/2026-07-29-automations-ctx-blueprint-query-design.md —
6
+ // a subset install that cannot resolve it aborts `bun install` outright.
7
+ import type { WhereNode } from '@frontera-sdk/blueprint/types'
8
+
9
+ /**
10
+ * Cron, manual, and agent for now; event and webhook land with Event Triggers.
11
+ *
12
+ * The `?: never` members are load-bearing. Without them `{ cron, manual }`
13
+ * typechecks — TypeScript's excess-property check admits any key present on
14
+ * *some* member of a union — and the runner would have to decide at runtime
15
+ * what a both-shaped trigger means.
16
+ *
17
+ * `agent` is deliberately NOT part of that exclusion. Cron and manual answer
18
+ * "what fires this on its own"; `agent: true` answers "may a bound agent call
19
+ * this", which is an orthogonal question — a nightly reconciliation that an
20
+ * analyst can also ask an agent to run on demand is one automation, not two.
21
+ * So `agent` rides alongside either, and the third arm exists for the
22
+ * agent-only automation, which has no self-starting trigger at all.
23
+ *
24
+ * Declaring it is only the AUTHOR's half of the permission. A workspace
25
+ * operator must still bind the automation to one named agent before any tool
26
+ * is projected; see the design in
27
+ * docs/superpowers/specs/2026-08-27-agent-callable-automations-design.md.
28
+ */
29
+ export type AutomationTrigger =
30
+ | { cron: string; manual?: never; agent?: true }
31
+ | { cron?: never; manual: true; agent?: true }
32
+ | { cron?: never; manual?: never; agent: true }
33
+
34
+ /**
35
+ * An author-time affordance, not a validation gate.
36
+ *
37
+ * `blueprint:read` is a literal, so the compiler completes it and offers "Did
38
+ * you mean 'blueprint:read'?" on a typo. `agent:${string}:run` admits any slug
39
+ * — including one that names no agent — so the union cannot be read as proof
40
+ * that a grant is well-formed. The runtime gate is `validateManifest` in
41
+ * `manifest.ts`; this exists to guide the author as they type.
42
+ *
43
+ * Widening it later (adding `notify:*`, `governed:*` with Governed Writes) is a
44
+ * non-breaking change. Narrowing `string` to a union later would break every
45
+ * automation already written, so it starts narrow.
46
+ */
47
+ export type Grant =
48
+ | 'blueprint:read'
49
+ | `agent:${string}:run`
50
+ /**
51
+ * One capability of one Plugin install: `plugin:<install>:<capability>`.
52
+ *
53
+ * `<install>` is the install's name as `frontera plugin list` shows it
54
+ * (lowercase, no spaces); `<capability>` is the capability's name on that
55
+ * install. One grant per capability — there is no wildcard, for the same
56
+ * reason `http:` has none: the manifest is the reviewable list of what the
57
+ * automation can reach.
58
+ */
59
+ | `plugin:${string}:${string}`
60
+ /** One EXACT host, no wildcards. `http:api.stripe.com` matches that host and
61
+ * nothing else — a wildcard would ask a reviewer to reason about
62
+ * subdomain-takeover risk, and the answer is usually wrong. */
63
+ | `http:${string}`
64
+ /** The NAME of a workspace secret. Its VALUE never enters this process: you
65
+ * name it, the platform injects it server-side. */
66
+ | `secret:${string}`
67
+ /** One EXACT published Action apiName. `governed:approveInvoice` permits
68
+ * submitting that Action and nothing else.
69
+ *
70
+ * Not wildcardable, for the same reason `http:` is not: a reviewer reading
71
+ * `governed:*` would have to know the whole current Action catalog — and the
72
+ * answer changes with every release — to know what the automation may do. */
73
+ | `governed:${string}`
74
+
75
+ /** One declared run input. A deliberate subset of JSON Schema — the same
76
+ * philosophy as the grant grammar: small enough that a wrong shape is
77
+ * refusable with a sentence, wide enough for real parameters. */
78
+ export interface InputFieldSpec {
79
+ type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'file'
80
+ /** Refused at run start when absent. Mutually exclusive with `default`. */
81
+ required?: boolean
82
+ /** Applied at run start when the field is absent. Cron runs rely on these.
83
+ * Not permitted on a `file` field — a file has no meaningful literal default. */
84
+ default?: unknown
85
+ description?: string
86
+ /** Allowed values — string and number types only. */
87
+ enum?: readonly (string | number)[]
88
+ /** `file` type only. Allowed MIME patterns, e.g. `['image/*', 'application/pdf']`.
89
+ * A declaration aid: the value on the run row is only a reference, so this is
90
+ * enforced server-side at upload and at run start, never against the value here. */
91
+ accept?: readonly string[]
92
+ /** `file` type only. Maximum upload size in bytes, enforced server-side. */
93
+ maxBytes?: number
94
+ /**
95
+ * Mask this field's VALUE wherever a person reads the run.
96
+ *
97
+ * What it changes: the run list, the run page and `frontera function runs`
98
+ * show a placeholder instead of the value. What it does NOT change: the
99
+ * handler, every resumption and every retried attempt receive the real value,
100
+ * because the run row still holds it — this is a display control, not
101
+ * storage encryption and not an access control.
102
+ *
103
+ * What it CANNOT cover, stated here so the flag never reads as a promise it
104
+ * does not keep:
105
+ *
106
+ * - `ctx.log('…', { key: ctx.input.token })` — an author writing a value
107
+ * into a step detail publishes it, and nothing here can intercept that.
108
+ * - a redacted value the author TRANSFORMS before using it. The agent
109
+ * transcript on the run page is masked by exact occurrence, so a value
110
+ * interpolated into a prompt — or quoted back in the reply — is replaced.
111
+ * A value upper-cased, truncated or reformatted first no longer matches
112
+ * and is not found. Exact match is what can be done without guessing at
113
+ * substrings; the alternative, withholding transcripts entirely for any
114
+ * run with a redacted input, would take the review surface away from
115
+ * exactly the runs that most need reviewing.
116
+ * - `default` and `enum`, which are published in the version manifest and in
117
+ * any tool schema built from it. Declaring either alongside `redact` is
118
+ * refused at deploy for exactly that reason.
119
+ * - the run's own `result` and error message. A handler that returns the
120
+ * value — `return { note: ctx.input.customer_note }` — or throws an error
121
+ * quoting it publishes it on the same run page, unmasked, next to the
122
+ * masked input it came from. Only the INPUT is masked; what the handler
123
+ * chooses to emit is the handler's decision.
124
+ * - anything already recorded. Versions are append-only and the mask is
125
+ * frozen onto each run when it starts, so adding `redact` masks future
126
+ * runs and never rewrites history.
127
+ *
128
+ * A credential still belongs in a `secret:` grant, whose value never enters
129
+ * this process at all. `redact` is for the ordinary personal or commercial
130
+ * detail a run legitimately takes and a bystander has no reason to read.
131
+ */
132
+ redact?: boolean
133
+ }
134
+
135
+
136
+ /**
137
+ * The value of a `file`-typed input.
138
+ *
139
+ * A REFERENCE to an already-uploaded file, never its bytes: `fileId` is the
140
+ * canonical handle from the platform's unified file registry (see
141
+ * docs/superpowers/plans/2026-08-27-unified-file-layer.md). Bytes live in
142
+ * storage; only this small id rides on the run row, so the 64KB input cap is
143
+ * untouched.
144
+ *
145
+ * One reference, two entry points: the run-form uploader gets a `fileId` for a
146
+ * file a person drops in, and an agent passes the `fileId` of a chat attachment
147
+ * it is already holding — both resolve identically downstream. The client never
148
+ * supplies a trusted path or URL; the SERVER authorizes the `fileId` against the
149
+ * caller's workspace (`resolveFile`) and resolves it to bytes/URL when read.
150
+ */
151
+ export interface FileRef {
152
+ fileId: string
153
+ }
154
+
155
+ export type InputsSchema = Record<string, InputFieldSpec>
156
+
157
+ export interface AutomationManifest {
158
+ name: string
159
+ trigger: AutomationTrigger
160
+ grants?: readonly Grant[]
161
+ /**
162
+ * Declared run inputs, validated and defaulted at run start. Absent means
163
+ * this automation takes no input — starting a run WITH input for such a
164
+ * version is refused. See `InputFieldSpec`.
165
+ */
166
+ inputs?: InputsSchema
167
+ concurrency?: number
168
+ /**
169
+ * Times the platform may retry a run that FAILED. Default 0, and the opt-in
170
+ * is the contract.
171
+ *
172
+ * Setting this asserts your handler is safe to run twice. With `ctx.http` that
173
+ * is a real claim rather than a formality — a retried run that charged a card
174
+ * charges it again, and the platform cannot check idempotency on your behalf.
175
+ * Per-automation, not global, because you are the only one who knows.
176
+ *
177
+ * Retries do NOT extend the ctx call budget: each attempt is a separate run
178
+ * with its own meter.
179
+ *
180
+ * With steps, this is a bound on RUN attempts, and a step that fails is what
181
+ * consumes one. Completed steps are not re-executed on the next attempt —
182
+ * they return their stored results — so a retry resumes from the failure
183
+ * rather than starting the work again. That is the point of putting a call
184
+ * that costs something inside a step: `retries: 2` on a handler whose work is
185
+ * all in steps re-runs only the step that failed, while the same setting on a
186
+ * handler with no steps re-runs everything.
187
+ */
188
+ retries?: number
189
+ description?: string
190
+ }
191
+
192
+ /**
193
+ * What `automation()` guarantees once defaults are applied — nothing optional
194
+ * left for a consumer to re-handle. Downstream code takes this, not
195
+ * `AutomationManifest`, so it never re-derives a fact already established.
196
+ */
197
+ export interface ResolvedAutomationManifest extends AutomationManifest {
198
+ // Every member is readonly, not just the two with defaults. `Object.freeze`
199
+ // in `define.ts` freezes the whole object at runtime, so leaving `name` or
200
+ // `description` mutable in the type means `d.manifest.name = 'x'` compiles
201
+ // and then throws — the same compile-clean/throw-at-runtime gap that the
202
+ // removed `as string[]` cast used to create.
203
+ readonly name: string
204
+ readonly trigger: Readonly<AutomationTrigger>
205
+ readonly grants: readonly Grant[]
206
+ readonly inputs?: Readonly<InputsSchema>
207
+ readonly concurrency: number
208
+ readonly retries: number
209
+ readonly description?: string
210
+ }
211
+
212
+ export interface AgentHandle {
213
+ /**
214
+ * One headless agent turn. `options.files` hands the agent already-uploaded
215
+ * files by canonical id — the same `{ fileId }` a `file`-typed run input
216
+ * carries, so an input forwards directly: `run(p, { files: [ctx.input.doc] })`.
217
+ * Each id is authorized against this run's workspace and the bytes are staged
218
+ * onto the agent's computer; the agent is told the staged paths.
219
+ */
220
+ run(prompt: string, options?: { files?: readonly FileRef[] }): Promise<{ text: string }>
221
+ }
222
+
223
+ /**
224
+ * What `ctx.file(ref)` resolves to: a short-lived signed URL plus the
225
+ * authoritative mime/size resolved at upload. `null` only in a dry dev run.
226
+ */
227
+ export interface ResolvedFileHandle {
228
+ fileId: string
229
+ signedUrl: string
230
+ mimeType: string
231
+ sizeBytes: number
232
+ name: string | null
233
+ }
234
+
235
+ /** What `ctx.plugin(install).call(...)` resolves to. */
236
+ export interface PluginCallResult<T = unknown> {
237
+ /** Whatever the capability returned. Shape is the plugin's, not the platform's. */
238
+ data: T
239
+ }
240
+
241
+ export interface PluginHandle {
242
+ /**
243
+ * Invoke one capability of this install.
244
+ *
245
+ * Governed by the install's policy exactly as an agent's tool call is —
246
+ * a disabled install, a `read_only` Action policy, a parameter constraint
247
+ * or a missing workspace account all refuse here with the reason named.
248
+ * A capability that requires approval cannot be called from an automation
249
+ * at all (nobody to ask), and `deploy` refuses the grant up front.
250
+ *
251
+ * A failure reported by the plugin itself is thrown, carrying the plugin's
252
+ * message. A success resolves to `{ data }` — there is no `ok` flag to
253
+ * branch on, only the value.
254
+ *
255
+ * Dry in a dev run: returns `{ data: null }` and sends nothing.
256
+ */
257
+ call<T = unknown>(
258
+ capability: string,
259
+ input?: Record<string, unknown>,
260
+ ): Promise<PluginCallResult<T>>
261
+ }
262
+
263
+ /**
264
+ * Durable steps.
265
+ *
266
+ * A step is the unit the platform can memoize, retry and draw. Work inside one
267
+ * runs at most once per run; work outside one runs again every time the
268
+ * platform resumes the handler, which it does after every step completes.
269
+ *
270
+ * That resumption is the whole model and it is what the three rules below are
271
+ * about — none of them is a style preference.
272
+ */
273
+ export interface StepApi {
274
+ /**
275
+ * Run `fn` as a durable step and return its result.
276
+ *
277
+ * Three rules, all enforced or observable rather than advisory:
278
+ *
279
+ * 1. **`name` must be unique within a run.** The platform memoizes by it, so a
280
+ * repeated name would silently hand back the FIRST call's result. Inside a
281
+ * loop, put the index in the name — `` `submit:${i}` ``. A repeat fails the
282
+ * run naming the collision rather than returning the wrong value.
283
+ * 2. **The result must be JSON-serializable.** It is stored and replayed, so a
284
+ * `Date` comes back as a string and a class instance comes back as a plain
285
+ * object. Return data, not objects with behaviour. It is also recorded on
286
+ * the step's row — capped, and replaced by its size when it is too large —
287
+ * so the run trace can show what the step produced. Never return a secret
288
+ * from a step: details are rendered verbatim in the Console.
289
+ * 3. **Code outside a step re-executes.** After each step the handler restarts
290
+ * from the top with completed steps returning their stored results. A
291
+ * `ctx.http` call sitting outside a step therefore fires once per step, and
292
+ * spends its call budget every time. The Console flags such calls on a run
293
+ * that used steps.
294
+ */
295
+ run<T>(name: string, fn: () => Promise<T>): Promise<T>
296
+
297
+ /**
298
+ * Park the run for `ms` milliseconds, durably, under a unique name.
299
+ *
300
+ * On the platform this is a real checkpoint: the run stops occupying a
301
+ * worker and resumes after the delay — pace provider polls with it (a
302
+ * measured 429 arrived after ~7 back-to-back polls). In `createTestContext`
303
+ * and in dev runs it records and returns immediately, so tests and dry runs
304
+ * never actually wait. Shares the name-uniqueness rule with `run`: the
305
+ * platform memoizes both by name.
306
+ */
307
+ sleep(name: string, ms: number): Promise<void>
308
+ }
309
+
310
+ /**
311
+ * The 13 lifecycle states a governed Action Request can hold.
312
+ *
313
+ * A deliberate copy of a WIRE contract, not shared code — same reasoning as
314
+ * `RegistryEntry` in the runner: this package must install from public npm with
315
+ * a three-package dependency list, and importing the service's own enum would
316
+ * drag drizzle and the schema into an author's `bun install`. The service's
317
+ * `ACTION_REQUEST_LIFECYCLE_STATES` is the source of truth; the response proves
318
+ * the two agree.
319
+ */
320
+ export type ActionRequestLifecycle =
321
+ | 'received'
322
+ | 'awaiting_approval'
323
+ | 'ready'
324
+ | 'executing'
325
+ | 'finalizing'
326
+ | 'succeeded'
327
+ | 'rejected'
328
+ | 'expired'
329
+ | 'cancelled'
330
+ | 'failed'
331
+ | 'outcome_unknown'
332
+ | 'awaiting_resolution'
333
+ | 'closed_unknown'
334
+
335
+ /**
336
+ * `subjectRef` and `expectedSubjectVersion` are paired deliberately.
337
+ *
338
+ * The plane requires BOTH for an Action over an existing subject and refuses
339
+ * BOTH for a create Action, so independently-optional fields would let an
340
+ * author write a submission that cannot be accepted and only find out at
341
+ * runtime. Which arm applies is the Action's decision, not the caller's — read
342
+ * it off the Action's `subject.mode`.
343
+ */
344
+ export type ActionSubmission = {
345
+ /** Published Action apiName. Requires a `governed:<apiName>` grant. */
346
+ action: string
347
+ input: Record<string, unknown>
348
+ /** Required when the Action's definition says so. */
349
+ reason?: string
350
+ /**
351
+ * Tells two submissions from the SAME step apart.
352
+ *
353
+ * A step submits once by default. The idempotency key is derived from the run
354
+ * and the step alone, so a submission re-reached by a resumption or by a
355
+ * retried attempt is the SAME key and the plane hands back the original
356
+ * request instead of making a second one. Your own retry loop behaves the
357
+ * same way: a submission that FAILED is not recorded, so submitting again
358
+ * after catching a transport error re-sends and the plane replays.
359
+ *
360
+ * What you cannot do by default is submit twice on purpose. Two submissions
361
+ * the platform cannot tell apart derive one key AND one semantic
362
+ * fingerprint, so the plane would replay the first and answer both calls with
363
+ * the same id — no error, one effect, a green run. Rather than let that
364
+ * happen, the second call is refused before it leaves your process, naming
365
+ * this field.
366
+ *
367
+ * Pass a distinct `submissionKey` per submission to say you meant it — a
368
+ * business identity is the right value, not a counter:
369
+ *
370
+ * ```ts
371
+ * await ctx.step.run('flag', async () => {
372
+ * for (const row of rows) {
373
+ * await ctx.action.submit({
374
+ * action: 'flagForAudit',
375
+ * // Stable for THIS row across every attempt. An array index is not:
376
+ * // if the re-read returns the rows in another order, an index would
377
+ * // bind row B's submission to row A's key.
378
+ * submissionKey: row.id,
379
+ * input: { rowId: row.id },
380
+ * })
381
+ * }
382
+ * })
383
+ * ```
384
+ *
385
+ * It must be stable across attempts for the same intended submission, which
386
+ * is why the platform cannot derive it for you — only your code knows which
387
+ * of two submissions is "the same one again". An empty string is refused;
388
+ * omit it entirely to mean "this step submits once".
389
+ */
390
+ submissionKey?: string
391
+ } & (
392
+ | {
393
+ subjectRef: { objectTypeId: string; objectId: string }
394
+ /** The version you believe the subject is at: a submission built from a
395
+ * stale read must lose rather than overwrite. */
396
+ expectedSubjectVersion: string
397
+ }
398
+ | { subjectRef?: never; expectedSubjectVersion?: never }
399
+ )
400
+
401
+ export interface ActionSubmitResult {
402
+ requestId: string
403
+ /**
404
+ * Where the request stopped, NOT whether the effect happened.
405
+ *
406
+ * `ready` means accepted and queued for dispatch. `awaiting_approval` means
407
+ * the Action requires a human and one has not decided yet — a normal return,
408
+ * not an error. Neither is a completed business fact.
409
+ */
410
+ lifecycle: ActionRequestLifecycle
411
+ }
412
+
413
+ /**
414
+ * `notify` still arrives with a later slice; `governed` is here.
415
+ */
416
+ export interface AutomationContext {
417
+ runId: string
418
+ workspaceId: string
419
+ /**
420
+ * The values this run was started with — validated against the manifest's
421
+ * `inputs` schema and fixed on the run row at start, so every resumption
422
+ * and retried attempt sees the same object. `{}` when the manifest declares
423
+ * no inputs. Visible in the run trace by design: never put a secret here —
424
+ * `secret:` grants are the credential path.
425
+ */
426
+ input: Record<string, unknown>
427
+ /** Never rejects — telemetry must not be able to fail a run. */
428
+ log(message: string, data?: Record<string, unknown>): Promise<void>
429
+ agent(slug: string): AgentHandle
430
+ /**
431
+ * Resolve one of THIS run's `file` inputs to a readable form (signed URL +
432
+ * authoritative mime/size). No grant — it only reads files the run was given;
433
+ * any other fileId is refused. `null` in a dry dev run.
434
+ */
435
+ file(ref: FileRef): Promise<ResolvedFileHandle | null>
436
+ /** One Plugin install, by the name `frontera plugin list` shows. Needs `plugin:<install>:<capability>` per call. */
437
+ plugin(install: string): PluginHandle
438
+ http: {
439
+ /**
440
+ * Call an allowlisted host, optionally with a workspace secret injected
441
+ * server-side.
442
+ *
443
+ * Requires an `http:<host>` grant, and an `secret:<name>` grant when `auth`
444
+ * is used. The secret's VALUE never enters this process — that is deliberate:
445
+ * a credential this process never held cannot be leaked by a stray
446
+ * `ctx.log`, an exception serialiser, or a dependency, and step details are
447
+ * rendered verbatim in the Console.
448
+ *
449
+ * An upstream 4xx/5xx comes back as `status`, not as a throw. An API
450
+ * answering 404 is data; only failures of the mechanism reject.
451
+ */
452
+ fetch(req: HttpRequest): Promise<HttpResponse>
453
+ }
454
+ blueprint: {
455
+ query<T = Record<string, unknown>>(
456
+ objectType: string,
457
+ options?: BlueprintQueryOptions,
458
+ ): Promise<BlueprintQueryResult<T>>
459
+ }
460
+ /**
461
+ * Durable steps. See `StepApi`.
462
+ *
463
+ * Present on every automation — a handler that uses no steps behaves exactly
464
+ * as it did before this existed, because a run with no steps is never
465
+ * resumed.
466
+ */
467
+ step: StepApi
468
+ action: {
469
+ /**
470
+ * Ask the governed write plane to perform one named business change.
471
+ *
472
+ * This is the ONLY way an automation changes a system of record. Your code
473
+ * never holds a write handle: you describe the change, and the plane
474
+ * authorizes it, approves it if the Action says so, dispatches it, confirms
475
+ * it and records it. An Action that declares `approval: required` cannot be
476
+ * talked out of it by the caller.
477
+ *
478
+ * Two rules:
479
+ *
480
+ * 1. **It must be called inside `ctx.step.run`.** Code outside a step
481
+ * re-executes after every step boundary, so a submit sitting there would
482
+ * fire once per boundary. Inside a step it runs once, and the step —
483
+ * identified by the row the service itself issued — is what makes the
484
+ * idempotency key stable across resumption and across a retried run.
485
+ *
486
+ * The service checks this rather than taking your word for it: the
487
+ * submission carries a step row id, and a submission whose id names no
488
+ * open step of this run is refused. What that check cannot do is make a
489
+ * determined bundle behave — your code runs unsandboxed in the same
490
+ * process as the run token, so it could open a step row purely to submit
491
+ * inside it. The bound is that such a step is a real row and shows up in
492
+ * the run trace, not that it is impossible.
493
+ * 2. **It never waits.** It returns as soon as the request is durably
494
+ * accepted. A run has nobody to ask for an approval and ten minutes to
495
+ * live, so blocking on a human is not something this can offer —
496
+ * `awaiting_approval` is a normal return value.
497
+ *
498
+ * The returned `lifecycle` is where the request stopped, not proof of
499
+ * effect. Poll the ledger, or let the Action's Business Event tell you.
500
+ */
501
+ submit(request: ActionSubmission): Promise<ActionSubmitResult>
502
+ }
503
+ }
504
+
505
+ export interface HttpRequest {
506
+ url: string
507
+ method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
508
+ headers?: Record<string, string>
509
+ /** String only — no streaming, no binary. */
510
+ body?: string
511
+ /** Inject a workspace secret into one header. Needs a `secret:<name>` grant. */
512
+ auth?: { header: string; secret: string; prefix?: string }
513
+ }
514
+
515
+ export interface HttpResponse {
516
+ status: number
517
+ headers: Record<string, string>
518
+ /** Capped at 1 MB. Exceeding the cap is an error, never a truncation — a
519
+ * silently shortened response is a wrong answer that looks right. */
520
+ body: string
521
+ }
522
+
523
+ export interface BlueprintQueryOptions {
524
+ /** The real Blueprint filter DSL, not a shorthand — `and`/`or`/`not`, ranges
525
+ * and date presets all work. A convenience subset with no escape hatch is the
526
+ * thing the first "overdue OR flagged" automation would have to work around. */
527
+ where?: WhereNode
528
+ select?: string[]
529
+ /** `dir`, not `direction` — matches `QueryRequest` exactly. */
530
+ orderBy?: Array<{ property: string; dir: 'asc' | 'desc' }>
531
+ /** Default 100, clamped to 1000. Exceeding the cap sets `hasMore`; it never
532
+ * truncates silently. */
533
+ limit?: number
534
+ /** Opaque. Pass back the previous result's `nextPageToken`; absent means the
535
+ * first page. Cursor-based, so a scan stays correct while the table moves
536
+ * underneath it — which a cron-driven automation's table always does. */
537
+ pageToken?: string
538
+ }
539
+
540
+ export interface BlueprintQueryResult<T = Record<string, unknown>> {
541
+ rows: T[]
542
+ /** True when the query matched more rows than were returned. */
543
+ hasMore: boolean
544
+ /** Present iff `hasMore`. Feed to the next call's `pageToken`.
545
+ *
546
+ * Forwarded rather than narrowed away on purpose: `hasMore` on its own is a
547
+ * fact the author can do nothing about, which is how a digest over the first
548
+ * 500 of 5,000 rows reports success. */
549
+ nextPageToken?: string
550
+ }
551
+
552
+ export type AutomationHandler = (ctx: AutomationContext) => Promise<unknown>
553
+
554
+ export interface AutomationDescriptor {
555
+ readonly manifest: ResolvedAutomationManifest
556
+ readonly handler: AutomationHandler
557
+ }
558
+
559
+ /**
560
+ * How a run came to exist, named once so the two sides of the invoke event
561
+ * cannot drift.
562
+ *
563
+ * This is not decoration. The service publishes `source` on the invoke event,
564
+ * the RUNNER resolves it back and posts it to the open-run call, and the
565
+ * service then refuses an invocation ticket arriving under a source that does
566
+ * not expect one. So a source the service knows and the runner does not is not
567
+ * a mis-filed run — it is no run at all: the ticket rides along, the open-run
568
+ * call 400s, and the caller waits forever on a request that never became
569
+ * anything. That is precisely how the App lane shipped broken.
570
+ *
571
+ * Both packages depend on this one, so the list lives here rather than being
572
+ * spelled out in each. Adding a source means adding it here, and the two
573
+ * consumers pick it up by construction.
574
+ *
575
+ * `cron` is deliberately absent: it is what the runner INFERS when the event
576
+ * names no source at all, so it is never carried on an event.
577
+ */
578
+ export const EVENT_TRIGGER_SOURCES = ['manual', 'rehearsal', 'agent', 'app'] as const
579
+ export type EventTriggerSource = (typeof EVENT_TRIGGER_SOURCES)[number]
580
+
581
+ /**
582
+ * The sources whose runs MUST arrive with an invocation ticket.
583
+ *
584
+ * A run under one of these has a caller whose identity exists only on the
585
+ * ticket, so a missing one is refused rather than opened unattributed. A ticket
586
+ * under any other source is refused too — it means the event was tampered with
587
+ * or two payloads got mixed.
588
+ */
589
+ export const TICKETED_TRIGGER_SOURCES = ['agent', 'app'] as const
590
+ export type TicketedTriggerSource = (typeof TICKETED_TRIGGER_SOURCES)[number]
591
+
592
+ export function isEventTriggerSource(value: unknown): value is EventTriggerSource {
593
+ return typeof value === 'string'
594
+ && (EVENT_TRIGGER_SOURCES as readonly string[]).includes(value)
595
+ }
596
+
597
+ export function isTicketedTriggerSource(value: unknown): value is TicketedTriggerSource {
598
+ return typeof value === 'string'
599
+ && (TICKETED_TRIGGER_SOURCES as readonly string[]).includes(value)
600
+ }