zklighter-perps 1.0.261 → 1.0.263

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.
@@ -30,17 +30,9 @@ jobs:
30
30
  sed 's/struct{}/{}/g' server.api > temp.txt && cp temp.txt server.api && rm temp.txt
31
31
  goctl api plugin --plugin ./goctl-swagger-amd="swagger -filename openapi.json -host mainnet.zklighter.elliot.ai -schemes https" --api server.api --dir .
32
32
 
33
- - run:
34
- name: openapi.json Post Process
35
- command: |
36
- cd zklighter-perps/service/apiserver
37
- jq 'del(.paths["/api/v1/feedback"], .definitions["ReqSendFeedback"], .paths["/api/v1/ws_status"], .paths["/stream"], .paths["/api/v1/permission"])' openapi.json > temp.json && mv temp.json openapi.json
38
- jq 'walk(if type == "object" then with_entries(select(.key != "")) else . end)' openapi.json > temp.json && mv temp.json openapi.json
39
- jq 'walk(if type == "object" and has("summary") then .description = .summary else . end)' openapi.json > temp.json && mv temp.json openapi.json
40
- jq 'walk(if type == "object" and has("operationId") then .summary = .operationId else . end)' openapi.json > temp.json && mv temp.json openapi.json
41
- jq 'walk(if type == "object" and has("responses") then .responses["400"] = { "description": "Bad request", "schema": { "$ref": "#/definitions/ResultCode" } } else . end)' openapi.json > tmp.json && mv tmp.json openapi.json
42
- jq 'walk(if type == "object" and .name == "types" then .type="array" | .items={"type":"integer", "format":"uint8"} | del(.format) else . end)' openapi.json > tmp.json && mv tmp.json openapi.json
43
- sed 's/"-",//g' openapi.json > temp.txt && mv temp.txt openapi.json
33
+ # NOTE: all openapi.json post-processing now lives in the dedicated
34
+ # `openapi_postprocess` job (.circleci/openapi_postprocess.py). This job
35
+ # only generates and persists the raw spec. See README.md.
44
36
 
45
37
  - store_artifacts:
46
38
  path: ~/project/zklighter-perps/service/apiserver/openapi.json
