@zapier/policy-context 2.0.0 → 3.0.0-mr.39.da73fd4a

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 CHANGED
@@ -16,7 +16,7 @@ npm install @zapier/policy-context zod
16
16
 
17
17
  ## What it provides
18
18
 
19
- Three layers, each building on the previous:
19
+ Four layers, each building on the previous:
20
20
 
21
21
  1. **Types** — Zod schemas for `HttpRequestContext`, `ActionRunContext`, and the
22
22
  `RequestContext` discriminated union. The canonical shapes stored in
@@ -25,7 +25,10 @@ Three layers, each building on the previous:
25
25
  body coercion) into validated contexts.
26
26
  3. **Conditions / Policies** — Convert stored contexts into
27
27
  [`@zapier/policy-schema`](https://www.npmjs.com/package/@zapier/policy-schema)
28
- `Condition[]` and full `Policy` documents.
28
+ `Condition[]` and full v4 policy documents with one rule tier (`rules: [[...]]`)
29
+ and randomly generated UUID rule IDs prefixed with `r_`.
30
+ 4. **Zapier policy validation** — `ZapierPolicySchema` composes the generic
31
+ `PolicySchema` with Zapier resource grammar and permission/resource rules.
29
32
 
30
33
  ## Usage
31
34
 
@@ -65,6 +68,11 @@ import { toExactPolicy } from "@zapier/policy-context";
65
68
  const policy = toExactPolicy(context);
66
69
  ```
67
70
 
71
+ Policy builders return v4 only, typed as `Extract<Policy, { version: 4 }>`, with
72
+ a single rule tier (`rules: [[...]]`) and `r_<uuid>` rule IDs. Rule content and
73
+ ordering are unchanged. Migrate `policy.statements[0][i]` to `policy.rules[0][i]`
74
+ and any statement-ID prefix handling from `s_` to `r_`.
75
+
68
76
  ### Connection-scoped policies
69
77
 
70
78
  ```ts
@@ -76,6 +84,24 @@ const policy = buildConnectionPolicy({
76
84
  });
77
85
  ```
78
86
 
87
+ ### Validate a Zapier policy
88
+
89
+ ```ts
90
+ import { ZapierPolicySchema } from "@zapier/policy-context";
91
+
92
+ const policy = ZapierPolicySchema.parse(input);
93
+ ```
94
+
95
+ `ZapierPolicySchema` output includes v4 documents as well as earlier versions.
96
+ This accepts `http`, `connection/{id|*}`, canonical
97
+ `action/{selected_api}/{action_type|*}/{action_key}` patterns, and the lone `*`
98
+ god-mode resource. It validates structure only; app, API version, and action-key
99
+ existence remain the caller's responsibility. It also rejects a statement or rule
100
+ when one higher-precedence statement or rule (`deny` > `ask` > `allow`) fully
101
+ subsumes it. Validation traverses each ordered tier of v3 `statements` and v4
102
+ `rules`, checking masking only within that tier. Same-effect redundancy and
103
+ coverage assembled from multiple statements or rules remain valid.
104
+
79
105
  ## License
80
106
 
81
107
  Published as part of Zapier's services. Use is governed by the
package/dist/index.cjs CHANGED
@@ -21,6 +21,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
23
  AccessLevelSchema: () => AccessLevelSchema,
24
+ ActionResourcePatternSchema: () => ActionResourcePatternSchema,
24
25
  ActionRunContextInputSchema: () => ActionRunContextInputSchema,
25
26
  ActionRunContextSchema: () => ActionRunContextSchema,
26
27
  ActionTypeSchema: () => ActionTypeSchema,
@@ -33,6 +34,7 @@ __export(index_exports, {
33
34
  READ_ACTION_TYPES: () => READ_ACTION_TYPES,
34
35
  READ_HTTP_METHODS: () => READ_HTTP_METHODS,
35
36
  RequestContextSchema: () => RequestContextSchema,
37
+ ZapierPolicySchema: () => ZapierPolicySchema,
36
38
  buildActionResource: () => buildActionResource,
37
39
  buildActionRunContext: () => buildActionRunContext,
38
40
  buildBroadActionResource: () => buildBroadActionResource,
@@ -55,6 +57,21 @@ module.exports = __toCommonJS(index_exports);
55
57
 
56
58
  // src/types.ts
57
59
  var import_zod = require("zod");
60
+ function containsUnsafeInteger(value) {
61
+ if (typeof value === "number") {
62
+ return Number.isInteger(value) && !Number.isSafeInteger(value);
63
+ }
64
+ if (Array.isArray(value)) {
65
+ return value.some(containsUnsafeInteger);
66
+ }
67
+ if (value !== null && typeof value === "object") {
68
+ return Object.values(value).some(containsUnsafeInteger);
69
+ }
70
+ return false;
71
+ }
72
+ var SafeJsonValueSchema = import_zod.z.json().refine((value) => !containsUnsafeInteger(value), {
73
+ message: "JSON integers must be within the IEEE-754 safe range; encode larger identifiers as strings"
74
+ });
58
75
  var ParsedUrlSchema = import_zod.z.object({
59
76
  scheme: import_zod.z.string().describe('URL scheme, e.g. "https"'),
60
77
  host: import_zod.z.string().describe('Hostname, e.g. "api.example.com"'),
@@ -113,7 +130,7 @@ var HttpRequestContextSchema = import_zod.z.object({
113
130
  * `z.json()` still rejects non-JSON values (`Date`, `undefined`,
114
131
  * functions) at parse time. Same inferred `JsonValue` type either way.
115
132
  */
116
- body_json: import_zod.z.unknown().pipe(import_zod.z.json()).nullable().describe("Parsed JSON body (if applicable)"),
133
+ body_json: import_zod.z.unknown().pipe(SafeJsonValueSchema).nullable().describe("Parsed JSON body (if applicable)"),
117
134
  /** Zapier connection ID used for this request */
118
135
  connection_id: import_zod.z.string().max(255).optional().describe("Zapier connection ID used for this request"),
119
136
  /** Optional human-readable reason for the request */
@@ -131,6 +148,11 @@ var ActionTypeSchema = import_zod.z.enum([
131
148
  ]).meta({ id: "action_type", description: "Canonical Zapier action type." });
132
149
  var ACTION_RESOURCE_SEGMENT_PATTERN = /^[A-Za-z0-9._+-]+(?:@[A-Za-z0-9._+-]+)?$/;
133
150
  var ACTION_KEY_SEGMENT_PATTERN = /^[A-Za-z0-9._+:-]+(?:@[A-Za-z0-9._+:-]+)?$/;
151
+ var CONNECTION_ID_CHARS = "[^/*\\x00-\\x20\\x7f-\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]";
152
+ var CONNECTION_ID_PATTERN = new RegExp(`^${CONNECTION_ID_CHARS}+$`);
153
+ var CONNECTION_RESOURCE_PATTERN = new RegExp(
154
+ `^connection/(?:\\*|${CONNECTION_ID_CHARS}+)$`
155
+ );
134
156
  function parseActionResource(resource) {
135
157
  const segments = resource.split("/");
136
158
  if (segments.length !== 4 || segments[0] !== "action") {
@@ -191,7 +213,7 @@ var ActionRunContextInputSchema = import_zod.z.object({
191
213
  }),
192
214
  connection_id: import_zod.z.string().max(255).optional().describe("Zapier connection ID used for this request"),
193
215
  /** Action input parameters. See body_json above for the pipe rationale. */
194
- inputs: import_zod.z.unknown().pipe(import_zod.z.json()).optional().describe("Action input parameters"),
216
+ inputs: import_zod.z.unknown().pipe(SafeJsonValueSchema).optional().describe("Action input parameters"),
195
217
  reason: import_zod.z.string().max(1024).optional().describe("Optional human-readable reason for the request")
196
218
  }).meta({
197
219
  id: "ActionRunContext",
@@ -222,7 +244,7 @@ var ActionRunContextSchema = ActionRunContextInputSchema.transform(
222
244
  ...input.connection_id !== void 0 && {
223
245
  connection_id: input.connection_id
224
246
  },
225
- ...input.inputs !== void 0 && { inputs: input.inputs },
247
+ ...input.inputs !== void 0 && input.inputs !== null && { inputs: input.inputs },
226
248
  ...input.reason !== void 0 && { reason: input.reason }
227
249
  };
228
250
  }
@@ -297,14 +319,14 @@ function buildExactHttpConditions(context) {
297
319
  conditions.push({
298
320
  path: ["url", "query", key],
299
321
  operator: "equals",
300
- value: values
322
+ value: structuredClone(values)
301
323
  });
302
324
  }
303
325
  if (context.body_json !== null) {
304
326
  conditions.push({
305
327
  path: ["body_json"],
306
328
  operator: "equals",
307
- value: context.body_json
329
+ value: structuredClone(context.body_json)
308
330
  });
309
331
  } else if (context.body !== null) {
310
332
  conditions.push({
@@ -339,7 +361,7 @@ function buildExactActionConditions(context) {
339
361
  conditions.push({
340
362
  path: ["inputs"],
341
363
  operator: "equals",
342
- value: context.inputs
364
+ value: structuredClone(context.inputs)
343
365
  });
344
366
  }
345
367
  return conditions;
@@ -364,7 +386,7 @@ var BuildHttpRequestContextOptionsSchema = import_zod2.z.object({
364
386
  /** Raw body string */
365
387
  body: import_zod2.z.string().nullable().optional(),
366
388
  /** Pre-parsed JSON body (e.g. from Fastify's automatic JSON parsing) */
367
- body_json: import_zod2.z.json().optional(),
389
+ body_json: SafeJsonValueSchema.optional(),
368
390
  /** Connection ID associated with this request */
369
391
  connection_id: import_zod2.z.string().optional()
370
392
  });
@@ -413,7 +435,7 @@ var BuildActionRunContextOptionsSchema = import_zod2.z.object({
413
435
  message: "action_key must contain only [A-Za-z0-9._@:+-] (no globs, slashes, or whitespace)"
414
436
  }),
415
437
  connection_id: import_zod2.z.string().optional(),
416
- inputs: import_zod2.z.json().optional(),
438
+ inputs: SafeJsonValueSchema.optional(),
417
439
  reason: import_zod2.z.string().optional()
418
440
  });
419
441
  function buildActionRunContext(options) {
@@ -434,6 +456,7 @@ function buildActionRunContext(options) {
434
456
  // src/policies.ts
435
457
  function buildAuthStatement(authenticationId) {
436
458
  return {
459
+ id: `r_${crypto.randomUUID()}`,
437
460
  effect: "allow",
438
461
  permissions: ["can_use"],
439
462
  resources: [`connection/${authenticationId}`],
@@ -441,36 +464,38 @@ function buildAuthStatement(authenticationId) {
441
464
  };
442
465
  }
443
466
  function httpContextToPolicy(context) {
444
- const statements = [];
445
- if (context.connection_id) {
446
- statements.push(buildAuthStatement(context.connection_id));
447
- }
448
467
  const conditions = buildExactHttpConditions(context);
449
468
  conditions.push({
450
469
  path: ["url", "query"],
451
470
  operator: "keys_in",
452
471
  value: Object.keys(context.url.query)
453
472
  });
454
- statements.push({
473
+ const statement = {
474
+ id: `r_${crypto.randomUUID()}`,
455
475
  effect: "allow",
456
476
  permissions: ["can_execute"],
457
477
  resources: ["http"],
458
478
  conditions
459
- });
460
- return { version: 2, statements };
479
+ };
480
+ const statements = context.connection_id ? [buildAuthStatement(context.connection_id), statement] : [statement];
481
+ return {
482
+ version: 4,
483
+ rules: [statements]
484
+ };
461
485
  }
462
486
  function actionContextToPolicy(context) {
463
- const statements = [];
464
- if (context.connection_id) {
465
- statements.push(buildAuthStatement(context.connection_id));
466
- }
467
- statements.push({
487
+ const statement = {
488
+ id: `r_${crypto.randomUUID()}`,
468
489
  effect: "allow",
469
490
  permissions: ["can_execute"],
470
491
  resources: [context.resource],
471
492
  conditions: buildExactActionConditions(context)
472
- });
473
- return { version: 2, statements };
493
+ };
494
+ const statements = context.connection_id ? [buildAuthStatement(context.connection_id), statement] : [statement];
495
+ return {
496
+ version: 4,
497
+ rules: [statements]
498
+ };
474
499
  }
475
500
  function toExactPolicy(context) {
476
501
  if (context.request_type === "http_request") {
@@ -479,9 +504,160 @@ function toExactPolicy(context) {
479
504
  return actionContextToPolicy(context);
480
505
  }
481
506
 
482
- // src/connection-policy.ts
507
+ // src/zapier-policy.ts
508
+ var import_policy = require("@zapier/policy");
509
+ var import_policy_schema = require("@zapier/policy-schema");
483
510
  var import_zod3 = require("zod");
484
- var AccessLevelSchema = import_zod3.z.enum(["read", "read_write"]);
511
+ var SELECTED_API_GLOB_PART_PATTERN = /^[A-Za-z0-9._+*-]+$/;
512
+ var ACTION_KEY_GLOB_PART_PATTERN = /^[A-Za-z0-9._+:*-]+$/;
513
+ var PERMISSIONS = /* @__PURE__ */ new Set([
514
+ "can_execute",
515
+ "can_use",
516
+ "can_view",
517
+ "can_edit",
518
+ "*"
519
+ ]);
520
+ var EFFECT_PRECEDENCE = { allow: 0, ask: 1, deny: 2 };
521
+ function isSelectedApiPattern(value) {
522
+ return isGlobSegment(
523
+ value,
524
+ ACTION_RESOURCE_SEGMENT_PATTERN,
525
+ SELECTED_API_GLOB_PART_PATTERN
526
+ );
527
+ }
528
+ function isActionKeyPattern(value) {
529
+ return isGlobSegment(
530
+ value,
531
+ ACTION_KEY_SEGMENT_PATTERN,
532
+ ACTION_KEY_GLOB_PART_PATTERN
533
+ );
534
+ }
535
+ function isGlobSegment(value, exactPattern, globPartPattern) {
536
+ if (exactPattern.test(value)) {
537
+ return true;
538
+ }
539
+ if (!value.includes("*") || value.includes("**")) {
540
+ return false;
541
+ }
542
+ const parts = value.split("@");
543
+ return parts.length <= 2 && parts.every((part) => globPartPattern.test(part));
544
+ }
545
+ function isActionResourcePattern(value) {
546
+ const segments = value.split("/");
547
+ if (segments.length !== 4 || segments[0] !== "action") {
548
+ return false;
549
+ }
550
+ const [, selectedApi, actionType, actionKey] = segments;
551
+ return selectedApi !== void 0 && actionType !== void 0 && actionKey !== void 0 && isSelectedApiPattern(selectedApi) && (actionType === "*" || ActionTypeSchema.safeParse(actionType).success) && isActionKeyPattern(actionKey);
552
+ }
553
+ var ActionResourcePatternSchema = import_zod3.z.string().refine(isActionResourcePattern, {
554
+ message: "Expected action/{selected_api}/{canonical_action_type|*}/{action_key_pattern}"
555
+ });
556
+ function resourceFamily(resource) {
557
+ if (resource === "*" || resource === "http") {
558
+ return resource;
559
+ }
560
+ if (ActionResourcePatternSchema.safeParse(resource).success) {
561
+ return "action";
562
+ }
563
+ if (CONNECTION_RESOURCE_PATTERN.test(resource)) {
564
+ return "connection";
565
+ }
566
+ return void 0;
567
+ }
568
+ function permissionAppliesTo(permission, family) {
569
+ if (permission === "*" || family === "*") {
570
+ return true;
571
+ }
572
+ return permission === "can_execute" ? family === "action" || family === "http" : family === "connection";
573
+ }
574
+ function statementGroups(policy) {
575
+ if (policy.version === 4) {
576
+ return policy.rules.map((rules, tierIndex) => ({
577
+ statements: rules,
578
+ path: ["rules", tierIndex],
579
+ entry: "rule"
580
+ }));
581
+ }
582
+ if (policy.version === 3) {
583
+ return policy.statements.map((statements, tierIndex) => ({
584
+ statements,
585
+ path: ["statements", tierIndex],
586
+ entry: "statement"
587
+ }));
588
+ }
589
+ return [
590
+ {
591
+ statements: policy.statements,
592
+ path: ["statements"],
593
+ entry: "statement"
594
+ }
595
+ ];
596
+ }
597
+ var ZapierPolicySchema = import_policy_schema.PolicySchema.superRefine((policy, ctx) => {
598
+ statementGroups(policy).forEach(({ statements, path, entry }) => {
599
+ const Entry = entry === "rule" ? "Rule" : "Statement";
600
+ statements.forEach((statement, statementIndex) => {
601
+ const families = statement.resources.map((resource, resourceIndex) => {
602
+ const family = resourceFamily(resource);
603
+ if (family === void 0) {
604
+ ctx.addIssue({
605
+ code: "custom",
606
+ message: 'Expected "http", "connection/{id|*}", a valid action pattern, or "*"',
607
+ path: [...path, statementIndex, "resources", resourceIndex]
608
+ });
609
+ }
610
+ return family;
611
+ });
612
+ statement.permissions.forEach((permission, permissionIndex) => {
613
+ if (!PERMISSIONS.has(permission)) {
614
+ ctx.addIssue({
615
+ code: "custom",
616
+ message: `Unknown Zapier permission "${permission}"`,
617
+ path: [...path, statementIndex, "permissions", permissionIndex]
618
+ });
619
+ return;
620
+ }
621
+ const incompatibleFamily = families.find(
622
+ (family) => family !== void 0 && !permissionAppliesTo(permission, family)
623
+ );
624
+ if (incompatibleFamily !== void 0) {
625
+ ctx.addIssue({
626
+ code: "custom",
627
+ message: `Permission "${permission}" cannot apply to ${incompatibleFamily} resources`,
628
+ path: [...path, statementIndex, "permissions", permissionIndex]
629
+ });
630
+ }
631
+ });
632
+ const maskingStatementIndex = statements.findIndex(
633
+ (maskingStatement, maskingIndex) => maskingIndex !== statementIndex && EFFECT_PRECEDENCE[maskingStatement.effect] > EFFECT_PRECEDENCE[statement.effect] && (0, import_policy.isStatementSubsumedBy)(statement, {
634
+ ...maskingStatement,
635
+ effect: statement.effect
636
+ })
637
+ );
638
+ if (maskingStatementIndex !== -1) {
639
+ const maskingStatement = statements[maskingStatementIndex];
640
+ const maskingId = maskingStatement.id ? ` (id "${maskingStatement.id}")` : "";
641
+ ctx.addIssue({
642
+ code: "custom",
643
+ message: `${Entry} is entirely masked by higher-precedence ${entry} at index ${maskingStatementIndex}${maskingId}`,
644
+ path: [...path, statementIndex]
645
+ });
646
+ }
647
+ });
648
+ });
649
+ });
650
+
651
+ // src/connection-policy.ts
652
+ var import_zod4 = require("zod");
653
+ var AccessLevelSchema = import_zod4.z.enum(["read", "read_write"]);
654
+ function requireAccessLevel(accessLevel) {
655
+ if (!AccessLevelSchema.safeParse(accessLevel).success) {
656
+ throw new Error(
657
+ `invalid access_level "${accessLevel}": expected "read" or "read_write"`
658
+ );
659
+ }
660
+ }
485
661
  var READ_HTTP_METHODS = ["GET", "HEAD", "OPTIONS"];
486
662
  var READ_ACTION_TYPES = [
487
663
  "read",
@@ -501,7 +677,19 @@ function isWriteRequest(context) {
501
677
  }
502
678
  function buildConnectionPolicy(options) {
503
679
  const { connectionId, selectedApi, accessLevel } = options;
680
+ requireAccessLevel(accessLevel);
681
+ if (!CONNECTION_ID_PATTERN.test(connectionId)) {
682
+ throw new Error(
683
+ `invalid connection_id "${connectionId}": must not contain "/", "*", or whitespace`
684
+ );
685
+ }
686
+ if (!ACTION_RESOURCE_SEGMENT_PATTERN.test(selectedApi)) {
687
+ throw new Error(
688
+ `invalid selected_api "${selectedApi}": must not contain "/", "*", or whitespace`
689
+ );
690
+ }
504
691
  const authStatement = {
692
+ id: `r_${crypto.randomUUID()}`,
505
693
  effect: "allow",
506
694
  permissions: ["can_use"],
507
695
  resources: [`connection/${connectionId}`],
@@ -521,6 +709,7 @@ function buildConnectionPolicy(options) {
521
709
  });
522
710
  }
523
711
  const httpStatement = {
712
+ id: `r_${crypto.randomUUID()}`,
524
713
  effect: "allow",
525
714
  permissions: ["can_execute"],
526
715
  resources: ["http"],
@@ -529,20 +718,22 @@ function buildConnectionPolicy(options) {
529
718
  const apiPattern = globSelectedApiVersion(selectedApi);
530
719
  const actionResources = accessLevel === "read" ? READ_ACTION_TYPES.map((t) => `action/${apiPattern}/${t}/*`) : [`action/${apiPattern}/*/*`];
531
720
  const actionStatement = {
721
+ id: `r_${crypto.randomUUID()}`,
532
722
  effect: "allow",
533
723
  permissions: ["can_execute"],
534
724
  resources: actionResources,
535
725
  conditions: [connectionCondition]
536
726
  };
537
727
  return {
538
- version: 2,
539
- statements: [authStatement, httpStatement, actionStatement]
728
+ version: 4,
729
+ rules: [[authStatement, httpStatement, actionStatement]]
540
730
  };
541
731
  }
542
732
 
543
733
  // src/connectionless-policy.ts
544
734
  function buildConnectionlessPolicy(options) {
545
735
  const { context, accessLevel } = options;
736
+ requireAccessLevel(accessLevel);
546
737
  if (context.request_type === "http_request") {
547
738
  const conditions = [
548
739
  {
@@ -559,27 +750,36 @@ function buildConnectionlessPolicy(options) {
559
750
  });
560
751
  }
561
752
  const statement2 = {
753
+ id: `r_${crypto.randomUUID()}`,
562
754
  effect: "allow",
563
755
  permissions: ["can_execute"],
564
756
  resources: ["http"],
565
757
  conditions
566
758
  };
567
- return { version: 2, statements: [statement2] };
759
+ return {
760
+ version: 4,
761
+ rules: [[statement2]]
762
+ };
568
763
  }
569
764
  const { selected_api } = parseActionResource(context.resource);
570
765
  const apiPattern = globSelectedApiVersion(selected_api);
571
766
  const resources = accessLevel === "read" ? READ_ACTION_TYPES.map((t) => `action/${apiPattern}/${t}/*`) : [`action/${apiPattern}/*/*`];
572
767
  const statement = {
768
+ id: `r_${crypto.randomUUID()}`,
573
769
  effect: "allow",
574
770
  permissions: ["can_execute"],
575
771
  resources,
576
772
  conditions: []
577
773
  };
578
- return { version: 2, statements: [statement] };
774
+ return {
775
+ version: 4,
776
+ rules: [[statement]]
777
+ };
579
778
  }
580
779
  // Annotate the CommonJS export names for ESM import in node:
581
780
  0 && (module.exports = {
582
781
  AccessLevelSchema,
782
+ ActionResourcePatternSchema,
583
783
  ActionRunContextInputSchema,
584
784
  ActionRunContextSchema,
585
785
  ActionTypeSchema,
@@ -592,6 +792,7 @@ function buildConnectionlessPolicy(options) {
592
792
  READ_ACTION_TYPES,
593
793
  READ_HTTP_METHODS,
594
794
  RequestContextSchema,
795
+ ZapierPolicySchema,
595
796
  buildActionResource,
596
797
  buildActionRunContext,
597
798
  buildBroadActionResource,