@bongos/core 1.25.16 → 1.25.17

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.
Files changed (45) hide show
  1. package/.bongos-core.json +95 -40
  2. package/bin/bongos.js +1 -0
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +14 -0
  5. package/clients/bongos-client/index.cjs +14 -0
  6. package/clients/bongos-client/index.d.ts +22 -2
  7. package/clients/bongos-client/index.mjs +14 -0
  8. package/docs/adr/0406-every-project-gets-a-sandbox-that-follows-main-and-live-moves-only-when-a-person-promotes.md +8 -0
  9. package/docs/api/openapi.json +415 -3
  10. package/docs/api-reference.md +13 -2
  11. package/docs/module-api-changelog.md +2 -0
  12. package/docs/page-readings.json +1 -1
  13. package/modules/government/decision-rules.js +1 -0
  14. package/modules/lifecycle/first-read-db.js +152 -0
  15. package/modules/lifecycle/first-read.js +265 -0
  16. package/modules/lifecycle/kickoff-checklist.js +12 -3
  17. package/modules/lifecycle/migrations/lifecycle_022_repo_maps.sql +44 -0
  18. package/modules/lifecycle/module.json +2 -1
  19. package/modules/lifecycle/routes/first-read.js +79 -0
  20. package/modules/provisioning/pollers/app-liveness.js +23 -1
  21. package/modules/provisioning/render-sandbox.js +113 -0
  22. package/modules/provisioning/render-standup.js +3 -1
  23. package/modules/provisioning/routes/provisioning.js +4 -1
  24. package/modules/provisioning/routes/render-standup.js +49 -1
  25. package/modules/public-landing/public/projects.html +101 -4
  26. package/package-lock.json +2 -2
  27. package/package.json +1 -1
  28. package/release-notes.json +10 -0
  29. package/scripts/bongos/first-read.js +116 -0
  30. package/scripts/bongos/government-walk.js +4 -4
  31. package/scripts/bongos/provision-render-sandbox.js +171 -0
  32. package/scripts/bongos/provision-render.js +37 -2
  33. package/scripts/bongos/provision-teardown.js +1 -1
  34. package/scripts/bongos/render-api.js +33 -3
  35. package/scripts/bongos/render-payload.js +39 -1
  36. package/scripts/gds/first-read.js +7 -0
  37. package/scripts/gds/provision-render-sandbox.js +6 -0
  38. package/src/module-api.js +1 -1
  39. package/tests/first_read.mjs +303 -0
  40. package/tests/government_act_alone_buildings.mjs +2 -0
  41. package/tests/provisioning_app_status.mjs +3 -2
  42. package/tests/provisioning_render_route.mjs +1 -1
  43. package/tests/provisioning_teardown_intent.mjs +50 -0
  44. package/tests/render_api.mjs +5 -2
  45. package/tests/render_sandbox.mjs +447 -0
