@bongos/core 1.19.1081 → 1.20.2

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 (253) hide show
  1. package/.bongos-core.json +528 -193
  2. package/.claude/skills/planning-session/SKILL.md +6 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +32 -0
  5. package/clients/bongos-client/index.cjs +32 -0
  6. package/clients/bongos-client/index.d.ts +51 -6
  7. package/clients/bongos-client/index.mjs +32 -0
  8. package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
  9. package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
  10. package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
  11. package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
  12. package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
  13. package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
  14. package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
  15. package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
  16. package/docs/adr/README.md +3 -0
  17. package/docs/api/openapi.json +851 -19
  18. package/docs/api-reference.md +33 -9
  19. package/docs/architecture.md +6 -2
  20. package/docs/copy-inventory.md +620 -553
  21. package/docs/copy-registry.json +1467 -820
  22. package/docs/file-map.md +2 -0
  23. package/docs/module-api-changelog.md +8 -0
  24. package/docs/modules-contract.md +1 -0
  25. package/docs/page-inventory.json +38 -4
  26. package/docs/page-readings.json +1666 -1558
  27. package/migrations/core_260_goals_working_area.sql +63 -0
  28. package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
  29. package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
  30. package/modules/agents/lib/validate.js +43 -0
  31. package/modules/autonomy/gauge.js +38 -2
  32. package/modules/grading/grader-subagent.js +37 -2
  33. package/modules/grading/grader-workers/reader.js +65 -5
  34. package/modules/hall-ui/public/approval-queue.css +33 -7
  35. package/modules/hall-ui/public/approval-queue.js +9 -2
  36. package/modules/hall-ui/public/atlas.html +1 -1
  37. package/modules/hall-ui/public/blockers.html +1 -1
  38. package/modules/hall-ui/public/board-room.html +1 -1
  39. package/modules/hall-ui/public/brand-holes.js +121 -0
  40. package/modules/hall-ui/public/collab.html +1 -1
  41. package/modules/hall-ui/public/collab.js +1 -1
  42. package/modules/hall-ui/public/copy-desk.html +1 -1
  43. package/modules/hall-ui/public/deploy.html +6 -1
  44. package/modules/hall-ui/public/deploy.js +22 -8
  45. package/modules/hall-ui/public/diagrams.html +1 -1
  46. package/modules/hall-ui/public/dom-utils.js +20 -0
  47. package/modules/hall-ui/public/drachmae.html +1 -1
  48. package/modules/hall-ui/public/fleet.html +1 -1
  49. package/modules/hall-ui/public/gate.html +1 -1
  50. package/modules/hall-ui/public/goal-inbox.js +1 -1
  51. package/modules/hall-ui/public/goals.html +1 -1
  52. package/modules/hall-ui/public/government.html +1 -1
  53. package/modules/hall-ui/public/idea.html +1 -1
  54. package/modules/hall-ui/public/idea.js +28 -0
  55. package/modules/hall-ui/public/ideas.html +1 -1
  56. package/modules/hall-ui/public/ideas.js +19 -3
  57. package/modules/hall-ui/public/index.html +5 -1
  58. package/modules/hall-ui/public/modules.html +1 -1
  59. package/modules/hall-ui/public/primer.html +1 -1
  60. package/modules/hall-ui/public/profile-nudge.js +21 -4
  61. package/modules/hall-ui/public/profile.html +1 -1
  62. package/modules/hall-ui/public/profile.js +1 -1
  63. package/modules/hall-ui/public/project-settings.html +1 -1
  64. package/modules/hall-ui/public/ranks.html +1 -1
  65. package/modules/hall-ui/public/roadmap.html +1 -1
  66. package/modules/hall-ui/public/roster.html +1 -1
  67. package/modules/hall-ui/public/sessions.html +1 -1
  68. package/modules/hall-ui/public/settings.html +19 -30
  69. package/modules/hall-ui/public/settings.js +24 -177
  70. package/modules/hall-ui/public/settings.states.json +1 -1
  71. package/modules/hall-ui/public/shell.js +4 -2
  72. package/modules/hall-ui/public/studio.css +14 -8
  73. package/modules/hall-ui/public/studio.html +5 -4
  74. package/modules/hall-ui/public/task.html +1 -1
  75. package/modules/hall-ui/public/thinking.css +6 -3
  76. package/modules/hall-ui/public/thinking.html +1 -1
  77. package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
  78. package/modules/hall-ui/public/tweak-editor.css +142 -11
  79. package/modules/hall-ui/public/tweak-editor.html +25 -9
  80. package/modules/hall-ui/public/tweak-editor.js +243 -10
  81. package/modules/hall-ui/public/watch.html +1 -1
  82. package/modules/hall-ui/public/work.html +1 -1
  83. package/modules/hall-ui/records/approval-queue.md +6 -0
  84. package/modules/hall-ui/records/tweak-editor.md +6 -0
  85. package/modules/ideas/ratify.js +36 -8
  86. package/modules/ideas/routes/ratify-goal.js +6 -0
  87. package/modules/lifecycle/db-analytics.js +79 -10
  88. package/modules/lifecycle/db-claims.js +48 -1
  89. package/modules/lifecycle/db-deps-criteria.js +94 -40
  90. package/modules/lifecycle/db-goals.js +4 -1
  91. package/modules/lifecycle/db-grade.js +9 -1
  92. package/modules/lifecycle/db-relevance-flags.js +183 -0
  93. package/modules/lifecycle/db-versions.js +4 -3
  94. package/modules/lifecycle/db.js +19 -0
  95. package/modules/lifecycle/done-when.js +5 -3
  96. package/modules/lifecycle/est-advisory.js +31 -4
  97. package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
  98. package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
  99. package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
  100. package/modules/lifecycle/module.json +2 -1
  101. package/modules/lifecycle/routes/claims.js +8 -1
  102. package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
  103. package/modules/lifecycle/routes/goals.js +1 -1
  104. package/modules/lifecycle/routes/tasks.js +27 -24
  105. package/modules/lifecycle/routes/visuals.js +9 -0
  106. package/modules/lifecycle/ship-preflight.js +7 -11
  107. package/modules/lifecycle/task-visuals.js +109 -5
  108. package/modules/npm-release/module.json +2 -1
  109. package/modules/npm-release/preview/commands.js +81 -0
  110. package/modules/npm-release/preview/divert.js +95 -0
  111. package/modules/npm-release/preview/env.js +78 -0
  112. package/modules/npm-release/preview/proxy.js +107 -0
  113. package/modules/npm-release/preview/runtime.js +83 -0
  114. package/modules/npm-release/preview/supervisor.js +175 -0
  115. package/modules/npm-release/public/work.js +65 -1
  116. package/modules/npm-release/routes/preview.js +133 -0
  117. package/modules/provisioning/app-status.js +83 -0
  118. package/modules/provisioning/module.json +2 -1
  119. package/modules/provisioning/pollers/app-liveness.js +110 -0
  120. package/modules/provisioning/render-lookup.js +68 -0
  121. package/modules/provisioning/render-standup.js +44 -1
  122. package/modules/provisioning/routes/render-standup.js +81 -6
  123. package/modules/provisioning/starter-bundles.js +4 -1
  124. package/modules/public-landing/public/account-private.states.json +14 -0
  125. package/modules/public-landing/public/account.html +513 -0
  126. package/modules/public-landing/public/account.probes.json +37 -0
  127. package/modules/public-landing/public/account.states.json +16 -0
  128. package/modules/public-landing/public/index.html +5 -2
  129. package/modules/public-landing/public/projects.html +661 -18
  130. package/modules/public-landing/public/projects.states.json +8 -1
  131. package/modules/render-deploy/deploys.js +203 -0
  132. package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
  133. package/modules/render-deploy/module.json +25 -0
  134. package/modules/render-deploy/public/deploy.css +10 -0
  135. package/modules/render-deploy/public/deploy.js +249 -0
  136. package/modules/render-deploy/render.js +56 -0
  137. package/modules/render-deploy/routes/act.js +88 -0
  138. package/modules/render-deploy/routes/app.js +65 -0
  139. package/modules/render-deploy/routes/history.js +38 -0
  140. package/modules/specialities/db.js +27 -8
  141. package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
  142. package/modules/specialities/routes/specialities.js +51 -3
  143. package/modules/specialities/skills.js +94 -0
  144. package/modules/specialities/specialities.js +56 -9
  145. package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
  146. package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
  147. package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
  148. package/modules/ui-design/kit/lib.js +3 -1
  149. package/modules/ui-design/kit/serve.js +96 -0
  150. package/package-lock.json +2 -2
  151. package/package.json +1 -1
  152. package/release-notes.json +169 -0
  153. package/scripts/gds/agents-sync.js +13 -3
  154. package/scripts/gds/autobongos-run.js +80 -2
  155. package/scripts/gds/autobongos-service.cmd +12 -0
  156. package/scripts/gds/copy-apply.js +14 -0
  157. package/scripts/gds/dev-box-guard.js +3 -2
  158. package/scripts/gds/fitness.js +9 -0
  159. package/scripts/gds/grade-correlation-audit.js +42 -12
  160. package/scripts/gds/grade-replay.js +1 -1
  161. package/scripts/gds/provision-core-upgrade.js +38 -7
  162. package/scripts/gds/provision-pin-key.js +176 -0
  163. package/scripts/gds/provision-render.js +62 -31
  164. package/scripts/gds/provision-teardown.js +7 -2
  165. package/scripts/gds/provision-units.js +31 -0
  166. package/scripts/gds/provision.js +10 -10
  167. package/scripts/gds/publish-manifest.js +1 -0
  168. package/scripts/gds/render-api.js +19 -0
  169. package/scripts/gds/render-payload.js +39 -65
  170. package/scripts/gds/run-unit-tests.js +10 -0
  171. package/scripts/gds/ship-finish.js +19 -20
  172. package/scripts/gds/smoke-dependencies.sh +37 -8
  173. package/scripts/gds/status.js +12 -4
  174. package/scripts/gds/upgrade.js +2 -2
  175. package/scripts/public-mirror-export.js +7 -1
  176. package/src/bongos/module-scope-map.js +13 -0
  177. package/src/bongos/serve-internal.js +3 -1
  178. package/src/module-api.js +10 -1
  179. package/src/platform-server.js +39 -0
  180. package/tests/account_privacy_flags.mjs +10 -6
  181. package/tests/agents_authoring.mjs +15 -1
  182. package/tests/agents_sync.mjs +106 -5
  183. package/tests/agents_validate.mjs +45 -0
  184. package/tests/api_path_404.mjs +6 -0
  185. package/tests/autobongos_cadence.mjs +7 -0
  186. package/tests/autobongos_loop.mjs +136 -1
  187. package/tests/autonomy_gauge.mjs +62 -0
  188. package/tests/blocker_hall_live_proof.mjs +275 -0
  189. package/tests/claim_gate_ci_unblock.mjs +116 -0
  190. package/tests/claim_gate_rebase.mjs +15 -3
  191. package/tests/collab_page.mjs +10 -0
  192. package/tests/conductor_main_e2e.mjs +97 -0
  193. package/tests/core_262_drop_box_blocked_db.mjs +143 -0
  194. package/tests/core_upgrade_runner.mjs +1 -0
  195. package/tests/dependency_writes_atomic.mjs +191 -0
  196. package/tests/deploy_page_projects.mjs +2 -1
  197. package/tests/effective_visibility_predicate.mjs +15 -0
  198. package/tests/est_advisory.mjs +27 -0
  199. package/tests/goal_map_page.mjs +26 -0
  200. package/tests/goal_task_relevance.mjs +518 -0
  201. package/tests/goal_working_area.mjs +146 -0
  202. package/tests/grade_attempts.mjs +61 -0
  203. package/tests/grade_attribution.mjs +23 -0
  204. package/tests/grade_correlation_audit.mjs +54 -1
  205. package/tests/grader_reader_lens.mjs +55 -2
  206. package/tests/grader_root_outage.mjs +6 -1
  207. package/tests/grader_subagent_tools_arg.mjs +102 -0
  208. package/tests/hall_approval_queue.mjs +54 -2
  209. package/tests/hall_settings_world.mjs +3 -1
  210. package/tests/hall_tweak_editor.mjs +316 -11
  211. package/tests/hub_account_page.mjs +394 -0
  212. package/tests/idea_detail_page.mjs +64 -0
  213. package/tests/idea_goal_ratification.mjs +64 -0
  214. package/tests/idea_spark_hall.mjs +60 -9
  215. package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
  216. package/tests/landing_page.mjs +6 -3
  217. package/tests/module-scope-map.mjs +31 -8
  218. package/tests/module_api.mjs +1 -0
  219. package/tests/module_loader.mjs +1 -1
  220. package/tests/nav_permission_atoms.mjs +1 -1
  221. package/tests/no_phantom_mirror_workflow.mjs +24 -0
  222. package/tests/npm_release_preview_commands.mjs +89 -0
  223. package/tests/npm_release_preview_divert.mjs +164 -0
  224. package/tests/npm_release_preview_env.mjs +100 -0
  225. package/tests/npm_release_preview_proxy.mjs +162 -0
  226. package/tests/npm_release_preview_routes.mjs +208 -0
  227. package/tests/npm_release_preview_supervisor.mjs +203 -0
  228. package/tests/pin_write_key.mjs +383 -0
  229. package/tests/planning_session_skill.mjs +27 -0
  230. package/tests/platform_boot.mjs +71 -0
  231. package/tests/profile_nudge_links.mjs +86 -0
  232. package/tests/profile_ui_cross_project.mjs +1 -1
  233. package/tests/projects_hub.mjs +2 -2
  234. package/tests/projects_hub_app_status.mjs +249 -0
  235. package/tests/projects_hub_app_step.mjs +282 -52
  236. package/tests/projects_hub_render_connect.mjs +295 -0
  237. package/tests/provision_render.mjs +121 -28
  238. package/tests/provision_settings_apply.mjs +62 -0
  239. package/tests/provisioning_app_status.mjs +204 -0
  240. package/tests/provisioning_render_route.mjs +126 -6
  241. package/tests/render_check.mjs +2 -1
  242. package/tests/render_deploy.mjs +495 -0
  243. package/tests/rot_inbox.mjs +8 -0
  244. package/tests/ship_preflight.mjs +61 -8
  245. package/tests/smoke_dependencies_witness.mjs +46 -0
  246. package/tests/speciality_skills.mjs +214 -0
  247. package/tests/studio_room_height.mjs +70 -0
  248. package/tests/task_detail_includes.mjs +10 -0
  249. package/tests/task_visuals_instance_root.mjs +150 -0
  250. package/tests/tweak_batch_apply.mjs +21 -0
  251. package/tests/ui_design_kit.mjs +36 -4
  252. package/tests/upgrade.mjs +1 -1
  253. package/tests/upgrade_persist_pin.mjs +24 -0
