@bongos/core 1.19.1077 → 1.19.1079

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.
@@ -0,0 +1,204 @@
1
+ // tests/module_entitlements.mjs — the entitlement record (task 1003813, ADR 0338 D1).
2
+ // DB-free: the migration text is pinned directly, and src/bongos/module-entitlements.js
3
+ // runs against a small in-memory stand-in for the one table. The live apply runs in
4
+ // the PG lane and at deploy.
5
+ //
6
+ // Run: node tests/module_entitlements.mjs
7
+
8
+ import { test } from 'node:test';
9
+ import { strict as assert } from 'node:assert';
10
+ import { readFileSync } from 'node:fs';
11
+ import { createRequire } from 'node:module';
12
+ import path from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ const require = createRequire(import.meta.url);
16
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
17
+ const SQL = readFileSync(path.join(ROOT, 'migrations', 'core_259_module_entitlements.sql'), 'utf8');
18
+ const DDL = SQL.replace(/--.*$/gm, '');
19
+ const {
20
+ grantEntitlement, recordAcquired, revokeEntitlement, checkEntitlement, listEntitlements,
21
+ } = require('../src/bongos/module-entitlements.js');
22
+
23
+ // ---- an in-memory stand-in for module_entitlements, keyed on the SQL's shape ----
24
+ function fakeDb() {
25
+ const rows = [];
26
+ let nextId = 1;
27
+ const find = (k, kind, ref) => rows.find((r) => r.module_key === k && r.holder_kind === kind && r.holder_ref === ref);
28
+ return {
29
+ rows,
30
+ async query(sql, p) {
31
+ const s = sql.replace(/\s+/g, ' ');
32
+ if (s.startsWith('INSERT INTO module_entitlements')) {
33
+ if (find(p[0], p[1], p[2])) return { rows: [] };
34
+ const r = { id: nextId++, module_key: p[0], holder_kind: p[1], holder_ref: p[2], source: p[3],
35
+ payment_ref: p[4], version: null, acquired_at: null, granted_by: p[5], granted_at: new Date(),
36
+ revoked_at: null, revoked_reason: null };
37
+ rows.push(r);
38
+ return { rows: [{ ...r }] };
39
+ }
40
+ if (s.startsWith('UPDATE module_entitlements SET revoked_at = NULL')) {
41
+ const r = find(p[0], p[1], p[2]);
42
+ if (!r || !r.revoked_at) return { rows: [] };
43
+ Object.assign(r, { revoked_at: null, revoked_reason: null, source: p[3], payment_ref: p[4], granted_by: p[5] });
44
+ return { rows: [{ ...r }] };
45
+ }
46
+ if (s.startsWith('UPDATE module_entitlements SET version')) {
47
+ const r = rows.find((x) => x.id === p[0]);
48
+ if (!r || r.revoked_at) return { rows: [] };
49
+ Object.assign(r, { version: p[1], acquired_at: new Date() });
50
+ return { rows: [{ ...r }] };
51
+ }
52
+ if (s.startsWith('UPDATE module_entitlements SET revoked_at = now()')) {
53
+ const r = find(p[0], p[1], p[2]);
54
+ if (!r || r.revoked_at) return { rows: [] };
55
+ Object.assign(r, { revoked_at: new Date(), revoked_reason: p[3] });
56
+ return { rows: [{ ...r }] };
57
+ }
58
+ if (s.includes('WHERE holder_kind = $1 AND holder_ref = $2')) {
59
+ return { rows: rows.filter((r) => r.holder_kind === p[0] && r.holder_ref === p[1]).map((r) => ({ ...r })).reverse() };
60
+ }
61
+ if (s.includes('WHERE module_key = $1 AND holder_kind = $2 AND holder_ref = $3')) {
62
+ const r = find(p[0], p[1], p[2]);
63
+ return { rows: r ? [{ ...r }] : [] };
64
+ }
65
+ throw new Error(`fake db: unhandled SQL: ${s}`);
66
+ },
67
+ };
68
+ }
69
+
70
+ const B = { moduleKey: 'weather', holderKind: 'builder', holderRef: '42' };
71
+
72
+ // ---- the migration ----
73
+
74
+ test('one idempotent table, forward-safe under the lint gate', () => {
75
+ assert.match(DDL, /CREATE TABLE IF NOT EXISTS module_entitlements \(/);
76
+ assert.doesNotMatch(DDL, /CREATE TABLE (?!IF NOT EXISTS)/);
77
+ assert.doesNotMatch(DDL, /CREATE INDEX (?!IF NOT EXISTS)/);
78
+ assert.match(DDL, /INSERT INTO schema_migrations \(version\) VALUES \('core_259_module_entitlements'\)\s+ON CONFLICT \(version\) DO NOTHING;/);
79
+ const lint = require('../scripts/gds/lint-migrations.js');
80
+ assert.deepEqual(lint.scanMigrationSql(SQL).unsafe || [], []);
81
+ });
82
+
83
+ test('a core-bundled module can hold an entitlement: module_key is not a foreign key', () => {
84
+ assert.doesNotMatch(DDL, /module_key[^,\n]*REFERENCES/);
85
+ assert.match(DDL, /source\s+text NOT NULL\s+CHECK \(source IN \('free', 'core', 'grant', 'purchase'\)\)/);
86
+ });
87
+
88
+ test('one row per module and holder; nothing cascades it away', () => {
89
+ assert.match(DDL, /UNIQUE \(module_key, holder_kind, holder_ref\)/);
90
+ assert.doesNotMatch(DDL, /ON DELETE CASCADE/);
91
+ });
92
+
93
+ test('the version is exact semver and moves with its timestamp; a revocation carries its reason', () => {
94
+ assert.match(DDL, /version IS NULL OR version ~ '\^\[0-9\]\+\\\.\[0-9\]\+\\\.\[0-9\]\+\$'/);
95
+ assert.match(DDL, /CHECK \(\(version IS NULL\) = \(acquired_at IS NULL\)\)/);
96
+ assert.match(DDL, /CHECK \(\(revoked_at IS NULL\) = \(revoked_reason IS NULL\)\)/);
97
+ });
98
+
99
+ test('the record models no balance, fee or payout — only an opaque payment_ref (area 8 owns money)', () => {
100
+ assert.doesNotMatch(DDL, /balance|payout|\bfee\b|price/i);
101
+ assert.match(DDL, /payment_ref\s+text,/);
102
+ });
103
+
104
+ // ---- the functions ----
105
+
106
+ test('every source is granted through the same record, with no payment involved', async () => {
107
+ const db = fakeDb();
108
+ for (const [i, source] of ['free', 'core', 'grant'].entries()) {
109
+ const r = await grantEntitlement({ ...B, moduleKey: `mod-${i}`, source }, { db });
110
+ assert.equal(r.created, true);
111
+ assert.equal(r.entitlement.payment_ref, null);
112
+ assert.equal((await checkEntitlement({ ...B, moduleKey: `mod-${i}` }, { db })).entitled, true);
113
+ }
114
+ });
115
+
116
+ test('granting twice is a no-op that keeps the first source', async () => {
117
+ const db = fakeDb();
118
+ await grantEntitlement({ ...B, source: 'grant', grantedBy: 7 }, { db });
119
+ const again = await grantEntitlement({ ...B, source: 'free' }, { db });
120
+ assert.equal(again.created, false);
121
+ assert.equal(again.entitlement.source, 'grant');
122
+ assert.equal(db.rows.length, 1);
123
+ });
124
+
125
+ test('a holder who was never granted is not entitled, and cannot record an acquisition', async () => {
126
+ const db = fakeDb();
127
+ assert.deepEqual(await checkEntitlement(B, { db }), { entitled: false, entitlement: null });
128
+ assert.deepEqual(await recordAcquired({ ...B, version: '1.0.0' }, { db }), { ok: false, code: 'not_entitled' });
129
+ });
130
+
131
+ test('an update moves the acquired version forward under the same entitlement', async () => {
132
+ const db = fakeDb();
133
+ await grantEntitlement({ ...B, source: 'purchase', paymentRef: 'pay_1' }, { db });
134
+ assert.equal((await recordAcquired({ ...B, version: '1.9.0' }, { db })).ok, true);
135
+ const up = await recordAcquired({ ...B, version: '1.10.0' }, { db });
136
+ assert.equal(up.ok, true, '1.10.0 is newer than 1.9.0 (numeric, not text, order)');
137
+ assert.equal(up.entitlement.version, '1.10.0');
138
+ assert.equal(up.entitlement.source, 'purchase', 'an update never rewrites how it was acquired');
139
+ assert.equal(db.rows.length, 1, 'no second purchase, no second row');
140
+ });
141
+
142
+ test('an older version is refused unless a rollback is asked for by name', async () => {
143
+ const db = fakeDb();
144
+ await grantEntitlement({ ...B, source: 'free' }, { db });
145
+ await recordAcquired({ ...B, version: '2.0.0' }, { db });
146
+ assert.deepEqual(await recordAcquired({ ...B, version: '1.5.0' }, { db }), { ok: false, code: 'version_older_than_held' });
147
+ assert.equal((await recordAcquired({ ...B, version: '1.5.0', allowDowngrade: true }, { db })).entitlement.version, '1.5.0');
148
+ });
149
+
150
+ test('a version that is not exact X.Y.Z is a caller error', async () => {
151
+ const db = fakeDb();
152
+ await grantEntitlement({ ...B, source: 'free' }, { db });
153
+ await assert.rejects(recordAcquired({ ...B, version: '1.0' }, { db }), /exact X\.Y\.Z/);
154
+ await assert.rejects(recordAcquired({ ...B, version: '^1.0.0' }, { db }), /exact X\.Y\.Z/);
155
+ });
156
+
157
+ test('revoking keeps the row, stamps why, and stops the entitlement; a re-grant lifts it', async () => {
158
+ const db = fakeDb();
159
+ await grantEntitlement({ ...B, source: 'purchase' }, { db });
160
+ await recordAcquired({ ...B, version: '1.0.0' }, { db });
161
+ await assert.rejects(revokeEntitlement({ ...B, reason: ' ' }, { db }), /needs a reason/);
162
+ const r = await revokeEntitlement({ ...B, reason: 'refunded' }, { db });
163
+ assert.equal(r.ok, true);
164
+ assert.equal(db.rows.length, 1, 'kept, never deleted');
165
+ const held = await checkEntitlement(B, { db });
166
+ assert.equal(held.entitled, false);
167
+ assert.equal(held.entitlement.revoked_reason, 'refunded');
168
+ assert.deepEqual(await recordAcquired({ ...B, version: '1.1.0' }, { db }), { ok: false, code: 'not_entitled' });
169
+ assert.deepEqual(await revokeEntitlement({ ...B, reason: 'again' }, { db }), { ok: false, code: 'not_entitled' });
170
+ const back = await grantEntitlement({ ...B, source: 'grant' }, { db });
171
+ assert.equal(back.reinstated, true);
172
+ assert.equal((await checkEntitlement(B, { db })).entitled, true);
173
+ assert.equal(back.entitlement.version, '1.0.0', 'what was acquired is remembered');
174
+ });
175
+
176
+ test('holdings are per holder: one builder never sees another\'s', async () => {
177
+ const db = fakeDb();
178
+ await grantEntitlement({ ...B, source: 'free' }, { db });
179
+ await grantEntitlement({ ...B, holderRef: '43', source: 'free' }, { db });
180
+ await grantEntitlement({ ...B, holderKind: 'instance', holderRef: '42', source: 'free' }, { db });
181
+ const mine = await listEntitlements({ holderKind: 'builder', holderRef: '42' }, { db });
182
+ assert.equal(mine.length, 1);
183
+ assert.equal(mine[0].holder_ref, '42');
184
+ });
185
+
186
+ test('bad arguments throw rather than write', async () => {
187
+ const db = fakeDb();
188
+ await assert.rejects(grantEntitlement({ ...B, moduleKey: '../x', source: 'free' }, { db }), /bad module key/);
189
+ await assert.rejects(grantEntitlement({ ...B, holderKind: 'team', source: 'free' }, { db }), /holder_kind/);
190
+ await assert.rejects(grantEntitlement({ ...B, source: 'gift' }, { db }), /source must be/);
191
+ assert.equal(db.rows.length, 0);
192
+ });
193
+
194
+ // ---- the read routes ----
195
+
196
+ test('the read routes are own-scoped: gated on requireBuilder, holder taken from the session', () => {
197
+ const src = readFileSync(path.join(ROOT, 'src/bongos/routes/modules.js'), 'utf8');
198
+ for (const p of ['/store/entitlements', '/store/modules/:key/entitlement']) {
199
+ assert.match(src, new RegExp(`router\\.get\\('${p.replace(/[/:.]/g, '\\$&')}', auth\\.requireBuilder,`));
200
+ }
201
+ const block = src.slice(src.indexOf("router.get('/store/entitlements'"));
202
+ assert.match(block, /holderRef: String\(req\.builder\.id\)/);
203
+ assert.doesNotMatch(block.slice(0, 1600), /req\.(query|body)\./, 'the holder is never named by the caller');
204
+ });
@@ -0,0 +1,224 @@
1
+ // tests/module_install.mjs — `bongos module install <key>` (task 1003785, ADR 0338 D1).
2
+ // No network, no database: cmdInstall runs against a fake store built from a REAL
3
+ // packed tarball (scripts/gds/module-artifact.js packModule), so verification is the
4
+ // real verifyModuleArtifact. The store's read side (src/bongos/module-store.js) runs
5
+ // against a fake pg client, and the three routes are pinned in their source.
6
+ //
7
+ // Run: node tests/module_install.mjs
8
+
9
+ import { test } from 'node:test';
10
+ import { strict as assert } from 'node:assert';
11
+ import { createRequire } from 'node:module';
12
+ import fs from 'node:fs';
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+
17
+ const require = createRequire(import.meta.url);
18
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
19
+ const { packModule, verifyModuleArtifact } = require('../scripts/gds/module-artifact.js');
20
+ const { readTar, buildTar } = require('../scripts/gds/artifact-format.js');
21
+ const { cmdInstall } = require('../scripts/gds/module.js');
22
+ const { isFreePrice, resolveAcquirable, versionArtifactFile, relativeArtifactPath } = require('../src/bongos/module-store.js');
23
+
24
+ const NOW = () => new Date('2026-09-29T00:00:00Z');
25
+
26
+ // A real packed tarball for one module, built from a temp source tree.
27
+ async function packed(mj = {}) {
28
+ const src = fs.mkdtempSync(path.join(os.tmpdir(), 'mod-install-src-'));
29
+ const manifest = { key: 'weather', title: 'Weather', description: 'Shows the weather.', version: '1.2.0', coreVersion: '^1.0.0', contributes: {}, ...mj };
30
+ const dir = path.join(src, manifest.key);
31
+ fs.mkdirSync(path.join(dir, 'routes'), { recursive: true });
32
+ fs.writeFileSync(path.join(dir, 'module.json'), JSON.stringify(manifest, null, 2));
33
+ fs.writeFileSync(path.join(dir, 'routes', 'weather.js'), 'module.exports = () => null;\n');
34
+ const { tgz } = packModule(manifest.key, { modulesDir: src, now: NOW, modeOf: () => '644' });
35
+ const v = await verifyModuleArtifact(tgz, { key: manifest.key });
36
+ assert.ok(v.ok, 'the fixture packs cleanly');
37
+ return { tgz, v };
38
+ }
39
+
40
+ // A fake store: answers the three install calls and records what it was asked.
41
+ function fakeStore({ tgz, v }, over = {}) {
42
+ const calls = [];
43
+ const describe = {
44
+ ok: true, module_key: v.moduleJson.key, version: v.version, core_version: v.coreVersion,
45
+ tarball_sha256: v.tarballSha256, tree_sha256: v.treeSha256, tarball_bytes: v.bytes, channel: 'general',
46
+ };
47
+ const request = async (method, urlPath, opts = {}) => {
48
+ calls.push({ method, urlPath, json: opts.json });
49
+ if (urlPath.endsWith('/acquire')) return over.acquire || { ok: true, status: 200, data: { ...describe, ...(over.describe || {}) } };
50
+ if (urlPath.endsWith('/tarball')) return over.download || { ok: true, status: 200, buf: over.buf || tgz };
51
+ if (urlPath.endsWith('/acquired')) return over.acquired || { ok: true, status: 200, data: { ok: true } };
52
+ throw new Error(`unexpected ${method} ${urlPath}`);
53
+ };
54
+ return { calls, request };
55
+ }
56
+
57
+ const dest = () => fs.mkdtempSync(path.join(os.tmpdir(), 'mod-install-dest-'));
58
+ const quiet = () => { const out = []; return { out, log: (m) => out.push(m), errlog: (m) => out.push(m) }; };
59
+
60
+ // ---- cmdInstall ----
61
+
62
+ test('installs a free module: verified, placed, recorded, and NOT enabled', async () => {
63
+ const p = await packed();
64
+ const store = fakeStore(p);
65
+ const modulesDir = dest();
66
+ const q = quiet();
67
+ const code = await cmdInstall(['weather'], { ...q, modulesDir, request: store.request, core: '1.19.0' });
68
+ assert.equal(code, 0, q.out.join('\n'));
69
+ const placed = path.join(modulesDir, 'weather');
70
+ assert.equal(JSON.parse(fs.readFileSync(path.join(placed, 'module.json'), 'utf8')).version, '1.2.0');
71
+ assert.equal(fs.readFileSync(path.join(placed, 'routes', 'weather.js'), 'utf8'), 'module.exports = () => null;\n');
72
+ assert.ok(!fs.existsSync(path.join(placed, '.bongos-module.json')), 'the store manifest is not placed as a module file');
73
+ assert.deepEqual(fs.readdirSync(modulesDir), ['weather'], 'no staging dir left behind, nothing else written');
74
+ assert.deepEqual(store.calls.map((c) => c.method + ' ' + c.urlPath), [
75
+ 'POST /store/modules/weather/acquire',
76
+ 'GET /store/modules/weather/versions/1.2.0/tarball',
77
+ 'POST /store/modules/weather/acquired',
78
+ ]);
79
+ assert.deepEqual(store.calls[2].json, { version: '1.2.0' }, 'records the exact version placed');
80
+ assert.match(q.out.join('\n'), /not switched on/);
81
+ });
82
+
83
+ test('--version asks the store for that exact version; a loose one is refused before any call', async () => {
84
+ const p = await packed();
85
+ const store = fakeStore(p);
86
+ const q = quiet();
87
+ await cmdInstall(['weather', '--version', '1.2.0'], { ...q, modulesDir: dest(), request: store.request, core: '1.19.0' });
88
+ assert.deepEqual(store.calls[0].json, { version: '1.2.0' });
89
+ const store2 = fakeStore(p);
90
+ assert.equal(await cmdInstall(['weather', '--version', '^1.2.0'], { ...quiet(), modulesDir: dest(), request: store2.request }), 2);
91
+ assert.equal(store2.calls.length, 0);
92
+ });
93
+
94
+ test('a module already on disk is refused, and the store is never asked', async () => {
95
+ const p = await packed();
96
+ const store = fakeStore(p);
97
+ const modulesDir = dest();
98
+ fs.mkdirSync(path.join(modulesDir, 'weather'));
99
+ assert.equal(await cmdInstall(['weather'], { ...quiet(), modulesDir, request: store.request }), 1);
100
+ assert.equal(store.calls.length, 0);
101
+ });
102
+
103
+ test('a tampered download fails verification: nothing placed, nothing recorded', async () => {
104
+ const p = await packed();
105
+ const entries = readTar(p.tgz);
106
+ const route = entries.find((e) => e.name.endsWith('routes/weather.js'));
107
+ route.buf = Buffer.from('require("child_process").execSync("evil");\n');
108
+ const store = fakeStore(p, { buf: buildTar(entries) });
109
+ const modulesDir = dest();
110
+ const q = quiet();
111
+ assert.equal(await cmdInstall(['weather'], { ...q, modulesDir, request: store.request, core: '1.19.0' }), 1);
112
+ assert.match(q.out.join('\n'), /failed verification \(hash_mismatch\)/);
113
+ assert.deepEqual(fs.readdirSync(modulesDir), []);
114
+ assert.ok(!store.calls.some((c) => c.urlPath.endsWith('/acquired')));
115
+ });
116
+
117
+ test('a download that verifies but is not the version the store described is refused', async () => {
118
+ const p = await packed();
119
+ const other = await packed({ version: '1.3.0' });
120
+ const store = fakeStore(p, { buf: other.tgz });
121
+ const modulesDir = dest();
122
+ assert.equal(await cmdInstall(['weather'], { ...quiet(), modulesDir, request: store.request, core: '1.19.0' }), 1);
123
+ assert.deepEqual(fs.readdirSync(modulesDir), []);
124
+ });
125
+
126
+ test('a priced module the caller does not hold is refused with the store\'s reason', async () => {
127
+ const p = await packed();
128
+ const store = fakeStore(p, { acquire: { ok: false, status: 402, data: { error: { code: 'payment_required', message: 'buying is not available yet' } } } });
129
+ const modulesDir = dest();
130
+ const q = quiet();
131
+ assert.equal(await cmdInstall(['weather'], { ...q, modulesDir, request: store.request }), 1);
132
+ assert.match(q.out.join('\n'), /HTTP 402\): buying is not available yet/);
133
+ assert.equal(store.calls.length, 1, 'no download is attempted');
134
+ assert.deepEqual(fs.readdirSync(modulesDir), []);
135
+ });
136
+
137
+ test('a module this core cannot mount is refused before download', async () => {
138
+ const p = await packed({ coreVersion: '^2.0.0' });
139
+ const store = fakeStore(p);
140
+ assert.equal(await cmdInstall(['weather'], { ...quiet(), modulesDir: dest(), request: store.request, core: '1.19.0' }), 1);
141
+ assert.equal(store.calls.length, 1);
142
+ });
143
+
144
+ test('a failed record after placing keeps the files and says so', async () => {
145
+ const p = await packed();
146
+ const store = fakeStore(p, { acquired: { ok: false, status: 500, data: { error: { message: 'db down' } } } });
147
+ const modulesDir = dest();
148
+ const q = quiet();
149
+ assert.equal(await cmdInstall(['weather'], { ...q, modulesDir, request: store.request, core: '1.19.0' }), 1);
150
+ assert.ok(fs.existsSync(path.join(modulesDir, 'weather', 'module.json')));
151
+ assert.match(q.out.join('\n'), /installed, but the store did not record the version \(db down\)/);
152
+ });
153
+
154
+ // ---- verifyModuleArtifact includeFiles ----
155
+
156
+ test('includeFiles returns the module files from the checked parse, manifest excluded', async () => {
157
+ const { tgz } = await packed();
158
+ const v = await verifyModuleArtifact(tgz, { key: 'weather', includeFiles: true });
159
+ assert.deepEqual(v.files.map((f) => f.path).sort(), ['module.json', 'routes/weather.js']);
160
+ const plain = await verifyModuleArtifact(tgz, { key: 'weather' });
161
+ assert.equal(plain.files, undefined, 'publish keeps its old shape');
162
+ });
163
+
164
+ // ---- the store's read side ----
165
+
166
+ test('an unpriced module is free (owner decision 2026-09-29); a zero in either currency is free', () => {
167
+ assert.equal(isFreePrice(null), true);
168
+ assert.equal(isFreePrice({ price_cents: 0, price_credits: null }), true);
169
+ assert.equal(isFreePrice({ price_cents: 500, price_credits: 0 }), true);
170
+ assert.equal(isFreePrice({ price_cents: 500, price_credits: null }), false);
171
+ assert.equal(isFreePrice({ price_cents: null, price_credits: 20 }), false);
172
+ });
173
+
174
+ function fakeDb({ mod, versions = [], price = null }) {
175
+ return {
176
+ async query(sql) {
177
+ if (sql.includes('FROM store_modules')) return { rows: mod ? [mod] : [] };
178
+ if (sql.includes('FROM store_module_versions')) return { rows: versions };
179
+ if (sql.includes('FROM store_module_prices')) return { rows: price ? [price] : [] };
180
+ throw new Error(`unhandled: ${sql}`);
181
+ },
182
+ };
183
+ }
184
+ const row = (version) => ({ id: 1, module_key: 'weather', version, core_version: '^1.0.0', tarball_sha256: 'a'.repeat(64), tree_sha256: 'b'.repeat(64), tarball_bytes: 10, artifact_path: `weather/weather-${version}.tgz` });
185
+
186
+ test('resolveAcquirable picks the newest version in semver order, not text order', async () => {
187
+ const r = await resolveAcquirable('weather', { db: fakeDb({ mod: { module_key: 'weather', status: 'listed' }, versions: [row('1.9.0'), row('1.10.0'), row('1.2.0')] }) });
188
+ assert.equal(r.ok, true);
189
+ assert.equal(r.version.version, '1.10.0');
190
+ assert.equal(r.free, true);
191
+ });
192
+
193
+ test('resolveAcquirable refuses an unknown module, a delisted one, a missing version, and a module with none', async () => {
194
+ assert.equal((await resolveAcquirable('weather', { db: fakeDb({}) })).code, 'module_not_found');
195
+ assert.equal((await resolveAcquirable('weather', { db: fakeDb({ mod: { status: 'delisted' }, versions: [row('1.0.0')] }) })).code, 'module_delisted');
196
+ assert.equal((await resolveAcquirable('weather', { version: '2.0.0', db: fakeDb({ mod: { status: 'listed' }, versions: [row('1.0.0')] }) })).code, 'version_not_found');
197
+ assert.equal((await resolveAcquirable('weather', { db: fakeDb({ mod: { status: 'listed' } }) })).code, 'no_versions');
198
+ const priced = await resolveAcquirable('weather', { db: fakeDb({ mod: { status: 'listed' }, versions: [row('1.0.0')], price: { price_cents: 900, price_credits: null } }) });
199
+ assert.equal(priced.free, false);
200
+ });
201
+
202
+ test('versionArtifactFile resolves inside the store dir and refuses a row naming another path', () => {
203
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mod-store-'));
204
+ const f = versionArtifactFile(row('1.0.0'), { dir });
205
+ assert.equal(relativeArtifactPath(f, { dir }), 'weather/weather-1.0.0.tgz');
206
+ assert.throws(() => versionArtifactFile({ ...row('1.0.0'), artifact_path: '../../etc/passwd' }, { dir }), /unexpected artifact path/);
207
+ });
208
+
209
+ // ---- the routes ----
210
+
211
+ test('the install routes are own-scoped and the download checks the entitlement before reading the file', () => {
212
+ const src = fs.readFileSync(path.join(ROOT, 'src/bongos/routes/modules.js'), 'utf8');
213
+ for (const decl of [
214
+ "router.post('/store/modules/:key/acquire', auth.requireBuilder,",
215
+ "router.get('/store/modules/:key/versions/:version/tarball', auth.requireBuilder,",
216
+ "router.post('/store/modules/:key/acquired', auth.requireBuilder,",
217
+ ]) assert.ok(src.includes(decl), decl);
218
+ const start = src.indexOf("router.post('/store/modules/:key/acquire'");
219
+ const block = src.slice(start, src.indexOf('return router;', start));
220
+ assert.doesNotMatch(block, /holderRef: (?!String\(req\.builder\.id\))/, 'the holder is always the caller');
221
+ const dl = block.slice(block.indexOf('/tarball'));
222
+ assert.ok(dl.indexOf('checkEntitlement') < dl.indexOf('readFile'), 'no bytes are read for a caller who does not hold it');
223
+ assert.match(block, /if \(!r\.free\)[\s\S]*?payment_required[\s\S]*?status: 402/, 'a priced module is not granted for free');
224
+ });