@bongos/core 1.24.2 → 1.24.4
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 +111 -51
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +6 -0
- package/clients/bongos-client/index.cjs +6 -0
- package/clients/bongos-client/index.d.ts +8 -0
- package/clients/bongos-client/index.mjs +6 -0
- package/config/branding.neutral.json +0 -1
- package/docs/adr/0396-google-sign-in-a-stand-in-login-and-a-browser-approved-cli-sign-in.md +11 -0
- package/docs/api/openapi.json +121 -4
- package/docs/api-reference.md +8 -1
- package/docs/branding-contract.md +1 -1
- package/docs/module-api-changelog.md +2 -0
- package/docs/page-readings.json +7 -7
- package/modules/discord/ship-broadcast.js +7 -0
- package/modules/economy/credit-wording.js +93 -0
- package/modules/economy/credits.js +26 -12
- package/modules/economy/governor-docket.js +12 -6
- package/modules/economy/reward-policy.js +49 -3
- package/modules/economy/reward.js +5 -0
- package/modules/government/routes/government.js +7 -0
- package/modules/hall-ui/public/street-inside.js +1 -1
- package/modules/ideas/routes/inbox.js +6 -0
- package/modules/lifecycle/routes/project-settings.js +6 -4
- package/modules/market-watch/ask.js +425 -0
- package/modules/market-watch/migrations/market_watch_003_ask_pool.sql +21 -0
- package/modules/market-watch/module.json +10 -3
- package/modules/market-watch/pollers/weekly-ask.js +89 -0
- package/modules/market-watch/public/hall-widget.js +271 -0
- package/modules/market-watch/public/weekly-ask.css +39 -0
- package/modules/market-watch/routes/ask.js +95 -0
- package/modules/onboarding/routes/access-requests.js +38 -10
- package/modules/platform-identity/account-logins.js +13 -0
- package/modules/platform-identity/routes/my-projects.js +10 -7
- package/modules/platform-identity/routes/sso.js +11 -6
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +22 -0
- package/scripts/bongos/backfill-subagent-rewards.js +7 -7
- package/scripts/bongos/run-unit-tests.js +4 -0
- package/scripts/hall-preview/server.js +13 -0
- package/src/bongos/auth-idp.js +17 -5
- package/src/bongos/db-kernel.js +14 -1
- package/src/bongos/module-scope-map.js +1 -0
- package/src/module-api.js +1 -1
- package/tests/account_logins.mjs +6 -4
- package/tests/backfill_subagent_rewards.mjs +7 -7
- package/tests/credit_grant.mjs +7 -7
- package/tests/credit_wording.mjs +154 -0
- package/tests/google_halls.mjs +211 -0
- package/tests/governor_docket.mjs +7 -3
- package/tests/hall_street_inside.mjs +2 -2
- package/tests/market_watch_ask.mjs +369 -0
- package/tests/market_watch_ask_db.mjs +122 -0
- package/tests/market_watch_data_db.mjs +4 -0
- package/tests/market_watch_module.mjs +9 -5
- package/tests/project_settings_page.mjs +1 -1
- package/tests/reward_cost_scale.mjs +200 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// modules/market-watch/public/hall-widget.js — the weekly ask card in the hall (task 1004940).
|
|
2
|
+
//
|
|
3
|
+
// WHERE IT SHOWS. An action awaiting a member goes in the hall's "Needs your attention" card
|
|
4
|
+
// (goal-inbox.js), never in a new Home card (hall-ui/CLAUDE.md, Placement). So this file adds
|
|
5
|
+
// one SLICE to that card (#attention-market-watch), drawn as the hall's ask card (hall-ask.css
|
|
6
|
+
// `.hask` on an inbox row, the same shape the Blockers slice uses), and calls
|
|
7
|
+
// OTBAttention.refresh() so the card's count and visibility include it. The server injects
|
|
8
|
+
// this file only while Market Watch is enabled (src/module-loader/loader.js
|
|
9
|
+
// hallWidgetScripts); module off, there is no slice.
|
|
10
|
+
//
|
|
11
|
+
// No card this week, a closed week or an answered one: GET /market-watch/ask says
|
|
12
|
+
// { card: null } and the slice stays empty. It never reminds anyone.
|
|
13
|
+
//
|
|
14
|
+
// Design of record: docs/design/mocks/market-watch/weekly-ask.html (option A, quick-tap
|
|
15
|
+
// chips). Every string from the server is escaped here (window.OTB.escapeHtml) — chip
|
|
16
|
+
// labels come from tracked items and the team rows carry members' own words.
|
|
17
|
+
//
|
|
18
|
+
// Under Node this file is require()d for its pure parts (cardHtml, answerBody), the
|
|
19
|
+
// hall-ask.js precedent; nothing touches the DOM until the browser half runs.
|
|
20
|
+
(function () {
|
|
21
|
+
'use strict';
|
|
22
|
+
const isNode = typeof window === 'undefined';
|
|
23
|
+
const esc = (s) => (isNode ? require('../../../src/module-api').escapeHtml : window.OTB.escapeHtml)(s);
|
|
24
|
+
const API = '/api/bongos/market-watch';
|
|
25
|
+
const SLICE_ID = 'attention-market-watch';
|
|
26
|
+
const MAX_TEXT = 80;
|
|
27
|
+
const MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'];
|
|
28
|
+
const DAYS = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
|
|
29
|
+
|
|
30
|
+
// "monday 3 nov", the mock's way of naming the next ask's day.
|
|
31
|
+
function dayName(iso) {
|
|
32
|
+
const d = new Date(`${String(iso).slice(0, 10)}T00:00:00Z`);
|
|
33
|
+
if (Number.isNaN(d.getTime())) return '';
|
|
34
|
+
return `${DAYS[d.getUTCDay()]} ${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const ADD = {
|
|
38
|
+
rival: { chip: '+ name one', placeholder: 'e.g. Kilo Code', aria: 'name a competitor' },
|
|
39
|
+
watch: { chip: '+ a topic', placeholder: 'e.g. EU AI Act rules', aria: 'name a topic' },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function streakHtml(n) {
|
|
43
|
+
if (!(n > 0)) return '';
|
|
44
|
+
const bars = [0, 1, 2, 3, 4].map((i) => `<i${i < Math.min(n, 5) ? '' : ' class="off"'}></i>`).join('');
|
|
45
|
+
return `<span class="mwask__streak" title="${esc(`${n} week${n === 1 ? '' : 's'} in a row`)}">${bars} ${esc(`${n}-week streak`)}</span>`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function chipHtml(q, o) {
|
|
49
|
+
return `<button class="mwask__chip" type="button" aria-pressed="false" data-mwask-q="${esc(q)}" data-mwask-choice="${esc(o.label)}">`
|
|
50
|
+
+ `${esc(o.label)}${o.example ? ` <small>${esc(o.example)}</small>` : ''}</button>`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function questionHtml(q, i) {
|
|
54
|
+
const add = ADD[q.key] || ADD.watch;
|
|
55
|
+
const head = i === 0
|
|
56
|
+
? `<h4 class="hask__q">${esc(q.prompt)}</h4><p class="hask__said">Tap any that fit. Each tap becomes a proposal the owner sees.</p>`
|
|
57
|
+
: `<p class="mwask__sub">${esc(`and ${q.prompt}`)}</p>`;
|
|
58
|
+
return head
|
|
59
|
+
+ `<div class="mwask__chips" data-mwask-group="${esc(q.key)}">${(q.options || []).map((o) => chipHtml(q.key, o)).join('')}`
|
|
60
|
+
+ `<button class="mwask__chip mwask__chip--add" type="button" data-mwask-add="${esc(q.key)}">${esc(add.chip)}</button></div>`
|
|
61
|
+
+ `<div class="mwask__add" data-mwask-addrow="${esc(q.key)}" hidden><input type="text" maxlength="${MAX_TEXT}" placeholder="${esc(add.placeholder)}" aria-label="${esc(add.aria)}">`
|
|
62
|
+
+ `<button class="btn-ghost" type="button" data-mwask-ok="${esc(q.key)}">add</button></div>`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// A decided proposal's pill, in words, never the status column itself.
|
|
66
|
+
const DECIDED = { approved: 'approved', applied: 'now watched' };
|
|
67
|
+
|
|
68
|
+
function rowHtml(r) {
|
|
69
|
+
const decided = r.status !== 'open';
|
|
70
|
+
const dot = decided ? ' mwask__dot--done' : (r.verb === 'Watch' ? ' mwask__dot--watch' : '');
|
|
71
|
+
const what = `${esc(r.verb)} <b>${esc(r.name)}</b>${r.tail ? ` ${esc(r.tail)}` : ''}`;
|
|
72
|
+
const pill = decided
|
|
73
|
+
? `<span class="mwask__pill">${esc(DECIDED[r.status] || 'decided')}</span>`
|
|
74
|
+
: `<span class="mwask__pill${r.agree >= 2 ? ' mwask__pill--hot' : ''}">${esc(`${r.agree} agree`)}</span>`;
|
|
75
|
+
return `<li class="mwask__row"><span class="mwask__dot${dot}" aria-hidden="true"></span><span class="mwask__what">${what}</span>`
|
|
76
|
+
+ (r.who ? `<span class="mwask__who">${esc(r.who)}</span>` : '') + `${pill}</li>`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// cardHtml(card, human) → the slice's markup. `human` is the HUMAN DECISION badge markup
|
|
80
|
+
// (OTBAsk.human), already safe; '' draws the plain dot.
|
|
81
|
+
function cardHtml(card, human = '') {
|
|
82
|
+
if (!card || !Array.isArray(card.questions) || !card.questions.length) return '';
|
|
83
|
+
const where = `market watch · week ${Number(card.week_no) || ''} · about 30 seconds`;
|
|
84
|
+
return '<h3 class="ginbox__sec-h">This week\'s market ask <span class="ginbox__sub">— about 30 seconds</span></h3>'
|
|
85
|
+
+ '<ul class="ginbox">'
|
|
86
|
+
+ `<li class="hask ginbox__item mwask" data-mwask data-mwask-week="${esc(card.week_of)}" data-mwask-streak="${Number(card.streak) || 0}" data-mwask-next="${esc(dayName(card.next_on))}">`
|
|
87
|
+
+ `<div class="hask__where">${human || '<span class="hask__dot" aria-hidden="true"></span>'}<span class="hask__where-t">${esc(where)}</span><span class="mwask__sp"></span>${streakHtml(Number(card.streak) || 0)}</div>`
|
|
88
|
+
+ '<div class="mwask__body">'
|
|
89
|
+
+ card.questions.map(questionHtml).join('')
|
|
90
|
+
+ '<p class="mwask__err" role="alert"></p>'
|
|
91
|
+
+ '<div class="hask__opts hask__opts--row">'
|
|
92
|
+
+ '<button class="hask__opt hask__opt--rec" type="button" data-mwask-send><span class="hask__label">Pick at least one</span><span class="hask__result">or skip below</span></button>'
|
|
93
|
+
+ '<button class="hask__opt" type="button" data-mwask-nothing><span class="hask__label">Nothing new this week</span><span class="hask__result">keeps your streak</span></button>'
|
|
94
|
+
+ '</div></div>'
|
|
95
|
+
+ '<div class="mwask__done" hidden><h4 class="hask__q" data-mwask-donet></h4><p class="hask__said" data-mwask-dones></p></div>'
|
|
96
|
+
+ '</li></ul>'
|
|
97
|
+
+ (card.team && card.team.length
|
|
98
|
+
? `<div class="mwask__lbl">this week's proposals from the team</div><ul class="mwask__rows">${card.team.map(rowHtml).join('')}</ul>`
|
|
99
|
+
: '');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// answerBody(weekOf, pressed) → the POST body. `pressed` is [{ question, choice } | { question, text }]:
|
|
103
|
+
// a chip carries its label as `choice`, an entry its words as `text`. No builder id: the
|
|
104
|
+
// server records the caller and refuses a body that names anyone else.
|
|
105
|
+
function answerBody(weekOf, pressed, nothing = false) {
|
|
106
|
+
if (nothing) return { week_of: weekOf, nothing: true };
|
|
107
|
+
const picks = [];
|
|
108
|
+
const names = [];
|
|
109
|
+
for (const p of pressed || []) {
|
|
110
|
+
if (typeof p.text === 'string') names.push({ question: p.question, text: p.text.slice(0, MAX_TEXT) });
|
|
111
|
+
else picks.push({ question: p.question, choice: p.choice });
|
|
112
|
+
}
|
|
113
|
+
return { week_of: weekOf, picks, names };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function sendLabel(n) {
|
|
117
|
+
return n
|
|
118
|
+
? { label: `Send ${n} proposal${n > 1 ? 's' : ''}`, result: 'the owner sees each one' }
|
|
119
|
+
: { label: 'Pick at least one', result: 'or skip below' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isNode) {
|
|
123
|
+
module.exports = { cardHtml, answerBody, sendLabel, dayName };
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ---- the browser half -------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
if (!window.OTB) return;
|
|
130
|
+
const client = window.BongosClient && window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
131
|
+
let box = null;
|
|
132
|
+
|
|
133
|
+
function css() {
|
|
134
|
+
if (document.querySelector('link[data-mwask-css]')) return;
|
|
135
|
+
const l = document.createElement('link');
|
|
136
|
+
l.rel = 'stylesheet';
|
|
137
|
+
l.href = '/modules/market-watch/public/weekly-ask.css';
|
|
138
|
+
l.setAttribute('data-mwask-css', '1');
|
|
139
|
+
document.head.appendChild(l);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const refresh = () => { if (window.OTBAttention) window.OTBAttention.refresh(); };
|
|
143
|
+
|
|
144
|
+
function pressedIn(li) {
|
|
145
|
+
return [...li.querySelectorAll('.mwask__chip[aria-pressed="true"]')].map((c) => (c.hasAttribute('data-mwask-free')
|
|
146
|
+
? { question: c.getAttribute('data-mwask-q'), text: c.getAttribute('data-mwask-free') }
|
|
147
|
+
: { question: c.getAttribute('data-mwask-q'), choice: c.getAttribute('data-mwask-choice') }));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function recount(li) {
|
|
151
|
+
const s = sendLabel(pressedIn(li).length);
|
|
152
|
+
const btn = li.querySelector('[data-mwask-send]');
|
|
153
|
+
btn.querySelector('.hask__label').textContent = s.label;
|
|
154
|
+
btn.querySelector('.hask__result').textContent = s.result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function done(li, title) {
|
|
158
|
+
const streak = (Number(li.getAttribute('data-mwask-streak')) || 0) + 1;
|
|
159
|
+
const next = li.getAttribute('data-mwask-next');
|
|
160
|
+
li.querySelector('.mwask__body').hidden = true;
|
|
161
|
+
li.querySelector('.mwask__body').style.display = 'none';
|
|
162
|
+
li.querySelector('[data-mwask-donet]').textContent = title;
|
|
163
|
+
li.querySelector('[data-mwask-dones]').textContent = `${streak}-week streak${next ? ` · next ask on ${next}` : ''}`;
|
|
164
|
+
li.querySelector('.mwask__done').hidden = false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function post(li, body) {
|
|
168
|
+
const err = li.querySelector('.mwask__err');
|
|
169
|
+
err.textContent = '';
|
|
170
|
+
li.querySelectorAll('.hask__opt').forEach((b) => { b.disabled = true; });
|
|
171
|
+
const res = client ? await client.request('POST', `${API}/ask/answer`, { body }) : { ok: false };
|
|
172
|
+
li.querySelectorAll('.hask__opt').forEach((b) => { b.disabled = false; });
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
const msg = res.data && res.data.error && res.data.error.message;
|
|
175
|
+
err.textContent = msg || 'Could not send that. Try again in a moment.';
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return res.data;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function addEntry(li, q) {
|
|
182
|
+
const row = li.querySelector(`[data-mwask-addrow="${q}"]`);
|
|
183
|
+
const input = row && row.querySelector('input');
|
|
184
|
+
const v = input ? input.value.replace(/\s+/g, ' ').trim().slice(0, MAX_TEXT) : '';
|
|
185
|
+
if (!v) return;
|
|
186
|
+
const c = document.createElement('button');
|
|
187
|
+
c.type = 'button';
|
|
188
|
+
c.className = 'mwask__chip';
|
|
189
|
+
c.setAttribute('aria-pressed', 'true');
|
|
190
|
+
c.setAttribute('data-mwask-q', q);
|
|
191
|
+
c.setAttribute('data-mwask-free', v);
|
|
192
|
+
c.textContent = v; // textContent: a member's words are never markup
|
|
193
|
+
const plus = li.querySelector(`[data-mwask-add="${q}"]`);
|
|
194
|
+
plus.parentNode.insertBefore(c, plus);
|
|
195
|
+
input.value = '';
|
|
196
|
+
row.hidden = true;
|
|
197
|
+
recount(li);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function onClick(e) {
|
|
201
|
+
const li = e.target.closest('[data-mwask]');
|
|
202
|
+
if (!li) return;
|
|
203
|
+
const chip = e.target.closest('.mwask__chip:not(.mwask__chip--add)');
|
|
204
|
+
if (chip) { chip.setAttribute('aria-pressed', chip.getAttribute('aria-pressed') === 'true' ? 'false' : 'true'); recount(li); return; }
|
|
205
|
+
const plus = e.target.closest('[data-mwask-add]');
|
|
206
|
+
if (plus) {
|
|
207
|
+
const row = li.querySelector(`[data-mwask-addrow="${plus.getAttribute('data-mwask-add')}"]`);
|
|
208
|
+
row.hidden = false;
|
|
209
|
+
row.querySelector('input').focus();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const ok = e.target.closest('[data-mwask-ok]');
|
|
213
|
+
if (ok) { addEntry(li, ok.getAttribute('data-mwask-ok')); return; }
|
|
214
|
+
const week = li.getAttribute('data-mwask-week');
|
|
215
|
+
if (e.target.closest('[data-mwask-send]')) {
|
|
216
|
+
const pressed = pressedIn(li);
|
|
217
|
+
if (!pressed.length) return;
|
|
218
|
+
const r = await post(li, answerBody(week, pressed));
|
|
219
|
+
if (r) done(li, `sent. ${pressed.length} proposal${pressed.length > 1 ? 's' : ''} in the queue.`);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (e.target.closest('[data-mwask-nothing]')) {
|
|
223
|
+
const r = await post(li, answerBody(week, [], true));
|
|
224
|
+
if (r) done(li, 'noted. nothing new this week.');
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function onKey(e) {
|
|
229
|
+
if (e.key !== 'Enter' || !e.target.matches('.mwask__add input')) return;
|
|
230
|
+
e.preventDefault();
|
|
231
|
+
const li = e.target.closest('[data-mwask]');
|
|
232
|
+
if (li) addEntry(li, e.target.closest('[data-mwask-addrow]').getAttribute('data-mwask-addrow'));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function load() {
|
|
236
|
+
if (!client || !box) return;
|
|
237
|
+
const res = await client.request('GET', `${API}/ask`);
|
|
238
|
+
const card = res && res.ok && res.data ? res.data.card : null;
|
|
239
|
+
const human = window.OTBAsk && typeof window.OTBAsk.human === 'function' ? window.OTBAsk.human('decision') : '';
|
|
240
|
+
box.innerHTML = cardHtml(card, human);
|
|
241
|
+
refresh();
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// The attention card builds its slices when the hall mounts its widgets, which is after
|
|
245
|
+
// this file runs (both are deferred, this one first). Wait for the container, once.
|
|
246
|
+
function mount(slices) {
|
|
247
|
+
if (box || !slices) return;
|
|
248
|
+
css();
|
|
249
|
+
box = document.createElement('div');
|
|
250
|
+
box.id = SLICE_ID;
|
|
251
|
+
// Beside the other personal asks (the profile nudge), ahead of project-wide queues.
|
|
252
|
+
const anchor = slices.querySelector('#attention-ideas');
|
|
253
|
+
slices.insertBefore(box, anchor || null);
|
|
254
|
+
box.addEventListener('click', onClick);
|
|
255
|
+
box.addEventListener('keydown', onKey);
|
|
256
|
+
load().catch(() => { box.innerHTML = ''; refresh(); });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function watch() {
|
|
260
|
+
const found = document.getElementById('attention-slices');
|
|
261
|
+
if (found) { mount(found); return; }
|
|
262
|
+
const mo = new MutationObserver(() => {
|
|
263
|
+
const s = document.getElementById('attention-slices');
|
|
264
|
+
if (s) { mo.disconnect(); mount(s); }
|
|
265
|
+
});
|
|
266
|
+
mo.observe(document.documentElement, { childList: true, subtree: true });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', watch);
|
|
270
|
+
else watch();
|
|
271
|
+
})();
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* modules/market-watch/public/weekly-ask.css — the weekly ask card's chips (task 1004940).
|
|
2
|
+
The card itself is the hall's ask card (hall-ask.css `.hask`, drawn as an inbox row);
|
|
3
|
+
this sheet adds only what option A (docs/design/mocks/market-watch/weekly-ask.html)
|
|
4
|
+
has that the hall card does not: the tap-to-toggle chips, the inline add, the streak
|
|
5
|
+
bars and the team's proposal rows. Colours are the hall's own tokens, never the mock's. */
|
|
6
|
+
|
|
7
|
+
.mwask__sp { flex: 1; }
|
|
8
|
+
.mwask__streak { display: inline-flex; gap: 6px; align-items: center; flex: none; font-size: 13px; color: var(--ink-soft); white-space: nowrap; }
|
|
9
|
+
.mwask__streak i { width: 18px; height: 6px; border-radius: 3px; background: var(--accent); display: inline-block; }
|
|
10
|
+
.mwask__streak i.off { background: var(--hask-edge); }
|
|
11
|
+
.mwask .hask__where { flex-wrap: wrap; row-gap: 6px; }
|
|
12
|
+
.mwask .hask__where-t { flex: 1 1 auto; min-width: 12ch; }
|
|
13
|
+
.mwask__body { display: contents; }
|
|
14
|
+
.mwask__chips { display: flex; flex-wrap: wrap; gap: 8px; }
|
|
15
|
+
.mwask__chip { display: inline-block; max-width: 100%; min-height: 36px; padding: 8px 14px; box-sizing: border-box; border-radius: 999px; border: 1px solid var(--hask-edge); background: transparent; color: var(--ink); font-family: var(--font-body); font-size: 14px; text-align: left; cursor: pointer; overflow-wrap: anywhere; }
|
|
16
|
+
.mwask__chip:hover { background: var(--hask-chip-pick); }
|
|
17
|
+
.mwask__chip small { margin-left: 4px; color: var(--ink-faint); font-size: 12px; }
|
|
18
|
+
.mwask__chip[aria-pressed="true"] { background: var(--accent-tint); border-color: var(--accent); }
|
|
19
|
+
.mwask__chip[aria-pressed="true"]::before { content: "\2713"; margin-right: 6px; color: var(--accent-ink); font-weight: 600; }
|
|
20
|
+
.mwask__chip--add { border-style: dashed; color: var(--ink-soft); }
|
|
21
|
+
.mwask__chip:focus-visible, .mwask__add input:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
22
|
+
.mwask__sub { margin: 2px 0 -4px; font-size: 13px; color: var(--ink-soft); }
|
|
23
|
+
.mwask__add { display: flex; gap: 8px; }
|
|
24
|
+
.mwask__add[hidden] { display: none; }
|
|
25
|
+
.mwask__add input { flex: 1; min-width: 0; min-height: 36px; padding: 8px 14px; box-sizing: border-box; border-radius: 999px; border: 1px solid var(--hask-edge-pick); background: transparent; color: var(--ink); font: inherit; font-size: 14px; }
|
|
26
|
+
.mwask__done { display: flex; flex-direction: column; gap: 6px; }
|
|
27
|
+
.mwask__done[hidden] { display: none; }
|
|
28
|
+
.mwask__err { margin: 0; font-size: 13px; color: var(--danger); }
|
|
29
|
+
.mwask__err:empty { display: none; }
|
|
30
|
+
.mwask__lbl { margin: 18px 0 8px; font-size: 12px; letter-spacing: .06em; color: var(--ink-faint); }
|
|
31
|
+
.mwask__rows { list-style: none; margin: 0; padding: 0; border-top: 1px solid var(--rule-soft); }
|
|
32
|
+
.mwask__row { display: flex; align-items: center; gap: 12px; min-height: 46px; padding: 8px 2px; border-bottom: 1px solid var(--rule-soft); white-space: nowrap; overflow: hidden; }
|
|
33
|
+
.mwask__row .mwask__what { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
|
|
34
|
+
.mwask__row .mwask__who { color: var(--ink-soft); min-width: 0; overflow: hidden; text-overflow: ellipsis; max-width: 30%; }
|
|
35
|
+
.mwask__dot { flex: none; display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); }
|
|
36
|
+
.mwask__dot--watch { background: var(--info); }
|
|
37
|
+
.mwask__dot--done { background: var(--hask-edge-pick); }
|
|
38
|
+
.mwask__pill { flex: none; font-size: 12px; padding: 2px 9px; border-radius: 999px; border: 1px solid var(--hask-edge); color: var(--ink-soft); white-space: nowrap; }
|
|
39
|
+
.mwask__pill--hot { background: var(--accent-tint); border-color: transparent; color: var(--accent-ink); }
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// modules/market-watch/routes/ask.js — the weekly ask card's two routes (task 1004940).
|
|
4
|
+
//
|
|
5
|
+
// GET /market-watch/ask the caller's card this week, or { card: null }
|
|
6
|
+
// POST /market-watch/ask/answer the caller's taps and entries
|
|
7
|
+
//
|
|
8
|
+
// MEMBERS ONLY, SERVER-CHECKED. requireBuilder resolves the session (a signed-out caller is
|
|
9
|
+
// 401, a deactivated builder 403) and the handler's gate() runs `isMember`, which admits
|
|
10
|
+
// exactly the active builders on the rank ladder, so a reserved or unknown rank is refused.
|
|
11
|
+
// No requireRank here: tests/government_require_permission.mjs refuses it under
|
|
12
|
+
// modules/*/routes, and no catalog permission names a module's member-only action.
|
|
13
|
+
//
|
|
14
|
+
// NOBODY ANSWERS FOR ANYONE ELSE. The answer is always recorded under req.builder.id. A body
|
|
15
|
+
// that names another builder (`builder_id`) is refused 403 rather than quietly re-pointed,
|
|
16
|
+
// so a client that tries it learns it cannot.
|
|
17
|
+
//
|
|
18
|
+
// The project is this instance's own public origin (seed.js projectKey), never the body's.
|
|
19
|
+
|
|
20
|
+
const express = require('express');
|
|
21
|
+
const api = require('../../../src/module-api');
|
|
22
|
+
const ask = require('../ask');
|
|
23
|
+
const { projectKey } = require('../seed');
|
|
24
|
+
|
|
25
|
+
const log = api.logger('market-watch');
|
|
26
|
+
|
|
27
|
+
function defaultProject() {
|
|
28
|
+
try { return projectKey(api.branding().domains.publicOrigin); } catch { return null; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// isMember(builder) — an active builder on this project's rank ladder.
|
|
32
|
+
function isMember(builder) {
|
|
33
|
+
if (!builder || builder.id == null) return false;
|
|
34
|
+
if (builder.status === 'inactive') return false;
|
|
35
|
+
return api.rankMeetsThreshold(builder.rank, ['xenos']);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const fail = (res, status, code, message) => res.status(status).json({ error: { code, message } });
|
|
39
|
+
|
|
40
|
+
module.exports = function buildMarketWatchAskRouter(deps = {}) {
|
|
41
|
+
const router = express.Router();
|
|
42
|
+
// deps (tests only): store — a store or a function returning one; project; now.
|
|
43
|
+
const store = () => (typeof deps.store === 'function' ? deps.store() : deps.store) || ask.createAskStore(api.pool);
|
|
44
|
+
const project = () => (deps.project !== undefined ? deps.project : defaultProject());
|
|
45
|
+
const now = () => (typeof deps.now === 'function' ? deps.now() : new Date());
|
|
46
|
+
|
|
47
|
+
function gate(req, res) {
|
|
48
|
+
if (!isMember(req.builder)) { fail(res, 403, 'not_a_member', 'Only members of this project get the weekly ask. Ask the project owner to add you.'); return null; }
|
|
49
|
+
const p = project();
|
|
50
|
+
if (!p) { fail(res, 503, 'no_project_origin', 'This project has no public origin set.'); return null; }
|
|
51
|
+
return p;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// rank: any-builder — every member reads their own card; nobody else's (isMember in gate()).
|
|
55
|
+
router.get('/market-watch/ask', api.requireBuilder, async (req, res) => {
|
|
56
|
+
const p = gate(req, res);
|
|
57
|
+
if (!p) return;
|
|
58
|
+
try {
|
|
59
|
+
res.set('Cache-Control', 'private, no-store');
|
|
60
|
+
res.json({ card: await ask.cardFor(store(), { project: p, builderId: req.builder.id, now: now() }) });
|
|
61
|
+
} catch (e) {
|
|
62
|
+
log.error(`weekly ask read failed: ${e && e.message}`);
|
|
63
|
+
fail(res, 500, 'ask_read_failed', "Could not read this week's ask.");
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// rank: any-builder — a member answers for themselves only (isMember in gate()).
|
|
68
|
+
router.post('/market-watch/ask/answer', api.requireBuilder, express.json({ limit: '16kb' }), async (req, res) => {
|
|
69
|
+
const p = gate(req, res);
|
|
70
|
+
if (!p) return;
|
|
71
|
+
const body = req.body || {};
|
|
72
|
+
if (body.builder_id != null && String(body.builder_id) !== String(req.builder.id)) {
|
|
73
|
+
fail(res, 403, 'not_your_answer', 'You can only answer for yourself.');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
// The envelope's shape, strict (an unknown field is refused). Each entry's own fields,
|
|
77
|
+
// lengths and the chip-must-be-offered rule are ask.js parseAnswer's and answer's.
|
|
78
|
+
if (api.validateOrRespond(req, res, {
|
|
79
|
+
week_of: { type: 'string', maxLength: 10 },
|
|
80
|
+
picks: { type: 'array', maxItems: 20 },
|
|
81
|
+
names: { type: 'array', maxItems: 6 },
|
|
82
|
+
nothing: { type: 'boolean' },
|
|
83
|
+
builder_id: { type: 'integer' },
|
|
84
|
+
})) return;
|
|
85
|
+
try {
|
|
86
|
+
const r = await ask.answer(store(), { project: p, builderId: req.builder.id, body, now: now() });
|
|
87
|
+
res.status(r.status).json(r.body);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
log.error(`weekly ask answer failed: ${e && e.message}`);
|
|
90
|
+
fail(res, 500, 'ask_answer_failed', 'Could not save your answer.');
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
return router;
|
|
95
|
+
};
|
|
@@ -83,6 +83,16 @@ const DOOR_REFUSAL = {
|
|
|
83
83
|
// GitHub usernames: 1–39 chars, alphanumeric or single hyphens, no leading/
|
|
84
84
|
// trailing hyphen. We accept a leading '@' and strip it (people paste "@name").
|
|
85
85
|
const GITHUB_LOGIN_RE = /^[a-zA-Z0-9](?:[a-zA-Z0-9]|-(?=[a-zA-Z0-9])){0,38}$/;
|
|
86
|
+
// A person who joined with Google has no GitHub login; their builder row carries
|
|
87
|
+
// the stand-in `~<handle>` core's standInLogin writes (ADR 0396). `~` is never in
|
|
88
|
+
// a GitHub login, so the two shapes cannot be confused.
|
|
89
|
+
const STAND_IN_LOGIN_RE = /^~[a-z0-9-]{1,39}$/;
|
|
90
|
+
|
|
91
|
+
// A login as people should read it: the stand-in's handle without its `~`.
|
|
92
|
+
function shownLogin(login) {
|
|
93
|
+
const s = String(login || '');
|
|
94
|
+
return STAND_IN_LOGIN_RE.test(s) ? s.slice(1) : s;
|
|
95
|
+
}
|
|
86
96
|
|
|
87
97
|
function normalizeLogin(raw) {
|
|
88
98
|
return String(raw || '').trim().replace(/^@/, '');
|
|
@@ -176,6 +186,10 @@ async function attachApplicantProfiles(rows) {
|
|
|
176
186
|
// re-invite nor keeps the hub's notice alive.
|
|
177
187
|
const STANDING_INVITE_SQL = `SELECT 1 FROM access_requests
|
|
178
188
|
WHERE lower(github_login) = lower($1) AND status = 'invited' AND ${inviteLiveSql()} LIMIT 1`;
|
|
189
|
+
// The same, for a person who joined with Google: keyed on the vouched person key,
|
|
190
|
+
// never the stand-in handle (task 1004994; mirrors core hasInvitedAccessRequest).
|
|
191
|
+
const STAND_IN_INVITE_SQL = `SELECT 1 FROM access_requests
|
|
192
|
+
WHERE applicant_github_id = $1 AND status = 'invited' AND ${inviteLiveSql()} LIMIT 1`;
|
|
179
193
|
|
|
180
194
|
function noticeHubInvited(login) {
|
|
181
195
|
api.notifyHubOfInvite(login, { invited: true });
|
|
@@ -235,8 +249,8 @@ function accessRequestDocketMatter(r) {
|
|
|
235
249
|
id: r.id,
|
|
236
250
|
rule: 'decide',
|
|
237
251
|
state: 'needs',
|
|
238
|
-
title: `Admit ${r.display_name || r.github_login} as a builder`,
|
|
239
|
-
short: `admit ${r.github_login}`,
|
|
252
|
+
title: `Admit ${r.display_name || shownLogin(r.github_login)} as a builder`,
|
|
253
|
+
short: `admit ${shownLogin(r.github_login)}`,
|
|
240
254
|
where: r.vouched ? 'people · waiting to join · vouched' : 'people · waiting to join',
|
|
241
255
|
opened_at: r.created_at,
|
|
242
256
|
deadline: null,
|
|
@@ -287,29 +301,42 @@ module.exports = function buildAccessRequestsRouter() {
|
|
|
287
301
|
})) return;
|
|
288
302
|
|
|
289
303
|
const login = normalizeLogin(req.body.github_login);
|
|
290
|
-
|
|
304
|
+
const standIn = STAND_IN_LOGIN_RE.test(login);
|
|
305
|
+
if (!standIn && !GITHUB_LOGIN_RE.test(login)) {
|
|
291
306
|
return res.fail('bad_github_login', { status: 400, message: 'That does not look like a GitHub username.' });
|
|
292
307
|
}
|
|
293
308
|
const displayName = (req.body.display_name || '').trim() || null;
|
|
294
309
|
const note = (req.body.note || '').trim() || null;
|
|
295
310
|
// A verified vouch that names this login → the account the hub confirmed; anything
|
|
296
311
|
// else → null, and the unvouched row this route always wrote (never a refusal,
|
|
297
|
-
// ADR 0201 D2.3). Descriptive only: no admission path reads it
|
|
312
|
+
// ADR 0201 D2.3). Descriptive only for a GitHub login: no admission path reads it
|
|
313
|
+
// (ADR 0016).
|
|
298
314
|
const vouch = api.readApplicationVouch(req.get(APPLICATION_VOUCH_HEADER), { githubLogin: login });
|
|
315
|
+
// A person who joined with Google applies by their stand-in `~handle`, and only
|
|
316
|
+
// through the hub's vouch for their person key (task 1004994): a stand-in is
|
|
317
|
+
// not an identity anyone may type, and their approval is matched on the key.
|
|
318
|
+
if (standIn && !vouch) {
|
|
319
|
+
return res.fail('bad_github_login', { status: 400, message: 'Apply to this project from your Cloud Bongos account.' });
|
|
320
|
+
}
|
|
299
321
|
|
|
300
322
|
try {
|
|
301
323
|
// Already a builder? They have an account — point them at sign-in instead
|
|
302
|
-
// of opening a redundant request. (2026-05-30)
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
[
|
|
306
|
-
|
|
324
|
+
// of opening a redundant request. (2026-05-30) A stand-in is matched on the
|
|
325
|
+
// vouched person key, since a handle can change hands.
|
|
326
|
+
const member = standIn
|
|
327
|
+
? await pool.query(`SELECT 1 FROM builders WHERE github_id = $1 LIMIT 1`, [vouch.githubId])
|
|
328
|
+
: await pool.query(`SELECT 1 FROM builders WHERE lower(github_login) = lower($1) LIMIT 1`, [login]);
|
|
307
329
|
if (member.rows.length > 0) {
|
|
308
330
|
return res.fail('already_member', { status: 409, message: 'You already have an account — just sign in with GitHub.' });
|
|
309
331
|
}
|
|
310
332
|
// Already approved (invited) but not yet signed in? No new request needed.
|
|
311
333
|
// An expired invite is not an approval any more, so its invitee may ask.
|
|
312
|
-
|
|
334
|
+
// A stand-in's approval is the vouched person's, so it is found by their key —
|
|
335
|
+
// the same match the sign-in gate makes (hasInvitedAccessRequest) — and a
|
|
336
|
+
// stale approval left under a handle someone else now holds never answers.
|
|
337
|
+
const invited = standIn
|
|
338
|
+
? await pool.query(STAND_IN_INVITE_SQL, [vouch.githubId])
|
|
339
|
+
: await pool.query(STANDING_INVITE_SQL, [login]);
|
|
313
340
|
if (invited.rows.length > 0) {
|
|
314
341
|
return res.fail('already_approved', { status: 409, message: "You're already approved — sign in with GitHub to enter." });
|
|
315
342
|
}
|
|
@@ -675,5 +702,6 @@ module.exports = function buildAccessRequestsRouter() {
|
|
|
675
702
|
};
|
|
676
703
|
|
|
677
704
|
module.exports.accessRequestDocketMatter = accessRequestDocketMatter;
|
|
705
|
+
module.exports.shownLogin = shownLogin;
|
|
678
706
|
|
|
679
707
|
module.exports.INVITE_DESK_ROW = INVITE_DESK_ROW;
|
|
@@ -58,6 +58,17 @@ function isMintedPersonKey(key) {
|
|
|
58
58
|
return /^-\d+$/.test(String(key ?? ''));
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// A project's hall that can take a minted key says so on its authorize link
|
|
62
|
+
// (`person_keys=1`, added by core's idpAuthorizeUrl from task 1004994). An older
|
|
63
|
+
// hall would store a negative key as a GitHub id, so it is never sent one
|
|
64
|
+
// (ADR 0393). Boundary-forced mirror of core's PERSON_KEYS_PARAM.
|
|
65
|
+
const PERSON_KEYS_PARAM = 'person_keys';
|
|
66
|
+
|
|
67
|
+
// May the hub hand this person's key to the hall whose authorize query this is?
|
|
68
|
+
function hallTakesPersonKey(key, query) {
|
|
69
|
+
return !isMintedPersonKey(key) || String((query && query[PERSON_KEYS_PARAM]) ?? '') === '1';
|
|
70
|
+
}
|
|
71
|
+
|
|
61
72
|
// An email as a provider vouched for it, trimmed and capped, or null. Shown to
|
|
62
73
|
// the person in their own settings and nothing else.
|
|
63
74
|
function cleanEmail(email) {
|
|
@@ -185,6 +196,8 @@ module.exports = {
|
|
|
185
196
|
LOGIN_PROVIDERS,
|
|
186
197
|
normalizeLogin,
|
|
187
198
|
isMintedPersonKey,
|
|
199
|
+
PERSON_KEYS_PARAM,
|
|
200
|
+
hallTakesPersonKey,
|
|
188
201
|
findPersonByLogin,
|
|
189
202
|
createAccountForLogin,
|
|
190
203
|
signInWithLogin,
|
|
@@ -77,8 +77,12 @@ const RELAY_SENTENCE = {
|
|
|
77
77
|
// because the fall-through is a 502 "could not be reached" — the exact lie about
|
|
78
78
|
// someone else's door that ADR 0194 D5 forbids.
|
|
79
79
|
not_published: 'This project isn’t published yet, so it isn’t taking anyone on. Its owner has to finish setting it up first.',
|
|
80
|
+
// A hall on a core older than task 1004994 refuses a Google-only person's
|
|
81
|
+
// stand-in `~handle` as "not a GitHub username". It is the project's version,
|
|
82
|
+
// not the person, so say that rather than "could not be reached".
|
|
83
|
+
needs_update: 'This project runs an older version that can’t take a Google sign-in yet. Ask its owner to update it, or apply with GitHub.',
|
|
80
84
|
};
|
|
81
|
-
function joinRelayOutcome(status, body) {
|
|
85
|
+
function joinRelayOutcome(status, body, { standIn = false } = {}) {
|
|
82
86
|
const err = (body && body.error) || null;
|
|
83
87
|
const code = err && (err.code || err);
|
|
84
88
|
let outcome;
|
|
@@ -86,6 +90,7 @@ function joinRelayOutcome(status, body) {
|
|
|
86
90
|
else if (code === 'invite_only' || code === 'open_enrollment' || code === 'not_published') outcome = code;
|
|
87
91
|
else if (status === 409) outcome = 'already_member';
|
|
88
92
|
else if (status === 401) outcome = 'members_only';
|
|
93
|
+
else if (status === 400 && code === 'bad_github_login' && standIn) outcome = 'needs_update';
|
|
89
94
|
else return null;
|
|
90
95
|
return { status: outcome, message: RELAY_SENTENCE[outcome] };
|
|
91
96
|
}
|
|
@@ -227,11 +232,9 @@ module.exports = function myProjectsRoutes() {
|
|
|
227
232
|
note: { type: 'string', maxLength: APPLICANT_NOTE_MAX },
|
|
228
233
|
guild: { type: 'string', maxLength: 39 },
|
|
229
234
|
})) return;
|
|
230
|
-
// A Google-only person
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
return res.fail('google_join_not_yet', { status: 409, message: 'Projects can’t be joined with a Google sign-in yet. That is coming soon.' });
|
|
234
|
-
}
|
|
235
|
+
// A Google-only person is relayed like anyone else, by their stand-in login
|
|
236
|
+
// and a vouch for their person key (task 1004994). A hall too old to take
|
|
237
|
+
// one answers bad_github_login, which joinRelayOutcome reads as needs_update.
|
|
235
238
|
try {
|
|
236
239
|
const client = await pi.getClientByOrigin(req.body.origin, { pool });
|
|
237
240
|
if (!client || client.status !== 'active' || !client.origin) {
|
|
@@ -274,7 +277,7 @@ module.exports = function myProjectsRoutes() {
|
|
|
274
277
|
}),
|
|
275
278
|
});
|
|
276
279
|
const body = await r.json().catch(() => null);
|
|
277
|
-
outcome = joinRelayOutcome(r.status, body);
|
|
280
|
+
outcome = joinRelayOutcome(r.status, body, { standIn: isMintedPersonKey(req.builder.github_id) });
|
|
278
281
|
} catch (e) {
|
|
279
282
|
console.log('[platform-identity] join relay failed (non-fatal):', e.message);
|
|
280
283
|
}
|
|
@@ -32,7 +32,11 @@ const guildEngagements = require('../guild-engagements');
|
|
|
32
32
|
const instanceFeedback = require('../instance-feedback');
|
|
33
33
|
const projectStats = require('../project-stats');
|
|
34
34
|
// Ways in other than GitHub (task 1004993, ADR 0393/0396).
|
|
35
|
-
const { signInWithLogin,
|
|
35
|
+
const { signInWithLogin, hallTakesPersonKey } = require('../account-logins');
|
|
36
|
+
|
|
37
|
+
// What a Google-only person reads when a project's hall runs a core too old to
|
|
38
|
+
// take them (task 1004994).
|
|
39
|
+
const OLDER_HALL_SENTENCE = 'This project runs an older version that can’t take a Google sign-in yet. Ask its owner to update it, or sign in to it with GitHub.';
|
|
36
40
|
|
|
37
41
|
// One limiter per hub process for POST /sso/feedback (task 1004462).
|
|
38
42
|
const feedbackLimiter = instanceFeedback.makeFeedbackLimiter();
|
|
@@ -327,11 +331,12 @@ module.exports = function ssoRoutes() {
|
|
|
327
331
|
: 'This session type cannot be used for cross-project sign-in.';
|
|
328
332
|
return res.status(403).type('text/html').send(errorPage(msg));
|
|
329
333
|
}
|
|
330
|
-
// A person who signed up with Google has a minted key
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
|
|
334
|
-
|
|
334
|
+
// A person who signed up with Google has a minted key. A hall takes one only
|
|
335
|
+
// when its authorize link says it can (`person_keys=1`, task 1004994): an older
|
|
336
|
+
// core would store the negative key as a GitHub id (ADR 0393). Say so plainly
|
|
337
|
+
// instead of sending it.
|
|
338
|
+
if (!hallTakesPersonKey(session.github_id, req.query)) {
|
|
339
|
+
return res.status(403).type('text/html').send(errorPage(OLDER_HALL_SENTENCE));
|
|
335
340
|
}
|
|
336
341
|
|
|
337
342
|
// A live hub session — record/refresh the platform account and mint a
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.4",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.4",
|
|
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.24.
|
|
3
|
+
"version": "1.24.4",
|
|
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",
|