@bongos/core 1.21.65 → 1.21.67
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 +114 -49
- package/docs/copy-inventory.md +432 -405
- package/docs/copy-registry.json +840 -571
- package/docs/file-map.md +1 -0
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +45 -13
- package/docs/page-readings.json +969 -917
- package/modules/hall-ui/public/profile.html +4 -7
- package/modules/hall-ui/public/profile.js +0 -5
- package/modules/platform-identity/routes/my-projects.js +1 -1
- package/modules/public-landing/public/account.html +1 -1
- package/modules/public-landing/public/assets/cosmos.css +12 -10
- package/modules/public-landing/public/contact.html +1 -1
- package/modules/public-landing/public/guild.html +1 -1
- package/modules/public-landing/public/index-cli.states.json +20 -0
- package/modules/public-landing/public/index.html +68 -1
- package/modules/public-landing/public/index.states.json +4 -4
- package/modules/public-landing/public/invitations-empty.states.json +56 -0
- package/modules/public-landing/public/invitations-picker.states.json +84 -0
- package/modules/public-landing/public/invitations.html +956 -0
- package/modules/public-landing/public/invitations.probes.json +430 -0
- package/modules/public-landing/public/invitations.states.json +473 -0
- package/modules/public-landing/public/privacy.html +1 -1
- package/modules/public-landing/public/projects.html +18 -543
- package/modules/public-landing/public/projects.probes.json +0 -29
- package/modules/public-landing/public/projects.states.json +0 -6
- package/modules/public-landing/public/terms.html +1 -1
- package/modules/ui-design/kit/fixtures/bongos-cli-latest-sample.json +10 -0
- package/modules/ui-design/kit/fixtures/featured-invitations.json +183 -0
- package/modules/ui-design/kit/fixtures/guilds-empty.json +1 -0
- package/modules/ui-design/kit/fixtures/me-guild-engagements-empty.json +1 -0
- package/modules/ui-design/kit/fixtures/me-guild-requests-empty.json +1 -0
- package/modules/ui-design/kit/fixtures/my-projects-applied.json +29 -0
- package/modules/ui-design/kit/fixtures/my-projects-invites-empty.json +1 -0
- package/modules/ui-design/kit/serve.js +5 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +16 -0
- package/src/module-api.js +1 -1
- package/src/platform-server.js +11 -0
- package/tests/canonical_profile_url.mjs +23 -157
- package/tests/hub_account_page.mjs +1 -1
- package/tests/hub_engagement_cards.mjs +44 -40
- package/tests/hub_guild_cards.mjs +24 -22
- package/tests/hub_invitations_page.mjs +414 -0
- package/tests/join_relay_outcome.mjs +1 -1
- package/tests/landing_legal_pages.mjs +8 -6
- package/tests/landing_page.mjs +125 -3
- package/tests/my_projects_surface.mjs +14 -6
- package/tests/public_landing_projects.mjs +12 -14
- package/tests/render_check.mjs +3 -2
- package/tests/ui_design_kit.mjs +3 -3
- package/modules/hall-ui/public/profile-canonical.js +0 -137
|
@@ -21,9 +21,10 @@
|
|
|
21
21
|
// audit could not confirm, so it is asserted here rather than described.
|
|
22
22
|
// 3. WHAT WAS STILL MISSING. The invitations half had no caller anywhere — no
|
|
23
23
|
// surface fetched the invite list, so a builder holding an invitation could
|
|
24
|
-
// not act on it. Closed by task 1003774
|
|
25
|
-
//
|
|
26
|
-
//
|
|
24
|
+
// not act on it. Closed by task 1003774, which put the list in the hub's
|
|
25
|
+
// /projects?view=mine (idea 1000903); task 1004276 moved it to the hub's own
|
|
26
|
+
// /invitations page. The pin below flipped from "no surface calls this" to
|
|
27
|
+
// "some surface does" for the same reason.
|
|
27
28
|
//
|
|
28
29
|
// Static reads only (no import of the app, no DB) → runs in the `unit` gate.
|
|
29
30
|
//
|
|
@@ -116,10 +117,17 @@ test('an invitations surface exists — the hub reads GET /my-projects/invites (
|
|
|
116
117
|
// dead end the moment a caller exists to press it. Each invite renders as a
|
|
117
118
|
// way IN (a link to the project) instead — signing in there is the acceptance
|
|
118
119
|
// act (task 1002969).
|
|
120
|
+
// Both hub pages: /invitations holds the invite list now (task 1004276), and
|
|
121
|
+
// /projects is where it lived and where a regression could put a caller back.
|
|
122
|
+
const HUB_PAGES = ['modules/public-landing/public/invitations.html', 'modules/public-landing/public/projects.html'];
|
|
119
123
|
test('the hub never calls the accept route — ADR 0335 retired that flip', () => {
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
124
|
+
for (const rel of HUB_PAGES) {
|
|
125
|
+
assert.doesNotMatch(read(rel), /invites\/[^'"]*\/accept/,
|
|
126
|
+
`${rel}: a hub caller of the accept route reopens the dead end ADR 0335 closed — render the invite as a link into the project instead`);
|
|
127
|
+
}
|
|
128
|
+
// the guard is only meaningful where the invite list is: it must be on one of these pages
|
|
129
|
+
assert.ok(HUB_PAGES.some((rel) => read(rel).includes("'/my-projects/invites'")),
|
|
130
|
+
'the invite list moved off both hub pages — point this guard at wherever it went');
|
|
123
131
|
});
|
|
124
132
|
|
|
125
133
|
// Task 1002818 (ADR 0353) finished the retirement on the server: the route is gone,
|
|
@@ -521,28 +521,26 @@ test('a stamped asset still serves — the query string is not part of the filen
|
|
|
521
521
|
// nowhere to live until now. loadMemberships() already fetched GET /my-projects
|
|
522
522
|
// for the map's Apply button (R10, task 1002286) — this just renders the rest
|
|
523
523
|
// of what that same read carries, plus the separate invites list.
|
|
524
|
-
|
|
524
|
+
// The invitations and the join-by-address field moved to their own page with task
|
|
525
|
+
// 1004276 (the owner's call, ADR 0337 D5); tests/hub_invitations_page.mjs holds that
|
|
526
|
+
// page as served. What stays here: building-on with Leave, and a door to the page.
|
|
527
|
+
test('#view-mine keeps building-on with Leave, and points at /invitations for the invitations and the join field', async () => {
|
|
525
528
|
const { body } = await request('/projects', 'game.test.local');
|
|
526
|
-
assert.match(body, /\/my-projects\/invites/, 'GET /my-projects/invites must be called');
|
|
527
|
-
assert.match(body, /id="invitesList"/);
|
|
528
529
|
assert.match(body, /id="buildingList"/);
|
|
529
|
-
assert.match(body, /\/my-projects\/join/, 'the join-by-URL field must post to /my-projects/join');
|
|
530
|
-
assert.match(body, /id="joinOrigin"/);
|
|
531
|
-
assert.match(body, /id="joinNote"/, 'the applicant note (task 1002330) rides along, and stays optional');
|
|
532
530
|
assert.match(body, /\/my-projects\/'\s*\+\s*encodeURIComponent/, 'Leave must address the project by its own client id');
|
|
533
|
-
|
|
534
|
-
|
|
531
|
+
for (const gone of ['invitesList', 'guildInviteList', 'engageList', 'guildRequestList', 'joinOrigin', 'joinNote', 'joinBtn', 'mineInvites']) {
|
|
532
|
+
assert.doesNotMatch(body, new RegExp(`id="${gone}"`), `#${gone} moved to /invitations`);
|
|
533
|
+
}
|
|
534
|
+
assert.doesNotMatch(body, /\/my-projects\/invites/, 'the invitations are read on their own page now');
|
|
535
|
+
assert.match(body, /<h2 id="mineToInvitesH">Invitations<\/h2>\s*<p class="lede">[^<]*<a href="\/invitations">Open your invitations<\/a><\/p>/, 'the mine view says where they went');
|
|
536
|
+
assert.match(body, /\/my-projects\/join/, 'the map card’s Apply to join still posts the join relay');
|
|
535
537
|
});
|
|
536
538
|
|
|
537
|
-
test('
|
|
539
|
+
test('the hub never calls the retired accept flip (ADR 0335)', async () => {
|
|
538
540
|
const { body } = await request('/projects', 'game.test.local');
|
|
539
541
|
// the hub-only accept flip never admits anyone to the project's own instance
|
|
540
|
-
// (ADR 0335) — a caller of it here would reopen the exact dead end that ADR
|
|
541
|
-
// retired, so the invite's action is an outbound link instead
|
|
542
|
+
// (ADR 0335) — a caller of it here would reopen the exact dead end that ADR retired
|
|
542
543
|
assert.doesNotMatch(body, /invites\/[^'"]*\/accept/, 'no caller of the retired hub accept route');
|
|
543
|
-
assert.match(body, /Open project/, 'the invite links into the project — signing in there is the acceptance act');
|
|
544
|
-
assert.match(body, /'\/my-projects\/invites\/' \+ encodeURIComponent\([^)]+\) \+ '\/decline'/,
|
|
545
|
-
'decline still works: it only clears the hub\'s own pending row');
|
|
546
544
|
});
|
|
547
545
|
|
|
548
546
|
test('the map card\'s Apply button reads as truly inert once applied, with a live status region (R10 fixups)', async () => {
|
package/tests/render_check.mjs
CHANGED
|
@@ -24,8 +24,9 @@ test('a shared asset reaches every page in its dir whose HTML names it', () => {
|
|
|
24
24
|
// sky-data.js is shared by the two doors of one page (task 1004232): /thinking and /goal-map.
|
|
25
25
|
assert.deepEqual(names(['modules/hall-ui/public/sky-data.js']), ['goal-map.states.json', 'thinking.states.json']);
|
|
26
26
|
assert.deepEqual(names(['modules/hall-ui/public/text-fit.js']), ['goals.states.json']);
|
|
27
|
-
// the account page links the hub's sheet too since task 1004259,
|
|
28
|
-
|
|
27
|
+
// the account page links the hub's sheet too since task 1004259, the guild page since task 1002301,
|
|
28
|
+
// and the invitations page since task 1004276
|
|
29
|
+
assert.deepEqual(names(['modules/public-landing/public/assets/cosmos.css']), ['account.states.json', 'guild.states.json', 'invitations.states.json', 'projects.states.json']);
|
|
29
30
|
});
|
|
30
31
|
|
|
31
32
|
test('the page a builder changed comes before the pages a shared file reaches, so the render cap never drops it', () => {
|
package/tests/ui_design_kit.mjs
CHANGED
|
@@ -34,7 +34,7 @@ test('the platform floors are a pinned list — a floor cannot vanish quietly',
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
test('the shipped states files validate and sit beside the page they name', () => {
|
|
37
|
-
for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/projects-empty.states.json', 'modules/public-landing/public/projects-dns-pending.states.json', 'modules/public-landing/public/projects-hall-live.states.json','modules/public-landing/public/index.states.json', 'modules/public-landing/public/account.states.json', 'modules/public-landing/public/account-private.states.json', 'modules/public-landing/public/guild.states.json', 'modules/hall-ui/public/ranks.states.json']) {
|
|
37
|
+
for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/projects-empty.states.json', 'modules/public-landing/public/projects-dns-pending.states.json', 'modules/public-landing/public/projects-hall-live.states.json','modules/public-landing/public/index.states.json', 'modules/public-landing/public/index-cli.states.json', 'modules/public-landing/public/account.states.json', 'modules/public-landing/public/account-private.states.json', 'modules/public-landing/public/guild.states.json', 'modules/public-landing/public/invitations.states.json', 'modules/public-landing/public/invitations-empty.states.json', 'modules/public-landing/public/invitations-picker.states.json', 'modules/hall-ui/public/ranks.states.json']) {
|
|
38
38
|
const json = readJson(rel);
|
|
39
39
|
const v = lib.validateStatesFile(json);
|
|
40
40
|
assert.deepEqual(v.errors, [], `${rel}: ${v.errors.join('; ')}`);
|
|
@@ -43,14 +43,14 @@ test('the shipped states files validate and sit beside the page they name', () =
|
|
|
43
43
|
}
|
|
44
44
|
// the apex is dark only (task 1003336): its states files name the one mode, and
|
|
45
45
|
// render.js reads it as the default so a plain `--page /` never renders light
|
|
46
|
-
for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/index.states.json', 'modules/public-landing/public/account.states.json', 'modules/public-landing/public/guild.states.json']) {
|
|
46
|
+
for (const rel of ['modules/public-landing/public/projects.states.json', 'modules/public-landing/public/index.states.json', 'modules/public-landing/public/account.states.json', 'modules/public-landing/public/guild.states.json', 'modules/public-landing/public/invitations.states.json']) {
|
|
47
47
|
assert.deepEqual(readJson(rel).modes, ['dark'], `${rel}: the apex renders dark only`);
|
|
48
48
|
assert.equal(readJson(rel).modeQuery, false, `${rel}: no query walks a second mode`);
|
|
49
49
|
}
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
test('the shipped probes files validate', () => {
|
|
53
|
-
for (const rel of ['modules/public-landing/public/projects.probes.json', 'modules/public-landing/public/index.probes.json', 'modules/public-landing/public/account.probes.json', 'modules/public-landing/public/guild.probes.json']) {
|
|
53
|
+
for (const rel of ['modules/public-landing/public/projects.probes.json', 'modules/public-landing/public/index.probes.json', 'modules/public-landing/public/account.probes.json', 'modules/public-landing/public/guild.probes.json', 'modules/public-landing/public/invitations.probes.json']) {
|
|
54
54
|
const v = lib.validateProbesFile(readJson(rel));
|
|
55
55
|
assert.deepEqual(v.errors, [], `${rel}: ${v.errors.join('; ')}`);
|
|
56
56
|
}
|
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
// modules/hall-ui/public/profile-canonical.js — the canonical-profile wiring on
|
|
2
|
-
// the hall's profile page (P08, task 1002804; ADR 0214, ADR 0171 D1,
|
|
3
|
-
// profile-page spec Decision 1).
|
|
4
|
-
//
|
|
5
|
-
// ONE ANSWER, THREE BEHAVIOURS. A builder has exactly one canonical public
|
|
6
|
-
// profile, at <hub>/u/<handle>. What this page does about that depends on which
|
|
7
|
-
// instance is serving it, and the two cases are opposites:
|
|
8
|
-
//
|
|
9
|
-
// * THE HUB serves the same person's canonical page itself, from the same
|
|
10
|
-
// database. Two URLs for one page on one host is the duplication Decision 1
|
|
11
|
-
// exists to end, so the numeric ?id= form REDIRECTS to the handle URL.
|
|
12
|
-
// * A FEDERATED INSTANCE shows its OWN rank, works and achievements here —
|
|
13
|
-
// genuinely different content — so it must not redirect. It stays the local
|
|
14
|
-
// view, declares the hub page canonical, and LINKS OUT to the cross-project
|
|
15
|
-
// record rather than inlining numbers it would have to keep fresh
|
|
16
|
-
// (ADR 0171 D1: an instance-side copy is stale the moment a builder hides).
|
|
17
|
-
// * A VANILLA INSTANCE has no hub. Nothing here does anything.
|
|
18
|
-
//
|
|
19
|
-
// A page cannot both redirect to a URL and declare it canonical, and it does not
|
|
20
|
-
// have to: those are two different instances.
|
|
21
|
-
//
|
|
22
|
-
// IT IS ALL DARK TODAY, ON PURPOSE. `/u/<handle>` does not exist yet — the page
|
|
23
|
-
// is goal 1000048 R14, and the ADR that fixes its PATH (R01) is unwritten. A
|
|
24
|
-
// rel=canonical naming a 404 consolidates nothing and risks the one page that
|
|
25
|
-
// works, so CANONICAL_PROFILE_PAGE_LIVE is false and every branch below is a
|
|
26
|
-
// no-op. ADR 0214 D1 carries the argument.
|
|
27
|
-
//
|
|
28
|
-
// NOTHING HERE IS AUTHORITY (ADR 0016): a canonical URL is an address, never a
|
|
29
|
-
// grant. And nothing here decides who is visible — the server already did that
|
|
30
|
-
// by omitting `handle` for an account that is absent, provisional or private,
|
|
31
|
-
// identically in all three cases (ADR 0214 D5).
|
|
32
|
-
|
|
33
|
-
(() => {
|
|
34
|
-
'use strict';
|
|
35
|
-
|
|
36
|
-
// BOUNDARY-FORCED DUPLICATION (the ADR 0091 §2 pattern, as
|
|
37
|
-
// platform-identity.js already does for the display-name cap): a browser
|
|
38
|
-
// script cannot require modules/platform-identity/canonical-profile.js, so the
|
|
39
|
-
// two constants below MIRROR it. tests/canonical_profile_url.mjs reads both
|
|
40
|
-
// files and fails if they ever disagree, which is what keeps this a mirror
|
|
41
|
-
// rather than a second opinion. Flip the module's copy and this one together.
|
|
42
|
-
const CANONICAL_PROFILE_PAGE_LIVE = false;
|
|
43
|
-
const CANONICAL_PROFILE_PREFIX = '/u/';
|
|
44
|
-
|
|
45
|
-
// Same charset the module re-checks: a handle is about to be interpolated into
|
|
46
|
-
// an href and a <link rel=canonical>, so it is validated here too rather than
|
|
47
|
-
// trusted because it arrived over the wire.
|
|
48
|
-
const HANDLE_RE = /^[a-z0-9-]{1,39}$/;
|
|
49
|
-
|
|
50
|
-
// The hub apex, stamped server-side into a <meta> (never a script global, and
|
|
51
|
-
// never straight into an href — an empty value in an href would silently
|
|
52
|
-
// become a same-host relative URL claiming to be the hub). Absent or empty
|
|
53
|
-
// means "no hub", and every branch below then does nothing.
|
|
54
|
-
function hubOrigin() {
|
|
55
|
-
const meta = document.querySelector('meta[name="hub-origin"]');
|
|
56
|
-
const raw = ((meta && meta.getAttribute('content')) || '').trim().replace(/\/+$/, '');
|
|
57
|
-
if (!raw) return null;
|
|
58
|
-
// Bare https origin only — a canonical must never name a downgraded scheme,
|
|
59
|
-
// and a value carrying a path is not an origin.
|
|
60
|
-
try {
|
|
61
|
-
const url = new URL(raw);
|
|
62
|
-
if (url.protocol !== 'https:' || url.username || url.password) return null;
|
|
63
|
-
if (url.pathname !== '/' || url.search || url.hash) return null;
|
|
64
|
-
return url.origin;
|
|
65
|
-
} catch (err) { return null; }
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// The canonical public profile URL, or null when we must not point anywhere:
|
|
69
|
-
// the page is not live, no hub is known, or the account has no public handle
|
|
70
|
-
// (absent / provisional / private — indistinguishable by design).
|
|
71
|
-
function canonicalProfileUrl(handle) {
|
|
72
|
-
if (!CANONICAL_PROFILE_PAGE_LIVE) return null;
|
|
73
|
-
const origin = hubOrigin();
|
|
74
|
-
if (!origin) return null;
|
|
75
|
-
const h = String(handle == null ? '' : handle).trim().toLowerCase();
|
|
76
|
-
if (!HANDLE_RE.test(h)) return null;
|
|
77
|
-
return `${origin}${CANONICAL_PROFILE_PREFIX}${h}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// "Am I the hub?" — the enabled-module set the server injects. `platform-
|
|
81
|
-
// identity` IS the hub flag rather than a proxy for it (src/platform-server.js
|
|
82
|
-
// says so), so this asks the instance what it is and nothing has to know the
|
|
83
|
-
// platform's hostname.
|
|
84
|
-
function isHub() {
|
|
85
|
-
const mods = window.__MODULES__;
|
|
86
|
-
return Array.isArray(mods) ? mods.includes('platform-identity') : !!(mods && mods['platform-identity']);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function setCanonicalLink(url) {
|
|
90
|
-
let link = document.querySelector('link[rel="canonical"]');
|
|
91
|
-
if (!link) {
|
|
92
|
-
link = document.createElement('link');
|
|
93
|
-
link.setAttribute('rel', 'canonical');
|
|
94
|
-
document.head.appendChild(link);
|
|
95
|
-
}
|
|
96
|
-
link.setAttribute('href', url);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// The visible link-out — ADR 0171 D1's federated render path. It reuses the
|
|
100
|
-
// page's existing cross-project section, which on a federated instance is
|
|
101
|
-
// empty and hidden (the response carries no cross_project key there), so the
|
|
102
|
-
// heading a reader already understands gets the link instead of numbers.
|
|
103
|
-
function renderLinkOut(url) {
|
|
104
|
-
const section = document.getElementById('xp-scroll');
|
|
105
|
-
const body = document.getElementById('xp-body');
|
|
106
|
-
if (!section || !body) return;
|
|
107
|
-
// Defence in depth, and honestly UNOBSERVABLE today: canonicalProfileUrl has
|
|
108
|
-
// already refused anything outside `https://<bare origin>/u/[a-z0-9-]{1,39}`,
|
|
109
|
-
// so no value reaching here can contain a character this changes. It is the
|
|
110
|
-
// second wall for the day someone loosens the handle charset — a mutation
|
|
111
|
-
// test cannot make it bite, and that is the point rather than a gap.
|
|
112
|
-
const escapeHtml = (window.OTB && window.OTB.escapeHtml) || ((s) => String(s));
|
|
113
|
-
body.innerHTML = `<p class="scroll__lede">This builder's work spans more than this project. `
|
|
114
|
-
+ `<a href="${escapeHtml(url)}" rel="noopener">See the cross-project record</a>.</p>`;
|
|
115
|
-
section.hidden = false;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// render(data) — called by profile.js once the profile response is in hand.
|
|
119
|
-
// `data.handle` is present iff the builder has a PUBLICLY VISIBLE platform
|
|
120
|
-
// account; absent for no account, provisional and private alike, so nothing
|
|
121
|
-
// here can tell those apart and neither can a visitor watching it.
|
|
122
|
-
function render(data) {
|
|
123
|
-
const url = canonicalProfileUrl(data && data.handle);
|
|
124
|
-
if (!url) return;
|
|
125
|
-
if (isHub()) {
|
|
126
|
-
// The hub's numeric form is an address for a page the hub also serves at
|
|
127
|
-
// the canonical URL. `replace`, not `assign`: the numeric URL should not
|
|
128
|
-
// sit in history as a place Back returns to.
|
|
129
|
-
if (new URLSearchParams(location.search).get('id')) location.replace(url);
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
setCanonicalLink(url);
|
|
133
|
-
renderLinkOut(url);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
window.OTBCanonical = { render, canonicalProfileUrl, hubOrigin, isHub };
|
|
137
|
-
})();
|