@voyantjs/workflows-orchestrator-cloudflare 0.6.7 → 0.6.9
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/cf-container-runner.d.ts +102 -0
- package/dist/cf-container-runner.d.ts.map +1 -0
- package/dist/cf-container-runner.js +153 -0
- package/dist/dispatch-handler.d.ts +20 -0
- package/dist/dispatch-handler.d.ts.map +1 -0
- package/dist/dispatch-handler.js +31 -0
- package/dist/do-store.d.ts +4 -0
- package/dist/do-store.d.ts.map +1 -0
- package/dist/do-store.js +34 -0
- package/dist/durable-object.d.ts +24 -0
- package/dist/durable-object.d.ts.map +1 -0
- package/dist/durable-object.js +173 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +34 -0
- package/dist/r2-sign.d.ts +18 -0
- package/dist/r2-sign.d.ts.map +1 -0
- package/dist/r2-sign.js +98 -0
- package/dist/types.d.ts +76 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/worker.d.ts +27 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +165 -0
- package/package.json +25 -19
- package/src/cf-container-runner.ts +63 -71
- package/src/dispatch-handler.ts +12 -14
- package/src/do-store.ts +15 -17
- package/src/durable-object.ts +83 -83
- package/src/index.ts +19 -19
- package/src/r2-sign.ts +53 -56
- package/src/types.ts +31 -28
- package/src/worker.ts +61 -59
package/src/index.ts
CHANGED
|
@@ -26,30 +26,30 @@
|
|
|
26
26
|
// See docs/runtime-protocol.md §2 and docs/design.md §6 for the
|
|
27
27
|
// design this adapter implements.
|
|
28
28
|
|
|
29
|
-
export
|
|
30
|
-
|
|
29
|
+
export {
|
|
30
|
+
type BundleLocation,
|
|
31
|
+
type CfContainerRunnerDeps,
|
|
32
|
+
type ContainerNamespaceLike,
|
|
33
|
+
createCfContainerStepRunner,
|
|
34
|
+
} from "./cf-container-runner.js"
|
|
31
35
|
export {
|
|
32
36
|
createDispatchStepHandler,
|
|
33
37
|
type DispatchHandlerDeps,
|
|
34
|
-
} from "./dispatch-handler.js"
|
|
38
|
+
} from "./dispatch-handler.js"
|
|
39
|
+
export { createDurableObjectRunStore } from "./do-store.js"
|
|
35
40
|
export {
|
|
36
|
-
handleDurableObjectRequest,
|
|
37
|
-
handleDurableObjectAlarm,
|
|
38
41
|
type DurableObjectDeps,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
type WorkerFetchDeps,
|
|
43
|
-
type DurableObjectNamespaceLike,
|
|
44
|
-
} from "./worker.js";
|
|
45
|
-
export {
|
|
46
|
-
createCfContainerStepRunner,
|
|
47
|
-
type BundleLocation,
|
|
48
|
-
type CfContainerRunnerDeps,
|
|
49
|
-
type ContainerNamespaceLike,
|
|
50
|
-
} from "./cf-container-runner.js";
|
|
42
|
+
handleDurableObjectAlarm,
|
|
43
|
+
handleDurableObjectRequest,
|
|
44
|
+
} from "./durable-object.js"
|
|
51
45
|
export {
|
|
52
46
|
createR2Presigner,
|
|
53
|
-
type R2PresignerOptions,
|
|
54
47
|
type PresignArgs,
|
|
55
|
-
|
|
48
|
+
type R2PresignerOptions,
|
|
49
|
+
} from "./r2-sign.js"
|
|
50
|
+
export * from "./types.js"
|
|
51
|
+
export {
|
|
52
|
+
type DurableObjectNamespaceLike,
|
|
53
|
+
handleWorkerRequest,
|
|
54
|
+
type WorkerFetchDeps,
|
|
55
|
+
} from "./worker.js"
|
package/src/r2-sign.ts
CHANGED
|
@@ -17,42 +17,44 @@
|
|
|
17
17
|
|
|
18
18
|
export interface R2PresignerOptions {
|
|
19
19
|
/** Cloudflare account id (32-char hex). */
|
|
20
|
-
accountId: string
|
|
20
|
+
accountId: string
|
|
21
21
|
/** R2 Access Key ID from your CF dashboard — scope read-only. */
|
|
22
|
-
accessKeyId: string
|
|
22
|
+
accessKeyId: string
|
|
23
23
|
/** R2 Secret Access Key. Store as a Worker Secret. */
|
|
24
|
-
secretAccessKey: string
|
|
24
|
+
secretAccessKey: string
|
|
25
25
|
/** R2 bucket name. */
|
|
26
|
-
bucket: string
|
|
26
|
+
bucket: string
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export interface PresignArgs {
|
|
30
30
|
/** Object key, e.g. `"prj_42/v1/container.mjs"`. Leading `/` is optional. */
|
|
31
|
-
key: string
|
|
31
|
+
key: string
|
|
32
32
|
/** Seconds until the URL stops being valid. Min 1, max 604800. */
|
|
33
|
-
expiresIn: number
|
|
33
|
+
expiresIn: number
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function createR2Presigner(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const
|
|
36
|
+
export function createR2Presigner(
|
|
37
|
+
opts: R2PresignerOptions,
|
|
38
|
+
): (args: PresignArgs) => Promise<string> {
|
|
39
|
+
const host = `${opts.accountId}.r2.cloudflarestorage.com`
|
|
40
|
+
const region = "auto" // R2's SigV4 region convention.
|
|
41
|
+
const service = "s3"
|
|
40
42
|
|
|
41
43
|
return async ({ key, expiresIn }) => {
|
|
42
44
|
if (expiresIn < 1 || expiresIn > 604_800) {
|
|
43
|
-
throw new Error(`R2 presign: expiresIn must be 1..604800, got ${expiresIn}`)
|
|
45
|
+
throw new Error(`R2 presign: expiresIn must be 1..604800, got ${expiresIn}`)
|
|
44
46
|
}
|
|
45
|
-
const normalizedKey = key.replace(/^\/+/, "")
|
|
47
|
+
const normalizedKey = key.replace(/^\/+/, "")
|
|
46
48
|
const encodedKey = normalizedKey
|
|
47
49
|
.split("/")
|
|
48
50
|
.map((seg) => encodeURIComponent(seg))
|
|
49
|
-
.join("/")
|
|
51
|
+
.join("/")
|
|
50
52
|
|
|
51
|
-
const now = new Date()
|
|
52
|
-
const amzDate = toAmzDate(now)
|
|
53
|
-
const shortDate = amzDate.slice(0, 8)
|
|
54
|
-
const credentialScope = `${shortDate}/${region}/${service}/aws4_request
|
|
55
|
-
const credential = `${opts.accessKeyId}/${credentialScope}
|
|
53
|
+
const now = new Date()
|
|
54
|
+
const amzDate = toAmzDate(now)
|
|
55
|
+
const shortDate = amzDate.slice(0, 8)
|
|
56
|
+
const credentialScope = `${shortDate}/${region}/${service}/aws4_request`
|
|
57
|
+
const credential = `${opts.accessKeyId}/${credentialScope}`
|
|
56
58
|
|
|
57
59
|
const params: Array<[string, string]> = [
|
|
58
60
|
["X-Amz-Algorithm", "AWS4-HMAC-SHA256"],
|
|
@@ -60,11 +62,11 @@ export function createR2Presigner(opts: R2PresignerOptions): (args: PresignArgs)
|
|
|
60
62
|
["X-Amz-Date", amzDate],
|
|
61
63
|
["X-Amz-Expires", String(expiresIn)],
|
|
62
64
|
["X-Amz-SignedHeaders", "host"],
|
|
63
|
-
]
|
|
64
|
-
params.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
65
|
+
]
|
|
66
|
+
params.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
|
|
65
67
|
const canonicalQuery = params
|
|
66
68
|
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
|
67
|
-
.join("&")
|
|
69
|
+
.join("&")
|
|
68
70
|
|
|
69
71
|
const canonicalRequest = [
|
|
70
72
|
"GET",
|
|
@@ -73,62 +75,57 @@ export function createR2Presigner(opts: R2PresignerOptions): (args: PresignArgs)
|
|
|
73
75
|
`host:${host}\n`,
|
|
74
76
|
"host",
|
|
75
77
|
"UNSIGNED-PAYLOAD",
|
|
76
|
-
].join("\n")
|
|
78
|
+
].join("\n")
|
|
77
79
|
|
|
78
80
|
const stringToSign = [
|
|
79
81
|
"AWS4-HMAC-SHA256",
|
|
80
82
|
amzDate,
|
|
81
83
|
credentialScope,
|
|
82
84
|
await sha256Hex(canonicalRequest),
|
|
83
|
-
].join("\n")
|
|
84
|
-
|
|
85
|
-
const signingKey = await deriveSigningKey(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
);
|
|
91
|
-
const signature = toHex(await hmac(signingKey, stringToSign));
|
|
92
|
-
|
|
93
|
-
return `https://${host}/${opts.bucket}/${encodedKey}?${canonicalQuery}&X-Amz-Signature=${signature}`;
|
|
94
|
-
};
|
|
85
|
+
].join("\n")
|
|
86
|
+
|
|
87
|
+
const signingKey = await deriveSigningKey(opts.secretAccessKey, shortDate, region, service)
|
|
88
|
+
const signature = toHex(await hmac(signingKey, stringToSign))
|
|
89
|
+
|
|
90
|
+
return `https://${host}/${opts.bucket}/${encodedKey}?${canonicalQuery}&X-Amz-Signature=${signature}`
|
|
91
|
+
}
|
|
95
92
|
}
|
|
96
93
|
|
|
97
94
|
// ---- Crypto helpers ----
|
|
98
95
|
|
|
99
96
|
function toAmzDate(d: Date): string {
|
|
100
|
-
const yyyy = d.getUTCFullYear()
|
|
101
|
-
const mm = String(d.getUTCMonth() + 1).padStart(2, "0")
|
|
102
|
-
const dd = String(d.getUTCDate()).padStart(2, "0")
|
|
103
|
-
const hh = String(d.getUTCHours()).padStart(2, "0")
|
|
104
|
-
const mi = String(d.getUTCMinutes()).padStart(2, "0")
|
|
105
|
-
const ss = String(d.getUTCSeconds()).padStart(2, "0")
|
|
106
|
-
return `${yyyy}${mm}${dd}T${hh}${mi}${ss}Z
|
|
97
|
+
const yyyy = d.getUTCFullYear()
|
|
98
|
+
const mm = String(d.getUTCMonth() + 1).padStart(2, "0")
|
|
99
|
+
const dd = String(d.getUTCDate()).padStart(2, "0")
|
|
100
|
+
const hh = String(d.getUTCHours()).padStart(2, "0")
|
|
101
|
+
const mi = String(d.getUTCMinutes()).padStart(2, "0")
|
|
102
|
+
const ss = String(d.getUTCSeconds()).padStart(2, "0")
|
|
103
|
+
return `${yyyy}${mm}${dd}T${hh}${mi}${ss}Z`
|
|
107
104
|
}
|
|
108
105
|
|
|
109
106
|
function toHex(bytes: ArrayBuffer): string {
|
|
110
|
-
const arr = new Uint8Array(bytes)
|
|
111
|
-
let out = ""
|
|
112
|
-
for (const b of arr) out += b.toString(16).padStart(2, "0")
|
|
113
|
-
return out
|
|
107
|
+
const arr = new Uint8Array(bytes)
|
|
108
|
+
let out = ""
|
|
109
|
+
for (const b of arr) out += b.toString(16).padStart(2, "0")
|
|
110
|
+
return out
|
|
114
111
|
}
|
|
115
112
|
|
|
116
113
|
async function sha256Hex(input: string): Promise<string> {
|
|
117
|
-
const bytes = new TextEncoder().encode(input)
|
|
118
|
-
const digest = await crypto.subtle.digest("SHA-256", bytes)
|
|
119
|
-
return toHex(digest)
|
|
114
|
+
const bytes = new TextEncoder().encode(input)
|
|
115
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes)
|
|
116
|
+
return toHex(digest)
|
|
120
117
|
}
|
|
121
118
|
|
|
122
119
|
async function hmac(key: ArrayBuffer | Uint8Array, msg: string): Promise<ArrayBuffer> {
|
|
123
|
-
const keyBuf = key instanceof Uint8Array ? key.slice().buffer : key
|
|
120
|
+
const keyBuf = key instanceof Uint8Array ? key.slice().buffer : key
|
|
124
121
|
const cryptoKey = await crypto.subtle.importKey(
|
|
125
122
|
"raw",
|
|
126
123
|
keyBuf,
|
|
127
124
|
{ name: "HMAC", hash: "SHA-256" },
|
|
128
125
|
false,
|
|
129
126
|
["sign"],
|
|
130
|
-
)
|
|
131
|
-
return crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(msg))
|
|
127
|
+
)
|
|
128
|
+
return crypto.subtle.sign("HMAC", cryptoKey, new TextEncoder().encode(msg))
|
|
132
129
|
}
|
|
133
130
|
|
|
134
131
|
async function deriveSigningKey(
|
|
@@ -137,9 +134,9 @@ async function deriveSigningKey(
|
|
|
137
134
|
region: string,
|
|
138
135
|
service: string,
|
|
139
136
|
): Promise<ArrayBuffer> {
|
|
140
|
-
const kDate = await hmac(new TextEncoder().encode(`AWS4${secret}`), shortDate)
|
|
141
|
-
const kRegion = await hmac(kDate, region)
|
|
142
|
-
const kService = await hmac(kRegion, service)
|
|
143
|
-
const kSigning = await hmac(kService, "aws4_request")
|
|
144
|
-
return kSigning
|
|
137
|
+
const kDate = await hmac(new TextEncoder().encode(`AWS4${secret}`), shortDate)
|
|
138
|
+
const kRegion = await hmac(kDate, region)
|
|
139
|
+
const kService = await hmac(kRegion, service)
|
|
140
|
+
const kSigning = await hmac(kService, "aws4_request")
|
|
141
|
+
return kSigning
|
|
145
142
|
}
|
package/src/types.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// a hard dependency on `@cloudflare/workers-types` — matching the
|
|
3
3
|
// shape is enough, and tests can pass plain objects.
|
|
4
4
|
|
|
5
|
-
import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator"
|
|
5
|
+
import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator"
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Subset of Cloudflare's `DurableObjectStorage` we actually use.
|
|
@@ -15,16 +15,16 @@ import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator";
|
|
|
15
15
|
* due — see `handleDurableObjectAlarm`.
|
|
16
16
|
*/
|
|
17
17
|
export interface DurableObjectStorageLike {
|
|
18
|
-
get<T>(key: string): Promise<T | undefined
|
|
19
|
-
put<T>(key: string, value: T): Promise<void
|
|
20
|
-
delete(key: string): Promise<boolean
|
|
21
|
-
list<T>(options?: { prefix?: string; limit?: number }): Promise<Map<string, T
|
|
18
|
+
get<T>(key: string): Promise<T | undefined>
|
|
19
|
+
put<T>(key: string, value: T): Promise<void>
|
|
20
|
+
delete(key: string): Promise<boolean>
|
|
21
|
+
list<T>(options?: { prefix?: string; limit?: number }): Promise<Map<string, T>>
|
|
22
22
|
/** ms-since-epoch of the scheduled alarm, or null if none. */
|
|
23
|
-
getAlarm?(): Promise<number | null
|
|
23
|
+
getAlarm?(): Promise<number | null>
|
|
24
24
|
/** Schedule the DO's alarm() method to fire at `wakeAt`. */
|
|
25
|
-
setAlarm?(wakeAt: number): Promise<void
|
|
25
|
+
setAlarm?(wakeAt: number): Promise<void>
|
|
26
26
|
/** Cancel any pending alarm. */
|
|
27
|
-
deleteAlarm?(): Promise<void
|
|
27
|
+
deleteAlarm?(): Promise<void>
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/**
|
|
@@ -33,42 +33,45 @@ export interface DurableObjectStorageLike {
|
|
|
33
33
|
* registered under that name.
|
|
34
34
|
*/
|
|
35
35
|
export interface DispatchNamespaceLike {
|
|
36
|
-
get(
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
get(
|
|
37
|
+
name: string,
|
|
38
|
+
args?: Record<string, unknown>,
|
|
39
|
+
): {
|
|
40
|
+
fetch(request: Request): Promise<Response>
|
|
41
|
+
}
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/** Args the Worker passes when routing to a run DO. */
|
|
42
45
|
export interface RunOperation {
|
|
43
|
-
op: "trigger" | "resume" | "cancel" | "get"
|
|
44
|
-
payload?: unknown
|
|
46
|
+
op: "trigger" | "resume" | "cancel" | "get"
|
|
47
|
+
payload?: unknown
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
/** Injection payload on resume ops. */
|
|
48
51
|
export interface ResumePayload {
|
|
49
|
-
injection: WaitpointInjection
|
|
52
|
+
injection: WaitpointInjection
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
/** Trigger payload. */
|
|
53
56
|
export interface TriggerPayload {
|
|
54
|
-
workflowId: string
|
|
55
|
-
workflowVersion: string
|
|
56
|
-
input: unknown
|
|
57
|
+
workflowId: string
|
|
58
|
+
workflowVersion: string
|
|
59
|
+
input: unknown
|
|
57
60
|
tenantMeta: {
|
|
58
|
-
tenantId: string
|
|
59
|
-
projectId: string
|
|
60
|
-
organizationId: string
|
|
61
|
+
tenantId: string
|
|
62
|
+
projectId: string
|
|
63
|
+
organizationId: string
|
|
61
64
|
/** Dispatch-namespace name to forward step requests to. */
|
|
62
|
-
tenantScript: string
|
|
63
|
-
}
|
|
64
|
-
environment?: "production" | "preview" | "development"
|
|
65
|
-
tags?: string[]
|
|
66
|
-
runId?: string
|
|
65
|
+
tenantScript: string
|
|
66
|
+
}
|
|
67
|
+
environment?: "production" | "preview" | "development"
|
|
68
|
+
tags?: string[]
|
|
69
|
+
runId?: string
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
/** Cancel payload. */
|
|
70
73
|
export interface CancelPayload {
|
|
71
|
-
reason?: string
|
|
74
|
+
reason?: string
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
/**
|
|
@@ -77,7 +80,7 @@ export interface CancelPayload {
|
|
|
77
80
|
*/
|
|
78
81
|
export interface AdapterEnv<DONamespace = unknown, DispatchNS = DispatchNamespaceLike> {
|
|
79
82
|
/** Durable Object namespace holding one DO per run. Typed loosely to avoid a CF types dep. */
|
|
80
|
-
WORKFLOW_RUN_DO: DONamespace
|
|
83
|
+
WORKFLOW_RUN_DO: DONamespace
|
|
81
84
|
/** Dispatch namespace containing tenant Workers. */
|
|
82
|
-
DISPATCHER: DispatchNS
|
|
85
|
+
DISPATCHER: DispatchNS
|
|
83
86
|
}
|
package/src/worker.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// POST /api/runs/:id/tokens/:token → inject a MANUAL (token) waitpoint
|
|
12
12
|
// POST /api/runs/:id/cancel → cancel a run
|
|
13
13
|
|
|
14
|
-
import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator"
|
|
14
|
+
import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator"
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* Minimal shape of a DO namespace. `idFromName` returns an opaque id;
|
|
@@ -19,80 +19,80 @@ import type { WaitpointInjection } from "@voyantjs/workflows-orchestrator";
|
|
|
19
19
|
* Typed loosely so tests can pass any matching object.
|
|
20
20
|
*/
|
|
21
21
|
export interface DurableObjectNamespaceLike<Id = unknown> {
|
|
22
|
-
idFromName(name: string): Id
|
|
23
|
-
get(id: Id): { fetch(req: Request): Promise<Response> }
|
|
22
|
+
idFromName(name: string): Id
|
|
23
|
+
get(id: Id): { fetch(req: Request): Promise<Response> }
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export interface WorkerFetchDeps<Id = unknown> {
|
|
27
|
-
runDO: DurableObjectNamespaceLike<Id
|
|
27
|
+
runDO: DurableObjectNamespaceLike<Id>
|
|
28
28
|
/**
|
|
29
29
|
* Called before any routing. Throws/rejects to reject the request.
|
|
30
30
|
* Typical implementation validates a tenant access token.
|
|
31
31
|
*/
|
|
32
|
-
verifyRequest?: (req: Request) => void | Promise<void
|
|
32
|
+
verifyRequest?: (req: Request) => void | Promise<void>
|
|
33
33
|
/** Optional logger. */
|
|
34
|
-
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void
|
|
34
|
+
logger?: (level: "info" | "warn" | "error", msg: string, data?: object) => void
|
|
35
35
|
/** id generator for new triggers; defaults to `run_<random>`. */
|
|
36
|
-
idGenerator?: () => string
|
|
36
|
+
idGenerator?: () => string
|
|
37
37
|
/** Injectable clock for id generation. */
|
|
38
|
-
now?: () => number
|
|
38
|
+
now?: () => number
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export async function handleWorkerRequest<Id>(
|
|
42
42
|
req: Request,
|
|
43
43
|
deps: WorkerFetchDeps<Id>,
|
|
44
44
|
): Promise<Response> {
|
|
45
|
-
const url = new URL(req.url)
|
|
45
|
+
const url = new URL(req.url)
|
|
46
46
|
|
|
47
47
|
if (req.method === "OPTIONS") {
|
|
48
48
|
return new Response(null, {
|
|
49
49
|
status: 204,
|
|
50
50
|
headers: corsHeaders("GET,POST,OPTIONS"),
|
|
51
|
-
})
|
|
51
|
+
})
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
try {
|
|
55
|
-
if (deps.verifyRequest) await deps.verifyRequest(req)
|
|
55
|
+
if (deps.verifyRequest) await deps.verifyRequest(req)
|
|
56
56
|
} catch (err) {
|
|
57
57
|
return json(401, {
|
|
58
58
|
error: "unauthorized",
|
|
59
59
|
message: err instanceof Error ? err.message : String(err),
|
|
60
|
-
})
|
|
60
|
+
})
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
// POST /api/runs — trigger a new run.
|
|
64
64
|
if (req.method === "POST" && url.pathname === "/api/runs") {
|
|
65
|
-
let payload: Record<string, unknown
|
|
65
|
+
let payload: Record<string, unknown>
|
|
66
66
|
try {
|
|
67
|
-
payload = (await req.json()) as Record<string, unknown
|
|
67
|
+
payload = (await req.json()) as Record<string, unknown>
|
|
68
68
|
} catch (err) {
|
|
69
|
-
return json(400, { error: "invalid_json", message: errMsg(err) })
|
|
69
|
+
return json(400, { error: "invalid_json", message: errMsg(err) })
|
|
70
70
|
}
|
|
71
|
-
const runId = typeof payload.runId === "string" ? payload.runId : defaultRunId(deps)
|
|
71
|
+
const runId = typeof payload.runId === "string" ? payload.runId : defaultRunId(deps)
|
|
72
72
|
const forward = new Request(`https://do-internal/trigger`, {
|
|
73
73
|
method: "POST",
|
|
74
74
|
headers: { "content-type": "application/json" },
|
|
75
75
|
body: JSON.stringify({ ...payload, runId }),
|
|
76
|
-
})
|
|
77
|
-
return forwardToRunDO(runId, forward, deps)
|
|
76
|
+
})
|
|
77
|
+
return forwardToRunDO(runId, forward, deps)
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
// Everything below operates on a specific runId.
|
|
81
|
-
const runMatch = url.pathname.match(/^\/api\/runs\/([^/]+)(\/.+)?$/)
|
|
81
|
+
const runMatch = url.pathname.match(/^\/api\/runs\/([^/]+)(\/.+)?$/)
|
|
82
82
|
if (!runMatch) {
|
|
83
|
-
return json(404, { error: "route_not_found", path: url.pathname })
|
|
83
|
+
return json(404, { error: "route_not_found", path: url.pathname })
|
|
84
84
|
}
|
|
85
|
-
const runId = decodeURIComponent(runMatch[1]!)
|
|
86
|
-
const tail = runMatch[2] ?? ""
|
|
85
|
+
const runId = decodeURIComponent(runMatch[1]!)
|
|
86
|
+
const tail = runMatch[2] ?? ""
|
|
87
87
|
|
|
88
88
|
if (req.method === "GET" && tail === "") {
|
|
89
|
-
const forward = new Request(`https://do-internal/get`, { method: "GET" })
|
|
90
|
-
return forwardToRunDO(runId, forward, deps)
|
|
89
|
+
const forward = new Request(`https://do-internal/get`, { method: "GET" })
|
|
90
|
+
return forwardToRunDO(runId, forward, deps)
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
if (req.method === "POST" && tail === "/cancel") {
|
|
94
|
-
const body = await safeJson(req)
|
|
95
|
-
if (isErrorBody(body)) return json(400, body)
|
|
94
|
+
const body = await safeJson(req)
|
|
95
|
+
if (isErrorBody(body)) return json(400, body)
|
|
96
96
|
return forwardToRunDO(
|
|
97
97
|
runId,
|
|
98
98
|
new Request(`https://do-internal/cancel`, {
|
|
@@ -101,14 +101,14 @@ export async function handleWorkerRequest<Id>(
|
|
|
101
101
|
body: JSON.stringify(body),
|
|
102
102
|
}),
|
|
103
103
|
deps,
|
|
104
|
-
)
|
|
104
|
+
)
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// Waitpoint injections: events, signals, tokens.
|
|
108
|
-
const body = await safeJson(req)
|
|
109
|
-
if (isErrorBody(body)) return json(400, body)
|
|
110
|
-
const injection = parseInjection(tail, body)
|
|
111
|
-
if ("error" in injection) return json(400, injection)
|
|
108
|
+
const body = await safeJson(req)
|
|
109
|
+
if (isErrorBody(body)) return json(400, body)
|
|
110
|
+
const injection = parseInjection(tail, body)
|
|
111
|
+
if ("error" in injection) return json(400, injection)
|
|
112
112
|
return forwardToRunDO(
|
|
113
113
|
runId,
|
|
114
114
|
new Request(`https://do-internal/resume`, {
|
|
@@ -117,13 +117,13 @@ export async function handleWorkerRequest<Id>(
|
|
|
117
117
|
body: JSON.stringify({ injection: injection.injection }),
|
|
118
118
|
}),
|
|
119
119
|
deps,
|
|
120
|
-
)
|
|
120
|
+
)
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
function isErrorBody(
|
|
124
124
|
body: Record<string, unknown> | { error: string; message: string },
|
|
125
125
|
): body is { error: string; message: string } {
|
|
126
|
-
return typeof (body as { error?: unknown }).error === "string"
|
|
126
|
+
return typeof (body as { error?: unknown }).error === "string"
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
function parseInjection(
|
|
@@ -132,19 +132,19 @@ function parseInjection(
|
|
|
132
132
|
): { injection: WaitpointInjection } | { error: string; message: string } {
|
|
133
133
|
if (tail === "/events") {
|
|
134
134
|
if (typeof body.eventType !== "string" || body.eventType.length === 0) {
|
|
135
|
-
return { error: "invalid_body", message: "`eventType` (string) is required" }
|
|
135
|
+
return { error: "invalid_body", message: "`eventType` (string) is required" }
|
|
136
136
|
}
|
|
137
137
|
return {
|
|
138
138
|
injection: { kind: "EVENT", eventType: body.eventType, payload: body.payload },
|
|
139
|
-
}
|
|
139
|
+
}
|
|
140
140
|
}
|
|
141
141
|
if (tail === "/signals") {
|
|
142
142
|
if (typeof body.name !== "string" || body.name.length === 0) {
|
|
143
|
-
return { error: "invalid_body", message: "`name` (string) is required" }
|
|
143
|
+
return { error: "invalid_body", message: "`name` (string) is required" }
|
|
144
144
|
}
|
|
145
|
-
return { injection: { kind: "SIGNAL", name: body.name, payload: body.payload } }
|
|
145
|
+
return { injection: { kind: "SIGNAL", name: body.name, payload: body.payload } }
|
|
146
146
|
}
|
|
147
|
-
const tokenMatch = tail.match(/^\/tokens\/([^/]+)$/)
|
|
147
|
+
const tokenMatch = tail.match(/^\/tokens\/([^/]+)$/)
|
|
148
148
|
if (tokenMatch) {
|
|
149
149
|
return {
|
|
150
150
|
injection: {
|
|
@@ -152,9 +152,9 @@ function parseInjection(
|
|
|
152
152
|
tokenId: decodeURIComponent(tokenMatch[1]!),
|
|
153
153
|
payload: body.payload,
|
|
154
154
|
},
|
|
155
|
-
}
|
|
155
|
+
}
|
|
156
156
|
}
|
|
157
|
-
return { error: "route_not_found", message: `unknown path suffix ${tail}` }
|
|
157
|
+
return { error: "route_not_found", message: `unknown path suffix ${tail}` }
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
async function forwardToRunDO<Id>(
|
|
@@ -162,36 +162,38 @@ async function forwardToRunDO<Id>(
|
|
|
162
162
|
req: Request,
|
|
163
163
|
deps: WorkerFetchDeps<Id>,
|
|
164
164
|
): Promise<Response> {
|
|
165
|
-
const id = deps.runDO.idFromName(runId)
|
|
166
|
-
const stub = deps.runDO.get(id)
|
|
167
|
-
const resp = await stub.fetch(req)
|
|
165
|
+
const id = deps.runDO.idFromName(runId)
|
|
166
|
+
const stub = deps.runDO.get(id)
|
|
167
|
+
const resp = await stub.fetch(req)
|
|
168
168
|
// Add CORS on outbound responses.
|
|
169
|
-
const out = new Response(resp.body, resp)
|
|
169
|
+
const out = new Response(resp.body, resp)
|
|
170
170
|
for (const [k, v] of Object.entries(corsHeaders("GET,POST,OPTIONS"))) {
|
|
171
|
-
out.headers.set(k, v)
|
|
171
|
+
out.headers.set(k, v)
|
|
172
172
|
}
|
|
173
|
-
return out
|
|
173
|
+
return out
|
|
174
174
|
}
|
|
175
175
|
|
|
176
176
|
function defaultRunId<Id>(deps: WorkerFetchDeps<Id>): string {
|
|
177
|
-
if (deps.idGenerator) return deps.idGenerator()
|
|
178
|
-
const now = deps.now ?? (() => Date.now())
|
|
179
|
-
const ts = now().toString(36)
|
|
180
|
-
const rand = Math.floor(Math.random() * 1_000_000)
|
|
181
|
-
|
|
177
|
+
if (deps.idGenerator) return deps.idGenerator()
|
|
178
|
+
const now = deps.now ?? (() => Date.now())
|
|
179
|
+
const ts = now().toString(36)
|
|
180
|
+
const rand = Math.floor(Math.random() * 1_000_000)
|
|
181
|
+
.toString(36)
|
|
182
|
+
.padStart(4, "0")
|
|
183
|
+
return `run_${ts}_${rand}`
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
async function safeJson(
|
|
185
187
|
req: Request,
|
|
186
188
|
): Promise<Record<string, unknown> | { error: string; message: string }> {
|
|
187
189
|
// Some requests are bodyless (GET). Only parse when we have a body.
|
|
188
|
-
if (req.method === "GET" || req.method === "HEAD") return {}
|
|
189
|
-
const text = await req.text()
|
|
190
|
-
if (text.length === 0) return {}
|
|
190
|
+
if (req.method === "GET" || req.method === "HEAD") return {}
|
|
191
|
+
const text = await req.text()
|
|
192
|
+
if (text.length === 0) return {}
|
|
191
193
|
try {
|
|
192
|
-
return JSON.parse(text) as Record<string, unknown
|
|
194
|
+
return JSON.parse(text) as Record<string, unknown>
|
|
193
195
|
} catch (err) {
|
|
194
|
-
return { error: "invalid_json", message: errMsg(err) }
|
|
196
|
+
return { error: "invalid_json", message: errMsg(err) }
|
|
195
197
|
}
|
|
196
198
|
}
|
|
197
199
|
|
|
@@ -200,7 +202,7 @@ function corsHeaders(methods: string): Record<string, string> {
|
|
|
200
202
|
"access-control-allow-origin": "*",
|
|
201
203
|
"access-control-allow-methods": methods,
|
|
202
204
|
"access-control-allow-headers": "content-type, x-voyant-protocol",
|
|
203
|
-
}
|
|
205
|
+
}
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
function json(status: number, body: unknown): Response {
|
|
@@ -210,9 +212,9 @@ function json(status: number, body: unknown): Response {
|
|
|
210
212
|
"content-type": "application/json; charset=utf-8",
|
|
211
213
|
...corsHeaders("GET,POST,OPTIONS"),
|
|
212
214
|
},
|
|
213
|
-
})
|
|
215
|
+
})
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
function errMsg(err: unknown): string {
|
|
217
|
-
return err instanceof Error ? err.message : String(err)
|
|
219
|
+
return err instanceof Error ? err.message : String(err)
|
|
218
220
|
}
|