@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/openapi.yaml CHANGED
@@ -877,6 +877,288 @@ paths:
877
877
  name: activity
878
878
  positional:
879
879
  - id
880
+ /api/admin/gates/{id}/whitelist:
881
+ get:
882
+ operationId: getGateWhitelist
883
+ summary: Read a gate's whitelist
884
+ description: |-
885
+ Returns the gate's whitelist — the always-first allowlist that admits the listed identities before any targeting rule or percentage rollout is evaluated.
886
+
887
+ A 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.
888
+
889
+ **Use case:** Check whether an account is already let through before adding it.
890
+ tags:
891
+ - Flags
892
+ parameters:
893
+ - $ref: '#/components/parameters/ProjectId'
894
+ - name: id
895
+ in: path
896
+ required: true
897
+ description: Stable opaque gate id (`gate_…`) or the gate's `name`.
898
+ schema:
899
+ $ref: '#/components/schemas/ResourceId'
900
+ responses:
901
+ '200':
902
+ description: Read a gate's whitelist
903
+ content:
904
+ application/json:
905
+ schema:
906
+ $ref: '#/components/schemas/GateWhitelist'
907
+ example:
908
+ id: gate_01j7w8a1b2c3d4e5f6g7h8i9j0
909
+ name: new_checkout
910
+ attr: email
911
+ entries:
912
+ - alice@acme.dev
913
+ - bob@acme.dev
914
+ '400':
915
+ $ref: '#/components/responses/BadRequest'
916
+ '401':
917
+ $ref: '#/components/responses/Unauthorized'
918
+ '403':
919
+ $ref: '#/components/responses/Forbidden'
920
+ '404':
921
+ $ref: '#/components/responses/NotFound'
922
+ '409':
923
+ $ref: '#/components/responses/Conflict'
924
+ '422':
925
+ $ref: '#/components/responses/UnprocessableEntity'
926
+ x-error-codes:
927
+ - BAD_REQUEST
928
+ - UNAUTHORIZED
929
+ - FORBIDDEN
930
+ - NOT_FOUND
931
+ x-cli:
932
+ name: whitelist
933
+ positional:
934
+ - id
935
+ put:
936
+ operationId: setGateWhitelist
937
+ summary: Replace a gate's whitelist
938
+ description: |-
939
+ 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.
940
+
941
+ This is the only whitelist call that can switch `attr` (`email` ⇄ `user_id`) or clear the block: `entries: []` removes the whitelist from the gate entirely.
942
+
943
+ **Use cases**
944
+
945
+ - **Pin an exact list** — `{ "entries": ["alice@acme.dev", "bob@acme.dev"] }`.
946
+ - **Switch to user ids** — `{ "attr": "user_id", "entries": ["usr_123"] }`.
947
+ - **Drop the whitelist** — `{ "entries": [] }`.
948
+ tags:
949
+ - Flags
950
+ parameters:
951
+ - $ref: '#/components/parameters/ProjectId'
952
+ - name: id
953
+ in: path
954
+ required: true
955
+ description: Stable opaque gate id (`gate_…`) or the gate's `name`.
956
+ schema:
957
+ $ref: '#/components/schemas/ResourceId'
958
+ requestBody:
959
+ required: true
960
+ content:
961
+ application/json:
962
+ schema:
963
+ $ref: '#/components/schemas/SetGateWhitelistRequest'
964
+ examples:
965
+ pinList:
966
+ summary: Pin an exact list of emails
967
+ value:
968
+ entries:
969
+ - alice@acme.dev
970
+ - bob@acme.dev
971
+ switchToUserIds:
972
+ summary: Re-key the whitelist onto user ids
973
+ description: Switching `attr` replaces the entries wholesale — the old emails are not translated.
974
+ value:
975
+ attr: user_id
976
+ entries:
977
+ - usr_01j7w8a1b2c3d4e5f6g7h8i9j0
978
+ clear:
979
+ summary: Remove the whitelist
980
+ description: An empty list drops the pinned block from the gate's stack.
981
+ value:
982
+ entries: []
983
+ responses:
984
+ '200':
985
+ description: Replace a gate's whitelist
986
+ content:
987
+ application/json:
988
+ schema:
989
+ $ref: '#/components/schemas/GateWhitelist'
990
+ example:
991
+ id: gate_01j7w8a1b2c3d4e5f6g7h8i9j0
992
+ name: new_checkout
993
+ attr: email
994
+ entries:
995
+ - alice@acme.dev
996
+ - bob@acme.dev
997
+ '400':
998
+ $ref: '#/components/responses/BadRequest'
999
+ '401':
1000
+ $ref: '#/components/responses/Unauthorized'
1001
+ '403':
1002
+ $ref: '#/components/responses/Forbidden'
1003
+ '404':
1004
+ $ref: '#/components/responses/NotFound'
1005
+ '409':
1006
+ $ref: '#/components/responses/Conflict'
1007
+ '422':
1008
+ $ref: '#/components/responses/UnprocessableEntity'
1009
+ x-error-codes:
1010
+ - BAD_REQUEST
1011
+ - UNAUTHORIZED
1012
+ - FORBIDDEN
1013
+ - NOT_FOUND
1014
+ - VALIDATION
1015
+ x-cli:
1016
+ name: whitelist-set
1017
+ positional:
1018
+ - id
1019
+ post:
1020
+ operationId: addToGateWhitelist
1021
+ summary: Add entries to a gate's whitelist
1022
+ description: |-
1023
+ 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.
1024
+
1025
+ Adding 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.
1026
+
1027
+ **Use case:** Let one more customer into a private beta without reading the current list first.
1028
+ tags:
1029
+ - Flags
1030
+ parameters:
1031
+ - $ref: '#/components/parameters/ProjectId'
1032
+ - name: id
1033
+ in: path
1034
+ required: true
1035
+ description: Stable opaque gate id (`gate_…`) or the gate's `name`.
1036
+ schema:
1037
+ $ref: '#/components/schemas/ResourceId'
1038
+ requestBody:
1039
+ required: true
1040
+ content:
1041
+ application/json:
1042
+ schema:
1043
+ $ref: '#/components/schemas/AddToGateWhitelistRequest'
1044
+ examples:
1045
+ addOne:
1046
+ summary: Admit one more email
1047
+ value:
1048
+ entries:
1049
+ - carol@acme.dev
1050
+ createOnUserIds:
1051
+ summary: Start a whitelist keyed on user ids
1052
+ description: '`attr` is honoured only because this gate has no whitelist yet.'
1053
+ value:
1054
+ attr: user_id
1055
+ entries:
1056
+ - usr_01j7w8a1b2c3d4e5f6g7h8i9j0
1057
+ responses:
1058
+ '200':
1059
+ description: Add entries to a gate's whitelist
1060
+ content:
1061
+ application/json:
1062
+ schema:
1063
+ $ref: '#/components/schemas/GateWhitelist'
1064
+ example:
1065
+ id: gate_01j7w8a1b2c3d4e5f6g7h8i9j0
1066
+ name: new_checkout
1067
+ attr: email
1068
+ entries:
1069
+ - alice@acme.dev
1070
+ - bob@acme.dev
1071
+ - carol@acme.dev
1072
+ '400':
1073
+ $ref: '#/components/responses/BadRequest'
1074
+ '401':
1075
+ $ref: '#/components/responses/Unauthorized'
1076
+ '403':
1077
+ $ref: '#/components/responses/Forbidden'
1078
+ '404':
1079
+ $ref: '#/components/responses/NotFound'
1080
+ '409':
1081
+ $ref: '#/components/responses/Conflict'
1082
+ '422':
1083
+ $ref: '#/components/responses/UnprocessableEntity'
1084
+ x-error-codes:
1085
+ - BAD_REQUEST
1086
+ - UNAUTHORIZED
1087
+ - FORBIDDEN
1088
+ - NOT_FOUND
1089
+ - IMMUTABLE_FIELD
1090
+ - VALIDATION
1091
+ x-cli:
1092
+ name: whitelist-add
1093
+ positional:
1094
+ - id
1095
+ delete:
1096
+ operationId: removeFromGateWhitelist
1097
+ summary: Remove entries from a gate's whitelist
1098
+ description: |-
1099
+ Removes identities from the gate's whitelist. Entries that aren't on the list are skipped, so the call is idempotent.
1100
+
1101
+ Removing the last entry leaves an empty whitelist block in place; to drop the block itself use `PUT` with `entries: []`.
1102
+
1103
+ **Use case:** Revoke one beta tester's access without touching anyone else's.
1104
+ tags:
1105
+ - Flags
1106
+ parameters:
1107
+ - $ref: '#/components/parameters/ProjectId'
1108
+ - name: id
1109
+ in: path
1110
+ required: true
1111
+ description: Stable opaque gate id (`gate_…`) or the gate's `name`.
1112
+ schema:
1113
+ $ref: '#/components/schemas/ResourceId'
1114
+ requestBody:
1115
+ required: true
1116
+ content:
1117
+ application/json:
1118
+ schema:
1119
+ $ref: '#/components/schemas/RemoveFromGateWhitelistRequest'
1120
+ examples:
1121
+ removeOne:
1122
+ summary: Revoke one email
1123
+ value:
1124
+ entries:
1125
+ - carol@acme.dev
1126
+ responses:
1127
+ '200':
1128
+ description: Remove entries from a gate's whitelist
1129
+ content:
1130
+ application/json:
1131
+ schema:
1132
+ $ref: '#/components/schemas/GateWhitelist'
1133
+ example:
1134
+ id: gate_01j7w8a1b2c3d4e5f6g7h8i9j0
1135
+ name: new_checkout
1136
+ attr: email
1137
+ entries:
1138
+ - alice@acme.dev
1139
+ - bob@acme.dev
1140
+ '400':
1141
+ $ref: '#/components/responses/BadRequest'
1142
+ '401':
1143
+ $ref: '#/components/responses/Unauthorized'
1144
+ '403':
1145
+ $ref: '#/components/responses/Forbidden'
1146
+ '404':
1147
+ $ref: '#/components/responses/NotFound'
1148
+ '409':
1149
+ $ref: '#/components/responses/Conflict'
1150
+ '422':
1151
+ $ref: '#/components/responses/UnprocessableEntity'
1152
+ x-error-codes:
1153
+ - BAD_REQUEST
1154
+ - UNAUTHORIZED
1155
+ - FORBIDDEN
1156
+ - NOT_FOUND
1157
+ - VALIDATION
1158
+ x-cli:
1159
+ name: whitelist-remove
1160
+ positional:
1161
+ - id
880
1162
  /api/admin/experiments:
