@bongos/core 1.23.5 → 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 +51 -36
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +4 -0
- package/clients/bongos-client/index.mjs +2 -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 +96 -5
- package/docs/api-reference.md +3 -2
- package/docs/module-api-changelog.md +2 -0
- package/docs/modules-contract.md +1 -0
- package/modules/onboarding/routes/access-requests.js +10 -9
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +10 -0
- package/scripts/bongos/login.js +13 -14
- package/scripts/bongos/module-store-cli.js +182 -0
- package/scripts/bongos/module.js +36 -129
- 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 +49 -1
- package/src/bongos/route-rank-check.js +3 -0
- package/src/bongos/routes/auth.js +7 -11
- package/src/bongos/routes/modules.js +32 -1
- 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/invite_expiry_db.mjs +1 -1
- package/tests/module_store_cli.mjs +1 -1
- package/tests/module_store_price.mjs +207 -0
|
@@ -357,9 +357,12 @@ module.exports = function buildAccessRequestsRouter() {
|
|
|
357
357
|
// (the device_code is spent), so the CLI can't re-poll the flow — it polls this
|
|
358
358
|
// instead, then restarts sign-in once admitted. Exposes nothing the public POST
|
|
359
359
|
// above doesn't already reveal through its 409s (already_member / already_approved).
|
|
360
|
-
// status ∈ member | invited |
|
|
361
|
-
//
|
|
362
|
-
//
|
|
360
|
+
// status ∈ member | invited | not_admitted; admitted = member||invited (ADR 0217 D4,
|
|
361
|
+
// task 1003359). `not_admitted` covers pending, declined, a lapsed invite and no
|
|
362
|
+
// request at all, so a caller who is not the subject cannot learn that someone
|
|
363
|
+
// applied here or was turned down. It does NOT close the membership oracle —
|
|
364
|
+
// `admitted` still answers that, and the limiter below is what bounds enumeration.
|
|
365
|
+
// A lapsed invite answering `not_admitted` rather than `expired` amends ADR 0356 D4.
|
|
363
366
|
// rank: public — a would-be builder is by definition outside the system.
|
|
364
367
|
// Declared BEFORE `GET /access-requests` so the literal `/status` path is matched
|
|
365
368
|
// as its own route (not swallowed as a query on the list route).
|
|
@@ -370,9 +373,7 @@ module.exports = function buildAccessRequestsRouter() {
|
|
|
370
373
|
// per-IP budget: core's GET /auth/web/admission-status (the boolean twin of
|
|
371
374
|
// this answer) mounts the very same instance, so the two cannot be alternated
|
|
372
375
|
// for double the rate. The limiter runs before the queries, so a refused probe
|
|
373
|
-
// costs no DB work.
|
|
374
|
-
// is not the subject cannot tell pending/dismissed/none apart — is the owner's
|
|
375
|
-
// call and is filed as a blocker, not decided here.
|
|
376
|
+
// costs no DB work.
|
|
376
377
|
router.get('/access-requests/status', accountExistenceReadRateLimit, async (req, res) => {
|
|
377
378
|
const login = normalizeLogin(req.query.github_login);
|
|
378
379
|
if (!GITHUB_LOGIN_RE.test(login)) {
|
|
@@ -386,7 +387,7 @@ module.exports = function buildAccessRequestsRouter() {
|
|
|
386
387
|
if (member.rows.length > 0) {
|
|
387
388
|
return res.json({ github_login: login, status: 'member', admitted: true });
|
|
388
389
|
}
|
|
389
|
-
//
|
|
390
|
+
// A live invite sorts first, so the top row admits only if it is one.
|
|
390
391
|
const ar = await pool.query(
|
|
391
392
|
`SELECT status, (status = 'invited' AND ${inviteExpiredSql()}) AS expired
|
|
392
393
|
FROM access_requests
|
|
@@ -396,8 +397,8 @@ module.exports = function buildAccessRequestsRouter() {
|
|
|
396
397
|
[login]
|
|
397
398
|
);
|
|
398
399
|
const top = ar.rows[0];
|
|
399
|
-
const
|
|
400
|
-
return res.json({ github_login: login, status
|
|
400
|
+
const invited = !!top && top.status === 'invited' && !top.expired;
|
|
401
|
+
return res.json({ github_login: login, status: invited ? 'invited' : 'not_admitted', admitted: invited });
|
|
401
402
|
} catch (err) {
|
|
402
403
|
console.error('[gds] GET /access-requests/status', err);
|
|
403
404
|
return res.fail('status_failed', { status: 500, message: 'internal error' });
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.6",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.23.
|
|
9
|
+
"version": "1.23.6",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.6",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -10413,5 +10413,15 @@
|
|
|
10413
10413
|
"id": "1004895",
|
|
10414
10414
|
"text": "A broken module version can now be pulled back from the store, so new installs and updates stop getting it while projects that already have it are told."
|
|
10415
10415
|
}
|
|
10416
|
+
],
|
|
10417
|
+
"1.23.6": [
|
|
10418
|
+
{
|
|
10419
|
+
"id": "1004894",
|
|
10420
|
+
"text": "Module authors can now set a price for their module, in money or credits; paid modules show their price and are marked not yet buyable until buying opens."
|
|
10421
|
+
},
|
|
10422
|
+
{
|
|
10423
|
+
"id": "1003359",
|
|
10424
|
+
"text": "People checking whether someone has applied to a project can no longer tell a pending, declined or never-filed request apart, and the sign-in tool no longer promises an approval that may never come."
|
|
10425
|
+
}
|
|
10416
10426
|
]
|
|
10417
10427
|
}
|
package/scripts/bongos/login.js
CHANGED
|
@@ -43,17 +43,24 @@ function normalizeInstanceBase(raw) {
|
|
|
43
43
|
return s;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Decide what an /access-requests/status poll means for the sign-in loop.
|
|
46
|
+
// Decide what an /access-requests/status poll means for the sign-in loop. The probe
|
|
47
|
+
// answers only admitted or not (ADR 0217 D4), so anything else is a wait — even an
|
|
48
|
+
// older instance's `dismissed`/`expired`, which the loop no longer reads.
|
|
47
49
|
function pollDecision(body) {
|
|
48
50
|
if (!body || typeof body !== 'object') return 'wait';
|
|
49
51
|
if (body.admitted === true) return 'admitted';
|
|
50
52
|
if (body.status === 'member' || body.status === 'invited') return 'admitted';
|
|
51
|
-
if (body.status === 'dismissed') return 'dismissed';
|
|
52
|
-
// An invite past its 30 days (task 1002969): the door is shut until they invite again.
|
|
53
|
-
if (body.status === 'expired') return 'expired';
|
|
54
53
|
return 'wait';
|
|
55
54
|
}
|
|
56
55
|
|
|
56
|
+
// What the loop says when it stops waiting. It cannot know whether the request is
|
|
57
|
+
// still pending, was declined, or rode a lapsed invite, so it promises nothing.
|
|
58
|
+
function waitTimeoutMessage(base) {
|
|
59
|
+
return "✖ Stopped waiting after 30 minutes — you haven't been let in yet. If your request was turned down "
|
|
60
|
+
+ "or your invite has run out, waiting longer won't change that. Ask the project's owner, "
|
|
61
|
+
+ `then re-run \`bongos login ${base}\`.`;
|
|
62
|
+
}
|
|
63
|
+
|
|
57
64
|
// --- I/O helpers -------------------------------------------------------------
|
|
58
65
|
|
|
59
66
|
async function fetchJson(url, opts = {}) {
|
|
@@ -300,16 +307,8 @@ async function main() {
|
|
|
300
307
|
log('\n ✓ Approved! Finishing sign-in — approve once more in your browser.');
|
|
301
308
|
break;
|
|
302
309
|
}
|
|
303
|
-
if (decision === 'dismissed') {
|
|
304
|
-
console.error('\n✖ your access request was declined.');
|
|
305
|
-
process.exit(1);
|
|
306
|
-
}
|
|
307
|
-
if (decision === 'expired') {
|
|
308
|
-
console.error('\n✖ your invite has expired — ask the project to invite you again.');
|
|
309
|
-
process.exit(1);
|
|
310
|
-
}
|
|
311
310
|
if (Date.now() > admitDeadline) {
|
|
312
|
-
console.error(
|
|
311
|
+
console.error(`\n${waitTimeoutMessage(base)}`);
|
|
313
312
|
process.exit(1);
|
|
314
313
|
}
|
|
315
314
|
process.stdout.write('.');
|
|
@@ -357,4 +356,4 @@ if (require.main === module) {
|
|
|
357
356
|
});
|
|
358
357
|
}
|
|
359
358
|
|
|
360
|
-
module.exports = { normalizeInstanceBase, pollDecision, runDeviceFlow, logOtherInstances };
|
|
359
|
+
module.exports = { normalizeInstanceBase, pollDecision, waitTimeoutMessage, runDeviceFlow, logOtherInstances };
|
|
@@ -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
|
|
@@ -958,18 +966,31 @@ async function cmdSubmit(args, {
|
|
|
958
966
|
// Distinct from `submit`, which proposes a module INTO core (ADR 0135).
|
|
959
967
|
async function cmdPublish(args, {
|
|
960
968
|
log = console.log, errlog = console.error,
|
|
961
|
-
modulesDir = MODULES_DIR, pack, upload, writeFile = fs.writeFileSync,
|
|
969
|
+
modulesDir = MODULES_DIR, pack, upload, writeFile = fs.writeFileSync, priceRequest,
|
|
962
970
|
} = {}) {
|
|
963
|
-
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]));
|
|
964
974
|
if (!key) {
|
|
965
|
-
errlog(
|
|
975
|
+
errlog(PUBLISH_USAGE);
|
|
966
976
|
return 2;
|
|
967
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
|
+
}
|
|
968
989
|
const dryRun = args.includes('--dry-run');
|
|
969
990
|
const outIdx = args.indexOf('--out');
|
|
970
991
|
const outDir = outIdx !== -1 ? args[outIdx + 1] : null;
|
|
971
992
|
if (outIdx !== -1 && (!outDir || outDir.startsWith('--'))) {
|
|
972
|
-
errlog(
|
|
993
|
+
errlog(PUBLISH_USAGE);
|
|
973
994
|
return 2;
|
|
974
995
|
}
|
|
975
996
|
|
|
@@ -1037,135 +1058,18 @@ async function cmdPublish(args, {
|
|
|
1037
1058
|
if (res.data && res.data.channel === 'tester') {
|
|
1038
1059
|
log(' It goes to testing projects first, and reaches every project once it has been tested (ADR 0344).');
|
|
1039
1060
|
}
|
|
1040
|
-
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;
|
|
1041
1067
|
}
|
|
1042
1068
|
|
|
1043
1069
|
// ---------------------------------------------------------------------------
|
|
1044
1070
|
// bongos module install <key> — acquire a module from the store (ADR 0338 D1)
|
|
1045
1071
|
// ---------------------------------------------------------------------------
|
|
1046
1072
|
|
|
1047
|
-
// The store half of the CLI, over three calls (src/bongos/routes/modules.js):
|
|
1048
|
-
// acquire (take the entitlement, learn the version), download the tarball, and
|
|
1049
|
-
// once the files are placed, record the version acquired. Injected in tests.
|
|
1050
|
-
function storeRequest() {
|
|
1051
|
-
// Lazy require — cli-lib's session/fetch plumbing must never load in the server
|
|
1052
|
-
// process, which requires this file too (the same reason cmdPublish gives).
|
|
1053
|
-
const { readSessionToken, dispatchedFetch, noKeepAliveDispatcher, CLI_USER_AGENT } = require('./cli-lib');
|
|
1054
|
-
const session = readSessionToken();
|
|
1055
|
-
if (!session) throw new Error('no Bongos session — run `bongos setup` first');
|
|
1056
|
-
const call = async (method, urlPath, { json, binary = false } = {}) => {
|
|
1057
|
-
const res = await dispatchedFetch(`${session.apiBase}/api/bongos${urlPath}`, {
|
|
1058
|
-
method,
|
|
1059
|
-
headers: {
|
|
1060
|
-
Authorization: `Bearer ${session.token}`, 'User-Agent': CLI_USER_AGENT,
|
|
1061
|
-
...(json ? { 'Content-Type': 'application/json' } : {}),
|
|
1062
|
-
},
|
|
1063
|
-
body: json ? JSON.stringify(json) : undefined,
|
|
1064
|
-
dispatcher: noKeepAliveDispatcher(),
|
|
1065
|
-
});
|
|
1066
|
-
if (binary && res.ok) return { ok: true, status: res.status, buf: Buffer.from(await res.arrayBuffer()) };
|
|
1067
|
-
const text = await res.text();
|
|
1068
|
-
let data;
|
|
1069
|
-
try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; }
|
|
1070
|
-
return { ok: res.ok, status: res.status, data };
|
|
1071
|
-
};
|
|
1072
|
-
// Where the store lives, so install can print a full link to the how-to page.
|
|
1073
|
-
call.apiBase = session.apiBase;
|
|
1074
|
-
return call;
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
// The hall page that shows a version's HOWTO.md (task 1004366) — the Modules tab,
|
|
1078
|
-
// opened on one version. null when the version shipped without one (published
|
|
1079
|
-
// before ADR 0347's gate), so install never prints a link that would 404.
|
|
1080
|
-
function howtoLink(key, version, files, apiBase) {
|
|
1081
|
-
if (!files.some((f) => f.path === 'HOWTO.md')) return null;
|
|
1082
|
-
const p = `/builders/modules?howto=${encodeURIComponent(key)}&version=${encodeURIComponent(version)}`;
|
|
1083
|
-
return apiBase ? `${String(apiBase).replace(/\/+$/, '')}${p}` : p;
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
function storeError(res) {
|
|
1087
|
-
const err = res.data && (res.data.error || res.data);
|
|
1088
|
-
return (err && (err.message || err.code)) || `HTTP ${res.status}`;
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
|
-
// ---------------------------------------------------------------------------
|
|
1092
|
-
// bongos module store — what the store lists (task 1003811)
|
|
1093
|
-
// ---------------------------------------------------------------------------
|
|
1094
|
-
|
|
1095
|
-
const STORE_USAGE = 'usage: bongos module store [--sort <part>] [--max-cents N] [--max-credits N] [--json]';
|
|
1096
|
-
|
|
1097
|
-
// The query GET /store/modules takes, from argv. Returns { query } or { error }.
|
|
1098
|
-
// The server checks the values too; this only refuses what is not a value at all.
|
|
1099
|
-
function storeQuery(args) {
|
|
1100
|
-
const params = [];
|
|
1101
|
-
for (const [flag, param] of [['--sort', 'sort'], ['--max-cents', 'max_cents'], ['--max-credits', 'max_credits']]) {
|
|
1102
|
-
const i = args.indexOf(flag);
|
|
1103
|
-
if (i === -1) continue;
|
|
1104
|
-
const v = args[i + 1];
|
|
1105
|
-
if (!v || v.startsWith('--')) return { error: `${flag} needs a value` };
|
|
1106
|
-
params.push(`${param}=${encodeURIComponent(v)}`);
|
|
1107
|
-
}
|
|
1108
|
-
return { query: params.length ? `?${params.join('&')}` : '' };
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
function formatPrice(price) {
|
|
1112
|
-
if (!price || price.free) return 'free';
|
|
1113
|
-
const parts = [];
|
|
1114
|
-
if (price.cents != null) parts.push(`$${(price.cents / 100).toFixed(2)}`);
|
|
1115
|
-
if (price.credits != null) parts.push(`${price.credits} credits`);
|
|
1116
|
-
return parts.join(' or ') || 'free';
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
function formatPlace(category) {
|
|
1120
|
-
if (!category) return '';
|
|
1121
|
-
return category.label ? `${category.parent_title} › ${category.label}` : category.parent_title;
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
// Pure renderer over the GET /store/modules body. It keeps the server's order and
|
|
1125
|
-
// adds nothing to it: listed in rank, then New, never ranked.
|
|
1126
|
-
function formatStore(body) {
|
|
1127
|
-
const out = [];
|
|
1128
|
-
out.push('bongos module store — what the store lists, best first');
|
|
1129
|
-
out.push(` floor ${body.floor} out of 100 · at most ${body.cap} per sub-category`);
|
|
1130
|
-
out.push(` ${body.ranking}`);
|
|
1131
|
-
if (body.sort) {
|
|
1132
|
-
const part = (body.sort_parts || []).find((x) => x.part === body.sort);
|
|
1133
|
-
out.push(` sorted by ${part ? part.title : body.sort}`);
|
|
1134
|
-
}
|
|
1135
|
-
const max = body.max_price;
|
|
1136
|
-
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 ')}`);
|
|
1137
|
-
out.push('');
|
|
1138
|
-
const listed = body.listed || [];
|
|
1139
|
-
if (!listed.length) out.push(' Nothing is listed yet.');
|
|
1140
|
-
listed.forEach((m, i) => {
|
|
1141
|
-
out.push(` ${String(i + 1).padStart(2)}. ${m.key}@${m.version} ${m.overall}/100 ${formatPrice(m.price)}${m.category ? ` ${formatPlace(m.category)}` : ''}`);
|
|
1142
|
-
});
|
|
1143
|
-
const fresh = body.new || [];
|
|
1144
|
-
if (fresh.length) {
|
|
1145
|
-
out.push('');
|
|
1146
|
-
out.push(' New — not ranked until projects have installed and used them:');
|
|
1147
|
-
for (const m of fresh) out.push(` ${m.key}@${m.version} ${formatPrice(m.price)}${m.category ? ` ${formatPlace(m.category)}` : ''}`);
|
|
1148
|
-
}
|
|
1149
|
-
out.push('');
|
|
1150
|
-
out.push(' Why a module is not here: GET /store/modules/<key>/listing');
|
|
1151
|
-
return out.join('\n');
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
async function cmdStore(args, { log = console.log, errlog = console.error, request } = {}) {
|
|
1155
|
-
const q = storeQuery(args);
|
|
1156
|
-
if (q.error) { errlog(`bongos module store: ${q.error}`); errlog(STORE_USAGE); return 2; }
|
|
1157
|
-
let call;
|
|
1158
|
-
try { call = request || storeRequest(); }
|
|
1159
|
-
catch (e) { errlog(`bongos module store: ${e.message}`); return 1; }
|
|
1160
|
-
let res;
|
|
1161
|
-
try { res = await call('GET', `/store/modules${q.query}`); }
|
|
1162
|
-
catch (e) { errlog(`bongos module store: the store could not be reached — ${e.message}`); return 1; }
|
|
1163
|
-
if (!res.ok) { errlog(`bongos module store: the store answered ${storeError(res)}`); return res.status === 400 ? 2 : 1; }
|
|
1164
|
-
if (args.includes('--json')) { log(JSON.stringify(res.data, null, 2)); return 0; }
|
|
1165
|
-
log(formatStore(res.data || {}));
|
|
1166
|
-
return 0;
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
1073
|
// Where install places a module: the INSTANCE's own modules/ root (ADR 0108 §1),
|
|
1170
1074
|
// which the loader discovers. In a single checkout that is this repo's modules/.
|
|
1171
1075
|
function instanceModulesDir() {
|
|
@@ -1427,13 +1331,15 @@ function printHelp(out = console.log) {
|
|
|
1427
1331
|
out(' Browse the module catalog — what you can enable, with author + origin credit');
|
|
1428
1332
|
out(' bongos module store [--sort <part>] [--max-cents N] [--max-credits N] [--json]');
|
|
1429
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)');
|
|
1430
1336
|
out(' bongos upgrade [--no-store] Pre-check enabled modules against the core version (fail-closed),');
|
|
1431
1337
|
out(' then list newer store versions of installed modules (never blocks)');
|
|
1432
1338
|
out(' bongos module check <key> [--sign-off "Name <email>"]');
|
|
1433
1339
|
out(' AGPL/publish pre-check before upstreaming + record a DCO-style sign-off');
|
|
1434
1340
|
out(' bongos module submit <key> [--sign-off "Name <email>"]');
|
|
1435
1341
|
out(' Pre-check + sign-off, then file into the rank-gated upstreaming review queue');
|
|
1436
|
-
out(' bongos module publish <key> [--dry-run] [--out <dir>]');
|
|
1342
|
+
out(' bongos module publish <key> [--dry-run] [--out <dir>] [--cents N] [--credits N]');
|
|
1437
1343
|
out(' Pack a module version (tarball + hash manifest) and upload it to the module store');
|
|
1438
1344
|
out(' bongos module install <key> [--version X.Y.Z]');
|
|
1439
1345
|
out(' Acquire a module from the store, verify it and place it under modules/ (not enabled)');
|
|
@@ -1447,6 +1353,7 @@ async function main(argv) {
|
|
|
1447
1353
|
if (sub === 'new') return cmdNew(args.slice(1));
|
|
1448
1354
|
if (sub === 'list' || sub === 'ls') return cmdList(args.slice(1));
|
|
1449
1355
|
if (sub === 'store') return cmdStore(args.slice(1));
|
|
1356
|
+
if (sub === 'price') return cmdPrice(args.slice(1));
|
|
1450
1357
|
if (sub === 'upgrade') return cmdUpgrade(args.slice(1));
|
|
1451
1358
|
if (sub === 'check') return cmdCheck(args.slice(1));
|
|
1452
1359
|
if (sub === 'submit') return cmdSubmit(args.slice(1));
|
|
@@ -1463,7 +1370,7 @@ async function main(argv) {
|
|
|
1463
1370
|
}
|
|
1464
1371
|
|
|
1465
1372
|
module.exports = {
|
|
1466
|
-
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,
|
|
1467
1374
|
storeRequest,
|
|
1468
1375
|
gatherCatalog, formatCatalog, ownProjectName,
|
|
1469
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
|
});
|