@zapier/policy-context 2.0.0 → 3.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.js CHANGED
@@ -1,5 +1,20 @@
1
1
  // src/types.ts
2
2
  import { z } from "zod";
3
+ function containsUnsafeInteger(value) {
4
+ if (typeof value === "number") {
5
+ return Number.isInteger(value) && !Number.isSafeInteger(value);
6
+ }
7
+ if (Array.isArray(value)) {
8
+ return value.some(containsUnsafeInteger);
9
+ }
10
+ if (value !== null && typeof value === "object") {
11
+ return Object.values(value).some(containsUnsafeInteger);
12
+ }
13
+ return false;
14
+ }
15
+ var SafeJsonValueSchema = z.json().refine((value) => !containsUnsafeInteger(value), {
16
+ message: "JSON integers must be within the IEEE-754 safe range; encode larger identifiers as strings"
17
+ });
3
18
  var ParsedUrlSchema = z.object({
4
19
  scheme: z.string().describe('URL scheme, e.g. "https"'),
5
20
  host: z.string().describe('Hostname, e.g. "api.example.com"'),
@@ -58,7 +73,7 @@ var HttpRequestContextSchema = z.object({
58
73
  * `z.json()` still rejects non-JSON values (`Date`, `undefined`,
59
74
  * functions) at parse time. Same inferred `JsonValue` type either way.
60
75
  */
61
- body_json: z.unknown().pipe(z.json()).nullable().describe("Parsed JSON body (if applicable)"),
76
+ body_json: z.unknown().pipe(SafeJsonValueSchema).nullable().describe("Parsed JSON body (if applicable)"),
62
77
  /** Zapier connection ID used for this request */
63
78
  connection_id: z.string().max(255).optional().describe("Zapier connection ID used for this request"),
64
79
  /** Optional human-readable reason for the request */
@@ -76,6 +91,11 @@ var ActionTypeSchema = z.enum([
76
91
  ]).meta({ id: "action_type", description: "Canonical Zapier action type." });
77
92
  var ACTION_RESOURCE_SEGMENT_PATTERN = /^[A-Za-z0-9._+-]+(?:@[A-Za-z0-9._+-]+)?$/;
78
93
  var ACTION_KEY_SEGMENT_PATTERN = /^[A-Za-z0-9._+:-]+(?:@[A-Za-z0-9._+:-]+)?$/;
94
+ var CONNECTION_ID_CHARS = "[^/*\\x00-\\x20\\x7f-\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]";
95
+ var CONNECTION_ID_PATTERN = new RegExp(`^${CONNECTION_ID_CHARS}+$`);
96
+ var CONNECTION_RESOURCE_PATTERN = new RegExp(
97
+ `^connection/(?:\\*|${CONNECTION_ID_CHARS}+)$`
98
+ );
79
99
  function parseActionResource(resource) {
80
100
  const segments = resource.split("/");
81
101
  if (segments.length !== 4 || segments[0] !== "action") {
@@ -136,7 +156,7 @@ var ActionRunContextInputSchema = z.object({
136
156
  }),
137
157
  connection_id: z.string().max(255).optional().describe("Zapier connection ID used for this request"),
138
158
  /** Action input parameters. See body_json above for the pipe rationale. */
139
- inputs: z.unknown().pipe(z.json()).optional().describe("Action input parameters"),
159
+ inputs: z.unknown().pipe(SafeJsonValueSchema).optional().describe("Action input parameters"),
140
160
  reason: z.string().max(1024).optional().describe("Optional human-readable reason for the request")
141
161
  }).meta({
142
162
  id: "ActionRunContext",
@@ -167,7 +187,7 @@ var ActionRunContextSchema = ActionRunContextInputSchema.transform(
167
187
  ...input.connection_id !== void 0 && {
168
188
  connection_id: input.connection_id
169
189
  },
170
- ...input.inputs !== void 0 && { inputs: input.inputs },
190
+ ...input.inputs !== void 0 && input.inputs !== null && { inputs: input.inputs },
171
191
  ...input.reason !== void 0 && { reason: input.reason }
172
192
  };
173
193
  }
@@ -242,14 +262,14 @@ function buildExactHttpConditions(context) {
242
262
  conditions.push({
243
263
  path: ["url", "query", key],
244
264
  operator: "equals",
245
- value: values
265
+ value: structuredClone(values)
246
266
  });
247
267
  }
248
268
  if (context.body_json !== null) {
249
269
  conditions.push({
250
270
  path: ["body_json"],
251
271
  operator: "equals",
252
- value: context.body_json
272
+ value: structuredClone(context.body_json)
253
273
  });
254
274
  } else if (context.body !== null) {
255
275
  conditions.push({
@@ -284,7 +304,7 @@ function buildExactActionConditions(context) {
284
304
  conditions.push({
285
305
  path: ["inputs"],
286
306
  operator: "equals",
287
- value: context.inputs
307
+ value: structuredClone(context.inputs)
288
308
  });
289
309
  }
290
310
  return conditions;
@@ -309,7 +329,7 @@ var BuildHttpRequestContextOptionsSchema = z2.object({
309
329
  /** Raw body string */
310
330
  body: z2.string().nullable().optional(),
311
331
  /** Pre-parsed JSON body (e.g. from Fastify's automatic JSON parsing) */
312
- body_json: z2.json().optional(),
332
+ body_json: SafeJsonValueSchema.optional(),
313
333
  /** Connection ID associated with this request */
314
334
  connection_id: z2.string().optional()
315
335
  });
@@ -358,7 +378,7 @@ var BuildActionRunContextOptionsSchema = z2.object({
358
378
  message: "action_key must contain only [A-Za-z0-9._@:+-] (no globs, slashes, or whitespace)"
359
379
  }),
360
380
  connection_id: z2.string().optional(),
361
- inputs: z2.json().optional(),
381
+ inputs: SafeJsonValueSchema.optional(),
362
382
  reason: z2.string().optional()
363
383
  });
364
384
  function buildActionRunContext(options) {
@@ -379,6 +399,7 @@ function buildActionRunContext(options) {
379
399
  // src/policies.ts
380
400
  function buildAuthStatement(authenticationId) {
381
401
  return {
402
+ id: `s_${crypto.randomUUID()}`,
382
403
  effect: "allow",
383
404
  permissions: ["can_use"],
384
405
  resources: [`connection/${authenticationId}`],
@@ -386,36 +407,38 @@ function buildAuthStatement(authenticationId) {
386
407
  };
387
408
  }
388
409
  function httpContextToPolicy(context) {
389
- const statements = [];
390
- if (context.connection_id) {
391
- statements.push(buildAuthStatement(context.connection_id));
392
- }
393
410
  const conditions = buildExactHttpConditions(context);
394
411
  conditions.push({
395
412
  path: ["url", "query"],
396
413
  operator: "keys_in",
397
414
  value: Object.keys(context.url.query)
398
415
  });
399
- statements.push({
416
+ const statement = {
417
+ id: `s_${crypto.randomUUID()}`,
400
418
  effect: "allow",
401
419
  permissions: ["can_execute"],
402
420
  resources: ["http"],
403
421
  conditions
404
- });
405
- return { version: 2, statements };
422
+ };
423
+ const statements = context.connection_id ? [buildAuthStatement(context.connection_id), statement] : [statement];
424
+ return {
425
+ version: 3,
426
+ statements: [statements]
427
+ };
406
428
  }
407
429
  function actionContextToPolicy(context) {
408
- const statements = [];
409
- if (context.connection_id) {
410
- statements.push(buildAuthStatement(context.connection_id));
411
- }
412
- statements.push({
430
+ const statement = {
431
+ id: `s_${crypto.randomUUID()}`,
413
432
  effect: "allow",
414
433
  permissions: ["can_execute"],
415
434
  resources: [context.resource],
416
435
  conditions: buildExactActionConditions(context)
417
- });
418
- return { version: 2, statements };
436
+ };
437
+ const statements = context.connection_id ? [buildAuthStatement(context.connection_id), statement] : [statement];
438
+ return {
439
+ version: 3,
440
+ statements: [statements]
441
+ };
419
442
  }
420
443
  function toExactPolicy(context) {
421
444
  if (context.request_type === "http_request") {
@@ -424,9 +447,140 @@ function toExactPolicy(context) {
424
447
  return actionContextToPolicy(context);
425
448
  }
426
449
 
427
- // src/connection-policy.ts
450
+ // src/zapier-policy.ts
451
+ import { isStatementSubsumedBy } from "@zapier/policy";
452
+ import { PolicySchema } from "@zapier/policy-schema";
428
453
  import { z as z3 } from "zod";
429
- var AccessLevelSchema = z3.enum(["read", "read_write"]);
454
+ var SELECTED_API_GLOB_PART_PATTERN = /^[A-Za-z0-9._+*-]+$/;
455
+ var ACTION_KEY_GLOB_PART_PATTERN = /^[A-Za-z0-9._+:*-]+$/;
456
+ var PERMISSIONS = /* @__PURE__ */ new Set([
457
+ "can_execute",
458
+ "can_use",
459
+ "can_view",
460
+ "can_edit",
461
+ "*"
462
+ ]);
463
+ var EFFECT_PRECEDENCE = { allow: 0, ask: 1, deny: 2 };
464
+ function isSelectedApiPattern(value) {
465
+ return isGlobSegment(
466
+ value,
467
+ ACTION_RESOURCE_SEGMENT_PATTERN,
468
+ SELECTED_API_GLOB_PART_PATTERN
469
+ );
470
+ }
471
+ function isActionKeyPattern(value) {
472
+ return isGlobSegment(
473
+ value,
474
+ ACTION_KEY_SEGMENT_PATTERN,
475
+ ACTION_KEY_GLOB_PART_PATTERN
476
+ );
477
+ }
478
+ function isGlobSegment(value, exactPattern, globPartPattern) {
479
+ if (exactPattern.test(value)) {
480
+ return true;
481
+ }
482
+ if (!value.includes("*") || value.includes("**")) {
483
+ return false;
484
+ }
485
+ const parts = value.split("@");
486
+ return parts.length <= 2 && parts.every((part) => globPartPattern.test(part));
487
+ }
488
+ function isActionResourcePattern(value) {
489
+ const segments = value.split("/");
490
+ if (segments.length !== 4 || segments[0] !== "action") {
491
+ return false;
492
+ }
493
+ const [, selectedApi, actionType, actionKey] = segments;
494
+ return selectedApi !== void 0 && actionType !== void 0 && actionKey !== void 0 && isSelectedApiPattern(selectedApi) && (actionType === "*" || ActionTypeSchema.safeParse(actionType).success) && isActionKeyPattern(actionKey);
495
+ }
496
+ var ActionResourcePatternSchema = z3.string().refine(isActionResourcePattern, {
497
+ message: "Expected action/{selected_api}/{canonical_action_type|*}/{action_key_pattern}"
498
+ });
499
+ function resourceFamily(resource) {
500
+ if (resource === "*" || resource === "http") {
501
+ return resource;
502
+ }
503
+ if (ActionResourcePatternSchema.safeParse(resource).success) {
504
+ return "action";
505
+ }
506
+ if (CONNECTION_RESOURCE_PATTERN.test(resource)) {
507
+ return "connection";
508
+ }
509
+ return void 0;
510
+ }
511
+ function permissionAppliesTo(permission, family) {
512
+ if (permission === "*" || family === "*") {
513
+ return true;
514
+ }
515
+ return permission === "can_execute" ? family === "action" || family === "http" : family === "connection";
516
+ }
517
+ var ZapierPolicySchema = PolicySchema.superRefine((policy, ctx) => {
518
+ const tiers = policy.version === 3 ? policy.statements.map((statements, tierIndex) => ({
519
+ statements,
520
+ path: ["statements", tierIndex]
521
+ })) : [{ statements: policy.statements, path: ["statements"] }];
522
+ tiers.forEach(({ statements, path }) => {
523
+ statements.forEach((statement, statementIndex) => {
524
+ const families = statement.resources.map((resource, resourceIndex) => {
525
+ const family = resourceFamily(resource);
526
+ if (family === void 0) {
527
+ ctx.addIssue({
528
+ code: "custom",
529
+ message: 'Expected "http", "connection/{id|*}", a valid action pattern, or "*"',
530
+ path: [...path, statementIndex, "resources", resourceIndex]
531
+ });
532
+ }
533
+ return family;
534
+ });
535
+ statement.permissions.forEach((permission, permissionIndex) => {
536
+ if (!PERMISSIONS.has(permission)) {
537
+ ctx.addIssue({
538
+ code: "custom",
539
+ message: `Unknown Zapier permission "${permission}"`,
540
+ path: [...path, statementIndex, "permissions", permissionIndex]
541
+ });
542
+ return;
543
+ }
544
+ const incompatibleFamily = families.find(
545
+ (family) => family !== void 0 && !permissionAppliesTo(permission, family)
546
+ );
547
+ if (incompatibleFamily !== void 0) {
548
+ ctx.addIssue({
549
+ code: "custom",
550
+ message: `Permission "${permission}" cannot apply to ${incompatibleFamily} resources`,
551
+ path: [...path, statementIndex, "permissions", permissionIndex]
552
+ });
553
+ }
554
+ });
555
+ const maskingStatementIndex = statements.findIndex(
556
+ (maskingStatement, maskingIndex) => maskingIndex !== statementIndex && EFFECT_PRECEDENCE[maskingStatement.effect] > EFFECT_PRECEDENCE[statement.effect] && isStatementSubsumedBy(statement, {
557
+ ...maskingStatement,
558
+ effect: statement.effect
559
+ })
560
+ );
561
+ if (maskingStatementIndex !== -1) {
562
+ const maskingStatement = statements[maskingStatementIndex];
563
+ const maskingId = maskingStatement.id ? ` (id "${maskingStatement.id}")` : "";
564
+ ctx.addIssue({
565
+ code: "custom",
566
+ message: `Statement is entirely masked by higher-precedence statement at index ${maskingStatementIndex}${maskingId}`,
567
+ path: [...path, statementIndex]
568
+ });
569
+ }
570
+ });
571
+ });
572
+ });
573
+
574
+ // src/connection-policy.ts
575
+ import { z as z4 } from "zod";
576
+ var AccessLevelSchema = z4.enum(["read", "read_write"]);
577
+ function requireAccessLevel(accessLevel) {
578
+ if (!AccessLevelSchema.safeParse(accessLevel).success) {
579
+ throw new Error(
580
+ `invalid access_level "${accessLevel}": expected "read" or "read_write"`
581
+ );
582
+ }
583
+ }
430
584
  var READ_HTTP_METHODS = ["GET", "HEAD", "OPTIONS"];
431
585
  var READ_ACTION_TYPES = [
432
586
  "read",
@@ -446,7 +600,19 @@ function isWriteRequest(context) {
446
600
  }
447
601
  function buildConnectionPolicy(options) {
448
602
  const { connectionId, selectedApi, accessLevel } = options;
603
+ requireAccessLevel(accessLevel);
604
+ if (!CONNECTION_ID_PATTERN.test(connectionId)) {
605
+ throw new Error(
606
+ `invalid connection_id "${connectionId}": must not contain "/", "*", or whitespace`
607
+ );
608
+ }
609
+ if (!ACTION_RESOURCE_SEGMENT_PATTERN.test(selectedApi)) {
610
+ throw new Error(
611
+ `invalid selected_api "${selectedApi}": must not contain "/", "*", or whitespace`
612
+ );
613
+ }
449
614
  const authStatement = {
615
+ id: `s_${crypto.randomUUID()}`,
450
616
  effect: "allow",
451
617
  permissions: ["can_use"],
452
618
  resources: [`connection/${connectionId}`],
@@ -466,6 +632,7 @@ function buildConnectionPolicy(options) {
466
632
  });
467
633
  }
468
634
  const httpStatement = {
635
+ id: `s_${crypto.randomUUID()}`,
469
636
  effect: "allow",
470
637
  permissions: ["can_execute"],
471
638
  resources: ["http"],
@@ -474,20 +641,22 @@ function buildConnectionPolicy(options) {
474
641
  const apiPattern = globSelectedApiVersion(selectedApi);
475
642
  const actionResources = accessLevel === "read" ? READ_ACTION_TYPES.map((t) => `action/${apiPattern}/${t}/*`) : [`action/${apiPattern}/*/*`];
476
643
  const actionStatement = {
644
+ id: `s_${crypto.randomUUID()}`,
477
645
  effect: "allow",
478
646
  permissions: ["can_execute"],
479
647
  resources: actionResources,
480
648
  conditions: [connectionCondition]
481
649
  };
482
650
  return {
483
- version: 2,
484
- statements: [authStatement, httpStatement, actionStatement]
651
+ version: 3,
652
+ statements: [[authStatement, httpStatement, actionStatement]]
485
653
  };
486
654
  }
487
655
 
488
656
  // src/connectionless-policy.ts
489
657
  function buildConnectionlessPolicy(options) {
490
658
  const { context, accessLevel } = options;
659
+ requireAccessLevel(accessLevel);
491
660
  if (context.request_type === "http_request") {
492
661
  const conditions = [
493
662
  {
@@ -504,26 +673,35 @@ function buildConnectionlessPolicy(options) {
504
673
  });
505
674
  }
506
675
  const statement2 = {
676
+ id: `s_${crypto.randomUUID()}`,
507
677
  effect: "allow",
508
678
  permissions: ["can_execute"],
509
679
  resources: ["http"],
510
680
  conditions
511
681
  };
512
- return { version: 2, statements: [statement2] };
682
+ return {
683
+ version: 3,
684
+ statements: [[statement2]]
685
+ };
513
686
  }
514
687
  const { selected_api } = parseActionResource(context.resource);
515
688
  const apiPattern = globSelectedApiVersion(selected_api);
516
689
  const resources = accessLevel === "read" ? READ_ACTION_TYPES.map((t) => `action/${apiPattern}/${t}/*`) : [`action/${apiPattern}/*/*`];
517
690
  const statement = {
691
+ id: `s_${crypto.randomUUID()}`,
518
692
  effect: "allow",
519
693
  permissions: ["can_execute"],
520
694
  resources,
521
695
  conditions: []
522
696
  };
523
- return { version: 2, statements: [statement] };
697
+ return {
698
+ version: 3,
699
+ statements: [[statement]]
700
+ };
524
701
  }
525
702
  export {
526
703
  AccessLevelSchema,
704
+ ActionResourcePatternSchema,
527
705
  ActionRunContextInputSchema,
528
706
  ActionRunContextSchema,
529
707
  ActionTypeSchema,
@@ -536,6 +714,7 @@ export {
536
714
  READ_ACTION_TYPES,
537
715
  READ_HTTP_METHODS,
538
716
  RequestContextSchema,
717
+ ZapierPolicySchema,
539
718
  buildActionResource,
540
719
  buildActionRunContext,
541
720
  buildBroadActionResource,