@bongos/core 1.19.419 → 1.19.420

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 (39) hide show
  1. package/.bongos-core.json +58 -43
  2. package/.claude/skills/new-project/SKILL.md +22 -11
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +2 -0
  5. package/clients/bongos-client/index.cjs +2 -0
  6. package/clients/bongos-client/index.d.ts +5 -1
  7. package/clients/bongos-client/index.mjs +2 -0
  8. package/docs/adr/0209-one-budget-for-the-account-existence-oracle.md +79 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/api/openapi.json +103 -5
  11. package/docs/api-reference.md +4 -3
  12. package/docs/copy-inventory.md +186 -174
  13. package/docs/copy-registry.json +348 -238
  14. package/docs/file-map.md +1 -0
  15. package/docs/module-api-changelog.md +2 -0
  16. package/modules/dev-box/app/src/vendor/bongos-client.cjs +2 -0
  17. package/modules/onboarding/routes/access-requests.js +16 -3
  18. package/modules/platform-identity/platform-identity.js +5 -5
  19. package/modules/platform-identity/tests/platform-identity.mjs +20 -0
  20. package/modules/provisioning/migrations/provisioning_013_project_detail.sql +54 -0
  21. package/modules/provisioning/onboard-plan.js +13 -0
  22. package/modules/provisioning/provisioning.js +113 -5
  23. package/modules/provisioning/routes/provisioning.js +105 -0
  24. package/modules/provisioning/tests/provisioning.mjs +84 -0
  25. package/package-lock.json +2 -2
  26. package/package.json +1 -1
  27. package/public-landing/projects.html +161 -42
  28. package/scripts/gds/fitness.js +9 -0
  29. package/scripts/gds/onboard.js +41 -1
  30. package/scripts/gds/provision-net.js +7 -3
  31. package/src/bongos/middleware/rate-limit.js +68 -2
  32. package/src/bongos/routes/auth.js +19 -12
  33. package/src/module-api.js +13 -1
  34. package/tests/account_existence_rate_limit.mjs +375 -0
  35. package/tests/module_api.mjs +1 -0
  36. package/tests/onboard.mjs +56 -0
  37. package/tests/projects_hub.mjs +69 -0
  38. package/tests/projects_hub_pre_uat.mjs +27 -5
  39. package/tests/provision.mjs +20 -0
