@bongos/core 1.18.0 → 1.18.3
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 +132 -322
- package/README.md +1 -1
- package/config/branding.neutral.json +5 -1
- package/docs/adr/0142-project-context-is-prose-only.md +41 -0
- package/docs/adr/0143-onboarding-repo-picker-oauth-scope.md +35 -0
- package/docs/adr/README.md +5 -2
- package/docs/api/openapi.json +1789 -450
- package/docs/api-reference.md +52 -5
- package/docs/file-map.md +7 -7
- package/docs/recipes/packaging-the-core.md +13 -0
- package/docs/recipes/upgrading-the-core.md +5 -2
- package/migrations/039_newcomer_friendly_column.sql +5 -0
- package/migrations/102_remove_box_first_provision_approval.sql +4 -0
- package/migrations/core_192_github_oauth_tokens.sql +33 -0
- package/modules/hall-ui/public/my-projects.html +78 -0
- package/modules/hall-ui/public/my-projects.js +142 -0
- package/modules/hall-ui/public/scouting.html +45 -0
- package/modules/hall-ui/public/scouting.js +73 -0
- package/modules/status-ui/public/explore.html +54 -0
- package/modules/status-ui/public/explore.js +67 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/create.html +211 -14
- package/scripts/gds/cli-lib.js +11 -5
- package/scripts/gds/lint-migrations.js +102 -0
- package/scripts/gds/login.js +42 -10
- package/scripts/gds/mirror-redact.js +5 -0
- package/scripts/gds/package-core.js +263 -4
- package/scripts/gds/provision.js +145 -7
- package/scripts/gds/publish-manifest.js +15 -0
- package/scripts/gds/run-unit-tests.js +38 -14
- package/scripts/gds/setup.js +41 -0
- package/scripts/gds/ship.js +115 -31
- package/scripts/gds/upgrade.js +147 -5
- package/scripts/migrate.sh +10 -0
- package/src/bongos/auth.js +362 -5
- package/src/bongos/db.js +66 -0
- package/src/bongos/github-repos.js +93 -0
- package/src/bongos/routes/auth.js +93 -11
- package/src/bongos/routes/github.js +90 -0
- package/src/bongos/routes/instance.js +15 -3
- package/src/bongos/routes.js +2 -0
- package/src/branding.js +17 -2
- package/src/instance-config.js +16 -0
- package/src/module-api.js +29 -1
- package/tests/branding.mjs +29 -23
- package/tests/github_repos_helpers.mjs +91 -0
- package/tests/github_token_store.mjs +121 -0
- package/tests/idp_device_exchange.mjs +105 -0
- package/tests/idp_federation.mjs +100 -0
- package/tests/migrations_forward_safe.mjs +86 -0
- package/tests/module_api.mjs +1 -1
- package/tests/package_core.mjs +246 -1
- package/tests/provision.mjs +157 -0
- package/tests/pruned_lockfile_integrity.mjs +79 -0
- package/tests/ship_ci_deploy.mjs +60 -0
- package/tests/upgrade.mjs +254 -5
- package/.gitignore +0 -91
- package/docs/adr/0139-platform-history-migration-to-cloudbongos.md +0 -94
- package/docs/adr/0140-cloudbongos-standalone-reprovision.md +0 -43
- package/scripts/hall-preview/fixtures/access-requests.json +0 -20
- package/scripts/hall-preview/fixtures/achievements.json +0 -76
- package/scripts/hall-preview/fixtures/analytics__builder.json +0 -251
- package/scripts/hall-preview/fixtures/audit-log.json +0 -68
- package/scripts/hall-preview/fixtures/blockers__detail.json +0 -12
- package/scripts/hall-preview/fixtures/box__me.json +0 -5
- package/scripts/hall-preview/fixtures/boxes__cost-ledger.json +0 -69
- package/scripts/hall-preview/fixtures/builders__active-claims.json +0 -1
- package/scripts/hall-preview/fixtures/builders__directory.json +0 -81
- package/scripts/hall-preview/fixtures/builders__roster.json +0 -48
- package/scripts/hall-preview/fixtures/goals.json +0 -502
- package/scripts/hall-preview/fixtures/goals__25.json +0 -96
- package/scripts/hall-preview/fixtures/goals__26.json +0 -67
- package/scripts/hall-preview/fixtures/goals__3.json +0 -103
- package/scripts/hall-preview/fixtures/goals__35.json +0 -49
- package/scripts/hall-preview/fixtures/goals__conflicts.json +0 -154
- package/scripts/hall-preview/fixtures/goals__detail.json +0 -96
- package/scripts/hall-preview/fixtures/goals__inbox.json +0 -5
- package/scripts/hall-preview/fixtures/goals__requests.json +0 -3
- package/scripts/hall-preview/fixtures/grades__by-builder.json +0 -32
- package/scripts/hall-preview/fixtures/inbox.json +0 -330
- package/scripts/hall-preview/fixtures/inbox__detail.json +0 -15
- package/scripts/hall-preview/fixtures/instance.json +0 -124
- package/scripts/hall-preview/fixtures/leaderboard.json +0 -184
- package/scripts/hall-preview/fixtures/me.json +0 -476
- package/scripts/hall-preview/fixtures/me__art-key__own.json +0 -6
- package/scripts/hall-preview/fixtures/me__event-sound-prefs.json +0 -209
- package/scripts/hall-preview/fixtures/me__recent-ships.json +0 -304
- package/scripts/hall-preview/fixtures/me__render.json +0 -5
- package/scripts/hall-preview/fixtures/me__skill-prefs.json +0 -143
- package/scripts/hall-preview/fixtures/me__sound-prefs.json +0 -67
- package/scripts/hall-preview/fixtures/me__wandering.json +0 -43
- package/scripts/hall-preview/fixtures/override-requests.json +0 -15
- package/scripts/hall-preview/fixtures/provisioning__fleet.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__fleet__cost-ledger.json +0 -1
- package/scripts/hall-preview/fixtures/provisioning__instances.json +0 -1
- package/scripts/hall-preview/fixtures/public__analytics__ecosystem.json +0 -516
- package/scripts/hall-preview/fixtures/public__bounty-table.json +0 -37
- package/scripts/hall-preview/fixtures/public__cost-summary.json +0 -742
- package/scripts/hall-preview/fixtures/public__estimation-drift.json +0 -41
- package/scripts/hall-preview/fixtures/public__grades.json +0 -50
- package/scripts/hall-preview/fixtures/public__progress.json +0 -124
- package/scripts/hall-preview/fixtures/public__recent-shipped.json +0 -426
- package/scripts/hall-preview/fixtures/public__repo-health.json +0 -160
- package/scripts/hall-preview/fixtures/public__repo-info.json +0 -5
- package/scripts/hall-preview/fixtures/security__docs.json +0 -10
- package/scripts/hall-preview/fixtures/security__reports.json +0 -31
- package/scripts/hall-preview/fixtures/sessions__search.json +0 -9
- package/scripts/hall-preview/fixtures/tasks.json +0 -2420
- package/scripts/hall-preview/fixtures/tasks__claimable.json +0 -2555
- package/scripts/hall-preview/fixtures/tasks__detail.json +0 -111
- package/scripts/hall-preview/fixtures/tasks__peer-votes__feed.json +0 -330
- package/scripts/hall-preview/fixtures/versions.json +0 -89
package/src/bongos/db.js
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
// grade-count SQL runs on both sides of the kernel↔module line — keep in sync.
|
|
26
26
|
|
|
27
27
|
const { pool } = require('./pool');
|
|
28
|
+
const secretBox = require('./secret-box');
|
|
28
29
|
// Module seam (ADR 0083 / BV1.R43). The rank/status-change paths EMIT
|
|
29
30
|
// `builder.box-reconcile-needed` AFTER COMMIT (best-effort) so the dev-box module
|
|
30
31
|
// can reconcile a droplet a demotion/graduation implies — never a direct require
|
|
@@ -205,6 +206,68 @@ async function updateBuilderDisciplines(builderId, disciplines) {
|
|
|
205
206
|
// { enc: null, ... } when no key is set.
|
|
206
207
|
|
|
207
208
|
|
|
209
|
+
// ---- GitHub OAuth repo token, encrypted at rest (core_192 / task 1002099) ----
|
|
210
|
+
//
|
|
211
|
+
// The onboarding wizard (public-landing/create.html) opts into `public_repo` at
|
|
212
|
+
// sign-in so it can list + create the user's repos. That user-to-server GitHub
|
|
213
|
+
// token is held here BRIEFLY: encrypted with secret-box (master key in server env
|
|
214
|
+
// only — the plaintext never touches the DB) and given a short TTL (one wizard
|
|
215
|
+
// sitting), read back by the repo picker/creator, then deleted on use. It is the
|
|
216
|
+
// auth system's OWN handshake token (distinct from a feature's third-party key
|
|
217
|
+
// like the art module's Gemini key, ADR 0102), so it lives in core auth. secret-box
|
|
218
|
+
// is the gate: with no master key configured we NEVER store a plaintext token —
|
|
219
|
+
// capture no-ops and the wizard degrades to manual repo entry (ADR 0143).
|
|
220
|
+
const GITHUB_TOKEN_TTL_MS = 60 * 60 * 1000; // 1h — one wizard sitting.
|
|
221
|
+
|
|
222
|
+
// Store (or replace) the builder's GitHub repo token. `token` is the raw
|
|
223
|
+
// user-to-server access token; it is encrypted here so the plaintext never touches
|
|
224
|
+
// the DB. No-ops when secret-box is unconfigured (returns { stored:false }).
|
|
225
|
+
async function upsertGithubToken({ builderId, token, scope = null, ttlMs = GITHUB_TOKEN_TTL_MS, p = pool }) {
|
|
226
|
+
if (!secretBox.isConfigured()) return { stored: false };
|
|
227
|
+
const enc = secretBox.encrypt(token);
|
|
228
|
+
const expiresAt = new Date(Date.now() + ttlMs);
|
|
229
|
+
await p.query(
|
|
230
|
+
`INSERT INTO github_oauth_tokens (builder_id, token_enc, scope, created_at, expires_at)
|
|
231
|
+
VALUES ($1, $2, $3, now(), $4)
|
|
232
|
+
ON CONFLICT (builder_id) DO UPDATE
|
|
233
|
+
SET token_enc = EXCLUDED.token_enc, scope = EXCLUDED.scope,
|
|
234
|
+
created_at = now(), expires_at = EXCLUDED.expires_at`,
|
|
235
|
+
[builderId, enc, scope, expiresAt],
|
|
236
|
+
);
|
|
237
|
+
return { stored: true, expires_at: expiresAt };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Read + decrypt the builder's GitHub token, or null when there is none, it has
|
|
241
|
+
// expired, or secret-box is unconfigured. Lazily deletes an expired row so the
|
|
242
|
+
// table self-cleans without a cron (mirrors the deviceFlows reaping in auth.js).
|
|
243
|
+
// The 503-vs-403 distinction the endpoint needs is made by the endpoint checking
|
|
244
|
+
// secretBox.isConfigured() itself before calling this.
|
|
245
|
+
async function getGithubToken({ builderId, p = pool }) {
|
|
246
|
+
if (!secretBox.isConfigured()) return null;
|
|
247
|
+
const { rows } = await p.query(
|
|
248
|
+
`SELECT token_enc, scope, expires_at FROM github_oauth_tokens WHERE builder_id = $1`,
|
|
249
|
+
[builderId],
|
|
250
|
+
);
|
|
251
|
+
const row = rows[0];
|
|
252
|
+
if (!row) return null;
|
|
253
|
+
if (new Date(row.expires_at).getTime() <= Date.now()) {
|
|
254
|
+
await p.query(`DELETE FROM github_oauth_tokens WHERE builder_id = $1`, [builderId]).catch(() => {});
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
let token;
|
|
258
|
+
try { token = secretBox.decrypt(row.token_enc); }
|
|
259
|
+
catch { return null; } // a blob that won't decrypt (e.g. rotated master key) reads as "none".
|
|
260
|
+
return { token, scope: row.scope, expires_at: row.expires_at };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Delete the builder's token (called after the wizard's terminal enqueue, and on
|
|
264
|
+
// expiry). Best-effort; safe to call when nothing is stored.
|
|
265
|
+
async function deleteGithubToken({ builderId, p = pool }) {
|
|
266
|
+
await p.query(`DELETE FROM github_oauth_tokens WHERE builder_id = $1`, [builderId]);
|
|
267
|
+
return { deleted: true };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
|
|
208
271
|
// leaderboard() carved into modules/economy/credits.js (BV1.R77 / ADR 0093 §1-2).
|
|
209
272
|
// Core read routes resolve it via the `reward` kernel port.
|
|
210
273
|
|
|
@@ -908,4 +971,7 @@ module.exports = {
|
|
|
908
971
|
upsertSecurityDoc,
|
|
909
972
|
getSecurityDoc,
|
|
910
973
|
listSecurityDocs,
|
|
974
|
+
upsertGithubToken,
|
|
975
|
+
getGithubToken,
|
|
976
|
+
deleteGithubToken,
|
|
911
977
|
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// GitHub repo list/create on behalf of the signed-in user — the server half of the
|
|
2
|
+
// onboarding wizard's repo picker (adopt) + auto-create (fresh). Task 1002099 / ADR 0143.
|
|
3
|
+
//
|
|
4
|
+
// Raw `fetch` against GitHub's REST API (the house style — see auth.js
|
|
5
|
+
// fetchGithubUser; no octokit dependency). The user-to-server token these take is
|
|
6
|
+
// the `public_repo`-scoped token the web callback captured and stored (db.js
|
|
7
|
+
// github_oauth_tokens); callers pass the decrypted token in, this module never
|
|
8
|
+
// touches storage. `fetchImpl` is injectable so tests can drive it with a mock and
|
|
9
|
+
// never hit the network.
|
|
10
|
+
|
|
11
|
+
const { userAgent } = require('../branding');
|
|
12
|
+
|
|
13
|
+
const GH_API = 'https://api.github.com';
|
|
14
|
+
const PER_PAGE = 100;
|
|
15
|
+
// Up to 300 most-recently-updated OWNED repos — plenty to back a picker for the vast
|
|
16
|
+
// majority of accounts, and a hard bound so a power user can't make us page forever
|
|
17
|
+
// (v1 cap; the picker filters this set client-and-server-side by substring).
|
|
18
|
+
const MAX_PAGES = 3;
|
|
19
|
+
|
|
20
|
+
class GithubApiError extends Error {
|
|
21
|
+
constructor(message, { status, code } = {}) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'GithubApiError';
|
|
24
|
+
this.status = status;
|
|
25
|
+
this.code = code;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ghHeaders(token, extra = {}) {
|
|
30
|
+
return {
|
|
31
|
+
Authorization: `Bearer ${token}`,
|
|
32
|
+
'User-Agent': userAgent(),
|
|
33
|
+
Accept: 'application/vnd.github+json',
|
|
34
|
+
...extra,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// List the signed-in user's OWN repos (affiliation=owner so `full_name` is always
|
|
39
|
+
// `login/repo` and the org-OAuth-policy complexity is out of scope for v1),
|
|
40
|
+
// most-recently-updated first, optionally filtered by a case-insensitive substring
|
|
41
|
+
// `q` on the full name. Returns [{ full_name, name, owner, private }].
|
|
42
|
+
async function listUserRepos(token, { q = '', fetchImpl = fetch } = {}) {
|
|
43
|
+
const out = [];
|
|
44
|
+
for (let page = 1; page <= MAX_PAGES; page++) {
|
|
45
|
+
const url = `${GH_API}/user/repos?per_page=${PER_PAGE}&page=${page}&sort=updated&affiliation=owner`;
|
|
46
|
+
const res = await fetchImpl(url, { headers: ghHeaders(token) });
|
|
47
|
+
if (res.status === 401) {
|
|
48
|
+
throw new GithubApiError('GitHub rejected the stored token', { status: 401, code: 'github_unauthorized' });
|
|
49
|
+
}
|
|
50
|
+
if (!res.ok) {
|
|
51
|
+
throw new GithubApiError(`GitHub /user/repos failed (${res.status})`, { status: res.status, code: 'github_error' });
|
|
52
|
+
}
|
|
53
|
+
const rows = await res.json();
|
|
54
|
+
if (!Array.isArray(rows) || rows.length === 0) break;
|
|
55
|
+
for (const r of rows) {
|
|
56
|
+
out.push({
|
|
57
|
+
full_name: r.full_name,
|
|
58
|
+
name: r.name,
|
|
59
|
+
owner: r.owner && r.owner.login,
|
|
60
|
+
private: !!r.private,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (rows.length < PER_PAGE) break;
|
|
64
|
+
}
|
|
65
|
+
const needle = String(q || '').trim().toLowerCase();
|
|
66
|
+
return needle ? out.filter((r) => String(r.full_name).toLowerCase().includes(needle)) : out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create an EMPTY repo in the signed-in user's personal account. auto_init:false =
|
|
70
|
+
// no README/first commit (exactly the "leave it completely empty" the manual step
|
|
71
|
+
// demanded); private:false because we hold only `public_repo`. Returns
|
|
72
|
+
// { full_name, html_url }. A 422 (name already exists / invalid) becomes a typed
|
|
73
|
+
// `name_taken` error the route maps to a friendly 409 rather than an opaque 500.
|
|
74
|
+
async function createUserRepo(token, { name, fetchImpl = fetch } = {}) {
|
|
75
|
+
const res = await fetchImpl(`${GH_API}/user/repos`, {
|
|
76
|
+
method: 'POST',
|
|
77
|
+
headers: ghHeaders(token, { 'Content-Type': 'application/json' }),
|
|
78
|
+
body: JSON.stringify({ name, private: false, auto_init: false }),
|
|
79
|
+
});
|
|
80
|
+
if (res.status === 201) {
|
|
81
|
+
const r = await res.json();
|
|
82
|
+
return { full_name: r.full_name, html_url: r.html_url };
|
|
83
|
+
}
|
|
84
|
+
if (res.status === 401) {
|
|
85
|
+
throw new GithubApiError('GitHub rejected the stored token', { status: 401, code: 'github_unauthorized' });
|
|
86
|
+
}
|
|
87
|
+
if (res.status === 422) {
|
|
88
|
+
throw new GithubApiError('A repository with that name already exists on your account', { status: 422, code: 'name_taken' });
|
|
89
|
+
}
|
|
90
|
+
throw new GithubApiError(`GitHub create-repo failed (${res.status})`, { status: res.status, code: 'github_error' });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { listUserRepos, createUserRepo, GithubApiError, MAX_PAGES };
|
|
@@ -93,6 +93,13 @@ const DISCORD_STATE_COOKIE = 'gds_discord_state';
|
|
|
93
93
|
// flow can land a builder back on the subdomain they started from (#726). Set
|
|
94
94
|
// at /auth/web/start, consumed + cleared at the callback. Defaults to /builders.
|
|
95
95
|
const RETURN_COOKIE = 'gds_return';
|
|
96
|
+
// Short-lived cookie marking that THIS sign-in opted into repo access (the
|
|
97
|
+
// onboarding wizard's `?repos=1`). Set at /auth/web/start, read at the callback so
|
|
98
|
+
// it captures + persists the returned GitHub token — and cleared there. Scoping
|
|
99
|
+
// capture to an explicit intent cookie (not just "the granted scope happens to
|
|
100
|
+
// include repo") means an ordinary hall sign-in never stores a token, even for a
|
|
101
|
+
// user who previously granted the broader scope (task 1002099 / ADR 0143).
|
|
102
|
+
const REPOS_COOKIE = 'gds_oauth_repos';
|
|
96
103
|
|
|
97
104
|
// The apex domain the instance is served under, read from the instance branding
|
|
98
105
|
// pack's cookieDomain (".amazonprimea.com" → "amazonprimea.com",
|
|
@@ -245,10 +252,47 @@ module.exports = function buildAuthRouter() {
|
|
|
245
252
|
}
|
|
246
253
|
});
|
|
247
254
|
|
|
255
|
+
// POST /auth/device/exchange — CLI federation (ADR 0141 §1). The CLI ran the
|
|
256
|
+
// device flow against the HUB and got a hub-signed assertion; it presents it here.
|
|
257
|
+
// The instance verifies the Ed25519 signature OFFLINE against the provisioned hub
|
|
258
|
+
// public key (aud must equal this instance's client_id, unexpired), then runs the
|
|
259
|
+
// instance's OWN admission gate + upsertBuilder + createSession(source='cli') and
|
|
260
|
+
// returns the existing { token, builder }. Only reachable when auth.idp is set.
|
|
261
|
+
//
|
|
262
|
+
// rank: public — an auth-establishment endpoint; the hub-signed assertion IS the
|
|
263
|
+
// bearer and is cryptographically verified before any session is minted.
|
|
264
|
+
router.post('/auth/device/exchange', async (req, res) => {
|
|
265
|
+
if (!auth.idpConfigured()) {
|
|
266
|
+
return res.fail('idp_not_configured', 503);
|
|
267
|
+
}
|
|
268
|
+
if (validateOrRespond(req, res, {
|
|
269
|
+
assertion: { required: true, type: 'string', maxLength: 4096 },
|
|
270
|
+
})) return;
|
|
271
|
+
try {
|
|
272
|
+
const { token, builder } = await auth.completeDeviceExchange({
|
|
273
|
+
assertion: req.body.assertion,
|
|
274
|
+
userAgent: req.headers['user-agent'],
|
|
275
|
+
});
|
|
276
|
+
res.json({ token, builder });
|
|
277
|
+
} catch (err) {
|
|
278
|
+
// Mirror the device/poll admission surface so the CLI prints the same
|
|
279
|
+
// "ask an Archon to invite you" path.
|
|
280
|
+
if (err && err.code === 'NOT_APPROVED') {
|
|
281
|
+
return res.status(403).json({ state: 'not_approved', error: 'not_approved' });
|
|
282
|
+
}
|
|
283
|
+
// A bad / expired / forged / wrong-audience assertion is an auth failure (401),
|
|
284
|
+
// not a server error — never mint a session for it.
|
|
285
|
+
console.error('[gds] /auth/device/exchange', err);
|
|
286
|
+
res.fail('invalid_assertion', { status: 401, message: String(err.message || 'invalid assertion').slice(0, 160) });
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
248
290
|
// /auth/web — browser flow (used by the /builders page).
|
|
249
291
|
// rank: public — entry into the GitHub OAuth handshake.
|
|
250
292
|
router.get('/auth/web/start', (req, res) => {
|
|
251
|
-
|
|
293
|
+
// Either direct-GitHub creds OR platform-IdP federation must be configured
|
|
294
|
+
// (ADR 0141 §1) — a federated instance has no GitHub app of its own.
|
|
295
|
+
if (!auth.authConfigured() && !auth.idpConfigured()) {
|
|
252
296
|
return res.status(503).type('text/plain').send('auth not configured');
|
|
253
297
|
}
|
|
254
298
|
const state = crypto.randomBytes(16).toString('hex');
|
|
@@ -281,12 +325,32 @@ module.exports = function buildAuthRouter() {
|
|
|
281
325
|
...sessionCookieDomainOpts(req),
|
|
282
326
|
});
|
|
283
327
|
}
|
|
284
|
-
|
|
328
|
+
// Opt-in repo access (task 1002099 / ADR 0143): the onboarding wizard passes
|
|
329
|
+
// ?repos=1 so it can list/create the user's repos. Remember the intent in a
|
|
330
|
+
// short-lived cookie (same recipe as the state cookie) and request the broader
|
|
331
|
+
// scope on the direct-GitHub leg. Absent the flag this is a no-op — hall + CLI
|
|
332
|
+
// sign-ins stay identity-only.
|
|
333
|
+
const wantsRepos = req.query.repos === '1' || req.query.repos === 'true';
|
|
334
|
+
if (wantsRepos) {
|
|
335
|
+
res.cookie(REPOS_COOKIE, '1', {
|
|
336
|
+
httpOnly: true,
|
|
337
|
+
sameSite: 'lax',
|
|
338
|
+
secure: req.secure || req.headers['x-forwarded-proto'] === 'https',
|
|
339
|
+
maxAge: 10 * 60 * 1000,
|
|
340
|
+
...sessionCookieDomainOpts(req),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
// Federate through the hub when configured (ADR 0141 §1); otherwise the direct
|
|
344
|
+
// GitHub authorize, carrying the repo-scope opt-in. Same CSRF state cookie either
|
|
345
|
+
// way — the callback validates it identically, so the hub leg is CSRF-protected too.
|
|
346
|
+
res.redirect(auth.idpConfigured()
|
|
347
|
+
? auth.buildIdpAuthorizeUrl(state, redirectUri)
|
|
348
|
+
: auth.buildAuthorizeUrl(state, redirectUri, { repos: wantsRepos }));
|
|
285
349
|
});
|
|
286
350
|
|
|
287
351
|
// rank: public — OAuth callback; the state cookie + code are the bearer.
|
|
288
352
|
router.get('/auth/web/callback', async (req, res) => {
|
|
289
|
-
if (!auth.authConfigured()) {
|
|
353
|
+
if (!auth.authConfigured() && !auth.idpConfigured()) {
|
|
290
354
|
return res.status(503).type('text/plain').send('auth not configured');
|
|
291
355
|
}
|
|
292
356
|
const expectedState = parseCookie(req, STATE_COOKIE) || parseCookie(req, STATE_COOKIE_LEGACY);
|
|
@@ -304,14 +368,19 @@ module.exports = function buildAuthRouter() {
|
|
|
304
368
|
));
|
|
305
369
|
}
|
|
306
370
|
try {
|
|
307
|
-
// Must match the PINNED redirect_uri used at /auth/web/start (#734) —
|
|
308
|
-
// GitHub token exchange
|
|
371
|
+
// Must match the PINNED redirect_uri used at /auth/web/start (#734) — both
|
|
372
|
+
// the GitHub token exchange and the hub /sso/token redemption require an
|
|
373
|
+
// exact string match against the value sent at authorize time.
|
|
309
374
|
const redirectUri = `${WEB_REDIRECT_ORIGIN}/api/gds/auth/web/callback`;
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
375
|
+
// Did THIS sign-in opt into repo access? (the wizard's ?repos=1 intent cookie)
|
|
376
|
+
const wantsRepos = parseCookie(req, REPOS_COOKIE) === '1';
|
|
377
|
+
// Federated (hub) vs direct-GitHub — same downstream: admission gate,
|
|
378
|
+
// upsertBuilder, createSession, gds_session cookie (ADR 0141 §1). The direct
|
|
379
|
+
// leg additionally captures the opt-in repo token (task 1002099 / ADR 0143);
|
|
380
|
+
// the hub leg has no direct GitHub token, so the wizard degrades to manual entry.
|
|
381
|
+
const { token, expiresAt, builder, githubToken, githubScope } = auth.idpConfigured()
|
|
382
|
+
? await auth.completeIdpFlow({ code, redirectUri, userAgent: req.headers['user-agent'] })
|
|
383
|
+
: await auth.completeWebFlow({ code, redirectUri, userAgent: req.headers['user-agent'], captureRepoToken: wantsRepos });
|
|
315
384
|
res.cookie(SESSION_COOKIE, token, {
|
|
316
385
|
httpOnly: true,
|
|
317
386
|
sameSite: 'lax',
|
|
@@ -322,15 +391,28 @@ module.exports = function buildAuthRouter() {
|
|
|
322
391
|
// amazonprimea.com + builders. + status. (#726); host-only elsewhere.
|
|
323
392
|
...sessionCookieDomainOpts(req),
|
|
324
393
|
});
|
|
394
|
+
// Persist the wizard's repo token (task 1002099 / ADR 0143): encrypted at rest,
|
|
395
|
+
// short-TTL, keyed to the builder — the repo picker/creator reads it back over
|
|
396
|
+
// the next few minutes. Gated on secret-box being configured (upsertGithubToken
|
|
397
|
+
// no-ops when it isn't). Best-effort: a capture failure must NEVER break sign-in.
|
|
398
|
+
if (wantsRepos && githubToken) {
|
|
399
|
+
try {
|
|
400
|
+
await db.upsertGithubToken({ builderId: builder.id, token: githubToken, scope: githubScope });
|
|
401
|
+
} catch (e) {
|
|
402
|
+
console.error('[gds] web/callback: repo-token capture failed (non-blocking):', e.message);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
325
405
|
// The state cookie is apex-scoped now (#734) — clear it with the same
|
|
326
406
|
// domain or the browser keeps it. Clear the host-only form too for any
|
|
327
|
-
// cookie set before this change.
|
|
407
|
+
// cookie set before this change. Clear the repos-intent cookie the same way.
|
|
328
408
|
const domainOpts = sessionCookieDomainOpts(req);
|
|
329
409
|
res.clearCookie(STATE_COOKIE);
|
|
330
410
|
res.clearCookie(STATE_COOKIE_LEGACY);
|
|
411
|
+
res.clearCookie(REPOS_COOKIE);
|
|
331
412
|
if (domainOpts.domain) {
|
|
332
413
|
res.clearCookie(STATE_COOKIE, domainOpts);
|
|
333
414
|
res.clearCookie(STATE_COOKIE_LEGACY, domainOpts);
|
|
415
|
+
res.clearCookie(REPOS_COOKIE, domainOpts);
|
|
334
416
|
}
|
|
335
417
|
// Land back where sign-in began if a validated return target was stashed
|
|
336
418
|
// (e.g. the builders subdomain bounced through here); else the hall.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// src/bongos/routes/github.js — GitHub repo list/create for the signed-in user, the
|
|
2
|
+
// server half of the onboarding wizard's repo picker (adopt) + auto-create (fresh).
|
|
3
|
+
// Task 1002099 / ADR 0143.
|
|
4
|
+
//
|
|
5
|
+
// These act on the user's OWN GitHub identity, so they live in core auth alongside
|
|
6
|
+
// the sign-in flow that captured the token — not the provisioning module (whose
|
|
7
|
+
// charter holds no tokens / calls no cloud API, ADR 0111 §2). They read the
|
|
8
|
+
// short-lived, secret-box-encrypted `public_repo` token the web callback stored
|
|
9
|
+
// (db.js github_oauth_tokens). Two "please reconnect / fall back" signals the wizard
|
|
10
|
+
// branches on:
|
|
11
|
+
// • no stored token → repo_scope_required (403) → prompt a repos sign-in
|
|
12
|
+
// • secret-box unconfigured → github_repos_unavailable (503) → manual repo entry
|
|
13
|
+
// Both are behind requireBuilder + the router-wide rate-limit middleware.
|
|
14
|
+
|
|
15
|
+
const express = require('express');
|
|
16
|
+
const auth = require('../auth');
|
|
17
|
+
const db = require('../db');
|
|
18
|
+
const secretBox = require('../secret-box');
|
|
19
|
+
const { listUserRepos, createUserRepo } = require('../github-repos');
|
|
20
|
+
const { validateOrRespond } = require('./_helpers');
|
|
21
|
+
|
|
22
|
+
// GitHub repo names: 1–100 chars of [A-Za-z0-9._-]. We validate strictly for a clean
|
|
23
|
+
// 400; anything GitHub still rejects surfaces as the 422→409 name_taken path.
|
|
24
|
+
const REPO_NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
|
|
25
|
+
|
|
26
|
+
// Resolve the caller's stored repo token, or send the right failure and return null.
|
|
27
|
+
// Centralizes the 503-vs-403 decision both endpoints share.
|
|
28
|
+
async function resolveRepoToken(req, res) {
|
|
29
|
+
if (!secretBox.isConfigured()) {
|
|
30
|
+
res.fail('github_repos_unavailable', { status: 503, message: 'Repo access is not configured on this server. Enter the repository manually.' });
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const row = await db.getGithubToken({ builderId: req.builder.id });
|
|
34
|
+
if (!row) {
|
|
35
|
+
res.fail('repo_scope_required', { status: 403, message: 'Grant repository access to list or create repos.' });
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return row.token;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = function buildGithubRouter() {
|
|
42
|
+
const router = express.Router();
|
|
43
|
+
|
|
44
|
+
// rank: any-builder — acts only on the caller's OWN GitHub identity (the token
|
|
45
|
+
// they granted). No rank gate beyond being a signed-in builder.
|
|
46
|
+
router.get('/github/repos', auth.requireBuilder, async (req, res) => {
|
|
47
|
+
const token = await resolveRepoToken(req, res);
|
|
48
|
+
if (!token) return;
|
|
49
|
+
try {
|
|
50
|
+
const q = typeof req.query.q === 'string' ? req.query.q.slice(0, 100) : '';
|
|
51
|
+
const repos = await listUserRepos(token, { q });
|
|
52
|
+
res.json({ repos });
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (err && err.status === 401) {
|
|
55
|
+
// The stored token was revoked/expired GitHub-side — drop it and re-prompt.
|
|
56
|
+
await db.deleteGithubToken({ builderId: req.builder.id }).catch(() => {});
|
|
57
|
+
return res.fail('repo_scope_required', { status: 403, message: 'Your GitHub authorization expired. Reconnect to list your repos.' });
|
|
58
|
+
}
|
|
59
|
+
console.error('[gds] GET /github/repos', err && err.message);
|
|
60
|
+
res.fail('github_error', { status: 502, message: 'GitHub could not be reached. Try again, or enter the repository manually.' });
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// rank: any-builder — creates a repo in the caller's OWN account.
|
|
65
|
+
router.post('/github/repos', auth.requireBuilder, async (req, res) => {
|
|
66
|
+
if (validateOrRespond(req, res, { name: { required: true, type: 'string', maxLength: 100 } })) return;
|
|
67
|
+
const name = String((req.body && req.body.name) || '').trim();
|
|
68
|
+
if (!REPO_NAME_RE.test(name)) {
|
|
69
|
+
return res.fail('bad_repo_name', { status: 400, message: 'Repository names use letters, numbers, and . _ - (1–100 chars).' });
|
|
70
|
+
}
|
|
71
|
+
const token = await resolveRepoToken(req, res);
|
|
72
|
+
if (!token) return;
|
|
73
|
+
try {
|
|
74
|
+
const repo = await createUserRepo(token, { name });
|
|
75
|
+
res.status(201).json({ repo });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err && err.code === 'name_taken') {
|
|
78
|
+
return res.fail('name_taken', { status: 409, message: 'You already have a repository with that name. Pick another, or adopt the existing one.' });
|
|
79
|
+
}
|
|
80
|
+
if (err && err.status === 401) {
|
|
81
|
+
await db.deleteGithubToken({ builderId: req.builder.id }).catch(() => {});
|
|
82
|
+
return res.fail('repo_scope_required', { status: 403, message: 'Your GitHub authorization expired. Reconnect to create the repo.' });
|
|
83
|
+
}
|
|
84
|
+
console.error('[gds] POST /github/repos', err && err.message);
|
|
85
|
+
res.fail('github_error', { status: 502, message: 'GitHub could not create the repo. Try again, or create it manually.' });
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return router;
|
|
90
|
+
};
|
|
@@ -32,7 +32,7 @@ const { corsPublicGet, asyncHandler } = require('./_helpers');
|
|
|
32
32
|
const { clientBranding } = require('../../branding');
|
|
33
33
|
const { clientModules, isModuleEnabled } = require('../../modules');
|
|
34
34
|
const { loadRepoInfo } = require('../repo-info');
|
|
35
|
-
const { authConfigured, openEnrollmentEnabled } = require('../auth');
|
|
35
|
+
const { authConfigured, openEnrollmentEnabled, loadIdpConfig } = require('../auth');
|
|
36
36
|
const { RANK_TIER } = require('../permission-path-check');
|
|
37
37
|
|
|
38
38
|
// The manifest format id — bumped when the SHAPE changes so a CLI can detect a
|
|
@@ -59,6 +59,7 @@ function buildInstanceManifest({
|
|
|
59
59
|
authConfigured: authOk,
|
|
60
60
|
openEnrollment,
|
|
61
61
|
devBoxEnabled,
|
|
62
|
+
idp = null,
|
|
62
63
|
} = {}) {
|
|
63
64
|
const b = branding || {};
|
|
64
65
|
const rankLabels = b.rankLabels || {};
|
|
@@ -89,8 +90,15 @@ function buildInstanceManifest({
|
|
|
89
90
|
ranks,
|
|
90
91
|
repo: repoOut,
|
|
91
92
|
auth: {
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
// Sign-in works when the instance has its OWN GitHub app (authOk) OR it
|
|
94
|
+
// federates through the hub (idp) — a hosted instance has no app of its own.
|
|
95
|
+
methods: (authOk || idp) ? AUTH_METHODS.slice() : [],
|
|
96
|
+
configured: !!authOk || !!idp,
|
|
97
|
+
// Federation hint (ADR 0141 §1): present only when this instance federates.
|
|
98
|
+
// origin + client_id are PUBLIC (like an OAuth client_id); the client SECRET
|
|
99
|
+
// is never projected here. The CLI reads this to route device-flow + web
|
|
100
|
+
// sign-in through the hub instead of expecting a local GitHub app.
|
|
101
|
+
idp: idp || null,
|
|
94
102
|
},
|
|
95
103
|
admission: { policy: openEnrollment ? 'open' : 'invite-only' },
|
|
96
104
|
devBox: { available: !!devBoxEnabled },
|
|
@@ -109,6 +117,7 @@ module.exports = function buildInstanceRouter() {
|
|
|
109
117
|
// err.message if branding/modules fail to resolve (misconfigured instance) —
|
|
110
118
|
// never leaks internals.
|
|
111
119
|
router.get('/instance', asyncHandler('GET /instance', (_req, res) => {
|
|
120
|
+
const idpCfg = loadIdpConfig();
|
|
112
121
|
res.json(buildInstanceManifest({
|
|
113
122
|
branding: clientBranding(),
|
|
114
123
|
modules: clientModules(),
|
|
@@ -117,6 +126,9 @@ module.exports = function buildInstanceRouter() {
|
|
|
117
126
|
authConfigured: authConfigured(),
|
|
118
127
|
openEnrollment: openEnrollmentEnabled(),
|
|
119
128
|
devBoxEnabled: isModuleEnabled('dev-box'),
|
|
129
|
+
// Project OUT the client secret — the manifest is public; only origin +
|
|
130
|
+
// client_id (both public) advertise federation.
|
|
131
|
+
idp: idpCfg ? { origin: idpCfg.origin, client_id: idpCfg.clientId } : null,
|
|
120
132
|
}));
|
|
121
133
|
}, { message: false, errorCode: 'instance_manifest_failed' }));
|
|
122
134
|
|
package/src/bongos/routes.js
CHANGED
|
@@ -20,6 +20,7 @@ const express = require('express');
|
|
|
20
20
|
const buildHealthzRouter = require('./routes/healthz');
|
|
21
21
|
const buildInstanceRouter = require('./routes/instance');
|
|
22
22
|
const buildAuthRouter = require('./routes/auth');
|
|
23
|
+
const buildGithubRouter = require('./routes/github');
|
|
23
24
|
const buildMeRouter = require('./routes/me');
|
|
24
25
|
const buildBuildersRouter = require('./routes/builders');
|
|
25
26
|
// The LIFECYCLE routes (tasks · claims · versions · done-when · goals ·
|
|
@@ -116,6 +117,7 @@ function buildGdsRouter() {
|
|
|
116
117
|
router.use(buildHealthzRouter());
|
|
117
118
|
router.use(buildInstanceRouter());
|
|
118
119
|
router.use(buildAuthRouter());
|
|
120
|
+
router.use(buildGithubRouter());
|
|
119
121
|
router.use(buildMeRouter());
|
|
120
122
|
router.use(buildBuildersRouter());
|
|
121
123
|
// tasks · claims · versions · done-when · goals · dependencies · analytics now
|
package/src/branding.js
CHANGED
|
@@ -44,6 +44,11 @@ const ENV_OVERRIDES = [
|
|
|
44
44
|
['OAUTH_ORIGIN', ['domains', 'oauthOrigin']],
|
|
45
45
|
['PROVISIONING_ORIGIN', ['domains', 'provisioningOrigin']],
|
|
46
46
|
['COOKIE_DOMAIN', ['domains', 'cookieDomain']],
|
|
47
|
+
// Cross-site identity federation flip (ADR 0141 §4). Provisioning sets
|
|
48
|
+
// <PREFIX>_AUTH_IDP=true per hosted instance (config/branding.neutral.json ships
|
|
49
|
+
// idp:false), so a co-tenant instance sharing the neutral pack still federates
|
|
50
|
+
// individually. 'bool'-coerced below — auth.js idpEnabled() reads it as strict === true.
|
|
51
|
+
['AUTH_IDP', ['auth', 'idp'], 'bool'],
|
|
47
52
|
];
|
|
48
53
|
|
|
49
54
|
const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
|
|
@@ -69,6 +74,12 @@ function deepFreeze(o) {
|
|
|
69
74
|
return o;
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
// This is the ONE structured host-identity contract. Project context (vision/world/visual-style
|
|
78
|
+
// prose in docs/project-context.md) is deliberately PROSE-ONLY — there is no parallel resolver for
|
|
79
|
+
// it, because nothing in the platform reads project context as data; it is written for agents to
|
|
80
|
+
// read. If a host value must be CODE-READ, add it HERE (branding), not a new project-context
|
|
81
|
+
// resolver. Decided in ADR 0142 (audit H11 / task 1002224).
|
|
82
|
+
//
|
|
72
83
|
// Pure resolver — all inputs injected, so tests drive it with zero I/O.
|
|
73
84
|
function resolveBranding({ neutral = {}, instance = {}, env = {} } = {}) {
|
|
74
85
|
// structuredClone so the merged result shares no nested references with the
|
|
@@ -76,9 +87,13 @@ function resolveBranding({ neutral = {}, instance = {}, env = {} } = {}) {
|
|
|
76
87
|
// the caller's input objects (deepMerge shallow-copies only the top level).
|
|
77
88
|
const resolved = structuredClone(deepMerge(neutral, instance));
|
|
78
89
|
const prefix = resolved.envPrefix || 'CLOUDBONGOS';
|
|
79
|
-
for (const [suffix, p] of ENV_OVERRIDES) {
|
|
90
|
+
for (const [suffix, p, coerce] of ENV_OVERRIDES) {
|
|
80
91
|
const v = env[`${prefix}_${suffix}`];
|
|
81
|
-
if (v
|
|
92
|
+
if (v === undefined || v === '') continue;
|
|
93
|
+
// Most overrides are string-valued (origins, cookie domain) and set verbatim. A
|
|
94
|
+
// 'bool'-tagged override (auth.idp) coerces the env string to a real boolean —
|
|
95
|
+
// idpEnabled() checks `=== true`, so an uncoerced "true" would never federate.
|
|
96
|
+
setPath(resolved, p, coerce === 'bool' ? /^(1|true|yes|on)$/i.test(String(v)) : v);
|
|
82
97
|
}
|
|
83
98
|
const missing = REQUIRED.filter((p) => {
|
|
84
99
|
const v = getPath(resolved, p);
|
package/src/instance-config.js
CHANGED
|
@@ -50,6 +50,21 @@ function envPrefix(b = safeBrand()) {
|
|
|
50
50
|
return b.envPrefix || FALLBACK_ENV_PREFIX;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// The default API base the CLI targets when there is no <PREFIX>_API_BASE env
|
|
54
|
+
// override AND no logged-in session api_base — i.e. the last-resort fallback.
|
|
55
|
+
// Resolves to THIS instance's own publicOrigin from the branding pack (OTB →
|
|
56
|
+
// https://amazonprimea.com, vanilla → http://localhost:3000), so the portable
|
|
57
|
+
// core never hard-codes another instance's URL (task 1002201 — pre-ADR-0139 the
|
|
58
|
+
// literal 'https://amazonprimea.com' was baked into cli-lib, sending every
|
|
59
|
+
// unconfigured consumer at the founder's game box). Single source of truth: every
|
|
60
|
+
// CLI base resolution routes through here. Last-resort http://localhost:3000 when
|
|
61
|
+
// branding can't be read at all — a local dev origin, never a foreign prod, so an
|
|
62
|
+
// unconfigured raw CLI fails safe rather than acting against someone else's DB.
|
|
63
|
+
function defaultApiBase(b = safeBrand()) {
|
|
64
|
+
const origin = b && b.domains && b.domains.publicOrigin;
|
|
65
|
+
return (typeof origin === 'string' && origin.trim()) || 'http://localhost:3000';
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
// Absolute path to the instance config dir — the WRITE target and first read.
|
|
54
69
|
function configHome(b = safeBrand()) {
|
|
55
70
|
return path.join(os.homedir(), '.config', configDirName(b));
|
|
@@ -186,6 +201,7 @@ module.exports = {
|
|
|
186
201
|
LEGACY_ENV_PREFIXES,
|
|
187
202
|
configDirName,
|
|
188
203
|
envPrefix,
|
|
204
|
+
defaultApiBase,
|
|
189
205
|
configHome,
|
|
190
206
|
configReadDirs,
|
|
191
207
|
configPath,
|
package/src/module-api.js
CHANGED
|
@@ -218,8 +218,27 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
218
218
|
// post-dates the published 1.17.3 tarball — a new user-facing feature set with no
|
|
219
219
|
// doorway export removal, so semver MINOR. NOT yet published to npm (that is the
|
|
220
220
|
// follow-on release task); amazonprimea still runs the vendored 1.17.3.
|
|
221
|
+
// 1.18.1 — go-live cut deployed to cloudbongos.com (owner-directed, 2026-07-14). Rolling
|
|
222
|
+
// 1.18.0 content PLUS the onboarding GitHub repo picker + auto-create (task 1002099,
|
|
223
|
+
// ADR 0143 — core auth: opt-in public_repo scope, encrypted short-TTL token store,
|
|
224
|
+
// GET/POST /github/repos) and the goal 1000041 cross-project hub features
|
|
225
|
+
// (platform-identity module, ADR 0141). Adds no doorway export → semver PATCH.
|
|
226
|
+
// 1.18.2 — go-live cut for cloudbongos.com (owner-directed, 2026-07-14). Rolling 1.18.1
|
|
227
|
+
// content PLUS the /create onboarding repo-picker rendering fix (task 1002228):
|
|
228
|
+
// the Home step's `hidden` attribute was defeated by author display rules
|
|
229
|
+
// (.field/.row2), so the adopt-picker / fresh-name / manual sub-panels all showed
|
|
230
|
+
// at once, and #ghConnect never appeared (toggled via `hidden` instead of its .on
|
|
231
|
+
// class). A CSS/markup-only fix in public-landing/create.html. No doorway export
|
|
232
|
+
// change → semver PATCH.
|
|
233
|
+
// 1.18.3 — package-core .gitignore pin fix (task 1002229, idea 1000543). `npm install` never
|
|
234
|
+
// materializes .gitignore/.npmignore/.npmrc into an installed package, but package-core
|
|
235
|
+
// pinned a tracked .gitignore in tree_sha256 → every vendored `bongos upgrade` pin-verify
|
|
236
|
+
// failed with `missing installed file: .gitignore` and auto-rolled-back (the workaround
|
|
237
|
+
// was --skip-pin-verify, which the unattended auto-upgrade subscription cannot pass).
|
|
238
|
+
// package-core now drops npm-stripped files from the manifest + pin so it describes exactly
|
|
239
|
+
// what npm installs. Build-tool fix, no doorway export change → semver PATCH.
|
|
221
240
|
// ---------------------------------------------------------------------------
|
|
222
|
-
const CORE_VERSION = '1.18.
|
|
241
|
+
const CORE_VERSION = '1.18.3'; // package-core pin honesty for vendored upgrades (task 1002229 / idea 1000543); see changelog above
|
|
223
242
|
|
|
224
243
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
225
244
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -245,6 +264,15 @@ module.exports = {
|
|
|
245
264
|
// tokens. A module's own bootstrap routes flag themselves; everything else
|
|
246
265
|
// stays closed to box tokens (the ADR 0016 trust boundary).
|
|
247
266
|
allowBoxScope: auth.allowBoxScope,
|
|
267
|
+
// resolveSession(req) — resolve the request to a live session row (or null)
|
|
268
|
+
// WITHOUT the 401-or-pass behaviour of requireBuilder. A module route that
|
|
269
|
+
// must make its own auth decision — e.g. a browser page that REDIRECTS to
|
|
270
|
+
// sign-in on a missing session rather than returning JSON 401 (the
|
|
271
|
+
// platform-identity /sso/authorize leg, ADR 0141) — needs the raw resolve.
|
|
272
|
+
// Returns the same shape lookupSession does (builder_id, github_id,
|
|
273
|
+
// github_login, display_name, avatar_url, rank, source, status); rank is
|
|
274
|
+
// still never an authority a module should trust for cross-instance identity.
|
|
275
|
+
resolveSession: (...a) => auth.resolveSession(...a),
|
|
248
276
|
|
|
249
277
|
// --- data — the shared Postgres pool. A module owns its namespaced tables
|
|
250
278
|
// (<key>_*) via its own migrations (BV1.R46); it reaches them through this.
|