@shipeasy/openapi 1.5.0 → 2.1.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.
Files changed (101) hide show
  1. package/README.md +37 -0
  2. package/dist/chunk-EPLLHLZN.js +1616 -0
  3. package/dist/chunk-EPLLHLZN.js.map +1 -0
  4. package/dist/chunk-SPH5HWCD.js +3326 -0
  5. package/dist/chunk-SPH5HWCD.js.map +1 -0
  6. package/dist/client-pVkoo3Bf.d.ts +11186 -0
  7. package/dist/client.d.ts +1 -0
  8. package/dist/client.js +215 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/custom.d.ts +64 -0
  11. package/dist/custom.js +292 -0
  12. package/dist/custom.js.map +1 -0
  13. package/dist/index.d.ts +29 -17
  14. package/dist/index.js +253 -2458
  15. package/dist/index.js.map +1 -0
  16. package/dist/node-context.d.ts +85 -0
  17. package/dist/node-context.js +110 -0
  18. package/dist/node-context.js.map +1 -0
  19. package/dist/schemas.d.ts +7920 -0
  20. package/dist/schemas.js +1083 -0
  21. package/dist/schemas.js.map +1 -0
  22. package/openapi.json +15297 -3044
  23. package/openapi.yaml +13979 -0
  24. package/package.json +37 -21
  25. package/spec/components/errors.yaml +53 -0
  26. package/spec/components/parameters.yaml +24 -0
  27. package/spec/components/responses.yaml +107 -0
  28. package/spec/components/schemas/alert-rules.yaml +175 -0
  29. package/spec/components/schemas/attributes.yaml +157 -0
  30. package/spec/components/schemas/common.yaml +87 -0
  31. package/spec/components/schemas/configs.yaml +371 -0
  32. package/spec/components/schemas/connectors.yaml +593 -0
  33. package/spec/components/schemas/errors-api.yaml +209 -0
  34. package/spec/components/schemas/events.yaml +321 -0
  35. package/spec/components/schemas/experiments.yaml +865 -0
  36. package/spec/components/schemas/gate-templates.yaml +198 -0
  37. package/spec/components/schemas/gates.yaml +890 -0
  38. package/spec/components/schemas/i18n.yaml +478 -0
  39. package/spec/components/schemas/keys.yaml +186 -0
  40. package/spec/components/schemas/killswitches.yaml +309 -0
  41. package/spec/components/schemas/metrics.yaml +547 -0
  42. package/spec/components/schemas/ops.yaml +1011 -0
  43. package/spec/components/schemas/projects.yaml +222 -0
  44. package/spec/components/schemas/search.yaml +46 -0
  45. package/spec/components/schemas/universes.yaml +127 -0
  46. package/spec/openapi.yaml +348 -0
  47. package/spec/paths/alert-rules.yaml +250 -0
  48. package/spec/paths/attributes.yaml +261 -0
  49. package/spec/paths/configs.yaml +744 -0
  50. package/spec/paths/connectors.yaml +748 -0
  51. package/spec/paths/errors.yaml +382 -0
  52. package/spec/paths/events.yaml +366 -0
  53. package/spec/paths/experiments.yaml +777 -0
  54. package/spec/paths/gate-templates.yaml +263 -0
  55. package/spec/paths/gates.yaml +451 -0
  56. package/spec/paths/i18n.yaml +544 -0
  57. package/spec/paths/keys.yaml +181 -0
  58. package/spec/paths/killswitches.yaml +557 -0
  59. package/spec/paths/metrics.yaml +302 -0
  60. package/spec/paths/ops.yaml +446 -0
  61. package/spec/paths/projects.yaml +210 -0
  62. package/spec/paths/search.yaml +62 -0
  63. package/spec/paths/universes.yaml +247 -0
  64. package/dist/resources/alert-rules.d.ts +0 -50
  65. package/dist/resources/alert-rules.js +0 -75
  66. package/dist/resources/configs.d.ts +0 -681
  67. package/dist/resources/configs.js +0 -509
  68. package/dist/resources/experiments.d.ts +0 -849
  69. package/dist/resources/experiments.js +0 -707
  70. package/dist/resources/gates.d.ts +0 -808
  71. package/dist/resources/gates.js +0 -522
  72. package/dist/resources/index.d.ts +0 -2749
  73. package/dist/resources/index.js +0 -2391
  74. package/dist/resources/killswitches.d.ts +0 -436
  75. package/dist/resources/killswitches.js +0 -514
  76. package/dist/resources/universes.d.ts +0 -185
  77. package/dist/resources/universes.js +0 -234
  78. package/dist/schemas/alert-rules.d.ts +0 -65
  79. package/dist/schemas/alert-rules.js +0 -37
  80. package/dist/schemas/configs.d.ts +0 -154
  81. package/dist/schemas/configs.js +0 -134
  82. package/dist/schemas/constants.d.ts +0 -6
  83. package/dist/schemas/constants.js +0 -7
  84. package/dist/schemas/experiments.d.ts +0 -229
  85. package/dist/schemas/experiments.js +0 -221
  86. package/dist/schemas/folder.d.ts +0 -10
  87. package/dist/schemas/folder.js +0 -8
  88. package/dist/schemas/gates.d.ts +0 -386
  89. package/dist/schemas/gates.js +0 -190
  90. package/dist/schemas/index.d.ts +0 -11
  91. package/dist/schemas/index.js +0 -762
  92. package/dist/schemas/killswitches.d.ts +0 -110
  93. package/dist/schemas/killswitches.js +0 -226
  94. package/dist/schemas/metric-name.d.ts +0 -5
  95. package/dist/schemas/metric-name.js +0 -6
  96. package/dist/schemas/pagination.d.ts +0 -37
  97. package/dist/schemas/pagination.js +0 -44
  98. package/dist/schemas/universes.d.ts +0 -35
  99. package/dist/schemas/universes.js +0 -55
  100. package/dist/transport.d.ts +0 -30
  101. package/dist/transport.js +0 -44
