alchemy 0.93.4 → 0.93.7
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/bin/alchemy.js +1966 -1825
- package/bin/alchemy.ts +2 -0
- package/bin/commands/state.ts +58 -0
- package/bin/services/execute-alchemy.ts +9 -1
- package/lib/alchemy.d.ts.map +1 -1
- package/lib/alchemy.js +7 -0
- package/lib/alchemy.js.map +1 -1
- package/lib/cloudflare/access-application.d.ts +204 -0
- package/lib/cloudflare/access-application.d.ts.map +1 -0
- package/lib/cloudflare/access-application.js +205 -0
- package/lib/cloudflare/access-application.js.map +1 -0
- package/lib/cloudflare/access-group.d.ts +115 -0
- package/lib/cloudflare/access-group.d.ts.map +1 -0
- package/lib/cloudflare/access-group.js +147 -0
- package/lib/cloudflare/access-group.js.map +1 -0
- package/lib/cloudflare/access-identity-provider.d.ts +261 -0
- package/lib/cloudflare/access-identity-provider.d.ts.map +1 -0
- package/lib/cloudflare/access-identity-provider.js +229 -0
- package/lib/cloudflare/access-identity-provider.js.map +1 -0
- package/lib/cloudflare/access-policy.d.ts +168 -0
- package/lib/cloudflare/access-policy.d.ts.map +1 -0
- package/lib/cloudflare/access-policy.js +176 -0
- package/lib/cloudflare/access-policy.js.map +1 -0
- package/lib/cloudflare/access-rule.d.ts +248 -0
- package/lib/cloudflare/access-rule.d.ts.map +1 -0
- package/lib/cloudflare/access-rule.js +37 -0
- package/lib/cloudflare/access-rule.js.map +1 -0
- package/lib/cloudflare/access-service-token.d.ts +86 -0
- package/lib/cloudflare/access-service-token.d.ts.map +1 -0
- package/lib/cloudflare/access-service-token.js +127 -0
- package/lib/cloudflare/access-service-token.js.map +1 -0
- package/lib/cloudflare/index.d.ts +6 -0
- package/lib/cloudflare/index.d.ts.map +1 -1
- package/lib/cloudflare/index.js +6 -0
- package/lib/cloudflare/index.js.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.d.ts.map +1 -1
- package/lib/cloudflare/miniflare/build-worker-options.js +17 -3
- package/lib/cloudflare/miniflare/build-worker-options.js.map +1 -1
- package/lib/state/file-system-state-store.d.ts +1 -0
- package/lib/state/file-system-state-store.d.ts.map +1 -1
- package/lib/state/file-system-state-store.js +16 -0
- package/lib/state/file-system-state-store.js.map +1 -1
- package/lib/state/operations.d.ts +1 -0
- package/lib/state/operations.d.ts.map +1 -1
- package/lib/state/operations.js +24 -0
- package/lib/state/operations.js.map +1 -1
- package/lib/state/proxy.d.ts +1 -0
- package/lib/state/proxy.d.ts.map +1 -1
- package/lib/state/proxy.js +3 -0
- package/lib/state/proxy.js.map +1 -1
- package/lib/state-cli.d.ts +9 -0
- package/lib/state-cli.d.ts.map +1 -0
- package/lib/state-cli.js +110 -0
- package/lib/state-cli.js.map +1 -0
- package/lib/state.d.ts +2 -0
- package/lib/state.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/alchemy.ts +9 -0
- package/src/cloudflare/access-application.ts +489 -0
- package/src/cloudflare/access-group.ts +271 -0
- package/src/cloudflare/access-identity-provider.ts +584 -0
- package/src/cloudflare/access-policy.ts +392 -0
- package/src/cloudflare/access-rule.ts +307 -0
- package/src/cloudflare/access-service-token.ts +255 -0
- package/src/cloudflare/index.ts +6 -0
- package/src/cloudflare/miniflare/build-worker-options.ts +20 -3
- package/src/state/file-system-state-store.ts +16 -0
- package/src/state/operations.ts +22 -0
- package/src/state/proxy.ts +4 -0
- package/src/state-cli.ts +134 -0
- package/src/state.ts +2 -0
- package/workers/cloudflare-state-store.js +17 -0
- package/workers/tunnel-proxy.js +1 -1
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import type { Context } from "../context.ts";
|
|
2
|
+
import { Resource, ResourceKind } from "../resource.ts";
|
|
3
|
+
import { logger } from "../util/logger.ts";
|
|
4
|
+
import { serializeAccessRule, type AccessRule } from "./access-rule.ts";
|
|
5
|
+
import { isCloudflareApiError } from "./api-error.ts";
|
|
6
|
+
import {
|
|
7
|
+
extractCloudflareResult,
|
|
8
|
+
type CloudflareApiListResponse,
|
|
9
|
+
} from "./api-response.ts";
|
|
10
|
+
import {
|
|
11
|
+
createCloudflareApi,
|
|
12
|
+
type CloudflareApi,
|
|
13
|
+
type CloudflareApiOptions,
|
|
14
|
+
} from "./api.ts";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Access decision a policy applies when its rules match.
|
|
18
|
+
* - `allow`: grant access when an authenticated user matches `include`.
|
|
19
|
+
* - `deny`: deny access when matched.
|
|
20
|
+
* - `non_identity`: allow without identity (e.g. service tokens, IP allowlists).
|
|
21
|
+
* - `bypass`: skip Access entirely (no auth required).
|
|
22
|
+
*/
|
|
23
|
+
export type AccessPolicyDecision = "allow" | "deny" | "non_identity" | "bypass";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* An approval group authorising an access request when
|
|
27
|
+
* `approvalRequired: true`.
|
|
28
|
+
*/
|
|
29
|
+
export interface AccessPolicyApprovalGroup {
|
|
30
|
+
/** Number of approvals required from this group. */
|
|
31
|
+
approvalsNeeded: number;
|
|
32
|
+
/** Email addresses of approvers. */
|
|
33
|
+
emailAddresses?: string[];
|
|
34
|
+
/** UUID of an Access email list. */
|
|
35
|
+
emailListUuid?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Optional MFA enforcement on top of the IdP's authentication.
|
|
40
|
+
*/
|
|
41
|
+
export interface AccessPolicyMfaConfig {
|
|
42
|
+
allowedAuthenticators?: string[];
|
|
43
|
+
mfaDisabled?: boolean;
|
|
44
|
+
/** Cloudflare duration string, e.g. `"30m"`. */
|
|
45
|
+
sessionDuration?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Connection-protocol-specific options (currently only RDP clipboard
|
|
50
|
+
* formats are supported by Cloudflare).
|
|
51
|
+
*/
|
|
52
|
+
export interface AccessPolicyConnectionRules {
|
|
53
|
+
rdp?: {
|
|
54
|
+
allowedClipboardFormats?: ("text" | "image" | "files")[];
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Properties for creating or updating an {@link AccessPolicy}.
|
|
60
|
+
*/
|
|
61
|
+
export interface AccessPolicyProps extends CloudflareApiOptions {
|
|
62
|
+
/**
|
|
63
|
+
* Display name of the policy.
|
|
64
|
+
*
|
|
65
|
+
* @default ${app}-${stage}-${id}
|
|
66
|
+
*/
|
|
67
|
+
name?: string;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Decision the policy applies. **Immutable** — changing the decision will
|
|
71
|
+
* trigger replacement of the underlying Cloudflare resource.
|
|
72
|
+
*/
|
|
73
|
+
decision: AccessPolicyDecision;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Rules a request must match to be considered (OR logic). Must be
|
|
77
|
+
* non-empty — Cloudflare rejects policies with no include rules.
|
|
78
|
+
*/
|
|
79
|
+
include: AccessRule[];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Rules that, when matched, exclude the request from this policy.
|
|
83
|
+
*/
|
|
84
|
+
exclude?: AccessRule[];
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Rules that must additionally match (AND logic).
|
|
88
|
+
*/
|
|
89
|
+
require?: AccessRule[];
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Require explicit approval before granting access.
|
|
93
|
+
*/
|
|
94
|
+
approvalRequired?: boolean;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Approver groups consulted when `approvalRequired: true`.
|
|
98
|
+
*/
|
|
99
|
+
approvalGroups?: AccessPolicyApprovalGroup[];
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Prompt the user for a purpose justification on each access.
|
|
103
|
+
*/
|
|
104
|
+
purposeJustificationRequired?: boolean;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Prompt text shown when `purposeJustificationRequired` is true.
|
|
108
|
+
*/
|
|
109
|
+
purposeJustificationPrompt?: string;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Force isolated browser rendering for this policy.
|
|
113
|
+
*/
|
|
114
|
+
isolationRequired?: boolean;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Optional MFA enforcement.
|
|
118
|
+
*/
|
|
119
|
+
mfaConfig?: AccessPolicyMfaConfig;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Override the default Access session duration (Cloudflare duration string).
|
|
123
|
+
*/
|
|
124
|
+
sessionDuration?: string;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Per-protocol connection rules (e.g. RDP clipboard restrictions).
|
|
128
|
+
*/
|
|
129
|
+
connectionRules?: AccessPolicyConnectionRules;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Adopt an existing policy with the same name instead of failing.
|
|
133
|
+
*
|
|
134
|
+
* @default false
|
|
135
|
+
*/
|
|
136
|
+
adopt?: boolean;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Whether to delete the policy when removed from Alchemy.
|
|
140
|
+
*
|
|
141
|
+
* @default true
|
|
142
|
+
*/
|
|
143
|
+
delete?: boolean;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Output for an {@link AccessPolicy}.
|
|
148
|
+
*/
|
|
149
|
+
export type AccessPolicy = Omit<AccessPolicyProps, "adopt" | "delete"> & {
|
|
150
|
+
/** Cloudflare-assigned policy UUID. */
|
|
151
|
+
id: string;
|
|
152
|
+
/** Display name. */
|
|
153
|
+
name: string;
|
|
154
|
+
/** Number of applications currently referencing this policy. */
|
|
155
|
+
appCount: number;
|
|
156
|
+
/** ISO 8601 creation timestamp. */
|
|
157
|
+
createdAt: string;
|
|
158
|
+
/** ISO 8601 last-update timestamp. */
|
|
159
|
+
updatedAt: string;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Type guard for {@link AccessPolicy}.
|
|
164
|
+
*/
|
|
165
|
+
export function isAccessPolicy(resource: any): resource is AccessPolicy {
|
|
166
|
+
return resource?.[ResourceKind] === "cloudflare::AccessPolicy";
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
interface CloudflareAccessPolicy {
|
|
170
|
+
id: string;
|
|
171
|
+
name: string;
|
|
172
|
+
decision: AccessPolicyDecision;
|
|
173
|
+
include: Record<string, unknown>[];
|
|
174
|
+
exclude?: Record<string, unknown>[];
|
|
175
|
+
require?: Record<string, unknown>[];
|
|
176
|
+
app_count?: number;
|
|
177
|
+
reusable?: boolean;
|
|
178
|
+
created_at: string;
|
|
179
|
+
updated_at: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Creates a reusable Cloudflare Zero Trust [Access policy](https://developers.cloudflare.com/cloudflare-one/policies/access/)
|
|
184
|
+
* that can be attached to one or more {@link AccessApplication} resources.
|
|
185
|
+
*
|
|
186
|
+
* @example
|
|
187
|
+
* // Allow employees from a specific email domain.
|
|
188
|
+
* const employees = await AccessPolicy("employees", {
|
|
189
|
+
* name: "Employees",
|
|
190
|
+
* decision: "allow",
|
|
191
|
+
* include: [{ email_domain: { domain: "acme.com" } }],
|
|
192
|
+
* });
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* // Bypass Access for an office IP range.
|
|
196
|
+
* const officeBypass = await AccessPolicy("office-bypass", {
|
|
197
|
+
* name: "Office Bypass",
|
|
198
|
+
* decision: "bypass",
|
|
199
|
+
* include: [{ ip: { ip: "203.0.113.0/24" } }],
|
|
200
|
+
* });
|
|
201
|
+
*
|
|
202
|
+
* @example
|
|
203
|
+
* // Reference an AccessGroup and require approval.
|
|
204
|
+
* const sensitive = await AccessPolicy("sensitive", {
|
|
205
|
+
* name: "Sensitive admin access",
|
|
206
|
+
* decision: "allow",
|
|
207
|
+
* include: [{ group: { id: adminGroup } }],
|
|
208
|
+
* approvalRequired: true,
|
|
209
|
+
* approvalGroups: [
|
|
210
|
+
* { approvalsNeeded: 2, emailAddresses: ["security@acme.com"] },
|
|
211
|
+
* ],
|
|
212
|
+
* isolationRequired: true,
|
|
213
|
+
* });
|
|
214
|
+
*/
|
|
215
|
+
export const AccessPolicy = Resource(
|
|
216
|
+
"cloudflare::AccessPolicy",
|
|
217
|
+
async function (
|
|
218
|
+
this: Context<AccessPolicy>,
|
|
219
|
+
id: string,
|
|
220
|
+
props: AccessPolicyProps,
|
|
221
|
+
): Promise<AccessPolicy> {
|
|
222
|
+
const api = await createCloudflareApi(props);
|
|
223
|
+
const name = props.name ?? this.scope.createPhysicalName(id);
|
|
224
|
+
const basePath = `/accounts/${api.accountId}/access/policies`;
|
|
225
|
+
|
|
226
|
+
if (this.phase === "delete") {
|
|
227
|
+
if (this.output?.id && props.delete !== false) {
|
|
228
|
+
await deleteAccessPolicy(api, this.output.id);
|
|
229
|
+
}
|
|
230
|
+
return this.destroy();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (!props.include || props.include.length === 0) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
`AccessPolicy "${name}" requires at least one rule in 'include'.`,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// decision is immutable — recreate if it changed.
|
|
240
|
+
if (
|
|
241
|
+
this.phase === "update" &&
|
|
242
|
+
this.output &&
|
|
243
|
+
this.output.decision !== props.decision
|
|
244
|
+
) {
|
|
245
|
+
this.replace(true);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const body: Record<string, unknown> = {
|
|
249
|
+
name,
|
|
250
|
+
decision: props.decision,
|
|
251
|
+
reusable: true,
|
|
252
|
+
include: props.include.map(serializeAccessRule),
|
|
253
|
+
exclude: (props.exclude ?? []).map(serializeAccessRule),
|
|
254
|
+
require: (props.require ?? []).map(serializeAccessRule),
|
|
255
|
+
};
|
|
256
|
+
if (props.approvalRequired !== undefined)
|
|
257
|
+
body.approval_required = props.approvalRequired;
|
|
258
|
+
if (props.approvalGroups)
|
|
259
|
+
body.approval_groups = props.approvalGroups.map((g) => ({
|
|
260
|
+
approvals_needed: g.approvalsNeeded,
|
|
261
|
+
email_addresses: g.emailAddresses,
|
|
262
|
+
email_list_uuid: g.emailListUuid,
|
|
263
|
+
}));
|
|
264
|
+
if (props.purposeJustificationRequired !== undefined)
|
|
265
|
+
body.purpose_justification_required = props.purposeJustificationRequired;
|
|
266
|
+
if (props.purposeJustificationPrompt !== undefined)
|
|
267
|
+
body.purpose_justification_prompt = props.purposeJustificationPrompt;
|
|
268
|
+
if (props.isolationRequired !== undefined)
|
|
269
|
+
body.isolation_required = props.isolationRequired;
|
|
270
|
+
if (props.sessionDuration !== undefined)
|
|
271
|
+
body.session_duration = props.sessionDuration;
|
|
272
|
+
if (props.mfaConfig)
|
|
273
|
+
body.mfa_config = {
|
|
274
|
+
allowed_authenticators: props.mfaConfig.allowedAuthenticators,
|
|
275
|
+
mfa_disabled: props.mfaConfig.mfaDisabled,
|
|
276
|
+
session_duration: props.mfaConfig.sessionDuration,
|
|
277
|
+
};
|
|
278
|
+
if (props.connectionRules?.rdp)
|
|
279
|
+
body.connection_rules = {
|
|
280
|
+
rdp: {
|
|
281
|
+
allowed_clipboard_formats:
|
|
282
|
+
props.connectionRules.rdp.allowedClipboardFormats,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
let policy: CloudflareAccessPolicy;
|
|
287
|
+
if (this.phase === "update" && this.output?.id) {
|
|
288
|
+
policy = await extractCloudflareResult<CloudflareAccessPolicy>(
|
|
289
|
+
`update access policy "${name}"`,
|
|
290
|
+
api.put(`${basePath}/${this.output.id}`, body),
|
|
291
|
+
);
|
|
292
|
+
} else {
|
|
293
|
+
const adopt = props.adopt ?? this.scope.adopt;
|
|
294
|
+
try {
|
|
295
|
+
policy = await extractCloudflareResult<CloudflareAccessPolicy>(
|
|
296
|
+
`create access policy "${name}"`,
|
|
297
|
+
api.post(basePath, body),
|
|
298
|
+
);
|
|
299
|
+
} catch (err) {
|
|
300
|
+
if (adopt && isAccessDuplicateNameError(err)) {
|
|
301
|
+
const existing = await findAccessPolicyByName(api, name);
|
|
302
|
+
if (!existing) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`Access policy "${name}" already exists but could not be found for adoption.`,
|
|
305
|
+
{ cause: err },
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
logger.log(
|
|
309
|
+
`Adopting existing access policy "${name}" (${existing.id})`,
|
|
310
|
+
);
|
|
311
|
+
policy = await extractCloudflareResult<CloudflareAccessPolicy>(
|
|
312
|
+
`adopt access policy "${name}"`,
|
|
313
|
+
api.put(`${basePath}/${existing.id}`, body),
|
|
314
|
+
);
|
|
315
|
+
} else {
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
id: policy.id,
|
|
323
|
+
name: policy.name,
|
|
324
|
+
decision: policy.decision,
|
|
325
|
+
include: props.include,
|
|
326
|
+
exclude: props.exclude,
|
|
327
|
+
require: props.require,
|
|
328
|
+
approvalRequired: props.approvalRequired,
|
|
329
|
+
approvalGroups: props.approvalGroups,
|
|
330
|
+
purposeJustificationRequired: props.purposeJustificationRequired,
|
|
331
|
+
purposeJustificationPrompt: props.purposeJustificationPrompt,
|
|
332
|
+
isolationRequired: props.isolationRequired,
|
|
333
|
+
mfaConfig: props.mfaConfig,
|
|
334
|
+
sessionDuration: props.sessionDuration,
|
|
335
|
+
connectionRules: props.connectionRules,
|
|
336
|
+
appCount: policy.app_count ?? 0,
|
|
337
|
+
createdAt: policy.created_at,
|
|
338
|
+
updatedAt: policy.updated_at,
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
function isAccessDuplicateNameError(err: unknown): boolean {
|
|
344
|
+
if (
|
|
345
|
+
isCloudflareApiError(err, { status: 409 }) ||
|
|
346
|
+
isCloudflareApiError(err, { status: 400 })
|
|
347
|
+
) {
|
|
348
|
+
const data = err.errorData;
|
|
349
|
+
return (
|
|
350
|
+
Array.isArray(data) &&
|
|
351
|
+
data.some(
|
|
352
|
+
(e) => "message" in e && /already exists/i.test(String(e.message)),
|
|
353
|
+
)
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function findAccessPolicyByName(
|
|
360
|
+
api: CloudflareApi,
|
|
361
|
+
name: string,
|
|
362
|
+
): Promise<CloudflareAccessPolicy | null> {
|
|
363
|
+
let page = 1;
|
|
364
|
+
const perPage = 50;
|
|
365
|
+
while (true) {
|
|
366
|
+
const response = await api.get(
|
|
367
|
+
`/accounts/${api.accountId}/access/policies?page=${page}&per_page=${perPage}`,
|
|
368
|
+
);
|
|
369
|
+
if (!response.ok) return null;
|
|
370
|
+
const data =
|
|
371
|
+
(await response.json()) as CloudflareApiListResponse<CloudflareAccessPolicy>;
|
|
372
|
+
const match = data.result.find((p) => p.name === name);
|
|
373
|
+
if (match) return match;
|
|
374
|
+
const info = data.result_info;
|
|
375
|
+
if (!info || info.page * info.per_page >= info.total_count) return null;
|
|
376
|
+
page++;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function deleteAccessPolicy(
|
|
381
|
+
api: CloudflareApi,
|
|
382
|
+
policyId: string,
|
|
383
|
+
): Promise<void> {
|
|
384
|
+
const response = await api.delete(
|
|
385
|
+
`/accounts/${api.accountId}/access/policies/${policyId}`,
|
|
386
|
+
);
|
|
387
|
+
if (!response.ok && response.status !== 404) {
|
|
388
|
+
logger.error(
|
|
389
|
+
`Error deleting access policy ${policyId}: ${response.status} ${response.statusText}`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { ResourceKind } from "../resource.ts";
|
|
2
|
+
import type { AccessGroup } from "./access-group.ts";
|
|
3
|
+
import type { AccessIdentityProvider } from "./access-identity-provider.ts";
|
|
4
|
+
import type { AccessServiceToken } from "./access-service-token.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Match anyone with this email address.
|
|
8
|
+
*/
|
|
9
|
+
export interface EmailAccessRule {
|
|
10
|
+
email: { email: string };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Match anyone with an email address ending in this domain.
|
|
15
|
+
*/
|
|
16
|
+
export interface EmailDomainAccessRule {
|
|
17
|
+
email_domain: { domain: string };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Match a single IP address (IPv4 or IPv6) or CIDR.
|
|
22
|
+
*/
|
|
23
|
+
export interface IpAccessRule {
|
|
24
|
+
ip: { ip: string };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Match an IP from a managed Cloudflare IP list.
|
|
29
|
+
*/
|
|
30
|
+
export interface IpListAccessRule {
|
|
31
|
+
ip_list: { id: string };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Match every user. Rarely useful in `include` — typical in `exclude` or as a fallback.
|
|
36
|
+
*/
|
|
37
|
+
export interface EveryoneAccessRule {
|
|
38
|
+
everyone: Record<string, never>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Match a member of the referenced Access group.
|
|
43
|
+
*/
|
|
44
|
+
export interface GroupAccessRule {
|
|
45
|
+
group: { id: string | AccessGroup };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Match requests authenticating with a specific service token.
|
|
50
|
+
*/
|
|
51
|
+
export interface ServiceTokenAccessRule {
|
|
52
|
+
service_token: { token_id: string | AccessServiceToken };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Match any valid service token in the account.
|
|
57
|
+
*/
|
|
58
|
+
export interface AnyValidServiceTokenAccessRule {
|
|
59
|
+
any_valid_service_token: Record<string, never>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Match an Azure AD group by object ID.
|
|
64
|
+
*/
|
|
65
|
+
export interface AzureGroupAccessRule {
|
|
66
|
+
azure: {
|
|
67
|
+
id: string;
|
|
68
|
+
identity_provider_id: string | AccessIdentityProvider;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Match an Okta group by name.
|
|
74
|
+
*/
|
|
75
|
+
export interface OktaGroupAccessRule {
|
|
76
|
+
okta: {
|
|
77
|
+
name: string;
|
|
78
|
+
identity_provider_id: string | AccessIdentityProvider;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Match a SAML attribute returned by the IdP.
|
|
84
|
+
*/
|
|
85
|
+
export interface SamlAttributeAccessRule {
|
|
86
|
+
saml: {
|
|
87
|
+
attribute_name: string;
|
|
88
|
+
attribute_value: string;
|
|
89
|
+
identity_provider_id: string | AccessIdentityProvider;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Match a Google Workspace group by email.
|
|
95
|
+
*/
|
|
96
|
+
export interface GsuiteGroupAccessRule {
|
|
97
|
+
gsuite: {
|
|
98
|
+
email: string;
|
|
99
|
+
identity_provider_id: string | AccessIdentityProvider;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Match a member of a GitHub organization or team.
|
|
105
|
+
*/
|
|
106
|
+
export interface GithubOrganizationAccessRule {
|
|
107
|
+
github_organization: {
|
|
108
|
+
name: string;
|
|
109
|
+
team?: string;
|
|
110
|
+
identity_provider_id: string | AccessIdentityProvider;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Match any client presenting a valid mTLS certificate.
|
|
116
|
+
*/
|
|
117
|
+
export interface CertificateAccessRule {
|
|
118
|
+
certificate: Record<string, never>;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Match an mTLS certificate with the given Common Name.
|
|
123
|
+
*/
|
|
124
|
+
export interface CommonNameAccessRule {
|
|
125
|
+
common_name: { common_name: string };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Match a device that satisfies a configured posture check.
|
|
130
|
+
*/
|
|
131
|
+
export interface DevicePostureAccessRule {
|
|
132
|
+
device_posture: { integration_uid: string };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Match by authentication method (e.g., `mfa`, `pwd`).
|
|
137
|
+
*/
|
|
138
|
+
export interface AuthMethodAccessRule {
|
|
139
|
+
auth_method: { auth_method: string };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Match users who logged in via a specific identity provider.
|
|
144
|
+
*/
|
|
145
|
+
export interface LoginMethodAccessRule {
|
|
146
|
+
login_method: { id: string | AccessIdentityProvider };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Match an authentication context (e.g., Azure AD CA).
|
|
151
|
+
*/
|
|
152
|
+
export interface AuthenticationContextAccessRule {
|
|
153
|
+
authentication_context: {
|
|
154
|
+
id: string;
|
|
155
|
+
ac_id: string;
|
|
156
|
+
identity_provider_id: string | AccessIdentityProvider;
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Match an OIDC claim returned by the IdP.
|
|
162
|
+
*/
|
|
163
|
+
export interface OidcClaimAccessRule {
|
|
164
|
+
oidc_claim: {
|
|
165
|
+
name: string;
|
|
166
|
+
value: string;
|
|
167
|
+
identity_provider_id: string | AccessIdentityProvider;
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Match users at or above the given Cloudflare user-risk score.
|
|
173
|
+
*/
|
|
174
|
+
export interface UserRiskScoreAccessRule {
|
|
175
|
+
user_risk_score: {
|
|
176
|
+
provider: string;
|
|
177
|
+
risk_level: "low" | "medium" | "high";
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Delegate evaluation to an external endpoint.
|
|
183
|
+
*/
|
|
184
|
+
export interface ExternalEvaluationAccessRule {
|
|
185
|
+
external_evaluation: { evaluate_url: string; keys_url: string };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Match by ISO 3166 country code (legacy alias of `geo`).
|
|
190
|
+
*/
|
|
191
|
+
export interface CountryAccessRule {
|
|
192
|
+
country: { country_code: string };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Match by ISO 3166 country code.
|
|
197
|
+
*/
|
|
198
|
+
export interface GeoAccessRule {
|
|
199
|
+
geo: { country_code: string };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Match a hostname/domain.
|
|
204
|
+
*/
|
|
205
|
+
export interface DomainAccessRule {
|
|
206
|
+
domain: { domain: string };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Match a token issued by a linked Access application.
|
|
211
|
+
*/
|
|
212
|
+
export interface LinkedAppTokenAccessRule {
|
|
213
|
+
linked_app_token: { app_uid: string };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Match an SSO authentication context (Azure AD CA equivalent).
|
|
218
|
+
*/
|
|
219
|
+
export interface AccessAuthContextAccessRule {
|
|
220
|
+
access_auth_context: {
|
|
221
|
+
id: string;
|
|
222
|
+
ac_id: string;
|
|
223
|
+
identity_provider_id: string | AccessIdentityProvider;
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* A single Access rule expression, used in `include`, `exclude`, and `require`
|
|
229
|
+
* arrays of {@link AccessPolicy} and {@link AccessGroup}.
|
|
230
|
+
*
|
|
231
|
+
* Each rule is a single-key object whose key is the rule type and whose value
|
|
232
|
+
* is the rule-specific configuration. Mirrors Cloudflare's wire format so
|
|
233
|
+
* expressions can be copy-pasted directly from the Cloudflare docs.
|
|
234
|
+
*
|
|
235
|
+
* @see https://developers.cloudflare.com/cloudflare-one/policies/access/
|
|
236
|
+
*/
|
|
237
|
+
export type AccessRule =
|
|
238
|
+
| EmailAccessRule
|
|
239
|
+
| EmailDomainAccessRule
|
|
240
|
+
| IpAccessRule
|
|
241
|
+
| IpListAccessRule
|
|
242
|
+
| EveryoneAccessRule
|
|
243
|
+
| GroupAccessRule
|
|
244
|
+
| ServiceTokenAccessRule
|
|
245
|
+
| AnyValidServiceTokenAccessRule
|
|
246
|
+
| AzureGroupAccessRule
|
|
247
|
+
| OktaGroupAccessRule
|
|
248
|
+
| SamlAttributeAccessRule
|
|
249
|
+
| GsuiteGroupAccessRule
|
|
250
|
+
| GithubOrganizationAccessRule
|
|
251
|
+
| CertificateAccessRule
|
|
252
|
+
| CommonNameAccessRule
|
|
253
|
+
| DevicePostureAccessRule
|
|
254
|
+
| AuthMethodAccessRule
|
|
255
|
+
| LoginMethodAccessRule
|
|
256
|
+
| AuthenticationContextAccessRule
|
|
257
|
+
| OidcClaimAccessRule
|
|
258
|
+
| UserRiskScoreAccessRule
|
|
259
|
+
| ExternalEvaluationAccessRule
|
|
260
|
+
| CountryAccessRule
|
|
261
|
+
| GeoAccessRule
|
|
262
|
+
| DomainAccessRule
|
|
263
|
+
| LinkedAppTokenAccessRule
|
|
264
|
+
| AccessAuthContextAccessRule;
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Serialize an {@link AccessRule} to its wire JSON shape, replacing any lifted
|
|
268
|
+
* Resource references (`group.id`, `service_token.token_id`,
|
|
269
|
+
* `*.identity_provider_id`, `login_method.id`) with the resource's `id`.
|
|
270
|
+
*
|
|
271
|
+
* @internal
|
|
272
|
+
*/
|
|
273
|
+
export function serializeAccessRule(rule: AccessRule): Record<string, unknown> {
|
|
274
|
+
const entries = Object.entries(rule);
|
|
275
|
+
if (entries.length !== 1) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Invalid AccessRule: expected exactly one key, got ${entries.length}`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const [key, value] = entries[0];
|
|
281
|
+
if (typeof value !== "object" || value === null) {
|
|
282
|
+
return { [key]: value };
|
|
283
|
+
}
|
|
284
|
+
const serialized: Record<string, unknown> = {};
|
|
285
|
+
for (const [field, fieldValue] of Object.entries(
|
|
286
|
+
value as Record<string, unknown>,
|
|
287
|
+
)) {
|
|
288
|
+
serialized[field] = isResourceRef(fieldValue)
|
|
289
|
+
? (fieldValue as { id: string }).id
|
|
290
|
+
: fieldValue;
|
|
291
|
+
}
|
|
292
|
+
return { [key]: serialized };
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* True for Alchemy Resource objects (carry the {@link ResourceKind} symbol),
|
|
297
|
+
* false for plain literal values — including future rule shapes that may nest
|
|
298
|
+
* literal config objects.
|
|
299
|
+
*/
|
|
300
|
+
function isResourceRef(value: unknown): value is { id: string } {
|
|
301
|
+
return (
|
|
302
|
+
!!value &&
|
|
303
|
+
typeof value === "object" &&
|
|
304
|
+
!Array.isArray(value) &&
|
|
305
|
+
(value as Record<symbol, unknown>)[ResourceKind] !== undefined
|
|
306
|
+
);
|
|
307
|
+
}
|