@bongos/core 1.21.82 → 1.21.83

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.
@@ -18,31 +18,96 @@
18
18
  //
19
19
  // Both writers touch ONLY a `pending` row, so a notice can never create, downgrade or
20
20
  // remove a real membership (a member is left as-is; the row is not theirs to change).
21
- // A login with no hub account is a no-op: there is nobody on the hub to notify, and
22
- // the project's own row still admits them when they sign in there.
21
+ //
22
+ // A login with no hub account is PARKED by login (task 1004646, migration 031): a
23
+ // platform account is only born when someone signs in to a project THROUGH the hub,
24
+ // so the usual invitee — new to the platform — has none, and dropping the notice
25
+ // meant they never saw it, even after signing in to the hub. The invitations read
26
+ // lists a parked notice for whoever signs in with that login (listParkedInvites).
23
27
  //
24
28
  // Its own file because platform-identity.js is at the size ratchet; it reuses that
25
29
  // file's account lookup and pending-row writers rather than restating them.
26
30
  'use strict';
27
31
 
32
+ const api = require('../../src/module-api');
28
33
  const pi = require('./platform-identity');
29
34
 
30
- // The project noticed an invite for `githubLogin` → a `pending` hub row, if the login
31
- // has a hub account and no membership in this project yet. Returns { noted } for the
35
+ const loginKey = (githubLogin) => String(githubLogin || '').trim().replace(/^@/, '').toLowerCase();
36
+
37
+ // The project noticed an invite for `githubLogin` → a `pending` hub row if the login
38
+ // has a hub account, else a parked notice under the login. Returns { noted } for the
32
39
  // route's own logging; the route never tells the caller which case happened (see it).
