@bongos/core 1.19.733 → 1.19.734

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.
@@ -54,11 +54,16 @@ test('clientBranding exposes project policy — the settings read-back (task 100
54
54
  // `artistGate` joined in task 1003575 (ADR 0241) and is the ONE key here whose
55
55
  // fallback is not today's behaviour: 'strict' is the owner's ruling, safe as a
56
56
  // default because it can only ever delay a pin move, never a builder's ship.
57
+ // `spendPayer` joined in task 1003884 (ADR 0282) — who pays when a module spends
58
+ // money for whoever called it. Its fallback IS today's behaviour ('none'), and it
59
+ // is published for a reason of its own: it is the one key that can REFUSE a
60
+ // module, so an owner looking at a module that will not turn on needs the
61
+ // instance's own answer rather than the platform's record of what they saved.
57
62
  assert.deepEqual(clientBranding({}).project,
58
- { platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'strict' });
63
+ { platformVisibility: 'public', joinability: 'apply', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' });
59
64
  assert.deepEqual(
60
- clientBranding({ project: { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off' } }).project,
61
- { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off' },
65
+ clientBranding({ project: { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder' } }).project,
66
+ { platformVisibility: 'gated', joinability: 'invite_only', visibility: 'stealth', joinGrant: 'view', artistGate: 'off', spendPayer: 'builder' },
62
67
  'what the instance actually resolved (env override included) is what it publishes');
63
68
  });
64
69
 
@@ -81,7 +86,7 @@ test('clientBranding is a SMALL allow-list — no server-only field leaks to the
81
86
  // public for the same read-back reason as its neighbours. Its companion stamp
82
87
  // (artistGateSince) deliberately does NOT: that is platform bookkeeping the server
83
88
  // reads, and no browser surface has a question it answers.
84
- assert.deepEqual(Object.keys(projected.project).sort(), ['artistGate', 'joinGrant', 'joinability', 'platformVisibility', 'visibility']);
89
+ assert.deepEqual(Object.keys(projected.project).sort(), ['artistGate', 'joinGrant', 'joinability', 'platformVisibility', 'spendPayer', 'visibility']);
85
90
  for (const leak of ['domains', 'repo', 'db', 'envPrefix', 'llmProjectName']) {
86
91
  assert.equal(projected[leak], undefined, `${leak} must not reach the client`);
87
92
  }
@@ -62,6 +62,7 @@ ${NEUTRAL_PREFIX}_JOINABILITY=apply
62
62
  ${NEUTRAL_PREFIX}_VISIBILITY=public
63
63
  ${NEUTRAL_PREFIX}_JOIN_GRANT=full
64
64
  ${NEUTRAL_PREFIX}_ARTIST_GATE=strict
65
+ ${NEUTRAL_PREFIX}_SPEND_PAYER=none
65
66
  ${NEUTRAL_PREFIX}_ARTIST_GATE_SINCE=
66
67
  `;
67
68
 
@@ -120,7 +121,12 @@ t('settingsEnvVarsFor: the runner pushes the SAME pair webEnvBody writes at stan
120
121
  [`${NEUTRAL_PREFIX}_VISIBILITY`]: 'public', [`${NEUTRAL_PREFIX}_JOIN_GRANT`]: 'full',
121
122
  // the artist gate + its non-retroactivity stamp (task 1003575, ADR 0241) ride
122
123
  // the same transport; the stamp is empty until the gate is actually RAISED
123
- [`${NEUTRAL_PREFIX}_ARTIST_GATE`]: 'strict', [`${NEUTRAL_PREFIX}_ARTIST_GATE_SINCE`]: '',
124
+ [`${NEUTRAL_PREFIX}_ARTIST_GATE`]: 'strict',
125
+ // who pays for metered module spend (task 1003884, ADR 0282) rides it too —
126
+ // it has to, because src/modules.js reads it at CONFIG LOAD, so the instance
127
+ // must already hold it in web.env before its first require
128
+ [`${NEUTRAL_PREFIX}_SPEND_PAYER`]: 'none',
129
+ [`${NEUTRAL_PREFIX}_ARTIST_GATE_SINCE`]: '',
124
130
  });
125
131
  const body = P.webEnvBody(inst, {});
126
132
  for (const [k, v] of Object.entries(vars)) assert.match(body, new RegExp(`^${k}=${v}$`, 'm'), `${k} at standup`);
@@ -132,10 +138,10 @@ const GATED = { settings: { platform_visibility: 'gated', joinability: 'invite_o
132
138
  const manifestWith = (project) => JSON.stringify({ schema: 'instance-manifest/v1', brand: project === undefined ? {} : { project } });
133
139
 
134
140
  t('true when every pushed key matches brand.project (camelCase, the ENV_OVERRIDES targets)', () => {
135
- assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict' }), GATED), true);
141
+ assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' }), GATED), true);
136
142
  });
137
143
  t('false when the instance still reports a different value', () => {
138
- assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'public', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict' }), GATED), false);
144
+ assert.equal(P.settingsConsumed(manifestWith({ platformVisibility: 'public', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' }), GATED), false);
139
145
  assert.equal(P.settingsConsumed(manifestWith({ joinability: 'invite_only' }), GATED), false, 'a missing key is a mismatch, not a pass');
140
146
  });
141
147
  t('null (unknown) when the manifest carries no project block — a core that predates task 1003139 — or does not parse', () => {
@@ -153,7 +159,7 @@ t('instanceManifestCmd asks the instance on loopback, like healthzCmd', () => {
153
159
  function recorder({ apply = false, envBody = LIVE_ENV, readFails = false, healthzFails = false, privileged = false,
154
160
  manifest = 'match', restartThrows = false, writeThrows = false } = {}) {
155
161
  const cmds = [], writes = [], events = [], probes = [], cleared = [], logs = [];
156
- const manifestOut = manifest === 'match' ? manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict' })
162
+ const manifestOut = manifest === 'match' ? manifestWith({ platformVisibility: 'gated', joinability: 'invite_only', visibility: 'public', joinGrant: 'full', artistGate: 'strict', spendPayer: 'none' })
157
163
  : manifest === 'mismatch' ? manifestWith({ platformVisibility: 'public', joinability: 'apply', visibility: 'public' })
158
164
  : manifest === 'none' ? manifestWith(undefined) : 'nope';
159
165
  const exec = (cmd, opts = {}) => {
@@ -222,7 +228,7 @@ await ta('dry-run PLANS the patch: reads nothing real, writes NOTHING (no synthe
222
228
  assert.equal(r.ok, true);
223
229
  assert.equal(r.healthy, null, 'nothing ran, nothing observed');
224
230
  assert.equal(writes.length, 0, 'a dry-run must not plan a two-line overwrite of web.env');
225
- assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY, .*_VISIBILITY, .*_JOIN_GRANT, .*_ARTIST_GATE, .*_ARTIST_GATE_SINCE in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
231
+ assert.ok(logs.some((l) => /would patch .*_PLATFORM_VISIBILITY, .*_JOINABILITY, .*_VISIBILITY, .*_JOIN_GRANT, .*_ARTIST_GATE, .*_SPEND_PAYER, .*_ARTIST_GATE_SINCE in \/etc\/spike\/web\.env in place/.test(l)), logs.join('\n'));
226
232
  assert.ok(cmds.some((c) => /restart spike/.test(c)), 'the restart is still in the plan');
227
233
  assert.ok(logs.some((l) => /dry-run — nothing ran/.test(l)), 'the closing line does not claim a restart happened');
228
234
  });
@@ -164,7 +164,7 @@ await ta('owner reads DEFAULTS (public / apply) on a row with empty settings —
164
164
  reset();
165
165
  const r = await get('/provisioning/instances/18/settings');
166
166
  assert.equal(r.status, 200, r.raw);
167
- assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
167
+ assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' });
168
168
  // the planet rides beside the settings (task 1003347): no default — null = the platform's own pick
169
169
  assert.deepEqual(r.body.planet, { planet_template: null, planet_accent: null, card_backdrop: null });
170
170
  assert.deepEqual(r.body.options, {
@@ -179,6 +179,10 @@ await ta('owner reads DEFAULTS (public / apply) on a row with empty settings —
179
179
  // offered to the form like the rest; what makes it different is only its
180
180
  // default, which is the owner's ruling rather than today's behaviour.
181
181
  artist_gate: ['off', 'advisory', 'strict'],
182
+ // who pays for metered module spend (task 1003884, ADR 0282) — the sixth
183
+ // policy axis, offered to the form like the rest. What makes it different is
184
+ // its CONSEQUENCE: it is the only setting that can refuse a module outright.
185
+ spend_payer: ['none', 'project', 'builder'],
182
186
  planet_template: [...provisioning.PLANET_TEMPLATES], planet_accent: ['sea', 'terracotta', 'gold-soft', 'sun-lit', 'accent'],
183
187
  // the card's backdrop (task 1003352) is the same kind of key: platform-drawn,
184
188
  // closed list, no default, no push — so it rides the same vocab
@@ -191,14 +195,14 @@ await ta('a stored planet reads back; a name that left the vocabulary reads null
191
195
  const r = await get('/provisioning/instances/18/settings');
192
196
  assert.equal(r.status, 200, r.raw);
193
197
  assert.deepEqual(r.body.planet, { planet_template: 'bead-obsidian-tint', planet_accent: null, card_backdrop: null });
194
- assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the planet is not a setting: the policy read is untouched');
198
+ assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' }, 'the planet is not a setting: the policy read is untouched');
195
199
  });
196
200
 
197
201
  await ta('stored keys overlay the defaults', async () => {
198
202
  reset({ inst: { owner_builder_id: 7, slug: 'spike', status: 'active', settings: { joinability: 'invite_only' } } });
199
203
  const r = await get('/provisioning/instances/18/settings');
200
204
  assert.equal(r.status, 200, r.raw);
201
- assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'invite_only', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
205
+ assert.deepEqual(r.body.settings, { platform_visibility: 'public', joinability: 'invite_only', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' });
202
206
  });
203
207
 
204
208
  await ta('ANOTHER builder\'s GET → 404 (no existence leak)', async () => {
@@ -226,7 +230,7 @@ await ta('owner changes one key — merged, returned effective, audited api:self
226
230
  reset();
227
231
  const r = await patch('/provisioning/instances/18/settings', { joinability: 'open' });
228
232
  assert.equal(r.status, 200, r.raw);
229
- assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the SAVED value — the response never claims the instance runs it (task 1003140)');
233
+ assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' }, 'the SAVED value — the response never claims the instance runs it (task 1003140)');
230
234
  assert.equal(r.body.push.queued, true, 'an active co-tenant gets the live push (task 1003140)');
231
235
  assert.deepEqual(updateCalls, [{ id: 18, patch: { joinability: 'open' } }], 'exactly the sent key, nothing else');
232
236
  assert.equal(events.length, 1);
@@ -239,7 +243,7 @@ await ta('owner changes both keys in one call', async () => {
239
243
  reset();
240
244
  const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated', joinability: 'invite_only' });
241
245
  assert.equal(r.status, 200, r.raw);
242
- assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'invite_only', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
246
+ assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'invite_only', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' });
243
247
  });
244
248
 
245
249
  await ta('the OWN-SCOPE WALL: another builder\'s PATCH → 404 and the write path is never reached', async () => {
@@ -278,7 +282,7 @@ await ta('the owner sets the door to stealth — stored, echoed effective, audit
278
282
  reset();
279
283
  const r = await patch('/provisioning/instances/18/settings', { visibility: 'stealth' });
280
284
  assert.equal(r.status, 200, r.raw);
281
- assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'stealth', join_grant: 'full', artist_gate: 'strict' });
285
+ assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'stealth', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' });
282
286
  assert.deepEqual(updateCalls, [{ id: 18, patch: { visibility: 'stealth' } }], 'exactly the sent key, nothing else');
283
287
  assert.equal(events[0].actor, 'api:self');
284
288
  assert.match(events[0].detail, /visibility=stealth/);
@@ -338,7 +342,7 @@ await ta('the owner picks a planet: saved, echoed as `planet`, audited — and N
338
342
  const r = await patch('/provisioning/instances/18/settings', { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' });
339
343
  assert.equal(r.status, 200, r.raw);
340
344
  assert.deepEqual(r.body.planet, { planet_template: 'bead-obsidian-tint', planet_accent: 'sea', card_backdrop: null });
341
- assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the policy half is untouched');
345
+ assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' }, 'the policy half is untouched');
342
346
  assert.deepEqual(updateCalls, [{ id: 18, patch: { planet_template: 'bead-obsidian-tint', planet_accent: 'sea' } }]);
343
347
  assert.equal(r.body.push.queued, false);
344
348
  assert.equal(r.body.push.restart, false, 'an active co-tenant would get a push for a POLICY change — never for the planet');
@@ -225,7 +225,7 @@ await ta('ACTIVE standalone: the row is written FIRST, then a settings-apply int
225
225
  reset();
226
226
  const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated' });
227
227
  assert.equal(r.status, 200, r.raw);
228
- assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict' }, 'the SAVED (requested) value — never `settings`, which a form would render as live');
228
+ assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'apply', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' }, 'the SAVED (requested) value — never `settings`, which a form would render as live');
229
229
  assert.equal(r.body.settings, undefined);
230
230
  assert.deepEqual(updateCalls.map((c) => c.patch), [{ platform_visibility: 'gated' }]);
231
231
  assert.equal(updateCalls[0].enqueuedSoFar, 0, 'the runner reads the row — the write must precede the intent');
@@ -274,7 +274,7 @@ await ta('an enqueue that THROWS still answers 200 "saved" — never a 500 that
274
274
  enqueueThrows = new Error('db down');
275
275
  const r = await patch('/provisioning/instances/18/settings', { joinability: 'open' });
276
276
  assert.equal(r.status, 200, r.raw);
277
- assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
277
+ assert.deepEqual(r.body.saved, { platform_visibility: 'public', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' });
278
278
  assert.equal(updateCalls.length, 1, 'the write landed');
279
279
  receiptShape(r.body.push, { queued: false, restart: true, reachable: true });
280
280
  assert.match(r.body.push.message, /Saved, but the restart could not be queued/);
@@ -287,7 +287,7 @@ await ta('same values as already stored → no write, no event, no intent, "No c
287
287
  reset({ inst: { ...ACTIVE, settings: { platform_visibility: 'gated', joinability: 'open' } } });
288
288
  const r = await patch('/provisioning/instances/18/settings', { platform_visibility: 'gated', joinability: 'open' });
289
289
  assert.equal(r.status, 200, r.raw);
290
- assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' });
290
+ assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' });
291
291
  assert.equal(updateCalls.length, 0, 'nothing written (no updated_at reshuffle)');
292
292
  assert.equal(events.length, 0, 'no "settings changed" event for a change that is not one');
293
293
  assert.equal(enqueueCalls.length, 0, 'a whole-form save must not bounce a live project');
@@ -404,7 +404,7 @@ await ta('save A queues → save B 409s while A RUNS → A settles → the UNCHA
404
404
  assert.equal(enqueueCalls.length, enqueuedBefore + 1, 'exactly one new intent');
405
405
  assert.equal(updateCalls.length, writesBefore, 'nothing re-written — the value was already stored in step 2');
406
406
  assert.deepEqual(owedCalls[owedCalls.length - 1], { id: 18, owed: false }, 'and the debt is settled');
407
- assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict' },
407
+ assert.deepEqual(r.body.saved, { platform_visibility: 'gated', joinability: 'open', visibility: 'public', join_grant: 'full', artist_gate: 'strict', spend_payer: 'none' },
408
408
  'the value that finally ships is the one the owner saved');
409
409
  });
410
410
 
@@ -117,13 +117,17 @@ t('settingsEnvVars is the single source both legs derive from — same keys, sam
117
117
  assert.deepEqual(vars, {
118
118
  CLOUDBONGOS_PLATFORM_VISIBILITY: 'gated', CLOUDBONGOS_JOINABILITY: 'open', CLOUDBONGOS_VISIBILITY: 'stealth',
119
119
  CLOUDBONGOS_JOIN_GRANT: 'view', CLOUDBONGOS_ARTIST_GATE: 'advisory', CLOUDBONGOS_ARTIST_GATE_SINCE: '',
120
+ // Who pays for metered module spend (task 1003884) — it rides the same
121
+ // transport because src/modules.js reads it at CONFIG LOAD, which means the
122
+ // instance must already have it in web.env before the first require.
123
+ CLOUDBONGOS_SPEND_PAYER: 'none',
120
124
  });
121
125
  // varsOf splits on '=', so an empty value reads as '' on both sides — the point
122
126
  // of the comparison is that the standup lines carry exactly the same key set.
123
127
  assert.deepEqual(varsOf(provisioning.settingsEnvLines(row, 'CLOUDBONGOS')), vars, 'the standup lines ARE the vars');
124
128
  assert.deepEqual(provisioning.settingsEnvVars({}, 'X'), {
125
129
  X_PLATFORM_VISIBILITY: 'public', X_JOINABILITY: 'apply', X_VISIBILITY: 'public', X_JOIN_GRANT: 'full',
126
- X_ARTIST_GATE: 'strict', X_ARTIST_GATE_SINCE: '',
130
+ X_ARTIST_GATE: 'strict', X_ARTIST_GATE_SINCE: '', X_SPEND_PAYER: 'none',
127
131
  }, 'defaults, explicitly');
128
132
  });
129
133