@bongos/core 1.19.172 → 1.19.173
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 +27 -22
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -0
- package/clients/bongos-client/index.cjs +4 -0
- package/clients/bongos-client/index.d.ts +5 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +60 -3
- package/docs/api-reference.md +7 -1
- package/modules/dev-box/app/src/vendor/bongos-client.cjs +4 -0
- package/modules/platform-identity/module.json +1 -1
- package/modules/platform-identity/platform-identity.js +56 -0
- package/modules/platform-identity/routes/public-profile.js +49 -0
- package/modules/platform-identity/tests/platform-identity.mjs +53 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/src/module-api.js +3 -1
- package/tests/handle_machinery.mjs +28 -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.173",
|
|
6
|
+
"core_contract": "1.19.173",
|
|
7
|
+
"source_commit": "dfc63944c504d88bf4ebb93b033c06b7b3b472a7",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-08-16T19:
|
|
9
|
+
"built_at": "2026-08-16T19:48:53.327Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 305,
|
|
13
13
|
"agent_docs_stubbed": 21,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 1272,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 1598,
|
|
20
|
+
"tree_sha256": "ac7b1f942e74709cb79d331fe15d21c4fcfdb8c30c8f0d88fd199404d06462b1",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/blocker-review/SKILL.md",
|
|
@@ -347,12 +347,12 @@
|
|
|
347
347
|
{
|
|
348
348
|
"path": "clients/bongos-client/README.md",
|
|
349
349
|
"mode": "0000644",
|
|
350
|
-
"sha256": "
|
|
350
|
+
"sha256": "6b865314ad2d5e2e3e0e017a6bed7ae435ecfbca3859bdf763b59411a1d3d9f8"
|
|
351
351
|
},
|
|
352
352
|
{
|
|
353
353
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
354
354
|
"mode": "0000644",
|
|
355
|
-
"sha256": "
|
|
355
|
+
"sha256": "c137a0e79a5e9065b071f87150639bff98cbda8f0d50a27027b42fa48ae200f6"
|
|
356
356
|
},
|
|
357
357
|
{
|
|
358
358
|
"path": "clients/bongos-client/examples/hello-world.mjs",
|
|
@@ -362,17 +362,17 @@
|
|
|
362
362
|
{
|
|
363
363
|
"path": "clients/bongos-client/index.cjs",
|
|
364
364
|
"mode": "0000644",
|
|
365
|
-
"sha256": "
|
|
365
|
+
"sha256": "2fef3ad1be5b853de1d6df155bb5717b581cf8b6afa3851add9ea81032c159a6"
|
|
366
366
|
},
|
|
367
367
|
{
|
|
368
368
|
"path": "clients/bongos-client/index.d.ts",
|
|
369
369
|
"mode": "0000644",
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "52dbea93e7039796ebf579e816bd3ed54727077e5383d27a348d36572a85b90d"
|
|
371
371
|
},
|
|
372
372
|
{
|
|
373
373
|
"path": "clients/bongos-client/index.mjs",
|
|
374
374
|
"mode": "0000644",
|
|
375
|
-
"sha256": "
|
|
375
|
+
"sha256": "b2d21163a9fb149a98051dfde08e1cee81969194e74d3e272bb7779da9461c1b"
|
|
376
376
|
},
|
|
377
377
|
{
|
|
378
378
|
"path": "clients/bongos-client/package.json",
|
|
@@ -1347,12 +1347,12 @@
|
|
|
1347
1347
|
{
|
|
1348
1348
|
"path": "docs/api-reference.md",
|
|
1349
1349
|
"mode": "0000644",
|
|
1350
|
-
"sha256": "
|
|
1350
|
+
"sha256": "74ee845dae6f2757b564c1a760042391f3eb3af21dd3abe4a8590b3c3f88c4b3"
|
|
1351
1351
|
},
|
|
1352
1352
|
{
|
|
1353
1353
|
"path": "docs/api/openapi.json",
|
|
1354
1354
|
"mode": "0000644",
|
|
1355
|
-
"sha256": "
|
|
1355
|
+
"sha256": "1c8b42344fa248bd6e38d370cc117c5017fdd58071ba9cb2ca6e55a1678c5e56"
|
|
1356
1356
|
},
|
|
1357
1357
|
{
|
|
1358
1358
|
"path": "docs/architecture.md",
|
|
@@ -2662,7 +2662,7 @@
|
|
|
2662
2662
|
{
|
|
2663
2663
|
"path": "modules/dev-box/app/src/vendor/bongos-client.cjs",
|
|
2664
2664
|
"mode": "0000644",
|
|
2665
|
-
"sha256": "
|
|
2665
|
+
"sha256": "2fef3ad1be5b853de1d6df155bb5717b581cf8b6afa3851add9ea81032c159a6"
|
|
2666
2666
|
},
|
|
2667
2667
|
{
|
|
2668
2668
|
"path": "modules/dev-box/app/test/unit.mjs",
|
|
@@ -3797,12 +3797,12 @@
|
|
|
3797
3797
|
{
|
|
3798
3798
|
"path": "modules/platform-identity/module.json",
|
|
3799
3799
|
"mode": "0000644",
|
|
3800
|
-
"sha256": "
|
|
3800
|
+
"sha256": "c4c02577f7318e994301bbe700e20dfb24519e411fee04539755f7374a73590b"
|
|
3801
3801
|
},
|
|
3802
3802
|
{
|
|
3803
3803
|
"path": "modules/platform-identity/platform-identity.js",
|
|
3804
3804
|
"mode": "0000644",
|
|
3805
|
-
"sha256": "
|
|
3805
|
+
"sha256": "59d9ef7d052890fdf09fd0a9f04432ab1baddc7c8dfe49950893b9dd67b89d66"
|
|
3806
3806
|
},
|
|
3807
3807
|
{
|
|
3808
3808
|
"path": "modules/platform-identity/pollers/code-sweep.js",
|
|
@@ -3829,6 +3829,11 @@
|
|
|
3829
3829
|
"mode": "0000644",
|
|
3830
3830
|
"sha256": "a3e25369ad9edc6aed1a8a6997d6b6cb794dce14abb3605954d535525af1a93a"
|
|
3831
3831
|
},
|
|
3832
|
+
{
|
|
3833
|
+
"path": "modules/platform-identity/routes/public-profile.js",
|
|
3834
|
+
"mode": "0000644",
|
|
3835
|
+
"sha256": "9032ff152e6440187f0f9bd2b1a3bb9ea24167c9f6d79ae3345a26a0e2fdc356"
|
|
3836
|
+
},
|
|
3832
3837
|
{
|
|
3833
3838
|
"path": "modules/platform-identity/routes/scouting.js",
|
|
3834
3839
|
"mode": "0000644",
|
|
@@ -3842,7 +3847,7 @@
|
|
|
3842
3847
|
{
|
|
3843
3848
|
"path": "modules/platform-identity/tests/platform-identity.mjs",
|
|
3844
3849
|
"mode": "0000644",
|
|
3845
|
-
"sha256": "
|
|
3850
|
+
"sha256": "53b5bdd5c9eb4a14695289dd1396231d29e0f53c8709bd591dc328362da88072"
|
|
3846
3851
|
},
|
|
3847
3852
|
{
|
|
3848
3853
|
"path": "modules/platform-identity/ui/.gitkeep",
|
|
@@ -4217,12 +4222,12 @@
|
|
|
4217
4222
|
{
|
|
4218
4223
|
"path": "package-lock.json",
|
|
4219
4224
|
"mode": "0000644",
|
|
4220
|
-
"sha256": "
|
|
4225
|
+
"sha256": "6c8e9ca33aba6a1b42addf6f862daad54f2e0ba87a43090129a2afe23edc53e6"
|
|
4221
4226
|
},
|
|
4222
4227
|
{
|
|
4223
4228
|
"path": "package.json",
|
|
4224
4229
|
"mode": "0000644",
|
|
4225
|
-
"sha256": "
|
|
4230
|
+
"sha256": "1c163fb61de2b5c9024266f6b1bd494687307421fb05370874842e67639e0311"
|
|
4226
4231
|
},
|
|
4227
4232
|
{
|
|
4228
4233
|
"path": "public-docs/index.html",
|
|
@@ -5827,7 +5832,7 @@
|
|
|
5827
5832
|
{
|
|
5828
5833
|
"path": "src/module-api.js",
|
|
5829
5834
|
"mode": "0000644",
|
|
5830
|
-
"sha256": "
|
|
5835
|
+
"sha256": "479f80926d0dfcd56bfb6b47dfe8fac208ce8ce3c873e6a016c4af0e5f015edd"
|
|
5831
5836
|
},
|
|
5832
5837
|
{
|
|
5833
5838
|
"path": "src/module-loader/catalog.js",
|
|
@@ -6912,7 +6917,7 @@
|
|
|
6912
6917
|
{
|
|
6913
6918
|
"path": "tests/handle_machinery.mjs",
|
|
6914
6919
|
"mode": "0000644",
|
|
6915
|
-
"sha256": "
|
|
6920
|
+
"sha256": "a86e615af8b148572abf562b3c7b0d556076286e76e2d8d576f782a90ba3d457"
|
|
6916
6921
|
},
|
|
6917
6922
|
{
|
|
6918
6923
|
"path": "tests/helpers.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
|
+
- 275 operations across 46 resource groups
|
|
9
9
|
|
|
10
10
|
## Install
|
|
11
11
|
|
|
@@ -502,6 +502,10 @@ function createClient(opts = {}) {
|
|
|
502
502
|
// POST /override-requests/{id}/decide — rank: metic+archon — POST /override-requests/:id/decide
|
|
503
503
|
postOverrideRequestsIdDecide: (args) => request("POST", "/override-requests/{id}/decide", { hasBody: true }, args),
|
|
504
504
|
},
|
|
505
|
+
"profiles": {
|
|
506
|
+
// GET /profiles/{handle} — rank: public — GET /profiles/:handle
|
|
507
|
+
getProfilesHandle: (args) => request("GET", "/profiles/{handle}", { hasBody: false }, args),
|
|
508
|
+
},
|
|
505
509
|
"projects": {
|
|
506
510
|
// GET /projects — rank: metic+archon — GET /projects
|
|
507
511
|
getProjects: (args) => request("GET", "/projects", { hasBody: false }, args),
|
|
@@ -501,6 +501,10 @@ function createClient(opts = {}) {
|
|
|
501
501
|
// POST /override-requests/{id}/decide — rank: metic+archon — POST /override-requests/:id/decide
|
|
502
502
|
postOverrideRequestsIdDecide: (args) => request("POST", "/override-requests/{id}/decide", { hasBody: true }, args),
|
|
503
503
|
},
|
|
504
|
+
"profiles": {
|
|
505
|
+
// GET /profiles/{handle} — rank: public — GET /profiles/:handle
|
|
506
|
+
getProfilesHandle: (args) => request("GET", "/profiles/{handle}", { hasBody: false }, args),
|
|
507
|
+
},
|
|
504
508
|
"projects": {
|
|
505
509
|
// GET /projects — rank: metic+archon — GET /projects
|
|
506
510
|
getProjects: (args) => request("GET", "/projects", { hasBody: false }, args),
|
|
@@ -68,6 +68,7 @@ export interface GetModulesSubmissionsResponse { submissions: unknown }
|
|
|
68
68
|
export interface GetMyProjectsInvitesResponse { invites: unknown }
|
|
69
69
|
export interface GetMyProjectsResponse { owned: unknown; building: unknown }
|
|
70
70
|
export interface GetOverrideRequestsResponse { requests: unknown }
|
|
71
|
+
export interface GetProfilesHandleResponse { account: unknown; cross_project: unknown }
|
|
71
72
|
export interface GetProjectsFeaturedResponse { projects: unknown }
|
|
72
73
|
export interface GetProjectsResponse { projects: unknown }
|
|
73
74
|
export interface GetProvisioningFleetResponse { instances: unknown }
|
|
@@ -743,6 +744,10 @@ export interface BongosClient {
|
|
|
743
744
|
/** POST /override-requests/{id}/decide — rank: metic+archon */
|
|
744
745
|
postOverrideRequestsIdDecide(args: RequestArgs & { body: PostOverrideRequestsIdDecideRequest }): Promise<ApiResponse>;
|
|
745
746
|
};
|
|
747
|
+
"profiles": {
|
|
748
|
+
/** GET /profiles/{handle} — rank: public */
|
|
749
|
+
getProfilesHandle(args?: RequestArgs): Promise<GetProfilesHandleResponse>;
|
|
750
|
+
};
|
|
746
751
|
"projects": {
|
|
747
752
|
/** GET /projects — rank: metic+archon */
|
|
748
753
|
getProjects(args?: RequestArgs): Promise<GetProjectsResponse>;
|
|
@@ -498,6 +498,10 @@ export function createClient(opts = {}) {
|
|
|
498
498
|
// POST /override-requests/{id}/decide — rank: metic+archon — POST /override-requests/:id/decide
|
|
499
499
|
postOverrideRequestsIdDecide: (args) => request("POST", "/override-requests/{id}/decide", { hasBody: true }, args),
|
|
500
500
|
},
|
|
501
|
+
"profiles": {
|
|
502
|
+
// GET /profiles/{handle} — rank: public — GET /profiles/:handle
|
|
503
|
+
getProfilesHandle: (args) => request("GET", "/profiles/{handle}", { hasBody: false }, args),
|
|
504
|
+
},
|
|
501
505
|
"projects": {
|
|
502
506
|
// GET /projects — rank: metic+archon — GET /projects
|
|
503
507
|
getProjects: (args) => request("GET", "/projects", { hasBody: false }, args),
|
package/docs/api/openapi.json
CHANGED
|
@@ -157,6 +157,10 @@
|
|
|
157
157
|
"name": "override-requests",
|
|
158
158
|
"description": "Endpoints under `/api/bongos/override-requests`."
|
|
159
159
|
},
|
|
160
|
+
{
|
|
161
|
+
"name": "profiles",
|
|
162
|
+
"description": "Endpoints under `/api/bongos/profiles`."
|
|
163
|
+
},
|
|
160
164
|
{
|
|
161
165
|
"name": "projects",
|
|
162
166
|
"description": "Endpoints under `/api/bongos/projects`."
|
|
@@ -8163,6 +8167,48 @@
|
|
|
8163
8167
|
]
|
|
8164
8168
|
}
|
|
8165
8169
|
},
|
|
8170
|
+
"/profiles/{handle}": {
|
|
8171
|
+
"get": {
|
|
8172
|
+
"operationId": "get_profiles_handle",
|
|
8173
|
+
"tags": [
|
|
8174
|
+
"profiles"
|
|
8175
|
+
],
|
|
8176
|
+
"summary": "GET /profiles/:handle",
|
|
8177
|
+
"description": "a handle-addressed public profile. Every field is already world-readable on the numeric profile route + the leaderboard; the handle is just the rename-proof address (spec Decision 3). Whitelisting happens inside the module (getPublicProfileByHandle); no-store because the payload embeds cross_project and hide must beat the cache (ADR 0171 D5).\n\n**Rank:** `public` — No authentication — any caller.",
|
|
8178
|
+
"x-rank": "public",
|
|
8179
|
+
"x-source": "modules/platform-identity/routes/public-profile.js",
|
|
8180
|
+
"parameters": [
|
|
8181
|
+
{
|
|
8182
|
+
"name": "handle",
|
|
8183
|
+
"in": "path",
|
|
8184
|
+
"required": true,
|
|
8185
|
+
"schema": {
|
|
8186
|
+
"type": "string"
|
|
8187
|
+
},
|
|
8188
|
+
"description": "Path parameter `handle`."
|
|
8189
|
+
}
|
|
8190
|
+
],
|
|
8191
|
+
"responses": {
|
|
8192
|
+
"200": {
|
|
8193
|
+
"description": "Success.",
|
|
8194
|
+
"content": {
|
|
8195
|
+
"application/json": {
|
|
8196
|
+
"schema": {
|
|
8197
|
+
"$ref": "#/components/schemas/GetProfilesHandleResponse"
|
|
8198
|
+
}
|
|
8199
|
+
}
|
|
8200
|
+
}
|
|
8201
|
+
},
|
|
8202
|
+
"400": {
|
|
8203
|
+
"$ref": "#/components/responses/BadRequest"
|
|
8204
|
+
},
|
|
8205
|
+
"404": {
|
|
8206
|
+
"$ref": "#/components/responses/NotFound"
|
|
8207
|
+
}
|
|
8208
|
+
},
|
|
8209
|
+
"security": []
|
|
8210
|
+
}
|
|
8211
|
+
},
|
|
8166
8212
|
"/projects": {
|
|
8167
8213
|
"get": {
|
|
8168
8214
|
"operationId": "get_projects",
|
|
@@ -13253,6 +13299,17 @@
|
|
|
13253
13299
|
"requests"
|
|
13254
13300
|
]
|
|
13255
13301
|
},
|
|
13302
|
+
"GetProfilesHandleResponse": {
|
|
13303
|
+
"type": "object",
|
|
13304
|
+
"properties": {
|
|
13305
|
+
"account": {},
|
|
13306
|
+
"cross_project": {}
|
|
13307
|
+
},
|
|
13308
|
+
"required": [
|
|
13309
|
+
"account",
|
|
13310
|
+
"cross_project"
|
|
13311
|
+
]
|
|
13312
|
+
},
|
|
13256
13313
|
"GetProjectsFeaturedResponse": {
|
|
13257
13314
|
"type": "object",
|
|
13258
13315
|
"properties": {
|
|
@@ -17298,9 +17355,9 @@
|
|
|
17298
17355
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
17299
17356
|
}
|
|
17300
17357
|
},
|
|
17301
|
-
"x-endpoint-count":
|
|
17302
|
-
"x-schema-count":
|
|
17358
|
+
"x-endpoint-count": 275,
|
|
17359
|
+
"x-schema-count": 309,
|
|
17303
17360
|
"x-undocumented-bodies": 8,
|
|
17304
|
-
"x-response-schemas":
|
|
17361
|
+
"x-response-schemas": 209,
|
|
17305
17362
|
"x-generated-by": "scripts/gds/gen-api-docs.js"
|
|
17306
17363
|
}
|
package/docs/api-reference.md
CHANGED
|
@@ -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.
|
|
5
|
+
> **Generated from the live route files** — the route file is authoritative. 275 endpoints across 46 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`.
|
|
@@ -352,6 +352,12 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/<redacted>
|
|
|
352
352
|
| GET | `/api/bongos/override-requests` | `metic+archon` | — | GET /override-requests?status=open — Archon queue. |
|
|
353
353
|
| POST | `/api/bongos/override-requests/:id/decide` | `metic+archon` | `decision`, `note` | POST /override-requests/:id/decide — body: { decision: 'approve'\|'deny', note? } On approve, atomically flips the task completed→confirme… |
|
|
354
354
|
|
|
355
|
+
## `profiles` (1)
|
|
356
|
+
|
|
357
|
+
| Method | Path | Rank | Body | Description |
|
|
358
|
+
|---|---|---|---|---|
|
|
359
|
+
| GET | `/api/bongos/profiles/:handle` | `public` | — | a handle-addressed public profile. |
|
|
360
|
+
|
|
355
361
|
## `projects` (5)
|
|
356
362
|
|
|
357
363
|
| Method | Path | Rank | Body | Description |
|
|
@@ -501,6 +501,10 @@ function createClient(opts = {}) {
|
|
|
501
501
|
// POST /override-requests/{id}/decide — rank: metic+archon — POST /override-requests/:id/decide
|
|
502
502
|
postOverrideRequestsIdDecide: (args) => request("POST", "/override-requests/{id}/decide", { hasBody: true }, args),
|
|
503
503
|
},
|
|
504
|
+
"profiles": {
|
|
505
|
+
// GET /profiles/{handle} — rank: public — GET /profiles/:handle
|
|
506
|
+
getProfilesHandle: (args) => request("GET", "/profiles/{handle}", { hasBody: false }, args),
|
|
507
|
+
},
|
|
504
508
|
"projects": {
|
|
505
509
|
// GET /projects — rank: metic+archon — GET /projects
|
|
506
510
|
getProjects: (args) => request("GET", "/projects", { hasBody: false }, args),
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"maintainer": "Cloud Bongos maintainers",
|
|
11
11
|
"maintenance": { "status": "core-maintained" },
|
|
12
12
|
"contributes": {
|
|
13
|
-
"routes": ["sso", "projects", "my-projects", "scouting", "profile"],
|
|
13
|
+
"routes": ["sso", "projects", "my-projects", "scouting", "profile", "public-profile"],
|
|
14
14
|
"migrations": true,
|
|
15
15
|
"pollers": ["code-sweep"]
|
|
16
16
|
},
|
|
@@ -748,6 +748,60 @@ async function setAccountHandle({ githubId, handle }, { pool = defaultPool } = {
|
|
|
748
748
|
}
|
|
749
749
|
}
|
|
750
750
|
|
|
751
|
+
// Resolve a public handle → its account (R05, task 1002281 — the /u/<handle>
|
|
752
|
+
// resolution). Case-insensitive. An exact current-handle match always wins;
|
|
753
|
+
// otherwise the single-slot previous_handle matches and answers a move to the
|
|
754
|
+
// current handle (the 301 window from P01). Only publicly addressable accounts
|
|
755
|
+
// resolve (profile_state = 'active', ADR 0171 D3) — provisional and unknown are
|
|
756
|
+
// the same null (the 404, indistinguishable by design). hide_stats deliberately
|
|
757
|
+
// does NOT gate resolution: hiding removes the rollup, not the page (D2 —
|
|
758
|
+
// addressability and hide are different invariants).
|
|
759
|
+
// Returns null | { moved_to } | { account: { github_id, github_login,
|
|
760
|
+
// display_name, avatar_url, handle, created_at } }.
|
|
761
|
+
async function resolveHandle(handle, { pool = defaultPool } = {}) {
|
|
762
|
+
const norm = normalizeHandle(handle);
|
|
763
|
+
if (!norm) return null;
|
|
764
|
+
const { rows } = await pool.query(
|
|
765
|
+
`SELECT github_id, github_login, display_name, avatar_url, handle, created_at,
|
|
766
|
+
(lower(handle) = $1) AS is_current
|
|
767
|
+
FROM platform_identity_accounts
|
|
768
|
+
WHERE profile_state = 'active'
|
|
769
|
+
AND (lower(handle) = $1 OR lower(previous_handle) = $1)
|
|
770
|
+
ORDER BY (lower(handle) = $1) DESC
|
|
771
|
+
LIMIT 1`,
|
|
772
|
+
[norm],
|
|
773
|
+
);
|
|
774
|
+
const row = rows[0];
|
|
775
|
+
if (!row) return null;
|
|
776
|
+
if (!row.is_current) return { moved_to: row.handle };
|
|
777
|
+
const { is_current, ...account } = row;
|
|
778
|
+
return { account };
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// The one public read the canonical /u/<handle> page consumes (R05; the page
|
|
782
|
+
// itself is goal 1000048 R14, task 1002365 — exactly one page gets built, this
|
|
783
|
+
// is its read). Whitelist-shaped INSIDE the module: the identity-band fields
|
|
784
|
+
// plus the same cross_project payload the numeric instance profile route serves
|
|
785
|
+
// (null = hidden / none, indistinguishable — D4). profile_state and github_id
|
|
786
|
+
// never cross — a 200 IS the addressability signal, and the join key stays
|
|
787
|
+
// internal.
|
|
788
|
+
async function getPublicProfileByHandle(handle, { pool = defaultPool } = {}) {
|
|
789
|
+
const hit = await resolveHandle(handle, { pool });
|
|
790
|
+
if (!hit) return null;
|
|
791
|
+
if (hit.moved_to) return { moved_to: hit.moved_to };
|
|
792
|
+
const a = hit.account;
|
|
793
|
+
return {
|
|
794
|
+
account: {
|
|
795
|
+
handle: a.handle,
|
|
796
|
+
github_login: a.github_login,
|
|
797
|
+
display_name: a.display_name,
|
|
798
|
+
avatar_url: a.avatar_url,
|
|
799
|
+
member_since: a.created_at,
|
|
800
|
+
},
|
|
801
|
+
cross_project: await getPublicProfileExtras(a.github_id, { pool }),
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
751
805
|
// -------------------------------------------------------------------------
|
|
752
806
|
// Authorization codes (single-use, short-lived, bound)
|
|
753
807
|
// -------------------------------------------------------------------------
|
|
@@ -856,6 +910,8 @@ module.exports = {
|
|
|
856
910
|
handleSeedBase,
|
|
857
911
|
ensureHandle,
|
|
858
912
|
setAccountHandle,
|
|
913
|
+
resolveHandle,
|
|
914
|
+
getPublicProfileByHandle,
|
|
859
915
|
// codes
|
|
860
916
|
mintAuthCode,
|
|
861
917
|
redeemAuthCode,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// modules/platform-identity/routes/public-profile.js — the public handle-based
|
|
2
|
+
// profile read (R05, task 1002281; profile-page spec Decisions 1/3, ADR 0171 D8).
|
|
3
|
+
//
|
|
4
|
+
// GET /profiles/:handle — resolve a handle to the public profile payload the
|
|
5
|
+
// canonical /u/<handle> page consumes (goal 1000048 R14 / task 1002365 builds
|
|
6
|
+
// the PAGE; this is its read — exactly one page gets built). Answers:
|
|
7
|
+
// 404 — unknown handle, or a provisional account (indistinguishable by
|
|
8
|
+
// design: fail-closed consent, ADR 0171 D3)
|
|
9
|
+
// 301 — the request named a previous handle inside P01's single-slot
|
|
10
|
+
// window; Location + body point at the current handle
|
|
11
|
+
// 200 — { account: identity-band fields, cross_project: … | null }
|
|
12
|
+
//
|
|
13
|
+
// Reaches core only through the doorway (ADR 0083).
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const express = require('express');
|
|
17
|
+
const api = require('../../../src/module-api');
|
|
18
|
+
const pi = require('../platform-identity');
|
|
19
|
+
|
|
20
|
+
const { pool, corsPublicGet } = api;
|
|
21
|
+
|
|
22
|
+
module.exports = function publicProfileRoutes() {
|
|
23
|
+
const router = express.Router();
|
|
24
|
+
|
|
25
|
+
router.use('/profiles/:handle', corsPublicGet('/profiles/:handle'));
|
|
26
|
+
|
|
27
|
+
// rank: public — a handle-addressed public profile. Every field is already
|
|
28
|
+
// world-readable on the numeric profile route + the leaderboard; the handle is
|
|
29
|
+
// just the rename-proof address (spec Decision 3). Whitelisting happens inside
|
|
30
|
+
// the module (getPublicProfileByHandle); no-store because the payload embeds
|
|
31
|
+
// cross_project and hide must beat the cache (ADR 0171 D5).
|
|
32
|
+
router.get('/profiles/:handle', async (req, res) => {
|
|
33
|
+
try {
|
|
34
|
+
const out = await pi.getPublicProfileByHandle(req.params.handle, { pool });
|
|
35
|
+
res.set('Cache-Control', 'no-store');
|
|
36
|
+
if (!out) return res.fail('profile_not_found', 404);
|
|
37
|
+
if (out.moved_to) {
|
|
38
|
+
res.set('Location', req.originalUrl.replace(/\/profiles\/[^/?]+/, `/profiles/${encodeURIComponent(out.moved_to)}`));
|
|
39
|
+
return res.status(301).json({ moved_to: out.moved_to });
|
|
40
|
+
}
|
|
41
|
+
res.json({ account: out.account, cross_project: out.cross_project });
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.error('[platform-identity] GET /profiles/:handle', err);
|
|
44
|
+
res.fail('profile_read_failed', { status: 500, message: 'internal error' });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return router;
|
|
49
|
+
};
|
|
@@ -719,5 +719,58 @@ await ta('setAccountHandle: a taken handle → handle_taken', async () => {
|
|
|
719
719
|
assert.deepEqual(out, { error: 'handle_taken' });
|
|
720
720
|
});
|
|
721
721
|
|
|
722
|
+
console.log('\nhandle resolution (R05, task 1002281):');
|
|
723
|
+
|
|
724
|
+
await ta('resolveHandle: active-only, current-beats-previous, lowercased lookup', async () => {
|
|
725
|
+
const row = { github_id: '7', github_login: 'x', display_name: 'X', avatar_url: null, handle: 'x', created_at: 'T', is_current: true };
|
|
726
|
+
const pool = mockPool([{ rows: [row], rowCount: 1 }]);
|
|
727
|
+
const out = await P.resolveHandle('X', { pool });
|
|
728
|
+
const c = pool.calls[0];
|
|
729
|
+
assert.match(c.text, /profile_state = 'active'/, 'provisional accounts are not addressable (ADR 0171 D3)');
|
|
730
|
+
assert.match(c.text, /ORDER BY \(lower\(handle\) = \$1\) DESC/, 'an exact current-handle match always wins over a previous one');
|
|
731
|
+
assert.doesNotMatch(c.text, /hide_stats/, 'hide gates the rollup, never addressability (D2)');
|
|
732
|
+
assert.equal(c.params[0], 'x', 'the lookup is lowercased');
|
|
733
|
+
assert.equal(out.account.handle, 'x');
|
|
734
|
+
assert.ok(!('is_current' in out.account), 'the SQL flag stays internal');
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
await ta('resolveHandle: a previous-handle hit answers the move to the current handle', async () => {
|
|
738
|
+
const row = { github_id: '7', github_login: 'x', display_name: 'X', avatar_url: null, handle: 'newname', created_at: 'T', is_current: false };
|
|
739
|
+
const pool = mockPool([{ rows: [row], rowCount: 1 }]);
|
|
740
|
+
assert.deepEqual(await P.resolveHandle('oldname', { pool }), { moved_to: 'newname' });
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
await ta('resolveHandle: an invalid handle → null without touching the DB', async () => {
|
|
744
|
+
const pool = mockPool([]);
|
|
745
|
+
assert.equal(await P.resolveHandle('not a handle!', { pool }), null);
|
|
746
|
+
assert.equal(await P.resolveHandle('', { pool }), null);
|
|
747
|
+
assert.equal(pool.calls.length, 0);
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
await ta('getPublicProfileByHandle: identity-band whitelist — github_id/profile_state never cross', async () => {
|
|
751
|
+
const row = { github_id: '7', github_login: 'x', display_name: 'X', avatar_url: 'a', handle: 'x', created_at: 'T', is_current: true };
|
|
752
|
+
const pool = mockPool([
|
|
753
|
+
{ rows: [row], rowCount: 1 },
|
|
754
|
+
// getPublicProfileExtras' flags read: hidden → the extras stay null and no
|
|
755
|
+
// rollup query fires.
|
|
756
|
+
{ rows: [{ profile_state: 'active', hide_stats: true }], rowCount: 1 },
|
|
757
|
+
]);
|
|
758
|
+
const out = await P.getPublicProfileByHandle('x', { pool });
|
|
759
|
+
assert.deepEqual(Object.keys(out.account).sort(), ['avatar_url', 'display_name', 'github_login', 'handle', 'member_since'].sort());
|
|
760
|
+
assert.equal(out.cross_project, null, 'hidden → null, indistinguishable from none');
|
|
761
|
+
const text = JSON.stringify(out);
|
|
762
|
+
assert.doesNotMatch(text, /github_id/, 'the join key stays internal');
|
|
763
|
+
assert.doesNotMatch(text, /profile_state/, 'a 200 IS the addressability signal');
|
|
764
|
+
assert.doesNotMatch(text, /hide_stats/);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
await ta('getPublicProfileByHandle: unknown → null; moved passes through', async () => {
|
|
768
|
+
const pool1 = mockPool([{ rows: [], rowCount: 0 }]);
|
|
769
|
+
assert.equal(await P.getPublicProfileByHandle('ghost', { pool: pool1 }), null);
|
|
770
|
+
const moved = { github_id: '7', github_login: 'x', display_name: 'X', avatar_url: null, handle: 'now', created_at: 'T', is_current: false };
|
|
771
|
+
const pool2 = mockPool([{ rows: [moved], rowCount: 1 }]);
|
|
772
|
+
assert.deepEqual(await P.getPublicProfileByHandle('then', { pool: pool2 }), { moved_to: 'now' });
|
|
773
|
+
});
|
|
774
|
+
|
|
722
775
|
console.log(`\nplatform-identity: ${passed} passed, ${failed} failed`);
|
|
723
776
|
if (failed > 0) process.exit(1);
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.173",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.173",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.173",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/src/module-api.js
CHANGED
|
@@ -771,8 +771,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
771
771
|
// landed since 1.19.170 with no explicit bump. run 31967255167. (task 1002620)
|
|
772
772
|
// 1.19.172 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
773
773
|
// landed since 1.19.171 with no explicit bump. run 31968188738. (task 1002620)
|
|
774
|
+
// 1.19.173 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
775
|
+
// landed since 1.19.172 with no explicit bump. run 31968656001. (task 1002620)
|
|
774
776
|
// ---------------------------------------------------------------------------
|
|
775
|
-
const CORE_VERSION = '1.19.
|
|
777
|
+
const CORE_VERSION = '1.19.173'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
776
778
|
|
|
777
779
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
778
780
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -57,6 +57,8 @@ try {
|
|
|
57
57
|
// rows exist so its seeding loop has NULL handles to seed — exactly the
|
|
58
58
|
// legacy-rows-at-deploy shape.
|
|
59
59
|
await q(migrationSql('platform_identity_001_tables.sql'));
|
|
60
|
+
await q(migrationSql('platform_identity_004_projects.sql'));
|
|
61
|
+
await q(migrationSql('platform_identity_005_builder_activity.sql'));
|
|
60
62
|
await q(migrationSql('platform_identity_007_profile_consent.sql'));
|
|
61
63
|
// On a DB where 008 already ran the columns exist and the raw inserts below
|
|
62
64
|
// simply land with handle NULL — the re-run seeds only those. Idempotence is
|
|
@@ -108,6 +110,32 @@ try {
|
|
|
108
110
|
const r5 = await pi.setAccountHandle({ githubId: FRESH, handle: 'u' }, { pool });
|
|
109
111
|
ok(r5.error === 'reserved_handle', 'reserved words are refused on rename');
|
|
110
112
|
|
|
113
|
+
// --- 5. resolution (R05, task 1002281) ------------------------------------
|
|
114
|
+
// FRESH is 'p01-again' with previous 'p01-renamed'; everything is still
|
|
115
|
+
// provisional (fail-closed) at this point.
|
|
116
|
+
const prov = await pi.resolveHandle('p01-again', { pool });
|
|
117
|
+
ok(prov === null, 'a provisional account is not addressable — even by its current handle');
|
|
118
|
+
|
|
119
|
+
await q(`UPDATE platform_identity_accounts SET profile_state = 'active' WHERE github_id = ANY($1::bigint[])`, [[FRESH, TWIN_A]]);
|
|
120
|
+
|
|
121
|
+
const cur = await pi.resolveHandle('P01-Again', { pool });
|
|
122
|
+
ok(cur && cur.account && cur.account.handle === 'p01-again', 'the current handle resolves case-insensitively');
|
|
123
|
+
|
|
124
|
+
const moved = await pi.resolveHandle('p01-renamed', { pool });
|
|
125
|
+
ok(moved && moved.moved_to === 'p01-again', 'the previous handle answers a move to the current one');
|
|
126
|
+
|
|
127
|
+
const page = await pi.getPublicProfileByHandle('p01-again', { pool });
|
|
128
|
+
ok(page && page.account && page.account.member_since && !('github_id' in page.account),
|
|
129
|
+
'the public read carries the identity band, never the join key');
|
|
130
|
+
ok('cross_project' in page, 'the cross_project key is always present on a 200 (null = hidden/none)');
|
|
131
|
+
|
|
132
|
+
// Another active account claims the freed previous handle → the exact
|
|
133
|
+
// current-handle match beats the stale 301 (documented squat-forward rule).
|
|
134
|
+
await q(`UPDATE platform_identity_accounts SET handle = 'p01-renamed' WHERE github_id = $1`, [TWIN_A]);
|
|
135
|
+
const claimed = await pi.resolveHandle('p01-renamed', { pool });
|
|
136
|
+
ok(claimed && claimed.account && Number(claimed.account.github_id) === TWIN_A,
|
|
137
|
+
'an exact current-handle match wins over another account previous-handle claim');
|
|
138
|
+
|
|
111
139
|
console.log(`\nhandle_machinery: ${pass} assertions passed`);
|
|
112
140
|
} finally {
|
|
113
141
|
await cleanup();
|