@notionhq/workers 0.0.82

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 (73) hide show
  1. package/README.md +22 -0
  2. package/dist/block.d.ts +321 -0
  3. package/dist/block.d.ts.map +1 -0
  4. package/dist/block.js +0 -0
  5. package/dist/builder.d.ts +117 -0
  6. package/dist/builder.d.ts.map +1 -0
  7. package/dist/builder.js +168 -0
  8. package/dist/capabilities/automation.d.ts +91 -0
  9. package/dist/capabilities/automation.d.ts.map +1 -0
  10. package/dist/capabilities/automation.js +40 -0
  11. package/dist/capabilities/automation.test.d.ts +2 -0
  12. package/dist/capabilities/automation.test.d.ts.map +1 -0
  13. package/dist/capabilities/context.d.ts +7 -0
  14. package/dist/capabilities/context.d.ts.map +1 -0
  15. package/dist/capabilities/context.js +15 -0
  16. package/dist/capabilities/oauth.d.ts +120 -0
  17. package/dist/capabilities/oauth.d.ts.map +1 -0
  18. package/dist/capabilities/oauth.js +55 -0
  19. package/dist/capabilities/oauth.test.d.ts +2 -0
  20. package/dist/capabilities/oauth.test.d.ts.map +1 -0
  21. package/dist/capabilities/sync.d.ts +180 -0
  22. package/dist/capabilities/sync.d.ts.map +1 -0
  23. package/dist/capabilities/sync.js +84 -0
  24. package/dist/capabilities/sync.test.d.ts +2 -0
  25. package/dist/capabilities/sync.test.d.ts.map +1 -0
  26. package/dist/capabilities/tool.d.ts +68 -0
  27. package/dist/capabilities/tool.d.ts.map +1 -0
  28. package/dist/capabilities/tool.js +174 -0
  29. package/dist/capabilities/tool.test.d.ts +2 -0
  30. package/dist/capabilities/tool.test.d.ts.map +1 -0
  31. package/dist/error.d.ts +12 -0
  32. package/dist/error.d.ts.map +1 -0
  33. package/dist/error.js +15 -0
  34. package/dist/icon-names.d.ts +6 -0
  35. package/dist/icon-names.d.ts.map +1 -0
  36. package/dist/icon-names.js +0 -0
  37. package/dist/index.d.ts +10 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +9 -0
  40. package/dist/json-schema.d.ts +117 -0
  41. package/dist/json-schema.d.ts.map +1 -0
  42. package/dist/json-schema.js +0 -0
  43. package/dist/json-schema.test.d.ts +2 -0
  44. package/dist/json-schema.test.d.ts.map +1 -0
  45. package/dist/schema.d.ts +178 -0
  46. package/dist/schema.d.ts.map +1 -0
  47. package/dist/schema.js +66 -0
  48. package/dist/types.d.ts +206 -0
  49. package/dist/types.d.ts.map +1 -0
  50. package/dist/types.js +0 -0
  51. package/dist/worker.d.ts +177 -0
  52. package/dist/worker.d.ts.map +1 -0
  53. package/dist/worker.js +219 -0
  54. package/package.json +68 -0
  55. package/src/block.ts +529 -0
  56. package/src/builder.ts +299 -0
  57. package/src/capabilities/automation.test.ts +152 -0
  58. package/src/capabilities/automation.ts +130 -0
  59. package/src/capabilities/context.ts +23 -0
  60. package/src/capabilities/oauth.test.ts +52 -0
  61. package/src/capabilities/oauth.ts +157 -0
  62. package/src/capabilities/sync.test.ts +104 -0
  63. package/src/capabilities/sync.ts +311 -0
  64. package/src/capabilities/tool.test.ts +351 -0
  65. package/src/capabilities/tool.ts +279 -0
  66. package/src/error.ts +19 -0
  67. package/src/icon-names.ts +890 -0
  68. package/src/index.ts +34 -0
  69. package/src/json-schema.test.ts +719 -0
  70. package/src/json-schema.ts +179 -0
  71. package/src/schema.ts +241 -0
  72. package/src/types.ts +272 -0
  73. package/src/worker.ts +285 -0