@@ -133,6 +125,23 @@ jobs:
133
125
  PR_URL=$(curl -X POST -H "Authorization: Bearer ${GITHUB_TOKEN}" -d '{"title": "'$BE_BRANCH'", "head": "'$BRANCH_NAME'", "base": "main"}' https://api.github.com/repos/elliottech/zklighter-perps-ts/pulls | jq -r '.html_url')
134
126
  curl -X POST -H 'Content-type: application/json' --data '{"text":"TypeScript SDK has been updated. Check the PR here: '$PR_URL'", "type": "mrkdwn"}' ${SLACK_URL}
135
127
 
128
+ typecheck:
129
+ docker:
130
+ - image: cimg/node:22.4.1
131
+
132
+ working_directory: ~/project
133
+
134
+ steps:
135
+ - checkout
136
+ - run:
137
+ name: Install dependencies
138
+ command: |
139
+ npm install
140
+ - run:
141
+ name: Type-check generated SDK
142
+ command: |
143
+ npm run typecheck
144
+
136
145
  update_npm_package:
137
146
  docker:
138
147
  - image: cimg/node:22.4.1
@@ -186,8 +195,14 @@ workflows:
186
195
  when:
187
196
  not: << pipeline.parameters.update_ts_sdk >>
188
197
  jobs:
198
+ # Runs on every branch (including the timestamped branches opened by the
199
+ # update_ts_sdk workflow) so a regenerated SDK that would break consumers
200
+ # fails its PR check before merge.
201
+ - typecheck
189
202
  - update_npm_package:
190
203
  filters:
191
204
  branches:
192
205
  only: main
206
+ requires:
207
+ - typecheck
193
208
 
@@ -1,139 +1,283 @@
1
- import json
1
+ """Post-process the swagger/openapi.json before the TypeScript SDK is generated.
2
+
3
+ This is the single source of truth for every transformation applied to the
4
+ spec emitted by goctl-swagger. The raw spec has a number of quirks (empty keys,
5
+ non-standard int16 types, missing error responses, placeholder enum values,
6
+ auto-generated operation ids, etc.) that either break the OpenAPI Generator or
7
+ produce an awkward SDK. See README.md ("OpenAPI post-processing") for the
8
+ rationale behind each step.
9
+
10
+ Previously some of these fixes lived as inline `jq`/`sed` commands in the
11
+ `update_openapi` CI job. They have been consolidated here so the logic is in one
12
+ place, testable, and easy to reason about.
13
+
14
+ The steps below run in the same order they used to run in the pipeline:
15
+ the former `jq`/`sed` transforms first, then the original Python transforms.
16
+ """
2
17
 
18
+ import json
3
19
 
4
20
  FILE = "./openapi.json"
21
+
22
+
23
+ def walk(node, fn):
24
+ """Apply ``fn`` to every node bottom-up, mirroring jq's ``walk``.
25
+
26
+ Children are transformed before their parent so ``fn`` always sees
27
+ already-processed sub-trees, matching the semantics of the jq commands
28
+ these helpers replaced.
29
+ """
30
+ if isinstance(node, dict):
31
+ node = {key: walk(value, fn) for key, value in node.items()}
32
+ elif isinstance(node, list):
33
+ node = [walk(item, fn) for item in node]
34
+ return fn(node)
35
+
36
+
5
37
  with open(FILE, "r") as f:
6
38
  data = json.load(f)
7
- for path in data["paths"]:
8
- methods = list(data["paths"][path].keys())
9
- has_multiple_methods = len(methods) > 1
10
-
11
- for method in methods:
12
- if "api/v1/" in path:
13
- base_name = path.split("api/v1/")[1].replace("/", "_")
14
- data["paths"][path][method]["summary"] = base_name
15
- if has_multiple_methods:
16
- data["paths"][path][method]["operationId"] = f"{base_name}_{method}"
17
- else:
18
- data["paths"][path][method]["operationId"] = base_name
19
- elif "api/v2/" in path:
20
- base_name = path.split("api/v2/")[1].replace("/", "_") + "_v2"
21
- data["paths"][path][method]["summary"] = base_name
22
- if has_multiple_methods:
23
- data["paths"][path][method]["operationId"] = f"{base_name}_{method}"
24
- else:
25
- data["paths"][path][method]["operationId"] = base_name
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # Step 1. Drop endpoints/definitions that must not be part of the public SDK.
42
+ # (was: jq 'del(.paths[...], .definitions["ReqSendFeedback"])')
43
+ # ---------------------------------------------------------------------------
44
+ for dropped_path in [
45
+ "/api/v1/feedback",
46
+ "/api/v1/ws_status",
47
+ "/stream",
48
+ "/api/v1/permission",
49
+ ]:
50
+ data.get("paths", {}).pop(dropped_path, None)
51
+ data.get("definitions", {}).pop("ReqSendFeedback", None)
52
+
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Step 2. Remove empty-string keys emitted by the swagger generator, which the
56
+ # OpenAPI Generator cannot handle.
57
+ # (was: jq 'walk(... with_entries(select(.key != "")) ...)')
58
+ # ---------------------------------------------------------------------------
59
+ def _strip_empty_keys(node):
60
+ if isinstance(node, dict):
61
+ return {key: value for key, value in node.items() if key != ""}
62
+ return node
63
+
64
+
65
+ data = walk(data, _strip_empty_keys)
66
+
67
+
68
+ # ---------------------------------------------------------------------------
69
+ # Step 3. Mirror `summary` into `description` so generated SDK doc comments keep
70
+ # the original human-readable text before `summary` is overwritten below.
71
+ # (was: jq 'walk(if has("summary") then .description = .summary end)')
72
+ # ---------------------------------------------------------------------------
73
+ def _summary_to_description(node):
74
+ if isinstance(node, dict) and "summary" in node:
75
+ node["description"] = node["summary"]
76
+ return node
77
+
78
+
79
+ data = walk(data, _summary_to_description)
80
+
81
+
82
+ # ---------------------------------------------------------------------------
83
+ # Step 4. Seed `summary` from `operationId` (Step 8 may refine it per-path).
84
+ # (was: jq 'walk(if has("operationId") then .summary = .operationId end)')
85
+ # ---------------------------------------------------------------------------
86
+ def _operation_id_to_summary(node):
87
+ if isinstance(node, dict) and "operationId" in node:
88
+ node["summary"] = node["operationId"]
89
+ return node
90
+
91
+
92
+ data = walk(data, _operation_id_to_summary)
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Step 5. Give every operation a documented 400 response referencing ResultCode.
97
+ # (was: jq 'walk(if has("responses") then .responses["400"] = {...} end)')
98
+ # ---------------------------------------------------------------------------
99
+ def _add_bad_request_response(node):
100
+ if isinstance(node, dict) and isinstance(node.get("responses"), dict):
101
+ node["responses"]["400"] = {
102
+ "description": "Bad request",
103
+ "schema": {"$ref": "#/definitions/ResultCode"},
104
+ }
105
+ return node
106
+
107
+
108
+ data = walk(data, _add_bad_request_response)
109
+
110
+
111
+ # ---------------------------------------------------------------------------
112
+ # Step 6. Model the `types` parameter as a byte array (array of uint8) instead of
113
+ # the non-standard scalar the spec declares.
114
+ # (was: jq 'walk(if .name == "types" then .type="array" | .items=... | del(.format) end)')
115
+ # ---------------------------------------------------------------------------
116
+ def _types_param_to_byte_array(node):
117
+ if isinstance(node, dict) and node.get("name") == "types":
118
+ node["type"] = "array"
119
+ node["items"] = {"type": "integer", "format": "uint8"}
120
+ node.pop("format", None)
121
+ return node
122
+
123
+
124
+ data = walk(data, _types_param_to_byte_array)
125
+
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Step 7. Remove placeholder "-" values from enum/array lists.
129
+ # The former `sed 's/"-",//g'` only stripped a "-" element when it was
130
+ # followed by another element (i.e. not the last item); we preserve that
131
+ # behaviour here.
132
+ # ---------------------------------------------------------------------------
133
+ def _drop_dash_values(node):
134
+ if isinstance(node, list):
135
+ last_index = len(node) - 1
136
+ return [
137
+ value
138
+ for index, value in enumerate(node)
139
+ if not (value == "-" and index != last_index)
140
+ ]
141
+ return node
142
+
143
+
144
+ data = walk(data, _drop_dash_values)
145
+
146
+
147
+ # ---------------------------------------------------------------------------
148
+ # Step 8. Derive stable, path-based operationId/summary so generated method
149
+ # names are predictable (e.g. /api/v1/account -> "account").
150
+ # ---------------------------------------------------------------------------
151
+ for path in data["paths"]:
152
+ methods = list(data["paths"][path].keys())
153
+ has_multiple_methods = len(methods) > 1
154
+
155
+ for method in methods:
156
+ if "api/v1/" in path:
157
+ base_name = path.split("api/v1/")[1].replace("/", "_")
158
+ data["paths"][path][method]["summary"] = base_name
159
+ if has_multiple_methods:
160
+ data["paths"][path][method]["operationId"] = f"{base_name}_{method}"
161
+ else:
162
+ data["paths"][path][method]["operationId"] = base_name
163
+ elif "api/v2/" in path:
164
+ base_name = path.split("api/v2/")[1].replace("/", "_") + "_v2"
165
+ data["paths"][path][method]["summary"] = base_name
166
+ if has_multiple_methods:
167
+ data["paths"][path][method]["operationId"] = f"{base_name}_{method}"
26
168
  else:
27
- base_name = path.split("/")[-1]
28
- data["paths"][path][method]["summary"] = base_name
29
- if has_multiple_methods:
30
- data["paths"][path][method]["operationId"] = f"{base_name}_{method}"
31
- else:
32
- data["paths"][path][method]["operationId"] = base_name
33
-
34
- if data["paths"][path][method]["summary"] == "":
35
- data["paths"][path][method]["summary"] = "status"
36
- if has_multiple_methods:
37
- data["paths"][path][method]["operationId"] = f"status_{method}"
38
- else:
39
- data["paths"][path][method]["operationId"] = "status"
40
-
41
- # Replace $ref to int16 types with inline equivalents across all definitions.
42
- # int16 and derived map types are not standard OpenAPI and cause the generator
43
- # to emit broken model references.
44
- def replace_int16_refs(obj):
45
- if isinstance(obj, dict):
46
- if obj.get("$ref") == "#/definitions/int16":
47
- return {"type": "integer", "format": "int64"}
48
- if obj.get("$ref") == "#/definitions/mapint16string":
49
- return {"type": "object", "additionalProperties": {"type": "string"}}
50
- if obj.get("$ref") == "#/definitions/mapstringfloat64":
51
- return {"type": "object", "additionalProperties": {"type": "number", "format": "double"}}
52
- return {k: replace_int16_refs(v) for k, v in obj.items()}
53
- if isinstance(obj, list):
54
- return [replace_int16_refs(i) for i in obj]
55
- return obj
56
-
57
- for defn_name in list(data["definitions"]):
58
- data["definitions"][defn_name] = replace_int16_refs(
59
- data["definitions"][defn_name]
60
- )
61
-
62
- # Fix enum placement for array types: move enum from array level into items
63
- for defn in data["definitions"].values():
64
- for prop in defn.get("properties", {}).values():
65
- if prop.get("type") == "array" and "enum" in prop:
66
- prop["items"]["enum"] = prop.pop("enum")
67
-
68
- # transfer_history type parameter to be array
69
- if "/api/v1/transfer/history" in data["paths"]:
70
- for method in data["paths"]["/api/v1/transfer/history"].values():
71
- for param in method.get("parameters", []):
72
- if (
73
- param.get("name") == "type"
74
- and param.get("type") == "string"
75
- and "enum" in param
76
- ):
77
- param["type"] = "array"
78
- param["items"] = {"type": "string", "enum": param.pop("enum")}
79
- break
80
-
81
- for path in data["definitions"]:
82
- if not path.startswith("Req"):
83
- required_fields = list(data["definitions"][path]["properties"].keys())
84
- if "message" in required_fields:
85
- required_fields.remove("message")
86
-
87
- if "next" in required_fields:
88
- required_fields.remove("next")
89
-
90
- if "next_cursor" in required_fields:
91
- required_fields.remove("next_cursor")
92
-
93
- if "account_share" in required_fields:
94
- required_fields.remove("account_share")
95
-
96
- if "total_funding_paid_out" in required_fields:
97
- required_fields.remove("total_funding_paid_out")
98
-
99
- if "pending_unlocks" in required_fields:
100
- required_fields.remove("pending_unlocks")
101
-
102
- if "account_trading_mode" in required_fields:
103
- required_fields.remove("account_trading_mode")
104
-
105
- if "funding_fee_discounts_enabled" in required_fields:
106
- required_fields.remove("funding_fee_discounts_enabled")
107
-
108
- if "hidden" in required_fields:
109
- required_fields.remove("hidden")
110
-
111
- if "approved_integrators" in required_fields:
112
- required_fields.remove("approved_integrators")
113
-
114
- if "ask_id_str" in required_fields:
115
- required_fields.remove("ask_id_str")
116
-
117
- if "bid_id_str" in required_fields:
118
- required_fields.remove("bid_id_str")
119
-
120
- if "market_maker_incentive_account_index" in required_fields:
121
- required_fields.remove("market_maker_incentive_account_index")
122
-
123
- if "user_tier_last_update" in required_fields:
124
- required_fields.remove("user_tier_last_update")
125
-
126
- if path == "Trade":
127
- if "taker_fee" in required_fields:
128
- required_fields.remove("taker_fee")
129
-
130
- if "maker_fee" in required_fields:
131
- required_fields.remove("maker_fee")
132
-
133
- if len(required_fields) > 0:
134
- data["definitions"][path]["required"] = required_fields
169
+ data["paths"][path][method]["operationId"] = base_name
170
+ else:
171
+ base_name = path.split("/")[-1]
172
+ data["paths"][path][method]["summary"] = base_name
173
+ if has_multiple_methods:
174
+ data["paths"][path][method]["operationId"] = f"{base_name}_{method}"
135
175
  else:
136
- data["definitions"][path].pop("required", None)
176
+ data["paths"][path][method]["operationId"] = base_name
177
+
178
+ if data["paths"][path][method]["summary"] == "":
179
+ data["paths"][path][method]["summary"] = "status"
180
+ if has_multiple_methods:
181
+ data["paths"][path][method]["operationId"] = f"status_{method}"
182
+ else:
183
+ data["paths"][path][method]["operationId"] = "status"
184
+
185
+ # Replace $ref to int16 types with inline equivalents across all definitions.
186
+ # int16 and derived map types are not standard OpenAPI and cause the generator
187
+ # to emit broken model references.
188
+ def replace_int16_refs(obj):
189
+ if isinstance(obj, dict):
190
+ if obj.get("$ref") == "#/definitions/int16":
191
+ return {"type": "integer", "format": "int64"}
192
+ if obj.get("$ref") == "#/definitions/mapint16string":
193
+ return {"type": "object", "additionalProperties": {"type": "string"}}
194
+ if obj.get("$ref") == "#/definitions/mapstringfloat64":
195
+ return {"type": "object", "additionalProperties": {"type": "number", "format": "double"}}
196
+ return {k: replace_int16_refs(v) for k, v in obj.items()}
197
+ if isinstance(obj, list):
198
+ return [replace_int16_refs(i) for i in obj]
199
+ return obj
200
+
201
+ for defn_name in list(data["definitions"]):
202
+ data["definitions"][defn_name] = replace_int16_refs(
203
+ data["definitions"][defn_name]
204
+ )
205
+
206
+ # Fix enum placement for array types: move enum from array level into items
207
+ for defn in data["definitions"].values():
208
+ for prop in defn.get("properties", {}).values():
209
+ if prop.get("type") == "array" and "enum" in prop:
210
+ prop["items"]["enum"] = prop.pop("enum")
211
+
212
+ # transfer_history type parameter to be array
213
+ if "/api/v1/transfer/history" in data["paths"]:
214
+ for method in data["paths"]["/api/v1/transfer/history"].values():
215
+ for param in method.get("parameters", []):
216
+ if (
217
+ param.get("name") == "type"
218
+ and param.get("type") == "string"
219
+ and "enum" in param
220
+ ):
221
+ param["type"] = "array"
222
+ param["items"] = {"type": "string", "enum": param.pop("enum")}
223
+ break
224
+
225
+ for path in data["definitions"]:
226
+ if not path.startswith("Req"):
227
+ required_fields = list(data["definitions"][path]["properties"].keys())
228
+ if "message" in required_fields:
229
+ required_fields.remove("message")
230
+
231
+ if "next" in required_fields:
232
+ required_fields.remove("next")
233
+
234
+ if "next_cursor" in required_fields:
235
+ required_fields.remove("next_cursor")
236
+
237
+ if "account_share" in required_fields:
238
+ required_fields.remove("account_share")
239
+
240
+ if "total_funding_paid_out" in required_fields:
241
+ required_fields.remove("total_funding_paid_out")
242
+
243
+ if "pending_unlocks" in required_fields:
244
+ required_fields.remove("pending_unlocks")
245
+
246
+ if "account_trading_mode" in required_fields:
247
+ required_fields.remove("account_trading_mode")
248
+
249
+ if "funding_fee_discounts_enabled" in required_fields:
250
+ required_fields.remove("funding_fee_discounts_enabled")
251
+
252
+ if "hidden" in required_fields:
253
+ required_fields.remove("hidden")
254
+
255
+ if "approved_integrators" in required_fields:
256
+ required_fields.remove("approved_integrators")
257
+
258
+ if "ask_id_str" in required_fields:
259
+ required_fields.remove("ask_id_str")
260
+
261
+ if "bid_id_str" in required_fields:
262
+ required_fields.remove("bid_id_str")
263
+
264
+ if "market_maker_incentive_account_index" in required_fields:
265
+ required_fields.remove("market_maker_incentive_account_index")
266
+
267
+ if "user_tier_last_update" in required_fields:
268
+ required_fields.remove("user_tier_last_update")
269
+
270
+ if path == "Trade":
271
+ if "taker_fee" in required_fields:
272
+ required_fields.remove("taker_fee")
273
+
274
+ if "maker_fee" in required_fields:
275
+ required_fields.remove("maker_fee")
276
+
277
+ if len(required_fields) > 0:
278
+ data["definitions"][path]["required"] = required_fields
279
+ else:
280
+ data["definitions"][path].pop("required", None)
137
281
 
138
282
  with open(FILE, "w") as f:
139
283
  json.dump(data, f, indent=2)
@@ -1,5 +1,6 @@
1
1
  apis/AccountApi.ts
2
2
  apis/AnnouncementApi.ts
3
+ apis/AtomicordersApi.ts
3
4
  apis/BlockApi.ts
4
5
  apis/BridgeApi.ts
5
6
  apis/CandlestickApi.ts
@@ -39,6 +40,8 @@ models/ApprovedIntegrator.ts
39
40
  models/Asset.ts
40
41
  models/AssetDetails.ts
41
42
  models/AssetStats.ts
43
+ models/AtomicOrder.ts
44
+ models/AtomicOrderLeg.ts
42
45
  models/Auth.ts
43
46
  models/Block.ts
44
47
  models/Blocks.ts
@@ -137,6 +140,7 @@ models/ReqGetAccountTxs.ts
137
140
  models/ReqGetAirdropAllocations.ts
138
141
  models/ReqGetApiTokens.ts
139
142
  models/ReqGetAssetDetails.ts
143
+ models/ReqGetAtomicOrder.ts
140
144
  models/ReqGetBlock.ts
141
145
  models/ReqGetBlockTxs.ts
142
146
  models/ReqGetBridgesByL1Addr.ts
@@ -182,7 +186,10 @@ models/ReqGetTx.ts
182
186
  models/ReqGetUserReferrals.ts
183
187
  models/ReqGetWithdrawHistory.ts
184
188
  models/ReqIsWhitelisted.ts
189
+ models/ReqListAtomicOrders.ts
185
190
  models/ReqListRFQs.ts
191
+ models/RespAtomicOrder.ts
192
+ models/RespAtomicOrderList.ts
186
193
  models/RespChangeAccountTier.ts
187
194
  models/RespCreateRFQ.ts
188
195
  models/RespGetApiTokens.ts
package/README.md ADDED
@@ -0,0 +1,80 @@
1
+ # zklighter-perps-ts
2
+
3
+ Auto-generated TypeScript SDK for the Lighter Perps API, published to npm as
4
+ [`zklighter-perps`](https://www.npmjs.com/package/zklighter-perps).
5
+
6
+ The SDK source (`index.ts`, `runtime.ts`, `apis/`, `models/`) is **generated** by
7
+ the [OpenAPI Generator](https://openapi-generator.tech) from the backend's
8
+ OpenAPI spec. Do not edit the generated files by hand — they are overwritten on
9
+ every regeneration. The package ships raw `.ts` (`"main": "index.ts"`), so
10
+ consumers compile it themselves.
11
+
12
+ ## Generation pipeline (CircleCI)
13
+
14
+ Defined in [`.circleci/config.yml`](.circleci/config.yml). The `update_ts_sdk`
15
+ workflow (manually triggered) runs three jobs in sequence:
16
+
17
+ 1. **`update_openapi`** — clones `zklighter-perps`, runs `goctl-swagger` to emit
18
+ the raw `openapi.json`, and persists it. This job no longer transforms the
19
+ spec; it only generates and persists it.
20
+ 2. **`openapi_postprocess`** — runs
21
+ [`.circleci/openapi_postprocess.py`](.circleci/openapi_postprocess.py), the
22
+ **single source of truth** for all spec fixups (see below).
23
+ 3. **`update_ts_sdk`** — runs the OpenAPI Generator on the post-processed spec,
24
+ bumps the package version, and opens a PR.
25
+
26
+ The `update_npm_package` workflow runs on `main`:
27
+
28
+ - **`typecheck`** — `npm install && npm run typecheck` (`tsc --noEmit` using
29
+ [`tsconfig.json`](tsconfig.json)). Runs on every branch, including the
30
+ timestamped branches opened by `update_ts_sdk`, so a regenerated SDK that
31
+ would fail to compile in a consumer (e.g. `perps-fe`) is caught on its PR.
32
+ - **`update_npm_package`** — publishes to npm (requires `typecheck` to pass).
33
+
34
+ ## OpenAPI post-processing
35
+
36
+ The spec emitted by `goctl-swagger` has quirks that either break the OpenAPI
37
+ Generator or produce an awkward SDK, so we post-process it before generation.
38
+ All of this lives in **one place** — `.circleci/openapi_postprocess.py`.
39
+ (Historically some of these fixes were inline `jq`/`sed` commands in the
40
+ `update_openapi` job; they have been consolidated into the script so the logic
41
+ is in a single, testable file.)
42
+
43
+ Why each transform exists:
44
+
45
+ 1. **Drop internal endpoints/definitions** (`/api/v1/feedback`,
46
+ `/api/v1/ws_status`, `/stream`, `/api/v1/permission`, `ReqSendFeedback`) —
47
+ not part of the public SDK surface.
48
+ 2. **Strip empty-string keys** — the generator emits `""` keys the OpenAPI
49
+ Generator cannot process.
50
+ 3. **Mirror `summary` into `description`** — preserves the original
51
+ human-readable text as the generated doc comment before `summary` is
52
+ overwritten in step 8.
53
+ 4. **Seed `summary` from `operationId`** — normalizes naming before step 8.
54
+ 5. **Add a documented `400` response** referencing `ResultCode` to every
55
+ operation, so the SDK models the standard error shape.
56
+ 6. **Model the `types` parameter as a byte array** (`array` of `uint8`) instead
57
+ of the non-standard scalar the spec declares.
58
+ 7. **Remove placeholder `"-"` enum/array values** that aren't real values.
59
+ 8. **Derive stable, path-based `operationId`/`summary`** (e.g.
60
+ `/api/v1/account` → `account`) so generated method names are predictable.
61
+ 9. **Inline non-standard `int16`/map `$ref`s** (`int16`, `mapint16string`,
62
+ `mapstringfloat64`) — these are not standard OpenAPI and make the generator
63
+ emit broken model references.
64
+ 10. **Move array-level `enum` into `items`** — correct placement for array
65
+ schemas.
66
+ 11. **Make the `transfer/history` `type` param an array** of enum strings.
67
+ 12. **Trim over-eager `required` fields** — the backend marks fields required
68
+ that are actually optional in responses; we relax them so deserialization
69
+ doesn't reject valid payloads.
70
+
71
+ ## Local development
72
+
73
+ ```bash
74
+ npm install # installs TypeScript (the only dependency)
75
+ npm run typecheck # same check CI runs: tsc --noEmit
76
+ ```
77
+
78
+ To test the post-processing script against a spec locally, place an
79
+ `openapi.json` next to the script and run `python3 openapi_postprocess.py`
80
+ (it reads/writes `./openapi.json`).