@bongos/core 1.19.418 → 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 (48) hide show
  1. package/.bongos-core.json +95 -45
  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 +18 -0
  5. package/clients/bongos-client/index.cjs +18 -0
  6. package/clients/bongos-client/index.d.ts +28 -1
  7. package/clients/bongos-client/index.mjs +18 -0
  8. package/docs/adr/0207-connections-implementation-departures-and-honest-limits.md +90 -0
  9. package/docs/adr/0209-one-budget-for-the-account-existence-oracle.md +79 -0
  10. package/docs/adr/README.md +2 -0
  11. package/docs/api/openapi.json +518 -5
  12. package/docs/api-reference.md +16 -3
  13. package/docs/copy-inventory.md +186 -174
  14. package/docs/copy-registry.json +348 -238
  15. package/docs/file-map.md +1 -0
  16. package/docs/module-api-changelog.md +4 -0
  17. package/modules/dev-box/app/src/vendor/bongos-client.cjs +18 -0
  18. package/modules/onboarding/routes/access-requests.js +16 -3
  19. package/modules/platform-identity/account-visibility.js +58 -0
  20. package/modules/platform-identity/connections.js +490 -0
  21. package/modules/platform-identity/migrations/platform_identity_015_connections.sql +78 -0
  22. package/modules/platform-identity/module.json +1 -1
  23. package/modules/platform-identity/platform-identity.js +5 -5
  24. package/modules/platform-identity/routes/connections.js +236 -0
  25. package/modules/platform-identity/tests/platform-identity.mjs +20 -0
  26. package/modules/provisioning/migrations/provisioning_013_project_detail.sql +54 -0
  27. package/modules/provisioning/onboard-plan.js +13 -0
  28. package/modules/provisioning/provisioning.js +113 -5
  29. package/modules/provisioning/routes/provisioning.js +105 -0
  30. package/modules/provisioning/tests/provisioning.mjs +84 -0
  31. package/package-lock.json +2 -2
  32. package/package.json +1 -1
  33. package/public-landing/projects.html +161 -42
  34. package/scripts/gds/fitness.js +9 -0
  35. package/scripts/gds/onboard.js +41 -1
  36. package/scripts/gds/provision-net.js +7 -3
  37. package/src/bongos/middleware/rate-limit.js +68 -2
  38. package/src/bongos/routes/auth.js +19 -12
  39. package/src/module-api.js +13 -1
  40. package/tests/account_existence_rate_limit.mjs +375 -0
  41. package/tests/connections_api.mjs +568 -0
  42. package/tests/connections_ownership.mjs +313 -0
  43. package/tests/module_api.mjs +1 -0
  44. package/tests/onboard.mjs +56 -0
  45. package/tests/own_scope_guard.mjs +5 -0
  46. package/tests/projects_hub.mjs +69 -0
  47. package/tests/projects_hub_pre_uat.mjs +27 -5
  48. package/tests/provision.mjs +20 -0
@@ -77,6 +77,10 @@
77
77
  "name": "closeness",
78
78
  "description": "Endpoints under `/api/bongos/closeness`."
79
79
  },
80
+ {
81
+ "name": "connections",
82
+ "description": "Endpoints under `/api/bongos/connections`."
83
+ },
80
84
  {
81
85
  "name": "copy-desk",
82
86
  "description": "Endpoints under `/api/bongos/copy-desk`."
@@ -368,7 +372,7 @@
368
372
  "access-requests"
369
373
  ],
370
374
  "summary": "GET /access-requests/status",
371
- "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.",
372
376
  "x-rank": "public",
373
377
  "x-source": "modules/onboarding/routes/access-requests.js",
374
378
  "responses": {
@@ -1155,7 +1159,7 @@
1155
1159
  "auth"
1156
1160
  ],
1157
1161
  "summary": "GET /auth/web/admission-status",
1158
- "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.",
1159
1163
  "x-rank": "public",
1160
1164
  "x-source": "src/bongos/routes/auth.js",
1161
1165
  "responses": {
@@ -3435,6 +3439,330 @@
3435
3439
  ]
3436
3440
  }
3437
3441
  },