33
- async function noteProjectInvite({ clientId, githubLogin }, { pool } = {}) {
40
+ async function noteProjectInvite({ clientId, githubLogin }, { pool = api.pool } = {}) {
34
41
  const account = await pi.getAccountByLogin(githubLogin, { pool });
35
- if (!account) return { noted: false };
42
+ if (!account) {
43
+ const login = loginKey(githubLogin);
44
+ if (!login) return { noted: false };
45
+ await pool.query(
46
+ `INSERT INTO platform_identity_parked_invite_notices (client_id, login_lower)
47
+ VALUES ($1, $2)
48
+ ON CONFLICT (client_id, login_lower) DO UPDATE SET noted_at = now()`,
49
+ [clientId, login],
50
+ );
51
+ return { noted: true, parked: true };
52
+ }
36
53
  const out = await pi.inviteToProject({ clientId, githubId: account.github_id }, { pool });
37
54
  return { noted: !!out.invited };
38
55
  }
39
56
 
40
- // The project rescinded the invite for `githubLogin` → drop the `pending` hub row.
57
+ // The project rescinded the invite for `githubLogin` → drop the `pending` hub row and
58
+ // any parked notice under the login (an account made since the notice keeps both
59
+ // reachable, so both go).
41
60
  async function clearProjectInvite({ clientId, githubLogin }, { pool } = {}) {
61
+ const parked = await removeParkedInvite({ clientId, githubLogin }, { pool });
42
62
  const account = await pi.getAccountByLogin(githubLogin, { pool });
43
- if (!account) return { cleared: false };
63
+ if (!account) return { cleared: parked.removed };
44
64
  const out = await pi.removePendingInvite({ clientId, githubId: account.github_id }, { pool });
45
- return { cleared: out.removed };
65
+ return { cleared: out.removed || parked.removed };
66
+ }
67
+
68
+ // The parked notices for one signed-in login, in the shape listPendingInvitesForAccount
69
+ // returns, minus any past the invite's 30 days (ADR 0356) and any project the reader
70
+ // already holds a real membership in — signing in there accepted it.
71
+ async function listParkedInvites({ githubLogin, githubId }, { pool = api.pool } = {}) {
72
+ const login = loginKey(githubLogin);
73
+ if (!login) return [];
74
+ const { rows } = await pool.query(
75
+ `SELECT n.client_id, n.noted_at AS invited_at,
76
+ c.name AS project_name, c.origin AS project_origin
77
+ FROM platform_identity_parked_invite_notices n
78
+ LEFT JOIN platform_identity_sso_clients c ON c.client_id = n.client_id
79
+ WHERE n.login_lower = $1
80
+ AND n.noted_at > now() - make_interval(days => $2)
81
+ AND NOT EXISTS (
82
+ SELECT 1 FROM platform_identity_project_memberships m
83
+ WHERE m.client_id = n.client_id AND m.github_id = $3 AND m.membership_kind <> 'pending')
84
+ ORDER BY n.noted_at DESC`,
85
+ [login, api.inviteExpiry.INVITE_EXPIRY_DAYS, String(githubId || '')],
86
+ );
87
+ return rows;
88
+ }
89
+
90
+ // The invitee declined, or the project rescinded: drop the parked notice for the login.
91
+ async function removeParkedInvite({ clientId, githubLogin }, { pool = api.pool } = {}) {
92
+ const login = loginKey(githubLogin);
93
+ if (!login) return { removed: false };
94
+ const { rowCount } = await pool.query(
95
+ `DELETE FROM platform_identity_parked_invite_notices WHERE client_id = $1 AND login_lower = $2`,
96
+ [clientId, login],
97
+ );
98
+ return { removed: rowCount > 0 };
99
+ }
100
+
101
+ // Everything waiting on one signed-in builder: their pending rows plus the notices
102
+ // parked under their login, one per project (a pending row wins — it is the same
103
+ // invite, written once the account existed).
104
+ async function listInvitesForBuilder({ githubId, githubLogin }, { pool = api.pool } = {}) {
105
+ const [pending, parked] = await Promise.all([
106
+ pi.listPendingInvitesForAccount(githubId, { pool }),
107
+ listParkedInvites({ githubLogin, githubId }, { pool }),
108
+ ]);
109
+ const seen = new Set(pending.map((r) => r.client_id));
110
+ return pending.concat(parked.filter((r) => !seen.has(r.client_id)));
46
111
  }
47
112
 
48
113
  // The project's own invite box for `githubLogin`: its hall's watch page, with the login
@@ -58,4 +123,7 @@ function hallInviteUrl(origin, githubLogin) {
58
123
  return `${u.origin}/builders/watch${login ? `?invite=${encodeURIComponent(login)}` : ''}`;
59
124
  }
60
125
 
61
- module.exports = { noteProjectInvite, clearProjectInvite, hallInviteUrl };
126
+ module.exports = {
127
+ noteProjectInvite, clearProjectInvite, hallInviteUrl,
128
+ removeParkedInvite, listInvitesForBuilder,
129
+ };
@@ -0,0 +1,45 @@
1
+ -- platform_identity_031_parked_invite_notices.sql — a project's invite notice for a
2
+ -- GitHub login the hub has no account for yet, kept until that person signs in
3
+ -- (task 1004646; ADR 0353 amended).
4
+ --
5
+ -- WHY. POST /sso/invites/notify resolves the invited login to a platform account and,
6
+ -- before this table, dropped the notice when there was none. A platform account is
7
+ -- only created when someone signs in to a PROJECT through the hub — signing in to the
8
+ -- hub itself creates none — so the usual invitee (new to the platform) never saw the
9
+ -- invite on /invitations, even after signing in. Now the notice is PARKED here by
10
+ -- login, and the invitations read lists it for whoever signs in with that login.
11
+ --
12
+ -- THE SHAPE CARRIES THE RULES:
13
+ -- * THE KEY IS (client_id, login_lower), so a re-invite cannot pile up rows: the
14
+ -- writer's upsert restarts noted_at in place.
15
+ -- * NO STATUS COLUMN (ADR 0201 D1.3, ADR 0353). A notice admits nobody: the
16
+ -- project's own access_requests row does, and signing in there accepts it. A
17
+ -- parked notice is hidden at read time once the reader holds a non-pending
18
+ -- membership in that project.
19
+ -- * THE 30-DAY WINDOW IS READ-TIME (ADR 0356, the doorway's INVITE_EXPIRY_DAYS),
20
+ -- exactly as for a pending membership row. Nothing sweeps the rows.
21
+ -- * KEYED BY LOGIN, like the project's own invited access_requests row, which is
22
+ -- what admits the same person — so the notice can claim no more than the invite.
23
+ --
24
+ -- NOTHING HERE IS AUTHORITY (ADR 0016 / ADR 0141).
25
+ --
26
+ -- Additive + platform_identity_-namespaced (ADR 0083 §5). CREATE only, so the
27
+ -- previous release still runs against this schema. Idempotent.
28
+
29
+ BEGIN;
30
+
31
+ CREATE TABLE IF NOT EXISTS platform_identity_parked_invite_notices (
32
+ client_id text NOT NULL,
33
+ login_lower text NOT NULL,
34
+ noted_at timestamptz NOT NULL DEFAULT now(),
35
+ PRIMARY KEY (client_id, login_lower)
36
+ );
37
+
38
+ -- The reader's side: every notice for one login.
39
+ CREATE INDEX IF NOT EXISTS platform_identity_parked_invite_notices_login
40
+ ON platform_identity_parked_invite_notices (login_lower);
41
+
42
+ COMMENT ON TABLE platform_identity_parked_invite_notices IS
43
+ 'A project''s invite notice for a GitHub login with no hub account yet (task 1004646). Listed on /invitations for whoever signs in with that login; a notice only, never a way in (ADR 0353). The 30-day window is applied when read (ADR 0356).';
44
+
45
+ COMMIT;
@@ -29,6 +29,7 @@ const projectStats = require('../project-stats');
29
29
  const applicationEcho = require('../application-echo');
30
30
  const guilds = require('../guilds');
31
31
  const guildEngagements = require('../guild-engagements');
32
+ const inviteNotices = require('../invite-notices');
32
33
  const hubKeys = require('../hub-keys');
33
34
  const { isAccountActive } = require('../account-visibility');
34
35
  const { projectPublishability } = require('../publish-gate');
@@ -302,10 +303,12 @@ module.exports = function myProjectsRoutes() {
302
303
  }
303
304
  });
