@shipeasy/openapi 3.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipeasy/openapi",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Shipeasy admin OpenAPI 3.2 spec (hand-authored, single source of truth) + the generated TypeScript client, Zod schemas, and types. Consumed by @shipeasy/cli and @shipeasy/mcp.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -40,17 +40,19 @@
40
40
  "default": "./src/codegen.ts"
41
41
  },
42
42
  "./openapi.yaml": "./openapi.yaml",
43
- "./openapi.json": "./openapi.json"
43
+ "./openapi.json": "./openapi.json",
44
+ "./openapi-sdk.json": "./openapi-sdk.json"
44
45
  },
45
46
  "files": [
46
47
  "dist/",
47
48
  "openapi.yaml",
48
49
  "openapi.json",
50
+ "openapi-sdk.json",
49
51
  "spec/"
50
52
  ],
51
53
  "scripts": {
52
- "lint:spec": "redocly lint spec/openapi.yaml",
53
- "bundle": "redocly bundle spec/openapi.yaml -o openapi.yaml && redocly bundle spec/openapi.yaml -o openapi.json",
54
+ "lint:spec": "redocly lint spec/openapi.yaml && redocly lint spec/openapi-sdk.yaml",
55
+ "bundle": "redocly bundle spec/openapi.yaml -o openapi.yaml && redocly bundle spec/openapi.yaml -o openapi.json && redocly bundle spec/openapi-sdk.yaml -o openapi-sdk.json",
54
56
  "gen:sdk": "openapi-ts",
55
57
  "gen": "pnpm bundle && pnpm gen:sdk",
56
58
  "prebuild": "pnpm gen",
@@ -120,3 +120,42 @@ BadGateway:
120
120
  summary: Downstream provider dispatch failed
121
121
  value:
122
122
  error: "GitHub API responded 403: resource not accessible by integration."
123
+ PayloadTooLarge:
124
+ description: The request body exceeded the intake's fixed size cap (16 KB).
125
+ content:
126
+ application/json:
127
+ schema:
128
+ $ref: ./errors.yaml#/Error
129
+ examples:
130
+ BAD_REQUEST:
131
+ summary: Body too large
132
+ value:
133
+ error: Body too large
134
+ code: BAD_REQUEST
135
+ TooManyRequests:
136
+ description: |-
137
+ The public intake is rate-limited per (project, client IP). Back off and retry;
138
+ the limit is a burst guard, not a quota, so a modest delay clears it.
139
+ content:
140
+ application/json:
141
+ schema:
142
+ $ref: ./errors.yaml#/Error
143
+ examples:
144
+ BAD_REQUEST:
145
+ summary: Rate limited
146
+ value:
147
+ error: Too many requests
148
+ code: BAD_REQUEST
149
+ ServiceUnavailable:
150
+ description: |-
151
+ A backing store the endpoint needs is not bound in this environment. Transient
152
+ and infrastructural — no stable `ErrorCode` is assigned, so `code` is omitted.
153
+ content:
154
+ application/json:
155
+ schema:
156
+ $ref: ./errors.yaml#/Error
157
+ examples:
158
+ UNAVAILABLE:
159
+ summary: Storage unbound
160
+ value:
161
+ error: Unavailable
@@ -980,3 +980,90 @@ ListGateActivityResponse:
980
980
  - createdAt
981
981
  additionalProperties: false
982
982
  description: Recent audit rows for one gate, newest first.
983
+ GateWhitelistAttr:
984
+ type: string
985
+ enum:
986
+ - email
987
+ - user_id
988
+ 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.
989
+ GateWhitelist:
990
+ type: object
991
+ properties:
992
+ id:
993
+ type: string
994
+ description: Resolved gate id.
995
+ name:
996
+ type: string
997
+ description: The gate's stable `name`.
998
+ attr:
999
+ $ref: "#/GateWhitelistAttr"
1000
+ entries:
1001
+ type: array
1002
+ items:
1003
+ type: string
1004
+ minLength: 1
1005
+ description: The whitelisted identities, in the order they are stored. Deduplicated case-sensitively; an empty array means the gate has no whitelist.
1006
+ required:
1007
+ - id
1008
+ - name
1009
+ - attr
1010
+ - entries
1011
+ additionalProperties: false
1012
+ 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.
1013
+ SetGateWhitelistRequest:
1014
+ type: object
1015
+ properties:
1016
+ attr:
1017
+ anyOf:
1018
+ - $ref: "#/GateWhitelistAttr"
1019
+ description: Identity attribute to match on. Defaults to the whitelist's current attribute, or `email` when the gate has no whitelist yet.
1020
+ entries:
1021
+ type: array
1022
+ items:
1023
+ type: string
1024
+ minLength: 1
1025
+ description: The complete whitelist after the call. Pass `[]` to remove the whitelist from the gate entirely.
1026
+ required:
1027
+ - entries
1028
+ additionalProperties: false
1029
+ 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.
1030
+ AddToGateWhitelistRequest:
1031
+ type: object
1032
+ properties:
1033
+ attr:
1034
+ anyOf:
1035
+ - $ref: "#/GateWhitelistAttr"
1036
+ 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.
1037
+ entries:
1038
+ type: array
1039
+ minItems: 1
1040
+ items:
1041
+ type: string
1042
+ minLength: 1
1043
+ description: Identities to admit. Already-listed entries are skipped, so the call is idempotent.
1044
+ required:
1045
+ - entries
1046
+ additionalProperties: false
1047
+ description: Body for `POST /api/admin/gates/{id}/whitelist`. Adds entries to the whitelist, creating it if the gate doesn't have one.
1048
+ RemoveFromGateWhitelistRequest:
1049
+ type: object
1050
+ properties:
1051
+ entries:
1052
+ type: array
1053
+ minItems: 1
1054
+ items:
1055
+ type: string
1056
+ minLength: 1
1057
+ description: Identities to stop admitting. Entries that aren't listed are skipped, so the call is idempotent.
1058
+ required:
1059
+ - entries
1060
+ additionalProperties: false
1061
+ 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."
1062
+ GetGateWhitelistResponse:
1063
+ $ref: "#/GateWhitelist"
1064
+ SetGateWhitelistResponse:
1065
+ $ref: "#/GateWhitelist"
1066
+ AddToGateWhitelistResponse:
1067
+ $ref: "#/GateWhitelist"
1068
+ RemoveFromGateWhitelistResponse:
1069
+ $ref: "#/GateWhitelist"
@@ -102,8 +102,8 @@ CreateKeyRequest:
102
102
  name:
103
103
  type: string
104
104
  minLength: 1
105
- maxLength: 80
106
- description: Optional human label. Programmatic (API) mints that omit it get an auto-generated descriptive name; dashboard mints may leave it blank.
105
+ maxLength: 160
106
+ 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."
107
107
  scopes:
108
108
  type: array
109
109
  items:
@@ -343,3 +343,67 @@ SetKillswitchValueResponse:
343
343
  - version
344
344
  - published
345
345
  additionalProperties: false
346
+ ToggleKillswitchRequest:
347
+ type: object
348
+ properties:
349
+ switchKey:
350
+ anyOf:
351
+ - type: string
352
+ maxLength: 64
353
+ pattern: ^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$
354
+ - type: "null"
355
+ 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.
356
+ value:
357
+ anyOf:
358
+ - type: boolean
359
+ - type: "null"
360
+ 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.
361
+ env:
362
+ anyOf:
363
+ - $ref: ./common.yaml#/Env
364
+ description: Environment to publish on. Defaults to `prod` — the environment an incident response means when it says "kill it".
365
+ additionalProperties: false
366
+ description: |-
367
+ 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:
368
+
369
+ - `{}` — flip the flat value on prod.
370
+ - `{ "switchKey": "eu_region" }` — flip that sub-switch on prod.
371
+ - `{ "switchKey": "eu_region", "value": true }` — set that sub-switch on prod, idempotently.
372
+ - `{ "switchKey": "eu_region", "value": true, "env": "staging" }` — the same, on a chosen env.
373
+ ToggleKillswitchResponse:
374
+ type: object
375
+ properties:
376
+ id:
377
+ type: string
378
+ description: Resolved killswitch id.
379
+ env:
380
+ $ref: ./common.yaml#/Env
381
+ switchKey:
382
+ anyOf:
383
+ - type: string
384
+ maxLength: 64
385
+ pattern: ^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$
386
+ - type: "null"
387
+ description: The sub-switch that was flipped, or `null` when the flat `value` was.
388
+ previous:
389
+ type: boolean
390
+ description: The value that was in effect before this call. Equal to `value` when an idempotent `value` matched what was already stored.
391
+ value:
392
+ type: boolean
393
+ description: The value now in effect on `env`.
394
+ version:
395
+ type: integer
396
+ minimum: -9007199254740991
397
+ maximum: 9007199254740991
398
+ description: Newly published version on `env`.
399
+ published:
400
+ $ref: ./common.yaml#/KillswitchValue
401
+ required:
402
+ - id
403
+ - env
404
+ - switchKey
405
+ - previous
406
+ - value
407
+ - version
408
+ - published
409
+ additionalProperties: false
@@ -1063,6 +1063,175 @@ CreateFeatureRequestRequest:
1063
1063
  - title
1064
1064
  additionalProperties: false
1065
1065
  description: "Feature-request-kind fields for `POST /api/admin/ops` (sent with `type: feature_request`)."
1066
+ CreatePublicBugRequest:
1067
+ type: object
1068
+ properties:
1069
+ title:
1070
+ type: string
1071
+ minLength: 1
1072
+ maxLength: 200
1073
+ pattern: '^\S(.*\S)?$'
1074
+ description: One-line bug title (no leading/trailing whitespace).
1075
+ stepsToReproduce:
1076
+ type: string
1077
+ maxLength: 8000
1078
+ default: ""
1079
+ description: How to reproduce the bug.
1080
+ actualResult:
1081
+ type: string
1082
+ maxLength: 8000
1083
+ default: ""
1084
+ description: What actually happened.
1085
+ expectedResult:
1086
+ type: string
1087
+ maxLength: 8000
1088
+ default: ""
1089
+ description: What was expected instead.
1090
+ priority:
1091
+ anyOf:
1092
+ - $ref: "#/OpsItemPriority"
1093
+ - type: "null"
1094
+ description: Initial triage priority, or `null`.
1095
+ status:
1096
+ anyOf:
1097
+ - $ref: "#/OpsItemStatus"
1098
+ description: Initial lifecycle status; defaults to `open` when omitted.
1099
+ assigneeId:
1100
+ anyOf:
1101
+ - type: string
1102
+ - type: "null"
1103
+ description: The `users.id` of the person to assign as owner at creation, or `null`.
1104
+ subscribers:
1105
+ type: array
1106
+ items:
1107
+ type: string
1108
+ format: email
1109
+ default: []
1110
+ description: Emails of teammates to subscribe to this item's Slack pings at creation.
1111
+ tags:
1112
+ type: array
1113
+ items:
1114
+ type: string
1115
+ default: []
1116
+ description: Tag names to attach at creation (get-or-created by name, deduped case-insensitively).
1117
+ reporterEmail:
1118
+ anyOf:
1119
+ - type: string
1120
+ format: email
1121
+ - type: "null"
1122
+ description: Email of the reporter, or `null`.
1123
+ pageUrl:
1124
+ anyOf:
1125
+ - type: string
1126
+ format: uri
1127
+ - type: "null"
1128
+ description: URL of the page the bug relates to, or `null`.
1129
+ userAgent:
1130
+ anyOf:
1131
+ - type: string
1132
+ maxLength: 500
1133
+ - type: "null"
1134
+ description: Reporter's user-agent string, or `null`.
1135
+ viewport:
1136
+ anyOf:
1137
+ - type: string
1138
+ maxLength: 40
1139
+ - type: "null"
1140
+ description: Reporter's viewport (e.g. `1280x720`), or `null`.
1141
+ context:
1142
+ anyOf:
1143
+ - type: object
1144
+ additionalProperties: {}
1145
+ - type: "null"
1146
+ description: Arbitrary capture context, or `null`.
1147
+ notify:
1148
+ anyOf:
1149
+ - $ref: ./common.yaml#/NotificationTarget
1150
+ - type: "null"
1151
+ description: Where this bug's completion notification lands.
1152
+ required:
1153
+ - title
1154
+ additionalProperties: false
1155
+ description: "Body for `POST /ops/bug`. The same bug fields as `CreateBugRequest`, minus the `type` discriminator — the path already says what is being filed."
1156
+ CreatePublicFeatureRequestRequest:
1157
+ type: object
1158
+ properties:
1159
+ title:
1160
+ type: string
1161
+ minLength: 1
1162
+ maxLength: 200
1163
+ pattern: '^\S(.*\S)?$'
1164
+ description: One-line feature-request title (no leading/trailing whitespace).
1165
+ description:
1166
+ type: string
1167
+ maxLength: 8000
1168
+ default: ""
1169
+ description: What the feature is.
1170
+ useCase:
1171
+ type: string
1172
+ maxLength: 8000
1173
+ default: ""
1174
+ description: Why it's needed / the use case.
1175
+ priority:
1176
+ anyOf:
1177
+ - $ref: "#/OpsItemPriority"
1178
+ - type: "null"
1179
+ description: Initial triage priority, or `null`.
1180
+ status:
1181
+ anyOf:
1182
+ - $ref: "#/OpsItemStatus"
1183
+ description: Initial lifecycle status; defaults to `open` when omitted.
1184
+ assigneeId:
1185
+ anyOf:
1186
+ - type: string
1187
+ - type: "null"
1188
+ description: The `users.id` of the person to assign as owner at creation, or `null`.
1189
+ subscribers:
1190
+ type: array
1191
+ items:
1192
+ type: string
1193
+ format: email
1194
+ default: []
1195
+ description: Emails of teammates to subscribe to this item's Slack pings at creation.
1196
+ tags:
1197
+ type: array
1198
+ items:
1199
+ type: string
1200
+ default: []
1201
+ description: Tag names to attach at creation (get-or-created by name, deduped case-insensitively).
1202
+ reporterEmail:
1203
+ anyOf:
1204
+ - type: string
1205
+ format: email
1206
+ - type: "null"
1207
+ description: Email of the reporter, or `null`.
1208
+ pageUrl:
1209
+ anyOf:
1210
+ - type: string
1211
+ format: uri
1212
+ - type: "null"
1213
+ description: URL of the page the request relates to, or `null`.
1214
+ userAgent:
1215
+ anyOf:
1216
+ - type: string
1217
+ maxLength: 500
1218
+ - type: "null"
1219
+ description: Reporter's user-agent string, or `null`.
1220
+ context:
1221
+ anyOf:
1222
+ - type: object
1223
+ additionalProperties: {}
1224
+ - type: "null"
1225
+ description: Arbitrary capture context, or `null`.
1226
+ notify:
1227
+ anyOf:
1228
+ - $ref: ./common.yaml#/NotificationTarget
1229
+ - type: "null"
1230
+ description: Where this request's completion notification lands.
1231
+ required:
1232
+ - title
1233
+ additionalProperties: false
1234
+ 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."
1066
1235
  CreateOpsItemRequest:
1067
1236
  oneOf:
1068
1237
  - $ref: "#/CreateBugRequest"
@@ -1073,6 +1242,22 @@ CreateOpsItemRequest:
1073
1242
  bug: "#/CreateBugRequest"
1074
1243
  feature_request: "#/CreateFeatureRequestRequest"
1075
1244
  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."
1245
+ CreatePublicTicketResponse:
1246
+ type: object
1247
+ properties:
1248
+ id:
1249
+ type: string
1250
+ description: The new item's id. Absent on a deduped response — nothing was created, so there is no new id to report.
1251
+ number:
1252
+ type: integer
1253
+ 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.
1254
+ deduped:
1255
+ type: boolean
1256
+ description: "`true` when an open ticket already tracked this report and nothing was filed (HTTP 200). Absent on a fresh file (HTTP 201)."
1257
+ required:
1258
+ - number
1259
+ additionalProperties: false
1260
+ 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`."
1076
1261
  CreateOpsItemResponse:
1077
1262
  type: object
1078
1263
  properties:
@@ -0,0 +1,86 @@
1
+ # The SERVER-SDK surface of the admin API — a second root over the same
2
+ # `paths/` and `components/schemas/` tree that `openapi.yaml` bundles.
3
+ #
4
+ # WHY A SECOND ROOT. The CLI and MCP consume the FULL spec (127 operations);
5
+ # the published server SDKs ship a deliberately tiny opt-in `AdminClient`, and
6
+ # vendoring the whole contract into eight repos bloated each generated client
7
+ # into the megabytes for operations nobody called. This file IS that smaller
8
+ # contract, declared rather than derived: `paths` below lists exactly the
9
+ # operations the SDKs expose, and `redocly bundle` pulls in only the schemas
10
+ # those operations transitively reference.
11
+ #
12
+ # WHY IT'S EXACT. Every path listed here is a dedicated, single-purpose route,
13
+ # so a path item never drags in a method the SDK shouldn't have. That is the
14
+ # reason the SDK-facing endpoints were built as their own paths instead of
15
+ # extra query/discriminator modes on the generic CRUD routes.
16
+ #
17
+ # ADDING AN OPERATION is one `$ref` here — then `pnpm gen` in this package and
18
+ # `pnpm sdk:spec:regen` in the monorepo, and commit the regenerated client
19
+ # inside each SDK submodule. A monorepo pre-commit hook blocks any commit that
20
+ # changes this contract while a vendored copy is stale.
21
+ #
22
+ # DELIBERATELY ABSENT, so nobody re-adds them by accident: everything else.
23
+ # Listing, generic CRUD, key minting, projects, connectors, i18n, errors,
24
+ # alerts, metrics, events, experiments, universes, templates, attributes, and
25
+ # the AI-agent working seam (ack / link-pr / investigations) all stay reachable
26
+ # through the CLI and MCP, which read `openapi.yaml`.
27
+ openapi: 3.2.0
28
+ info:
29
+ title: Shipeasy Admin API (server-SDK surface)
30
+ version: 2.0.0
31
+ description: |-
32
+ The slice of the Shipeasy admin API that the published server SDKs expose as their optional `AdminClient`. Authenticate with an admin SDK key (`Authorization: Bearer sdk_admin_…`) and scope every request to a project via the `X-Project-Id` header.
33
+
34
+ Three capabilities, nothing else:
35
+
36
+ - **File a public ticket** — a bug or a feature request onto the project's ops queue.
37
+ - **Toggle a kill switch** — flip the switch itself, or one of its named sub-switches, on one environment.
38
+ - **Manage a flag's whitelist** — read, replace, add to, or remove from the allowlist that admits specific identities ahead of every targeting rule.
39
+
40
+ Everything else in the admin API is reachable through the Shipeasy CLI and MCP server, which speak the complete contract (`openapi.yaml`).
41
+ contact:
42
+ name: Shipeasy
43
+ url: https://shipeasy.ai
44
+ license:
45
+ name: Proprietary
46
+ identifier: LicenseRef-Proprietary
47
+ servers:
48
+ - url: https://shipeasy.ai
49
+ description: Production
50
+ - url: http://localhost:3000
51
+ description: Local Next.js dev server
52
+ security:
53
+ - bearerSdkKey: []
54
+ tags:
55
+ - name: Flags
56
+ description: A feature gate's whitelist — the always-first allowlist that admits named identities before any targeting rule or percentage rollout runs.
57
+ - name: Killswitch
58
+ description: Per-env boolean overrides for incident response. `toggle` reads the current value and publishes its opposite in one call, on the flat value or on one named sub-switch.
59
+ - name: Ops
60
+ description: The public ticket queue — filing a bug or a feature request, which also fires the project's GitHub/Slack connectors.
61
+ paths:
62
+ /ops/bug:
63
+ $ref: ./paths/ops.yaml#/~1ops~1bug
64
+ /ops/feature-request:
65
+ $ref: ./paths/ops.yaml#/~1ops~1feature-request
66
+ /api/admin/killswitches/{id}/toggle:
67
+ $ref: ./paths/killswitches.yaml#/~1api~1admin~1killswitches~1{id}~1toggle
68
+ /api/admin/gates/{id}/whitelist:
69
+ $ref: ./paths/gates.yaml#/~1api~1admin~1gates~1{id}~1whitelist
70
+ components:
71
+ securitySchemes:
72
+ bearerSdkKey:
73
+ type: http
74
+ scheme: bearer
75
+ bearerFormat: sdk_admin_*
76
+ description: 'Pass an admin SDK key as `Authorization: Bearer sdk_admin_…`. Mint via `POST /api/admin/keys` with `type: "admin"`.'
77
+ clientSdkKey:
78
+ type: apiKey
79
+ in: header
80
+ name: X-SDK-Key
81
+ description: |-
82
+ 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.
83
+
84
+ Client 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.
85
+
86
+ The key must additionally carry the `tickets:public_create` scope, and its project must have public ticket creation enabled.
package/spec/openapi.yaml CHANGED
@@ -282,6 +282,8 @@ paths:
282
282
  $ref: ./paths/gates.yaml#/~1api~1admin~1gates~1{id}~1disable
283
283
  /api/admin/gates/{id}/activity:
284
284
  $ref: ./paths/gates.yaml#/~1api~1admin~1gates~1{id}~1activity
285
+ /api/admin/gates/{id}/whitelist:
286
+ $ref: ./paths/gates.yaml#/~1api~1admin~1gates~1{id}~1whitelist
285
287
  /api/admin/experiments:
286
288
  $ref: ./paths/experiments.yaml#/~1api~1admin~1experiments
287
289
  /api/admin/experiments/{id}:
@@ -322,6 +324,8 @@ paths:
322
324
  $ref: ./paths/killswitches.yaml#/~1api~1admin~1killswitches~1{id}~1switch
323
325
  /api/admin/killswitches/{id}/value:
324
326
  $ref: ./paths/killswitches.yaml#/~1api~1admin~1killswitches~1{id}~1value
327
+ /api/admin/killswitches/{id}/toggle:
328
+ $ref: ./paths/killswitches.yaml#/~1api~1admin~1killswitches~1{id}~1toggle
325
329
  /api/admin/universes:
326
330
  $ref: ./paths/universes.yaml#/~1api~1admin~1universes
327
331
  /api/admin/universes/{id}:
@@ -352,6 +356,10 @@ paths:
352
356
  $ref: ./paths/events.yaml#/~1api~1admin~1events~1{id}~1approve
353
357
  /api/admin/ops:
354
358
  $ref: ./paths/ops.yaml#/~1api~1admin~1ops
359
+ /ops/bug:
360
+ $ref: ./paths/ops.yaml#/~1ops~1bug
361
+ /ops/feature-request:
362
+ $ref: ./paths/ops.yaml#/~1ops~1feature-request
355
363
  /api/admin/ops/{handle}:
356
364
  $ref: ./paths/ops.yaml#/~1api~1admin~1ops~1{handle}
357
365
  /api/admin/ops/{handle}/link-pr:
@@ -437,3 +445,13 @@ components:
437
445
  scheme: bearer
438
446
  bearerFormat: sdk_admin_*
439
447
  description: 'Pass an admin SDK key as `Authorization: Bearer sdk_admin_…`. Mint via `POST /api/admin/keys` with `type: "admin"`.'
448
+ clientSdkKey:
449
+ type: apiKey
450
+ in: header
451
+ name: X-SDK-Key
452
+ description: |-
453
+ 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.
454
+
455
+ Client 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.
456
+
457
+ The key must additionally carry the `tickets:public_create` scope, and its project must have public ticket creation enabled.