effect-inngest 0.1.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/LICENSE +21 -0
- package/README.md +457 -0
- package/dist/Client.d.ts +167 -0
- package/dist/Client.js +144 -0
- package/dist/Events.d.ts +110 -0
- package/dist/Events.js +93 -0
- package/dist/Function.d.ts +384 -0
- package/dist/Function.js +104 -0
- package/dist/Group.d.ts +152 -0
- package/dist/Group.js +164 -0
- package/dist/HttpApi.d.ts +75 -0
- package/dist/HttpApi.js +47 -0
- package/dist/_virtual/rolldown_runtime.js +18 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +8 -0
- package/dist/internal/constants.js +15 -0
- package/dist/internal/driver.d.ts +5 -0
- package/dist/internal/driver.js +117 -0
- package/dist/internal/errors.d.ts +56 -0
- package/dist/internal/errors.js +61 -0
- package/dist/internal/handler.d.ts +20 -0
- package/dist/internal/handler.js +145 -0
- package/dist/internal/helpers.js +44 -0
- package/dist/internal/interrupts.d.ts +2 -0
- package/dist/internal/interrupts.js +45 -0
- package/dist/internal/memo.js +56 -0
- package/dist/internal/protocol.d.ts +1 -0
- package/dist/internal/protocol.js +191 -0
- package/dist/internal/signature.d.ts +18 -0
- package/dist/internal/signature.js +97 -0
- package/dist/internal/step.d.ts +59 -0
- package/dist/internal/step.js +183 -0
- package/package.json +121 -0
- package/src/Client.ts +279 -0
- package/src/Events.ts +87 -0
- package/src/Function.ts +493 -0
- package/src/Group.ts +314 -0
- package/src/HttpApi.ts +82 -0
- package/src/index.ts +171 -0
- package/src/internal/constants.ts +11 -0
- package/src/internal/driver.ts +194 -0
- package/src/internal/errors.ts +130 -0
- package/src/internal/handler.ts +222 -0
- package/src/internal/helpers.ts +58 -0
- package/src/internal/interrupts.ts +62 -0
- package/src/internal/memo.ts +73 -0
- package/src/internal/protocol.ts +218 -0
- package/src/internal/signature.ts +158 -0
- package/src/internal/step.ts +377 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { Headers, RegisterResponse, SDKRequestBody, SDKRequestContext, UserError } from "./protocol.js";
|
|
2
|
+
import { InngestClient } from "../Client.js";
|
|
3
|
+
import { Signature, SignatureError } from "./signature.js";
|
|
4
|
+
import { execute } from "./driver.js";
|
|
5
|
+
import * as HttpClient from "@effect/platform/HttpClient";
|
|
6
|
+
import "@effect/platform/HttpServerRequest";
|
|
7
|
+
import "effect/Context";
|
|
8
|
+
import * as Effect from "effect/Effect";
|
|
9
|
+
import * as Schema from "effect/Schema";
|
|
10
|
+
import * as HttpClientRequest from "@effect/platform/HttpClientRequest";
|
|
11
|
+
import * as HttpClientResponse from "@effect/platform/HttpClientResponse";
|
|
12
|
+
import * as Predicate from "effect/Predicate";
|
|
13
|
+
|
|
14
|
+
//#region src/internal/handler.ts
|
|
15
|
+
/**
|
|
16
|
+
* Internal handler implementation.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
var InvalidRequestError = class extends Schema.TaggedError()("InvalidRequestError", { message: Schema.String }) {};
|
|
20
|
+
const SDK_VERSION = "2.0.0";
|
|
21
|
+
const baseHeaders = () => ({
|
|
22
|
+
"Content-Type": "application/json",
|
|
23
|
+
[Headers.SDK]: `effect-inngest:v${SDK_VERSION}`,
|
|
24
|
+
[Headers.RequestVersion]: "1"
|
|
25
|
+
});
|
|
26
|
+
const buildServeUrl = (requestUrl, serveHost, servePath) => {
|
|
27
|
+
const url = new URL(requestUrl);
|
|
28
|
+
if (servePath) url.pathname = servePath;
|
|
29
|
+
if (serveHost) return new URL(url.pathname + url.search, serveHost);
|
|
30
|
+
return url;
|
|
31
|
+
};
|
|
32
|
+
const verifyAndParseRequestBody = (request) => Effect.gen(function* () {
|
|
33
|
+
const client = yield* InngestClient;
|
|
34
|
+
const sig = yield* Signature;
|
|
35
|
+
const config = client.config;
|
|
36
|
+
const isDev = client.mode === "dev";
|
|
37
|
+
const bodyText = yield* request.text.pipe(Effect.mapError((error) => new InvalidRequestError({ message: `Failed to read request body: ${String(error)}` })));
|
|
38
|
+
yield* sig.verify({
|
|
39
|
+
body: new TextEncoder().encode(bodyText),
|
|
40
|
+
signatureHeader: request.headers[Headers.Signature.toLowerCase()],
|
|
41
|
+
signingKey: config.signingKey,
|
|
42
|
+
signingKeyFallback: config.signingKeyFallback,
|
|
43
|
+
isDev
|
|
44
|
+
});
|
|
45
|
+
return yield* Schema.decodeUnknown(Schema.parseJson(SDKRequestBody))(bodyText).pipe(Effect.mapError((error) => new InvalidRequestError({ message: `Invalid request body: ${String(error)}` })));
|
|
46
|
+
});
|
|
47
|
+
const handleIntrospection = (group, _requestUrl) => Effect.gen(function* () {
|
|
48
|
+
const client = yield* InngestClient;
|
|
49
|
+
const config = client.config;
|
|
50
|
+
const body = {
|
|
51
|
+
function_count: group.functions.size,
|
|
52
|
+
has_event_key: config.eventKey !== void 0,
|
|
53
|
+
has_signing_key: config.signingKey !== void 0,
|
|
54
|
+
has_signing_key_fallback: config.signingKeyFallback !== void 0,
|
|
55
|
+
mode: client.mode === "dev" ? "dev" : "cloud",
|
|
56
|
+
schema_version: "2024-05-24",
|
|
57
|
+
authentication_succeeded: null
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
status: 200,
|
|
61
|
+
headers: baseHeaders(),
|
|
62
|
+
body
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
const RegisterRequest = Schema.Struct({
|
|
66
|
+
url: Schema.String,
|
|
67
|
+
v: Schema.String,
|
|
68
|
+
deployType: Schema.Literal("ping"),
|
|
69
|
+
sdk: Schema.String,
|
|
70
|
+
appName: Schema.String,
|
|
71
|
+
framework: Schema.String,
|
|
72
|
+
functions: Schema.Array(Schema.Unknown)
|
|
73
|
+
});
|
|
74
|
+
const handleRegistration = (group, requestUrl) => Effect.gen(function* () {
|
|
75
|
+
const client = yield* InngestClient;
|
|
76
|
+
const httpClient = yield* HttpClient.HttpClient;
|
|
77
|
+
const config = client.config;
|
|
78
|
+
const url = buildServeUrl(requestUrl, config.serveHost, config.servePath);
|
|
79
|
+
const functions = Array.from(group.functions.values()).map((fn) => fn.toRegistration({
|
|
80
|
+
appId: config.id,
|
|
81
|
+
url: url.href
|
|
82
|
+
}));
|
|
83
|
+
const registerUrl = new URL("fn/register", client.apiBaseUrl).toString();
|
|
84
|
+
const response = yield* HttpClientRequest.post(registerUrl).pipe(HttpClientRequest.setHeaders({
|
|
85
|
+
...baseHeaders(),
|
|
86
|
+
Authorization: `Bearer ${config.signingKey ?? ""}`
|
|
87
|
+
}), HttpClientRequest.schemaBodyJson(RegisterRequest)({
|
|
88
|
+
url: url.href,
|
|
89
|
+
v: "0.1",
|
|
90
|
+
deployType: "ping",
|
|
91
|
+
sdk: `effect-inngest:v${SDK_VERSION}`,
|
|
92
|
+
appName: config.id,
|
|
93
|
+
framework: "effect",
|
|
94
|
+
functions
|
|
95
|
+
})).pipe(Effect.flatMap(httpClient.execute), Effect.flatMap(HttpClientResponse.schemaBodyJson(RegisterResponse)), Effect.scoped, Effect.catchAll((error) => Effect.succeed({ message: `Registration failed: ${Predicate.hasProperty(error, "message") ? error.message : "Unknown error"}` })));
|
|
96
|
+
return {
|
|
97
|
+
status: 200,
|
|
98
|
+
headers: baseHeaders(),
|
|
99
|
+
body: response
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
const handleExecution = (group, fnId, urlStepId, body, traceHeaders = {}) => Effect.gen(function* () {
|
|
103
|
+
const client = yield* InngestClient;
|
|
104
|
+
const context = yield* Effect.context();
|
|
105
|
+
const appId = client.config.id;
|
|
106
|
+
const prefix = `${appId}-`;
|
|
107
|
+
const fnTag = fnId.startsWith(prefix) ? fnId.slice(prefix.length) : fnId;
|
|
108
|
+
const fn = group.functions.get(fnTag);
|
|
109
|
+
const entry = fn ? context.unsafeMap.get(fn.key) : void 0;
|
|
110
|
+
if (!fn || !entry) return {
|
|
111
|
+
status: 404,
|
|
112
|
+
headers: {
|
|
113
|
+
...baseHeaders(),
|
|
114
|
+
[Headers.NoRetry]: "true"
|
|
115
|
+
},
|
|
116
|
+
body: { error: UserError.make({
|
|
117
|
+
name: "FunctionNotFoundError",
|
|
118
|
+
message: `Unknown function: ${fnId}`
|
|
119
|
+
}) }
|
|
120
|
+
};
|
|
121
|
+
const effectiveBody = urlStepId && urlStepId !== body.ctx.step_id ? SDKRequestBody.make({
|
|
122
|
+
event: body.event,
|
|
123
|
+
events: body.events,
|
|
124
|
+
steps: body.steps,
|
|
125
|
+
ctx: SDKRequestContext.make({
|
|
126
|
+
fn_id: body.ctx.fn_id,
|
|
127
|
+
run_id: body.ctx.run_id,
|
|
128
|
+
step_id: urlStepId,
|
|
129
|
+
attempt: body.ctx.attempt,
|
|
130
|
+
disable_immediate_execution: body.ctx.disable_immediate_execution,
|
|
131
|
+
use_api: body.ctx.use_api,
|
|
132
|
+
stack: body.ctx.stack
|
|
133
|
+
}),
|
|
134
|
+
use_api: body.use_api
|
|
135
|
+
}) : body;
|
|
136
|
+
const result = yield* Effect.provide(execute(fn, entry.handler, effectiveBody, appId, traceHeaders), entry.context);
|
|
137
|
+
return {
|
|
138
|
+
status: result.status,
|
|
139
|
+
headers: result.headers,
|
|
140
|
+
body: result.body
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
//#endregion
|
|
145
|
+
export { InvalidRequestError, handleExecution, handleIntrospection, handleRegistration, verifyAndParseRequestBody };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as Duration from "effect/Duration";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/helpers.ts
|
|
4
|
+
/**
|
|
5
|
+
* Internal helper utilities.
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
const second = 1 * 1e3;
|
|
9
|
+
const minute = second * 60;
|
|
10
|
+
const hour = minute * 60;
|
|
11
|
+
const day = hour * 24;
|
|
12
|
+
const periods = [
|
|
13
|
+
["w", day * 7],
|
|
14
|
+
["d", day],
|
|
15
|
+
["h", hour],
|
|
16
|
+
["m", minute],
|
|
17
|
+
["s", second]
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Convert a Duration to an Inngest-compatible time string (e.g. `"1d"` or `"2h30m"`).
|
|
21
|
+
*
|
|
22
|
+
* Supports weeks, days, hours, minutes, and seconds. Years/months are converted
|
|
23
|
+
* to their equivalent in weeks/days.
|
|
24
|
+
*/
|
|
25
|
+
const timeStr = (input) => {
|
|
26
|
+
let ms = Duration.toMillis(Duration.decode(input));
|
|
27
|
+
const [, result] = periods.reduce(([num, str], [suffix, period]) => {
|
|
28
|
+
const numPeriods = Math.floor(num / period);
|
|
29
|
+
if (numPeriods > 0) return [num % period, `${str}${numPeriods}${suffix}`];
|
|
30
|
+
return [num, str];
|
|
31
|
+
}, [ms, ""]);
|
|
32
|
+
return result || "0s";
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Format a timestamp as an ISO string for Inngest's sleepUntil.
|
|
36
|
+
*/
|
|
37
|
+
const formatTimestamp = (timestamp) => {
|
|
38
|
+
if (timestamp instanceof Date) return timestamp.toISOString();
|
|
39
|
+
if (typeof timestamp === "number") return new Date(timestamp).toISOString();
|
|
40
|
+
return timestamp;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
//#endregion
|
|
44
|
+
export { formatTimestamp, timeStr };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { GeneratorOpcode, UserError, invokeFunction, sleep, stepError, stepPlanned, stepRun, waitForEvent } from "./protocol.js";
|
|
2
|
+
import * as Schema from "effect/Schema";
|
|
3
|
+
import * as Predicate from "effect/Predicate";
|
|
4
|
+
|
|
5
|
+
//#region src/internal/interrupts.ts
|
|
6
|
+
/**
|
|
7
|
+
* StepInterrupt schema and factory functions.
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
10
|
+
/** @internal */
|
|
11
|
+
var StepInterrupt = class extends Schema.TaggedClass()("StepInterrupt", {
|
|
12
|
+
opcode: GeneratorOpcode,
|
|
13
|
+
retryAfterMs: Schema.optional(Schema.Number)
|
|
14
|
+
}) {};
|
|
15
|
+
const toUserError = (error) => UserError.make({
|
|
16
|
+
name: Predicate.hasProperty(error, "name") ? String(error.name) : "Error",
|
|
17
|
+
message: Predicate.hasProperty(error, "message") ? String(error.message) : String(error),
|
|
18
|
+
stack: Predicate.hasProperty(error, "stack") ? String(error.stack) : void 0
|
|
19
|
+
});
|
|
20
|
+
/** @internal */
|
|
21
|
+
const sleepInterrupt = (opts) => StepInterrupt.make({ opcode: sleep(opts.info, opts.duration) });
|
|
22
|
+
/** @internal */
|
|
23
|
+
const waitForEventInterrupt = (opts) => StepInterrupt.make({ opcode: waitForEvent(opts.info, {
|
|
24
|
+
event: opts.event,
|
|
25
|
+
timeout: opts.timeout,
|
|
26
|
+
if: opts.if
|
|
27
|
+
}) });
|
|
28
|
+
/** @internal */
|
|
29
|
+
const invokeInterrupt = (opts) => StepInterrupt.make({ opcode: invokeFunction(opts.info, {
|
|
30
|
+
function_id: opts.functionId,
|
|
31
|
+
payload: opts.payload,
|
|
32
|
+
timeout: opts.timeout
|
|
33
|
+
}) });
|
|
34
|
+
/** @internal */
|
|
35
|
+
const plannedInterrupt = (opts) => StepInterrupt.make({ opcode: stepPlanned(opts.info) });
|
|
36
|
+
/** @internal */
|
|
37
|
+
const runInterrupt = (opts) => StepInterrupt.make({ opcode: stepRun(opts.info, opts.data) });
|
|
38
|
+
/** @internal */
|
|
39
|
+
const errorInterrupt = (opts) => StepInterrupt.make({
|
|
40
|
+
opcode: stepError(opts.info, toUserError(opts.error), opts.noRetry),
|
|
41
|
+
retryAfterMs: opts.retryAfterMs
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
export { StepInterrupt, errorInterrupt, invokeInterrupt, plannedInterrupt, runInterrupt, sleepInterrupt, waitForEventInterrupt };
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import * as Schema from "effect/Schema";
|
|
2
|
+
import * as Predicate from "effect/Predicate";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/memo.ts
|
|
5
|
+
/**
|
|
6
|
+
* Step memoization schemas for decoding cached step results.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
const hasKey = (key) => (u) => Predicate.isRecord(u) && Predicate.hasProperty(u, key);
|
|
10
|
+
const MemoDataSchema = Schema.TaggedStruct("MemoData", { data: Schema.Unknown });
|
|
11
|
+
const MemoErrorSchema = Schema.TaggedStruct("MemoError", { error: Schema.Unknown });
|
|
12
|
+
const MemoInputSchema = Schema.TaggedStruct("MemoInput", { input: Schema.Unknown });
|
|
13
|
+
const MemoTimeoutSchema = Schema.TaggedStruct("MemoTimeout", {});
|
|
14
|
+
const MemoNoneSchema = Schema.TaggedStruct("MemoNone", {});
|
|
15
|
+
const DataWire = Schema.Unknown.pipe(Schema.filter(hasKey("data")), Schema.transform(MemoDataSchema, {
|
|
16
|
+
decode: (v) => ({
|
|
17
|
+
_tag: "MemoData",
|
|
18
|
+
data: v.data
|
|
19
|
+
}),
|
|
20
|
+
encode: ({ data }) => ({ data })
|
|
21
|
+
}));
|
|
22
|
+
const ErrorWire = Schema.Unknown.pipe(Schema.filter(hasKey("error")), Schema.transform(MemoErrorSchema, {
|
|
23
|
+
decode: (v) => ({
|
|
24
|
+
_tag: "MemoError",
|
|
25
|
+
error: v.error
|
|
26
|
+
}),
|
|
27
|
+
encode: ({ error }) => ({ error })
|
|
28
|
+
}));
|
|
29
|
+
const InputWire = Schema.Unknown.pipe(Schema.filter(hasKey("input")), Schema.transform(MemoInputSchema, {
|
|
30
|
+
decode: (v) => ({
|
|
31
|
+
_tag: "MemoInput",
|
|
32
|
+
input: v.input
|
|
33
|
+
}),
|
|
34
|
+
encode: ({ input }) => ({ input })
|
|
35
|
+
}));
|
|
36
|
+
const TimeoutWire = Schema.transform(Schema.Null, MemoTimeoutSchema, {
|
|
37
|
+
decode: () => ({ _tag: "MemoTimeout" }),
|
|
38
|
+
encode: () => null
|
|
39
|
+
});
|
|
40
|
+
const NoneWire = Schema.transform(Schema.Undefined, MemoNoneSchema, {
|
|
41
|
+
decode: () => ({ _tag: "MemoNone" }),
|
|
42
|
+
encode: () => void 0
|
|
43
|
+
});
|
|
44
|
+
const MemoSchema = Schema.Union(ErrorWire, InputWire, DataWire, TimeoutWire, NoneWire);
|
|
45
|
+
/**
|
|
46
|
+
* Decode a step result into a Memo type.
|
|
47
|
+
* Order matters: error > input > data (more specific properties first).
|
|
48
|
+
*/
|
|
49
|
+
const decodeMemo = (value) => {
|
|
50
|
+
const result = Schema.decodeUnknownOption(MemoSchema)(value);
|
|
51
|
+
if (result._tag === "Some") return result.value;
|
|
52
|
+
return MemoNoneSchema.make();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
//#endregion
|
|
56
|
+
export { decodeMemo };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "effect/Schema";
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { Predicate, Struct } from "effect";
|
|
2
|
+
import * as Schema$1 from "effect/Schema";
|
|
3
|
+
|
|
4
|
+
//#region src/internal/protocol.ts
|
|
5
|
+
/**
|
|
6
|
+
* Wire protocol schemas and opcode factories for Inngest communication.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
const stripTags = (value) => {
|
|
10
|
+
if (Predicate.isRecord(value)) {
|
|
11
|
+
const stripped = Struct.omit(value, "_tag");
|
|
12
|
+
return Object.fromEntries(Object.entries(stripped).map(([k, v]) => [k, stripTags(v)]));
|
|
13
|
+
}
|
|
14
|
+
if (Array.isArray(value)) return value.map(stripTags);
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
const WireUnknown = Schema$1.transform(Schema$1.Unknown, Schema$1.Unknown, {
|
|
18
|
+
strict: true,
|
|
19
|
+
decode: (value) => value,
|
|
20
|
+
encode: (value) => stripTags(value)
|
|
21
|
+
});
|
|
22
|
+
const Opcode = {
|
|
23
|
+
None: "None",
|
|
24
|
+
Step: "Step",
|
|
25
|
+
StepRun: "StepRun",
|
|
26
|
+
StepError: "StepError",
|
|
27
|
+
StepPlanned: "StepPlanned",
|
|
28
|
+
Sleep: "Sleep",
|
|
29
|
+
WaitForEvent: "WaitForEvent",
|
|
30
|
+
InvokeFunction: "InvokeFunction",
|
|
31
|
+
AIGateway: "AIGateway",
|
|
32
|
+
Gateway: "Gateway",
|
|
33
|
+
WaitForSignal: "WaitForSignal",
|
|
34
|
+
RunComplete: "RunComplete",
|
|
35
|
+
StepFailed: "StepFailed",
|
|
36
|
+
SyncRunComplete: "SyncRunComplete",
|
|
37
|
+
DiscoveryRequest: "DiscoveryRequest"
|
|
38
|
+
};
|
|
39
|
+
var UserError = class extends Schema$1.Class("UserError")({
|
|
40
|
+
name: Schema$1.String,
|
|
41
|
+
message: Schema$1.String,
|
|
42
|
+
stack: Schema$1.optional(Schema$1.String),
|
|
43
|
+
data: Schema$1.optional(Schema$1.Unknown),
|
|
44
|
+
noRetry: Schema$1.optional(Schema$1.Boolean),
|
|
45
|
+
cause: Schema$1.optional(Schema$1.Unknown)
|
|
46
|
+
}) {};
|
|
47
|
+
const StepResult = Schema$1.NullOr(Schema$1.Record({
|
|
48
|
+
key: Schema$1.String,
|
|
49
|
+
value: Schema$1.Unknown
|
|
50
|
+
}).pipe(Schema$1.annotations({ identifier: "StepResultObject" })));
|
|
51
|
+
var FunctionStack = class extends Schema$1.Class("FunctionStack")({
|
|
52
|
+
stack: Schema$1.Array(Schema$1.String),
|
|
53
|
+
current: Schema$1.Number
|
|
54
|
+
}) {};
|
|
55
|
+
var InngestEvent = class extends Schema$1.Class("InngestEvent")({
|
|
56
|
+
id: Schema$1.optional(Schema$1.String),
|
|
57
|
+
name: Schema$1.String,
|
|
58
|
+
data: Schema$1.optionalWith(Schema$1.Record({
|
|
59
|
+
key: Schema$1.String,
|
|
60
|
+
value: Schema$1.Unknown
|
|
61
|
+
}), {
|
|
62
|
+
default: () => ({}),
|
|
63
|
+
nullable: true
|
|
64
|
+
}),
|
|
65
|
+
ts: Schema$1.optional(Schema$1.Number),
|
|
66
|
+
user: Schema$1.optional(Schema$1.Record({
|
|
67
|
+
key: Schema$1.String,
|
|
68
|
+
value: Schema$1.Unknown
|
|
69
|
+
})),
|
|
70
|
+
v: Schema$1.optional(Schema$1.String)
|
|
71
|
+
}) {};
|
|
72
|
+
var SDKRequestContext = class extends Schema$1.Class("SDKRequestContext")({
|
|
73
|
+
fn_id: Schema$1.String,
|
|
74
|
+
run_id: Schema$1.String,
|
|
75
|
+
env: Schema$1.optionalWith(Schema$1.String, { default: () => "dev" }),
|
|
76
|
+
step_id: Schema$1.optionalWith(Schema$1.String, { default: () => "step" }),
|
|
77
|
+
attempt: Schema$1.optionalWith(Schema$1.Number, { default: () => 0 }),
|
|
78
|
+
max_attempts: Schema$1.optionalWith(Schema$1.Number, { default: () => 4 }),
|
|
79
|
+
stack: Schema$1.optionalWith(FunctionStack, { default: () => FunctionStack.make({
|
|
80
|
+
stack: [],
|
|
81
|
+
current: 0
|
|
82
|
+
}) }),
|
|
83
|
+
qi_id: Schema$1.optionalWith(Schema$1.String, { default: () => "" }),
|
|
84
|
+
disable_immediate_execution: Schema$1.optionalWith(Schema$1.Boolean, { default: () => false }),
|
|
85
|
+
use_api: Schema$1.optionalWith(Schema$1.Boolean, { default: () => false })
|
|
86
|
+
}) {};
|
|
87
|
+
var SDKRequestBody = class extends Schema$1.Class("SDKRequestBody")({
|
|
88
|
+
event: InngestEvent,
|
|
89
|
+
events: Schema$1.Array(InngestEvent),
|
|
90
|
+
steps: Schema$1.optionalWith(Schema$1.Record({
|
|
91
|
+
key: Schema$1.String,
|
|
92
|
+
value: StepResult
|
|
93
|
+
}), { default: () => ({}) }),
|
|
94
|
+
ctx: SDKRequestContext,
|
|
95
|
+
version: Schema$1.optionalWith(Schema$1.Number, { default: () => 1 }),
|
|
96
|
+
use_api: Schema$1.optionalWith(Schema$1.Boolean, { default: () => false })
|
|
97
|
+
}) {};
|
|
98
|
+
const Headers = {
|
|
99
|
+
SDK: "X-Inngest-SDK",
|
|
100
|
+
Signature: "X-Inngest-Signature",
|
|
101
|
+
RequestVersion: "x-inngest-req-version",
|
|
102
|
+
NoRetry: "X-Inngest-No-Retry",
|
|
103
|
+
RetryAfter: "Retry-After",
|
|
104
|
+
ServerKind: "X-Inngest-Server-Kind",
|
|
105
|
+
ExpectedServerKind: "X-Inngest-Expected-Server-Kind",
|
|
106
|
+
RunID: "X-Run-ID",
|
|
107
|
+
Framework: "X-Inngest-Framework",
|
|
108
|
+
Platform: "X-Inngest-Platform",
|
|
109
|
+
Env: "X-Inngest-Env"
|
|
110
|
+
};
|
|
111
|
+
var GeneratorOpcode = class extends Schema$1.Class("GeneratorOpcode")({
|
|
112
|
+
op: Schema$1.Literal(Opcode.None, Opcode.Step, Opcode.StepRun, Opcode.StepError, Opcode.StepPlanned, Opcode.Sleep, Opcode.WaitForEvent, Opcode.InvokeFunction, Opcode.AIGateway, Opcode.Gateway, Opcode.WaitForSignal, Opcode.RunComplete, Opcode.StepFailed, Opcode.SyncRunComplete, Opcode.DiscoveryRequest),
|
|
113
|
+
id: Schema$1.String,
|
|
114
|
+
name: Schema$1.String,
|
|
115
|
+
mode: Schema$1.optional(Schema$1.Literal("sync", "async")),
|
|
116
|
+
opts: Schema$1.optional(WireUnknown),
|
|
117
|
+
data: Schema$1.optional(WireUnknown),
|
|
118
|
+
error: Schema$1.optional(UserError),
|
|
119
|
+
displayName: Schema$1.optional(Schema$1.String),
|
|
120
|
+
userland: Schema$1.optional(Schema$1.Struct({ id: Schema$1.String }))
|
|
121
|
+
}) {};
|
|
122
|
+
const mkOpcode = (info, op, extra) => GeneratorOpcode.make({
|
|
123
|
+
op,
|
|
124
|
+
id: info.hash,
|
|
125
|
+
name: info.id,
|
|
126
|
+
displayName: info.name,
|
|
127
|
+
...extra
|
|
128
|
+
});
|
|
129
|
+
const stepPlanned = (info) => mkOpcode(info, Opcode.StepPlanned);
|
|
130
|
+
const stepRun = (info, data) => mkOpcode(info, Opcode.StepRun, { data });
|
|
131
|
+
const stepError = (info, error, noRetry) => {
|
|
132
|
+
const errorWithNoRetry = noRetry !== void 0 ? UserError.make({
|
|
133
|
+
name: error.name,
|
|
134
|
+
message: error.message,
|
|
135
|
+
stack: error.stack,
|
|
136
|
+
noRetry
|
|
137
|
+
}) : error;
|
|
138
|
+
return mkOpcode(info, Opcode.StepError, { error: errorWithNoRetry });
|
|
139
|
+
};
|
|
140
|
+
const sleep = (info, duration) => GeneratorOpcode.make({
|
|
141
|
+
op: Opcode.Sleep,
|
|
142
|
+
id: info.hash,
|
|
143
|
+
name: duration,
|
|
144
|
+
displayName: info.name,
|
|
145
|
+
mode: "async"
|
|
146
|
+
});
|
|
147
|
+
const waitForEvent = (info, opts) => mkOpcode(info, Opcode.WaitForEvent, {
|
|
148
|
+
mode: "async",
|
|
149
|
+
opts
|
|
150
|
+
});
|
|
151
|
+
const invokeFunction = (info, opts) => mkOpcode(info, Opcode.InvokeFunction, {
|
|
152
|
+
mode: "async",
|
|
153
|
+
opts,
|
|
154
|
+
userland: { id: info.id }
|
|
155
|
+
});
|
|
156
|
+
const IntrospectionBase = Schema$1.Struct({
|
|
157
|
+
function_count: Schema$1.Number,
|
|
158
|
+
has_event_key: Schema$1.Boolean,
|
|
159
|
+
has_signing_key: Schema$1.Boolean,
|
|
160
|
+
has_signing_key_fallback: Schema$1.Boolean,
|
|
161
|
+
mode: Schema$1.Literal("cloud", "dev"),
|
|
162
|
+
schema_version: Schema$1.Literal("2024-05-24"),
|
|
163
|
+
extra: Schema$1.optional(Schema$1.Record({
|
|
164
|
+
key: Schema$1.String,
|
|
165
|
+
value: Schema$1.Unknown
|
|
166
|
+
}))
|
|
167
|
+
});
|
|
168
|
+
const IntrospectionUnauthenticated = Schema$1.extend(IntrospectionBase, Schema$1.Struct({
|
|
169
|
+
authentication_succeeded: Schema$1.Union(Schema$1.Literal(false), Schema$1.Null),
|
|
170
|
+
functions: Schema$1.optionalWith(Schema$1.Array(Schema$1.Unknown), { exact: true })
|
|
171
|
+
}));
|
|
172
|
+
const IntrospectionAuthenticated = Schema$1.extend(IntrospectionBase, Schema$1.Struct({
|
|
173
|
+
authentication_succeeded: Schema$1.Literal(true),
|
|
174
|
+
api_origin: Schema$1.String,
|
|
175
|
+
app_id: Schema$1.String,
|
|
176
|
+
env: Schema$1.NullOr(Schema$1.String),
|
|
177
|
+
event_api_origin: Schema$1.String,
|
|
178
|
+
event_key_hash: Schema$1.NullOr(Schema$1.String),
|
|
179
|
+
framework: Schema$1.String,
|
|
180
|
+
sdk_language: Schema$1.String,
|
|
181
|
+
sdk_version: Schema$1.String,
|
|
182
|
+
serve_origin: Schema$1.NullOr(Schema$1.String),
|
|
183
|
+
serve_path: Schema$1.NullOr(Schema$1.String),
|
|
184
|
+
signing_key_fallback_hash: Schema$1.NullOr(Schema$1.String),
|
|
185
|
+
signing_key_hash: Schema$1.NullOr(Schema$1.String)
|
|
186
|
+
}));
|
|
187
|
+
const IntrospectionResponse = Schema$1.Union(IntrospectionAuthenticated, IntrospectionUnauthenticated);
|
|
188
|
+
const RegisterResponse = Schema$1.Struct({ message: Schema$1.optional(Schema$1.String) });
|
|
189
|
+
|
|
190
|
+
//#endregion
|
|
191
|
+
export { GeneratorOpcode, Headers, IntrospectionResponse, Opcode, RegisterResponse, SDKRequestBody, SDKRequestContext, UserError, invokeFunction, sleep, stepError, stepPlanned, stepRun, waitForEvent };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import "effect/Context";
|
|
2
|
+
import "effect/Effect";
|
|
3
|
+
import "effect/Layer";
|
|
4
|
+
import * as Schema from "effect/Schema";
|
|
5
|
+
|
|
6
|
+
//#region src/internal/signature.d.ts
|
|
7
|
+
declare const SignatureError_base: Schema.TaggedErrorClass<SignatureError, "SignatureError", {
|
|
8
|
+
readonly _tag: Schema.tag<"SignatureError">;
|
|
9
|
+
} & {
|
|
10
|
+
reason: Schema.Literal<["missing_header", "invalid_format", "expired", "invalid_signature", "missing_signing_key"]>;
|
|
11
|
+
message: typeof Schema.String;
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
declare class SignatureError extends SignatureError_base {}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { SignatureError };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { Headers } from "./protocol.js";
|
|
2
|
+
import * as Context from "effect/Context";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import * as Layer from "effect/Layer";
|
|
5
|
+
import * as Schema from "effect/Schema";
|
|
6
|
+
import * as Crypto from "node:crypto";
|
|
7
|
+
import * as DateTime from "effect/DateTime";
|
|
8
|
+
|
|
9
|
+
//#region src/internal/signature.ts
|
|
10
|
+
/**
|
|
11
|
+
* Signature verification service for Inngest requests.
|
|
12
|
+
* @internal
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
var SignatureError = class extends Schema.TaggedError()("SignatureError", {
|
|
18
|
+
reason: Schema.Literal("missing_header", "invalid_format", "expired", "invalid_signature", "missing_signing_key"),
|
|
19
|
+
message: Schema.String
|
|
20
|
+
}) {};
|
|
21
|
+
/**
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
var Signature = class extends Context.Tag("effect-inngest/Signature")() {};
|
|
25
|
+
const TimestampSeconds = Schema.NumberFromString.pipe(Schema.int(), Schema.positive());
|
|
26
|
+
const SignatureHex = Schema.String.pipe(Schema.pattern(/^[a-fA-F0-9]{64}$/), Schema.transform(Schema.String, {
|
|
27
|
+
decode: (s) => s.toLowerCase(),
|
|
28
|
+
encode: (s) => s
|
|
29
|
+
}));
|
|
30
|
+
const SignatureParams = Schema.Struct({
|
|
31
|
+
t: TimestampSeconds,
|
|
32
|
+
s: SignatureHex
|
|
33
|
+
});
|
|
34
|
+
const SIGNATURE_VALIDITY_WINDOW_MS = 300 * 1e3;
|
|
35
|
+
const parseSignatureHeader = (header) => {
|
|
36
|
+
const params = new URLSearchParams(header);
|
|
37
|
+
const raw = {
|
|
38
|
+
t: params.get("t") ?? "",
|
|
39
|
+
s: params.get("s") ?? ""
|
|
40
|
+
};
|
|
41
|
+
return Schema.decodeUnknown(SignatureParams)(raw).pipe(Effect.mapError(() => new SignatureError({
|
|
42
|
+
reason: "invalid_format",
|
|
43
|
+
message: `Invalid signature format: expected t=<int>&s=<64-hex>, got: ${header}`
|
|
44
|
+
})));
|
|
45
|
+
};
|
|
46
|
+
const extractKeyBytes = (signingKey) => {
|
|
47
|
+
const keyWithoutPrefix = signingKey.replace(/^signkey-\w+-/, "");
|
|
48
|
+
return Buffer.from(keyWithoutPrefix, "hex");
|
|
49
|
+
};
|
|
50
|
+
const computeSignature = (keyBytes, body, timestamp) => Crypto.createHmac("sha256", keyBytes).update(body).update(timestamp).digest("hex");
|
|
51
|
+
const timingSafeEqual = (a, b) => {
|
|
52
|
+
if (a.length !== b.length) return false;
|
|
53
|
+
try {
|
|
54
|
+
return Crypto.timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const checkSignature = (signature, body, timestamp, signingKey) => {
|
|
60
|
+
return timingSafeEqual(signature, computeSignature(extractKeyBytes(signingKey), body, timestamp));
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* @internal
|
|
64
|
+
*/
|
|
65
|
+
const SignatureLive = Layer.effect(Signature, Effect.succeed({
|
|
66
|
+
verify: ({ body, signatureHeader, signingKey, signingKeyFallback, isDev }) => Effect.gen(function* () {
|
|
67
|
+
if (isDev) return true;
|
|
68
|
+
if (!signingKey) return yield* new SignatureError({
|
|
69
|
+
reason: "missing_signing_key",
|
|
70
|
+
message: "No signing key configured for production mode"
|
|
71
|
+
});
|
|
72
|
+
if (!signatureHeader) return yield* new SignatureError({
|
|
73
|
+
reason: "missing_header",
|
|
74
|
+
message: `Missing ${Headers.Signature} header`
|
|
75
|
+
});
|
|
76
|
+
const { t: timestampSeconds, s: signature } = yield* parseSignatureHeader(signatureHeader);
|
|
77
|
+
const timestampMs = timestampSeconds * 1e3;
|
|
78
|
+
const now = yield* DateTime.now;
|
|
79
|
+
if (Math.abs(now.epochMillis - timestampMs) > SIGNATURE_VALIDITY_WINDOW_MS) return yield* new SignatureError({
|
|
80
|
+
reason: "expired",
|
|
81
|
+
message: `Signature expired: timestamp ${timestampSeconds} is outside the validity window`
|
|
82
|
+
});
|
|
83
|
+
const timestamp = String(timestampSeconds);
|
|
84
|
+
if ([signingKey, signingKeyFallback].filter(Boolean).some((key) => checkSignature(signature, body, timestamp, key))) return true;
|
|
85
|
+
return yield* new SignatureError({
|
|
86
|
+
reason: "invalid_signature",
|
|
87
|
+
message: "Invalid signature"
|
|
88
|
+
});
|
|
89
|
+
}),
|
|
90
|
+
sign: (body, signingKey) => DateTime.now.pipe(Effect.map((now) => {
|
|
91
|
+
const ts = Math.floor(now.epochMillis / 1e3);
|
|
92
|
+
return `t=${ts}&s=${computeSignature(extractKeyBytes(signingKey), body, String(ts))}`;
|
|
93
|
+
}))
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
//#endregion
|
|
97
|
+
export { Signature, SignatureError, SignatureLive };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { InngestFunction } from "../Function.js";
|
|
2
|
+
import "./protocol.js";
|
|
3
|
+
import { SendEventError, StepError } from "./errors.js";
|
|
4
|
+
import "./interrupts.js";
|
|
5
|
+
import * as Duration from "effect/Duration";
|
|
6
|
+
import "effect/Context";
|
|
7
|
+
import * as Effect from "effect/Effect";
|
|
8
|
+
import * as Option from "effect/Option";
|
|
9
|
+
import * as Schema from "effect/Schema";
|
|
10
|
+
import "effect/HashMap";
|
|
11
|
+
import "effect/Ref";
|
|
12
|
+
|
|
13
|
+
//#region src/internal/step.d.ts
|
|
14
|
+
interface StepOptions {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly name?: string;
|
|
17
|
+
}
|
|
18
|
+
type StepOptionsOrId = string | StepOptions;
|
|
19
|
+
interface WaitForEventOptions {
|
|
20
|
+
readonly timeout: Duration.DurationInput;
|
|
21
|
+
readonly if?: string;
|
|
22
|
+
}
|
|
23
|
+
interface InvokeOptionsBase<F extends InngestFunction.Any> {
|
|
24
|
+
readonly function: F;
|
|
25
|
+
readonly user?: Record<string, unknown>;
|
|
26
|
+
readonly v?: string;
|
|
27
|
+
readonly timeout?: Duration.DurationInput;
|
|
28
|
+
}
|
|
29
|
+
type InvokeOptions<F extends InngestFunction.Any> = [InngestFunction.EventType<F>] extends [never] ? InvokeOptionsBase<F> : InvokeOptionsBase<F> & {
|
|
30
|
+
readonly data: InngestFunction.EventType<F>;
|
|
31
|
+
};
|
|
32
|
+
type EventSchema = Schema.Schema.Any & {
|
|
33
|
+
readonly _tag: string;
|
|
34
|
+
};
|
|
35
|
+
type TaggedEvent = {
|
|
36
|
+
readonly _tag: string;
|
|
37
|
+
};
|
|
38
|
+
interface StepTools {
|
|
39
|
+
readonly run: <A, Err, R>(id: StepOptionsOrId, effect: Effect.Effect<A, Err, R>) => Effect.Effect<A, StepError | Err, R>;
|
|
40
|
+
readonly sleep: (id: StepOptionsOrId, duration: Duration.DurationInput) => Effect.Effect<void>;
|
|
41
|
+
readonly sleepUntil: (id: StepOptionsOrId, timestamp: Date | number | string) => Effect.Effect<void>;
|
|
42
|
+
readonly waitForEvent: <E extends EventSchema>(id: StepOptionsOrId, event: E, options: WaitForEventOptions) => Effect.Effect<Option.Option<Schema.Schema.Type<E>>>;
|
|
43
|
+
readonly invoke: <F extends InngestFunction.Any>(id: StepOptionsOrId, options: InvokeOptions<F>) => Effect.Effect<InngestFunction.Success<F>, StepError>;
|
|
44
|
+
readonly sendEvent: (id: StepOptionsOrId, payload: TaggedEvent | ReadonlyArray<TaggedEvent>) => Effect.Effect<{
|
|
45
|
+
readonly ids: ReadonlyArray<string>;
|
|
46
|
+
}, SendEventError>;
|
|
47
|
+
}
|
|
48
|
+
interface RunContext {
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly attempt: number;
|
|
51
|
+
readonly maxAttempts: number;
|
|
52
|
+
}
|
|
53
|
+
interface HandlerContext<F extends InngestFunction.Any> {
|
|
54
|
+
readonly event: InngestFunction.EventType<F>;
|
|
55
|
+
readonly step: StepTools;
|
|
56
|
+
readonly run: RunContext;
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { HandlerContext };
|