3442
+ "/connections": {
3443
+ "get": {
3444
+ "operationId": "get_connections",
3445
+ "tags": [
3446
+ "connections"
3447
+ ],
3448
+ "summary": "GET /connections",
3449
+ "description": "MY accepted connections (own data, keyed on the session). Own scope, so the counterparty floors on profile_state alone: a private account you connected with appears here and in no public list (D4 §2).\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
3450
+ "x-rank": "any-builder",
3451
+ "x-source": "modules/platform-identity/routes/connections.js",
3452
+ "responses": {
3453
+ "200": {
3454
+ "description": "Success.",
3455
+ "content": {
3456
+ "application/json": {
3457
+ "schema": {
3458
+ "$ref": "#/components/schemas/GetConnectionsResponse"
3459
+ }
3460
+ }
3461
+ }
3462
+ },
3463
+ "400": {
3464
+ "$ref": "#/components/responses/BadRequest"
3465
+ },
3466
+ "401": {
3467
+ "$ref": "#/components/responses/Unauthorized"
3468
+ },
3469
+ "403": {
3470
+ "$ref": "#/components/responses/Forbidden"
3471
+ }
3472
+ },
3473
+ "security": [
3474
+ {
3475
+ "builderSession": []
3476
+ }
3477
+ ]
3478
+ },
3479
+ "post": {
3480
+ "operationId": "post_connections",
3481
+ "tags": [
3482
+ "connections"
3483
+ ],
3484
+ "summary": "POST /connections",
3485
+ "description": "ask to connect. The REQUESTER is always the session; the body names only who is being asked, and it names them by public handle.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
3486
+ "x-rank": "any-builder",
3487
+ "x-source": "modules/platform-identity/routes/connections.js",
3488
+ "requestBody": {
3489
+ "required": true,
3490
+ "content": {
3491
+ "application/json": {
3492
+ "schema": {
3493
+ "$ref": "#/components/schemas/PostConnectionsRequest"
3494
+ }
3495
+ }
3496
+ },
3497
+ "x-validated": true
3498
+ },
3499
+ "responses": {
3500
+ "200": {
3501
+ "description": "Success.",
3502
+ "content": {
3503
+ "application/json": {
3504
+ "schema": {
3505
+ "$ref": "#/components/schemas/PostConnectionsResponse"
3506
+ }
3507
+ }
3508
+ }
3509
+ },
3510
+ "400": {
3511
+ "$ref": "#/components/responses/ValidationFailed"
3512
+ },
3513
+ "401": {
3514
+ "$ref": "#/components/responses/Unauthorized"
3515
+ },
3516
+ "403": {
3517
+ "$ref": "#/components/responses/Forbidden"
3518
+ }
3519
+ },
3520
+ "security": [
3521
+ {
3522
+ "builderSession": []
3523
+ }
3524
+ ]
3525
+ }
3526
+ },
3527
+ "/connections/degree/{handle}": {
3528
+ "get": {
3529
+ "operationId": "get_connections_degree_handle",
3530
+ "tags": [
3531
+ "connections"
3532
+ ],
3533
+ "summary": "GET /connections/degree/:handle",
3534
+ "description": "the degree label between the CALLER and one handle, plus the mutuals it was computed from (own-scope read, D4 §4). Bounded by construction: degreeBetween caps every level and answers null past the caps.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
3535
+ "x-rank": "any-builder",
3536
+ "x-source": "modules/platform-identity/routes/connections.js",
3537
+ "parameters": [
3538
+ {
3539
+ "name": "handle",
3540
+ "in": "path",
3541
+ "required": true,
3542
+ "schema": {
3543
+ "type": "string"
3544
+ },
3545
+ "description": "Path parameter `handle`."
3546
+ }
3547
+ ],
3548
+ "responses": {
3549
+ "200": {
3550
+ "description": "Success.",
3551
+ "content": {
3552
+ "application/json": {
3553
+ "schema": {
3554
+ "$ref": "#/components/schemas/GetConnectionsDegreeHandleResponse"
3555
+ }
3556
+ }
3557
+ }
3558
+ },
3559
+ "400": {
3560
+ "$ref": "#/components/responses/BadRequest"
3561
+ },
3562
+ "401": {
3563
+ "$ref": "#/components/responses/Unauthorized"
3564
+ },
3565
+ "403": {
3566
+ "$ref": "#/components/responses/Forbidden"
3567
+ },
3568
+ "404": {
3569
+ "$ref": "#/components/responses/NotFound"
3570
+ }
3571
+ },
3572
+ "security": [
3573
+ {
3574
+ "builderSession": []
3575
+ }
3576
+ ]
3577
+ }
3578
+ },
3579
+ "/connections/requests": {
3580
+ "get": {
3581
+ "operationId": "get_connections_requests",
3582
+ "tags": [
3583
+ "connections"
3584
+ ],
3585
+ "summary": "GET /connections/requests",
3586
+ "description": "MY pending requests, both directions (own data).\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
3587
+ "x-rank": "any-builder",
3588
+ "x-source": "modules/platform-identity/routes/connections.js",
3589
+ "responses": {
3590
+ "200": {
3591
+ "description": "Success.",
3592
+ "content": {
3593
+ "application/json": {
3594
+ "schema": {
3595
+ "$ref": "#/components/schemas/GetConnectionsRequestsResponse"
3596
+ }
3597
+ }
3598
+ }
3599
+ },
3600
+ "400": {
3601
+ "$ref": "#/components/responses/BadRequest"
3602
+ },
3603
+ "401": {
3604
+ "$ref": "#/components/responses/Unauthorized"
3605
+ },
3606
+ "403": {
3607
+ "$ref": "#/components/responses/Forbidden"
3608
+ }
3609
+ },
3610
+ "security": [
3611
+ {
3612
+ "builderSession": []
3613
+ }
3614
+ ]
3615
+ }
3616
+ },
3617
+ "/connections/{id}": {
3618
+ "delete": {
3619
+ "operationId": "delete_connections_id",
3620
+ "tags": [
3621
+ "connections"
3622
+ ],
3623
+ "summary": "DELETE /connections/:id",
3624
+ "description": "remove an accepted connection (either side may end it) or cancel a pending request I sent. An ADDRESSEE ending a pending row is a decline, not a delete, and is 404'd here so the cooldown cannot be dodged.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
3625
+ "x-rank": "any-builder",
3626
+ "x-source": "modules/platform-identity/routes/connections.js",
3627
+ "parameters": [
3628
+ {
3629
+ "name": "id",
3630
+ "in": "path",
3631
+ "required": true,
3632
+ "schema": {
3633
+ "type": "string"
3634
+ },
3635
+ "description": "Path parameter `id`."
3636
+ }
3637
+ ],
3638
+ "responses": {
3639
+ "200": {
3640
+ "description": "Success.",
3641
+ "content": {
3642
+ "application/json": {
3643
+ "schema": {
3644
+ "$ref": "#/components/schemas/DeleteConnectionsIdResponse"
3645
+ }
3646
+ }
3647
+ }
3648
+ },
3649
+ "400": {
3650
+ "$ref": "#/components/responses/BadRequest"
3651
+ },
3652
+ "401": {
3653
+ "$ref": "#/components/responses/Unauthorized"
3654
+ },
3655
+ "403": {
3656
+ "$ref": "#/components/responses/Forbidden"
3657
+ },
3658
+ "404": {
3659
+ "$ref": "#/components/responses/NotFound"
3660
+ }
3661
+ },
3662
+ "security": [
3663
+ {
3664
+ "builderSession": []
3665
+ }
3666
+ ]
3667
+ }
3668
+ },
3669
+ "/connections/{id}/accept": {
3670
+ "post": {
3671
+ "operationId": "post_connections_id_accept",
3672
+ "tags": [
3673
+ "connections"
3674
+ ],
3675
+ "summary": "POST /connections/:id/accept",
3676
+ "description": "accept a request addressed to ME. The addressee is the session; a row belonging to anyone else simply does not match, and the 404 that follows is what stops a builder acting on someone else's connection.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
3677
+ "x-rank": "any-builder",
3678
+ "x-source": "modules/platform-identity/routes/connections.js",
3679
+ "parameters": [
3680
+ {
3681
+ "name": "id",
3682
+ "in": "path",
3683
+ "required": true,
3684
+ "schema": {
3685
+ "type": "string"
3686
+ },
3687
+ "description": "Path parameter `id`."
3688
+ }
3689
+ ],
3690
+ "responses": {
3691
+ "200": {
3692
+ "description": "Success.",
3693
+ "content": {
3694
+ "application/json": {
3695
+ "schema": {
3696
+ "$ref": "#/components/schemas/PostConnectionsIdAcceptResponse"
3697
+ }
3698
+ }
3699
+ }
3700
+ },
3701
+ "400": {
3702
+ "$ref": "#/components/responses/BadRequest"
3703
+ },
3704
+ "401": {
3705
+ "$ref": "#/components/responses/Unauthorized"
3706
+ },
3707
+ "403": {
3708
+ "$ref": "#/components/responses/Forbidden"
3709
+ },
3710
+ "404": {
3711
+ "$ref": "#/components/responses/NotFound"
3712
+ }
3713
+ },
3714
+ "security": [
3715
+ {
3716
+ "builderSession": []
3717
+ }
3718
+ ]
3719
+ }
3720
+ },
3721
+ "/connections/{id}/decline": {
3722
+ "post": {
3723
+ "operationId": "post_connections_id_decline",
3724
+ "tags": [
3725
+ "connections"
3726
+ ],
3727
+ "summary": "POST /connections/:id/decline",
3728
+ "description": "decline a request addressed to ME. 204: the requester is never told, so there is nothing to return that they could ever observe.\n\n**Rank:** `any-builder` — Any authenticated builder (row-level ownership enforced in-handler).",
3729
+ "x-rank": "any-builder",
3730
+ "x-source": "modules/platform-identity/routes/connections.js",
3731
+ "parameters": [
3732
+ {
3733
+ "name": "id",
3734
+ "in": "path",
3735
+ "required": true,
3736
+ "schema": {
3737
+ "type": "string"
3738
+ },
3739
+ "description": "Path parameter `id`."
3740
+ }
3741
+ ],
3742
+ "responses": {
3743
+ "200": {
3744
+ "description": "Success."
3745
+ },
3746
+ "400": {
3747
+ "$ref": "#/components/responses/BadRequest"
3748
+ },
3749
+ "401": {
3750
+ "$ref": "#/components/responses/Unauthorized"
3751
+ },
3752
+ "403": {
3753
+ "$ref": "#/components/responses/Forbidden"
3754
+ },
3755
+ "404": {
3756
+ "$ref": "#/components/responses/NotFound"
3757
+ }
3758
+ },
3759
+ "security": [
3760
+ {
3761
+ "builderSession": []
3762
+ }
3763
+ ]
3764
+ }
3765
+ },
3438
3766
  "/copy-desk/flags": {
3439
3767
  "post": {
3440
3768
  "operationId": "post_copy_desk_flags",
@@ -10661,6 +10989,69 @@
10661
10989
  ]
10662
10990
  }
