robotrock 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,60 +1,114 @@
1
1
  import {
2
- AskHumanExpiredError,
3
- AskHumanTimeoutError,
4
2
  RobotRock,
5
3
  RobotRockError,
4
+ TaskExpiredError,
5
+ TaskTimeoutError,
6
+ attachWebhookToActions,
6
7
  createClient,
7
8
  resolveRobotRockClient,
8
9
  resolveRobotRockConfig,
9
- taskContextSchema,
10
10
  toDiscriminatedApprovalResult
11
- } from "./chunk-TUQXDKV6.js";
11
+ } from "./chunk-THVGHUTX.js";
12
+ import {
13
+ assignToSchema,
14
+ createTaskBodySchema,
15
+ taskContextSchema
16
+ } from "./chunk-7FVE6OYZ.js";
12
17
 
13
- // src/ask-human.ts
14
- var DEFAULT_POLL_INTERVAL_MS = 2e3;
15
- var DEFAULT_TIMEOUT_MS = 24 * 60 * 60 * 1e3;
16
- function sleep(ms) {
17
- return new Promise((resolve) => setTimeout(resolve, ms));
18
+ // src/webhook.ts
19
+ import { createHmac, timingSafeEqual } from "crypto";
20
+ import { z } from "zod";
21
+ var ROBOTROCK_SIGNATURE_HEADER = "x-robotrock-signature";
22
+ var robotRockWebhookPayloadBodySchema = z.object({
23
+ taskId: z.string().min(1),
24
+ action: z.object({
25
+ id: z.string().min(1),
26
+ title: z.string().min(1),
27
+ data: z.unknown()
28
+ }),
29
+ handledBy: z.string().min(1).optional(),
30
+ handledAt: z.string().min(1),
31
+ handlerType: z.string().min(1)
32
+ });
33
+ var robotRockWebhookPayloadSchema = robotRockWebhookPayloadBodySchema.extend({
34
+ headers: z.record(z.string())
35
+ });
36
+ var RobotRockWebhookError = class extends Error {
37
+ constructor(message, code, details) {
38
+ super(message);
39
+ this.code = code;
40
+ this.details = details;
41
+ this.name = "RobotRockWebhookError";
42
+ }
43
+ };
44
+ async function verifyRobotRockWebhook(request, options = {}) {
45
+ const signatureHeaderName = options.signatureHeader ?? ROBOTROCK_SIGNATURE_HEADER;
46
+ const signature = request.headers.get(signatureHeaderName);
47
+ const secret = options.secret ?? process.env.ROBOTROCK_WEBHOOK_SECRET;
48
+ if (!secret) {
49
+ throw new RobotRockWebhookError(
50
+ "Missing ROBOTROCK_WEBHOOK_SECRET for webhook verification",
51
+ "MISSING_WEBHOOK_SECRET"
52
+ );
53
+ }
54
+ if (!signature) {
55
+ throw new RobotRockWebhookError(
56
+ `Missing webhook signature header: ${signatureHeaderName}`,
57
+ "MISSING_SIGNATURE"
58
+ );
59
+ }
60
+ const rawBody = await request.text();
61
+ assertValidSignature(rawBody, signature, secret);
62
+ let parsedBody;
63
+ try {
64
+ parsedBody = JSON.parse(rawBody);
65
+ } catch (error) {
66
+ throw new RobotRockWebhookError("Webhook body is not valid JSON", "INVALID_JSON", {
67
+ cause: error instanceof Error ? error.message : String(error)
68
+ });
69
+ }
70
+ const payloadResult = robotRockWebhookPayloadBodySchema.safeParse(parsedBody);
71
+ if (!payloadResult.success) {
72
+ throw new RobotRockWebhookError(
73
+ "Webhook payload schema validation failed",
74
+ "INVALID_PAYLOAD",
75
+ payloadResult.error.flatten()
76
+ );
77
+ }
78
+ return {
79
+ ...payloadResult.data,
80
+ headers: normalizeHeaders(request.headers)
81
+ };
18
82
  }
19
- async function askHuman(params) {
20
- const {
21
- task,
22
- client: explicitClient,
23
- apiKey,
24
- baseUrl,
25
- pollInterval = DEFAULT_POLL_INTERVAL_MS,
26
- timeout = DEFAULT_TIMEOUT_MS,
27
- idempotencyKey
28
- } = params;
29
- const client = resolveRobotRockClient(explicitClient, { apiKey, baseUrl });
30
- const response = await client.createTask(
31
- task,
32
- { idempotencyKey }
33
- );
34
- const streamId = response.task.streamId;
35
- const deadline = Date.now() + timeout;
36
- while (Date.now() < deadline) {
37
- const existing = await client.getTask(streamId);
38
- if (existing?.status === "handled" && existing.handled) {
39
- return toDiscriminatedApprovalResult(task.actions, existing, streamId);
40
- }
41
- if (existing?.status === "expired") {
42
- throw new AskHumanExpiredError("Task expired before a human completed it");
43
- }
44
- await sleep(pollInterval);
83
+ function assertValidSignature(rawBody, signature, secret) {
84
+ const expected = `sha256=${createHmac("sha256", secret).update(rawBody).digest("hex")}`;
85
+ const expectedBuffer = Buffer.from(expected);
86
+ const receivedBuffer = Buffer.from(signature);
87
+ if (expectedBuffer.length !== receivedBuffer.length || !timingSafeEqual(expectedBuffer, receivedBuffer)) {
88
+ throw new RobotRockWebhookError("Webhook signature verification failed", "INVALID_SIGNATURE");
45
89
  }
46
- throw new AskHumanTimeoutError(`No human response within ${timeout}ms`);
90
+ }
91
+ function normalizeHeaders(headers) {
92
+ const result = {};
93
+ headers.forEach((value, key) => {
94
+ result[key] = value;
95
+ });
96
+ return result;
47
97
  }
48
98
  export {
49
- AskHumanExpiredError,
50
- AskHumanTimeoutError,
51
99
  RobotRock,
52
100
  RobotRockError,
53
- askHuman,
101
+ RobotRockWebhookError,
102
+ TaskExpiredError,
103
+ TaskTimeoutError,
104
+ assignToSchema,
105
+ attachWebhookToActions,
54
106
  createClient,
107
+ createTaskBodySchema,
55
108
  resolveRobotRockClient,
56
109
  resolveRobotRockConfig,
57
110
  taskContextSchema,
58
- toDiscriminatedApprovalResult
111
+ toDiscriminatedApprovalResult,
112
+ verifyRobotRockWebhook
59
113
  };
60
114
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/ask-human.ts"],"sourcesContent":["import {\n AskHumanExpiredError,\n AskHumanTimeoutError,\n toDiscriminatedApprovalResult,\n} from \"./approval-result.js\";\nimport type { RobotRock, RobotRockConfig, CreateTaskOptions } from \"./client.js\";\nimport { resolveRobotRockClient } from \"./env.js\";\nimport type {\n DiscriminatedApprovalResult,\n TaskAction,\n TaskContextInput,\n} from \"@robotrock/core\";\n\nconst DEFAULT_POLL_INTERVAL_MS = 2_000;\nconst DEFAULT_TIMEOUT_MS = 24 * 60 * 60 * 1_000;\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n/** Task payload for {@link askHuman}; `actions` must stay a literal tuple for inference. */\nexport type AskHumanTask<A extends readonly TaskAction[]> = Omit<TaskContextInput, \"actions\"> & {\n readonly actions: A;\n};\n\nexport type AskHumanParams<A extends readonly TaskAction[]> = {\n task: AskHumanTask<A>;\n /** Pre-configured client; when omitted, one is created from env / `apiKey` / `baseUrl`. */\n client?: RobotRock;\n apiKey?: string;\n baseUrl?: string;\n /** Poll interval while waiting for a human (ms). @default 2000 */\n pollInterval?: number;\n /** Max wait time (ms). @default 86400000 (24h) */\n timeout?: number;\n} & Pick<CreateTaskOptions, \"idempotencyKey\">;\n\n/**\n * Create a human-in-the-loop task and block until someone completes it.\n *\n * Uses polling against the RobotRock API. For durable waits inside Trigger.dev,\n * use `robotrock/trigger` instead.\n *\n * The return type is inferred from `task.actions`: `actionId` narrows `data`.\n */\nexport async function askHuman<const A extends readonly TaskAction[]>(\n params: AskHumanParams<A>\n): Promise<DiscriminatedApprovalResult<A>> {\n const {\n task,\n client: explicitClient,\n apiKey,\n baseUrl,\n pollInterval = DEFAULT_POLL_INTERVAL_MS,\n timeout = DEFAULT_TIMEOUT_MS,\n idempotencyKey,\n } = params;\n\n const client = resolveRobotRockClient(explicitClient, { apiKey, baseUrl });\n const response = await client.createTask(\n task as unknown as TaskContextInput,\n { idempotencyKey }\n );\n const streamId = response.task.streamId;\n const deadline = Date.now() + timeout;\n\n while (Date.now() < deadline) {\n const existing = await client.getTask(streamId);\n\n if (existing?.status === \"handled\" && existing.handled) {\n return toDiscriminatedApprovalResult(task.actions, existing, streamId);\n }\n\n if (existing?.status === \"expired\") {\n throw new AskHumanExpiredError(\"Task expired before a human completed it\");\n }\n\n await sleep(pollInterval);\n }\n\n throw new AskHumanTimeoutError(`No human response within ${timeout}ms`);\n}\n\nexport type { RobotRockConfig };\n"],"mappings":";;;;;;;;;;;;;AAaA,IAAM,2BAA2B;AACjC,IAAM,qBAAqB,KAAK,KAAK,KAAK;AAE1C,SAAS,MAAM,IAA2B;AACxC,SAAO,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;AA2BA,eAAsB,SACpB,QACyC;AACzC,QAAM;AAAA,IACJ;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA,eAAe;AAAA,IACf,UAAU;AAAA,IACV;AAAA,EACF,IAAI;AAEJ,QAAM,SAAS,uBAAuB,gBAAgB,EAAE,QAAQ,QAAQ,CAAC;AACzE,QAAM,WAAW,MAAM,OAAO;AAAA,IAC5B;AAAA,IACA,EAAE,eAAe;AAAA,EACnB;AACA,QAAM,WAAW,SAAS,KAAK;AAC/B,QAAM,WAAW,KAAK,IAAI,IAAI;AAE9B,SAAO,KAAK,IAAI,IAAI,UAAU;AAC5B,UAAM,WAAW,MAAM,OAAO,QAAQ,QAAQ;AAE9C,QAAI,UAAU,WAAW,aAAa,SAAS,SAAS;AACtD,aAAO,8BAA8B,KAAK,SAAS,UAAU,QAAQ;AAAA,IACvE;AAEA,QAAI,UAAU,WAAW,WAAW;AAClC,YAAM,IAAI,qBAAqB,0CAA0C;AAAA,IAC3E;AAEA,UAAM,MAAM,YAAY;AAAA,EAC1B;AAEA,QAAM,IAAI,qBAAqB,4BAA4B,OAAO,IAAI;AACxE;","names":[]}
1
+ {"version":3,"sources":["../src/webhook.ts"],"sourcesContent":["import { createHmac, timingSafeEqual } from \"node:crypto\";\nimport { z } from \"zod\";\n\nconst ROBOTROCK_SIGNATURE_HEADER = \"x-robotrock-signature\";\n\nconst robotRockWebhookPayloadBodySchema = z.object({\n taskId: z.string().min(1),\n action: z.object({\n id: z.string().min(1),\n title: z.string().min(1),\n data: z.unknown(),\n }),\n handledBy: z.string().min(1).optional(),\n handledAt: z.string().min(1),\n handlerType: z.string().min(1),\n});\n\nconst robotRockWebhookPayloadSchema = robotRockWebhookPayloadBodySchema.extend({\n headers: z.record(z.string()),\n});\n\nexport type RobotRockWebhookErrorCode =\n | \"MISSING_WEBHOOK_SECRET\"\n | \"MISSING_SIGNATURE\"\n | \"INVALID_SIGNATURE\"\n | \"INVALID_JSON\"\n | \"INVALID_PAYLOAD\";\n\nexport class RobotRockWebhookError extends Error {\n constructor(\n message: string,\n public readonly code: RobotRockWebhookErrorCode,\n public readonly details?: unknown\n ) {\n super(message);\n this.name = \"RobotRockWebhookError\";\n }\n}\n\nexport type RobotRockWebhookPayload = z.infer<typeof robotRockWebhookPayloadSchema>;\n\nexport interface VerifyRobotRockWebhookOptions {\n /**\n * Override shared secret (defaults to ROBOTROCK_WEBHOOK_SECRET).\n * Keep undefined in production to enforce the canonical env var.\n */\n secret?: string;\n /** Signature header to read. @default \"x-robotrock-signature\" */\n signatureHeader?: string;\n}\n\n/**\n * Verify a RobotRock webhook request and return a validated payload.\n * Throws RobotRockWebhookError with machine-readable `code` for audit logging.\n */\nexport async function verifyRobotRockWebhook(\n request: Request,\n options: VerifyRobotRockWebhookOptions = {}\n): Promise<RobotRockWebhookPayload> {\n const signatureHeaderName = options.signatureHeader ?? ROBOTROCK_SIGNATURE_HEADER;\n const signature = request.headers.get(signatureHeaderName);\n const secret = options.secret ?? process.env.ROBOTROCK_WEBHOOK_SECRET;\n\n if (!secret) {\n throw new RobotRockWebhookError(\n \"Missing ROBOTROCK_WEBHOOK_SECRET for webhook verification\",\n \"MISSING_WEBHOOK_SECRET\"\n );\n }\n\n if (!signature) {\n throw new RobotRockWebhookError(\n `Missing webhook signature header: ${signatureHeaderName}`,\n \"MISSING_SIGNATURE\"\n );\n }\n\n const rawBody = await request.text();\n assertValidSignature(rawBody, signature, secret);\n\n let parsedBody: unknown;\n try {\n parsedBody = JSON.parse(rawBody);\n } catch (error) {\n throw new RobotRockWebhookError(\"Webhook body is not valid JSON\", \"INVALID_JSON\", {\n cause: error instanceof Error ? error.message : String(error),\n });\n }\n\n const payloadResult = robotRockWebhookPayloadBodySchema.safeParse(parsedBody);\n if (!payloadResult.success) {\n throw new RobotRockWebhookError(\n \"Webhook payload schema validation failed\",\n \"INVALID_PAYLOAD\",\n payloadResult.error.flatten()\n );\n }\n\n return {\n ...payloadResult.data,\n headers: normalizeHeaders(request.headers),\n };\n}\n\nfunction assertValidSignature(rawBody: string, signature: string, secret: string): void {\n const expected = `sha256=${createHmac(\"sha256\", secret).update(rawBody).digest(\"hex\")}`;\n const expectedBuffer = Buffer.from(expected);\n const receivedBuffer = Buffer.from(signature);\n\n if (\n expectedBuffer.length !== receivedBuffer.length ||\n !timingSafeEqual(expectedBuffer, receivedBuffer)\n ) {\n throw new RobotRockWebhookError(\"Webhook signature verification failed\", \"INVALID_SIGNATURE\");\n }\n}\n\nfunction normalizeHeaders(headers: Headers): Record<string, string> {\n const result: Record<string, string> = {};\n headers.forEach((value, key) => {\n result[key] = value;\n });\n return result;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA,SAAS,YAAY,uBAAuB;AAC5C,SAAS,SAAS;AAElB,IAAM,6BAA6B;AAEnC,IAAM,oCAAoC,EAAE,OAAO;AAAA,EACjD,QAAQ,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EACxB,QAAQ,EAAE,OAAO;AAAA,IACf,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACpB,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,IACvB,MAAM,EAAE,QAAQ;AAAA,EAClB,CAAC;AAAA,EACD,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EACtC,WAAW,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC3B,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC;AAC/B,CAAC;AAED,IAAM,gCAAgC,kCAAkC,OAAO;AAAA,EAC7E,SAAS,EAAE,OAAO,EAAE,OAAO,CAAC;AAC9B,CAAC;AASM,IAAM,wBAAN,cAAoC,MAAM;AAAA,EAC/C,YACE,SACgB,MACA,SAChB;AACA,UAAM,OAAO;AAHG;AACA;AAGhB,SAAK,OAAO;AAAA,EACd;AACF;AAkBA,eAAsB,uBACpB,SACA,UAAyC,CAAC,GACR;AAClC,QAAM,sBAAsB,QAAQ,mBAAmB;AACvD,QAAM,YAAY,QAAQ,QAAQ,IAAI,mBAAmB;AACzD,QAAM,SAAS,QAAQ,UAAU,QAAQ,IAAI;AAE7C,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,WAAW;AACd,UAAM,IAAI;AAAA,MACR,qCAAqC,mBAAmB;AAAA,MACxD;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAAU,MAAM,QAAQ,KAAK;AACnC,uBAAqB,SAAS,WAAW,MAAM;AAE/C,MAAI;AACJ,MAAI;AACF,iBAAa,KAAK,MAAM,OAAO;AAAA,EACjC,SAAS,OAAO;AACd,UAAM,IAAI,sBAAsB,kCAAkC,gBAAgB;AAAA,MAChF,OAAO,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,IAC9D,CAAC;AAAA,EACH;AAEA,QAAM,gBAAgB,kCAAkC,UAAU,UAAU;AAC5E,MAAI,CAAC,cAAc,SAAS;AAC1B,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA,cAAc,MAAM,QAAQ;AAAA,IAC9B;AAAA,EACF;AAEA,SAAO;AAAA,IACL,GAAG,cAAc;AAAA,IACjB,SAAS,iBAAiB,QAAQ,OAAO;AAAA,EAC3C;AACF;AAEA,SAAS,qBAAqB,SAAiB,WAAmB,QAAsB;AACtF,QAAM,WAAW,UAAU,WAAW,UAAU,MAAM,EAAE,OAAO,OAAO,EAAE,OAAO,KAAK,CAAC;AACrF,QAAM,iBAAiB,OAAO,KAAK,QAAQ;AAC3C,QAAM,iBAAiB,OAAO,KAAK,SAAS;AAE5C,MACE,eAAe,WAAW,eAAe,UACzC,CAAC,gBAAgB,gBAAgB,cAAc,GAC/C;AACA,UAAM,IAAI,sBAAsB,yCAAyC,mBAAmB;AAAA,EAC9F;AACF;AAEA,SAAS,iBAAiB,SAA0C;AAClE,QAAM,SAAiC,CAAC;AACxC,UAAQ,QAAQ,CAAC,OAAO,QAAQ;AAC9B,WAAO,GAAG,IAAI;AAAA,EAChB,CAAC;AACD,SAAO;AACT;","names":[]}