@notionhq/workers 0.2.0 → 0.4.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 (60) hide show
  1. package/dist/capabilities/ai_connector.d.ts +97 -0
  2. package/dist/capabilities/ai_connector.d.ts.map +1 -0
  3. package/dist/capabilities/ai_connector.js +54 -0
  4. package/dist/capabilities/ai_connector.test.d.ts +2 -0
  5. package/dist/capabilities/ai_connector.test.d.ts.map +1 -0
  6. package/dist/capabilities/automation.d.ts.map +1 -1
  7. package/dist/capabilities/automation.js +11 -11
  8. package/dist/capabilities/context.d.ts +2 -1
  9. package/dist/capabilities/context.d.ts.map +1 -1
  10. package/dist/capabilities/context.js +21 -3
  11. package/dist/capabilities/output.d.ts +5 -0
  12. package/dist/capabilities/output.d.ts.map +1 -0
  13. package/dist/capabilities/output.js +10 -0
  14. package/dist/capabilities/stateful-capability.d.ts +30 -0
  15. package/dist/capabilities/stateful-capability.d.ts.map +1 -0
  16. package/dist/capabilities/stateful-capability.js +50 -0
  17. package/dist/capabilities/stateful-capability.test.d.ts +2 -0
  18. package/dist/capabilities/stateful-capability.test.d.ts.map +1 -0
  19. package/dist/capabilities/sync.d.ts +9 -19
  20. package/dist/capabilities/sync.d.ts.map +1 -1
  21. package/dist/capabilities/sync.js +34 -61
  22. package/dist/capabilities/tool.d.ts +21 -0
  23. package/dist/capabilities/tool.d.ts.map +1 -1
  24. package/dist/capabilities/tool.js +8 -22
  25. package/dist/capabilities/webhook.d.ts +101 -0
  26. package/dist/capabilities/webhook.d.ts.map +1 -0
  27. package/dist/capabilities/webhook.js +47 -0
  28. package/dist/error.d.ts +36 -0
  29. package/dist/error.d.ts.map +1 -1
  30. package/dist/error.js +14 -0
  31. package/dist/index.d.ts +5 -1
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +4 -0
  34. package/dist/pacer_internal.d.ts +7 -0
  35. package/dist/pacer_internal.d.ts.map +1 -1
  36. package/dist/pacer_internal.js +17 -0
  37. package/dist/schema.d.ts +28 -11
  38. package/dist/schema.d.ts.map +1 -1
  39. package/dist/schema.js +2 -2
  40. package/dist/worker.d.ts +92 -9
  41. package/dist/worker.d.ts.map +1 -1
  42. package/dist/worker.js +111 -1
  43. package/package.json +2 -2
  44. package/src/capabilities/ai_connector.test.ts +341 -0
  45. package/src/capabilities/ai_connector.ts +194 -0
  46. package/src/capabilities/automation.ts +11 -7
  47. package/src/capabilities/context.ts +45 -3
  48. package/src/capabilities/output.ts +8 -0
  49. package/src/capabilities/stateful-capability.test.ts +25 -0
  50. package/src/capabilities/stateful-capability.ts +87 -0
  51. package/src/capabilities/sync.test.ts +197 -4
  52. package/src/capabilities/sync.ts +58 -87
  53. package/src/capabilities/tool.test.ts +63 -0
  54. package/src/capabilities/tool.ts +28 -13
  55. package/src/capabilities/webhook.ts +148 -0
  56. package/src/error.ts +40 -0
  57. package/src/index.ts +28 -1
  58. package/src/pacer_internal.ts +34 -0
  59. package/src/schema.ts +29 -12
  60. package/src/worker.ts +139 -10
@@ -222,6 +222,69 @@ describe("Worker.tool", () => {
222
222
  expect(capability.config.schema).toBeDefined();
223
223
  });
224
224
 
