@ezghcloud/workflows 0.0.0-stage → 1.0.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 (51) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +417 -2
  3. package/dist/client.d.ts +209 -0
  4. package/dist/client.js +259 -0
  5. package/dist/env.d.ts +2 -0
  6. package/dist/env.js +32 -0
  7. package/dist/index.d.ts +15 -0
  8. package/dist/index.js +7 -0
  9. package/dist/protocol/bytes.d.ts +8 -0
  10. package/dist/protocol/bytes.js +49 -0
  11. package/dist/protocol/callback.d.ts +6 -0
  12. package/dist/protocol/callback.js +31 -0
  13. package/dist/protocol/codec.d.ts +6 -0
  14. package/dist/protocol/codec.js +38 -0
  15. package/dist/protocol/derived.d.ts +5 -0
  16. package/dist/protocol/derived.js +41 -0
  17. package/dist/protocol/index.d.ts +14 -0
  18. package/dist/protocol/index.js +21 -0
  19. package/dist/protocol/public.d.ts +2 -0
  20. package/dist/protocol/public.js +1 -0
  21. package/dist/protocol/signature.d.ts +17 -0
  22. package/dist/protocol/signature.js +101 -0
  23. package/dist/protocol/stepid.d.ts +4 -0
  24. package/dist/protocol/stepid.js +9 -0
  25. package/dist/protocol/tokens.d.ts +2 -0
  26. package/dist/protocol/tokens.js +38 -0
  27. package/dist/protocol/types.d.ts +185 -0
  28. package/dist/protocol/types.js +1 -0
  29. package/dist/protocol/validate.d.ts +15 -0
  30. package/dist/protocol/validate.js +279 -0
  31. package/dist/queues.d.ts +47 -0
  32. package/dist/queues.js +126 -0
  33. package/dist/runtime/context.d.ts +82 -0
  34. package/dist/runtime/context.js +1 -0
  35. package/dist/runtime/errors.d.ts +49 -0
  36. package/dist/runtime/errors.js +160 -0
  37. package/dist/runtime/execution.d.ts +17 -0
  38. package/dist/runtime/execution.js +608 -0
  39. package/dist/runtime/invocation.d.ts +5 -0
  40. package/dist/runtime/invocation.js +47 -0
  41. package/dist/runtime/live.d.ts +23 -0
  42. package/dist/runtime/live.js +103 -0
  43. package/dist/runtime/workflow.d.ts +26 -0
  44. package/dist/runtime/workflow.js +49 -0
  45. package/dist/serve.d.ts +12 -0
  46. package/dist/serve.js +149 -0
  47. package/dist/version.d.ts +2 -0
  48. package/dist/version.js +2 -0
  49. package/dist/worker.d.ts +43 -0
  50. package/dist/worker.js +364 -0
  51. package/package.json +49 -4
