breakaway 2.0.0-main.99 → 2.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/.agents/skills/tasks/SKILL.md +3 -2
- package/LICENSE +45 -68
- package/README.md +17 -14
- package/npm-shrinkwrap.json +3 -3
- package/package.json +6 -2
- package/prompts/core.md +7 -4
- package/scripts/board-files.mjs +8 -1
- package/scripts/brand-lint.mjs +1 -0
- package/scripts/lib/brand-lint.js +25 -1
- package/scripts/site-roadmap.mjs +40 -0
- package/scripts/tasks/cli.js +14 -1
- package/scripts/tasks/infra-read.js +2 -0
- package/scripts/tasks/infra.js +15 -61
- package/scripts/tasks/pipeline.js +3 -170
- package/scripts/tasks.mjs +23 -4
- package/src/infra-cloudflare-analytics.js +24 -5
- package/src/infra-cloudflare-plan.js +38 -3
- package/src/infra-cloudflare-tokens.js +133 -0
- package/src/infra-cloudflare.js +385 -101
- package/src/infra-desired.js +10 -2
- package/src/infra-health.js +196 -0
- package/src/infra-inventory.js +11 -8
- package/src/infra-plans.js +3 -2
- package/src/infra-policy.js +198 -92
- package/src/infra-provider.js +217 -3
- package/src/infra-runner-render.js +172 -0
- package/src/init.js +1 -1
- package/src/workflow-render.js +180 -0
- package/template/README.md +1 -1
- package/template/infra/apply.yml +4 -0
package/src/infra-policy.js
CHANGED
|
@@ -14,10 +14,15 @@
|
|
|
14
14
|
* A file is:
|
|
15
15
|
*
|
|
16
16
|
* { "version": 1, "costLimit": 5, "budget": 20,
|
|
17
|
-
* "environments": { "production": { "costLimit": 50, "budget": 200 } },
|
|
17
|
+
* "environments": { "production": { "costLimit": 50, "budget": 200, "allow": [] } },
|
|
18
18
|
* "access": { "kinds": ["route"], "settings": ["public"] },
|
|
19
19
|
* "allow": [ { "name": "small staging changes", "environments": ["staging"], "changes": ["update", "scale"] } ] }
|
|
20
20
|
*
|
|
21
|
+
* Two levels (WEB-123): the top of the file is the repository's rules, and `environments` overrides them for one
|
|
22
|
+
* environment by name. An environment's `costLimit` and `budget` replace the repository's, its `access` adds kinds and
|
|
23
|
+
* settings to the repository's (it can't take one away), and its `allow`, when it has one, replaces the repository's
|
|
24
|
+
* rules for that environment: `"allow": []` makes every plan there wait for the owner whatever the repository allows.
|
|
25
|
+
*
|
|
21
26
|
* Limits are a month, in the board's currency (BRK-226): the owner's, set in Settings, which the plan's cost change is
|
|
22
27
|
* converted into before it's checked (infra-currency.js). Pure and Node-safe, so `npx
|
|
23
28
|
* breakaway infra check` (CLI-14) can use it: no store and no network.
|
|
@@ -75,12 +80,22 @@ export const DEFAULT_POLICY = Object.freeze({
|
|
|
75
80
|
* @property {number} [maxChanges] the most changes a plan it lets through has
|
|
76
81
|
*/
|
|
77
82
|
|
|
83
|
+
/**
|
|
84
|
+
* One environment's rules (WEB-123): limits that replace the repository's, access kinds and settings added to the
|
|
85
|
+
* repository's, and allow rules that replace the repository's for it when present.
|
|
86
|
+
* @typedef {object} EnvironmentRules
|
|
87
|
+
* @property {number} [costLimit]
|
|
88
|
+
* @property {number} [budget]
|
|
89
|
+
* @property {{ kinds: string[], settings: string[] }} [access]
|
|
90
|
+
* @property {AllowRule[]} [allow]
|
|
91
|
+
*/
|
|
92
|
+
|
|
78
93
|
/**
|
|
79
94
|
* @typedef {object} Policy
|
|
80
95
|
* @property {number} version
|
|
81
96
|
* @property {number} costLimit the most one plan may add to an environment's monthly cost before it waits
|
|
82
97
|
* @property {number} budget what one environment may cost a month
|
|
83
|
-
* @property {Record<string,
|
|
98
|
+
* @property {Record<string, EnvironmentRules>} environments one environment's rules by name, over the repository's
|
|
84
99
|
* @property {{ kinds: string[], settings: string[] }} access resource kinds and settings that decide who or what can
|
|
85
100
|
* reach something, on top of the ones the provider declares
|
|
86
101
|
* @property {AllowRule[]} allow
|
|
@@ -90,6 +105,9 @@ const TOP = ['version', 'costLimit', 'budget', 'environments', 'access', 'allow'
|
|
|
90
105
|
const LIMITS = ['costLimit', 'budget'];
|
|
91
106
|
const ACCESS = ['kinds', 'settings'];
|
|
92
107
|
const RULE = ['name', 'environments', 'environmentKinds', 'changes', 'kinds', 'maxChanges'];
|
|
108
|
+
/** An environment's own rule names no environments: it's already that environment's. */
|
|
109
|
+
const ENV_RULE = ['name', 'changes', 'kinds', 'maxChanges'];
|
|
110
|
+
const ENV_KEYS = [...LIMITS, 'access', 'allow'];
|
|
93
111
|
const ENV_NAME = /^[a-z0-9][a-z0-9-]{0,39}$/u;
|
|
94
112
|
const KIND = /^[a-z][a-z0-9-]{0,39}$/u;
|
|
95
113
|
const SETTING = /^[A-Za-z_][\w.-]{0,79}$/u;
|
|
@@ -153,8 +171,9 @@ export function checkPolicyFile(source) {
|
|
|
153
171
|
|
|
154
172
|
/** @param {unknown} value @param {string} field @param {RegExp} shape @param {string} what */
|
|
155
173
|
const names = (value, field, shape, what) => {
|
|
156
|
-
if (
|
|
157
|
-
return wrong(field, `${field.split('.').pop()} is
|
|
174
|
+
if (Array.isArray(value) && value.length === 0)
|
|
175
|
+
return wrong(field, `${field.split('.').pop()} is empty: leave it out, or list ${what}`);
|
|
176
|
+
if (!Array.isArray(value)) return wrong(field, `${field.split('.').pop()} is a list of ${what}`);
|
|
158
177
|
if (value.length > LIST_MAX)
|
|
159
178
|
return wrong(field, `${field.split('.').pop()} has ${value.length}: at most ${LIST_MAX}`);
|
|
160
179
|
for (let n = 0; n < value.length; n += 1)
|
|
@@ -162,125 +181,176 @@ export function checkPolicyFile(source) {
|
|
|
162
181
|
return null;
|
|
163
182
|
};
|
|
164
183
|
|
|
165
|
-
/**
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return wrong(at, `${name} isn’t an environment’s name: lowercase letters, digits, and -, like staging`);
|
|
177
|
-
if (!isObject(limits)) return wrong(at, 'each environment has a costLimit, a budget, or both');
|
|
178
|
-
for (const key of Object.keys(limits))
|
|
179
|
-
if (!LIMITS.includes(key))
|
|
180
|
-
return wrong(`${at}.${key}`, `“${key}” isn’t an environment’s limit: it has costLimit and budget`);
|
|
181
|
-
for (const key of LIMITS)
|
|
182
|
-
if (limits[key] !== undefined) {
|
|
183
|
-
const bad = limit(limits[key], `${at}.${key}`);
|
|
184
|
-
if (bad) return bad;
|
|
185
|
-
}
|
|
186
|
-
environments[name] = { ...limits };
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const access = { kinds: /** @type {string[]} */ ([]), settings: /** @type {string[]} */ ([]) };
|
|
191
|
-
if (file.access !== undefined) {
|
|
192
|
-
if (!isObject(file.access))
|
|
193
|
-
return wrong(
|
|
194
|
-
'access',
|
|
195
|
-
'access names more resource kinds and settings that decide who or what can reach something',
|
|
196
|
-
);
|
|
197
|
-
for (const key of Object.keys(file.access))
|
|
184
|
+
/**
|
|
185
|
+
* An `access` object, at the top or for one environment.
|
|
186
|
+
* @param {any} value @param {string} at
|
|
187
|
+
*/
|
|
188
|
+
const accessOf = (value, at) => {
|
|
189
|
+
const out = { kinds: /** @type {string[]} */ ([]), settings: /** @type {string[]} */ ([]) };
|
|
190
|
+
if (!isObject(value))
|
|
191
|
+
return {
|
|
192
|
+
bad: wrong(at, 'access names more resource kinds and settings that decide who or what can reach something'),
|
|
193
|
+
};
|
|
194
|
+
for (const key of Object.keys(value))
|
|
198
195
|
if (!ACCESS.includes(key))
|
|
199
|
-
return wrong(
|
|
200
|
-
if (
|
|
201
|
-
const bad = names(
|
|
202
|
-
if (bad) return bad;
|
|
203
|
-
|
|
196
|
+
return { bad: wrong(`${at}.${key}`, `“${key}” isn’t part of access: it has kinds and settings`) };
|
|
197
|
+
if (value.kinds !== undefined) {
|
|
198
|
+
const bad = names(value.kinds, `${at}.kinds`, KIND, 'a resource kind, lowercase, like route');
|
|
199
|
+
if (bad) return { bad };
|
|
200
|
+
out.kinds = [...value.kinds];
|
|
204
201
|
}
|
|
205
|
-
if (
|
|
206
|
-
const bad = names(
|
|
207
|
-
if (bad) return bad;
|
|
208
|
-
|
|
202
|
+
if (value.settings !== undefined) {
|
|
203
|
+
const bad = names(value.settings, `${at}.settings`, SETTING, 'a setting’s name, like public');
|
|
204
|
+
if (bad) return { bad };
|
|
205
|
+
out.settings = [...value.settings];
|
|
209
206
|
}
|
|
210
|
-
|
|
207
|
+
return { access: out };
|
|
208
|
+
};
|
|
211
209
|
|
|
212
|
-
/**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
210
|
+
/**
|
|
211
|
+
* An `allow` list, at the top or for one environment, whose rules may not name environments.
|
|
212
|
+
* @param {unknown} list @param {string} at @param {string[]} keys
|
|
213
|
+
*/
|
|
214
|
+
const allowOf = (list, at, keys) => {
|
|
215
|
+
/** @type {AllowRule[]} */
|
|
216
|
+
const allow = [];
|
|
217
|
+
if (!Array.isArray(list))
|
|
218
|
+
return { bad: wrong(at, 'allow is a list of rules that let a kind of plan through: [ { "name": …, … } ]') };
|
|
219
|
+
if (list.length > POLICY_MAX_RULES)
|
|
220
|
+
return { bad: wrong(at, `allow has ${list.length} rules: at most ${POLICY_MAX_RULES}`) };
|
|
219
221
|
const seen = new Map();
|
|
220
|
-
for (let n = 0; n <
|
|
221
|
-
const
|
|
222
|
-
const r =
|
|
223
|
-
if (!isObject(r)) return wrong(
|
|
222
|
+
for (let n = 0; n < list.length; n += 1) {
|
|
223
|
+
const here = `${at}[${n}]`;
|
|
224
|
+
const r = list[n];
|
|
225
|
+
if (!isObject(r)) return { bad: wrong(here, 'each rule is an object with a name, and what it covers') };
|
|
224
226
|
for (const key of Object.keys(r))
|
|
225
|
-
if (!
|
|
226
|
-
return
|
|
227
|
+
if (!keys.includes(key))
|
|
228
|
+
return {
|
|
229
|
+
bad: wrong(
|
|
230
|
+
`${here}.${key}`,
|
|
231
|
+
RULE.includes(key)
|
|
232
|
+
? `“${key}” isn’t part of an environment’s own rule: it’s already that environment’s`
|
|
233
|
+
: `“${key}” isn’t part of a rule: it has ${keys.join(', ')}`,
|
|
234
|
+
),
|
|
235
|
+
};
|
|
227
236
|
if (typeof r.name !== 'string' || r.name.trim() === '' || r.name.length > RULE_NAME_MAX)
|
|
228
|
-
return
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
237
|
+
return {
|
|
238
|
+
bad: wrong(
|
|
239
|
+
r.name === undefined ? here : `${here}.name`,
|
|
240
|
+
`name says what the rule lets through, up to ${RULE_NAME_MAX} characters, like small staging changes`,
|
|
241
|
+
),
|
|
242
|
+
};
|
|
232
243
|
const name = r.name.trim();
|
|
233
244
|
if (seen.has(name))
|
|
234
|
-
return wrong(`${
|
|
245
|
+
return { bad: wrong(`${here}.name`, `${name} is the name of ${at}[${seen.get(name)}] too: give each its own`) };
|
|
235
246
|
seen.set(name, n);
|
|
236
247
|
if (GUARDS.includes(name) || name === EVERY)
|
|
237
|
-
return wrong(`${
|
|
248
|
+
return { bad: wrong(`${here}.name`, `${name} is the name of one of the board’s own rules: pick another`) };
|
|
238
249
|
/** @type {AllowRule} */
|
|
239
250
|
const rule = { name };
|
|
240
251
|
if (r.environments !== undefined) {
|
|
241
|
-
const bad = names(r.environments, `${
|
|
242
|
-
if (bad) return bad;
|
|
252
|
+
const bad = names(r.environments, `${here}.environments`, ENV_NAME, 'an environment’s name, like staging');
|
|
253
|
+
if (bad) return { bad };
|
|
243
254
|
rule.environments = [...r.environments];
|
|
244
255
|
}
|
|
245
256
|
if (r.environmentKinds !== undefined) {
|
|
246
257
|
const bad = names(
|
|
247
258
|
r.environmentKinds,
|
|
248
|
-
`${
|
|
259
|
+
`${here}.environmentKinds`,
|
|
249
260
|
KIND,
|
|
250
261
|
`a kind of environment: ${ENVIRONMENT_KINDS.join(', ')}`,
|
|
251
262
|
);
|
|
252
|
-
if (bad) return bad;
|
|
263
|
+
if (bad) return { bad };
|
|
253
264
|
const unknown = r.environmentKinds.findIndex((k) => !ENVIRONMENT_KINDS.includes(k));
|
|
254
265
|
if (unknown >= 0)
|
|
255
|
-
return
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
266
|
+
return {
|
|
267
|
+
bad: wrong(
|
|
268
|
+
`${here}.environmentKinds[${unknown}]`,
|
|
269
|
+
`${r.environmentKinds[unknown]} isn’t a kind of environment: ${ENVIRONMENT_KINDS.join(', ')}`,
|
|
270
|
+
),
|
|
271
|
+
};
|
|
259
272
|
rule.environmentKinds = [...r.environmentKinds];
|
|
260
273
|
}
|
|
261
274
|
if (r.changes !== undefined) {
|
|
262
|
-
const bad = names(r.changes, `${
|
|
263
|
-
if (bad) return bad;
|
|
275
|
+
const bad = names(r.changes, `${here}.changes`, KIND, `a change kind: ${CHANGE_KINDS.join(', ')}`);
|
|
276
|
+
if (bad) return { bad };
|
|
264
277
|
const unknown = r.changes.findIndex((c) => !CHANGE_KINDS.includes(c));
|
|
265
278
|
if (unknown >= 0)
|
|
266
|
-
return
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
279
|
+
return {
|
|
280
|
+
bad: wrong(
|
|
281
|
+
`${here}.changes[${unknown}]`,
|
|
282
|
+
`${r.changes[unknown]} isn’t a change kind: ${CHANGE_KINDS.join(', ')}`,
|
|
283
|
+
),
|
|
284
|
+
};
|
|
270
285
|
rule.changes = [...r.changes];
|
|
271
286
|
}
|
|
272
287
|
if (r.kinds !== undefined) {
|
|
273
|
-
const bad = names(r.kinds, `${
|
|
274
|
-
if (bad) return bad;
|
|
288
|
+
const bad = names(r.kinds, `${here}.kinds`, KIND, 'a resource kind, lowercase, like service');
|
|
289
|
+
if (bad) return { bad };
|
|
275
290
|
rule.kinds = [...r.kinds];
|
|
276
291
|
}
|
|
277
292
|
if (r.maxChanges !== undefined) {
|
|
278
293
|
if (!Number.isInteger(r.maxChanges) || r.maxChanges < 1 || r.maxChanges > 1000)
|
|
279
|
-
return wrong(`${
|
|
294
|
+
return { bad: wrong(`${here}.maxChanges`, 'maxChanges is a whole number from 1 to 1000') };
|
|
280
295
|
rule.maxChanges = r.maxChanges;
|
|
281
296
|
}
|
|
282
297
|
allow.push(rule);
|
|
283
298
|
}
|
|
299
|
+
return { allow };
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
/** @type {Policy['environments']} */
|
|
303
|
+
const environments = {};
|
|
304
|
+
if (file.environments !== undefined) {
|
|
305
|
+
if (!isObject(file.environments))
|
|
306
|
+
return wrong(
|
|
307
|
+
'environments',
|
|
308
|
+
'environments is an object of rules by environment: { "production": { "budget": 200 } }',
|
|
309
|
+
);
|
|
310
|
+
for (const [name, rules] of Object.entries(file.environments)) {
|
|
311
|
+
const at = `environments.${name}`;
|
|
312
|
+
if (!ENV_NAME.test(name))
|
|
313
|
+
return wrong(at, `${name} isn’t an environment’s name: lowercase letters, digits, and -, like staging`);
|
|
314
|
+
if (!isObject(rules))
|
|
315
|
+
return wrong(at, 'each environment has a costLimit, a budget, access, allow, or some of them');
|
|
316
|
+
for (const key of Object.keys(rules))
|
|
317
|
+
if (!ENV_KEYS.includes(key))
|
|
318
|
+
return wrong(`${at}.${key}`, `“${key}” isn’t an environment’s rule: it has ${ENV_KEYS.join(', ')}`);
|
|
319
|
+
/** @type {EnvironmentRules} */
|
|
320
|
+
const own = {};
|
|
321
|
+
for (const key of LIMITS)
|
|
322
|
+
if (rules[key] !== undefined) {
|
|
323
|
+
const bad = limit(rules[key], `${at}.${key}`);
|
|
324
|
+
if (bad) return bad;
|
|
325
|
+
own[key] = rules[key];
|
|
326
|
+
}
|
|
327
|
+
if (rules.access !== undefined) {
|
|
328
|
+
const got = accessOf(rules.access, `${at}.access`);
|
|
329
|
+
if (got.bad) return got.bad;
|
|
330
|
+
own.access = got.access;
|
|
331
|
+
}
|
|
332
|
+
if (rules.allow !== undefined) {
|
|
333
|
+
const got = allowOf(rules.allow, `${at}.allow`, ENV_RULE);
|
|
334
|
+
if (got.bad) return got.bad;
|
|
335
|
+
own.allow = got.allow;
|
|
336
|
+
}
|
|
337
|
+
environments[name] = own;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let access = { kinds: /** @type {string[]} */ ([]), settings: /** @type {string[]} */ ([]) };
|
|
342
|
+
if (file.access !== undefined) {
|
|
343
|
+
const got = accessOf(file.access, 'access');
|
|
344
|
+
if (got.bad) return got.bad;
|
|
345
|
+
access = got.access;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** @type {AllowRule[]} */
|
|
349
|
+
let allow = [];
|
|
350
|
+
if (file.allow !== undefined) {
|
|
351
|
+
const got = allowOf(file.allow, 'allow', RULE);
|
|
352
|
+
if (got.bad) return got.bad;
|
|
353
|
+
allow = got.allow;
|
|
284
354
|
}
|
|
285
355
|
|
|
286
356
|
return {
|
|
@@ -302,6 +372,31 @@ export function limitsFor(policy, environment) {
|
|
|
302
372
|
return { costLimit: own.costLimit ?? policy.costLimit, budget: own.budget ?? policy.budget };
|
|
303
373
|
}
|
|
304
374
|
|
|
375
|
+
/**
|
|
376
|
+
* The allow rules that decide for one environment (WEB-123): its own when it has an `allow`, else the repository's.
|
|
377
|
+
* @param {Policy} policy
|
|
378
|
+
* @param {string} environment
|
|
379
|
+
* @returns {{ allow: AllowRule[], level: 'environment' | 'repository' }}
|
|
380
|
+
*/
|
|
381
|
+
export function allowFor(policy, environment) {
|
|
382
|
+
const own = policy.environments?.[environment]?.allow;
|
|
383
|
+
return own ? { allow: own, level: 'environment' } : { allow: policy.allow ?? [], level: 'repository' };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* The access kinds and settings a policy names for one environment: the repository's, with the environment's added.
|
|
388
|
+
* @param {Policy} policy
|
|
389
|
+
* @param {string} environment
|
|
390
|
+
*/
|
|
391
|
+
export function accessFor(policy, environment) {
|
|
392
|
+
const own = policy.environments?.[environment]?.access;
|
|
393
|
+
const union = (a = [], b = []) => [...new Set([...a, ...b])];
|
|
394
|
+
return {
|
|
395
|
+
kinds: union(policy.access?.kinds, own?.kinds),
|
|
396
|
+
settings: union(policy.access?.settings, own?.settings),
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
305
400
|
/**
|
|
306
401
|
* An amount in words, in the plan's currency when it has one: "$6", "€4.60".
|
|
307
402
|
* @param {number} amount
|
|
@@ -343,16 +438,17 @@ function accessChange(change, access) {
|
|
|
343
438
|
return keys.length ? keys : null;
|
|
344
439
|
}
|
|
345
440
|
|
|
346
|
-
/** The provider's access kinds and settings, with the policy's added. */
|
|
347
|
-
function accessOf(policy, provider) {
|
|
348
|
-
const
|
|
441
|
+
/** The provider's access kinds and settings, with the policy's for the environment added. */
|
|
442
|
+
function accessOf(policy, provider, environment) {
|
|
443
|
+
const named = accessFor(policy, environment);
|
|
444
|
+
const kinds = new Set(named.kinds);
|
|
349
445
|
/** @type {Map<string, Set<string>>} */
|
|
350
446
|
const settings = new Map();
|
|
351
447
|
for (const [kind, spec] of Object.entries(provider?.kinds ?? {})) {
|
|
352
448
|
if (spec?.access) kinds.add(kind);
|
|
353
449
|
if (Array.isArray(spec?.accessSettings)) settings.set(kind, new Set(spec.accessSettings));
|
|
354
450
|
}
|
|
355
|
-
return { kinds, settings, anyKind: new Set(
|
|
451
|
+
return { kinds, settings, anyKind: new Set(named.settings) };
|
|
356
452
|
}
|
|
357
453
|
|
|
358
454
|
/** A change in a few words, for a reason: "deletes the `main` database". */
|
|
@@ -438,7 +534,7 @@ export function evaluatePolicy(
|
|
|
438
534
|
destroys.length ? `Can’t be undone: it ${andMore(undo)}.` : 'Every change can be undone, and nothing is deleted.',
|
|
439
535
|
);
|
|
440
536
|
|
|
441
|
-
const access = accessOf(policy, provider);
|
|
537
|
+
const access = accessOf(policy, provider, env);
|
|
442
538
|
const reaching = diff.changes.map((c) => ({ c, keys: accessChange(c, access) })).filter((x) => x.keys !== null);
|
|
443
539
|
add(
|
|
444
540
|
'access',
|
|
@@ -491,11 +587,19 @@ export function evaluatePolicy(
|
|
|
491
587
|
outcome = 'needs-owner';
|
|
492
588
|
rule = asked[0].rule;
|
|
493
589
|
} else {
|
|
494
|
-
const
|
|
590
|
+
const { allow, level } = allowFor(policy, env);
|
|
591
|
+
const allowed = allow.find((a) => allows(a, env, diff, environment.kind));
|
|
495
592
|
if (allowed) {
|
|
496
593
|
outcome = 'allowed';
|
|
497
594
|
rule = allowed.name;
|
|
498
|
-
add(
|
|
595
|
+
add(
|
|
596
|
+
allowed.name,
|
|
597
|
+
true,
|
|
598
|
+
'allow',
|
|
599
|
+
level === 'environment'
|
|
600
|
+
? `Allowed by ${env}’s own rule “${allowed.name}” in your policy.`
|
|
601
|
+
: `Allowed by your policy’s rule “${allowed.name}”.`,
|
|
602
|
+
);
|
|
499
603
|
} else {
|
|
500
604
|
outcome = 'needs-owner';
|
|
501
605
|
rule = EVERY;
|
|
@@ -503,9 +607,11 @@ export function evaluatePolicy(
|
|
|
503
607
|
EVERY,
|
|
504
608
|
true,
|
|
505
609
|
'ask',
|
|
506
|
-
|
|
610
|
+
allow.length
|
|
507
611
|
? 'No rule in your policy lets it through, so it needs you.'
|
|
508
|
-
:
|
|
612
|
+
: level === 'environment'
|
|
613
|
+
? `Every plan in ${env} needs you: your policy lets nothing through there.`
|
|
614
|
+
: 'Every plan needs you: the default policy lets nothing through.',
|
|
509
615
|
);
|
|
510
616
|
}
|
|
511
617
|
}
|