@zapier/policy-context 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -22,13 +22,16 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AccessLevelSchema: () => AccessLevelSchema,
24
24
  ActionRunContextSchema: () => ActionRunContextSchema,
25
+ ActionTypeSchema: () => ActionTypeSchema,
25
26
  BuildActionRunContextOptionsSchema: () => BuildActionRunContextOptionsSchema,
26
27
  BuildHttpRequestContextOptionsSchema: () => BuildHttpRequestContextOptionsSchema,
27
28
  DEFAULT_PORTS: () => DEFAULT_PORTS,
28
29
  HttpRequestContextSchema: () => HttpRequestContextSchema,
29
30
  IGNORED_HEADERS: () => IGNORED_HEADERS,
30
- JsonValueSchema: () => JsonValueSchema,
31
+ JsonValueSchema: () => import_policy_schema.JsonValueSchema,
31
32
  ParsedUrlSchema: () => ParsedUrlSchema,
33
+ READ_ACTION_TYPES: () => READ_ACTION_TYPES,
34
+ READ_HTTP_METHODS: () => READ_HTTP_METHODS,
32
35
  RequestContextSchema: () => RequestContextSchema,
33
36
  buildActionResource: () => buildActionResource,
34
37
  buildActionRunContext: () => buildActionRunContext,
@@ -39,6 +42,8 @@ __export(index_exports, {
39
42
  buildExactHttpConditions: () => buildExactHttpConditions,
40
43
  buildHttpRequestContext: () => buildHttpRequestContext,
41
44
  buildUrlString: () => buildUrlString,
45
+ isWriteRequest: () => isWriteRequest,
46
+ parseActionResource: () => parseActionResource,
42
47
  parseUrl: () => parseUrl,
43
48
  toExactPolicy: () => toExactPolicy
44
49
  });
@@ -46,14 +51,20 @@ module.exports = __toCommonJS(index_exports);
46
51
 
47
52
  // src/types.ts
48
53
  var import_zod = require("zod");
