@bongos/core 1.23.4 → 1.23.6
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 +67 -42
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +9 -1
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/adr/0356-a-recruit-invite-expires-30-days-after-it-is-sent-read-time.md +2 -1
- package/docs/api/openapi.json +204 -7
- package/docs/api-reference.md +4 -2
- package/docs/copy-inventory.md +26 -24
- package/docs/copy-registry.json +43 -25
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +2 -0
- package/docs/page-readings.json +8 -8
- package/migrations/core_281_module_version_withdraw_note.sql +25 -0
- package/modules/government/catalog.js +1 -1
- package/modules/hall-ui/public/modules.js +10 -1
- package/modules/onboarding/routes/access-requests.js +10 -9
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/scripts/bongos/login.js +13 -14
- package/scripts/bongos/module-store-cli.js +182 -0
- package/scripts/bongos/module.js +60 -134
- package/scripts/gds/module-store-cli.js +6 -0
- package/src/bongos/auth-admission.js +3 -3
- package/src/bongos/middleware/rate-limit.js +1 -1
- package/src/bongos/module-listing.js +12 -2
- package/src/bongos/module-store.js +90 -1
- package/src/bongos/route-rank-check.js +6 -0
- package/src/bongos/routes/auth.js +7 -11
- package/src/bongos/routes/modules.js +76 -3
- package/src/module-api.js +1 -1
- package/tests/account_existence_rate_limit.mjs +7 -3
- package/tests/application_lifecycle.mjs +4 -3
- package/tests/bongos_login.mjs +15 -4
- package/tests/hall_catalog_world.mjs +1 -1
- package/tests/invite_expiry_db.mjs +1 -1
- package/tests/module_store_cli.mjs +1 -1
- package/tests/module_store_price.mjs +207 -0
- package/tests/module_store_withdraw.mjs +201 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
//
|
|
3
|
+
// scripts/bongos/module-store-cli.js — the module STORE from the terminal: the store
|
|
4
|
+
// client every store verb calls, and `bongos module store` / `bongos module price`
|
|
5
|
+
// (tasks 1003811, 1004894). Carved out of scripts/bongos/module.js, which requires
|
|
6
|
+
// and re-exports all of it, so `bongos module <verb>` and every importer are
|
|
7
|
+
// unchanged.
|
|
8
|
+
//
|
|
9
|
+
// WHY its own file. module.js crossed the 1,500-line ratchet (fitness.js) when the
|
|
10
|
+
// price command landed, and these pieces share one concern — talking to the store —
|
|
11
|
+
// while the rest of module.js works on the checkout's own modules/ tree.
|
|
12
|
+
//
|
|
13
|
+
// Like module.js it may be required by the server process, so cli-lib's
|
|
14
|
+
// session/fetch plumbing is required lazily inside storeRequest, never at load.
|
|
15
|
+
|
|
16
|
+
const { KEY_RE } = require('../../src/module-loader/manifest-schema');
|
|
17
|
+
|
|
18
|
+
// The store half of the CLI, over three calls (src/bongos/routes/modules.js):
|
|
19
|
+
// acquire (take the entitlement, learn the version), download the tarball, and
|
|
20
|
+
// once the files are placed, record the version acquired. Injected in tests.
|
|
21
|
+
function storeRequest() {
|
|
22
|
+
// Lazy require — cli-lib's session/fetch plumbing must never load in the server
|
|
23
|
+
// process, which requires this file too (the same reason cmdPublish gives).
|
|
24
|
+
const { readSessionToken, dispatchedFetch, noKeepAliveDispatcher, CLI_USER_AGENT } = require('./cli-lib');
|
|
25
|
+
const session = readSessionToken();
|
|
26
|
+
if (!session) throw new Error('no Bongos session — run `bongos setup` first');
|
|
27
|
+
const call = async (method, urlPath, { json, binary = false } = {}) => {
|
|
28
|
+
const res = await dispatchedFetch(`${session.apiBase}/api/bongos${urlPath}`, {
|
|
29
|
+
method,
|
|
30
|
+
headers: {
|
|
31
|
+
Authorization: `Bearer ${session.token}`, 'User-Agent': CLI_USER_AGENT,
|
|
32
|
+
...(json ? { 'Content-Type': 'application/json' } : {}),
|
|
33
|
+
},
|
|
34
|
+
body: json ? JSON.stringify(json) : undefined,
|
|
35
|
+
dispatcher: noKeepAliveDispatcher(),
|
|
36
|
+
});
|
|
37
|
+
if (binary && res.ok) return { ok: true, status: res.status, buf: Buffer.from(await res.arrayBuffer()) };
|
|
38
|
+
const text = await res.text();
|
|
39
|
+
let data;
|
|
40
|
+
try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; }
|
|
41
|
+
return { ok: res.ok, status: res.status, data };
|
|
42
|
+
};
|
|
43
|
+
// Where the store lives, so install can print a full link to the how-to page.
|
|
44
|
+
call.apiBase = session.apiBase;
|
|
45
|
+
return call;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The hall page that shows a version's HOWTO.md (task 1004366) — the Modules tab,
|
|
49
|
+
// opened on one version. null when the version shipped without one (published
|
|
50
|
+
// before ADR 0347's gate), so install never prints a link that would 404.
|
|
51
|
+
function howtoLink(key, version, files, apiBase) {
|
|
52
|
+
if (!files.some((f) => f.path === 'HOWTO.md')) return null;
|
|
53
|
+
const p = `/builders/modules?howto=${encodeURIComponent(key)}&version=${encodeURIComponent(version)}`;
|
|
54
|
+
return apiBase ? `${String(apiBase).replace(/\/+$/, '')}${p}` : p;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function storeError(res) {
|
|
58
|
+
const err = res.data && (res.data.error || res.data);
|
|
59
|
+
return (err && (err.message || err.code)) || `HTTP ${res.status}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// bongos module store — what the store lists (task 1003811)
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
const STORE_USAGE = 'usage: bongos module store [--sort <part>] [--max-cents N] [--max-credits N] [--json]';
|
|
67
|
+
|
|
68
|
+
// The query GET /store/modules takes, from argv. Returns { query } or { error }.
|
|
69
|
+
// The server checks the values too; this only refuses what is not a value at all.
|
|
70
|
+
function storeQuery(args) {
|
|
71
|
+
const params = [];
|
|
72
|
+
for (const [flag, param] of [['--sort', 'sort'], ['--max-cents', 'max_cents'], ['--max-credits', 'max_credits']]) {
|
|
73
|
+
const i = args.indexOf(flag);
|
|
74
|
+
if (i === -1) continue;
|
|
75
|
+
const v = args[i + 1];
|
|
76
|
+
if (!v || v.startsWith('--')) return { error: `${flag} needs a value` };
|
|
77
|
+
params.push(`${param}=${encodeURIComponent(v)}`);
|
|
78
|
+
}
|
|
79
|
+
return { query: params.length ? `?${params.join('&')}` : '' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function formatPrice(price) {
|
|
83
|
+
if (!price || price.free) return 'free';
|
|
84
|
+
const parts = [];
|
|
85
|
+
if (price.cents != null) parts.push(`$${(price.cents / 100).toFixed(2)}`);
|
|
86
|
+
if (price.credits != null) parts.push(`${price.credits} credits`);
|
|
87
|
+
return parts.join(' or ') || 'free';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function formatPlace(category) {
|
|
91
|
+
if (!category) return '';
|
|
92
|
+
return category.label ? `${category.parent_title} › ${category.label}` : category.parent_title;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Pure renderer over the GET /store/modules body. It keeps the server's order and
|
|
96
|
+
// adds nothing to it: listed in rank, then New, never ranked.
|
|
97
|
+
function formatStore(body) {
|
|
98
|
+
const out = [];
|
|
99
|
+
out.push('bongos module store — what the store lists, best first');
|
|
100
|
+
out.push(` floor ${body.floor} out of 100 · at most ${body.cap} per sub-category`);
|
|
101
|
+
out.push(` ${body.ranking}`);
|
|
102
|
+
if (body.sort) {
|
|
103
|
+
const part = (body.sort_parts || []).find((x) => x.part === body.sort);
|
|
104
|
+
out.push(` sorted by ${part ? part.title : body.sort}`);
|
|
105
|
+
}
|
|
106
|
+
const max = body.max_price;
|
|
107
|
+
if (max) out.push(` at most ${[max.cents != null ? `$${(max.cents / 100).toFixed(2)}` : null, max.credits != null ? `${max.credits} credits` : null].filter(Boolean).join(' or ')}`);
|
|
108
|
+
out.push('');
|
|
109
|
+
const listed = body.listed || [];
|
|
110
|
+
if (!listed.length) out.push(' Nothing is listed yet.');
|
|
111
|
+
listed.forEach((m, i) => {
|
|
112
|
+
out.push(` ${String(i + 1).padStart(2)}. ${m.key}@${m.version} ${m.overall}/100 ${formatPrice(m.price)}${m.buyable === false ? ' (not yet buyable)' : ''}${m.category ? ` ${formatPlace(m.category)}` : ''}`);
|
|
113
|
+
});
|
|
114
|
+
const fresh = body.new || [];
|
|
115
|
+
if (fresh.length) {
|
|
116
|
+
out.push('');
|
|
117
|
+
out.push(' New — not ranked until projects have installed and used them:');
|
|
118
|
+
for (const m of fresh) out.push(` ${m.key}@${m.version} ${formatPrice(m.price)}${m.buyable === false ? ' (not yet buyable)' : ''}${m.category ? ` ${formatPlace(m.category)}` : ''}`);
|
|
119
|
+
}
|
|
120
|
+
out.push('');
|
|
121
|
+
out.push(' Why a module is not here: GET /store/modules/<key>/listing');
|
|
122
|
+
return out.join('\n');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function cmdStore(args, { log = console.log, errlog = console.error, request } = {}) {
|
|
126
|
+
const q = storeQuery(args);
|
|
127
|
+
if (q.error) { errlog(`bongos module store: ${q.error}`); errlog(STORE_USAGE); return 2; }
|
|
128
|
+
let call;
|
|
129
|
+
try { call = request || storeRequest(); }
|
|
130
|
+
catch (e) { errlog(`bongos module store: ${e.message}`); return 1; }
|
|
131
|
+
let res;
|
|
132
|
+
try { res = await call('GET', `/store/modules${q.query}`); }
|
|
133
|
+
catch (e) { errlog(`bongos module store: the store could not be reached — ${e.message}`); return 1; }
|
|
134
|
+
if (!res.ok) { errlog(`bongos module store: the store answered ${storeError(res)}`); return res.status === 400 ? 2 : 1; }
|
|
135
|
+
if (args.includes('--json')) { log(JSON.stringify(res.data, null, 2)); return 0; }
|
|
136
|
+
log(formatStore(res.data || {}));
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// bongos module price — the author sets a module's price (task 1004894)
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
const PRICE_USAGE = 'usage: bongos module price <key> [--cents N] [--credits N] (0 in either makes it free)';
|
|
145
|
+
|
|
146
|
+
// argv -> { key, cents, credits } or { error }. Pure.
|
|
147
|
+
function priceArgs(args) {
|
|
148
|
+
const key = args[0];
|
|
149
|
+
if (!key || key.startsWith('--') || !KEY_RE.test(key)) return { error: 'name the module: a lowercase kebab-case key' };
|
|
150
|
+
const out = { key, cents: null, credits: null };
|
|
151
|
+
for (const [flag, field] of [['--cents', 'cents'], ['--credits', 'credits']]) {
|
|
152
|
+
const i = args.indexOf(flag);
|
|
153
|
+
if (i === -1) continue;
|
|
154
|
+
const v = args[i + 1];
|
|
155
|
+
if (!v || !/^\d{1,9}$/.test(v)) return { error: `${flag} needs a whole number, 0 or more` };
|
|
156
|
+
out[field] = Number(v);
|
|
157
|
+
}
|
|
158
|
+
if (out.cents === null && out.credits === null) return { error: 'give --cents, --credits or both' };
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function cmdPrice(args, { log = console.log, errlog = console.error, request } = {}) {
|
|
163
|
+
const p = priceArgs(args);
|
|
164
|
+
if (p.error) { errlog(`bongos module price: ${p.error}`); errlog(PRICE_USAGE); return 2; }
|
|
165
|
+
let call;
|
|
166
|
+
try { call = request || storeRequest(); }
|
|
167
|
+
catch (e) { errlog(`bongos module price: ${e.message}`); return 1; }
|
|
168
|
+
let res;
|
|
169
|
+
try { res = await call('POST', `/store/modules/${encodeURIComponent(p.key)}/price`, { json: { cents: p.cents, credits: p.credits } }); }
|
|
170
|
+
catch (e) { errlog(`bongos module price: the store could not be reached — ${e.message}`); return 1; }
|
|
171
|
+
if (!res.ok) { errlog(`bongos module price: the store answered ${storeError(res)}`); return res.status === 400 ? 2 : 1; }
|
|
172
|
+
const withFree = (p) => ({ ...p, free: p.cents === 0 || p.credits === 0 });
|
|
173
|
+
const shown = formatPrice(withFree(res.data.price));
|
|
174
|
+
const was = res.data.previous ? formatPrice(withFree(res.data.previous)) : 'free (no price set)';
|
|
175
|
+
log(`✓ "${p.key}" now costs ${shown} (was ${was}).`);
|
|
176
|
+
if (shown !== 'free') log(' Buying is not open yet, so the store lists it as not yet buyable until it is.');
|
|
177
|
+
return 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
storeRequest, storeError, howtoLink, formatStore, cmdStore, priceArgs, cmdPrice,
|
|
182
|
+
};
|
package/scripts/bongos/module.js
CHANGED
|
@@ -39,6 +39,12 @@
|
|
|
39
39
|
// sub-category live in one place (src/bongos/module-listing.js), so the hall
|
|
40
40
|
// and the terminal cannot disagree about which modules are the best.
|
|
41
41
|
//
|
|
42
|
+
// bongos module price <key> [--cents N] [--credits N]
|
|
43
|
+
// Task 1004894 — the module's author sets its price in the store, in money
|
|
44
|
+
// (cents), credits or both; 0 in either makes it free. The store keeps every
|
|
45
|
+
// change and shows the newest. A paid module is listed as not yet buyable until
|
|
46
|
+
// buying arrives (task 1003788).
|
|
47
|
+
//
|
|
42
48
|
// bongos module check <key>
|
|
43
49
|
// ADR 0107 §4 — the submit-time pre-check a module must clear before it can
|
|
44
50
|
// be upstreamed into the shared core catalog. REUSES (never re-types) the
|
|
@@ -82,6 +88,8 @@ const { validateManifest, assertValidManifest, KEY_RE, SUNSET_STATUSES } = requi
|
|
|
82
88
|
const { parseVersion, cmp } = require('../../src/module-loader/semver');
|
|
83
89
|
const loader = require('../../src/module-loader/loader');
|
|
84
90
|
const catalog = require('../../src/module-loader/catalog');
|
|
91
|
+
// The store client and the `store` / `price` verbs (task 1004894 carve).
|
|
92
|
+
const { storeRequest, storeError, howtoLink, formatStore, cmdStore, priceArgs, cmdPrice } = require('./module-store-cli');
|
|
85
93
|
const { resolveModules, MODULE_REGISTRY, NEUTRAL_PATH, INSTANCE_PATH } = require('../../src/modules');
|
|
86
94
|
// The public-mirror publish boundary (ADR 0098) — REUSED, never re-typed, for
|
|
87
95
|
// the ADR 0107 §4 upstreaming pre-check. Only matchesDeny is used (not the full
|
|
@@ -712,7 +720,7 @@ async function gatherUpdates(rows, { request } = {}) {
|
|
|
712
720
|
// A 404 here is a server whose core predates this route, not a missing module.
|
|
713
721
|
if (res.status === 404) return { checked: false, reason: "this server's store does not answer update checks yet (its core is older)" };
|
|
714
722
|
if (!res.ok) return { checked: false, reason: `the store answered ${storeError(res)}` };
|
|
715
|
-
return { checked: true, latest: (res.data && res.data.latest) || {} };
|
|
723
|
+
return { checked: true, latest: (res.data && res.data.latest) || {}, withdrawn: (res.data && res.data.withdrawn) || {} };
|
|
716
724
|
}
|
|
717
725
|
|
|
718
726
|
// Decide what is stale from rows + the store's answer (pure — tests drive it).
|
|
@@ -722,13 +730,22 @@ async function gatherUpdates(rows, { request } = {}) {
|
|
|
722
730
|
// installed module the store has delisted: it keeps working, but no further
|
|
723
731
|
// versions will come (ADR 0338 D2). Neither ever blocks — an update is news, and
|
|
724
732
|
// what blocks `bongos upgrade` is still only an incompatible ENABLED module.
|
|
725
|
-
function evaluateUpdates(rows = [], latest = {}, core) {
|
|
733
|
+
function evaluateUpdates(rows = [], latest = {}, core, withdrawnByKey = {}) {
|
|
726
734
|
const { satisfies } = require('../../src/module-loader/semver');
|
|
727
735
|
const updates = [];
|
|
728
736
|
const delisted = [];
|
|
737
|
+
const withdrawn = [];
|
|
729
738
|
for (const r of rows) {
|
|
730
739
|
const store = latest[r.key];
|
|
731
740
|
const have = r.version && parseVersion(String(r.version));
|
|
741
|
+
// WITHDRAWN (task 1004895) = the store took back the exact version installed
|
|
742
|
+
// here. It keeps working; the update below, when there is one, is the way off it.
|
|
743
|
+
const taken = have && (withdrawnByKey[r.key] || [])
|
|
744
|
+
.map((w) => (typeof w === 'string' ? { version: w, note: null } : w))
|
|
745
|
+
.find((w) => w && w.version === String(r.version));
|
|
746
|
+
if (taken) {
|
|
747
|
+
withdrawn.push({ key: r.key, installed: r.version, latest: store ? store.version : null, enabled: r.enabled, note: taken.note || null });
|
|
748
|
+
}
|
|
732
749
|
if (!store || !have) continue;
|
|
733
750
|
if (store.status === 'delisted') { delisted.push({ key: r.key, installed: r.version, enabled: r.enabled }); continue; }
|
|
734
751
|
const next = parseVersion(String(store.version));
|
|
@@ -737,7 +754,7 @@ function evaluateUpdates(rows = [], latest = {}, core) {
|
|
|
737
754
|
try { fits = !!(core && store.core_version && satisfies(core, store.core_version)); } catch { fits = false; }
|
|
738
755
|
updates.push({ key: r.key, installed: r.version, latest: store.version, coreVersion: store.core_version, enabled: r.enabled, fits });
|
|
739
756
|
}
|
|
740
|
-
return { updates, delisted };
|
|
757
|
+
return { updates, delisted, withdrawn };
|
|
741
758
|
}
|
|
742
759
|
|
|
743
760
|
function reportUpdates(found, verdict, { log, errlog }) {
|
|
@@ -746,7 +763,7 @@ function reportUpdates(found, verdict, { log, errlog }) {
|
|
|
746
763
|
log(` not checked — ${found.reason}. The compatibility check above still stands.`);
|
|
747
764
|
return;
|
|
748
765
|
}
|
|
749
|
-
const { updates, delisted } = evaluateUpdates(verdict.rows, found.latest, verdict.core);
|
|
766
|
+
const { updates, delisted, withdrawn } = evaluateUpdates(verdict.rows, found.latest, verdict.core, found.withdrawn);
|
|
750
767
|
for (const u of updates) {
|
|
751
768
|
const how = u.fits
|
|
752
769
|
? `run \`bongos module update ${u.key}\``
|
|
@@ -756,7 +773,17 @@ function reportUpdates(found, verdict, { log, errlog }) {
|
|
|
756
773
|
for (const d of delisted) {
|
|
757
774
|
errlog(` ! ${d.key.padEnd(18)} ${d.installed} — delisted in the store: it keeps working, but no further versions will come`);
|
|
758
775
|
}
|
|
759
|
-
|
|
776
|
+
for (const w of withdrawn) {
|
|
777
|
+
// Update only moves forward, so an OLDER served version is no way off it: until a
|
|
778
|
+
// fixed version is published, say so rather than name a command that refuses.
|
|
779
|
+
const newer = w.latest && parseVersion(String(w.latest))
|
|
780
|
+
&& cmp(parseVersion(String(w.latest)), parseVersion(String(w.installed))) > 0;
|
|
781
|
+
const next = newer
|
|
782
|
+
? `move to ${w.latest} with \`bongos module update ${w.key}\``
|
|
783
|
+
: 'no newer version is out yet; `bongos upgrade` will offer the fix once one is published';
|
|
784
|
+
errlog(` ! ${w.key.padEnd(18)} ${w.installed} — withdrawn in the store${w.note ? ` (${w.note})` : ''}: it keeps working here, but ${next}`);
|
|
785
|
+
}
|
|
786
|
+
if (!updates.length && !delisted.length && !withdrawn.length) log(' every installed store module is on its newest version.');
|
|
760
787
|
}
|
|
761
788
|
|
|
762
789
|
async function cmdUpgrade(args = [], { log = console.log, errlog = console.error, gather = gatherUpgrade, checkUpdates = gatherUpdates } = {}) {
|
|
@@ -939,18 +966,31 @@ async function cmdSubmit(args, {
|
|
|
939
966
|
// Distinct from `submit`, which proposes a module INTO core (ADR 0135).
|
|
940
967
|
async function cmdPublish(args, {
|
|
941
968
|
log = console.log, errlog = console.error,
|
|
942
|
-
modulesDir = MODULES_DIR, pack, upload, writeFile = fs.writeFileSync,
|
|
969
|
+
modulesDir = MODULES_DIR, pack, upload, writeFile = fs.writeFileSync, priceRequest,
|
|
943
970
|
} = {}) {
|
|
944
|
-
const
|
|
971
|
+
const PUBLISH_USAGE = 'usage: bongos module publish <key> [--dry-run] [--out <dir>] [--cents N] [--credits N]';
|
|
972
|
+
const VALUED = new Set(['--out', '--cents', '--credits']);
|
|
973
|
+
const key = args.find((a, i) => !a.startsWith('-') && !VALUED.has(args[i - 1]));
|
|
945
974
|
if (!key) {
|
|
946
|
-
errlog(
|
|
975
|
+
errlog(PUBLISH_USAGE);
|
|
947
976
|
return 2;
|
|
948
977
|
}
|
|
978
|
+
// An optional price set right after the upload (task 1004894), with the same rules
|
|
979
|
+
// as `bongos module price`, checked BEFORE anything is packed or sent.
|
|
980
|
+
const priceFlags = [];
|
|
981
|
+
for (const flag of ['--cents', '--credits']) {
|
|
982
|
+
const i = args.indexOf(flag);
|
|
983
|
+
if (i !== -1) priceFlags.push(flag, args[i + 1]);
|
|
984
|
+
}
|
|
985
|
+
if (priceFlags.length) {
|
|
986
|
+
const p = priceArgs([key, ...priceFlags]);
|
|
987
|
+
if (p.error) { errlog(`bongos module publish: ${p.error}`); errlog(PUBLISH_USAGE); return 2; }
|
|
988
|
+
}
|
|
949
989
|
const dryRun = args.includes('--dry-run');
|
|
950
990
|
const outIdx = args.indexOf('--out');
|
|
951
991
|
const outDir = outIdx !== -1 ? args[outIdx + 1] : null;
|
|
952
992
|
if (outIdx !== -1 && (!outDir || outDir.startsWith('--'))) {
|
|
953
|
-
errlog(
|
|
993
|
+
errlog(PUBLISH_USAGE);
|
|
954
994
|
return 2;
|
|
955
995
|
}
|
|
956
996
|
|
|
@@ -1018,135 +1058,18 @@ async function cmdPublish(args, {
|
|
|
1018
1058
|
if (res.data && res.data.channel === 'tester') {
|
|
1019
1059
|
log(' It goes to testing projects first, and reaches every project once it has been tested (ADR 0344).');
|
|
1020
1060
|
}
|
|
1021
|
-
return 0;
|
|
1061
|
+
if (!priceFlags.length) return 0;
|
|
1062
|
+
// The version is published whatever happens next; a price that fails is reported
|
|
1063
|
+
// with the one command that sets it, and the exit code says something needs doing.
|
|
1064
|
+
const priced = await cmdPrice([key, ...priceFlags], { log, errlog, request: priceRequest });
|
|
1065
|
+
if (priced !== 0) errlog(` The version is published, but its price was not set. Run \`bongos module price ${key} ${priceFlags.join(' ')}\` to set it.`);
|
|
1066
|
+
return priced === 0 ? 0 : 1;
|
|
1022
1067
|
}
|
|
1023
1068
|
|
|
1024
1069
|
// ---------------------------------------------------------------------------
|
|
1025
1070
|
// bongos module install <key> — acquire a module from the store (ADR 0338 D1)
|
|
1026
1071
|
// ---------------------------------------------------------------------------
|
|
1027
1072
|
|
|
1028
|
-
// The store half of the CLI, over three calls (src/bongos/routes/modules.js):
|
|
1029
|
-
// acquire (take the entitlement, learn the version), download the tarball, and
|
|
1030
|
-
// once the files are placed, record the version acquired. Injected in tests.
|
|
1031
|
-
function storeRequest() {
|
|
1032
|
-
// Lazy require — cli-lib's session/fetch plumbing must never load in the server
|
|
1033
|
-
// process, which requires this file too (the same reason cmdPublish gives).
|
|
1034
|
-
const { readSessionToken, dispatchedFetch, noKeepAliveDispatcher, CLI_USER_AGENT } = require('./cli-lib');
|
|
1035
|
-
const session = readSessionToken();
|
|
1036
|
-
if (!session) throw new Error('no Bongos session — run `bongos setup` first');
|
|
1037
|
-
const call = async (method, urlPath, { json, binary = false } = {}) => {
|
|
1038
|
-
const res = await dispatchedFetch(`${session.apiBase}/api/bongos${urlPath}`, {
|
|
1039
|
-
method,
|
|
1040
|
-
headers: {
|
|
1041
|
-
Authorization: `Bearer ${session.token}`, 'User-Agent': CLI_USER_AGENT,
|
|
1042
|
-
...(json ? { 'Content-Type': 'application/json' } : {}),
|
|
1043
|
-
},
|
|
1044
|
-
body: json ? JSON.stringify(json) : undefined,
|
|
1045
|
-
dispatcher: noKeepAliveDispatcher(),
|
|
1046
|
-
});
|
|
1047
|
-
if (binary && res.ok) return { ok: true, status: res.status, buf: Buffer.from(await res.arrayBuffer()) };
|
|
1048
|
-
const text = await res.text();
|
|
1049
|
-
let data;
|
|
1050
|
-
try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; }
|
|
1051
|
-
return { ok: res.ok, status: res.status, data };
|
|
1052
|
-
};
|
|
1053
|
-
// Where the store lives, so install can print a full link to the how-to page.
|
|
1054
|
-
call.apiBase = session.apiBase;
|
|
1055
|
-
return call;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
// The hall page that shows a version's HOWTO.md (task 1004366) — the Modules tab,
|
|
1059
|
-
// opened on one version. null when the version shipped without one (published
|
|
1060
|
-
// before ADR 0347's gate), so install never prints a link that would 404.
|
|
1061
|
-
function howtoLink(key, version, files, apiBase) {
|
|
1062
|
-
if (!files.some((f) => f.path === 'HOWTO.md')) return null;
|
|
1063
|
-
const p = `/builders/modules?howto=${encodeURIComponent(key)}&version=${encodeURIComponent(version)}`;
|
|
1064
|
-
return apiBase ? `${String(apiBase).replace(/\/+$/, '')}${p}` : p;
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
function storeError(res) {
|
|
1068
|
-
const err = res.data && (res.data.error || res.data);
|
|
1069
|
-
return (err && (err.message || err.code)) || `HTTP ${res.status}`;
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
// ---------------------------------------------------------------------------
|
|
1073
|
-
// bongos module store — what the store lists (task 1003811)
|
|
1074
|
-
// ---------------------------------------------------------------------------
|
|
1075
|
-
|
|
1076
|
-
const STORE_USAGE = 'usage: bongos module store [--sort <part>] [--max-cents N] [--max-credits N] [--json]';
|
|
1077
|
-
|
|
1078
|
-
// The query GET /store/modules takes, from argv. Returns { query } or { error }.
|
|
1079
|
-
// The server checks the values too; this only refuses what is not a value at all.
|
|
1080
|
-
function storeQuery(args) {
|
|
1081
|
-
const params = [];
|
|
1082
|
-
for (const [flag, param] of [['--sort', 'sort'], ['--max-cents', 'max_cents'], ['--max-credits', 'max_credits']]) {
|
|
1083
|
-
const i = args.indexOf(flag);
|
|
1084
|
-
if (i === -1) continue;
|
|
1085
|
-
const v = args[i + 1];
|
|
1086
|
-
if (!v || v.startsWith('--')) return { error: `${flag} needs a value` };
|
|
1087
|
-
params.push(`${param}=${encodeURIComponent(v)}`);
|
|
1088
|
-
}
|
|
1089
|
-
return { query: params.length ? `?${params.join('&')}` : '' };
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
function formatPrice(price) {
|
|
1093
|
-
if (!price || price.free) return 'free';
|
|
1094
|
-
const parts = [];
|
|
1095
|
-
if (price.cents != null) parts.push(`$${(price.cents / 100).toFixed(2)}`);
|
|
1096
|
-
if (price.credits != null) parts.push(`${price.credits} credits`);
|
|
1097
|
-
return parts.join(' or ') || 'free';
|
|
1098
|
-
}
|
|
1099
|
-
|
|
1100
|
-
function formatPlace(category) {
|
|
1101
|
-
if (!category) return '';
|
|
1102
|
-
return category.label ? `${category.parent_title} › ${category.label}` : category.parent_title;
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
// Pure renderer over the GET /store/modules body. It keeps the server's order and
|
|
1106
|
-
// adds nothing to it: listed in rank, then New, never ranked.
|
|
1107
|
-
function formatStore(body) {
|
|
1108
|
-
const out = [];
|
|
1109
|
-
out.push('bongos module store — what the store lists, best first');
|
|
1110
|
-
out.push(` floor ${body.floor} out of 100 · at most ${body.cap} per sub-category`);
|
|
1111
|
-
out.push(` ${body.ranking}`);
|
|
1112
|
-
if (body.sort) {
|
|
1113
|
-
const part = (body.sort_parts || []).find((x) => x.part === body.sort);
|
|
1114
|
-
out.push(` sorted by ${part ? part.title : body.sort}`);
|
|
1115
|
-
}
|
|
1116
|
-
const max = body.max_price;
|
|
1117
|
-
if (max) out.push(` at most ${[max.cents != null ? `$${(max.cents / 100).toFixed(2)}` : null, max.credits != null ? `${max.credits} credits` : null].filter(Boolean).join(' or ')}`);
|
|
1118
|
-
out.push('');
|
|
1119
|
-
const listed = body.listed || [];
|
|
1120
|
-
if (!listed.length) out.push(' Nothing is listed yet.');
|
|
1121
|
-
listed.forEach((m, i) => {
|
|
1122
|
-
out.push(` ${String(i + 1).padStart(2)}. ${m.key}@${m.version} ${m.overall}/100 ${formatPrice(m.price)}${m.category ? ` ${formatPlace(m.category)}` : ''}`);
|
|
1123
|
-
});
|
|
1124
|
-
const fresh = body.new || [];
|
|
1125
|
-
if (fresh.length) {
|
|
1126
|
-
out.push('');
|
|
1127
|
-
out.push(' New — not ranked until projects have installed and used them:');
|
|
1128
|
-
for (const m of fresh) out.push(` ${m.key}@${m.version} ${formatPrice(m.price)}${m.category ? ` ${formatPlace(m.category)}` : ''}`);
|
|
1129
|
-
}
|
|
1130
|
-
out.push('');
|
|
1131
|
-
out.push(' Why a module is not here: GET /store/modules/<key>/listing');
|
|
1132
|
-
return out.join('\n');
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
async function cmdStore(args, { log = console.log, errlog = console.error, request } = {}) {
|
|
1136
|
-
const q = storeQuery(args);
|
|
1137
|
-
if (q.error) { errlog(`bongos module store: ${q.error}`); errlog(STORE_USAGE); return 2; }
|
|
1138
|
-
let call;
|
|
1139
|
-
try { call = request || storeRequest(); }
|
|
1140
|
-
catch (e) { errlog(`bongos module store: ${e.message}`); return 1; }
|
|
1141
|
-
let res;
|
|
1142
|
-
try { res = await call('GET', `/store/modules${q.query}`); }
|
|
1143
|
-
catch (e) { errlog(`bongos module store: the store could not be reached — ${e.message}`); return 1; }
|
|
1144
|
-
if (!res.ok) { errlog(`bongos module store: the store answered ${storeError(res)}`); return res.status === 400 ? 2 : 1; }
|
|
1145
|
-
if (args.includes('--json')) { log(JSON.stringify(res.data, null, 2)); return 0; }
|
|
1146
|
-
log(formatStore(res.data || {}));
|
|
1147
|
-
return 0;
|
|
1148
|
-
}
|
|
1149
|
-
|
|
1150
1073
|
// Where install places a module: the INSTANCE's own modules/ root (ADR 0108 §1),
|
|
1151
1074
|
// which the loader discovers. In a single checkout that is this repo's modules/.
|
|
1152
1075
|
function instanceModulesDir() {
|
|
@@ -1408,13 +1331,15 @@ function printHelp(out = console.log) {
|
|
|
1408
1331
|
out(' Browse the module catalog — what you can enable, with author + origin credit');
|
|
1409
1332
|
out(' bongos module store [--sort <part>] [--max-cents N] [--max-credits N] [--json]');
|
|
1410
1333
|
out(' What the store lists, best first: above the quality floor, ranked, ten per sub-category');
|
|
1334
|
+
out(' bongos module price <key> [--cents N] [--credits N]');
|
|
1335
|
+
out(' Set your module\'s price in the store (0 in either makes it free)');
|
|
1411
1336
|
out(' bongos upgrade [--no-store] Pre-check enabled modules against the core version (fail-closed),');
|
|
1412
1337
|
out(' then list newer store versions of installed modules (never blocks)');
|
|
1413
1338
|
out(' bongos module check <key> [--sign-off "Name <email>"]');
|
|
1414
1339
|
out(' AGPL/publish pre-check before upstreaming + record a DCO-style sign-off');
|
|
1415
1340
|
out(' bongos module submit <key> [--sign-off "Name <email>"]');
|
|
1416
1341
|
out(' Pre-check + sign-off, then file into the rank-gated upstreaming review queue');
|
|
1417
|
-
out(' bongos module publish <key> [--dry-run] [--out <dir>]');
|
|
1342
|
+
out(' bongos module publish <key> [--dry-run] [--out <dir>] [--cents N] [--credits N]');
|
|
1418
1343
|
out(' Pack a module version (tarball + hash manifest) and upload it to the module store');
|
|
1419
1344
|
out(' bongos module install <key> [--version X.Y.Z]');
|
|
1420
1345
|
out(' Acquire a module from the store, verify it and place it under modules/ (not enabled)');
|
|
@@ -1428,6 +1353,7 @@ async function main(argv) {
|
|
|
1428
1353
|
if (sub === 'new') return cmdNew(args.slice(1));
|
|
1429
1354
|
if (sub === 'list' || sub === 'ls') return cmdList(args.slice(1));
|
|
1430
1355
|
if (sub === 'store') return cmdStore(args.slice(1));
|
|
1356
|
+
if (sub === 'price') return cmdPrice(args.slice(1));
|
|
1431
1357
|
if (sub === 'upgrade') return cmdUpgrade(args.slice(1));
|
|
1432
1358
|
if (sub === 'check') return cmdCheck(args.slice(1));
|
|
1433
1359
|
if (sub === 'submit') return cmdSubmit(args.slice(1));
|
|
@@ -1444,7 +1370,7 @@ async function main(argv) {
|
|
|
1444
1370
|
}
|
|
1445
1371
|
|
|
1446
1372
|
module.exports = {
|
|
1447
|
-
main, cmdNew, cmdList, cmdStore, formatStore, cmdUpgrade, cmdCheck, cmdSubmit, cmdPublish, cmdInstall, cmdUpdate, printHelp,
|
|
1373
|
+
main, cmdNew, cmdList, cmdStore, formatStore, cmdPrice, cmdUpgrade, cmdCheck, cmdSubmit, cmdPublish, cmdInstall, cmdUpdate, printHelp,
|
|
1448
1374
|
storeRequest,
|
|
1449
1375
|
gatherCatalog, formatCatalog, ownProjectName,
|
|
1450
1376
|
scaffoldModule, gatherUpgrade, evaluateUpgrade, gatherUpdates, evaluateUpdates, buildManifest,
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// scripts/gds/module-store-cli.js — @bongos-forwarder (task 1004848). Generated by
|
|
2
|
+
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
3
|
+
// The real script is scripts/bongos/module-store-cli.js. This forwarder keeps callers that still name
|
|
4
|
+
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
5
|
+
'use strict';
|
|
6
|
+
require('../bongos/forwarder.js').forward(module, 'module-store-cli.js');
|
|
@@ -373,9 +373,9 @@ function firstTimeAdmissionBlocked({ existing, approved, login }) {
|
|
|
373
373
|
// existing request-access page rather than a spinner that never resolves.
|
|
374
374
|
//
|
|
375
375
|
// NB the onboarding module's public GET /access-requests/status (used by the
|
|
376
|
-
// CLI) reports member|invited|
|
|
377
|
-
// firstAdmin bypass or open enrollment — so it reports the OWNER as
|
|
378
|
-
// forever. That is exactly why this auth-domain probe exists.
|
|
376
|
+
// CLI) reports member|invited|not_admitted but knows nothing about the
|
|
377
|
+
// firstAdmin bypass or open enrollment — so it reports the OWNER as
|
|
378
|
+
// 'not_admitted' forever. That is exactly why this auth-domain probe exists.
|
|
379
379
|
async function webAdmissionStatus(login) {
|
|
380
380
|
const name = String(login || '').trim();
|
|
381
381
|
if (!name) return { admitted: false, expectedSoon: false };
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
// by BOTH routes that answer "does this GitHub login
|
|
39
39
|
// have an account here?": the onboarding module's
|
|
40
40
|
// GET /access-requests/status (the richer
|
|
41
|
-
// member|invited|
|
|
41
|
+
// member|invited|not_admitted) and core's
|
|
42
42
|
// GET /auth/web/admission-status (its boolean twin).
|
|
43
43
|
// Task 1003339 / ADR 0209. They are ONE oracle read
|
|
44
44
|
// two ways, so they share one budget — give each its
|
|
@@ -152,6 +152,15 @@ function priceOf(row) {
|
|
|
152
152
|
return { cents, credits, free: !row || cents === 0 || credits === 0 };
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
// Whether a module can be bought today. Buying arrives with task 1003788 (area 8's
|
|
156
|
+
// wallet); until then only a free module can be had, and a paid one is listed with
|
|
157
|
+
// its price and marked not yet buyable (owner, 2026-10-26). Flip BUYING_OPEN when
|
|
158
|
+
// the buy action ships, and nothing else needs to change. Pure.
|
|
159
|
+
const BUYING_OPEN = false;
|
|
160
|
+
function buyableNow(price) {
|
|
161
|
+
return !!(price && (price.free || BUYING_OPEN));
|
|
162
|
+
}
|
|
163
|
+
|
|
155
164
|
// Within a buyer's maximum ({ cents, credits }, either may be null): a free module
|
|
156
165
|
// always is; a priced one is when it is offered at or under a cap the buyer set. Pure.
|
|
157
166
|
function withinMaxPrice(price, max) {
|
|
@@ -228,7 +237,7 @@ function newestPerModule(rows) {
|
|
|
228
237
|
}
|
|
229
238
|
|
|
230
239
|
// The catalog: { floor, cap, ranking, sort, sort_parts, max_price, listed: [...], new: [...] },
|
|
231
|
-
// each entry { key, title, version, published_at, price, category, overall, is_new,
|
|
240
|
+
// each entry { key, title, version, published_at, price, buyable, category, overall, is_new,
|
|
232
241
|
// parts, strengths, weaknesses, unmeasured } (the last four from readParts). Excluded
|
|
233
242
|
// versions are left out entirely: a module below the bar is not listed at all, nor
|
|
234
243
|
// is one past the cap of its sub-category, nor one over the buyer's `maxPrice`.
|
|
@@ -291,9 +300,10 @@ async function buildCatalog({ db, floor = QUALITY_FLOOR, sort = null, maxPrice =
|
|
|
291
300
|
const score = scores.get(String(r.version_id)) || null;
|
|
292
301
|
const { placement } = placeVersion(score, floor);
|
|
293
302
|
if (placement === 'excluded') continue;
|
|
303
|
+
const price = priceOf(prices.get(r.module_key));
|
|
294
304
|
out[placement].push({
|
|
295
305
|
key: r.module_key, title: r.title, version: r.version, published_at: r.published_at,
|
|
296
|
-
price:
|
|
306
|
+
price, category: categoryOf(cats.get(r.module_key)), buyable: buyableNow(price),
|
|
297
307
|
overall: placement === 'new' ? null : score.overall, is_new: placement === 'new',
|
|
298
308
|
...readParts(signals.get(String(r.version_id)) || [], { floor }),
|
|
299
309
|
});
|
|
@@ -383,6 +383,95 @@ async function latestVersions(keys, { db, channelsFor = null } = {}) {
|
|
|
383
383
|
return out;
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
// Set a module's price (task 1004894, criterion wa5-module-economy). Only its
|
|
387
|
+
// author may, and a delisted module takes no new price (it can no longer be
|
|
388
|
+
// acquired). The price is a HISTORY: each change appends a row and the newest is
|
|
389
|
+
// current, which is what the catalog (module-listing.js priceOf) and acquire
|
|
390
|
+
// (isFreePrice above) already read. `cents` / `credits`: a whole number of 0 or
|
|
391
|
+
// more, or null for "not offered in that currency"; at least one must be given
|
|
392
|
+
// (core_257's CHECK), and 0 in either is free. A paid price may be set before
|
|
393
|
+
// buying exists (owner, 2026-10-26): the module is listed with it and marked not yet
|
|
394
|
+
// buyable, and acquire keeps refusing it without an entitlement. Price is never an
|
|
395
|
+
// input to the score (ADR 0343 D6). The history IS the audit of price changes:
|
|
396
|
+
// every row names who set it and when, and the row before it is what it changed
|
|
397
|
+
// from (the audit middleware also logs the request). One transaction, the module row
|
|
398
|
+
// locked FOR UPDATE as publishVersion does, so two changes at once each report the
|
|
399
|
+
// price they really replaced. Returns { ok: true, previous, price } (each
|
|
400
|
+
// { cents, credits } or null) or { ok: false, status, code, message }.
|
|
401
|
+
async function setPrice({ key, cents = null, credits = null, builderId }, { db } = {}) {
|
|
402
|
+
const pool = db || require('./pool').pool;
|
|
403
|
+
const refuse = (status, code, message) => ({ ok: false, status, code, message });
|
|
404
|
+
const client = await pool.connect();
|
|
405
|
+
try {
|
|
406
|
+
await client.query('BEGIN');
|
|
407
|
+
const { rows: [mod] } = await client.query(
|
|
408
|
+
'SELECT module_key, author_id, status FROM store_modules WHERE module_key = $1 FOR UPDATE', [key]);
|
|
409
|
+
let out;
|
|
410
|
+
if (!mod) out = refuse(404, 'module_not_found', `The store has no module "${key}".`);
|
|
411
|
+
else if (String(mod.author_id) !== String(builderId)) out = refuse(403, 'not_module_author', `Only the author of "${key}" may set its price.`);
|
|
412
|
+
else if (mod.status === 'delisted') out = refuse(409, 'module_delisted', `"${key}" is delisted: it takes no new price.`);
|
|
413
|
+
if (out) { await client.query('ROLLBACK'); return out; }
|
|
414
|
+
const { rows: [prev] } = await client.query(
|
|
415
|
+
`SELECT price_cents, price_credits FROM store_module_prices
|
|
416
|
+
WHERE module_key = $1 ORDER BY set_at DESC, id DESC LIMIT 1`, [key]);
|
|
417
|
+
await client.query(
|
|
418
|
+
'INSERT INTO store_module_prices (module_key, price_cents, price_credits, set_by) VALUES ($1, $2, $3, $4)',
|
|
419
|
+
[key, cents, credits, builderId]);
|
|
420
|
+
await client.query('COMMIT');
|
|
421
|
+
return {
|
|
422
|
+
ok: true,
|
|
423
|
+
previous: prev ? { cents: prev.price_cents, credits: prev.price_credits } : null,
|
|
424
|
+
price: { cents, credits },
|
|
425
|
+
};
|
|
426
|
+
} catch (e) {
|
|
427
|
+
try { await client.query('ROLLBACK'); } catch { /* the connection may be gone */ }
|
|
428
|
+
throw e;
|
|
429
|
+
} finally {
|
|
430
|
+
client.release();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Withdraw one version (task 1004895, criterion wa5-staged-rollout): the store
|
|
435
|
+
// stops handing it out. Staged rollout catches a bad version on the tester channel;
|
|
436
|
+
// this is the way back once one has reached general. It is one channel row,
|
|
437
|
+
// 'withdrawn' (core_276), which servedChannels never serves, so acquire, install,
|
|
438
|
+
// update and the update check all stop offering it with no other change. Like a
|
|
439
|
+
// delist it reaches nothing a project already holds: an installed copy keeps
|
|
440
|
+
// working (ADR 0338 D2), and `bongos upgrade` and the hall tell its holder, with
|
|
441
|
+
// `note` — the reason the person gave, kept on the row (core_281). The insert only
|
|
442
|
+
// lands while the version is not already withdrawn, so two people withdrawing at
|
|
443
|
+
// once write one row. Returns { ok: true, channel } or { ok: false, status, code, message }.
|
|
444
|
+
const WITHDRAWN_CHANNEL = 'withdrawn';
|
|
445
|
+
|
|
446
|
+
async function withdrawVersion(versionId, builderId, note, { db } = {}) {
|
|
447
|
+
const pool = db || require('./pool').pool;
|
|
448
|
+
const { rowCount } = await pool.query(
|
|
449
|
+
`INSERT INTO store_module_version_channels (version_id, channel, reason, set_by, note)
|
|
450
|
+
SELECT $1, '${WITHDRAWN_CHANNEL}', 'withdrawn_by_person', $2, $3
|
|
451
|
+
WHERE (SELECT c.channel FROM store_module_version_channels c
|
|
452
|
+
WHERE c.version_id = $1 ORDER BY c.set_at DESC, c.id DESC LIMIT 1)
|
|
453
|
+
IS DISTINCT FROM '${WITHDRAWN_CHANNEL}'`, [versionId, builderId, note]);
|
|
454
|
+
if (rowCount === 1) return { ok: true, channel: WITHDRAWN_CHANNEL };
|
|
455
|
+
return { ok: false, status: 409, code: 'already_withdrawn', message: 'This version is already withdrawn: the store does not hand it out.' };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Which versions of these module keys are withdrawn, as { key: [{ version, note }] }
|
|
459
|
+
// — so the update check and the hall can tell a project that the version it runs
|
|
460
|
+
// was taken back, and why. `note` is the reason from the withdrawing row.
|
|
461
|
+
async function withdrawnVersions(keys, { db } = {}) {
|
|
462
|
+
if (!keys.length) return {};
|
|
463
|
+
const pool = db || require('./pool').pool;
|
|
464
|
+
const { rows } = await pool.query(
|
|
465
|
+
`SELECT v.module_key, v.version,
|
|
466
|
+
(SELECT c.note FROM store_module_version_channels c
|
|
467
|
+
WHERE c.version_id = v.id ORDER BY c.set_at DESC, c.id DESC LIMIT 1) AS note
|
|
468
|
+
FROM store_module_versions v
|
|
469
|
+
WHERE v.module_key = ANY($1) AND ${currentChannelSql('v.id')} = '${WITHDRAWN_CHANNEL}'`, [keys]);
|
|
470
|
+
const out = {};
|
|
471
|
+
for (const r of rows) (out[r.module_key] = out[r.module_key] || []).push({ version: r.version, note: r.note || null });
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
|
|
386
475
|
// Delist a module (task 1003797, ADR 0338 D2): it leaves the catalog, takes no new
|
|
387
476
|
// acquisitions or versions, and serves no further downloads — every read above
|
|
388
477
|
// already refuses on status 'delisted', so this one UPDATE is the whole act. It
|
|
@@ -427,5 +516,5 @@ function versionArtifactFile(row, { dir = storeDir() } = {}) {
|
|
|
427
516
|
module.exports = {
|
|
428
517
|
stageArtifact, commitArtifact, removeArtifact, relativeArtifactPath, publishVersion,
|
|
429
518
|
isFreePrice, servedChannels, resolveAcquirable, resolveReadable, publishedVersionSet, latestVersions, versionArtifactFile,
|
|
430
|
-
delistModule, delistedKeySet, storeDir, storeDirFor, migrateLegacyStore,
|
|
519
|
+
delistModule, delistedKeySet, setPrice, withdrawVersion, withdrawnVersions, storeDir, storeDirFor, migrateLegacyStore,
|
|
431
520
|
};
|