225
+ describe("hints", () => {
226
+ it("omits hints from config when not specified (existing tools unchanged)", () => {
227
+ const capability = createToolCapability<{ name: string }, string>(
228
+ "noHints",
229
+ {
230
+ title: "No Hints",
231
+ description: "A tool without hints",
232
+ schema: j.object({ name: j.string() }),
233
+ execute: ({ name }) => `Hello, ${name}`,
234
+ },
235
+ );
236
+
237
+ expect(capability.config.hints).toBeUndefined();
238
+ });
239
+
240
+ it("flows readOnlyHint=true through to config", () => {
241
+ const capability = createToolCapability<{ query: string }, string>(
242
+ "readOnly",
243
+ {
244
+ title: "Read Only",
245
+ description: "A read-only tool",
246
+ schema: j.object({ query: j.string() }),
247
+ hints: { readOnlyHint: true },
248
+ execute: ({ query }) => query,
249
+ },
250
+ );
251
+
252
+ expect(capability.config.hints).toEqual({ readOnlyHint: true });
253
+ });
254
+
255
+ it("flows readOnlyHint=false through to config", () => {
256
+ const capability = createToolCapability<{ pageId: string }, string>(
257
+ "writeTool",
258
+ {
259
+ title: "Write Tool",
260
+ description: "A write tool",
261
+ schema: j.object({ pageId: j.string() }),
262
+ hints: { readOnlyHint: false },
263
+ execute: ({ pageId }) => pageId,
264
+ },
265
+ );
266
+
267
+ expect(capability.config.hints).toEqual({ readOnlyHint: false });
268
+ });
269
+
270
+ it("does not affect execution behavior", async () => {
271
+ const capability = createToolCapability<{ x: number }, number>(
272
+ "doubleReadOnly",
273
+ {
274
+ title: "Double",
275
+ description: "Doubles a number",
276
+ schema: j.object({ x: j.number() }),
277
+ hints: { readOnlyHint: true },
278
+ execute: ({ x }) => x * 2,
279
+ },
280
+ );
281
+
282
+ const result = await capability.handler({ x: 7 });
283
+
284
+ expect(result).toEqual({ _tag: "success", value: 14 });
285
+ });
286
+ });
287
+
225
288
  it("invalid output with custom output schema", async () => {
226
289
  const capability = createToolCapability<
227
290
  { value: number },
@@ -6,6 +6,7 @@ import { getSchema } from "../schema-builder.js";
6
6
  import type { HandlerOptions, JSONValue } from "../types.js";
7
7
  import type { CapabilityContext } from "./context.js";
8
8
  import { createCapabilityContext } from "./context.js";
9
+ import { writeOutput } from "./output.js";
9
10
 
10
11
  /**
11
12
  * Validates that every object schema in the tree has all its `properties`
@@ -69,6 +70,26 @@ function validateRequiredProperties(
69
70
  }
70
71
  }
71
72
 
73
+ /**
74
+ * Hints describing a tool's behavior. Advisory metadata read by the platform
75
+ * (not the model) to decide whether a call is safe to auto-execute or
76
+ * requires user confirmation.
77
+ *
78
+ * Field names mirror the MCP tool annotation shape
79
+ * (`McpServerToolAnnotations`) so worker tools flow through the same
80
+ * confirmation / auto-execute machinery as MCP tools.
81
+ *
82
+ * Only hints with active consumers in the Notion client are included.
83
+ */
84
+ export interface ToolHints {
85
+ /**
86
+ * The tool only reads state and has no side effects. Safe to call
87
+ * repeatedly; safe to auto-execute under default policy. Tools without
88
+ * this hint are treated as write tools and prompt for confirmation.
89
+ */
90
+ readOnlyHint?: boolean;
91
+ }
92
+
72
93
  export interface ToolConfiguration<
73
94
  I extends JSONValue,
74
95
  O extends JSONValue = JSONValue,
@@ -77,6 +98,7 @@ export interface ToolConfiguration<
77
98
  description: string;
78
99
  schema: SchemaBuilder<I>;
79
100
  outputSchema?: SchemaBuilder<O>;
101
+ hints?: ToolHints;
80
102
  execute: (input: I, context: CapabilityContext) => O | Promise<O>;
81
103
  }
82
104
 
@@ -211,15 +233,13 @@ export function createToolCapability<
211
233
  error: { name: error.name, message: error.message, trace: error.stack },
212
234
  };
