breakaway 2.0.0-main.70 → 2.0.0-main.72
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/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/src/infra-cloudflare-plan.js +53 -6
- package/src/infra-cloudflare.js +105 -4
- package/src/infra-desired.js +14 -4
- package/src/infra-provider.js +8 -1
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakaway",
|
|
3
|
-
"version": "2.0.0-main.
|
|
3
|
+
"version": "2.0.0-main.72",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "breakaway",
|
|
9
|
-
"version": "2.0.0-main.
|
|
9
|
+
"version": "2.0.0-main.72",
|
|
10
10
|
"license": "FSL-1.1-Apache-2.0",
|
|
11
11
|
"bin": {
|
|
12
12
|
"breakaway": "scripts/tasks.mjs"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "breakaway",
|
|
3
|
-
"version": "2.0.0-main.
|
|
3
|
+
"version": "2.0.0-main.72",
|
|
4
4
|
"description": "The task board for you and your coding agents: a Cloudflare Worker, its web app, Taskwarrior sync, and the CLI (npx breakaway).",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
* Only Workers can be scoped one by one (Workers Editor, BRK-243); a database, namespace, bucket, or queue permission
|
|
13
13
|
* reaches every one on the account. So the provider keeps every change inside the environment's scope itself: it
|
|
14
14
|
* refuses to make a resource that already exists on the account outside the scope, a database, namespace, bucket, or
|
|
15
|
-
* queue no Worker in the desired state binds, and a delete of something a remaining Worker still binds.
|
|
15
|
+
* queue no Worker in the desired state binds, and a delete of something a remaining Worker still binds. Discovery
|
|
16
|
+
* follows bindings to other Workers, so the plan also refuses a change to the install's own Worker (`scope.board`), to
|
|
17
|
+
* another environment's target (`scope.others`), or to anything reached only through one of them, never deletes them,
|
|
18
|
+
* and sends a route or custom domain only to the environment's own Workers, on a pattern or hostname nobody else has
|
|
19
|
+
* (BRK-251).
|
|
16
20
|
*
|
|
17
21
|
* `apply` runs only inside the apply runner (CLI-12), with the environment's write token. It discovers again first
|
|
18
22
|
* and refuses a change whose resource moved since it was planned, then calls Cloudflare one change at a time, in the
|
|
@@ -33,6 +37,7 @@ import {
|
|
|
33
37
|
cloudflare,
|
|
34
38
|
cost,
|
|
35
39
|
discover,
|
|
40
|
+
ownership,
|
|
36
41
|
priceResource,
|
|
37
42
|
reader,
|
|
38
43
|
refuses,
|
|
@@ -263,7 +268,14 @@ export async function plan(ctx, desired) {
|
|
|
263
268
|
const live = /** @type {LiveAccount} */ (found.live);
|
|
264
269
|
if (!live.account || !live.scripts.includes(String(ctx.scope?.target ?? '')))
|
|
265
270
|
refuse(`${ctx.scope?.target ?? 'the target'} isn’t a Worker on the account yet: deploy it first, then plan`);
|
|
271
|
+
if (ctx.scope?.board && ctx.scope.board === ctx.scope.target)
|
|
272
|
+
refuse(
|
|
273
|
+
`${ctx.scope.target} is the Worker this board runs on: Architect only observes it, so it never plans for it`,
|
|
274
|
+
);
|
|
266
275
|
const have = viewOf(found);
|
|
276
|
+
const own = ownership(found, ctx.scope);
|
|
277
|
+
/** Why the environment can't change a resource it found, or null when it's its own (BRK-251). */
|
|
278
|
+
const notOurs = (h) => own.why(ctx.environment, h);
|
|
267
279
|
const cf = reader(ctx);
|
|
268
280
|
|
|
269
281
|
// Match each desired resource to what's there: by ID, else by kind and name.
|
|
@@ -318,6 +330,20 @@ export async function plan(ctx, desired) {
|
|
|
318
330
|
return boundAfter[type].has(r.kind === 'r2' || r.kind === 'queue' ? r.name : cfId(r.id));
|
|
319
331
|
};
|
|
320
332
|
|
|
333
|
+
/** Whether a route or custom domain may send to this Worker: one of the environment's, or one this plan makes. */
|
|
334
|
+
const mine = (worker) => own.workers.has(worker) || desiredWorkers.some((w) => w.name === worker && !match.has(w.id));
|
|
335
|
+
/** Refuses a route pattern or custom domain hostname another Worker already has: it's that Worker's traffic. */
|
|
336
|
+
const inUse = (r) => {
|
|
337
|
+
const held =
|
|
338
|
+
r.kind === 'route'
|
|
339
|
+
? live.routes.find((x) => x.pattern === r.name)
|
|
340
|
+
: live.domains.find((x) => x.hostname === r.name);
|
|
341
|
+
if (held)
|
|
342
|
+
refuse(
|
|
343
|
+
`${r.name} already sends to ${held.worker}: a ${r.kind} can’t take a ${r.kind === 'route' ? 'pattern' : 'hostname'} that’s already in use, so remove it there first or pick another`,
|
|
344
|
+
);
|
|
345
|
+
};
|
|
346
|
+
|
|
321
347
|
/** @type {Change[]} */
|
|
322
348
|
const changes = [];
|
|
323
349
|
for (const r of desired.resources) {
|
|
@@ -325,7 +351,7 @@ export async function plan(ctx, desired) {
|
|
|
325
351
|
const managed = MANAGED[r.kind] ?? [];
|
|
326
352
|
const wanted = isObject(r.attrs) ? r.attrs : {};
|
|
327
353
|
if (!h) {
|
|
328
|
-
changes.push(await created(ctx, cf, live, r, wanted, boundAfter));
|
|
354
|
+
changes.push(await created(ctx, cf, live, r, wanted, boundAfter, { mine, inUse }));
|
|
329
355
|
continue;
|
|
330
356
|
}
|
|
331
357
|
/** @type {Record<string, unknown>} */
|
|
@@ -363,8 +389,14 @@ export async function plan(ctx, desired) {
|
|
|
363
389
|
changed = true;
|
|
364
390
|
}
|
|
365
391
|
}
|
|
366
|
-
if (
|
|
367
|
-
|
|
392
|
+
if (changed && notOurs(h)) refuse(/** @type {string} */ (notOurs(h)));
|
|
393
|
+
if ((r.kind === 'route' || r.kind === 'custom-domain') && changed) {
|
|
394
|
+
if (!live.scripts.includes(String(after.worker)))
|
|
395
|
+
refuse(`${r.kind} ${r.name} would send to ${after.worker}, which isn’t a Worker on the account`);
|
|
396
|
+
if (!mine(String(after.worker)))
|
|
397
|
+
refuse(`${r.kind} ${r.name} would send to ${after.worker}, which isn’t one of ${ctx.environment}’s Workers`);
|
|
398
|
+
if (r.name !== h.name) inUse(r);
|
|
399
|
+
}
|
|
368
400
|
if (changed)
|
|
369
401
|
changes.push({
|
|
370
402
|
op: 'update',
|
|
@@ -378,6 +410,8 @@ export async function plan(ctx, desired) {
|
|
|
378
410
|
}
|
|
379
411
|
for (const h of have.values()) {
|
|
380
412
|
if (taken.has(h.id)) continue;
|
|
413
|
+
// What only the install's Worker or another environment reaches isn't this environment's to delete (BRK-251).
|
|
414
|
+
if (notOurs(h)) continue;
|
|
381
415
|
if (h.kind === 'durable-object')
|
|
382
416
|
refuse(
|
|
383
417
|
`${h.name} isn’t in the desired state: a Durable Object class is deleted by a migration in its Worker’s code, so keep it listed and remove it in the code`,
|
|
@@ -388,6 +422,13 @@ export async function plan(ctx, desired) {
|
|
|
388
422
|
refuse(`${h.name} is still bound by a Worker: remove the binding in the desired state before you delete it`);
|
|
389
423
|
if (h.kind === 'worker' && boundAfter.service.has(h.name))
|
|
390
424
|
refuse(`${h.name} is still called by a Worker’s service binding: remove the binding before you delete it`);
|
|
425
|
+
const outsider = found.relations.find(
|
|
426
|
+
(rel) => rel.to === h.id && rel.from.startsWith('worker:') && !own.workers.has(cfId(rel.from)),
|
|
427
|
+
);
|
|
428
|
+
if (outsider)
|
|
429
|
+
refuse(
|
|
430
|
+
`${h.name} is still used by ${cfId(outsider.from)}, which is outside ${ctx.environment}: Architect never deletes what another Worker relies on`,
|
|
431
|
+
);
|
|
391
432
|
const reversible = !LOST[h.kind];
|
|
392
433
|
changes.push({
|
|
393
434
|
op: 'delete',
|
|
@@ -411,10 +452,11 @@ export async function plan(ctx, desired) {
|
|
|
411
452
|
|
|
412
453
|
/**
|
|
413
454
|
* A resource to make, checked against the scope: nothing of the same name already on the account, and every
|
|
414
|
-
* database, namespace, bucket, and queue bound by a desired Worker, so the next discovery finds it.
|
|
455
|
+
* database, namespace, bucket, and queue bound by a desired Worker, so the next discovery finds it. A route or custom
|
|
456
|
+
* domain sends to one of the environment's Workers, on a pattern or hostname no other Worker has (BRK-251).
|
|
415
457
|
* @returns {Promise<Change>}
|
|
416
458
|
*/
|
|
417
|
-
async function created(ctx, cf, live, r, wanted, boundAfter) {
|
|
459
|
+
async function created(ctx, cf, live, r, wanted, boundAfter, { mine, inUse }) {
|
|
418
460
|
const a = encodeURIComponent(live.account);
|
|
419
461
|
const outside = `${r.name} already exists on the account outside ${ctx.environment}’s scope: Architect never takes over a resource another environment may run on`;
|
|
420
462
|
/** @type {Record<string, unknown>} */
|
|
@@ -435,6 +477,11 @@ async function created(ctx, cf, live, r, wanted, boundAfter) {
|
|
|
435
477
|
refuse(
|
|
436
478
|
`${r.kind} ${r.name} needs attrs.zone, one of the zones the token reaches (${Object.keys(live.zones).sort().join(', ') || 'none'})`,
|
|
437
479
|
);
|
|
480
|
+
if (!mine(wanted.worker))
|
|
481
|
+
refuse(
|
|
482
|
+
`${r.kind} ${r.name} would send to ${wanted.worker}, which isn’t one of ${ctx.environment}’s Workers: send it to the target or a Worker it calls`,
|
|
483
|
+
);
|
|
484
|
+
inUse(r);
|
|
438
485
|
after.zone = wanted.zone;
|
|
439
486
|
} else {
|
|
440
487
|
const type = Object.keys(BOUND_KIND).find((t) => BOUND_KIND[t][0] === r.kind);
|
package/src/infra-cloudflare.js
CHANGED
|
@@ -57,12 +57,21 @@ const ENVELOPE = {
|
|
|
57
57
|
container: { changes: ['scale', 'restart'], scales: 'maxInstances' },
|
|
58
58
|
queue: { changes: ['scale'], scales: 'maxConcurrency' },
|
|
59
59
|
};
|
|
60
|
+
/**
|
|
61
|
+
* The kinds that decide what reaches a Worker: a route or a custom domain can send a hostname somewhere else, so every
|
|
62
|
+
* change to one asks the owner, whatever a repository's allow rules say (the policy's access guard; BRK-229).
|
|
63
|
+
*/
|
|
64
|
+
const ACCESS = ['route', 'custom-domain'];
|
|
65
|
+
|
|
60
66
|
export const CLOUDFLARE_KINDS = Object.fromEntries(
|
|
61
67
|
Object.entries(MANAGED).map(([k, settings]) => [
|
|
62
68
|
k,
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
{
|
|
70
|
+
...(ENVELOPE[k]
|
|
71
|
+
? { changes: [...BASE, ...ENVELOPE[k].changes], scales: ENVELOPE[k].scales, settings: [...settings] }
|
|
72
|
+
: { changes: [...BASE], settings: [...settings] }),
|
|
73
|
+
...(ACCESS.includes(k) ? { access: true } : {}),
|
|
74
|
+
},
|
|
66
75
|
]),
|
|
67
76
|
);
|
|
68
77
|
|
|
@@ -93,6 +102,92 @@ export function refuses(r, op) {
|
|
|
93
102
|
return null;
|
|
94
103
|
}
|
|
95
104
|
|
|
105
|
+
/**
|
|
106
|
+
* What an environment owns of what discovery found (BRK-251). Discovery follows the target's service and Durable
|
|
107
|
+
* Object bindings to other Workers, so it can reach the install's own Worker (`scope.board`) or another environment's
|
|
108
|
+
* target (`scope.others`): those are foreign, and so is everything reached only through them. The environment's own
|
|
109
|
+
* Workers are the target and the Workers it reaches without passing through a foreign one. A database, namespace,
|
|
110
|
+
* bucket, or queue is its own when one of its Workers binds, sends to, or consumes it; a Durable Object namespace when
|
|
111
|
+
* its class runs in one of its Workers, and a container application when its namespace is its own; a route or custom
|
|
112
|
+
* domain when it serves one of its Workers.
|
|
113
|
+
* @param {import('./infra-provider.js').Discovery} found
|
|
114
|
+
* @param {Record<string, unknown> | undefined} scope
|
|
115
|
+
*/
|
|
116
|
+
export function ownership(found, scope) {
|
|
117
|
+
const target = String(scope?.target ?? '');
|
|
118
|
+
const board = typeof scope?.board === 'string' && scope.board !== target ? scope.board : null;
|
|
119
|
+
const others = new Set(
|
|
120
|
+
(Array.isArray(scope?.others) ? scope.others : []).filter((w) => typeof w === 'string' && w && w !== target),
|
|
121
|
+
);
|
|
122
|
+
const foreign = (name) => name === board || others.has(name);
|
|
123
|
+
const byId = new Map(found.resources.map((r) => [r.id, r]));
|
|
124
|
+
const name = (id) => id.slice(id.indexOf(':') + 1);
|
|
125
|
+
const from = new Map();
|
|
126
|
+
for (const rel of found.relations) from.set(rel.from, [...(from.get(rel.from) ?? []), rel]);
|
|
127
|
+
/** @type {Set<string>} */
|
|
128
|
+
const workers = new Set();
|
|
129
|
+
const queue = byId.has(rid('worker', target)) ? [target] : [];
|
|
130
|
+
while (queue.length) {
|
|
131
|
+
const w = /** @type {string} */ (queue.shift());
|
|
132
|
+
if (workers.has(w) || foreign(w)) continue;
|
|
133
|
+
workers.add(w);
|
|
134
|
+
for (const rel of from.get(rid('worker', w)) ?? []) {
|
|
135
|
+
if (rel.kind === 'calls') queue.push(name(rel.to));
|
|
136
|
+
if (rel.kind === 'uses' && rel.to.startsWith('durable-object:'))
|
|
137
|
+
for (const runs of from.get(rel.to) ?? []) if (runs.kind === 'runs-in') queue.push(name(runs.to));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** @type {Set<string>} */
|
|
141
|
+
const own = new Set([...workers].map((w) => rid('worker', w)));
|
|
142
|
+
for (const rel of found.relations) {
|
|
143
|
+
const r = byId.get(rel.to);
|
|
144
|
+
if (!r || !own.has(rel.from) || r.kind === 'durable-object' || r.kind === 'worker') continue;
|
|
145
|
+
own.add(r.id);
|
|
146
|
+
}
|
|
147
|
+
for (const rel of found.relations)
|
|
148
|
+
if (rel.kind === 'runs-in' && own.has(rel.to)) {
|
|
149
|
+
own.add(rel.from);
|
|
150
|
+
for (const runs of from.get(rel.from) ?? []) if (runs.kind === 'runs') own.add(runs.to);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
workers,
|
|
154
|
+
/** @param {string} w a Worker's name */
|
|
155
|
+
foreign,
|
|
156
|
+
/**
|
|
157
|
+
* Why the environment can't change this resource, in words, or null when it's its own.
|
|
158
|
+
* @param {string} environment
|
|
159
|
+
* @param {Resource} r
|
|
160
|
+
*/
|
|
161
|
+
why(environment, r) {
|
|
162
|
+
if (r.kind === 'worker' && r.name === board)
|
|
163
|
+
return `${r.name} is the Worker this board runs on: Architect only observes it, so no plan or act changes it`;
|
|
164
|
+
if (r.kind === 'worker' && others.has(r.name))
|
|
165
|
+
return `${r.name} is another environment’s target: change it in that environment, not ${environment}`;
|
|
166
|
+
if (own.has(r.id)) return null;
|
|
167
|
+
return `${r.name} is reached only through a Worker outside ${environment} (the install’s own or another environment’s), so it isn’t ${environment}’s to change`;
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Why an act can't change this resource in the environment (BRK-251): `ownership`'s answer, and for a queue's scale,
|
|
174
|
+
* the Worker consuming it, whose concurrency it sets, must be the environment's too.
|
|
175
|
+
* @param {import('./infra-provider.js').ProviderContext} ctx
|
|
176
|
+
* @param {import('./infra-provider.js').Discovery} found
|
|
177
|
+
* @param {Resource} r
|
|
178
|
+
* @returns {string | null}
|
|
179
|
+
*/
|
|
180
|
+
export function outside(ctx, found, r) {
|
|
181
|
+
const own = ownership(found, ctx.scope);
|
|
182
|
+
const why = own.why(ctx.environment, r);
|
|
183
|
+
if (why) return why;
|
|
184
|
+
const consumers = r.kind === 'queue' ? /** @type {any[]} */ (r.attrs?.consumers ?? []) : [];
|
|
185
|
+
const consumer = consumers.find((c) => c?.type === 'worker');
|
|
186
|
+
if (consumer && !own.workers.has(String(consumer.worker ?? '')))
|
|
187
|
+
return `${r.name} is consumed by ${consumer.worker}, which is outside ${ctx.environment}, so its concurrency isn’t ${ctx.environment}’s to change`;
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
96
191
|
/**
|
|
97
192
|
* Cloudflare's Workers role the board reads Workers with (BRK-243): Metadata Read-Only at the Workers product scope,
|
|
98
193
|
* which reads settings and never a Worker's code. It replaces the legacy Workers Scripts Read, which Cloudflare maps to
|
|
@@ -305,6 +400,9 @@ export function bindingTarget(b) {
|
|
|
305
400
|
* @property {string[]} scripts
|
|
306
401
|
* @property {Record<string, Array<Record<string, string>>>} bindings by Worker
|
|
307
402
|
* @property {Record<string, string>} zones zone IDs by name
|
|
403
|
+
* @property {Array<{ pattern: string, zone: string, worker: string }>} routes every route on those zones, whichever
|
|
404
|
+
* Worker it sends to, so a new one can't take a pattern another Worker already has (BRK-251)
|
|
405
|
+
* @property {Array<{ hostname: string, worker: string }>} domains every custom domain on the account, likewise
|
|
308
406
|
*/
|
|
309
407
|
|
|
310
408
|
/**
|
|
@@ -324,7 +422,7 @@ export async function discover(ctx, { live = false } = {}) {
|
|
|
324
422
|
/** @type {string[]} */
|
|
325
423
|
const missing = [];
|
|
326
424
|
/** @type {LiveAccount} */
|
|
327
|
-
const seen = { account: '', scripts: [], bindings: {}, zones: {} };
|
|
425
|
+
const seen = { account: '', scripts: [], bindings: {}, zones: {}, routes: [], domains: [] };
|
|
328
426
|
const add = (r) => resources.set(r.id, r);
|
|
329
427
|
const relate = (from, to, kind) => relations.set(`${from} ${kind} ${to}`, { from, to, kind });
|
|
330
428
|
const done = () => ({
|
|
@@ -583,6 +681,7 @@ export async function discover(ctx, { live = false } = {}) {
|
|
|
583
681
|
const routes =
|
|
584
682
|
(await cf.get(`/zones/${enc(zone.id)}/workers/routes`, { permission: 'Workers Routes Read' })).result ?? [];
|
|
585
683
|
for (const route of routes) {
|
|
684
|
+
seen.routes.push({ pattern: String(route.pattern), zone: String(zone.name), worker: String(route.script ?? '') });
|
|
586
685
|
if (!workers.has(String(route.script))) continue;
|
|
587
686
|
add({
|
|
588
687
|
id: rid('route', route.id),
|
|
@@ -597,6 +696,7 @@ export async function discover(ctx, { live = false } = {}) {
|
|
|
597
696
|
// Custom domains attached to a Worker in scope.
|
|
598
697
|
const domains = (await cf.get(`/accounts/${a}/workers/domains`, { permission: WORKERS_READ })).result ?? [];
|
|
599
698
|
for (const d of domains) {
|
|
699
|
+
seen.domains.push({ hostname: String(d.hostname), worker: String(d.service ?? '') });
|
|
600
700
|
if (!workers.has(String(d.service))) continue;
|
|
601
701
|
add({
|
|
602
702
|
id: rid('custom-domain', d.id),
|
|
@@ -1198,6 +1298,7 @@ export const cloudflare = {
|
|
|
1198
1298
|
check: checkToken,
|
|
1199
1299
|
},
|
|
1200
1300
|
refuses,
|
|
1301
|
+
outside,
|
|
1201
1302
|
discover,
|
|
1202
1303
|
plan: (ctx, desired) => plan(ctx, desired),
|
|
1203
1304
|
apply: (ctx, p) => apply(ctx, p),
|
package/src/infra-desired.js
CHANGED
|
@@ -29,6 +29,8 @@ export const DESIRED_MAX_BYTES = 256 * 1024;
|
|
|
29
29
|
export const DESIRED_MAX_RESOURCES = 500;
|
|
30
30
|
/** How many files one read takes from the folder: an environment per file, and a repository has at most 50. */
|
|
31
31
|
export const DESIRED_MAX_FILES = 60;
|
|
32
|
+
/** How deep a file may nest: far more than any real one, and well inside the stack. */
|
|
33
|
+
export const MAX_DEPTH = 64;
|
|
32
34
|
|
|
33
35
|
const TOP = ['version', 'provider', 'resources'];
|
|
34
36
|
const RESOURCE = ['id', 'kind', 'name', 'attrs'];
|
|
@@ -115,8 +117,10 @@ export function parseWithLines(text) {
|
|
|
115
117
|
i += match[0].length;
|
|
116
118
|
return JSON.parse(match[0]);
|
|
117
119
|
};
|
|
118
|
-
/** @param {string} path */
|
|
119
|
-
const value = (path) => {
|
|
120
|
+
/** @param {string} path @param {number} depth */
|
|
121
|
+
const value = (path, depth = 0) => {
|
|
122
|
+
// A file nested deeper than any real one would overflow the stack: it's refused with its line (BRK-229).
|
|
123
|
+
if (depth > MAX_DEPTH) fail(`it’s nested more than ${MAX_DEPTH} deep`);
|
|
120
124
|
space();
|
|
121
125
|
lines.set(path, line);
|
|
122
126
|
const c = src[i];
|
|
@@ -137,7 +141,13 @@ export function parseWithLines(text) {
|
|
|
137
141
|
space();
|
|
138
142
|
if (src[i] !== ':') fail(`expected : after “${key}”, and found ${shown()}`);
|
|
139
143
|
i += 1;
|
|
140
|
-
|
|
144
|
+
// Defined, never assigned, so a key like __proto__ is a plain key and never the object's prototype.
|
|
145
|
+
Object.defineProperty(out, key, {
|
|
146
|
+
value: value(path ? `${path}.${key}` : key, depth + 1),
|
|
147
|
+
enumerable: true,
|
|
148
|
+
writable: true,
|
|
149
|
+
configurable: true,
|
|
150
|
+
});
|
|
141
151
|
space();
|
|
142
152
|
if (src[i] === ',') {
|
|
143
153
|
i += 1;
|
|
@@ -159,7 +169,7 @@ export function parseWithLines(text) {
|
|
|
159
169
|
return out;
|
|
160
170
|
}
|
|
161
171
|
for (;;) {
|
|
162
|
-
out.push(value(`${path}[${out.length}]
|
|
172
|
+
out.push(value(`${path}[${out.length}]`, depth + 1));
|
|
163
173
|
space();
|
|
164
174
|
if (src[i] === ',') {
|
|
165
175
|
i += 1;
|
package/src/infra-provider.js
CHANGED
|
@@ -129,7 +129,9 @@ export const COST_NOTE_MAX = 500;
|
|
|
129
129
|
* which only the apply runner holds. `observeOnly` environments (the board's own install, BRK-169) refuse apply.
|
|
130
130
|
* @typedef {object} ProviderContext
|
|
131
131
|
* @property {string} environment
|
|
132
|
-
* @property {Record<string, unknown>} [scope] which of the platform's resources belong to the environment
|
|
132
|
+
* @property {Record<string, unknown>} [scope] which of the platform's resources belong to the environment: its
|
|
133
|
+
* `target`, and for a plan or an act the install's own Worker (`board`) and the other environments' targets
|
|
134
|
+
* (`others`), which are never its to change (BRK-251)
|
|
133
135
|
* @property {boolean} [observeOnly]
|
|
134
136
|
* @property {string} [token]
|
|
135
137
|
* @property {string} [writeToken] the environment's write token: only the apply runner sets it (CLI-12, from its
|
|
@@ -192,6 +194,10 @@ export const COST_NOTE_MAX = 500;
|
|
|
192
194
|
* @property {(resource: Resource, op: string) => string | null} [refuses] why the platform can't make an envelope's
|
|
193
195
|
* `scale` or `restart` to this one resource although its kind declares it (a setting of the resource rules it out),
|
|
194
196
|
* in words, or null when it can; an act asks it before it plans (BRK-227)
|
|
197
|
+
* @property {(ctx: ProviderContext, found: Discovery, resource: Resource) => string | null} [outside] why the
|
|
198
|
+
* resource, though `discover` found it, isn't the environment's to change (it's only reached through the install's
|
|
199
|
+
* Worker, `scope.board`, or another environment's target, `scope.others`), in words, or null when it is; an act asks
|
|
200
|
+
* it before it plans (BRK-251)
|
|
195
201
|
* @property {ReadToken} [readToken] the read-only token it needs, if any
|
|
196
202
|
* @property {(ctx: ProviderContext) => Promise<Discovery>} discover
|
|
197
203
|
* @property {(ctx: ProviderContext, desired: DesiredState) => Promise<PlanDiff>} plan
|
|
@@ -256,6 +262,7 @@ export function checkProvider(provider) {
|
|
|
256
262
|
}
|
|
257
263
|
if (provider.readToken !== undefined) checkReadToken(what, provider.readToken);
|
|
258
264
|
if (provider.refuses !== undefined && typeof provider.refuses !== 'function') fail(what, 'refuses is not a function');
|
|
265
|
+
if (provider.outside !== undefined && typeof provider.outside !== 'function') fail(what, 'outside is not a function');
|
|
259
266
|
if (provider.estimate !== undefined && typeof provider.estimate !== 'function')
|
|
260
267
|
fail(what, 'estimate is not a function');
|
|
261
268
|
return provider;
|