@bongos/core 1.25.19 → 1.25.21
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 +237 -112
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +12 -0
- package/clients/bongos-client/index.cjs +12 -0
- package/clients/bongos-client/index.d.ts +26 -9
- package/clients/bongos-client/index.mjs +12 -0
- package/docs/adr/0406-every-project-gets-a-sandbox-that-follows-main-and-live-moves-only-when-a-person-promotes.md +10 -0
- package/docs/adr/0407-a-project-grant-is-made-by-the-owner-not-witnessed-at-sign-in.md +3 -1
- package/docs/api/openapi.json +444 -37
- package/docs/api-reference.md +11 -5
- package/docs/copy-inventory.md +223 -221
- package/docs/copy-registry.json +262 -244
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +348 -347
- package/docs/project-context.template.md +1 -1
- package/modules/front-door-guide/handoff.js +120 -0
- package/modules/front-door-guide/module.json +1 -1
- package/modules/front-door-guide/routes/front-door-guide-plan.js +24 -0
- package/modules/government/board.js +8 -7
- package/modules/government/charter-advice.js +10 -3
- package/modules/government/charter.js +88 -4
- package/modules/government/migrations/government_039_vote_cast_via_filed.sql +22 -0
- package/modules/government/routes/government.js +12 -12
- package/modules/government/sole-member.js +75 -0
- package/modules/hall-ui/public/ai-studio.html +1 -1
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/genesis-home.js +35 -9
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/home-city-draw.js +14 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/market-watch-item.html +1 -1
- package/modules/hall-ui/public/market-watch-rules.html +1 -1
- package/modules/hall-ui/public/market-watch.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/money.html +1 -1
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/settings.html +1 -1
- package/modules/hall-ui/public/shell.js +32 -20
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +9 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/lifecycle/home-city.js +17 -3
- package/modules/onboarding/founding-mode.js +45 -1
- package/modules/onboarding/genesis-home.js +16 -2
- package/modules/onboarding/guide-handoff.js +109 -0
- package/modules/onboarding/routes/genesis-work.js +9 -5
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/project-standing.js +49 -0
- package/modules/platform-identity/routes/my-projects.js +8 -0
- package/modules/provisioning/grants.js +126 -0
- package/modules/provisioning/guide-handoff.js +65 -0
- package/modules/provisioning/migrations/provisioning_038_render_promote.sql +43 -0
- package/modules/provisioning/migrations/provisioning_039_project_grants.sql +40 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/project-refusal.js +107 -0
- package/modules/provisioning/provisioning.js +4 -4
- package/modules/provisioning/render-lookup.js +36 -1
- package/modules/provisioning/render-promote.js +80 -0
- package/modules/provisioning/route-helpers.js +70 -16
- package/modules/provisioning/routes/env-manifest.js +21 -5
- package/modules/provisioning/routes/grants.js +141 -0
- package/modules/provisioning/routes/provisioning.js +12 -9
- package/modules/provisioning/routes/render-standup.js +82 -3
- package/modules/public-landing/public/projects.html +178 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +28 -0
- package/scripts/bongos/genesis.js +11 -1
- package/scripts/bongos/government-walk-sole.js +41 -0
- package/scripts/bongos/government-walk.js +15 -16
- package/scripts/bongos/provision-render-common.js +48 -0
- package/scripts/bongos/provision-render-promote.js +93 -0
- package/scripts/bongos/provision-render-sandbox.js +14 -2
- package/scripts/bongos/provision-render.js +4 -36
- package/scripts/bongos/provision.js +1 -0
- package/scripts/bongos/render-api.js +17 -2
- package/scripts/gds/government-walk-sole.js +6 -0
- package/scripts/gds/provision-render-common.js +6 -0
- package/scripts/gds/provision-render-promote.js +6 -0
- package/src/bongos/founding-page-gate.js +67 -0
- package/src/bongos/serve-internal.js +3 -0
- package/src/branding.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/fleet_cost_view.mjs +2 -0
- package/tests/founding_mode.mjs +84 -8
- package/tests/founding_page_gate.mjs +105 -0
- package/tests/front_door_guide_existing.mjs +1 -0
- package/tests/genesis_government.mjs +5 -1
- package/tests/genesis_home.mjs +41 -0
- package/tests/government_charter_advice.mjs +49 -0
- package/tests/government_charter_forms.mjs +14 -3
- package/tests/government_seed.mjs +1 -0
- package/tests/government_sole_member.mjs +153 -0
- package/tests/guide_handoff.mjs +322 -0
- package/tests/home_city.mjs +52 -0
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/project_catalog_routes.mjs +2 -0
- package/tests/provision_settings_apply.mjs +2 -1
- package/tests/provisioning_env_manifest.mjs +4 -0
- package/tests/provisioning_env_manifest_edit_delete.mjs +2 -0
- package/tests/provisioning_project_grants.mjs +458 -0
- package/tests/provisioning_project_owner_gate.mjs +3 -1
- package/tests/provisioning_project_refusal.mjs +316 -0
- package/tests/provisioning_render_route.mjs +1 -1
- package/tests/provisioning_restart.mjs +2 -0
- package/tests/provisioning_settings.mjs +2 -0
- package/tests/provisioning_settings_apply.mjs +1 -1
- package/tests/provisioning_settings_env.mjs +2 -2
- package/tests/provisioning_teardown_intent.mjs +2 -2
- package/tests/render_promote.mjs +309 -0
- package/tests/render_sandbox.mjs +9 -4
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
const fs = require('fs');
|
|
50
50
|
const os = require('os');
|
|
51
51
|
const path = require('path');
|
|
52
|
+
const { decidedOf, adoptedByFiling, soleMemberRows } = require('./government-walk-sole'); // task 1005130: a board of one decides by filing
|
|
52
53
|
|
|
53
54
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
54
55
|
const DOC_PATH = path.join(ROOT, 'docs', 'governance', 'government-walk.md');
|
|
@@ -262,9 +263,9 @@ function makeStore(catalog) {
|
|
|
262
263
|
}
|
|
263
264
|
return item.opened_by ?? null;
|
|
264
265
|
},
|
|
265
|
-
async insertBoardVote({ itemId, voterId, direction, reasonMd = null, sectionKey = null }) {
|
|
266
|
+
async insertBoardVote({ itemId, voterId, direction, reasonMd = null, sectionKey = null, castVia = 'ballot' }) {
|
|
266
267
|
if (s.votes.some((v) => v.item_id === String(itemId) && v.voter_id === String(voterId))) return null;
|
|
267
|
-
const row = { id: next(), item_id: String(itemId), voter_id: String(voterId), direction, reason_md: reasonMd, section_key: sectionKey, cast_at: tick() };
|
|
268
|
+
const row = { id: next(), item_id: String(itemId), voter_id: String(voterId), direction, reason_md: reasonMd, section_key: sectionKey, cast_via: castVia, cast_at: tick() };
|
|
268
269
|
s.votes.push(row);
|
|
269
270
|
return copy(row);
|
|
270
271
|
},
|
|
@@ -774,20 +775,17 @@ async function runWalk() {
|
|
|
774
775
|
return { got: `${r.status}, stage ${stage}`, pass: r.status === 200 && stage === 'decide' };
|
|
775
776
|
});
|
|
776
777
|
let charterItem = null;
|
|
777
|
-
await check(`Deciding with ${CHOSEN} saves the plan and files the pick as a charter amendment`, 'plan saved, charter
|
|
778
|
+
await check(`Deciding with ${CHOSEN} saves the plan and files the pick as a charter amendment`, 'plan saved, charter adopted', async () => {
|
|
778
779
|
const r = await call(FOUNDER, 'POST', '/founding/plan', { government: CHOSEN });
|
|
779
780
|
const c = (r.json && r.json.charter) || {};
|
|
780
781
|
charterItem = c.item_id || null;
|
|
781
|
-
return { got: `${r.status}, charter ${c.state}
|
|
782
|
+
return { got: `${r.status}, charter ${c.state}`, pass: r.status === 200 && c.state === 'adopted' && !!charterItem };
|
|
782
783
|
});
|
|
783
784
|
await check('Saving the plan moved the project to its first stage', 'stage scope', async () => {
|
|
784
785
|
const g = await genesis.readGenesis();
|
|
785
786
|
return { got: `stage ${g && g.stage}`, pass: !!g && g.stage === 'scope' };
|
|
786
787
|
});
|
|
787
|
-
await check('The founder\'s one yes ratifies the pick under the Monarchy it is born with', 'passed', async () => {
|
|
788
|
-
const r = await vote(FOUNDER, charterItem);
|
|
789
|
-
return { got: `${r.status} ${r.json && r.json.outcome}`, pass: r.status === 201 && r.json.outcome === 'passed' };
|
|
790
|
-
});
|
|
788
|
+
await check('The founder\'s one yes ratifies the pick under the Monarchy it is born with: as its only member, choosing it was that yes', 'passed; the founder\'s yes, cast by filing', async () => adoptedByFiling({ store, stateOf, itemId: charterItem, voterId: FOUNDER }));
|
|
791
789
|
await check('The constitution now reads as the government chosen at decide', `reads as ${CHOSEN}, no divergence`, async () => {
|
|
792
790
|
const c = await constitution();
|
|
793
791
|
const f = c && c.charter && c.charter.reads_as && c.charter.reads_as.form;
|
|
@@ -928,8 +926,8 @@ async function runWalk() {
|
|
|
928
926
|
const r = await call(FOUNDER, 'POST', '/government/board/decision-rules', { rules });
|
|
929
927
|
const itemId = r.json && r.json.item && r.json.item.id;
|
|
930
928
|
if (r.status !== 201 || !itemId) return { ok: false, why: `${r.status} ${code(r)}` };
|
|
931
|
-
const
|
|
932
|
-
return { ok:
|
|
929
|
+
const d = decidedOf(r); // the founder is the Monarchy's only member, so filing it is their one yes
|
|
930
|
+
return { ok: d.closed === true && d.outcome === 'passed', why: `filed ${d.outcome || 'undecided'}` };
|
|
933
931
|
}
|
|
934
932
|
const rowOf = async (area) => {
|
|
935
933
|
const c = await constitution();
|
|
@@ -1094,12 +1092,13 @@ async function walkNewMachinery({ call, code, vote, voteAll, check, section, sta
|
|
|
1094
1092
|
const s = await positionsPort().lawsForSession();
|
|
1095
1093
|
return { got: `${book.in_force} in force; ${s.inject ? 'a laws block' : 'no laws block'}`, pass: book.in_force === 0 && s.inject === false };
|
|
1096
1094
|
});
|
|
1097
|
-
await check('A law is proposed: filed as a laws amendment, and proposing writes nothing', '201, sitting open; still 0 in force', async () => {
|
|
1098
|
-
const r = await call(
|
|
1095
|
+
await check('A law is proposed by a builder the board does not seat: filed as a laws amendment, and proposing writes nothing', '201, sitting open; still 0 in force', async () => {
|
|
1096
|
+
const r = await call(BEN, 'POST', '/government/board/laws', { enact: [SHORT_LAW] });
|
|
1099
1097
|
lawItem = itemIdOf(r);
|
|
1100
1098
|
const book = await lawsInForce();
|
|
1101
1099
|
return { got: `${r.status}, sitting ${stateOf(lawItem)}; still ${book.in_force} in force`, pass: r.status === 201 && stateOf(lawItem) === 'open' && book.in_force === 0 };
|
|
1102
1100
|
});
|
|
1101
|
+
await soleMemberRows({ check, call, vote, code, store, stateOf, itemIdOf, ids: { EVE, BEN, FOUNDER_ID }, SHORT_LAW });
|
|
1103
1102
|
await check('Someone the board does not seat cannot pass it', '403 not_board_member', async () => {
|
|
1104
1103
|
const r = await vote(BEN, lawItem);
|
|
1105
1104
|
return { got: `${r.status} ${code(r)}`, pass: r.status === 403 && code(r) === 'not_board_member' };
|
|
@@ -1134,9 +1133,9 @@ async function walkNewMachinery({ call, code, vote, voteAll, check, section, sta
|
|
|
1134
1133
|
const r = await call(BEN, 'PATCH', '/me/interaction', { verbosity: 'concise' });
|
|
1135
1134
|
return { got: `${r.status}`, pass: r.status === 200 };
|
|
1136
1135
|
});
|
|
1137
|
-
await check('Repealed by
|
|
1136
|
+
await check('Repealed by the founder, the board\'s only member, whose filing is their one yes: the law leaves every session and unlocks the setting', 'passed; 0 in force; no laws block; 200', async () => {
|
|
1138
1137
|
const r = await call(FOUNDER_ID, 'POST', '/government/board/laws', { repeal: [lawId] });
|
|
1139
|
-
const v =
|
|
1138
|
+
const v = { json: { outcome: decidedOf(r).outcome } };
|
|
1140
1139
|
const book = await lawsInForce();
|
|
1141
1140
|
const s = await positionsPort().lawsForSession();
|
|
1142
1141
|
const w = await call(BEN, 'PATCH', '/me/interaction', { verbosity: 'detailed' });
|
|
@@ -1152,7 +1151,7 @@ async function walkNewMachinery({ call, code, vote, voteAll, check, section, sta
|
|
|
1152
1151
|
await check(`Strictness: ${from} to ${key} is one vote, and its locks bite`, `one sitting; passed; ${key} in force; exploratory wandering ${wander}`, async () => {
|
|
1153
1152
|
const r = await call(FOUNDER_ID, 'POST', '/government/board/presets', { preset: key });
|
|
1154
1153
|
const id = itemIdOf(r);
|
|
1155
|
-
const v =
|
|
1154
|
+
const v = { json: { outcome: decidedOf(r).outcome } }; // the only member's filing is their one yes
|
|
1156
1155
|
const p = await presetNow();
|
|
1157
1156
|
const w = await call(BEN, 'PATCH', '/me/wandering', { wandering: 3 });
|
|
1158
1157
|
const ws = `${w.status}${w.status === 200 ? '' : ` ${code(w)}`}`;
|
|
@@ -1162,7 +1161,7 @@ async function walkNewMachinery({ call, code, vote, voteAll, check, section, sta
|
|
|
1162
1161
|
await check('Every preset law is repealed by one more vote, so the book ends empty', 'passed; 0 in force; no preset', async () => {
|
|
1163
1162
|
const ids = ((await lawsInForce()).laws || []).map((l) => l.id);
|
|
1164
1163
|
const r = await call(FOUNDER_ID, 'POST', '/government/board/laws', { repeal: ids });
|
|
1165
|
-
const v =
|
|
1164
|
+
const v = { json: { outcome: decidedOf(r).outcome } }; // the only member's filing is their one yes
|
|
1166
1165
|
const book = await lawsInForce();
|
|
1167
1166
|
const p = await presetNow();
|
|
1168
1167
|
return { got: `${v.json && v.json.outcome}; ${book.in_force} in force; ${p.current ? `${p.current} in force` : 'no preset'}`, pass: book.in_force === 0 && p.current == null };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// provision-render-common.js — the pieces every Render runner leg shares: the client for a
|
|
3
|
+
// lent key, the one app_host writer, clearing the key, and an owner-readable reason
|
|
4
|
+
// (task 1005290; first written in provision-render.js by task 1004183).
|
|
5
|
+
//
|
|
6
|
+
// WHY ITS OWN FILE. The standup leg (provision-render.js), the sandbox leg
|
|
7
|
+
// (provision-render-sandbox.js) and the promote leg (provision-render-promote.js) all
|
|
8
|
+
// need these. Kept in the standup file, the sandbox leg reaching back for them made a
|
|
9
|
+
// require cycle (the standup file requires the sandbox file), which the fitness ratchet
|
|
10
|
+
// rightly refuses. Here, every leg depends on this file and on nothing of each other's.
|
|
11
|
+
|
|
12
|
+
const { RenderApi } = require('./render-api.js');
|
|
13
|
+
|
|
14
|
+
// A key waiting longer than this was lent for a run that never came. It is refused and
|
|
15
|
+
// cleared rather than spent late.
|
|
16
|
+
const KEY_TTL_MS = 30 * 60 * 1000;
|
|
17
|
+
|
|
18
|
+
function renderApiFor(deps, token, ownerId) {
|
|
19
|
+
if (deps.renderApiFactory) return deps.renderApiFactory({ token, ownerId });
|
|
20
|
+
return new RenderApi({ token, ownerId, dryRun: !deps.apply, fetchImpl: deps.fetchImpl, log: deps.log });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Merge `patch` into the row's app_host. The runner is the only writer while an intent
|
|
24
|
+
// runs, and the web tier only writes it when enqueuing, which the open-intent index
|
|
25
|
+
// serialises — so a merge needs no read-modify-write.
|
|
26
|
+
async function recordAppHost(db, instId, patch) {
|
|
27
|
+
await db.query(
|
|
28
|
+
`UPDATE provisioning_instances SET app_host = COALESCE(app_host, '{}'::jsonb) || $2::jsonb WHERE id = $1`,
|
|
29
|
+
[instId, JSON.stringify({ ...patch, updatedAt: new Date().toISOString() })]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function clearBorrowedKey(db, intentId) {
|
|
33
|
+
await db.query(`UPDATE provisioning_intents SET borrowed_key_enc = NULL WHERE id = $1`, [intentId]);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// The owner reads this reason on the setup screen (app-status.js oneSentence). A refusal
|
|
37
|
+
// from Render's API used to arrive as its raw line — "Render POST /services failed: …" —
|
|
38
|
+
// so an answer from Render is re-said as a sentence with Render's own reason kept whole
|
|
39
|
+
// (task 1004464). Anything else is already the leg's own words.
|
|
40
|
+
function plainReason(e) {
|
|
41
|
+
const msg = e && e.message ? e.message : String(e);
|
|
42
|
+
const said = e && e.status != null && e.body && typeof e.body.message === 'string' ? e.body.message.trim() : '';
|
|
43
|
+
if (said) return `Render would not set up the app: ${said}`;
|
|
44
|
+
const m = /^Render (?:GET|POST|PATCH|PUT|DELETE) \S+ failed: ([\s\S]+)$/.exec(msg);
|
|
45
|
+
return m ? `Render would not set up the app: ${m[1].trim()}` : msg;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { KEY_TTL_MS, renderApiFor, recordAppHost, clearBorrowedKey, plainReason };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// provision-render-promote.js — a person promotes what ran on the sandbox to the live app:
|
|
3
|
+
// the runner half of the `render-promote` intent (task 1005290; ADR 0406 §2, ADR 0327).
|
|
4
|
+
//
|
|
5
|
+
// WHY. Once a project has a sandbox, finished work lands on the sandbox only (it follows
|
|
6
|
+
// `main`), and the live Render service no longer deploys on every merge. The live app
|
|
7
|
+
// moves only here, when a person pressed promote and lent their Render key for that one
|
|
8
|
+
// action. That is the project law "Land only. Ask before going live" made physical: no
|
|
9
|
+
// ship, in Auto or Manual mode, reaches this file; only an owner's press does.
|
|
10
|
+
//
|
|
11
|
+
// WHAT IT DOES, in order, and why the order matters:
|
|
12
|
+
// 1. checks the chosen change RAN on the sandbox (it is one of the sandbox's own deploys
|
|
13
|
+
// that went live there), and that the live app still runs what the owner was shown;
|
|
14
|
+
// 2. RECORDS the promotion (provisioning_promotions) — before anything is sent, so
|
|
15
|
+
// nothing reaches the live app through Bongos without a row saying who and what;
|
|
16
|
+
// 3. switches the live service to manual deploys (idempotent), so the next merge cannot
|
|
17
|
+
// reach it by itself;
|
|
18
|
+
// 4. asks Render to deploy exactly that commit to the live service.
|
|
19
|
+
//
|
|
20
|
+
// Like the standup leg: the key is decrypted here, used for this run, and cleared in a
|
|
21
|
+
// `finally`; every failure is terminal (the owner presses again with the key); every call
|
|
22
|
+
// goes through scripts/bongos/render-api.js, which pins the workspace.
|
|
23
|
+
|
|
24
|
+
const COMMIT_RE = /^[0-9a-f]{7,40}$/;
|
|
25
|
+
// Only a deploy that served on the sandbox can be promoted: one still building, or one
|
|
26
|
+
// whose build failed, never ran there for anyone to look at.
|
|
27
|
+
const SERVED = ['live', 'deactivated'];
|
|
28
|
+
|
|
29
|
+
const commitOf = (d) => (d && d.commit && d.commit.id) || null;
|
|
30
|
+
|
|
31
|
+
async function promoteInstance(inst, deps, intent) {
|
|
32
|
+
const { db, log } = deps;
|
|
33
|
+
const render = require('./provision-render-common.js');
|
|
34
|
+
const secretBox = deps.secretBox || require('../../src/bongos/secret-box');
|
|
35
|
+
const host = inst.app_host || {};
|
|
36
|
+
const ask = host.promote || {};
|
|
37
|
+
let promotionId = null;
|
|
38
|
+
try {
|
|
39
|
+
if (!intent || !intent.borrowed_key_enc) return await refuse('no Render key came with the promotion — press promote again');
|
|
40
|
+
const age = Date.now() - new Date(intent.created_at || 0).getTime();
|
|
41
|
+
if (!(age < render.KEY_TTL_MS)) return await refuse('the Render key waited too long to be used and was cleared — press promote again');
|
|
42
|
+
if (host.platform !== 'render' || !host.serviceId || !host.ownerId) return await refuse('this project has no app on Render to promote to');
|
|
43
|
+
const sb = host.sandbox || {};
|
|
44
|
+
if (!sb.serviceId) return await refuse('this project has no sandbox to promote from');
|
|
45
|
+
if (!COMMIT_RE.test(String(ask.commitId || ''))) return await refuse('no change was chosen to promote');
|
|
46
|
+
|
|
47
|
+
const api = render.renderApiFor(deps, secretBox.decrypt(intent.borrowed_key_enc), host.ownerId);
|
|
48
|
+
const owners = await api.listOwners();
|
|
49
|
+
if (!owners.some((o) => o.id === host.ownerId)) return await refuse(`this Render key cannot reach workspace ${host.ownerId}`);
|
|
50
|
+
|
|
51
|
+
// 1. it ran on the sandbox, and live is what the owner was shown
|
|
52
|
+
const sandboxDeploys = await api.listDeploys(sb.serviceId, { limit: 50 });
|
|
53
|
+
const ran = sandboxDeploys.find((d) => commitOf(d) === ask.commitId && SERVED.includes(d.status));
|
|
54
|
+
if (!ran) return await refuse('that change has not run on the sandbox, so it cannot be promoted');
|
|
55
|
+
const liveDeploys = await api.listDeploys(host.serviceId, { limit: 10 });
|
|
56
|
+
const liveCommit = commitOf(liveDeploys.find((d) => d.status === 'live'));
|
|
57
|
+
if ((ask.expectLiveCommit || null) !== liveCommit) return await refuse('what your live app runs changed since you looked, so nothing was done — look again and promote');
|
|
58
|
+
if (liveCommit === ask.commitId) return await refuse('that change is already on your live app');
|
|
59
|
+
|
|
60
|
+
// 2. the record comes first
|
|
61
|
+
const message = String((ran.commit && ran.commit.message) || '').split('\n')[0].slice(0, 300);
|
|
62
|
+
const { rows } = await db.query(
|
|
63
|
+
`INSERT INTO provisioning_promotions (instance_id, commit_id, commit_message, from_commit_id, sandbox_deploy_id, requested_by, state)
|
|
64
|
+
VALUES ($1, $2, $3, $4, $5, $6, 'started') RETURNING id`,
|
|
65
|
+
[inst.id, ask.commitId, message, liveCommit, ran.id || null, ask.requestedBy == null ? null : String(ask.requestedBy)]);
|
|
66
|
+
promotionId = rows && rows[0] ? rows[0].id : null;
|
|
67
|
+
|
|
68
|
+
// 3. live stops following main, so only promotions move it
|
|
69
|
+
await api.setAutoDeploy(host.serviceId, false);
|
|
70
|
+
// 4. exactly that commit
|
|
71
|
+
const dep = await api.triggerDeploy(host.serviceId, { commitId: ask.commitId });
|
|
72
|
+
const deployId = (dep && dep.id) || null;
|
|
73
|
+
await db.query(`UPDATE provisioning_promotions SET state = 'sent', render_deploy_id = $2, updated_at = now() WHERE id = $1`, [promotionId, deployId]);
|
|
74
|
+
await render.recordAppHost(db, inst.id, { liveAutoDeploy: 'off', promote: { ...ask, state: 'sent', error: null, renderDeployId: deployId } });
|
|
75
|
+
log(` [render] ${inst.slug}: promoted ${ask.commitId.slice(0, 7)} from the sandbox to the live app (deploy ${deployId || 'started'})`);
|
|
76
|
+
return { ok: true };
|
|
77
|
+
} catch (e) {
|
|
78
|
+
return await refuse(render.plainReason(e).replace(/^Render would not set up the app/, 'Render would not promote it'));
|
|
79
|
+
} finally {
|
|
80
|
+
if (intent && intent.id != null) await render.clearBorrowedKey(db, intent.id);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function refuse(error) {
|
|
84
|
+
if (promotionId != null) {
|
|
85
|
+
await db.query(`UPDATE provisioning_promotions SET state = 'failed', error = $2, updated_at = now() WHERE id = $1`, [promotionId, error]).catch(() => {});
|
|
86
|
+
}
|
|
87
|
+
await render.recordAppHost(db, inst.id, { promote: { ...ask, state: 'failed', error } }).catch(() => {});
|
|
88
|
+
log(` ✖ [render] ${inst.slug} promote: ${error}`);
|
|
89
|
+
return { ok: false, terminal: true, error };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { promoteInstance, COMMIT_RE };
|
|
@@ -20,14 +20,15 @@
|
|
|
20
20
|
// 3. SERVICE — a free web service named `<slug>-sandbox`, auto-deploying from `main`,
|
|
21
21
|
// with BONGOS_SANDBOX=1. Found by name first, so a re-run never makes a second one.
|
|
22
22
|
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
23
|
+
// Once the sandbox stands, the LIVE service is switched to manual deploys, so only a
|
|
24
|
+
// person's promotion moves it (task 1005290; scripts/bongos/provision-render-promote.js).
|
|
25
25
|
//
|
|
26
26
|
// TEARDOWN removes the sandbox service and the sandbox database only — never the live app
|
|
27
27
|
// or its data (ADR 0406 §7). It needs the owner's key; without one the sandbox is
|
|
28
28
|
// forgotten and the owner is told exactly what to delete.
|
|
29
29
|
|
|
30
30
|
const payload = require('./render-payload.js');
|
|
31
|
+
const { recordAppHost } = require('./provision-render-common.js');
|
|
31
32
|
const { parseTargetRef } = require('./provision-repo-auth.js');
|
|
32
33
|
|
|
33
34
|
async function recordSandbox(db, instId, patch) {
|
|
@@ -120,6 +121,17 @@ async function standupSandbox(api, inst, host, deps) {
|
|
|
120
121
|
}
|
|
121
122
|
const url = (svc.serviceDetails && svc.serviceDetails.url) || null;
|
|
122
123
|
await recordSandbox(db, inst.id, { state: 'deploying', serviceId: svc.id, serviceUrl: url, error: null });
|
|
124
|
+
// From here on finished work reaches the sandbox only: the live service stops deploying
|
|
125
|
+
// on every merge, and moves when a person promotes (task 1005290, ADR 0406 §2). Only once
|
|
126
|
+
// the sandbox exists, so live is never left with nowhere for work to land. A failure is
|
|
127
|
+
// said on the row and the sandbox stands; the next promotion switches it again.
|
|
128
|
+
try {
|
|
129
|
+
await api.setAutoDeploy(host.serviceId, false);
|
|
130
|
+
await recordAppHost(db, inst.id, { liveAutoDeploy: 'off' }); // the one app_host writer
|
|
131
|
+
log(' [render] the live app now moves only when a person promotes');
|
|
132
|
+
} catch (e) {
|
|
133
|
+
log(` (the live app still deploys every merge: ${e && e.message})`);
|
|
134
|
+
}
|
|
123
135
|
return { ok: true };
|
|
124
136
|
} catch (e) {
|
|
125
137
|
const error = `The sandbox could not be set up: ${String((e && e.message) || e).split('\n')[0].slice(0, 240)}`;
|
|
@@ -31,32 +31,12 @@
|
|
|
31
31
|
// Every Render call goes through scripts/bongos/render-api.js, which pins the workspace and
|
|
32
32
|
// asserts it on every response, and which cannot delete.
|
|
33
33
|
|
|
34
|
-
const { RenderApi } = require('./render-api.js');
|
|
35
34
|
const payload = require('./render-payload.js');
|
|
36
35
|
const { parseTargetRef } = require('./provision-repo-auth.js');
|
|
37
36
|
const sandbox = require('./provision-render-sandbox.js');
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
const KEY_TTL_MS = 30 * 60 * 1000;
|
|
42
|
-
|
|
43
|
-
function renderApiFor(deps, token, ownerId) {
|
|
44
|
-
if (deps.renderApiFactory) return deps.renderApiFactory({ token, ownerId });
|
|
45
|
-
return new RenderApi({ token, ownerId, dryRun: !deps.apply, fetchImpl: deps.fetchImpl, log: deps.log });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Merge `patch` into the row's app_host. The runner is the only writer while an intent
|
|
49
|
-
// runs, and the web tier only writes it when enqueuing, which the open-intent index
|
|
50
|
-
// serialises — so a merge needs no read-modify-write.
|
|
51
|
-
async function recordAppHost(db, instId, patch) {
|
|
52
|
-
await db.query(
|
|
53
|
-
`UPDATE provisioning_instances SET app_host = COALESCE(app_host, '{}'::jsonb) || $2::jsonb WHERE id = $1`,
|
|
54
|
-
[instId, JSON.stringify({ ...patch, updatedAt: new Date().toISOString() })]);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async function clearBorrowedKey(db, intentId) {
|
|
58
|
-
await db.query(`UPDATE provisioning_intents SET borrowed_key_enc = NULL WHERE id = $1`, [intentId]);
|
|
59
|
-
}
|
|
37
|
+
// The pieces every Render leg shares live in their own file, so the sandbox and promote
|
|
38
|
+
// legs can use them without a require cycle back into this one (task 1005290).
|
|
39
|
+
const { KEY_TTL_MS, renderApiFor, recordAppHost, clearBorrowedKey, plainReason } = require('./provision-render-common.js');
|
|
60
40
|
|
|
61
41
|
// Whether a Render service's repository is this project's. Render reports it as a URL;
|
|
62
42
|
// compared as owner/name, case-insensitively, the way GitHub resolves them.
|
|
@@ -118,18 +98,6 @@ async function greenfield(api, inst, host, deps) {
|
|
|
118
98
|
return { ok: true };
|
|
119
99
|
}
|
|
120
100
|
|
|
121
|
-
// The owner reads this reason on the setup screen (app-status.js oneSentence). A refusal
|
|
122
|
-
// from Render's API used to arrive as its raw line — "Render POST /services failed: …" —
|
|
123
|
-
// so an answer from Render is re-said as a sentence with Render's own reason kept whole
|
|
124
|
-
// (task 1004464). Anything else is already the leg's own words.
|
|
125
|
-
function plainReason(e) {
|
|
126
|
-
const msg = e && e.message ? e.message : String(e);
|
|
127
|
-
const said = e && e.status != null && e.body && typeof e.body.message === 'string' ? e.body.message.trim() : '';
|
|
128
|
-
if (said) return `Render would not set up the app: ${said}`;
|
|
129
|
-
const m = /^Render (?:GET|POST|PATCH|PUT|DELETE) \S+ failed: ([\s\S]+)$/.exec(msg);
|
|
130
|
-
return m ? `Render would not set up the app: ${m[1].trim()}` : msg;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
101
|
async function renderStandupInstance(inst, deps, intent) {
|
|
134
102
|
const { db, log } = deps;
|
|
135
103
|
const secretBox = deps.secretBox || require('../../src/bongos/secret-box');
|
|
@@ -233,4 +201,4 @@ async function forgetRenderApp(inst, deps) {
|
|
|
233
201
|
if (apply) await recordAppHost(db, inst.id, { state: 'disconnected' });
|
|
234
202
|
}
|
|
235
203
|
|
|
236
|
-
module.exports = { renderStandupInstance, afterProvision, forgetRenderApp, clearBorrowedKey, recordAppHost, KEY_TTL_MS };
|
|
204
|
+
module.exports = { renderStandupInstance, afterProvision, forgetRenderApp, clearBorrowedKey, recordAppHost, renderApiFor, plainReason, KEY_TTL_MS };
|
|
@@ -1255,6 +1255,7 @@ async function cmdRunIntents(deps) {
|
|
|
1255
1255
|
else if (intent.action === 'core-upgrade') result = await coreUpgradeInstance(inst, deps, intent); // also reads the INTENT: the version the owner previewed (ADR 0293 D4)
|
|
1256
1256
|
else if (intent.action === 'core-upgrade-preflight') result = await preflightCoreUpgrade(inst, deps, intent); // changes nothing; caches what the move WOULD say (task 1004121)
|
|
1257
1257
|
else if (intent.action === 'repo-private' || intent.action === 'render-standup') result = intent.action === 'repo-private' ? await require('./provision-repo-private.js').repoPrivateInstance(inst, deps) : await require('./provision-render.js').renderStandupInstance(inst, deps, intent); // wire the deploy key, THEN flip (task 1004193) · spend the borrowed Render key once (task 1004183); required here — this file is at the size ratchet
|
|
1258
|
+
else if (intent.action === 'render-promote') result = await require('./provision-render-promote.js').promoteInstance(inst, deps, intent); // a person's promotion of a sandbox change to the live app (task 1005290, ADR 0406)
|
|
1258
1259
|
else result = { ok: false, error: `unknown_action '${intent.action}' — runner core ${coreVersionSafe() || '?'} does not know it (runner too old?); refusing to guess` };
|
|
1259
1260
|
if (result && result.ok === false) {
|
|
1260
1261
|
// WAIT OR RETIRE (task 1004447, intent-retry.js): a transient failure waits ~15m/1h/6h; a
|
|
@@ -163,9 +163,24 @@ class RenderApi extends JsonHttpApi {
|
|
|
163
163
|
return super._req('DELETE', path, null);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
// Start a deploy. With `commitId` Render builds THAT commit rather than the branch's
|
|
167
|
+
// newest — how a promotion moves the live app to exactly what ran on the sandbox (ADR
|
|
168
|
+
// 0406 §2, task 1005290). The answer carries no owner, so the service is read first.
|
|
169
|
+
async triggerDeploy(serviceId, { commitId } = {}) {
|
|
167
170
|
await this.getService(serviceId);
|
|
168
|
-
|
|
171
|
+
const body = commitId ? { commitId } : {};
|
|
172
|
+
return this._req('POST', `/services/${encodeURIComponent(serviceId)}/deploys`, body, { dryRunResult: { id: null } });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Whether the service deploys on every push to its branch. The live app is switched to
|
|
176
|
+
// `no` once its project has a sandbox, so only a person's promotion moves it (ADR 0406
|
|
177
|
+
// §2, task 1005290). A write to the owner's service, through the guard, nothing else.
|
|
178
|
+
async setAutoDeploy(serviceId, on) {
|
|
179
|
+
await this.getService(serviceId);
|
|
180
|
+
const json = await this._req('PATCH', `/services/${encodeURIComponent(serviceId)}`, { autoDeploy: on ? 'yes' : 'no' }, {
|
|
181
|
+
dryRunResult: { id: serviceId, ownerId: this.ownerId, autoDeploy: on ? 'yes' : 'no' },
|
|
182
|
+
});
|
|
183
|
+
return this.assertOwned(json && json.service ? json.service : json, `service ${serviceId}`);
|
|
169
184
|
}
|
|
170
185
|
|
|
171
186
|
// A service's deploys, newest first, as the bare deploy objects (Render wraps each in
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// scripts/gds/government-walk-sole.js — @bongos-forwarder (task 1004848). Generated by
|
|
2
|
+
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
3
|
+
// The real script is scripts/bongos/government-walk-sole.js. This forwarder keeps callers that still name
|
|
4
|
+
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
5
|
+
'use strict';
|
|
6
|
+
require('../bongos/forwarder.js').forward(module, 'government-walk-sole.js');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// scripts/gds/provision-render-common.js — @bongos-forwarder (task 1004848). Generated by
|
|
2
|
+
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
3
|
+
// The real script is scripts/bongos/provision-render-common.js. This forwarder keeps callers that still name
|
|
4
|
+
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
5
|
+
'use strict';
|
|
6
|
+
require('../bongos/forwarder.js').forward(module, 'provision-render-common.js');
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// scripts/gds/provision-render-promote.js — @bongos-forwarder (task 1004848). Generated by
|
|
2
|
+
// scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
|
|
3
|
+
// The real script is scripts/bongos/provision-render-promote.js. This forwarder keeps callers that still name
|
|
4
|
+
// the old path working, and warns once so they get repointed. Task 1003706 removes it.
|
|
5
|
+
'use strict';
|
|
6
|
+
require('../bongos/forwarder.js').forward(module, 'provision-render-promote.js');
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bongos/founding-page-gate.js — A SLEEPING ROOM IS NOT REACHABLE BY URL (task 1005112).
|
|
4
|
+
//
|
|
5
|
+
// While a project is being founded, the hall's sidebar holds only the rooms its genesis
|
|
6
|
+
// stages have opened. Hiding a room and refusing its page are ONE change: a room nobody
|
|
7
|
+
// can see but anyone can type the address of would be a wall the product only claims to
|
|
8
|
+
// have. So this is the page half. It asks the server's own answer, the onboarding port's
|
|
9
|
+
// foundingModeFor() (the same read /me carries as founding_mode.asleep, keyed by the
|
|
10
|
+
// sidebar's room ids), and never a browser's opinion (ADR 0016).
|
|
11
|
+
//
|
|
12
|
+
// A sleeping room's page answers with a redirect home carrying ?asleep=<room>&opens=<stage>,
|
|
13
|
+
// where the shell says what the room is and which stage opens it, instead of a dead end.
|
|
14
|
+
//
|
|
15
|
+
// What it never touches: a project that is not founding (foundingModeFor answers null,
|
|
16
|
+
// and the page is served exactly as before), a room that is awake, the Primer and the
|
|
17
|
+
// Board Room (never asleep), and every page that is not one of the rooms below. An
|
|
18
|
+
// unreadable founding state is null too, so a failure opens the page, never shuts it:
|
|
19
|
+
// founding-mode.js fails closed to OFF for the same reason (a hall wrongly asleep would
|
|
20
|
+
// hijack it). Rooms are keyed by the sidebar id, and each room's page slug IS that id.
|
|
21
|
+
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const seams = require('../module-seams');
|
|
24
|
+
|
|
25
|
+
// The rooms a genesis stage can put to sleep (founding-mode.js WAKES + WAKES_WHEN_ALIVE),
|
|
26
|
+
// by page slug. tests/founding_page_gate.mjs holds this list to that map.
|
|
27
|
+
const ROOM_PAGE_RE = /^\/builders\/(work|goals|goal-map|collab|roster)(?:\.html)?\/?$/;
|
|
28
|
+
|
|
29
|
+
// asleepRoomFor(room, founding) — PURE. The room id when that room sleeps under this
|
|
30
|
+
// founding answer, else null.
|
|
31
|
+
function asleepRoomFor(room, founding) {
|
|
32
|
+
if (!founding || founding.active !== true || !founding.asleep || typeof founding.asleep !== 'object') return null;
|
|
33
|
+
return Object.prototype.hasOwnProperty.call(founding.asleep, room) ? room : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// readFounding() — the server's answer, or null on any failure (fail open: OFF).
|
|
37
|
+
async function readFounding(resolve = seams.resolveOptional) {
|
|
38
|
+
try {
|
|
39
|
+
const onboarding = resolve('onboarding');
|
|
40
|
+
if (!onboarding || typeof onboarding.foundingModeFor !== 'function') return null;
|
|
41
|
+
return await onboarding.foundingModeFor();
|
|
42
|
+
} catch (_) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// foundingPageGate({ skip, isBuildersHost, read }) — the Express middleware. `skip(req)`
|
|
48
|
+
// is the caller's module-surface test; `read` is injectable for tests. The founding read
|
|
49
|
+
// is made only for one of the five room pages, so every other request pays nothing.
|
|
50
|
+
function foundingPageGate({ skip = () => false, isBuildersHost = () => false, read = readFounding } = {}) {
|
|
51
|
+
return async function gate(req, res, next) {
|
|
52
|
+
if (skip(req) || !String(req.path || '').startsWith('/builders')) return next();
|
|
53
|
+
const m = ROOM_PAGE_RE.exec(path.posix.normalize(req.path));
|
|
54
|
+
if (!m) return next();
|
|
55
|
+
let founding = null;
|
|
56
|
+
try { founding = await read(); } catch (_) { founding = null; } // fail open: OFF
|
|
57
|
+
const room = asleepRoomFor(m[1], founding);
|
|
58
|
+
if (!room) return next();
|
|
59
|
+
const opens = String(founding.asleep[room] || '');
|
|
60
|
+
res.set('Cache-Control', 'no-store');
|
|
61
|
+
return res.redirect(302, `${isBuildersHost(req) ? '' : '/builders'}/?asleep=${encodeURIComponent(room)}`
|
|
62
|
+
+ (/^[a-z]{1,20}$/.test(opens) ? `&opens=${opens}` : ''));
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// One export: the pure parts are proven through it (tests/founding_page_gate.mjs).
|
|
67
|
+
module.exports = { foundingPageGate };
|
|
@@ -50,6 +50,7 @@ const { linkPreviewTags } = require('../link-preview');
|
|
|
50
50
|
const { prelaunchGate } = require('./prelaunch-gate');
|
|
51
51
|
const { platformVisibilityGate } = require('./platform-visibility-gate');
|
|
52
52
|
const { schemaPending } = require('./schema-lag');
|
|
53
|
+
const { foundingPageGate } = require('./founding-page-gate');
|
|
53
54
|
const { clientModules, isModuleEnabled } = require('../modules');
|
|
54
55
|
const { hallWidgetScripts, moduleWebSurfaces } = require('../module-loader/loader');
|
|
55
56
|
const { resolveCoreRoot, resolveDocsRoot, resolveInstanceRoot, resolveEnv } = require('../instance-config');
|
|
@@ -1353,6 +1354,8 @@ function mountInternalSurfaces(app) {
|
|
|
1353
1354
|
}
|
|
1354
1355
|
next();
|
|
1355
1356
|
});
|
|
1357
|
+
// task 1005112: a founding project's sleeping room is refused by URL, after sign-in.
|
|
1358
|
+
app.use(foundingPageGate({ skip: isModuleSurfaceHost, isBuildersHost }));
|
|
1356
1359
|
|
|
1357
1360
|
// task 1965: the read-only Task Board (/board, task 751) was consolidated into
|
|
1358
1361
|
// the Work page (/work) — the hall keeps one task-listing surface. 301 old
|
package/src/branding.js
CHANGED
|
@@ -112,6 +112,10 @@ const ENV_OVERRIDES = [
|
|
|
112
112
|
// The ship mode the founder picked in the wizard (task 1004629), empty when none was.
|
|
113
113
|
// resolveShipMode reads it only while project_settings['ship.mode'] is unset.
|
|
114
114
|
['SHIP_MODE', ['founding', 'shipMode']],
|
|
115
|
+
// The start-a-project guide's hand-off (task 1005274, modules/provisioning/guide-handoff.js): base64url
|
|
116
|
+
// JSON { scope, markdown }, empty when the founder never used the guide. modules/onboarding/guide-handoff.js
|
|
117
|
+
// writes it once: the scope answers as carried over, and the hand-off file. SERVER-ONLY, like the birth.
|
|
118
|
+
['GUIDE_HANDOFF', ['founding', 'guideHandoff']],
|
|
115
119
|
];
|
|
116
120
|
|
|
117
121
|
const isObj = (v) => v && typeof v === 'object' && !Array.isArray(v);
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.25.
|
|
84
|
+
const CORE_VERSION = '1.25.21'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -135,6 +135,8 @@ const provisioning = require(path.join(ROOT, 'modules', 'provisioning', 'provisi
|
|
|
135
135
|
const api = require(path.join(ROOT, 'src', 'module-api.js'));
|
|
136
136
|
let detailRow = null;
|
|
137
137
|
provisioning.getInstanceById = async () => detailRow;
|
|
138
|
+
// The gate also admits an owner-made grant (task 1004161, ADR 0407); nobody in this file holds one.
|
|
139
|
+
require(path.join(ROOT, 'modules', 'provisioning', 'grants.js')).liveGrant = async () => null;
|
|
138
140
|
provisioning.getOpenIntent = async () => null;
|
|
139
141
|
provisioning.getLatestOAuthManifestForInstance = async () => null;
|
|
140
142
|
require(path.join(ROOT, 'modules', 'provisioning', 'catalog-bridge.js')).healInstanceCatalog = async () => {};
|