10663
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
+ },
10664
11055
  "/provisioning/instances/{id}/domain": {
10665
11056
  "post": {
10666
11057
  "operationId": "post_provisioning_instances_id_domain",
@@ -15051,6 +15442,15 @@
15051
15442
  "deleted"
15052
15443
  ]
15053
15444
  },
15445
+ "DeleteConnectionsIdResponse": {
15446
+ "type": "object",
15447
+ "properties": {
15448
+ "outcome": {}
15449
+ },
15450
+ "required": [
15451
+ "outcome"
15452
+ ]
15453
+ },
15054
15454
  "DeleteDependenciesResponse": {
15055
15455
  "type": "object",
15056
15456
  "properties": {
@@ -15485,6 +15885,49 @@
15485
15885
  "why"
15486
15886
  ]
15487
15887
  },
15888
+ "GetConnectionsDegreeHandleResponse": {
15889
+ "type": "object",
15890
+ "properties": {
15891
+ "handle": {},
15892
+ "degree": {},
15893
+ "mutuals": {},
15894
+ "mutual_count": {},
15895
+ "truncated": {}
15896
+ },
15897
+ "required": [
15898
+ "handle",
15899
+ "degree",
15900
+ "mutuals",
15901
+ "mutual_count",
15902
+ "truncated"
15903
+ ]
15904
+ },
15905
+ "GetConnectionsRequestsResponse": {
15906
+ "type": "object",
15907
+ "properties": {
15908
+ "incoming": {},
15909
+ "outgoing": {},
15910
+ "incoming_count": {},
15911
+ "outgoing_count": {}
15912
+ },
15913
+ "required": [
15914
+ "incoming",
15915
+ "outgoing",
15916
+ "incoming_count",
15917
+ "outgoing_count"
15918
+ ]
15919
+ },
15920
+ "GetConnectionsResponse": {
15921
+ "type": "object",
15922
+ "properties": {
15923
+ "connections": {},
15924
+ "count": {}
15925
+ },
15926
+ "required": [
15927
+ "connections",
15928
+ "count"
15929
+ ]
15930
+ },
15488
15931
  "GetDiscordChannelsPlanResponse": {
15489
15932
  "type": "object",
15490
15933
  "properties": {
@@ -17090,6 +17533,35 @@
17090
17533
  "project"
17091
17534
  ]
17092
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
+ },
17093
17565
  "PatchProvisioningInstancesIdSettingsRequest": {
17094
17566
  "type": "object",
17095
17567
  "properties": {
@@ -17932,6 +18404,41 @@
17932
18404
  "reward"
17933
18405
  ]
17934
18406
  },
