@bongos/core 1.19.173 → 1.19.174
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 +15 -10
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +3 -1
- package/tests/hub_profile_leak.mjs +220 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.174",
|
|
6
|
+
"core_contract": "1.19.174",
|
|
7
|
+
"source_commit": "01b7a9adb8720d04035ce515c8ed16155486b694",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-16T19:
|
|
9
|
+
"built_at": "2026-08-16T19:56:25.262Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 305,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1273,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1599,
|
|
20
|
+
"tree_sha256": "8bdaa736f97566f66d4afb3f53a7a779eaf343a244651e18831cbf3629c05957",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -4222,12 +4222,12 @@
|
|
|
4222
4222
|
{
|
|
4223
4223
|
"path": "package-lock.json",
|
|
4224
4224
|
"mode": "0000644",
|
|
4225
|
-
"sha256": "
|
|
4225
|
+
"sha256": "3218421cd172f2d2201d5ea5573d36a5534f08a47e82eca9940eb3084275ef52"
|
|
4226
4226
|
},
|
|
4227
4227
|
{
|
|
4228
4228
|
"path": "package.json",
|
|
4229
4229
|
"mode": "0000644",
|
|
4230
|
-
"sha256": "
|
|
4230
|
+
"sha256": "907133ee8dd8cab837d620480df2af2d001825a2a9094ef528cf37a7e93a58da"
|
|
4231
4231
|
},
|
|
4232
4232
|
{
|
|
4233
4233
|
"path": "public-docs/index.html",
|
|
@@ -5832,7 +5832,7 @@
|
|
|
5832
5832
|
{
|
|
5833
5833
|
"path": "src/module-api.js",
|
|
5834
5834
|
"mode": "0000644",
|
|
5835
|
-
"sha256": "
|
|
5835
|
+
"sha256": "c4c789739d3759c20717b498e03783b14b3d8db3e53995415616885fa99d2cfc"
|
|
5836
5836
|
},
|
|
5837
5837
|
{
|
|
5838
5838
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6944,6 +6944,11 @@
|
|
|
6944
6944
|
"mode": "0000644",
|
|
6945
6945
|
"sha256": "7d384cd84cdc93b8e7b57df291a4600a40f43346fd2a52168324ecde219f1388"
|
|
6946
6946
|
},
|
|
6947
|
+
{
|
|
6948
|
+
"path": "tests/hub_profile_leak.mjs",
|
|
6949
|
+
"mode": "0000644",
|
|
6950
|
+
"sha256": "f0934fad2cd869a0955b9b087cab449932e921032ca94fc356a952e88f528d82"
|
|
6951
|
+
},
|
|
6947
6952
|
{
|
|
6948
6953
|
"path": "tests/idea_completeness.mjs",
|
|
6949
6954
|
"mode": "0000644",
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.174",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.174",
|
|
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.19.
|
|
3
|
+
"version": "1.19.174",
|
|
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/src/module-api.js
CHANGED
|
@@ -773,8 +773,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
773
773
|
// landed since 1.19.171 with no explicit bump. run 31968188738. (task 1002620)
|
|
774
774
|
// 1.19.173 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
775
775
|
// landed since 1.19.172 with no explicit bump. run 31968656001. (task 1002620)
|
|
776
|
+
// 1.19.174 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
777
|
+
// landed since 1.19.173 with no explicit bump. run 31969032984. (task 1002620)
|
|
776
778
|
// ---------------------------------------------------------------------------
|
|
777
|
-
const CORE_VERSION = '1.19.
|
|
779
|
+
const CORE_VERSION = '1.19.174'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
778
780
|
|
|
779
781
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
780
782
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
// tests/hub_profile_leak.mjs — P05, task 1002801 (goal 1000045; real DB).
|
|
2
|
+
//
|
|
3
|
+
// The mirror of tests/profile_route.mjs for the HUB profile surface (spec
|
|
4
|
+
// Constraints §1: "the whitelist is the pattern, not the exception" — the new
|
|
5
|
+
// cross-project read needs its OWN sentinel leak test over the account + rollup
|
|
6
|
+
// projection before it is public). Where profile_route.mjs stubs the hub port,
|
|
7
|
+
// this runs the REAL module projection over real SQL and the REAL public route
|
|
8
|
+
// (GET /profiles/:handle, R05), with sentinel strings planted on every
|
|
9
|
+
// module-internal value that must never serialize:
|
|
10
|
+
//
|
|
11
|
+
// - client_id + secret_hash on both reporting sso_clients,
|
|
12
|
+
// - a non-public project's name (counted in more_projects, never named),
|
|
13
|
+
// - the account's previous_handle (the 301 window key — resolvable as a URL,
|
|
14
|
+
// never a response value),
|
|
15
|
+
// - and the consent/join machinery itself: profile_state, hide_stats,
|
|
16
|
+
// github_id must not appear anywhere in a 200 body.
|
|
17
|
+
//
|
|
18
|
+
// Plus the behaviour edges: 404 on unknown / invalid / provisional
|
|
19
|
+
// (indistinguishable, fail-closed D3), hidden → cross_project null with no
|
|
20
|
+
// partial numbers, previous handle → 301 to the current one, no-store.
|
|
21
|
+
//
|
|
22
|
+
// Real-DB integration test: self-skips without a reachable Postgres; in the
|
|
23
|
+
// INTEGRATION set so the default DB-free lane never runs it.
|
|
24
|
+
//
|
|
25
|
+
// Run: DATABASE_URL=... node tests/hub_profile_leak.mjs
|
|
26
|
+
|
|
27
|
+
import { strict as assert } from 'node:assert';
|
|
28
|
+
import { createRequire } from 'node:module';
|
|
29
|
+
import http from 'node:http';
|
|
30
|
+
import { readFileSync } from 'node:fs';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
32
|
+
import { dirname, join } from 'node:path';
|
|
33
|
+
const require = createRequire(import.meta.url);
|
|
34
|
+
const express = require('express');
|
|
35
|
+
const { Pool } = require('pg');
|
|
36
|
+
|
|
37
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
38
|
+
const MIG_DIR = join(here, '..', 'modules', 'platform-identity', 'migrations');
|
|
39
|
+
const migrationSql = (f) => readFileSync(join(MIG_DIR, f), 'utf8');
|
|
40
|
+
|
|
41
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL || undefined });
|
|
42
|
+
try {
|
|
43
|
+
await pool.query('SELECT 1');
|
|
44
|
+
} catch (e) {
|
|
45
|
+
console.log(`hub_profile_leak.mjs SKIPPED — no reachable Postgres (${e.code || e.message}).`);
|
|
46
|
+
await pool.end().catch(() => {});
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The real route over the real domain — only the doorway is pinned so the
|
|
51
|
+
// route's pool IS the fixture DB (the profile_route.mjs require.cache pattern).
|
|
52
|
+
const moduleApiPath = require.resolve('../src/module-api.js');
|
|
53
|
+
require.cache[moduleApiPath] = {
|
|
54
|
+
id: moduleApiPath, filename: moduleApiPath, loaded: true,
|
|
55
|
+
exports: { pool, corsPublicGet: () => (_req, _res, next) => next() },
|
|
56
|
+
};
|
|
57
|
+
const pi = require('../modules/platform-identity/platform-identity.js');
|
|
58
|
+
const buildPublicProfileRoutes = require('../modules/platform-identity/routes/public-profile.js');
|
|
59
|
+
const { attachFail } = require('../src/bongos/middleware/error-envelope.js');
|
|
60
|
+
|
|
61
|
+
// ---- fixtures (sentinels on everything that must never serialize) ---------
|
|
62
|
+
|
|
63
|
+
const ACTIVE = 9002801001, HIDDEN = 9002801002, PROV = 9002801003;
|
|
64
|
+
const ALL_IDS = [ACTIVE, HIDDEN, PROV];
|
|
65
|
+
const SECRET_CLIENT_PUB = 'cli-DO-NOT-LEAK-p05-pub';
|
|
66
|
+
const SECRET_CLIENT_PRIV = 'cli-DO-NOT-LEAK-p05-priv';
|
|
67
|
+
const SECRET_HASH = 'HASH-DO-NOT-LEAK-p05';
|
|
68
|
+
const SECRET_PROJECT = 'P05-PRIVATE-DO-NOT-LEAK';
|
|
69
|
+
const SECRET_PREV = 'p05-prev-sentinel';
|
|
70
|
+
const PUB_ORIGIN = 'https://p05-pub.example', PRIV_ORIGIN = 'https://p05-priv.example';
|
|
71
|
+
const CP_KEYS = ['totals', 'disciplines', 'last_active', 'projects', 'more_projects'];
|
|
72
|
+
const ACCOUNT_KEYS = ['handle', 'github_login', 'display_name', 'avatar_url', 'member_since'];
|
|
73
|
+
|
|
74
|
+
const q = (sql, params) => pool.query(sql, params);
|
|
75
|
+
async function cleanup() {
|
|
76
|
+
await q(`DELETE FROM platform_identity_builder_project_activity WHERE github_id = ANY($1::bigint[])`, [ALL_IDS]).catch(() => {});
|
|
77
|
+
await q(`DELETE FROM platform_identity_accounts WHERE github_id = ANY($1::bigint[])`, [ALL_IDS]).catch(() => {});
|
|
78
|
+
await q(`DELETE FROM platform_identity_projects WHERE origin = ANY($1::text[])`, [[PUB_ORIGIN, PRIV_ORIGIN]]).catch(() => {});
|
|
79
|
+
await q(`DELETE FROM platform_identity_sso_clients WHERE client_id = ANY($1::text[])`, [[SECRET_CLIENT_PUB, SECRET_CLIENT_PRIV]]).catch(() => {});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let passed = 0, failed = 0;
|
|
83
|
+
async function t(name, fn) {
|
|
84
|
+
try { await fn(); passed++; console.log(` ok ${name}`); }
|
|
85
|
+
catch (e) { failed++; console.error(` FAIL ${name}\n ${e.message}`); }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function get(port, path) {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
const req = http.request({ host: '127.0.0.1', port, method: 'GET', path }, (res) => {
|
|
91
|
+
const chunks = [];
|
|
92
|
+
res.on('data', (c) => chunks.push(c));
|
|
93
|
+
res.on('end', () => {
|
|
94
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
95
|
+
let json = null; try { json = JSON.parse(text); } catch (_) {}
|
|
96
|
+
resolve({ status: res.statusCode, json, text, headers: res.headers });
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
req.on('error', reject);
|
|
100
|
+
req.end();
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const NEVER_IN_A_BODY = [
|
|
105
|
+
SECRET_CLIENT_PUB, SECRET_CLIENT_PRIV, SECRET_HASH, SECRET_PROJECT, SECRET_PREV,
|
|
106
|
+
'profile_state', 'hide_stats', 'github_id',
|
|
107
|
+
];
|
|
108
|
+
function assertNoSentinels(text, where) {
|
|
109
|
+
for (const s of NEVER_IN_A_BODY) {
|
|
110
|
+
assert.ok(!text.includes(s), `"${s}" leaked in ${where}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let server = null;
|
|
115
|
+
try {
|
|
116
|
+
await q(migrationSql('platform_identity_001_tables.sql'));
|
|
117
|
+
await q(migrationSql('platform_identity_004_projects.sql'));
|
|
118
|
+
await q(migrationSql('platform_identity_005_builder_activity.sql'));
|
|
119
|
+
await q(migrationSql('platform_identity_007_profile_consent.sql'));
|
|
120
|
+
await q(migrationSql('platform_identity_008_handle.sql'));
|
|
121
|
+
await cleanup();
|
|
122
|
+
|
|
123
|
+
await q(
|
|
124
|
+
`INSERT INTO platform_identity_accounts
|
|
125
|
+
(github_id, github_login, display_name, avatar_url, profile_state, hide_stats, handle, previous_handle)
|
|
126
|
+
VALUES ($1, 'p05-active-login', 'P05 Active', 'https://a.example/a.png', 'active', false, 'p05-active', $4),
|
|
127
|
+
($2, 'p05-hidden-login', 'P05 Hidden', NULL, 'active', true, 'p05-hidden', NULL),
|
|
128
|
+
($3, 'p05-prov-login', 'P05 Prov', NULL, 'provisional', false, 'p05-prov', NULL)`,
|
|
129
|
+
[ACTIVE, HIDDEN, PROV, SECRET_PREV],
|
|
130
|
+
);
|
|
131
|
+
await q(
|
|
132
|
+
`INSERT INTO platform_identity_sso_clients (client_id, secret_hash, name, origin)
|
|
133
|
+
VALUES ($1, $3, 'P05 Pub Proj', $4), ($2, $3, $5, $6)`,
|
|
134
|
+
[SECRET_CLIENT_PUB, SECRET_CLIENT_PRIV, SECRET_HASH, PUB_ORIGIN, SECRET_PROJECT, PRIV_ORIGIN],
|
|
135
|
+
);
|
|
136
|
+
await q(
|
|
137
|
+
`INSERT INTO platform_identity_projects (origin, name, featured, status)
|
|
138
|
+
VALUES ($1, 'P05 Pub Proj', true, 'active'), ($2, $3, false, 'active')`,
|
|
139
|
+
[PUB_ORIGIN, PRIV_ORIGIN, SECRET_PROJECT],
|
|
140
|
+
);
|
|
141
|
+
await q(
|
|
142
|
+
`INSERT INTO platform_identity_builder_project_activity
|
|
143
|
+
(client_id, github_id, credits, tasks_shipped, karma, rank_held, disciplines, last_active)
|
|
144
|
+
VALUES ($1, $3, 100, 5, 3, 'metic', '{engineer}', '2026-08-01T00:00:00Z'),
|
|
145
|
+
($2, $3, 50, 2, 1, 'archon', '{ui}', '2026-08-02T00:00:00Z'),
|
|
146
|
+
($1, $4, 10, 1, 0, 'metic', '{engineer}', '2026-08-03T00:00:00Z')`,
|
|
147
|
+
[SECRET_CLIENT_PUB, SECRET_CLIENT_PRIV, ACTIVE, HIDDEN],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const app = express();
|
|
151
|
+
app.use(attachFail);
|
|
152
|
+
app.use('/api/bongos', buildPublicProfileRoutes());
|
|
153
|
+
const port = await new Promise((resolve) => {
|
|
154
|
+
server = app.listen(0, '127.0.0.1', () => resolve(server.address().port));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await t('active handle → 200: identity-band whitelist, exact cross_project contract, no sentinels', async () => {
|
|
158
|
+
const r = await get(port, '/api/bongos/profiles/p05-active');
|
|
159
|
+
assert.equal(r.status, 200);
|
|
160
|
+
assert.equal(r.headers['cache-control'], 'no-store', 'hide must beat the cache (D5)');
|
|
161
|
+
assert.deepEqual(Object.keys(r.json.account).sort(), [...ACCOUNT_KEYS].sort());
|
|
162
|
+
assert.deepEqual(Object.keys(r.json.cross_project).sort(), [...CP_KEYS].sort());
|
|
163
|
+
assert.equal(r.json.cross_project.totals.projects, 2, 'both projects count');
|
|
164
|
+
assert.equal(r.json.cross_project.projects.length, 1, 'only the publicly listed project is named');
|
|
165
|
+
assert.equal(r.json.cross_project.projects[0].name, 'P05 Pub Proj');
|
|
166
|
+
assert.deepEqual(Object.keys(r.json.cross_project.projects[0]).sort(), ['name', 'origin', 'rank_held'].sort());
|
|
167
|
+
assert.equal(r.json.cross_project.more_projects, 1, 'the non-public project is counted, never named');
|
|
168
|
+
assertNoSentinels(r.text, 'the 200 body');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await t('lookup is case-insensitive', async () => {
|
|
172
|
+
const r = await get(port, '/api/bongos/profiles/P05-ACTIVE');
|
|
173
|
+
assert.equal(r.status, 200);
|
|
174
|
+
assert.equal(r.json.account.handle, 'p05-active');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await t('hidden builder → 200 with cross_project null — no partial numbers', async () => {
|
|
178
|
+
const r = await get(port, '/api/bongos/profiles/p05-hidden');
|
|
179
|
+
assert.equal(r.status, 200);
|
|
180
|
+
assert.equal(r.json.cross_project, null);
|
|
181
|
+
assert.ok(!r.text.includes('totals'), 'nothing derived from the rollup serializes');
|
|
182
|
+
assertNoSentinels(r.text, 'the hidden body');
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
await t('provisional → 404, indistinguishable from unknown (fail-closed D3)', async () => {
|
|
186
|
+
const prov = await get(port, '/api/bongos/profiles/p05-prov');
|
|
187
|
+
const ghost = await get(port, '/api/bongos/profiles/p05-no-such-handle');
|
|
188
|
+
assert.equal(prov.status, 404);
|
|
189
|
+
assert.equal(ghost.status, 404);
|
|
190
|
+
assert.equal(prov.json.error.code, ghost.json.error.code, 'same envelope for both');
|
|
191
|
+
assertNoSentinels(prov.text, 'the provisional 404 body');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
await t('an invalid handle → 404, not a 500', async () => {
|
|
195
|
+
const r = await get(port, '/api/bongos/profiles/not%20a%20handle!');
|
|
196
|
+
assert.equal(r.status, 404);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
await t('a previous handle → 301 to the current one; the old handle never echoes', async () => {
|
|
200
|
+
const r = await get(port, `/api/bongos/profiles/${SECRET_PREV}`);
|
|
201
|
+
assert.equal(r.status, 301);
|
|
202
|
+
assert.ok(r.headers.location.endsWith('/profiles/p05-active'), `Location points at the current handle (got ${r.headers.location})`);
|
|
203
|
+
assert.deepEqual(r.json, { moved_to: 'p05-active' });
|
|
204
|
+
assert.ok(!r.text.includes(SECRET_PREV), 'the requested old handle is not echoed');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await t('domain projection is clean even off-route (the port surface)', async () => {
|
|
208
|
+
const extras = await pi.getPublicProfileExtras(ACTIVE, { pool });
|
|
209
|
+
assertNoSentinels(JSON.stringify(extras), 'getPublicProfileExtras');
|
|
210
|
+
const page = await pi.getPublicProfileByHandle('p05-active', { pool });
|
|
211
|
+
assertNoSentinels(JSON.stringify(page), 'getPublicProfileByHandle');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
console.log(`\nhub_profile_leak: ${passed} passed, ${failed} failed`);
|
|
215
|
+
if (failed > 0) process.exitCode = 1;
|
|
216
|
+
} finally {
|
|
217
|
+
if (server) server.close();
|
|
218
|
+
await cleanup();
|
|
219
|
+
await pool.end().catch(() => {});
|
|
220
|
+
}
|