package/src/builder.ts ADDED
@@ -0,0 +1,299 @@
1
+ import type { NoticonName } from "./icon-names.js";
2
+ import type {
3
+ DateValue,
4
+ Icon,
5
+ NoticonColor,
6
+ PeopleValue,
7
+ PlaceValue,
8
+ RelationReference,
9
+ TextValue,
10
+ } from "./types.js";
11
+
12
+ /**
13
+ * Creates a rich text value.
14
+ */
15
+ export function richText(content: string): TextValue {
16
+ return [[content]];
17
+ }
18
+
19
+ /**
20
+ * Creates a URL value.
21
+ */
22
+ export function url(url: string): TextValue {
23
+ return [[url]];
24
+ }
25
+
26
+ /**
27
+ * Creates a title value.
28
+ */
29
+ export function title(content: string): TextValue {
30
+ return [[content]];
31
+ }
32
+
33
+ /**
34
+ * Creates a text value.
35
+ */
36
+ export function text(content: string): TextValue {
37
+ return [[content]];
38
+ }
39
+
40
+ /**
41
+ * Creates an email value.
42
+ */
43
+ export function email(email: string): TextValue {
44
+ return [[email]];
45
+ }
46
+
47
+ /**
48
+ * Creates a phone number value.
49
+ */
50
+ export function phoneNumber(phone: string): TextValue {
51
+ return [[phone]];
52
+ }
53
+
54
+ /**
55
+ * Creates a checkbox value.
56
+ */
57
+ export function checkbox(checked: boolean): TextValue {
58
+ return checked ? [["Yes"]] : [["No"]];
59
+ }
60
+
61
+ /**
62
+ * Creates a file URL value.
63
+ * @param fileUrl - The URL of the file
64
+ * @param fileName - Optional display name for the file (defaults to URL)
65
+ */
66
+ export function file(fileUrl: string, fileName?: string): TextValue {
67
+ return [[fileName ?? fileUrl, [["a", fileUrl]]]];
68
+ }
69
+
70
+ /**
71
+ * Creates a number value.
72
+ */
73
+ export function number(value: number): TextValue {
74
+ if (Number.isNaN(value)) {
75
+ return [];
76
+ }
77
+ return [[value.toString()]];
78
+ }
79
+
80
+ /**
81
+ * Creates a date value from a date string (YYYY-MM-DD).
82
+ */
83
+ export function date(dateString: string): TextValue {
84
+ validateDateString(dateString);
85
+
86
+ const dateValue: DateValue = {
87
+ type: "date",
88
+ start_date: dateString,
89
+ };
90
+
91
+ return createDateToken(dateValue);
92
+ }
93
+
94
+ /**
95
+ * Creates a datetime value from a Date object.
96
+ */
97
+ export function dateTime(date: Date, timeZone?: string): TextValue {
98
+ const dateValue: DateValue = {
99
+ type: "datetime",
100
+ start_date: formatDate(date),
101
+ start_time: formatTime(date),
102
+ };
103
+
104
+ if (timeZone) {
105
+ dateValue.time_zone = timeZone;
106
+ }
107
+
108
+ return createDateToken(dateValue);
109
+ }
110
+
111
+ /**
112
+ * Creates a date range value from date strings.
113
+ */
114
+ export function dateRange(startDate: string, endDate: string): TextValue {
115
+ validateDateString(startDate);
116
+ validateDateString(endDate);
117
+
118
+ const dateValue: DateValue = {
119
+ type: "daterange",
120
+ start_date: startDate,
121
+ end_date: endDate,
122
+ };
123
+
124
+ return createDateToken(dateValue);
125
+ }
126
+
127
+ /**
128
+ * Creates a datetime range value from Date objects.
129
+ */
130
+ export function dateTimeRange(
131
+ startDate: Date,
132
+ endDate: Date,
133
+ timeZone?: string,
134
+ ): TextValue {
135
+ const dateValue: DateValue = {
136
+ type: "datetimerange",
137
+ start_date: formatDate(startDate),
138
+ start_time: formatTime(startDate),
139
+ end_date: formatDate(endDate),
140
+ end_time: formatTime(endDate),
141
+ };
142
+
143
+ if (timeZone) {
144
+ dateValue.time_zone = timeZone;
145
+ }
146
+
147
+ return createDateToken(dateValue);
148
+ }
149
+
150
+ /**
151
+ * Creates a link with custom display text.
152
+ * @param displayText - The text to display
153
+ * @param url - The URL to link to
154
+ */
155
+ export function link(displayText: string, url: string): TextValue {
156
+ return [[displayText, [["a", url]]]];
157
+ }
158
+
159
+ /**
160
+ * Creates a select value from a single option.
161
+ */
162
+ export function select(value: string): TextValue {
163
+ return [[value]];
164
+ }
165
+
166
+ /**
167
+ * Creates a multi-select value from multiple options.
168
+ * @param values - Array of option names to select
169
+ */
170
+ export function multiSelect(...values: string[]): TextValue {
171
+ if (values.length === 0) {
172
+ return [];
173
+ }
174
+ return [[values.join(",")]];
175
+ }
176
+
177
+ /**
178
+ * Creates a status value from a status option name.
179
+ */
180
+ export function status(value: string): TextValue {
181
+ return [[value]];
182
+ }
183
+
184
+ /**
185
+ * Creates a people value from email addresses.
186
+ * @param emails - Array of email addresses for people to include
187
+ */
188
+ export function people(...emails: string[]): PeopleValue {
189
+ return emails.map((email) => ({ email }));
190
+ }
191
+
192
+ /**
193
+ * Creates a place value for a geographic location.
194
+ * @param value - The place value with lat/lon coordinates and optional name/address
195
+ */
196
+ export function place(value: PlaceValue): PlaceValue {
197
+ if (typeof value.lat !== "number" || typeof value.lon !== "number") {
198
+ throw new Error("Place value must have numeric lat and lon coordinates");
199
+ }
200
+ return value;
201
+ }
202
+
203
+ /**
204
+ * Creates a relation reference from a primary key of a related record.
205
+ * Use an array of relation references as the property value.
206
+ *
207
+ * @param primaryKey - The primary key of the related record
208
+ * @example
209
+ * ```typescript
210
+ * // Single relation
211
+ * { Project: [Builder.relation("project-123")] }
212
+ *
213
+ * // Multiple relations
214
+ * { Projects: [Builder.relation("project-123"), Builder.relation("project-456")] }
215
+ * ```
216
+ */
217
+ export function relation(primaryKey: string): RelationReference {
218
+ return { type: "primaryKey", value: primaryKey };
219
+ }
220
+
221
+ /**
222
+ * Validates a date string is in YYYY-MM-DD format and represents a valid date.
223
+ */
224
+ function validateDateString(dateString: string): void {
225
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(dateString)) {
226
+ throw new Error(
227
+ `Invalid date format: ${dateString}. Expected YYYY-MM-DD format.`,
228
+ );
229
+ }
230
+
231
+ const date = new Date(dateString);
232
+ if (Number.isNaN(date.getTime())) {
233
+ throw new Error(`Invalid date: ${dateString}`);
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Internal helper to create a date mention token from a DateValue object.
239
+ */
240
+ function createDateToken(dateValue: DateValue): TextValue {
241
+ return [["\u2023", [["d", dateValue]]]];
242
+ }
243
+
244
+ /**
245
+ * Formats a Date object to YYYY-MM-DD string.
246
+ */
247
+ function formatDate(date: Date): string {
248
+ const year = date.getFullYear();
249
+ const month = String(date.getMonth() + 1).padStart(2, "0");
250
+ const day = String(date.getDate()).padStart(2, "0");
251
+ return `${year}-${month}-${day}`;
252
+ }
253
+
254
+ /**
255
+ * Formats a Date object to HH:mm string.
256
+ */
257
+ function formatTime(date: Date): string {
258
+ const hours = String(date.getHours()).padStart(2, "0");
259
+ const minutes = String(date.getMinutes()).padStart(2, "0");
260
+ return `${hours}:${minutes}`;
261
+ }
262
+
263
+ /**
264
+ * Creates an emoji icon.
265
+ * @param emoji - An emoji string (e.g., "🎯", "✨", "🚀")
266
+ */
267
+ export function emojiIcon(emoji: string): Icon {
268
+ return {
269
+ type: "emoji",
270
+ value: emoji,
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Creates a Notion icon with a specific color.
276
+ * @param icon - The name of the Notion icon (e.g., "checkmark", "pizza", "rocket")
277
+ * @param color - The color variant (defaults to "gray")
278
+ */
279
+ export function notionIcon(
280
+ icon: NoticonName,
281
+ color: NoticonColor = "gray",
282
+ ): Icon {
283
+ return {
284
+ type: "notion",
285
+ icon,
286
+ color,
287
+ };
288
+ }
289
+
290
+ /**
291
+ * Creates an image icon from an external URL.
292
+ * @param url - The URL of the image (e.g., "https://example.com/icon.png")
293
+ */
294
+ export function imageIcon(url: string): Icon {
295
+ return {
296
+ type: "image",
297
+ url,
298
+ };
299
+ }
@@ -0,0 +1,152 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ type Mock,
8
+ vi,
9
+ } from "vitest";
10
+ import { ExecutionError } from "../error.js";
11
+ import type { PageObjectResponse } from "./automation.js";
12
+ import { createAutomationCapability } from "./automation.js";
13
+
14
+ describe("createAutomationCapability", () => {
15
+ let stdoutSpy: Mock<typeof process.stdout.write>;
16
+
17
+ beforeEach(() => {
18
+ stdoutSpy = vi
19
+ .spyOn(process.stdout, "write")
20
+ .mockImplementation(() => true);
21
+ });
22
+
23
+ afterEach(() => {
24
+ vi.restoreAllMocks();
25
+ });
26
+
27
+ it("creates automation capability with correct structure", () => {
28
+ const capability = createAutomationCapability("testAutomation", {
29
+ title: "Test Automation",
30
+ description: "Test automation description",
31
+ execute: () => {},
32
+ });
33
+
34
+ expect(capability).toBeDefined();
35
+ expect(capability?._tag).toBe("automation");
36
+ expect(capability.config.title).toBe("Test Automation");
37
+ expect(capability.config.description).toBe("Test automation description");
38
+ });
39
+
40
+ it("executes automation without page data", async () => {
41
+ const executeFn = vi.fn();
42
+ const capability = createAutomationCapability("syncAutomation", {
43
+ title: "Sync Automation",
44
+ description: "Executes synchronously",
45
+ execute: executeFn,
46
+ });
47
+
48
+ const event = {
49
+ pageId: "page-123",
50
+ actionType: "test_action",
51
+ };
52
+
53
+ await capability.handler(event);
54
+
55
+ expect(executeFn).toHaveBeenCalledWith(
56
+ event,
57
+ expect.objectContaining({
58
+ notion: expect.any(Object),
59
+ }),
60
+ );
61
+ expect(stdoutSpy).toHaveBeenCalledWith(
62
+ `\n<output>{"status":"success"}</output>\n`,
63
+ );
64
+ });
65
+
66
+ it("executes automation with page data", async () => {
67
+ const executeFn = vi.fn();
68
+ const capability = createAutomationCapability("pageAutomation", {
69
+ title: "Page Automation",
70
+ description: "Processes page data",
71
+ execute: executeFn,
72
+ });
73
+
74
+ const pageData: PageObjectResponse = {
75
+ object: "page",
76
+ id: "page-789",
77
+ created_time: "2023-01-01T00:00:00.000Z",
78
+ last_edited_time: "2023-01-02T00:00:00.000Z",
79
+ created_by: { id: "user-1" },
80
+ last_edited_by: { id: "user-2" },
81
+ cover: null,
82
+ icon: null,
83
+ parent: { type: "database_id", database_id: "db-123" },
84
+ archived: false,
85
+ properties: {
86
+ Name: { id: "title", type: "title", title: [] },
87
+ Status: { id: "status", type: "status", status: { name: "Done" } },
88
+ },
89
+ url: "https://notion.so/page-789",
90
+ public_url: null,
91
+ };
92
+
93
+ const event = {
94
+ pageId: "page-789",
95
+ actionType: "process_page",
96
+ pageData,
97
+ };
98
+
99
+ await capability.handler(event);
100
+
101
+ expect(executeFn).toHaveBeenCalledWith(
102
+ event,
103
+ expect.objectContaining({
104
+ notion: expect.any(Object),
105
+ }),
106
+ );
107
+ expect(stdoutSpy).toHaveBeenCalledWith(
108
+ `\n<output>{"status":"success"}</output>\n`,
109
+ );
110
+ });
111
+
112
+ it("handles execution error from function", async () => {
113
+ const capability = createAutomationCapability("errorAutomation", {
114
+ title: "Error Automation",
115
+ description: "Throws an error",
116
+ execute: () => {
117
+ throw new Error("Something went wrong");
118
+ },
119
+ });
120
+
121
+ const event = {
122
+ pageId: "page-error",
123
+ actionType: "error_action",
124
+ };
125
+
126
+ await expect(capability.handler(event)).rejects.toThrow(ExecutionError);
127
+
128
+ expect(stdoutSpy).toHaveBeenCalledWith(
129
+ `\n<output>{"status":"error","error":{"name":"ExecutionError","message":"Error during worker execution: Error: Something went wrong"}}</output>\n`,
130
+ );
131
+ });
132
+
133
+ it("handles execution error with non-Error object", async () => {
134
+ const capability = createAutomationCapability("nonErrorAutomation", {
135
+ title: "Non-Error Automation",
136
+ description: "Throws a non-Error object",
137
+ execute: () => {
138
+ throw "String error";
139
+ },
140
+ });
141
+
142
+ const event = {
143
+ actionType: "string_error_action",
144
+ };
145
+
146
+ await expect(capability.handler(event)).rejects.toThrow(ExecutionError);
147
+
148
+ expect(stdoutSpy).toHaveBeenCalledWith(
149
+ `\n<output>{"status":"error","error":{"name":"ExecutionError","message":"Error during worker execution: String error"}}</output>\n`,
150
+ );
151
+ });
152
+ });
@@ -0,0 +1,130 @@
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
+
6
+ /**
7
+ * Event provided to automation execute functions
8
+ */
9
+ export interface AutomationEvent {
10
+ /**
11
+ * The ID of the page that triggered the automation (if applicable)
12
+ */
13
+ pageId?: string;
14
+ /**
15
+ * The type of automation action that was triggered
16
+ */
17
+ actionType: string;
18
+ /**
19
+ * The full page object from Notion's Public API (if triggered by a database page)
20
+ */
21
+ pageData?: PageObjectResponse;
22
+ }
23
+
24
+ /**
25
+ * Page object from Notion's Public API
26
+ * This represents a database page with all its properties.
27
+ * Properties are in Notion's Public API format.
28
+ * See: https://developers.notion.com/reference/page
29
+ */
30
+ export interface PageObjectResponse {
31
+ object: "page";
32
+ id: string;
33
+ created_time: string;
34
+ last_edited_time: string;
35
+ created_by: { id: string };
36
+ last_edited_by: { id: string };
37
+ cover: unknown;
38
+ icon: unknown;
39
+ parent: unknown;
40
+ archived: boolean;
41
+ properties: Record<string, unknown>;
42
+ url: string;
43
+ public_url?: string | null;
44
+ }
45
+
46
+ /**
47
+ * Configuration for an automation capability
48
+ */
49
+ export interface AutomationConfiguration {
50
+ /**
51
+ * Title of the automation - shown in the UI when selecting automations
52
+ */
53
+ title: string;
54
+
55
+ /**
56
+ * Description of what this automation does - shown in the UI
57
+ */
58
+ description: string;
59
+
60
+ /**
61
+ * The function that executes when the automation is triggered
62
+ * @param event - Event data about the automation trigger, including page data if applicable
63
+ * @param context - The capability execution context (Notion client, etc.)
64
+ * @returns A promise that resolves when the automation completes
65
+ */
66
+ execute: (
67
+ event: AutomationEvent,
68
+ context: CapabilityContext,
69
+ ) => Promise<void> | void;
70
+ }
71
+
72
+ /**
73
+ * Result returned from automation execution
74
+ */
75
+ export interface AutomationHandlerResult {
76
+ title: string;
77
+ description: string;
78
+ }
79
+
80
+ export type AutomationCapability = ReturnType<
81
+ typeof createAutomationCapability
82
+ >;
83
+
84
+ /**
85
+ * Creates an automation capability from configuration.
86
+ *
87
+ * @param key - The unique name for this capability.
88
+ * @param config - The automation configuration.
89
+ * @returns The capability object.
90
+ */
91
+ export function createAutomationCapability(
92
+ key: string,
93
+ config: AutomationConfiguration,
94
+ ) {
95
+ return {
96
+ _tag: "automation" as const,
97
+ key,
98
+ config: {
99
+ title: config.title,
100
+ description: config.description,
101
+ },
102
+ async handler(
103
+ event: AutomationEvent,
104
+ options?: HandlerOptions,
105
+ ): Promise<{ status: "success" } | undefined> {
106
+ try {
107
+ const capabilityContext = createCapabilityContext();
108
+ await config.execute(event, capabilityContext);
109
+
110
+ if (options?.concreteOutput) {
111
+ return { status: "success" };
112
+ } else {
113
+ process.stdout.write(
114
+ `\n<output>${JSON.stringify({ status: "success" })}</output>\n`,
115
+ );
116
+ }
117
+ } catch (err) {
118
+ const error = new ExecutionError(err);
119
+
120
+ if (!options?.concreteOutput) {
121
+ process.stdout.write(
122
+ `\n<output>${JSON.stringify({ status: "error", error: { name: error.name, message: error.message } })}</output>\n`,
123
+ );
124
+ }
125
+
126
+ throw error;
127
+ }
128
+ },
129
+ };
130
+ }
@@ -0,0 +1,23 @@
1
+ import { Client } from "@notionhq/client";
2
+ import type { ClientOptions } from "@notionhq/client/build/src/Client.js";
3
+
4
+ export type CapabilityContext = {
5
+ /** Notion API SDK client for this execution. */
6
+ notion: Client;
7
+ };
8
+
9
+ export function createCapabilityContext(): CapabilityContext {
10
+ const options: ClientOptions = {};
11
+
12
+ if (process.env.NOTION_API_BASE_URL) {
13
+ options.baseUrl = process.env.NOTION_API_BASE_URL;
14
+ }
15
+
16
+ if (process.env.NOTION_API_TOKEN) {
17
+ options.auth = process.env.NOTION_API_TOKEN;
18
+ }
19
+
20
+ const notion = new Client(options);
21
+
22
+ return { notion };
23
+ }
@@ -0,0 +1,52 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { createOAuthCapability } from "./oauth.js";
3
+
4
+ describe("oauth", () => {
5
+ afterEach(() => {
6
+ // Clean up env changes between tests
7
+ delete process.env.OAUTH_676F6F676C6541757468_ACCESS_TOKEN;
8
+ delete process.env.OAUTH_676F6F676C652D63616C656E646172_ACCESS_TOKEN;
9
+ });
10
+
11
+ it("creates notion-managed oauth capability with accessToken helper", async () => {
12
+ const capability = createOAuthCapability("googleAuth", {
13
+ name: "googleAuth",
14
+ provider: "google",
15
+ accessTokenExpireMs: 60_000,
16
+ });
17
+
18
+ expect(capability).toBeDefined();
19
+ expect(capability?._tag).toBe("oauth");
20
+ expect(capability.config.type).toBe("notion_managed");
21
+ expect(capability.config.accessTokenExpireMs).toBe(60_000);
22
+
23
+ expect(typeof capability.accessToken).toBe("function");
24
+
25
+ process.env.OAUTH_676F6F676C6541757468_ACCESS_TOKEN = "token-123";
26
+ await expect(capability.accessToken()).resolves.toBe("token-123");
27
+ });
28
+
29
+ it("normalizes non-alphanumeric characters in the identifier", () => {
30
+ const capability = createOAuthCapability("googleCalendar", {
31
+ name: "google-calendar",
32
+ provider: "google",
33
+ accessTokenExpireMs: 3600_000,
34
+ });
35
+
36
+ expect(capability).toBeDefined();
37
+ expect(capability?._tag).toBe("oauth");
38
+ expect(capability.config.type).toBe("notion_managed");
39
+ expect(capability.config.accessTokenExpireMs).toBe(3600_000);
40
+ });
41
+
42
+ it("throws a helpful error when the token env var is missing", async () => {
43
+ const capability = createOAuthCapability("googleAuthMissing", {
44
+ name: "googleAuth",
45
+ provider: "google",
46
+ });
47
+
48
+ await expect(capability.accessToken()).rejects.toThrow(
49
+ /Missing OAuth access token env var "OAUTH_676F6F676C6541757468_ACCESS_TOKEN"/,
50
+ );
51
+ });
52
+ });