@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/README.md +24 -2
- package/dist/index.cjs +209 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +499 -6
- package/dist/index.d.ts +499 -6
- package/dist/index.js +207 -28
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
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(
|
|
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(
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
409
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
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:
|
|
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 {
|
|
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 {
|
|
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,
|