@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/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: `r_${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: `r_${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: 4,
426
+ rules: [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: `r_${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: 4,
440
+ rules: [statements]
441
+ };
419
442
  }
420
443
  function toExactPolicy(context) {
421
444
  if (context.request_type === "http_request") {
@@ -424,9 +447,162 @@ 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 {
453
+ PolicySchema
454
+ } from "@zapier/policy-schema";
428
455
  import { z as z3 } from "zod";
429
- var AccessLevelSchema = z3.enum(["read", "read_write"]);
456
+ var SELECTED_API_GLOB_PART_PATTERN = /^[A-Za-z0-9._+*-]+$/;
457
+ var ACTION_KEY_GLOB_PART_PATTERN = /^[A-Za-z0-9._+:*-]+$/;
458
+ var PERMISSIONS = /* @__PURE__ */ new Set([
459
+ "can_execute",
460
+ "can_use",
461
+ "can_view",
462
+ "can_edit",
463
+ "*"
464
+ ]);
465
+ var EFFECT_PRECEDENCE = { allow: 0, ask: 1, deny: 2 };
466
+ function isSelectedApiPattern(value) {
467
+ return isGlobSegment(
468
+ value,
469
+ ACTION_RESOURCE_SEGMENT_PATTERN,
470
+ SELECTED_API_GLOB_PART_PATTERN
471
+ );
472
+ }
473
+ function isActionKeyPattern(value) {
474
+ return isGlobSegment(
475
+ value,
476
+ ACTION_KEY_SEGMENT_PATTERN,
477
+ ACTION_KEY_GLOB_PART_PATTERN
478
+ );
479
+ }
480
+ function isGlobSegment(value, exactPattern, globPartPattern) {
481
+ if (exactPattern.test(value)) {
482
+ return true;
483
+ }
484
+ if (!value.includes("*") || value.includes("**")) {
485
+ return false;
486
+ }
487
+ const parts = value.split("@");
488
+ return parts.length <= 2 && parts.every((part) => globPartPattern.test(part));
489
+ }
490
+ function isActionResourcePattern(value) {
491
+ const segments = value.split("/");
492
+ if (segments.length !== 4 || segments[0] !== "action") {
493
+ return false;
494
+ }
495
+ const [, selectedApi, actionType, actionKey] = segments;
496
+ return selectedApi !== void 0 && actionType !== void 0 && actionKey !== void 0 && isSelectedApiPattern(selectedApi) && (actionType === "*" || ActionTypeSchema.safeParse(actionType).success) && isActionKeyPattern(actionKey);
497
+ }
498
+ var ActionResourcePatternSchema = z3.string().refine(isActionResourcePattern, {
499
+ message: "Expected action/{selected_api}/{canonical_action_type|*}/{action_key_pattern}"
500
+ });
501
+ function resourceFamily(resource) {
502
+ if (resource === "*" || resource === "http") {
503
+ return resource;
504
+ }
505
+ if (ActionResourcePatternSchema.safeParse(resource).success) {
506
+ return "action";
507
+ }
508
+ if (CONNECTION_RESOURCE_PATTERN.test(resource)) {
509
+ return "connection";
510
+ }
511
+ return void 0;
512
+ }
513
+ function permissionAppliesTo(permission, family) {
514
+ if (permission === "*" || family === "*") {
515
+ return true;
516
+ }
517
+ return permission === "can_execute" ? family === "action" || family === "http" : family === "connection";
518
+ }
519
+ function statementGroups(policy) {
520
+ if (policy.version === 4) {
521
+ return policy.rules.map((rules, tierIndex) => ({
522
+ statements: rules,
523
+ path: ["rules", tierIndex],
524
+ entry: "rule"
525
+ }));
526
+ }
527
+ if (policy.version === 3) {
528
+ return policy.statements.map((statements, tierIndex) => ({
529
+ statements,
530
+ path: ["statements", tierIndex],
531
+ entry: "statement"
532
+ }));
533
+ }
534
+ return [
535
+ {
536
+ statements: policy.statements,
537
+ path: ["statements"],
538
+ entry: "statement"
539
+ }
540
+ ];
541
+ }
542
+ var ZapierPolicySchema = PolicySchema.superRefine((policy, ctx) => {
543
+ statementGroups(policy).forEach(({ statements, path, entry }) => {
544
+ const Entry = entry === "rule" ? "Rule" : "Statement";
545
+ statements.forEach((statement, statementIndex) => {
546
+ const families = statement.resources.map((resource, resourceIndex) => {
547
+ const family = resourceFamily(resource);
548
+ if (family === void 0) {
549
+ ctx.addIssue({
550
+ code: "custom",
551
+ message: 'Expected "http", "connection/{id|*}", a valid action pattern, or "*"',
552
+ path: [...path, statementIndex, "resources", resourceIndex]
553
+ });
554
+ }
555
+ return family;
556
+ });
557
+ statement.permissions.forEach((permission, permissionIndex) => {
558
+ if (!PERMISSIONS.has(permission)) {
559
+ ctx.addIssue({
560
+ code: "custom",
561
+ message: `Unknown Zapier permission "${permission}"`,
562
+ path: [...path, statementIndex, "permissions", permissionIndex]
563
+ });
564
+ return;
565
+ }
566
+ const incompatibleFamily = families.find(
567
+ (family) => family !== void 0 && !permissionAppliesTo(permission, family)
568
+ );
569
+ if (incompatibleFamily !== void 0) {
570
+ ctx.addIssue({
571
+ code: "custom",
572
+ message: `Permission "${permission}" cannot apply to ${incompatibleFamily} resources`,
573
+ path: [...path, statementIndex, "permissions", permissionIndex]
574
+ });
575
+ }
576
+ });
577
+ const maskingStatementIndex = statements.findIndex(
578
+ (maskingStatement, maskingIndex) => maskingIndex !== statementIndex && EFFECT_PRECEDENCE[maskingStatement.effect] > EFFECT_PRECEDENCE[statement.effect] && isStatementSubsumedBy(statement, {
579
+ ...maskingStatement,
580
+ effect: statement.effect
581
+ })
582
+ );
583
+ if (maskingStatementIndex !== -1) {
584
+ const maskingStatement = statements[maskingStatementIndex];
585
+ const maskingId = maskingStatement.id ? ` (id "${maskingStatement.id}")` : "";
586
+ ctx.addIssue({
587
+ code: "custom",
588
+ message: `${Entry} is entirely masked by higher-precedence ${entry} at index ${maskingStatementIndex}${maskingId}`,
589
+ path: [...path, statementIndex]
590
+ });
591
+ }
592
+ });
593
+ });
594
+ });
595
+
596
+ // src/connection-policy.ts
597
+ import { z as z4 } from "zod";
598
+ var AccessLevelSchema = z4.enum(["read", "read_write"]);
599
+ function requireAccessLevel(accessLevel) {
600
+ if (!AccessLevelSchema.safeParse(accessLevel).success) {
601
+ throw new Error(
602
+ `invalid access_level "${accessLevel}": expected "read" or "read_write"`
603
+ );
604
+ }
605
+ }
430
606
  var READ_HTTP_METHODS = ["GET", "HEAD", "OPTIONS"];
431
607
  var READ_ACTION_TYPES = [
432
608
  "read",
@@ -446,7 +622,19 @@ function isWriteRequest(context) {
446
622
  }
447
623
  function buildConnectionPolicy(options) {
448
624
  const { connectionId, selectedApi, accessLevel } = options;
625
+ requireAccessLevel(accessLevel);
626
+ if (!CONNECTION_ID_PATTERN.test(connectionId)) {
627
+ throw new Error(
628
+ `invalid connection_id "${connectionId}": must not contain "/", "*", or whitespace`
629
+ );
630
+ }
631
+ if (!ACTION_RESOURCE_SEGMENT_PATTERN.test(selectedApi)) {
632
+ throw new Error(
633
+ `invalid selected_api "${selectedApi}": must not contain "/", "*", or whitespace`
634
+ );
635
+ }
449
636
  const authStatement = {
637
+ id: `r_${crypto.randomUUID()}`,
450
638
  effect: "allow",
451
639
  permissions: ["can_use"],
452
640
  resources: [`connection/${connectionId}`],
@@ -466,6 +654,7 @@ function buildConnectionPolicy(options) {
466
654
  });
467
655
  }
468
656
  const httpStatement = {
657
+ id: `r_${crypto.randomUUID()}`,
469
658
  effect: "allow",
470
659
  permissions: ["can_execute"],
471
660
  resources: ["http"],
@@ -474,20 +663,22 @@ function buildConnectionPolicy(options) {
474
663
  const apiPattern = globSelectedApiVersion(selectedApi);
475
664
  const actionResources = accessLevel === "read" ? READ_ACTION_TYPES.map((t) => `action/${apiPattern}/${t}/*`) : [`action/${apiPattern}/*/*`];
476
665
  const actionStatement = {
666
+ id: `r_${crypto.randomUUID()}`,
477
667
  effect: "allow",
478
668
  permissions: ["can_execute"],
479
669
  resources: actionResources,
480
670
  conditions: [connectionCondition]
481
671
  };
482
672
  return {
483
- version: 2,
484
- statements: [authStatement, httpStatement, actionStatement]
673
+ version: 4,
674
+ rules: [[authStatement, httpStatement, actionStatement]]
485
675
  };
486
676
  }
487
677
 
488
678
  // src/connectionless-policy.ts
489
679
  function buildConnectionlessPolicy(options) {
490
680
  const { context, accessLevel } = options;
681
+ requireAccessLevel(accessLevel);
491
682
  if (context.request_type === "http_request") {
492
683
  const conditions = [
493
684
  {
@@ -504,26 +695,35 @@ function buildConnectionlessPolicy(options) {
504
695
  });
505
696
  }
506
697
  const statement2 = {
698
+ id: `r_${crypto.randomUUID()}`,
507
699
  effect: "allow",
508
700
  permissions: ["can_execute"],
509
701
  resources: ["http"],
510
702
  conditions
511
703
  };
512
- return { version: 2, statements: [statement2] };
704
+ return {
705
+ version: 4,
706
+ rules: [[statement2]]
707
+ };
513
708
  }
514
709
  const { selected_api } = parseActionResource(context.resource);
515
710
  const apiPattern = globSelectedApiVersion(selected_api);
516
711
  const resources = accessLevel === "read" ? READ_ACTION_TYPES.map((t) => `action/${apiPattern}/${t}/*`) : [`action/${apiPattern}/*/*`];
517
712
  const statement = {
713
+ id: `r_${crypto.randomUUID()}`,
518
714
  effect: "allow",
519
715
  permissions: ["can_execute"],
520
716
  resources,
521
717
  conditions: []
522
718
  };
523
- return { version: 2, statements: [statement] };
719
+ return {
720
+ version: 4,
721
+ rules: [[statement]]
722
+ };
524
723
  }
525
724
  export {
526
725
  AccessLevelSchema,
726
+ ActionResourcePatternSchema,
527
727
  ActionRunContextInputSchema,
528
728
  ActionRunContextSchema,
529
729
  ActionTypeSchema,
@@ -536,6 +736,7 @@ export {
536
736
  READ_ACTION_TYPES,
537
737
  READ_HTTP_METHODS,
538
738
  RequestContextSchema,
739
+ ZapierPolicySchema,
539
740
  buildActionResource,
540
741
  buildActionRunContext,
541
742
  buildBroadActionResource,