18407
+ "PostConnectionsIdAcceptResponse": {
18408
+ "type": "object",
18409
+ "properties": {
18410
+ "status": {}
18411
+ },
18412
+ "required": [
18413
+ "status"
18414
+ ]
18415
+ },
18416
+ "PostConnectionsRequest": {
18417
+ "type": "object",
18418
+ "properties": {
18419
+ "handle": {
18420
+ "type": "string",
18421
+ "minLength": 1,
18422
+ "maxLength": 39
18423
+ },
18424
+ "note": {
18425
+ "type": "string"
18426
+ }
18427
+ },
18428
+ "required": [
18429
+ "handle"
18430
+ ],
18431
+ "additionalProperties": false
18432
+ },
18433
+ "PostConnectionsResponse": {
18434
+ "type": "object",
18435
+ "properties": {
18436
+ "status": {}
18437
+ },
18438
+ "required": [
18439
+ "status"
18440
+ ]
18441
+ },
17935
18442
  "PostCopyDeskFlagsIdCloseRequest": {
17936
18443
  "type": "object",
17937
18444
  "properties": {
@@ -19604,6 +20111,12 @@
19604
20111
  },
19605
20112
  "type": {
19606
20113
  "type": "string"
20114
+ },
20115
+ "description": {
20116
+ "type": "string"
20117
+ },
20118
+ "detail": {
20119
+ "type": "object"
19607
20120
  }
19608
20121
  },