304
305
 
305
- // rank: any-builder — the caller's OWN pending invitations (ADR 0141 §5).
306
+ // rank: any-builder — the caller's OWN pending invitations (ADR 0141 §5), plus the
307
+ // notices parked under their login before they had a hub account (task 1004646).
306
308
  router.get('/my-projects/invites', requireBuilder, async (req, res) => {
307
309
  try {
308
- const invites = await pi.listPendingInvitesForAccount(req.builder.github_id, { pool });
310
+ const invites = await inviteNotices.listInvitesForBuilder(
311
+ { githubId: req.builder.github_id, githubLogin: req.builder.github_login }, { pool });
309
312
  res.json({ invites: invites.map((i) => ({ client_id: i.client_id, name: i.project_name || i.project_origin || i.client_id, origin: i.project_origin || null, invited_at: i.invited_at })) });
310
313
  } catch (err) {
311
314
  console.error('[platform-identity] GET /my-projects/invites', err);
@@ -324,7 +327,8 @@ module.exports = function myProjectsRoutes() {
324
327
  if (!clientId) return res.fail('bad_client_id', 400);
325
328
  try {
326
329
  const out = await pi.removePendingInvite({ clientId, githubId: req.builder.github_id }, { pool });
327
- if (!out.removed) return res.fail('no_pending_invite', 404);
330
+ const parked = await inviteNotices.removeParkedInvite({ clientId, githubLogin: req.builder.github_login }, { pool });
331
+ if (!out.removed && !parked.removed) return res.fail('no_pending_invite', 404);
328
332
  res.json({ ok: true, accepted: false });
329
333
  } catch (err) {
330
334
  console.error('[platform-identity] decline invite', err);
@@ -624,6 +624,7 @@
624
624
  });
625
625
  })();
626
626
  </script>
627
+ <script src="/assets/invite-badge.js"></script>
627
628
  <!-- the Artist review button: signed-in builders only (task 1004499) -->
628
629
  <script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
629
630
  </body>
@@ -628,6 +628,14 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
628
628
  .signinDoor:hover{background:var(--ink);color:var(--bg);}
629
629
  /* a long GitHub login truncates instead of pushing Sign out off the screen */
630
630
  .who>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:14ch;}
