@bongos/core 1.19.173 → 1.19.175
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
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.175",
|
|
6
|
+
"core_contract": "1.19.175",
|
|
7
|
+
"source_commit": "93e75e5bd9554037be1ec34ec0a428d9eec8db0c",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-16T20:19:55.300Z",
|
|
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": "e99154c282f827663b8bf2628c82b2ea12e0285235bc7d46844f88d25a8f2fd7",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -1332,7 +1332,7 @@
|
|
|
1332
1332
|
{
|
|
1333
1333
|
"path": "docs/adr/0171-profile-rollup-join-and-consent.md",
|
|
1334
1334
|
"mode": "0000644",
|
|
1335
|
-
"sha256": "
|
|
1335
|
+
"sha256": "93a7a996a52b1305e2247fb4b2cf0444ce973712837862b2ecba397e6f2adc71"
|
|
1336
1336
|
},
|
|
1337
1337
|
{
|
|
1338
1338
|
"path": "docs/adr/0172-editable-rank-roles-substrate-axis.md",
|
|
@@ -4222,12 +4222,12 @@
|
|
|
4222
4222
|
{
|
|
4223
4223
|
"path": "package-lock.json",
|
|
4224
4224
|
"mode": "0000644",
|
|
4225
|
-
"sha256": "
|
|
4225
|
+
"sha256": "76f039bc2d79508787cf452ae0d84cafde39f3b43e492af622baf67f64b881a6"
|
|
4226
4226
|
},
|
|
4227
4227
|
{
|
|
4228
4228
|
"path": "package.json",
|
|
4229
4229
|
"mode": "0000644",
|
|
4230
|
-
"sha256": "
|
|
4230
|
+
"sha256": "9628ea87ea7a532bfb4776e9587bb916e4b9249b91cf72da1d28d0b7d3589ad2"
|
|
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": "1a5f88144d3ee5bcd4729388f02e9d5bc62541740df4ef256914c9aa8434d1e6"
|
|
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",
|
|
@@ -27,6 +27,8 @@ Five facts found during this spike shape the decisions:
|
|
|
27
27
|
|
|
28
28
|
**3. Consent is a fail-closed `profile_state` on `platform_identity_accounts`, with one discriminator.** Migration `<redacted>.sql` (lands with R02) adds `profile_state text NOT NULL DEFAULT 'provisional' CHECK (profile_state IN ('provisional','active'))` and `hide_stats boolean NOT NULL DEFAULT false`. The single rule: **an account is `active` iff it was touched by a hub-session `/sso/authorize`** — the one path where a human is demonstrably at the hub (and where T&C acceptance can later be captured, task 1002913). Device-poll and check-in create `provisional`; the rollup endpoint creates nothing (orphan activity rows are never publicly addressable). Backfill: **all existing accounts start `provisional`** and lazily upgrade on their next hub-session authorize — no join to core `builders` (that join is type-unsafe against `system:*` text ids and would put a core-table read inside a module migration; the price is that existing hub builders' profiles publish on their next sign-in rather than immediately). Public surfaces read only `active` accounts. **R02 ships this fail-closed default without waiting on P03**, because the asymmetry only runs one way: P03 can then only ever *widen* exposure, never claw back something already published. This deliberately narrows the Phase-4 interview's "no extra consent gate" to the accounts whose consent premise actually holds; P03 is the owner either ratifying that boundary (recommended) or reverting to the interview decision verbatim — and the revert branch has a named precondition: check-in/device-created identities have **no self-serve hide path** (no hub builder session exists for them; hub admission is invite-gated), so rejecting P03 first requires an assertion-authenticated hide path or open hub enrollment. Task 1002799 carries this full decision payload.
|
|
29
29
|
|
|
30
|
+
> **P03 outcome — RATIFIED (owner, 2026-08-16, task 1002799).** The fail-closed boundary stands: check-in/device-created accounts stay `provisional` (not publicly listed, not at `/u/<handle>`, not in the sitemap or hub search) until their human signs in at the hub at least once. The owner's rationale: in the intended product flow a builder passes through cloudbongos.com at least once anyway — to find or create the projects they join — so the activation gate is satisfied naturally and costs nothing; the cloudbongos builders hall predating the hub flow is a one-time bootstrap exception, and those accounts lazily activate on their next hub sign-in exactly as the backfill rule above already provides (no backfill activation). Tasks 1002913 (T&C artifact) and 1002914 (criterion text amendments) unblock on this ship.
|
|
31
|
+
|
|
30
32
|
**4. The C2 hide flag is account-global and lives on `platform_identity_accounts.hide_stats` — not in `behavior_prefs`.** Three reasons: the data it guards is account-global hub data, so a per-instance jsonb key would fork the flag per instance; the public profile route must not grow a core→`builder-settings` read dependency that doesn't exist today; and enforcement belongs in the module's read functions (Decision 2), next to the data, where no caller can forget it. (C2's "(builder-settings)" parenthetical is therefore one of the criterion texts task 1002914 amends — the control is still self-serve in hall Settings; only its storage moves.) The write path is an own-scoped `PATCH` route in `platform-identity` (keyed by `req.builder.github_id`, `validateOrRespond`-validated so it needs no `WRITE_VALIDATION_EXEMPT` entry). Public profile responses stay **viewer-independent** — the surviving justification, with edge caching gone (Decision 5), is that a single response shape is the only thing the mirror leak test can actually pin — so the profile page renders the self-toggle client-side by comparing the already-available `/me` identity, and a builder whose stats are hidden inspects their own numbers in Settings via the own-scoped read (the C1 amendment in task 1002914 confirms Settings as the self-view home). Hidden → the track-record section renders the "keeps their track record private" line and **nothing** derived from the rollup ("no partial numbers; a count plus a hidden total is a leak by subtraction"); the local-instance profile keeps its local data (C2's fallback clause applies to the instance render path — the hub has no local data to fall back to). One residual stated honestly: hide removes the hub's *aggregated* view, not the per-instance public numbers it is derived from — every instance's `/public/leaderboard` stays public by design, so a determined reader can still reconstruct a sum the hard way. The C2 amendment text carries that caveat.
|
|
31
33
|
|
|
32
34
|
**5. Hide must beat the cache: when the port resolves (hub), profile responses are `Cache-Control: no-store`.** The route's current `public, max-age=60` would keep serving a builder's stats for up to 60s after they hide — failing C2's "takes effect immediately" and the R06 proof as literally specified (a browser-cached copy survives a reload). On plain instances the existing `max-age=60` stays. The server-rendered `/u` page inherits the same rule for hide-sensitive content (its caching posture is otherwise goal 1000048's call). Cost: hub profile API responses aren't edge-cached — acceptable at current scale (the rollup read is one indexed aggregate per view).
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.175",
|
|
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.175",
|
|
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.175",
|
|
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,12 @@ 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)
|
|
778
|
+
// 1.19.175 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
779
|
+
// landed since 1.19.174 with no explicit bump. run 31970199477. (task 1002620)
|
|
776
780
|
// ---------------------------------------------------------------------------
|
|
777
|
-
const CORE_VERSION = '1.19.
|
|
781
|
+
const CORE_VERSION = '1.19.175'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
778
782
|
|
|
779
783
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
780
784
|
// 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
|
+
}
|