@bongos/core 1.25.4 → 1.25.5
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 +48 -28
- 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 +8 -0
- package/clients/bongos-client/index.mjs +4 -0
- package/docs/api/openapi.json +185 -3
- package/docs/api-reference.md +5 -3
- package/docs/module-api-changelog.md +2 -0
- package/modules/provisioning/migrations/provisioning_037_update_subscribed.sql +21 -0
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/routes/provisioning.js +4 -0
- package/modules/provisioning/routes/update-subscription.js +51 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +10 -0
- package/scripts/bongos/update-channel.js +27 -4
- package/scripts/bongos/update-sweep.js +17 -3
- package/src/bongos/api-errors.js +2 -0
- package/src/bongos/db.js +43 -0
- package/src/bongos/route-rank-check.js +1 -0
- package/src/bongos/routes/builders.js +37 -0
- package/src/module-api.js +1 -1
- package/tests/builder_card_requirement.mjs +75 -0
- package/tests/provisioning_update_subscription_route.mjs +141 -0
- package/tests/update_channel_db.mjs +24 -0
package/.bongos-core.json
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
"artifact": "bongos-core",
|
|
3
3
|
"manifest_schema": 1,
|
|
4
4
|
"generator": "scripts/bongos/package-core.js",
|
|
5
|
-
"core_version": "1.25.
|
|
6
|
-
"core_contract": "1.25.
|
|
7
|
-
"source_commit": "
|
|
5
|
+
"core_version": "1.25.5",
|
|
6
|
+
"core_contract": "1.25.5",
|
|
7
|
+
"source_commit": "095393e44481a16a8764084c02bc478f88127826",
|
|
8
8
|
"source_ref": "HEAD",
|
|
9
|
-
"built_at": "2026-11-
|
|
9
|
+
"built_at": "2026-11-13T04:03:13.962Z",
|
|
10
10
|
"redaction": {
|
|
11
11
|
"model": "docs-redacted+functional-verbatim",
|
|
12
12
|
"docs_redacted": 654,
|
|
13
13
|
"agent_docs_stubbed": 32,
|
|
14
|
-
"functional_verbatim":
|
|
14
|
+
"functional_verbatim": 3996,
|
|
15
15
|
"rules": 3,
|
|
16
16
|
"gate_literals": 3,
|
|
17
17
|
"gate": "passed"
|
|
18
18
|
},
|
|
19
|
-
"file_count":
|
|
20
|
-
"tree_sha256": "
|
|
19
|
+
"file_count": 4683,
|
|
20
|
+
"tree_sha256": "7bee86259715879e702ebce474621fdcacaefdcbfec4f526604e05b59c9b3484",
|
|
21
21
|
"files": [
|
|
22
22
|
{
|
|
23
23
|
"path": ".claude/skills/ask-for-help/SKILL.md",
|
|
@@ -332,12 +332,12 @@
|
|
|
332
332
|
{
|
|
333
333
|
"path": "clients/bongos-client/README.md",
|
|
334
334
|
"mode": "0000644",
|
|
335
|
-
"sha256": "
|
|
335
|
+
"sha256": "f708a93778599fa4e5598d265766436184beb03b1ab2876aecd68c41eeab6358"
|
|
336
336
|
},
|
|
337
337
|
{
|
|
338
338
|
"path": "clients/bongos-client/bongos-client.global.js",
|
|
339
339
|
"mode": "0000644",
|
|
340
|
-
"sha256": "
|
|
340
|
+
"sha256": "01e1b81c1bd594dd0d2f044dfa2a366aa0b185dde4cd041001c551742e1b52d3"
|
|
341
341
|
},
|
|
342
342
|
{
|
|
343
343
|
"path": "clients/bongos-client/examples/hello-world.mjs",
|
|
@@ -347,17 +347,17 @@
|
|
|
347
347
|
{
|
|
348
348
|
"path": "clients/bongos-client/index.cjs",
|
|
349
349
|
"mode": "0000644",
|
|
350
|
-
"sha256": "
|
|
350
|
+
"sha256": "1e8e8ff332ccdd9fcf9f5b0847b6a7938903bf9564abc9a1e3c1070682afd1c7"
|
|
351
351
|
},
|
|
352
352
|
{
|
|
353
353
|
"path": "clients/bongos-client/index.d.ts",
|
|
354
354
|
"mode": "0000644",
|
|
355
|
-
"sha256": "
|
|
355
|
+
"sha256": "9fa2ee83d1d6b068f727c32f510dd9fdf1bd98d6caa1a401e9b263fd5af5eea9"
|
|
356
356
|
},
|
|
357
357
|
{
|
|
358
358
|
"path": "clients/bongos-client/index.mjs",
|
|
359
359
|
"mode": "0000644",
|
|
360
|
-
"sha256": "
|
|
360
|
+
"sha256": "6e9d62b3dcf0e8e760afa2aab9afcd0470ae4a1f649918486f8839ba12fd18c8"
|
|
361
361
|
},
|
|
362
362
|
{
|
|
363
363
|
"path": "clients/bongos-client/package.json",
|
|
@@ -2497,12 +2497,12 @@
|
|
|
2497
2497
|
{
|
|
2498
2498
|
"path": "docs/api-reference.md",
|
|
2499
2499
|
"mode": "0000644",
|
|
2500
|
-
"sha256": "
|
|
2500
|
+
"sha256": "2ad27bf8ccbe4b3349d7f45ac2756bb314478eac2563239dd708cc82b7871e6a"
|
|
2501
2501
|
},
|
|
2502
2502
|
{
|
|
2503
2503
|
"path": "docs/api/openapi.json",
|
|
2504
2504
|
"mode": "0000644",
|
|
2505
|
-
"sha256": "
|
|
2505
|
+
"sha256": "98b41ef963ce5a1009851a8d2cea6e421b02edb46c2f50a9cb809c2d1670bf6b"
|
|
2506
2506
|
},
|
|
2507
2507
|
{
|
|
2508
2508
|
"path": "docs/architecture.md",
|
|
@@ -3087,7 +3087,7 @@
|
|
|
3087
3087
|
{
|
|
3088
3088
|
"path": "docs/module-api-changelog.md",
|
|
3089
3089
|
"mode": "0000644",
|
|
3090
|
-
"sha256": "
|
|
3090
|
+
"sha256": "6c8db5f3d03887690f13f153942bcd1a8485e51473bd6e8a8584fc9de13c46f5"
|
|
3091
3091
|
},
|
|
3092
3092
|
{
|
|
3093
3093
|
"path": "docs/modules-contract.md",
|
|
@@ -10139,10 +10139,15 @@
|
|
|
10139
10139
|
"mode": "0000644",
|
|
10140
10140
|
"sha256": "ff20f9e15d437638b4122392f21283598681c0564bb50788f6fb7ec770722e35"
|
|
10141
10141
|
},
|
|
10142
|
+
{
|
|
10143
|
+
"path": "modules/provisioning/migrations/provisioning_037_update_subscribed.sql",
|
|
10144
|
+
"mode": "0000644",
|
|
10145
|
+
"sha256": "bb1f63759204c9c8022bda6dc2619dc9f12d6e512c1c129a30ba8c6e9ec41ab6"
|
|
10146
|
+
},
|
|
10142
10147
|
{
|
|
10143
10148
|
"path": "modules/provisioning/module.json",
|
|
10144
10149
|
"mode": "0000644",
|
|
10145
|
-
"sha256": "
|
|
10150
|
+
"sha256": "cf3310beddd37559b57a4fa996430d73e6957b297bfa675ed46148b926dd4150"
|
|
10146
10151
|
},
|
|
10147
10152
|
{
|
|
10148
10153
|
"path": "modules/provisioning/onboard-plan.js",
|
|
@@ -10272,7 +10277,7 @@
|
|
|
10272
10277
|
{
|
|
10273
10278
|
"path": "modules/provisioning/routes/provisioning.js",
|
|
10274
10279
|
"mode": "0000644",
|
|
10275
|
-
"sha256": "
|
|
10280
|
+
"sha256": "955f854e4b90892aae4bfc58f258f96f72de06af35389362f61b204cef80bfaf"
|
|
10276
10281
|
},
|
|
10277
10282
|
{
|
|
10278
10283
|
"path": "modules/provisioning/routes/render-standup.js",
|
|
@@ -10284,6 +10289,11 @@
|
|
|
10284
10289
|
"mode": "0000644",
|
|
10285
10290
|
"sha256": "70a66e07e1bec13759b7b41b76ef009931c674867a6fa5270aff7bf3f6d31ecc"
|
|
10286
10291
|
},
|
|
10292
|
+
{
|
|
10293
|
+
"path": "modules/provisioning/routes/update-subscription.js",
|
|
10294
|
+
"mode": "0000644",
|
|
10295
|
+
"sha256": "6595fcab0d5de5a6281aae1ec3db376aa5cbdf8406e3dfbb4d750a7ff0be2a64"
|
|
10296
|
+
},
|
|
10287
10297
|
{
|
|
10288
10298
|
"path": "modules/provisioning/screening.js",
|
|
10289
10299
|
"mode": "0000644",
|
|
@@ -12227,12 +12237,12 @@
|
|
|
12227
12237
|
{
|
|
12228
12238
|
"path": "package-lock.json",
|
|
12229
12239
|
"mode": "0000644",
|
|
12230
|
-
"sha256": "
|
|
12240
|
+
"sha256": "78d48441e049a15bee43f6853bc005719770cdb7144bb4b40404f06b87cbf65a"
|
|
12231
12241
|
},
|
|
12232
12242
|
{
|
|
12233
12243
|
"path": "package.json",
|
|
12234
12244
|
"mode": "0000644",
|
|
12235
|
-
"sha256": "
|
|
12245
|
+
"sha256": "cdec3c6aaf13f30b623f43c601a6ca5765a39d9408f35c0901335ea6d55ff3e6"
|
|
12236
12246
|
},
|
|
12237
12247
|
{
|
|
12238
12248
|
"path": "public-docs/index.html",
|
|
@@ -12252,7 +12262,7 @@
|
|
|
12252
12262
|
{
|
|
12253
12263
|
"path": "release-notes.json",
|
|
12254
12264
|
"mode": "0000644",
|
|
12255
|
-
"sha256": "
|
|
12265
|
+
"sha256": "678e09f8d3f0e83ad0c1b9e02421db99231530fb7b5d951e603e4257c1fa14e0"
|
|
12256
12266
|
},
|
|
12257
12267
|
{
|
|
12258
12268
|
"path": "scripts/bongos/CLAUDE.md",
|
|
@@ -13877,12 +13887,12 @@
|
|
|
13877
13887
|
{
|
|
13878
13888
|
"path": "scripts/bongos/update-channel.js",
|
|
13879
13889
|
"mode": "0000644",
|
|
13880
|
-
"sha256": "
|
|
13890
|
+
"sha256": "b36cf627b9dd862f171822156dcb6d9623ba2d61a609125c75b38ab8dd6536d4"
|
|
13881
13891
|
},
|
|
13882
13892
|
{
|
|
13883
13893
|
"path": "scripts/bongos/update-sweep.js",
|
|
13884
13894
|
"mode": "0000644",
|
|
13885
|
-
"sha256": "
|
|
13895
|
+
"sha256": "6aa3e527c2cb49ab147d8156a49bda9084131a708122b28eba797b090836048b"
|
|
13886
13896
|
},
|
|
13887
13897
|
{
|
|
13888
13898
|
"path": "scripts/bongos/upgrade-artist-gate.js",
|
|
@@ -15707,7 +15717,7 @@
|
|
|
15707
15717
|
{
|
|
15708
15718
|
"path": "src/bongos/api-errors.js",
|
|
15709
15719
|
"mode": "0000644",
|
|
15710
|
-
"sha256": "
|
|
15720
|
+
"sha256": "b3527d2014da3f141a6351f7c1daa2aec1ef43935b779a99d1d2a7faa21521ae"
|
|
15711
15721
|
},
|
|
15712
15722
|
{
|
|
15713
15723
|
"path": "src/bongos/api-path-404.js",
|
|
@@ -15802,7 +15812,7 @@
|
|
|
15802
15812
|
{
|
|
15803
15813
|
"path": "src/bongos/db.js",
|
|
15804
15814
|
"mode": "0000644",
|
|
15805
|
-
"sha256": "
|
|
15815
|
+
"sha256": "db2eefddf1d69a5ae5a43f1c6a5235f951ee1e23cca9ff4ce45910f1c7880a61"
|
|
15806
15816
|
},
|
|
15807
15817
|
{
|
|
15808
15818
|
"path": "src/bongos/device-approval.js",
|
|
@@ -16012,7 +16022,7 @@
|
|
|
16012
16022
|
{
|
|
16013
16023
|
"path": "src/bongos/route-rank-check.js",
|
|
16014
16024
|
"mode": "0000644",
|
|
16015
|
-
"sha256": "
|
|
16025
|
+
"sha256": "374034d7015bccdb28729f328e752773219f8b703f89affe154c04fdb130865d"
|
|
16016
16026
|
},
|
|
16017
16027
|
{
|
|
16018
16028
|
"path": "src/bongos/routes.js",
|
|
@@ -16057,7 +16067,7 @@
|
|
|
16057
16067
|
{
|
|
16058
16068
|
"path": "src/bongos/routes/builders.js",
|
|
16059
16069
|
"mode": "0000644",
|
|
16060
|
-
"sha256": "
|
|
16070
|
+
"sha256": "9fccaac46052db95614c66396719b7b710e5480e5b1381ba1677fb768239576e"
|
|
16061
16071
|
},
|
|
16062
16072
|
{
|
|
16063
16073
|
"path": "src/bongos/routes/cli-commands.js",
|
|
@@ -16262,7 +16272,7 @@
|
|
|
16262
16272
|
{
|
|
16263
16273
|
"path": "src/module-api.js",
|
|
16264
16274
|
"mode": "0000644",
|
|
16265
|
-
"sha256": "
|
|
16275
|
+
"sha256": "020b1e30dd86433757c2939761aa01abe097e1e5aa6f1043d0a0050138b4d06c"
|
|
16266
16276
|
},
|
|
16267
16277
|
{
|
|
16268
16278
|
"path": "src/module-loader/catalog.js",
|
|
@@ -17109,6 +17119,11 @@
|
|
|
17109
17119
|
"mode": "0000644",
|
|
17110
17120
|
"sha256": "215f53db8e7fd45d0f229ff7e67d8b1d16ec80b25e8d9f7070dfe65898b6cb8a"
|
|
17111
17121
|
},
|
|
17122
|
+
{
|
|
17123
|
+
"path": "tests/builder_card_requirement.mjs",
|
|
17124
|
+
"mode": "0000644",
|
|
17125
|
+
"sha256": "612308a0650bc6bb7eedbd3340bb863f5893a907bc62ae4a8b2f49829820af71"
|
|
17126
|
+
},
|
|
17112
17127
|
{
|
|
17113
17128
|
"path": "tests/builder_needs.mjs",
|
|
17114
17129
|
"mode": "0000644",
|
|
@@ -21714,6 +21729,11 @@
|
|
|
21714
21729
|
"mode": "0000644",
|
|
21715
21730
|
"sha256": "31f7a766772450f2f79a43126154f9840d9a22972f0fcc9399f71dfbf95c43a2"
|
|
21716
21731
|
},
|
|
21732
|
+
{
|
|
21733
|
+
"path": "tests/provisioning_update_subscription_route.mjs",
|
|
21734
|
+
"mode": "0000644",
|
|
21735
|
+
"sha256": "5ffe5b0271b8a256da33617e30b20baf4a037523fc6e23298b6826fdb8fc3208"
|
|
21736
|
+
},
|
|
21717
21737
|
{
|
|
21718
21738
|
"path": "tests/provisioning_wizard_create_xenos.mjs",
|
|
21719
21739
|
"mode": "0000644",
|
|
@@ -23117,7 +23137,7 @@
|
|
|
23117
23137
|
{
|
|
23118
23138
|
"path": "tests/update_channel_db.mjs",
|
|
23119
23139
|
"mode": "0000644",
|
|
23120
|
-
"sha256": "
|
|
23140
|
+
"sha256": "ee73e3a43fc0b9133b72a032b1ffc88f5be2a8f4abcb39333a58e4a2c47aaedd"
|
|
23121
23141
|
},
|
|
23122
23142
|
{
|
|
23123
23143
|
"path": "tests/update_pill.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
|
+
- 701 operations across 85 resource groups
|
|
9
9
|
|
|
10
10
|
## Use it from your project
|
|
11
11
|
|
|
@@ -293,6 +293,8 @@ function createClient(opts = {}) {
|
|
|
293
293
|
patchBuildersIdBudget: (args) => request("PATCH", "/builders/{id}/budget", { hasBody: true }, args),
|
|
294
294
|
// PATCH /builders/{id}/capabilities — rank: archon — PATCH /builders/:id/capabilities
|
|
295
295
|
patchBuildersIdCapabilities: (args) => request("PATCH", "/builders/{id}/capabilities", { hasBody: true }, args),
|
|
296
|
+
// PATCH /builders/{id}/card-requirement — rank: archon — PATCH /builders/:id/card-requirement
|
|
297
|
+
patchBuildersIdCardRequirement: (args) => request("PATCH", "/builders/{id}/card-requirement", { hasBody: true }, args),
|
|
296
298
|
// GET /builders/{id}/onboarding — rank: public — GET /builders/:id/onboarding
|
|
297
299
|
getBuildersIdOnboarding: (args) => request("GET", "/builders/{id}/onboarding", { hasBody: false }, args),
|
|
298
300
|
// GET /builders/{id}/profile — rank: public — GET /builders/:id/profile
|
|
@@ -1279,6 +1281,8 @@ function createClient(opts = {}) {
|
|
|
1279
1281
|
postProvisioningInstancesIdTeardown: (args) => request("POST", "/provisioning/instances/{id}/teardown", { hasBody: true }, args),
|
|
1280
1282
|
// POST /provisioning/instances/{id}/update-channel — rank: archon — POST /provisioning/instances/:id/update-channel
|
|
1281
1283
|
postProvisioningInstancesIdUpdateChannel: (args) => request("POST", "/provisioning/instances/{id}/update-channel", { hasBody: true }, args),
|
|
1284
|
+
// POST /provisioning/instances/{id}/update-subscription — rank: metic+archon — POST /provisioning/instances/:id/update-subscription
|
|
1285
|
+
postProvisioningInstancesIdUpdateSubscription: (args) => request("POST", "/provisioning/instances/{id}/update-subscription", { hasBody: true }, args),
|
|
1282
1286
|
// GET /provisioning/logos/{file} — rank: public — GET /provisioning/logos/:file
|
|
1283
1287
|
getProvisioningLogosFile: (args) => request("GET", "/provisioning/logos/{file}", { hasBody: false }, args),
|
|
1284
1288
|
// GET /provisioning/onboard-plan — rank: public — GET /provisioning/onboard-plan
|
|
@@ -292,6 +292,8 @@ function createClient(opts = {}) {
|
|
|
292
292
|
patchBuildersIdBudget: (args) => request("PATCH", "/builders/{id}/budget", { hasBody: true }, args),
|
|
293
293
|
// PATCH /builders/{id}/capabilities — rank: archon — PATCH /builders/:id/capabilities
|
|
294
294
|
patchBuildersIdCapabilities: (args) => request("PATCH", "/builders/{id}/capabilities", { hasBody: true }, args),
|
|
295
|
+
// PATCH /builders/{id}/card-requirement — rank: archon — PATCH /builders/:id/card-requirement
|
|
296
|
+
patchBuildersIdCardRequirement: (args) => request("PATCH", "/builders/{id}/card-requirement", { hasBody: true }, args),
|
|
295
297
|
// GET /builders/{id}/onboarding — rank: public — GET /builders/:id/onboarding
|
|
296
298
|
getBuildersIdOnboarding: (args) => request("GET", "/builders/{id}/onboarding", { hasBody: false }, args),
|
|
297
299
|
// GET /builders/{id}/profile — rank: public — GET /builders/:id/profile
|
|
@@ -1278,6 +1280,8 @@ function createClient(opts = {}) {
|
|
|
1278
1280
|
postProvisioningInstancesIdTeardown: (args) => request("POST", "/provisioning/instances/{id}/teardown", { hasBody: true }, args),
|
|
1279
1281
|
// POST /provisioning/instances/{id}/update-channel — rank: archon — POST /provisioning/instances/:id/update-channel
|
|
1280
1282
|
postProvisioningInstancesIdUpdateChannel: (args) => request("POST", "/provisioning/instances/{id}/update-channel", { hasBody: true }, args),
|
|
1283
|
+
// POST /provisioning/instances/{id}/update-subscription — rank: metic+archon — POST /provisioning/instances/:id/update-subscription
|
|
1284
|
+
postProvisioningInstancesIdUpdateSubscription: (args) => request("POST", "/provisioning/instances/{id}/update-subscription", { hasBody: true }, args),
|
|
1281
1285
|
// GET /provisioning/logos/{file} — rank: public — GET /provisioning/logos/:file
|
|
1282
1286
|
getProvisioningLogosFile: (args) => request("GET", "/provisioning/logos/{file}", { hasBody: false }, args),
|
|
1283
1287
|
// GET /provisioning/onboard-plan — rank: public — GET /provisioning/onboard-plan
|
|
@@ -231,6 +231,8 @@ export interface PatchBuildersIdBudgetRequest { monthly_budget_usd?: unknown }
|
|
|
231
231
|
export interface PatchBuildersIdBudgetResponse { builder: unknown }
|
|
232
232
|
export interface PatchBuildersIdCapabilitiesRequest { server: boolean }
|
|
233
233
|
export interface PatchBuildersIdCapabilitiesResponse { builder: unknown }
|
|
234
|
+
export interface PatchBuildersIdCardRequirementRequest { require_card: boolean }
|
|
235
|
+
export interface PatchBuildersIdCardRequirementResponse { builder: unknown }
|
|
234
236
|
export interface PatchBuildersIdRankRequest { rank: string }
|
|
235
237
|
export interface PatchBuildersIdRankResponse { builder: unknown; previous_rank: unknown; direction: unknown; credit_delta: unknown; broadcast: unknown }
|
|
236
238
|
export interface PatchBuildersIdStatusRequest { status: "active" | "inactive"; reason?: string }
|
|
@@ -602,6 +604,8 @@ export interface PostProvisioningInstancesIdRenderResponse { ok: boolean; queued
|
|
|
602
604
|
export interface PostProvisioningInstancesIdRepoPrivateResponse { ok: boolean; queued: unknown; action: unknown; status: unknown; message: unknown }
|
|
603
605
|
export interface PostProvisioningInstancesIdRestartResponse { ok: boolean; queued: unknown; action: unknown; status: unknown; message: unknown }
|
|
604
606
|
export interface PostProvisioningInstancesIdTeardownResponse { ok: boolean; queued: unknown; action: unknown; status: unknown; superseded: unknown; message: unknown }
|
|
607
|
+
export interface PostProvisioningInstancesIdUpdateSubscriptionRequest { subscribed: boolean }
|
|
608
|
+
export interface PostProvisioningInstancesIdUpdateSubscriptionResponse { ok: boolean; update_subscribed: unknown; previous: unknown; changed: unknown }
|
|
605
609
|
export interface PostProvisioningInstancesRequest { slug: string; target_ref?: string; hosting_shape?: string; domain?: string; no_address?: boolean; tier?: string; onboard_mode?: string; type?: string; description?: string; detail?: Record<string, unknown>; modules?: string[]; terms_acknowledged?: boolean; demo?: Record<string, unknown>; ship_mode?: "auto" | "manual"; genesis?: boolean }
|
|
606
610
|
export interface PostProvisioningPhysicsCheckRequest { description?: string; detail?: Record<string, unknown> }
|
|
607
611
|
export interface PostProvisioningPhysicsCheckResponse { stop: boolean; message: unknown; soft_limits: unknown }
|
|
@@ -960,6 +964,8 @@ export interface BongosClient {
|
|
|
960
964
|
patchBuildersIdBudget(args?: RequestArgs & { body?: PatchBuildersIdBudgetRequest }): Promise<PatchBuildersIdBudgetResponse>;
|
|
961
965
|
/** PATCH /builders/{id}/capabilities — rank: archon */
|
|
962
966
|
patchBuildersIdCapabilities(args: RequestArgs & { body: PatchBuildersIdCapabilitiesRequest }): Promise<PatchBuildersIdCapabilitiesResponse>;
|
|
967
|
+
/** PATCH /builders/{id}/card-requirement — rank: archon */
|
|
968
|
+
patchBuildersIdCardRequirement(args: RequestArgs & { body: PatchBuildersIdCardRequirementRequest }): Promise<PatchBuildersIdCardRequirementResponse>;
|
|
963
969
|
/** GET /builders/{id}/onboarding — rank: public */
|
|
964
970
|
getBuildersIdOnboarding(args?: RequestArgs): Promise<GetBuildersIdOnboardingResponse>;
|
|
965
971
|
/** GET /builders/{id}/profile — rank: public */
|
|
@@ -1946,6 +1952,8 @@ export interface BongosClient {
|
|
|
1946
1952
|
postProvisioningInstancesIdTeardown(args?: RequestArgs): Promise<PostProvisioningInstancesIdTeardownResponse>;
|
|
1947
1953
|
/** POST /provisioning/instances/{id}/update-channel — rank: archon */
|
|
1948
1954
|
postProvisioningInstancesIdUpdateChannel(args?: RequestArgs): Promise<ApiResponse>;
|
|
1955
|
+
/** POST /provisioning/instances/{id}/update-subscription — rank: metic+archon */
|
|
1956
|
+
postProvisioningInstancesIdUpdateSubscription(args: RequestArgs & { body: PostProvisioningInstancesIdUpdateSubscriptionRequest }): Promise<PostProvisioningInstancesIdUpdateSubscriptionResponse>;
|
|
1949
1957
|
/** GET /provisioning/logos/{file} — rank: public */
|
|
1950
1958
|
getProvisioningLogosFile(args?: RequestArgs): Promise<ApiResponse>;
|
|
1951
1959
|
/** GET /provisioning/onboard-plan — rank: public */
|
|
@@ -289,6 +289,8 @@ export function createClient(opts = {}) {
|
|
|
289
289
|
patchBuildersIdBudget: (args) => request("PATCH", "/builders/{id}/budget", { hasBody: true }, args),
|
|
290
290
|
// PATCH /builders/{id}/capabilities — rank: archon — PATCH /builders/:id/capabilities
|
|
291
291
|
patchBuildersIdCapabilities: (args) => request("PATCH", "/builders/{id}/capabilities", { hasBody: true }, args),
|
|
292
|
+
// PATCH /builders/{id}/card-requirement — rank: archon — PATCH /builders/:id/card-requirement
|
|
293
|
+
patchBuildersIdCardRequirement: (args) => request("PATCH", "/builders/{id}/card-requirement", { hasBody: true }, args),
|
|
292
294
|
// GET /builders/{id}/onboarding — rank: public — GET /builders/:id/onboarding
|
|
293
295
|
getBuildersIdOnboarding: (args) => request("GET", "/builders/{id}/onboarding", { hasBody: false }, args),
|
|
294
296
|
// GET /builders/{id}/profile — rank: public — GET /builders/:id/profile
|
|
@@ -1275,6 +1277,8 @@ export function createClient(opts = {}) {
|
|
|
1275
1277
|
postProvisioningInstancesIdTeardown: (args) => request("POST", "/provisioning/instances/{id}/teardown", { hasBody: true }, args),
|
|
1276
1278
|
// POST /provisioning/instances/{id}/update-channel — rank: archon — POST /provisioning/instances/:id/update-channel
|
|
1277
1279
|
postProvisioningInstancesIdUpdateChannel: (args) => request("POST", "/provisioning/instances/{id}/update-channel", { hasBody: true }, args),
|
|
1280
|
+
// POST /provisioning/instances/{id}/update-subscription — rank: metic+archon — POST /provisioning/instances/:id/update-subscription
|
|
1281
|
+
postProvisioningInstancesIdUpdateSubscription: (args) => request("POST", "/provisioning/instances/{id}/update-subscription", { hasBody: true }, args),
|
|
1278
1282
|
// GET /provisioning/logos/{file} — rank: public — GET /provisioning/logos/:file
|
|
1279
1283
|
getProvisioningLogosFile: (args) => request("GET", "/provisioning/logos/{file}", { hasBody: false }, args),
|
|
1280
1284
|
// GET /provisioning/onboard-plan — rank: public — GET /provisioning/onboard-plan
|
package/docs/api/openapi.json
CHANGED
|
@@ -4324,6 +4324,72 @@
|
|
|
4324
4324
|
]
|
|
4325
4325
|
}
|
|
4326
4326
|
},
|
|
4327
|
+
"/builders/{id}/card-requirement": {
|
|
4328
|
+
"patch": {
|
|
4329
|
+
"operationId": "patch_builders_id_card_requirement",
|
|
4330
|
+
"tags": [
|
|
4331
|
+
"builders"
|
|
4332
|
+
],
|
|
4333
|
+
"summary": "PATCH /builders/:id/card-requirement",
|
|
4334
|
+
"description": "PATCH /builders/:id/card-requirement — an Archon switches the hardware-card requirement on or off for one builder (task 1004748, ADR 0382 D4). Body: { require_card: boolean }. Default is off for everyone. Switching ON refuses 409 no_card_registered unless the builder has a registered card, so the switch cannot lock anyone out. You cannot switch OFF your own requirement (403 cannot_disable_own_card_requirement) — a stolen session must not be able to drop it; another Archon does it. Every change writes an audit row naming the target. rank: archon. Gate: requirePermission('builder.rank.set') (the non-delegable atom that guards the step-up's own target route) then requireCardStepUp; read fresh per request.\n\n**Rank:** `archon` — Archon only (rank and identity management + the escalation keys — the trust boundary).\n\n**Permissions:** `builder.rank.set` (all required).",
|
|
4335
|
+
"x-rank": "archon",
|
|
4336
|
+
"x-source": "src/bongos/routes/builders.js",
|
|
4337
|
+
"x-permissions": [
|
|
4338
|
+
"builder.rank.set"
|
|
4339
|
+
],
|
|
4340
|
+
"parameters": [
|
|
4341
|
+
{
|
|
4342
|
+
"name": "id",
|
|
4343
|
+
"in": "path",
|
|
4344
|
+
"required": true,
|
|
4345
|
+
"schema": {
|
|
4346
|
+
"type": "string"
|
|
4347
|
+
},
|
|
4348
|
+
"description": "Path parameter `id`."
|
|
4349
|
+
}
|
|
4350
|
+
],
|
|
4351
|
+
"requestBody": {
|
|
4352
|
+
"required": true,
|
|
4353
|
+
"content": {
|
|
4354
|
+
"application/json": {
|
|
4355
|
+
"schema": {
|
|
4356
|
+
"$ref": "#/components/schemas/PatchBuildersIdCardRequirementRequest"
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
},
|
|
4360
|
+
"x-validated": true
|
|
4361
|
+
},
|
|
4362
|
+
"responses": {
|
|
4363
|
+
"200": {
|
|
4364
|
+
"description": "Success.",
|
|
4365
|
+
"content": {
|
|
4366
|
+
"application/json": {
|
|
4367
|
+
"schema": {
|
|
4368
|
+
"$ref": "#/components/schemas/PatchBuildersIdCardRequirementResponse"
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
}
|
|
4372
|
+
},
|
|
4373
|
+
"400": {
|
|
4374
|
+
"$ref": "#/components/responses/ValidationFailed"
|
|
4375
|
+
},
|
|
4376
|
+
"401": {
|
|
4377
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
4378
|
+
},
|
|
4379
|
+
"403": {
|
|
4380
|
+
"$ref": "#/components/responses/Forbidden"
|
|
4381
|
+
},
|
|
4382
|
+
"404": {
|
|
4383
|
+
"$ref": "#/components/responses/NotFound"
|
|
4384
|
+
}
|
|
4385
|
+
},
|
|
4386
|
+
"security": [
|
|
4387
|
+
{
|
|
4388
|
+
"builderSession": []
|
|
4389
|
+
}
|
|
4390
|
+
]
|
|
4391
|
+
}
|
|
4392
|
+
},
|
|
4327
4393
|
"/builders/{id}/onboarding": {
|
|
4328
4394
|
"get": {
|
|
4329
4395
|
"operationId": "get_builders_id_onboarding",
|
|
@@ -24804,6 +24870,72 @@
|
|
|
24804
24870
|
]
|
|
24805
24871
|
}
|
|
24806
24872
|
},
|
|
24873
|
+
"/provisioning/instances/{id}/update-subscription": {
|
|
24874
|
+
"post": {
|
|
24875
|
+
"operationId": "post_provisioning_instances_id_update_subscription",
|
|
24876
|
+
"tags": [
|
|
24877
|
+
"provisioning"
|
|
24878
|
+
],
|
|
24879
|
+
"summary": "POST /provisioning/instances/:id/update-subscription",
|
|
24880
|
+
"description": "Body: { subscribed: boolean }. Gate: the 'provisioning.fleet.manage' permission — the fleet's own.\n\n**Rank:** `metic+archon` — Metic or Archon rank (review/triage powers).\n\n**Permissions:** `provisioning.fleet.manage` (all required).",
|
|
24881
|
+
"x-rank": "metic+archon",
|
|
24882
|
+
"x-source": "modules/provisioning/routes/update-subscription.js",
|
|
24883
|
+
"x-permissions": [
|
|
24884
|
+
"provisioning.fleet.manage"
|
|
24885
|
+
],
|
|
24886
|
+
"parameters": [
|
|
24887
|
+
{
|
|
24888
|
+
"name": "id",
|
|
24889
|
+
"in": "path",
|
|
24890
|
+
"required": true,
|
|
24891
|
+
"schema": {
|
|
24892
|
+
"type": "string"
|
|
24893
|
+
},
|
|
24894
|
+
"description": "Path parameter `id`."
|
|
24895
|
+
}
|
|
24896
|
+
],
|
|
24897
|
+
"requestBody": {
|
|
24898
|
+
"required": true,
|
|
24899
|
+
"content": {
|
|
24900
|
+
"application/json": {
|
|
24901
|
+
"schema": {
|
|
24902
|
+
"$ref": "#/components/schemas/PostProvisioningInstancesIdUpdateSubscriptionRequest"
|
|
24903
|
+
}
|
|
24904
|
+
}
|
|
24905
|
+
},
|
|
24906
|
+
"x-validated": true
|
|
24907
|
+
},
|
|
24908
|
+
"responses": {
|
|
24909
|
+
"200": {
|
|
24910
|
+
"description": "Success.",
|
|
24911
|
+
"content": {
|
|
24912
|
+
"application/json": {
|
|
24913
|
+
"schema": {
|
|
24914
|
+
"$ref": "#/components/schemas/PostProvisioningInstancesIdUpdateSubscriptionResponse"
|
|
24915
|
+
}
|
|
24916
|
+
}
|
|
24917
|
+
}
|
|
24918
|
+
},
|
|
24919
|
+
"400": {
|
|
24920
|
+
"$ref": "#/components/responses/ValidationFailed"
|
|
24921
|
+
},
|
|
24922
|
+
"401": {
|
|
24923
|
+
"$ref": "#/components/responses/Unauthorized"
|
|
24924
|
+
},
|
|
24925
|
+
"403": {
|
|
24926
|
+
"$ref": "#/components/responses/Forbidden"
|
|
24927
|
+
},
|
|
24928
|
+
"404": {
|
|
24929
|
+
"$ref": "#/components/responses/NotFound"
|
|
24930
|
+
}
|
|
24931
|
+
},
|
|
24932
|
+
"security": [
|
|
24933
|
+
{
|
|
24934
|
+
"builderSession": []
|
|
24935
|
+
}
|
|
24936
|
+
]
|
|
24937
|
+
}
|
|
24938
|
+
},
|
|
24807
24939
|
"/provisioning/logos/{file}": {
|
|
24808
24940
|
"get": {
|
|
24809
24941
|
"operationId": "get_provisioning_logos_file",
|
|
@@ -35357,6 +35489,27 @@
|
|
|
35357
35489
|
"builder"
|
|
35358
35490
|
]
|
|
35359
35491
|
},
|
|
35492
|
+
"PatchBuildersIdCardRequirementRequest": {
|
|
35493
|
+
"type": "object",
|
|
35494
|
+
"properties": {
|
|
35495
|
+
"require_card": {
|
|
35496
|
+
"type": "boolean"
|
|
35497
|
+
}
|
|
35498
|
+
},
|
|
35499
|
+
"required": [
|
|
35500
|
+
"require_card"
|
|
35501
|
+
],
|
|
35502
|
+
"additionalProperties": false
|
|
35503
|
+
},
|
|
35504
|
+
"PatchBuildersIdCardRequirementResponse": {
|
|
35505
|
+
"type": "object",
|
|
35506
|
+
"properties": {
|
|
35507
|
+
"builder": {}
|
|
35508
|
+
},
|
|
35509
|
+
"required": [
|
|
35510
|
+
"builder"
|
|
35511
|
+
]
|
|
35512
|
+
},
|
|
35360
35513
|
"PatchBuildersIdRankRequest": {
|
|
35361
35514
|
"type": "object",
|
|
35362
35515
|
"properties": {
|
|
@@ -41304,6 +41457,35 @@
|
|
|
41304
41457
|
"message"
|
|
41305
41458
|
]
|
|
41306
41459
|
},
|
|
41460
|
+
"PostProvisioningInstancesIdUpdateSubscriptionRequest": {
|
|
41461
|
+
"type": "object",
|
|
41462
|
+
"properties": {
|
|
41463
|
+
"subscribed": {
|
|
41464
|
+
"type": "boolean"
|
|
41465
|
+
}
|
|
41466
|
+
},
|
|
41467
|
+
"required": [
|
|
41468
|
+
"subscribed"
|
|
41469
|
+
],
|
|
41470
|
+
"additionalProperties": false
|
|
41471
|
+
},
|
|
41472
|
+
"PostProvisioningInstancesIdUpdateSubscriptionResponse": {
|
|
41473
|
+
"type": "object",
|
|
41474
|
+
"properties": {
|
|
41475
|
+
"ok": {
|
|
41476
|
+
"type": "boolean"
|
|
41477
|
+
},
|
|
41478
|
+
"update_subscribed": {},
|
|
41479
|
+
"previous": {},
|
|
41480
|
+
"changed": {}
|
|
41481
|
+
},
|
|
41482
|
+
"required": [
|
|
41483
|
+
"ok",
|
|
41484
|
+
"update_subscribed",
|
|
41485
|
+
"previous",
|
|
41486
|
+
"changed"
|
|
41487
|
+
]
|
|
41488
|
+
},
|
|
41307
41489
|
"PostProvisioningInstancesRequest": {
|
|
41308
41490
|
"type": "object",
|
|
41309
41491
|
"properties": {
|
|
@@ -43737,9 +43919,9 @@
|
|
|
43737
43919
|
"description": "A required dependency/feature is not configured or is temporarily down."
|
|
43738
43920
|
}
|
|
43739
43921
|
},
|
|
43740
|
-
"x-endpoint-count":
|
|
43741
|
-
"x-schema-count":
|
|
43922
|
+
"x-endpoint-count": 701,
|
|
43923
|
+
"x-schema-count": 738,
|
|
43742
43924
|
"x-undocumented-bodies": 12,
|
|
43743
|
-
"x-response-schemas":
|
|
43925
|
+
"x-response-schemas": 466,
|
|
43744
43926
|
"x-generated-by": "scripts/bongos/gen-api-docs.js"
|
|
43745
43927
|
}
|
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. 701 endpoints across 118 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/0016-trust-boundary-server-enforced-permissions.md)): `public` < `any-builder` < `metic+archon` < `archon`.
|
|
@@ -161,7 +161,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
161
161
|
|---|---|---|---|---|
|
|
162
162
|
| GET | `/api/bongos/bug-attachments/:name` | `any-builder` | — | GET /api/bongos/bug-attachments/:name — stream one stored image. |
|
|
163
163
|
|
|
164
|
-
## `builders` (
|
|
164
|
+
## `builders` (15)
|
|
165
165
|
|
|
166
166
|
| Method | Path | Rank | Body | Description |
|
|
167
167
|
|---|---|---|---|---|
|
|
@@ -171,6 +171,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
171
171
|
| GET | `/api/bongos/builders/:id/activity` | `any-builder` | — | GET /builders/:id/activity — the profile's PER-CRAFT activity log (task 1003281, goal 1000072, criterion legibility-activity-by-role). |
|
|
172
172
|
| PATCH | `/api/bongos/builders/:id/budget` | `metic+archon` | `monthly_budget_usd` | PATCH /builders/:id/budget — set or clear a builder's monthly spend ceiling (V3.R84 task 302, criterion C3). |
|
|
173
173
|
| PATCH | `/api/bongos/builders/:id/capabilities` | `archon` | `server` | PATCH /builders/:id/capabilities — the owner gives or takes a builder's server access (task 1004950, ADR 0395). |
|
|
174
|
+
| PATCH | `/api/bongos/builders/:id/card-requirement` | `archon` | `require_card` | PATCH /builders/:id/card-requirement — an Archon switches the hardware-card requirement on or off for one builder (task 1004748, ADR 0382… |
|
|
174
175
|
| GET | `/api/bongos/builders/:id/onboarding` | `public` | — | GET /builders/:id/onboarding — V3.R75 (task 294) onboarding checklist read. |
|
|
175
176
|
| GET | `/api/bongos/builders/:id/profile` | `public` | — | GET /builders/:id/profile — the viewable builder profile page's read (task 1467, goal 25 "bx-identity"). |
|
|
176
177
|
| PATCH | `/api/bongos/builders/:id/rank` | `archon` | `rank` | sole chokepoint that mutates builders.rank; ADR 0016 §enforcement. |
|
|
@@ -826,7 +827,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
826
827
|
| POST | `/api/bongos/projects/invite` | `metic+archon` | `client_id`, `github_login` | own-or-metic in effect: where to invite a builder into a project, asked by a `project.curate` holder OR the project's OWN OWNER (task 100… |
|
|
827
828
|
| POST | `/api/bongos/projects/invite-guild` | `metic+archon` | `client_id`, `guild` | own-or-metic in effect, the SAME gate as POST /projects/invite above and for the same reasons (a non-inviter gets that route's unchanged … |
|
|
828
829
|
|
|
829
|
-
## `provisioning` (
|
|
830
|
+
## `provisioning` (47)
|
|
830
831
|
|
|
831
832
|
| Method | Path | Rank | Body | Description |
|
|
832
833
|
|---|---|---|---|---|
|
|
@@ -868,6 +869,7 @@ Base path: `/api/bongos`. Ranks (enforced server-side, [ADR 0016](adr/0016-trust
|
|
|
868
869
|
| PATCH | `/api/bongos/provisioning/instances/:id/settings` | `any-builder` | validated | PATCH /provisioning/instances/:id/settings — change a project's settings. |
|
|
869
870
|
| POST | `/api/bongos/provisioning/instances/:id/teardown` | `any-builder` | — | POST /provisioning/instances/:id/teardown — request teardown of the caller's OWN instance. |
|
|
870
871
|
| POST | `/api/bongos/provisioning/instances/:id/update-channel` | `archon` | — | POST /provisioning/instances/:id/update-channel — set the project's update rule (task 1004445). |
|
|
872
|
+
| POST | `/api/bongos/provisioning/instances/:id/update-subscription` | `metic+archon` | `subscribed` | Body: { subscribed: boolean }. |
|
|
871
873
|
| GET | `/api/bongos/provisioning/logos/:file` | `public` | — | a logo is shown on a public hall, and its address is its own sha256, so it names no project and cannot be guessed. |
|
|
872
874
|
| GET | `/api/bongos/provisioning/onboard-plan` | `public` | — | GET /provisioning/onboard-plan?mode=greenfield\|adopt&domain=<host> — the CANONICAL onboarding step sequence (task 2085; goal 35 / criteri… |
|
|
873
875
|
| POST | `/api/bongos/provisioning/physics-check` | `any-builder` | `description`, `detail` | POST /provisioning/physics-check — the planet-physics questions' read BEFORE anything exists (task 1004418, BV2.PS07). |
|
|
@@ -3450,5 +3450,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
|
|
|
3450
3450
|
landed since 1.25.2 with no explicit bump. run 43328266045. (task 1002620)
|
|
3451
3451
|
1.25.4 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3452
3452
|
landed since 1.25.3 with no explicit bump. run 43331422033. (task 1002620)
|
|
3453
|
+
1.25.5 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
3454
|
+
landed since 1.25.4 with no explicit bump. run 43362791111. (task 1002620)
|
|
3453
3455
|
---------------------------------------------------------------------------
|
|
3454
3456
|
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
-- provisioning_037_update_subscribed.sql — whether an instance takes unattended patch upgrades,
|
|
2
|
+
-- stored on the instance row (task 1004150, ADR 0325 decision 5).
|
|
3
|
+
--
|
|
4
|
+
-- WHY. Every other fleet fact lives in provisioning_instances; "which instances the nightly sweep
|
|
5
|
+
-- moves on its own" lived only in config/update-subscriptions.json. That left GET /provisioning/fleet
|
|
6
|
+
-- unable to answer the one question a fleet view exists for, and let a restored control-plane backup
|
|
7
|
+
-- bring back every instance row with an empty roster — the sweep then reported failed=0 while
|
|
8
|
+
-- upgrading nothing. On the row, the subscription and its update_channel (provisioning_029) travel
|
|
9
|
+
-- and restore together.
|
|
10
|
+
--
|
|
11
|
+
-- DEFAULT false: an instance is armed only by an explicit act, as the shipped-empty roster was
|
|
12
|
+
-- (ADR 0136). The roster file stays honoured as an override, so an operator's current arming is
|
|
13
|
+
-- not silently dropped.
|
|
14
|
+
--
|
|
15
|
+
-- Additive and namespaced (ADR 0083): no down-migration, idempotent — safe to re-run.
|
|
16
|
+
|
|
17
|
+
BEGIN;
|
|
18
|
+
|
|
19
|
+
ALTER TABLE provisioning_instances ADD COLUMN IF NOT EXISTS update_subscribed boolean NOT NULL DEFAULT false;
|
|
20
|
+
|
|
21
|
+
COMMIT;
|
|
@@ -164,6 +164,10 @@ function ownerInstance(row) {
|
|
|
164
164
|
function fleetInstance(row) {
|
|
165
165
|
return {
|
|
166
166
|
owner: { id: row.owner_builder_id, github_login: row.github_login, display_name: row.display_name, rank: row.rank },
|
|
167
|
+
// Fleet-only (task 1004150): which rows take unattended upgrades, and under what rule. Kept off
|
|
168
|
+
// publicInstance, which feeds owner reads — who auto-upgrades is an operator's question.
|
|
169
|
+
update_channel: row.update_channel || 'patch',
|
|
170
|
+
update_subscribed: row.update_subscribed === true,
|
|
167
171
|
...ownerInstance(row),
|
|
168
172
|
};
|
|
169
173
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// modules/provisioning/routes/update-subscription.js — POST /provisioning/instances/:id/update-subscription
|
|
2
|
+
// (task 1004150, ADR 0325 decision 5).
|
|
3
|
+
//
|
|
4
|
+
// WHY. Whether the nightly sweep moves an instance on its own was only a line in
|
|
5
|
+
// config/update-subscriptions.json, so the fleet roster could not report it and a restored
|
|
6
|
+
// backup brought back every row with an empty roster. The flag lives on the row now
|
|
7
|
+
// (provisioning_037); this is how an operator arms or disarms it. It changes a rule, never the
|
|
8
|
+
// core: nothing is queued.
|
|
9
|
+
//
|
|
10
|
+
// Its own routes file (declared in module.json) because routes/provisioning.js sits at the
|
|
11
|
+
// size ratchet — the routes/repo-private.js precedent.
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const express = require('express');
|
|
15
|
+
const api = require('../../../src/module-api');
|
|
16
|
+
const { pool, validateOrRespond, parseId } = api;
|
|
17
|
+
const provisioning = require('../provisioning');
|
|
18
|
+
const { failFrom } = require('../public-refusal');
|
|
19
|
+
|
|
20
|
+
const log = api.logger('provisioning');
|
|
21
|
+
|
|
22
|
+
module.exports = function buildUpdateSubscriptionRouter() {
|
|
23
|
+
const router = express.Router();
|
|
24
|
+
|
|
25
|
+
// Body: { subscribed: boolean }. Gate: the 'provisioning.fleet.manage' permission — the fleet's own.
|
|
26
|
+
router.post('/provisioning/instances/:id/update-subscription', api.requireBuilder, api.requirePermission('provisioning.fleet.manage'), async (req, res) => {
|
|
27
|
+
const id = parseId(req, res);
|
|
28
|
+
if (!id) return;
|
|
29
|
+
if (validateOrRespond(req, res, { subscribed: { type: 'boolean', required: true } }, { strict: true })) return;
|
|
30
|
+
try {
|
|
31
|
+
const inst = await provisioning.getInstanceById(pool, id);
|
|
32
|
+
if (!inst) return res.fail('instance_not_found', 404);
|
|
33
|
+
const subscribed = req.body.subscribed === true;
|
|
34
|
+
const was = inst.update_subscribed === true;
|
|
35
|
+
if (was !== subscribed) {
|
|
36
|
+
await pool.query('UPDATE provisioning_instances SET update_subscribed = $2, updated_at = now() WHERE id = $1', [id, subscribed]);
|
|
37
|
+
await provisioning.recordEvent(pool, {
|
|
38
|
+
instanceId: id, ownerBuilderId: inst.owner_builder_id, event: 'update-subscription-changed',
|
|
39
|
+
detail: `unattended patch upgrades ${subscribed ? 'armed' : 'disarmed'} by ${req.builder.github_login || req.builder.id}`,
|
|
40
|
+
actor: `api:fleet:${req.builder.id}`,
|
|
41
|
+
}).catch(() => {});
|
|
42
|
+
}
|
|
43
|
+
res.json({ ok: true, update_subscribed: subscribed, previous: was, changed: was !== subscribed });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
log.error('[provisioning] POST /provisioning/instances/:id/update-subscription', err);
|
|
46
|
+
failFrom(res, err, 'update_subscription_failed');
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return router;
|
|
51
|
+
};
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.25.
|
|
3
|
+
"version": "1.25.5",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.25.
|
|
9
|
+
"version": "1.25.5",
|
|
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.25.
|
|
3
|
+
"version": "1.25.5",
|
|
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/release-notes.json
CHANGED
|
@@ -11718,5 +11718,15 @@
|
|
|
11718
11718
|
"id": "1005249",
|
|
11719
11719
|
"text": "Your details now sit on your character at the bottom left of the game: hover to see them, click to open your full profile without leaving the scene. The old profile card at the top right is gone."
|
|
11720
11720
|
}
|
|
11721
|
+
],
|
|
11722
|
+
"1.25.5": [
|
|
11723
|
+
{
|
|
11724
|
+
"id": "1004748",
|
|
11725
|
+
"text": "Archon can now switch on a card requirement for a builder who has registered a card, with every change recorded."
|
|
11726
|
+
},
|
|
11727
|
+
{
|
|
11728
|
+
"id": "1004150",
|
|
11729
|
+
"text": "The fleet view can now show which projects get automatic patch upgrades, and the nightly upgrade run says so out loud when it has nothing to do."
|
|
11730
|
+
}
|
|
11721
11731
|
]
|
|
11722
11732
|
}
|
|
@@ -333,13 +333,14 @@ function loadSubscriptions({ instanceDir = process.cwd(), fsImpl = fs, file = SU
|
|
|
333
333
|
const CHANNEL_DB_ENV = 'UPDATE_CHANNEL_DB';
|
|
334
334
|
const CHANNEL_SOURCE_DEPLOY = 'deploy';
|
|
335
335
|
const CHANNEL_SOURCE_ROSTER = 'roster';
|
|
336
|
-
const DB_CHANNELS_SQL = 'SELECT slug, update_channel FROM provisioning_instances';
|
|
336
|
+
const DB_CHANNELS_SQL = 'SELECT slug, update_channel, update_subscribed FROM provisioning_instances';
|
|
337
337
|
const DB_CHANNELS_TIMEOUT_MS = 15000;
|
|
338
338
|
|
|
339
339
|
// PURE. psql's stderr reduced to a class that is safe to log.
|
|
340
340
|
function psqlFailureClass(stderr) {
|
|
341
341
|
const s = String(stderr || '');
|
|
342
342
|
if (/relation .*provisioning_instances.* does not exist/i.test(s)) return 'no provisioning_instances table (provisioning module off?)';
|
|
343
|
+
if (/column .*update_subscribed.* does not exist/i.test(s)) return 'no update_subscribed column (migration provisioning_037 not applied)';
|
|
343
344
|
if (/column .*update_channel.* does not exist/i.test(s)) return 'no update_channel column (migration provisioning_029 not applied)';
|
|
344
345
|
if (/authentication failed|role .* does not exist|permission denied/i.test(s)) return 'the sweep\'s database login was refused';
|
|
345
346
|
if (/could not connect|connection refused|no such file or directory|could not translate host/i.test(s)) return 'database unreachable';
|
|
@@ -366,11 +367,32 @@ function readDbChannels({ env = process.env, run = spawnSync } = {}) {
|
|
|
366
367
|
}
|
|
367
368
|
if (r.status !== 0) return { ok: false, reason: `psql exit ${r.status}: ${psqlFailureClass(r.stderr)}` };
|
|
368
369
|
const channels = new Map();
|
|
370
|
+
const subscribed = new Map();
|
|
369
371
|
for (const line of String(r.stdout || '').split('\n')) {
|
|
370
|
-
const [slug, ch] = line.split('\t');
|
|
371
|
-
if (slug
|
|
372
|
+
const [slug, ch, sub] = line.split('\t');
|
|
373
|
+
if (!slug || !ch) continue;
|
|
374
|
+
channels.set(slug.trim(), ch.trim());
|
|
375
|
+
subscribed.set(slug.trim(), String(sub || '').trim() === 't');
|
|
372
376
|
}
|
|
373
|
-
return { ok: true, channels };
|
|
377
|
+
return { ok: true, channels, subscribed };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// PURE. Merge the two places a subscription can be written: the project row
|
|
381
|
+
// (provisioning_instances.update_subscribed, task 1004150) and the roster file. The file is an
|
|
382
|
+
// OVERRIDE — an entry in it is subscribed whatever the row says, so an operator's current arming
|
|
383
|
+
// is not dropped — but a row subscribed with NO file entry can't be upgraded (the file carries the
|
|
384
|
+
// `dir`), and it is returned in `unreachable` rather than vanishing, because "subscribed on the
|
|
385
|
+
// fleet view, never touched by the sweep" is the silent failure this column exists to end.
|
|
386
|
+
function applyDbSubscriptions(instances, db) {
|
|
387
|
+
const subscribed = db && db.ok && db.subscribed instanceof Map ? db.subscribed : null;
|
|
388
|
+
const slugs = new Set(instances.map((i) => i.slug));
|
|
389
|
+
const out = instances.map((inst) => {
|
|
390
|
+
if (!subscribed) return { ...inst, subscribedSource: CHANNEL_SOURCE_ROSTER };
|
|
391
|
+
if (subscribed.get(inst.slug) === true) return { ...inst, subscribedSource: CHANNEL_SOURCE_DEPLOY };
|
|
392
|
+
return { ...inst, subscribedSource: CHANNEL_SOURCE_ROSTER, subscribedNote: 'subscribed by the roster file only (the project row says not subscribed)' };
|
|
393
|
+
});
|
|
394
|
+
const unreachable = subscribed ? [...subscribed].filter(([slug, on]) => on && !slugs.has(slug)).map(([slug]) => slug) : [];
|
|
395
|
+
return { instances: out, unreachable };
|
|
374
396
|
}
|
|
375
397
|
|
|
376
398
|
// PURE. Give each roster entry the channel it is actually under, and say where it came from:
|
|
@@ -496,6 +518,7 @@ module.exports = {
|
|
|
496
518
|
readDbChannels,
|
|
497
519
|
CHANNEL_DB_ENV,
|
|
498
520
|
applyDbChannels,
|
|
521
|
+
applyDbSubscriptions,
|
|
499
522
|
channelLine,
|
|
500
523
|
psqlFailureClass,
|
|
501
524
|
CHANNEL_SOURCE_DEPLOY,
|
|
@@ -593,16 +593,29 @@ function main({ instanceRoot: defaultRoot = process.cwd() } = {}) {
|
|
|
593
593
|
// CONTROL PLANE's database — this process's own env, never an entry's `env`, which points at that
|
|
594
594
|
// instance's database. A core too old to carry the reader keeps the roster, as before.
|
|
595
595
|
const db = typeof channel.readDbChannels === 'function' ? channel.readDbChannels({ env: process.env }) : null;
|
|
596
|
-
const
|
|
597
|
-
if (db && !db.ok &&
|
|
596
|
+
const withChannels = db ? channel.applyDbChannels(loaded.instances, db) : loaded.instances;
|
|
597
|
+
if (db && !db.ok && withChannels.length) console.log(` • update rules: read from the roster — ${db.reason}`);
|
|
598
|
+
// Who is subscribed is also on the project row (task 1004150). The roster file stays an override.
|
|
599
|
+
const merged = db && typeof channel.applyDbSubscriptions === 'function'
|
|
600
|
+
? channel.applyDbSubscriptions(withChannels, db) : { instances: withChannels, unreachable: [] };
|
|
601
|
+
const instances = merged.instances;
|
|
598
602
|
|
|
599
603
|
const roster = only ? instances.filter((i) => i.slug === only || i.dir === only) : instances;
|
|
600
604
|
const mode = apply ? 'apply' : 'dry-run';
|
|
601
605
|
console.log(`core-update-subscription (${mode}) — ${roster.length} subscribed instance(s)${present ? '' : ' [no roster file — nothing subscribed]'}`);
|
|
602
|
-
for (const inst of roster)
|
|
606
|
+
for (const inst of roster) {
|
|
607
|
+
if (inst.channelNote) console.log(channel.channelLine(inst));
|
|
608
|
+
if (inst.subscribedNote) console.log(` • ${inst.slug}: ${inst.subscribedNote}`);
|
|
609
|
+
}
|
|
603
610
|
if (only && roster.length === 0) console.error(` ! --only ${only} matched no subscribed instance`);
|
|
604
611
|
|
|
605
612
|
let upgraded = 0, skipped = 0, failed = 0, upToDate = 0, healed = 0;
|
|
613
|
+
// Subscribed on the project row but absent from this roster: there is no `dir` to upgrade, so say
|
|
614
|
+
// so and count it a skip. Silence here is how a restored backup reported failed=0 on a dead sweep.
|
|
615
|
+
for (const slug of only ? [] : merged.unreachable) {
|
|
616
|
+
console.error(` ! ${slug}: subscribed on its project row, but this roster has no entry with a dir for it — not upgraded`);
|
|
617
|
+
skipped++;
|
|
618
|
+
}
|
|
606
619
|
const refusalsFile = refusalsPath();
|
|
607
620
|
const refusals = readRefusals(refusalsFile);
|
|
608
621
|
|
|
@@ -817,6 +830,7 @@ function main({ instanceRoot: defaultRoot = process.cwd() } = {}) {
|
|
|
817
830
|
upgraded++;
|
|
818
831
|
}
|
|
819
832
|
|
|
833
|
+
if (!only && roster.length === 0 && skipped === 0) console.log('\ncore-update-subscription: NOTHING SUBSCRIBED — no instance was considered, so no upgrade was attempted (this is not a clean run).');
|
|
820
834
|
console.log(`\ncore-update-subscription: upgraded=${upgraded} up-to-date=${upToDate} skipped=${skipped} failed=${failed}${healed ? ` healed=${healed}` : ''} (${mode})`);
|
|
821
835
|
return failed > 0 ? 1 : 0;
|
|
822
836
|
}
|
package/src/bongos/api-errors.js
CHANGED
|
@@ -89,6 +89,8 @@ const OVERRIDES = Object.freeze({
|
|
|
89
89
|
bfg_write_disabled: 'forbidden',
|
|
90
90
|
target_is_system_principal: 'forbidden',
|
|
91
91
|
cannot_change_own_tester: 'forbidden',
|
|
92
|
+
cannot_disable_own_card_requirement: 'forbidden',
|
|
93
|
+
no_card_registered: 'conflict',
|
|
92
94
|
self_vote_forbidden: 'forbidden',
|
|
93
95
|
self_dep: 'conflict',
|
|
94
96
|
newcomer_must_ship_first: 'forbidden',
|
package/src/bongos/db.js
CHANGED
|
@@ -724,6 +724,48 @@ async function setBuilderTester({ builderId, isTester }) {
|
|
|
724
724
|
});
|
|
725
725
|
}
|
|
726
726
|
|
|
727
|
+
// setRequireCard — an Archon switches the hardware-card requirement on or off for one
|
|
728
|
+
// builder (task 1004748, ADR 0382 D3/D4). Switching ON needs at least one registered
|
|
729
|
+
// card, so nobody is locked out by the switch. The audit row is written in the SAME
|
|
730
|
+
// transaction as the flag, so a change cannot happen without its entry; unlike the
|
|
731
|
+
// generic request log it names the builder it was done TO.
|
|
732
|
+
async function setRequireCard({ builderId, required, actorBuilderId, ip = null, userAgent = null }) {
|
|
733
|
+
return withTx(async (client) => {
|
|
734
|
+
const { rows: found } = await client.query(
|
|
735
|
+
`SELECT id, system_role, require_card FROM builders WHERE id = $1 FOR UPDATE`,
|
|
736
|
+
[builderId]
|
|
737
|
+
);
|
|
738
|
+
const before = found[0];
|
|
739
|
+
if (!before) return null;
|
|
740
|
+
if (before.system_role) throw { code: 'TARGET_IS_SYSTEM_PRINCIPAL' };
|
|
741
|
+
if (required) {
|
|
742
|
+
const { rows: cards } = await client.query(
|
|
743
|
+
`SELECT 1 FROM passkey_credentials WHERE builder_id = $1 LIMIT 1 FOR SHARE`,
|
|
744
|
+
[builderId]
|
|
745
|
+
);
|
|
746
|
+
if (!cards[0]) throw { code: 'NO_CARD_REGISTERED' };
|
|
747
|
+
}
|
|
748
|
+
const { rows } = await client.query(
|
|
749
|
+
`UPDATE builders SET require_card = $2 WHERE id = $1
|
|
750
|
+
RETURNING id, github_login, display_name, rank, status, require_card`,
|
|
751
|
+
[builderId, !!required]
|
|
752
|
+
);
|
|
753
|
+
await client.query(
|
|
754
|
+
`INSERT INTO audit_log
|
|
755
|
+
(builder_id, route, method, request_body_redacted, response_status, ip, user_agent, surface)
|
|
756
|
+
VALUES ($1, $2, 'PATCH', $3, 200, $4, $5, NULL)`,
|
|
757
|
+
[
|
|
758
|
+
actorBuilderId,
|
|
759
|
+
`${require('./api-prefix').API_PREFIX}/builders/:id/card-requirement`,
|
|
760
|
+
{ target_builder_id: String(builderId), require_card: !!required, was: before.require_card === true },
|
|
761
|
+
ip,
|
|
762
|
+
userAgent,
|
|
763
|
+
]
|
|
764
|
+
);
|
|
765
|
+
return rows[0];
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
727
769
|
async function reactivateBuilder({ builderId, actorBuilderId }) {
|
|
728
770
|
return withTx(async (client) => {
|
|
729
771
|
const { rows } = await client.query(
|
|
@@ -1183,6 +1225,7 @@ module.exports = {
|
|
|
1183
1225
|
RANKS,
|
|
1184
1226
|
setBuilderRank,
|
|
1185
1227
|
setBuilderTester,
|
|
1228
|
+
setRequireCard,
|
|
1186
1229
|
getLatestRankChangeForBuilder,
|
|
1187
1230
|
deactivateBuilder,
|
|
1188
1231
|
reactivateBuilder,
|
|
@@ -194,6 +194,7 @@ const EXPECTED_RANKS = {
|
|
|
194
194
|
// builder authority — rank + lifecycle status mutation
|
|
195
195
|
'PATCH /builders/:id/rank': 'perm:builder.rank.set',
|
|
196
196
|
'PATCH /builders/:id/status': 'perm:builder.status.set',
|
|
197
|
+
'PATCH /builders/:id/card-requirement': 'perm:builder.rank.set',
|
|
197
198
|
// task 1004561: the Treasury desk sets a builder's monthly limit through it
|
|
198
199
|
'PATCH /builders/:id/budget': 'perm:builder.budget.set',
|
|
199
200
|
// session revocation (kill another builder's / all sessions)
|
|
@@ -421,6 +421,43 @@ module.exports = function buildBuildersRouter() {
|
|
|
421
421
|
}
|
|
422
422
|
});
|
|
423
423
|
|
|
424
|
+
// PATCH /builders/:id/card-requirement — an Archon switches the hardware-card
|
|
425
|
+
// requirement on or off for one builder (task 1004748, ADR 0382 D4). Body:
|
|
426
|
+
// { require_card: boolean }. Default is off for everyone. Switching ON refuses 409
|
|
427
|
+
// no_card_registered unless the builder has a registered card, so the switch cannot
|
|
428
|
+
// lock anyone out. You cannot switch OFF your own requirement (403
|
|
429
|
+
// cannot_disable_own_card_requirement) — a stolen session must not be able to drop
|
|
430
|
+
// it; another Archon does it. Every change writes an audit row naming the target.
|
|
431
|
+
// rank: archon. Gate: requirePermission('builder.rank.set') (the non-delegable atom that
|
|
432
|
+
// guards the step-up's own target route) then requireCardStepUp; read fresh per request.
|
|
433
|
+
router.patch('/builders/:id/card-requirement', auth.requireBuilder, auth.requirePermission('builder.rank.set'), requireCardStepUp, async (req, res) => {
|
|
434
|
+
if (validateOrRespond(req, res, { require_card: { required: true, type: 'boolean' } })) return;
|
|
435
|
+
const targetId = parseId(req, res, { code: 'bad_builder_id', positiveInt: true });
|
|
436
|
+
if (targetId === null) return;
|
|
437
|
+
const required = req.body.require_card;
|
|
438
|
+
if (!required && String(targetId) === String(req.builder.id)) {
|
|
439
|
+
return res.fail('cannot_disable_own_card_requirement', 403);
|
|
440
|
+
}
|
|
441
|
+
try {
|
|
442
|
+
const builder = await db.setRequireCard({
|
|
443
|
+
builderId: targetId,
|
|
444
|
+
required,
|
|
445
|
+
actorBuilderId: req.builder.id,
|
|
446
|
+
ip: req.ip || null,
|
|
447
|
+
userAgent: req.headers['user-agent'] || null,
|
|
448
|
+
});
|
|
449
|
+
if (!builder) return res.fail('builder_not_found', 404);
|
|
450
|
+
res.json({ builder });
|
|
451
|
+
} catch (err) {
|
|
452
|
+
if (err && err.code === 'TARGET_IS_SYSTEM_PRINCIPAL') return res.fail('target_is_system_principal', 403);
|
|
453
|
+
if (err && err.code === 'NO_CARD_REGISTERED') {
|
|
454
|
+
return res.fail('no_card_registered', 409, { message: 'That builder has no registered card yet; they must register one before the requirement can be switched on.' });
|
|
455
|
+
}
|
|
456
|
+
log.error('[gds] PATCH /builders/:id/card-requirement', err);
|
|
457
|
+
res.fail('card_requirement_change_failed', { status: 500, message: 'internal error' });
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
424
461
|
// PATCH /builders/:id/capabilities — the owner gives or takes a builder's server
|
|
425
462
|
// access (task 1004950, ADR 0395). Body: { server: boolean }. A task that needs
|
|
426
463
|
// the server is shown to and claimable by a builder only while this is on.
|
package/src/module-api.js
CHANGED
|
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
|
|
|
81
81
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
82
82
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
83
83
|
|
|
84
|
-
const CORE_VERSION = '1.25.
|
|
84
|
+
const CORE_VERSION = '1.25.5'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
85
85
|
|
|
86
86
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
87
87
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// tests/builder_card_requirement.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1004748 (ADR 0382 D3/D4) — an Archon switches the hardware-card requirement on
|
|
4
|
+
// per builder. DB-free: the shared pool's client is a recorder.
|
|
5
|
+
//
|
|
6
|
+
// Run: node tests/builder_card_requirement.mjs
|
|
7
|
+
|
|
8
|
+
import { strict as assert } from 'node:assert';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
10
|
+
import { makeRunner } from './helpers.mjs';
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const { pool } = require('../src/bongos/pool');
|
|
14
|
+
const { test, summary } = makeRunner();
|
|
15
|
+
|
|
16
|
+
let builderRow; let hasCard; const sent = [];
|
|
17
|
+
pool.connect = async () => ({
|
|
18
|
+
query: async (sql, params = []) => {
|
|
19
|
+
const q = String(sql);
|
|
20
|
+
sent.push({ sql: q, params });
|
|
21
|
+
if (/^\s*(BEGIN|COMMIT|ROLLBACK)/i.test(q)) return { rows: [] };
|
|
22
|
+
if (/FROM builders WHERE id/.test(q)) return { rows: builderRow ? [builderRow] : [] };
|
|
23
|
+
if (/FROM passkey_credentials/.test(q)) return { rows: hasCard ? [{}] : [] };
|
|
24
|
+
if (/UPDATE builders SET require_card/.test(q)) return { rows: [{ id: params[0], require_card: params[1] }] };
|
|
25
|
+
return { rows: [] };
|
|
26
|
+
},
|
|
27
|
+
release() {},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const { setRequireCard } = require('../src/bongos/db');
|
|
31
|
+
const reset = () => { sent.length = 0; };
|
|
32
|
+
|
|
33
|
+
await test('switching on refuses a builder with no registered card, writes nothing', async () => {
|
|
34
|
+
reset(); builderRow = { id: '7', system_role: null, require_card: false }; hasCard = false;
|
|
35
|
+
await assert.rejects(setRequireCard({ builderId: '7', required: true, actorBuilderId: '1' }), { code: 'NO_CARD_REGISTERED' });
|
|
36
|
+
assert.equal(sent.some((s) => /UPDATE builders|INSERT INTO audit_log/.test(s.sql)), false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await test('switching on with a card sets the flag and audits the target in the same transaction', async () => {
|
|
40
|
+
reset(); hasCard = true;
|
|
41
|
+
const r = await setRequireCard({ builderId: '7', required: true, actorBuilderId: '1' });
|
|
42
|
+
assert.equal(r.require_card, true);
|
|
43
|
+
const audit = sent.find((s) => /INSERT INTO audit_log/.test(s.sql));
|
|
44
|
+
assert.ok(audit);
|
|
45
|
+
assert.equal(audit.params[0], '1');
|
|
46
|
+
assert.deepEqual(audit.params[2], { target_builder_id: '7', require_card: true, was: false });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await test('switching off needs no card and is audited', async () => {
|
|
50
|
+
reset(); hasCard = false; builderRow = { id: '7', system_role: null, require_card: true };
|
|
51
|
+
const r = await setRequireCard({ builderId: '7', required: false, actorBuilderId: '1' });
|
|
52
|
+
assert.equal(r.require_card, false);
|
|
53
|
+
assert.deepEqual(sent.find((s) => /INSERT INTO audit_log/.test(s.sql)).params[2].was, true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await test('unknown builder is null; a system principal is refused', async () => {
|
|
57
|
+
reset(); builderRow = null;
|
|
58
|
+
assert.equal(await setRequireCard({ builderId: '9', required: false, actorBuilderId: '1' }), null);
|
|
59
|
+
builderRow = { id: '9', system_role: 'runner', require_card: false };
|
|
60
|
+
await assert.rejects(setRequireCard({ builderId: '9', required: false, actorBuilderId: '1' }), { code: 'TARGET_IS_SYSTEM_PRINCIPAL' });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
await test('route: rank atom + step-up gate, self-disable refused, 409 mapped, errors classified', async () => {
|
|
64
|
+
const fs = await import('node:fs');
|
|
65
|
+
const src = fs.readFileSync(new URL('../src/bongos/routes/builders.js', import.meta.url), 'utf8');
|
|
66
|
+
const line = src.split(/\r?\n/).find((l) => l.includes("router.patch('/builders/:id/card-requirement'"));
|
|
67
|
+
assert.match(line, /requirePermission\('builder\.rank\.set'\), requireCardStepUp/);
|
|
68
|
+
assert.match(src, /cannot_disable_own_card_requirement', 403/);
|
|
69
|
+
assert.match(src, /NO_CARD_REGISTERED[\s\S]{0,80}no_card_registered', 409/);
|
|
70
|
+
const { classify } = require('../src/bongos/api-errors');
|
|
71
|
+
assert.equal(classify('no_card_registered'), 'conflict');
|
|
72
|
+
assert.equal(classify('cannot_disable_own_card_requirement'), 'forbidden');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
summary();
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// tests/provisioning_update_subscription_route.mjs
|
|
2
|
+
//
|
|
3
|
+
// Task 1004150 (ADR 0325 decision 5): POST /provisioning/instances/:id/update-subscription — the
|
|
4
|
+
// operator's switch for "does the nightly sweep move this instance on its own".
|
|
5
|
+
//
|
|
6
|
+
// The load-bearing assertions:
|
|
7
|
+
// • the fleet permission gate is what stands in front of it (an ungated route could arm any
|
|
8
|
+
// instance for unattended upgrades)
|
|
9
|
+
// • the body is strict and boolean: a string, a missing field or an extra field is a 400 and
|
|
10
|
+
// nothing is written
|
|
11
|
+
// • a real change writes the column and one audit event; a no-op writes and records nothing
|
|
12
|
+
// • an unknown id is a 404
|
|
13
|
+
//
|
|
14
|
+
// DB-free: the pool and data access are stubbed on the cached module objects over a real mounted
|
|
15
|
+
// router (the provisioning_repo_private_route.mjs pattern).
|
|
16
|
+
//
|
|
17
|
+
// Run: node tests/provisioning_update_subscription_route.mjs
|
|
18
|
+
|
|
19
|
+
import { strict as assert } from 'node:assert';
|
|
20
|
+
import { createRequire } from 'node:module';
|
|
21
|
+
import http from 'node:http';
|
|
22
|
+
|
|
23
|
+
const require = createRequire(import.meta.url);
|
|
24
|
+
const provisioning = require('../modules/provisioning/provisioning.js');
|
|
25
|
+
const api = require('../src/module-api.js');
|
|
26
|
+
|
|
27
|
+
let passed = 0, failed = 0;
|
|
28
|
+
async function ta(name, fn) {
|
|
29
|
+
try { await fn(); passed++; console.log(` PASS ${name}`); }
|
|
30
|
+
catch (e) { failed++; console.log(` FAIL ${name}\n ${e.message}`); }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let instance = null;
|
|
34
|
+
let writes = [];
|
|
35
|
+
let events = [];
|
|
36
|
+
let permissionAsked = null;
|
|
37
|
+
let allowed = true;
|
|
38
|
+
provisioning.getInstanceById = async (_db, id) => (instance ? { ...instance, id } : null);
|
|
39
|
+
provisioning.recordEvent = async (_db, e) => { events.push(e); };
|
|
40
|
+
api.pool.query = async (sql, params) => { writes.push({ sql, params }); return { rows: [] }; };
|
|
41
|
+
api.requireBuilder = (req, _res, next) => { req.builder = { id: 5, github_login: 'op', rank: 'metic' }; next(); };
|
|
42
|
+
api.requirePermission = (atom) => {
|
|
43
|
+
permissionAsked = atom;
|
|
44
|
+
return (req, res, next) => (allowed ? next() : res.status(403).json({ error: { code: 'forbidden' } }));
|
|
45
|
+
};
|
|
46
|
+
api.logger = () => ({ error() {}, warn() {}, info() {}, debug() {} });
|
|
47
|
+
|
|
48
|
+
const express = require('express');
|
|
49
|
+
const buildRouter = require('../modules/provisioning/routes/update-subscription.js');
|
|
50
|
+
const app = express();
|
|
51
|
+
app.use(express.json());
|
|
52
|
+
app.use((_req, res, next) => {
|
|
53
|
+
res.fail = (code, opts) => {
|
|
54
|
+
const status = typeof opts === 'number' ? opts : (opts && opts.status) || 400;
|
|
55
|
+
res.status(status).json({ error: { code, ...(typeof opts === 'object' ? opts : {}) } });
|
|
56
|
+
};
|
|
57
|
+
next();
|
|
58
|
+
});
|
|
59
|
+
app.use('/api/bongos', buildRouter());
|
|
60
|
+
|
|
61
|
+
const server = app.listen(0);
|
|
62
|
+
await new Promise((r) => server.once('listening', r));
|
|
63
|
+
const base = `http://127.0.0.1:${server.address().port}/api/bongos`;
|
|
64
|
+
|
|
65
|
+
function post(path, body) {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const payload = JSON.stringify(body);
|
|
68
|
+
const req = http.request(`${base}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }, (res) => {
|
|
69
|
+
let d = '';
|
|
70
|
+
res.on('data', (c) => { d += c; });
|
|
71
|
+
res.on('end', () => { let j = null; try { j = JSON.parse(d); } catch { /* non-JSON */ } resolve({ status: res.statusCode, body: j, raw: d }); });
|
|
72
|
+
});
|
|
73
|
+
req.on('error', reject);
|
|
74
|
+
req.write(payload);
|
|
75
|
+
req.end();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function reset(patch = {}) {
|
|
80
|
+
instance = { owner_builder_id: 7, slug: 'spike', update_subscribed: false, ...patch };
|
|
81
|
+
writes = []; events = []; allowed = true;
|
|
82
|
+
}
|
|
83
|
+
const URL_18 = '/provisioning/instances/18/update-subscription';
|
|
84
|
+
|
|
85
|
+
console.log('POST /provisioning/instances/:id/update-subscription:');
|
|
86
|
+
|
|
87
|
+
await ta('is gated on the fleet permission, and a caller without it writes nothing', async () => {
|
|
88
|
+
reset(); allowed = false;
|
|
89
|
+
const r = await post(URL_18, { subscribed: true });
|
|
90
|
+
assert.equal(permissionAsked, 'provisioning.fleet.manage');
|
|
91
|
+
assert.equal(r.status, 403, r.raw);
|
|
92
|
+
assert.equal(writes.length, 0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await ta('arming writes the column once and records one audit event', async () => {
|
|
96
|
+
reset();
|
|
97
|
+
const r = await post(URL_18, { subscribed: true });
|
|
98
|
+
assert.equal(r.status, 200, r.raw);
|
|
99
|
+
assert.deepEqual(r.body, { ok: true, update_subscribed: true, previous: false, changed: true });
|
|
100
|
+
assert.equal(writes.length, 1);
|
|
101
|
+
assert.match(writes[0].sql, /SET update_subscribed = \$2/);
|
|
102
|
+
assert.deepEqual(writes[0].params, [18, true]);
|
|
103
|
+
assert.equal(events.length, 1);
|
|
104
|
+
assert.equal(events[0].event, 'update-subscription-changed');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await ta('a no-op (already in that state) writes nothing and records nothing', async () => {
|
|
108
|
+
reset({ update_subscribed: true });
|
|
109
|
+
const r = await post(URL_18, { subscribed: true });
|
|
110
|
+
assert.equal(r.status, 200, r.raw);
|
|
111
|
+
assert.equal(r.body.changed, false);
|
|
112
|
+
assert.equal(writes.length, 0);
|
|
113
|
+
assert.equal(events.length, 0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await ta('disarming is symmetric', async () => {
|
|
117
|
+
reset({ update_subscribed: true });
|
|
118
|
+
const r = await post(URL_18, { subscribed: false });
|
|
119
|
+
assert.equal(r.body.update_subscribed, false);
|
|
120
|
+
assert.deepEqual(writes[0].params, [18, false]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await ta('a non-boolean, a missing field and an extra field are all 400 with nothing written', async () => {
|
|
124
|
+
for (const body of [{ subscribed: 'yes' }, {}, { subscribed: true, channel: 'minor' }]) {
|
|
125
|
+
reset();
|
|
126
|
+
const r = await post(URL_18, body);
|
|
127
|
+
assert.equal(r.status, 400, `${JSON.stringify(body)} → ${r.raw}`);
|
|
128
|
+
assert.equal(writes.length, 0);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await ta('an unknown id is 404', async () => {
|
|
133
|
+
reset(); instance = null;
|
|
134
|
+
const r = await post(URL_18, { subscribed: true });
|
|
135
|
+
assert.equal(r.status, 404, r.raw);
|
|
136
|
+
assert.equal(r.body.error.code, 'instance_not_found');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
server.close();
|
|
140
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
141
|
+
process.exit(failed ? 1 : 0);
|
|
@@ -129,3 +129,27 @@ test('update-sweep.js reads the DB from its own env, never an entry env (which n
|
|
|
129
129
|
// A core too old to carry the reader must keep the roster rather than crash the sweep.
|
|
130
130
|
assert.match(src, /typeof channel\.readDbChannels === 'function'/);
|
|
131
131
|
});
|
|
132
|
+
|
|
133
|
+
// ---- task 1004150: the subscription roster lives on the project row too ----
|
|
134
|
+
|
|
135
|
+
const subDb = (subs) => ({ ok: true, channels: new Map(), subscribed: new Map(Object.entries(subs)) });
|
|
136
|
+
|
|
137
|
+
test('subscribed on the row: reported as deploy-sourced; a file-only entry is kept as an override', () => {
|
|
138
|
+
const { instances, unreachable } = uc.applyDbSubscriptions([entry(), entry({ slug: 'charter' })], subDb({ cloudbongos: true, charter: false }));
|
|
139
|
+
assert.equal(instances[0].subscribedSource, uc.CHANNEL_SOURCE_DEPLOY);
|
|
140
|
+
assert.equal(instances[1].subscribedSource, uc.CHANNEL_SOURCE_ROSTER);
|
|
141
|
+
assert.match(instances[1].subscribedNote, /roster file only/);
|
|
142
|
+
assert.deepEqual(unreachable, []);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('subscribed on the row with no roster entry: surfaced as unreachable, never dropped', () => {
|
|
146
|
+
const { instances, unreachable } = uc.applyDbSubscriptions([], subDb({ mercury: true, venus: false }));
|
|
147
|
+
assert.equal(instances.length, 0);
|
|
148
|
+
assert.deepEqual(unreachable, ['mercury']);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test('database unreachable: the roster alone decides and nothing is reported unreachable', () => {
|
|
152
|
+
const { instances, unreachable } = uc.applyDbSubscriptions([entry()], { ok: false, reason: 'psql not found' });
|
|
153
|
+
assert.equal(instances.length, 1);
|
|
154
|
+
assert.deepEqual(unreachable, []);
|
|
155
|
+
});
|