@@ -0,0 +1,68 @@
1
+ // modules/provisioning/render-lookup.js — the ONE Render call the web tier may make: a
2
+ // read-only look at what an owner's key can see (task 1004352; ADR 0348).
3
+ //
4
+ // The setup wizard has to tell the owner at once when a key is refused, and has to make
5
+ // them choose among the workspaces the key reaches (a Render key belongs to a USER and
6
+ // reaches every workspace that user is in — ADR 0327). Only a call to Render can answer
7
+ // either, and the owner is still on the page. So ADR 0348 makes one narrow exception to
8
+ // ADR 0111 §2 ("the web tier never calls a cloud API"), and this file is all of it:
9
+ //
10
+ // - GET only — two calls, the workspace list and the services in ONE named workspace.
11
+ // Nothing here can create, change or delete anything, and there is no way to ask it to.
12
+ // - the key is the owner's own, held in memory for this one request; it is never
13
+ // stored, logged or returned, and it is not encrypted here because it is not kept.
14
+ // - every service in the answer is asserted to carry the workspace asked about — the
15
+ // same guard the runner's client applies (scripts/gds/render-api.js). A module may
16
+ // not require that file (ADR 0083), so this carries its own two GETs rather than a
17
+ // second client.
18
+ //
19
+ // `fetchImpl` is injectable so tests never touch the network.
20
+ 'use strict';
21
+
22
+ const RENDER_API = 'https://api.render.com/v1';
23
+ const TIMEOUT_MS = 10_000;
24
+
25
+ class RenderLookupError extends Error {
26
+ constructor(code, message) { super(message); this.code = code; }
27
+ }
28
+
29
+ async function get(path, key, fetchImpl) {
30
+ let res;
31
+ try {
32
+ res = await fetchImpl(`${RENDER_API}${path}`, {
33
+ method: 'GET',
34
+ headers: { Authorization: `Bearer ${key}`, Accept: 'application/json' },
35
+ signal: AbortSignal.timeout(TIMEOUT_MS),
36
+ });
37
+ } catch (_) {
38
+ throw new RenderLookupError('render_unreachable', 'Render didn’t answer. Try again in a moment.');
39
+ }
40
+ if (res.status === 401 || res.status === 403) {
41
+ throw new RenderLookupError('render_key_refused', 'Render refused that key. Check you copied all of it, and that it hasn’t been deleted.');
42
+ }
43
+ if (!res.ok) throw new RenderLookupError('render_unreachable', `Render answered ${res.status}. Try again in a moment.`);
44
+ try { return await res.json(); } catch (_) { throw new RenderLookupError('render_unreachable', 'Render sent an answer we couldn’t read.'); }
45
+ }
46
+
47
+ const unwrap = (json, field) => (Array.isArray(json) ? json : []).map((e) => (e && e[field]) || e).filter((o) => o && typeof o.id === 'string');
48
+
49
+ // What the key can see: every workspace, and — when `ownerId` names one of them — the
50
+ // web services in it. Throws RenderLookupError with a code the route maps to a reply.
51
+ async function lookupRender(key, { ownerId = null, fetchImpl = globalThis.fetch } = {}) {
52
+ const workspaces = unwrap(await get('/owners?limit=100', key, fetchImpl), 'owner')
53
+ .map((o) => ({ id: o.id, name: String(o.name || o.email || o.id), type: o.type === 'user' ? 'user' : 'team' }));
54
+ if (!ownerId) return { workspaces };
55
+ if (!workspaces.some((w) => w.id === ownerId)) {
56
+ throw new RenderLookupError('render_workspace_unreachable', 'That key can’t reach the workspace you chose.');
57
+ }
58
+ const services = unwrap(await get(`/services?ownerId=${encodeURIComponent(ownerId)}&type=web_service&limit=100`, key, fetchImpl), 'service')
59
+ .map((s) => {
60
+ if (s.ownerId !== ownerId) {
61
+ throw new RenderLookupError('render_owner_mismatch', 'Render answered with a service from another workspace, so nothing was shown.');
62
+ }
63
+ return { id: s.id, name: String(s.name || s.id), repo: s.repo || null, url: (s.serviceDetails && s.serviceDetails.url) || null };
64
+ });
65
+ return { workspaces, services };
66
+ }
67
+
68
+ module.exports = { lookupRender, RenderLookupError, RENDER_API };
@@ -10,6 +10,12 @@
10
10
  // compromised web tier — the key arrives here in plaintext before anything can encrypt