631
+ /* what is waiting on you (task 1004646, assets/invite-badge.js): a count on the chip and on
632
+ the band's Invitations item — the accent fill with --accent-on ink, the .btn pairing */
633
+ .invBadge{display:inline-flex;align-items:center;justify-content:center;min-width:20px;height:20px;padding:0 6px;
634
+ border-radius:var(--radius-pill);background:var(--accent);color:var(--accent-on);
635
+ font-family:var(--font-body);font-size:12px;font-weight:700;line-height:1;letter-spacing:0;vertical-align:middle;}
636
+ .who>.invBadge{max-width:none;overflow:visible;}
637
+ .bandT .invBadge{margin-left:10px;}
638
+ .invWaiting{margin:6px 0 4px;font-size:15.5px;font-weight:600;}
631
639
  /* keyboard focus on the two search pills is VISIBLE — the input's own outline
632
640
  is off by design, so the pill carries the ring */
633
641
  .search:focus-within{box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 40%,transparent);}
@@ -0,0 +1,91 @@
1
+ /* assets/invite-badge.js — WHAT IS WAITING ON YOU, said where you already look
2
+ (task 1004646; the owner's report of 2026-10-08 that invitations "aren't obvious").
3
+
4
+ /invitations lists what asks you in, but the only way there was the profile band,
5
+ with nothing saying anything was waiting. This counts what is waiting — a project's
6
+ invite (GET /my-projects/invites) and a guild's invite or a join request to a guild
7
+ you own (GET /me/guild-requests) — and, when it is more than none, paints:
8
+ - a count on the identity chip (#who) and on the profile band's Invitations item;
9
+ - on /projects?view=mine, a line "You have N invitations waiting →" under the
10
+ view's heading.
11
+ A guild's engagement with a project is left out: it asks nothing of you.
12
+
13
+ Hub only, with no flag of its own: the Invitations item sits inside the apex-only
14
+ region, which a child instance strips, so there this file finds no item and reads
15
+ nothing. Signed out, both reads 401 and nothing is painted. A failed read counts
16
+ as none — a badge is a convenience, never the record.
17
+
18
+ Built from nodes only. window.InviteBadge.refresh() re-reads, for a page
19
+ that has just answered one (the invitations page's Decline and Accept). */
20
+ (function () {
21
+ 'use strict';
22
+ var API = '/api/bongos';
23
+ function $(id) { return document.getElementById(id); }
24
+ function item() { var m = $('menuProfile'); return m && m.querySelector('a[href="/invitations"]'); }
25
+
26
+ function readCount(path, pick) {
27
+ return fetch(API + path, { credentials: 'same-origin', headers: { Accept: 'application/json' } })
28
+ .then(function (res) { return res.ok ? res.json() : null; })
29
+ .then(function (d) { return d ? pick(d) : 0; })
30
+ .catch(function () { return 0; });
31
+ }
32
+ function len(x) { return Array.isArray(x) ? x.length : 0; }
33
+
34
+ function badge(host, n, label) {
35
+ if (!host) return;
36
+ var b = host.querySelector('.invBadge');
37
+ if (!n) { if (b) b.remove(); return; }
38
+ if (!b) {
39
+ b = document.createElement('b');
40
+ b.className = 'invBadge';
41
+ var shown = document.createElement('span');
42
+ shown.setAttribute('aria-hidden', 'true');
43
+ var said = document.createElement('span');
44
+ said.className = 'vh';
45
+ b.appendChild(shown);
46
+ b.appendChild(said);
47
+ // before the chip's chevron; at the end of a band item
48
+ host.insertBefore(b, host.querySelector('svg.cv'));
49
+ }
50
+ b.firstChild.textContent = n > 99 ? '99+' : String(n);
51
+ b.lastChild.textContent = ', ' + label;
52
+ }
53
+
54
+ // at the top of the mine view, under its heading and lede — the list of projects
55
+ // below it can be long, and the Invitations pointer sits at its foot
56
+ function mineLine(n, label) {
57
+ var view = $('view-mine');
58
+ var hero = view && view.querySelector('.hero');
59
+ if (!hero) return;
60
+ var p = $('mineInvitesWaiting');
61
+ if (!n) { if (p) p.remove(); return; }
62
+ if (!p) {
63
+ p = document.createElement('p');
64
+ p.id = 'mineInvitesWaiting';
65
+ p.className = 'invWaiting';
66
+ var a = document.createElement('a');
67
+ a.href = '/invitations';
68
+ p.appendChild(a);
69
+ hero.appendChild(p);
70
+ }
71
+ p.firstChild.textContent = 'You have ' + label + ' waiting →';
72
+ }
73
+
74
+ function paint(n) {
75
+ var label = n + (n === 1 ? ' invitation' : ' invitations');
76
+ badge($('who'), n, label + ' waiting');
77
+ badge(item(), n, label + ' waiting');
78
+ mineLine(n, label);
79
+ }
80
+
81
+ function refresh() {
82
+ if (!item()) return Promise.resolve(0);
83
+ return Promise.all([
84
+ readCount('/my-projects/invites', function (d) { return len(d.invites); }),
85
+ readCount('/me/guild-requests', function (d) { return len(d.incoming) + len(d.owner_incoming); }),
86
+ ]).then(function (c) { var n = c[0] + c[1]; paint(n); return n; });
87
+ }
88
+
89
+ window.InviteBadge = { refresh: refresh, paint: paint };
90
+ refresh();
91
+ })();
@@ -120,6 +120,7 @@
120
120
 