@@ -1,522 +0,0 @@
1
- // src/resources/gates.ts
2
- import { z as z3 } from "zod";
3
-
4
- // src/schemas/gates.ts
5
- import { z as z2 } from "zod";
6
-
7
- // src/schemas/folder.ts
8
- import { z } from "zod";
9
- var folderSchema = z.string().max(256).regex(/^[a-zA-Z0-9_-]+$/, "Folder must be alphanumeric, `_` or `-` (no `/`).").nullable().optional().describe(
10
- "Optional folder name used to group items in the dashboard. Part of the SDK lookup key: an item in folder `checkout` named `new-cart` is referenced as `checkout/new-cart` from the SDK."
11
- );
12
-
13
- // src/schemas/gates.ts
14
- var gateRuleSchema = z2.object({
15
- attr: z2.string().min(1).describe(
16
- "Attribute key on the evaluation context (e.g. `country`, `plan`, `email`). Matched case-sensitively against `Shipeasy.checkGate(user, name)` input."
17
- ),
18
- op: z2.enum([
19
- "eq",
20
- "neq",
21
- "in",
22
- "not_in",
23
- "gt",
24
- "gte",
25
- "lt",
26
- "lte",
27
- "contains",
28
- "regex",
29
- "semver_gt",
30
- "semver_gte",
31
- "semver_lt",
32
- "semver_lte",
33
- "gate_pass",
34
- "exp_in"
35
- ]).describe(
36
- 'Comparison operator. Equality: `eq`/`neq`. Set membership: `in`/`not_in` (value is an array). Numeric order: `gt`/`gte`/`lt`/`lte`. Semver order: `semver_gt`/`semver_gte`/`semver_lt`/`semver_lte` (value is a dotted version like `4.2.0`). String: `contains` (substring), `regex` (JS-flavour pattern, value is the pattern string). Cross-resource references (evaluated only on the worker hot path, where both KV blobs load): `gate_pass` (`value` is another gate\'s name \u2014 true when that gate passes for the same caller); `exp_in` (`attr` is a running experiment\'s name, `value` is a group name, `"$holdout"`, or `"$any"` \u2014 true when the caller is in that variant / the universe holdout / any variant).'
37
- ),
38
- value: z2.unknown().describe(
39
- 'Operand. Shape depends on `op`: scalar for `eq/neq/gt/gte/lt/lte/contains/semver_*`; array of scalars for `in/not_in`; string pattern for `regex`; referenced gate name for `gate_pass`; group name / `"$holdout"` / `"$any"` for `exp_in`.'
40
- )
41
- }).describe(
42
- "Single targeting predicate. A gate's `rules` array is evaluated with AND semantics; use the gatekeeper `stack` for OR / fall-through logic."
43
- );
44
- var gateNameSchema = z2.string().regex(
45
- /^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?)?$/,
46
- "lowercase, digits, _/-; optional folder.name shape"
47
- ).max(128).describe(
48
- "Stable gate key used by SDKs (`Shipeasy.checkGate(user, '<name>')`). Single segment or `folder.name`. Lowercase letters, digits, `_` or `-`; max 128 chars. Immutable after create \u2014 rename = delete + recreate."
49
- );
50
- var rampSchema = z2.object({
51
- from: z2.number().int().min(0).max(1e4).describe("Start of the ramp in basis points (0\u201310000)."),
52
- to: z2.number().int().min(0).max(1e4).describe("End of the ramp in basis points (0\u201310000)."),
53
- startAt: z2.number().int().nonnegative().describe("Ramp start, epoch milliseconds (UTC)."),
54
- durationMs: z2.number().int().positive().describe("Ramp length in milliseconds.")
55
- }).describe(
56
- "Linear rollout ramp. The effective rollout % interpolates `from`\u2192`to` over `[startAt, startAt+durationMs]`, clamped outside that window, evaluated against the wall-clock at eval time. Overrides the entry's static `rolloutPct` while present."
57
- );
58
- var stackedConditionSchema = z2.object({
59
- id: z2.string().min(1).describe(
60
- "Client-supplied stable id for the stack entry (used for React keys, audit diffs, and as the default per-condition rollout salt)."
61
- ),
62
- type: z2.literal("condition").describe("Discriminator. `condition` = rule-based predicate sub-gate."),
63
- name: z2.string().optional().describe("Human label shown in the dashboard."),
64
- fromTemplate: z2.string().nullable().optional().describe(
65
- "Id of the project template this entry was seeded from, or `null` if hand-authored."
66
- ),
67
- pass: z2.enum(["all", "any"]).optional().describe("Combinator across `rules`. `all` = AND (default), `any` = OR."),
68
- rules: z2.array(gateRuleSchema).default([]).describe("Predicates evaluated under `pass`. Empty array = match nothing."),
69
- rolloutPct: z2.number().int().min(0).max(1e4).optional().describe(
70
- "Per-condition rollout in basis points. After the rules match, the caller is bucketed at this %; in-bucket \u2192 gate `true`, otherwise fall through to the next entry. Absent \u21D2 10000 (100%, every matching caller passes)."
71
- ),
72
- bucketBy: z2.string().optional().describe("Attribute used to hash the caller into the per-condition bucket. Default `userID`."),
73
- salt: z2.string().optional().describe("Per-condition hash salt. Defaults to the entry `id`, then the gate salt."),
74
- ramp: rampSchema.optional(),
75
- locked: z2.boolean().optional().describe("If `true`, dashboard hides edit controls (e.g. the trailing public-floor entry).")
76
- }).describe(
77
- "Rule-based sub-gate. When the rules match and the caller falls in the per-condition rollout bucket, evaluation short-circuits and the gate returns `true`; otherwise it falls through to the next entry."
78
- );
79
- var stackedRolloutSchema = z2.object({
80
- id: z2.string().min(1).describe("Client-supplied stable id for the stack entry."),
81
- type: z2.literal("rollout").describe("Discriminator. `rollout` = percentage-bucket sub-gate."),
82
- name: z2.string().optional().describe("Human label shown in the dashboard."),
83
- fromTemplate: z2.string().nullable().optional().describe("Source template id, or `null`."),
84
- rolloutPct: z2.number().int().min(0).max(1e4).describe(
85
- "Bucket size in basis points (0\u201310000 = 0%\u2013100%). `5000` = 50%, `100` = 1%, allows sub-1% precision."
86
- ),
87
- bucketBy: z2.string().optional().describe(
88
- "Attribute used to hash the caller into a bucket. Defaults to `user.userID`. Use e.g. `accountID` to bucket whole accounts together."
89
- ),
90
- salt: z2.string().optional().describe(
91
- "Per-entry hash salt. Defaults to the gate's salt. Set explicitly to keep two rollouts independent or correlated across gates."
92
- ),
93
- ramp: rampSchema.optional(),
94
- locked: z2.boolean().optional().describe("If `true`, dashboard hides edit controls.")
95
- }).describe(
96
- "Percentage-rollout sub-gate. Hashes `bucketBy` (default `userID`) with `salt` and returns `true` for `rolloutPct/10000` fraction of callers (or the ramp's effective % when `ramp` is set)."
97
- );
98
- var stackedGateEntrySchema = z2.discriminatedUnion("type", [stackedConditionSchema, stackedRolloutSchema]).describe(
99
- "One entry in the ordered gatekeeper stack. Evaluated top-to-bottom; first matching entry decides. Mix `condition` and `rollout` entries to express `internal-only` + `1% beta` + `50% public` in a single gate."
100
- );
101
- var gateMetadataFields = {
102
- title: z2.string().max(140).optional().describe("Human-readable title shown in the dashboard. Free-form, no key format constraint."),
103
- description: z2.string().max(2e3).optional().describe("Long-form description / runbook. Markdown is rendered in the dashboard."),
104
- folder: folderSchema,
105
- group: z2.string().max(64).optional().describe("Group label for dashboard organisation (e.g. team or product area)."),
106
- owner_email: z2.string().max(190).optional().describe("Owner contact. Displayed verbatim; not used for auth.")
107
- };
108
- var gateMetadataPatchSchema = z2.object(gateMetadataFields);
109
- var gateCreateSchema = z2.object({
110
- name: gateNameSchema,
111
- enabled: z2.boolean().default(true).describe(
112
- "Master switch. Defaults to `true`. Set `false` to create the gate disabled (evaluates to `false` regardless of rules/rollout); flip on via `POST /{id}/enable` or PATCH."
113
- ),
114
- rollout_pct: z2.number().int().min(0).max(1e4).default(0).describe(
115
- "Initial rollout in **basis points** (0\u201310000 = 0%\u2013100%) \u2014 `100` here means **1%**, not 100%. Use `rollout_percent` (0\u2013100) below if you'd rather think in percent. Use `0` to create the gate dark and ramp via PATCH after deploy validation."
116
- ),
117
- rollout_percent: z2.number().min(0).max(100).optional().describe(
118
- "Initial rollout as a **percentage** (0\u2013100, fractional ok). Friendlier alias for `rollout_pct`; converted internally to basis points (e.g. `100` here = 10000 bp = 100%). If both `rollout_pct` and `rollout_percent` are set, `rollout_percent` wins."
119
- ),
120
- rules: z2.array(gateRuleSchema).default([]).describe(
121
- "Targeting predicates. AND-combined. If non-empty, the gate returns `true` only for callers that satisfy every rule **and** fall under `rollout_pct`."
122
- ),
123
- salt: z2.string().min(1).max(64).optional().describe(
124
- "Hash salt for percentage bucketing. Auto-generated if omitted. Provide explicitly to keep a gate's buckets stable across delete/recreate. **Immutable after create** \u2014 there is no PATCH for `salt` because changing it would re-bucket every caller."
125
- ),
126
- stack: z2.array(stackedGateEntrySchema).nullable().optional().describe(
127
- "Optional gatekeeper stack. When provided, takes precedence over `rules` + `rollout_pct` at evaluation time. Omit (or pass `null`) for a flat gate."
128
- ),
129
- ...gateMetadataFields
130
- }).describe(
131
- "Body for `POST /api/admin/gates`. At minimum supply `name`; everything else has sensible defaults."
132
- );
133
- var gateUpdateSchema = z2.object({
134
- rollout_pct: z2.number().int().min(0).max(1e4).optional().describe(
135
- "New rollout in **basis points** (0\u201310000 = 0%\u2013100%) \u2014 `100` here means **1%**. Use `rollout_percent` (0\u2013100) below for percent. Omit both to leave unchanged."
136
- ),
137
- rollout_percent: z2.number().min(0).max(100).optional().describe(
138
- "New rollout as a **percentage** (0\u2013100). Friendlier alias for `rollout_pct`; converted internally. Wins over `rollout_pct` if both are supplied. Omit both to leave unchanged."
139
- ),
140
- rules: z2.array(gateRuleSchema).optional().describe(
141
- "Replaces the rule list wholesale. To add a value to an `in` rule, send the full new `rules` array with the augmented `value` (e.g. previous `['US','CA']` \u2192 `['US','CA','GB']`)."
142
- ),
143
- enabled: z2.boolean().optional().describe(
144
- "Master switch. `false` makes the gate evaluate to `false` for every caller regardless of `rollout_pct`, `rules`, or `stack` \u2014 use as kill switch."
145
- ),
146
- stack: z2.array(stackedGateEntrySchema).nullable().optional().describe(
147
- "Replaces the gatekeeper stack wholesale. Send `null` to revert to flat `rules` + `rollout_pct` evaluation."
148
- ),
149
- ...gateMetadataFields
150
- }).describe(
151
- "Body for `PATCH /api/admin/gates/{id}`. Partial \u2014 only supplied fields change. Array fields (`rules`, `stack`) replace, not merge."
152
- );
153
- var gateResponseSchema = z2.object({
154
- id: z2.string().describe("Stable opaque gate id (`gat_\u2026`)."),
155
- name: gateNameSchema,
156
- /** D1 stores enabled as 0/1; clients see either form. */
157
- enabled: z2.union([z2.boolean(), z2.number().int().min(0).max(1)]).describe("Master switch. Returned as `1`/`0` from D1; treat truthy as enabled."),
158
- /** Rollout percentage in basis points (0–10000 = 0%–100%). */
159
- rolloutPct: z2.number().int().min(0).max(1e4).describe("Current rollout in basis points (0\u201310000)."),
160
- rules: z2.array(gateRuleSchema).optional(),
161
- salt: z2.string().optional().describe("Hash salt used for percentage bucketing."),
162
- title: z2.string().nullable().optional(),
163
- description: z2.string().nullable().optional(),
164
- folder: z2.string().nullable().optional(),
165
- groupName: z2.string().nullable().optional().describe("Group label (note: `groupName` in response, `group` in request)."),
166
- ownerEmail: z2.string().nullable().optional().describe("Owner email (note: `ownerEmail` in response, `owner_email` in request)."),
167
- stack: z2.array(stackedGateEntrySchema).nullable().optional(),
168
- updatedAt: z2.string().describe("ISO-8601 timestamp of last mutation.")
169
- });
170
- var gateCreateResponseSchema = z2.object({
171
- id: z2.string().describe("Newly assigned gate id (`gat_\u2026`)."),
172
- name: gateNameSchema
173
- });
174
- var gateUpdateResponseSchema = z2.object({ id: z2.string().describe("Gate id that was updated.") }).describe("Update returns only the id \u2014 re-fetch via `GET /api/admin/gates` for the full row.");
175
- var gateDeleteResponseSchema = z2.object({ ok: z2.literal(true) });
176
- var gateToggleResponseSchema = z2.object({
177
- id: z2.string(),
178
- enabled: z2.boolean()
179
- });
180
-
181
- // src/transport.ts
182
- var ApiError = class extends Error {
183
- constructor(message, status) {
184
- super(message);
185
- this.status = status;
186
- this.name = "ApiError";
187
- }
188
- status;
189
- };
190
-
191
- // src/resources/gates.ts
192
- var gateListResponseSchema = z3.object({
193
- data: z3.array(gateResponseSchema),
194
- next_cursor: z3.string().nullable()
195
- });
196
- var BASE = "/api/admin/gates";
197
- function gatesClient(t) {
198
- async function list(opts = {}) {
199
- const query = {};
200
- if (opts.limit !== void 0) query.limit = String(opts.limit);
201
- if (opts.cursor) query.cursor = opts.cursor;
202
- return t.request("GET", BASE, void 0, query);
203
- }
204
- async function listAll() {
205
- const out = [];
206
- let cursor;
207
- do {
208
- const page = await list({ limit: 500, cursor });
209
- out.push(...page.data);
210
- cursor = page.next_cursor ?? void 0;
211
- } while (cursor);
212
- return out;
213
- }
214
- async function resolve(idOrName) {
215
- const all = await listAll();
216
- const found = all.find((g) => g.id === idOrName) ?? all.find((g) => g.name === idOrName);
217
- if (!found) throw new ApiError(`Gate '${idOrName}' not found`, 404);
218
- return found;
219
- }
220
- return {
221
- list,
222
- listAll,
223
- resolve,
224
- getByName: async (name) => {
225
- const found = (await listAll()).find((g) => g.name === name);
226
- if (!found) throw new ApiError(`Gate '${name}' not found`, 404);
227
- return found;
228
- },
229
- create: (input) => t.request("POST", BASE, gateCreateSchema.parse(input)),
230
- update: (id, input) => t.request("PATCH", `${BASE}/${id}`, gateUpdateSchema.parse(input)),
231
- delete: (id) => t.request("DELETE", `${BASE}/${id}`),
232
- enable: (id) => t.request("POST", `${BASE}/${id}/enable`),
233
- disable: (id) => t.request("POST", `${BASE}/${id}/disable`),
234
- setRollout: async (name, pct) => {
235
- if (!Number.isFinite(pct) || pct < 0 || pct > 100) {
236
- throw new ApiError("rollout must be a number between 0 and 100", 400);
237
- }
238
- const gate = await resolve(name);
239
- return t.request("PATCH", `${BASE}/${gate.id}`, {
240
- rollout_pct: Math.round(pct * 100)
241
- });
242
- }
243
- };
244
- }
245
- var gatesResource = {
246
- name: "gates",
247
- basePath: BASE,
248
- describeOne: "feature gate",
249
- describeMany: "feature gates",
250
- tag: {
251
- name: "Gates",
252
- description: [
253
- "Feature gates: boolean flags evaluated at runtime against project rules + a percentage rollout.",
254
- "",
255
- "**Identity.** Each gate is keyed by a stable `name` (a-z, 0-9, `_`/`-`, max 64 chars) which is what SDKs pass to `Shipeasy.checkGate(user, '<name>')`. The `name` is immutable \u2014 rename means delete + recreate.",
256
- "",
257
- "**Evaluation model.** A gate returns `true` when (a) `enabled` is true, and (b) the caller satisfies the gate's rules. There are two evaluation shapes:",
258
- "- **Flat** \u2014 `rules` (AND-combined predicates) gate the caller, then `rollout_pct` (basis points, 0\u201310000) hashes them into a bucket. Used for simple `is in X% rollout` gates.",
259
- "- **Gatekeeper stack** \u2014 an ordered array of `condition` and `rollout` sub-gates, evaluated top-to-bottom; first match wins. Used to express `internal-only \u222A 1% beta \u222A 50% public` in one gate. When `stack` is present it takes precedence over the flat fields.",
260
- "",
261
- "**Rules.** Each rule is `{ attr, op, value }`. Supported ops: `eq`, `neq`, `in`, `not_in`, `gt`, `gte`, `lt`, `lte`, `contains`, `regex`. Attribute names match the keys on the SDK evaluation context (e.g. `country`, `plan`, `email`).",
262
- "",
263
- "**Rollout basis points.** `rollout_pct` is in **basis points**, not percent. `0` = 0%, `100` = 1%, `5000` = 50%, `10000` = 100%. This allows sub-1% precision (e.g. `7` = 0.07%).",
264
- "",
265
- "**Lifecycle.** Create dark (`rollout_pct: 0`) \u2192 attach rules \u2192 ramp via PATCH \u2192 flip kill-switch via `disable`/`enable` \u2192 delete once retired. Deletion is blocked while a running experiment references the gate as a targeting gate."
266
- ].join("\n")
267
- },
268
- schemas: {
269
- create: gateCreateSchema,
270
- update: gateUpdateSchema
271
- },
272
- /** Custom action endpoints beyond CRUD (POST <basePath>/:id/<action>). */
273
- actions: [
274
- { name: "enable", description: "Enable a gate" },
275
- { name: "disable", description: "Disable a gate" }
276
- ],
277
- /**
278
- * Full operation list for the OpenAPI doc. Order here is the order shown in
279
- * the docs sidebar. The typed client (`gatesClient`) is the runtime contract;
280
- * this is the documented surface.
281
- */
282
- endpoints: [
283
- {
284
- operationId: "listGates",
285
- method: "GET",
286
- path: "",
287
- summary: "List feature gates",
288
- description: "Returns a single page of gates ordered by `updated_at desc, id desc`. Use the `cursor` query parameter to paginate.",
289
- response: gateListResponseSchema,
290
- examples: {
291
- response: {
292
- data: [
293
- {
294
- id: "gat_01j7w7m9q4hxbf6npe6s9zr3vc",
295
- name: "checkout_v2",
296
- enabled: 1,
297
- rolloutPct: 5e3,
298
- rules: [
299
- { attr: "country", op: "in", value: ["US", "CA", "GB"] },
300
- { attr: "plan", op: "neq", value: "free" }
301
- ],
302
- salt: "9c1f4f1f2c0c4a5fa1c2b6d3e7c8e3a1",
303
- title: "Checkout v2",
304
- description: "New checkout flow. Pro users in US/CA/GB only.",
305
- folder: "checkout",
306
- groupName: "growth",
307
- ownerEmail: "ana@example.com",
308
- stack: null,
309
- updatedAt: "2026-05-09T16:01:22.000Z"
310
- }
311
- ],
312
- next_cursor: null
313
- }
314
- },
315
- useCase: "Snapshot every gate in the project \u2014 for example to render an admin overview or to drive a CI check that asserts no gate is left at 100% in staging."
316
- },
317
- {
318
- operationId: "createGate",
319
- method: "POST",
320
- path: "",
321
- summary: "Create a feature gate",
322
- description: [
323
- "Creates a new gate. Default `enabled: true` at the supplied `rollout_pct` (`0` = fully dark).",
324
- "",
325
- "Only `name` is required. Request fields use snake_case (`owner_email`); the GET response returns camelCase (`ownerEmail`, `groupName`).",
326
- "",
327
- "Returns `409` if `name` already exists in the project (case-sensitive)."
328
- ].join("\n"),
329
- successStatus: 201,
330
- request: gateCreateSchema,
331
- response: gateCreateResponseSchema,
332
- examples: {
333
- requestExamples: {
334
- minimal: {
335
- summary: "Minimal \u2014 dark gate, name only",
336
- description: "Smallest valid body. The gate is created `enabled: true` at `rollout_pct: 0`, with no rules. Ramp it later via PATCH.",
337
- value: { name: "checkout_v2" }
338
- },
339
- targeted: {
340
- summary: "Targeted rollout \u2014 rules + percentage",
341
- description: "Gate Pro users in US/CA/GB, then hash 10% of that audience into the on-bucket (`rollout_pct: 1000` = 10.00%). Adds dashboard metadata so the gate is self-documenting.",
342
- value: {
343
- name: "checkout_v2",
344
- rollout_pct: 1e3,
345
- rules: [
346
- { attr: "country", op: "in", value: ["US", "CA", "GB"] },
347
- { attr: "plan", op: "neq", value: "free" }
348
- ],
349
- title: "Checkout v2",
350
- description: "New checkout flow. Pro users in US/CA/GB only.",
351
- folder: "checkout",
352
- group: "growth",
353
- owner_email: "ana@example.com"
354
- }
355
- },
356
- gatekeeperStack: {
357
- summary: "Gatekeeper stack \u2014 internal \u222A beta \u222A public",
358
- description: "Three-layer fall-through: employees always on, opted-in beta accounts always on, then 25% of everyone else. First match wins; evaluation stops as soon as one entry returns `true`.",
359
- value: {
360
- name: "checkout_v2",
361
- title: "Checkout v2",
362
- owner_email: "ana@example.com",
363
- stack: [
364
- {
365
- id: "01HKJ0EMP",
366
- type: "condition",
367
- name: "Employees",
368
- pass: "any",
369
- rules: [{ attr: "email", op: "regex", value: "@acme\\.com$" }]
370
- },
371
- {
372
- id: "01HKJ0BETA",
373
- type: "condition",
374
- name: "Beta opt-in",
375
- pass: "all",
376
- rules: [{ attr: "beta_opt_in", op: "eq", value: true }]
377
- },
378
- {
379
- id: "01HKJ0PUB",
380
- type: "rollout",
381
- name: "Public",
382
- rolloutPct: 2500,
383
- bucketBy: "userID",
384
- locked: true
385
- }
386
- ]
387
- }
388
- },
389
- disabledOnCreate: {
390
- summary: "Disabled on create \u2014 provision then enable later",
391
- description: "Create with `enabled: false` so the gate evaluates to `false` for every caller until a follow-up `POST /{id}/enable`. Useful when provisioning ahead of a launch.",
392
- value: { name: "checkout_v2", enabled: false, rollout_pct: 0 }
393
- }
394
- },
395
- response: { id: "gat_01j7w7m9q4hxbf6npe6s9zr3vc", name: "checkout_v2" }
396
- },
397
- useCase: [
398
- '- **Dark create + ramp later** \u2014 `{ "name": "checkout_v2" }` at 0% rollout. Ramp via PATCH after deploy validation.',
399
- "- **Targeted rollout** \u2014 supply `rules` to gate the caller (e.g. only `plan = pro` users) plus a `rollout_pct` to bucket within that audience.",
400
- "- **Gatekeeper stack** \u2014 supply `stack` instead of `rules`/`rollout_pct` for `internal \u222A beta \u222A public` fall-through. Stack entries evaluated top-to-bottom; first match wins.",
401
- "- **Dashboard metadata** \u2014 populate `title`, `description`, `folder`, `group`, `owner_email` so the admin UI is self-documenting from day one.",
402
- "- **Disabled on create** \u2014 pre-provision with `enabled: false` for a future launch; flip on with `POST /{id}/enable` at go-live."
403
- ].join("\n")
404
- },
405
- {
406
- operationId: "updateGate",
407
- method: "PATCH",
408
- path: "/{id}",
409
- summary: "Update a feature gate",
410
- description: [
411
- "Partial update \u2014 only supplied fields change. Array fields (`rules`, `stack`) **replace** wholesale; there is no merge or append.",
412
- "",
413
- "`name` and the gate id are immutable. The response carries only `{ id }` \u2014 re-fetch via `GET /api/admin/gates` for the new row."
414
- ].join("\n"),
415
- pathParams: { id: "Stable opaque gate id (`gat_\u2026`)." },
416
- request: gateUpdateSchema,
417
- response: gateUpdateResponseSchema,
418
- examples: {
419
- requestExamples: {
420
- rampRollout: {
421
- summary: "Ramp rollout \u2014 0 \u2192 50%",
422
- description: "Single-field patch. `5000` basis points = 50%.",
423
- value: { rollout_pct: 5e3 }
424
- },
425
- killSwitch: {
426
- summary: "Kill switch \u2014 disable the gate",
427
- description: "Forces evaluation to `false` for every caller, regardless of rules or rollout. Same effect as `POST /{id}/disable` but composable with other patches.",
428
- value: { enabled: false }
429
- },
430
- extendInRule: {
431
- summary: "Add a value to an `in` rule",
432
- description: "Array fields replace wholesale \u2014 there is no per-rule patch. To add `'AU'` to a country whitelist that previously held `['US','CA','GB']`, send the full new `rules` array. Same pattern applies to `not_in`.",
433
- value: {
434
- rules: [
435
- { attr: "country", op: "in", value: ["US", "CA", "GB", "AU"] },
436
- { attr: "plan", op: "neq", value: "free" }
437
- ]
438
- }
439
- },
440
- replaceStack: {
441
- summary: "Replace gatekeeper stack",
442
- description: 'Stack is replace-wholesale. Send the full new ordered list. Pass `"stack": null` to revert the gate to flat `rules` + `rollout_pct` evaluation.',
443
- value: {
444
- stack: [
445
- {
446
- id: "01HKJ0EMP",
447
- type: "condition",
448
- name: "Employees",
449
- pass: "any",
450
- rules: [{ attr: "email", op: "regex", value: "@acme\\.com$" }]
451
- },
452
- {
453
- id: "01HKJ0PUB",
454
- type: "rollout",
455
- name: "Public",
456
- rolloutPct: 5e3,
457
- locked: true
458
- }
459
- ]
460
- }
461
- },
462
- updateMetadata: {
463
- summary: "Update dashboard metadata",
464
- description: "Any subset of `title`, `description`, `folder`, `group`, `owner_email`. Snake-case in the request; the GET response returns camelCase (`groupName`, `ownerEmail`).",
465
- value: {
466
- title: "Checkout v2 (50%)",
467
- description: "Bumped to 50% after passing canary. Added AU.",
468
- folder: "checkout",
469
- owner_email: "ana@example.com"
470
- }
471
- }
472
- },
473
- response: { id: "gat_01j7w7m9q4hxbf6npe6s9zr3vc" }
474
- },
475
- useCase: [
476
- '- **Ramp rollout** \u2014 `{ "rollout_pct": 5000 }` for 50%. Basis points (0\u201310000); `100` = 1%.',
477
- '- **Kill switch** \u2014 `{ "enabled": false }`. Forces evaluation to `false` for every caller regardless of rules/rollout. Re-enable with `POST /{id}/enable` or `{ "enabled": true }`.',
478
- '- **Modify a rule\'s `in` set** \u2014 send the full new `rules` array. To add `\'GB\'` to `[\'US\',\'CA\']`: `{ "rules": [{ "attr": "country", "op": "in", "value": ["US","CA","GB"] }] }`. No per-rule patch endpoint.',
479
- '- **Add targeting from scratch** \u2014 `{ "rules": [{ "attr": "email", "op": "regex", "value": "@acme\\\\.com$" }] }`.',
480
- '- **Switch to gatekeeper stack** \u2014 send a non-null `stack`. To revert to flat eval, send `{ "stack": null }`.',
481
- "- **Update metadata** \u2014 any subset of `title`, `description`, `folder`, `group`, `owner_email`."
482
- ].join("\n")
483
- },
484
- {
485
- operationId: "deleteGate",
486
- method: "DELETE",
487
- path: "/{id}",
488
- summary: "Delete a feature gate",
489
- description: "Soft-deletes the gate. Returns 409 if the gate is still referenced by a running experiment as a targeting gate \u2014 stop the experiment first.",
490
- pathParams: { id: "Stable opaque gate id (`gat_\u2026`)." },
491
- response: gateDeleteResponseSchema,
492
- examples: { response: { ok: true } },
493
- useCase: "Tear down a gate after a feature has fully shipped and the rollout flag is no longer needed."
494
- },
495
- {
496
- operationId: "enableGate",
497
- method: "POST",
498
- path: "/{id}/enable",
499
- summary: "Enable a gate",
500
- description: "Sets `enabled: true`. The current `rollout_pct` is preserved.",
501
- pathParams: { id: "Stable opaque gate id." },
502
- response: gateToggleResponseSchema,
503
- examples: { response: { id: "gat_01j7w7m9q4hxbf6npe6s9zr3vc", enabled: true } },
504
- useCase: "Re-enable a previously disabled gate without re-issuing a full update."
505
- },
506
- {
507
- operationId: "disableGate",
508
- method: "POST",
509
- path: "/{id}/disable",
510
- summary: "Disable a gate",
511
- description: "Sets `enabled: false` so the gate evaluates to `false` for every caller, regardless of `rollout_pct` or `rules`. Use as a quick kill switch.",
512
- pathParams: { id: "Stable opaque gate id." },
513
- response: gateToggleResponseSchema,
514
- examples: { response: { id: "gat_01j7w7m9q4hxbf6npe6s9zr3vc", enabled: false } },
515
- useCase: "Flip a gate off in production without redeploying \u2014 the canonical kill-switch flow."
516
- }
517
- ]
518
- };
519
- export {
520
- gatesClient,
521
- gatesResource
522
- };