213
235
 
214
- process.stdout.write(
215
- `\n<__notion_output__>${JSON.stringify(result)}</__notion_output__>\n`,
216
- );
236
+ writeOutput(result);
217
237
 
218
238
  return result;
219
239
  }
220
240
 
221
241
  try {
222
- const capabilityContext = createCapabilityContext();
242
+ const capabilityContext = createCapabilityContext("tool");
223
243
  const result = await config.execute(input, capabilityContext);
224
244
  if (validateOutput && !validateOutput(result)) {
225
245
  const error = new InvalidToolOutputError(
@@ -239,9 +259,7 @@ export function createToolCapability<
239
259
  },
240
260
  };
241
261
 
242
- process.stdout.write(
243
- `\n<__notion_output__>${JSON.stringify(result)}</__notion_output__>\n`,
244
- );
262
+ writeOutput(result);
245
263
 
246
264
  return result;
247
265
  }
@@ -250,9 +268,7 @@ export function createToolCapability<
250
268
  return result;
251
269
  }
252
270
 
253
- process.stdout.write(
254
- `\n<__notion_output__>${JSON.stringify({ _tag: "success", value: result })}</__notion_output__>\n`,
255
- );
271
+ writeOutput({ _tag: "success", value: result });
256
272
 
257
273
  return {
258
274
  _tag: "success" as const,
@@ -272,9 +288,7 @@ export function createToolCapability<
272
288
  error: { name: error.name, message: error.message, trace: error.stack },
273
289
  };
274
290
 
275
- process.stdout.write(
276
- `\n<__notion_output__>${JSON.stringify(result)}</__notion_output__>\n`,
277
- );
291
+ writeOutput(result);
278
292
 
279
293
  return result;
280
294
  }
@@ -288,6 +302,7 @@ export function createToolCapability<
288
302
  description: config.description,
289
303
  schema: inputSchema,
290
304
  outputSchema: outputSchema,
305
+ hints: config.hints,
291
306
  },
292
307
  handler,
293
308
  };