@@ -0,0 +1,209 @@
1
+ import type { RetryPolicy, SDKInfo, WireError } from "./protocol/index.ts";
2
+ import type { FetchLike } from "./runtime/live.ts";
3
+ export type RunStatus = "running" | "cancelling" | "completed" | "failed" | "cancelled" | "timedOut" | (string & {});
4
+ export interface RunStep {
5
+ seq: number;
6
+ stepId: string;
7
+ name: string;
8
+ kind: string;
9
+ status: string;
10
+ attempt: number;
11
+ hasOutput: boolean;
12
+ error: WireError | null;
13
+ }
14
+ export interface Run {
15
+ id: string;
16
+ runKey: string | null;
17
+ appId: string;
18
+ workflow: string;
19
+ status: RunStatus;
20
+ attempt: number;
21
+ error: WireError | null;
22
+ causeStepId: string | null;
23
+ steps: RunStep[];
24
+ createdAt: string;
25
+ updatedAt: string;
26
+ endedAt: string | null;
27
+ }
28
+ export interface RunEvent {
29
+ id: string;
30
+ at: string;
31
+ type: string;
32
+ stepId: string | null;
33
+ invocationId: string | null;
34
+ deployment: string | null;
35
+ detail: unknown;
36
+ }
37
+ export interface AppWorkflow {
38
+ name: string;
39
+ retry?: RetryPolicy;
40
+ timeoutMs?: number;
41
+ }
42
+ export interface App {
43
+ id: string;
44
+ slug: string;
45
+ resourceName: string;
46
+ organizationId: string;
47
+ projectId: string;
48
+ name: string;
49
+ kind: "push" | "pull" | (string & {});
50
+ endpoint: string | null;
51
+ syncStatus: "unsynced" | "synced" | "failed" | (string & {});
52
+ workflows: AppWorkflow[];
53
+ maxInvocationSeconds: number | null;
54
+ protocolVersion: string | null;
55
+ sdk: SDKInfo | null;
56
+ deployment: string | null;
57
+ health: string;
58
+ lastSyncAt: string | null;
59
+ createdAt: string;
60
+ updatedAt: string;
61
+ queue?: string | null;
62
+ partitions?: number | null;
63
+ }
64
+ export interface Schedule {
65
+ id: string;
66
+ resourceName: string;
67
+ appId: string;
68
+ workflow: string;
69
+ name: string;
70
+ cron: string;
71
+ timeZone: string;
72
+ overlap: "skip" | "allow" | "bufferOne" | (string & {});
73
+ jitterSeconds: number;
74
+ catchupWindowSeconds: number;
75
+ paused: boolean;
76
+ revision: number;
77
+ nextFireAt: string | null;
78
+ nextScheduledTime: string | null;
79
+ lastScheduledTime: string | null;
80
+ activeRunId: string | null;
81
+ missed: number;
82
+ skipped: number;
83
+ hasInput: boolean;
84
+ createdAt: string;
85
+ updatedAt: string;
86
+ }
87
+ export interface ScheduleSummary {
88
+ id: string;
89
+ resourceName: string;
90
+ appId: string;
91
+ workflow: string;
92
+ name: string;
93
+ cron: string;
94
+ timeZone: string;
95
+ paused: boolean;
96
+ revision: number;
97
+ updatedAt: string;
98
+ }
99
+ export interface ScheduleInput {
100
+ appId: string;
101
+ workflow: string;
102
+ name: string;
103
+ cron: string;
104
+ timeZone?: string;
105
+ input?: unknown;
106
+ overlap?: "skip" | "allow" | "bufferOne";
107
+ jitterSeconds?: number;
108
+ catchupWindowSeconds?: number;
109
+ paused?: boolean;
110
+ }
111
+ export type ScheduleUpdate = Partial<Omit<ScheduleInput, "appId" | "paused">>;
112
+ export interface AppWithSecret {
113
+ app: App;
114
+ signingSecret: string | null;
115
+ }
116
+ export declare class WorkflowsApiError extends Error {
117
+ readonly status: number;
118
+ readonly code: string;
119
+ readonly retryAfterSeconds?: number | undefined;
120
+ name: string;
121
+ constructor(status: number, code: string, message: string, retryAfterSeconds?: number | undefined);
122
+ }
123
+ export interface ClientOptions {
124
+ apiKey: string;
125
+ organizationId: string;
126
+ projectId: string;
127
+ baseUrl?: string;
128
+ fetch?: FetchLike;
129
+ retries?: number;
130
+ maxRetryDelayMs?: number;
131
+ timeoutMs?: number;
132
+ }
133
+ export declare function completeCallbackURL(url: string, value: unknown, opts?: {
134
+ fetch?: FetchLike;
135
+ timeoutMs?: number;
136
+ }): Promise<void>;
137
+ export declare class Client {
138
+ #private;
139
+ constructor(opts: ClientOptions);
140
+ private call;
141
+ private send;
142
+ private withSecret;
143
+ createApp(name: string, opts?: {
144
+ endpoint?: string;
145
+ kind?: "push" | "pull";
146
+ queue?: string;
147
+ partitions?: number;
148
+ idempotencyKey?: string;
149
+ }): Promise<AppWithSecret>;
150
+ getApp(appId: string): Promise<App>;
151
+ listApps(opts?: {
152
+ cursor?: string;
153
+ limit?: number;
154
+ }): Promise<{
155
+ apps: App[];
156
+ nextCursor: string | null;
157
+ }>;
158
+ devApp(opts?: {
159
+ user?: string;
160
+ name?: string;
161
+ partitions?: number;
162
+ }): Promise<App>;
163
+ sync(appId: string, opts?: {
164
+ endpoint?: string;
165
+ deployment?: string;
166
+ }): Promise<App>;
167
+ rotateSecret(appId: string): Promise<AppWithSecret>;
168
+ start(appId: string, workflow: string, input?: unknown, opts?: {
169
+ runKey?: string;
170
+ idempotencyKey?: string;
171
+ }): Promise<Run>;
172
+ get(runId: string): Promise<Run>;
173
+ cancel(runId: string, opts?: {
174
+ gracePeriodSeconds?: number;
175
+ }): Promise<Run>;
176
+ retry(runId: string): Promise<Run>;
177
+ output(runId: string): Promise<unknown>;
178
+ input(runId: string): Promise<unknown>;
179
+ stepOutput(runId: string, stepId: string): Promise<unknown>;
180
+ events(runId: string, opts?: {
181
+ cursor?: string;
182
+ limit?: number;
183
+ }): Promise<{
184
+ events: RunEvent[];
185
+ nextCursor: string | null;
186
+ }>;
187
+ createSchedule(input: ScheduleInput, opts?: {
188
+ idempotencyKey?: string;
189
+ }): Promise<Schedule>;
190
+ getSchedule(scheduleId: string): Promise<Schedule>;
191
+ listSchedules(opts?: {
192
+ cursor?: string;
193
+ limit?: number;
194
+ }): Promise<{
195
+ schedules: ScheduleSummary[];
196
+ nextCursor: string | null;
197
+ }>;
198
+ updateSchedule(scheduleId: string, patch: ScheduleUpdate, opts: {
199
+ revision: number;
200
+ }): Promise<Schedule>;
201
+ pauseSchedule(scheduleId: string): Promise<Schedule>;
202
+ resumeSchedule(scheduleId: string): Promise<Schedule>;
203
+ deleteSchedule(scheduleId: string): Promise<void>;
204
+ triggerSchedule(scheduleId: string, opts?: {
205
+ idempotencyKey?: string;
206
+ }): Promise<Run>;
207
+ scheduleInput(scheduleId: string): Promise<unknown>;
208
+ completeCallback(runId: string, stepId: string, value: unknown): Promise<Run>;
209
+ }
package/dist/client.js ADDED
@@ -0,0 +1,259 @@
1
+ import { devQueue } from "./worker.js";
2
+ // An error answer from the Workflows API: the HTTP status and the platform error code. Status 0
3
+ // with code "outcome_unknown": the request may or may not have taken effect (a network error on
4
+ // createApp or rotateSecret, which aren't resent; see there).
5
+ export class WorkflowsApiError extends Error {
6
+ status;
7
+ code;
8
+ retryAfterSeconds;
9
+ name = "WorkflowsApiError";
10
+ constructor(status, code, message, retryAfterSeconds) {
11
+ super(message);
12
+ this.status = status;
13
+ this.code = code;
14
+ this.retryAfterSeconds = retryAfterSeconds;
15
+ }
16
+ }
17
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
18
+ const id = encodeURIComponent;
19
+ const DEFAULT_TIMEOUT_MS = 30_000;
20
+ // What an answer to a request that returns a secret (createApp, rotateSecret) can't be resent on:
21
+ // the request may have taken effect, and its replay returns no secret.
22
+ const outcomeUnknown = (why) => new WorkflowsApiError(0, "outcome_unknown", `${why}, so the request may have taken effect: a resend would return no secret. Check with getApp, and rotate the app's secret if you didn't receive it.`);
23
+ function checkLimit(limit) {
24
+ if (limit !== undefined && (!Number.isInteger(limit) || limit < 1))
25
+ throw new TypeError("limit is a whole number ≥ 1");
26
+ }
27
+ function apiError(res, data) {
28
+ const e = data?.error;
29
+ const ra = Number(res.headers.get("Retry-After"));
30
+ if (res.status >= 300 && res.status < 400)
31
+ return new WorkflowsApiError(res.status, "redirect", `The API answered a redirect (${res.status}); requests are never redirected, so check baseUrl`);
32
+ return new WorkflowsApiError(res.status, e?.code ?? "unknown", e?.message ?? `HTTP ${res.status}`, Number.isFinite(ra) && ra >= 0 && res.headers.get("Retry-After") !== null ? ra : undefined);
33
+ }
34
+ // completeCallbackURL completes a callback by its URL (step.createCallback's url): it POSTs value
35
+ // as JSON with no credential (the URL's token is the credential, so errors never include it). The
36
+ // first completion wins: the same value again succeeds, a different one is 409
37
+ // callback_completed, and after the timeout or the run's end it's 410 callback_expired.
38
+ export async function completeCallbackURL(url, value, opts = {}) {
39
+ const fetchImpl = opts.fetch ?? ((u, i) => fetch(u, i));
40
+ let res;
41
+ try {
42
+ res = await fetchImpl(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(value === undefined ? null : value),
43
+ redirect: "manual", signal: AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS) });
44
+ }
45
+ catch (e) {
46
+ throw new WorkflowsApiError(0, "network_error", `Completing the callback failed in transit: ${e instanceof Error ? e.name : "error"}`);
47
+ }
48
+ const text = await res.text();
49
+ if (res.ok)
50
+ return;
51
+ let data;
52
+ try {
53
+ data = text ? JSON.parse(text) : undefined;
54
+ }
55
+ catch {
56
+ data = undefined;
57
+ }
58
+ throw apiError(res, data);
59
+ }
60
+ // Client calls the Workflows API for one project: apps (create, sync, rotate the secret) and runs
61
+ // (start, inspect, cancel, retry, payloads, events).
62
+ export class Client {
63
+ // ES private fields: the API key never shows in console.log, util.inspect or JSON.stringify.
64
+ #apiKey;
65
+ #base;
66
+ #fetch;
67
+ #retries;
68
+ #maxDelay;
69
+ #timeout;
70
+ constructor(opts) {
71
+ this.#apiKey = opts.apiKey;
72
+ this.#base = `${(opts.baseUrl ?? "https://workflows-api.ezghcloud.com").replace(/\/+$/, "")}/v1/organizations/${id(opts.organizationId)}/projects/${id(opts.projectId)}`;
73
+ this.#fetch = opts.fetch ?? ((u, i) => fetch(u, i));
74
+ this.#retries = opts.retries ?? 3;
75
+ this.#maxDelay = opts.maxRetryDelayMs ?? 10_000;
76
+ if (!Number.isInteger(this.#retries) || this.#retries < 0)
77
+ throw new TypeError("retries is a whole number ≥ 0");
78
+ if (!Number.isFinite(this.#maxDelay) || this.#maxDelay < 0)
79
+ throw new TypeError("maxRetryDelayMs is a number ≥ 0");
80
+ this.#timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
81
+ if (!Number.isFinite(this.#timeout) || this.#timeout <= 0)
82
+ throw new TypeError("timeoutMs is a number > 0");
83
+ }
84
+ // call sends one request, retrying 429, 503, a 409 idempotency_in_progress and network errors
85
+ // (timeouts included) with the same Idempotency-Key, after Retry-After. Redirects are errors.
86
+ // returnsSecret (createApp, rotateSecret): only a 429 is retried; a network error, 503 or 409
87
+ // in progress is outcome_unknown, since the request may have taken effect and its replay would
88
+ // return no secret.
89
+ call(method, path, body, idempotencyKey, returnsSecret = false, extraHeaders = {}) {
90
+ if (idempotencyKey !== undefined && (typeof idempotencyKey !== "string" || idempotencyKey === ""))
91
+ throw new TypeError("An idempotencyKey is a non-empty string");
92
+ return this.send(method, path, body, idempotencyKey, returnsSecret, extraHeaders);
93
+ }
94
+ async send(method, path, body, idempotencyKey, returnsSecret, extraHeaders) {
95
+ const headers = { Authorization: `Bearer ${this.#apiKey}`, Accept: "application/json", ...extraHeaders };
96
+ if (body !== undefined)
97
+ headers["Content-Type"] = "application/json";
98
+ if (method !== "GET")
99
+ headers["Idempotency-Key"] = idempotencyKey ?? crypto.randomUUID();
100
+ const retries = this.#retries;
101
+ const maxDelay = this.#maxDelay;
102
+ for (let attempt = 0;; attempt++) {
103
+ let res;
104
+ try {
105
+ res = await this.#fetch(this.#base + path, { method, headers, ...(body !== undefined ? { body: JSON.stringify(body) } : {}), redirect: "manual", signal: AbortSignal.timeout(this.#timeout) });
106
+ }
107
+ catch (e) {
108
+ if (returnsSecret)
109
+ throw outcomeUnknown(`The request failed in transit (${e instanceof Error ? e.message : String(e)})`);
110
+ if (attempt >= retries)
111
+ throw e;
112
+ await sleep(Math.min(maxDelay, 500 * 2 ** attempt));
113
+ continue;
114
+ }
115
+ const text = await res.text();
116
+ let data;
117
+ try {
118
+ data = text ? JSON.parse(text) : undefined;
119
+ }
120
+ catch {
121
+ data = undefined;
122
+ }
123
+ if (res.ok)
124
+ return data;
125
+ const err = apiError(res, data);
126
+ const inProgress = res.status === 409 && err.code === "idempotency_in_progress";
127
+ if (returnsSecret && (res.status === 503 || inProgress))
128
+ throw outcomeUnknown(`The API answered ${res.status} ${err.code}`);
129
+ if ((res.status !== 429 && res.status !== 503 && !inProgress) || attempt >= retries)
130
+ throw err;
131
+ await sleep(Math.min(maxDelay, (err.retryAfterSeconds ?? 1) * 1000));
132
+ }
133
+ }
134
+ // withSecret is a secret-bearing answer: a push app's success without its secret is the replay of
135
+ // an earlier request (an Idempotency-Key reused), and the secret is lost: outcome_unknown.
136
+ async withSecret(p, pull = false) {
137
+ const r = await p;
138
+ if (!pull && r?.signingSecret == null)
139
+ throw outcomeUnknown("The API answered with no signing secret (the replay of an earlier request with this Idempotency-Key)");
140
+ return r;
141
+ }
142
+ // Apps
143
+ // createApp returns the app and its signing secret, once. An answer that leaves it unknown
144
+ // whether the app was created (a network error, a timeout, a 503, a 409 in progress, or a
145
+ // replay's null secret) throws outcome_unknown: a resend would return no secret. A 429 (not
146
+ // processed) is retried. A pull app has no secret.
147
+ createApp(name, opts = {}) {
148
+ const kind = opts.kind ?? "push";
149
+ return this.withSecret(this.call("POST", "/apps", { name, kind, ...(opts.endpoint ? { endpoint: opts.endpoint } : {}), ...(opts.queue ? { queue: opts.queue } : {}), ...(opts.partitions !== undefined ? { partitions: opts.partitions } : {}) }, opts.idempotencyKey, true), kind === "pull");
150
+ }
151
+ getApp(appId) { return this.call("GET", `/apps/${id(appId)}`); }
152
+ listApps(opts = {}) {
153
+ checkLimit(opts.limit);
154
+ const q = new URLSearchParams();
155
+ if (opts.cursor)
156
+ q.set("cursor", opts.cursor);
157
+ if (opts.limit !== undefined)
158
+ q.set("limit", String(opts.limit));
159
+ const qs = q.toString();
160
+ return this.call("GET", `/apps${qs ? `?${qs}` : ""}`);
161
+ }
162
+ // devApp creates, or finds, a developer's own pull app for dev mode: named and queued
163
+ // devQueue(user) (dev-<user>), so worker({ app: app.id, dev: true }) polls it. The live host
164
+ // serves only an app's own queue, so each developer needs an app of their own.
165
+ async devApp(opts = {}) {
166
+ const queue = devQueue(opts.user);
167
+ const name = opts.name ?? queue;
168
+ try {
169
+ return (await this.createApp(name, { kind: "pull", queue, ...(opts.partitions !== undefined ? { partitions: opts.partitions } : {}) })).app;
170
+ }
171
+ catch (e) {
172
+ // It exists (409 app_exists), or the create's outcome is unknown: find it by name.
173
+ if (!(e instanceof WorkflowsApiError && ((e.status === 409 && e.code === "app_exists") || e.code === "outcome_unknown")))
174
+ throw e;
175
+ }
176
+ for (let cursor;;) {
177
+ const page = await this.listApps({ limit: 100, ...(cursor ? { cursor } : {}) });
178
+ const app = page.apps.find((a) => a.name === name);
179
+ if (app) {
180
+ if (app.kind !== "pull" || app.queue !== queue)
181
+ throw new WorkflowsApiError(409, "app_exists", `The app "${name}" exists but isn't a pull app with the queue ${queue}; pass devApp({ name }) another name`);
182
+ return app;
183
+ }
184
+ if (!page.nextCursor)
185
+ throw new WorkflowsApiError(404, "not_found", `The dev app "${name}" was neither created nor found`);
186
+ cursor = page.nextCursor;
187
+ }
188
+ }
189
+ // sync asks EZGH to call the app's endpoint and record its workflows; run it after each deploy.
190
+ // A failed sync is a 409 sync_failed.
191
+ sync(appId, opts = {}) {
192
+ return this.call("POST", `/apps/${id(appId)}/sync`, opts);
193
+ }
194
+ // rotateSecret returns a new signing secret; the old ones stay valid for 24 hours.
195
+ // A network error isn't resent, as for createApp (outcome_unknown).
196
+ rotateSecret(appId) { return this.withSecret(this.call("POST", `/apps/${id(appId)}/rotate-secret`, {}, undefined, true)); }
197
+ // Runs
198
+ // start a run: 201 for a new run. With runKey, starting again with the same workflow and input
199
+ // returns the same run, and different input is a 409 run_key_conflict.
200
+ start(appId, workflow, input, opts = {}) {
201
+ return this.call("POST", "/runs", { appId, workflow, ...(input !== undefined ? { input } : {}), ...(opts.runKey ? { runKey: opts.runKey } : {}) }, opts.idempotencyKey);
202
+ }
203
+ get(runId) { return this.call("GET", `/runs/${id(runId)}`); }
204
+ // cancel: the run is cancelling; its code gets gracePeriodSeconds (0–3600, default 300) to clean up.
205
+ cancel(runId, opts = {}) {
206
+ return this.call("POST", `/runs/${id(runId)}/cancel`, opts.gracePeriodSeconds === undefined ? {} : { gracePeriodSeconds: opts.gracePeriodSeconds });
207
+ }
208
+ // retry a failed run from its failed step; completed steps aren't run again.
209
+ retry(runId) { return this.call("POST", `/runs/${id(runId)}/retry`, {}); }
210
+ // The run's output (workflows.runs.getPayload); 409 run_not_completed until it completes.
211
+ output(runId) { return this.call("GET", `/runs/${id(runId)}/payloads/output`); }
212
+ input(runId) { return this.call("GET", `/runs/${id(runId)}/payloads/input`); }
213
+ stepOutput(runId, stepId) { return this.call("GET", `/runs/${id(runId)}/steps/${id(stepId)}/output`); }
214
+ events(runId, opts = {}) {
215
+ checkLimit(opts.limit);
216
+ const q = new URLSearchParams();
217
+ if (opts.cursor)
218
+ q.set("cursor", opts.cursor);
219
+ if (opts.limit !== undefined)
220
+ q.set("limit", String(opts.limit));
221
+ const qs = q.toString();
222
+ return this.call("GET", `/runs/${id(runId)}/events${qs ? `?${qs}` : ""}`);
223
+ }
224
+ // Schedules: cron schedules that start runs of one workflow (design-phase-2 "Schedules"), with
225
+ // the workflows.schedules.* actions.
226
+ createSchedule(input, opts = {}) { return this.call("POST", "/schedules", input, opts.idempotencyKey); }
227
+ getSchedule(scheduleId) { return this.call("GET", `/schedules/${id(scheduleId)}`); }
228
+ listSchedules(opts = {}) {
229
+ checkLimit(opts.limit);
230
+ const q = new URLSearchParams();
231
+ if (opts.cursor)
232
+ q.set("cursor", opts.cursor);
233
+ if (opts.limit !== undefined)
234
+ q.set("limit", String(opts.limit));
235
+ const qs = q.toString();
236
+ return this.call("GET", `/schedules${qs ? `?${qs}` : ""}`);
237
+ }
238
+ // updateSchedule changes the given fields. revision is the schedule's revision you read
239
+ // (If-Match): a change since then answers 412.
240
+ updateSchedule(scheduleId, patch, opts) {
241
+ return this.call("PATCH", `/schedules/${id(scheduleId)}`, patch, undefined, false, { "If-Match": `"${opts.revision}"` });
242
+ }
243
+ pauseSchedule(scheduleId) { return this.call("POST", `/schedules/${id(scheduleId)}/pause`, {}); }
244
+ // resumeSchedule fires again from now, with no catch-up.
245
+ resumeSchedule(scheduleId) { return this.call("POST", `/schedules/${id(scheduleId)}/resume`, {}); }
246
+ async deleteSchedule(scheduleId) { await this.call("DELETE", `/schedules/${id(scheduleId)}`); }
247
+ // triggerSchedule starts a run now. The Idempotency-Key names the run, so the same key always
248
+ // answers the same run (without one, each call starts a new run).
249
+ triggerSchedule(scheduleId, opts = {}) { return this.call("POST", `/schedules/${id(scheduleId)}/trigger`, {}, opts.idempotencyKey); }
250
+ // The input every run of the schedule gets (workflows.runs.getPayload); null when none.
251
+ scheduleInput(scheduleId) { return this.call("GET", `/schedules/${id(scheduleId)}/payloads/input`); }
252
+ // Callbacks
253
+ // completeCallback completes a waiting callback as an authenticated caller
254
+ // (workflows.callbacks.complete, in the workflows:runs scope) and answers the run. The first
255
+ // completion wins: the same value again succeeds, a different one is 409 callback_completed,
256
+ // and after the timeout or the run's end it's 410 callback_expired. The public alternative is
257
+ // POSTing the value to the callback's URL.
258
+ completeCallback(runId, stepId, value) { return this.call("POST", `/runs/${id(runId)}/callbacks/${id(stepId)}`, value === undefined ? null : value); }
259
+ }
package/dist/env.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function cleanDeployment(s: string | undefined): string | undefined;
2
+ export declare function detectDeployment(): string | undefined;
package/dist/env.js ADDED
@@ -0,0 +1,32 @@
1
+ import { truncateUtf8 } from "./protocol/bytes.js";
2
+ // The longest deployment label the engine stores (internal/protocol MaxDeploymentLen), in bytes.
3
+ const MAX_DEPLOYMENT_BYTES = 256;
4
+ // cleanDeployment: a deployment label as the engine takes it: NUL characters removed, cut to
5
+ // 256 bytes of UTF-8. Empty is none.
6
+ export function cleanDeployment(s) {
7
+ if (typeof s !== "string")
8
+ return undefined;
9
+ return truncateUtf8(s.replaceAll("\u0000", ""), MAX_DEPLOYMENT_BYTES) || undefined;
10
+ }
11
+ // Deployment IDs from the platform's environment (design, "Runs continue on whatever code is
12
+ // deployed"): Vercel, AWS Lambda (function:version, but not $LATEST), Cloud Run, Fly.io and Render.
13
+ export function detectDeployment() {
14
+ try {
15
+ // Deno throws on process.env without --allow-env.
16
+ const env = globalThis.process?.env;
17
+ if (!env)
18
+ return undefined;
19
+ if (env.VERCEL_DEPLOYMENT_ID)
20
+ return cleanDeployment(env.VERCEL_DEPLOYMENT_ID);
21
+ const fn = env.AWS_LAMBDA_FUNCTION_NAME, version = env.AWS_LAMBDA_FUNCTION_VERSION;
22
+ if (fn && version && version !== "$LATEST")
23
+ return cleanDeployment(`${fn}:${version}`);
24
+ for (const k of ["K_REVISION", "FLY_MACHINE_VERSION", "RENDER_GIT_COMMIT"])
25
+ if (env[k])
26
+ return cleanDeployment(env[k]);
27
+ return undefined;
28
+ }
29
+ catch {
30
+ return undefined;
31
+ }
32
+ }
@@ -0,0 +1,15 @@
1
+ export { SDK_NAME, SDK_VERSION } from "./version.ts";
2
+ export { workflow } from "./runtime/workflow.ts";
3
+ export type { Workflow, WorkflowOptions } from "./runtime/workflow.ts";
4
+ export type { WorkflowContext, Step, StepRunContext, StepRunOptions, Jsonify, Callback, CreateCallbackOptions, InvokeOptions, ReceivedMessage, WaitForMessageOptions } from "./runtime/context.ts";
5
+ export { NonRetriableError, RetryAfterError, CancelledError, WorkflowError, CallbackTimeout, ChildFailed } from "./runtime/errors.ts";
6
+ export { serve } from "./serve.ts";
7
+ export type { ServeOptions } from "./serve.ts";
8
+ export { Client, WorkflowsApiError, completeCallbackURL } from "./client.ts";
9
+ export type { App, AppWithSecret, AppWorkflow, ClientOptions, Run, RunEvent, RunStatus, RunStep, Schedule, ScheduleInput, ScheduleSummary, ScheduleUpdate } from "./client.ts";
10
+ export type { FetchLike } from "./runtime/live.ts";
11
+ export type { RetryPolicy, ScheduleInfo } from "./protocol/types.ts";
12
+ export { worker, devQueue, WorkerAuthError, WorkerConfigError } from "./worker.ts";
13
+ export type { Worker, WorkerOptions } from "./worker.ts";
14
+ export { QueuesClient, QueuesApiError } from "./queues.ts";
15
+ export type { QueuesClientOptions, QueueMessageInput, SentMessage, SentMessages } from "./queues.ts";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { SDK_NAME, SDK_VERSION } from "./version.js";
2
+ export { workflow } from "./runtime/workflow.js";
3
+ export { NonRetriableError, RetryAfterError, CancelledError, WorkflowError, CallbackTimeout, ChildFailed } from "./runtime/errors.js";
4
+ export { serve } from "./serve.js";
5
+ export { Client, WorkflowsApiError, completeCallbackURL } from "./client.js";
6
+ export { worker, devQueue, WorkerAuthError, WorkerConfigError } from "./worker.js";
7
+ export { QueuesClient, QueuesApiError } from "./queues.js";
@@ -0,0 +1,8 @@
1
+ export declare const utf8: (s: string) => Uint8Array<ArrayBuffer>;
2
+ export declare function toHex(b: Uint8Array): string;
3
+ export declare function fromHex(h: string): Uint8Array<ArrayBuffer>;
4
+ export declare function toBase64url(b: Uint8Array): string;
5
+ export declare function fromBase64url(s: string): Uint8Array<ArrayBuffer>;
6
+ export declare function concat(a: Uint8Array, b: Uint8Array): Uint8Array<ArrayBuffer>;
7
+ export declare const utf8Length: (s: string) => number;
8
+ export declare function truncateUtf8(s: string, maxBytes: number): string;
@@ -0,0 +1,49 @@
1
+ export const utf8 = (s) => new TextEncoder().encode(s);
2
+ export function toHex(b) {
3
+ let s = "";
4
+ for (const x of b)
5
+ s += x.toString(16).padStart(2, "0");
6
+ return s;
7
+ }
8
+ export function fromHex(h) {
9
+ const out = new Uint8Array(h.length / 2);
10
+ for (let i = 0; i < out.length; i++)
11
+ out[i] = parseInt(h.slice(2 * i, 2 * i + 2), 16);
12
+ return out;
13
+ }
14
+ export function toBase64url(b) {
15
+ let bin = "";
16
+ for (const x of b)
17
+ bin += String.fromCharCode(x);
18
+ return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
19
+ }
20
+ // fromBase64url decodes unpadded base64url strictly: only the canonical encoding of the bytes is
21
+ // accepted (Go's base64.RawURLEncoding.Strict()), so one secret has exactly one text form.
22
+ export function fromBase64url(s) {
23
+ if (!/^[A-Za-z0-9_-]*$/.test(s) || s.length % 4 === 1)
24
+ throw new TypeError("not base64url");
25
+ const bin = atob(s.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat((4 - (s.length % 4)) % 4));
26
+ const out = new Uint8Array(bin.length);
27
+ for (let i = 0; i < bin.length; i++)
28
+ out[i] = bin.charCodeAt(i);
29
+ if (toBase64url(out) !== s)
30
+ throw new TypeError("not canonical base64url");
31
+ return out;
32
+ }
33
+ export function concat(a, b) {
34
+ const out = new Uint8Array(a.length + b.length);
35
+ out.set(a, 0);
36
+ out.set(b, a.length);
37
+ return out;
38
+ }
39
+ export const utf8Length = (s) => utf8(s).length;
40
+ // truncateUtf8 cuts s to at most maxBytes of UTF-8, never inside a code point.
41
+ export function truncateUtf8(s, maxBytes) {
42
+ const b = utf8(s);
43
+ if (b.length <= maxBytes)
44
+ return s;
45
+ let cut = maxBytes;
46
+ while (cut > 0 && (b[cut] & 0xc0) === 0x80)
47
+ cut--;
48
+ return new TextDecoder().decode(b.subarray(0, cut));
49
+ }
@@ -0,0 +1,6 @@
1
+ export declare function mintCallbackToken(shard: number, random?: (n: number) => Uint8Array): string;
2
+ export declare function parseCallbackToken(token: string): {
3
+ shard: number;
4
+ secret: Uint8Array;
5
+ };
6
+ export declare function callbackTokenHash(token: string): Promise<string>;
@@ -0,0 +1,31 @@
1
+ import { fromBase64url, toBase64url, toHex } from "./bytes.js";
2
+ // Callback tokens (design-phase-2, "Callbacks"): "<shard 3 lowercase hex>.<43 base64url>", 256
3
+ // random bits. The shard is the run's, so workflows-api routes a completion without a lookup; the
4
+ // engine stores SHA-256 of the secret (internal/protocol CallbackToken).
5
+ const TOKEN = /^([0-9a-f]{3})\.([A-Za-z0-9_-]{43})$/;
6
+ export function mintCallbackToken(shard, random = (n) => crypto.getRandomValues(new Uint8Array(n))) {
7
+ if (!Number.isInteger(shard) || shard < 0 || shard > 1023)
8
+ throw new TypeError("A callback token's shard is 0–1023");
9
+ const secret = random(32);
10
+ if (secret.length !== 32)
11
+ throw new TypeError("A callback token's secret is 32 bytes");
12
+ return `${shard.toString(16).padStart(3, "0")}.${toBase64url(secret)}`;
13
+ }
14
+ // parseCallbackToken parses the wire form strictly (lowercase hex, canonical base64url).
15
+ export function parseCallbackToken(token) {
16
+ const m = typeof token === "string" ? TOKEN.exec(token) : null;
17
+ if (!m)
18
+ throw new TypeError("Not a callback token");
19
+ const shard = parseInt(m[1], 16);
20
+ if (shard > 1023)
21
+ throw new TypeError("Not a callback token: the shard is out of range");
22
+ const secret = fromBase64url(m[2]);
23
+ if (secret.length !== 32)
24
+ throw new TypeError("Not a callback token: the secret isn't 32 bytes");
25
+ return { shard, secret };
26
+ }
27
+ // callbackTokenHash: lowercase hex SHA-256 of the token's secret (what the engine stores).
28
+ export async function callbackTokenHash(token) {
29
+ const { secret } = parseCallbackToken(token);
30
+ return toHex(new Uint8Array(await crypto.subtle.digest("SHA-256", secret)));
31
+ }
@@ -0,0 +1,6 @@
1
+ import type { RetryPolicy } from "./types.ts";
2
+ export declare const MAX_OUTPUT_BYTES = 1048576;
3
+ export declare function encodeValue(v: unknown): unknown;
4
+ export declare function jsonByteLength(v: unknown): number;
5
+ export declare function parseDuration(d: string | number): number;
6
+ export declare function backoff(p: RetryPolicy, attempt: number): number;