@bongos/core 1.19.564 → 1.19.565
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 +60 -30
- 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/0253-the-takedown-lever-has-two-writers-and-provenance-is-a-column.md +144 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +91 -4
- package/docs/api-reference.md +3 -2
- package/docs/module-api-changelog.md +2 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
- package/modules/government/catalog.js +1 -1
- package/modules/platform-identity/migrations/platform_identity_020_visibility_pull.sql +106 -0
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/pollers/visibility-pull.js +99 -0
- package/modules/platform-identity/project-catalog.js +62 -4
- package/modules/platform-identity/project-stats.js +31 -0
- package/modules/platform-identity/projects-feed.js +17 -1
- package/modules/platform-identity/routes/projects.js +48 -1
- package/modules/platform-identity/visibility-pull.js +201 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/bongos/middleware/rate-limit.js +44 -2
- package/src/module-api.js +12 -1
- package/tests/module_api.mjs +1 -0
- package/tests/project_takedown_lever.mjs +177 -0
- package/tests/visibility_pull.mjs +213 -0
- package/tests/visible_projects_feed.mjs +12 -1
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// tests/visibility_pull.mjs — the self-hosted visibility PULL (R08, task 1002327;
|
|
2
|
+
// ADR 0252, corrected by ADR 0253).
|
|
3
|
+
//
|
|
4
|
+
// DB-free. The three properties worth a test are the three that are easy to get wrong in
|
|
5
|
+
// the direction that silently breaks the map:
|
|
6
|
+
//
|
|
7
|
+
// 1. THE TRI-STATE DOES NOT PURGE. A project that answered once and is unreachable now
|
|
8
|
+
// keeps its door AND its stamp. The naive version — treat "no answer" as a value —
|
|
9
|
+
// quietly empties the map one outage at a time.
|
|
10
|
+
// 2. THE PULL NEVER TOUCHES A CONTROL-PLANE ROW, at the decision AND in the SQL. Two
|
|
11
|
+
// writers on one column is the drift ADR 0246's projection exists to prevent.
|
|
12
|
+
// 3. THE PULL NEVER MOVES `status`. This is what makes the retraction lever real
|
|
13
|
+
// (ADR 0252 §5.2) — asserted against the statement text, so adding a status write
|
|
14
|
+
// later reds this test rather than silently undoing every takedown.
|
|
15
|
+
|
|
16
|
+
import { test } from 'node:test';
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
const vp = require('../modules/platform-identity/visibility-pull.js');
|
|
22
|
+
const feed = require('../modules/platform-identity/projects-feed.js');
|
|
23
|
+
|
|
24
|
+
// A pool that records every statement and answers from a queue.
|
|
25
|
+
function recordingPool(responses = []) {
|
|
26
|
+
const calls = [];
|
|
27
|
+
const queue = [...responses];
|
|
28
|
+
return {
|
|
29
|
+
calls,
|
|
30
|
+
query(text, params) {
|
|
31
|
+
calls.push({ text, params });
|
|
32
|
+
return Promise.resolve(queue.shift() || { rows: [] });
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const manifest = (visibility) => ({ brand: { project: { visibility } } });
|
|
38
|
+
|
|
39
|
+
// ── 1. What a manifest is allowed to say ──
|
|
40
|
+
|
|
41
|
+
test('reads the join door from the live manifest key, which is `brand`, not `branding`', () => {
|
|
42
|
+
// ADR 0252 §2 and task 1002327's body both write `branding.project.visibility`, but
|
|
43
|
+
// buildInstanceManifest projects the pack under `brand`. Reading only the documented
|
|
44
|
+
// spelling would have made every pull a silent no-verdict — the failure that looks
|
|
45
|
+
// exactly like "no self-hosted project ever reports".
|
|
46
|
+
assert.equal(vp.visibilityFromManifest({ brand: { project: { visibility: 'private' } } }), 'private');
|
|
47
|
+
assert.equal(vp.visibilityFromManifest({ branding: { project: { visibility: 'private' } } }), 'private');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('an off-vocabulary or absent answer is NO VERDICT, never a default', () => {
|
|
51
|
+
// Defaulting to 'public' would let a project that said nothing be read as having asked
|
|
52
|
+
// to be on the map — ADR 0252 §2's "no verdict, never a default".
|
|
53
|
+
for (const body of [
|
|
54
|
+
{}, null, undefined,
|
|
55
|
+
{ brand: {} },
|
|
56
|
+
{ brand: { project: {} } },
|
|
57
|
+
{ brand: { project: { visibility: 'unlisted' } } },
|
|
58
|
+
{ brand: { project: { visibility: 'PUBLIC' } } }, // case is not normalised away
|
|
59
|
+
{ brand: { project: { visibility: 42 } } },
|
|
60
|
+
{ brand: { project: { visibility: { public: true } } } },
|
|
61
|
+
]) {
|
|
62
|
+
assert.equal(vp.visibilityFromManifest(body), null, `expected no verdict for ${JSON.stringify(body)}`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('the channel carries visibility ONLY — no name, tagline or art can ride it', () => {
|
|
67
|
+
// ADR 0252 §7: today no project owner controls a free-text field that reaches the
|
|
68
|
+
// public feed. A visibility report is not the place to spend that property.
|
|
69
|
+
const out = vp.visibilityFromManifest({
|
|
70
|
+
brand: { project: { visibility: 'public' }, identity: { productName: 'Injected' } },
|
|
71
|
+
name: 'Injected', tagline: 'Injected', art_url: 'https://evil.example/x.png',
|
|
72
|
+
});
|
|
73
|
+
assert.equal(out, 'public');
|
|
74
|
+
assert.equal(typeof out, 'string');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ── 2. The tri-state ──
|
|
78
|
+
|
|
79
|
+
test('never read + no answer => stays dark; nothing is written', () => {
|
|
80
|
+
const d = vp.pullDecision({ visibility: 'public', visibility_reported_at: null, visibility_source: null }, null);
|
|
81
|
+
assert.equal(d.write, false);
|
|
82
|
+
assert.equal(d.reason, 'never_reported');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('READ BEFORE + unreachable now => KEEPS its last state and its stamp', () => {
|
|
86
|
+
// The load-bearing half of the tri-state. A project does not leave the map because its
|
|
87
|
+
// server was down for one sweep (ADR 0199 / 0226). Implemented as "write nothing",
|
|
88
|
+
// which is also what keeps `visibility_reported_at` meaning LAST SUCCESSFUL read.
|
|
89
|
+
const row = { visibility: 'public', visibility_reported_at: new Date('2026-09-01'), visibility_source: 'pull' };
|
|
90
|
+
const d = vp.pullDecision(row, null);
|
|
91
|
+
assert.equal(d.write, false);
|
|
92
|
+
assert.equal(d.reason, 'unreachable_keeps_last_state');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('a NARROWING report is applied immediately — hiding yourself needs no trust', () => {
|
|
96
|
+
const row = { visibility: 'public', visibility_reported_at: new Date('2026-09-01'), visibility_source: 'pull' };
|
|
97
|
+
const d = vp.pullDecision(row, 'stealth');
|
|
98
|
+
assert.deepEqual(d, { write: true, visibility: 'stealth' });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('a WIDENING report is applied too — the pull is the door\'s only source for these rows', () => {
|
|
102
|
+
const row = { visibility: 'stealth', visibility_reported_at: new Date('2026-09-01'), visibility_source: 'pull' };
|
|
103
|
+
assert.deepEqual(vp.pullDecision(row, 'public'), { write: true, visibility: 'public' });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('an unchanged answer writes nothing, so a sweep cannot churn feed ordering', () => {
|
|
107
|
+
// listPublicProjects orders on updated_at, so bumping it every 15 minutes would make a
|
|
108
|
+
// row's position on the map track the sweep clock instead of the project.
|
|
109
|
+
const row = { visibility: 'public', visibility_reported_at: new Date('2026-09-01'), visibility_source: 'pull' };
|
|
110
|
+
assert.equal(vp.pullDecision(row, 'public').write, false);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('a first successful read DOES write, even when the value equals the column default', () => {
|
|
114
|
+
// The row starts at the NOT NULL DEFAULT 'public' with no stamp. Treating that as
|
|
115
|
+
// "unchanged" would leave visibility_reported_at NULL forever, so the project would
|
|
116
|
+
// stay dark matter no matter how many times it answered — the bug the `visibility_source
|
|
117
|
+
// === 'pull'` half of the unchanged-check exists to prevent.
|
|
118
|
+
const row = { visibility: 'public', visibility_reported_at: null, visibility_source: null };
|
|
119
|
+
assert.deepEqual(vp.pullDecision(row, 'public'), { write: true, visibility: 'public' });
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ── 3. The two writers never share a row ──
|
|
123
|
+
|
|
124
|
+
test('a control-plane row is refused at the decision, whatever it reports', () => {
|
|
125
|
+
for (const reported of ['public', 'private', 'stealth', null]) {
|
|
126
|
+
const d = vp.pullDecision({ visibility: 'private', visibility_source: 'control-plane' }, reported);
|
|
127
|
+
assert.equal(d.write, false);
|
|
128
|
+
assert.equal(d.reason, 'control_plane_row');
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('the exclusion is ALSO in the SQL, at both the read and the write', async () => {
|
|
133
|
+
// A bridge projection can land between the SELECT and the UPDATE, so the invariant has
|
|
134
|
+
// to hold at the write, not merely at the read. `IS DISTINCT FROM` and not `<>`: a NULL
|
|
135
|
+
// source is the unclaimed state, and plain inequality drops exactly those rows.
|
|
136
|
+
assert.match(vp.PULLABLE_SQL, /IS DISTINCT FROM 'control-plane'/);
|
|
137
|
+
|
|
138
|
+
const readPool = recordingPool([{ rows: [] }]);
|
|
139
|
+
await vp.pullableProjects({ pool: readPool });
|
|
140
|
+
assert.ok(readPool.calls[0].text.includes(vp.PULLABLE_SQL), 'the read must filter on PULLABLE_SQL');
|
|
141
|
+
|
|
142
|
+
const writePool = recordingPool([{ rows: [{ id: 1 }] }]);
|
|
143
|
+
await vp.recordVisibilityPull(1, 'public', { pool: writePool });
|
|
144
|
+
assert.ok(writePool.calls[0].text.includes(vp.PULLABLE_SQL), 'the write must re-assert PULLABLE_SQL');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('the write refuses an off-vocabulary value rather than trusting its caller', async () => {
|
|
148
|
+
const pool = recordingPool();
|
|
149
|
+
await assert.rejects(() => vp.recordVisibilityPull(1, 'unlisted', { pool }), /off-vocabulary/);
|
|
150
|
+
assert.equal(pool.calls.length, 0, 'nothing may reach the database');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ── 4. The pull may not move `status` ──
|
|
154
|
+
|
|
155
|
+
test('NO statement in the pull writes `status` — the retraction lever stays real', async () => {
|
|
156
|
+
// ADR 0252 §5.2: a retracted project that keeps reporting `public` STAYS hidden. The
|
|
157
|
+
// cheapest possible proof is that the column never appears in a write this file issues.
|
|
158
|
+
const pool = recordingPool([{ rows: [{ id: 7 }] }]);
|
|
159
|
+
await vp.recordVisibilityPull(7, 'public', { pool });
|
|
160
|
+
const sql = pool.calls.map((c) => c.text).join('\n');
|
|
161
|
+
assert.ok(!/\bSET\b[\s\S]*\bstatus\b/.test(sql), `a pull statement writes status:\n${sql}`);
|
|
162
|
+
assert.ok(!/status_pinned/.test(sql), 'a pull may not touch the takedown latch either');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ── 5. End to end, and the failure isolation the sweep depends on ──
|
|
166
|
+
|
|
167
|
+
test('an unreachable instance is a no-verdict, not a crash and not a value', async () => {
|
|
168
|
+
const pool = recordingPool();
|
|
169
|
+
const out = await vp.pullOne(
|
|
170
|
+
{ id: 1, origin: 'https://a.example', visibility: 'public', visibility_reported_at: new Date(), visibility_source: 'pull' },
|
|
171
|
+
{ pool, fetchVisibility: async () => { throw new Error('ECONNREFUSED'); } },
|
|
172
|
+
);
|
|
173
|
+
assert.equal(out.written, false);
|
|
174
|
+
assert.equal(out.reason, 'unreachable_keeps_last_state');
|
|
175
|
+
assert.equal(pool.calls.length, 0, 'a failed read must not write');
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('one hostile instance does not stop the sweep reading the others', async () => {
|
|
179
|
+
const rows = [
|
|
180
|
+
{ id: 1, origin: 'https://bad.example', visibility: 'public', visibility_reported_at: null, visibility_source: null },
|
|
181
|
+
{ id: 2, origin: 'https://good.example', visibility: 'public', visibility_reported_at: null, visibility_source: null },
|
|
182
|
+
];
|
|
183
|
+
const pool = recordingPool([{ rows }, { rows: [{ id: 2 }] }]);
|
|
184
|
+
const out = await vp.sweepVisibilityOnce({
|
|
185
|
+
pool,
|
|
186
|
+
fetchVisibility: async (origin) => {
|
|
187
|
+
if (origin === 'https://bad.example') throw new Error('boom');
|
|
188
|
+
return manifest('stealth');
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
assert.equal(out.scanned, 2);
|
|
192
|
+
assert.equal(out.updated, 1);
|
|
193
|
+
assert.equal(out.results[1].visibility, 'stealth');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
// ── 6. The feed predicate, and the trap it would be without its scoping ──
|
|
197
|
+
|
|
198
|
+
test('the map predicate admits a control-plane row with no stamp — ADR 0247 D4\'s trap', () => {
|
|
199
|
+
// Every row on the live table has visibility_reported_at NULL, because the column is
|
|
200
|
+
// new. A predicate that required a stamp of EVERY row would empty the public map on
|
|
201
|
+
// deploy — the exact shape ADR 0247 D4 found live, one column over. The
|
|
202
|
+
// `visibility_source = 'control-plane'` disjunct plus migration 020's backfill are what
|
|
203
|
+
// keep the default scoped to rows that have genuinely never answered.
|
|
204
|
+
assert.match(feed.DOOR_KNOWN_SQL, /visibility_source = 'control-plane'/);
|
|
205
|
+
assert.match(feed.DOOR_KNOWN_SQL, /visibility_reported_at IS NOT NULL/);
|
|
206
|
+
assert.match(feed.DOOR_KNOWN_SQL, /\bOR\b/, 'the two halves must be a disjunction, not a conjunction');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('the public feed actually carries the predicate', async () => {
|
|
210
|
+
const pool = recordingPool([{ rows: [] }]);
|
|
211
|
+
await feed.listPublicProjects({ pool });
|
|
212
|
+
assert.ok(pool.calls[0].text.includes(feed.DOOR_KNOWN_SQL), 'listPublicProjects must join DOOR_KNOWN_SQL');
|
|
213
|
+
});
|
|
@@ -256,9 +256,20 @@ async function callFeaturedFeed(rows) {
|
|
|
256
256
|
let body = null;
|
|
257
257
|
const res = {
|
|
258
258
|
json: (b) => { body = b; },
|
|
259
|
+
set: () => {},
|
|
259
260
|
fail: (code, detail) => { throw new Error(`route failed: ${code} ${JSON.stringify(detail)}`); },
|
|
260
261
|
};
|
|
261
|
-
|
|
262
|
+
// Drive the WHOLE stack, not stack[0]. Since task 1002327 the per-IP feed limiter
|
|
263
|
+
// sits in front of the handler (ADR 0252 §5.3), so calling the first entry alone
|
|
264
|
+
// would exercise the ceiling and never reach the route — and, worse, would keep
|
|
265
|
+
// passing as a silent no-op if the handler were ever removed. A fresh req per call
|
|
266
|
+
// keeps each test's reads out of the previous one's budget.
|
|
267
|
+
const req = { ip: `198.51.100.${Math.floor(Math.random() * 200) + 1}` };
|
|
268
|
+
for (const entry of layer.route.stack) {
|
|
269
|
+
let advanced = false;
|
|
270
|
+
await entry.handle(req, res, () => { advanced = true; });
|
|
271
|
+
if (!advanced) break; // something short-circuited (a refusal), or it was the handler
|
|
272
|
+
}
|
|
262
273
|
return { body, askedFor };
|
|
263
274
|
} finally {
|
|
264
275
|
pi.listPublicProjects = savedList;
|