@zapier/policy-context 1.3.0 → 3.0.0-mr.39.bc800a62
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 +45 -2
- package/dist/index.cjs +255 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +739 -9
- package/dist/index.d.ts +739 -9
- package/dist/index.js +252 -40
- 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,11 @@ 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 policy documents. Canonical builders emit v4 with one
|
|
29
|
+
rule tier (`rules: [[...]]`) and randomly generated `r_<uuid>` rule IDs;
|
|
30
|
+
`toExactPolicyV3` is an explicit legacy exact-request escape hatch.
|
|
31
|
+
4. **Zapier policy validation** — `ZapierPolicySchema` composes the generic
|
|
32
|
+
`PolicySchema` with Zapier resource grammar and permission/resource rules.
|
|
29
33
|
|
|
30
34
|
## Usage
|
|
31
35
|
|
|
@@ -65,6 +69,27 @@ import { toExactPolicy } from "@zapier/policy-context";
|
|
|
65
69
|
const policy = toExactPolicy(context);
|
|
66
70
|
```
|
|
67
71
|
|
|
72
|
+
`toExactPolicy` is the canonical exact builder and returns
|
|
73
|
+
`Extract<Policy, { version: 4 }>`, with a single rule tier (`rules: [[...]]`)
|
|
74
|
+
and `r_<uuid>` rule IDs. Rule content and ordering are unchanged. Migrate
|
|
75
|
+
`policy.statements[0][i]` to `policy.rules[0][i]` and any statement-ID prefix
|
|
76
|
+
handling from `s_` to `r_`.
|
|
77
|
+
|
|
78
|
+
For a legacy consumer that still requires an exact v3 document, use the explicit
|
|
79
|
+
factory instead:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { toExactPolicyV3 } from "@zapier/policy-context";
|
|
83
|
+
|
|
84
|
+
const legacyPolicy = toExactPolicyV3(context);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`toExactPolicyV3` returns `Extract<Policy, { version: 3 }>`, with `version: 3`,
|
|
88
|
+
one statement tier (`statements: [[...]]`), and `s_<uuid>` statement IDs. It
|
|
89
|
+
preserves the canonical exact builder's conditions, content, and ordering.
|
|
90
|
+
`buildConnectionPolicy` and `buildConnectionlessPolicy` remain v4; this escape
|
|
91
|
+
hatch is only for exact-request policies.
|
|
92
|
+
|
|
68
93
|
### Connection-scoped policies
|
|
69
94
|
|
|
70
95
|
```ts
|
|
@@ -76,6 +101,24 @@ const policy = buildConnectionPolicy({
|
|
|
76
101
|
});
|
|
77
102
|
```
|
|
78
103
|
|
|
104
|
+
### Validate a Zapier policy
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { ZapierPolicySchema } from "@zapier/policy-context";
|
|
108
|
+
|
|
109
|
+
const policy = ZapierPolicySchema.parse(input);
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`ZapierPolicySchema` output includes v4 documents as well as earlier versions.
|
|
113
|
+
This accepts `http`, `connection/{id|*}`, canonical
|
|
114
|
+
`action/{selected_api}/{action_type|*}/{action_key}` patterns, and the lone `*`
|
|
115
|
+
god-mode resource. It validates structure only; app, API version, and action-key
|
|
116
|
+
existence remain the caller's responsibility. It also rejects a statement or rule
|
|
117
|
+
when one higher-precedence statement or rule (`deny` > `ask` > `allow`) fully
|
|
118
|
+
subsumes it. Validation traverses each ordered tier of v3 `statements` and v4
|
|
119
|
+
`rules`, checking masking only within that tier. Same-effect redundancy and
|
|
120
|
+
coverage assembled from multiple statements or rules remain valid.
|
|
121
|
+
|
|
79
122
|
## License
|
|
80
123
|
|
|
81
124
|
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,16 +46,33 @@ __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,
|
|
50
53
|
toAppKey: () => toAppKey,
|
|
51
|
-
toExactPolicy: () => toExactPolicy
|
|
54
|
+
toExactPolicy: () => toExactPolicy,
|
|
55
|
+
toExactPolicyV3: () => toExactPolicyV3
|
|
52
56
|
});
|
|
53
57
|
module.exports = __toCommonJS(index_exports);
|
|
54
58
|
|
|
55
59
|
// src/types.ts
|
|
56
60
|
var import_zod = require("zod");
|
|
61
|
+
function containsUnsafeInteger(value) {
|
|
62
|
+
if (typeof value === "number") {
|
|
63
|
+
return Number.isInteger(value) && !Number.isSafeInteger(value);
|
|
64
|
+
}
|
|
65
|
+
if (Array.isArray(value)) {
|
|
66
|
+
return value.some(containsUnsafeInteger);
|
|
67
|
+
}
|
|
68
|
+
if (value !== null && typeof value === "object") {
|
|
69
|
+
return Object.values(value).some(containsUnsafeInteger);
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
var SafeJsonValueSchema = import_zod.z.json().refine((value) => !containsUnsafeInteger(value), {
|
|
74
|
+
message: "JSON integers must be within the IEEE-754 safe range; encode larger identifiers as strings"
|
|
75
|
+
});
|
|
57
76
|
var ParsedUrlSchema = import_zod.z.object({
|
|
58
77
|
scheme: import_zod.z.string().describe('URL scheme, e.g. "https"'),
|
|
59
78
|
host: import_zod.z.string().describe('Hostname, e.g. "api.example.com"'),
|
|
@@ -112,7 +131,7 @@ var HttpRequestContextSchema = import_zod.z.object({
|
|
|
112
131
|
* `z.json()` still rejects non-JSON values (`Date`, `undefined`,
|
|
113
132
|
* functions) at parse time. Same inferred `JsonValue` type either way.
|
|
114
133
|
*/
|
|
115
|
-
body_json: import_zod.z.unknown().pipe(
|
|
134
|
+
body_json: import_zod.z.unknown().pipe(SafeJsonValueSchema).nullable().describe("Parsed JSON body (if applicable)"),
|
|
116
135
|
/** Zapier connection ID used for this request */
|
|
117
136
|
connection_id: import_zod.z.string().max(255).optional().describe("Zapier connection ID used for this request"),
|
|
118
137
|
/** Optional human-readable reason for the request */
|
|
@@ -130,6 +149,11 @@ var ActionTypeSchema = import_zod.z.enum([
|
|
|
130
149
|
]).meta({ id: "action_type", description: "Canonical Zapier action type." });
|
|
131
150
|
var ACTION_RESOURCE_SEGMENT_PATTERN = /^[A-Za-z0-9._+-]+(?:@[A-Za-z0-9._+-]+)?$/;
|
|
132
151
|
var ACTION_KEY_SEGMENT_PATTERN = /^[A-Za-z0-9._+:-]+(?:@[A-Za-z0-9._+:-]+)?$/;
|
|
152
|
+
var CONNECTION_ID_CHARS = "[^/*\\x00-\\x20\\x7f-\\xa0\\u1680\\u2000-\\u200a\\u2028\\u2029\\u202f\\u205f\\u3000\\ufeff]";
|
|
153
|
+
var CONNECTION_ID_PATTERN = new RegExp(`^${CONNECTION_ID_CHARS}+$`);
|
|
154
|
+
var CONNECTION_RESOURCE_PATTERN = new RegExp(
|
|
155
|
+
`^connection/(?:\\*|${CONNECTION_ID_CHARS}+)$`
|
|
156
|
+
);
|
|
133
157
|
function parseActionResource(resource) {
|
|
134
158
|
const segments = resource.split("/");
|
|
135
159
|
if (segments.length !== 4 || segments[0] !== "action") {
|
|
@@ -190,7 +214,7 @@ var ActionRunContextInputSchema = import_zod.z.object({
|
|
|
190
214
|
}),
|
|
191
215
|
connection_id: import_zod.z.string().max(255).optional().describe("Zapier connection ID used for this request"),
|
|
192
216
|
/** Action input parameters. See body_json above for the pipe rationale. */
|
|
193
|
-
inputs: import_zod.z.unknown().pipe(
|
|
217
|
+
inputs: import_zod.z.unknown().pipe(SafeJsonValueSchema).optional().describe("Action input parameters"),
|
|
194
218
|
reason: import_zod.z.string().max(1024).optional().describe("Optional human-readable reason for the request")
|
|
195
219
|
}).meta({
|
|
196
220
|
id: "ActionRunContext",
|
|
@@ -221,7 +245,7 @@ var ActionRunContextSchema = ActionRunContextInputSchema.transform(
|
|
|
221
245
|
...input.connection_id !== void 0 && {
|
|
222
246
|
connection_id: input.connection_id
|
|
223
247
|
},
|
|
224
|
-
...input.inputs !== void 0 && { inputs: input.inputs },
|
|
248
|
+
...input.inputs !== void 0 && input.inputs !== null && { inputs: input.inputs },
|
|
225
249
|
...input.reason !== void 0 && { reason: input.reason }
|
|
226
250
|
};
|
|
227
251
|
}
|
|
@@ -265,8 +289,15 @@ var IGNORED_HEADERS = /* @__PURE__ */ new Set([
|
|
|
265
289
|
"x-real-ip",
|
|
266
290
|
// Proxy internal — sdkapi-specific headers
|
|
267
291
|
"x-is-interactive-session",
|
|
268
|
-
"x-relay-callback-url"
|
|
292
|
+
"x-relay-callback-url",
|
|
293
|
+
// Subsumed by isIgnoredHeader's x-zapier- prefix rule; kept for byte-parity
|
|
294
|
+
// with the approvalsapi source and for consumers reading the set directly.
|
|
295
|
+
"x-zapier-approval-token"
|
|
269
296
|
]);
|
|
297
|
+
function isIgnoredHeader(name) {
|
|
298
|
+
const normalized = name.toLowerCase();
|
|
299
|
+
return IGNORED_HEADERS.has(normalized) || normalized.startsWith("zapier-") || normalized.startsWith("x-zapier-");
|
|
300
|
+
}
|
|
270
301
|
|
|
271
302
|
// src/builders.ts
|
|
272
303
|
var import_zod2 = require("zod");
|
|
@@ -289,14 +320,14 @@ function buildExactHttpConditions(context) {
|
|
|
289
320
|
conditions.push({
|
|
290
321
|
path: ["url", "query", key],
|
|
291
322
|
operator: "equals",
|
|
292
|
-
value: values
|
|
323
|
+
value: structuredClone(values)
|
|
293
324
|
});
|
|
294
325
|
}
|
|
295
326
|
if (context.body_json !== null) {
|
|
296
327
|
conditions.push({
|
|
297
328
|
path: ["body_json"],
|
|
298
329
|
operator: "equals",
|
|
299
|
-
value: context.body_json
|
|
330
|
+
value: structuredClone(context.body_json)
|
|
300
331
|
});
|
|
301
332
|
} else if (context.body !== null) {
|
|
302
333
|
conditions.push({
|
|
@@ -306,7 +337,7 @@ function buildExactHttpConditions(context) {
|
|
|
306
337
|
});
|
|
307
338
|
}
|
|
308
339
|
for (const [key, value] of Object.entries(context.headers)) {
|
|
309
|
-
if (!
|
|
340
|
+
if (!isIgnoredHeader(key) && typeof value === "string") {
|
|
310
341
|
conditions.push({
|
|
311
342
|
path: ["headers", key.toLowerCase()],
|
|
312
343
|
operator: "equals",
|
|
@@ -331,7 +362,7 @@ function buildExactActionConditions(context) {
|
|
|
331
362
|
conditions.push({
|
|
332
363
|
path: ["inputs"],
|
|
333
364
|
operator: "equals",
|
|
334
|
-
value: context.inputs
|
|
365
|
+
value: structuredClone(context.inputs)
|
|
335
366
|
});
|
|
336
367
|
}
|
|
337
368
|
return conditions;
|
|
@@ -348,7 +379,7 @@ var BuildHttpRequestContextOptionsSchema = import_zod2.z.object({
|
|
|
348
379
|
method: import_zod2.z.string(),
|
|
349
380
|
/** Raw URL string. Will be normalized with https:// if no protocol present. */
|
|
350
381
|
url: import_zod2.z.string(),
|
|
351
|
-
/** Raw headers from the request. Will be filtered through
|
|
382
|
+
/** Raw headers from the request. Will be filtered through isIgnoredHeader. */
|
|
352
383
|
headers: import_zod2.z.record(
|
|
353
384
|
import_zod2.z.string(),
|
|
354
385
|
import_zod2.z.union([import_zod2.z.string(), import_zod2.z.array(import_zod2.z.string()), import_zod2.z.undefined()])
|
|
@@ -356,7 +387,7 @@ var BuildHttpRequestContextOptionsSchema = import_zod2.z.object({
|
|
|
356
387
|
/** Raw body string */
|
|
357
388
|
body: import_zod2.z.string().nullable().optional(),
|
|
358
389
|
/** Pre-parsed JSON body (e.g. from Fastify's automatic JSON parsing) */
|
|
359
|
-
body_json:
|
|
390
|
+
body_json: SafeJsonValueSchema.optional(),
|
|
360
391
|
/** Connection ID associated with this request */
|
|
361
392
|
connection_id: import_zod2.z.string().optional()
|
|
362
393
|
});
|
|
@@ -368,7 +399,7 @@ function buildHttpRequestContext(options) {
|
|
|
368
399
|
if (parsed.headers) {
|
|
369
400
|
for (const [key, value] of Object.entries(parsed.headers)) {
|
|
370
401
|
const lowerKey = key.toLowerCase();
|
|
371
|
-
if (!
|
|
402
|
+
if (!isIgnoredHeader(lowerKey) && typeof value === "string") {
|
|
372
403
|
headers[lowerKey] = value;
|
|
373
404
|
}
|
|
374
405
|
}
|
|
@@ -391,7 +422,7 @@ function buildHttpRequestContext(options) {
|
|
|
391
422
|
method: parsed.method.toUpperCase(),
|
|
392
423
|
url,
|
|
393
424
|
headers,
|
|
394
|
-
body: parsed.body ?? null,
|
|
425
|
+
body: body_json === null ? parsed.body ?? null : null,
|
|
395
426
|
body_json,
|
|
396
427
|
connection_id: parsed.connection_id
|
|
397
428
|
});
|
|
@@ -405,7 +436,7 @@ var BuildActionRunContextOptionsSchema = import_zod2.z.object({
|
|
|
405
436
|
message: "action_key must contain only [A-Za-z0-9._@:+-] (no globs, slashes, or whitespace)"
|
|
406
437
|
}),
|
|
407
438
|
connection_id: import_zod2.z.string().optional(),
|
|
408
|
-
inputs:
|
|
439
|
+
inputs: SafeJsonValueSchema.optional(),
|
|
409
440
|
reason: import_zod2.z.string().optional()
|
|
410
441
|
});
|
|
411
442
|
function buildActionRunContext(options) {
|
|
@@ -424,56 +455,212 @@ function buildActionRunContext(options) {
|
|
|
424
455
|
}
|
|
425
456
|
|
|
426
457
|
// src/policies.ts
|
|
427
|
-
function buildAuthStatement(authenticationId) {
|
|
458
|
+
function buildAuthStatement(authenticationId, idPrefix) {
|
|
428
459
|
return {
|
|
460
|
+
id: `${idPrefix}_${crypto.randomUUID()}`,
|
|
429
461
|
effect: "allow",
|
|
430
462
|
permissions: ["can_use"],
|
|
431
463
|
resources: [`connection/${authenticationId}`],
|
|
432
464
|
conditions: []
|
|
433
465
|
};
|
|
434
466
|
}
|
|
435
|
-
function
|
|
436
|
-
const statements = [];
|
|
437
|
-
if (context.connection_id) {
|
|
438
|
-
statements.push(buildAuthStatement(context.connection_id));
|
|
439
|
-
}
|
|
467
|
+
function httpContextToTier(context, idPrefix) {
|
|
440
468
|
const conditions = buildExactHttpConditions(context);
|
|
441
469
|
conditions.push({
|
|
442
470
|
path: ["url", "query"],
|
|
443
471
|
operator: "keys_in",
|
|
444
472
|
value: Object.keys(context.url.query)
|
|
445
473
|
});
|
|
446
|
-
|
|
474
|
+
const statement = {
|
|
475
|
+
id: `${idPrefix}_${crypto.randomUUID()}`,
|
|
447
476
|
effect: "allow",
|
|
448
477
|
permissions: ["can_execute"],
|
|
449
478
|
resources: ["http"],
|
|
450
479
|
conditions
|
|
451
|
-
}
|
|
452
|
-
return
|
|
480
|
+
};
|
|
481
|
+
return context.connection_id ? [buildAuthStatement(context.connection_id, idPrefix), statement] : [statement];
|
|
453
482
|
}
|
|
454
|
-
function
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
statements.push(buildAuthStatement(context.connection_id));
|
|
458
|
-
}
|
|
459
|
-
statements.push({
|
|
483
|
+
function actionContextToTier(context, idPrefix) {
|
|
484
|
+
const statement = {
|
|
485
|
+
id: `${idPrefix}_${crypto.randomUUID()}`,
|
|
460
486
|
effect: "allow",
|
|
461
487
|
permissions: ["can_execute"],
|
|
462
488
|
resources: [context.resource],
|
|
463
489
|
conditions: buildExactActionConditions(context)
|
|
464
|
-
}
|
|
465
|
-
return
|
|
490
|
+
};
|
|
491
|
+
return context.connection_id ? [buildAuthStatement(context.connection_id, idPrefix), statement] : [statement];
|
|
466
492
|
}
|
|
467
493
|
function toExactPolicy(context) {
|
|
468
|
-
|
|
469
|
-
|
|
494
|
+
return {
|
|
495
|
+
version: 4,
|
|
496
|
+
rules: [
|
|
497
|
+
context.request_type === "http_request" ? httpContextToTier(context, "r") : actionContextToTier(context, "r")
|
|
498
|
+
]
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
function toExactPolicyV3(context) {
|
|
502
|
+
return {
|
|
503
|
+
version: 3,
|
|
504
|
+
statements: [
|
|
505
|
+
context.request_type === "http_request" ? httpContextToTier(context, "s") : actionContextToTier(context, "s")
|
|
506
|
+
]
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// src/zapier-policy.ts
|
|
511
|
+
var import_policy = require("@zapier/policy");
|
|
512
|
+
var import_policy_schema = require("@zapier/policy-schema");
|
|
513
|
+
var import_zod3 = require("zod");
|
|
514
|
+
var SELECTED_API_GLOB_PART_PATTERN = /^[A-Za-z0-9._+*-]+$/;
|
|
515
|
+
var ACTION_KEY_GLOB_PART_PATTERN = /^[A-Za-z0-9._+:*-]+$/;
|
|
516
|
+
var PERMISSIONS = /* @__PURE__ */ new Set([
|
|
517
|
+
"can_execute",
|
|
518
|
+
"can_use",
|
|
519
|
+
"can_view",
|
|
520
|
+
"can_edit",
|
|
521
|
+
"*"
|
|
522
|
+
]);
|
|
523
|
+
var EFFECT_PRECEDENCE = { allow: 0, ask: 1, deny: 2 };
|
|
524
|
+
function isSelectedApiPattern(value) {
|
|
525
|
+
return isGlobSegment(
|
|
526
|
+
value,
|
|
527
|
+
ACTION_RESOURCE_SEGMENT_PATTERN,
|
|
528
|
+
SELECTED_API_GLOB_PART_PATTERN
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
function isActionKeyPattern(value) {
|
|
532
|
+
return isGlobSegment(
|
|
533
|
+
value,
|
|
534
|
+
ACTION_KEY_SEGMENT_PATTERN,
|
|
535
|
+
ACTION_KEY_GLOB_PART_PATTERN
|
|
536
|
+
);
|
|
537
|
+
}
|
|
538
|
+
function isGlobSegment(value, exactPattern, globPartPattern) {
|
|
539
|
+
if (exactPattern.test(value)) {
|
|
540
|
+
return true;
|
|
541
|
+
}
|
|
542
|
+
if (!value.includes("*") || value.includes("**")) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
const parts = value.split("@");
|
|
546
|
+
return parts.length <= 2 && parts.every((part) => globPartPattern.test(part));
|
|
547
|
+
}
|
|
548
|
+
function isActionResourcePattern(value) {
|
|
549
|
+
const segments = value.split("/");
|
|
550
|
+
if (segments.length !== 4 || segments[0] !== "action") {
|
|
551
|
+
return false;
|
|
470
552
|
}
|
|
471
|
-
|
|
553
|
+
const [, selectedApi, actionType, actionKey] = segments;
|
|
554
|
+
return selectedApi !== void 0 && actionType !== void 0 && actionKey !== void 0 && isSelectedApiPattern(selectedApi) && (actionType === "*" || ActionTypeSchema.safeParse(actionType).success) && isActionKeyPattern(actionKey);
|
|
472
555
|
}
|
|
556
|
+
var ActionResourcePatternSchema = import_zod3.z.string().refine(isActionResourcePattern, {
|
|
557
|
+
message: "Expected action/{selected_api}/{canonical_action_type|*}/{action_key_pattern}"
|
|
558
|
+
});
|
|
559
|
+
function resourceFamily(resource) {
|
|
560
|
+
if (resource === "*" || resource === "http") {
|
|
561
|
+
return resource;
|
|
562
|
+
}
|
|
563
|
+
if (ActionResourcePatternSchema.safeParse(resource).success) {
|
|
564
|
+
return "action";
|
|
565
|
+
}
|
|
566
|
+
if (CONNECTION_RESOURCE_PATTERN.test(resource)) {
|
|
567
|
+
return "connection";
|
|
568
|
+
}
|
|
569
|
+
return void 0;
|
|
570
|
+
}
|
|
571
|
+
function permissionAppliesTo(permission, family) {
|
|
572
|
+
if (permission === "*" || family === "*") {
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
return permission === "can_execute" ? family === "action" || family === "http" : family === "connection";
|
|
576
|
+
}
|
|
577
|
+
function statementGroups(policy) {
|
|
578
|
+
if (policy.version === 4) {
|
|
579
|
+
return policy.rules.map((rules, tierIndex) => ({
|
|
580
|
+
statements: rules,
|
|
581
|
+
path: ["rules", tierIndex],
|
|
582
|
+
entry: "rule"
|
|
583
|
+
}));
|
|
584
|
+
}
|
|
585
|
+
if (policy.version === 3) {
|
|
586
|
+
return policy.statements.map((statements, tierIndex) => ({
|
|
587
|
+
statements,
|
|
588
|
+
path: ["statements", tierIndex],
|
|
589
|
+
entry: "statement"
|
|
590
|
+
}));
|
|
591
|
+
}
|
|
592
|
+
return [
|
|
593
|
+
{
|
|
594
|
+
statements: policy.statements,
|
|
595
|
+
path: ["statements"],
|
|
596
|
+
entry: "statement"
|
|
597
|
+
}
|
|
598
|
+
];
|
|
599
|
+
}
|
|
600
|
+
var ZapierPolicySchema = import_policy_schema.PolicySchema.superRefine((policy, ctx) => {
|
|
601
|
+
statementGroups(policy).forEach(({ statements, path, entry }) => {
|
|
602
|
+
const Entry = entry === "rule" ? "Rule" : "Statement";
|
|
603
|
+
statements.forEach((statement, statementIndex) => {
|
|
604
|
+
const families = statement.resources.map((resource, resourceIndex) => {
|
|
605
|
+
const family = resourceFamily(resource);
|
|
606
|
+
if (family === void 0) {
|
|
607
|
+
ctx.addIssue({
|
|
608
|
+
code: "custom",
|
|
609
|
+
message: 'Expected "http", "connection/{id|*}", a valid action pattern, or "*"',
|
|
610
|
+
path: [...path, statementIndex, "resources", resourceIndex]
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
return family;
|
|
614
|
+
});
|
|
615
|
+
statement.permissions.forEach((permission, permissionIndex) => {
|
|
616
|
+
if (!PERMISSIONS.has(permission)) {
|
|
617
|
+
ctx.addIssue({
|
|
618
|
+
code: "custom",
|
|
619
|
+
message: `Unknown Zapier permission "${permission}"`,
|
|
620
|
+
path: [...path, statementIndex, "permissions", permissionIndex]
|
|
621
|
+
});
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const incompatibleFamily = families.find(
|
|
625
|
+
(family) => family !== void 0 && !permissionAppliesTo(permission, family)
|
|
626
|
+
);
|
|
627
|
+
if (incompatibleFamily !== void 0) {
|
|
628
|
+
ctx.addIssue({
|
|
629
|
+
code: "custom",
|
|
630
|
+
message: `Permission "${permission}" cannot apply to ${incompatibleFamily} resources`,
|
|
631
|
+
path: [...path, statementIndex, "permissions", permissionIndex]
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
const maskingStatementIndex = statements.findIndex(
|
|
636
|
+
(maskingStatement, maskingIndex) => maskingIndex !== statementIndex && EFFECT_PRECEDENCE[maskingStatement.effect] > EFFECT_PRECEDENCE[statement.effect] && (0, import_policy.isStatementSubsumedBy)(statement, {
|
|
637
|
+
...maskingStatement,
|
|
638
|
+
effect: statement.effect
|
|
639
|
+
})
|
|
640
|
+
);
|
|
641
|
+
if (maskingStatementIndex !== -1) {
|
|
642
|
+
const maskingStatement = statements[maskingStatementIndex];
|
|
643
|
+
const maskingId = maskingStatement.id ? ` (id "${maskingStatement.id}")` : "";
|
|
644
|
+
ctx.addIssue({
|
|
645
|
+
code: "custom",
|
|
646
|
+
message: `${Entry} is entirely masked by higher-precedence ${entry} at index ${maskingStatementIndex}${maskingId}`,
|
|
647
|
+
path: [...path, statementIndex]
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
});
|
|
473
653
|
|
|
474
654
|
// src/connection-policy.ts
|
|
475
|
-
var
|
|
476
|
-
var AccessLevelSchema =
|
|
655
|
+
var import_zod4 = require("zod");
|
|
656
|
+
var AccessLevelSchema = import_zod4.z.enum(["read", "read_write"]);
|
|
657
|
+
function requireAccessLevel(accessLevel) {
|
|
658
|
+
if (!AccessLevelSchema.safeParse(accessLevel).success) {
|
|
659
|
+
throw new Error(
|
|
660
|
+
`invalid access_level "${accessLevel}": expected "read" or "read_write"`
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
477
664
|
var READ_HTTP_METHODS = ["GET", "HEAD", "OPTIONS"];
|
|
478
665
|
var READ_ACTION_TYPES = [
|
|
479
666
|
"read",
|
|
@@ -493,7 +680,19 @@ function isWriteRequest(context) {
|
|
|
493
680
|
}
|
|
494
681
|
function buildConnectionPolicy(options) {
|
|
495
682
|
const { connectionId, selectedApi, accessLevel } = options;
|
|
683
|
+
requireAccessLevel(accessLevel);
|
|
684
|
+
if (!CONNECTION_ID_PATTERN.test(connectionId)) {
|
|
685
|
+
throw new Error(
|
|
686
|
+
`invalid connection_id "${connectionId}": must not contain "/", "*", or whitespace`
|
|
687
|
+
);
|
|
688
|
+
}
|
|
689
|
+
if (!ACTION_RESOURCE_SEGMENT_PATTERN.test(selectedApi)) {
|
|
690
|
+
throw new Error(
|
|
691
|
+
`invalid selected_api "${selectedApi}": must not contain "/", "*", or whitespace`
|
|
692
|
+
);
|
|
693
|
+
}
|
|
496
694
|
const authStatement = {
|
|
695
|
+
id: `r_${crypto.randomUUID()}`,
|
|
497
696
|
effect: "allow",
|
|
498
697
|
permissions: ["can_use"],
|
|
499
698
|
resources: [`connection/${connectionId}`],
|
|
@@ -513,6 +712,7 @@ function buildConnectionPolicy(options) {
|
|
|
513
712
|
});
|
|
514
713
|
}
|
|
515
714
|
const httpStatement = {
|
|
715
|
+
id: `r_${crypto.randomUUID()}`,
|
|
516
716
|
effect: "allow",
|
|
517
717
|
permissions: ["can_execute"],
|
|
518
718
|
resources: ["http"],
|
|
@@ -521,20 +721,22 @@ function buildConnectionPolicy(options) {
|
|
|
521
721
|
const apiPattern = globSelectedApiVersion(selectedApi);
|
|
522
722
|
const actionResources = accessLevel === "read" ? READ_ACTION_TYPES.map((t) => `action/${apiPattern}/${t}/*`) : [`action/${apiPattern}/*/*`];
|
|
523
723
|
const actionStatement = {
|
|
724
|
+
id: `r_${crypto.randomUUID()}`,
|
|
524
725
|
effect: "allow",
|
|
525
726
|
permissions: ["can_execute"],
|
|
526
727
|
resources: actionResources,
|
|
527
728
|
conditions: [connectionCondition]
|
|
528
729
|
};
|
|
529
730
|
return {
|
|
530
|
-
version:
|
|
531
|
-
|
|
731
|
+
version: 4,
|
|
732
|
+
rules: [[authStatement, httpStatement, actionStatement]]
|
|
532
733
|
};
|
|
533
734
|
}
|
|
534
735
|
|
|
535
736
|
// src/connectionless-policy.ts
|
|
536
737
|
function buildConnectionlessPolicy(options) {
|
|
537
738
|
const { context, accessLevel } = options;
|
|
739
|
+
requireAccessLevel(accessLevel);
|
|
538
740
|
if (context.request_type === "http_request") {
|
|
539
741
|
const conditions = [
|
|
540
742
|
{
|
|
@@ -551,27 +753,36 @@ function buildConnectionlessPolicy(options) {
|
|
|
551
753
|
});
|
|
552
754
|
}
|
|
553
755
|
const statement2 = {
|
|
756
|
+
id: `r_${crypto.randomUUID()}`,
|
|
554
757
|
effect: "allow",
|
|
555
758
|
permissions: ["can_execute"],
|
|
556
759
|
resources: ["http"],
|
|
557
760
|
conditions
|
|
558
761
|
};
|
|
559
|
-
return {
|
|
762
|
+
return {
|
|
763
|
+
version: 4,
|
|
764
|
+
rules: [[statement2]]
|
|
765
|
+
};
|
|
560
766
|
}
|
|
561
767
|
const { selected_api } = parseActionResource(context.resource);
|
|
562
768
|
const apiPattern = globSelectedApiVersion(selected_api);
|
|
563
769
|
const resources = accessLevel === "read" ? READ_ACTION_TYPES.map((t) => `action/${apiPattern}/${t}/*`) : [`action/${apiPattern}/*/*`];
|
|
564
770
|
const statement = {
|
|
771
|
+
id: `r_${crypto.randomUUID()}`,
|
|
565
772
|
effect: "allow",
|
|
566
773
|
permissions: ["can_execute"],
|
|
567
774
|
resources,
|
|
568
775
|
conditions: []
|
|
569
776
|
};
|
|
570
|
-
return {
|
|
777
|
+
return {
|
|
778
|
+
version: 4,
|
|
779
|
+
rules: [[statement]]
|
|
780
|
+
};
|
|
571
781
|
}
|
|
572
782
|
// Annotate the CommonJS export names for ESM import in node:
|
|
573
783
|
0 && (module.exports = {
|
|
574
784
|
AccessLevelSchema,
|
|
785
|
+
ActionResourcePatternSchema,
|
|
575
786
|
ActionRunContextInputSchema,
|
|
576
787
|
ActionRunContextSchema,
|
|
577
788
|
ActionTypeSchema,
|
|
@@ -584,6 +795,7 @@ function buildConnectionlessPolicy(options) {
|
|
|
584
795
|
READ_ACTION_TYPES,
|
|
585
796
|
READ_HTTP_METHODS,
|
|
586
797
|
RequestContextSchema,
|
|
798
|
+
ZapierPolicySchema,
|
|
587
799
|
buildActionResource,
|
|
588
800
|
buildActionRunContext,
|
|
589
801
|
buildBroadActionResource,
|
|
@@ -595,10 +807,12 @@ function buildConnectionlessPolicy(options) {
|
|
|
595
807
|
buildHttpRequestContext,
|
|
596
808
|
buildUrlString,
|
|
597
809
|
globSelectedApiVersion,
|
|
810
|
+
isIgnoredHeader,
|
|
598
811
|
isWriteRequest,
|
|
599
812
|
parseActionResource,
|
|
600
813
|
parseUrl,
|
|
601
814
|
toAppKey,
|
|
602
|
-
toExactPolicy
|
|
815
|
+
toExactPolicy,
|
|
816
|
+
toExactPolicyV3
|
|
603
817
|
});
|
|
604
818
|
//# sourceMappingURL=index.cjs.map
|