@@ -0,0 +1,171 @@
1
+ 'use strict';
2
+ // provision-render-sandbox.js — the project's SANDBOX on Render: set it up beside the app,
3
+ // and remove it at teardown (task 1005289; ADR 0406, ADR 0327).
4
+ //
5
+ // WHY. Every project whose app runs on Render gets a second, free app from the same
6
+ // repository that follows `main` on every merge, so finished work reaches a test copy
7
+ // first and the live site moves only when a person promotes (ADR 0406 §2). This file is
8
+ // the runner half; the web tier queues it (modules/provisioning/render-sandbox.js) and the
9
+ // standup leg calls it after the app (provision-render.js), with the same borrowed key.
10
+ //
11
+ // THREE STAGES, kept apart so one failing never takes the others down:
12
+ // 1. BUILD — the app's own build settings: what the owner gave (new app), or read off
13
+ // their existing service (a linked one).
14
+ // 2. DATABASE — never Render's free database (it expires, one per workspace: ADR 0406
15
+ // §4). When the app has a PAID database we created, a second database is made INSIDE
16
+ // that server with plain SQL (Render's docs: "Adding multiple databases to a single
17
+ // instance" — there is no API call for it). No app database → no sandbox database.
18
+ // A failure here leaves the sandbox running with NO database and says why; it never
19
+ // falls back to buying one — that is the owner's call (ADR 0406 §6 choice 1).
20
+ // 3. SERVICE — a free web service named `<slug>-sandbox`, auto-deploying from `main`,
21
+ // with BONGOS_SANDBOX=1. Found by name first, so a re-run never makes a second one.
22
+ //
23
+ // The live service is not touched here: switching it to manual deploys belongs with the
24
+ // promote button that replaces them (task 1005290).
25
+ //
26
+ // TEARDOWN removes the sandbox service and the sandbox database only — never the live app
27
+ // or its data (ADR 0406 §7). It needs the owner's key; without one the sandbox is
28
+ // forgotten and the owner is told exactly what to delete.
29
+
30
+ const payload = require('./render-payload.js');
31
+ const { parseTargetRef } = require('./provision-repo-auth.js');
32
+
33
+ async function recordSandbox(db, instId, patch) {
34
+ await db.query(
35
+ `UPDATE provisioning_instances
36
+ SET app_host = COALESCE(app_host, '{}'::jsonb)
37
+ || jsonb_build_object('sandbox', COALESCE(app_host->'sandbox', '{}'::jsonb) || $2::jsonb)
38
+ WHERE id = $1`,
39
+ [instId, JSON.stringify({ ...patch, updatedAt: new Date().toISOString() })]);
40
+ }
41
+
42
+ // Run SQL against the app's database SERVER from the runner. Uses the server's external
43
+ // address (the runner is outside Render's network). Injected in tests: no real database.
44
+ async function defaultSqlRunner(connectionString, statements) {
45
+ const { Client } = require('pg');
46
+ const c = new Client({ connectionString, ssl: { rejectUnauthorized: true }, connectionTimeoutMillis: 15000 });
47
+ await c.connect();
48
+ try {
49
+ const out = [];
50
+ for (const s of statements) out.push(await c.query(s.text, s.values || []));
51
+ return out;
52
+ } finally { await c.end().catch(() => {}); }
53
+ }
54
+
55
+ // The same connection string pointed at another database on the same server.
56
+ function withDatabase(connectionString, dbName) {
57
+ const u = new URL(connectionString);
58
+ u.pathname = `/${dbName}`;
59
+ return u.toString();
60
+ }
61
+
62
+ // Stage 2. Returns { state: 'ready', name, url } | { state: 'none' | 'failed', note }.
63
+ async function sandboxDatabase(api, inst, host, deps) {
64
+ if (!host.postgresId) {
65
+ return { state: 'none', note: host.mode === 'adopt'
66
+ ? 'Bongos did not create your app’s database, so the sandbox has none. It runs your app without data.'
67
+ : 'Your app has no database, so its sandbox has none either.' };
68
+ }
69
+ try {
70
+ const pg = await api.getPostgres(host.postgresId);
71
+ if (pg.plan === 'free') return { state: 'none', note: 'Your app’s database is on Render’s free plan, which cannot hold a sandbox database, so the sandbox has none.' };
72
+ const conn = await api.postgresConnectionInfo(pg.id);
73
+ const ext = conn && conn.externalConnectionString;
74
+ const int = conn && conn.internalConnectionString;
75
+ if (!ext || !int) throw new Error('Render gave no connection address for your database');
76
+ const name = payload.sandboxDbName(inst);
77
+ const run = deps.sqlRunner || defaultSqlRunner;
78
+ const [exists] = await run(ext, [{ text: 'SELECT 1 FROM pg_database WHERE datname = $1', values: [name] }]);
79
+ if (!(exists && exists.rows && exists.rows.length)) await run(ext, [{ text: `CREATE DATABASE "${name}"` }]);
80
+ return { state: 'ready', name, url: withDatabase(int, name) };
81
+ } catch (e) {
82
+ return { state: 'failed', note: `The sandbox’s own database could not be made, so the sandbox runs with no data: ${String((e && e.message) || e).split('\n')[0].slice(0, 200)}` };
83
+ }
84
+ }
85
+
86
+ // Stages 1-3. Never throws: a sandbox failure is recorded on the sandbox and never fails
87
+ // the app it sits beside. Returns { ok, error? }.
88
+ async function standupSandbox(api, inst, host, deps) {
89
+ const { db, log } = deps;
90
+ try {
91
+ const repo = parseTargetRef(inst.target_ref);
92
+ if (!repo) throw new Error('the project has no GitHub repository for the sandbox to build from');
93
+ // 1. build settings
94
+ let build, region = host.region || payload.DEFAULTS.region;
95
+ if (host.mode === 'adopt') {
96
+ const live = await api.getService(host.serviceId);
97
+ build = payload.buildFromService(live);
98
+ if (!build) throw new Error('Render did not say how your linked service is built, so the sandbox could not copy it');
99
+ region = (live.serviceDetails && live.serviceDetails.region) || region;
100
+ } else {
101
+ build = payload.buildFor(host);
102
+ }
103
+ await recordSandbox(db, inst.id, { state: 'creating', error: null });
104
+ // 2. database — its own stage; its failure is the sandbox's note, not its end
105
+ const sb = host.sandbox || {};
106
+ const dbStage = await sandboxDatabase(api, inst, host, deps);
107
+ await recordSandbox(db, inst.id, { database: dbStage.state, dbName: dbStage.name || null, dbNote: dbStage.note || null });
108
+ log(` [render] sandbox database: ${dbStage.state}${dbStage.name ? ` (${dbStage.name})` : ''}`);
109
+ // 3. the free service, found before created
110
+ const name = payload.resourceName(inst, 'sandbox');
111
+ let svc = sb.serviceId ? await api.getService(sb.serviceId).catch(() => null) : null;
112
+ if (!svc) svc = await api.findServiceByName(name);
113
+ if (!svc) {
114
+ const created = await api.createService(payload.sandboxServicePayload(inst, {
115
+ ownerId: api.ownerId, repoUrl: `https://github.com/${repo.owner}/${repo.name}`, region, build,
116
+ dbUrl: dbStage.state === 'ready' ? dbStage.url : null,
117
+ }));
118
+ svc = created.service;
119
+ log(` [render] created sandbox service ${svc.id} (free, follows main)`);
120
+ }
121
+ const url = (svc.serviceDetails && svc.serviceDetails.url) || null;
122
+ await recordSandbox(db, inst.id, { state: 'deploying', serviceId: svc.id, serviceUrl: url, error: null });
123
+ return { ok: true };
124
+ } catch (e) {
125
+ const error = `The sandbox could not be set up: ${String((e && e.message) || e).split('\n')[0].slice(0, 240)}`;
126
+ await recordSandbox(db, inst.id, { state: 'failed', error }).catch(() => {});
127
+ log(` ✖ [render] ${inst.slug} sandbox: ${error}`);
128
+ return { ok: false, error };
129
+ }
130
+ }
131
+
132
+ // Teardown's half for the sandbox. `api` is a Render client built from the key the owner
133
+ // lent with the teardown, or null when they lent none.
134
+ async function removeSandbox(inst, { db, log, apply, sqlRunner }, api) {
135
+ const host = inst.app_host || {};
136
+ const sb = host.sandbox;
137
+ if (host.platform !== 'render' || !sb || (!sb.serviceId && !sb.dbName)) return { removed: false, reason: 'none' };
138
+ if (!api) {
139
+ const what = [sb.serviceId && `the service ${payload.resourceName(inst, 'sandbox')}`, sb.dbName && `the database ${sb.dbName} inside your app’s database`].filter(Boolean).join(' and ');
140
+ log(` [render] sandbox left on Render (no key lent with the teardown): ${what}`);
141
+ if (apply) await recordSandbox(db, inst.id, { state: 'left', error: `No Render key came with the teardown, so the sandbox is still on your Render account: remove ${what} there.` });
142
+ return { removed: false, reason: 'no_key' };
143
+ }
144
+ const notes = [];
145
+ let dbGone = !sb.dbName;
146
+ if (sb.serviceId) {
147
+ if (apply) await api.deleteSandboxService(sb.serviceId);
148
+ notes.push(`service ${sb.serviceId}`);
149
+ }
150
+ if (sb.dbName && host.postgresId && payload.SANDBOX_DB_RE.test(sb.dbName)) {
151
+ try {
152
+ const conn = await api.postgresConnectionInfo(host.postgresId);
153
+ if (apply && conn && conn.externalConnectionString) {
154
+ await (sqlRunner || defaultSqlRunner)(conn.externalConnectionString, [{ text: `DROP DATABASE IF EXISTS "${sb.dbName}" WITH (FORCE)` }]);
155
+ }
156
+ notes.push(`database ${sb.dbName}`);
157
+ dbGone = true;
158
+ } catch (e) {
159
+ log(` (sandbox database ${sb.dbName} not dropped: ${e && e.message})`);
160
+ }
161
+ }
162
+ log(` [render] ${apply ? 'removed' : 'would remove'} the sandbox (${notes.join(' and ')}); the live app is untouched`);
163
+ if (apply) {
164
+ await recordSandbox(db, inst.id, dbGone
165
+ ? { state: 'removed', serviceId: null, serviceUrl: null, dbName: null, error: null }
166
+ : { state: 'removed', serviceId: null, serviceUrl: null, error: `The sandbox app was removed, but its database ${sb.dbName} is still inside your app’s database server.` });
167
+ }
168
+ return { removed: true };
169
+ }
170
+
171
+ module.exports = { standupSandbox, removeSandbox, sandboxDatabase, recordSandbox, withDatabase };
@@ -34,6 +34,7 @@
34
34
  const { RenderApi } = require('./render-api.js');
