@notionhq/apps 0.0.18 → 0.0.20

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 (54) hide show
  1. package/README.md +22 -1
  2. package/dist/cli/build.d.ts.map +1 -1
  3. package/dist/cli/build.js +12 -7
  4. package/dist/cli/emit-manifest.d.ts +1 -1
  5. package/dist/cli/emit-manifest.d.ts.map +1 -1
  6. package/dist/cli/emit-manifest.js +66 -1
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -1
  10. package/dist/notion-as-code/custom-agent.d.ts +1 -0
  11. package/dist/notion-as-code/custom-agent.d.ts.map +1 -1
  12. package/dist/notion-as-code/custom-agent.js +1 -1
  13. package/dist/notion-as-code/database.d.ts +2 -0
  14. package/dist/notion-as-code/database.d.ts.map +1 -1
  15. package/dist/notion-as-code/database.js +3 -0
  16. package/dist/notion-as-code/date.d.ts +3 -2
  17. package/dist/notion-as-code/date.d.ts.map +1 -1
  18. package/dist/notion-as-code/handles.d.ts +8 -11
  19. package/dist/notion-as-code/handles.d.ts.map +1 -1
  20. package/dist/notion-as-code/handles.js +8 -7
  21. package/dist/notion-as-code/index.d.ts +8 -11
  22. package/dist/notion-as-code/index.d.ts.map +1 -1
  23. package/dist/notion-as-code/page.d.ts +50 -1
  24. package/dist/notion-as-code/page.d.ts.map +1 -1
  25. package/dist/notion-as-code/page.js +1 -0
  26. package/dist/notion-as-code/page.test.d.ts +2 -0
  27. package/dist/notion-as-code/page.test.d.ts.map +1 -0
  28. package/dist/workflow-access-types.test.d.ts +2 -0
  29. package/dist/workflow-access-types.test.d.ts.map +1 -0
  30. package/dist/workflow-access.d.ts +68 -0
  31. package/dist/workflow-access.d.ts.map +1 -0
  32. package/dist/workflow-access.js +135 -0
  33. package/dist/workflow.d.ts +17 -6
  34. package/dist/workflow.d.ts.map +1 -1
  35. package/dist/workflow.js +8 -1
  36. package/docs/BUILD.md +38 -4
  37. package/package.json +1 -1
  38. package/skills/notion-as-code/SKILL.md +16 -3
  39. package/skills/workflow/SKILL.md +53 -16
  40. package/src/cli/build.test.ts +24 -0
  41. package/src/cli/build.ts +13 -8
  42. package/src/cli/emit-manifest.ts +107 -0
  43. package/src/index.ts +20 -1
  44. package/src/notion-as-code/custom-agent.ts +2 -1
  45. package/src/notion-as-code/database.ts +5 -0
  46. package/src/notion-as-code/date.ts +4 -2
  47. package/src/notion-as-code/handles.ts +16 -12
  48. package/src/notion-as-code/index.ts +10 -1
  49. package/src/notion-as-code/page.test.ts +35 -0
  50. package/src/notion-as-code/page.ts +62 -2
  51. package/src/workflow-access-types.test.ts +69 -0
  52. package/src/workflow-access.ts +299 -0
  53. package/src/workflow.test.ts +112 -0
  54. package/src/workflow.ts +50 -7
