@notionhq/apps 0.0.13 → 0.0.14
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/README.md +32 -28
- package/dist/box.generated.d.ts +164 -0
- package/dist/box.generated.d.ts.map +1 -0
- package/dist/box.generated.js +24 -0
- package/dist/calendar.generated.d.ts +1036 -0
- package/dist/calendar.generated.d.ts.map +1 -0
- package/dist/calendar.generated.js +42 -0
- package/dist/cli/build.js +7 -6
- package/dist/confluence.generated.d.ts +98 -0
- package/dist/confluence.generated.d.ts.map +1 -0
- package/dist/confluence.generated.js +18 -0
- package/dist/connection-actions.d.ts +2 -0
- package/dist/connection-actions.d.ts.map +1 -0
- package/dist/connection-actions.js +0 -0
- package/dist/connection-trigger-definitions.generated.d.ts +34 -0
- package/dist/connection-trigger-definitions.generated.d.ts.map +1 -0
- package/dist/connection-trigger-definitions.generated.js +45 -0
- package/dist/connections.d.ts +15 -0
- package/dist/connections.d.ts.map +1 -0
- package/dist/connections.js +79 -0
- package/dist/connections.test.d.ts +2 -0
- package/dist/connections.test.d.ts.map +1 -0
- package/dist/cursor.generated.d.ts +154 -0
- package/dist/cursor.generated.d.ts.map +1 -0
- package/dist/cursor.generated.js +18 -0
- package/dist/discord.generated.d.ts +279 -0
- package/dist/discord.generated.d.ts.map +1 -0
- package/dist/discord.generated.js +33 -0
- package/dist/gmail.generated.d.ts +184 -0
- package/dist/gmail.generated.d.ts.map +1 -0
- package/dist/gmail.generated.js +21 -0
- package/dist/googleCalendar.generated.d.ts +83 -0
- package/dist/googleCalendar.generated.d.ts.map +1 -0
- package/dist/googleCalendar.generated.js +15 -0
- package/dist/googleDrive.generated.d.ts +210 -0
- package/dist/googleDrive.generated.d.ts.map +1 -0
- package/dist/googleDrive.generated.js +27 -0
- package/dist/googleDriveOauth.generated.d.ts +851 -0
- package/dist/googleDriveOauth.generated.d.ts.map +1 -0
- package/dist/googleDriveOauth.generated.js +84 -0
- package/dist/outlook.generated.d.ts +224 -0
- package/dist/outlook.generated.d.ts.map +1 -0
- package/dist/outlook.generated.js +24 -0
- package/dist/providers.generated.d.ts +117 -0
- package/dist/providers.generated.d.ts.map +1 -0
- package/dist/providers.generated.js +108 -0
- package/dist/salesforce.generated.d.ts +151 -0
- package/dist/salesforce.generated.d.ts.map +1 -0
- package/dist/salesforce.generated.js +24 -0
- package/dist/slack.generated.d.ts +290 -0
- package/dist/slack.generated.d.ts.map +1 -0
- package/dist/slack.generated.js +30 -0
- package/dist/triggers.generated.d.ts +107 -32
- package/dist/triggers.generated.d.ts.map +1 -1
- package/dist/triggers.generated.js +36 -24
- package/dist/workflow-connections-types.test.d.ts +2 -0
- package/dist/workflow-connections-types.test.d.ts.map +1 -0
- package/dist/workflow-types.test.d.ts +2 -0
- package/dist/workflow-types.test.d.ts.map +1 -0
- package/dist/workflow.d.ts +20 -16
- package/dist/workflow.d.ts.map +1 -1
- package/dist/workflow.js +14 -17
- package/docs/BUILD.md +83 -14
- package/docs/CONNECTIONS.md +129 -0
- package/package.json +1 -1
- package/src/box.generated.ts +187 -0
- package/src/calendar.generated.ts +1117 -0
- package/src/cli/build.test.ts +33 -0
- package/src/cli/build.ts +6 -5
- package/src/confluence.generated.ts +109 -0
- package/src/connection-actions.ts +2 -0
- package/src/connection-trigger-definitions.generated.ts +48 -0
- package/src/connections.test.ts +157 -0
- package/src/connections.ts +126 -0
- package/src/cursor.generated.ts +167 -0
- package/src/discord.generated.ts +318 -0
- package/src/gmail.generated.ts +205 -0
- package/src/googleCalendar.generated.ts +95 -0
- package/src/googleDrive.generated.ts +235 -0
- package/src/googleDriveOauth.generated.ts +980 -0
- package/src/outlook.generated.ts +249 -0
- package/src/providers.generated.ts +207 -0
- package/src/salesforce.generated.ts +170 -0
- package/src/slack.generated.ts +330 -0
- package/src/triggers.generated.ts +112 -32
- package/src/workflow-connections-types.test.ts +59 -0
- package/src/workflow-types.test.ts +144 -0
- package/src/workflow.test.ts +112 -2
- package/src/workflow.ts +58 -29
package/src/cli/build.test.ts
CHANGED
|
@@ -216,6 +216,39 @@ describe("buildApp", () => {
|
|
|
216
216
|
});
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
+
it("removes provisioning when a rebuild no longer declares Notion resources", async () => {
|
|
220
|
+
const root = await fs.promises.mkdtemp(path.join(FIXTURES, "provisioning-rebuild-"));
|
|
221
|
+
try {
|
|
222
|
+
await fs.promises.cp(
|
|
223
|
+
path.join(FIXTURES, "notion-as-code", "src"),
|
|
224
|
+
path.join(root, "src"),
|
|
225
|
+
{
|
|
226
|
+
recursive: true,
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
await buildApp(root);
|
|
230
|
+
const provisioningPath = path.join(root, "dist", "provisioning.json");
|
|
231
|
+
const provisioning = JSON.parse(await fs.promises.readFile(provisioningPath, "utf8"));
|
|
232
|
+
expect(provisioning.intents).toEqual(
|
|
233
|
+
expect.arrayContaining([expect.objectContaining({ resourceId: "issues-db" })]),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
await fs.promises.rm(path.join(root, "src"), { recursive: true });
|
|
237
|
+
await fs.promises.cp(path.join(EXAMPLES, "workflow", "src"), path.join(root, "src"), {
|
|
238
|
+
recursive: true,
|
|
239
|
+
});
|
|
240
|
+
const { manifest } = await buildApp(root);
|
|
241
|
+
expect(manifest.capabilities).toEqual([
|
|
242
|
+
expect.objectContaining({ type: "workflow", key: "onPageCreated" }),
|
|
243
|
+
]);
|
|
244
|
+
await expect(fs.promises.readFile(provisioningPath)).rejects.toMatchObject({
|
|
245
|
+
code: "ENOENT",
|
|
246
|
+
});
|
|
247
|
+
} finally {
|
|
248
|
+
await fs.promises.rm(root, { recursive: true, force: true });
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
219
252
|
it("builds a workflow bundle and manifest", async () => {
|
|
220
253
|
const { manifest, bundlePath, manifestPath } = await buildApp(example("workflow"));
|
|
221
254
|
|
package/src/cli/build.ts
CHANGED
|
@@ -72,13 +72,14 @@ async function emitProvisioningArtifact(
|
|
|
72
72
|
capabilities: readonly DiscoveredCapability[],
|
|
73
73
|
): Promise<void> {
|
|
74
74
|
const entityProvisioning = await buildMetadata(projectRoot, capabilities);
|
|
75
|
-
|
|
75
|
+
const provisioningPath = path.join(projectRoot, "dist", "provisioning.json");
|
|
76
|
+
if (entityProvisioning.intents.length === 0) {
|
|
77
|
+
await fs.promises.rm(provisioningPath, { force: true });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
76
80
|
|
|
77
81
|
const artifact = createProvisioningArtifact(entityProvisioning.intents);
|
|
78
|
-
await fs.promises.writeFile(
|
|
79
|
-
path.join(projectRoot, "dist", "provisioning.json"),
|
|
80
|
-
`${JSON.stringify(artifact, null, "\t")}\n`,
|
|
81
|
-
);
|
|
82
|
+
await fs.promises.writeFile(provisioningPath, `${JSON.stringify(artifact, null, "\t")}\n`);
|
|
82
83
|
}
|
|
83
84
|
|
|
84
85
|
/** Bundle capabilities separately to record their Notion-as-Code declarations without shipping them. */
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Generated by notion tool-core codegen-script-types --connections. Do not edit.
|
|
2
|
+
import type { ConnectionActionInvoker } from "./connection-actions.js"
|
|
3
|
+
/**
|
|
4
|
+
* Script input for the "Query Confluence with CQL" effect (`confluence.cqlQuery`),
|
|
5
|
+
* derived from its Tool Core definition's call_function input projection.
|
|
6
|
+
*
|
|
7
|
+
* Run a CQL query and return results.
|
|
8
|
+
*/
|
|
9
|
+
export type CqlQueryScriptInput = {
|
|
10
|
+
/**
|
|
11
|
+
* CQL query string. Only read-only queries are supported. Example: type=page AND space=DEV AND text~"deployment"
|
|
12
|
+
*/
|
|
13
|
+
query: string
|
|
14
|
+
/**
|
|
15
|
+
* Maximum number of results to return. Defaults to 20.
|
|
16
|
+
*/
|
|
17
|
+
maxResults?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Canonical Tool Core output for the "Query Confluence with CQL" effect (`confluence.cqlQuery`).
|
|
22
|
+
* This is the definition's output schema; the effect's script surface may
|
|
23
|
+
* re-encode it before returning.
|
|
24
|
+
*/
|
|
25
|
+
export type CqlQueryScriptOutput = {
|
|
26
|
+
results: Array<{
|
|
27
|
+
id: string
|
|
28
|
+
title: string
|
|
29
|
+
blocks: Array<string>
|
|
30
|
+
}>
|
|
31
|
+
query: string
|
|
32
|
+
baseUrl: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Script input for the "Load Confluence page" effect (`confluence.loadPage`),
|
|
37
|
+
* derived from its Tool Core definition's call_function input projection.
|
|
38
|
+
*
|
|
39
|
+
* Load a Confluence page by ID.
|
|
40
|
+
*/
|
|
41
|
+
export type LoadPageScriptInput = {
|
|
42
|
+
/**
|
|
43
|
+
* The Confluence page ID to load.
|
|
44
|
+
*/
|
|
45
|
+
pageId: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Canonical Tool Core output for the "Load Confluence page" effect (`confluence.loadPage`).
|
|
50
|
+
* This is the definition's output schema; the effect's script surface may
|
|
51
|
+
* re-encode it before returning.
|
|
52
|
+
*/
|
|
53
|
+
export type LoadPageScriptOutput = {
|
|
54
|
+
type: "confluence-page"
|
|
55
|
+
title: string
|
|
56
|
+
blocks: Array<string>
|
|
57
|
+
pageId: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Script input for the "Search Confluence" effect (`confluence.search`),
|
|
62
|
+
* derived from its Tool Core definition's call_function input projection.
|
|
63
|
+
*
|
|
64
|
+
* Search Confluence via the configured connector.
|
|
65
|
+
*/
|
|
66
|
+
export type SearchScriptInput = {
|
|
67
|
+
/**
|
|
68
|
+
* A single, focused question to search for.
|
|
69
|
+
*/
|
|
70
|
+
question: string
|
|
71
|
+
/**
|
|
72
|
+
* Brief keywords capturing the core entities.
|
|
73
|
+
*/
|
|
74
|
+
keywords?: string
|
|
75
|
+
/**
|
|
76
|
+
* Optional time window. Use only "default", "all_time", a duration matching /^\d+[dwmy]$/ (for example, "7d", "2w", "3m", or "1y"), or a date in "YYYY-MM-DD" format. Do not use natural-language durations or combine units.
|
|
77
|
+
*/
|
|
78
|
+
lookback?: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Canonical Tool Core output for the "Search Confluence" effect (`confluence.search`).
|
|
83
|
+
* This is the definition's output schema; the effect's script surface may
|
|
84
|
+
* re-encode it before returning.
|
|
85
|
+
*/
|
|
86
|
+
export type SearchScriptOutput = {
|
|
87
|
+
results: Array<{
|
|
88
|
+
id: string
|
|
89
|
+
title: string
|
|
90
|
+
path: string
|
|
91
|
+
text: string
|
|
92
|
+
lastEdited: string
|
|
93
|
+
isPrivate: boolean
|
|
94
|
+
pageId: string
|
|
95
|
+
}>
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class ConfluenceConnectionClient {
|
|
99
|
+
constructor(private readonly invoke: ConnectionActionInvoker) {}
|
|
100
|
+
cqlQuery(args: CqlQueryScriptInput): Promise<CqlQueryScriptOutput> {
|
|
101
|
+
return this.invoke<CqlQueryScriptOutput>("cqlQuery", args)
|
|
102
|
+
}
|
|
103
|
+
loadPage(args: LoadPageScriptInput): Promise<LoadPageScriptOutput> {
|
|
104
|
+
return this.invoke<LoadPageScriptOutput>("loadPage", args)
|
|
105
|
+
}
|
|
106
|
+
search(args: SearchScriptInput): Promise<SearchScriptOutput> {
|
|
107
|
+
return this.invoke<SearchScriptOutput>("search", args)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Generated by notion tool-core codegen-script-types --connections. Do not edit.
|
|
2
|
+
export const connectionTriggerDefinitions = [
|
|
3
|
+
{
|
|
4
|
+
type: "calendar.event.created",
|
|
5
|
+
provider: "calendar",
|
|
6
|
+
description: "Triggers immediately when a calendar event is created",
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
type: "calendar.event.updated",
|
|
10
|
+
provider: "calendar",
|
|
11
|
+
description: "Triggers immediately when a calendar event is updated",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
type: "calendar.event.canceled",
|
|
15
|
+
provider: "calendar",
|
|
16
|
+
description: "Triggers immediately when a calendar event is canceled",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
type: "slack.message",
|
|
20
|
+
provider: "slack",
|
|
21
|
+
description:
|
|
22
|
+
"Triggers immediately after a new message is posted in Slack (includes new threads and replies)",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: "slack.reaction.added",
|
|
26
|
+
provider: "slack",
|
|
27
|
+
description:
|
|
28
|
+
"Triggers immediately after a reaction is added to a message in Slack",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
type: "slack.app.mention",
|
|
32
|
+
provider: "slack",
|
|
33
|
+
description:
|
|
34
|
+
"Triggers when the agent is mentioned in a Slack channel or direct message",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
type: "googleDriveOauth.filesChanged",
|
|
38
|
+
provider: "googleDriveOauth",
|
|
39
|
+
description:
|
|
40
|
+
"Watches for file changes in a specific Google Drive folder (direct children only) or shared drive. The My Drive root is not supported. Fires on a schedule and provides change tokens for fetching the list of changes.",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
type: "discord.interaction",
|
|
44
|
+
provider: "discord",
|
|
45
|
+
description:
|
|
46
|
+
"Triggers when a user invokes the /ai slash command in a configured Discord channel",
|
|
47
|
+
},
|
|
48
|
+
] as const
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Client } from "@notionhq/client";
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
connections,
|
|
5
|
+
createWorkflowConnections,
|
|
6
|
+
validateConnectionRequirements,
|
|
7
|
+
} from "./connections.js";
|
|
8
|
+
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
vi.unstubAllEnvs();
|
|
11
|
+
vi.restoreAllMocks();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
describe("connection requirements", () => {
|
|
15
|
+
it("declares generated providers", () => {
|
|
16
|
+
expect(connections.calendar()).toEqual({ type: "calendar" });
|
|
17
|
+
expect(connections.box({ key: "documents" })).toEqual({ type: "box", key: "documents" });
|
|
18
|
+
expect(connections.googleDriveOauth()).toEqual({ type: "googleDriveOauth" });
|
|
19
|
+
expect(connections.discord({ key: "community" })).toEqual({
|
|
20
|
+
type: "discord",
|
|
21
|
+
key: "community",
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("rejects duplicate keys across providers", () => {
|
|
26
|
+
expect(() =>
|
|
27
|
+
validateConnectionRequirements([
|
|
28
|
+
connections.calendar({ key: "work" }),
|
|
29
|
+
connections.slack({ key: "work" }),
|
|
30
|
+
]),
|
|
31
|
+
).toThrow("Duplicate");
|
|
32
|
+
});
|
|
33
|
+
it("rejects invalid keys and excessive requirements", () => {
|
|
34
|
+
expect(() =>
|
|
35
|
+
validateConnectionRequirements([connections.slack({ key: "bad/key" })]),
|
|
36
|
+
).toThrow("Invalid");
|
|
37
|
+
expect(() =>
|
|
38
|
+
validateConnectionRequirements(
|
|
39
|
+
Array.from({ length: 101 }, (_, i) => connections.slack({ key: `team${i}` })),
|
|
40
|
+
),
|
|
41
|
+
).toThrow("100");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("workflow connection runtime", () => {
|
|
46
|
+
it("preserves pagination through a generated OAuth provider method", async () => {
|
|
47
|
+
vi.stubEnv(
|
|
48
|
+
"NOTION_CONNECTION_BINDINGS",
|
|
49
|
+
'[{"key":"documents","type":"googleDriveOauth","id":"drive-id"}]',
|
|
50
|
+
);
|
|
51
|
+
const notion = new Client({ auth: "test" });
|
|
52
|
+
const request = vi
|
|
53
|
+
.spyOn(notion, "request")
|
|
54
|
+
.mockResolvedValue({ result: { files: [], nextPageToken: "next" } });
|
|
55
|
+
const result = await createWorkflowConnections(notion)
|
|
56
|
+
.googleDriveOauth("documents")
|
|
57
|
+
.listFiles({ folderId: "folder", pageToken: "previous" });
|
|
58
|
+
expect(result.nextPageToken).toBe("next");
|
|
59
|
+
expect(request).toHaveBeenCalledWith({
|
|
60
|
+
path: "tools/run/internal",
|
|
61
|
+
method: "post",
|
|
62
|
+
body: {
|
|
63
|
+
connection: "drive-id",
|
|
64
|
+
action: "listFiles",
|
|
65
|
+
args: { folderId: "folder", pageToken: "previous" },
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
it("uses the configured connector binding without choosing a user in the SDK", async () => {
|
|
70
|
+
vi.stubEnv(
|
|
71
|
+
"NOTION_CONNECTION_BINDINGS",
|
|
72
|
+
'[{"key":"documents","type":"box","id":"box-id"}]',
|
|
73
|
+
);
|
|
74
|
+
const notion = new Client({ auth: "test" });
|
|
75
|
+
const request = vi.spyOn(notion, "request").mockResolvedValue({ result: [] });
|
|
76
|
+
await createWorkflowConnections(notion)
|
|
77
|
+
.box("documents")
|
|
78
|
+
.search({ question: "Quarterly report" });
|
|
79
|
+
expect(request).toHaveBeenCalledWith({
|
|
80
|
+
path: "tools/run/internal",
|
|
81
|
+
method: "post",
|
|
82
|
+
body: {
|
|
83
|
+
connection: "box-id",
|
|
84
|
+
action: "search",
|
|
85
|
+
args: { question: "Quarterly report" },
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("rejects a mismatched provider before sending requests", () => {
|
|
91
|
+
vi.stubEnv("NOTION_CONNECTION_BINDINGS", '[{"key":"support","type":"calendar","id":"a"}]');
|
|
92
|
+
const notion = new Client({ auth: "test" });
|
|
93
|
+
const request = vi.spyOn(notion, "request");
|
|
94
|
+
expect(() => createWorkflowConnections(notion).slack("support")).toThrow("not slack");
|
|
95
|
+
expect(request).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
it("invokes a typed Calendar method with the default binding", async () => {
|
|
98
|
+
vi.stubEnv(
|
|
99
|
+
"NOTION_CONNECTION_BINDINGS",
|
|
100
|
+
'[{"key":"calendar","type":"calendar","id":"calendar-id"}]',
|
|
101
|
+
);
|
|
102
|
+
const notion = new Client({ auth: "test" });
|
|
103
|
+
const request = vi.spyOn(notion, "request").mockResolvedValue({ result: { accounts: [] } });
|
|
104
|
+
const result = await createWorkflowConnections(notion).calendar().listCalendars({});
|
|
105
|
+
expect(result.accounts).toEqual([]);
|
|
106
|
+
expect(request).toHaveBeenCalledWith({
|
|
107
|
+
path: "tools/run/internal",
|
|
108
|
+
method: "post",
|
|
109
|
+
body: { connection: "calendar-id", action: "listCalendars", args: {} },
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("invokes the exact named binding and unwraps its result", async () => {
|
|
114
|
+
vi.stubEnv(
|
|
115
|
+
"NOTION_CONNECTION_BINDINGS",
|
|
116
|
+
JSON.stringify([{ key: "support", type: "slack", id: "configured-id" }]),
|
|
117
|
+
);
|
|
118
|
+
const notion = new Client({ auth: "test" });
|
|
119
|
+
const request = vi.spyOn(notion, "request").mockResolvedValue({ result: { ok: true } });
|
|
120
|
+
const runtime = createWorkflowConnections(notion);
|
|
121
|
+
await expect(
|
|
122
|
+
runtime.slack("support").findUserByEmail({ email: "person@example.com" }),
|
|
123
|
+
).resolves.toEqual({
|
|
124
|
+
ok: true,
|
|
125
|
+
});
|
|
126
|
+
expect(request).toHaveBeenCalledWith({
|
|
127
|
+
path: "tools/run/internal",
|
|
128
|
+
method: "post",
|
|
129
|
+
body: {
|
|
130
|
+
connection: "configured-id",
|
|
131
|
+
action: "findUserByEmail",
|
|
132
|
+
args: { email: "person@example.com" },
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
expect(() => runtime.slack("slack")).toThrow("not bound");
|
|
136
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
137
|
+
});
|
|
138
|
+
it.each([
|
|
139
|
+
"{}",
|
|
140
|
+
'[{"key":"support","type":"slack","id":1}]',
|
|
141
|
+
'[{"key":"support","type":"slack","id":"a"},{"key":"support","type":"slack","id":"b"}]',
|
|
142
|
+
])("rejects malformed or ambiguous bindings: %s", (metadata) => {
|
|
143
|
+
vi.stubEnv("NOTION_CONNECTION_BINDINGS", metadata);
|
|
144
|
+
expect(() => createWorkflowConnections(new Client()).slack("support")).toThrow();
|
|
145
|
+
});
|
|
146
|
+
it("propagates permission failures without retrying an alternate connection", async () => {
|
|
147
|
+
vi.stubEnv("NOTION_CONNECTION_BINDINGS", '[{"key":"support","type":"slack","id":"a"}]');
|
|
148
|
+
const notion = new Client({ auth: "test" });
|
|
149
|
+
const request = vi.spyOn(notion, "request").mockRejectedValue(new Error("forbidden"));
|
|
150
|
+
await expect(
|
|
151
|
+
createWorkflowConnections(notion)
|
|
152
|
+
.slack("support")
|
|
153
|
+
.findUserByEmail({ email: "person@example.com" }),
|
|
154
|
+
).rejects.toThrow("forbidden");
|
|
155
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { connectionTriggerDefinitions } from "./connection-trigger-definitions.generated.js";
|
|
2
|
+
import type { WorkflowTrigger } from "./triggers.generated.js";
|
|
3
|
+
import type { Client } from "@notionhq/client";
|
|
4
|
+
import { createConnectionProviders, type ConnectionProviders } from "./providers.generated.js";
|
|
5
|
+
import type { ConnectionActionInvoker } from "./connection-actions.js";
|
|
6
|
+
|
|
7
|
+
/** A setup requirement. The server decides which providers are available. */
|
|
8
|
+
export type WorkflowConnection<TProvider extends string = string, TKey extends string = string> = {
|
|
9
|
+
type: TProvider;
|
|
10
|
+
key?: TKey;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Keys declared for one provider in a workflow. */
|
|
14
|
+
export type WorkflowConnectionKeys<
|
|
15
|
+
TConnections extends readonly WorkflowConnection[],
|
|
16
|
+
TProvider extends string,
|
|
17
|
+
> = WorkflowConnectionKey<Extract<TConnections[number], { type: TProvider }>>;
|
|
18
|
+
|
|
19
|
+
type WorkflowConnectionKey<TConnection extends WorkflowConnection> =
|
|
20
|
+
TConnection extends WorkflowConnection
|
|
21
|
+
? "key" extends keyof TConnection
|
|
22
|
+
? Exclude<TConnection["key"], undefined>
|
|
23
|
+
: TConnection["type"]
|
|
24
|
+
: never;
|
|
25
|
+
type ConnectionBinding = { key: string; type: string; id: string };
|
|
26
|
+
|
|
27
|
+
/** Requirements are declarations, not credentials or permission grants. */
|
|
28
|
+
export { connections } from "./providers.generated.js";
|
|
29
|
+
|
|
30
|
+
export type WorkflowConnections<
|
|
31
|
+
TConnections extends readonly WorkflowConnection[] = readonly WorkflowConnection[],
|
|
32
|
+
> = Pick<ConnectionProviders, Extract<keyof ConnectionProviders, TConnections[number]["type"]>>;
|
|
33
|
+
|
|
34
|
+
/** @internal */
|
|
35
|
+
export function validateConnectionRequirements(requirements: readonly WorkflowConnection[]): void {
|
|
36
|
+
if (requirements.length > 100) throw new Error("Workflows support at most 100 connections.");
|
|
37
|
+
const keys = new Set<string>();
|
|
38
|
+
for (const requirement of requirements) {
|
|
39
|
+
if (!requirement.type) throw new Error("A workflow connection needs a provider type.");
|
|
40
|
+
const key = requirement.key ?? requirement.type;
|
|
41
|
+
if (!/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(key)) {
|
|
42
|
+
throw new Error("Invalid workflow connection key: " + key);
|
|
43
|
+
}
|
|
44
|
+
if (keys.has(key)) throw new Error("Duplicate workflow connection: " + key);
|
|
45
|
+
keys.add(key);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @internal */
|
|
50
|
+
export function validateTriggerConnections(
|
|
51
|
+
triggers: readonly WorkflowTrigger[],
|
|
52
|
+
requirements: readonly WorkflowConnection[],
|
|
53
|
+
): void {
|
|
54
|
+
const seen = new Set<string>();
|
|
55
|
+
for (const trigger of triggers) {
|
|
56
|
+
if (!("connectionKey" in trigger) || trigger.connectionKey === undefined) continue;
|
|
57
|
+
const requirement = requirements.find(
|
|
58
|
+
(connection) => (connection.key ?? connection.type) === trigger.connectionKey,
|
|
59
|
+
);
|
|
60
|
+
const definition = connectionTriggerDefinitions.find(
|
|
61
|
+
(definition) => definition.type === trigger.type,
|
|
62
|
+
);
|
|
63
|
+
if (!requirement || requirement.type !== definition?.provider) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`Trigger "${trigger.type}" requires a declared ${definition?.provider ?? "supported"} connection: ${trigger.connectionKey}`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const identity = JSON.stringify([trigger.type, trigger.connectionKey]);
|
|
69
|
+
if (seen.has(identity))
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Duplicate connection trigger: ${trigger.type} (${trigger.connectionKey})`,
|
|
72
|
+
);
|
|
73
|
+
seen.add(identity);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** @internal */
|
|
78
|
+
export function createWorkflowConnections(notion: Client): WorkflowConnections {
|
|
79
|
+
// Capture the execution's bindings; defer parsing so non-connection workflows
|
|
80
|
+
// remain usable without runtime connection metadata.
|
|
81
|
+
const rawBindings = process.env.NOTION_CONNECTION_BINDINGS;
|
|
82
|
+
function resolveBinding(key: string): ConnectionBinding {
|
|
83
|
+
const bindings: unknown = JSON.parse(rawBindings ?? "[]");
|
|
84
|
+
if (!Array.isArray(bindings) || !bindings.every(isConnectionBinding)) {
|
|
85
|
+
throw new Error("Invalid NOTION_CONNECTION_BINDINGS metadata.");
|
|
86
|
+
}
|
|
87
|
+
const matches = bindings.filter((binding: ConnectionBinding) => binding.key === key);
|
|
88
|
+
if (matches.length !== 1 || !matches[0]) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Workflow connection "${key}" is not bound. Deploy and configure this requirement before running it.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return { ...matches[0] };
|
|
94
|
+
}
|
|
95
|
+
function invoker(provider: string, key: string): ConnectionActionInvoker {
|
|
96
|
+
const binding = resolveBinding(key);
|
|
97
|
+
if (binding.type !== provider) {
|
|
98
|
+
throw new Error(`Workflow connection "${key}" is ${binding.type}, not ${provider}.`);
|
|
99
|
+
}
|
|
100
|
+
return async <Result>(action: string, args: object): Promise<Result> => {
|
|
101
|
+
const response = await notion.request<{ result: Result }>({
|
|
102
|
+
path: "tools/run/internal",
|
|
103
|
+
method: "post",
|
|
104
|
+
body: { connection: binding.id, action, args },
|
|
105
|
+
});
|
|
106
|
+
return response.result;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
return createConnectionProviders(invoker);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function isConnectionBinding(value: unknown): value is ConnectionBinding {
|
|
113
|
+
return (
|
|
114
|
+
typeof value === "object" &&
|
|
115
|
+
value !== null &&
|
|
116
|
+
"key" in value &&
|
|
117
|
+
typeof value.key === "string" &&
|
|
118
|
+
value.key.length > 0 &&
|
|
119
|
+
"type" in value &&
|
|
120
|
+
typeof value.type === "string" &&
|
|
121
|
+
value.type.length > 0 &&
|
|
122
|
+
"id" in value &&
|
|
123
|
+
typeof value.id === "string" &&
|
|
124
|
+
value.id.length > 0
|
|
125
|
+
);
|
|
126
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
// Generated by notion tool-core codegen-script-types --connections. Do not edit.
|
|
2
|
+
import type { ConnectionActionInvoker } from "./connection-actions.js"
|
|
3
|
+
/**
|
|
4
|
+
* Script input for the "Create Cursor agent" effect (`cursor.createAgent`),
|
|
5
|
+
* derived from its Tool Core definition's call_function input projection.
|
|
6
|
+
*
|
|
7
|
+
* Create a new Cursor agent
|
|
8
|
+
*/
|
|
9
|
+
export type CreateAgentScriptInput = {
|
|
10
|
+
/**
|
|
11
|
+
* What you want the agent to do
|
|
12
|
+
*/
|
|
13
|
+
prompt: string
|
|
14
|
+
/**
|
|
15
|
+
* GitHub repository URL or owner/repo format (e.g. 'owner/repo' or 'https://github.com/owner/repo')
|
|
16
|
+
*/
|
|
17
|
+
repository: string
|
|
18
|
+
/**
|
|
19
|
+
* Optional branch or ref to work from (e.g. 'main', 'develop')
|
|
20
|
+
*/
|
|
21
|
+
ref?: string
|
|
22
|
+
/**
|
|
23
|
+
* Custom branch name for the agent to create in the target repository.
|
|
24
|
+
*/
|
|
25
|
+
branchName?: string
|
|
26
|
+
/**
|
|
27
|
+
* Whether to open the pull request as the Cursor GitHub App instead of as the user (default: true). Only applies when a PR is auto-created.
|
|
28
|
+
*/
|
|
29
|
+
openAsCursorGithubApp?: boolean
|
|
30
|
+
/**
|
|
31
|
+
* Whether to skip adding the user as a reviewer on the pull request. Only applies if autoCreatePr is true and the PR is opened as the Cursor GitHub App (default: false).
|
|
32
|
+
*/
|
|
33
|
+
skipReviewerRequest?: boolean
|
|
34
|
+
webhook?: {
|
|
35
|
+
/**
|
|
36
|
+
* Webhook URL to receive agent status updates.
|
|
37
|
+
*/
|
|
38
|
+
url: string
|
|
39
|
+
/**
|
|
40
|
+
* Optional shared secret for verifying webhook signatures.
|
|
41
|
+
*/
|
|
42
|
+
secret?: string
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Canonical Tool Core output for the "Create Cursor agent" effect (`cursor.createAgent`).
|
|
48
|
+
* This is the definition's output schema; the effect's script surface may
|
|
49
|
+
* re-encode it before returning.
|
|
50
|
+
*/
|
|
51
|
+
export type CreateAgentScriptOutput = {
|
|
52
|
+
id: string
|
|
53
|
+
name: string
|
|
54
|
+
/**
|
|
55
|
+
* One of: `CREATING`, `RUNNING`, `FINISHED`, `ERROR`, `EXPIRED`
|
|
56
|
+
*/
|
|
57
|
+
status: "CREATING" | "RUNNING" | "FINISHED" | "ERROR" | "EXPIRED"
|
|
58
|
+
source: {
|
|
59
|
+
repository: string
|
|
60
|
+
ref?: string
|
|
61
|
+
}
|
|
62
|
+
target: {
|
|
63
|
+
url: string
|
|
64
|
+
prUrl?: string
|
|
65
|
+
branchName?: string
|
|
66
|
+
autoCreatePr?: boolean
|
|
67
|
+
autoBranch?: boolean
|
|
68
|
+
openAsCursorGithubApp?: boolean
|
|
69
|
+
skipReviewerRequest?: boolean
|
|
70
|
+
}
|
|
71
|
+
createdAt: string
|
|
72
|
+
summary?: string
|
|
73
|
+
conversation?: {
|
|
74
|
+
id: string
|
|
75
|
+
messages: Array<{
|
|
76
|
+
id: string
|
|
77
|
+
type: string
|
|
78
|
+
text: string
|
|
79
|
+
}>
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Script input for the "Get Cursor agent" effect (`cursor.getAgent`),
|
|
85
|
+
* derived from its Tool Core definition's call_function input projection.
|
|
86
|
+
*
|
|
87
|
+
* Get a Cursor agent by ID
|
|
88
|
+
*/
|
|
89
|
+
export type GetAgentScriptInput = {
|
|
90
|
+
/**
|
|
91
|
+
* The ID of the Cursor agent to get (e.g. 'agent-123')
|
|
92
|
+
*/
|
|
93
|
+
agentId: string
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Canonical Tool Core output for the "Get Cursor agent" effect (`cursor.getAgent`).
|
|
98
|
+
* This is the definition's output schema; the effect's script surface may
|
|
99
|
+
* re-encode it before returning.
|
|
100
|
+
*/
|
|
101
|
+
export type GetAgentScriptOutput = {
|
|
102
|
+
id: string
|
|
103
|
+
name: string
|
|
104
|
+
/**
|
|
105
|
+
* One of: `CREATING`, `RUNNING`, `FINISHED`, `ERROR`, `EXPIRED`
|
|
106
|
+
*/
|
|
107
|
+
status: "CREATING" | "RUNNING" | "FINISHED" | "ERROR" | "EXPIRED"
|
|
108
|
+
source: {
|
|
109
|
+
repository: string
|
|
110
|
+
ref?: string
|
|
111
|
+
}
|
|
112
|
+
target: {
|
|
113
|
+
url: string
|
|
114
|
+
prUrl?: string
|
|
115
|
+
branchName?: string
|
|
116
|
+
autoCreatePr?: boolean
|
|
117
|
+
autoBranch?: boolean
|
|
118
|
+
openAsCursorGithubApp?: boolean
|
|
119
|
+
skipReviewerRequest?: boolean
|
|
120
|
+
}
|
|
121
|
+
createdAt: string
|
|
122
|
+
summary?: string
|
|
123
|
+
conversation?: {
|
|
124
|
+
id: string
|
|
125
|
+
messages: Array<{
|
|
126
|
+
id: string
|
|
127
|
+
type: string
|
|
128
|
+
text: string
|
|
129
|
+
}>
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Script input for the "List Cursor repositories" effect (`cursor.listRepositories`),
|
|
135
|
+
* derived from its Tool Core definition's call_function input projection.
|
|
136
|
+
*
|
|
137
|
+
* List accessible Cursor repositories
|
|
138
|
+
*/
|
|
139
|
+
export type ListRepositoriesScriptInput = Record<string, never>
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Canonical Tool Core output for the "List Cursor repositories" effect (`cursor.listRepositories`).
|
|
143
|
+
* This is the definition's output schema; the effect's script surface may
|
|
144
|
+
* re-encode it before returning.
|
|
145
|
+
*/
|
|
146
|
+
export type ListRepositoriesScriptOutput = {
|
|
147
|
+
repositories: Array<{
|
|
148
|
+
owner: string
|
|
149
|
+
name: string
|
|
150
|
+
repository: string
|
|
151
|
+
}>
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export class CursorConnectionClient {
|
|
155
|
+
constructor(private readonly invoke: ConnectionActionInvoker) {}
|
|
156
|
+
createAgent(args: CreateAgentScriptInput): Promise<CreateAgentScriptOutput> {
|
|
157
|
+
return this.invoke<CreateAgentScriptOutput>("createAgent", args)
|
|
158
|
+
}
|
|
159
|
+
getAgent(args: GetAgentScriptInput): Promise<GetAgentScriptOutput> {
|
|
160
|
+
return this.invoke<GetAgentScriptOutput>("getAgent", args)
|
|
161
|
+
}
|
|
162
|
+
listRepositories(
|
|
163
|
+
args: ListRepositoriesScriptInput,
|
|
164
|
+
): Promise<ListRepositoriesScriptOutput> {
|
|
165
|
+
return this.invoke<ListRepositoriesScriptOutput>("listRepositories", args)
|
|
166
|
+
}
|
|
167
|
+
}
|