@bongos/core 1.21.94 → 1.21.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +45 -35
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +8 -0
- package/clients/bongos-client/index.cjs +8 -0
- package/clients/bongos-client/index.d.ts +13 -3
- package/clients/bongos-client/index.mjs +8 -0
- package/docs/api/openapi.json +181 -7
- package/docs/api-reference.md +9 -5
- package/docs/copy-inventory.md +367 -358
- package/docs/copy-registry.json +536 -425
- package/docs/design/project-startup-direction.md +17 -1
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +351 -348
- package/modules/provisioning/github-setup.js +27 -5
- package/modules/provisioning/routes/callback-page.js +20 -10
- package/modules/provisioning/routes/github-setup.js +111 -2
- package/modules/provisioning/routes/provisioning.js +29 -4
- package/modules/public-landing/public/projects.html +367 -24
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/src/bongos/github-repos.js +129 -0
- package/src/bongos/routes/github.js +71 -5
- package/src/module-api.js +1 -1
- package/tests/github_handback_any_device.mjs +340 -0
- package/tests/github_org_account.mjs +378 -0
- package/tests/projects_hub.mjs +6 -2
- package/tests/provisioning_callback_page.mjs +3 -2
- package/tests/provisioning_github_app_routes.mjs +15 -10
- package/tests/wizard_draft_resume.mjs +4 -0
- package/tests/wizard_github_feedback.mjs +2 -1
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.96",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.96",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.96",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -9394,5 +9394,17 @@
|
|
|
9394
9394
|
"id": "1004649",
|
|
9395
9395
|
"text": "Connecting GitHub to a new project is now one trip. The founder clicks \"Connect GitHub\", approves the project's app on GitHub, and GitHub carries straight on to installing it on their repo (already picked when the repo is publ"
|
|
9396
9396
|
}
|
|
9397
|
+
],
|
|
9398
|
+
"1.21.95": [
|
|
9399
|
+
{
|
|
9400
|
+
"id": "1004650",
|
|
9401
|
+
"text": "Connecting GitHub no longer depends on staying in one browser tab. After you approve on GitHub, even on your phone or in another window, you land back on your own project's GitHub checklist. Our server remembers which project"
|
|
9402
|
+
}
|
|
9403
|
+
],
|
|
9404
|
+
"1.21.96": [
|
|
9405
|
+
{
|
|
9406
|
+
"id": "1004651",
|
|
9407
|
+
"text": "Founders can now put a new project under a GitHub organisation, not only their own account. When connecting GitHub, they choose \"On my account\" or \"On an organisation\". Their organisations are listed; one they only belong to ("
|
|
9408
|
+
}
|
|
9397
9409
|
]
|
|
9398
9410
|
}
|
|
@@ -103,6 +103,134 @@ async function createUserRepo(token, { name, fetchImpl = fetch } = {}) {
|
|
|
103
103
|
throw new GithubApiError(`GitHub create-repo failed (${res.status})`, { status: res.status, code: 'github_error' });
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// ---- organisations (task 1004651) ----
|
|
107
|
+
// A founder can put the project under a GitHub organisation instead of their personal
|
|
108
|
+
// account: the empty repo (createOrgRepo) and the project's sign-in app (the manifest
|
|
109
|
+
// flow's /organizations/<org>/settings/apps/new, built by the provisioning module).
|
|
110
|
+
//
|
|
111
|
+
// THE SCOPE LIMIT, said once. The hub's sign-in asks for `read:user repo` (ADR 0155/0364)
|
|
112
|
+
// and not `read:org`, so GitHub may refuse the membership reads below for this token.
|
|
113
|
+
// Widening the sign-in scope is auth-config.js's call, not this file's, so every read
|
|
114
|
+
// here DEGRADES rather than fails: the role-carrying membership list when GitHub allows
|
|
115
|
+
// it, else the organisations the founder's repos sit in (role unknown), and always a
|
|
116
|
+
// typed name the founder can have checked. A role we could not read is said as such,
|
|
117
|
+
// never guessed as "owner", and GitHub's own page still refuses a non-owner.
|
|
118
|
+
|
|
119
|
+
// A GitHub login: 1-39 chars, letters/digits and single inner hyphens. The gate every
|
|
120
|
+
// org name passes before it reaches a GitHub URL (the page's copy is never trusted).
|
|
121
|
+
const GITHUB_LOGIN_RE = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
|
|
122
|
+
function validGithubLogin(s) { return typeof s === 'string' && GITHUB_LOGIN_RE.test(s); }
|
|
123
|
+
|
|
124
|
+
// GitHub's refusal when the org restricts which OAuth apps may act for it. Matched on
|
|
125
|
+
// the message because GitHub gives it no code of its own.
|
|
126
|
+
function isAppRestricted(body) {
|
|
127
|
+
return /OAuth App access restrictions|third-party application access/i.test(String((body && body.message) || ''));
|
|
128
|
+
}
|
|
129
|
+
async function readBody(res) {
|
|
130
|
+
try { return await res.json(); } catch (_) { return {}; }
|
|
131
|
+
}
|
|
132
|
+
function unauthorized() {
|
|
133
|
+
return new GithubApiError('GitHub rejected the stored token', { status: 401, code: 'github_unauthorized' });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// One membership → what the page shows. Only an ACTIVE admin (an org owner) can make
|
|
137
|
+
// an app on the org; anyone else gets the reason in words. role null = unknown. PURE.
|
|
138
|
+
function orgAccess({ login, role = null, state = null }) {
|
|
139
|
+
if (role === 'admin' && state === 'active') return { login, role, can_create_apps: true, reason: null };
|
|
140
|
+
if (role && state === 'pending') {
|
|
141
|
+
return { login, role, can_create_apps: false, reason: `Your invitation to ${login} is still waiting. Accept it on GitHub first.` };
|
|
142
|
+
}
|
|
143
|
+
if (role) {
|
|
144
|
+
return { login, role, can_create_apps: false, reason: `You're a member of ${login}, not an owner. Only its owners can make apps there.` };
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
login, role: null, can_create_apps: null,
|
|
148
|
+
reason: `GitHub didn't tell us your role in ${login}. Only its owners can make the app there; if you aren't one, GitHub will say so on its own page.`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// The founder's organisations: { orgs: [orgAccess...], listed: 'memberships'|'repos'|'none' }.
|
|
153
|
+
// 'memberships' carries real roles; 'repos' is the fallback for a token GitHub will not
|
|
154
|
+
// show memberships to (the orgs the founder's repos live in, roles unknown); 'none' means
|
|
155
|
+
// GitHub showed neither, and the page offers only the typed name. Sorted by login.
|
|
156
|
+
async function listUserOrgs(token, { fetchImpl = fetch } = {}) {
|
|
157
|
+
const byLogin = (a, b) => a.login.toLowerCase().localeCompare(b.login.toLowerCase());
|
|
158
|
+
const res = await fetchImpl(`${GH_API}/user/memberships/orgs?per_page=${PER_PAGE}`, { headers: ghHeaders(token) });
|
|
159
|
+
if (res.status === 401) throw unauthorized();
|
|
160
|
+
if (res.ok) {
|
|
161
|
+
const rows = await readBody(res);
|
|
162
|
+
const orgs = (Array.isArray(rows) ? rows : [])
|
|
163
|
+
.map((m) => ({ login: m && m.organization && m.organization.login, role: m && m.role, state: m && m.state }))
|
|
164
|
+
.filter((m) => validGithubLogin(m.login))
|
|
165
|
+
.map(orgAccess)
|
|
166
|
+
.sort(byLogin);
|
|
167
|
+
return { orgs, listed: 'memberships' };
|
|
168
|
+
}
|
|
169
|
+
// No membership read for this token: the organisations its repos sit in. One page,
|
|
170
|
+
// a hint list and never a gate; the typed name covers the rest.
|
|
171
|
+
const r2 = await fetchImpl(`${GH_API}/user/repos?per_page=${PER_PAGE}&sort=updated&affiliation=organization_member`, { headers: ghHeaders(token) });
|
|
172
|
+
if (r2.status === 401) throw unauthorized();
|
|
173
|
+
if (!r2.ok) return { orgs: [], listed: 'none' };
|
|
174
|
+
const repos = await readBody(r2);
|
|
175
|
+
const seen = new Map();
|
|
176
|
+
for (const r of Array.isArray(repos) ? repos : []) {
|
|
177
|
+
const o = r && r.owner;
|
|
178
|
+
if (o && o.type === 'Organization' && validGithubLogin(o.login) && !seen.has(o.login.toLowerCase())) {
|
|
179
|
+
seen.set(o.login.toLowerCase(), orgAccess({ login: o.login }));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return { orgs: [...seen.values()].sort(byLogin), listed: 'repos' };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Check ONE organisation the founder typed: its orgAccess, or a typed org_not_found when
|
|
186
|
+
// GitHub has no organisation by that name (a personal account's login included). The
|
|
187
|
+
// membership read answers with the role when this token may read it; otherwise the
|
|
188
|
+
// public org read proves the org exists and the role stays unknown.
|
|
189
|
+
async function getOrgAccess(token, { org, fetchImpl = fetch } = {}) {
|
|
190
|
+
if (!validGithubLogin(org)) throw new GithubApiError('Not a GitHub organisation name', { status: 400, code: 'bad_org' });
|
|
191
|
+
const res = await fetchImpl(`${GH_API}/user/memberships/orgs/${encodeURIComponent(org)}`, { headers: ghHeaders(token) });
|
|
192
|
+
if (res.status === 401) throw unauthorized();
|
|
193
|
+
if (res.ok) {
|
|
194
|
+
const m = await readBody(res);
|
|
195
|
+
const login = m && m.organization && validGithubLogin(m.organization.login) ? m.organization.login : org;
|
|
196
|
+
return orgAccess({ login, role: (m && m.role) || null, state: (m && m.state) || null });
|
|
197
|
+
}
|
|
198
|
+
const r2 = await fetchImpl(`${GH_API}/orgs/${encodeURIComponent(org)}`, { headers: ghHeaders(token) });
|
|
199
|
+
if (r2.status === 401) throw unauthorized();
|
|
200
|
+
if (r2.status === 404) throw new GithubApiError('GitHub has no organisation by that name', { status: 404, code: 'org_not_found' });
|
|
201
|
+
if (!r2.ok) throw new GithubApiError(`GitHub /orgs read failed (${r2.status})`, { status: r2.status, code: 'github_error' });
|
|
202
|
+
const o = await readBody(r2);
|
|
203
|
+
if (o && o.type && o.type !== 'Organization') throw new GithubApiError('That name is a personal account', { status: 404, code: 'org_not_found' });
|
|
204
|
+
return orgAccess({ login: o && validGithubLogin(o.login) ? o.login : org });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Create an EMPTY repo under an organisation: the org twin of createUserRepo, with the
|
|
208
|
+
// same fixed body (private, no first commit; ADR 0370). GitHub decides who may, and a
|
|
209
|
+
// refusal is typed so the page can say WHY:
|
|
210
|
+
// org_app_restricted - the org limits which OAuth apps may act for it
|
|
211
|
+
// org_forbidden - this member may not create repos there
|
|
212
|
+
// org_not_found - no such org, or this account cannot see it
|
|
213
|
+
async function createOrgRepo(token, { org, name, fetchImpl = fetch } = {}) {
|
|
214
|
+
if (!validGithubLogin(org)) throw new GithubApiError('Not a GitHub organisation name', { status: 400, code: 'bad_org' });
|
|
215
|
+
const res = await fetchImpl(`${GH_API}/orgs/${encodeURIComponent(org)}/repos`, {
|
|
216
|
+
method: 'POST',
|
|
217
|
+
headers: ghHeaders(token, { 'Content-Type': 'application/json' }),
|
|
218
|
+
body: JSON.stringify({ name, private: true, auto_init: false }),
|
|
219
|
+
});
|
|
220
|
+
if (res.status === 201) {
|
|
221
|
+
const r = await res.json();
|
|
222
|
+
return { full_name: r.full_name, html_url: r.html_url };
|
|
223
|
+
}
|
|
224
|
+
if (res.status === 401) throw unauthorized();
|
|
225
|
+
if (res.status === 422) throw new GithubApiError('A repository with that name already exists in the organisation', { status: 422, code: 'name_taken' });
|
|
226
|
+
if (res.status === 403) {
|
|
227
|
+
const body = await readBody(res);
|
|
228
|
+
throw new GithubApiError('GitHub refused the org repo', { status: 403, code: isAppRestricted(body) ? 'org_app_restricted' : 'org_forbidden' });
|
|
229
|
+
}
|
|
230
|
+
if (res.status === 404) throw new GithubApiError('GitHub answered 404 for that organisation', { status: 404, code: 'org_not_found' });
|
|
231
|
+
throw new GithubApiError(`GitHub create-org-repo failed (${res.status})`, { status: res.status, code: 'github_error' });
|
|
232
|
+
}
|
|
233
|
+
|
|
106
234
|
// Read one repo as GitHub answers it FOR THIS TOKEN — the manage page's
|
|
107
235
|
// visibility read (task 1003045). GitHub's own 404 covers both "no such repo"
|
|
108
236
|
// and "no access", which is exactly the no-leak shape the caller wants to
|
|
@@ -252,6 +380,7 @@ async function deleteDeployKey(token, { owner, name, keyId, fetchImpl = fetch }
|
|
|
252
380
|
|
|
253
381
|
module.exports = {
|
|
254
382
|
listUserRepos, createUserRepo, getUserRepo, makeRepoPublic, makeRepoPrivate,
|
|
383
|
+
listUserOrgs, getOrgAccess, createOrgRepo, validGithubLogin,
|
|
255
384
|
listDeployKeys, addDeployKey, deleteDeployKey,
|
|
256
385
|
GithubApiError, MAX_PAGES,
|
|
257
386
|
};
|
|
@@ -19,12 +19,13 @@ const express = require('express');
|
|
|
19
19
|
const auth = require('../auth');
|
|
20
20
|
const db = require('../db');
|
|
21
21
|
const secretBox = require('../secret-box');
|
|
22
|
-
const { listUserRepos, createUserRepo } = require('../github-repos');
|
|
22
|
+
const { listUserRepos, createUserRepo, listUserOrgs, getOrgAccess, createOrgRepo, validGithubLogin } = require('../github-repos');
|
|
23
23
|
// Namespace require for the visibility helpers (task 1003045): the route tests
|
|
24
24
|
// stub these on the cached module object, which a load-time destructure would
|
|
25
25
|
// pin to the real implementations.
|
|
26
26
|
const githubRepos = require('../github-repos');
|
|
27
27
|
const { validateOrRespond } = require('./_helpers');
|
|
28
|
+
const log = require('../logger').logger.child({ src: 'github' });
|
|
28
29
|
|
|
29
30
|
// GitHub repo names: 1–100 chars of [A-Za-z0-9._-]. We validate strictly for a clean
|
|
30
31
|
// 400; anything GitHub still rejects surfaces as the 422→409 name_taken path.
|
|
@@ -33,6 +34,14 @@ const REPO_NAME_RE = /^[A-Za-z0-9._-]{1,100}$/;
|
|
|
33
34
|
// hyphens) over a repo name. Strict for a clean 400; GitHub still owns the truth.
|
|
34
35
|
const REPO_REF_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})?\/[A-Za-z0-9._-]{1,100}$/;
|
|
35
36
|
|
|
37
|
+
// The two org refusals the org routes share (task 1004651).
|
|
38
|
+
function badOrg(res) {
|
|
39
|
+
return res.fail('bad_org', { status: 400, message: 'That isn’t a GitHub organisation name: letters, numbers and single dashes, up to 39.' });
|
|
40
|
+
}
|
|
41
|
+
function orgNotFound(res, org) {
|
|
42
|
+
return res.fail('org_not_found', { status: 404, message: `GitHub didn’t show your account an organisation called ${org}.` });
|
|
43
|
+
}
|
|
44
|
+
|
|
36
45
|
// Parse + validate the owner/name ref, or send a 400 and return null.
|
|
37
46
|
function parseRepoRef(raw, res) {
|
|
38
47
|
const ref = String(raw || '').trim();
|
|
@@ -92,22 +101,79 @@ module.exports = function buildGithubRouter() {
|
|
|
92
101
|
}
|
|
93
102
|
});
|
|
94
103
|
|
|
95
|
-
// rank: any-builder —
|
|
104
|
+
// rank: any-builder — the caller's OWN GitHub organisations (task 1004651), for the
|
|
105
|
+
// project page's "on an organisation" choice. Each row says whether the caller can make
|
|
106
|
+
// an app there and, when not, why. `listed` says how much GitHub showed this token
|
|
107
|
+
// (github-repos.listUserOrgs): the page always offers a typed name beside the list.
|
|
108
|
+
router.get('/github/orgs', auth.requireBuilder, async (req, res) => {
|
|
109
|
+
const token = await resolveRepoToken(req, res);
|
|
110
|
+
if (!token) return;
|
|
111
|
+
try {
|
|
112
|
+
res.json(await listUserOrgs(token));
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (err && err.status === 401) {
|
|
115
|
+
await db.deleteGithubToken({ builderId: req.builder.id }).catch(() => {});
|
|
116
|
+
return res.fail('repo_scope_required', { status: 403, message: 'Your GitHub authorization expired. Reconnect to list your organisations.' });
|
|
117
|
+
}
|
|
118
|
+
log.error('[gds] GET /github/orgs', err && err.message);
|
|
119
|
+
res.fail('github_error', { status: 502, message: 'GitHub could not be reached. Try again in a moment.' });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// rank: any-builder — checks ONE organisation name the caller typed, as their own
|
|
124
|
+
// GitHub credential sees it (task 1004651). The name is checked against GitHub's login
|
|
125
|
+
// rules before it reaches a URL.
|
|
126
|
+
router.get('/github/orgs/:org', auth.requireBuilder, async (req, res) => {
|
|
127
|
+
const org = String(req.params.org || '').trim();
|
|
128
|
+
if (!validGithubLogin(org)) return badOrg(res);
|
|
129
|
+
const token = await resolveRepoToken(req, res);
|
|
130
|
+
if (!token) return;
|
|
131
|
+
try {
|
|
132
|
+
res.json({ org: await getOrgAccess(token, { org }) });
|
|
133
|
+
} catch (err) {
|
|
134
|
+
if (err && err.status === 401) {
|
|
135
|
+
await db.deleteGithubToken({ builderId: req.builder.id }).catch(() => {});
|
|
136
|
+
return res.fail('repo_scope_required', { status: 403, message: 'Your GitHub authorization expired. Reconnect to check the organisation.' });
|
|
137
|
+
}
|
|
138
|
+
if (err && err.code === 'org_not_found') return orgNotFound(res, org);
|
|
139
|
+
log.error('[gds] GET /github/orgs/:org', err && err.message);
|
|
140
|
+
res.fail('github_error', { status: 502, message: 'GitHub could not be reached. Try again in a moment.' });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// rank: any-builder — creates a repo in the caller's OWN account, or (task 1004651)
|
|
145
|
+
// under an organisation they name in `org`. GitHub decides whether they may; its
|
|
146
|
+
// refusals come back as 409s with the reason, never as a 403 (the page reads a 403 as
|
|
147
|
+
// "sign in again").
|
|
96
148
|
router.post('/github/repos', auth.requireBuilder, async (req, res) => {
|
|
97
|
-
if (validateOrRespond(req, res, {
|
|
149
|
+
if (validateOrRespond(req, res, {
|
|
150
|
+
name: { required: true, type: 'string', maxLength: 100 },
|
|
151
|
+
org: { type: 'string', maxLength: 39 },
|
|
152
|
+
})) return;
|
|
98
153
|
const name = String((req.body && req.body.name) || '').trim();
|
|
99
154
|
if (!REPO_NAME_RE.test(name)) {
|
|
100
155
|
return res.fail('bad_repo_name', { status: 400, message: 'Repository names use letters, numbers, and . _ - (1–100 chars).' });
|
|
101
156
|
}
|
|
157
|
+
const org = req.body && req.body.org != null ? String(req.body.org).trim() : '';
|
|
158
|
+
if (org && !validGithubLogin(org)) return badOrg(res);
|
|
102
159
|
const token = await resolveRepoToken(req, res);
|
|
103
160
|
if (!token) return;
|
|
104
161
|
try {
|
|
105
|
-
const repo = await createUserRepo(token, { name });
|
|
162
|
+
const repo = org ? await createOrgRepo(token, { org, name }) : await createUserRepo(token, { name });
|
|
106
163
|
res.status(201).json({ repo });
|
|
107
164
|
} catch (err) {
|
|
108
165
|
if (err && err.code === 'name_taken') {
|
|
109
|
-
return res.fail('name_taken', { status: 409, message:
|
|
166
|
+
return res.fail('name_taken', { status: 409, message: org
|
|
167
|
+
? `${org} already has a repository with that name. Pick another, or adopt the existing one.`
|
|
168
|
+
: 'You already have a repository with that name. Pick another, or adopt the existing one.' });
|
|
169
|
+
}
|
|
170
|
+
if (org && err && err.code === 'org_app_restricted') {
|
|
171
|
+
return res.fail('org_app_restricted', { status: 409, message: `${org} only lets apps its owners have approved act for it. An owner can approve this platform in the organisation's settings (Third-party access), or create the repo yourself.` });
|
|
172
|
+
}
|
|
173
|
+
if (org && err && err.code === 'org_forbidden') {
|
|
174
|
+
return res.fail('org_forbidden', { status: 409, message: `GitHub didn't let your account create a repository in ${org}. Ask one of its owners, or put the project on your own account.` });
|
|
110
175
|
}
|
|
176
|
+
if (org && err && err.code === 'org_not_found') return orgNotFound(res, org);
|
|
111
177
|
if (err && err.status === 401) {
|
|
112
178
|
await db.deleteGithubToken({ builderId: req.builder.id }).catch(() => {});
|
|
113
179
|
return res.fail('repo_scope_required', { status: 403, message: 'Your GitHub authorization expired. Reconnect to create the repo.' });
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.96'; // 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');
|