@bongos/core 1.24.8 → 1.24.10
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/.bongos-core.json +131 -86
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +11 -1
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/adr/0385-money-lives-with-a-processor-credits-stay-a-project-meter.md +1 -1
- package/docs/api/openapi.json +185 -6
- package/docs/api-reference.md +7 -3
- package/docs/architecture.md +2 -2
- package/docs/copy-inventory.md +58 -28
- package/docs/copy-registry.json +333 -33
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +3 -3
- package/docs/page-inventory.json +34 -4
- package/docs/page-readings.json +294 -237
- package/migrations/core_287_module_prices_cash_only.sql +24 -0
- package/modules/hall-ui/public/ai-studio.html +2 -2
- package/modules/hall-ui/public/atlas.html +2 -2
- package/modules/hall-ui/public/blockers.html +2 -2
- package/modules/hall-ui/public/board-room.html +2 -2
- package/modules/hall-ui/public/city.html +2 -2
- package/modules/hall-ui/public/collab.html +2 -2
- package/modules/hall-ui/public/commands.html +2 -2
- package/modules/hall-ui/public/copy-desk.html +2 -2
- package/modules/hall-ui/public/craft-mode.js +1 -1
- package/modules/hall-ui/public/deploy.html +2 -2
- package/modules/hall-ui/public/diagrams.html +2 -2
- package/modules/hall-ui/public/drachmae.html +2 -2
- package/modules/hall-ui/public/fleet.html +2 -2
- package/modules/hall-ui/public/gate.html +2 -2
- package/modules/hall-ui/public/goal-map.html +2 -2
- package/modules/hall-ui/public/goals.html +2 -2
- package/modules/hall-ui/public/government.html +2 -2
- package/modules/hall-ui/public/idea.html +2 -2
- package/modules/hall-ui/public/ideas.html +2 -2
- package/modules/hall-ui/public/index.html +2 -2
- package/modules/hall-ui/public/market-watch-rules.html +2 -2
- package/modules/hall-ui/public/market-watch-settings.css +217 -0
- package/modules/hall-ui/public/market-watch-settings.html +113 -0
- package/modules/hall-ui/public/market-watch-settings.js +230 -0
- package/modules/hall-ui/public/market-watch-settings.states.json +14 -0
- package/modules/hall-ui/public/modules.html +2 -2
- package/modules/hall-ui/public/modules.js +5 -0
- package/modules/hall-ui/public/my-agents.html +2 -2
- package/modules/hall-ui/public/primer.html +2 -2
- package/modules/hall-ui/public/profile.html +2 -2
- package/modules/hall-ui/public/project-settings.html +2 -2
- package/modules/hall-ui/public/ranks.html +2 -2
- package/modules/hall-ui/public/roadmap.html +2 -2
- package/modules/hall-ui/public/roster.html +2 -2
- package/modules/hall-ui/public/sessions.html +2 -2
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +3 -0
- package/modules/hall-ui/public/street.html +2 -2
- package/modules/hall-ui/public/studio.html +2 -2
- package/modules/hall-ui/public/task.html +2 -2
- package/modules/hall-ui/public/thinking.html +2 -2
- package/modules/hall-ui/public/tweak-editor.html +2 -2
- package/modules/hall-ui/public/watch.html +2 -2
- package/modules/hall-ui/public/work.html +2 -2
- package/modules/hall-ui/records/market-watch-settings.md +11 -0
- package/modules/market-watch/module.json +3 -2
- package/modules/market-watch/routes/settings.js +128 -0
- package/modules/market-watch/settings.js +207 -0
- package/modules/provisioning/starter-bundles.js +11 -6
- package/modules/ui-design/kit/serve.js +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/bongos/module-store-cli.js +21 -21
- package/scripts/bongos/module.js +8 -8
- package/src/bongos/module-listing.js +47 -20
- package/src/bongos/module-scope-map.js +1 -0
- package/src/bongos/module-store.js +20 -12
- package/src/bongos/routes/modules.js +31 -21
- package/src/bongos/serve-internal.js +2 -2
- package/src/module-api.js +1 -1
- package/tests/hall_nav.mjs +4 -2
- package/tests/hall_page_gate_map.mjs +4 -0
- package/tests/market_watch_data_db.mjs +27 -0
- package/tests/market_watch_module.mjs +8 -4
- package/tests/market_watch_settings.mjs +392 -0
- package/tests/module_store_cli.mjs +14 -3
- package/tests/module_store_listing.mjs +65 -14
- package/tests/module_store_price.mjs +97 -26
- package/tests/module_store_registry_migration.mjs +11 -0
- package/tests/nav_permission_atoms.mjs +3 -2
|
@@ -30,8 +30,11 @@ let state = fresh();
|
|
|
30
30
|
const newestPrice = (key) => state.prices.filter((p) => p.module_key === key).at(-1) || null;
|
|
31
31
|
const query = async (sql, params = []) => {
|
|
32
32
|
if (/^INSERT INTO store_module_prices/.test(sql)) {
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// Reads the column list rather than assuming it, so a writer that still sent
|
|
34
|
+
// price_credits would be recorded as such and fail the cash-only cases below.
|
|
35
|
+
const cols = sql.match(/\(([^)]*)\)/)[1].split(',').map((c) => c.trim());
|
|
36
|
+
const row = Object.fromEntries(cols.map((c, i) => [c, params[i]]));
|
|
37
|
+
state.prices.push({ price_credits: null, ...row, set_at: ++state.setAt });
|
|
35
38
|
return { rows: [] };
|
|
36
39
|
}
|
|
37
40
|
if (/FROM store_module_prices\s+WHERE module_key = \$1/.test(sql)) {
|
|
@@ -105,9 +108,9 @@ test('only the author may set the price; a stranger and a delisted module are re
|
|
|
105
108
|
assert.equal(state.prices.length, 0, 'nothing was written');
|
|
106
109
|
});
|
|
107
110
|
|
|
108
|
-
test('a price must be whole
|
|
111
|
+
test('a price must be a whole number of cents, 0 or more', async () => {
|
|
109
112
|
state = fresh();
|
|
110
|
-
for (const body of [{}, { cents: null
|
|
113
|
+
for (const body of [{}, { cents: null }, { cents: -1 }, { cents: 1.5 }, { cents: '10' }, { cents: 1e12 }]) {
|
|
111
114
|
const r = await setPrice(body);
|
|
112
115
|
assert.equal(r.statusCode, 400, JSON.stringify(body));
|
|
113
116
|
assert.ok(['bad_price', 'validation_failed'].includes(r.body.error.code), r.body.error.code);
|
|
@@ -115,21 +118,34 @@ test('a price must be whole numbers of 0 or more, and name at least one currency
|
|
|
115
118
|
assert.equal(state.prices.length, 0);
|
|
116
119
|
});
|
|
117
120
|
|
|
118
|
-
|
|
121
|
+
// Task 1004989 (ADR 0385 decision 2): credits never price or buy a module.
|
|
122
|
+
test('a credit price is refused by name, with or without cents, and nothing is written', async () => {
|
|
123
|
+
state = fresh();
|
|
124
|
+
for (const body of [{ credits: 40 }, { credits: 0 }, { cents: 500, credits: 40 }, { credits: '10' }]) {
|
|
125
|
+
const r = await setPrice(body);
|
|
126
|
+
assert.equal(r.statusCode, 400, JSON.stringify(body));
|
|
127
|
+
assert.equal(r.body.error.code, 'credit_price_refused', JSON.stringify(body));
|
|
128
|
+
assert.match(r.body.error.message, /Credits cannot price a module/);
|
|
129
|
+
}
|
|
130
|
+
assert.equal(state.prices.length, 0, 'no row, not even the cash half');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('each change appends a cash-only row naming who set it, and the newest is what acquire reads', async () => {
|
|
119
134
|
state = fresh();
|
|
120
135
|
const first = await setPrice({ cents: 900 });
|
|
121
136
|
assert.equal(first.statusCode, 200);
|
|
122
|
-
assert.deepEqual(first.body, { ok: true, module_key: 'weather', previous: null, price: { cents: 900
|
|
123
|
-
const second = await setPrice({ cents: 500
|
|
124
|
-
assert.deepEqual(second.body.previous, { cents: 900,
|
|
137
|
+
assert.deepEqual(first.body, { ok: true, module_key: 'weather', previous: null, price: { cents: 900 } });
|
|
138
|
+
const second = await setPrice({ cents: 500 });
|
|
139
|
+
assert.deepEqual(second.body.previous, { cents: 900, coming: false });
|
|
125
140
|
assert.equal(state.prices.length, 2, 'the history is kept');
|
|
126
141
|
assert.equal(state.prices[1].set_by, 42);
|
|
142
|
+
assert.ok(state.prices.every((p) => p.price_credits == null), 'no new row carries a credit price');
|
|
127
143
|
const acq = await store.resolveAcquirable('weather', {});
|
|
128
144
|
assert.equal(acq.ok, true);
|
|
129
|
-
assert.
|
|
145
|
+
assert.equal(acq.price.price_cents, 500);
|
|
130
146
|
assert.equal(acq.free, false);
|
|
131
|
-
await setPrice({
|
|
132
|
-
assert.equal((await store.resolveAcquirable('weather', {})).free, true, '
|
|
147
|
+
await setPrice({ cents: 0 });
|
|
148
|
+
assert.equal((await store.resolveAcquirable('weather', {})).free, true, 'a cash price of 0 is free');
|
|
133
149
|
});
|
|
134
150
|
|
|
135
151
|
test('the catalog shows the price, and marks a paid module not yet buyable until buying opens', async () => {
|
|
@@ -139,26 +155,79 @@ test('the catalog shows the price, and marks a paid module not yet buyable until
|
|
|
139
155
|
assert.equal(cat.listed[0].buyable, true, 'a free module can be had today');
|
|
140
156
|
await setPrice({ cents: 900 });
|
|
141
157
|
cat = await listing.listCatalog({});
|
|
142
|
-
assert.deepEqual(cat.listed[0].price, { cents: 900,
|
|
158
|
+
assert.deepEqual(cat.listed[0].price, { cents: 900, free: false, coming: false });
|
|
143
159
|
assert.equal(cat.listed[0].buyable, false, 'buying arrives with task 1003788');
|
|
144
160
|
});
|
|
145
161
|
|
|
146
|
-
test('
|
|
162
|
+
test('an OLD credit-only row: listed as price coming, never free, acquire refuses with no credit path, the author is asked', async () => {
|
|
163
|
+
state = fresh();
|
|
164
|
+
state.prices.push({ module_key: 'weather', price_cents: null, price_credits: 40, set_by: 42, set_at: ++state.setAt });
|
|
165
|
+
const cat = await listing.listCatalog({});
|
|
166
|
+
assert.deepEqual(cat.listed[0].price, { cents: null, free: false, coming: true });
|
|
167
|
+
assert.equal(cat.listed[0].buyable, false);
|
|
168
|
+
const acq = await store.resolveAcquirable('weather', {});
|
|
169
|
+
assert.equal(acq.free, false, 'credits never make it free or buyable');
|
|
170
|
+
// The author's listing read carries the ask; a stranger's does not.
|
|
171
|
+
const mine = await listing.readListing('weather', { viewerId: 42 });
|
|
172
|
+
assert.equal(mine.price_prompt, listing.PRICE_COMING_PROMPT);
|
|
173
|
+
assert.match(mine.price_prompt, /set a cash price/);
|
|
174
|
+
const theirs = await listing.readListing('weather', { viewerId: 7 });
|
|
175
|
+
assert.equal(theirs.price_prompt, null);
|
|
176
|
+
assert.deepEqual(theirs.price, { cents: null, free: false, coming: true });
|
|
177
|
+
// Setting a cash price replaces it; the reply says what it was.
|
|
178
|
+
const set = await setPrice({ cents: 300 });
|
|
179
|
+
assert.deepEqual(set.body.previous, { cents: null, coming: true });
|
|
180
|
+
assert.equal((await listing.readListing('weather', { viewerId: 42 })).price_prompt, null, 'the ask goes away');
|
|
181
|
+
// An old 0-credit offer stays free, as it was.
|
|
182
|
+
state = fresh();
|
|
183
|
+
state.prices.push({ module_key: 'weather', price_cents: null, price_credits: 0, set_by: 42, set_at: ++state.setAt });
|
|
184
|
+
assert.equal((await store.resolveAcquirable('weather', {})).free, true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// The hall store row: the real storeFacts lifted out of modules.js and run over a
|
|
188
|
+
// real catalog entry, so the page and the read cannot disagree about the flag.
|
|
189
|
+
test('the hall store row marks a price-coming module, and nothing else', async () => {
|
|
190
|
+
const src = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/modules.js'), 'utf8');
|
|
191
|
+
const start = src.indexOf(' function storeFacts(m) {');
|
|
192
|
+
const end = src.indexOf('\n }\n', start);
|
|
193
|
+
assert.ok(start > 0 && end > start, 'storeFacts is in modules.js');
|
|
194
|
+
const storeFacts = new Function('escapeHtml', 'sortedPartFact', 'partsList',
|
|
195
|
+
`${src.slice(start, end + 4)}; return storeFacts;`)((s) => String(s), () => '', () => '');
|
|
196
|
+
state = fresh();
|
|
197
|
+
state.prices.push({ module_key: 'weather', price_cents: null, price_credits: 40, set_by: 42, set_at: ++state.setAt });
|
|
198
|
+
const coming = (await listing.listCatalog({})).listed[0];
|
|
199
|
+
assert.match(storeFacts(coming), /price coming/);
|
|
200
|
+
assert.doesNotMatch(storeFacts(coming), /40/, 'the credit amount is never drawn');
|
|
201
|
+
state = fresh();
|
|
202
|
+
await setPrice({ cents: 900 });
|
|
203
|
+
assert.doesNotMatch(storeFacts((await listing.listCatalog({})).listed[0]), /price coming/);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('the acquire refusal never offers credits as a way to pay', () => {
|
|
207
|
+
const src = fs.readFileSync(path.join(ROOT, 'src/bongos/routes/modules.js'), 'utf8');
|
|
208
|
+
const at = src.indexOf("'payment_required'");
|
|
209
|
+
const block = src.slice(at, at + 600);
|
|
210
|
+
assert.ok(at > 0);
|
|
211
|
+
assert.doesNotMatch(block, /credit economy/);
|
|
212
|
+
assert.match(block, /has no cash price yet/);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('bongos module price: sends the cash price, says what it was and now is, and that paid is not yet buyable', async () => {
|
|
147
216
|
const sent = [];
|
|
148
217
|
const request = async (method, url, opts) => {
|
|
149
218
|
sent.push({ method, url, json: opts && opts.json });
|
|
150
|
-
return { ok: true, status: 200, data: { ok: true, previous: { cents:
|
|
219
|
+
return { ok: true, status: 200, data: { ok: true, previous: { cents: null, coming: true }, price: { cents: 900 } } };
|
|
151
220
|
};
|
|
152
221
|
const out = [];
|
|
153
|
-
assert.equal(await mod.cmdPrice(['weather', '--cents', '900'
|
|
154
|
-
assert.deepEqual(sent, [{ method: 'POST', url: '/store/modules/weather/price', json: { cents: 900
|
|
155
|
-
assert.match(out.join('\n'), /"weather" now costs \$9\.00
|
|
222
|
+
assert.equal(await mod.cmdPrice(['weather', '--cents', '900'], { log: (l) => out.push(l), errlog: () => {}, request }), 0);
|
|
223
|
+
assert.deepEqual(sent, [{ method: 'POST', url: '/store/modules/weather/price', json: { cents: 900 } }]);
|
|
224
|
+
assert.match(out.join('\n'), /"weather" now costs \$9\.00 \(was price coming\)/);
|
|
156
225
|
assert.match(out.join('\n'), /not yet buyable/);
|
|
157
226
|
});
|
|
158
227
|
|
|
159
|
-
test('bongos module price: refuses no key, no amount
|
|
228
|
+
test('bongos module price: refuses no key, no amount, a non-number and any credit price before asking the store', async () => {
|
|
160
229
|
const request = async () => { throw new Error('must not be called'); };
|
|
161
|
-
for (const args of [[], ['--cents', '5'], ['weather'], ['weather', '--cents', 'five'], ['weather', '--credits', '
|
|
230
|
+
for (const args of [[], ['--cents', '5'], ['weather'], ['weather', '--cents', 'five'], ['weather', '--credits', '40'], ['weather', '--cents', '5', '--credits', '1']]) {
|
|
162
231
|
const errs = [];
|
|
163
232
|
assert.equal(await mod.cmdPrice(args, { log: () => {}, errlog: (l) => errs.push(l), request }), 2, args.join(' '));
|
|
164
233
|
assert.match(errs.join('\n'), /usage: bongos module price/);
|
|
@@ -183,7 +252,7 @@ test('the price history is the audit: who set each price and when, and what it r
|
|
|
183
252
|
assert.match(src, /FROM store_modules WHERE module_key = \$1 FOR UPDATE/, 'one transaction, the module row locked');
|
|
184
253
|
});
|
|
185
254
|
|
|
186
|
-
test('bongos module publish --cents
|
|
255
|
+
test('bongos module publish --cents sets the price after the upload, and checks it (credits refused) before packing', async () => {
|
|
187
256
|
const packed = { tgz: Buffer.from('x'), sidecar: { module_version: '1.0.0', core_version: '^1.0.0', file_count: 1, tree_sha256: 'a'.repeat(64), tarball: { name: 'x.tgz' } } };
|
|
188
257
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'publish-price-'));
|
|
189
258
|
fs.mkdirSync(path.join(dir, 'weather'));
|
|
@@ -195,13 +264,15 @@ test('bongos module publish --cents/--credits sets the price after the upload, a
|
|
|
195
264
|
const out = [];
|
|
196
265
|
const code = await mod.cmdPublish(['--cents', '900', 'weather'], { log: (l) => out.push(l), errlog: (l) => out.push(l), modulesDir: dir, pack: () => packed, upload, priceRequest });
|
|
197
266
|
assert.equal(code, 0, out.join('\n'));
|
|
198
|
-
assert.deepEqual(sent, [['POST', '/store/modules/weather/price', { cents: 900
|
|
267
|
+
assert.deepEqual(sent, [['POST', '/store/modules/weather/price', { cents: 900 }]], 'the key is found even after a valued flag');
|
|
199
268
|
let packedAtAll = false;
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
269
|
+
for (const flags of [['--cents', 'nine'], ['--credits', '40']]) {
|
|
270
|
+
const bad = await mod.cmdPublish(['weather', ...flags], { log: () => {}, errlog: () => {}, modulesDir: dir, pack: () => { packedAtAll = true; return packed; }, upload, priceRequest });
|
|
271
|
+
assert.equal(bad, 2, flags.join(' '));
|
|
272
|
+
}
|
|
273
|
+
assert.equal(packedAtAll, false, 'a bad or credit price refuses before anything is packed or sent');
|
|
203
274
|
const failing = async () => ({ ok: false, status: 403, data: { error: { message: 'Only the author' } } });
|
|
204
275
|
const errs = [];
|
|
205
|
-
assert.equal(await mod.cmdPublish(['weather', '--
|
|
206
|
-
assert.match(errs.join('\n'), /The version is published, but its price was not set\. Run `bongos module price weather --
|
|
276
|
+
assert.equal(await mod.cmdPublish(['weather', '--cents', '40'], { log: () => {}, errlog: (l) => errs.push(l), modulesDir: dir, pack: () => packed, upload, priceRequest: failing }), 1);
|
|
277
|
+
assert.match(errs.join('\n'), /The version is published, but its price was not set\. Run `bongos module price weather --cents 40`/);
|
|
207
278
|
});
|
|
@@ -43,6 +43,17 @@ test('a price of zero is allowed in both currencies — free modules are first-c
|
|
|
43
43
|
assert.match(t, /CHECK \(price_cents IS NOT NULL OR price_credits IS NOT NULL\)/);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
+
// Task 1004989 (ADR 0385 decision 2): a NEW price row may not carry credits, and
|
|
47
|
+
// the old rows stay readable — so the CHECK is NOT VALID and must stay that way.
|
|
48
|
+
test('core_287: new price rows are cash only; old credit rows are kept, not re-checked', () => {
|
|
49
|
+
const sql = readFileSync(path.join(ROOT, 'migrations', 'core_287_module_prices_cash_only.sql'), 'utf8')
|
|
50
|
+
.replace(/--[^\n]*/g, '');
|
|
51
|
+
assert.match(sql, /ADD CONSTRAINT store_module_prices_no_new_credit_price CHECK \(price_credits IS NULL\) NOT VALID;/);
|
|
52
|
+
assert.match(sql, /DROP CONSTRAINT IF EXISTS store_module_prices_no_new_credit_price;/, 'idempotent on a re-run');
|
|
53
|
+
assert.doesNotMatch(sql, /VALIDATE CONSTRAINT|UPDATE store_module_prices|DELETE FROM store_module_prices/,
|
|
54
|
+
'no old row is validated, rewritten or deleted');
|
|
55
|
+
});
|
|
56
|
+
|
|
46
57
|
test('a version is exact semver and can be published only once per module', () => {
|
|
47
58
|
const t = table('store_module_versions');
|
|
48
59
|
assert.match(t, /version\s+text NOT NULL\s+CHECK \(version ~ '\^\[0-9\]\+\\\.\[0-9\]\+\\\.\[0-9\]\+\$'\)/);
|
|
@@ -92,8 +92,9 @@ test('the hall-preview fixture carries every atom /me resolves', () => {
|
|
|
92
92
|
// the seats read from /me's server-resolved positions block; task 1004730: the banner
|
|
93
93
|
// says a held core version was tried and rolled back; task 1004731: the one-time
|
|
94
94
|
// "Upgraded to core X" notice after a move; task 1004826: Standing is Accomplishments;
|
|
95
|
-
// task 1004846: the new logo (its bump landed late, in task 1004852)
|
|
96
|
-
|
|
95
|
+
// task 1004846: the new logo (its bump landed late, in task 1004852); task 1004938: the
|
|
96
|
+
// Market Watch settings nav item and its fuel glyph.
|
|
97
|
+
const SHELL_SHA = '4022e01fbb4045ad';
|
|
97
98
|
|
|
98
99
|
test('shell.js has not changed without its cache-bust being bumped', () => {
|
|
99
100
|
const actual = crypto.createHash('sha256').update(SHELL).digest('hex').slice(0, 16);
|