@bongos/core 1.19.950 → 1.19.952

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.
@@ -11,7 +11,8 @@
11
11
  // provisioning.fleet.manage.
12
12
  //
13
13
  // The fix gates the SHAPE, not the route: ordinary project creation must not require a rank,
14
- // so `co-tenant` — the front door — stays open to any authenticated builder. Everything else
14
+ // so the front door — `byo-host` since ADR 0323, `co-tenant` before it — stays open to any
15
+ // authenticated builder. Everything else
15
16
  // demands provisioning.fleet.manage, deliberately the same atom that already guards the fleet
16
17
  // cost-ledger and force-teardown, since spending on the fleet and managing the fleet are one
17
18
  // authority.
@@ -110,17 +111,79 @@ function post(path, body) {
110
111
 
111
112
  // ── the front door stays open — this is the half a careless fix would break ───────
112
113
 
113
- await ta('co-tenant (and an absent shape, which defaults to it) stays open with no permission', async () => {
114
+ await ta('byo-host (and an absent shape, which defaults to it) stays open with no permission', async () => {
115
+ // ADR 0323 moved the front door from 'co-tenant' to 'byo-host'. It is a STRONGER
116
+ // front door: co-tenant cost nothing to invoice but still ran the caller's process on
117
+ // our box, whereas byo-host runs on the owner's own machine and never touches ours.
118
+ // The absent-shape case is the one a careless fix breaks — createInstance coerces an
119
+ // unknown or missing shape to byo-host, and if the gate's own default disagreed with
120
+ // that coercion the wizard's shapeless create would 403 instead of 201.
114
121
  for (const body of [
115
- { slug: 'demo', hosting_shape: 'co-tenant', no_address: true },
122
+ { slug: 'demo', hosting_shape: 'byo-host', no_address: true },
116
123
  { slug: 'demo', no_address: true },
117
124
  ]) {
118
125
  held = [];
126
+ lastCreateArgs = null;
119
127
  const res = await post('/provisioning/instances', body);
120
128
  assert.equal(res.status, 201, `expected 201 for ${JSON.stringify(body)}, got ${res.status} ${res.raw}`);
129
+ // Assert the STORED shape, not just the status. A 201 alone does not prove the row is
130
+ // right: the create route resolves an absent shape independently of this gate, and at
131
+ // task 1004141 that default was briefly still 'co-tenant' — the gate admitted the
132
+ // request as byo-host and the route stored the retired shape, with no error anywhere.
133
+ // Checking the status only is what let that through.
134
+ assert.equal(lastCreateArgs && lastCreateArgs.hostingShape, 'byo-host',
135
+ `${JSON.stringify(body)} must STORE byo-host, got ${lastCreateArgs && lastCreateArgs.hostingShape}`);
121
136
  }
122
137
  });
123
138
 
139
+ await ta('co-tenant is RETIRED: refused for every caller, and not as a permission problem', async () => {
140
+ // ADR 0323 retires the shape whose name meant "a process on OUR box" — the opposite
141
+ // of bring-your-own-host, so the name could not be carried over. It stays a valid
142
+ // STORED value (one torn-down row carries it, and HOSTING_SHAPES still lists it) but
143
+ // becomes UNREQUESTABLE, which is the difference between closing a door and rewriting
144
+ // the record. The refusal must NOT be a 403-for-want-of-permission: no rank admits a
145
+ // retired shape, so granting fleet.manage must not open it either.
146
+ held = [];
147
+ lastCreateArgs = null;
148
+ let res = await post('/provisioning/instances', { slug: 'demo', hosting_shape: 'co-tenant', no_address: true });
149
+ assert.equal(res.status, 403, `expected 403, got ${res.status} ${res.raw}`);
150
+ assert.equal(res.body.error.code, 'shape_not_requestable');
151
+ assert.equal(lastCreateArgs, null, 'a retired shape must not create a row');
152
+
153
+ held = ['provisioning.fleet.manage'];
154
+ res = await post('/provisioning/instances', { slug: 'demo', hosting_shape: 'co-tenant', no_address: true });
155
+ assert.equal(res.status, 403, `every permission still must not admit it, got ${res.status} ${res.raw}`);
156
+ assert.equal(res.body.error.code, 'shape_not_requestable');
157
+ });
158
+
159
+ await ta('the refusal speaks WIRE vocabulary — no owner-facing label is translated here', async () => {
160
+ // The single-translation invariant, ENFORCED rather than stated. ADR 0323 puts the
161
+ // stored value → owner-facing name map in exactly one place (SHAPE_LABEL in
162
+ // modules/public-landing/public/projects.html), and an API error string is the worst
163
+ // place for a second copy: whoever renames the product next will change the label map
164
+ // and a wizard heading, and a sentence buried in a 403 will go on selling a name that
165
+ // no longer exists, with nothing failing. This assertion is what fails instead.
166
+ //
167
+ // It checks BOTH unrequestable shapes, because the retired one is the tempting case —
168
+ // it is the refusal that wants to suggest an alternative, which is exactly where a
169
+ // product name creeps in. Suggesting one is fine; `details.requestable` already does
170
+ // it in stored values, and the message points at that field.
171
+ held = [];
172
+ for (const shape of ['co-tenant', 'control-plane']) {
173
+ const res = await post('/provisioning/instances', { slug: 'demo', hosting_shape: shape, no_address: true });
174
+ assert.equal(res.status, 403, `expected 403 for ${shape}, got ${res.status} ${res.raw}`);
175
+ const message = res.body.error.message || '';
176
+ for (const label of ['bring your own host', 'cloud host', 'dedicated cloud host']) {
177
+ assert.ok(!message.toLowerCase().includes(label),
178
+ `the ${shape} refusal translates a stored value into the owner-facing label "${label}" — that map lives only in projects.html's SHAPE_LABEL (ADR 0323): ${message}`);
179
+ }
180
+ }
181
+ // And the caller is still told what they CAN ask for, in the vocabulary they sent.
182
+ const res = await post('/provisioning/instances', { slug: 'demo', hosting_shape: 'co-tenant', no_address: true });
183
+ assert.deepEqual([...res.body.error.details.requestable].sort(), ['byo-host', 'dedicated', 'standalone'],
184
+ 'a refusal that names no alternative is a dead end — the stored values are the alternative');
185
+ });
186
+
124
187
  // ── standalone runs the caller's OWN code on the platform's box (task 1003370) ────
125
188
 
126
189
  await ta('standalone: REFUSED for a builder without provisioning.fleet.manage', async () => {
@@ -232,20 +295,27 @@ await ta('HOSTING_SHAPES is pinned, so a new shape forces a written decision abo
232
295
  // POST for a second control plane until task 1003370.
233
296
  //
234
297
  // So the question this list now forces is the one that generalises. The gate is an
235
- // allow-list: `co-tenant` needs nothing, `control-plane` is refused outright, and
298
+ // allow-list: `byo-host` needs nothing, `control-plane` and `co-tenant` are refused
299
+ // outright (for different reasons — never-yours vs retired), and
236
300
  // EVERY other shape — including one added after this file was last read — demands
237
301
  // provisioning.fleet.manage by omission. A fifth shape therefore arrives already
238
302
  // gated, and this assertion is the prompt to confirm that is what was wanted
239
303
  // rather than the thing that has to catch the hole.
304
+ //
305
+ // It fired a SECOND time at task 1004141 (ADR 0323), which added 'byo-host' and
306
+ // retired 'co-tenant'. That retirement is why the list below still carries a shape
307
+ // no caller may request: the value stays storable so one torn-down row remains
308
+ // readable, and the door is shut in UNREQUESTABLE_SHAPES instead. Dropping it from
309
+ // HOSTING_SHAPES would make that historical row violate its own CHECK constraint.
240
310
  assert.deepEqual([...provisioning.HOSTING_SHAPES].sort(),
241
- ['co-tenant', 'control-plane', 'dedicated', 'standalone']);
311
+ ['byo-host', 'co-tenant', 'control-plane', 'dedicated', 'standalone']);
242
312
  });
243
313
 
244
314
  await ta('an unknown shape is answered by the validator, not by the gate', () => {
245
315
  // The gate DEFERS anything outside HOSTING_SHAPES so a typo gets `bad_hosting_shape`
246
316
  // naming the valid set, rather than a 403 about a permission the caller would not
247
317
  // have needed. Deferring is safe: the validator refuses it microseconds later, and
248
- // createInstance coerces an unrecognised shape to co-tenant rather than storing it,
318
+ // createInstance coerces an unrecognised shape to byo-host rather than storing it,
249
319
  // so no deferred value can become a privileged row.
250
320
  return post('/provisioning/instances', { slug: 'demo', hosting_shape: 'co-tenamt', no_address: true })
251
321
  .then((res) => {