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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +457 -0
  3. package/dist/Client.d.ts +167 -0
  4. package/dist/Client.js +144 -0
  5. package/dist/Events.d.ts +110 -0
  6. package/dist/Events.js +93 -0
  7. package/dist/Function.d.ts +384 -0
  8. package/dist/Function.js +104 -0
  9. package/dist/Group.d.ts +152 -0
  10. package/dist/Group.js +164 -0
  11. package/dist/HttpApi.d.ts +75 -0
  12. package/dist/HttpApi.js +47 -0
  13. package/dist/_virtual/rolldown_runtime.js +18 -0
  14. package/dist/index.d.ts +7 -0
  15. package/dist/index.js +8 -0
  16. package/dist/internal/constants.js +15 -0
  17. package/dist/internal/driver.d.ts +5 -0
  18. package/dist/internal/driver.js +117 -0
  19. package/dist/internal/errors.d.ts +56 -0
  20. package/dist/internal/errors.js +61 -0
  21. package/dist/internal/handler.d.ts +20 -0
  22. package/dist/internal/handler.js +145 -0
  23. package/dist/internal/helpers.js +44 -0
  24. package/dist/internal/interrupts.d.ts +2 -0
  25. package/dist/internal/interrupts.js +45 -0
  26. package/dist/internal/memo.js +56 -0
  27. package/dist/internal/protocol.d.ts +1 -0
  28. package/dist/internal/protocol.js +191 -0
  29. package/dist/internal/signature.d.ts +18 -0
  30. package/dist/internal/signature.js +97 -0
  31. package/dist/internal/step.d.ts +59 -0
  32. package/dist/internal/step.js +183 -0
  33. package/package.json +121 -0
  34. package/src/Client.ts +279 -0
  35. package/src/Events.ts +87 -0
  36. package/src/Function.ts +493 -0
  37. package/src/Group.ts +314 -0
  38. package/src/HttpApi.ts +82 -0
  39. package/src/index.ts +171 -0
  40. package/src/internal/constants.ts +11 -0
  41. package/src/internal/driver.ts +194 -0
  42. package/src/internal/errors.ts +130 -0
  43. package/src/internal/handler.ts +222 -0
  44. package/src/internal/helpers.ts +58 -0
  45. package/src/internal/interrupts.ts +62 -0
  46. package/src/internal/memo.ts +73 -0
  47. package/src/internal/protocol.ts +218 -0
  48. package/src/internal/signature.ts +158 -0
  49. package/src/internal/step.ts +377 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Step memoization schemas for decoding cached step results.