@@ -0,0 +1,299 @@
1
+ import type { CustomAgentHandle } from "./notion-as-code/custom-agent.js";
2
+ import type {
3
+ DataSourceHandle,
4
+ DatabaseHandle,
5
+ SingleSourceDatabaseHandle,
6
+ } from "./notion-as-code/database.js";
7
+ import type { PageHandle } from "./notion-as-code/page.js";
8
+
9
+ /** A Notion-as-Code resource that can provide a named workflow binding. */
10
+ export type WorkflowAccessResource =
11
+ | PageHandle
12
+ | DatabaseHandle
13
+ | SingleSourceDatabaseHandle
14
+ | DataSourceHandle
15
+ | CustomAgentHandle;
16
+
17
+ /** Resource kinds accepted by workflow access declarations. */
18
+ export type WorkflowAccessResourceType = WorkflowAccessResource["resourceType"];
19
+
20
+ /** Levels accepted by pages, databases, and data sources. */
21
+ export type WorkflowAccessResourceLevel = "view" | "comment" | "edit" | "fullAccess";
22
+
23
+ /** The only level accepted by a custom agent. */
24
+ export type WorkflowAccessAgentLevel = "call";
25
+
26
+ /** Every level accepted by at least one workflow access resource. */
27
+ export type WorkflowAccessLevel = WorkflowAccessResourceLevel | WorkflowAccessAgentLevel;
28
+
29
+ /** The level set accepted by a particular resource kind. */
30
+ export type WorkflowAccessLevelFor<TType extends WorkflowAccessResourceType> =
31
+ TType extends "customAgent" ? WorkflowAccessAgentLevel : WorkflowAccessResourceLevel;
32
+
33
+ type AccessResourceFor<TType extends WorkflowAccessResourceType> = Extract<
34
+ WorkflowAccessResource,
35
+ { resourceType: TType }
36
+ >;
37
+
38
+ /** A named binding declaration pairing one NaC handle with its allowed level. */
39
+ export type WorkflowAccessDeclaration = {
40
+ [TType in WorkflowAccessResourceType]: {
41
+ readonly resource: AccessResourceFor<TType>;
42
+ readonly level: WorkflowAccessLevelFor<TType>;
43
+ };
44
+ }[WorkflowAccessResourceType];
45
+
46
+ /** Named binding declarations keyed by the names exposed on workflow context. */
47
+ export type WorkflowAccessDeclarations = Readonly<Record<string, WorkflowAccessDeclaration>>;
48
+
49
+ /** A normalized named-binding requirement, including the required NaC resourceId. */
50
+ export type WorkflowAccessRequirement = {
51
+ [TType in WorkflowAccessResourceType]: {
52
+ readonly type: TType;
53
+ readonly resourceId: string;
54
+ readonly level: WorkflowAccessLevelFor<TType>;
55
+ };
56
+ }[WorkflowAccessResourceType];
57
+
58
+ /** Normalized named-binding requirements keyed by public alias. */
59
+ export type WorkflowAccessRequirements = Readonly<Record<string, WorkflowAccessRequirement>>;
60
+
61
+ /** The resolved NaC binding exposed to a workflow handler. */
62
+ export type WorkflowAccessValue<
63
+ TType extends WorkflowAccessResourceType = WorkflowAccessResourceType,
64
+ > = Readonly<{
65
+ type: TType;
66
+ id: string;
67
+ }>;
68
+
69
+ /** Resolved access values keyed by their public aliases. */
70
+ export type WorkflowAccess<
71
+ TAccess extends WorkflowAccessDeclarations = WorkflowAccessDeclarations,
72
+ > = Readonly<{
73
+ [TKey in keyof TAccess]: TAccess[TKey] extends {
74
+ readonly resource: { readonly resourceType: infer TType };
75
+ }
76
+ ? TType extends WorkflowAccessResourceType
77
+ ? WorkflowAccessValue<TType>
78
+ : never
79
+ : never;
80
+ }>;
81
+
82
+ /** Resolved NaC bindings keyed by public aliases, without declaration metadata. */
83
+ export type WorkflowAccessBindings = Readonly<Record<string, WorkflowAccessValue>>;
84
+
85
+ const ACCESS_ALIAS_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/;
86
+ const RESERVED_ACCESS_ALIASES = new Set<string>(["__proto__", "prototype", "constructor"]);
87
+ const RESOURCE_TYPES: Record<WorkflowAccessResourceType, true> = {
88
+ page: true,
89
+ database: true,
90
+ dataSource: true,
91
+ customAgent: true,
92
+ };
93
+
94
+ /**
95
+ * Normalize public binding declarations without retaining the handle objects.
96
+ * Handles contain methods and schema objects, so they must never be cloned into
97
+ * the manifest or captured by the runtime transport. These declarations do not
98
+ * grant or change the workflow's existing Notion-module permissions.
99
+ */
100
+ export function normalizeWorkflowAccess(
101
+ declarations: WorkflowAccessDeclarations | undefined,
102
+ ): WorkflowAccessRequirements {
103
+ const source = declarations ?? {};
104
+ assertPlainObject(source, "Workflow access must be an object keyed by alias.");
105
+ const keys = Object.keys(source);
106
+ if (Reflect.ownKeys(source).length !== keys.length) {
107
+ throw new Error("Workflow access aliases must be enumerable strings.");
108
+ }
109
+ if (keys.length > 100) {
110
+ throw new Error("Workflows support at most 100 access requirements.");
111
+ }
112
+
113
+ const normalized: Record<string, WorkflowAccessRequirement> = Object.create(null) as Record<
114
+ string,
115
+ WorkflowAccessRequirement
116
+ >;
117
+ for (const alias of keys) {
118
+ if (!ACCESS_ALIAS_PATTERN.test(alias) || RESERVED_ACCESS_ALIASES.has(alias)) {
119
+ throw new Error(`Invalid workflow access alias: ${alias}`);
120
+ }
121
+
122
+ const declaration: unknown = source[alias];
123
+ if (
124
+ !isRecord(declaration) ||
125
+ !Object.hasOwn(declaration, "resource") ||
126
+ !Object.hasOwn(declaration, "level")
127
+ ) {
128
+ throw new Error(`Workflow access "${alias}" must declare a resource and level.`);
129
+ }
130
+ const resource: unknown = declaration.resource;
131
+ if (
132
+ !isRecord(resource) ||
133
+ !Object.hasOwn(resource, "resourceType") ||
134
+ !Object.hasOwn(resource, "resourceId") ||
135
+ typeof resource.resourceId !== "string" ||
136
+ resource.resourceId.length === 0 ||
137
+ !isWorkflowAccessResourceType(resource.resourceType)
138
+ ) {
139
+ throw new Error(
140
+ `Workflow access "${alias}" must reference a supported NaC resource handle.`,
141
+ );
142
+ }
143
+
144
+ let requirement: WorkflowAccessRequirement;
145
+ if (resource.resourceType === "customAgent") {
146
+ if (declaration.level !== "call") {
147
+ throw new Error(
148
+ `Workflow access "${alias}" custom agents only support level "call".`,
149
+ );
150
+ }
151
+ requirement = {
152
+ type: resource.resourceType,
153
+ resourceId: resource.resourceId,
154
+ level: declaration.level,
155
+ };
156
+ } else {
157
+ if (!isWorkflowAccessResourceLevel(declaration.level)) {
158
+ throw new Error(
159
+ `Workflow access "${alias}" ${resource.resourceType} level must be view, comment, edit, or fullAccess.`,
160
+ );
161
+ }
162
+ requirement = {
163
+ type: resource.resourceType,
164
+ resourceId: resource.resourceId,
165
+ level: declaration.level,
166
+ };
167
+ }
168
+ normalized[alias] = Object.freeze(requirement);
169
+ }
170
+
171
+ return Object.freeze(normalized);
172
+ }
173
+
174
+ /**
175
+ * Resolve trusted per-invocation NaC bindings into the minimal context value.
176
+ * This exposes only declared aliases and does not determine Notion API
177
+ * authorization; the workflow's existing Notion-module permissions do that.
178
+ * The environment is deliberately not parsed when no binding is required.
179
+ */
180
+ export function hydrateWorkflowAccess<TAccess extends WorkflowAccessDeclarations>(
181
+ requirements: WorkflowAccessRequirements | undefined,
182
+ ): WorkflowAccess<TAccess>;
183
+ export function hydrateWorkflowAccess(
184
+ requirements: WorkflowAccessRequirements | undefined,
185
+ ): WorkflowAccessBindings {
186
+ const expected = requirements ?? {};
187
+ const aliases = Object.keys(expected);
188
+ const empty: Record<string, WorkflowAccessValue> = Object.create(null) as Record<
189
+ string,
190
+ WorkflowAccessValue
191
+ >;
192
+ if (aliases.length === 0) {
193
+ return Object.freeze(empty);
194
+ }
195
+
196
+ const rawBindings = process.env.NOTION_ACCESS_BINDINGS;
197
+ if (rawBindings === undefined) {
198
+ throw new Error(
199
+ "Workflow access requirements are not bound. Deploy and configure this requirement before running it.",
200
+ );
201
+ }
202
+ if (rawBindings.length === 0) {
203
+ throw new Error("Invalid NOTION_ACCESS_BINDINGS metadata.");
204
+ }
205
+
206
+ let parsed: unknown;
207
+ try {
208
+ parsed = JSON.parse(rawBindings);
209
+ } catch (error) {
210
+ throw new Error("Invalid NOTION_ACCESS_BINDINGS metadata.", { cause: error });
211
+ }
212
+ assertPlainObject(parsed, "Invalid NOTION_ACCESS_BINDINGS metadata.");
213
+ const bindingKeys = Object.keys(parsed);
214
+ if (
215
+ Reflect.ownKeys(parsed).length !== bindingKeys.length ||
216
+ bindingKeys.length !== aliases.length ||
217
+ bindingKeys.some((alias) => !Object.hasOwn(expected, alias))
218
+ ) {
219
+ throw new Error(
220
+ "Invalid NOTION_ACCESS_BINDINGS metadata: aliases do not match workflow access.",
221
+ );
222
+ }
223
+
224
+ const resolved: Record<string, WorkflowAccessValue> = Object.create(null) as Record<
225
+ string,
226
+ WorkflowAccessValue
227
+ >;
228
+ for (const alias of aliases) {
229
+ const requirement = expected[alias];
230
+ if (!requirement) {
231
+ throw new Error(`Workflow access "${alias}" is not declared.`);
232
+ }
233
+ const binding: unknown = parsed[alias];
234
+ if (!isBinding(binding)) {
235
+ throw new Error(`Invalid NOTION_ACCESS_BINDINGS metadata for access "${alias}".`);
236
+ }
237
+ if (
238
+ binding.type !== requirement.type ||
239
+ binding.resourceId !== requirement.resourceId ||
240
+ binding.level !== requirement.level
241
+ ) {
242
+ throw new Error(
243
+ `Workflow access "${alias}" binding does not match its declared type, resourceId, or level.`,
244
+ );
245
+ }
246
+ resolved[alias] = Object.freeze({ type: binding.type, id: binding.id });
247
+ }
248
+
249
+ return Object.freeze(resolved);
250
+ }
251
+
252
+ function isWorkflowAccessResourceType(value: unknown): value is WorkflowAccessResourceType {
253
+ return typeof value === "string" && Object.hasOwn(RESOURCE_TYPES, value);
254
+ }
255
+
256
+ function isWorkflowAccessResourceLevel(value: unknown): value is WorkflowAccessResourceLevel {
257
+ return value === "view" || value === "comment" || value === "edit" || value === "fullAccess";
258
+ }
259
+
260
+ function isBinding(value: unknown): value is {
261
+ type: WorkflowAccessResourceType;
262
+ resourceId: string;
263
+ level: WorkflowAccessResourceLevel | WorkflowAccessAgentLevel;
264
+ id: string;
265
+ } {
266
+ if (!isRecord(value) || Reflect.ownKeys(value).length !== 4) return false;
267
+ const keys = Object.keys(value);
268
+ if (
269
+ keys.length !== 4 ||
270
+ !keys.includes("type") ||
271
+ !keys.includes("resourceId") ||
272
+ !keys.includes("level") ||
273
+ !keys.includes("id")
274
+ ) {
275
+ return false;
276
+ }
277
+ return (
278
+ isWorkflowAccessResourceType(value.type) &&
279
+ typeof value.resourceId === "string" &&
280
+ value.resourceId.length > 0 &&
281
+ typeof value.level === "string" &&
282
+ (isWorkflowAccessResourceLevel(value.level) || value.level === "call") &&
283
+ typeof value.id === "string" &&
284
+ value.id.length > 0
285
+ );
286
+ }
287
+
288
+ function assertPlainObject(
289
+ value: unknown,
290
+ message: string,
291
+ ): asserts value is Record<string, unknown> {
292
+ if (!isRecord(value)) throw new Error(message);
293
+ const prototype = Object.getPrototypeOf(value);
294
+ if (prototype !== Object.prototype && prototype !== null) throw new Error(message);
295
+ }
296
+
297
+ function isRecord(value: unknown): value is Record<string, unknown> {
298
+ return typeof value === "object" && value !== null && !Array.isArray(value);
299
+ }
@@ -8,6 +8,9 @@ import { afterEach, beforeEach, describe, expect, expectTypeOf, it, vi, type Moc
8
8
  import { ExecutionError } from "./error.js";
9
9
  import type { RunMetadata } from "./runtime-metadata.js";
10
10
  import { triggers, type NotionPageCreatedEvent } from "./triggers.generated.js";
11
+ import { customAgent } from "./notion-as-code/custom-agent.js";
12
+ import { database } from "./notion-as-code/database.js";
13
+ import { page } from "./notion-as-code/page.js";
11
14
  import {
12
15
  connections,
13
16
  workflow,
@@ -22,6 +25,7 @@ let metadataDirectory: string | undefined;
22
25
 
23
26
  beforeEach(async () => {
24
27
  stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
28
+ delete process.env.NOTION_ACCESS_BINDINGS;
25
29
  delete process.env.NOTION_WORKFLOW_STEP_DIRECTORY;
26
30
  process.env.NOTION_WORKFLOW_CAPABILITY_KEY = "workflow-key";
27
31
  checkpointDirectory = undefined;
@@ -31,6 +35,7 @@ beforeEach(async () => {
31
35
 
32
36
  afterEach(async () => {
33
37
  stdoutSpy.mockRestore();
38
+ delete process.env.NOTION_ACCESS_BINDINGS;
34
39
  delete process.env.NOTION_WORKFLOW_STEP_DIRECTORY;
35
40
  delete process.env.NOTION_WORKFLOW_CAPABILITY_KEY;
36
41
  delete process.env.NOTION_INPUT_PAYLOAD_PATH;
@@ -1190,6 +1195,113 @@ describe("workflow", () => {
1190
1195
  ),
1191
1196
  );
1192
1197
  });
1198
+ it("normalizes NaC access and hydrates all supported resource kinds", async () => {
1199
+ const home = page({ resourceId: "home" });
1200
+ const table = database("table", {
1201
+ dataSourceResourceId: "table-source",
1202
+ name: "Table",
1203
+ schema: { Title: { resourceId: "title", type: "title" } },
1204
+ });
1205
+ const agent = customAgent({ resourceId: "agent", name: "Agent" });
1206
+ let resolvedAccess:
1207
+ | {
1208
+ readonly home: { readonly type: "page"; readonly id: string };
1209
+ readonly table: { readonly type: "database"; readonly id: string };
1210
+ readonly source: { readonly type: "dataSource"; readonly id: string };
1211
+ readonly agent: { readonly type: "customAgent"; readonly id: string };
1212
+ }
1213
+ | undefined;
1214
+ const definition = workflow({
1215
+ name: "Access",
1216
+ description: "Hydrates access",
1217
+ triggers: [triggers.notionPageCreated()],
1218
+ access: {
1219
+ home: { resource: home, level: "view" },
1220
+ table: { resource: table, level: "edit" },
1221
+ source: { resource: table.dataSource, level: "comment" },
1222
+ agent: { resource: agent, level: "call" },
1223
+ },
1224
+ handler: (_event, context) => {
1225
+ expectTypeOf(context.access.home.type).toEqualTypeOf<"page">();
1226
+ expectTypeOf(context.access.table.type).toEqualTypeOf<"database">();
1227
+ expectTypeOf(context.access.source.type).toEqualTypeOf<"dataSource">();
1228
+ expectTypeOf(context.access.agent.type).toEqualTypeOf<"customAgent">();
1229
+ resolvedAccess = context.access;
1230
+ },
1231
+ });
1232
+
1233
+ expect(definition.config.access).toEqual({
1234
+ home: { type: "page", resourceId: "home", level: "view" },
1235
+ table: { type: "database", resourceId: "table", level: "edit" },
1236
+ source: { type: "dataSource", resourceId: "table-source", level: "comment" },
1237
+ agent: { type: "customAgent", resourceId: "agent", level: "call" },
1238
+ });
1239
+ process.env.NOTION_ACCESS_BINDINGS = JSON.stringify({
1240
+ home: { type: "page", resourceId: "home", level: "view", id: "page-id" },
1241
+ table: { type: "database", resourceId: "table", level: "edit", id: "database-id" },
1242
+ source: {
1243
+ type: "dataSource",
1244
+ resourceId: "table-source",
1245
+ level: "comment",
1246
+ id: "data-source-id",
1247
+ },
1248
+ agent: { type: "customAgent", resourceId: "agent", level: "call", id: "agent-id" },
1249
+ });
1250
+
1251
+ await definition.handler(event, { concreteOutput: true });
1252
+
1253
+ expect(resolvedAccess).toEqual({
1254
+ home: { type: "page", id: "page-id" },
1255
+ table: { type: "database", id: "database-id" },
1256
+ source: { type: "dataSource", id: "data-source-id" },
1257
+ agent: { type: "customAgent", id: "agent-id" },
1258
+ });
1259
+ expect(Object.isFrozen(resolvedAccess)).toBe(true);
1260
+ expect(Object.isFrozen(resolvedAccess?.home)).toBe(true);
1261
+ });
1262
+
1263
+ it("rejects missing or mismatched access bindings before the handler", async () => {
1264
+ const handler = vi.fn();
1265
+ const definition = workflow({
1266
+ name: "Access",
1267
+ description: "Requires access",
1268
+ triggers: [triggers.notionPageCreated()],
1269
+ access: { home: { resource: page({ resourceId: "home" }), level: "view" } },
1270
+ handler,
1271
+ });
1272
+
1273
+ await expect(definition.handler(event, { concreteOutput: true })).rejects.toThrow(
1274
+ "not bound",
1275
+ );
1276
+ expect(handler).not.toHaveBeenCalled();
1277
+
1278
+ process.env.NOTION_ACCESS_BINDINGS = JSON.stringify({
1279
+ home: { type: "page", resourceId: "other", level: "view", id: "page-id" },
1280
+ });
1281
+ await expect(definition.handler(event, { concreteOutput: true })).rejects.toThrow(
1282
+ "does not match",
1283
+ );
1284
+ expect(handler).not.toHaveBeenCalled();
1285
+ });
1286
+
1287
+ it("does not parse unused access metadata when no access is declared", async () => {
1288
+ process.env.NOTION_ACCESS_BINDINGS = "{malformed";
1289
+ let contextAccess: unknown;
1290
+ const definition = workflow({
1291
+ name: "No Access",
1292
+ description: "Runs without access",
1293
+ triggers: [triggers.notionPageCreated()],
1294
+ handler: (_event, context) => {
1295
+ contextAccess = context.access;
1296
+ },
1297
+ });
1298
+
1299
+ await expect(definition.handler(event, { concreteOutput: true })).resolves.toEqual({
1300
+ status: "success",
1301
+ });
1302
+ expect(contextAccess).toEqual({});
1303
+ expect(Object.isFrozen(contextAccess)).toBe(true);
1304
+ });
1193
1305
  });
1194
1306
 
1195
1307
  describe("connection-bound triggers", () => {
package/src/workflow.ts CHANGED
@@ -6,6 +6,13 @@ import {
6
6
  type WorkflowConnectionRequirement,
7
7
  type WorkflowConnections,
8
8
  } from "./connections.js";
9
+ import {
10
+ hydrateWorkflowAccess,
11
+ normalizeWorkflowAccess,
12
+ type WorkflowAccess,
13
+ type WorkflowAccessDeclarations,
14
+ type WorkflowAccessRequirements,
15
+ } from "./workflow-access.js";
9
16
  import { createHash } from "node:crypto";
10
17
  import { readFile } from "node:fs/promises";
11
18
  import { join } from "node:path";
@@ -69,6 +76,21 @@ type WorkflowHandlerResult = { status: "success" } | WorkflowWaitUntilResult;
69
76
 
70
77
  const MAX_WORKFLOW_WAIT_MS = 7 * 24 * 60 * 60 * 1_000;
71
78
 
79
+ export type {
80
+ WorkflowAccess,
81
+ WorkflowAccessAgentLevel,
82
+ WorkflowAccessBindings,
83
+ WorkflowAccessDeclaration,
84
+ WorkflowAccessDeclarations,
85
+ WorkflowAccessLevel,
86
+ WorkflowAccessLevelFor,
87
+ WorkflowAccessResource,
88
+ WorkflowAccessResourceLevel,
89
+ WorkflowAccessResourceType,
90
+ WorkflowAccessRequirement,
91
+ WorkflowAccessRequirements,
92
+ WorkflowAccessValue,
93
+ } from "./workflow-access.js";
72
94
  export type WorkflowEvent = WorkflowEventMap[keyof WorkflowEventMap];
73
95
 
74
96
  export type WorkflowEventForTrigger<T extends WorkflowTrigger> = WorkflowEventMap[T["type"]];
@@ -92,6 +114,7 @@ export {
92
114
  export type WorkflowConfiguration<
93
115
  TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
94
116
  TConnections extends WorkflowConnectionDeclarations = WorkflowConnectionDeclarations,
117
+ TAccess extends WorkflowAccessDeclarations = WorkflowAccessDeclarations,
95
118
  > = {
96
119
  /**
97
120
  * A human-readable name for the workflow, shown in the UI when viewing workflows
@@ -110,6 +133,13 @@ export type WorkflowConfiguration<
110
133
  */
111
134
  triggers: TTriggers;
112
135
 
136
+ /**
137
+ * Notion-as-Code resources whose access is configured during cloud deployment.
138
+ * Deployment reconciles grants through the existing Notion-module permissions
139
+ * and supplies named runtime bindings on `context.access`.
140
+ */
141
+ access?: TAccess;
142
+
113
143
  /**
114
144
  * Connections that must be set up before this workflow can run.
115
145
  */
@@ -117,7 +147,7 @@ export type WorkflowConfiguration<
117
147
 
118
148
  handler: (
119
149
  event: WorkflowEventForTriggers<TTriggers>,
120
- context: WorkflowContext<TConnections>,
150
+ context: WorkflowContext<TConnections, TAccess>,
121
151
  ) => Promise<void> | void;
122
152
  };
123
153
 
@@ -125,7 +155,8 @@ export type WorkflowConfiguration<
125
155
  export type WorkflowTriggerConfiguration<
126
156
  TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
127
157
  TConnections extends WorkflowConnectionDeclarations,
128
- > = Omit<WorkflowConfiguration<TTriggers, TConnections>, "triggers"> & {
158
+ TAccess extends WorkflowAccessDeclarations = WorkflowAccessDeclarations,
159
+ > = Omit<WorkflowConfiguration<TTriggers, TConnections, TAccess>, "triggers"> & {
129
160
  triggers: (context: { triggers: WorkflowTriggerCreators<NoInfer<TConnections>> }) => TTriggers;
130
161
  };
131
162
 
@@ -147,6 +178,7 @@ export type Workflow<
147
178
  description: string;
148
179
  triggers: TTriggers;
149
180
  connections?: readonly WorkflowConnectionRequirement[];
181
+ access?: WorkflowAccessRequirements;
150
182
  };
151
183
  handler: (
152
184
  event: WorkflowEventForTriggers<TTriggers>,
@@ -186,20 +218,26 @@ export function workflow<
186
218
  ...WorkflowTriggerForConnections<NoInfer<TConnections>>[],
187
219
  ],
188
220
  const TConnections extends WorkflowConnectionDeclarations = Record<never, never>,
189
- >(configuration: WorkflowTriggerConfiguration<TTriggers, TConnections>): Workflow<TTriggers>;
221
+ const TAccess extends WorkflowAccessDeclarations = Record<never, never>,
222
+ >(
223
+ configuration: WorkflowTriggerConfiguration<TTriggers, TConnections, TAccess>,
224
+ ): Workflow<TTriggers>;
190
225
  export function workflow<
191
226
  const TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
192
227
  const TConnections extends WorkflowConnectionDeclarations = Record<never, never>,
193
- >(configuration: WorkflowConfiguration<TTriggers, TConnections>): Workflow<TTriggers>;
228
+ const TAccess extends WorkflowAccessDeclarations = Record<never, never>,
229
+ >(configuration: WorkflowConfiguration<TTriggers, TConnections, TAccess>): Workflow<TTriggers>;
194
230
  export function workflow<
195
231
  const TTriggers extends readonly [WorkflowTrigger, ...WorkflowTrigger[]],
196
232
  const TConnections extends WorkflowConnectionDeclarations = Record<never, never>,
233
+ const TAccess extends WorkflowAccessDeclarations = Record<never, never>,
197
234
  >(
198
235
  configuration:
199
- | WorkflowConfiguration<TTriggers, TConnections>
200
- | WorkflowTriggerConfiguration<TTriggers, TConnections>,
236
+ | WorkflowConfiguration<TTriggers, TConnections, TAccess>
237
+ | WorkflowTriggerConfiguration<TTriggers, TConnections, TAccess>,
201
238
  ): Workflow<TTriggers> {
202
239
  const requirements = normalizeConnectionRequirements(configuration.connections ?? {});
240
+ const accessRequirements = normalizeWorkflowAccess(configuration.access);
203
241
  const connectionDeclarations =
204
242
  configuration.connections === undefined
205
243
  ? undefined
@@ -217,6 +255,7 @@ export function workflow<
217
255
  description: configuration.description,
218
256
  triggers,
219
257
  ...(configuration.connections === undefined ? {} : { connections: requirements }),
258
+ ...(configuration.access === undefined ? {} : { access: accessRequirements }),
220
259
  },
221
260
  async handler(
222
261
  event: WorkflowEventForTriggers<TTriggers>,
@@ -227,8 +266,9 @@ export function workflow<
227
266
  const runMetadata = readRunMetadata();
228
267
  const baseContext = createCapabilityContext();
229
268
  const step = createStep(runMetadata);
230
- const capabilityContext: WorkflowContext<TConnections> = {
269
+ const capabilityContext: WorkflowContext<TConnections, TAccess> = {
231
270
  ...baseContext,
271
+ access: hydrateWorkflowAccess<TAccess>(accessRequirements),
232
272
  connections: createWorkflowConnections(
233
273
  baseContext.notion,
234
274
  connectionDeclarations,
@@ -324,7 +364,10 @@ type WorkflowWait = {
324
364
  /** Context passed to a workflow handler. */
325
365
  export type WorkflowContext<
326
366
  TConnections extends WorkflowConnectionDeclarations = WorkflowConnectionDeclarations,
367
+ TAccess extends WorkflowAccessDeclarations = WorkflowAccessDeclarations,
327
368
  > = CapabilityContext & {
369
+ /** Resolved NaC bindings for this invocation; UI-granted resources are not added here. */
370
+ readonly access: WorkflowAccess<TAccess>;
328
371
  connections: WorkflowConnections<TConnections>;
329
372
  } & RunMetadata & {
330
373
  /**