@bongos/core 1.21.69 → 1.21.71
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 +119 -64
- package/.claude/skills/bongos-feedback/SKILL.md +10 -46
- package/.claude/skills/goal-create/SKILL.md +21 -3
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -2
- package/clients/bongos-client/index.cjs +8 -2
- package/clients/bongos-client/index.d.ts +15 -6
- package/clients/bongos-client/index.mjs +8 -2
- package/docs/adr/0368-red-and-blue-teams-practice-on-a-range-never-on-live.md +99 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +196 -19
- package/docs/api-reference.md +9 -6
- package/docs/copy-inventory.md +25 -21
- package/docs/copy-registry.json +76 -30
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +8 -8
- package/docs/recipes/core-release-pipeline.md +6 -0
- package/docs/recipes/upgrading-the-core.md +2 -0
- package/modules/hall-ui/public/settings-software-update.js +77 -2
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/lifecycle/db-tasks.js +12 -0
- package/modules/lifecycle/goal-spec-task.js +129 -0
- package/modules/lifecycle/routes/goals.js +7 -9
- package/modules/lifecycle/routes/task-write-routes.js +11 -1
- package/modules/lifecycle/upkeep-home.js +55 -0
- package/modules/platform-identity/instance-feedback.js +108 -7
- package/modules/platform-identity/routes/sso.js +71 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +42 -0
- package/scripts/gds/claude-materialize.js +125 -31
- package/scripts/gds/claude-md-template.js +47 -10
- package/scripts/gds/cli-lib.js +4 -4
- package/scripts/gds/feedback-send.js +146 -73
- package/scripts/gds/paste-token.js +1 -1
- package/scripts/gds/provision-net.js +1 -8
- package/scripts/gds/provision-units.js +28 -2
- package/scripts/gds/reauth.js +1 -1
- package/scripts/gds/run-unit-tests.js +3 -0
- package/scripts/gds/setup.js +1 -1
- package/scripts/gds/start.js +4 -4
- package/scripts/gds/surface.js +15 -3
- package/scripts/gds/task.js +32 -5
- package/scripts/gds/upgrade-operator.js +73 -0
- package/scripts/gds/upgrade.js +7 -2
- package/scripts/hall-preview/server.js +3 -0
- package/src/bongos/platform-visibility-gate.js +3 -0
- package/src/bongos/routes/core-update.js +98 -4
- package/src/bongos/routes/feedback.js +100 -76
- package/src/bongos/upgrade-request.js +158 -0
- package/src/module-api.js +1 -1
- package/tests/claude_materialize.mjs +195 -18
- package/tests/cli_allowlist_permissions.mjs +6 -2
- package/tests/cli_surface.mjs +2 -2
- package/tests/fixtures/page-ledger.mjs +5 -0
- package/tests/goal_admission_override.mjs +1 -1
- package/tests/goal_category.mjs +1 -1
- package/tests/goal_create_spec_entry.mjs +61 -0
- package/tests/goal_spec_task.mjs +197 -0
- package/tests/goal_spec_task_db.mjs +95 -0
- package/tests/instance_feedback.mjs +267 -68
- package/tests/provision_settings_apply.mjs +25 -1
- package/tests/software_update.mjs +18 -1
- package/tests/start_env_guard.mjs +2 -2
- package/tests/upgrade_operator.mjs +105 -0
- package/tests/upgrade_request.mjs +348 -0
- package/tests/upkeep_home.mjs +156 -0
|
@@ -1,110 +1,134 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// src/bongos/routes/feedback.js —
|
|
3
|
+
// src/bongos/routes/feedback.js — the project's half of `bongos feedback`: a
|
|
4
|
+
// one-time RECEIPT that proves who sent a report (tasks 1004462, 1004591).
|
|
4
5
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// The report itself goes straight from the builder's CLI to the Cloud Bongos hub,
|
|
7
|
+
// never through this project — so a project with no hub link (self-hosted, or a
|
|
8
|
+
// hosted one provisioned before federation) can still send. What the hub cannot
|
|
9
|
+
// know on its own is WHO sent it. This project can: the builder holds a session
|
|
10
|
+
// here. So the CLI first asks for a receipt naming the signed-in builder and a
|
|
11
|
+
// hash of the text; the hub then calls this project's public receipt URL and
|
|
12
|
+
// files the report only if the project vouches for both. Nothing for the builder
|
|
13
|
+
// to do beyond the session they already have.
|
|
10
14
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
15
|
+
// Two routes:
|
|
16
|
+
// POST /feedback/receipt — signed-in builder; records { who, sha256, kind }.
|
|
17
|
+
// GET /feedback/receipt/:id — public, single-use; the hub redeems it once.
|
|
13
18
|
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// itself) answers 409 not_hub_connected — told, not silently dropped.
|
|
19
|
+
// Receipts live in memory for RECEIPT_TTL_MS: the CLI redeems within seconds, and
|
|
20
|
+
// a restart in between only loses that one report. In the CORE, not a module, for
|
|
21
|
+
// the reason core-update.js gives: every project must have this door.
|
|
18
22
|
|
|
23
|
+
const crypto = require('node:crypto');
|
|
19
24
|
const express = require('express');
|
|
20
25
|
const { asyncHandler, validateOrRespond } = require('./_helpers');
|
|
21
26
|
const auth = require('../auth');
|
|
22
|
-
const
|
|
23
|
-
const { userAgent } = require('../../branding');
|
|
27
|
+
const ic = require('../../instance-config');
|
|
24
28
|
|
|
25
|
-
const
|
|
26
|
-
const
|
|
29
|
+
const RECEIPT_TTL_MS = 10 * 60 * 1000;
|
|
30
|
+
const RECEIPTS_MAX = 1000;
|
|
27
31
|
|
|
28
|
-
|
|
29
|
-
|
|
32
|
+
// Where reports go: the Bongos maintainers' hub. <PREFIX>_FEEDBACK_HUB overrides
|
|
33
|
+
// it (a test hub, a fork with its own maintainers). A project that IS the hub
|
|
34
|
+
// answers null — the CLI then sends to the project it already talks to.
|
|
35
|
+
const DEFAULT_FEEDBACK_HUB = 'https://cloudbongos.com';
|
|
36
|
+
|
|
37
|
+
function feedbackHub({ env, isHub } = {}) {
|
|
38
|
+
const override = ic.resolveEnv('FEEDBACK_HUB', env ? { env } : undefined);
|
|
39
|
+
if (override) return String(override).replace(/\/+$/, '');
|
|
40
|
+
let hub = isHub;
|
|
41
|
+
if (hub === undefined) {
|
|
42
|
+
try { hub = require('../../modules').isModuleEnabled('platform-identity'); } catch { hub = false; }
|
|
43
|
+
}
|
|
44
|
+
return hub ? null : DEFAULT_FEEDBACK_HUB;
|
|
30
45
|
}
|
|
31
46
|
|
|
32
47
|
function runningCore() {
|
|
33
48
|
try { return require('../../module-api').CORE_VERSION || null; } catch { return null; }
|
|
34
49
|
}
|
|
35
50
|
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
|
|
51
|
+
// makeReceiptStore — issue/redeem with expiry, single use, and a size cap so an
|
|
52
|
+
// abusive signed-in caller cannot grow the map without bound. A full store
|
|
53
|
+
// refuses the NEW receipt (issue → null) rather than evicting a live one, so one
|
|
54
|
+
// noisy builder can never drop another builder's pending report.
|
|
55
|
+
function makeReceiptStore({ now = Date.now, ttlMs = RECEIPT_TTL_MS, max = RECEIPTS_MAX } = {}) {
|
|
56
|
+
const rows = new Map(); // id -> { ...receipt, exp }
|
|
57
|
+
function prune() {
|
|
58
|
+
const t = now();
|
|
59
|
+
for (const [id, r] of rows) if (r.exp <= t) rows.delete(id);
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
issue(receipt) {
|
|
63
|
+
prune();
|
|
64
|
+
if (rows.size >= max) return null;
|
|
65
|
+
const id = crypto.randomBytes(18).toString('base64url');
|
|
66
|
+
rows.set(id, { ...receipt, exp: now() + ttlMs });
|
|
67
|
+
return id;
|
|
68
|
+
},
|
|
69
|
+
redeem(id) {
|
|
70
|
+
const r = rows.get(id);
|
|
71
|
+
rows.delete(id);
|
|
72
|
+
if (!r || r.exp <= now()) return null;
|
|
73
|
+
const { exp: _exp, ...receipt } = r;
|
|
74
|
+
return receipt;
|
|
75
|
+
},
|
|
76
|
+
_rows: rows,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
39
79
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
github_id: builder.github_id,
|
|
48
|
-
message,
|
|
49
|
-
kind,
|
|
50
|
-
context,
|
|
51
|
-
}),
|
|
52
|
-
signal: AbortSignal.timeout(HUB_TIMEOUT_MS),
|
|
53
|
-
});
|
|
54
|
-
let body = null;
|
|
55
|
-
try { body = await res.json(); } catch { /* non-JSON answer */ }
|
|
56
|
-
return { status: res.status, body };
|
|
80
|
+
// The process's one receipt store. Shared, because a receipt is redeemed at the public
|
|
81
|
+
// route below whoever issued it: the CLI's POST /feedback/receipt, or this project's own
|
|
82
|
+
// "Ask for this upgrade" (src/bongos/upgrade-request.js, task 1004595).
|
|
83
|
+
let sharedStore = null;
|
|
84
|
+
function defaultReceiptStore() {
|
|
85
|
+
if (!sharedStore) sharedStore = makeReceiptStore();
|
|
86
|
+
return sharedStore;
|
|
57
87
|
}
|
|
58
88
|
|
|
59
|
-
function buildFeedbackRouter({
|
|
89
|
+
function buildFeedbackRouter({ store = defaultReceiptStore(), hub = feedbackHub } = {}) {
|
|
60
90
|
const router = express.Router();
|
|
61
91
|
|
|
62
|
-
// rank: any signed-in builder —
|
|
63
|
-
// here, and the hub files every report
|
|
92
|
+
// rank: any signed-in builder — a receipt only says "this builder sent this
|
|
93
|
+
// text"; it grants nothing here, and the hub files every report behind a nod.
|
|
64
94
|
router.post(
|
|
65
|
-
'/feedback',
|
|
95
|
+
'/feedback/receipt',
|
|
66
96
|
auth.requireBuilder,
|
|
67
|
-
asyncHandler('POST /feedback', async (req, res) => {
|
|
97
|
+
asyncHandler('POST /feedback/receipt', async (req, res) => {
|
|
68
98
|
if (validateOrRespond(req, res, {
|
|
69
|
-
|
|
99
|
+
sha256: { required: true, type: 'string', maxLength: 64 },
|
|
70
100
|
kind: { type: 'string', enum: ['bug', 'idea'] },
|
|
71
|
-
context: {},
|
|
72
101
|
})) return;
|
|
73
|
-
const b = req.body
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const ctx = b.context && typeof b.context === 'object' ? b.context : {};
|
|
82
|
-
const context = {
|
|
102
|
+
const b = req.body;
|
|
103
|
+
if (!/^[a-f0-9]{64}$/.test(b.sha256)) return res.fail('validation_failed', 400, [{ field: 'sha256', reason: 'not_a_sha256' }]);
|
|
104
|
+
const receipt = store.issue({
|
|
105
|
+
github_id: String(req.builder.github_id),
|
|
106
|
+
github_login: req.builder.github_login || null,
|
|
107
|
+
sha256: b.sha256,
|
|
108
|
+
kind: b.kind || 'bug',
|
|
83
109
|
core_version: runningCore(),
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
let hub;
|
|
89
|
-
try {
|
|
90
|
-
hub = await relayToHub({ idp, builder: req.builder, message, kind, context, fetchImpl });
|
|
91
|
-
} catch {
|
|
92
|
-
return res.fail('hub_unreachable', 502);
|
|
93
|
-
}
|
|
94
|
-
if (hub.status === 201 && hub.body && hub.body.ok) {
|
|
95
|
-
const { ok: _ok, ...landing } = hub.body;
|
|
96
|
-
return res.status(201).json({ ok: true, hub_origin: idp.origin, ...landing });
|
|
97
|
-
}
|
|
98
|
-
const err = hub.body && hub.body.error && typeof hub.body.error === 'object' ? hub.body.error : null;
|
|
99
|
-
if (err && PASS_THROUGH.has(err.code)) {
|
|
100
|
-
const retry = err.details && err.details.retry_after_seconds;
|
|
101
|
-
return res.fail(err.code, hub.status, retry ? { retry_after_seconds: retry } : undefined);
|
|
102
|
-
}
|
|
103
|
-
return res.fail('hub_refused', 502, { hub_status: hub.status });
|
|
104
|
-
}, { errorCode: 'feedback_failed', message: false }),
|
|
110
|
+
});
|
|
111
|
+
if (!receipt) return res.fail('rate_limited', 429, { retry_after_seconds: Math.ceil(RECEIPT_TTL_MS / 1000) });
|
|
112
|
+
res.status(201).json({ ok: true, receipt, hub: hub() });
|
|
113
|
+
}, { errorCode: 'feedback_receipt_failed', message: false }),
|
|
105
114
|
);
|
|
106
115
|
|
|
116
|
+
// rank: public — the hub redeems a receipt with no session of its own. The id
|
|
117
|
+
// is 144 random bits, known only to the CLI that asked for it and the hub it
|
|
118
|
+
// sent it to, and it works once.
|
|
119
|
+
router.get('/feedback/receipt/:id', (req, res) => {
|
|
120
|
+
const id = String(req.params.id || '');
|
|
121
|
+
const receipt = /^[A-Za-z0-9_-]{16,64}$/.test(id) ? store.redeem(id) : null;
|
|
122
|
+
res.set('Cache-Control', 'no-store');
|
|
123
|
+
if (!receipt) return res.fail('receipt_unknown', 404);
|
|
124
|
+
res.json({ ok: true, ...receipt });
|
|
125
|
+
});
|
|
126
|
+
|
|
107
127
|
return router;
|
|
108
128
|
}
|
|
109
129
|
|
|
110
130
|
module.exports = buildFeedbackRouter;
|
|
131
|
+
module.exports.makeReceiptStore = makeReceiptStore;
|
|
132
|
+
module.exports.defaultReceiptStore = defaultReceiptStore;
|
|
133
|
+
module.exports.feedbackHub = feedbackHub;
|
|
134
|
+
module.exports.DEFAULT_FEEDBACK_HUB = DEFAULT_FEEDBACK_HUB;
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bongos/upgrade-request.js — "Ask for this upgrade" on Settings → Software update
|
|
4
|
+
// (task 1004595).
|
|
5
|
+
//
|
|
6
|
+
// WHY. A project that Cloud Bongos hosts is upgraded by the people who run its server,
|
|
7
|
+
// not by its owner. Its Settings page said "Update ready" and then nothing: no button,
|
|
8
|
+
// no next step (Will, mercury, 2026-10-06, 46 versions behind with nobody told). This
|
|
9
|
+
// file gives that owner one press that reaches the operator, and remembers that it was
|
|
10
|
+
// pressed so the page can say the request is pending.
|
|
11
|
+
//
|
|
12
|
+
// THE CHANNEL IS ONE THAT ALREADY EXISTS: the direct feedback door (task 1004591). The
|
|
13
|
+
// project issues a one-time receipt for the signed-in builder in its OWN receipt store
|
|
14
|
+
// (src/bongos/routes/feedback.js), then posts the request straight to the hub's
|
|
15
|
+
// POST /api/bongos/sso/feedback/direct. The hub redeems the receipt at this project's
|
|
16
|
+
// public URL, so the request lands in the maintainers' backlog attributed to the person
|
|
17
|
+
// who really pressed the button, with no new credential and no new hub route.
|
|
18
|
+
//
|
|
19
|
+
// ONE REQUEST PER TARGET VERSION. Asking again for a version already asked for is
|
|
20
|
+
// answered from the stored row and sends nothing. A different version may be asked for
|
|
21
|
+
// once the cooldown has passed, so a project that falls further behind can say so,
|
|
22
|
+
// without a press per patch release flooding the backlog.
|
|
23
|
+
|
|
24
|
+
const crypto = require('node:crypto');
|
|
25
|
+
|
|
26
|
+
const SETTING_KEY = 'software_update.request';
|
|
27
|
+
const COOLDOWN_MS = 24 * 60 * 60 * 1000;
|
|
28
|
+
const HUB_TIMEOUT_MS = 15000;
|
|
29
|
+
|
|
30
|
+
const VERSION_RE = /^\d{1,4}\.\d{1,5}\.\d{1,6}$/;
|
|
31
|
+
const LOGIN_RE = /^[A-Za-z0-9-]{1,39}$/;
|
|
32
|
+
|
|
33
|
+
// The host this hall answers on, as the hub will need it to redeem the receipt: a bare
|
|
34
|
+
// public DNS name. Anything else (an IP, a port, localhost, a dev name) cannot be
|
|
35
|
+
// reached by the hub, so it is not askable. Mirrors the hub's own projectOrigin check.
|
|
36
|
+
function projectHost(raw) {
|
|
37
|
+
const host = String(raw || '').trim().toLowerCase();
|
|
38
|
+
if (!/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(host)) return null;
|
|
39
|
+
if (/^\d+(\.\d+)+$/.test(host)) return null;
|
|
40
|
+
if (/(^|\.)(localhost|local|internal|test|invalid)$/.test(host)) return null;
|
|
41
|
+
return host;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function hubHostOf(hub) {
|
|
45
|
+
try { return new URL(String(hub)).host.toLowerCase(); } catch { return null; }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Is this a project the hub's operator runs? Its door is the hub's, or it answers on a
|
|
49
|
+
// name under the hub's own domain (the same rule the hub uses to call a sender "verified").
|
|
50
|
+
function isOperatorRun({ doorKind, host, hub }) {
|
|
51
|
+
if (doorKind === 'local' || !hub) return false;
|
|
52
|
+
if (doorKind === 'hub') return true;
|
|
53
|
+
const h = hubHostOf(hub);
|
|
54
|
+
return !!(h && host && (host === h || host.endsWith(`.${h}`)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// The stored request, or null. A row this file did not write reads as no request.
|
|
58
|
+
function parseStored(row) {
|
|
59
|
+
if (!row || typeof row.value !== 'string') return null;
|
|
60
|
+
let v;
|
|
61
|
+
try { v = JSON.parse(row.value); } catch { return null; }
|
|
62
|
+
if (!v || !VERSION_RE.test(String(v.version || ''))) return null;
|
|
63
|
+
const at = Date.parse(v.requested_at);
|
|
64
|
+
if (!Number.isFinite(at)) return null;
|
|
65
|
+
return {
|
|
66
|
+
version: String(v.version),
|
|
67
|
+
requested_at: new Date(at).toISOString(),
|
|
68
|
+
by: typeof v.by === 'string' && LOGIN_RE.test(v.by) ? v.by : null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// What the page needs: may it offer the button, and what was asked before.
|
|
73
|
+
// `ask_again_after` is when a request for a DIFFERENT version would be taken.
|
|
74
|
+
function askState({ offered, host, doorKind, hub, stored }) {
|
|
75
|
+
const available = !!offered && isOperatorRun({ doorKind, host, hub }) && !!projectHost(host);
|
|
76
|
+
const ask = { available, hub_host: hub ? hubHostOf(hub) : null, requested: stored || null, ask_again_after: null };
|
|
77
|
+
if (stored) ask.ask_again_after = new Date(Date.parse(stored.requested_at) + COOLDOWN_MS).toISOString();
|
|
78
|
+
return ask;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// The text the operator reads. Its first line becomes the hub task's title.
|
|
82
|
+
function requestMessage({ host, running, wanted, login }) {
|
|
83
|
+
const who = login && LOGIN_RE.test(login) ? `@${login}` : 'its owner';
|
|
84
|
+
return [
|
|
85
|
+
`Upgrade request: ${host} asks for core ${wanted} (it runs ${running || 'an unknown version'})`,
|
|
86
|
+
'',
|
|
87
|
+
`${who} pressed "Ask for this upgrade" in Settings, Software update on ${host}.`,
|
|
88
|
+
'This project cannot upgrade itself: the people who run its server do it.',
|
|
89
|
+
`Move its core to ${wanted} or newer, or tell them when it will be.`,
|
|
90
|
+
].join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Send one upgrade request, or answer from the stored one. Never throws for an
|
|
95
|
+
* expected refusal; returns { status, body } for the route to send.
|
|
96
|
+
*
|
|
97
|
+
* deps: { hub, issueReceipt(receipt) → id|null, fetchImpl, getSetting, setSetting, now }
|
|
98
|
+
*/
|
|
99
|
+
async function requestUpgrade({ host, running, wanted, builder, doorKind }, deps) {
|
|
100
|
+
const now = deps.now || Date.now;
|
|
101
|
+
const hub = deps.hub;
|
|
102
|
+
const target = projectHost(host);
|
|
103
|
+
if (!wanted || !VERSION_RE.test(String(wanted))) return { status: 409, body: { error: 'nothing_to_ask', message: 'This project is up to date.' } };
|
|
104
|
+
if (!target || !isOperatorRun({ doorKind, host: target, hub })) {
|
|
105
|
+
return { status: 409, body: { error: 'not_operator_run', message: 'This project is not upgraded by an operator, so there is no one to ask.' } };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const stored = parseStored(await deps.getSetting(SETTING_KEY));
|
|
109
|
+
if (stored && stored.version === wanted) return { status: 200, body: { ok: true, already: true, request: stored } };
|
|
110
|
+
if (stored && now() < Date.parse(stored.requested_at) + COOLDOWN_MS) {
|
|
111
|
+
const retry = Math.ceil((Date.parse(stored.requested_at) + COOLDOWN_MS - now()) / 1000);
|
|
112
|
+
return { status: 429, body: { error: 'asked_recently', message: 'This project already asked for an upgrade today. Ask again tomorrow.', retry_after_seconds: retry, request: stored } };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const login = builder && typeof builder.github_login === 'string' && LOGIN_RE.test(builder.github_login) ? builder.github_login : null;
|
|
116
|
+
const message = requestMessage({ host: target, running, wanted, login });
|
|
117
|
+
const sha256 = crypto.createHash('sha256').update(message, 'utf8').digest('hex');
|
|
118
|
+
const receipt = deps.issueReceipt({
|
|
119
|
+
github_id: String(builder && builder.github_id),
|
|
120
|
+
github_login: login,
|
|
121
|
+
sha256,
|
|
122
|
+
kind: 'bug',
|
|
123
|
+
core_version: running || null,
|
|
124
|
+
});
|
|
125
|
+
if (!receipt) return { status: 429, body: { error: 'rate_limited', message: 'Too many requests just now. Try again in ten minutes.', retry_after_seconds: 600 } };
|
|
126
|
+
|
|
127
|
+
let res;
|
|
128
|
+
try {
|
|
129
|
+
res = await (deps.fetchImpl || fetch)(`${String(hub).replace(/\/+$/, '')}/api/bongos/sso/feedback/direct`, {
|
|
130
|
+
method: 'POST',
|
|
131
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
132
|
+
body: JSON.stringify({ origin: `https://${target}`, receipt, message, kind: 'bug', context: {} }),
|
|
133
|
+
redirect: 'error',
|
|
134
|
+
signal: AbortSignal.timeout(HUB_TIMEOUT_MS),
|
|
135
|
+
});
|
|
136
|
+
} catch {
|
|
137
|
+
return { status: 502, body: { error: 'operator_unreachable', message: 'Could not reach the people who run this project. Try again in a few minutes.' } };
|
|
138
|
+
}
|
|
139
|
+
if (res.status === 429) return { status: 429, body: { error: 'rate_limited', message: 'Too many requests just now. Try again in an hour.', retry_after_seconds: 3600 } };
|
|
140
|
+
if (res.status !== 202) {
|
|
141
|
+
return { status: 502, body: { error: 'operator_unreachable', message: `The request was not taken (HTTP ${res.status}). Try again later.` } };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const request = { version: String(wanted), requested_at: new Date(now()).toISOString(), by: login };
|
|
145
|
+
await deps.setSetting(SETTING_KEY, JSON.stringify(request), builder && builder.id != null ? builder.id : null);
|
|
146
|
+
return { status: 201, body: { ok: true, already: false, request } };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = {
|
|
150
|
+
SETTING_KEY,
|
|
151
|
+
COOLDOWN_MS,
|
|
152
|
+
projectHost,
|
|
153
|
+
isOperatorRun,
|
|
154
|
+
parseStored,
|
|
155
|
+
askState,
|
|
156
|
+
requestMessage,
|
|
157
|
+
requestUpgrade,
|
|
158
|
+
};
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.71'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|