35
35
  const payload = require('./render-payload.js');
36
36
  const { parseTargetRef } = require('./provision-repo-auth.js');
37
+ const sandbox = require('./provision-render-sandbox.js');
37
38
 
38
39
  // A key waiting longer than this was lent for a run that never came. It is refused and
39
40
  // cleared rather than spent late.
@@ -147,7 +148,18 @@ async function renderStandupInstance(inst, deps, intent) {
147
148
  const owners = await api.listOwners();
148
149
  if (!owners.some((o) => o.id === host.ownerId)) return refuse(`this Render key cannot reach workspace ${host.ownerId}`);
149
150
 
150
- return host.mode === 'adopt' ? await adopt(api, inst, host, deps) : await greenfield(api, inst, host, deps);
151
+ // An app already up that only asked for its sandbox (render-sandbox.js
152
+ // enqueueSandboxAdd) is not touched again: the run goes straight to the sandbox.
153
+ const sandboxOnly = !!(host.sandboxWanted && host.sandbox && host.sandbox.state === 'requested'
154
+ && host.serviceId && ['deploying', 'linked', 'live'].includes(host.state));
155
+ const app = sandboxOnly ? { ok: true } : (host.mode === 'adopt' ? await adopt(api, inst, host, deps) : await greenfield(api, inst, host, deps));
156
+ // The sandbox (ADR 0406, task 1005289): after the app, with the same key. Its failure
157
+ // is recorded on the sandbox and never fails the app.
158
+ if (app.ok && host.sandboxWanted) {
159
+ const fresh = deps.provisioning && deps.provisioning.getInstanceById ? ((await deps.provisioning.getInstanceById(db, inst.id)) || inst) : inst;
160
+ await sandbox.standupSandbox(api, fresh, { ...host, ...(fresh.app_host || {}) }, deps);
161
+ }
162
+ return app;
151
163
  } catch (e) {
152
164
  return refuse(plainReason(e));
153
165
  } finally {
@@ -190,9 +202,32 @@ async function afterProvision(inst, deps, intent, result) {
190
202
  // Teardown's half (ADR 0327 §3): forget the Render app, never delete it. The ids stay on
191
203
  // the row so an operator can still say which services were the project's; the owner
192
204
  // deletes them in their own Render dashboard if they want to stop paying.
193
- async function forgetRenderApp(inst, { db, log, apply }) {
205
+ //
206
+ // The SANDBOX is the one exception (ADR 0406 §7, task 1005289): the platform made it, so
207
+ // the platform removes it — when the owner lent their key with the teardown (the running
208
+ // teardown intent's borrowed_key_enc). The key is cleared here whatever happens.
209
+ async function forgetRenderApp(inst, deps) {
210
+ const { db, log, apply } = deps;
194
211
  const host = inst.app_host;
195
212
  if (!host || host.platform !== 'render') return;
213
+ if (host.sandbox) {
214
+ let keyEnc = null, intentId = null;
215
+ try {
216
+ const { rows } = await db.query(
217
+ `SELECT id, borrowed_key_enc FROM provisioning_intents
218
+ WHERE instance_id = $1 AND action = 'teardown' AND state IN ('pending', 'running') AND borrowed_key_enc IS NOT NULL
219
+ ORDER BY created_at DESC LIMIT 1`, [inst.id]);
220
+ if (rows && rows[0]) { keyEnc = rows[0].borrowed_key_enc; intentId = rows[0].id; }
221
+ const secretBox = deps.secretBox || require('../../src/bongos/secret-box');
222
+ const api = keyEnc && host.ownerId ? renderApiFor(deps, secretBox.decrypt(keyEnc), host.ownerId) : null;
223
+ await sandbox.removeSandbox(inst, deps, api);
224
+ } catch (e) {
225
+ log(` (sandbox not removed: ${e && e.message})`);
226
+ if (apply) await sandbox.recordSandbox(db, inst.id, { state: 'left', error: `The sandbox could not be removed, so it is still on your Render account: ${String((e && e.message) || e).split('\n')[0].slice(0, 200)}` }).catch(() => {});
227
+ } finally {
228
+ if (intentId != null) await clearBorrowedKey(db, intentId);
229
+ }
230
+ }
196
231
  const what = [host.serviceId && `service ${host.serviceId}`, host.postgresId && `database ${host.postgresId}`].filter(Boolean).join(' and ') || 'nothing yet';
197
232
  log(` [render] ${apply ? 'disconnected' : 'would disconnect'} from Render (${what}); nothing on Render is deleted — the owner removes it in their own dashboard`);
198
233
  if (apply) await recordAppHost(db, inst.id, { state: 'disconnected' });
@@ -40,7 +40,7 @@ async function teardownInstance(inst, deps) {
40
40
  if (inst.db_name) log(` (left for the operator: dropdb ${dbName(inst)}, prune ${CONFIG.backupDir}/${dbName(inst)}-*.sql.gz)`);
41
41
  parkAddress(inst, { controlExec, log, apply });
42
42
  // The app's Render services are the OWNER's: forget them, never delete them (ADR 0327 §3, task 1004183).
43
- await require('./provision-render.js').forgetRenderApp(inst, { db, log, apply });
43
+ await require('./provision-render.js').forgetRenderApp(inst, { ...deps, db, log, apply }); // …except the sandbox, removed when a key came with the teardown (ADR 0406 §7)
44
44
  // Revoke the instance's deploy key (ADR 0176) and remove its local halves. The
45
45
  // checkout itself is deliberately left in place (data is precious), so the key
46
46
  // files must NOT ride along — and a revoke skip must be LOUD: a torn-down project
@@ -24,9 +24,13 @@
24
24
  // resource through the guard, so nothing is ever sent to or read from a resource whose
25
25
  // workspace was not checked.
26
26
  //
27
- // NO DELETE. The platform must not hold the ability to destroy infrastructure in an
28
- // account it does not own (ADR 0327 §3). There is no delete method, and _req refuses the
29
- // verb outright, so adding one later is a visible change to this file, not a one-liner.
27
+ // NO DELETE — with ONE narrow exception. The platform must not hold the ability to destroy
28
+ // infrastructure in an account it does not own (ADR 0327 §3), and _req refuses the verb
29
+ // outright. The exception is the project's SANDBOX (ADR 0406 §7, task 1005289): a free
30
+ // service the platform itself created, removed when the owner tears the project down with
31
+ // their key lent. deleteSandboxService reads the service through the guard first and
32
+ // refuses anything not named `…-sandbox` on the free plan, so it can never reach the
33
+ // owner's live app; it is the only path to a DELETE in this file.
30
34
 
31
35
  'use strict';
32
36
 
@@ -133,6 +137,32 @@ class RenderApi extends JsonHttpApi {
133
137
  return this._req('PUT', `/services/${encodeURIComponent(serviceId)}/env-vars/${encodeURIComponent(key)}`, { value }, { dryRunResult: { key } });
134
138
  }
135
139
 
140
+ // The service of this exact name in this workspace, or null. Lets a re-run find a
141
+ // sandbox an earlier run created before its id reached the row, instead of making a
142
+ // second one. Every element of the list is checked through the guard.
143
+ async findServiceByName(name) {
144
+ const q = `/services?name=${encodeURIComponent(name)}&ownerId=${encodeURIComponent(this.ownerId)}&limit=20`;
145
+ const json = await this._req('GET', q, null, { dryRunResult: { items: [] } });
146
+ const items = Array.isArray(json) ? json : (json && json.items) || [];
147
+ const found = items.map((e) => (e && e.service) || e).filter((s) => s && typeof s.id === 'string');
148
+ found.forEach((s) => this.assertOwned(s, `service ${s.id}`));
149
+ return found.find((s) => s.name === name) || null;
150
+ }
151
+
152
+ // Remove a project's SANDBOX service — and nothing else (see the header). The service is
153
+ // read through the workspace guard, and refused unless it is named `…-sandbox` AND on
154
+ // the free plan: the owner's live app is never either.
155
+ async deleteSandboxService(serviceId) {
156
+ const svc = await this.getService(serviceId);
157
+ const plan = svc.serviceDetails && svc.serviceDetails.plan;
158
+ if (!/-sandbox$/.test(String(svc.name || '')) || plan !== 'free') {
159
+ throw new RenderApiError(`refusing to delete service ${serviceId}: it is not a free sandbox the platform created (ADR 0406 §7)`);
160
+ }
161
+ const path = `/services/${encodeURIComponent(serviceId)}`;
162
+ if (this.dryRun) { this.log(`[dry-run] Render DELETE ${path}`); return { dryRun: true }; }
163
+ return super._req('DELETE', path, null);
164
+ }
165
+
136
166
  async triggerDeploy(serviceId) {
137
167
  await this.getService(serviceId);
138
168
  return this._req('POST', `/services/${encodeURIComponent(serviceId)}/deploys`, {}, { dryRunResult: { id: null } });
@@ -82,4 +82,42 @@ function renderServicePayload(inst, { ownerId, repoUrl, branch, webPlan, region,
82
82
  };
83
83
  }
84
84
 
85
- module.exports = { DEFAULTS, resourceName, plansFor, buildFor, renderPostgresPayload, renderServicePayload };
85
+ // POST /services, for the project's SANDBOX (ADR 0406, task 1005289): the same repository
86
+ // and build as the app, on Render's FREE plan, following `main` on every merge. It runs
87
+ // with BONGOS_SANDBOX=1 so the app (and the starter's outbound gate, task 1005291) knows
88
+ // it is not the live site. `dbUrl` is the sandbox's OWN database, never the live one.
89
+ const SANDBOX_SUFFIX = 'sandbox';
90
+ function sandboxServicePayload(inst, { ownerId, repoUrl, region, build, dbUrl }) {
91
+ const p = renderServicePayload(inst, { ownerId, repoUrl, branch: 'main', webPlan: 'free', region, build, dbUrl });
92
+ p.name = resourceName(inst, SANDBOX_SUFFIX);
93
+ p.autoDeploy = 'yes';
94
+ p.envVars = [{ key: 'BONGOS_SANDBOX', value: '1' }, ...p.envVars];
95
+ return p;
96
+ }
97
+
98
+ // The sandbox's database name inside the app's own database server: the project's slug
99
+ // with underscores, ending `_sandbox`. Checked against a strict shape because it is put
100
+ // into a CREATE / DROP DATABASE statement as an identifier.
101
+ const SANDBOX_DB_RE = /^[a-z][a-z0-9_]{0,40}_sandbox$/;
102
+ function sandboxDbName(inst) {
103
+ let base = resourceName(inst).replace(/-/g, '_').replace(/^[^a-z]+/, '').slice(0, 40);
104
+ if (!base) base = `project_${Number(inst.id) || 0}`;
105
+ const name = `${base}_sandbox`;
106
+ if (!SANDBOX_DB_RE.test(name)) throw new Error(`could not make a safe sandbox database name from '${inst.slug}'`);
107
+ return name;
108
+ }
109
+
110
+ // How an adopted service is built, read off Render's own answer, so the sandbox copies
111
+ // it (ADR 0406 §7: "the sandbox is a new free service copying its build settings").
112
+ // Returns null when the answer does not say enough to copy.
113
+ function buildFromService(svc) {
114
+ const d = (svc && svc.serviceDetails) || {};
115
+ const runtime = d.runtime || d.env;
116
+ if (!RUNTIMES.includes(runtime)) return null;
117
+ if (runtime === 'docker') return { runtime: 'docker' };
118
+ const e = d.envSpecificDetails || {};
119
+ if (!e.startCommand) return null;
120
+ return { runtime, buildCommand: e.buildCommand || BUILD_DEFAULTS[runtime], startCommand: e.startCommand };
121
+ }
122
+
123
+ module.exports = { DEFAULTS, resourceName, plansFor, buildFor, renderPostgresPayload, renderServicePayload, sandboxServicePayload, sandboxDbName, buildFromService, SANDBOX_DB_RE };
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/first-read.js — @bongos-forwarder (task 1004848). Generated by
3
+ // scripts/bongos/codemod-rename-scripts-gds.js; do not edit.
4
+ // The real script is scripts/bongos/first-read.js. This forwarder keeps callers that still name
5
+ // the old path working, and warns once so they get repointed. Task 1003706 removes it.
6
+ 'use strict';
7
+ require('../bongos/forwarder.js').forward(module, 'first-read.js');
@@ -0,0 +1,6 @@
1
+ // scripts/gds/provision-render-sandbox.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-sandbox.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-sandbox.js');
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.16'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.25.17'; // 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');