19609
20122
  "required": [
@@ -20847,9 +21360,9 @@
20847
21360
  "description": "A required dependency/feature is not configured or is temporarily down."
20848
21361
  }
20849
21362
  },
20850
- "x-endpoint-count": 330,
20851
- "x-schema-count": 365,
21363
+ "x-endpoint-count": 338,
21364
+ "x-schema-count": 374,
20852
21365
  "x-undocumented-bodies": 9,
20853
- "x-response-schemas": 247,
21366
+ "x-response-schemas": 254,
20854
21367
  "x-generated-by": "scripts/gds/gen-api-docs.js"
20855
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. 330 endpoints across 53 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`.
@@ -153,6 +153,18 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
153
153
  | GET | `/api/bongos/closeness/weights` | `any-builder` | — | the MODEL is not private, and publishing it is the whole point of the weights being data. |
154
154
  | GET | `/api/bongos/closeness/with/:builderId` | `any-builder` | — | One pair — "why am I close to THEM", asked directly rather than by paging the graph. |
155
155
 
156
+ ## `connections` (7)
157
+
158
+ | Method | Path | Rank | Body | Description |
159
+ |---|---|---|---|---|
160
+ | GET | `/api/bongos/connections` | `any-builder` | — | MY accepted connections (own data, keyed on the session). |
161
+ | POST | `/api/bongos/connections` | `any-builder` | `handle`, `note` | ask to connect. |
162
+ | DELETE | `/api/bongos/connections/:id` | `any-builder` | — | remove an accepted connection (either side may end it) or cancel a pending request I sent. |
163
+ | POST | `/api/bongos/connections/:id/accept` | `any-builder` | — | accept a request addressed to ME. |
164
+ | POST | `/api/bongos/connections/:id/decline` | `any-builder` | — | decline a request addressed to ME. |
165
+ | GET | `/api/bongos/connections/degree/:handle` | `any-builder` | — | the degree label between the CALLER and one handle, plus the mutuals it was computed from (own-scope read, D4 §4). |
166
+ | GET | `/api/bongos/connections/requests` | `any-builder` | — | MY pending requests, both directions (own data). |
167
+
156
168
  ## `copy-desk` (4)
157
169
 
158
170
  | Method | Path | Rank | Body | Description |
@@ -425,7 +437,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
425
437
  | GET | `/api/bongos/projects/featured` | `public` | — | a curated public showcase; no auth by design. |
426
438
  | POST | `/api/bongos/projects/invite` | `metic+archon` | `client_id`, `github_login` | invite a builder into a project (ADR 0141 §5, task #1002115). |
427
439
 
428
- ## `provisioning` (18)
440
+ ## `provisioning` (19)
429
441
 
430
442
  | Method | Path | Rank | Body | Description |
431
443
  |---|---|---|---|---|
@@ -435,8 +447,9 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
435
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… |
436
448
  | GET | `/api/bongos/provisioning/health` | `public` | — | GET /provisioning/health — liveness + the core version this module mounted against. |
437
449
  | GET | `/api/bongos/provisioning/instances` | `any-builder` | — | GET /provisioning/instances — the caller's own instances + each open intent. |
438
- | 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. |
439
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… |
440
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… |
441
454
  | POST | `/api/bongos/provisioning/instances/:id/force-teardown` | `metic+archon` | — | POST /provisioning/instances/:id/force-teardown — Archon tears down ANY instance. |
442
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… |