@@ -294,6 +294,7 @@ This keeps the decision history honest and traceable.
294
294
  | 0204 | [**The apex is dark only; light is the builders' hall's** ([task 1003336](https://cloudbongos.com/builders#/task/1003336), goal 1000080). Supersedes in part [ADR 0196](<redacted>.md) (the light-mode half of the landing build) and the landing direction's 2026-08-27 pick of "light rebuilt to the dark standard". The owner, reviewing the planets: "only black or only white is a bit bland… leaning towards only dark mode for this page and keep the dark/light mode to be solely for the builder hall", confirmed as the WHOLE apex. Decision: `/`, `/projects`, `/privacy`, `/terms` and `/contact` pin `data-mode="dark"` on `<html>` — no pre-paint script, no `cb-color-mode`, no `?mode=`, no OS-preference read; the moon/sun glyph and its wiring leave both bars; the hall's own `hall-theme` switch is the platform's only light/dark control. The light tier of `world.css` / `cosmos.css` and the dark-twins rule STAY until the tier's own removal (the light-pair contrast tests keep measuring). The kit learns `"modes": ["dark"]` in a states file (validated; `render.js` takes it as the default). Rejected: a dark default with the toggle kept; deleting the light tier in the same diff; `--modes dark` on every kit call instead of the page's contract.](<redacted>.md) | apex / design system |
295
295
  | 0205 | [**A federated check-in may not speak for a person the hub never saw sign in: the membership carries the hub's own witness, and identity is never re-synced from client credentials** ([task 1003340](https://cloudbongos.com/builders#/task/1003340), goal 1000045 — closes security report 1000027, high, LIVE on the hub). `POST /sso/membership/check-in` authenticated the CALLER (`client_id` + `client_secret`) and took the SUBJECT (`github_id`, `github_login`, `avatar_url`) from the request body — client credentials say which PROJECT is calling, never who the person is. So any registered SSO client could (1) rewrite a real person's public `github_login` and `avatar_url`, which `upsertAccount`'s `ON CONFLICT` re-synced unconditionally and stored with no scheme check, and (2) mint the `<redacted>` row that [ADR 0171](<redacted>.md) D7's `membership_required` precheck reads — so the anti-impersonation gate on `POST /sso/activity/rollup` was satisfiable by the very call it was meant to gate, and forged credits/karma/rank reached a public profile. Decisions: (1) migration `<redacted>` adds `signin_verified_at`, written ONLY by `recordFederatedSignin` from the two moments the HUB itself authenticated a human for a client — `/sso/token` (a code minted under a live hub session, bound to the pair) and `/sso/device/poll` (a GitHub approval, audience bound from the typed origin) — so no request-body field reaches the statement and the column is evidence a caller structurally cannot write; (2) `upsertMembership` becomes `touchMembership`, an UPDATE gated on that column — a check-in can no longer CREATE a membership, answering `409 signin_required` and writing nothing, while signing in accepts a standing `pending` invite and leaves any other coarse kind to the instance to report; (3) a federated client gets insert-only `ensureAccount` (`DO NOTHING`), and `upsertAccount`'s identity re-sync belongs to hub-authenticated callers only — nothing is lost, because both of those already re-sync on every sign-in from GitHub's own answer; (4) the rollup gate reads `membershipBacksRollup` (non-pending AND witnessed), not row-existence; (5) `safeAvatarUrl` admits absolute https only, applied inside both account writers rather than at each render; (6) NO backfill — no predicate separates a legitimate check-in row from a forged one, and the first sign-in after deploy heals a real membership because the witness is stamped BEFORE the check-in and rollup that follow it, so a profile goes stale, never blank. No wire-format change: the request schemas are byte-identical and the ADR 0177 `membership_kind` window is untouched, so no federated instance upgrades. Proof `tests/sso_checkin_identity_spoof.mjs` (DB-free, in the default unit gate; 10 of 14 assertions fail against the pre-fix code, 2 controls prove the gate is not a wall). Rejected: carrying the hub assertion in the check-in body (strict validation would 400 every un-upgraded instance); ADR 0156-style source-guarding (answers "did the user set this", the wrong question when the writer is a third party); backfilling `signin_verified_at = joined_at`; a silent 200; retro-filtering `listMembershipsForAccount`.](<redacted>.md) | platform identity / security |
296
296
  | 0206 | [**A leaked bearer's remedy is the account holder's own revoke, not an absolute session ceiling** ([task 1003341](https://cloudbongos.com/builders#/task/1003341), goal 1000045 — criterion `i-control-my-own-access-and-exposure`). `lookupSession` rewrites `expires_at = now() + 24h` on every authenticated request, so a CLI bearer in daily use never expires; if it leaks, the holder keeps it alive simply by using it. `POST /auth/logout` kills only the token presented on that same request — useless when the token you want dead is the one you no longer hold — and the two endpoints that can kill another session are gated on the admin `auth.session.revoke` atom, so the victim had to find an Archon, and a single-owner self-hosted instance may have none. Decision: ship the OWN-scoped remedy (`GET /me/sessions`, `POST /me/sessions/revoke`, `POST /me/sessions/revoke-all` on `requireBuilder` alone, every statement keyed on the caller's own id from the presented session, the token value never selected or returned, `revoke-all` deliberately including the requesting session) and deliberately NOT an absolute ceiling on the sliding refresh: a ceiling is unconditional (migration 035 already had to backfill a 24h grace so it would not kill the session running it), any cap loose enough not to be a daily nuisance leaves a leaked token live for weeks, and the visibility this task ships — `created_at` beside `last_used_at` in the list — is the compounding half of the fix. A demotion already sweeps sessions, and a deactivated builder is denied on the hot path, so the authority-drift case is closed by other means. Rejected: a `created_at + N` cap now; a shorter TTL; three more routes inside the already-largest `routes/auth.js` (a separate file is what lets the no-caller-named-owner rule be asserted over a WHOLE file); a `keep_current` flag; 404 on a session id that is not yours (it would confirm which ids exist); lowering the `auth.session.revoke` floor. Revisit for machine principals, a compliance maximum, or evidence that people do not prune what the list shows them.](<redacted>.md) | auth / account security |
297
+ | 0209 | [**Two routes answering one question spend one budget; whether the answer itself collapses is the owner's call** ([task 1003339](https://cloudbongos.com/builders#/task/1003339), goal 1000045 — criterion `i-control-my-own-access-and-exposure`). `GET /access-requests/status` (public, returns `member|invited|pending|dismissed|none`) and `GET /auth/web/admission-status` (its bare-boolean twin) both answer "does this GitHub login have an account here?", and neither was throttled — `rateLimitMiddleware` returns early for every non-write method, and the one read limiter is mounted only under `/public/*`. Scripted at line rate either becomes a login→membership roster: who builds here, and confirmation that a named person is present. Decision: ship the ceiling as ONE budget — `accountExistenceReadRateLimit`, 120 reads/60s per IP over a single bucket, exported as one middleware INSTANCE rather than a factory and mounted on both routes, because two limiters each configured 120/60s look identical in a diff, pass any test that counts to the limit per route, and hand an enumerator 240/min by alternating. The onboarding module reaches it through the doorway, so `src/bongos/middleware/rate-limit.js` joins `KERNEL_FILES` on `logger.js`'s argument: authz decides WHO may ask, the ceiling decides HOW OFTEN, and the file imports nothing. Deliberately NOT decided here: collapsing the response so a non-subject caller cannot tell `pending`/`dismissed`/`none` from "not admitted" — the consumer sweep found route 1 has exactly ONE consumer (`scripts/gds/login.js`), which already treats `pending` and `none` identically, so collapsing costs exactly one thing: the only message in the codebase that tells a declined applicant they were declined. That is a judgement about what an applicant is owed, so it goes to the owner as a blocker with the evidence. Noted either way: collapsing does not close the oracle — `admitted:true` still means member-or-invited. Rejected: a separate limiter per route; a `createReadRateLimit` factory (two matching call sites get two buckets — the generality loses the property); vendoring a fourth copy of the sliding window; hardening only the smaller route; collapsing on my own judgement because "nothing much" reads it.](<redacted>.md) | security / rate limiting |
297
298
  | 0207 | [**Building the connection graph: three departures from ADR 0202, and the three limits it is honest about** ([task 1002295](https://cloudbongos.com/builders#/task/1002295), goal 1000045 criterion C6 — the build of the connections wave). ADR 0202 decided the graph; building it surfaced three places where the decision's MECHANISM could not be written literally without losing the PROPERTY it was about, and three places where the shipped code cannot promise as much as a casual reading suggests. Departures: (1) **level-wise CTEs, not `WITH RECURSIVE`** — a recursive term can carry neither a per-level fan-out cap nor an ordered per-level truncation, which are the two things D3 actually decided, so a recursive build would have had a depth bound and no fan-out bound; a test asserts `RECURSIVE` appears in neither query. (2) **1st degree is an unbounded pair-index probe**, never a scan of the capped frontier — reading it off the `FANOUT_1`-truncated 1-hop reports "2nd" for someone in the viewer's own connections list once they pass 500 connections, and the rule this states is that **a cap may only ever make an answer less specific (`null`), never wrong**. (3) The shared visibility predicate lands in its own file, `modules/platform-identity/account-visibility.js` (`isAccountVisible` / `accountVisibleSql` / `accountActiveSql`), because task 1002967 had not shipped and `platform-identity.js` sat at the size ratchet with zero headroom — **1002967 adopts it rather than writing a second predicate**. Also decided: node visibility during traversal is viewer-relative and asymmetric on purpose (your OWN 1-hop floors on consent alone so a friend who goes private does not vanish from your list; every deeper hop clears the public predicate); cancel is a `DELETE` an ADDRESSEE may not use, so the 90-day cooldown cannot be dodged by calling a decline a delete; and the create response is `201 { status: 'pending' }` and nothing else, so a swallowed re-ask has nothing to fabricate. Limits stated rather than implied: a swallowed re-ask is silent in the RESPONSE and observable by its ABSENCE from the requester's outbox (the claim is narrowed, not the code — a decoy row would be a lie in the database to protect a lie in the API); the TARGET's 1-hop is capped too and that truncation is deliberately NOT reported, because saying so leaks one bit about a third party's degree; and the query COUNT is bounded while the frontier's WORK is O(sum of degree over the 1-hop), since `ORDER BY … LIMIT` materialises before it truncates — the revisit trigger is a measured p95, and the cheap next move is a `statement_timeout` returning the `null` D3 already chose. Deferred and named: a per-builder cap on OUTBOUND requests (the cooldown stops repeat asks to one person, not one builder asking a hundred — a real number is owner-signed), and a real-Postgres pass on the expire-then-insert write path, which CI cannot run.](<redacted>.md) | platform identity / social graph / privacy |
298
299
 
299
300
  > ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](<redacted>.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
@@ -372,7 +372,7 @@
372
372
  "access-requests"
373
373
  ],
374
374
  "summary": "GET /access-requests/status",
375
- "description": "GET /access-requests/status?github_login=X — PUBLIC. The `bongos login` CLI polls this after filing a request: the device flow's not_approved is TERMINAL (the device_code is spent), so the CLI can't re-poll the flow — it polls this instead, then restarts sign-in once admitted. Exposes nothing the public POST above doesn't already reveal through its 409s (already_member / already_approved). status ∈ member | invited | pending | dismissed | none; admitted = member||invited. rank: public — a would-be builder is by definition outside the system. Declared BEFORE `GET /access-requests` so the literal `/status` path is matched as its own route (not swallowed as a query on the list route).\n\n**Rank:** `public` — No authentication — any caller.",
375
+ "description": "GET /access-requests/status?github_login=X — PUBLIC. The `bongos login` CLI polls this after filing a request: the device flow's not_approved is TERMINAL (the device_code is spent), so the CLI can't re-poll the flow — it polls this instead, then restarts sign-in once admitted. Exposes nothing the public POST above doesn't already reveal through its 409s (already_member / already_approved). status ∈ member | invited | pending | dismissed | none; admitted = member||invited. rank: public — a would-be builder is by definition outside the system. Declared BEFORE `GET /access-requests` so the literal `/status` path is matched as its own route (not swallowed as a query on the list route). RATE-LIMITED (task 1003339 / ADR 0209). Public + unthrottled, this was a login→membership oracle an attacker could script at line rate to learn who builds here. accountExistenceReadRateLimit is the doorway's SHARED 120/60s per-IP budget: core's GET /auth/web/admission-status (the boolean twin of this answer) mounts the very same instance, so the two cannot be alternated for double the rate. The limiter runs before the queries, so a refused probe costs no DB work. Whether the ANSWER should also collapse — so a caller who is not the subject cannot tell pending/dismissed/none apart — is the owner's call and is filed as a blocker, not decided here.\n\n**Rank:** `public` — No authentication — any caller.",
376
376
  "x-rank": "public",
377
377
  "x-source": "modules/onboarding/routes/access-requests.js",
378
378
  "responses": {
@@ -1159,7 +1159,7 @@
1159
1159
  "auth"
1160
1160
  ],
1161
1161
  "summary": "GET /auth/web/admission-status",
1162
- "description": "GET /auth/web/admission-status?login=X — the probe renderAccessPendingPage polls (task 1002097). Answers ONE question: would this login be admitted right now? It mirrors the real gate via auth.webAdmissionStatus so the page can never invite someone in the gate would refuse. rank: public — it must be reachable by a visitor with no session (that is the entire situation it exists for). It grants nothing: admission still runs through the normal sign-in, and this only reads state. WHAT IT LEAKS, stated plainly (task 1002430 — an earlier version of this comment claimed the opposite and was wrong). Returning only a boolean does NOT make it non-enumerable: with open enrollment off, and for any login that is not the configured firstAdmin, `admitted:true` means that login is a member or has been invited. That reveals account existence to an unauthenticated caller. It is also NOT throttled — rateLimitMiddleware returns early for non-write methods, so GETs bypass it entirely; the page's 3s poll bounds the honest client, not an attacker. Why it is nonetheless not a new exposure: the onboarding module's GET /access-requests/status?github_login=X is ALREADY public and returns the strictly richer member|invited|pending|dismissed|none plus its own `admitted`. Anything derivable here is already derivable there, so this adds no net-new surface — and hardening only this route would accomplish nothing while that one stays open. Whether unauthenticated login-existence probing is acceptable at all is a real question, but it belongs to both routes together, not to a comment here. That endpoint is also why this one exists: it models neither the firstAdmin bypass nor open enrollment, so it reports the OWNER as 'none' forever — the exact case this page has to resolve.\n\n**Rank:** `public` — No authentication — any caller.",
1162
+ "description": "GET /auth/web/admission-status?login=X — the probe renderAccessPendingPage polls (task 1002097). Answers ONE question: would this login be admitted right now? It mirrors the real gate via auth.webAdmissionStatus so the page can never invite someone in the gate would refuse. rank: public — it must be reachable by a visitor with no session (that is the entire situation it exists for). It grants nothing: admission still runs through the normal sign-in, and this only reads state. WHAT IT LEAKS, stated plainly (task 1002430 — an earlier version of this comment claimed the opposite and was wrong). Returning only a boolean does NOT make it non-enumerable: with open enrollment off, and for any login that is not the configured firstAdmin, `admitted:true` means that login is a member or has been invited. That reveals account existence to an unauthenticated caller. IT IS NOW THROTTLED (task 1003339 / ADR 0209). It used to say \"NOT throttled\" here, and that was true: rateLimitMiddleware returns early for non-write methods, so GETs bypassed it entirely and the page's 3s poll bounded the honest client, not an attacker. accountExistenceReadRateLimit below is the ceiling — 120 reads/60s per IP, and the SAME instance is mounted on the onboarding module's GET /access-requests/status, which is this route's strictly richer twin (member|invited|pending|dismissed|none plus its own `admitted`). One budget across both, because they are one oracle: hardening either alone would accomplish nothing, and giving each its own budget would just let an enumerator alternate for double the rate. WHAT IS STILL OPEN and is deliberately NOT decided here: whether the answer itself should collapse, so a caller who is not the subject cannot tell pending/dismissed/none apart from a bare \"not admitted\". That changes what a real invited user sees during onboarding, so it is the owner's call — filed as a blocker against task 1003339, not settled by a comment in this file. That endpoint is also why this one exists: it models neither the firstAdmin bypass nor open enrollment, so it reports the OWNER as 'none' forever — the exact case this page has to resolve.\n\n**Rank:** `public` — No authentication — any caller.",
1163
1163
  "x-rank": "public",
1164
1164
  "x-source": "src/bongos/routes/auth.js",
1165
1165
  "responses": {
@@ -10989,6 +10989,69 @@
10989
10989
  ]
10990
10990
  }
10991
10991
  },
10992
+ "/provisioning/instances/{id}/detail": {
10993
+ "patch": {
10994
+ "operationId": "patch_provisioning_instances_id_detail",
10995
+ "tags": [
10996
+ "provisioning"
10997
+ ],
10998
+ "summary": "PATCH /provisioning/instances/:id/detail",
10999
+ "description": "── The optional project detail (task 1002334, goal 1000046) ────────────── PATCH /provisioning/instances/:id/detail — answer (or re-answer) the \"tell us about your project\" step after creation. Body: any subset of { description, detail }. rank: any authenticated builder (own resource; archon may change any — same gate as settings). THIS ROUTE IS WHAT MAKES THE STEP SKIPPABLE. Skipping at creation leaves the project dark-matter — off the map and un-joinable (ADR 0182 D4) — and that is a supported default only because there is a way back. Without this route \"optional\" would mean \"permanently unpublishable\", i.e. a dead end, which is precisely what criterion `proj-detail-interview` refuses. Deliberately NOT a settings-apply: nothing here is a policy the running instance obeys, so there is no web.env to patch and no restart to queue. The answers live on the platform row until a catalog row materializes (the provisioning_007 `type` precedent), and R16 (task 1002335) is the surface that reads `description` to compute publishability and name what is still missing.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
11000
+ "x-rank": "any-builder",
11001
+ "x-source": "modules/provisioning/routes/provisioning.js",
11002
+ "parameters": [
11003
+ {
11004
+ "name": "id",
11005
+ "in": "path",
11006
+ "required": true,
11007
+ "schema": {
11008
+ "type": "string"
11009
+ },
11010
+ "description": "Path parameter `id`."
11011
+ }
11012
+ ],
11013
+ "requestBody": {
11014
+ "required": false,
11015
+ "content": {
11016
+ "application/json": {
11017
+ "schema": {
11018
+ "$ref": "#/components/schemas/PatchProvisioningInstancesIdDetailRequest"
11019
+ }
11020
+ }
11021
+ },
11022
+ "x-validated": true
11023
+ },
11024
+ "responses": {
11025
+ "200": {
11026
+ "description": "Success.",
11027
+ "content": {
11028
+ "application/json": {
11029
+ "schema": {
11030
+ "$ref": "#/components/schemas/PatchProvisioningInstancesIdDetailResponse"
11031
+ }
11032
+ }
11033
+ }
11034
+ },
11035
+ "400": {
11036
+ "$ref": "#/components/responses/ValidationFailed"
11037
+ },
11038
+ "401": {
11039
+ "$ref": "#/components/responses/Unauthorized"
11040
+ },
11041
+ "403": {
11042
+ "$ref": "#/components/responses/Forbidden"
11043
+ },
11044
+ "404": {
11045
+ "$ref": "#/components/responses/NotFound"
11046
+ }
11047
+ },
11048
+ "security": [
11049
+ {
11050
+ "builderSession": []
11051
+ }
11052
+ ]
11053
+ }
11054
+ },
10992
11055
  "/provisioning/instances/{id}/domain": {
10993
11056
  "post": {
10994
11057
  "operationId": "post_provisioning_instances_id_domain",
@@ -17470,6 +17533,35 @@
17470
17533
  "project"
17471
17534
  ]
17472
17535
  },
17536
+ "PatchProvisioningInstancesIdDetailRequest": {
17537
+ "type": "object",
17538
+ "properties": {
17539
+ "description": {
17540
+ "type": "string"
17541
+ },
17542
+ "detail": {
17543
+ "type": "object"
17544
+ }
17545
+ },
17546
+ "additionalProperties": false
17547
+ },
17548
+ "PatchProvisioningInstancesIdDetailResponse": {
17549
+ "type": "object",
17550
+ "properties": {
17551
+ "ok": {
17552
+ "type": "boolean"
17553
+ },
17554
+ "description": {},
17555
+ "detail": {},
17556
+ "options": {}
17557
+ },
17558
+ "required": [
17559
+ "ok",
17560
+ "description",
17561
+ "detail",
17562
+ "options"
17563
+ ]
17564
+ },
17473
17565
  "PatchProvisioningInstancesIdSettingsRequest": {
17474
17566
  "type": "object",
17475
17567
  "properties": {
@@ -20019,6 +20111,12 @@
20019
20111
  },
20020
20112
  "type": {
20021
20113
  "type": "string"
20114
+ },
20115
+ "description": {
20116
+ "type": "string"
20117
+ },
20118
+ "detail": {
20119
+ "type": "object"
20022
20120
  }
20023
20121
  },
20024
20122
  "required": [
@@ -21262,9 +21360,9 @@
21262
21360
  "description": "A required dependency/feature is not configured or is temporarily down."
21263
21361
  }
21264
21362
  },
21265
- "x-endpoint-count": 337,
21266
- "x-schema-count": 372,
21363
+ "x-endpoint-count": 338,
21364
+ "x-schema-count": 374,
21267
21365
  "x-undocumented-bodies": 9,
21268
- "x-response-schemas": 253,
21366
+ "x-response-schemas": 254,
21269
21367
  "x-generated-by": "scripts/gds/gen-api-docs.js"
21270
21368
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Bongos API reference
4
4
 
5
- > **Generated from the live route files** — the route file is authoritative. 337 endpoints across 54 route files.
5
+ > **Generated from the live route files** — the route file is authoritative. 338 endpoints across 54 route files.
6
6
  > Machine-readable spec: [`docs/api/openapi.json`](api/openapi.json) (OpenAPI 3.1). Rendered docs site: **`/docs`** (e.g. `cloudbongos.com/docs`).
7
7
 
8
8
  Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>.md)): `public` < `any-builder` < `metic+archon` < `archon`.
@@ -437,7 +437,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
437
437
  | GET | `/api/bongos/projects/featured` | `public` | — | a curated public showcase; no auth by design. |
438
438
  | POST | `/api/bongos/projects/invite` | `metic+archon` | `client_id`, `github_login` | invite a builder into a project (ADR 0141 §5, task #1002115). |
439
439
 
440
- ## `provisioning` (18)
440
+ ## `provisioning` (19)
441
441
 
442
442
  | Method | Path | Rank | Body | Description |
443
443
  |---|---|---|---|---|
@@ -447,8 +447,9 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
447
447
  | GET | `/api/bongos/provisioning/github-app/status` | `public` | — | GET /provisioning/github-app/status?state=<> — the PUBLIC progress read the onboarding callback page polls (task 2096) to know when setup… |
448
448
  | GET | `/api/bongos/provisioning/health` | `public` | — | GET /provisioning/health — liveness + the core version this module mounted against. |
449
449
  | GET | `/api/bongos/provisioning/instances` | `any-builder` | — | GET /provisioning/instances — the caller's own instances + each open intent. |
450
- | POST | `/api/bongos/provisioning/instances` | `any-builder` | `slug`, `target_ref`, `hosting_shape`, `domain`, `no_address`, `tier`, `onboard_mode`, `type` | POST /provisioning/instances — request a new instance. |
450
+ | POST | `/api/bongos/provisioning/instances` | `any-builder` | `slug`, `target_ref`, `hosting_shape`, `domain`, `no_address`, `tier`, `onboard_mode`, `type`, `description`, `detail` | POST /provisioning/instances — request a new instance. |
451
451
  | GET | `/api/bongos/provisioning/instances/:id` | `any-builder` | — | GET /provisioning/instances/:id — read one instance (the manage page's read). |
452
+ | PATCH | `/api/bongos/provisioning/instances/:id/detail` | `any-builder` | `description`, `detail` | ── The optional project detail (task 1002334, goal 1000046) ────────────── PATCH /provisioning/instances/:id/detail — answer (or re-answe… |
452
453
  | POST | `/api/bongos/provisioning/instances/:id/domain` | `any-builder` | `domain` | POST /provisioning/instances/:id/domain — give the caller's OWN instance its address (task 1002692, gap 5: no path existed to add a domai… |
453
454
  | POST | `/api/bongos/provisioning/instances/:id/force-teardown` | `metic+archon` | — | POST /provisioning/instances/:id/force-teardown — Archon tears down ANY instance. |
454
455
  | POST | `/api/bongos/provisioning/instances/:id/github-app` | `any-builder` | `account` | POST /provisioning/instances/:id/github-app — mint a GitHub-App-Manifest onboarding link for the caller's OWN instance (task 2080, ADR 01… |