@zapier/policy-context 1.3.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
  }
@@ -211,8 +231,15 @@ var IGNORED_HEADERS = /* @__PURE__ */ new Set([
211
231
  "x-real-ip",
212
232
  // Proxy internal — sdkapi-specific headers
213
233
  "x-is-interactive-session",
214
- "x-relay-callback-url"
234
+ "x-relay-callback-url",
235
+ // Subsumed by isIgnoredHeader's x-zapier- prefix rule; kept for byte-parity
236
+ // with the approvalsapi source and for consumers reading the set directly.
237
+ "x-zapier-approval-token"
215
238
  ]);
239
+ function isIgnoredHeader(name) {
240
+ const normalized = name.toLowerCase();
241
+ return IGNORED_HEADERS.has(normalized) || normalized.startsWith("zapier-") || normalized.startsWith("x-zapier-");
242
+ }
216
243
 
217
244
  // src/builders.ts
218
245
  import { z as z2 } from "zod";
@@ -235,14 +262,14 @@ function buildExactHttpConditions(context) {
235
262
  conditions.push({
236
263
  path: ["url", "query", key],
237
264
  operator: "equals",
238
- value: values
265
+ value: structuredClone(values)
239
266
  });
240
267
  }
241
268
  if (context.body_json !== null) {
242
269
  conditions.push({
243
270
  path: ["body_json"],
244
271
  operator: "equals",
245
- value: context.body_json
272
+ value: structuredClone(context.body_json)
246
273
  });
247
274
  } else if (context.body !== null) {
248
275
  conditions.push({
@@ -252,7 +279,7 @@ function buildExactHttpConditions(context) {
252
279
  });
253
280
  }
254
281
  for (const [key, value] of Object.entries(context.headers)) {
255
- if (!IGNORED_HEADERS.has(key.toLowerCase()) && typeof value === "string") {
282
+ if (!isIgnoredHeader(key) && typeof value === "string") {
256
283
  conditions.push({
257
284
  path: ["headers", key.toLowerCase()],
258
285
  operator: "equals",
@@ -277,7 +304,7 @@ function buildExactActionConditions(context) {
277
304
  conditions.push({
278
305
  path: ["inputs"],
279
306
  operator: "equals",
280
- value: context.inputs
307
+ value: structuredClone(context.inputs)
281
308
  });
282
309
  }
283
310
  return conditions;
@@ -294,7 +321,7 @@ var BuildHttpRequestContextOptionsSchema = z2.object({
294
321
  method: z2.string(),
295
322
  /** Raw URL string. Will be normalized with https:// if no protocol present. */
296
323
  url: z2.string(),
297
- /** Raw headers from the request. Will be filtered through IGNORED_HEADERS. */
324
+ /** Raw headers from the request. Will be filtered through isIgnoredHeader. */
298
325
  headers: z2.record(
299
326
  z2.string(),
300
327
  z2.union([z2.string(), z2.array(z2.string()), z2.undefined()])
@@ -302,7 +329,7 @@ var BuildHttpRequestContextOptionsSchema = z2.object({
302
329
  /** Raw body string */
303
330
  body: z2.string().nullable().optional(),
304
331
  /** Pre-parsed JSON body (e.g. from Fastify's automatic JSON parsing) */
305
- body_json: z2.json().optional(),
332
+ body_json: SafeJsonValueSchema.optional(),
306
333
  /** Connection ID associated with this request */
307
334
  connection_id: z2.string().optional()
308
335
  });
@@ -314,7 +341,7 @@ function buildHttpRequestContext(options) {
314
341
  if (parsed.headers) {
315
342
  for (const [key, value] of Object.entries(parsed.headers)) {
316
343
  const lowerKey = key.toLowerCase();
317
- if (!IGNORED_HEADERS.has(lowerKey) && typeof value === "string") {
344
+ if (!isIgnoredHeader(lowerKey) && typeof value === "string") {
318
345
  headers[lowerKey] = value;
319
346
  }
320
347
  }
@@ -337,7 +364,7 @@ function buildHttpRequestContext(options) {
337
364
  method: parsed.method.toUpperCase(),
338
365
  url,
339
366
  headers,
340
- body: parsed.body ?? null,
367
+ body: body_json === null ? parsed.body ?? null : null,
341
368
  body_json,
342
369
  connection_id: parsed.connection_id
343
370
  });
@@ -351,7 +378,7 @@ var BuildActionRunContextOptionsSchema = z2.object({
351
378
  message: "action_key must contain only [A-Za-z0-9._@:+-] (no globs, slashes, or whitespace)"
352
379
  }),
353
380
  connection_id: z2.string().optional(),
354
- inputs: z2.json().optional(),
381
+ inputs: SafeJsonValueSchema.optional(),
355
382
  reason: z2.string().optional()
356
383
  });
357
384
  function buildActionRunContext(options) {
@@ -372,6 +399,7 @@ function buildActionRunContext(options) {
372
399
  // src/policies.ts
373
400
  function buildAuthStatement(authenticationId) {
374
401
  return {
402
+ id: `r_${crypto.randomUUID()}`,
375
403
  effect: "allow",
376
404
  permissions: ["can_use"],
377
405
  resources: [`connection/${authenticationId}`],
@@ -379,36 +407,38 @@ function buildAuthStatement(authenticationId) {
379
407
  };
380
408
  }
381
409
  function httpContextToPolicy(context) {
382
- const statements = [];
383
- if (context.connection_id) {
384
- statements.push(buildAuthStatement(context.connection_id));
385
- }
386
410
  const conditions = buildExactHttpConditions(context);
387
411
  conditions.push({
388
412
  path: ["url", "query"],
389
413
  operator: "keys_in",
390
414
  value: Object.keys(context.url.query)
391
415
  });
392
- statements.push({
416
+ const statement = {
417
+ id: `r_${crypto.randomUUID()}`,
393
418
  effect: "allow",
394
419
  permissions: ["can_execute"],
395
420
  resources: ["http"],
396
421
  conditions
397
- });
398
- 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
+ };
399
428
  }
400
429
  function actionContextToPolicy(context) {
401
- const statements = [];
402
- if (context.connection_id) {
403
- statements.push(buildAuthStatement(context.connection_id));
404
- }
405
- statements.push({
430
+ const statement = {
431
+ id: `r_${crypto.randomUUID()}`,
406
432
  effect: "allow",
407
433
  permissions: ["can_execute"],
408
434
  resources: [context.resource],
409
435
  conditions: buildExactActionConditions(context)
410
- });
411
- 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
+ };
412
442
  }
413
443
  function toExactPolicy(context) {
414
444
  if (context.request_type === "http_request") {
@@ -417,9 +447,162 @@ function toExactPolicy(context) {
417
447
  return actionContextToPolicy(context);
418
448
  }
419
449
 
420
- // src/connection-policy.ts
450
+ // src/zapier-policy.ts
451
+ import { isStatementSubsumedBy } from "@zapier/policy";
452
+ import {
453
+ PolicySchema
454
+ } from "@zapier/policy-schema";
421
455
  import { z as z3 } from "zod";
422
- 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
+ }
423
606
  var READ_HTTP_METHODS = ["GET", "HEAD", "OPTIONS"];
424
607
  var READ_ACTION_TYPES = [
425
608
  "read",
@@ -439,7 +622,19 @@ function isWriteRequest(context) {
439
622
  }
440
623
  function buildConnectionPolicy(options) {
441
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
+ }
442
636
  const authStatement = {
637
+ id: `r_${crypto.randomUUID()}`,
443
638
  effect: "allow",
444
639
  permissions: ["can_use"],
445
640
  resources: [`connection/${connectionId}`],
@@ -459,6 +654,7 @@ function buildConnectionPolicy(options) {
459
654
  });
460
655
  }
461
656
  const httpStatement = {
657
+ id: `r_${crypto.randomUUID()}`,
462
658
  effect: "allow",
463
659
  permissions: ["can_execute"],
464
660
  resources: ["http"],
@@ -467,20 +663,22 @@ function buildConnectionPolicy(options) {
467
663
  const apiPattern = globSelectedApiVersion(selectedApi);
468
664
  const actionResources = accessLevel === "read" ? READ_ACTION_TYPES.map((t) => `action/${apiPattern}/${t}/*`) : [`action/${apiPattern}/*/*`];
469
665
  const actionStatement = {
666
+ id: `r_${crypto.randomUUID()}`,
470
667
  effect: "allow",
471
668
  permissions: ["can_execute"],
472
669
  resources: actionResources,
473
670
  conditions: [connectionCondition]
474
671
  };
475
672
  return {
476
- version: 2,
477
- statements: [authStatement, httpStatement, actionStatement]
673
+ version: 4,
674
+ rules: [[authStatement, httpStatement, actionStatement]]
478
675
  };
479
676
  }
480
677
 
481
678
  // src/connectionless-policy.ts
482
679
  function buildConnectionlessPolicy(options) {
483
680
  const { context, accessLevel } = options;
681
+ requireAccessLevel(accessLevel);
484
682
  if (context.request_type === "http_request") {
485
683
  const conditions = [
486
684
  {
@@ -497,26 +695,35 @@ function buildConnectionlessPolicy(options) {
497
695
  });
498
696
  }
499
697
  const statement2 = {
698
+ id: `r_${crypto.randomUUID()}`,
500
699
  effect: "allow",
501
700
  permissions: ["can_execute"],
502
701
  resources: ["http"],
503
702
  conditions
504
703
  };
505
- return { version: 2, statements: [statement2] };
704
+ return {
705
+ version: 4,
706
+ rules: [[statement2]]
707
+ };
506
708
  }
507
709
  const { selected_api } = parseActionResource(context.resource);
508
710
  const apiPattern = globSelectedApiVersion(selected_api);
509
711
  const resources = accessLevel === "read" ? READ_ACTION_TYPES.map((t) => `action/${apiPattern}/${t}/*`) : [`action/${apiPattern}/*/*`];
510
712
  const statement = {
713
+ id: `r_${crypto.randomUUID()}`,
511
714
  effect: "allow",
512
715
  permissions: ["can_execute"],
513
716
  resources,
514
717
  conditions: []
515
718
  };
516
- return { version: 2, statements: [statement] };
719
+ return {
720
+ version: 4,
721
+ rules: [[statement]]
722
+ };
517
723
  }
518
724
  export {
519
725
  AccessLevelSchema,
726
+ ActionResourcePatternSchema,
520
727
  ActionRunContextInputSchema,
521
728
  ActionRunContextSchema,
522
729
  ActionTypeSchema,
@@ -529,6 +736,7 @@ export {
529
736
  READ_ACTION_TYPES,
530
737
  READ_HTTP_METHODS,
531
738
  RequestContextSchema,
739
+ ZapierPolicySchema,
532
740
  buildActionResource,
533
741
  buildActionRunContext,
534
742
  buildBroadActionResource,
@@ -540,6 +748,7 @@ export {
540
748
  buildHttpRequestContext,
541
749
  buildUrlString,
542
750
  globSelectedApiVersion,
751
+ isIgnoredHeader,
543
752
  isWriteRequest,
544
753
  parseActionResource,
545
754
  parseUrl,