881
1163
  get:
882
1164
  operationId: listExperiments
@@ -3343,6 +3625,102 @@ paths:
3343
3625
  name: set-value
3344
3626
  positional:
3345
3627
  - id
3628
+ /api/admin/killswitches/{id}/toggle:
3629
+ post:
3630
+ operationId: toggleKillswitch
3631
+ summary: Toggle a killswitch or one of its switches
3632
+ description: |-
3633
+ 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.
3634
+
3635
+ Every body field is optional, which is what makes the call widen cleanly:
3636
+
3637
+ - **Flip the killswitch** — `{}`. Flips the flat `value` on `prod`.
3638
+ - **Flip one sub-switch** — `{ "switchKey": "eu_region" }`. Flips that entry on `prod`, creating it (from `false`) if it isn't in the map yet.
3639
+ - **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".
3640
+ - **Choose the environment** — add `"env": "staging"`. Omitted, `env` is `prod`.
3641
+
3642
+ The response reports both `previous` and `value`, so a caller that asked for a flip can see what it actually changed.
3643
+
3644
+ Prefer this over `PUT /{id}/value` and `PUT /{id}/switch` unless you specifically need those endpoints' unconditional set semantics.
3645
+ tags:
3646
+ - Killswitch
3647
+ parameters:
3648
+ - $ref: '#/components/parameters/ProjectId'
3649
+ - name: id
3650
+ in: path
3651
+ required: true
3652
+ description: Stable opaque killswitch id (`ksw_…`) or the killswitch's `name`.
3653
+ schema:
3654
+ $ref: '#/components/schemas/ResourceId'
3655
+ requestBody:
3656
+ required: false
3657
+ content:
3658
+ application/json:
3659
+ schema:
3660
+ $ref: '#/components/schemas/ToggleKillswitchRequest'
3661
+ examples:
3662
+ flipOnProd:
3663
+ summary: Flip the killswitch on prod
3664
+ description: No body fields — reads the flat `value` on prod and publishes its opposite.
3665
+ value: {}
3666
+ flipOneSwitch:
3667
+ summary: Flip one sub-switch on prod
3668
+ description: Flips `switches.eu_region` on prod, leaving the flat `value` and every other key alone.
3669
+ value:
3670
+ switchKey: eu_region
3671
+ setOneSwitch:
3672
+ summary: Trip one sub-switch idempotently
3673
+ description: Publishes `switches.eu_region = true` on prod whatever it was before — safe to retry.
3674
+ value:
3675
+ switchKey: eu_region
3676
+ value: true
3677
+ setOneSwitchOnStaging:
3678
+ summary: Trip one sub-switch on a chosen env
3679
+ description: The same idempotent set, published on staging instead of prod.
3680
+ value:
3681
+ switchKey: eu_region
3682
+ value: true
3683
+ env: staging
3684
+ responses:
3685
+ '200':
3686
+ description: Toggle a killswitch or one of its switches
3687
+ content:
3688
+ application/json:
3689
+ schema:
3690
+ $ref: '#/components/schemas/ToggleKillswitchResponse'
3691
+ example:
3692
+ id: ksw_01j7w9d8h2k4m6n8p0q2r4s6t8
3693
+ env: prod
3694
+ switchKey: eu_region
3695
+ previous: false
3696
+ value: true
3697
+ version: 7
3698
+ published:
3699
+ value: false
3700
+ switches:
3701
+ eu_region: true
3702
+ '400':
3703
+ $ref: '#/components/responses/BadRequest'
3704
+ '401':
3705
+ $ref: '#/components/responses/Unauthorized'
3706
+ '403':
3707
+ $ref: '#/components/responses/Forbidden'
3708
+ '404':
3709
+ $ref: '#/components/responses/NotFound'
3710
+ '409':
3711
+ $ref: '#/components/responses/Conflict'
3712
+ '422':
3713
+ $ref: '#/components/responses/UnprocessableEntity'
3714
+ x-error-codes:
3715
+ - BAD_REQUEST
3716
+ - UNAUTHORIZED
3717
+ - FORBIDDEN
3718
+ - NOT_FOUND
3719
+ - VALIDATION
3720
+ x-cli:
3721
+ name: toggle
3722
+ positional:
3723
+ - id
3346
3724
  /api/admin/universes:
3347
3725
  get:
3348
3726
  operationId: listUniverses
@@ -5222,6 +5600,171 @@ paths:
5222
5600
  summary: File a feature request.
5223
5601
  preset:
5224
5602
  type: feature_request
5603
+ /ops/bug:
5604
+ post:
5605
+ operationId: createPublicBug
5606
+ summary: File a bug
5607
+ description: |-
5608
+ 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.
5609
+
5610
+ Three gates decide whether a ticket is filed, and nothing else the caller sends can widen them:
5611
+
5612
+ 1. the key is a `client` key carrying the `tickets:public_create` scope,
5613
+ 2. the key's project has public ticket creation enabled, and
5614
+ 3. the item is filed as `pending_approval` — parked out of the work queue until a human promotes it in the dashboard.
5615
+
5616
+ The 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.
5617
+
5618
+ This endpoint is served by the Shipeasy **edge worker** (`api.shipeasy.ai`), not the admin API — see `servers` below.
5619
+
5620
+ **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": "…" }`.
5621
+ tags:
5622
+ - Ops
5623
+ servers:
5624
+ - url: https://api.shipeasy.ai
5625
+ description: Shipeasy edge worker (production)
5626
+ - url: http://localhost:8787
5627
+ description: Local `wrangler dev`
5628
+ security:
5629
+ - clientSdkKey: []
5630
+ requestBody:
5631
+ required: true
5632
+ content:
5633
+ application/json:
5634
+ schema:
5635
+ $ref: '#/components/schemas/CreatePublicBugRequest'
5636
+ examples:
5637
+ minimal:
5638
+ summary: Just a title
5639
+ description: Every other field is optional — the queue triages from `open` at default priority.
5640
+ value:
5641
+ title: Checkout button misaligned on mobile
5642
+ full:
5643
+ summary: A full report
5644
+ value:
5645
+ title: Checkout button misaligned on mobile
5646
+ stepsToReproduce: Open the cart on iOS Safari and tap the price row.
5647
+ actualResult: The primary CTA overlaps the price.
5648
+ expectedResult: The CTA sits below the price with clear spacing.
5649
+ priority: high
5650
+ reporterEmail: alice@acme.dev
5651
+ pageUrl: https://acme.dev/cart
5652
+ responses:
5653
+ '200':
5654
+ description: An open ticket already tracks this report; nothing was filed
5655
+ content:
5656
+ application/json:
5657
+ schema:
5658
+ $ref: '#/components/schemas/CreatePublicTicketResponse'
5659
+ example:
5660
+ number: 7
5661
+ deduped: true
5662
+ '201':
5663
+ description: File a bug
5664
+ content:
5665
+ application/json:
5666
+ schema:
5667
+ $ref: '#/components/schemas/CreatePublicTicketResponse'
5668
+ example:
5669
+ id: fb_01j7w8a1b2c3d4e5f6g7h8i9j0
5670
+ number: 7
5671
+ '400':
5672
+ $ref: '#/components/responses/BadRequest'
5673
+ '401':
5674
+ $ref: '#/components/responses/Unauthorized'
5675
+ '403':
5676
+ $ref: '#/components/responses/Forbidden'
5677
+ '413':
5678
+ $ref: '#/components/responses/PayloadTooLarge'
5679
+ '429':
5680
+ $ref: '#/components/responses/TooManyRequests'
5681
+ '503':
5682
+ $ref: '#/components/responses/ServiceUnavailable'
5683
+ x-error-codes:
5684
+ - BAD_REQUEST
5685
+ - UNAUTHORIZED
5686
+ - FORBIDDEN
5687
+ - VALIDATION
5688
+ x-cli:
5689
+ hidden: true
5690
+ /ops/feature-request:
5691
+ post:
5692
+ operationId: createPublicFeatureRequest
5693
+ summary: File a feature request
5694
+ description: |-
5695
+ 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.
5696
+
5697
+ The 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.
5698
+
5699
+ This endpoint is served by the Shipeasy **edge worker** (`api.shipeasy.ai`), not the admin API — see `servers` below.
5700
+
5701
+ **Use case:** An in-product "request a feature" form posts what the user asked for — `{ "title": "Dark mode", "useCase": "Reduce eye strain at night" }`.
5702
+ tags:
5703
+ - Ops
5704
+ servers:
5705
+ - url: https://api.shipeasy.ai
5706
+ description: Shipeasy edge worker (production)
5707
+ - url: http://localhost:8787
5708
+ description: Local `wrangler dev`
5709
+ security:
5710
+ - clientSdkKey: []
5711
+ requestBody:
5712
+ required: true
5713
+ content:
5714
+ application/json:
5715
+ schema:
5716
+ $ref: '#/components/schemas/CreatePublicFeatureRequestRequest'
5717
+ examples:
5718
+ minimal:
5719
+ summary: Just a title
5720
+ value:
5721
+ title: Dark mode for the dashboard
5722
+ full:
5723
+ summary: A full request
5724
+ value:
5725
+ title: Dark mode for the dashboard
5726
+ description: Add a theme toggle that persists per user.
5727
+ useCase: Reduce eye strain for users working at night.
5728
+ priority: nice_to_have
5729
+ reporterEmail: alice@acme.dev
5730
+ responses:
5731
+ '200':
5732
+ description: An open ticket already tracks this report; nothing was filed
5733
+ content:
5734
+ application/json:
5735
+ schema:
5736
+ $ref: '#/components/schemas/CreatePublicTicketResponse'
5737
+ example:
5738
+ number: 7
5739
+ deduped: true
5740
+ '201':
5741
+ description: File a feature request
5742
+ content:
5743
+ application/json:
5744
+ schema:
5745
+ $ref: '#/components/schemas/CreatePublicTicketResponse'
5746
+ example:
5747
+ id: fb_01j7w8a1b2c3d4e5f6g7h8i9j0
5748
+ number: 8
5749
+ '400':
5750
+ $ref: '#/components/responses/BadRequest'
5751
+ '401':
5752
+ $ref: '#/components/responses/Unauthorized'
5753
+ '403':
5754
+ $ref: '#/components/responses/Forbidden'
5755
+ '413':
5756
+ $ref: '#/components/responses/PayloadTooLarge'
5757
+ '429':
5758
+ $ref: '#/components/responses/TooManyRequests'
5759
+ '503':
5760
+ $ref: '#/components/responses/ServiceUnavailable'
5761
+ x-error-codes:
5762
+ - BAD_REQUEST
5763
+ - UNAUTHORIZED
5764
+ - FORBIDDEN
5765
+ - VALIDATION
5766
+ x-cli:
5767
+ hidden: true
5225
5768
  /api/admin/ops/{handle}:
5226
5769
  get:
5227
5770
  operationId: getOpsItem
@@ -9081,6 +9624,16 @@ components:
9081
9624
  scheme: bearer
9082
9625
  bearerFormat: sdk_admin_*
9083
9626
  description: 'Pass an admin SDK key as `Authorization: Bearer sdk_admin_…`. Mint via `POST /api/admin/keys` with `type: "admin"`.'
9627
+ clientSdkKey:
9628
+ type: apiKey
9629
+ in: header
9630
+ name: X-SDK-Key
9631
+ description: |-
9632
+ 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.
9633
+
9634
+ 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.
9635
+
9636
+ The key must additionally carry the `tickets:public_create` scope, and its project must have public ticket creation enabled.
9084
9637
  parameters:
9085
9638
  ProjectId:
9086
9639
  name: X-Project-Id
@@ -10185,6 +10738,85 @@ components:
10185
10738
  - createdAt
10186
10739
  additionalProperties: false
10187
10740
  description: Recent audit rows for one gate, newest first.
10741
+ GateWhitelistAttr:
10742
+ type: string
10743
+ enum:
10744
+ - email
10745
+ - user_id
10746
+ 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.
10747
+ GateWhitelist:
10748
+ type: object
10749
+ properties:
10750
+ id:
10751
+ type: string
10752
+ description: Resolved gate id.
10753
+ name:
10754
+ type: string
10755
+ description: The gate's stable `name`.
10756
+ attr:
10757
+ $ref: '#/components/schemas/GateWhitelistAttr'
10758
+ entries:
10759
+ type: array
10760
+ items:
10761
+ type: string
10762
+ minLength: 1
10763
+ description: The whitelisted identities, in the order they are stored. Deduplicated case-sensitively; an empty array means the gate has no whitelist.
10764
+ required:
10765
+ - id
10766
+ - name
10767
+ - attr
10768
+ - entries
10769
+ additionalProperties: false
10770
+ 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.
10771
+ SetGateWhitelistRequest:
10772
+ type: object
10773
+ properties:
10774
+ attr:
10775
+ anyOf:
10776
+ - $ref: '#/components/schemas/GateWhitelistAttr'
10777
+ description: Identity attribute to match on. Defaults to the whitelist's current attribute, or `email` when the gate has no whitelist yet.
10778
+ entries:
10779
+ type: array
10780
+ items:
10781
+ type: string
10782
+ minLength: 1
10783
+ description: The complete whitelist after the call. Pass `[]` to remove the whitelist from the gate entirely.
10784
+ required:
10785
+ - entries
10786
+ additionalProperties: false
10787
+ 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.
10788
+ AddToGateWhitelistRequest:
10789
+ type: object
10790
+ properties:
10791
+ attr:
10792
+ anyOf:
10793
+ - $ref: '#/components/schemas/GateWhitelistAttr'
10794
+ 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.
10795
+ entries:
10796
+ type: array
10797
+ minItems: 1
10798
+ items:
10799
+ type: string
10800
+ minLength: 1
10801
+ description: Identities to admit. Already-listed entries are skipped, so the call is idempotent.
10802
+ required:
10803
+ - entries
10804
+ additionalProperties: false
10805
+ description: Body for `POST /api/admin/gates/{id}/whitelist`. Adds entries to the whitelist, creating it if the gate doesn't have one.
10806
+ RemoveFromGateWhitelistRequest:
10807
+ type: object
10808
+ properties:
10809
+ entries:
10810
+ type: array
10811
+ minItems: 1
10812
+ items:
10813
+ type: string
10814
+ minLength: 1
10815
+ description: Identities to stop admitting. Entries that aren't listed are skipped, so the call is idempotent.
10816
+ required:
10817
+ - entries
10818
+ additionalProperties: false
10819
+ 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.'
10188
10820
  ExperimentApiRow:
10189
10821
  type: object
10190
10822
  properties:
@@ -12214,6 +12846,70 @@ components:
12214
12846
  - version
12215
12847
  - published
12216
12848
  additionalProperties: false
12849
+ ToggleKillswitchRequest:
12850
+ type: object
12851
+ properties:
12852
+ switchKey:
12853
+ anyOf:
12854
+ - type: string
12855
+ maxLength: 64
12856
+ pattern: ^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$
12857
+ - type: 'null'
12858
+ 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.
12859
+ value:
12860
+ anyOf:
12861
+ - type: boolean
12862
+ - type: 'null'
12863
+ 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.
12864
+ env:
12865
+ anyOf:
12866
+ - $ref: '#/components/schemas/Env'
12867
+ description: Environment to publish on. Defaults to `prod` — the environment an incident response means when it says "kill it".
12868
+ additionalProperties: false
12869
+ description: |-
12870
+ 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:
12871
+
12872
+ - `{}` — flip the flat value on prod.
12873
+ - `{ "switchKey": "eu_region" }` — flip that sub-switch on prod.
12874
+ - `{ "switchKey": "eu_region", "value": true }` — set that sub-switch on prod, idempotently.
12875
+ - `{ "switchKey": "eu_region", "value": true, "env": "staging" }` — the same, on a chosen env.
12876
+ ToggleKillswitchResponse:
12877
+ type: object
12878
+ properties:
12879
+ id:
12880
+ type: string
12881
+ description: Resolved killswitch id.
12882
+ env:
12883
+ $ref: '#/components/schemas/Env'
12884
+ switchKey:
12885
+ anyOf:
12886
+ - type: string
12887
+ maxLength: 64
12888
+ pattern: ^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$
12889
+ - type: 'null'
12890
+ description: The sub-switch that was flipped, or `null` when the flat `value` was.
12891
+ previous:
12892
+ type: boolean
12893
+ description: The value that was in effect before this call. Equal to `value` when an idempotent `value` matched what was already stored.
12894
+ value:
12895
+ type: boolean
12896
+ description: The value now in effect on `env`.
12897
+ version:
12898
+ type: integer
12899
+ minimum: -9007199254740991
12900
+ maximum: 9007199254740991
12901
+ description: Newly published version on `env`.
12902
+ published:
12903
+ $ref: '#/components/schemas/KillswitchValue'
12904
+ required:
12905
+ - id
12906
+ - env
12907
+ - switchKey
12908
+ - previous
12909
+ - value
12910
+ - version
12911
+ - published
12912
+ additionalProperties: false
12217
12913
  UniverseParam:
