@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/README.md +28 -2
- package/dist/index.cjs +243 -33
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +735 -9
- package/dist/index.d.ts +735 -9
- package/dist/index.js +242 -33
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npm install @zapier/policy-context zod
|
|
|
16
16
|
|
|
17
17
|
## What it provides
|
|
18
18
|
|
|
19
|
-
|
|
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 `
|
|
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,
|
|
@@ -44,6 +46,7 @@ __export(index_exports, {
|
|
|
44
46
|
buildHttpRequestContext: () => buildHttpRequestContext,
|
|
45
47
|
buildUrlString: () => buildUrlString,
|
|
46
48
|
globSelectedApiVersion: () => globSelectedApiVersion,
|
|
49
|
+
isIgnoredHeader: () => isIgnoredHeader,
|
|
47
50
|
isWriteRequest: () => isWriteRequest,
|
|
48
51
|
parseActionResource: () => parseActionResource,
|
|
49
52
|
parseUrl: () => parseUrl,
|
|
@@ -54,6 +57,21 @@ module.exports = __toCommonJS(index_exports);
|
|
|
54
57
|
|
|
55
58
|
// src/types.ts
|
|
56
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
|
+
});
|
|
57
75
|
var ParsedUrlSchema = import_zod.z.object({
|
|
58
76
|
scheme: import_zod.z.string().describe('URL scheme, e.g. "https"'),
|
|
59
77
|
host: import_zod.z.string().describe('Hostname, e.g. "api.example.com"'),
|
|
@@ -112,7 +130,7 @@ var HttpRequestContextSchema = import_zod.z.object({
|
|
|
112
130
|
* `z.json()` still rejects non-JSON values (`Date`, `undefined`,
|
|
113
131
|
* functions) at parse time. Same inferred `JsonValue` type either way.
|
|
114
132
|
*/
|
|
115
|
-
body_json: import_zod.z.unknown().pipe(
|
|
133
|
+
body_json: import_zod.z.unknown().pipe(SafeJsonValueSchema).nullable().describe("Parsed JSON body (if applicable)"),
|
|
116
134
|
/** Zapier connection ID used for this request */
|
|
117
135
|
connection_id: import_zod.z.string().max(255).optional().describe("Zapier connection ID used for this request"),
|
|
118
136
|
/** Optional human-readable reason for the request */
|
|
@@ -130,6 +148,11 @@ var ActionTypeSchema = import_zod.z.enum([
|
|
|
130
148
|
]).meta({ id: "action_type", description: "Canonical Zapier action type." });
|
|
131
149
|
var ACTION_RESOURCE_SEGMENT_PATTERN = /^[A-Za-z0-9._+-]+(?:@[A-Za-z0-9._+-]+)?$/;
|
|
132
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
|
+
);
|
|
133
156
|
function parseActionResource(resource) {
|
|
134
157
|
const segments = resource.split("/");
|
|
135
158
|
if (segments.length !== 4 || segments[0] !== "action") {
|
|
@@ -190,7 +213,7 @@ var ActionRunContextInputSchema = import_zod.z.object({
|
|
|
190
213
|
}),
|
|
191
214
|
connection_id: import_zod.z.string().max(255).optional().describe("Zapier connection ID used for this request"),
|
|
192
215
|
/** Action input parameters. See body_json above for the pipe rationale. */
|
|
193
|
-
inputs: import_zod.z.unknown().pipe(
|
|
216
|
+
inputs: import_zod.z.unknown().pipe(SafeJsonValueSchema).optional().describe("Action input parameters"),
|
|
194
217
|
reason: import_zod.z.string().max(1024).optional().describe("Optional human-readable reason for the request")
|
|
195
218
|
}).meta({
|
|
196
219
|
id: "ActionRunContext",
|
|
@@ -221,7 +244,7 @@ var ActionRunContextSchema = ActionRunContextInputSchema.transform(
|
|
|
221
244
|
...input.connection_id !== void 0 && {
|
|
222
245
|
connection_id: input.connection_id
|
|
223
246
|
},
|
|
224
|
-
...input.inputs !== void 0 && { inputs: input.inputs },
|
|
247
|
+
...input.inputs !== void 0 && input.inputs !== null && { inputs: input.inputs },
|
|
225
248
|
...input.reason !== void 0 && { reason: input.reason }
|
|
226
249
|
};
|
|
227
250
|
}
|
|
@@ -265,8 +288,15 @@ var IGNORED_HEADERS = /* @__PURE__ */ new Set([
|
|
|
265
288
|
"x-real-ip",
|
|
266
289
|
// Proxy internal — sdkapi-specific headers
|
|
267
290
|
"x-is-interactive-session",
|
|
268
|
-
"x-relay-callback-url"
|
|
291
|
+
"x-relay-callback-url",
|
|
292
|
+
// Subsumed by isIgnoredHeader's x-zapier- prefix rule; kept for byte-parity
|
|
293
|
+
// with the approvalsapi source and for consumers reading the set directly.
|
|
294
|
+
"x-zapier-approval-token"
|
|
269
295
|
]);
|
|
296
|
+
function isIgnoredHeader(name) {
|
|
297
|
+
const normalized = name.toLowerCase();
|
|
298
|
+
return IGNORED_HEADERS.has(normalized) || normalized.startsWith("zapier-") || normalized.startsWith("x-zapier-");
|
|
299
|
+
}
|
|
270
300
|
|
|
271
301
|
// src/builders.ts
|
|
272
302
|
var import_zod2 = require("zod");
|
|
@@ -289,14 +319,14 @@ function buildExactHttpConditions(context) {
|
|
|
289
319
|
conditions.push({
|
|
290
320
|
path: ["url", "query", key],
|
|
291
321
|
operator: "equals",
|
|
292
|
-
value: values
|
|
322
|
+
value: structuredClone(values)
|
|
293
323
|
});
|
|
294
324
|
}
|
|
295
325
|
if (context.body_json !== null) {
|
|
296
326
|
conditions.push({
|
|
297
327
|
path: ["body_json"],
|
|
298
328
|
operator: "equals",
|
|
299
|
-
value: context.body_json
|
|
329
|
+
value: structuredClone(context.body_json)
|
|
300
330
|
});
|
|
301
331
|
} else if (context.body !== null) {
|
|
302
332
|
conditions.push({
|
|
@@ -306,7 +336,7 @@ function buildExactHttpConditions(context) {
|
|
|
306
336
|
});
|
|
307
337
|
}
|
|
308
338
|
for (const [key, value] of Object.entries(context.headers)) {
|
|
309
|
-
if (!
|
|
339
|
+
if (!isIgnoredHeader(key) && typeof value === "string") {
|
|
310
340
|
conditions.push({
|
|
311
341
|
path: ["headers", key.toLowerCase()],
|
|
312
342
|
operator: "equals",
|
|
@@ -331,7 +361,7 @@ function buildExactActionConditions(context) {
|
|
|
331
361
|
conditions.push({
|
|
332
362
|
path: ["inputs"],
|
|
333
363
|
operator: "equals",
|
|
334
|
-
value: context.inputs
|
|
364
|
+
value: structuredClone(context.inputs)
|
|
335
365
|
});
|
|
336
366
|
}
|
|
337
367
|
return conditions;
|
|
@@ -348,7 +378,7 @@ var BuildHttpRequestContextOptionsSchema = import_zod2.z.object({
|
|
|
348
378
|
method: import_zod2.z.string(),
|
|
349
379
|
/** Raw URL string. Will be normalized with https:// if no protocol present. */
|
|
350
380
|
url: import_zod2.z.string(),
|
|
351
|
-
/** Raw headers from the request. Will be filtered through
|
|
381
|
+
/** Raw headers from the request. Will be filtered through isIgnoredHeader. */
|
|
352
382
|
headers: import_zod2.z.record(
|
|
353
383
|
import_zod2.z.string(),
|
|
354
384
|
import_zod2.z.union([import_zod2.z.string(), import_zod2.z.array(import_zod2.z.string()), import_zod2.z.undefined()])
|
|
@@ -356,7 +386,7 @@ var BuildHttpRequestContextOptionsSchema = import_zod2.z.object({
|
|
|
356
386
|
/** Raw body string */
|
|
357
387
|
body: import_zod2.z.string().nullable().optional(),
|
|
358
388
|
/** Pre-parsed JSON body (e.g. from Fastify's automatic JSON parsing) */
|
|
359
|
-
body_json:
|
|
389
|
+
body_json: SafeJsonValueSchema.optional(),
|
|
360
390
|
/** Connection ID associated with this request */
|
|
361
391
|
connection_id: import_zod2.z.string().optional()
|
|
362
392
|
});
|
|
@@ -368,7 +398,7 @@ function buildHttpRequestContext(options) {
|
|
|
368
398
|
if (parsed.headers) {
|
|
369
399
|
for (const [key, value] of Object.entries(parsed.headers)) {
|
|
370
400
|
const lowerKey = key.toLowerCase();
|
|
371
|
-
if (!
|
|
401
|
+
if (!isIgnoredHeader(lowerKey) && typeof value === "string") {
|
|
372
402
|
headers[lowerKey] = value;
|
|
373
403
|
}
|
|
374
404
|
}
|
|
@@ -391,7 +421,7 @@ function buildHttpRequestContext(options) {
|
|
|
391
421
|
method: parsed.method.toUpperCase(),
|
|
392
422
|
url,
|
|
393
423
|
headers,
|
|
394
|
-
body: parsed.body ?? null,
|
|
424
|
+
body: body_json === null ? parsed.body ?? null : null,
|
|
395
425
|
body_json,
|
|
396
426
|
connection_id: parsed.connection_id
|
|
397
427
|
});
|
|
@@ -405,7 +435,7 @@ var BuildActionRunContextOptionsSchema = import_zod2.z.object({
|
|
|
405
435
|
message: "action_key must contain only [A-Za-z0-9._@:+-] (no globs, slashes, or whitespace)"
|
|
406
436
|
}),
|
|
407
437
|
connection_id: import_zod2.z.string().optional(),
|
|
408
|
-
inputs:
|
|
438
|
+
inputs: SafeJsonValueSchema.optional(),
|
|
409
439
|
reason: import_zod2.z.string().optional()
|
|
410
440
|
});
|
|
411
441
|
function buildActionRunContext(options) {
|
|
@@ -426,6 +456,7 @@ function buildActionRunContext(options) {
|
|
|
426
456
|
// src/policies.ts
|
|
427
457
|
function buildAuthStatement(authenticationId) {
|
|
428
458
|
return {
|
|
459
|
+
id: `r_${crypto.randomUUID()}`,
|
|
429
460
|
effect: "allow",
|
|
430
461
|
permissions: ["can_use"],
|
|
431
462
|
resources: [`connection/${authenticationId}`],
|
|
@@ -433,36 +464,38 @@ function buildAuthStatement(authenticationId) {
|
|
|
433
464
|
};
|
|
434
465
|
}
|
|
435
466
|
function httpContextToPolicy(context) {
|
|
436
|
-
const statements = [];
|
|
437
|
-
if (context.connection_id) {
|
|
438
|
-
statements.push(buildAuthStatement(context.connection_id));
|
|
439
|
-
}
|
|
440
467
|
const conditions = buildExactHttpConditions(context);
|
|
441
468
|
conditions.push({
|
|
442
469
|
path: ["url", "query"],
|
|
443
470
|
operator: "keys_in",
|
|
444
471
|
value: Object.keys(context.url.query)
|
|
445
472
|
});
|
|
446
|
-
|
|
473
|
+
const statement = {
|
|
474
|
+
id: `r_${crypto.randomUUID()}`,
|
|
447
475
|
effect: "allow",
|
|
448
476
|
permissions: ["can_execute"],
|
|
449
477
|
resources: ["http"],
|
|
450
478
|
conditions
|
|
451
|
-
}
|
|
452
|
-
|
|
479
|
+
};
|
|
480
|
+
const statements = context.connection_id ? [buildAuthStatement(context.connection_id), statement] : [statement];
|
|
481
|
+
return {
|
|
482
|
+
version: 4,
|
|
483
|
+
rules: [statements]
|
|
484
|
+
};
|
|
453
485
|
}
|
|
454
486
|
function actionContextToPolicy(context) {
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
statements.push(buildAuthStatement(context.connection_id));
|
|
458
|
-
}
|
|
459
|
-
statements.push({
|
|
487
|
+
const statement = {
|
|
488
|
+
id: `r_${crypto.randomUUID()}`,
|
|
460
489
|
effect: "allow",
|
|
461
490
|
permissions: ["can_execute"],
|
|
462
491
|
resources: [context.resource],
|
|
463
492
|
conditions: buildExactActionConditions(context)
|
|
464
|
-
}
|
|
465
|
-
|
|
493
|
+
};
|
|
494
|
+
const statements = context.connection_id ? [buildAuthStatement(context.connection_id), statement] : [statement];
|
|
495
|
+
return {
|
|
496
|
+
version: 4,
|
|
497
|
+
rules: [statements]
|
|
498
|
+
};
|
|
466
499
|
}
|
|
467
500
|
function toExactPolicy(context) {
|
|
468
501
|
if (context.request_type === "http_request") {
|
|
@@ -471,9 +504,160 @@ function toExactPolicy(context) {
|
|
|
471
504
|
return actionContextToPolicy(context);
|
|
472
505
|
}
|
|
473
506
|
|
|
474
|
-
// src/
|
|
507
|
+
// src/zapier-policy.ts
|
|
508
|
+
var import_policy = require("@zapier/policy");
|
|
509
|
+
var import_policy_schema = require("@zapier/policy-schema");
|
|
475
510
|
var import_zod3 = require("zod");
|
|
476
|
-
var
|
|
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
|
+
}
|
|
477
661
|
var READ_HTTP_METHODS = ["GET", "HEAD", "OPTIONS"];
|
|
478
662
|
var READ_ACTION_TYPES = [
|
|
479
663
|
"read",
|
|
@@ -493,7 +677,19 @@ function isWriteRequest(context) {
|
|
|
493
677
|
}
|
|
494
678
|
function buildConnectionPolicy(options) {
|
|
495
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
|
+
}
|
|
496
691
|
const authStatement = {
|
|
692
|
+
id: `r_${crypto.randomUUID()}`,
|
|
497
693
|
effect: "allow",
|
|
498
694
|
permissions: ["can_use"],
|
|
499
695
|
resources: [`connection/${connectionId}`],
|
|
@@ -513,6 +709,7 @@ function buildConnectionPolicy(options) {
|
|
|
513
709
|
});
|
|
514
710
|
}
|
|
515
711
|
const httpStatement = {
|
|
712
|
+
id: `r_${crypto.randomUUID()}`,
|
|
516
713
|
effect: "allow",
|
|
517
714
|
permissions: ["can_execute"],
|
|
518
715
|
resources: ["http"],
|
|
@@ -521,20 +718,22 @@ function buildConnectionPolicy(options) {
|
|
|
521
718
|
const apiPattern = globSelectedApiVersion(selectedApi);
|
|
522
719
|
const actionResources = accessLevel === "read" ? READ_ACTION_TYPES.map((t) => `action/${apiPattern}/${t}/*`) : [`action/${apiPattern}/*/*`];
|
|
523
720
|
const actionStatement = {
|
|
721
|
+
id: `r_${crypto.randomUUID()}`,
|
|
524
722
|
effect: "allow",
|
|
525
723
|
permissions: ["can_execute"],
|
|
526
724
|
resources: actionResources,
|
|
527
725
|
conditions: [connectionCondition]
|
|
528
726
|
};
|
|
529
727
|
return {
|
|
530
|
-
version:
|
|
531
|
-
|
|
728
|
+
version: 4,
|
|
729
|
+
rules: [[authStatement, httpStatement, actionStatement]]
|
|
532
730
|
};
|
|
533
731
|
}
|
|
534
732
|
|
|
535
733
|
// src/connectionless-policy.ts
|
|
536
734
|
function buildConnectionlessPolicy(options) {
|
|
537
735
|
const { context, accessLevel } = options;
|
|
736
|
+
requireAccessLevel(accessLevel);
|
|
538
737
|
if (context.request_type === "http_request") {
|
|
539
738
|
const conditions = [
|
|
540
739
|
{
|
|
@@ -551,27 +750,36 @@ function buildConnectionlessPolicy(options) {
|
|
|
551
750
|
});
|
|
552
751
|
}
|
|
553
752
|
const statement2 = {
|
|
753
|
+
id: `r_${crypto.randomUUID()}`,
|
|
554
754
|
effect: "allow",
|
|
555
755
|
permissions: ["can_execute"],
|
|
556
756
|
resources: ["http"],
|
|
557
757
|
conditions
|
|
558
758
|
};
|
|
559
|
-
return {
|
|
759
|
+
return {
|
|
760
|
+
version: 4,
|
|
761
|
+
rules: [[statement2]]
|
|
762
|
+
};
|
|
560
763
|
}
|
|
561
764
|
const { selected_api } = parseActionResource(context.resource);
|
|
562
765
|
const apiPattern = globSelectedApiVersion(selected_api);
|
|
563
766
|
const resources = accessLevel === "read" ? READ_ACTION_TYPES.map((t) => `action/${apiPattern}/${t}/*`) : [`action/${apiPattern}/*/*`];
|
|
564
767
|
const statement = {
|
|
768
|
+
id: `r_${crypto.randomUUID()}`,
|
|
565
769
|
effect: "allow",
|
|
566
770
|
permissions: ["can_execute"],
|
|
567
771
|
resources,
|
|
568
772
|
conditions: []
|
|
569
773
|
};
|
|
570
|
-
return {
|
|
774
|
+
return {
|
|
775
|
+
version: 4,
|
|
776
|
+
rules: [[statement]]
|
|
777
|
+
};
|
|
571
778
|
}
|
|
572
779
|
// Annotate the CommonJS export names for ESM import in node:
|
|
573
780
|
0 && (module.exports = {
|
|
574
781
|
AccessLevelSchema,
|
|
782
|
+
ActionResourcePatternSchema,
|
|
575
783
|
ActionRunContextInputSchema,
|
|
576
784
|
ActionRunContextSchema,
|
|
577
785
|
ActionTypeSchema,
|
|
@@ -584,6 +792,7 @@ function buildConnectionlessPolicy(options) {
|
|
|
584
792
|
READ_ACTION_TYPES,
|
|
585
793
|
READ_HTTP_METHODS,
|
|
586
794
|
RequestContextSchema,
|
|
795
|
+
ZapierPolicySchema,
|
|
587
796
|
buildActionResource,
|
|
588
797
|
buildActionRunContext,
|
|
589
798
|
buildBroadActionResource,
|
|
@@ -595,6 +804,7 @@ function buildConnectionlessPolicy(options) {
|
|
|
595
804
|
buildHttpRequestContext,
|
|
596
805
|
buildUrlString,
|
|
597
806
|
globSelectedApiVersion,
|
|
807
|
+
isIgnoredHeader,
|
|
598
808
|
isWriteRequest,
|
|
599
809
|
parseActionResource,
|
|
600
810
|
parseUrl,
|