49
- var JsonValueSchema = import_zod.z.custom();
54
+ var import_zod_to_openapi = require("@asteasolutions/zod-to-openapi");
55
+ var import_policy_schema = require("@zapier/policy-schema");
56
+ (0, import_zod_to_openapi.extendZodWithOpenApi)(import_zod.z);
50
57
  var ParsedUrlSchema = import_zod.z.object({
51
- scheme: import_zod.z.string(),
52
- host: import_zod.z.string(),
53
- port: import_zod.z.number(),
54
- path: import_zod.z.string(),
55
- query: import_zod.z.record(import_zod.z.string(), import_zod.z.array(import_zod.z.string()))
56
- });
58
+ scheme: import_zod.z.string().openapi({ description: 'URL scheme, e.g. "https"' }),
59
+ host: import_zod.z.string().openapi({ description: 'Hostname, e.g. "api.example.com"' }),
60
+ port: import_zod.z.number().int().openapi({
61
+ description: "Port number (defaults to 443 for https, 80 for http)"
62
+ }),
63
+ path: import_zod.z.string().openapi({ description: 'URL path, e.g. "/v1/foo"' }),
64
+ query: import_zod.z.record(import_zod.z.string(), import_zod.z.array(import_zod.z.string())).openapi({
65
+ description: "Query parameters as key to string array mapping"
66
+ })
67
+ }).openapi("ParsedUrl");
57
68
  var DEFAULT_PORTS = {
58
69
  http: 80,
59
70
  https: 443
@@ -88,33 +99,136 @@ function buildUrlString(parsed) {
88
99
  }
89
100
  var HttpRequestContextSchema = import_zod.z.object({
90
101
  request_type: import_zod.z.literal("http_request"),
91
- method: import_zod.z.string(),
102
+ method: import_zod.z.string().max(10).transform((m) => m.toUpperCase()),
92
103
  /** Parsed URL components for policy condition matching */
93
104
  url: ParsedUrlSchema,
94
105
  /** Filtered headers (IGNORED_HEADERS excluded, keys lowercased) */
95
- headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()),
106
+ headers: import_zod.z.record(import_zod.z.string(), import_zod.z.string()).openapi({
107
+ description: "Filtered headers (IGNORED_HEADERS excluded, keys lowercased)"
108
+ }),
96
109
  /** Raw body string */
97
- body: import_zod.z.string().nullable(),
110
+ body: import_zod.z.string().max(65536).nullable(),
98
111
  /** Parsed JSON body */
99
- body_json: JsonValueSchema.nullable(),
112
+ body_json: import_policy_schema.JsonValueSchema.nullable().openapi({
113
+ // `type: 'object'` is a placeholder so zod-to-openapi short-circuits
114
+ // introspection of the recursive JsonValueSchema (z.lazy). The
115
+ // generator script strips this `type` after emit so the produced
116
+ // schema matches the original "any JSON value" shape.
117
+ type: "object",
118
+ description: "Parsed JSON body (if applicable)"
119
+ }),
100
120
  /** Zapier connection ID used for this request */
101
- connection_id: import_zod.z.string().optional(),
121
+ connection_id: import_zod.z.string().max(255).optional().openapi({ description: "Zapier connection ID used for this request" }),
102
122
  /** Optional human-readable reason for the request */
103
- reason: import_zod.z.string().optional()
104
- });
105
- var ActionRunContextSchema = import_zod.z.object({
123
+ reason: import_zod.z.string().max(1024).optional().openapi({ description: "Optional human-readable reason for the request" })
124
+ }).openapi("HttpRequestContext");
125
+ var ActionTypeSchema = import_zod.z.enum([
126
+ "filter",
127
+ "read",
128
+ "read_bulk",
129
+ "run",
130
+ "search",
131
+ "search_and_write",
132
+ "search_or_write",
133
+ "write"
134
+ ]).openapi("action_type", { description: "Canonical Zapier action type." });
135
+ var ACTION_RESOURCE_SEGMENT_PATTERN = /^[A-Za-z0-9._+-]+(?:@[A-Za-z0-9._+-]+)?$/;
136
+ function parseActionResource(resource) {
137
+ const segments = resource.split("/");
138
+ if (segments.length !== 4 || segments[0] !== "action") {
139
+ throw new Error(
140
+ `action resource must be action/{selected_api}/{action_type}/{action_key}: got ${resource}`
141
+ );
142
+ }
143
+ const [, selectedApi, actionTypeRaw, actionKey] = segments;
144
+ if (!selectedApi || !actionTypeRaw || !actionKey) {
145
+ throw new Error(`action resource has empty segment: ${resource}`);
146
+ }
147
+ const actionType = ActionTypeSchema.safeParse(actionTypeRaw);
148
+ if (!actionType.success) {
149
+ throw new Error(
150
+ `action resource has unknown action_type "${actionTypeRaw}": ${resource}`
151
+ );
152
+ }
153
+ if (!ACTION_RESOURCE_SEGMENT_PATTERN.test(selectedApi)) {
154
+ throw new Error(
155
+ `action resource has invalid selected_api segment "${selectedApi}": ${resource}`
156
+ );
157
+ }
158
+ if (!ACTION_RESOURCE_SEGMENT_PATTERN.test(actionKey)) {
159
+ throw new Error(
160
+ `action resource has invalid action_key segment "${actionKey}": ${resource}`
161
+ );
162
+ }
163
+ return {
164
+ selected_api: selectedApi,
165
+ action_type: actionType.data,
166
+ action_key: actionKey
167
+ };
168
+ }
169
+ var ActionRunContextInputSchema = import_zod.z.object({
106
170
  request_type: import_zod.z.literal("action_run"),
107
- selected_api: import_zod.z.string(),
108
- action_type: import_zod.z.string(),
109
- action_key: import_zod.z.string(),
110
- connection_id: import_zod.z.string().optional(),
111
- inputs: JsonValueSchema.optional(),
112
- reason: import_zod.z.string().optional()
171
+ resource: import_zod.z.string().max(1024).optional().openapi({
172
+ description: "Canonical action resource, e.g. action/Slack@1.0.0/write/send_message"
173
+ }),
174
+ selected_api: import_zod.z.string().max(255).optional().openapi({
175
+ deprecated: true,
176
+ description: "Legacy: app identifier, e.g. Slack@1.0.0. Prefer `resource`."
177
+ }),
178
+ action_type: ActionTypeSchema.optional().openapi({
179
+ deprecated: true,
180
+ description: "Legacy: canonical Zapier action type. Prefer `resource`."
181
+ }),
182
+ action_key: import_zod.z.string().max(255).optional().openapi({
183
+ deprecated: true,
184
+ description: "Legacy: action key, e.g. send_message. Prefer `resource`."
185
+ }),
186
+ connection_id: import_zod.z.string().max(255).optional().openapi({ description: "Zapier connection ID used for this request" }),
187
+ inputs: import_policy_schema.JsonValueSchema.optional().openapi({
188
+ // `type: 'object'` is a placeholder so zod-to-openapi short-circuits
189
+ // introspection of the recursive JsonValueSchema (z.lazy). The
190
+ // generator script strips this `type` after emit so the produced
191
+ // schema matches the original "any inputs" shape.
192
+ type: "object",
193
+ description: "Action input parameters"
194
+ }),
195
+ reason: import_zod.z.string().max(1024).optional().openapi({ description: "Optional human-readable reason for the request" })
196
+ }).openapi("ActionRunContext", {
197
+ description: "Action run context. The canonical shape carries the action identity in a single `resource` field of the form `action/{selected_api}/{action_type}/{action_key}`. The legacy `{selected_api, action_type, action_key}` shape is also accepted on input during the migration window \u2014 approvalsapi normalizes it to the canonical form before storage."
198
+ });
199
+ var ActionRunContextSchema = ActionRunContextInputSchema.transform(
200
+ (input, ctx) => {
201
+ const resource = input.resource ?? (input.selected_api && input.action_type && input.action_key ? `action/${input.selected_api}/${input.action_type}/${input.action_key}` : void 0);
202
+ if (resource === void 0) {
203
+ ctx.addIssue({
204
+ code: "custom",
205
+ message: "action_run context requires either `resource` or all of `selected_api`/`action_type`/`action_key`"
206
+ });
207
+ return import_zod.z.NEVER;
208
+ }
209
+ try {
210
+ parseActionResource(resource);
211
+ } catch (err) {
212
+ ctx.addIssue({
213
+ code: "custom",
214
+ message: err instanceof Error ? err.message : String(err)
215
+ });
216
+ return import_zod.z.NEVER;
217
+ }
218
+ return {
219
+ request_type: "action_run",
220
+ resource,
221
+ ...input.connection_id !== void 0 && {
222
+ connection_id: input.connection_id
223
+ },
224
+ ...input.inputs !== void 0 && { inputs: input.inputs },
225
+ ...input.reason !== void 0 && { reason: input.reason }
226
+ };
227
+ }
228
+ );
229
+ var RequestContextSchema = import_zod.z.union([HttpRequestContextSchema, ActionRunContextSchema]).openapi("RequestContext", {
230
+ description: "Discriminated union over request_type."
113
231
  });
