@shipeasy/openapi 3.0.0 → 3.2.0

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/openapi.json CHANGED
@@ -1001,6 +1001,393 @@
1001
1001
  }
1002
1002
  }
1003
1003
  },
1004
+ "/api/admin/gates/{id}/whitelist": {
1005
+ "get": {
1006
+ "operationId": "getGateWhitelist",
1007
+ "summary": "Read a gate's whitelist",
1008
+ "description": "Returns the gate's whitelist — the always-first allowlist that admits the listed identities before any targeting rule or percentage rollout is evaluated.\n\nA gate with no whitelist returns `entries: []` (and the default `attr`), never a 404 — so a caller can read-then-write without special-casing the empty gate.\n\n**Use case:** Check whether an account is already let through before adding it.",
1009
+ "tags": [
1010
+ "Flags"
1011
+ ],
1012
+ "parameters": [
1013
+ {
1014
+ "$ref": "#/components/parameters/ProjectId"
1015
+ },
1016
+ {
1017
+ "name": "id",
1018
+ "in": "path",
1019
+ "required": true,
1020
+ "description": "Stable opaque gate id (`gate_…`) or the gate's `name`.",
1021
+ "schema": {
1022
+ "$ref": "#/components/schemas/ResourceId"
1023
+ }
1024
+ }
1025
+ ],
1026
+ "responses": {
1027
+ "200": {
1028
+ "description": "Read a gate's whitelist",
1029
+ "content": {
1030
+ "application/json": {
1031
+ "schema": {
1032
+ "$ref": "#/components/schemas/GateWhitelist"
1033
+ },
1034
+ "example": {
1035
+ "id": "gate_01j7w8a1b2c3d4e5f6g7h8i9j0",
1036
+ "name": "new_checkout",
1037
+ "attr": "email",
1038
+ "entries": [
1039
+ "alice@acme.dev",
1040
+ "bob@acme.dev"
1041
+ ]
1042
+ }
1043
+ }
1044
+ }
1045
+ },
1046
+ "400": {
1047
+ "$ref": "#/components/responses/BadRequest"
1048
+ },
1049
+ "401": {
1050
+ "$ref": "#/components/responses/Unauthorized"
1051
+ },
1052
+ "403": {
1053
+ "$ref": "#/components/responses/Forbidden"
1054
+ },
1055
+ "404": {
1056
+ "$ref": "#/components/responses/NotFound"
1057
+ },
1058
+ "409": {
1059
+ "$ref": "#/components/responses/Conflict"
1060
+ },
1061
+ "422": {
1062
+ "$ref": "#/components/responses/UnprocessableEntity"
1063
+ }
1064
+ },
1065
+ "x-error-codes": [
1066
+ "BAD_REQUEST",
1067
+ "UNAUTHORIZED",
1068
+ "FORBIDDEN",
1069
+ "NOT_FOUND"
1070
+ ],
1071
+ "x-cli": {
1072
+ "name": "whitelist",
1073
+ "positional": [
1074
+ "id"
1075
+ ]
1076
+ }
1077
+ },
1078
+ "put": {
1079
+ "operationId": "setGateWhitelist",
1080
+ "summary": "Replace a gate's whitelist",
1081
+ "description": "Replaces the gate's whole whitelist with `entries`, creating the block if the gate didn't have one. Idempotent — the same call twice leaves the same list.\n\nThis is the only whitelist call that can switch `attr` (`email` ⇄ `user_id`) or clear the block: `entries: []` removes the whitelist from the gate entirely.\n\n**Use cases**\n\n- **Pin an exact list** — `{ \"entries\": [\"alice@acme.dev\", \"bob@acme.dev\"] }`.\n- **Switch to user ids** — `{ \"attr\": \"user_id\", \"entries\": [\"usr_123\"] }`.\n- **Drop the whitelist** — `{ \"entries\": [] }`.",
1082
+ "tags": [
1083
+ "Flags"
1084
+ ],
1085
+ "parameters": [
1086
+ {
1087
+ "$ref": "#/components/parameters/ProjectId"
1088
+ },
1089
+ {
1090
+ "name": "id",
1091
+ "in": "path",
1092
+ "required": true,
1093
+ "description": "Stable opaque gate id (`gate_…`) or the gate's `name`.",
1094
+ "schema": {
1095
+ "$ref": "#/components/schemas/ResourceId"
1096
+ }
1097
+ }
1098
+ ],
1099
+ "requestBody": {
1100
+ "required": true,
1101
+ "content": {
1102
+ "application/json": {
1103
+ "schema": {
1104
+ "$ref": "#/components/schemas/SetGateWhitelistRequest"
1105
+ },
1106
+ "examples": {
1107
+ "pinList": {
1108
+ "summary": "Pin an exact list of emails",
1109
+ "value": {
1110
+ "entries": [
1111
+ "alice@acme.dev",
1112
+ "bob@acme.dev"
1113
+ ]
1114
+ }
1115
+ },
1116
+ "switchToUserIds": {
1117
+ "summary": "Re-key the whitelist onto user ids",
1118
+ "description": "Switching `attr` replaces the entries wholesale — the old emails are not translated.",
1119
+ "value": {
1120
+ "attr": "user_id",
1121
+ "entries": [
1122
+ "usr_01j7w8a1b2c3d4e5f6g7h8i9j0"
1123
+ ]
1124
+ }
1125
+ },
1126
+ "clear": {
1127
+ "summary": "Remove the whitelist",
1128
+ "description": "An empty list drops the pinned block from the gate's stack.",
1129
+ "value": {
1130
+ "entries": []
1131
+ }
1132
+ }
1133
+ }
1134
+ }
1135
+ }
1136
+ },
1137
+ "responses": {
1138
+ "200": {
1139
+ "description": "Replace a gate's whitelist",
1140
+ "content": {
1141
+ "application/json": {
1142
+ "schema": {
1143
+ "$ref": "#/components/schemas/GateWhitelist"
1144
+ },
1145
+ "example": {
1146
+ "id": "gate_01j7w8a1b2c3d4e5f6g7h8i9j0",
1147
+ "name": "new_checkout",
1148
+ "attr": "email",
1149
+ "entries": [
1150
+ "alice@acme.dev",
1151
+ "bob@acme.dev"
1152
+ ]
1153
+ }
1154
+ }
1155
+ }
1156
+ },
1157
+ "400": {
1158
+ "$ref": "#/components/responses/BadRequest"
1159
+ },
1160
+ "401": {
1161
+ "$ref": "#/components/responses/Unauthorized"
1162
+ },
1163
+ "403": {
1164
+ "$ref": "#/components/responses/Forbidden"
1165
+ },
1166
+ "404": {
1167
+ "$ref": "#/components/responses/NotFound"
1168
+ },
1169
+ "409": {
1170
+ "$ref": "#/components/responses/Conflict"
1171
+ },
1172
+ "422": {
1173
+ "$ref": "#/components/responses/UnprocessableEntity"
1174
+ }
1175
+ },
1176
+ "x-error-codes": [
1177
+ "BAD_REQUEST",
1178
+ "UNAUTHORIZED",
1179
+ "FORBIDDEN",
1180
+ "NOT_FOUND",
1181
+ "VALIDATION"
1182
+ ],
1183
+ "x-cli": {
1184
+ "name": "whitelist-set",
1185
+ "positional": [
1186
+ "id"
1187
+ ]
1188
+ }
1189
+ },
1190
+ "post": {
1191
+ "operationId": "addToGateWhitelist",
1192
+ "summary": "Add entries to a gate's whitelist",
1193
+ "description": "Adds identities to the gate's whitelist, creating the block if the gate doesn't have one yet. Entries already on the list are skipped, so the call is idempotent and safe to retry.\n\nAdding to a gate that already has a whitelist keyed on the other attribute is rejected (409) rather than silently re-keying the entries already there — use `PUT` to switch `attr` deliberately.\n\n**Use case:** Let one more customer into a private beta without reading the current list first.",
1194
+ "tags": [
1195
+ "Flags"
1196
+ ],
1197
+ "parameters": [
1198
+ {
1199
+ "$ref": "#/components/parameters/ProjectId"
1200
+ },
1201
+ {
1202
+ "name": "id",
1203
+ "in": "path",
1204
+ "required": true,
1205
+ "description": "Stable opaque gate id (`gate_…`) or the gate's `name`.",
1206
+ "schema": {
1207
+ "$ref": "#/components/schemas/ResourceId"
1208
+ }
1209
+ }
1210
+ ],
1211
+ "requestBody": {
1212
+ "required": true,
1213
+ "content": {
1214
+ "application/json": {
1215
+ "schema": {
1216
+ "$ref": "#/components/schemas/AddToGateWhitelistRequest"
1217
+ },
1218
+ "examples": {
1219
+ "addOne": {
1220
+ "summary": "Admit one more email",
1221
+ "value": {
1222
+ "entries": [
1223
+ "carol@acme.dev"
1224
+ ]
1225
+ }
1226
+ },
1227
+ "createOnUserIds": {
1228
+ "summary": "Start a whitelist keyed on user ids",
1229
+ "description": "`attr` is honoured only because this gate has no whitelist yet.",
1230
+ "value": {
1231
+ "attr": "user_id",
1232
+ "entries": [
1233
+ "usr_01j7w8a1b2c3d4e5f6g7h8i9j0"
1234
+ ]
1235
+ }
1236
+ }
1237
+ }
1238
+ }
1239
+ }
1240
+ },
1241
+ "responses": {
1242
+ "200": {
1243
+ "description": "Add entries to a gate's whitelist",
1244
+ "content": {
1245
+ "application/json": {
1246
+ "schema": {
1247
+ "$ref": "#/components/schemas/GateWhitelist"
1248
+ },
1249
+ "example": {
1250
+ "id": "gate_01j7w8a1b2c3d4e5f6g7h8i9j0",
1251
+ "name": "new_checkout",
1252
+ "attr": "email",
1253
+ "entries": [
1254
+ "alice@acme.dev",
1255
+ "bob@acme.dev",
1256
+ "carol@acme.dev"
1257
+ ]
1258
+ }
1259
+ }
1260
+ }
1261
+ },
1262
+ "400": {
1263
+ "$ref": "#/components/responses/BadRequest"
1264
+ },
1265
+ "401": {
1266
+ "$ref": "#/components/responses/Unauthorized"
1267
+ },
1268
+ "403": {
1269
+ "$ref": "#/components/responses/Forbidden"
1270
+ },
1271
+ "404": {
1272
+ "$ref": "#/components/responses/NotFound"
1273
+ },
1274
+ "409": {
1275
+ "$ref": "#/components/responses/Conflict"
1276
+ },
1277
+ "422": {
1278
+ "$ref": "#/components/responses/UnprocessableEntity"
1279
+ }
1280
+ },
1281
+ "x-error-codes": [
1282
+ "BAD_REQUEST",
1283
+ "UNAUTHORIZED",
1284
+ "FORBIDDEN",
1285
+ "NOT_FOUND",
1286
+ "IMMUTABLE_FIELD",
1287
+ "VALIDATION"
1288
+ ],
1289
+ "x-cli": {
1290
+ "name": "whitelist-add",
1291
+ "positional": [
1292
+ "id"
1293
+ ]
1294
+ }
1295
+ },
1296
+ "delete": {
1297
+ "operationId": "removeFromGateWhitelist",
1298
+ "summary": "Remove entries from a gate's whitelist",
1299
+ "description": "Removes identities from the gate's whitelist. Entries that aren't on the list are skipped, so the call is idempotent.\n\nRemoving the last entry leaves an empty whitelist block in place; to drop the block itself use `PUT` with `entries: []`.\n\n**Use case:** Revoke one beta tester's access without touching anyone else's.",
1300
+ "tags": [
1301
+ "Flags"
1302
+ ],
1303
+ "parameters": [
1304
+ {
1305
+ "$ref": "#/components/parameters/ProjectId"
1306
+ },
1307
+ {
1308
+ "name": "id",
1309
+ "in": "path",
1310
+ "required": true,
1311
+ "description": "Stable opaque gate id (`gate_…`) or the gate's `name`.",
1312
+ "schema": {
1313
+ "$ref": "#/components/schemas/ResourceId"
1314
+ }
1315
+ }
1316
+ ],
1317
+ "requestBody": {
1318
+ "required": true,
1319
+ "content": {
1320
+ "application/json": {
1321
+ "schema": {
1322
+ "$ref": "#/components/schemas/RemoveFromGateWhitelistRequest"
1323
+ },
1324
+ "examples": {
1325
+ "removeOne": {
1326
+ "summary": "Revoke one email",
1327
+ "value": {
1328
+ "entries": [
1329
+ "carol@acme.dev"
1330
+ ]
1331
+ }
1332
+ }
1333
+ }
1334
+ }
1335
+ }
1336
+ },
1337
+ "responses": {
1338
+ "200": {
1339
+ "description": "Remove entries from a gate's whitelist",
1340
+ "content": {
1341
+ "application/json": {
1342
+ "schema": {
1343
+ "$ref": "#/components/schemas/GateWhitelist"
1344
+ },
1345
+ "example": {
1346
+ "id": "gate_01j7w8a1b2c3d4e5f6g7h8i9j0",
1347
+ "name": "new_checkout",
1348
+ "attr": "email",
1349
+ "entries": [
1350
+ "alice@acme.dev",
1351
+ "bob@acme.dev"
1352
+ ]
1353
+ }
1354
+ }
1355
+ }
1356
+ },
1357
+ "400": {
1358
+ "$ref": "#/components/responses/BadRequest"
1359
+ },
1360
+ "401": {
1361
+ "$ref": "#/components/responses/Unauthorized"
1362
+ },
1363
+ "403": {
1364
+ "$ref": "#/components/responses/Forbidden"
1365
+ },
1366
+ "404": {
1367
+ "$ref": "#/components/responses/NotFound"
1368
+ },
1369
+ "409": {
1370
+ "$ref": "#/components/responses/Conflict"
1371
+ },
1372
+ "422": {
1373
+ "$ref": "#/components/responses/UnprocessableEntity"
1374
+ }
1375
+ },
1376
+ "x-error-codes": [
1377
+ "BAD_REQUEST",
1378
+ "UNAUTHORIZED",
1379
+ "FORBIDDEN",
1380
+ "NOT_FOUND",
1381
+ "VALIDATION"
1382
+ ],
1383
+ "x-cli": {
1384
+ "name": "whitelist-remove",
1385
+ "positional": [
1386
+ "id"
1387
+ ]
1388
+ }
1389
+ }
1390
+ },
1004
1391
  "/api/admin/experiments": {
1005
1392
  "get": {
1006
1393
  "operationId": "listExperiments",
@@ -4316,15 +4703,115 @@
4316
4703
  "content": {
4317
4704
  "application/json": {
4318
4705
  "schema": {
4319
- "$ref": "#/components/schemas/UnsetKillswitchSwitchRequest"
4706
+ "$ref": "#/components/schemas/UnsetKillswitchSwitchRequest"
4707
+ },
4708
+ "examples": {
4709
+ "removeRegion": {
4710
+ "summary": "Remove eu_region from prod",
4711
+ "description": "Drops the `eu_region` override on prod so the flat `value` applies again.",
4712
+ "value": {
4713
+ "env": "prod",
4714
+ "switchKey": "eu_region"
4715
+ }
4716
+ }
4717
+ }
4718
+ }
4719
+ }
4720
+ },
4721
+ "responses": {
4722
+ "200": {
4723
+ "description": "Remove one switch entry",
4724
+ "content": {
4725
+ "application/json": {
4726
+ "schema": {
4727
+ "$ref": "#/components/schemas/UnsetKillswitchSwitchResponse"
4728
+ },
4729
+ "example": {
4730
+ "id": "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8",
4731
+ "env": "prod",
4732
+ "switchKey": "eu_region",
4733
+ "removed": true
4734
+ }
4735
+ }
4736
+ }
4737
+ },
4738
+ "400": {
4739
+ "$ref": "#/components/responses/BadRequest"
4740
+ },
4741
+ "401": {
4742
+ "$ref": "#/components/responses/Unauthorized"
4743
+ },
4744
+ "403": {
4745
+ "$ref": "#/components/responses/Forbidden"
4746
+ },
4747
+ "404": {
4748
+ "$ref": "#/components/responses/NotFound"
4749
+ },
4750
+ "409": {
4751
+ "$ref": "#/components/responses/Conflict"
4752
+ },
4753
+ "422": {
4754
+ "$ref": "#/components/responses/UnprocessableEntity"
4755
+ }
4756
+ },
4757
+ "x-error-codes": [
4758
+ "BAD_REQUEST",
4759
+ "UNAUTHORIZED",
4760
+ "FORBIDDEN",
4761
+ "NOT_FOUND"
4762
+ ],
4763
+ "x-cli": {
4764
+ "name": "unset",
4765
+ "positional": [
4766
+ "id"
4767
+ ]
4768
+ }
4769
+ }
4770
+ },
4771
+ "/api/admin/killswitches/{id}/value": {
4772
+ "put": {
4773
+ "operationId": "setKillswitchValue",
4774
+ "summary": "Set the flat value on one env",
4775
+ "description": "Sets the flat `value` on a single `env`, publishing one new version on that env only. `switches` and other envs are untouched.\n\nUse this to trip (or untrip) a killswitch on one environment without replacing its per-key overrides.\n\n**Use cases**\n\n- **Trip on prod** — `{ \"env\": \"prod\", \"value\": true }`.\n- **Untrip on prod** — `{ \"env\": \"prod\", \"value\": false }`.",
4776
+ "tags": [
4777
+ "Killswitch"
4778
+ ],
4779
+ "parameters": [
4780
+ {
4781
+ "$ref": "#/components/parameters/ProjectId"
4782
+ },
4783
+ {
4784
+ "name": "id",
4785
+ "in": "path",
4786
+ "required": true,
4787
+ "description": "Stable opaque killswitch id (`ksw_…`) or the killswitch's `name`.",
4788
+ "schema": {
4789
+ "$ref": "#/components/schemas/ResourceId"
4790
+ }
4791
+ }
4792
+ ],
4793
+ "requestBody": {
4794
+ "required": true,
4795
+ "content": {
4796
+ "application/json": {
4797
+ "schema": {
4798
+ "$ref": "#/components/schemas/SetKillswitchValueRequest"
4320
4799
  },
4321
4800
  "examples": {
4322
- "removeRegion": {
4323
- "summary": "Remove eu_region from prod",
4324
- "description": "Drops the `eu_region` override on prod so the flat `value` applies again.",
4801
+ "tripProd": {
4802
+ "summary": "Trip on prod",
4803
+ "description": "Publishes `value: true` on prod only, bumping that env's version.",
4325
4804
  "value": {
4326
4805
  "env": "prod",
4327
- "switchKey": "eu_region"
4806
+ "value": true
4807
+ }
4808
+ },
4809
+ "untripProd": {
4810
+ "summary": "Untrip on prod",
4811
+ "description": "Publishes `value: false` on prod only.",
4812
+ "value": {
4813
+ "env": "prod",
4814
+ "value": false
4328
4815
  }
4329
4816
  }
4330
4817
  }
@@ -4333,17 +4820,22 @@
4333
4820
  },
4334
4821
  "responses": {
4335
4822
  "200": {
4336
- "description": "Remove one switch entry",
4823
+ "description": "Set the flat value on one env",
4337
4824
  "content": {
4338
4825
  "application/json": {
4339
4826
  "schema": {
4340
- "$ref": "#/components/schemas/UnsetKillswitchSwitchResponse"
4827
+ "$ref": "#/components/schemas/SetKillswitchValueResponse"
4341
4828
  },
4342
4829
  "example": {
4343
4830
  "id": "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8",
4344
4831
  "env": "prod",
4345
- "switchKey": "eu_region",
4346
- "removed": true
4832
+ "version": 6,
4833
+ "published": {
4834
+ "value": true,
4835
+ "switches": {
4836
+ "eu_region": true
4837
+ }
4838
+ }
4347
4839
  }
4348
4840
  }
4349
4841
  }
@@ -4371,21 +4863,22 @@
4371
4863
  "BAD_REQUEST",
4372
4864
  "UNAUTHORIZED",
4373
4865
  "FORBIDDEN",
4374
- "NOT_FOUND"
4866
+ "NOT_FOUND",
4867
+ "VALIDATION"
4375
4868
  ],
4376
4869
  "x-cli": {
4377
- "name": "unset",
4870
+ "name": "set-value",
4378
4871
  "positional": [
4379
4872
  "id"
4380
4873
  ]
4381
4874
  }
4382
4875
  }
4383
4876
  },
4384
- "/api/admin/killswitches/{id}/value": {
4385
- "put": {
4386
- "operationId": "setKillswitchValue",
4387
- "summary": "Set the flat value on one env",
4388
- "description": "Sets the flat `value` on a single `env`, publishing one new version on that env only. `switches` and other envs are untouched.\n\nUse this to trip (or untrip) a killswitch on one environment without replacing its per-key overrides.\n\n**Use cases**\n\n- **Trip on prod** — `{ \"env\": \"prod\", \"value\": true }`.\n- **Untrip on prod** — `{ \"env\": \"prod\", \"value\": false }`.",
4877
+ "/api/admin/killswitches/{id}/toggle": {
4878
+ "post": {
4879
+ "operationId": "toggleKillswitch",
4880
+ "summary": "Toggle a killswitch or one of its switches",
4881
+ "description": "Flips a killswitch on one environment and publishes a new version there. This is the one-call incident verb: it reads the current value, flips it, and publishes, so you don't have to fetch the killswitch first.\n\nEvery body field is optional, which is what makes the call widen cleanly:\n\n- **Flip the killswitch** — `{}`. Flips the flat `value` on `prod`.\n- **Flip one sub-switch** — `{ \"switchKey\": \"eu_region\" }`. Flips that entry on `prod`, creating it (from `false`) if it isn't in the map yet.\n- **Set it idempotently** — `{ \"switchKey\": \"eu_region\", \"value\": true }`. Publishes exactly that value, so a retried call can't undo the first one. A `null` `value` means \"flip\", not \"set to null\".\n- **Choose the environment** — add `\"env\": \"staging\"`. Omitted, `env` is `prod`.\n\nThe response reports both `previous` and `value`, so a caller that asked for a flip can see what it actually changed.\n\nPrefer this over `PUT /{id}/value` and `PUT /{id}/switch` unless you specifically need those endpoints' unconditional set semantics.",
4389
4882
  "tags": [
4390
4883
  "Killswitch"
4391
4884
  ],
@@ -4404,27 +4897,40 @@
4404
4897
  }
4405
4898
  ],
4406
4899
  "requestBody": {
4407
- "required": true,
4900
+ "required": false,
4408
4901
  "content": {
4409
4902
  "application/json": {
4410
4903
  "schema": {
4411
- "$ref": "#/components/schemas/SetKillswitchValueRequest"
4904
+ "$ref": "#/components/schemas/ToggleKillswitchRequest"
4412
4905
  },
4413
4906
  "examples": {
4414
- "tripProd": {
4415
- "summary": "Trip on prod",
4416
- "description": "Publishes `value: true` on prod only, bumping that env's version.",
4907
+ "flipOnProd": {
4908
+ "summary": "Flip the killswitch on prod",
4909
+ "description": "No body fields — reads the flat `value` on prod and publishes its opposite.",
4910
+ "value": {}
4911
+ },
4912
+ "flipOneSwitch": {
4913
+ "summary": "Flip one sub-switch on prod",
4914
+ "description": "Flips `switches.eu_region` on prod, leaving the flat `value` and every other key alone.",
4417
4915
  "value": {
4418
- "env": "prod",
4916
+ "switchKey": "eu_region"
4917
+ }
4918
+ },
4919
+ "setOneSwitch": {
4920
+ "summary": "Trip one sub-switch idempotently",
4921
+ "description": "Publishes `switches.eu_region = true` on prod whatever it was before — safe to retry.",
4922
+ "value": {
4923
+ "switchKey": "eu_region",
4419
4924
  "value": true
4420
4925
  }
4421
4926
  },
4422
- "untripProd": {
4423
- "summary": "Untrip on prod",
4424
- "description": "Publishes `value: false` on prod only.",
4927
+ "setOneSwitchOnStaging": {
4928
+ "summary": "Trip one sub-switch on a chosen env",
4929
+ "description": "The same idempotent set, published on staging instead of prod.",
4425
4930
  "value": {
4426
- "env": "prod",
4427
- "value": false
4931
+ "switchKey": "eu_region",
4932
+ "value": true,
4933
+ "env": "staging"
4428
4934
  }
4429
4935
  }
4430
4936
  }
@@ -4433,18 +4939,21 @@
4433
4939
  },
4434
4940
  "responses": {
4435
4941
  "200": {
4436
- "description": "Set the flat value on one env",
4942
+ "description": "Toggle a killswitch or one of its switches",
4437
4943
  "content": {
4438
4944
  "application/json": {
4439
4945
  "schema": {
4440
- "$ref": "#/components/schemas/SetKillswitchValueResponse"
4946
+ "$ref": "#/components/schemas/ToggleKillswitchResponse"
4441
4947
  },
4442
4948
  "example": {
4443
4949
  "id": "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8",
4444
4950
  "env": "prod",
4445
- "version": 6,
4951
+ "switchKey": "eu_region",
4952
+ "previous": false,
4953
+ "value": true,
4954
+ "version": 7,
4446
4955
  "published": {
4447
- "value": true,
4956
+ "value": false,
4448
4957
  "switches": {
4449
4958
  "eu_region": true
4450
4959
  }
@@ -4480,7 +4989,7 @@
4480
4989
  "VALIDATION"
4481
4990
  ],
4482
4991
  "x-cli": {
4483
- "name": "set-value",
4992
+ "name": "toggle",
4484
4993
  "positional": [
4485
4994
  "id"
4486
4995
  ]
@@ -6939,7 +7448,7 @@
6939
7448
  "name": "status",
6940
7449
  "in": "query",
6941
7450
  "required": false,
6942
- "description": "Filter by lifecycle status, or `all`. The human-gated holding states (`pending_approval`, `triage`) are excluded from `all`/default and returned only when requested as the exact status.",
7451
+ "description": "Filter by lifecycle status, or `all`. The human-gated holding state (`pending_approval`) is excluded from `all`/default and returned only when requested as the exact status.",
6943
7452
  "schema": {
6944
7453
  "anyOf": [
6945
7454
  {
@@ -7016,35 +7525,273 @@
7016
7525
  "403": {
7017
7526
  "$ref": "#/components/responses/Forbidden"
7018
7527
  },
7019
- "404": {
7020
- "$ref": "#/components/responses/NotFound"
7528
+ "404": {
7529
+ "$ref": "#/components/responses/NotFound"
7530
+ },
7531
+ "409": {
7532
+ "$ref": "#/components/responses/Conflict"
7533
+ },
7534
+ "422": {
7535
+ "$ref": "#/components/responses/UnprocessableEntity"
7536
+ }
7537
+ },
7538
+ "x-error-codes": [
7539
+ "BAD_REQUEST",
7540
+ "UNAUTHORIZED",
7541
+ "FORBIDDEN"
7542
+ ],
7543
+ "x-cli": {
7544
+ "name": "list"
7545
+ }
7546
+ },
7547
+ "post": {
7548
+ "operationId": "createOpsItem",
7549
+ "summary": "File a queue item",
7550
+ "description": "Files one queue item — a bug report or a feature request — and fires the project's connectors (GitHub issue / Slack). `type` selects which; only the two user-fileable types are accepted (`error`/`alert` tickets are auto-filed by the platform). Returns the new id and per-project number.\n\n**Use cases**\n\n- **File a bug** — `{ \"type\": \"bug\", \"title\": \"Checkout 500s on Safari\", \"stepsToReproduce\": \"…\" }`.\n- **File a feature request** — `{ \"type\": \"feature_request\", \"title\": \"Dark mode\", \"priority\": \"nice_to_have\" }`.",
7551
+ "tags": [
7552
+ "Ops"
7553
+ ],
7554
+ "parameters": [
7555
+ {
7556
+ "$ref": "#/components/parameters/ProjectId"
7557
+ }
7558
+ ],
7559
+ "requestBody": {
7560
+ "required": true,
7561
+ "content": {
7562
+ "application/json": {
7563
+ "schema": {
7564
+ "$ref": "#/components/schemas/CreateOpsItemRequest"
7565
+ },
7566
+ "examples": {
7567
+ "bug": {
7568
+ "summary": "File a bug report",
7569
+ "value": {
7570
+ "type": "bug",
7571
+ "title": "Checkout button misaligned on mobile",
7572
+ "stepsToReproduce": "Open the cart on iOS Safari and tap the price row.",
7573
+ "actualResult": "The primary CTA overlaps the price.",
7574
+ "expectedResult": "The CTA sits below the price with clear spacing.",
7575
+ "priority": "high"
7576
+ }
7577
+ },
7578
+ "featureRequest": {
7579
+ "summary": "File a feature request",
7580
+ "value": {
7581
+ "type": "feature_request",
7582
+ "title": "Dark mode for the dashboard",
7583
+ "description": "Add a theme toggle that persists per user.",
7584
+ "useCase": "Reduce eye strain for users working at night.",
7585
+ "priority": "nice_to_have"
7586
+ }
7587
+ }
7588
+ }
7589
+ }
7590
+ }
7591
+ },
7592
+ "responses": {
7593
+ "201": {
7594
+ "description": "File a queue item",
7595
+ "content": {
7596
+ "application/json": {
7597
+ "schema": {
7598
+ "$ref": "#/components/schemas/CreateOpsItemResponse"
7599
+ },
7600
+ "example": {
7601
+ "id": "fb_01j7w8a1b2c3d4e5f6g7h8i9j0",
7602
+ "number": 7
7603
+ }
7604
+ }
7605
+ }
7606
+ },
7607
+ "400": {
7608
+ "$ref": "#/components/responses/BadRequest"
7609
+ },
7610
+ "401": {
7611
+ "$ref": "#/components/responses/Unauthorized"
7612
+ },
7613
+ "403": {
7614
+ "$ref": "#/components/responses/Forbidden"
7615
+ },
7616
+ "404": {
7617
+ "$ref": "#/components/responses/NotFound"
7618
+ },
7619
+ "409": {
7620
+ "$ref": "#/components/responses/Conflict"
7621
+ },
7622
+ "422": {
7623
+ "$ref": "#/components/responses/UnprocessableEntity"
7624
+ }
7625
+ },
7626
+ "x-error-codes": [
7627
+ "BAD_REQUEST",
7628
+ "UNAUTHORIZED",
7629
+ "FORBIDDEN",
7630
+ "ALREADY_EXISTS",
7631
+ "VALIDATION"
7632
+ ],
7633
+ "x-cli": {
7634
+ "name": "create",
7635
+ "positional": [
7636
+ "title"
7637
+ ],
7638
+ "commands": [
7639
+ {
7640
+ "name": "create",
7641
+ "summary": "File a queue item (bug or feature request) — pass --type."
7642
+ },
7643
+ {
7644
+ "name": "bug",
7645
+ "summary": "File a bug report.",
7646
+ "preset": {
7647
+ "type": "bug"
7648
+ }
7649
+ },
7650
+ {
7651
+ "name": "feature",
7652
+ "summary": "File a feature request.",
7653
+ "preset": {
7654
+ "type": "feature_request"
7655
+ }
7656
+ }
7657
+ ]
7658
+ }
7659
+ }
7660
+ },
7661
+ "/ops/bug": {
7662
+ "post": {
7663
+ "operationId": "createPublicBug",
7664
+ "summary": "File a bug",
7665
+ "description": "Files one bug onto a project's queue, awaiting human approval. This is the **public** intake: it authenticates with a *client* SDK key rather than an admin key, so it can be called from a CLI, an installer script, a devtools overlay, or any shipped code — the same places a client key already lives.\n\nThree gates decide whether a ticket is filed, and nothing else the caller sends can widen them:\n\n1. the key is a `client` key carrying the `tickets:public_create` scope,\n2. the key's project has public ticket creation enabled, and\n3. the item is filed as `pending_approval` — parked out of the work queue until a human promotes it in the dashboard.\n\nThe project is the key's own project; there is no `X-Project-Id` to pass and no way to file into someone else's queue. Repeat submissions of the same title dedupe against the open ticket already tracking it, which returns `200` with `deduped: true` instead of filing again.\n\nThis endpoint is served by the Shipeasy **edge worker** (`api.shipeasy.ai`), not the admin API — see `servers` below.\n\n**Use case:** `shipeasy setup` fails on a customer's machine and self-reports the failure with the user's consent — `{ \"title\": \"Setup failed at Feature installs\", \"stepsToReproduce\": \"…\", \"actualResult\": \"…\" }`.",
7666
+ "tags": [
7667
+ "Ops"
7668
+ ],
7669
+ "servers": [
7670
+ {
7671
+ "url": "https://api.shipeasy.ai",
7672
+ "description": "Shipeasy edge worker (production)"
7673
+ },
7674
+ {
7675
+ "url": "http://localhost:8787",
7676
+ "description": "Local `wrangler dev`"
7677
+ }
7678
+ ],
7679
+ "security": [
7680
+ {
7681
+ "clientSdkKey": []
7682
+ }
7683
+ ],
7684
+ "requestBody": {
7685
+ "required": true,
7686
+ "content": {
7687
+ "application/json": {
7688
+ "schema": {
7689
+ "$ref": "#/components/schemas/CreatePublicBugRequest"
7690
+ },
7691
+ "examples": {
7692
+ "minimal": {
7693
+ "summary": "Just a title",
7694
+ "description": "Every other field is optional — the queue triages from `open` at default priority.",
7695
+ "value": {
7696
+ "title": "Checkout button misaligned on mobile"
7697
+ }
7698
+ },
7699
+ "full": {
7700
+ "summary": "A full report",
7701
+ "value": {
7702
+ "title": "Checkout button misaligned on mobile",
7703
+ "stepsToReproduce": "Open the cart on iOS Safari and tap the price row.",
7704
+ "actualResult": "The primary CTA overlaps the price.",
7705
+ "expectedResult": "The CTA sits below the price with clear spacing.",
7706
+ "priority": "high",
7707
+ "reporterEmail": "alice@acme.dev",
7708
+ "pageUrl": "https://acme.dev/cart"
7709
+ }
7710
+ }
7711
+ }
7712
+ }
7713
+ }
7714
+ },
7715
+ "responses": {
7716
+ "200": {
7717
+ "description": "An open ticket already tracks this report; nothing was filed",
7718
+ "content": {
7719
+ "application/json": {
7720
+ "schema": {
7721
+ "$ref": "#/components/schemas/CreatePublicTicketResponse"
7722
+ },
7723
+ "example": {
7724
+ "number": 7,
7725
+ "deduped": true
7726
+ }
7727
+ }
7728
+ }
7729
+ },
7730
+ "201": {
7731
+ "description": "File a bug",
7732
+ "content": {
7733
+ "application/json": {
7734
+ "schema": {
7735
+ "$ref": "#/components/schemas/CreatePublicTicketResponse"
7736
+ },
7737
+ "example": {
7738
+ "id": "fb_01j7w8a1b2c3d4e5f6g7h8i9j0",
7739
+ "number": 7
7740
+ }
7741
+ }
7742
+ }
7743
+ },
7744
+ "400": {
7745
+ "$ref": "#/components/responses/BadRequest"
7746
+ },
7747
+ "401": {
7748
+ "$ref": "#/components/responses/Unauthorized"
7749
+ },
7750
+ "403": {
7751
+ "$ref": "#/components/responses/Forbidden"
7752
+ },
7753
+ "413": {
7754
+ "$ref": "#/components/responses/PayloadTooLarge"
7021
7755
  },
7022
- "409": {
7023
- "$ref": "#/components/responses/Conflict"
7756
+ "429": {
7757
+ "$ref": "#/components/responses/TooManyRequests"
7024
7758
  },
7025
- "422": {
7026
- "$ref": "#/components/responses/UnprocessableEntity"
7759
+ "503": {
7760
+ "$ref": "#/components/responses/ServiceUnavailable"
7027
7761
  }
7028
7762
  },
7029
7763
  "x-error-codes": [
7030
7764
  "BAD_REQUEST",
7031
7765
  "UNAUTHORIZED",
7032
- "FORBIDDEN"
7766
+ "FORBIDDEN",
7767
+ "VALIDATION"
7033
7768
  ],
7034
7769
  "x-cli": {
7035
- "name": "list"
7770
+ "hidden": true
7036
7771
  }
7037
- },
7772
+ }
7773
+ },
7774
+ "/ops/feature-request": {
7038
7775
  "post": {
7039
- "operationId": "createOpsItem",
7040
- "summary": "File a queue item",
7041
- "description": "Files one queue item — a bug report or a feature request — and fires the project's connectors (GitHub issue / Slack). `type` selects which; only the two user-fileable types are accepted (`error`/`alert` tickets are auto-filed by the platform). Returns the new id and per-project number.\n\n**Use cases**\n\n- **File a bug** — `{ \"type\": \"bug\", \"title\": \"Checkout 500s on Safari\", \"stepsToReproduce\": \"…\" }`.\n- **File a feature request** — `{ \"type\": \"feature_request\", \"title\": \"Dark mode\", \"priority\": \"nice_to_have\" }`.",
7776
+ "operationId": "createPublicFeatureRequest",
7777
+ "summary": "File a feature request",
7778
+ "description": "Files one feature request onto a project's queue, awaiting human approval. The feature-request counterpart to `POST /ops/bug`, with the same three gates: a `client` key carrying `tickets:public_create`, a project that has opted in, and a `pending_approval` state forced server-side.\n\nThe project is the key's own project; there is no `X-Project-Id` to pass. Repeat submissions of the same title dedupe against the open ticket already tracking it, which returns `200` with `deduped: true` instead of filing again.\n\nThis endpoint is served by the Shipeasy **edge worker** (`api.shipeasy.ai`), not the admin API — see `servers` below.\n\n**Use case:** An in-product \"request a feature\" form posts what the user asked for — `{ \"title\": \"Dark mode\", \"useCase\": \"Reduce eye strain at night\" }`.",
7042
7779
  "tags": [
7043
7780
  "Ops"
7044
7781
  ],
7045
- "parameters": [
7782
+ "servers": [
7046
7783
  {
7047
- "$ref": "#/components/parameters/ProjectId"
7784
+ "url": "https://api.shipeasy.ai",
7785
+ "description": "Shipeasy edge worker (production)"
7786
+ },
7787
+ {
7788
+ "url": "http://localhost:8787",
7789
+ "description": "Local `wrangler dev`"
7790
+ }
7791
+ ],
7792
+ "security": [
7793
+ {
7794
+ "clientSdkKey": []
7048
7795
  }
7049
7796
  ],
7050
7797
  "requestBody": {
@@ -7052,28 +7799,23 @@
7052
7799
  "content": {
7053
7800
  "application/json": {
7054
7801
  "schema": {
7055
- "$ref": "#/components/schemas/CreateOpsItemRequest"
7802
+ "$ref": "#/components/schemas/CreatePublicFeatureRequestRequest"
7056
7803
  },
7057
7804
  "examples": {
7058
- "bug": {
7059
- "summary": "File a bug report",
7805
+ "minimal": {
7806
+ "summary": "Just a title",
7060
7807
  "value": {
7061
- "type": "bug",
7062
- "title": "Checkout button misaligned on mobile",
7063
- "stepsToReproduce": "Open the cart on iOS Safari and tap the price row.",
7064
- "actualResult": "The primary CTA overlaps the price.",
7065
- "expectedResult": "The CTA sits below the price with clear spacing.",
7066
- "priority": "high"
7808
+ "title": "Dark mode for the dashboard"
7067
7809
  }
7068
7810
  },
7069
- "featureRequest": {
7070
- "summary": "File a feature request",
7811
+ "full": {
7812
+ "summary": "A full request",
7071
7813
  "value": {
7072
- "type": "feature_request",
7073
7814
  "title": "Dark mode for the dashboard",
7074
7815
  "description": "Add a theme toggle that persists per user.",
7075
7816
  "useCase": "Reduce eye strain for users working at night.",
7076
- "priority": "nice_to_have"
7817
+ "priority": "nice_to_have",
7818
+ "reporterEmail": "alice@acme.dev"
7077
7819
  }
7078
7820
  }
7079
7821
  }
@@ -7081,16 +7823,30 @@
7081
7823
  }
7082
7824
  },
7083
7825
  "responses": {
7826
+ "200": {
7827
+ "description": "An open ticket already tracks this report; nothing was filed",
7828
+ "content": {
7829
+ "application/json": {
7830
+ "schema": {
7831
+ "$ref": "#/components/schemas/CreatePublicTicketResponse"
7832
+ },
7833
+ "example": {
7834
+ "number": 7,
7835
+ "deduped": true
7836
+ }
7837
+ }
7838
+ }
7839
+ },
7084
7840
  "201": {
7085
- "description": "File a queue item",
7841
+ "description": "File a feature request",
7086
7842
  "content": {
7087
7843
  "application/json": {
7088
7844
  "schema": {
7089
- "$ref": "#/components/schemas/CreateOpsItemResponse"
7845
+ "$ref": "#/components/schemas/CreatePublicTicketResponse"
7090
7846
  },
7091
7847
  "example": {
7092
7848
  "id": "fb_01j7w8a1b2c3d4e5f6g7h8i9j0",
7093
- "number": 7
7849
+ "number": 8
7094
7850
  }
7095
7851
  }
7096
7852
  }
@@ -7104,48 +7860,24 @@
7104
7860
  "403": {
7105
7861
  "$ref": "#/components/responses/Forbidden"
7106
7862
  },
7107
- "404": {
7108
- "$ref": "#/components/responses/NotFound"
7863
+ "413": {
7864
+ "$ref": "#/components/responses/PayloadTooLarge"
7109
7865
  },
7110
- "409": {
7111
- "$ref": "#/components/responses/Conflict"
7866
+ "429": {
7867
+ "$ref": "#/components/responses/TooManyRequests"
7112
7868
  },
7113
- "422": {
7114
- "$ref": "#/components/responses/UnprocessableEntity"
7869
+ "503": {
7870
+ "$ref": "#/components/responses/ServiceUnavailable"
7115
7871
  }
7116
7872
  },
7117
7873
  "x-error-codes": [
7118
7874
  "BAD_REQUEST",
7119
7875
  "UNAUTHORIZED",
7120
7876
  "FORBIDDEN",
7121
- "ALREADY_EXISTS",
7122
7877
  "VALIDATION"
7123
7878
  ],
7124
7879
  "x-cli": {
7125
- "name": "create",
7126
- "positional": [
7127
- "title"
7128
- ],
7129
- "commands": [
7130
- {
7131
- "name": "create",
7132
- "summary": "File a queue item (bug or feature request) — pass --type."
7133
- },
7134
- {
7135
- "name": "bug",
7136
- "summary": "File a bug report.",
7137
- "preset": {
7138
- "type": "bug"
7139
- }
7140
- },
7141
- {
7142
- "name": "feature",
7143
- "summary": "File a feature request.",
7144
- "preset": {
7145
- "type": "feature_request"
7146
- }
7147
- }
7148
- ]
7880
+ "hidden": true
7149
7881
  }
7150
7882
  }
7151
7883
  },
@@ -12187,6 +12919,12 @@
12187
12919
  "scheme": "bearer",
12188
12920
  "bearerFormat": "sdk_admin_*",
12189
12921
  "description": "Pass an admin SDK key as `Authorization: Bearer sdk_admin_…`. Mint via `POST /api/admin/keys` with `type: \"admin\"`."
12922
+ },
12923
+ "clientSdkKey": {
12924
+ "type": "apiKey",
12925
+ "in": "header",
12926
+ "name": "X-SDK-Key",
12927
+ "description": "Pass a **client** SDK key as `X-SDK-Key: sdk_client_…`. Used only by the public ticket intake (`POST /ops/bug`, `POST /ops/feature-request`), which is served by the edge worker rather than the admin API.\n\nClient keys are designed to be embedded in shipped code — a CLI, an installer, a browser bundle — so presenting one here is safe by construction: it can do nothing but file a rate-limited ticket into its own project, in a state a human must approve. It carries no read access and cannot reach any other operation in this contract.\n\nThe key must additionally carry the `tickets:public_create` scope, and its project must have public ticket creation enabled."
12190
12928
  }
12191
12929
  },
12192
12930
  "parameters": {
@@ -13683,6 +14421,118 @@
13683
14421
  },
13684
14422
  "description": "Recent audit rows for one gate, newest first."
13685
14423
  },
14424
+ "GateWhitelistAttr": {
14425
+ "type": "string",
14426
+ "enum": [
14427
+ "email",
14428
+ "user_id"
14429
+ ],
14430
+ "description": "Which identity attribute the whitelist matches on. `email` compares the caller's `email`; `user_id` compares the caller's `userID`. A whitelist matches on exactly one of the two at a time."
14431
+ },
14432
+ "GateWhitelist": {
14433
+ "type": "object",
14434
+ "properties": {
14435
+ "id": {
14436
+ "type": "string",
14437
+ "description": "Resolved gate id."
14438
+ },
14439
+ "name": {
14440
+ "type": "string",
14441
+ "description": "The gate's stable `name`."
14442
+ },
14443
+ "attr": {
14444
+ "$ref": "#/components/schemas/GateWhitelistAttr"
14445
+ },
14446
+ "entries": {
14447
+ "type": "array",
14448
+ "items": {
14449
+ "type": "string",
14450
+ "minLength": 1
14451
+ },
14452
+ "description": "The whitelisted identities, in the order they are stored. Deduplicated case-sensitively; an empty array means the gate has no whitelist."
14453
+ }
14454
+ },
14455
+ "required": [
14456
+ "id",
14457
+ "name",
14458
+ "attr",
14459
+ "entries"
14460
+ ],
14461
+ "additionalProperties": false,
14462
+ "description": "A gate's whitelist — the always-first allowlist that admits the listed identities before any targeting rule or rollout runs. Backed by the pinned `whitelist` entry at the head of the gate's `stack`, so it is the same list the dashboard's Whitelist block edits."
14463
+ },
14464
+ "SetGateWhitelistRequest": {
14465
+ "type": "object",
14466
+ "properties": {
14467
+ "attr": {
14468
+ "anyOf": [
14469
+ {
14470
+ "$ref": "#/components/schemas/GateWhitelistAttr"
14471
+ }
14472
+ ],
14473
+ "description": "Identity attribute to match on. Defaults to the whitelist's current attribute, or `email` when the gate has no whitelist yet."
14474
+ },
14475
+ "entries": {
14476
+ "type": "array",
14477
+ "items": {
14478
+ "type": "string",
14479
+ "minLength": 1
14480
+ },
14481
+ "description": "The complete whitelist after the call. Pass `[]` to remove the whitelist from the gate entirely."
14482
+ }
14483
+ },
14484
+ "required": [
14485
+ "entries"
14486
+ ],
14487
+ "additionalProperties": false,
14488
+ "description": "Body for `PUT /api/admin/gates/{id}/whitelist`. Replaces the whole list — idempotent, and the only call that can switch `attr` or clear the whitelist."
14489
+ },
14490
+ "AddToGateWhitelistRequest": {
14491
+ "type": "object",
14492
+ "properties": {
14493
+ "attr": {
14494
+ "anyOf": [
14495
+ {
14496
+ "$ref": "#/components/schemas/GateWhitelistAttr"
14497
+ }
14498
+ ],
14499
+ "description": "Identity attribute to match on. Only honoured when the gate has no whitelist yet (this call creates it); passing an attribute that disagrees with an existing whitelist is a 409 rather than a silent re-key of the entries already there."
14500
+ },
14501
+ "entries": {
14502
+ "type": "array",
14503
+ "minItems": 1,
14504
+ "items": {
14505
+ "type": "string",
14506
+ "minLength": 1
14507
+ },
14508
+ "description": "Identities to admit. Already-listed entries are skipped, so the call is idempotent."
14509
+ }
14510
+ },
14511
+ "required": [
14512
+ "entries"
14513
+ ],
14514
+ "additionalProperties": false,
14515
+ "description": "Body for `POST /api/admin/gates/{id}/whitelist`. Adds entries to the whitelist, creating it if the gate doesn't have one."
14516
+ },
14517
+ "RemoveFromGateWhitelistRequest": {
14518
+ "type": "object",
14519
+ "properties": {
14520
+ "entries": {
14521
+ "type": "array",
14522
+ "minItems": 1,
14523
+ "items": {
14524
+ "type": "string",
14525
+ "minLength": 1
14526
+ },
14527
+ "description": "Identities to stop admitting. Entries that aren't listed are skipped, so the call is idempotent."
14528
+ }
14529
+ },
14530
+ "required": [
14531
+ "entries"
14532
+ ],
14533
+ "additionalProperties": false,
14534
+ "description": "Body for `DELETE /api/admin/gates/{id}/whitelist`. Removes entries from the whitelist. Removing the last entry leaves an empty whitelist in place — use `PUT` with `entries: []` to drop the block itself."
14535
+ },
13686
14536
  "ExperimentApiRow": {
13687
14537
  "type": "object",
13688
14538
  "properties": {
@@ -16713,8 +17563,96 @@
16713
17563
  "id": {
16714
17564
  "type": "string"
16715
17565
  },
16716
- "env": {
16717
- "$ref": "#/components/schemas/Env"
17566
+ "env": {
17567
+ "$ref": "#/components/schemas/Env"
17568
+ },
17569
+ "version": {
17570
+ "type": "integer",
17571
+ "minimum": -9007199254740991,
17572
+ "maximum": 9007199254740991,
17573
+ "description": "Newly published version on `env`."
17574
+ },
17575
+ "published": {
17576
+ "$ref": "#/components/schemas/KillswitchValue"
17577
+ }
17578
+ },
17579
+ "required": [
17580
+ "id",
17581
+ "env",
17582
+ "version",
17583
+ "published"
17584
+ ],
17585
+ "additionalProperties": false
17586
+ },
17587
+ "ToggleKillswitchRequest": {
17588
+ "type": "object",
17589
+ "properties": {
17590
+ "switchKey": {
17591
+ "anyOf": [
17592
+ {
17593
+ "type": "string",
17594
+ "maxLength": 64,
17595
+ "pattern": "^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$"
17596
+ },
17597
+ {
17598
+ "type": "null"
17599
+ }
17600
+ ],
17601
+ "description": "Which target to flip. Omit (or `null`) to flip the killswitch's own flat `value`; name a switch key to flip that nested sub-switch instead, creating the entry if it doesn't exist yet."
17602
+ },
17603
+ "value": {
17604
+ "anyOf": [
17605
+ {
17606
+ "type": "boolean"
17607
+ },
17608
+ {
17609
+ "type": "null"
17610
+ }
17611
+ ],
17612
+ "description": "The value to publish. Omit (or `null`) to flip whatever is stored now — read-modify-write in one call. Pass an explicit `true`/`false` to make the call idempotent, so a retry can't undo the first attempt."
17613
+ },
17614
+ "env": {
17615
+ "anyOf": [
17616
+ {
17617
+ "$ref": "#/components/schemas/Env"
17618
+ }
17619
+ ],
17620
+ "description": "Environment to publish on. Defaults to `prod` — the environment an incident response means when it says \"kill it\"."
17621
+ }
17622
+ },
17623
+ "additionalProperties": false,
17624
+ "description": "Body for `POST /api/admin/killswitches/{id}/toggle`. Every field is optional, so the four useful calls read as one method with a widening argument list:\n\n- `{}` — flip the flat value on prod.\n- `{ \"switchKey\": \"eu_region\" }` — flip that sub-switch on prod.\n- `{ \"switchKey\": \"eu_region\", \"value\": true }` — set that sub-switch on prod, idempotently.\n- `{ \"switchKey\": \"eu_region\", \"value\": true, \"env\": \"staging\" }` — the same, on a chosen env."
17625
+ },
17626
+ "ToggleKillswitchResponse": {
17627
+ "type": "object",
17628
+ "properties": {
17629
+ "id": {
17630
+ "type": "string",
17631
+ "description": "Resolved killswitch id."
17632
+ },
17633
+ "env": {
17634
+ "$ref": "#/components/schemas/Env"
17635
+ },
17636
+ "switchKey": {
17637
+ "anyOf": [
17638
+ {
17639
+ "type": "string",
17640
+ "maxLength": 64,
17641
+ "pattern": "^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$"
17642
+ },
17643
+ {
17644
+ "type": "null"
17645
+ }
17646
+ ],
17647
+ "description": "The sub-switch that was flipped, or `null` when the flat `value` was."
17648
+ },
17649
+ "previous": {
17650
+ "type": "boolean",
17651
+ "description": "The value that was in effect before this call. Equal to `value` when an idempotent `value` matched what was already stored."
17652
+ },
17653
+ "value": {
17654
+ "type": "boolean",
17655
+ "description": "The value now in effect on `env`."
16718
17656
  },
16719
17657
  "version": {
16720
17658
  "type": "integer",
@@ -16729,6 +17667,9 @@
16729
17667
  "required": [
16730
17668
  "id",
16731
17669
  "env",
17670
+ "switchKey",
17671
+ "previous",
17672
+ "value",
16732
17673
  "version",
16733
17674
  "published"
16734
17675
  ],
@@ -19272,8 +20213,6 @@
19272
20213
  "enum": [
19273
20214
  "open",
19274
20215
  "pending_approval",
19275
- "triage",
19276
- "triaged",
19277
20216
  "investigating_by_ai",
19278
20217
  "in_progress",
19279
20218
  "blocked",
@@ -19281,7 +20220,7 @@
19281
20220
  "resolved",
19282
20221
  "wont_fix"
19283
20222
  ],
19284
- "description": "Lifecycle status of a queue item. The working flow is `open` → `triaged` → `in_progress` → `ready_for_qa` → `resolved` (or `wont_fix`, terminal from any earlier stage). `blocked` marks an item that can't progress until an external dependency clears — a working state a human sets and clears. `ready_for_qa` is what a developer sets once a fix lands; `resolved` is the QA sign-off, normally flipped in the dashboard after verification — set it directly from code only when the fix has been verified end-to-end. `investigating_by_ai` is a system-owned display state — set when the AI agent (Jarvis) picks an item up to investigate, never chosen by a human — so it is shown but not offered as a manual choice. Two human-gated holding states park an item OUT of the work queue until a human promotes it to `open` in the dashboard, so `GET /api/admin/ops` excludes them under `status=all`/default and returns them only when requested as an exact `status`: `pending_approval` is the pre-open approval gate for untriaged inbound (e.g. connector requests filed from a customer's connectors panel) so it never gets auto-implemented — approving = flipping the status to `open`; `triage` is the onboarding-help bucket — questions/errors submitted to the \"Stuck in onboarding?\" assistant are funnelled into the platform project as `triage` rows so the team can see where people get stuck and follow up, keeping onboarding chatter out of the work queue until a human moves real items to `open`."
20223
+ "description": "Lifecycle status of a queue item. The working flow is `open` → `in_progress` → `ready_for_qa` → `resolved` (or `wont_fix`, terminal from any earlier stage). `blocked` marks an item that can't progress until an external dependency clears — a working state a human sets and clears. `ready_for_qa` is what a developer sets once a fix lands; `resolved` is the QA sign-off, normally flipped in the dashboard after verification — set it directly from code only when the fix has been verified end-to-end. `investigating_by_ai` is a system-owned display state — set when the AI agent (Jarvis) picks an item up to investigate, never chosen by a human — so it is shown but not offered as a manual choice. `pending_approval` is the one human-gated holding state: it parks an item OUT of the work queue until a human promotes it to `open` in the dashboard, so `GET /api/admin/ops` excludes it under `status=all`/default and returns it only when requested as an exact `status`. It covers untriaged inbound that must never be auto-implemented — connector requests filed from a customer's connectors panel, and questions funnelled in from the \"Stuck in onboarding?\" assistant — where approving means flipping the status to `open`. Two earlier values were removed in favour of this single gate: `triage` (the onboarding-help bucket, now `pending_approval`) and `triaged` (a redundant \"looked at but not started\" step, now plain `open`)."
19285
20224
  },
19286
20225
  "OpsItemPriority": {
19287
20226
  "type": "string",
@@ -19297,17 +20236,16 @@
19297
20236
  "type": "string",
19298
20237
  "enum": [
19299
20238
  "pr_ready",
20239
+ "pr_merged",
19300
20240
  "investigated",
19301
20241
  "detected",
19302
20242
  "question",
19303
20243
  "ready_for_qa",
19304
20244
  "pending_approval",
19305
- "needs_triage",
19306
20245
  "working",
19307
- "triaged_low",
19308
20246
  "backlog"
19309
20247
  ],
19310
- "description": "Where automation has taken an item in the investigation lifecycle — the signal the ops cockpit renders as \"what AI did\" plus the one action expected of the user. It is **written as a side-effect of the actions that already happen**, never authored on its own: linking a PR sets `pr_ready`, moving to `in_progress` sets `working`, `ready_for_qa` sets `ready_for_qa`, a system/assistant reply sets `question`, resolving clears it, and creation seeds `needs_triage`/`backlog` (by priority) or `investigated`/`detected` for auto-filed error/alert tickets. `triaged_low` marks a low-signal item AI has looked at but that nothing is moving. Absent on rows filed before the field existed — consumers should treat a missing value as \"derive from the other fields\"."
20248
+ "description": "Where automation has taken an item in the investigation lifecycle — the signal the ops cockpit renders as \"what AI did\" plus the one action expected of the user. It is **written as a side-effect of the actions that already happen**, never authored on its own: linking a PR sets `pr_ready`, moving to `in_progress` sets `working`, `ready_for_qa` sets `ready_for_qa`, a system/assistant reply sets `question`, resolving clears it, and creation seeds `backlog` or `investigated`/`detected` for auto-filed error/alert tickets. `backlog` is the single \"nothing has happened yet\" state — how urgent that is reads off `priority`, not off a separate state, and its one action is to hand the item to an agent. `pr_merged` is derived client-side by scanning the linked pull request on GitHub, so it is accepted on write but not currently emitted by the API. Absent on rows filed before the field existed — consumers should treat a missing value as \"derive from the other fields\"."
19311
20249
  },
19312
20250
  "OpsBrowserContext": {
19313
20251
  "type": "object",
@@ -20497,42 +21435,382 @@
20497
21435
  },
20498
21436
  "createdAt": {
20499
21437
  "type": "string",
20500
- "description": "ISO-8601 creation timestamp."
21438
+ "description": "ISO-8601 creation timestamp."
21439
+ },
21440
+ "updatedAt": {
21441
+ "type": "string",
21442
+ "description": "ISO-8601 last-update timestamp."
21443
+ }
21444
+ },
21445
+ "required": [
21446
+ "id",
21447
+ "number",
21448
+ "type",
21449
+ "title",
21450
+ "status",
21451
+ "priority",
21452
+ "source",
21453
+ "owner",
21454
+ "run",
21455
+ "lastRun",
21456
+ "createdAt",
21457
+ "updatedAt"
21458
+ ],
21459
+ "additionalProperties": {},
21460
+ "description": "One queue item — any type — plus its resolved `owner` and run state."
21461
+ },
21462
+ "description": "A page of queue items, newest first."
21463
+ },
21464
+ "CreateBugRequest": {
21465
+ "type": "object",
21466
+ "properties": {
21467
+ "type": {
21468
+ "type": "string",
21469
+ "enum": [
21470
+ "bug"
21471
+ ],
21472
+ "description": "Discriminator — files a bug."
21473
+ },
21474
+ "title": {
21475
+ "type": "string",
21476
+ "minLength": 1,
21477
+ "maxLength": 200,
21478
+ "pattern": "^\\S(.*\\S)?$",
21479
+ "description": "One-line bug title (no leading/trailing whitespace)."
21480
+ },
21481
+ "stepsToReproduce": {
21482
+ "type": "string",
21483
+ "maxLength": 8000,
21484
+ "default": "",
21485
+ "description": "How to reproduce the bug."
21486
+ },
21487
+ "actualResult": {
21488
+ "type": "string",
21489
+ "maxLength": 8000,
21490
+ "default": "",
21491
+ "description": "What actually happened."
21492
+ },
21493
+ "expectedResult": {
21494
+ "type": "string",
21495
+ "maxLength": 8000,
21496
+ "default": "",
21497
+ "description": "What was expected instead."
21498
+ },
21499
+ "priority": {
21500
+ "anyOf": [
21501
+ {
21502
+ "$ref": "#/components/schemas/OpsItemPriority"
21503
+ },
21504
+ {
21505
+ "type": "null"
21506
+ }
21507
+ ],
21508
+ "description": "Initial triage priority, or `null`."
21509
+ },
21510
+ "status": {
21511
+ "anyOf": [
21512
+ {
21513
+ "$ref": "#/components/schemas/OpsItemStatus"
21514
+ }
21515
+ ],
21516
+ "description": "Initial lifecycle status; defaults to `open` when omitted."
21517
+ },
21518
+ "assigneeId": {
21519
+ "anyOf": [
21520
+ {
21521
+ "type": "string"
21522
+ },
21523
+ {
21524
+ "type": "null"
21525
+ }
21526
+ ],
21527
+ "description": "The `users.id` of the person to assign as owner at creation, or `null`."
21528
+ },
21529
+ "subscribers": {
21530
+ "type": "array",
21531
+ "items": {
21532
+ "type": "string",
21533
+ "format": "email"
21534
+ },
21535
+ "default": [],
21536
+ "description": "Emails of teammates to subscribe to this item's Slack pings at creation."
21537
+ },
21538
+ "tags": {
21539
+ "type": "array",
21540
+ "items": {
21541
+ "type": "string"
21542
+ },
21543
+ "default": [],
21544
+ "description": "Tag names to attach at creation (get-or-created by name, deduped case-insensitively)."
21545
+ },
21546
+ "reporterEmail": {
21547
+ "anyOf": [
21548
+ {
21549
+ "type": "string",
21550
+ "format": "email"
21551
+ },
21552
+ {
21553
+ "type": "null"
21554
+ }
21555
+ ],
21556
+ "description": "Email of the reporter, or `null`."
21557
+ },
21558
+ "pageUrl": {
21559
+ "anyOf": [
21560
+ {
21561
+ "type": "string",
21562
+ "format": "uri"
21563
+ },
21564
+ {
21565
+ "type": "null"
21566
+ }
21567
+ ],
21568
+ "description": "URL of the page the bug relates to, or `null`."
21569
+ },
21570
+ "userAgent": {
21571
+ "anyOf": [
21572
+ {
21573
+ "type": "string",
21574
+ "maxLength": 500
21575
+ },
21576
+ {
21577
+ "type": "null"
21578
+ }
21579
+ ],
21580
+ "description": "Reporter's user-agent string, or `null`."
21581
+ },
21582
+ "viewport": {
21583
+ "anyOf": [
21584
+ {
21585
+ "type": "string",
21586
+ "maxLength": 40
21587
+ },
21588
+ {
21589
+ "type": "null"
21590
+ }
21591
+ ],
21592
+ "description": "Reporter's viewport (e.g. `1280x720`), or `null`."
21593
+ },
21594
+ "context": {
21595
+ "anyOf": [
21596
+ {
21597
+ "type": "object",
21598
+ "additionalProperties": {}
21599
+ },
21600
+ {
21601
+ "type": "null"
21602
+ }
21603
+ ],
21604
+ "description": "Arbitrary capture context, or `null`."
21605
+ },
21606
+ "notify": {
21607
+ "anyOf": [
21608
+ {
21609
+ "$ref": "#/components/schemas/NotificationTarget"
21610
+ },
21611
+ {
21612
+ "type": "null"
21613
+ }
21614
+ ],
21615
+ "description": "Where this bug's completion notification lands."
21616
+ }
21617
+ },
21618
+ "required": [
21619
+ "type",
21620
+ "title"
21621
+ ],
21622
+ "additionalProperties": false,
21623
+ "description": "Bug-kind fields for `POST /api/admin/ops` (sent with `type: bug`)."
21624
+ },
21625
+ "CreateFeatureRequestRequest": {
21626
+ "type": "object",
21627
+ "properties": {
21628
+ "type": {
21629
+ "type": "string",
21630
+ "enum": [
21631
+ "feature_request"
21632
+ ],
21633
+ "description": "Discriminator — files a feature request."
21634
+ },
21635
+ "title": {
21636
+ "type": "string",
21637
+ "minLength": 1,
21638
+ "maxLength": 200,
21639
+ "pattern": "^\\S(.*\\S)?$",
21640
+ "description": "One-line feature-request title (no leading/trailing whitespace)."
21641
+ },
21642
+ "description": {
21643
+ "type": "string",
21644
+ "maxLength": 8000,
21645
+ "default": "",
21646
+ "description": "What the feature is."
21647
+ },
21648
+ "useCase": {
21649
+ "type": "string",
21650
+ "maxLength": 8000,
21651
+ "default": "",
21652
+ "description": "Why it's needed / the use case."
21653
+ },
21654
+ "priority": {
21655
+ "anyOf": [
21656
+ {
21657
+ "$ref": "#/components/schemas/OpsItemPriority"
21658
+ },
21659
+ {
21660
+ "type": "null"
21661
+ }
21662
+ ],
21663
+ "description": "Initial triage priority, or `null`."
21664
+ },
21665
+ "status": {
21666
+ "anyOf": [
21667
+ {
21668
+ "$ref": "#/components/schemas/OpsItemStatus"
21669
+ }
21670
+ ],
21671
+ "description": "Initial lifecycle status; defaults to `open` when omitted."
21672
+ },
21673
+ "assigneeId": {
21674
+ "anyOf": [
21675
+ {
21676
+ "type": "string"
21677
+ },
21678
+ {
21679
+ "type": "null"
21680
+ }
21681
+ ],
21682
+ "description": "The `users.id` of the person to assign as owner at creation, or `null`."
21683
+ },
21684
+ "subscribers": {
21685
+ "type": "array",
21686
+ "items": {
21687
+ "type": "string",
21688
+ "format": "email"
20501
21689
  },
20502
- "updatedAt": {
20503
- "type": "string",
20504
- "description": "ISO-8601 last-update timestamp."
20505
- }
21690
+ "default": [],
21691
+ "description": "Emails of teammates to subscribe to this item's Slack pings at creation."
20506
21692
  },
20507
- "required": [
20508
- "id",
20509
- "number",
20510
- "type",
20511
- "title",
20512
- "status",
20513
- "priority",
20514
- "source",
20515
- "owner",
20516
- "run",
20517
- "lastRun",
20518
- "createdAt",
20519
- "updatedAt"
20520
- ],
20521
- "additionalProperties": {},
20522
- "description": "One queue item — any type — plus its resolved `owner` and run state."
21693
+ "tags": {
21694
+ "type": "array",
21695
+ "items": {
21696
+ "type": "string"
21697
+ },
21698
+ "default": [],
21699
+ "description": "Tag names to attach at creation (get-or-created by name, deduped case-insensitively)."
21700
+ },
21701
+ "reporterEmail": {
21702
+ "anyOf": [
21703
+ {
21704
+ "type": "string",
21705
+ "format": "email"
21706
+ },
21707
+ {
21708
+ "type": "null"
21709
+ }
21710
+ ],
21711
+ "description": "Email of the reporter, or `null`."
21712
+ },
21713
+ "pageUrl": {
21714
+ "anyOf": [
21715
+ {
21716
+ "type": "string",
21717
+ "format": "uri"
21718
+ },
21719
+ {
21720
+ "type": "null"
21721
+ }
21722
+ ],
21723
+ "description": "URL of the page the request relates to, or `null`."
21724
+ },
21725
+ "userAgent": {
21726
+ "anyOf": [
21727
+ {
21728
+ "type": "string",
21729
+ "maxLength": 500
21730
+ },
21731
+ {
21732
+ "type": "null"
21733
+ }
21734
+ ],
21735
+ "description": "Reporter's user-agent string, or `null`."
21736
+ },
21737
+ "context": {
21738
+ "anyOf": [
21739
+ {
21740
+ "type": "object",
21741
+ "additionalProperties": {}
21742
+ },
21743
+ {
21744
+ "type": "null"
21745
+ }
21746
+ ],
21747
+ "description": "Arbitrary capture context, or `null`."
21748
+ },
21749
+ "notify": {
21750
+ "anyOf": [
21751
+ {
21752
+ "$ref": "#/components/schemas/NotificationTarget"
21753
+ },
21754
+ {
21755
+ "type": "null"
21756
+ }
21757
+ ],
21758
+ "description": "Where this request's completion notification lands."
21759
+ }
20523
21760
  },
20524
- "description": "A page of queue items, newest first."
21761
+ "required": [
21762
+ "type",
21763
+ "title"
21764
+ ],
21765
+ "additionalProperties": false,
21766
+ "description": "Feature-request-kind fields for `POST /api/admin/ops` (sent with `type: feature_request`)."
20525
21767
  },
20526
- "CreateBugRequest": {
21768
+ "CreateOpsItemRequest": {
21769
+ "oneOf": [
21770
+ {
21771
+ "$ref": "#/components/schemas/CreateBugRequest"
21772
+ },
21773
+ {
21774
+ "$ref": "#/components/schemas/CreateFeatureRequestRequest"
21775
+ }
21776
+ ],
21777
+ "discriminator": {
21778
+ "propertyName": "type",
21779
+ "mapping": {
21780
+ "bug": "#/components/schemas/CreateBugRequest",
21781
+ "feature_request": "#/components/schemas/CreateFeatureRequestRequest"
21782
+ }
21783
+ },
21784
+ "description": "Body for `POST /api/admin/ops`. A discriminated union on `type`: `bug` carries the bug fields, `feature_request` the feature fields. Only these two user-fileable types are accepted — `error`, `alert`, and `measure_plan` tickets are auto-filed by the platform and cannot be created over the API."
21785
+ },
21786
+ "CreateOpsItemResponse": {
20527
21787
  "type": "object",
20528
21788
  "properties": {
20529
- "type": {
21789
+ "id": {
20530
21790
  "type": "string",
20531
- "enum": [
20532
- "bug"
20533
- ],
20534
- "description": "Discriminator — files a bug."
21791
+ "description": "Newly created item id."
20535
21792
  },
21793
+ "number": {
21794
+ "description": "Per-project item number assigned to the new item.",
21795
+ "anyOf": [
21796
+ {
21797
+ "type": "number"
21798
+ },
21799
+ {
21800
+ "type": "null"
21801
+ }
21802
+ ]
21803
+ }
21804
+ },
21805
+ "required": [
21806
+ "id"
21807
+ ],
21808
+ "additionalProperties": false,
21809
+ "description": "Response for `POST /api/admin/ops`."
21810
+ },
21811
+ "CreatePublicBugRequest": {
21812
+ "type": "object",
21813
+ "properties": {
20536
21814
  "title": {
20537
21815
  "type": "string",
20538
21816
  "minLength": 1,
@@ -20678,22 +21956,36 @@
20678
21956
  }
20679
21957
  },
20680
21958
  "required": [
20681
- "type",
20682
21959
  "title"
20683
21960
  ],
20684
21961
  "additionalProperties": false,
20685
- "description": "Bug-kind fields for `POST /api/admin/ops` (sent with `type: bug`)."
21962
+ "description": "Body for `POST /ops/bug`. The same bug fields as `CreateBugRequest`, minus the `type` discriminator — the path already says what is being filed."
20686
21963
  },
20687
- "CreateFeatureRequestRequest": {
21964
+ "CreatePublicTicketResponse": {
20688
21965
  "type": "object",
20689
21966
  "properties": {
20690
- "type": {
21967
+ "id": {
20691
21968
  "type": "string",
20692
- "enum": [
20693
- "feature_request"
20694
- ],
20695
- "description": "Discriminator — files a feature request."
21969
+ "description": "The new item's id. Absent on a deduped response — nothing was created, so there is no new id to report."
21970
+ },
21971
+ "number": {
21972
+ "type": "integer",
21973
+ "description": "Per-project item number. On a deduped response this is the number of the OPEN ticket already tracking the report, which is what a caller should show the user either way."
20696
21974
  },
21975
+ "deduped": {
21976
+ "type": "boolean",
21977
+ "description": "`true` when an open ticket already tracked this report and nothing was filed (HTTP 200). Absent on a fresh file (HTTP 201)."
21978
+ }
21979
+ },
21980
+ "required": [
21981
+ "number"
21982
+ ],
21983
+ "additionalProperties": false,
21984
+ "description": "Response for the public ticket intake. A fresh file returns `201` with `id` + `number`; a repeat of a report already tracked by an open ticket returns `200` with that ticket's `number` and `deduped: true`."
21985
+ },
21986
+ "CreatePublicFeatureRequestRequest": {
21987
+ "type": "object",
21988
+ "properties": {
20697
21989
  "title": {
20698
21990
  "type": "string",
20699
21991
  "minLength": 1,
@@ -20821,54 +22113,10 @@
20821
22113
  }
20822
22114
  },
20823
22115
  "required": [
20824
- "type",
20825
22116
  "title"
20826
22117
  ],
20827
22118
  "additionalProperties": false,
20828
- "description": "Feature-request-kind fields for `POST /api/admin/ops` (sent with `type: feature_request`)."
20829
- },
20830
- "CreateOpsItemRequest": {
20831
- "oneOf": [
20832
- {
20833
- "$ref": "#/components/schemas/CreateBugRequest"
20834
- },
20835
- {
20836
- "$ref": "#/components/schemas/CreateFeatureRequestRequest"
20837
- }
20838
- ],
20839
- "discriminator": {
20840
- "propertyName": "type",
20841
- "mapping": {
20842
- "bug": "#/components/schemas/CreateBugRequest",
20843
- "feature_request": "#/components/schemas/CreateFeatureRequestRequest"
20844
- }
20845
- },
20846
- "description": "Body for `POST /api/admin/ops`. A discriminated union on `type`: `bug` carries the bug fields, `feature_request` the feature fields. Only these two user-fileable types are accepted — `error`, `alert`, and `measure_plan` tickets are auto-filed by the platform and cannot be created over the API."
20847
- },
20848
- "CreateOpsItemResponse": {
20849
- "type": "object",
20850
- "properties": {
20851
- "id": {
20852
- "type": "string",
20853
- "description": "Newly created item id."
20854
- },
20855
- "number": {
20856
- "description": "Per-project item number assigned to the new item.",
20857
- "anyOf": [
20858
- {
20859
- "type": "number"
20860
- },
20861
- {
20862
- "type": "null"
20863
- }
20864
- ]
20865
- }
20866
- },
20867
- "required": [
20868
- "id"
20869
- ],
20870
- "additionalProperties": false,
20871
- "description": "Response for `POST /api/admin/ops`."
22119
+ "description": "Body for `POST /ops/feature-request`. The same feature-request fields as `CreateFeatureRequestRequest`, minus the `type` discriminator — the path already says what is being filed."
20872
22120
  },
20873
22121
  "OpsItemAttachment": {
20874
22122
  "type": "object",
@@ -25745,8 +26993,8 @@
25745
26993
  "name": {
25746
26994
  "type": "string",
25747
26995
  "minLength": 1,
25748
- "maxLength": 80,
25749
- "description": "Optional human label. Programmatic (API) mints that omit it get an auto-generated descriptive name; dashboard mints may leave it blank."
26996
+ "maxLength": 160,
26997
+ "description": "Optional human label. Programmatic (API) mints that omit it get an auto-generated descriptive name; dashboard mints may leave it blank. The cap is generous because provenance labels are composed, not typed — `shipeasy setup` mints keys named with the stack, package, date, and operator that produced them, which does not fit in a one-line-input budget."
25750
26998
  },
25751
26999
  "scopes": {
25752
27000
  "type": "array",
@@ -26093,6 +27341,62 @@
26093
27341
  }
26094
27342
  }
26095
27343
  },
27344
+ "PayloadTooLarge": {
27345
+ "description": "The request body exceeded the intake's fixed size cap (16 KB).",
27346
+ "content": {
27347
+ "application/json": {
27348
+ "schema": {
27349
+ "$ref": "#/components/schemas/Error"
27350
+ },
27351
+ "examples": {
27352
+ "BAD_REQUEST": {
27353
+ "summary": "Body too large",
27354
+ "value": {
27355
+ "error": "Body too large",
27356
+ "code": "BAD_REQUEST"
27357
+ }
27358
+ }
27359
+ }
27360
+ }
27361
+ }
27362
+ },
27363
+ "TooManyRequests": {
27364
+ "description": "The public intake is rate-limited per (project, client IP). Back off and retry;\nthe limit is a burst guard, not a quota, so a modest delay clears it.",
27365
+ "content": {
27366
+ "application/json": {
27367
+ "schema": {
27368
+ "$ref": "#/components/schemas/Error"
27369
+ },
27370
+ "examples": {
27371
+ "BAD_REQUEST": {
27372
+ "summary": "Rate limited",
27373
+ "value": {
27374
+ "error": "Too many requests",
27375
+ "code": "BAD_REQUEST"
27376
+ }
27377
+ }
27378
+ }
27379
+ }
27380
+ }
27381
+ },
27382
+ "ServiceUnavailable": {
27383
+ "description": "A backing store the endpoint needs is not bound in this environment. Transient\nand infrastructural — no stable `ErrorCode` is assigned, so `code` is omitted.",
27384
+ "content": {
27385
+ "application/json": {
27386
+ "schema": {
27387
+ "$ref": "#/components/schemas/Error"
27388
+ },
27389
+ "examples": {
27390
+ "UNAVAILABLE": {
27391
+ "summary": "Storage unbound",
27392
+ "value": {
27393
+ "error": "Unavailable"
27394
+ }
27395
+ }
27396
+ }
27397
+ }
27398
+ }
27399
+ },
26096
27400
  "BadGateway": {
26097
27401
  "description": "The request was well-formed and the resource exists, but a downstream provider\nthe endpoint delegates to (e.g. a connector's Slack/GitHub/cloud-agent dispatch)\nfailed. The failure is upstream of this API, not a client error. No stable\n`ErrorCode` is assigned to upstream failures, so `code` is omitted.",
26098
27402
  "content": {