@bongos/core 1.21.64 → 1.21.66
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 +45 -35
- package/docs/adr/0367-the-support-bot-answers-from-public-docs-only.md +69 -0
- package/docs/adr/README.md +1 -0
- package/docs/copy-inventory.md +19 -20
- package/docs/copy-registry.json +22 -31
- package/docs/file-map.md +1 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +0 -1
- package/docs/page-readings.json +10 -10
- package/modules/discord/db.js +32 -0
- package/modules/discord/discord-bot.js +14 -0
- package/modules/discord/discord-support.js +493 -0
- package/modules/discord/module.json +2 -2
- package/modules/hall-ui/public/profile.html +4 -7
- package/modules/hall-ui/public/profile.js +0 -5
- package/modules/memory/module.json +3 -1
- package/modules/memory/routes/search.js +9 -0
- package/modules/memory/search.js +68 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/credential-presence.js +5 -0
- package/src/module-api.js +1 -1
- package/tests/canonical_profile_url.mjs +23 -157
- package/tests/discord_support.mjs +399 -0
- package/tests/search_isolation.mjs +21 -0
- package/modules/hall-ui/public/profile-canonical.js +0 -137
package/modules/memory/search.js
CHANGED
|
@@ -84,7 +84,7 @@ function clampLimit(limit) {
|
|
|
84
84
|
// builderRank — the caller's LIVE rank (min_rank scoping).
|
|
85
85
|
// Optional: limit (clamped 1..50), sourceKind ('doc' | 'db' | null = both).
|
|
86
86
|
async function searchChunks(
|
|
87
|
-
{ q, limit = DEFAULT_LIMIT, sourceKind = null, builderId, builderRank } = {},
|
|
87
|
+
{ q, limit = DEFAULT_LIMIT, sourceKind = null, builderId, builderRank, sourceRefPatterns = null } = {},
|
|
88
88
|
{ pool: p = pool, client = null } = {}
|
|
89
89
|
) {
|
|
90
90
|
const query = typeof q === 'string' ? q.trim() : '';
|
|
@@ -93,9 +93,13 @@ async function searchChunks(
|
|
|
93
93
|
const allowedRanks = ranksAtOrBelow(builderRank);
|
|
94
94
|
const ownerId = Number.isInteger(builderId) ? builderId : (Number(builderId) || null);
|
|
95
95
|
const kind = sourceKind === 'doc' || sourceKind === 'db' ? sourceKind : null;
|
|
96
|
+
// An optional source_ref allowlist (LIKE patterns from refPatterns), applied in
|
|
97
|
+
// the same scoped CTE as the rank/owner predicate. Null = no narrowing (the
|
|
98
|
+
// route never passes it); searchPublicDocs always does.
|
|
99
|
+
const refs = Array.isArray(sourceRefPatterns) ? sourceRefPatterns : null;
|
|
96
100
|
|
|
97
101
|
// $1 q · $2 allowedRanks[] · $3 ownerId · $4 sourceKind|null · $5 legCandidates · $6 finalLimit
|
|
98
|
-
// · $7 RRF_K · $8 usefulnessWeight · $9 usefulnessScale (ADR 0095 C3)
|
|
102
|
+
// · $7 RRF_K · $8 usefulnessWeight · $9 usefulnessScale (ADR 0095 C3) · $10 refPatterns[]|null
|
|
99
103
|
// The scope predicate is shared by both legs (built once as a SQL fragment).
|
|
100
104
|
// websearch_to_tsquery tolerates arbitrary user input (no tsquery syntax to
|
|
101
105
|
// escape); the `<%` operator uses pg_trgm.word_similarity_threshold.
|
|
@@ -106,6 +110,7 @@ async function searchChunks(
|
|
|
106
110
|
WHERE min_rank = ANY($2::text[])
|
|
107
111
|
AND (owner_builder_id IS NULL OR owner_builder_id = $3::bigint)
|
|
108
112
|
AND ($4::text IS NULL OR source_kind = $4::text)
|
|
113
|
+
AND ($10::text[] IS NULL OR source_ref LIKE ANY($10::text[]))
|
|
109
114
|
),
|
|
110
115
|
fts AS (
|
|
111
116
|
SELECT id,
|
|
@@ -150,7 +155,7 @@ async function searchChunks(
|
|
|
150
155
|
ORDER BY score DESC, c.id
|
|
151
156
|
LIMIT $6;
|
|
152
157
|
`;
|
|
153
|
-
const params = [query, allowedRanks, ownerId, kind, LEG_CANDIDATES, lim, RRF_K, USEFULNESS_WEIGHT, USEFULNESS_SCALE];
|
|
158
|
+
const params = [query, allowedRanks, ownerId, kind, LEG_CANDIDATES, lim, RRF_K, USEFULNESS_WEIGHT, USEFULNESS_SCALE, refs];
|
|
154
159
|
let rows;
|
|
155
160
|
if (client) {
|
|
156
161
|
// Test seam (#950 isolation suite): the caller owns the transaction AND the
|
|
@@ -204,6 +209,64 @@ async function searchChunks(
|
|
|
204
209
|
});
|
|
205
210
|
}
|
|
206
211
|
|
|
212
|
+
// refPatterns — a docs allowlist as SQL LIKE patterns. An entry ending in '/' is
|
|
213
|
+
// a folder (prefix match); any other entry is one exact file. LIKE's own
|
|
214
|
+
// wildcards are escaped so a path with '_' or '%' in it matches only itself.
|
|
215
|
+
// Anything that is not a plain repo-relative path (absolute, '..', a backslash,
|
|
216
|
+
// empty) is dropped, so a malformed entry narrows the corpus, never widens it.
|
|
217
|
+
function refPatterns(refs) {
|
|
218
|
+
if (!Array.isArray(refs)) return [];
|
|
219
|
+
const out = [];
|
|
220
|
+
for (const raw of refs) {
|
|
221
|
+
const ref = typeof raw === 'string' ? raw.trim() : '';
|
|
222
|
+
if (!ref || ref.startsWith('/') || ref.includes('\\') || ref.split('/').includes('..')) continue;
|
|
223
|
+
const escaped = ref.replace(/[%_]/g, (c) => `\\${c}`);
|
|
224
|
+
out.push(ref.endsWith('/') ? `${escaped}%` : escaped);
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// searchPublicDocs — recall for someone with NO session: the Discord #support
|
|
230
|
+
// answerer (task 1002534, ADR 0367), served through the `memory.searchPublicDocs`
|
|
231
|
+
// port. Its scope is FIXED HERE and never taken from the caller:
|
|
232
|
+
// source_kind = 'doc' (no DB prose: task bodies, learnings, session rows)
|
|
233
|
+
// min_rank = 'xenos' (no rank-gated doc: ADR 0043's protected set is metic)
|
|
234
|
+
// owner = none (no builder's private chunk)
|
|
235
|
+
// source_ref ∈ refs (the caller's allowlist of public docs; REQUIRED)
|
|
236
|
+
// xenos is still a signed-in rank, which is why the allowlist is required: an
|
|
237
|
+
// empty or missing list returns [] rather than everything a xenos may read.
|
|
238
|
+
// Returns up to `limit` hits with the chunk body (capped at maxChars), and the
|
|
239
|
+
// body read re-applies the whole scope, so it can't widen what search found.
|
|
240
|
+
async function searchPublicDocs(
|
|
241
|
+
{ q, refs, limit = 5, maxChars = 1500 } = {},
|
|
242
|
+
{ pool: p = pool, client = null } = {}
|
|
243
|
+
) {
|
|
244
|
+
const patterns = refPatterns(refs);
|
|
245
|
+
if (!patterns.length) return [];
|
|
246
|
+
const hits = await searchChunks(
|
|
247
|
+
{ q, limit: Math.min(clampLimit(limit), 10), sourceKind: 'doc', builderId: null, builderRank: 'xenos', sourceRefPatterns: patterns },
|
|
248
|
+
{ pool: p, client }
|
|
249
|
+
);
|
|
250
|
+
if (!hits.length) return [];
|
|
251
|
+
const cap = Math.max(200, Math.min(Number(maxChars) || 1500, 4000));
|
|
252
|
+
const sql = `
|
|
253
|
+
SELECT id, source_ref, heading_path, title, left(body, $2::int) AS body
|
|
254
|
+
FROM search_chunks
|
|
255
|
+
WHERE id = ANY($1::bigint[])
|
|
256
|
+
AND source_kind = 'doc'
|
|
257
|
+
AND min_rank = 'xenos'
|
|
258
|
+
AND owner_builder_id IS NULL
|
|
259
|
+
AND source_ref LIKE ANY($3::text[])`;
|
|
260
|
+
const params = [hits.map((h) => h.id), cap, patterns];
|
|
261
|
+
const { rows } = client ? await client.query(sql, params) : await p.query(sql, params);
|
|
262
|
+
const byId = new Map(rows.map((r) => [Number(r.id), r]));
|
|
263
|
+
// Keep the search's ranking order.
|
|
264
|
+
return hits.filter((h) => byId.has(h.id)).map((h) => {
|
|
265
|
+
const r = byId.get(h.id);
|
|
266
|
+
return { id: h.id, source_ref: r.source_ref, heading_path: r.heading_path, title: r.title, body: r.body || '', score: h.score };
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
207
270
|
// fetchChunkBody — the EXPAND half of progressive disclosure (ADR 0095 C1 / task
|
|
208
271
|
// 1658). Given a chunk id, return its FULL body — but scoped exactly like
|
|
209
272
|
// searchChunks (same min_rank + owner predicate built from the authenticated
|
|
@@ -505,6 +568,8 @@ function classifyEmptyResult(health) {
|
|
|
505
568
|
|
|
506
569
|
module.exports = {
|
|
507
570
|
searchChunks,
|
|
571
|
+
searchPublicDocs,
|
|
572
|
+
refPatterns,
|
|
508
573
|
// task 1003097: a zero-result search must say WHICH of three things happened —
|
|
509
574
|
// a dead index, a scoping wall, or a genuine miss.
|
|
510
575
|
corpusHealth,
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.66",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.66",
|
|
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.21.
|
|
3
|
+
"version": "1.21.66",
|
|
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/release-notes.json
CHANGED
|
@@ -9160,5 +9160,17 @@
|
|
|
9160
9160
|
"id": "1002304",
|
|
9161
9161
|
"text": "Owners now see people who applied with a guild grouped under that guild's name in the applications queue, with one \"Invite all\" button that still admits each person individually."
|
|
9162
9162
|
}
|
|
9163
|
+
],
|
|
9164
|
+
"1.21.65": [
|
|
9165
|
+
{
|
|
9166
|
+
"id": "1002534",
|
|
9167
|
+
"text": "The Discord bot can now answer questions in #support when someone @-mentions it, using only the project's public newcomer docs. It ships switched off: it starts answering once the owner adds an Anthropic key and turns it on, a"
|
|
9168
|
+
}
|
|
9169
|
+
],
|
|
9170
|
+
"1.21.66": [
|
|
9171
|
+
{
|
|
9172
|
+
"id": "1003451",
|
|
9173
|
+
"text": "The builder profile page no longer downloads a script that could never run; nothing on the page changes for anyone."
|
|
9174
|
+
}
|
|
9163
9175
|
]
|
|
9164
9176
|
}
|
|
@@ -96,6 +96,11 @@ const CREDENTIALS = [
|
|
|
96
96
|
names: ['DISCORD_BOT_TOKEN'],
|
|
97
97
|
purpose: 'the Discord bot (modules/discord); also readable from discord-bot.json in the config dir',
|
|
98
98
|
},
|
|
99
|
+
{
|
|
100
|
+
id: 'discord-support', file: 'web.env', need: 'optional',
|
|
101
|
+
names: ['DISCORD_SUPPORT_ANTHROPIC_API_KEY'],
|
|
102
|
+
purpose: 'the #support answerer (modules/discord/discord-support.js, ADR 0367); its own key, never a host-wide ANTHROPIC_API_KEY',
|
|
103
|
+
},
|
|
99
104
|
{
|
|
100
105
|
id: 'discord-oauth', file: 'web.env', need: 'optional',
|
|
101
106
|
names: ['DISCORD_CLIENT_ID', 'DISCORD_CLIENT_SECRET'],
|
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.66'; // 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');
|
|
@@ -9,14 +9,15 @@
|
|
|
9
9
|
// OWN WHERE CLAUSE and throws on a placeholder or a literal comparison it did
|
|
10
10
|
// not consume, so a predicate this file cannot read can never degrade into "no
|
|
11
11
|
// constraint" and quietly pass. The REAL core router runs above it over a REAL
|
|
12
|
-
// loopback socket
|
|
13
|
-
// Nothing here asserts against a canned response.
|
|
12
|
+
// loopback socket. Nothing here asserts against a canned response.
|
|
14
13
|
//
|
|
15
|
-
// THE FLAG IS FALSE
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
14
|
+
// THE FLAG IS FALSE (ADR 0214 D1), and the page that would flip it (goal
|
|
15
|
+
// 1000048 R14, task 1002365) was abandoned. So the browser half that would have
|
|
16
|
+
// rendered rel=canonical, the link-out and the numeric-id redirect was removed
|
|
17
|
+
// (task 1003451): it shipped 137 lines of permanently dead branches to every
|
|
18
|
+
// /profile reader. The server half stays — getPublicHandle is live, and the
|
|
19
|
+
// resolver is what a rebuilt page will call. The test below pins the absence,
|
|
20
|
+
// and fails the moment the flag flips without the browser half coming back.
|
|
20
21
|
//
|
|
21
22
|
// LANE: DB-free. It must run on every PR, where there is no Postgres. The last
|
|
22
23
|
// test asserts that membership from the runner's own source (learning 1000218 —
|
|
@@ -26,7 +27,6 @@
|
|
|
26
27
|
import { test } from 'node:test';
|
|
27
28
|
import assert from 'node:assert/strict';
|
|
28
29
|
import http from 'node:http';
|
|
29
|
-
import vm from 'node:vm';
|
|
30
30
|
import express from 'express';
|
|
31
31
|
import { createRequire } from 'node:module';
|
|
32
32
|
import { readFileSync } from 'node:fs';
|
|
@@ -48,7 +48,6 @@ const require = createRequire(import.meta.url);
|
|
|
48
48
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
49
49
|
const read = (...p) => readFileSync(join(ROOT, ...p), 'utf8');
|
|
50
50
|
|
|
51
|
-
const CLIENT_SRC = read('modules', 'hall-ui', 'public', 'profile-canonical.js');
|
|
52
51
|
const MODULE_SRC = read('modules', 'platform-identity', 'canonical-profile.js');
|
|
53
52
|
const PROFILE_JS = read('modules', 'hall-ui', 'public', 'profile.js');
|
|
54
53
|
const PROFILE_HTML = read('modules', 'hall-ui', 'public', 'profile.html');
|
|
@@ -429,154 +428,21 @@ test('an older hub provider without getPublicHandle degrades to null instead of
|
|
|
429
428
|
test.after(() => server.close());
|
|
430
429
|
|
|
431
430
|
// ===========================================================================
|
|
432
|
-
// The
|
|
431
|
+
// The wiring: the page and the stamp
|
|
433
432
|
// ===========================================================================
|
|
434
433
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
function boot(src, { hubOriginMeta, modules, search } = {}) {
|
|
444
|
-
const head = { children: [], appendChild(el) { this.children.push(el); } };
|
|
445
|
-
const els = new Map();
|
|
446
|
-
const el = (id) => {
|
|
447
|
-
if (!els.has(id)) els.set(id, { id, hidden: true, _html: '', attrs: {}, setAttribute(k, v) { this.attrs[k] = v; }, getAttribute(k) { return this.attrs[k] ?? null; }, set innerHTML(v) { this._html = String(v); }, get innerHTML() { return this._html; } });
|
|
448
|
-
return els.get(id);
|
|
449
|
-
};
|
|
450
|
-
const meta = hubOriginMeta === undefined ? null : { getAttribute: () => hubOriginMeta };
|
|
451
|
-
const replaced = [];
|
|
452
|
-
const assigned = [];
|
|
453
|
-
const sandbox = {
|
|
454
|
-
console: { error() {}, warn() {}, log() {} },
|
|
455
|
-
URL,
|
|
456
|
-
URLSearchParams,
|
|
457
|
-
document: {
|
|
458
|
-
head,
|
|
459
|
-
querySelector: (sel) => (sel === 'meta[name="hub-origin"]' ? meta : (head.children.find((c) => c.attrs.rel === 'canonical') || null)),
|
|
460
|
-
getElementById: (id) => (id === 'xp-scroll' || id === 'xp-body' ? el(id) : null),
|
|
461
|
-
createElement: () => ({ attrs: {}, setAttribute(k, v) { this.attrs[k] = v; }, getAttribute(k) { return this.attrs[k] ?? null; } }),
|
|
462
|
-
},
|
|
463
|
-
// `assign` is stubbed too, and deliberately records into a SEPARATE list:
|
|
464
|
-
// swapping replace→assign must fail on the assertion (the numeric URL would
|
|
465
|
-
// stay in history as a place Back returns to), not crash on an undefined
|
|
466
|
-
// method, which would "pass" the mutation check for the wrong reason.
|
|
467
|
-
location: { search: search || '', replace: (u) => replaced.push(u), assign: (u) => assigned.push(u), href: '' },
|
|
468
|
-
window: { OTB: { escapeHtml: (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') }, __MODULES__: modules || [] },
|
|
469
|
-
};
|
|
470
|
-
sandbox.globalThis = sandbox;
|
|
471
|
-
vm.createContext(sandbox);
|
|
472
|
-
vm.runInContext(src, sandbox, { filename: 'profile-canonical.js' });
|
|
473
|
-
return {
|
|
474
|
-
api: sandbox.window.OTBCanonical,
|
|
475
|
-
canonicalHref: () => { const l = head.children.find((c) => c.attrs.rel === 'canonical'); return l ? l.attrs.href : null; },
|
|
476
|
-
xp: () => el('xp-scroll'),
|
|
477
|
-
xpHtml: () => el('xp-body').innerHTML,
|
|
478
|
-
replaced,
|
|
479
|
-
assigned,
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
const FEDERATED = { hubOriginMeta: 'https://cloudbongos.com', modules: ['lifecycle', 'hall-ui'] };
|
|
484
|
-
const HUB = { hubOriginMeta: 'https://cloudbongos.com', modules: ['platform-identity', 'hall-ui'] };
|
|
485
|
-
|
|
486
|
-
test('DARK today: with the shipped source nothing is emitted, anywhere', () => {
|
|
487
|
-
const fed = boot(CLIENT_SRC, FEDERATED);
|
|
488
|
-
fed.api.render({ handle: 'ada' });
|
|
489
|
-
assert.equal(fed.canonicalHref(), null, 'a canonical must not name a URL that 404s');
|
|
490
|
-
assert.equal(fed.xp().hidden, true);
|
|
491
|
-
const hub = boot(CLIENT_SRC, { ...HUB, search: '?id=7' });
|
|
492
|
-
hub.api.render({ handle: 'ada' });
|
|
493
|
-
assert.deepEqual(hub.replaced, [], 'redirecting a working page to a 404 would be the worst of the three');
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
test('LIVE, federated: rel=canonical points at the hub and the link-out is visible', () => {
|
|
497
|
-
const page = boot(LIVE_SRC, FEDERATED);
|
|
498
|
-
page.api.render({ handle: 'ada' });
|
|
499
|
-
assert.equal(page.canonicalHref(), 'https://cloudbongos.com/u/ada');
|
|
500
|
-
assert.equal(page.xp().hidden, false);
|
|
501
|
-
assert.match(page.xpHtml(), /href="https:\/\/cloudbongos\.com\/u\/ada"/);
|
|
502
|
-
assert.match(page.xpHtml(), /See the cross-project record/,
|
|
503
|
-
'the link-out is what ADR 0171 D1 asks a federated instance to render instead of inlining numbers');
|
|
504
|
-
assert.deepEqual(page.replaced, [], 'a federated instance shows its OWN record — it must never redirect away from it');
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
test('LIVE, hub: the numeric ?id= form redirects to the canonical handle URL', () => {
|
|
508
|
-
const page = boot(LIVE_SRC, { ...HUB, search: '?id=7' });
|
|
509
|
-
page.api.render({ handle: 'ada' });
|
|
510
|
-
assert.deepEqual(page.replaced, ['https://cloudbongos.com/u/ada']);
|
|
511
|
-
assert.equal(page.canonicalHref(), null, 'a page that redirects must not also declare a canonical');
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
test('LIVE, hub without ?id=: the self view is not redirected', () => {
|
|
515
|
-
const page = boot(LIVE_SRC, { ...HUB, search: '' });
|
|
516
|
-
page.api.render({ handle: 'ada' });
|
|
517
|
-
assert.deepEqual(page.replaced, [], 'the handle-less /profile is the viewer\'s own page, not a numeric address');
|
|
518
|
-
});
|
|
519
|
-
|
|
520
|
-
test('LIVE: no handle means no canonical, no link-out and NO REDIRECT — the fallback is a working page', () => {
|
|
521
|
-
for (const data of [{}, { handle: null }, { handle: '' }]) {
|
|
522
|
-
const fed = boot(LIVE_SRC, FEDERATED);
|
|
523
|
-
fed.api.render(data);
|
|
524
|
-
assert.equal(fed.canonicalHref(), null);
|
|
525
|
-
assert.equal(fed.xp().hidden, true);
|
|
526
|
-
|
|
527
|
-
const hub = boot(LIVE_SRC, { ...HUB, search: '?id=7' });
|
|
528
|
-
hub.api.render(data);
|
|
529
|
-
assert.deepEqual(hub.replaced, [],
|
|
530
|
-
'absent, provisional and private all arrive here as a missing handle and all get the ordinary local page — there is no error shape to read a signal off');
|
|
531
|
-
}
|
|
532
|
-
});
|
|
533
|
-
|
|
534
|
-
test('LIVE: a vanilla instance with no hub origin emits nothing', () => {
|
|
535
|
-
for (const meta of [undefined, '', ' ', 'http://cloudbongos.com', 'https://cloudbongos.com/u']) {
|
|
536
|
-
const page = boot(LIVE_SRC, { hubOriginMeta: meta, modules: [] });
|
|
537
|
-
page.api.render({ handle: 'ada' });
|
|
538
|
-
assert.equal(page.canonicalHref(), null, `an unusable hub-origin meta (${JSON.stringify(meta)}) must produce no canonical, never a relative one`);
|
|
539
|
-
assert.equal(page.xp().hidden, true);
|
|
540
|
-
}
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
test('LIVE: a hostile handle from the wire never reaches an href', () => {
|
|
544
|
-
for (const handle of ['ada/../../admin', 'ada"onload=x', '../evil', 'a b']) {
|
|
545
|
-
const page = boot(LIVE_SRC, FEDERATED);
|
|
546
|
-
page.api.render({ handle });
|
|
547
|
-
assert.equal(page.canonicalHref(), null, `handle ${JSON.stringify(handle)} must be refused, not encoded`);
|
|
548
|
-
}
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
// ===========================================================================
|
|
552
|
-
// The wiring: the mirror, the page, and the stamp
|
|
553
|
-
// ===========================================================================
|
|
554
|
-
|
|
555
|
-
test('the client mirror and the module agree on both constants', () => {
|
|
556
|
-
const liveInModule = /const CANONICAL_PROFILE_PAGE_LIVE = (true|false);/.exec(MODULE_SRC);
|
|
557
|
-
const liveInClient = /const CANONICAL_PROFILE_PAGE_LIVE = (true|false);/.exec(CLIENT_SRC);
|
|
558
|
-
assert.ok(liveInModule && liveInClient, 'both files must spell the flag as a plain const so this guard can read it');
|
|
559
|
-
assert.equal(liveInClient[1], liveInModule[1],
|
|
560
|
-
'the browser mirror has drifted from modules/platform-identity/canonical-profile.js — flip both or neither');
|
|
561
|
-
assert.equal(String(CANONICAL_PROFILE_PAGE_LIVE), liveInModule[1], 'the module export disagrees with its own source');
|
|
562
|
-
|
|
563
|
-
const prefixInClient = /const CANONICAL_PROFILE_PREFIX = '([^']+)';/.exec(CLIENT_SRC);
|
|
564
|
-
assert.ok(prefixInClient, 'the client must spell the prefix as a plain const');
|
|
565
|
-
assert.equal(prefixInClient[1], CANONICAL_PROFILE_PREFIX, 'the two canonical namespaces have drifted apart');
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
test('the profile page loads the script BEFORE profile.js, and profile.js calls it', () => {
|
|
569
|
-
// Match the <script> TAGS, not the filenames: both names also appear in the
|
|
570
|
-
// page's prose, and an ordering assertion that reads a comment proves nothing.
|
|
434
|
+
test('while the page is dark, /profile ships no canonical wiring at all (task 1003451)', () => {
|
|
435
|
+
// The browser half (profile-canonical.js) was removed when R14 — the
|
|
436
|
+
// /u/<handle> page that would have turned it on — was abandoned: 137 lines
|
|
437
|
+
// whose every branch sat behind a false flag, shipped to every /profile
|
|
438
|
+
// reader. Rebuild it with the page. Until then the module's flag is the one
|
|
439
|
+
// statement of "dark", and nothing in the hall may branch on it.
|
|
440
|
+
assert.equal(CANONICAL_PROFILE_PAGE_LIVE, false,
|
|
441
|
+
'the flag flipped: the /u/<handle> page now resolves, so the browser half (rel=canonical, link-out, numeric-id redirect) has to be rebuilt in this same change');
|
|
571
442
|
const tags = [...PROFILE_HTML.matchAll(/<script\s+src="\/builders\/([a-z-]+\.js)/g)].map((m) => m[1]);
|
|
572
|
-
assert.ok(tags.includes('profile
|
|
573
|
-
|
|
574
|
-
assert.
|
|
575
|
-
tags.indexOf('profile-canonical.js') < tags.indexOf('profile.js'),
|
|
576
|
-
`profile.js must load AFTER profile-canonical.js so window.OTBCanonical exists when renderProfile runs — got ${tags.join(', ')}`,
|
|
577
|
-
);
|
|
578
|
-
assert.match(PROFILE_JS, /window\.OTBCanonical\.render\(data\)/,
|
|
579
|
-
'profile.js must hand the profile response to the canonical wiring');
|
|
443
|
+
assert.ok(tags.includes('profile.js'), 'the tag reader matched nothing, so the next assertion would pass vacuously');
|
|
444
|
+
assert.ok(!tags.includes('profile-canonical.js'), 'the page loads a canonical script whose every branch is dead');
|
|
445
|
+
assert.doesNotMatch(PROFILE_JS, /OTBCanonical/, 'profile.js branches on a canonical wiring that does not exist');
|
|
580
446
|
});
|
|
581
447
|
|
|
582
448
|
test('the hub origin is stamped as a meta, and never straight into an href', () => {
|
|
@@ -593,9 +459,9 @@ test('the hub origin is stamped as a meta, and never straight into an href', ()
|
|
|
593
459
|
});
|
|
594
460
|
|
|
595
461
|
test('the resolver is the ONLY place that spells the canonical namespace', () => {
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
const owners = ['modules/platform-identity/canonical-profile.js'
|
|
462
|
+
// One file carries it: the module. A second would be a second opinion
|
|
463
|
+
// about where a profile lives.
|
|
464
|
+
const owners = ['modules/platform-identity/canonical-profile.js'];
|
|
599
465
|
const offenders = [];
|
|
600
466
|
const walk = (rel) => {
|
|
601
467
|
for (const e of require('node:fs').readdirSync(join(ROOT, rel), { withFileTypes: true })) {
|