121
121
  <footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud&nbsp;Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms">Terms</a><a href="/contact" aria-current="page">Contact</a><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a></nav></footer>
122
122
  <script src="/assets/hub-bar.js"></script>
123
+ <script src="/assets/invite-badge.js"></script>
123
124
  <!-- the Artist review button: signed-in builders only (task 1004499) -->
124
125
  <script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
125
126
  </body>
@@ -629,6 +629,7 @@
629
629
  load();
630
630
  })();
631
631
  </script>
632
+ <script src="/assets/invite-badge.js"></script>
632
633
  <!-- the Artist review button: signed-in builders only (task 1004499) -->
633
634
  <script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
634
635
  </body>
@@ -232,11 +232,13 @@
232
232
  button on failure. `onRemoved` carries whatever else the caller needs after
233
233
  the row is gone (the empty-line flip). The busy/idle label swap the /projects
234
234
  copy carries is for its Leave button, which never moved here. */
235
+ /* the bar's waiting count (assets/invite-badge.js, task 1004646) follows an answer here */
236
+ function recount() { if (typeof window !== 'undefined' && window.InviteBadge) window.InviteBadge.refresh(); }
235
237
  function postThenRemoveRow(url, row, btn, onRemoved) {
236
238
  btn.disabled = true;
237
239
  fetch(url, { method: 'POST', credentials: 'same-origin' })
238
240
  .then(function (r) { return r.ok ? r.json() : Promise.reject(); })
239
- .then(function () { row.remove(); if (onRemoved) onRemoved(); })
241
+ .then(function () { row.remove(); if (onRemoved) onRemoved(); recount(); })
240
242
  .catch(function () { btn.disabled = false; });
241
243
  }
242
244
 
@@ -481,7 +483,7 @@
481
483
  msg.className = 'projMsg';
482
484
  msg.textContent = '';
483
485
  guildPost('/guild-requests/' + encodeURIComponent(id) + '/decline').then(function (res) {
484
- if (res.ok) { row.remove(); paintInvitesSection(); return; }
486
+ if (res.ok) { row.remove(); paintInvitesSection(); recount(); return; }
485
487
  btns.forEach(function (b) { b.disabled = false; });
486
488
  if (res.status === 401) { sessionExpired(msg); return; }
487
489
  msg.className = 'projMsg err';
@@ -582,6 +584,7 @@
582
584
  radios.forEach(function (r) { r.disabled = true; });
583
585
  page.hidden = false;
584
586
  msg.className = 'projMsg ok';
587
+ recount();
585
588
  msg.textContent = 'You’ve joined ' + name + (on ? ', and your numbers count toward its totals.' : ', and your numbers are left out of its totals.');
586
589
  return;
587
590
  }
@@ -660,6 +663,7 @@
660
663
  hint.hidden = true;
661
664
  msg.className = 'projMsg ok';
662
665
  msg.textContent = who + ' is in ' + (r.guild.name || r.guild.slug) + ' now.';
666
+ recount();
663
667
  return;
664
668
  }