3
+ * @internal
4
+ */
5
+ import * as Predicate from "effect/Predicate";
6
+ import * as Schema from "effect/Schema";
7
+
8
+ // Wire format: require property KEY to exist (not just value to be defined)
9
+ const hasKey =
10
+ (key: string) =>
11
+ (u: unknown): u is Record<string, unknown> =>
12
+ Predicate.isRecord(u) && Predicate.hasProperty(u, key);
13
+
14
+ // Output schemas (tagged structs)
15
+ const MemoDataSchema = Schema.TaggedStruct("MemoData", { data: Schema.Unknown });
16
+ const MemoErrorSchema = Schema.TaggedStruct("MemoError", { error: Schema.Unknown });
17
+ const MemoInputSchema = Schema.TaggedStruct("MemoInput", { input: Schema.Unknown });
18
+ const MemoTimeoutSchema = Schema.TaggedStruct("MemoTimeout", {});
19
+ const MemoNoneSchema = Schema.TaggedStruct("MemoNone", {});
20
+
21
+ // Wire schemas with property existence filters + tag attachment
22
+ const DataWire = Schema.Unknown.pipe(
23
+ Schema.filter(hasKey("data")),
24
+ Schema.transform(MemoDataSchema, {
25
+ decode: (v) => ({ _tag: "MemoData" as const, data: (v as Record<string, unknown>).data }),
26
+ encode: ({ data }) => ({ data }),
27
+ }),
28
+ );
29
+
30
+ const ErrorWire = Schema.Unknown.pipe(
31
+ Schema.filter(hasKey("error")),
32
+ Schema.transform(MemoErrorSchema, {
33
+ decode: (v) => ({ _tag: "MemoError" as const, error: (v as Record<string, unknown>).error }),
34
+ encode: ({ error }) => ({ error }),
35
+ }),
36
+ );
37
+
38
+ const InputWire = Schema.Unknown.pipe(
39
+ Schema.filter(hasKey("input")),
40
+ Schema.transform(MemoInputSchema, {
41
+ decode: (v) => ({ _tag: "MemoInput" as const, input: (v as Record<string, unknown>).input }),
42
+ encode: ({ input }) => ({ input }),
43
+ }),
44
+ );
45
+
46
+ const TimeoutWire = Schema.transform(Schema.Null, MemoTimeoutSchema, {
47
+ decode: () => ({ _tag: "MemoTimeout" as const }),
48
+ encode: () => null,
49
+ });
50
+
51
+ const NoneWire = Schema.transform(Schema.Undefined, MemoNoneSchema, {
52
+ decode: () => ({ _tag: "MemoNone" as const }),
53
+ encode: () => undefined,
54
+ });
55
+
56
+ // Union order matters: error > input > data (more specific first)
57
+ const MemoSchema = Schema.Union(ErrorWire, InputWire, DataWire, TimeoutWire, NoneWire);
58
+
59
+ // Derived type (only union is needed externally - Match.tag uses string literals)
60
+ export type Memo = Schema.Schema.Type<typeof MemoSchema>;
61
+
62
+ /**
63
+ * Decode a step result into a Memo type.
64
+ * Order matters: error > input > data (more specific properties first).
65
+ */
66
+ export const decodeMemo = (value: unknown): Memo => {
67
+ const result = Schema.decodeUnknownOption(MemoSchema)(value);
68
+ if (result._tag === "Some") {
69
+ return result.value;
70
+ }
71
+
72
+ return MemoNoneSchema.make();
73
+ };
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Wire protocol schemas and opcode factories for Inngest communication.
3
+ * @internal
4
+ */
5
+ import { Predicate, Struct } from "effect";
6
+ import * as Schema from "effect/Schema";
7
+
8
+ const stripTags = (value: unknown): unknown => {
9
+ if (Predicate.isRecord(value)) {
10
+ const stripped = Struct.omit(value as Record<string, unknown>, "_tag");
11
+ return Object.fromEntries(Object.entries(stripped).map(([k, v]) => [k, stripTags(v)]));
12
+ }
13
+ if (Array.isArray(value)) {
14
+ return value.map(stripTags);
15
+ }
16
+ return value;
17
+ };
18
+
19
+ const WireUnknown = Schema.transform(Schema.Unknown, Schema.Unknown, {
20
+ strict: true,
21
+ decode: (value) => value,
22
+ encode: (value) => stripTags(value),
23
+ });
24
+
25
+ export const Opcode = {
26
+ None: "None",
27
+ Step: "Step",
28
+ StepRun: "StepRun",
29
+ StepError: "StepError",
30
+ StepPlanned: "StepPlanned",
31
+ Sleep: "Sleep",
32
+ WaitForEvent: "WaitForEvent",
33
+ InvokeFunction: "InvokeFunction",
34
+ AIGateway: "AIGateway",
35
+ Gateway: "Gateway",
36
+ WaitForSignal: "WaitForSignal",
37
+ RunComplete: "RunComplete",
38
+ StepFailed: "StepFailed",
39
+ SyncRunComplete: "SyncRunComplete",
40
+ DiscoveryRequest: "DiscoveryRequest",
41
+ } as const;
42
+
43
+ type OpcodeValue = (typeof Opcode)[keyof typeof Opcode];
44
+
45
+ export class UserError extends Schema.Class<UserError>("UserError")({
46
+ name: Schema.String,
47
+ message: Schema.String,
48
+ stack: Schema.optional(Schema.String),
49
+ data: Schema.optional(Schema.Unknown),
50
+ noRetry: Schema.optional(Schema.Boolean),
51
+ cause: Schema.optional(Schema.Unknown),
52
+ }) {}
53
+
54
+ export const StepResult = Schema.NullOr(
55
+ Schema.Record({ key: Schema.String, value: Schema.Unknown }).pipe(
56
+ Schema.annotations({ identifier: "StepResultObject" }),
57
+ ),
58
+ );
59
+
60
+ export class FunctionStack extends Schema.Class<FunctionStack>("FunctionStack")({
61
+ stack: Schema.Array(Schema.String),
62
+ current: Schema.Number,
63
+ }) {}
64
+
65
+ export class InngestEvent extends Schema.Class<InngestEvent>("InngestEvent")({
66
+ id: Schema.optional(Schema.String),
67
+ name: Schema.String,
68
+ data: Schema.optionalWith(Schema.Record({ key: Schema.String, value: Schema.Unknown }), {
69
+ default: () => ({}),
70
+ nullable: true,
71
+ }),
72
+
73
+ ts: Schema.optional(Schema.Number),
74
+ user: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })),
75
+ v: Schema.optional(Schema.String),
76
+ }) {}
77
+
78
+ export class SDKRequestContext extends Schema.Class<SDKRequestContext>("SDKRequestContext")({
79
+ fn_id: Schema.String,
80
+ run_id: Schema.String,
81
+ env: Schema.optionalWith(Schema.String, { default: () => "dev" }),
82
+ step_id: Schema.optionalWith(Schema.String, { default: () => "step" }),
83
+ attempt: Schema.optionalWith(Schema.Number, { default: () => 0 }),
84
+ max_attempts: Schema.optionalWith(Schema.Number, { default: () => 4 }),
85
+ stack: Schema.optionalWith(FunctionStack, { default: () => FunctionStack.make({ stack: [], current: 0 }) }),
86
+ qi_id: Schema.optionalWith(Schema.String, { default: () => "" }),
87
+ disable_immediate_execution: Schema.optionalWith(Schema.Boolean, { default: () => false }),
88
+ use_api: Schema.optionalWith(Schema.Boolean, { default: () => false }),
89
+ }) {}
90
+
91
+ export class SDKRequestBody extends Schema.Class<SDKRequestBody>("SDKRequestBody")({
92
+ event: InngestEvent,
93
+ events: Schema.Array(InngestEvent),
94
+ steps: Schema.optionalWith(Schema.Record({ key: Schema.String, value: StepResult }), { default: () => ({}) }),
95
+ ctx: SDKRequestContext,
96
+ version: Schema.optionalWith(Schema.Number, { default: () => 1 }),
97
+ use_api: Schema.optionalWith(Schema.Boolean, { default: () => false }),
98
+ }) {}
99
+
100
+ export const Headers = {
101
+ SDK: "X-Inngest-SDK",
102
+ Signature: "X-Inngest-Signature",
103
+ RequestVersion: "x-inngest-req-version",
104
+ NoRetry: "X-Inngest-No-Retry",
105
+ RetryAfter: "Retry-After",
106
+ ServerKind: "X-Inngest-Server-Kind",
107
+ ExpectedServerKind: "X-Inngest-Expected-Server-Kind",
108
+ RunID: "X-Run-ID",
109
+ Framework: "X-Inngest-Framework",
110
+ Platform: "X-Inngest-Platform",
111
+ Env: "X-Inngest-Env",
112
+ } as const;
113
+
114
+ export class GeneratorOpcode extends Schema.Class<GeneratorOpcode>("GeneratorOpcode")({
115
+ op: Schema.Literal(
116
+ Opcode.None,
117
+ Opcode.Step,
118
+ Opcode.StepRun,
119
+ Opcode.StepError,
120
+ Opcode.StepPlanned,
121
+ Opcode.Sleep,
122
+ Opcode.WaitForEvent,
123
+ Opcode.InvokeFunction,
124
+ Opcode.AIGateway,
125
+ Opcode.Gateway,
126
+ Opcode.WaitForSignal,
127
+ Opcode.RunComplete,
128
+ Opcode.StepFailed,
129
+ Opcode.SyncRunComplete,
130
+ Opcode.DiscoveryRequest,
131
+ ),
132
+ id: Schema.String,
133
+ name: Schema.String,
134
+ mode: Schema.optional(Schema.Literal("sync", "async")),
135
+ opts: Schema.optional(WireUnknown),
136
+ data: Schema.optional(WireUnknown),
137
+ error: Schema.optional(UserError),
138
+ displayName: Schema.optional(Schema.String),
139
+ userland: Schema.optional(Schema.Struct({ id: Schema.String })),
140
+ }) {}
141
+
142
+ interface StepInfo {
143
+ readonly id: string;
144
+ readonly name: string;
145
+ readonly hash: string;
146
+ }
147
+
148
+ const mkOpcode = (info: StepInfo, op: OpcodeValue, extra?: object): GeneratorOpcode =>
149
+ GeneratorOpcode.make({ op, id: info.hash, name: info.id, displayName: info.name, ...extra });
150
+
151
+ export const stepPlanned = (info: StepInfo): GeneratorOpcode => mkOpcode(info, Opcode.StepPlanned);
152
+
153
+ export const stepRun = (info: StepInfo, data: unknown): GeneratorOpcode => mkOpcode(info, Opcode.StepRun, { data });
154
+
155
+ export const stepError = (info: StepInfo, error: UserError, noRetry?: boolean): GeneratorOpcode => {
156
+ // noRetry must be in the error object for Inngest executor to recognize it
157
+ const errorWithNoRetry =
158
+ noRetry !== undefined
159
+ ? UserError.make({ name: error.name, message: error.message, stack: error.stack, noRetry })
160
+ : error;
161
+ return mkOpcode(info, Opcode.StepError, { error: errorWithNoRetry });
162
+ };
163
+
164
+ export const sleep = (info: StepInfo, duration: string): GeneratorOpcode =>
165
+ // Official SDK puts duration/timestamp in `name` field, not opts.duration
166
+ // mode: "async" is required per the Op type definition
167
+ GeneratorOpcode.make({ op: Opcode.Sleep, id: info.hash, name: duration, displayName: info.name, mode: "async" });
168
+
169
+ export const waitForEvent = (info: StepInfo, opts: { event: string; timeout: string; if?: string }): GeneratorOpcode =>
170
+ mkOpcode(info, Opcode.WaitForEvent, { mode: "async", opts });
171
+
172
+ export const invokeFunction = (
173
+ info: StepInfo,
174
+ opts: { function_id: string; payload: unknown; timeout: string },
175
+ ): GeneratorOpcode => mkOpcode(info, Opcode.InvokeFunction, { mode: "async", opts, userland: { id: info.id } });
176
+
177
+ const IntrospectionBase = Schema.Struct({
178
+ function_count: Schema.Number,
179
+ has_event_key: Schema.Boolean,
180
+ has_signing_key: Schema.Boolean,
181
+ has_signing_key_fallback: Schema.Boolean,
182
+ mode: Schema.Literal("cloud", "dev"),
183
+ schema_version: Schema.Literal("2024-05-24"),
184
+ extra: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.Unknown })),
185
+ });
186
+
187
+ export const IntrospectionUnauthenticated = Schema.extend(
188
+ IntrospectionBase,
189
+ Schema.Struct({
190
+ authentication_succeeded: Schema.Union(Schema.Literal(false), Schema.Null),
191
+ functions: Schema.optionalWith(Schema.Array(Schema.Unknown), { exact: true }),
192
+ }),
193
+ );
194
+
195
+ export const IntrospectionAuthenticated = Schema.extend(
196
+ IntrospectionBase,
197
+ Schema.Struct({
198
+ authentication_succeeded: Schema.Literal(true),
199
+ api_origin: Schema.String,
200
+ app_id: Schema.String,
201
+ env: Schema.NullOr(Schema.String),
202
+ event_api_origin: Schema.String,
203
+ event_key_hash: Schema.NullOr(Schema.String),
204
+ framework: Schema.String,
205
+ sdk_language: Schema.String,
206
+ sdk_version: Schema.String,
207
+ serve_origin: Schema.NullOr(Schema.String),
208
+ serve_path: Schema.NullOr(Schema.String),
209
+ signing_key_fallback_hash: Schema.NullOr(Schema.String),
210
+ signing_key_hash: Schema.NullOr(Schema.String),
211
+ }),
212
+ );
213
+
214
+ export const IntrospectionResponse = Schema.Union(IntrospectionAuthenticated, IntrospectionUnauthenticated);
215
+
216
+ export const RegisterResponse = Schema.Struct({
217
+ message: Schema.optional(Schema.String),
218
+ });
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Signature verification service for Inngest requests.
3
+ * @internal
4
+ */
5
+ import * as Crypto from "node:crypto";
6
+ import * as Context from "effect/Context";
7
+ import * as DateTime from "effect/DateTime";
8
+ import * as Effect from "effect/Effect";
9
+ import * as Layer from "effect/Layer";
10
+ import * as Schema from "effect/Schema";
11
+ import * as Protocol from "./protocol.js";
12
+
13
+ /**
14
+ * @internal
15
+ */
16
+ export class SignatureError extends Schema.TaggedError<SignatureError>()("SignatureError", {
17
+ reason: Schema.Literal("missing_header", "invalid_format", "expired", "invalid_signature", "missing_signing_key"),
18
+ message: Schema.String,
19
+ }) {}
20
+
21
+ /**
22
+ * @internal
23
+ */
24
+ export interface VerifyOptions {
25
+ readonly body: Uint8Array;
26
+ readonly signatureHeader: string | undefined;
27
+ readonly signingKey: string | undefined;
28
+ readonly signingKeyFallback?: string | undefined;
29
+ readonly isDev: boolean;
30
+ }
31
+
32
+ /**
33
+ * @internal
34
+ */
35
+ export interface SignatureService {
36
+ readonly verify: (options: VerifyOptions) => Effect.Effect<boolean, SignatureError>;
37
+ readonly sign: (body: Uint8Array, signingKey: string) => Effect.Effect<string, SignatureError>;
38
+ }
39
+
40
+ /**
41
+ * @internal
42
+ */
43
+ export class Signature extends Context.Tag("effect-inngest/Signature")<Signature, SignatureService>() {}
44
+
45
+ // ─────────────────────────────────────────────────────────────
46
+ // Schemas (internal)
47
+ // ─────────────────────────────────────────────────────────────
48
+
49
+ const TimestampSeconds = Schema.NumberFromString.pipe(Schema.int(), Schema.positive());
50
+ // Case-insensitive hex, normalized to lowercase in parseSignatureHeader
51
+ const SignatureHex = Schema.String.pipe(
52
+ Schema.pattern(/^[a-fA-F0-9]{64}$/),
53
+ Schema.transform(Schema.String, {
54
+ decode: (s) => s.toLowerCase(),
55
+ encode: (s) => s,
56
+ }),
57
+ );
58
+ const SignatureParams = Schema.Struct({ t: TimestampSeconds, s: SignatureHex });
59
+
60
+ // ─────────────────────────────────────────────────────────────
61
+ // Internal helpers
62
+ // ─────────────────────────────────────────────────────────────
63
+
64
+ const SIGNATURE_VALIDITY_WINDOW_MS = 5 * 60 * 1000;
65
+
66
+ const parseSignatureHeader = (header: string) => {
67
+ const params = new URLSearchParams(header);
68
+ const raw = { t: params.get("t") ?? "", s: params.get("s") ?? "" };
69
+ return Schema.decodeUnknown(SignatureParams)(raw).pipe(
70
+ Effect.mapError(
71
+ () =>
72
+ new SignatureError({
73
+ reason: "invalid_format",
74
+ message: `Invalid signature format: expected t=<int>&s=<64-hex>, got: ${header}`,
75
+ }),
76
+ ),
77
+ );
78
+ };
79
+
80
+ const extractKeyBytes = (signingKey: string): Buffer => {
81
+ const keyWithoutPrefix = signingKey.replace(/^signkey-\w+-/, "");
82
+ return Buffer.from(keyWithoutPrefix, "hex");
83
+ };
84
+
85
+ const computeSignature = (keyBytes: Buffer, body: Uint8Array, timestamp: string): string =>
86
+ Crypto.createHmac("sha256", keyBytes).update(body).update(timestamp).digest("hex");
87
+
88
+ const timingSafeEqual = (a: string, b: string): boolean => {
89
+ if (a.length !== b.length) return false;
90
+ try {
91
+ return Crypto.timingSafeEqual(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
92
+ } catch {
93
+ return false;
94
+ }
95
+ };
96
+
97
+ const checkSignature = (signature: string, body: Uint8Array, timestamp: string, signingKey: string): boolean => {
98
+ const keyBytes = extractKeyBytes(signingKey);
99
+ const expected = computeSignature(keyBytes, body, timestamp);
100
+ return timingSafeEqual(signature, expected);
101
+ };
102
+
103
+ // ─────────────────────────────────────────────────────────────
104
+ // Live Layer
105
+ // ─────────────────────────────────────────────────────────────
106
+
107
+ /**
108
+ * @internal
109
+ */
110
+ export const SignatureLive: Layer.Layer<Signature> = Layer.effect(
111
+ Signature,
112
+ Effect.succeed({
113
+ verify: ({ body, signatureHeader, signingKey, signingKeyFallback, isDev }) =>
114
+ Effect.gen(function* () {
115
+ if (isDev) return true;
116
+ if (!signingKey) {
117
+ return yield* new SignatureError({
118
+ reason: "missing_signing_key",
119
+ message: "No signing key configured for production mode",
120
+ });
121
+ }
122
+
123
+ if (!signatureHeader) {
124
+ return yield* new SignatureError({
125
+ reason: "missing_header",
126
+ message: `Missing ${Protocol.Headers.Signature} header`,
127
+ });
128
+ }
129
+
130
+ const { t: timestampSeconds, s: signature } = yield* parseSignatureHeader(signatureHeader);
131
+ const timestampMs = timestampSeconds * 1000;
132
+ const now = yield* DateTime.now;
133
+
134
+ if (Math.abs(now.epochMillis - timestampMs) > SIGNATURE_VALIDITY_WINDOW_MS) {
135
+ return yield* new SignatureError({
136
+ reason: "expired",
137
+ message: `Signature expired: timestamp ${timestampSeconds} is outside the validity window`,
138
+ });
139
+ }
140
+
141
+ const timestamp = String(timestampSeconds);
142
+ const keys = [signingKey, signingKeyFallback].filter(Boolean) as string[];
143
+ const valid = keys.some((key) => checkSignature(signature, body, timestamp, key));
144
+
145
+ if (valid) return true;
146
+
147
+ return yield* new SignatureError({ reason: "invalid_signature", message: "Invalid signature" });
148
+ }),
149
+
150
+ sign: (body, signingKey) =>
151
+ DateTime.now.pipe(
152
+ Effect.map((now) => {
153
+ const ts = Math.floor(now.epochMillis / 1000);
154
+ return `t=${ts}&s=${computeSignature(extractKeyBytes(signingKey), body, String(ts))}`;
155
+ }),
156
+ ),
157
+ }),
158
+ );