@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.
- package/.bongos-core.json +528 -193
- package/.claude/skills/planning-session/SKILL.md +6 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +32 -0
- package/clients/bongos-client/index.cjs +32 -0
- package/clients/bongos-client/index.d.ts +51 -6
- package/clients/bongos-client/index.mjs +32 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +5 -1
- package/docs/adr/0111-instance-hosting-provisioning-module.md +1 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +17 -0
- package/docs/adr/0176-private-repo-deploy-keys.md +2 -0
- package/docs/adr/0310-a-speciality-offers-skills-and-the-adopter-chooses-them.md +1 -1
- package/docs/adr/0348-the-web-tier-may-look-read-only-at-what-an-owners-render-key-can-see.md +53 -0
- package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +106 -0
- package/docs/adr/0350-the-hub-holds-a-per-project-write-deploy-key-so-a-hosted-upgrade-reaches-github.md +103 -0
- package/docs/adr/README.md +3 -0
- package/docs/api/openapi.json +851 -19
- package/docs/api-reference.md +33 -9
- package/docs/architecture.md +6 -2
- package/docs/copy-inventory.md +620 -553
- package/docs/copy-registry.json +1467 -820
- package/docs/file-map.md +2 -0
- package/docs/module-api-changelog.md +8 -0
- package/docs/modules-contract.md +1 -0
- package/docs/page-inventory.json +38 -4
- package/docs/page-readings.json +1666 -1558
- package/migrations/core_260_goals_working_area.sql +63 -0
- package/migrations/core_261_grade_attempts_verdicts.sql +24 -0
- package/migrations/core_262_drop_builder_box_blocked.sql +33 -0
- package/modules/agents/lib/validate.js +43 -0
- package/modules/autonomy/gauge.js +38 -2
- package/modules/grading/grader-subagent.js +37 -2
- package/modules/grading/grader-workers/reader.js +65 -5
- package/modules/hall-ui/public/approval-queue.css +33 -7
- package/modules/hall-ui/public/approval-queue.js +9 -2
- 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/brand-holes.js +121 -0
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/deploy.html +6 -1
- package/modules/hall-ui/public/deploy.js +22 -8
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/dom-utils.js +20 -0
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-inbox.js +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/idea.html +1 -1
- package/modules/hall-ui/public/idea.js +28 -0
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/ideas.js +19 -3
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile-nudge.js +21 -4
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/profile.js +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings.html +19 -30
- package/modules/hall-ui/public/settings.js +24 -177
- package/modules/hall-ui/public/settings.states.json +1 -1
- package/modules/hall-ui/public/shell.js +4 -2
- package/modules/hall-ui/public/studio.css +14 -8
- package/modules/hall-ui/public/studio.html +5 -4
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.css +6 -3
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +82 -1
- package/modules/hall-ui/public/tweak-editor.css +142 -11
- package/modules/hall-ui/public/tweak-editor.html +25 -9
- package/modules/hall-ui/public/tweak-editor.js +243 -10
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/modules/hall-ui/records/approval-queue.md +6 -0
- package/modules/hall-ui/records/tweak-editor.md +6 -0
- package/modules/ideas/ratify.js +36 -8
- package/modules/ideas/routes/ratify-goal.js +6 -0
- package/modules/lifecycle/db-analytics.js +79 -10
- package/modules/lifecycle/db-claims.js +48 -1
- package/modules/lifecycle/db-deps-criteria.js +94 -40
- package/modules/lifecycle/db-goals.js +4 -1
- package/modules/lifecycle/db-grade.js +9 -1
- package/modules/lifecycle/db-relevance-flags.js +183 -0
- package/modules/lifecycle/db-versions.js +4 -3
- package/modules/lifecycle/db.js +19 -0
- package/modules/lifecycle/done-when.js +5 -3
- package/modules/lifecycle/est-advisory.js +31 -4
- package/modules/lifecycle/goal-task-relevance-advisory.js +153 -0
- package/modules/lifecycle/goal-task-relevance-judge.js +226 -0
- package/modules/lifecycle/migrations/lifecycle_014_goal_task_relevance_flags.sql +82 -0
- package/modules/lifecycle/module.json +2 -1
- package/modules/lifecycle/routes/claims.js +8 -1
- package/modules/lifecycle/routes/goal-task-relevance.js +72 -0
- package/modules/lifecycle/routes/goals.js +1 -1
- package/modules/lifecycle/routes/tasks.js +27 -24
- package/modules/lifecycle/routes/visuals.js +9 -0
- package/modules/lifecycle/ship-preflight.js +7 -11
- package/modules/lifecycle/task-visuals.js +109 -5
- package/modules/npm-release/module.json +2 -1
- package/modules/npm-release/preview/commands.js +81 -0
- package/modules/npm-release/preview/divert.js +95 -0
- package/modules/npm-release/preview/env.js +78 -0
- package/modules/npm-release/preview/proxy.js +107 -0
- package/modules/npm-release/preview/runtime.js +83 -0
- package/modules/npm-release/preview/supervisor.js +175 -0
- package/modules/npm-release/public/work.js +65 -1
- package/modules/npm-release/routes/preview.js +133 -0
- package/modules/provisioning/app-status.js +83 -0
- package/modules/provisioning/module.json +2 -1
- package/modules/provisioning/pollers/app-liveness.js +110 -0
- package/modules/provisioning/render-lookup.js +68 -0
- package/modules/provisioning/render-standup.js +44 -1
- package/modules/provisioning/routes/render-standup.js +81 -6
- package/modules/provisioning/starter-bundles.js +4 -1
- package/modules/public-landing/public/account-private.states.json +14 -0
- package/modules/public-landing/public/account.html +513 -0
- package/modules/public-landing/public/account.probes.json +37 -0
- package/modules/public-landing/public/account.states.json +16 -0
- package/modules/public-landing/public/index.html +5 -2
- package/modules/public-landing/public/projects.html +661 -18
- package/modules/public-landing/public/projects.states.json +8 -1
- package/modules/render-deploy/deploys.js +203 -0
- package/modules/render-deploy/migrations/render_deploy_001_app.sql +22 -0
- package/modules/render-deploy/module.json +25 -0
- package/modules/render-deploy/public/deploy.css +10 -0
- package/modules/render-deploy/public/deploy.js +249 -0
- package/modules/render-deploy/render.js +56 -0
- package/modules/render-deploy/routes/act.js +88 -0
- package/modules/render-deploy/routes/app.js +65 -0
- package/modules/render-deploy/routes/history.js +38 -0
- package/modules/specialities/db.js +27 -8
- package/modules/specialities/migrations/specialities_003_skills.sql +46 -0
- package/modules/specialities/routes/specialities.js +51 -3
- package/modules/specialities/skills.js +94 -0
- package/modules/specialities/specialities.js +56 -9
- package/modules/ui-design/kit/fixtures/me-cross-project-private.json +12 -0
- package/modules/ui-design/kit/fixtures/me__cross-project.json +15 -0
- package/modules/ui-design/kit/fixtures/provisioning-instance-render.json +39 -0
- package/modules/ui-design/kit/lib.js +3 -1
- package/modules/ui-design/kit/serve.js +96 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +169 -0
- package/scripts/gds/agents-sync.js +13 -3
- package/scripts/gds/autobongos-run.js +80 -2
- package/scripts/gds/autobongos-service.cmd +12 -0
- package/scripts/gds/copy-apply.js +14 -0
- package/scripts/gds/dev-box-guard.js +3 -2
- package/scripts/gds/fitness.js +9 -0
- package/scripts/gds/grade-correlation-audit.js +42 -12
- package/scripts/gds/grade-replay.js +1 -1
- package/scripts/gds/provision-core-upgrade.js +38 -7
- package/scripts/gds/provision-pin-key.js +176 -0
- package/scripts/gds/provision-render.js +62 -31
- package/scripts/gds/provision-teardown.js +7 -2
- package/scripts/gds/provision-units.js +31 -0
- package/scripts/gds/provision.js +10 -10
- package/scripts/gds/publish-manifest.js +1 -0
- package/scripts/gds/render-api.js +19 -0
- package/scripts/gds/render-payload.js +39 -65
- package/scripts/gds/run-unit-tests.js +10 -0
- package/scripts/gds/ship-finish.js +19 -20
- package/scripts/gds/smoke-dependencies.sh +37 -8
- package/scripts/gds/status.js +12 -4
- package/scripts/gds/upgrade.js +2 -2
- package/scripts/public-mirror-export.js +7 -1
- package/src/bongos/module-scope-map.js +13 -0
- package/src/bongos/serve-internal.js +3 -1
- package/src/module-api.js +10 -1
- package/src/platform-server.js +39 -0
- package/tests/account_privacy_flags.mjs +10 -6
- package/tests/agents_authoring.mjs +15 -1
- package/tests/agents_sync.mjs +106 -5
- package/tests/agents_validate.mjs +45 -0
- package/tests/api_path_404.mjs +6 -0
- package/tests/autobongos_cadence.mjs +7 -0
- package/tests/autobongos_loop.mjs +136 -1
- package/tests/autonomy_gauge.mjs +62 -0
- package/tests/blocker_hall_live_proof.mjs +275 -0
- package/tests/claim_gate_ci_unblock.mjs +116 -0
- package/tests/claim_gate_rebase.mjs +15 -3
- package/tests/collab_page.mjs +10 -0
- package/tests/conductor_main_e2e.mjs +97 -0
- package/tests/core_262_drop_box_blocked_db.mjs +143 -0
- package/tests/core_upgrade_runner.mjs +1 -0
- package/tests/dependency_writes_atomic.mjs +191 -0
- package/tests/deploy_page_projects.mjs +2 -1
- package/tests/effective_visibility_predicate.mjs +15 -0
- package/tests/est_advisory.mjs +27 -0
- package/tests/goal_map_page.mjs +26 -0
- package/tests/goal_task_relevance.mjs +518 -0
- package/tests/goal_working_area.mjs +146 -0
- package/tests/grade_attempts.mjs +61 -0
- package/tests/grade_attribution.mjs +23 -0
- package/tests/grade_correlation_audit.mjs +54 -1
- package/tests/grader_reader_lens.mjs +55 -2
- package/tests/grader_root_outage.mjs +6 -1
- package/tests/grader_subagent_tools_arg.mjs +102 -0
- package/tests/hall_approval_queue.mjs +54 -2
- package/tests/hall_settings_world.mjs +3 -1
- package/tests/hall_tweak_editor.mjs +316 -11
- package/tests/hub_account_page.mjs +394 -0
- package/tests/idea_detail_page.mjs +64 -0
- package/tests/idea_goal_ratification.mjs +64 -0
- package/tests/idea_spark_hall.mjs +60 -9
- package/tests/ideator_full_idea_shapes_space_proof.mjs +1 -1
- package/tests/landing_page.mjs +6 -3
- package/tests/module-scope-map.mjs +31 -8
- package/tests/module_api.mjs +1 -0
- package/tests/module_loader.mjs +1 -1
- package/tests/nav_permission_atoms.mjs +1 -1
- package/tests/no_phantom_mirror_workflow.mjs +24 -0
- package/tests/npm_release_preview_commands.mjs +89 -0
- package/tests/npm_release_preview_divert.mjs +164 -0
- package/tests/npm_release_preview_env.mjs +100 -0
- package/tests/npm_release_preview_proxy.mjs +162 -0
- package/tests/npm_release_preview_routes.mjs +208 -0
- package/tests/npm_release_preview_supervisor.mjs +203 -0
- package/tests/pin_write_key.mjs +383 -0
- package/tests/planning_session_skill.mjs +27 -0
- package/tests/platform_boot.mjs +71 -0
- package/tests/profile_nudge_links.mjs +86 -0
- package/tests/profile_ui_cross_project.mjs +1 -1
- package/tests/projects_hub.mjs +2 -2
- package/tests/projects_hub_app_status.mjs +249 -0
- package/tests/projects_hub_app_step.mjs +282 -52
- package/tests/projects_hub_render_connect.mjs +295 -0
- package/tests/provision_render.mjs +121 -28
- package/tests/provision_settings_apply.mjs +62 -0
- package/tests/provisioning_app_status.mjs +204 -0
- package/tests/provisioning_render_route.mjs +126 -6
- package/tests/render_check.mjs +2 -1
- package/tests/render_deploy.mjs +495 -0
- package/tests/rot_inbox.mjs +8 -0
- package/tests/ship_preflight.mjs +61 -8
- package/tests/smoke_dependencies_witness.mjs +46 -0
- package/tests/speciality_skills.mjs +214 -0
- package/tests/studio_room_height.mjs +70 -0
- package/tests/task_detail_includes.mjs +10 -0
- package/tests/task_visuals_instance_root.mjs +150 -0
- package/tests/tweak_batch_apply.mjs +21 -0
- package/tests/ui_design_kit.mjs +36 -4
- package/tests/upgrade.mjs +1 -1
- 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
|
-
|
|
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
|
|
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 === '
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
+
}
|