@bongos/core 1.21.69 → 1.21.70
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 +24 -24
- package/.claude/skills/bongos-feedback/SKILL.md +10 -46
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -2
- package/clients/bongos-client/index.cjs +6 -2
- package/clients/bongos-client/index.d.ts +10 -3
- package/clients/bongos-client/index.mjs +6 -2
- package/docs/api/openapi.json +151 -15
- package/docs/api-reference.md +6 -4
- package/docs/module-api-changelog.md +2 -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 +5 -0
- package/scripts/gds/feedback-send.js +144 -71
- package/src/bongos/platform-visibility-gate.js +3 -0
- package/src/bongos/routes/feedback.js +91 -77
- package/src/module-api.js +1 -1
- package/tests/instance_feedback.mjs +267 -68
|
@@ -1,110 +1,124 @@
|
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
+
};
|
|
57
78
|
}
|
|
58
79
|
|
|
59
|
-
function buildFeedbackRouter({
|
|
80
|
+
function buildFeedbackRouter({ store = makeReceiptStore(), hub = feedbackHub } = {}) {
|
|
60
81
|
const router = express.Router();
|
|
61
82
|
|
|
62
|
-
// rank: any signed-in builder —
|
|
63
|
-
// here, and the hub files every report
|
|
83
|
+
// rank: any signed-in builder — a receipt only says "this builder sent this
|
|
84
|
+
// text"; it grants nothing here, and the hub files every report behind a nod.
|
|
64
85
|
router.post(
|
|
65
|
-
'/feedback',
|
|
86
|
+
'/feedback/receipt',
|
|
66
87
|
auth.requireBuilder,
|
|
67
|
-
asyncHandler('POST /feedback', async (req, res) => {
|
|
88
|
+
asyncHandler('POST /feedback/receipt', async (req, res) => {
|
|
68
89
|
if (validateOrRespond(req, res, {
|
|
69
|
-
|
|
90
|
+
sha256: { required: true, type: 'string', maxLength: 64 },
|
|
70
91
|
kind: { type: 'string', enum: ['bug', 'idea'] },
|
|
71
|
-
context: {},
|
|
72
92
|
})) 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 = {
|
|
93
|
+
const b = req.body;
|
|
94
|
+
if (!/^[a-f0-9]{64}$/.test(b.sha256)) return res.fail('validation_failed', 400, [{ field: 'sha256', reason: 'not_a_sha256' }]);
|
|
95
|
+
const receipt = store.issue({
|
|
96
|
+
github_id: String(req.builder.github_id),
|
|
97
|
+
github_login: req.builder.github_login || null,
|
|
98
|
+
sha256: b.sha256,
|
|
99
|
+
kind: b.kind || 'bug',
|
|
83
100
|
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 }),
|
|
101
|
+
});
|
|
102
|
+
if (!receipt) return res.fail('rate_limited', 429, { retry_after_seconds: Math.ceil(RECEIPT_TTL_MS / 1000) });
|
|
103
|
+
res.status(201).json({ ok: true, receipt, hub: hub() });
|
|
104
|
+
}, { errorCode: 'feedback_receipt_failed', message: false }),
|
|
105
105
|
);
|
|
106
106
|
|
|
107
|
+
// rank: public — the hub redeems a receipt with no session of its own. The id
|
|
108
|
+
// is 144 random bits, known only to the CLI that asked for it and the hub it
|
|
109
|
+
// sent it to, and it works once.
|
|
110
|
+
router.get('/feedback/receipt/:id', (req, res) => {
|
|
111
|
+
const id = String(req.params.id || '');
|
|
112
|
+
const receipt = /^[A-Za-z0-9_-]{16,64}$/.test(id) ? store.redeem(id) : null;
|
|
113
|
+
res.set('Cache-Control', 'no-store');
|
|
114
|
+
if (!receipt) return res.fail('receipt_unknown', 404);
|
|
115
|
+
res.json({ ok: true, ...receipt });
|
|
116
|
+
});
|
|
117
|
+
|
|
107
118
|
return router;
|
|
108
119
|
}
|
|
109
120
|
|
|
110
121
|
module.exports = buildFeedbackRouter;
|
|
122
|
+
module.exports.makeReceiptStore = makeReceiptStore;
|
|
123
|
+
module.exports.feedbackHub = feedbackHub;
|
|
124
|
+
module.exports.DEFAULT_FEEDBACK_HUB = DEFAULT_FEEDBACK_HUB;
|
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.70'; // 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');
|