@@ -0,0 +1,148 @@
1
+ import { ExecutionError } from "../error.js";
2
+ import type { HandlerOptions } from "../types.js";
3
+ import type { CapabilityContext } from "./context.js";
4
+ import { createCapabilityContext } from "./context.js";
5
+ import { writeOutput } from "./output.js";
6
+
7
+ /**
8
+ * A single incoming webhook request.
9
+ */
10
+ export interface WebhookEvent {
11
+ /**
12
+ * Unique ID for this webhook delivery, stable across retries.
13
+ * Best-effort idempotency key — prefer using the webhook provider's
14
+ * own delivery/event ID when available, since providers may redeliver
15
+ * the same event with a new deliveryId.
16
+ */
17
+ deliveryId: string;
18
+ /**
19
+ * The parsed JSON body of the incoming request, or an empty object if
20
+ * the body is not valid JSON.
21
+ */
22
+ body: Record<string, unknown>;
23
+ /**
24
+ * The raw request body as a string, useful for signature verification.
25
+ */
26
+ rawBody: string;
27
+ /**
28
+ * The HTTP headers from the incoming request
29
+ */
30
+ headers: Record<string, string>;
31
+ /**
32
+ * The HTTP method of the incoming request (e.g. "POST")
33
+ */
34
+ method: string;
35
+ }
36
+
37
+ /**
38
+ * Throw this error from your webhook execute handler to signal that
39
+ * signature verification failed. After 5 consecutive verification
40
+ * failures, the platform will short-circuit and reject all incoming
41
+ * requests for this webhook without executing the handler.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * worker.webhook("onGithubPush", {
46
+ * title: "GitHub Push",
47
+ * description: "Handles GitHub push events",
48
+ * execute: async (events) => {
49
+ * const secret = process.env.GITHUB_WEBHOOK_SECRET;
50
+ * if (!secret) {
51
+ * throw new WebhookVerificationError("GITHUB_WEBHOOK_SECRET not set");
52
+ * }
53
+ * for (const event of events) {
54
+ * if (!verifyGitHubSignature(event.rawBody, event.headers, secret)) {
55
+ * throw new WebhookVerificationError("Invalid GitHub signature");
56
+ * }
57
+ * // ... handle verified event
58
+ * }
59
+ * },
60
+ * });
61
+ * ```
62
+ */
63
+ export class WebhookVerificationError extends ExecutionError {
64
+ constructor(cause?: unknown) {
65
+ super(cause ?? "Webhook signature verification failed");
66
+ this.name = "WebhookVerificationError";
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Configuration for a webhook capability
72
+ */
73
+ export interface WebhookConfiguration {
74
+ /**
75
+ * Title of the webhook - shown in the UI when viewing webhooks
76
+ */
77
+ title: string;
78
+
79
+ /**
80
+ * Description of what this webhook does - shown in the UI
81
+ */
82
+ description: string;
83
+
84
+ /**
85
+ * The function that executes when the webhook is triggered.
86
+ * Receives an array of events (currently always a single event,
87
+ * but may be batched in the future).
88
+ *
89
+ * To signal a verification failure, throw a `WebhookVerificationError`.
90
+ * After 5 consecutive verification failures, the platform will
91
+ * short-circuit and stop executing the handler.
92
+ *
93
+ * @param events - The incoming webhook events
94
+ * @param context - The capability execution context (Notion client, etc.)
95
+ * @returns A promise that resolves when the webhook processing completes
96
+ */
97
+ execute: (
98
+ events: WebhookEvent[],
99
+ context: CapabilityContext,
100
+ ) => Promise<void> | void;
101
+ }
102
+
103
+ export type WebhookCapability = ReturnType<typeof createWebhookCapability>;
104
+
105
+ export function createWebhookCapability(
106
+ key: string,
107
+ config: WebhookConfiguration,
108
+ ) {
109
+ return {
110
+ _tag: "webhook" as const,
111
+ key,
112
+ config: {
113
+ title: config.title,
114
+ description: config.description,
115
+ },
116
+ async handler(
117
+ events: WebhookEvent[],
118
+ options?: HandlerOptions,
119
+ ): Promise<{ status: "success" } | undefined> {
120
+ try {
121
+ const capabilityContext = createCapabilityContext("webhook");
122
+ await config.execute(events, capabilityContext);
123
+
124
+ if (options?.concreteOutput) {
125
+ return { status: "success" };
126
+ } else {
127
+ writeOutput({ _tag: "success", value: { status: "success" } });
128
+ }
129
+ } catch (err) {
130
+ const error =
131
+ err instanceof ExecutionError ? err : new ExecutionError(err);
132
+
133
+ if (!options?.concreteOutput) {
134
+ writeOutput({
135
+ _tag: "error",
136
+ error: {
137
+ name: error.name,
138
+ message: error.message,
139
+ trace: error.stack,
140
+ },
141
+ });
142
+ }
143
+
144
+ throw error;
145
+ }
146
+ },
147
+ };
148
+ }
package/src/error.ts CHANGED
@@ -11,6 +11,46 @@ export class ExecutionError extends Error {
11
11
  }
12
12
  }
13
13
 
14
+ /**
15
+ * Throw this error from your sync execute handler to signal that the
16
+ * external API returned a rate-limit response (e.g. HTTP 429).
17
+ *
18
+ * When the platform receives this error it applies exponential backoff
19
+ * instead of retrying immediately. If `retryAfter` is provided, the
20
+ * platform will wait at least that many seconds before the next attempt.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * worker.sync("issues", {
25
+ * database: issuesDb,
26
+ * execute: async (state, ctx) => {
27
+ * const res = await fetch("https://api.example.com/issues");
28
+ * if (res.status === 429) {
29
+ * const retryAfter = Number(res.headers.get("Retry-After"));
30
+ * throw new RateLimitError({
31
+ * retryAfter: Number.isFinite(retryAfter) ? retryAfter : undefined,
32
+ * });
33
+ * }
34
+ * // ...
35
+ * },
36
+ * });
37
+ * ```
38
+ */
39
+ export class RateLimitError extends ExecutionError {
40
+ /**
41
+ * Optional number of seconds the external API asked us to wait
42
+ * before retrying. The platform takes the max of this value and its
43
+ * own exponential-backoff delay.
44
+ */
45
+ readonly retryAfter: number | undefined;
46
+
47
+ constructor(options?: { retryAfter?: number }) {
48
+ super("Rate limited by external API");
49
+ this.name = "RateLimitError";
50
+ this.retryAfter = options?.retryAfter;
51
+ }
52
+ }
53
+
14
54
  /**
15
55
  * Helper for exhaustive switch statements. TypeScript will error if a case is not handled.
16
56
  */
package/src/index.ts CHANGED
@@ -1,4 +1,20 @@
1
1
  export { emojiIcon, imageIcon, notionIcon, place } from "./builder.js";
2
+ export type {
3
+ AiConnectorArchetype,
4
+ AiConnectorCapability,
5
+ AiConnectorChange,
6
+ AiConnectorChangeUpsert,
7
+ AiConnectorChatAuthor,
8
+ AiConnectorChatChannel,
9
+ AiConnectorChatMessage,
10
+ AiConnectorChatRecord,
11
+ AiConnectorConfiguration,
12
+ AiConnectorExecutionResult,
13
+ AiConnectorMode,
14
+ AiConnectorProjectBlock,
15
+ AiConnectorProjectRecord,
16
+ AiConnectorRecordByArchetype,
17
+ } from "./capabilities/ai_connector.js";
2
18
  export type {
3
19
  AutomationCapability,
4
20
  AutomationConfiguration,
@@ -21,7 +37,18 @@ export type {
21
37
  SyncExecutionResult,
22
38
  SyncMode,
23
39
  } from "./capabilities/sync.js";
24
- export type { ToolCapability, ToolConfiguration } from "./capabilities/tool.js";
40
+ export type {
41
+ ToolCapability,
42
+ ToolConfiguration,
43
+ ToolHints,
44
+ } from "./capabilities/tool.js";
45
+ export type {
46
+ WebhookCapability,
47
+ WebhookConfiguration,
48
+ WebhookEvent,
49
+ } from "./capabilities/webhook.js";
50
+ export { WebhookVerificationError } from "./capabilities/webhook.js";
51
+ export { RateLimitError } from "./error.js";
25
52
  export type { AnyJSONSchema, JSONSchema } from "./json-schema.js";
26
53
  export type { Infer, SchemaBuilder } from "./schema-builder.js";
27
54
  export { getSchema, j } from "./schema-builder.js";
@@ -14,6 +14,11 @@ export type PacerState = {
14
14
  pacers: Record<string, PacerEntry>;
15
15
  };
16
16
 
17
+ export type PacerDeclaration = {
18
+ readonly key: string;
19
+ readonly config: { allowedRequests: number; intervalMs: number };
20
+ };
21
+
17
22
  let pacerState: PacerState = { pacers: {} };
18
23
 
19
24
  export function setPacerState(state: PacerState): void {
@@ -24,6 +29,35 @@ export function getPacerState(): PacerState {
24
29
  return pacerState;
25
30
  }
26
31
 
32
+ /**
33
+ * Initialize pacer state from runtime context, falling back to the worker's
34
+ * own declarations for local execution.
35
+ * @internal
36
+ */
37
+ export function initPacerState(
38
+ runtimePacers: Record<string, PacerEntry> | undefined,
39
+ declarations?: readonly PacerDeclaration[],
40
+ ): void {
41
+ const pacers = runtimePacers ?? {};
42
+ if (
43
+ Object.keys(pacers).length === 0 &&
44
+ declarations &&
45
+ declarations.length > 0
46
+ ) {
47
+ const localPacers: Record<string, PacerEntry> = {};
48
+ for (const decl of declarations) {
49
+ localPacers[decl.key] = {
50
+ lastScheduledAtMs: 0,
51
+ allowedRequests: decl.config.allowedRequests,
52
+ intervalMs: decl.config.intervalMs,
53
+ };
54
+ }
55
+ setPacerState({ pacers: localPacers });
56
+ } else {
57
+ setPacerState({ pacers });
58
+ }
59
+ }
60
+
27
61
  /**
28
62
  * Core wait logic for pacing API requests.
29
63
  * Sleeps if needed to stay under the rate limit.
package/src/schema.ts CHANGED
@@ -68,10 +68,10 @@ export type PropertyConfiguration =
68
68
  | {
69
69
  type: "relation";
70
70
  /**
71
- * The export name of the sync capability that defines the related database.
72
- * This must match the export name used when defining the related sync capability.
71
+ * The key of the related database declaration.
72
+ * This must match the key passed to `worker.database()`.
73
73
  */
74
- relatedSyncKey: string;
74
+ relatedDatabaseKey: string;
75
75
  config: { twoWay: false } | { twoWay: true; relatedPropertyName: string };
76
76
  };
77
77
 
@@ -203,32 +203,49 @@ export function place(): PropertyConfiguration {
203
203
  }
204
204
 
205
205
  /**
206
- * Creates a relation property definition that references another sync capability.
207
- * The related database must be defined by a sync capability in the same worker.
206
+ * Creates a relation property definition that references another database.
207
+ * The related database must be declared in the same worker.
208
208
  *
209
- * @param relatedSyncKey - The export name of the sync capability that defines the related database.
209
+ * @param relatedDatabaseKey - The key of the related database declaration.
210
210
  * @example
211
211
  * ```typescript
212
- * export const projectsSync = sync({...});
212
+ * const projects = worker.database("projects", {
213
+ * type: "managed",
214
+ * initialTitle: "Projects",
215
+ * primaryKeyProperty: "Project ID",
216
+ * schema: {
217
+ * properties: {
218
+ * "Project Name": Schema.title(),
219
+ * "Project ID": Schema.richText(),
220
+ * },
221
+ * },
222
+ * });
213
223
  *
214
- * export const tasksSync = sync({
224
+ * const tasks = worker.database("tasks", {
225
+ * type: "managed",
226
+ * initialTitle: "Tasks",
227
+ * primaryKeyProperty: "Task ID",
215
228
  * schema: {
216
229
  * properties: {
217
230
  * "Task Title": Schema.title(),
218
- * "Project": Schema.relation("projectsSync"),
231
+ * "Project": Schema.relation("projects"),
219
232
  * },
220
233
  * },
221
- * ...
234
+ * });
235
+ *
236
+ * export const tasksSync = worker.sync("tasksSync", {
237
+ * database: tasks,
238
+ * execute: async () => ({ changes: [], hasMore: false }),
222
239
  * });
223
240
  * ```
224
241
  */
225
242
  export function relation(
226
- relatedSyncKey: string,
243
+ relatedDatabaseKey: string,
227
244
  config?: { twoWay: false } | { twoWay: true; relatedPropertyName: string },
228
245
  ): PropertyConfiguration {
229
246
  return {
230
247
  type: "relation",
231
- relatedSyncKey,
248
+ relatedDatabaseKey,
232
249
  config: config ?? { twoWay: false },
233
250
  };
234
251
  }