665
669
  accept.textContent = 'Accept';
@@ -961,6 +965,7 @@
961
965
  });
962
966
  })();
963
967
  </script>
968
+ <script src="/assets/invite-badge.js"></script>
964
969
  <!-- the Artist review button: signed-in builders only (task 1004499) -->
965
970
  <script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
966
971
  </body>
@@ -129,6 +129,7 @@
129
129
 
130
130
  <footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud&nbsp;Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy" aria-current="page">Privacy</a><a href="/terms">Terms</a><a href="/contact">Contact</a><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a></nav></footer>
131
131
  <script src="/assets/hub-bar.js"></script>
132
+ <script src="/assets/invite-badge.js"></script>
132
133
  <!-- the Artist review button: signed-in builders only (task 1004499) -->
133
134
  <script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
134
135
  </body>
@@ -11142,6 +11142,8 @@ summary{min-height:24px;padding:3px 0;}
11142
11142
  .then(function (data) {
11143
11143
  me = data.builder || data.me || data;
11144
11144
  renderWho();
11145
+ /* renderWho rebuilds the chip, so the waiting count is painted again on it (task 1004646) */
11146
+ if (window.InviteBadge) window.InviteBadge.refresh();
11145
11147
  paintShellIdentity();
11146
11148
  wizard.onAuth();
11147
11149
  $('gate').hidden = true;
@@ -11174,6 +11176,7 @@ summary{min-height:24px;padding:3px 0;}
11174
11176
  .then(loadSelfShipped, loadSelfShipped);
11175
11177
  })();
11176
11178
  </script>
11179
+ <script src="/assets/invite-badge.js"></script>
11177
11180
  <!-- the Artist review button: signed-in builders only (task 1004499) -->
11178
11181
  <script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
11179
11182
  </body>
@@ -134,6 +134,7 @@
134
134
 
135
135
  <footer class="foot"><!--apex-only:start--><span>A non-profit, capture-resistant commons. </span><!--apex-only:end-->Built by agents, on Cloud&nbsp;Bongos. <a href="https://www.npmjs.com/package/@bongos/core" target="_blank" rel="noopener">Open source</a> (<a href="https://www.gnu.org/licenses/agpl-3.0.html" target="_blank" rel="noopener">AGPL-3.0</a>).<nav class="footNav" aria-label="Site"><a href="/privacy">Privacy</a><a href="/terms" aria-current="page">Terms</a><a href="/contact">Contact</a><a data-link-hall href="{{buildersOrigin}}">Builders' hall</a></nav></footer>
136
136
  <script src="/assets/hub-bar.js"></script>
137
+ <script src="/assets/invite-badge.js"></script>
137
138
  <!-- the Artist review button: signed-in builders only (task 1004499) -->
138
139
  <script src="/assets/artist-review.js" data-hall="{{buildersOrigin}}"></script>
139
140
  </body>
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.82",
3
+ "version": "1.21.83",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.82",
9
+ "version": "1.21.83",
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.82",
3
+ "version": "1.21.83",
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",
@@ -9318,5 +9318,11 @@
9318
9318
  "id": "1004481",
9319
9319
  "text": "Custom ranks can now be deleted from the Government page, so trying a recommended rank set is no longer a permanent choice. A rank someone still holds can't be deleted, and the page says who holds it. The four built-in ranks c"
9320
9320
  }
9321
+ ],
9322
+ "1.21.83": [
9323
+ {
9324
+ "id": "1004646",
9325
+ "text": "Project invites now reach people on cloudbongos.com. Before, an invite sent by GitHub username was silently lost if the person had never signed in to a project through cloudbongos.com, which is almost every new invitee, so the"
9326
+ }
9321
9327
  ]
9322
9328
  }
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.82'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.83'; // 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');