11
11
  // it. Nothing here stores the key anywhere else, logs it, or sends it back.
12
12
  //
13
+ // A PROJECT STILL BEING SET UP. The wizard sends the key right after Create, while the
14
+ // project's own `provision` run holds the one open-intent slot. Rather than refuse, the key
15
+ // rides THAT run (task 1004352): the runner sets the app up on Render straight after the
16
+ // project is up, in the same drain, and clears the key either way
17
+ // (scripts/gds/provision-render.js afterProvision).
18
+ //
13
19
  // Its own file because provisioning.js sits at the size ratchet.
14
20
  'use strict';
15
21
 
@@ -19,6 +25,9 @@ const WEB_PLANS = ['starter', 'standard', 'pro', 'pro_plus', 'pro_max', 'pro_ult
19
25
  const DB_PLANS = ['basic_256mb', 'basic_1gb', 'basic_4gb', 'pro_4gb', 'pro_8gb'];
20
26
  const REGIONS = ['virginia', 'ohio', 'oregon', 'frankfurt', 'singapore'];
21
27
  const MODES = ['adopt', 'greenfield'];
28
+ // How the owner's app is built (task 1004350): a native runtime with their commands, or
29
+ // `docker`, which builds the repository's own Dockerfile.
30
+ const RUNTIMES = ['node', 'python', 'ruby', 'go', 'rust', 'elixir', 'docker'];
22
31
 
23
32
  // Render API keys start `rnd_`; workspaces are `tea-…` (a team) or `usr-…` (a personal
24
33
  // one); services `srv-…`. Shape checks only — whether the key works, and whether it can
@@ -36,9 +45,16 @@ function badRequest(body) {
36
45
  if (!MODES.includes(b.mode)) return 'Say whether to link a Render service you already have, or create a new one.';
37
46
  if (b.mode === 'adopt' && !SERVICE_RE.test(String(b.service_id || ''))) return 'Choose the Render service to link. Its id starts with “srv-”.';
38
47
  if (b.mode === 'greenfield' && b.service_id) return 'A new setup creates its own service, so leave the service out.';
48
+ if (b.mode === 'adopt' && (b.runtime != null || b.build_command != null || b.start_command != null || b.database != null)) return 'Linking keeps your Render service exactly as it is, so leave out how it is built.';
49
+ if (b.mode === 'greenfield') {
50
+ if (!RUNTIMES.includes(b.runtime)) return 'Say what your app is built with — for example Node or Python, or Docker to use your repository’s Dockerfile.';
51
+ if (b.runtime === 'docker' && (b.build_command || b.start_command)) return 'Docker builds and starts your app from your Dockerfile, so leave the commands out.';
52
+ if (b.runtime !== 'docker' && !String(b.start_command || '').trim()) return 'Say how your app starts — the command Render should run, for example “npm start”.';
53
+ }
39
54
  if (b.web_plan != null && !WEB_PLANS.includes(b.web_plan)) return `“${b.web_plan}” is not a Render web plan.`;
40
55
  if (b.db_plan != null && !DB_PLANS.includes(b.db_plan)) return `“${b.db_plan}” is not a Render database plan.`;
41
56
  if (b.region != null && !REGIONS.includes(b.region)) return `“${b.region}” is not a Render region.`;
57
+ if (b.db_plan != null && b.database !== true) return 'A database plan needs a database — ask for one, or leave the plan out.';
42
58
  return null;
43
59
  }
44
60
 
@@ -46,6 +62,11 @@ function badRequest(body) {
46
62
  function appHostFrom(body) {
47
63
  const h = { platform: 'render', mode: body.mode, ownerId: body.owner_id, state: 'requested', error: null };
48
64
  if (body.mode === 'adopt') h.serviceId = body.service_id;
65
+ if (body.mode === 'greenfield') {
66
+ h.runtime = body.runtime;
67
+ if (body.runtime !== 'docker') { h.startCommand = body.start_command.trim(); if (body.build_command) h.buildCommand = body.build_command.trim(); }
68
+ h.database = body.database === true;
69
+ }
49
70
  if (body.web_plan) h.webPlan = body.web_plan;
50
71
  if (body.db_plan) h.dbPlan = body.db_plan;
51
72
  if (body.region) h.region = body.region;
@@ -79,6 +100,8 @@ async function enqueueRenderStandup(db, instanceId, { keyEnc, appHost, requested
79
100
  return { intent: rows[0] };
80
101
  } catch (e) {
81
102
  if (e && e.code === '23505') {
103
+ const attached = await attachToProvision(db, instanceId, { keyEnc, appHost });
104
+ if (attached) return { intent: attached, attached: true };
82
105
  const { rows } = await db.query(
83
106
  `SELECT id, action, state FROM provisioning_intents
84
107
  WHERE instance_id = $1 AND state IN ('pending', 'running') ORDER BY created_at DESC LIMIT 1`, [instanceId]);
@@ -88,4 +111,24 @@ async function enqueueRenderStandup(db, instanceId, { keyEnc, appHost, requested
88
111
  }
89
112
  }
90
113
 
91
- module.exports = { WEB_PLANS, DB_PLANS, REGIONS, MODES, badRequest, appHostFrom, enqueueRenderStandup };
114
+ // Put the key on the project's OPEN provision run, and record the request on the row, in
115
+ // one statement. Only a run that holds no key yet: a second request never overwrites the
116
+ // first. Returns the intent, or null when there is no such run (the caller then reports
117
+ // the conflict as usual). The table's CHECK still refuses a key on a resolved intent, so
118
+ // a run that finished a moment ago cannot be handed one.
119
+ async function attachToProvision(db, instanceId, { keyEnc, appHost }) {
120
+ const { rows } = await db.query(
121
+ `WITH i AS (
122
+ UPDATE provisioning_intents SET borrowed_key_enc = $2, updated_at = now()
123
+ WHERE instance_id = $1 AND action = 'provision' AND state IN ('pending', 'running') AND borrowed_key_enc IS NULL
124
+ RETURNING id, instance_id, action, state, created_at
125
+ ), u AS (
126
+ UPDATE provisioning_instances SET app_host = $3::jsonb
127
+ WHERE id = $1 AND EXISTS (SELECT 1 FROM i) RETURNING id
128
+ )
129
+ SELECT * FROM i`,
130
+ [instanceId, keyEnc, JSON.stringify(appHost)]);
131
+ return rows[0] || null;
132
+ }
133
+
134
+ module.exports = { attachToProvision, KEY_RE, OWNER_RE, WEB_PLANS, DB_PLANS, REGIONS, MODES, RUNTIMES, badRequest, appHostFrom, enqueueRenderStandup };
@@ -2,7 +2,8 @@
2
2
  // (task 1004183; ADR 0327, ADR 0345).
3
3
  //
4
4
  // The owner lends their Render API key once, names the workspace, and says whether to link
5
- // a service they already run (adopt) or create a new one (greenfield). This route checks
5
+ // a service they already run for this repository (adopt) or create a new one for their app
6
+ // (greenfield: how it is built and started, and whether it needs a database — task 1004350). This route checks
6
7
  // the shape, encrypts the key with the secret box, and queues a `render-standup` intent;
7
8
  // the runner (scripts/gds/provision-render.js) spends the key in one drain and clears it.
8
9
  // This process never calls Render (ADR 0111 §2).
@@ -22,6 +23,10 @@ const { pool, validateOrRespond, parseId } = api;
22
23
  const provisioning = require('../provisioning');
23
24
  const renderStandup = require('../render-standup');
24
25
  const { failFrom } = require('../public-refusal');
26
+ const { lookupRender, RenderLookupError } = require('../render-lookup');
27
+ const { appStatus } = require('../app-status');
28
+ const envManifest = require('../env-manifest');
29
+ const { makeAskRateLimiter } = require('../rate-limit');
25
30
 
26
31
  const log = api.logger('provisioning');
27
32
 
@@ -29,12 +34,78 @@ const log = api.logger('provisioning');
29
34
  // before lending a key. The runner keeps its own checks.
30
35
  function unavailableReason(inst, mode) {
31
36
  if (inst.status === 'torn_down' || inst.status === 'tearing_down') return 'This project has been removed, so it can’t be set up on Render.';
32
- if (mode === 'greenfield' && !inst.target_ref) return 'This project has no GitHub repository linked, so Render has nothing to build.';
37
+ if (!inst.target_ref) return mode === 'adopt' ? 'This project has no GitHub repository linked, so there is no app to check the Render service against.' : 'This project has no GitHub repository linked, so Render has nothing to build.';
33
38
  return null;
34
39
  }
35
40
 
36
- module.exports = function buildRenderStandupRouter() {
41
+ // The lookup is a Render call made on the owner's behalf, so it is capped per builder:
42
+ // a wizard needs a handful (check the key, then one workspace or two), never a stream.
43
+ const LOOKUP_STATUS = { render_key_refused: 422, render_workspace_unreachable: 422, render_owner_mismatch: 502, render_unreachable: 502 };
44
+
45
+ // OWNER ONLY, no archon override, for every Render route: the key and the account it acts
46
+ // in are the owner's. A missing project and someone else's answer the same 404, so an id is
47
+ // never confirmed.
48
+ function ownedBy(inst, builder) { return !!(inst && builder && String(inst.owner_builder_id) === String(builder.id)); }
49
+
50
+ module.exports = function buildRenderStandupRouter({ fetchImpl } = {}) {
37
51
  const router = express.Router();
52
+ const lookupLimit = makeAskRateLimiter({ windowMs: 60_000, max: 20, key: (req) => `b:${req.builder && req.builder.id}` });
53
+ const statusLimit = makeAskRateLimiter({ windowMs: 60_000, max: 60, key: (req) => `s:${req.builder && req.builder.id}` });
54
+
55
+ // POST /provisioning/render/lookup — rank: any authenticated builder. What the owner's
56
+ // Render key can see: its workspaces, and the web services in the one they name. The
57
+ // ONLY Render call this process makes (ADR 0348): GET only, the key held for this
58
+ // request and never stored, logged or sent back. POST so the key travels in a body,
59
+ // never a URL.
60
+ router.post('/provisioning/render/lookup', api.requireBuilder, lookupLimit, async (req, res) => {
61
+ if (validateOrRespond(req, res, {
62
+ key: { required: true, type: 'string', maxLength: 200 },
63
+ owner_id: { type: 'string', maxLength: 80 },
64
+ })) return;
65
+ if (!renderStandup.KEY_RE.test(req.body.key)) {
66
+ return res.fail('render_request_invalid', { status: 400, message: 'That doesn’t look like a Render API key. It starts with “rnd_”.' });
67
+ }
68
+ if (req.body.owner_id != null && !renderStandup.OWNER_RE.test(req.body.owner_id)) {
69
+ return res.fail('render_request_invalid', { status: 400, message: 'That isn’t a Render workspace id.' });
70
+ }
71
+ try {
72
+ const out = await lookupRender(req.body.key, { ownerId: req.body.owner_id || null, fetchImpl: fetchImpl || globalThis.fetch });
73
+ res.json({ ok: true, ...out });
74
+ } catch (err) {
75
+ if (err instanceof RenderLookupError) return res.fail(err.code, { status: LOOKUP_STATUS[err.code] || 502, message: err.message });
76
+ log.error('[provisioning] POST /provisioning/render/lookup', err && err.message);
77
+ failFrom(res, err, 'render_lookup_failed');
78
+ }
79
+ });
80
+
81
+ // GET /provisioning/instances/:id/render — rank: any authenticated builder, and OWNER
82
+ // ONLY, like the POST (task 1004353). Where the project's app setup on Render stands:
83
+ // the stage, the app's address, the Render links, a failure as one sentence with the
84
+ // answers a Retry re-sends, and the env var NAMES still to set there (task 1004062's
85
+ // manifest — never a value; this platform holds none). Read from the row alone: nothing
86
+ // here calls Render, and there is no key to hold. `app` is null when the project's app
87
+ // has no platform chosen.
88
+ router.get('/provisioning/instances/:id/render', api.requireBuilder, statusLimit, async (req, res) => {
89
+ const id = parseId(req, res, { positiveInt: true });
90
+ if (!id) return;
91
+ try {
92
+ const inst = await provisioning.getInstanceById(pool, id);
93
+ if (!ownedBy(inst, req.builder)) return res.fail('instance_not_found', 404);
94
+ // A run that still carries the lent key has not spent it yet: the owner waits.
95
+ const { rows } = await pool.query(
96
+ `SELECT EXISTS (SELECT 1 FROM provisioning_intents WHERE instance_id = $1 AND state IN ('pending', 'running') AND borrowed_key_enc IS NOT NULL) AS waiting`,
97
+ [id]);
98
+ const app = appStatus(inst.app_host, { keyWaiting: !!(rows[0] && rows[0].waiting) });
99
+ const dbSet = !!(inst.app_host && inst.app_host.database === true);
100
+ const envVars = app && app.state !== 'disconnected'
101
+ ? (await envManifest.listEnvVars(pool, id)).map((v) => ({ name: v.name, required: v.required !== false, set_for_you: dbSet && v.name === 'DATABASE_URL' }))
102
+ : [];
103
+ res.json({ ok: true, app, env_vars: envVars, values_held: false });
104
+ } catch (err) {
105
+ log.error('[provisioning] GET /provisioning/instances/:id/render', err && err.message);
106
+ failFrom(res, err, 'render_status_failed');
107
+ }
108
+ });
38
109
 
39
110
  // POST /provisioning/instances/:id/render — rank: any authenticated builder, and OWNER
40
111
  // ONLY: the key is the owner's and so is the account it acts in, so an archon on
@@ -51,10 +122,14 @@ module.exports = function buildRenderStandupRouter() {
51
122
  web_plan: { type: 'string', maxLength: 40 },
52
123
  db_plan: { type: 'string', maxLength: 40 },
53
124
  region: { type: 'string', maxLength: 40 },
125
+ runtime: { type: 'string', maxLength: 20 },
126
+ build_command: { type: 'string', maxLength: 500 },
127
+ start_command: { type: 'string', maxLength: 500 },
128
+ database: { type: 'boolean' },
54
129
  })) return;
55
130
  try {
56
131
  const inst = await provisioning.getInstanceById(pool, id);
57
- if (!inst || String(inst.owner_builder_id) !== String(req.builder.id)) return res.fail('instance_not_found', 404);
132
+ if (!ownedBy(inst, req.builder)) return res.fail('instance_not_found', 404);
58
133
  const bad = renderStandup.badRequest(req.body);
59
134
  if (bad) return res.fail('render_request_invalid', { status: 400, message: bad });
60
135
  const why = unavailableReason(inst, req.body.mode);
@@ -75,10 +150,10 @@ module.exports = function buildRenderStandupRouter() {
75
150
  instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'render-standup',
76
151
  detail: `Render ${appHost.mode === 'adopt' ? 'link' : 'setup'} requested by owner (workspace ${appHost.ownerId})`, actor: 'api:self',
77
152
  }).catch(() => {});
78
- res.json({ ok: true, queued: true, action: 'render-standup', mode: appHost.mode,
153
+ res.json({ ok: true, queued: true, action: 'render-standup', mode: appHost.mode, with_setup: !!r.attached,
79
154
  message: appHost.mode === 'adopt'
80
155
  ? 'Queued. The platform will check the service in your Render account and link it, without changing anything there. Your key is used once and then thrown away.'
81
- : 'Queued. The platform will create your database and web service in your Render account. Your key is used once and then thrown away; Render bills you directly.' });
156
+ : `Queued. The platform will create ${appHost.database ? 'a database and ' : ''}a web service for your app in your Render account. Your key is used once and then thrown away; Render bills you directly.` });
82
157
  } catch (err) {
83
158
  // Never log the request: it carries the key.
84
159
  log.error('[provisioning] POST /provisioning/instances/:id/render', err && err.message);
@@ -73,7 +73,10 @@ const ALWAYS_ON_CORE = Object.freeze([
73
73
  // the hub creates receives those releases, so none is offered the publisher's page. It
74
74
  // is package-agnostic by construction, so offering it to a project that publishes its
75
75
  // own npm package is a later, deliberate removal from this list — with picker copy.
76
- const HUB_ONLY_MODULES = Object.freeze(['platform-identity', 'provisioning', 'npm-release']);
76
+ // `render-deploy` (task 1004270) is the deploy page of a project whose app deploys through
77
+ // Render: it is turned on by the connect-Render step, never chosen in this picker, so it is
78
+ // kept out of the opt-in catalog by the same list.
79
+ const HUB_ONLY_MODULES = Object.freeze(['platform-identity', 'provisioning', 'npm-release', 'render-deploy']);
77
80
 
78
81
  // The OPT-IN catalog as the creation picker renders it (BV1.R20, task 1002339):
79
82
  // every module an owner may actually choose, in the order the picker lists them,
@@ -0,0 +1,14 @@
1
+ {
2
+ "_": "The account page for a private account with its record hidden and no published profile yet (task 1004259): the recruiter copy shrinks to name and picture and says it is the one exception to 'not found', the record line says nothing has been reported and that the profile is not published, and the empty profile fields show their placeholders. stub.fixtures points the account read at fixtures/me-cross-project-private.json.",
3
+ "page": "/account",
4
+ "surface": "public-landing",
5
+ "stub": { "fixtures": { "me__cross-project": "me-cross-project-private" } },
6
+ "modes": ["dark"],
7
+ "modeQuery": false,
8
+ "ignoreRequests": ["/api/(gds|bongos)/me/cross-project$"],
9
+ "ignoreConsole": ["/api/(gds|bongos)/me/cross-project", "\\b401\\b", "Unauthorized"],
10
+ "ignoreControls": [".vh"],
11
+ "states": {
12
+ "in-private": { "auth": true, "actions": [["wait", 400]], "expect": { "visible": ["#acctBody", "#recWhat"], "checked": "input[name=visibility][value=private]", "value": [["#fName", ""], ["#fBio", ""]], "text": [["#recordLine", "No cross-project activity reported yet. Your profile is not published yet; it publishes after your next sign-in."]] } }
13
+ }
14
+ }