@pylonsync/functions 0.19.1 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/define.d.ts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/response.d.ts +39 -0
- package/dist/runtime.d.ts +3 -1
- package/dist/ssr-runtime.d.ts +7 -0
- package/dist/types.d.ts +202 -8
- package/dist/workflows.d.ts +19 -2
- package/package.json +1 -1
- package/src/define.ts +2 -1
- package/src/index.ts +7 -0
- package/src/response.test.ts +42 -0
- package/src/response.ts +109 -0
- package/src/runtime.ts +70 -5
- package/src/ssr-runtime.test.ts +25 -0
- package/src/ssr-runtime.ts +35 -3
- package/src/types.ts +211 -9
- package/src/workflows.test.ts +74 -7
- package/src/workflows.ts +81 -19
package/dist/define.d.ts
CHANGED
|
@@ -44,7 +44,8 @@ interface CommonDef<TSchema extends ValidatorSchema | undefined = ValidatorSchem
|
|
|
44
44
|
* IDLE-timeout SECONDS for this function: how long it may go without
|
|
45
45
|
* producing any activity (a stream chunk, a `ctx.db` op, an LLM
|
|
46
46
|
* event) before the host cancels the call. Defaults to
|
|
47
|
-
* `PYLON_FN_CALL_TIMEOUT` (30s
|
|
47
|
+
* `PYLON_FN_CALL_TIMEOUT` (30s; Stack0 Cloud sets 300s). Activity
|
|
48
|
+
* restarts the budget, so a
|
|
48
49
|
* streaming agent run stays alive as long as it keeps producing; a
|
|
49
50
|
* silent hang is cancelled at the budget. Total lifetime is capped at
|
|
50
51
|
* 10× this value however chatty the call is.
|
package/dist/index.d.ts
CHANGED
|
@@ -20,10 +20,11 @@
|
|
|
20
20
|
export { query, mutation, action } from "./define";
|
|
21
21
|
export { v } from "./validators";
|
|
22
22
|
export { workflow } from "./workflows";
|
|
23
|
+
export type { RawResponse, RawResponseInit } from "./response";
|
|
23
24
|
export { agent, isAgentDefinition, validatorToJsonSchema, validatorSchemaToJsonSchema, } from "./agent";
|
|
24
25
|
export type { AgentDefinition, AgentTool, AgentCallArgs, AgentResult, } from "./agent";
|
|
25
26
|
export type { WorkflowDefinition, WorkflowRun, WorkflowRunRequest, WorkflowRunnerResponse, WorkflowStepResult, } from "./workflows";
|
|
26
27
|
export { resetDb, installTestIsolation } from "./testing";
|
|
27
28
|
export { slugifyName, availableSlug } from "./slugify";
|
|
28
29
|
export type { SsrResponse, SsrCookieOptions, SsrMetadata, Sitemap, SitemapEntry, Robots, RobotsRule, } from "./ssr-runtime";
|
|
29
|
-
export type { QueryCtx, MutationCtx, ActionCtx, DbReader, DbWriter, Stream, Scheduler, AuthInfo, AuthMode, AuthRequirement, FnDefinition, Validator, AnyValidator, ValidatorSchema, InferValidator, InferArgs, RequireMember, RequireMemberOptions, Shards, ShardsReader, ShardsWriter, ShardInfo, ShardTransfer, MemberRow, Workflows, VectorSearchQuery, VectorSearchResult, SearchResult, PaginationResult, Llm, LlmMessage, LlmContentBlock, LlmTool, LlmCompleteRequest, LlmCompleteResponse, LlmStreamEvent, Rooms, Domains, TenantDomainResult, TenantDomainDns, DomainAvailability, DomainContact, RegisterDomainOptions, RegisteredDomainResult, } from "./types";
|
|
30
|
+
export type { QueryCtx, MutationCtx, ActionCtx, DbReader, DbWriter, Stream, Scheduler, AuthInfo, AuthMode, AuthRequirement, FnDefinition, Validator, AnyValidator, ValidatorSchema, InferValidator, InferArgs, RequireMember, RequireMemberOptions, Shards, ShardsReader, ShardsWriter, ShardInfo, ShardTransfer, MemberRow, Workflows, WorkflowRunSummary, WorkflowRunStatus, Audit, AuditLog, AuditEntry, VectorSearchQuery, VectorSearchResult, SearchResult, PaginationResult, Llm, LlmMessage, LlmContentBlock, LlmTool, LlmCompleteRequest, LlmCompleteResponse, LlmStreamEvent, Rooms, Domains, TenantDomainResult, TenantDomainRecord, TenantDomainDns, DomainAvailability, DomainContact, RegisterDomainOptions, RegisteredDomainResult, } from "./types";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw HTTP responses from webhook actions.
|
|
3
|
+
*
|
|
4
|
+
* An action called through `/api/webhooks/<name>` normally answers with its
|
|
5
|
+
* return value as JSON and status 200. Returning `ctx.response(...)` sends
|
|
6
|
+
* the given status, content type, headers, and body instead. Every other
|
|
7
|
+
* route (`/api/fn/<name>`, `ctx.runAction`) returns the value as ordinary
|
|
8
|
+
* JSON data.
|
|
9
|
+
*
|
|
10
|
+
* The host enforces the same rules (crates/router/src/raw_response.rs), so
|
|
11
|
+
* a hand-built object cannot bypass them. The two header lists must stay in
|
|
12
|
+
* sync.
|
|
13
|
+
*/
|
|
14
|
+
/** What `ctx.response` accepts. */
|
|
15
|
+
export interface RawResponseInit {
|
|
16
|
+
/** HTTP status, 200-599. Default 200. */
|
|
17
|
+
status?: number;
|
|
18
|
+
/** Shorthand for the `Content-Type` header. Default `text/plain; charset=utf-8`. */
|
|
19
|
+
contentType?: string;
|
|
20
|
+
/** Extra response headers. Values must be visible ASCII. */
|
|
21
|
+
headers?: Record<string, string>;
|
|
22
|
+
/** Response body. Default empty. Must be empty for 204, 205, and 304. */
|
|
23
|
+
body?: string;
|
|
24
|
+
}
|
|
25
|
+
/** The marked value an action returns. Build it with `ctx.response`. */
|
|
26
|
+
export interface RawResponse {
|
|
27
|
+
readonly __pylonResponse: 1;
|
|
28
|
+
readonly status: number;
|
|
29
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
30
|
+
readonly body: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Build a raw HTTP response for a webhook action to return.
|
|
34
|
+
*
|
|
35
|
+
* ```ts
|
|
36
|
+
* return ctx.response({ contentType: "text/xml", body: "<Response/>" });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export declare function response(init?: RawResponseInit): RawResponse;
|
package/dist/runtime.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* each ctx.db / ctx.scheduler / ctx.runMutation call), so the map never
|
|
15
15
|
* needs to queue multiple RPCs per call_id.
|
|
16
16
|
*/
|
|
17
|
-
import type { DbReader, DbWriter, Llm, Rooms, Workflows } from "./types";
|
|
17
|
+
import type { DbReader, DbWriter, Llm, Rooms, Workflows, Audit } from "./types";
|
|
18
18
|
export declare function buildDbReader(callId: string, ssrRead?: boolean): DbReader;
|
|
19
19
|
export declare function buildSsrFnCaller(callId: string): (name: string, args?: Record<string, unknown>) => Promise<unknown>;
|
|
20
20
|
export declare function buildDbWriter(callId: string): DbWriter;
|
|
@@ -46,4 +46,6 @@ export declare function buildRooms(callId: string): Rooms;
|
|
|
46
46
|
* takes it from there, so `start` returns the instance id immediately.
|
|
47
47
|
* Uses the queued call_id-keyed `rpc` (the reply carries no op_id).
|
|
48
48
|
*/
|
|
49
|
+
/** Build `ctx.audit` — round-trips `audit_op` frames to the host. */
|
|
50
|
+
export declare function buildAudit(callId: string): Audit;
|
|
49
51
|
export declare function buildWorkflows(callId: string): Workflows;
|
package/dist/ssr-runtime.d.ts
CHANGED
|
@@ -337,6 +337,13 @@ export declare function moduleExistsIn(fs: any, path: any, cwd: string, dir: str
|
|
|
337
337
|
* in the process.
|
|
338
338
|
*/
|
|
339
339
|
export declare function findBoundaryIn(fs: any, path: any, cwd: string, componentPath: string, fileName: string): string | null;
|
|
340
|
+
/** Loopback host? `host` is a lowercase authority (`host[:port]`,
|
|
341
|
+
* `[ipv6]:port`). True for exactly `localhost`, dotted 127.0.0.0/8
|
|
342
|
+
* without leading zeros, `::1`, or `0.0.0.0`, with an optional numeric
|
|
343
|
+
* port. The match is exact, so
|
|
344
|
+
* `localhost.example.com` is not loopback. Mirrors `is_loopback_host` in
|
|
345
|
+
* crates/router/src/public_url.rs. Exported for tests. */
|
|
346
|
+
export declare function isLoopbackHost(host: string): boolean;
|
|
340
347
|
/** Pure origin resolution (exported for tests).
|
|
341
348
|
*
|
|
342
349
|
* SECURITY: the request `Host` (and `X-Forwarded-Proto`) is attacker-
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type definitions for the function system.
|
|
3
3
|
*/
|
|
4
|
+
import type { RawResponse, RawResponseInit } from "./response";
|
|
4
5
|
/**
|
|
5
6
|
* Declarative auth requirement for a function. The framework
|
|
6
7
|
* enforces this BEFORE the handler runs — if the caller doesn't
|
|
@@ -296,8 +297,23 @@ export interface Scheduler {
|
|
|
296
297
|
runAfter(delayMs: number, fnName: string, args: Record<string, unknown>): Promise<string>;
|
|
297
298
|
/** Schedule a function to run at a specific time (Unix ms). */
|
|
298
299
|
runAt(timestamp: number, fnName: string, args: Record<string, unknown>): Promise<string>;
|
|
299
|
-
/**
|
|
300
|
-
|
|
300
|
+
/**
|
|
301
|
+
* Cancel a scheduled function that has not started. Pass the id
|
|
302
|
+
* `runAfter` or `runAt` returned.
|
|
303
|
+
*
|
|
304
|
+
* Resolves `{ cancelled: true }` when the job will not run, and
|
|
305
|
+
* `{ cancelled: false }` when the id is unknown or the job already
|
|
306
|
+
* started or finished (a running job is not interrupted). Rejects with
|
|
307
|
+
* `SCHEDULE_CANCEL_FAILED` when the cancel could not be saved; the job
|
|
308
|
+
* then still runs.
|
|
309
|
+
*
|
|
310
|
+
* Inside a mutation, the cancel commits or rolls back with the
|
|
311
|
+
* mutation: if the mutation throws, the job stays scheduled. From an
|
|
312
|
+
* action, it takes effect at once.
|
|
313
|
+
*/
|
|
314
|
+
cancel(scheduleId: string): Promise<{
|
|
315
|
+
cancelled: boolean;
|
|
316
|
+
}>;
|
|
301
317
|
}
|
|
302
318
|
/**
|
|
303
319
|
* Transactional email transport.
|
|
@@ -523,19 +539,154 @@ export interface Workflows {
|
|
|
523
539
|
/**
|
|
524
540
|
* Start a workflow instance by name. Returns immediately with the
|
|
525
541
|
* instance id — the engine's background driver executes the steps.
|
|
542
|
+
*
|
|
543
|
+
* With `key` (for example a lead id), at most one non-terminal run
|
|
544
|
+
* exists per workflow name and key. If one exists, no new run starts
|
|
545
|
+
* and its id is returned with `created: false`. A finished run frees
|
|
546
|
+
* the key.
|
|
526
547
|
*/
|
|
527
|
-
start(name: string, input?: unknown
|
|
548
|
+
start(name: string, input?: unknown, opts?: {
|
|
549
|
+
key?: string;
|
|
550
|
+
}): Promise<{
|
|
528
551
|
id: string;
|
|
552
|
+
created: boolean;
|
|
529
553
|
}>;
|
|
530
554
|
/**
|
|
531
|
-
*
|
|
532
|
-
* `wf.waitForEvent(event)
|
|
533
|
-
*
|
|
555
|
+
* Send an event to a run. If the run is waiting for it
|
|
556
|
+
* (`wf.waitForEvent(event)`), the run resumes and `delivered` is true.
|
|
557
|
+
* Otherwise the event is buffered (`buffered: true`) and the run's next
|
|
558
|
+
* `waitForEvent(event)` consumes it. Rejects when the run has finished
|
|
559
|
+
* or already holds 100 buffered events. Events sent without data
|
|
560
|
+
* arrive as `{}`.
|
|
534
561
|
*/
|
|
535
562
|
sendEvent(workflowId: string, event: string, data?: unknown): Promise<{
|
|
536
563
|
delivered: boolean;
|
|
564
|
+
buffered: boolean;
|
|
565
|
+
}>;
|
|
566
|
+
/**
|
|
567
|
+
* Cancel a run. No further step runs. A step already in progress may
|
|
568
|
+
* finish, but its result is discarded. Resolves `cancelled: false` when
|
|
569
|
+
* the run had already finished. Rejects for an unknown id.
|
|
570
|
+
*/
|
|
571
|
+
cancel(workflowId: string, opts?: {
|
|
572
|
+
reason?: string;
|
|
573
|
+
}): Promise<{
|
|
574
|
+
cancelled: boolean;
|
|
575
|
+
}>;
|
|
576
|
+
/** Read one run, or null if the id is unknown. */
|
|
577
|
+
get(workflowId: string): Promise<WorkflowRunSummary | null>;
|
|
578
|
+
/**
|
|
579
|
+
* List runs, newest first. `status: "active"` matches every
|
|
580
|
+
* non-terminal status. `limit` defaults to 100 (max 1000).
|
|
581
|
+
*
|
|
582
|
+
* ```ts
|
|
583
|
+
* // Seller texted STOP: end every cadence for the lead.
|
|
584
|
+
* const runs = await ctx.workflows.list({ key: leadId, status: "active" });
|
|
585
|
+
* for (const run of runs) {
|
|
586
|
+
* await ctx.workflows.cancel(run.id, { reason: "STOP" });
|
|
587
|
+
* }
|
|
588
|
+
* ```
|
|
589
|
+
*/
|
|
590
|
+
list(filter?: {
|
|
591
|
+
name?: string;
|
|
592
|
+
key?: string;
|
|
593
|
+
status?: WorkflowRunStatus | "active";
|
|
594
|
+
limit?: number;
|
|
595
|
+
}): Promise<WorkflowRunSummary[]>;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* The application audit log (`ctx.audit` on mutations and actions).
|
|
599
|
+
* Append-only, stored with the auth audit events.
|
|
600
|
+
*/
|
|
601
|
+
export interface AuditLog {
|
|
602
|
+
/**
|
|
603
|
+
* Record an event. The actor and tenant come from the caller's session.
|
|
604
|
+
* `action` is a short dotted name (`lead.export`, `script.update`) and
|
|
605
|
+
* is stored as `app.<action>`. `meta` values are stored as strings
|
|
606
|
+
* (non-strings as JSON text); keep secrets out of it.
|
|
607
|
+
*
|
|
608
|
+
* Inside a mutation the event is written after the mutation commits and
|
|
609
|
+
* dropped if it rolls back. Rejects with `AUDIT_WRITE_FAILED` when an
|
|
610
|
+
* action's event could not be stored.
|
|
611
|
+
*/
|
|
612
|
+
log(event: {
|
|
613
|
+
action: string;
|
|
614
|
+
entity?: string;
|
|
615
|
+
entityId?: string;
|
|
616
|
+
/** The user the event is about, when not the actor. */
|
|
617
|
+
subject?: string;
|
|
618
|
+
meta?: Record<string, unknown>;
|
|
619
|
+
}): Promise<{
|
|
620
|
+
id: string;
|
|
537
621
|
}>;
|
|
538
622
|
}
|
|
623
|
+
/**
|
|
624
|
+
* `ctx.audit` on actions: {@link AuditLog.log} plus reads. Mutations get
|
|
625
|
+
* only `log`: in a Postgres mutation the transaction holds a connection,
|
|
626
|
+
* and a read would need a second.
|
|
627
|
+
*/
|
|
628
|
+
export interface Audit extends AuditLog {
|
|
629
|
+
/**
|
|
630
|
+
* Read events, newest first. A caller with a tenant reads that tenant's
|
|
631
|
+
* events; a caller without one reads only events it performed. Admin
|
|
632
|
+
* callers may pass `tenant` or read all tenants. `action` matches
|
|
633
|
+
* `lead.export` as `app.lead.export`; framework actions
|
|
634
|
+
* (`entity.update`, `retention.delete`) and auth action names match as
|
|
635
|
+
* given. `limit` defaults to 100 (max 1000). To page,
|
|
636
|
+
* pass `beforeId` set to the last event's `id`. `before` (unix seconds)
|
|
637
|
+
* limits by time.
|
|
638
|
+
*/
|
|
639
|
+
list(filter?: {
|
|
640
|
+
entity?: string;
|
|
641
|
+
entityId?: string;
|
|
642
|
+
actor?: string;
|
|
643
|
+
action?: string;
|
|
644
|
+
tenant?: string;
|
|
645
|
+
before?: number;
|
|
646
|
+
beforeId?: string;
|
|
647
|
+
limit?: number;
|
|
648
|
+
}): Promise<AuditEntry[]>;
|
|
649
|
+
}
|
|
650
|
+
export interface AuditEntry {
|
|
651
|
+
id: string;
|
|
652
|
+
/** Unix seconds. */
|
|
653
|
+
createdAt: number;
|
|
654
|
+
/** `app.<action>`, `entity.insert|update|delete`, `retention.delete`, or an auth action. */
|
|
655
|
+
action: string;
|
|
656
|
+
actor: string | null;
|
|
657
|
+
subject: string | null;
|
|
658
|
+
tenant: string | null;
|
|
659
|
+
entity: string | null;
|
|
660
|
+
entityId: string | null;
|
|
661
|
+
ip: string | null;
|
|
662
|
+
success: boolean;
|
|
663
|
+
reason: string | null;
|
|
664
|
+
/** For `entity.*` events, `fields` lists the changed field names. */
|
|
665
|
+
meta: Record<string, string>;
|
|
666
|
+
}
|
|
667
|
+
export type WorkflowRunStatus = "pending" | "running" | "sleeping" | "waiting" | "completed" | "failed" | "cancelled";
|
|
668
|
+
/** A workflow run without its step history. Times are unix seconds. */
|
|
669
|
+
export interface WorkflowRunSummary {
|
|
670
|
+
id: string;
|
|
671
|
+
name: string;
|
|
672
|
+
key: string | null;
|
|
673
|
+
status: WorkflowRunStatus;
|
|
674
|
+
input: unknown;
|
|
675
|
+
output: unknown;
|
|
676
|
+
error: string | null;
|
|
677
|
+
cancelReason: string | null;
|
|
678
|
+
/** Event name the run is waiting for, when `status` is "waiting". */
|
|
679
|
+
waitingFor: string | null;
|
|
680
|
+
/** When the current wait times out, if it has a timeout. */
|
|
681
|
+
waitDeadline: number | null;
|
|
682
|
+
/** When the current sleep ends. */
|
|
683
|
+
wakeAt: number | null;
|
|
684
|
+
/** Events sent but not yet consumed by a `waitForEvent`. */
|
|
685
|
+
bufferedEvents: number;
|
|
686
|
+
createdAt: number | null;
|
|
687
|
+
startedAt: number | null;
|
|
688
|
+
completedAt: number | null;
|
|
689
|
+
}
|
|
539
690
|
export interface LlmMessage {
|
|
540
691
|
role: "user" | "assistant" | "system" | "tool";
|
|
541
692
|
content: string | LlmContentBlock[];
|
|
@@ -915,6 +1066,8 @@ export interface MutationCtx<R extends AuthRequirement = "optional"> {
|
|
|
915
1066
|
connections: Connections;
|
|
916
1067
|
/** Durable workflows: start / deliver events — see {@link Workflows}. */
|
|
917
1068
|
workflows: Workflows;
|
|
1069
|
+
/** Application audit log (write only in mutations) — see {@link AuditLog}. */
|
|
1070
|
+
audit: AuditLog;
|
|
918
1071
|
/** Signed file-download URLs — see {@link Files}. */
|
|
919
1072
|
files: Files;
|
|
920
1073
|
/** Shard tickets, reads, and inputs after the commit — see {@link Shards}. */
|
|
@@ -947,8 +1100,18 @@ export interface TenantDomainResult {
|
|
|
947
1100
|
status: string;
|
|
948
1101
|
/** Whether the hostname + TLS certificate are both active. */
|
|
949
1102
|
active?: boolean;
|
|
950
|
-
/**
|
|
951
|
-
|
|
1103
|
+
/**
|
|
1104
|
+
* True for an apex domain (`acme.com`). An apex cannot be a CNAME, so the
|
|
1105
|
+
* customer adds A/AAAA records pointing at the app instead.
|
|
1106
|
+
*/
|
|
1107
|
+
apex?: boolean;
|
|
1108
|
+
/**
|
|
1109
|
+
* Every DNS record the customer adds, for both kinds of domain: a CNAME
|
|
1110
|
+
* (subdomain) or A/AAAA (apex), plus ownership and certificate records.
|
|
1111
|
+
*/
|
|
1112
|
+
records?: TenantDomainRecord[];
|
|
1113
|
+
/** The CNAME the customer points a subdomain at; null for an apex. */
|
|
1114
|
+
cnameTarget: string | null;
|
|
952
1115
|
/** TXT record proving domain ownership before DNS cutover (or null). */
|
|
953
1116
|
ownership: TenantDomainDns | null;
|
|
954
1117
|
/** TXT records that issue the DV certificate. */
|
|
@@ -956,6 +1119,13 @@ export interface TenantDomainResult {
|
|
|
956
1119
|
/** Human-readable provisioning errors, empty when healthy. */
|
|
957
1120
|
errors: string[];
|
|
958
1121
|
}
|
|
1122
|
+
export interface TenantDomainRecord {
|
|
1123
|
+
type: "A" | "AAAA" | "CNAME" | "TXT";
|
|
1124
|
+
name: string;
|
|
1125
|
+
value: string;
|
|
1126
|
+
/** What the record is for, to show the customer. */
|
|
1127
|
+
purpose: string;
|
|
1128
|
+
}
|
|
959
1129
|
/**
|
|
960
1130
|
* Platform domains: attach / register / detach custom domains for your app's
|
|
961
1131
|
* OWN end-customers, so each tenant can reach your app on their own hostname.
|
|
@@ -1081,6 +1251,8 @@ export interface ActionCtx<R extends AuthRequirement = "optional"> {
|
|
|
1081
1251
|
connections: Connections;
|
|
1082
1252
|
/** Durable workflows: start / deliver events — see {@link Workflows}. */
|
|
1083
1253
|
workflows: Workflows;
|
|
1254
|
+
/** Application audit log — see {@link Audit}. */
|
|
1255
|
+
audit: Audit;
|
|
1084
1256
|
/** Attach / register custom domains for your app's OWN end-customers
|
|
1085
1257
|
* (platform domains) — see {@link Domains}. Cloud-only. */
|
|
1086
1258
|
domains: Domains;
|
|
@@ -1125,12 +1297,34 @@ export interface ActionCtx<R extends AuthRequirement = "optional"> {
|
|
|
1125
1297
|
* ```
|
|
1126
1298
|
*/
|
|
1127
1299
|
request?: RequestInfo;
|
|
1300
|
+
/**
|
|
1301
|
+
* Build a raw HTTP response. When an action called through
|
|
1302
|
+
* `/api/webhooks/<name>` returns it, the route sends this status,
|
|
1303
|
+
* content type, headers, and body instead of JSON. Other routes return
|
|
1304
|
+
* it as ordinary JSON data. Throws on an invalid status or header.
|
|
1305
|
+
*
|
|
1306
|
+
* ```ts
|
|
1307
|
+
* // Twilio expects TwiML, not JSON.
|
|
1308
|
+
* return ctx.response({ contentType: "text/xml", body: "<Response/>" });
|
|
1309
|
+
* ```
|
|
1310
|
+
*/
|
|
1311
|
+
response(init?: RawResponseInit): RawResponse;
|
|
1128
1312
|
}
|
|
1129
1313
|
/** HTTP request metadata available on an action's ctx when invoked via an
|
|
1130
1314
|
* HTTP route binding. Header names are lowercased. */
|
|
1131
1315
|
export interface RequestInfo {
|
|
1132
1316
|
method: string;
|
|
1317
|
+
/** Request path with the query string, as received. */
|
|
1133
1318
|
path: string;
|
|
1319
|
+
/**
|
|
1320
|
+
* The full URL the client requested: scheme, host, path, and query.
|
|
1321
|
+
* Behind a proxy, the host comes from `X-Forwarded-Host` or `Host`
|
|
1322
|
+
* only when it is trusted (loopback, the `PYLON_PUBLIC_URL` host,
|
|
1323
|
+
* `PYLON_CANONICAL_HOST`, a `PYLON_TRUSTED_HOSTS` entry, or one of the
|
|
1324
|
+
* app's domains); otherwise the `PYLON_PUBLIC_URL` origin is used. Use
|
|
1325
|
+
* it to verify providers that sign the whole URL, such as Twilio.
|
|
1326
|
+
*/
|
|
1327
|
+
url: string;
|
|
1134
1328
|
headers: Record<string, string>;
|
|
1135
1329
|
rawBody: string;
|
|
1136
1330
|
}
|
package/dist/workflows.d.ts
CHANGED
|
@@ -57,6 +57,7 @@ export type WorkflowRunnerResponse = {
|
|
|
57
57
|
} | {
|
|
58
58
|
action: "wait_event";
|
|
59
59
|
event: string;
|
|
60
|
+
timeout?: string;
|
|
60
61
|
} | {
|
|
61
62
|
action: "complete";
|
|
62
63
|
output: unknown;
|
|
@@ -80,10 +81,26 @@ export interface WorkflowRun<TInput = unknown> {
|
|
|
80
81
|
/** Pause the workflow for a duration ("30s", "5m", "24h", "7d"). */
|
|
81
82
|
sleep(duration: string): Promise<void>;
|
|
82
83
|
/**
|
|
83
|
-
* Pause until
|
|
84
|
-
* Resolves with the event's data
|
|
84
|
+
* Pause until the event arrives through `ctx.workflows.sendEvent` or
|
|
85
|
+
* `POST /api/workflows/<id>/event`. Resolves with the event's data
|
|
86
|
+
* (`{}` when it was sent without data).
|
|
87
|
+
*
|
|
88
|
+
* Events sent while the run is not waiting for them are buffered. The
|
|
89
|
+
* next `waitForEvent` with that name consumes the oldest buffered
|
|
90
|
+
* event, so an event that arrives during a step is not lost.
|
|
91
|
+
*
|
|
92
|
+
* The same event name can be awaited more than once in a run; each
|
|
93
|
+
* call consumes one event.
|
|
85
94
|
*/
|
|
86
95
|
waitForEvent<T = unknown>(eventName: string): Promise<T>;
|
|
96
|
+
/**
|
|
97
|
+
* Same as above, but resolves with `null` if no event arrives within
|
|
98
|
+
* `timeout` ("60s", "5m", "24h", "7d"). The timeout is durable: it
|
|
99
|
+
* survives restarts, like `sleep`.
|
|
100
|
+
*/
|
|
101
|
+
waitForEvent<T = unknown>(eventName: string, opts: {
|
|
102
|
+
timeout: string;
|
|
103
|
+
}): Promise<T | null>;
|
|
87
104
|
}
|
|
88
105
|
export interface WorkflowDefinition<TInput = unknown> {
|
|
89
106
|
readonly __pylonWorkflow: true;
|
package/package.json
CHANGED
package/src/define.ts
CHANGED
|
@@ -59,7 +59,8 @@ interface CommonDef<
|
|
|
59
59
|
* IDLE-timeout SECONDS for this function: how long it may go without
|
|
60
60
|
* producing any activity (a stream chunk, a `ctx.db` op, an LLM
|
|
61
61
|
* event) before the host cancels the call. Defaults to
|
|
62
|
-
* `PYLON_FN_CALL_TIMEOUT` (30s
|
|
62
|
+
* `PYLON_FN_CALL_TIMEOUT` (30s; Stack0 Cloud sets 300s). Activity
|
|
63
|
+
* restarts the budget, so a
|
|
63
64
|
* streaming agent run stays alive as long as it keeps producing; a
|
|
64
65
|
* silent hang is cancelled at the budget. Total lifetime is capped at
|
|
65
66
|
* 10× this value however chatty the call is.
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
export { query, mutation, action } from "./define";
|
|
22
22
|
export { v } from "./validators";
|
|
23
23
|
export { workflow } from "./workflows";
|
|
24
|
+
export type { RawResponse, RawResponseInit } from "./response";
|
|
24
25
|
export {
|
|
25
26
|
agent,
|
|
26
27
|
isAgentDefinition,
|
|
@@ -80,6 +81,11 @@ export type {
|
|
|
80
81
|
ShardTransfer,
|
|
81
82
|
MemberRow,
|
|
82
83
|
Workflows,
|
|
84
|
+
WorkflowRunSummary,
|
|
85
|
+
WorkflowRunStatus,
|
|
86
|
+
Audit,
|
|
87
|
+
AuditLog,
|
|
88
|
+
AuditEntry,
|
|
83
89
|
VectorSearchQuery,
|
|
84
90
|
VectorSearchResult,
|
|
85
91
|
SearchResult,
|
|
@@ -99,6 +105,7 @@ export type {
|
|
|
99
105
|
// results (the DNS the end-customer must set).
|
|
100
106
|
Domains,
|
|
101
107
|
TenantDomainResult,
|
|
108
|
+
TenantDomainRecord,
|
|
102
109
|
TenantDomainDns,
|
|
103
110
|
DomainAvailability,
|
|
104
111
|
DomainContact,
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { response } from "./response";
|
|
3
|
+
|
|
4
|
+
describe("ctx.response", () => {
|
|
5
|
+
test("defaults to an empty 200", () => {
|
|
6
|
+
expect(response()).toEqual({ __pylonResponse: 1, status: 200, headers: {}, body: "" });
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test("contentType sets the content-type header and replaces one in headers", () => {
|
|
10
|
+
const r = response({
|
|
11
|
+
contentType: "text/xml",
|
|
12
|
+
headers: { "Content-Type": "text/plain", "X-Id": "1" },
|
|
13
|
+
body: "<Response/>",
|
|
14
|
+
});
|
|
15
|
+
expect(r.headers).toEqual({ "X-Id": "1", "content-type": "text/xml" });
|
|
16
|
+
expect(r.body).toBe("<Response/>");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("rejects out-of-range and non-integer status", () => {
|
|
20
|
+
for (const status of [99, 101, 600, 200.5, Number.NaN]) {
|
|
21
|
+
expect(() => response({ status })).toThrow("status");
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("rejects a body on 204", () => {
|
|
26
|
+
expect(() => response({ status: 204, body: "x" })).toThrow("cannot carry a body");
|
|
27
|
+
expect(response({ status: 204 }).status).toBe(204);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("rejects header injection and invalid names", () => {
|
|
31
|
+
expect(() => response({ headers: { "X-A": "a\r\nSet-Cookie: x=1" } })).toThrow("control");
|
|
32
|
+
expect(() => response({ headers: { "X-A": "café" } })).toThrow("control");
|
|
33
|
+
expect(() => response({ headers: { "X A": "v" } })).toThrow("token");
|
|
34
|
+
expect(() => response({ contentType: "text/xml\r\nX: y" })).toThrow("control");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("rejects headers the server owns, in any case", () => {
|
|
38
|
+
for (const name of ["Content-Length", "set-cookie", "Access-Control-Allow-Origin", "Connection"]) {
|
|
39
|
+
expect(() => response({ headers: { [name]: "v" } })).toThrow("set by the server");
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
});
|
package/src/response.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw HTTP responses from webhook actions.
|
|
3
|
+
*
|
|
4
|
+
* An action called through `/api/webhooks/<name>` normally answers with its
|
|
5
|
+
* return value as JSON and status 200. Returning `ctx.response(...)` sends
|
|
6
|
+
* the given status, content type, headers, and body instead. Every other
|
|
7
|
+
* route (`/api/fn/<name>`, `ctx.runAction`) returns the value as ordinary
|
|
8
|
+
* JSON data.
|
|
9
|
+
*
|
|
10
|
+
* The host enforces the same rules (crates/router/src/raw_response.rs), so
|
|
11
|
+
* a hand-built object cannot bypass them. The two header lists must stay in
|
|
12
|
+
* sync.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** What `ctx.response` accepts. */
|
|
16
|
+
export interface RawResponseInit {
|
|
17
|
+
/** HTTP status, 200-599. Default 200. */
|
|
18
|
+
status?: number;
|
|
19
|
+
/** Shorthand for the `Content-Type` header. Default `text/plain; charset=utf-8`. */
|
|
20
|
+
contentType?: string;
|
|
21
|
+
/** Extra response headers. Values must be visible ASCII. */
|
|
22
|
+
headers?: Record<string, string>;
|
|
23
|
+
/** Response body. Default empty. Must be empty for 204, 205, and 304. */
|
|
24
|
+
body?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The marked value an action returns. Build it with `ctx.response`. */
|
|
28
|
+
export interface RawResponse {
|
|
29
|
+
readonly __pylonResponse: 1;
|
|
30
|
+
readonly status: number;
|
|
31
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
32
|
+
readonly body: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Header names the server sets itself. Compared lowercase. */
|
|
36
|
+
const RESERVED_HEADERS = new Set([
|
|
37
|
+
"connection",
|
|
38
|
+
"content-length",
|
|
39
|
+
"keep-alive",
|
|
40
|
+
"proxy-authenticate",
|
|
41
|
+
"proxy-authorization",
|
|
42
|
+
"proxy-connection",
|
|
43
|
+
"te",
|
|
44
|
+
"trailer",
|
|
45
|
+
"transfer-encoding",
|
|
46
|
+
"upgrade",
|
|
47
|
+
"set-cookie",
|
|
48
|
+
"permissions-policy",
|
|
49
|
+
"referrer-policy",
|
|
50
|
+
"x-content-type-options",
|
|
51
|
+
"x-frame-options",
|
|
52
|
+
"x-xss-protection",
|
|
53
|
+
]);
|
|
54
|
+
const RESERVED_HEADER_PREFIXES = ["access-control-"];
|
|
55
|
+
|
|
56
|
+
const TOKEN_RE = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
57
|
+
const VALUE_RE = /^[\t\x20-\x7e]*$/;
|
|
58
|
+
|
|
59
|
+
function checkHeader(name: string, value: unknown): void {
|
|
60
|
+
if (!TOKEN_RE.test(name)) {
|
|
61
|
+
throw new Error(`ctx.response: header name ${JSON.stringify(name)} is not a valid HTTP token`);
|
|
62
|
+
}
|
|
63
|
+
if (typeof value !== "string") {
|
|
64
|
+
throw new Error(`ctx.response: header "${name}" must have a string value`);
|
|
65
|
+
}
|
|
66
|
+
if (!VALUE_RE.test(value)) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`ctx.response: header "${name}" has a value with control or non-ASCII characters`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
const lower = name.toLowerCase();
|
|
72
|
+
if (RESERVED_HEADERS.has(lower) || RESERVED_HEADER_PREFIXES.some((p) => lower.startsWith(p))) {
|
|
73
|
+
throw new Error(`ctx.response: header "${name}" is set by the server`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build a raw HTTP response for a webhook action to return.
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* return ctx.response({ contentType: "text/xml", body: "<Response/>" });
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
export function response(init: RawResponseInit = {}): RawResponse {
|
|
85
|
+
const status = init.status ?? 200;
|
|
86
|
+
if (!Number.isInteger(status) || status < 200 || status > 599) {
|
|
87
|
+
throw new Error(`ctx.response: status must be an integer from 200 to 599, got ${status}`);
|
|
88
|
+
}
|
|
89
|
+
const body = init.body ?? "";
|
|
90
|
+
if (typeof body !== "string") {
|
|
91
|
+
throw new Error("ctx.response: body must be a string");
|
|
92
|
+
}
|
|
93
|
+
if (body !== "" && (status === 204 || status === 205 || status === 304)) {
|
|
94
|
+
throw new Error(`ctx.response: status ${status} cannot carry a body`);
|
|
95
|
+
}
|
|
96
|
+
const headers: Record<string, string> = {};
|
|
97
|
+
for (const [name, value] of Object.entries(init.headers ?? {})) {
|
|
98
|
+
checkHeader(name, value);
|
|
99
|
+
headers[name] = value;
|
|
100
|
+
}
|
|
101
|
+
if (init.contentType !== undefined) {
|
|
102
|
+
checkHeader("Content-Type", init.contentType);
|
|
103
|
+
for (const name of Object.keys(headers)) {
|
|
104
|
+
if (name.toLowerCase() === "content-type") delete headers[name];
|
|
105
|
+
}
|
|
106
|
+
headers["content-type"] = init.contentType;
|
|
107
|
+
}
|
|
108
|
+
return { __pylonResponse: 1, status, headers, body };
|
|
109
|
+
}
|