12218
12914
  type: object
12219
12915
  properties:
@@ -14897,6 +15593,191 @@ components:
14897
15593
  - id
14898
15594
  additionalProperties: false
14899
15595
  description: Response for `POST /api/admin/ops`.
15596
+ CreatePublicBugRequest:
15597
+ type: object
15598
+ properties:
15599
+ title:
15600
+ type: string
15601
+ minLength: 1
15602
+ maxLength: 200
15603
+ pattern: ^\S(.*\S)?$
15604
+ description: One-line bug title (no leading/trailing whitespace).
15605
+ stepsToReproduce:
15606
+ type: string
15607
+ maxLength: 8000
15608
+ default: ''
15609
+ description: How to reproduce the bug.
15610
+ actualResult:
15611
+ type: string
15612
+ maxLength: 8000
15613
+ default: ''
15614
+ description: What actually happened.
15615
+ expectedResult:
15616
+ type: string
15617
+ maxLength: 8000
15618
+ default: ''
15619
+ description: What was expected instead.
15620
+ priority:
15621
+ anyOf:
15622
+ - $ref: '#/components/schemas/OpsItemPriority'
15623
+ - type: 'null'
15624
+ description: Initial triage priority, or `null`.
15625
+ status:
15626
+ anyOf:
15627
+ - $ref: '#/components/schemas/OpsItemStatus'
15628
+ description: Initial lifecycle status; defaults to `open` when omitted.
15629
+ assigneeId:
15630
+ anyOf:
15631
+ - type: string
15632
+ - type: 'null'
15633
+ description: The `users.id` of the person to assign as owner at creation, or `null`.
15634
+ subscribers:
15635
+ type: array
15636
+ items:
15637
+ type: string
15638
+ format: email
15639
+ default: []
15640
+ description: Emails of teammates to subscribe to this item's Slack pings at creation.
15641
+ tags:
15642
+ type: array
15643
+ items:
15644
+ type: string
15645
+ default: []
15646
+ description: Tag names to attach at creation (get-or-created by name, deduped case-insensitively).
15647
+ reporterEmail:
15648
+ anyOf:
15649
+ - type: string
15650
+ format: email
15651
+ - type: 'null'
15652
+ description: Email of the reporter, or `null`.
15653
+ pageUrl:
15654
+ anyOf:
15655
+ - type: string
15656
+ format: uri
15657
+ - type: 'null'
15658
+ description: URL of the page the bug relates to, or `null`.
15659
+ userAgent:
15660
+ anyOf:
15661
+ - type: string
15662
+ maxLength: 500
15663
+ - type: 'null'
15664
+ description: Reporter's user-agent string, or `null`.
15665
+ viewport:
15666
+ anyOf:
15667
+ - type: string
15668
+ maxLength: 40
15669
+ - type: 'null'
15670
+ description: Reporter's viewport (e.g. `1280x720`), or `null`.
15671
+ context:
15672
+ anyOf:
15673
+ - type: object
15674
+ additionalProperties: {}
15675
+ - type: 'null'
15676
+ description: Arbitrary capture context, or `null`.
15677
+ notify:
15678
+ anyOf:
15679
+ - $ref: '#/components/schemas/NotificationTarget'
15680
+ - type: 'null'
15681
+ description: Where this bug's completion notification lands.
15682
+ required:
15683
+ - title
15684
+ additionalProperties: false
15685
+ description: Body for `POST /ops/bug`. The same bug fields as `CreateBugRequest`, minus the `type` discriminator — the path already says what is being filed.
15686
+ CreatePublicTicketResponse:
15687
+ type: object
15688
+ properties:
15689
+ id:
15690
+ type: string
15691
+ description: The new item's id. Absent on a deduped response — nothing was created, so there is no new id to report.
15692
+ number:
15693
+ type: integer
15694
+ 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.
15695
+ deduped:
15696
+ type: boolean
15697
+ description: '`true` when an open ticket already tracked this report and nothing was filed (HTTP 200). Absent on a fresh file (HTTP 201).'
15698
+ required:
15699
+ - number
15700
+ additionalProperties: false
15701
+ 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`.'
15702
+ CreatePublicFeatureRequestRequest:
15703
+ type: object
15704
+ properties:
15705
+ title:
15706
+ type: string
15707
+ minLength: 1
15708
+ maxLength: 200
15709
+ pattern: ^\S(.*\S)?$
15710
+ description: One-line feature-request title (no leading/trailing whitespace).
15711
+ description:
15712
+ type: string
15713
+ maxLength: 8000
15714
+ default: ''
15715
+ description: What the feature is.
15716
+ useCase:
15717
+ type: string
15718
+ maxLength: 8000
15719
+ default: ''
15720
+ description: Why it's needed / the use case.
15721
+ priority:
15722
+ anyOf:
15723
+ - $ref: '#/components/schemas/OpsItemPriority'
15724
+ - type: 'null'
15725
+ description: Initial triage priority, or `null`.
15726
+ status:
15727
+ anyOf:
15728
+ - $ref: '#/components/schemas/OpsItemStatus'
15729
+ description: Initial lifecycle status; defaults to `open` when omitted.
15730
+ assigneeId:
15731
+ anyOf:
15732
+ - type: string
15733
+ - type: 'null'
15734
+ description: The `users.id` of the person to assign as owner at creation, or `null`.
15735
+ subscribers:
15736
+ type: array
15737
+ items:
15738
+ type: string
15739
+ format: email
15740
+ default: []
15741
+ description: Emails of teammates to subscribe to this item's Slack pings at creation.
15742
+ tags:
15743
+ type: array
15744
+ items:
15745
+ type: string
15746
+ default: []
15747
+ description: Tag names to attach at creation (get-or-created by name, deduped case-insensitively).
15748
+ reporterEmail:
15749
+ anyOf:
15750
+ - type: string
15751
+ format: email
15752
+ - type: 'null'
15753
+ description: Email of the reporter, or `null`.
15754
+ pageUrl:
15755
+ anyOf:
15756
+ - type: string
15757
+ format: uri
15758
+ - type: 'null'
15759
+ description: URL of the page the request relates to, or `null`.
15760
+ userAgent:
15761
+ anyOf:
15762
+ - type: string
15763
+ maxLength: 500
15764
+ - type: 'null'
15765
+ description: Reporter's user-agent string, or `null`.
15766
+ context:
15767
+ anyOf:
15768
+ - type: object
15769
+ additionalProperties: {}
15770
+ - type: 'null'
15771
+ description: Arbitrary capture context, or `null`.
15772
+ notify:
15773
+ anyOf:
15774
+ - $ref: '#/components/schemas/NotificationTarget'
15775
+ - type: 'null'
15776
+ description: Where this request's completion notification lands.
15777
+ required:
15778
+ - title
15779
+ additionalProperties: false
15780
+ 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.
14900
15781
  OpsItemAttachment:
14901
15782
  type: object
14902
15783
  properties:
@@ -18222,8 +19103,8 @@ components:
18222
19103
  name:
18223
19104
  type: string
18224
19105
  minLength: 1
18225
- maxLength: 80
18226
- description: Optional human label. Programmatic (API) mints that omit it get an auto-generated descriptive name; dashboard mints may leave it blank.
19106
+ maxLength: 160
19107
+ 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.
18227
19108
  scopes:
18228
19109
  type: array
18229
19110
  items:
@@ -18459,6 +19340,45 @@ components:
18459
19340
  value:
18460
19341
  error: The referenced event is still pending review and cannot back a metric yet.
18461
19342
  code: EVENT_PENDING
19343
+ PayloadTooLarge:
19344
+ description: The request body exceeded the intake's fixed size cap (16 KB).
19345
+ content:
19346
+ application/json:
19347
+ schema:
19348
+ $ref: '#/components/schemas/Error'
19349
+ examples:
19350
+ BAD_REQUEST:
19351
+ summary: Body too large
19352
+ value:
19353
+ error: Body too large
19354
+ code: BAD_REQUEST
19355
+ TooManyRequests:
19356
+ description: |-
19357
+ The public intake is rate-limited per (project, client IP). Back off and retry;
19358
+ the limit is a burst guard, not a quota, so a modest delay clears it.
19359
+ content:
19360
+ application/json:
19361
+ schema:
19362
+ $ref: '#/components/schemas/Error'
19363
+ examples:
19364
+ BAD_REQUEST:
19365
+ summary: Rate limited
19366
+ value:
19367
+ error: Too many requests
19368
+ code: BAD_REQUEST
19369
+ ServiceUnavailable:
19370
+ description: |-
19371
+ A backing store the endpoint needs is not bound in this environment. Transient
19372
+ and infrastructural — no stable `ErrorCode` is assigned, so `code` is omitted.
19373
+ content:
19374
+ application/json:
19375
+ schema:
19376
+ $ref: '#/components/schemas/Error'
19377
+ examples:
19378
+ UNAVAILABLE:
19379
+ summary: Storage unbound
19380
+ value:
19381
+ error: Unavailable
18462
19382
  BadGateway:
18463
19383
  description: |-
18464
19384
  The request was well-formed and the resource exists, but a downstream provider