@bongos/core 1.19.418 → 1.19.419
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 +58 -23
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +16 -0
- package/clients/bongos-client/index.cjs +16 -0
- package/clients/bongos-client/index.d.ts +23 -0
- package/clients/bongos-client/index.mjs +16 -0
- package/docs/adr/0207-connections-implementation-departures-and-honest-limits.md +90 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +418 -3
- package/docs/api-reference.md +13 -1
- package/docs/module-api-changelog.md +2 -0
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +16 -0
- package/modules/platform-identity/account-visibility.js +58 -0
- package/modules/platform-identity/connections.js +490 -0
- package/modules/platform-identity/migrations/platform_identity_015_connections.sql +78 -0
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/routes/connections.js +236 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +1 -1
- package/tests/connections_api.mjs +568 -0
- package/tests/connections_ownership.mjs +313 -0
- package/tests/own_scope_guard.mjs +5 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/gds/package-core.js",
|
|
5
|
-
"core_version": "1.19.
|
|
6
|
-
"core_contract": "1.19.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.19.419",
|
|
6
|
+
"core_contract": "1.19.419",
|
|
7
|
+
"source_commit": "d433ea7793766e404ce4b09ee9f652d5c09df897",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-
|
|
9
|
+
"built_at": "2026-08-29T17:21:12.343Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
|
-
"docs_redacted":
|
|
12
|
+
"docs_redacted": 358,
|
|
13
13
|
"agent_docs_stubbed": 23,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1710,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 2091,
|
|
20
|
+
"tree_sha256": "95da3db8c2006aa8998116a9f4679ee33c902f365d0aa3904d7af6d3412ac78d",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -352,12 +352,12 @@
|
|
|
352
352
|
{
|
|
353
353
|
"path": "clients/bongos-client/README.md",
|
|
354
354
|
"mode": "0000644",
|
|
355
|
-
"sha256": "
|
|
355
|
+
"sha256": "d80581bbcb2577fe16cba30d6e0e1ff4ede4ca49dd08c306d1a3bcbfdf1d2cdc"
|
|
356
356
|
},
|
|
357
357
|
{
|
|
358
358
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
359
359
|
"mode": "0000644",
|
|
360
|
-
"sha256": "
|
|
360
|
+
"sha256": "64cccfdbd4d3d86798bdb60dbcf336e474aeb6af3854225c0a87f9d466c86acd"
|
|
361
361
|
},
|
|
362
362
|
{
|
|
363
363
|
"path": "clients/bongos-client/examples/hello-world.mjs",
|
|
@@ -367,17 +367,17 @@
|
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/index.cjs",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "b11537927f1268e2e14d8307589fe2eb58ef01e245556decd0251efc17f707d3"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.d.ts",
|
|
374
374
|
"mode": "0000644",
|
|
375
|
-
"sha256": "
|
|
375
|
+
"sha256": "227105130a432d4e7a294b4359455e022beaaac87bcd4014855d9d34e2faeaf4"
|
|
376
376
|
},
|
|
377
377
|
{
|
|
378
378
|
"path": "clients/bongos-client/index.mjs",
|
|
379
379
|
"mode": "0000644",
|
|
380
|
-
"sha256": "
|
|
380
|
+
"sha256": "aaa84ab1bc5fd8054a85d36d6c70dd4667c8103c21e1c5a027de0b3806096029"
|
|
381
381
|
},
|
|
382
382
|
{
|
|
383
383
|
"path": "clients/bongos-client/package.json",
|
|
@@ -1534,20 +1534,25 @@
|
|
|
1534
1534
|
"mode": "0000644",
|
|
1535
1535
|
"sha256": "634404f2eb0d29386460e0ea008a826069d80bfbd42721aa7b2cd4d74cb4fa86"
|
|
1536
1536
|
},
|
|
1537
|
+
{
|
|
1538
|
+
"path": "docs/adr/0207-connections-implementation-departures-and-honest-limits.md",
|
|
1539
|
+
"mode": "0000644",
|
|
1540
|
+
"sha256": "72c859bdfc70fd4fc0d104439985a43eaf60cce281bbaae8b8faaffb40546973"
|
|
1541
|
+
},
|
|
1537
1542
|
{
|
|
1538
1543
|
"path": "docs/adr/README.md",
|
|
1539
1544
|
"mode": "0000644",
|
|
1540
|
-
"sha256": "
|
|
1545
|
+
"sha256": "0f92a7a74afdf213a1aba92da88ea5348878ba0df67c3fc9e33d06b8f7ebade9"
|
|
1541
1546
|
},
|
|
1542
1547
|
{
|
|
1543
1548
|
"path": "docs/api-reference.md",
|
|
1544
1549
|
"mode": "0000644",
|
|
1545
|
-
"sha256": "
|
|
1550
|
+
"sha256": "e58b3d540eeaa8f38e028c72e64f4a2373c16c4c185edabc2d2538d247da9a29"
|
|
1546
1551
|
},
|
|
1547
1552
|
{
|
|
1548
1553
|
"path": "docs/api/openapi.json",
|
|
1549
1554
|
"mode": "0000644",
|
|
1550
|
-
"sha256": "
|
|
1555
|
+
"sha256": "091c9eecdbec83a540fc5d92017525dfdfdf192604b39d8cf60ec2cfb3466819"
|
|
1551
1556
|
},
|
|
1552
1557
|
{
|
|
1553
1558
|
"path": "docs/architecture.md",
|
|
@@ -1922,7 +1927,7 @@
|
|
|
1922
1927
|
{
|
|
1923
1928
|
"path": "docs/module-api-changelog.md",
|
|
1924
1929
|
"mode": "0000644",
|
|
1925
|
-
"sha256": "
|
|
1930
|
+
"sha256": "793b13d001dfdb2742b623d7f43d1c5850c67df8a8b12ffb469c675f15a109e2"
|
|
1926
1931
|
},
|
|
1927
1932
|
{
|
|
1928
1933
|
"path": "docs/modules-contract.md",
|
|
@@ -3232,7 +3237,7 @@
|
|
|
3232
3237
|
{
|
|
3233
3238
|
"path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
|
|
3234
3239
|
"mode": "0000644",
|
|
3235
|
-
"sha256": "
|
|
3240
|
+
"sha256": "b11537927f1268e2e14d8307589fe2eb58ef01e245556decd0251efc17f707d3"
|
|
3236
3241
|
},
|
|
3237
3242
|
{
|
|
3238
3243
|
"path": "modules/dev-box/app/test/unit.mjs",
|
|
@@ -4764,6 +4769,16 @@
|
|
|
4764
4769
|
"mode": "0000644",
|
|
4765
4770
|
"sha256": "7cb9dac9cb87b679e1ae50e4112b3a7928a38ebb9902c8947a74d17bec4d9009"
|
|
4766
4771
|
},
|
|
4772
|
+
{
|
|
4773
|
+
"path": "modules/platform-identity/account-visibility.js",
|
|
4774
|
+
"mode": "0000644",
|
|
4775
|
+
"sha256": "744a7293bbdf4e2340baf0dc7f789df03a63ba7970741b8aef5922c02cb8910e"
|
|
4776
|
+
},
|
|
4777
|
+
{
|
|
4778
|
+
"path": "modules/platform-identity/connections.js",
|
|
4779
|
+
"mode": "0000644",
|
|
4780
|
+
"sha256": "3bdd5edf87ff2095035e8945afb7d3a0779cbb9f691e6e634f12a6500edeb21b"
|
|
4781
|
+
},
|
|
4767
4782
|
{
|
|
4768
4783
|
"path": "modules/platform-identity/hub-device.js",
|
|
4769
4784
|
"mode": "0000644",
|
|
@@ -4854,10 +4869,15 @@
|
|
|
4854
4869
|
"mode": "0000644",
|
|
4855
4870
|
"sha256": "cf29832de04b2b31e1d36b5d595489bbbb09d14b12c86da7c5ac7557917475bf"
|
|
4856
4871
|
},
|
|
4872
|
+
{
|
|
4873
|
+
"path": "modules/platform-identity/migrations/platform_identity_015_connections.sql",
|
|
4874
|
+
"mode": "0000644",
|
|
4875
|
+
"sha256": "edaced87ae340bb4c0a8c73d5dc40f8578bf7be0bd4064a02d6c4fcf0cf94be3"
|
|
4876
|
+
},
|
|
4857
4877
|
{
|
|
4858
4878
|
"path": "modules/platform-identity/module.json",
|
|
4859
4879
|
"mode": "0000644",
|
|
4860
|
-
"sha256": "
|
|
4880
|
+
"sha256": "d7c74b8b41b3c0f6d0d072632f4a8d04f11b06c39203c6f759dcaf4e324fd9b0"
|
|
4861
4881
|
},
|
|
4862
4882
|
{
|
|
4863
4883
|
"path": "modules/platform-identity/platform-identity.js",
|
|
@@ -4874,6 +4894,11 @@
|
|
|
4874
4894
|
"mode": "0000644",
|
|
4875
4895
|
"sha256": "2e898799632a2b5c2ead9cb44fa0b8029fcb6d217f7d5f26c8a39b34c75bbea7"
|
|
4876
4896
|
},
|
|
4897
|
+
{
|
|
4898
|
+
"path": "modules/platform-identity/routes/connections.js",
|
|
4899
|
+
"mode": "0000644",
|
|
4900
|
+
"sha256": "d4b7ddb7ec100899e95e73428fba8dc5e3f41005e021fcf887ea80ba8ae0e1b6"
|
|
4901
|
+
},
|
|
4877
4902
|
{
|
|
4878
4903
|
"path": "modules/platform-identity/routes/my-projects.js",
|
|
4879
4904
|
"mode": "0000644",
|
|
@@ -5442,12 +5467,12 @@
|
|
|
5442
5467
|
{
|
|
5443
5468
|
"path": "package-lock.json",
|
|
5444
5469
|
"mode": "0000644",
|
|
5445
|
-
"sha256": "
|
|
5470
|
+
"sha256": "9bcfa46cd421336fba313885ceb399c9f025a85f5c07759abdb9d653bbd34105"
|
|
5446
5471
|
},
|
|
5447
5472
|
{
|
|
5448
5473
|
"path": "package.json",
|
|
5449
5474
|
"mode": "0000644",
|
|
5450
|
-
"sha256": "
|
|
5475
|
+
"sha256": "cff6d489fea88ab0fe13b221de09f4bfa4b7d95ce5d8a1386de53ce94236b44e"
|
|
5451
5476
|
},
|
|
5452
5477
|
{
|
|
5453
5478
|
"path": "public-docs/index.html",
|
|
@@ -7567,7 +7592,7 @@
|
|
|
7567
7592
|
{
|
|
7568
7593
|
"path": "src/module-api.js",
|
|
7569
7594
|
"mode": "0000644",
|
|
7570
|
-
"sha256": "
|
|
7595
|
+
"sha256": "d70b98299c5a454682784d75bed90dc711525c826fb861bf11c8e5126ea0e155"
|
|
7571
7596
|
},
|
|
7572
7597
|
{
|
|
7573
7598
|
"path": "src/module-loader/catalog.js",
|
|
@@ -8129,6 +8154,16 @@
|
|
|
8129
8154
|
"mode": "0000644",
|
|
8130
8155
|
"sha256": "69f4e40c66e49e4d2c58f1f41ec1c515b2cb44891e40b6192ed898484d10671d"
|
|
8131
8156
|
},
|
|
8157
|
+
{
|
|
8158
|
+
"path": "tests/connections_api.mjs",
|
|
8159
|
+
"mode": "0000644",
|
|
8160
|
+
"sha256": "fe844d310fbe79e38f199dc623a96e0facd751967a9a247be5cbe9c15f80f13a"
|
|
8161
|
+
},
|
|
8162
|
+
{
|
|
8163
|
+
"path": "tests/connections_ownership.mjs",
|
|
8164
|
+
"mode": "0000644",
|
|
8165
|
+
"sha256": "1d716fc81ebdb81daedade019f5adcdda2161c06df40711c8fb78f3ec5d605b0"
|
|
8166
|
+
},
|
|
8132
8167
|
{
|
|
8133
8168
|
"path": "tests/consumer_layout_boot.mjs",
|
|
8134
8169
|
"mode": "0000644",
|
|
@@ -9477,7 +9512,7 @@
|
|
|
9477
9512
|
{
|
|
9478
9513
|
"path": "tests/own_scope_guard.mjs",
|
|
9479
9514
|
"mode": "0000644",
|
|
9480
|
-
"sha256": "
|
|
9515
|
+
"sha256": "074346fee8dbc4de221600d21a4066852125f4f3d0b4f792093e9546ba2cd900"
|
|
9481
9516
|
},
|
|
9482
9517
|
{
|
|
9483
9518
|
"path": "tests/own_session_revoke.mjs",
|
|
@@ -5,7 +5,7 @@ A **generated**, zero-dependency typed client for the Bongos API — produced fr
|
|
|
5
5
|
by hand; it regenerates when the spec changes, so it can never drift from the routes.
|
|
6
6
|
|
|
7
7
|
- API version: **v1** (served at `/api/bongos/v1`)
|
|
8
|
-
-
|
|
8
|
+
- 337 operations across 53 resource groups
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -264,6 +264,22 @@ function createClient(opts = {}) {
|
|
|
264
264
|
// GET /closeness/with/{builderId} — rank: any-builder — GET /closeness/with/:builderId
|
|
265
265
|
getClosenessWithBuilderId: (args) => request("GET", "/closeness/with/{builderId}", { hasBody: false }, args),
|
|
266
266
|
},
|
|
267
|
+
"connections": {
|
|
268
|
+
// GET /connections — rank: any-builder — GET /connections
|
|
269
|
+
getConnections: (args) => request("GET", "/connections", { hasBody: false }, args),
|
|
270
|
+
// POST /connections — rank: any-builder — POST /connections
|
|
271
|
+
postConnections: (args) => request("POST", "/connections", { hasBody: true }, args),
|
|
272
|
+
// DELETE /connections/{id} — rank: any-builder — DELETE /connections/:id
|
|
273
|
+
deleteConnectionsId: (args) => request("DELETE", "/connections/{id}", { hasBody: false }, args),
|
|
274
|
+
// POST /connections/{id}/accept — rank: any-builder — POST /connections/:id/accept
|
|
275
|
+
postConnectionsIdAccept: (args) => request("POST", "/connections/{id}/accept", { hasBody: true }, args),
|
|
276
|
+
// POST /connections/{id}/decline — rank: any-builder — POST /connections/:id/decline
|
|
277
|
+
postConnectionsIdDecline: (args) => request("POST", "/connections/{id}/decline", { hasBody: true }, args),
|
|
278
|
+
// GET /connections/degree/{handle} — rank: any-builder — GET /connections/degree/:handle
|
|
279
|
+
getConnectionsDegreeHandle: (args) => request("GET", "/connections/degree/{handle}", { hasBody: false }, args),
|
|
280
|
+
// GET /connections/requests — rank: any-builder — GET /connections/requests
|
|
281
|
+
getConnectionsRequests: (args) => request("GET", "/connections/requests", { hasBody: false }, args),
|
|
282
|
+
},
|
|
267
283
|
"copyDesk": {
|
|
268
284
|
// POST /copy-desk/flags — rank: any-builder — POST /copy-desk/flags
|
|
269
285
|
postCopyDeskFlags: (args) => request("POST", "/copy-desk/flags", { hasBody: true }, args),
|
|
@@ -263,6 +263,22 @@ function createClient(opts = {}) {
|
|
|
263
263
|
// GET /closeness/with/{builderId} — rank: any-builder — GET /closeness/with/:builderId
|
|
264
264
|
getClosenessWithBuilderId: (args) => request("GET", "/closeness/with/{builderId}", { hasBody: false }, args),
|
|
265
265
|
},
|
|
266
|
+
"connections": {
|
|
267
|
+
// GET /connections — rank: any-builder — GET /connections
|
|
268
|
+
getConnections: (args) => request("GET", "/connections", { hasBody: false }, args),
|
|
269
|
+
// POST /connections — rank: any-builder — POST /connections
|
|
270
|
+
postConnections: (args) => request("POST", "/connections", { hasBody: true }, args),
|
|
271
|
+
// DELETE /connections/{id} — rank: any-builder — DELETE /connections/:id
|
|
272
|
+
deleteConnectionsId: (args) => request("DELETE", "/connections/{id}", { hasBody: false }, args),
|
|
273
|
+
// POST /connections/{id}/accept — rank: any-builder — POST /connections/:id/accept
|
|
274
|
+
postConnectionsIdAccept: (args) => request("POST", "/connections/{id}/accept", { hasBody: true }, args),
|
|
275
|
+
// POST /connections/{id}/decline — rank: any-builder — POST /connections/:id/decline
|
|
276
|
+
postConnectionsIdDecline: (args) => request("POST", "/connections/{id}/decline", { hasBody: true }, args),
|
|
277
|
+
// GET /connections/degree/{handle} — rank: any-builder — GET /connections/degree/:handle
|
|
278
|
+
getConnectionsDegreeHandle: (args) => request("GET", "/connections/degree/{handle}", { hasBody: false }, args),
|
|
279
|
+
// GET /connections/requests — rank: any-builder — GET /connections/requests
|
|
280
|
+
getConnectionsRequests: (args) => request("GET", "/connections/requests", { hasBody: false }, args),
|
|
281
|
+
},
|
|
266
282
|
"copyDesk": {
|
|
267
283
|
// POST /copy-desk/flags — rank: any-builder — POST /copy-desk/flags
|
|
268
284
|
postCopyDeskFlags: (args) => request("POST", "/copy-desk/flags", { hasBody: true }, args),
|
|
@@ -6,6 +6,7 @@ export declare const DEFAULT_BASE_URL: string;
|
|
|
6
6
|
// Request-body + shared types, generated from the OpenAPI components.schemas.
|
|
7
7
|
export interface ApiErrorObject { code: string; message: string; details?: unknown }
|
|
8
8
|
export interface DeleteBoxSshKeyIdResponse { ok: boolean; deleted: unknown }
|
|
9
|
+
export interface DeleteConnectionsIdResponse { outcome: unknown }
|
|
9
10
|
export interface DeleteDependenciesResponse { ok: boolean }
|
|
10
11
|
export interface DeleteGovernmentAssignmentsBuilderIdRankKeyResponse { ok: boolean; builder_id: unknown; rank_key: unknown; removed: unknown }
|
|
11
12
|
export interface DeleteGovernmentRanksRankKeyPermissionsPermissionKeyResponse { ok: boolean; rank_key: unknown; permission_key: unknown; removed: unknown }
|
|
@@ -39,6 +40,9 @@ export interface GetBuildersRosterResponse { builders: unknown; stale_hours: unk
|
|
|
39
40
|
export interface GetClosenessForMeResponse { closeness: unknown; count: unknown }
|
|
40
41
|
export interface GetClosenessWeightsResponse { weights: unknown; window_days: unknown; note: unknown }
|
|
41
42
|
export interface GetClosenessWithBuilderIdResponse { closeness: unknown; why: unknown }
|
|
43
|
+
export interface GetConnectionsDegreeHandleResponse { handle: unknown; degree: unknown; mutuals: unknown; mutual_count: unknown; truncated: unknown }
|
|
44
|
+
export interface GetConnectionsRequestsResponse { incoming: unknown; outgoing: unknown; incoming_count: unknown; outgoing_count: unknown }
|
|
45
|
+
export interface GetConnectionsResponse { connections: unknown; count: unknown }
|
|
42
46
|
export interface GetDiscordChannelsPlanResponse { ok: boolean; guild_id: unknown; op_count: unknown; ops: unknown; warnings: unknown }
|
|
43
47
|
export interface GetDoneWhenPendingReviewResponse { criteria: unknown; count: unknown }
|
|
44
48
|
export interface GetGithubRepoResponse { ok: boolean; repo: unknown; private_deployable: boolean }
|
|
@@ -216,6 +220,9 @@ export interface PostClaimsIdReleaseOnBehalfRequest { reason: string }
|
|
|
216
220
|
export interface PostClaimsIdResolveRequest { outcome: "shipped" | "abandoned" | "partial"; notes_md?: string; value_summary?: string; verification?: Record<string, unknown>; force?: boolean; committed_files?: string[]; sandbox_review?: Record<string, unknown> }
|
|
217
221
|
export interface PostClaimsRequest { task_id: StringifiableId; worktree_name?: string; head_sha?: string; bind_session?: boolean; enforce_touches?: boolean }
|
|
218
222
|
export interface PostClaimsResponse { claim: unknown; task: unknown; just_unlocked: unknown; overlap_warning: unknown; reward: unknown }
|
|
223
|
+
export interface PostConnectionsIdAcceptResponse { status: unknown }
|
|
224
|
+
export interface PostConnectionsRequest { handle: string; note?: string }
|
|
225
|
+
export interface PostConnectionsResponse { status: unknown }
|
|
219
226
|
export interface PostCopyDeskFlagsIdCloseRequest { status: string; resolution_note: string; resolved_task_id?: number }
|
|
220
227
|
export interface PostCopyDeskFlagsIdCloseResponse { ok: boolean; flag: unknown }
|
|
221
228
|
export interface PostCopyDeskFlagsRequest { scope?: string; surface: string; string_id?: string; reason: string; kind?: string }
|
|
@@ -562,6 +569,22 @@ export interface BongosClient {
|
|
|
562
569
|
/** GET /closeness/with/{builderId} — rank: any-builder */
|
|
563
570
|
getClosenessWithBuilderId(args?: RequestArgs): Promise<GetClosenessWithBuilderIdResponse>;
|
|
564
571
|
};
|
|
572
|
+
"connections": {
|
|
573
|
+
/** GET /connections — rank: any-builder */
|
|
574
|
+
getConnections(args?: RequestArgs): Promise<GetConnectionsResponse>;
|
|
575
|
+
/** POST /connections — rank: any-builder */
|
|
576
|
+
postConnections(args: RequestArgs & { body: PostConnectionsRequest }): Promise<PostConnectionsResponse>;
|
|
577
|
+
/** DELETE /connections/{id} — rank: any-builder */
|
|
578
|
+
deleteConnectionsId(args?: RequestArgs): Promise<DeleteConnectionsIdResponse>;
|
|
579
|
+
/** POST /connections/{id}/accept — rank: any-builder */
|
|
580
|
+
postConnectionsIdAccept(args?: RequestArgs): Promise<PostConnectionsIdAcceptResponse>;
|
|
581
|
+
/** POST /connections/{id}/decline — rank: any-builder */
|
|
582
|
+
postConnectionsIdDecline(args?: RequestArgs): Promise<ApiResponse>;
|
|
583
|
+
/** GET /connections/degree/{handle} — rank: any-builder */
|
|
584
|
+
getConnectionsDegreeHandle(args?: RequestArgs): Promise<GetConnectionsDegreeHandleResponse>;
|
|
585
|
+
/** GET /connections/requests — rank: any-builder */
|
|
586
|
+
getConnectionsRequests(args?: RequestArgs): Promise<GetConnectionsRequestsResponse>;
|
|
587
|
+
};
|
|
565
588
|
"copyDesk": {
|
|
566
589
|
/** POST /copy-desk/flags — rank: any-builder */
|
|
567
590
|
postCopyDeskFlags(args: RequestArgs & { body: PostCopyDeskFlagsRequest }): Promise<PostCopyDeskFlagsResponse>;
|
|
@@ -260,6 +260,22 @@ export function createClient(opts = {}) {
|
|
|
260
260
|
// GET /closeness/with/{builderId} — rank: any-builder — GET /closeness/with/:builderId
|
|
261
261
|
getClosenessWithBuilderId: (args) => request("GET", "/closeness/with/{builderId}", { hasBody: false }, args),
|
|
262
262
|
},
|
|
263
|
+
"connections": {
|
|
264
|
+
// GET /connections — rank: any-builder — GET /connections
|
|
265
|
+
getConnections: (args) => request("GET", "/connections", { hasBody: false }, args),
|
|
266
|
+
// POST /connections — rank: any-builder — POST /connections
|
|
267
|
+
postConnections: (args) => request("POST", "/connections", { hasBody: true }, args),
|
|
268
|
+
// DELETE /connections/{id} — rank: any-builder — DELETE /connections/:id
|
|
269
|
+
deleteConnectionsId: (args) => request("DELETE", "/connections/{id}", { hasBody: false }, args),
|
|
270
|
+
// POST /connections/{id}/accept — rank: any-builder — POST /connections/:id/accept
|
|
271
|
+
postConnectionsIdAccept: (args) => request("POST", "/connections/{id}/accept", { hasBody: true }, args),
|
|
272
|
+
// POST /connections/{id}/decline — rank: any-builder — POST /connections/:id/decline
|
|
273
|
+
postConnectionsIdDecline: (args) => request("POST", "/connections/{id}/decline", { hasBody: true }, args),
|
|
274
|
+
// GET /connections/degree/{handle} — rank: any-builder — GET /connections/degree/:handle
|
|
275
|
+
getConnectionsDegreeHandle: (args) => request("GET", "/connections/degree/{handle}", { hasBody: false }, args),
|
|
276
|
+
// GET /connections/requests — rank: any-builder — GET /connections/requests
|
|
277
|
+
getConnectionsRequests: (args) => request("GET", "/connections/requests", { hasBody: false }, args),
|
|
278
|
+
},
|
|
263
279
|
"copyDesk": {
|
|
264
280
|
// POST /copy-desk/flags — rank: any-builder — POST /copy-desk/flags
|
|
265
281
|
postCopyDeskFlags: (args) => request("POST", "/copy-desk/flags", { hasBody: true }, args),
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# ADR 0207 — Building the connection graph: three departures from ADR 0202, and the three limits it is honest about
|
|
2
|
+
|
|
3
|
+
- **Status:** accepted
|
|
4
|
+
- **Date:** 2026-08-29
|
|
5
|
+
- **Task:** [task 1002295](https://cloudbongos.com/builders#/task/1002295) (goal 1000045 — Public builder profiles, criterion C6)
|
|
6
|
+
- **Amends:** [ADR 0202](<redacted>.md) (the design of record — this ADR does not replace any decision there, it records where the build had to choose and what the build cannot promise)
|
|
7
|
+
- **Relates to:** ADR 0016 (a connection is never authority), ADR 0171 D4 (the leak-by-subtraction ban), ADR 0182 (a predicate written once as JS and as a SQL fragment), ADR 0083 §5 (module-owned, namespaced migrations), ADR 0206 (the own-scope guard the connection surface joins)
|
|
8
|
+
|
|
9
|
+
> ADR **0205** and **0206** were taken by sibling sessions in flight; this ADR takes 0207 rather than race them (the ADR 0199 / 0202 precedent).
|
|
10
|
+
|
|
11
|
+
## Context
|
|
12
|
+
|
|
13
|
+
ADR 0202 decided the connection graph completely enough to build from, which is what a design ADR should do. Building it surfaced three places where the decision's *mechanism* could not be implemented as literally written without losing the *property* the decision was actually about, and three places where the shipped code cannot deliver as much as a casual reading of 0202 would suggest.
|
|
14
|
+
|
|
15
|
+
Both kinds are worth writing down. A departure nobody records reads later as a bug. A limit nobody records reads later as a guarantee — and this feature's limits are all privacy-shaped, which is exactly the category where a false guarantee is expensive.
|
|
16
|
+
|
|
17
|
+
None of this changes what 0202 decided. Every decision below is downstream of one of its four.
|
|
18
|
+
|
|
19
|
+
## Decision
|
|
20
|
+
|
|
21
|
+
### D1 — Level-wise CTEs, not `WITH RECURSIVE`
|
|
22
|
+
|
|
23
|
+
ADR 0202 D3 says "a recursive CTE seeded at the viewer, hard-capped at depth 3 with a per-level fan-out cap and a deterministic frontier order". **We implement the semantics and not the mechanism**, because a recursive term in PostgreSQL cannot carry either of the two properties D3 was actually deciding: you cannot apply a `LIMIT` to a single level of a recursive term, and you cannot order a level before truncating it. A recursive implementation would therefore have had a *depth* bound and no *fan-out* bound — the half of D3 that matters for a hub-heavy account.
|
|
24
|
+
|
|
25
|
+
`degreeBetween` instead runs level-wise CTEs, staged exactly as D3 describes the three shapes: one query answers 1st and 2nd (and yields the mutual list free from the intersection), and a second runs only when the first found nothing. Each level is `ORDER BY gid LIMIT $n` with the cap bound as a parameter, so a truncation is reproducible rather than a different answer each call — which was D3's stated reason for a deterministic frontier order.
|
|
26
|
+
|
|
27
|
+
**Consequence for readers:** `tests/connections_api.mjs` asserts `RECURSIVE` appears in neither query. That is not a style rule; it is the check that nobody "fixes" this back toward the ADR's wording and loses the per-level cap.
|
|
28
|
+
|
|
29
|
+
### D2 — 1st degree is an unbounded pair-index probe, never a scan of the capped frontier
|
|
30
|
+
|
|
31
|
+
D3's own words: "1st = one index hit on the accepted-pair index." The first implementation read `is_first` off the already-materialised `v1` frontier instead, because it was there — which is wrong the moment a builder passes `FANOUT_1` connections: a target whose `github_id` sorts past the 500th smallest would be reported **2nd** while simultaneously appearing in that viewer's own connections list.
|
|
32
|
+
|
|
33
|
+
The fix is to keep 1st degree on `platform_identity_connections_pair_accepted`, unbounded, exactly as decided. **A cap may only ever make an answer *less* specific (`null`), never *wrong*.** That sentence is the rule this decision exists to state, and it is why the dedicated partial index earns its write cost.
|
|
34
|
+
|
|
35
|
+
### D3 — The shared visibility predicate lands in its own file, and task 1002967 adopts it
|
|
36
|
+
|
|
37
|
+
ADR 0202 D4 requires connections to consume "the single effective-visibility reader task 1002967 establishes… published in both forms the ADR 0182 precedent requires". At build time 1002967 had not shipped and `platform-identity.js` was at the 1500-line fitness ratchet with zero headroom, so there was nowhere to put it inside the file the ADR imagined.
|
|
38
|
+
|
|
39
|
+
The definition therefore lands alone in **`modules/platform-identity/account-visibility.js`** — `isAccountVisible(account)`, `accountVisibleSql(alias)`, and `accountActiveSql(alias)` (the consent-only floor the own-scope reads use). **Task 1002967 must ADOPT this file rather than write a second predicate**; a second one is the precise drift D4 exists to prevent, and this ADR is the pointer that keeps that from happening by accident.
|
|
40
|
+
|
|
41
|
+
**Rejected.** *Inlining the predicate in `connections.js`* — it is the drift, one release early. *Growing `platform-identity.js`* — the ratchet is shrink-only and the file is a different concern anyway.
|
|
42
|
+
|
|
43
|
+
### D4 — Node visibility during traversal is viewer-relative, and asymmetric on purpose
|
|
44
|
+
|
|
45
|
+
0202 says two things that only reconcile one way. D4 §2: a private account's connections "appear in its own-scope list and in the counterparty's own-scope list". The ADR's title: every degree label is computed "over the graph the viewer is allowed to see". So:
|
|
46
|
+
|
|
47
|
+
- **The viewer's OWN 1-hop floors on `profile_state = 'active'` alone.** Your own connections are yours to see whatever their visibility — otherwise a friend who goes private silently disappears from your own list, which §2 forbids.
|
|
48
|
+
- **Every DEEPER hop clears the full public predicate** (`accountVisibleSql`). A bridge you may not see yields `null`, never "3rd".
|
|
49
|
+
|
|
50
|
+
The asymmetry is the model, not an inconsistency: the first hop is a relationship you are party to, and the rest is other people's.
|
|
51
|
+
|
|
52
|
+
### D5 — Cancel is a `DELETE`, and an addressee may not use it
|
|
53
|
+
|
|
54
|
+
0202 names `cancelled` in the lifecycle but gives it no verb, and the task title does not list one. `DELETE /connections/:id` does both jobs, keyed on the row's status: hard-delete an `accepted` row (either party may end it) or `cancelled` a `pending` one (**the requester only**). An addressee calling `DELETE` on a pending row gets 404 and must use `/decline` — otherwise the 90-day cooldown is dodged by calling a decline a delete, which would quietly undo D2's whole point.
|
|
55
|
+
|
|
56
|
+
### D6 — The create response is `201 { status: 'pending' }` and nothing else
|
|
57
|
+
|
|
58
|
+
D2 §2 requires a swallowed re-ask to return "the same body it returns for a request that was genuinely created". The cheapest way to guarantee that for all time is for the body to contain nothing that a real create has and a swallowed one could not fabricate — no id, no timestamps. Clients read the new row from `GET /connections/requests`.
|
|
59
|
+
|
|
60
|
+
## The three limits, stated rather than implied
|
|
61
|
+
|
|
62
|
+
### L1 — A swallowed re-ask is silent in the response and observable in the outbox
|
|
63
|
+
|
|
64
|
+
Nothing is written, so the request never appears in the requester's own outbox, while a genuine one does within milliseconds. The **response** discloses nothing (D2 §2 holds); the **absence afterwards** is observable by anyone who looks.
|
|
65
|
+
|
|
66
|
+
We accept this, because ADR 0202's own Consequences already do — *"a builder can send the same request twice a year and never learn why nothing happens"* — and the alternative is worse: writing a decoy row would put a lie in the database to protect a lie in the API, and a decoy is data neither party consented to. **What changes here is only the claim**: the code comments and this ADR say "the response is silent", not "the system is silent".
|
|
67
|
+
|
|
68
|
+
### L2 — The target's 1-hop is capped too, and that truncation is deliberately not reported
|
|
69
|
+
|
|
70
|
+
`truncated` is derived from the **viewer's** counts only. If the *target* has more than `FANOUT_1` connections, a genuine mutual can fall outside the intersection and downgrade a real 2nd degree to `null` while `truncated` still reads `false`.
|
|
71
|
+
|
|
72
|
+
Reporting it would leak one bit about a third party's degree ("this account has ≥500 connections") on a read that is otherwise entirely about the viewer. **An occasional `null` is a better trade than a reliable side channel**, and `null` already means "out of network", which is what the UI must render anyway. D2 above removes the case that actually mattered (a 1st-degree connection mislabelled); what remains degrades in the safe direction.
|
|
73
|
+
|
|
74
|
+
### L3 — The query count is bounded; the frontier's work is not bounded by `FANOUT_2`
|
|
75
|
+
|
|
76
|
+
`degreeBetween` issues at most two queries whatever the graph does, and the *output* of each level is capped — both are asserted against the SQL that really ran. But `ORDER BY … LIMIT` must materialise and sort the frontier before it truncates, so the 2-hop expansion's **work** is O(Σ degree over the viewer's 1-hop), not O(`FANOUT_2`).
|
|
77
|
+
|
|
78
|
+
ADR 0202 D3's envelope ("the cap converts that from a slow query into a *missing* 3rd-degree label") is therefore true of the response and not of the cost. At the hub's present scale — order 10² accounts — the difference is unobservable. **The revisit trigger is a measured p95 on this endpoint, not a guess**; the cheap next move if it bites is a `statement_timeout` on the far query that returns `degree: null, truncated: true`, which is the degradation D3 already chose.
|
|
79
|
+
|
|
80
|
+
## Consequences
|
|
81
|
+
|
|
82
|
+
- ADR 0202 remains the design of record. This ADR is where a reader finds out why the code does not contain `WITH RECURSIVE`, why `account-visibility.js` exists as its own file, and which of the feature's silences are real.
|
|
83
|
+
- Three rules are now enforced by tests rather than by memory: a cap may not make an answer wrong (D2), the own-scope list floors on consent and not on the public predicate (D4), and a decline may not be resurrected from an expired request (D5's sibling — the expiry guard `acceptConnection` always had and `declineConnection` initially lacked).
|
|
84
|
+
- The connection surface joins `tests/own_scope_guard.mjs`'s inventory (ADR 0206's guard), so a future edit that lets a route take its subject from the request fails in CI rather than in review.
|
|
85
|
+
- `neighbours(githubId)` and `mutualCount(viewer, target)` — named in ADR 0202's "what this unblocks" — are **not** exported yet. The repo's knip ratchet counts an export with no caller as dead code, so they land with their first consumer ([task 1002309](https://cloudbongos.com/builders#/task/1002309)); the one-hop definition they must reuse is `neighboursSql` in `connections.js`, and re-deriving a direction-aware `WHERE` instead is the drift that file's header forbids.
|
|
86
|
+
|
|
87
|
+
## Deferred (named, not silently dropped)
|
|
88
|
+
|
|
89
|
+
- **A per-builder cap on outbound requests.** The 90-day cooldown stops repeat asks to the *same* person; nothing stops one builder asking a hundred different people, and only the global write limiter is in the way. A real number is an owner-signed value on the same privacy matrix ADR 0202 D2 came from, so it is named here rather than invented by a builder.
|
|
90
|
+
- **A real-Postgres pass on the write path.** CI has no Postgres, so the shipped proofs are DB-free by necessity. The statement most worth executing once against a live PG 15 is `requestConnection`'s expire-then-insert: it depends on the `guard` CTE forcing the modifying CTE to drain before the INSERT's unique check, and on the just-expired tuple reading as dead to that check. Both were reasoned through and reviewed; neither has been observed.
|
package/docs/adr/README.md
CHANGED
|
@@ -294,5 +294,6 @@ 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
|
+
| 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 |
|
|
297
298
|
|
|
298
299
|
> ⚠️ **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.
|