114
- var RequestContextSchema = import_zod.z.discriminatedUnion("request_type", [
115
- HttpRequestContextSchema,
116
- ActionRunContextSchema
117
- ]);
118
232
 
119
233
  // src/constants.ts
120
234
  var IGNORED_HEADERS = /* @__PURE__ */ new Set([
@@ -148,74 +262,6 @@ var IGNORED_HEADERS = /* @__PURE__ */ new Set([
148
262
 
149
263
  // src/builders.ts
150
264
  var import_zod2 = require("zod");
151
- var BuildHttpRequestContextOptionsSchema = import_zod2.z.object({
152
- method: import_zod2.z.string(),
153
- /** Raw URL string. Will be normalized with https:// if no protocol present. */
154
- url: import_zod2.z.string(),
155
- /** Raw headers from the request. Will be filtered through IGNORED_HEADERS. */
156
- headers: import_zod2.z.record(
157
- import_zod2.z.string(),
158
- import_zod2.z.union([import_zod2.z.string(), import_zod2.z.array(import_zod2.z.string()), import_zod2.z.undefined()])
159
- ).optional(),
160
- /** Raw body string */
161
- body: import_zod2.z.string().nullable().optional(),
162
- /** Pre-parsed JSON body (e.g. from Fastify's automatic JSON parsing) */
163
- body_json: JsonValueSchema.optional(),
164
- /** Connection ID associated with this request */
165
- connection_id: import_zod2.z.string().optional()
166
- });
167
- function buildHttpRequestContext(options) {
168
- const rawUrl = options.url.includes("://") ? options.url : `https://${options.url}`;
169
- const url = parseUrl(rawUrl);
170
- const headers = {};
171
- if (options.headers) {
172
- for (const [key, value] of Object.entries(options.headers)) {
173
- const lowerKey = key.toLowerCase();
174
- if (!IGNORED_HEADERS.has(lowerKey) && typeof value === "string") {
175
- headers[lowerKey] = value;
176
- }
177
- }
178
- }
179
- let body_json = null;
180
- if (options.body_json !== void 0) {
181
- body_json = options.body_json;
182
- } else if (options.body) {
183
- const contentType = headers["content-type"];
184
- const isJson = typeof contentType === "string" && (contentType.includes("application/json") || contentType.includes("application/") && contentType.includes("+json"));
185
- if (isJson) {
186
- try {
187
- body_json = JSON.parse(options.body);
188
- } catch {
189
- }
190
- }
191
- }
192
- return HttpRequestContextSchema.parse({
193
- request_type: "http_request",
194
- method: options.method,
195
- url,
196
- headers,
197
- body: options.body ?? null,
198
- body_json,
199
- connection_id: options.connection_id
200
- });
201
- }
202
- var BuildActionRunContextOptionsSchema = import_zod2.z.object({
203
- selected_api: import_zod2.z.string(),
204
- action_type: import_zod2.z.string(),
205
- action_key: import_zod2.z.string(),
206
- connection_id: import_zod2.z.string().optional(),
207
- inputs: JsonValueSchema.optional()
208
- });
209
- function buildActionRunContext(options) {
210
- return ActionRunContextSchema.parse({
211
- request_type: "action_run",
212
- selected_api: options.selected_api,
213
- action_type: options.action_type,
214
- action_key: options.action_key,
215
- connection_id: options.connection_id,
216
- inputs: options.inputs
217
- });
218
- }
219
265
 
220
266
  // src/conditions.ts
221
267
  function buildExactHttpConditions(context) {
@@ -244,6 +290,12 @@ function buildExactHttpConditions(context) {
244
290
  operator: "equals",
245
291
  value: context.body_json
246
292
  });
293
+ } else if (context.body !== null) {
294
+ conditions.push({
295
+ path: ["body"],
296
+ operator: "equals",
297
+ value: context.body
298
+ });
247
299
  }
248
300
  for (const [key, value] of Object.entries(context.headers)) {
249
301
  if (!IGNORED_HEADERS.has(key.toLowerCase()) && typeof value === "string") {
@@ -267,7 +319,7 @@ function buildDomainHttpConditions(context) {
267
319
  }
268
320
  function buildExactActionConditions(context) {
269
321
  const conditions = [];
270
- if (context.inputs !== void 0 && context.inputs !== null && typeof context.inputs === "object") {
322
+ if (context.inputs !== void 0 && context.inputs !== null) {
271
323
  conditions.push({
272
324
  path: ["inputs"],
273
325
  operator: "equals",
@@ -276,11 +328,91 @@ function buildExactActionConditions(context) {
276
328
  }
277
329
  return conditions;
278
330
  }
279
- function buildActionResource(context) {
280
- return `action/${context.selected_api}/${context.action_type}/${context.action_key}`;
331
+ function buildActionResource(parts) {
332
+ return `action/${parts.selected_api}/${parts.action_type}/${parts.action_key}`;
281
333
  }
282
- function buildBroadActionResource(context) {
283
- return `action/${context.selected_api}/*/*`;
334
+ function buildBroadActionResource(parts) {
335
+ return `action/${parts.selected_api}/${parts.action_type}/*`;
336
+ }
337
+
338
+ // src/builders.ts
339
+ var BuildHttpRequestContextOptionsSchema = import_zod2.z.object({
340
+ method: import_zod2.z.string(),
341
+ /** Raw URL string. Will be normalized with https:// if no protocol present. */
342
+ url: import_zod2.z.string(),
343
+ /** Raw headers from the request. Will be filtered through IGNORED_HEADERS. */
344
+ headers: import_zod2.z.record(
345
+ import_zod2.z.string(),
346
+ import_zod2.z.union([import_zod2.z.string(), import_zod2.z.array(import_zod2.z.string()), import_zod2.z.undefined()])
347
+ ).optional(),
348
+ /** Raw body string */
349
+ body: import_zod2.z.string().nullable().optional(),
350
+ /** Pre-parsed JSON body (e.g. from Fastify's automatic JSON parsing) */
351
+ body_json: import_policy_schema.JsonValueSchema.optional(),
352
+ /** Connection ID associated with this request */
353
+ connection_id: import_zod2.z.string().optional()
354
+ });
355
+ function buildHttpRequestContext(options) {
356
+ const parsed = BuildHttpRequestContextOptionsSchema.parse(options);
357
+ const rawUrl = parsed.url.includes("://") ? parsed.url : `https://${parsed.url}`;
358
+ const url = parseUrl(rawUrl);
359
+ const headers = {};
360
+ if (parsed.headers) {
361
+ for (const [key, value] of Object.entries(parsed.headers)) {
362
+ const lowerKey = key.toLowerCase();
363
+ if (!IGNORED_HEADERS.has(lowerKey) && typeof value === "string") {
364
+ headers[lowerKey] = value;
365
+ }
366
+ }
367
+ }
368
+ let body_json = null;
369
+ if (parsed.body_json !== void 0) {
370
+ body_json = parsed.body_json;
371
+ } else if (parsed.body) {
372
+ const contentType = headers["content-type"];
373
+ const isJson = typeof contentType === "string" && (contentType.includes("application/json") || contentType.includes("application/") && contentType.includes("+json"));
374
+ if (isJson) {
375
+ try {
376
+ body_json = JSON.parse(parsed.body);
377
+ } catch {
378
+ }
379
+ }
380
+ }
381
+ return HttpRequestContextSchema.parse({
382
+ request_type: "http_request",
383
+ method: parsed.method.toUpperCase(),
384
+ url,
385
+ headers,
386
+ body: parsed.body ?? null,
387
+ body_json,
388
+ connection_id: parsed.connection_id
389
+ });
390
+ }
391
+ var BuildActionRunContextOptionsSchema = import_zod2.z.object({
392
+ selected_api: import_zod2.z.string().regex(ACTION_RESOURCE_SEGMENT_PATTERN, {
393
+ message: "selected_api must contain only [A-Za-z0-9._@+-] (no globs, slashes, or whitespace)"
394
+ }),
395
+ action_type: ActionTypeSchema,
396
+ action_key: import_zod2.z.string().regex(ACTION_RESOURCE_SEGMENT_PATTERN, {
397
+ message: "action_key must contain only [A-Za-z0-9._@+-] (no globs, slashes, or whitespace)"
398
+ }),
399
+ connection_id: import_zod2.z.string().optional(),
400
+ inputs: import_policy_schema.JsonValueSchema.optional(),
401
+ reason: import_zod2.z.string().optional()
402
+ });
403
+ function buildActionRunContext(options) {
404
+ const parsed = BuildActionRunContextOptionsSchema.parse(options);
405
+ return ActionRunContextSchema.parse({
406
+ request_type: "action_run",
407
+ resource: buildActionResource({
408
+ selected_api: parsed.selected_api,
409
+ action_type: parsed.action_type,
410
+ action_key: parsed.action_key
411
+ }),
412
+ connection_id: parsed.connection_id,
413
+ inputs: parsed.inputs,
414
+ reason: parsed.reason
415
+ });
284
416
  }
285
417
 
286
418
  // src/policies.ts
@@ -319,7 +451,7 @@ function actionContextToPolicy(context) {
319
451
  statements.push({
320
452
  effect: "allow",
321
453
  permissions: ["can_execute"],
322
- resources: [buildActionResource(context)],
454
+ resources: [context.resource],
323
455
  conditions: buildExactActionConditions(context)
324
456
  });
325
457
  return { version: 2, statements };
@@ -333,9 +465,28 @@ function toExactPolicy(context) {
333
465
 
334
466
  // src/connection-policy.ts
335
467
  var import_zod3 = require("zod");
468
+ function globSelectedApiVersion(selectedApi) {
469
+ const at = selectedApi.indexOf("@");
470
+ return at === -1 ? selectedApi : selectedApi.slice(0, at) + "@*";
471
+ }
336
472
  var AccessLevelSchema = import_zod3.z.enum(["read", "read_write"]);
337
473
  var READ_HTTP_METHODS = ["GET", "HEAD", "OPTIONS"];
338
- var READ_ACTION_TYPES = ["read", "search"];
474
+ var READ_ACTION_TYPES = [
475
+ "read",
476
+ "read_bulk",
477
+ "search"
478
+ ];
479
+ var READ_HTTP_METHOD_SET = new Set(READ_HTTP_METHODS);
480
+ var READ_ACTION_TYPE_SET = new Set(
481
+ READ_ACTION_TYPES
482
+ );
483
+ function isWriteRequest(context) {
484
+ if (context.request_type === "http_request") {
485
+ return !READ_HTTP_METHOD_SET.has(context.method);
486
+ }
487
+ const { action_type } = parseActionResource(context.resource);
488
+ return !READ_ACTION_TYPE_SET.has(action_type);
489
+ }
339
490
  function buildConnectionPolicy(options) {
340
491
  const { connectionId, selectedApi, accessLevel } = options;
341
492
  const authStatement = {
@@ -354,7 +505,7 @@ function buildConnectionPolicy(options) {
354
505
  httpConditions.push({
355
506
  path: ["method"],
356
507
  operator: "in",
357
- value: READ_HTTP_METHODS
508
+ value: [...READ_HTTP_METHODS]
358
509
  });
359
510
  }
360
511
  const httpStatement = {
@@ -363,19 +514,13 @@ function buildConnectionPolicy(options) {
363
514
  resources: ["http"],
364
515
  conditions: httpConditions
365
516
  };
366
- const actionConditions = [connectionCondition];
367
- if (accessLevel === "read") {
368
- actionConditions.push({
369
- path: ["action_type"],
370
- operator: "in",
371
- value: READ_ACTION_TYPES
372
- });
373
- }
517
+ const apiPattern = globSelectedApiVersion(selectedApi);
518
+ const actionResources = accessLevel === "read" ? READ_ACTION_TYPES.map((t) => `action/${apiPattern}/${t}/*`) : [`action/${apiPattern}/*/*`];
374
519
  const actionStatement = {
375
520
  effect: "allow",
376
521
  permissions: ["can_execute"],
377
- resources: [`action/${selectedApi}/*/*`],
378
- conditions: actionConditions
522
+ resources: actionResources,
523
+ conditions: [connectionCondition]
379
524
  };
380
525
  return {
381
526
  version: 2,
@@ -386,6 +531,7 @@ function buildConnectionPolicy(options) {
386
531
  0 && (module.exports = {
387
532
  AccessLevelSchema,
388
533
  ActionRunContextSchema,
534
+ ActionTypeSchema,
389
535
  BuildActionRunContextOptionsSchema,
390
536
  BuildHttpRequestContextOptionsSchema,
391
537
  DEFAULT_PORTS,
@@ -393,6 +539,8 @@ function buildConnectionPolicy(options) {
393
539
  IGNORED_HEADERS,
394
540
  JsonValueSchema,
395
541
  ParsedUrlSchema,
542
+ READ_ACTION_TYPES,
543
+ READ_HTTP_METHODS,
396
544
  RequestContextSchema,
397
545
  buildActionResource,
398
546
  buildActionRunContext,
@@ -403,6 +551,8 @@ function buildConnectionPolicy(options) {
403
551
  buildExactHttpConditions,
404
552
  buildHttpRequestContext,
405
553
  buildUrlString,
554
+ isWriteRequest,
555
+ parseActionResource,
406
556
  parseUrl,
407
557
  toExactPolicy
408
558
  });