@voxgig/apidef 8.11.0 → 8.12.1

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 (79) hide show
  1. package/dist/apidef.d.ts +4 -1
  2. package/dist/apidef.js +12 -34
  3. package/dist/apidef.js.map +1 -1
  4. package/dist/builder/entity/entity.js +0 -33
  5. package/dist/builder/entity/entity.js.map +1 -1
  6. package/dist/builder/entity/info.js +0 -4
  7. package/dist/builder/entity/info.js.map +1 -1
  8. package/dist/builder/flow.d.ts +4 -1
  9. package/dist/builder/flow.js +36 -27
  10. package/dist/builder/flow.js.map +1 -1
  11. package/dist/guide/graphql01.js +0 -55
  12. package/dist/guide/graphql01.js.map +1 -1
  13. package/dist/guide/guide.js +3 -126
  14. package/dist/guide/guide.js.map +1 -1
  15. package/dist/guide/heuristic01.js +40 -181
  16. package/dist/guide/heuristic01.js.map +1 -1
  17. package/dist/parse/graphql.js +0 -25
  18. package/dist/parse/graphql.js.map +1 -1
  19. package/dist/parse.js +51 -56
  20. package/dist/parse.js.map +1 -1
  21. package/dist/resolved.d.ts +23 -0
  22. package/dist/resolved.js +92 -0
  23. package/dist/resolved.js.map +1 -0
  24. package/dist/resolver.js +0 -2
  25. package/dist/resolver.js.map +1 -1
  26. package/dist/transform/args.js +2 -25
  27. package/dist/transform/args.js.map +1 -1
  28. package/dist/transform/casecollide.d.ts +3 -0
  29. package/dist/transform/casecollide.js +67 -0
  30. package/dist/transform/casecollide.js.map +1 -0
  31. package/dist/transform/contract.js +43 -38
  32. package/dist/transform/contract.js.map +1 -1
  33. package/dist/transform/entity.js +0 -56
  34. package/dist/transform/entity.js.map +1 -1
  35. package/dist/transform/field.js +1 -368
  36. package/dist/transform/field.js.map +1 -1
  37. package/dist/transform/flowstep.js +0 -49
  38. package/dist/transform/flowstep.js.map +1 -1
  39. package/dist/transform/graphql.js +0 -36
  40. package/dist/transform/graphql.js.map +1 -1
  41. package/dist/transform/operation.js +0 -23
  42. package/dist/transform/operation.js.map +1 -1
  43. package/dist/transform/top.js +1 -128
  44. package/dist/transform/top.js.map +1 -1
  45. package/dist/transform.js.map +1 -1
  46. package/dist/tsconfig.tsbuildinfo +1 -1
  47. package/dist/types.d.ts +2 -0
  48. package/dist/types.js.map +1 -1
  49. package/dist/utility.d.ts +2 -1
  50. package/dist/utility.js +17 -316
  51. package/dist/utility.js.map +1 -1
  52. package/model/apidef.aon +20 -113
  53. package/model/guide.aon +1 -32
  54. package/package.json +5 -5
  55. package/src/apidef.ts +22 -33
  56. package/src/builder/entity/entity.ts +0 -33
  57. package/src/builder/entity/info.ts +0 -4
  58. package/src/builder/flow.ts +39 -24
  59. package/src/desc.ts +0 -22
  60. package/src/guide/graphql01.ts +0 -75
  61. package/src/guide/guide.ts +5 -127
  62. package/src/guide/heuristic01.ts +47 -182
  63. package/src/model.ts +6 -85
  64. package/src/parse/graphql.ts +0 -25
  65. package/src/parse.ts +61 -56
  66. package/src/resolved.ts +136 -0
  67. package/src/resolver.ts +0 -2
  68. package/src/transform/args.ts +2 -25
  69. package/src/transform/casecollide.ts +80 -0
  70. package/src/transform/contract.ts +44 -34
  71. package/src/transform/entity.ts +0 -56
  72. package/src/transform/field.ts +2 -368
  73. package/src/transform/flowstep.ts +0 -49
  74. package/src/transform/graphql.ts +0 -36
  75. package/src/transform/operation.ts +0 -23
  76. package/src/transform/top.ts +1 -131
  77. package/src/transform.ts +0 -1
  78. package/src/types.ts +4 -13
  79. package/src/utility.ts +18 -314
@@ -11,23 +11,6 @@ import {
11
11
  } from '../utility'
12
12
 
13
13
 
14
- // Detect a path-parameter that is in fact the entity's own id, after URL
15
- // renaming. Three ways an identity can show up:
16
- // 1. The param literally has name 'id' (the common case for e.g. /things/{id}).
17
- // 2. The param's lower-camelCase name appears in `point.rename.param` mapping
18
- // to 'id' — e.g. `{connectionId: 'id'}` for `/companies/{company_id}/connections/{id}`.
19
- // In this case the param's own name is `connection_id` (apidef snake-cased
20
- // it), which doesn't equal 'id' but the segment's `var` is 'id'.
21
- // 3. Positional convention: for singleton ops (load/update/remove), the
22
- // LAST variable segment in the path is the entity's own id. Catches
23
- // cases where the entity name and path placeholder differ in spelling
24
- // (e.g. entity `enviroment` vs path `/environments/{environment_id}`)
25
- // and apidef therefore didn't synthesize a rename-to-id.
26
- //
27
- // Without this helper, the flow generator double-counts the entity's id —
28
- // emitting it as both `srcdatavar.id` AND a separate body field — which the
29
- // in-memory test mock then requires to match a non-existent field on the
30
- // stored entity.
31
14
  function isEntityIdParam(point: any, param: any, opname?: string): boolean {
32
15
  if ('id' === param?.name) return true
33
16
  const renameMap = point?.rename?.param
@@ -79,8 +62,6 @@ const flowstepTransform: Transform = async function(
79
62
  const ent: ModelEntity = kit.entity[flow.entity]
80
63
  const opmap = ent.op
81
64
 
82
- // TODO: spec parameter passed into each step func, used semantically by generator
83
- // validation: part of spec, semantic name and params, up to generator how to use it
84
65
  const ref01 = ent.name + '_ref01'
85
66
 
86
67
  createStep(opmap, flow, ent, { input: { ref: ref01 } })
@@ -186,12 +167,6 @@ const createStep: MakeFlowStep = (
186
167
 
187
168
  each(point.args.params, (param: any) => {
188
169
  if ('id' === param.name) {
189
- // For CREATE, `id` in the path is NOT the entity's own id (entity is
190
- // being created here — its id doesn't exist yet). It's some parent's
191
- // id renamed by apidef's path normalization (e.g. `space_id` → `id`
192
- // in `/spaces/{id}/space_memberships` for SpaceMembership). Recover
193
- // the original snake_case name so the test seeds the parent's id
194
- // into both the created entity's data AND the URL.
195
170
  const origName = originalSnakeNameOfRenamedId(point)
196
171
  if (origName) {
197
172
  step.match[origName] = args.input?.[origName] ?? origName.replace(/_id/, '') + '01'
@@ -203,12 +178,6 @@ const createStep: MakeFlowStep = (
203
178
  step.match[param.name] = args.input?.[param.name] ?? param.name.replace(/_id/, '') + '01'
204
179
  })
205
180
 
206
- // Also seed any path-param fields required by other ops (typically LIST
207
- // through a sibling parent path), so the in-memory test mock can find
208
- // the just-created entity when a later step queries by those fields.
209
- // Without this, a metric created at /pages/{page_id}/metrics/data lacks
210
- // the page_access_user_id field required by
211
- // /pages/{page_id}/page_access_users/{page_access_user_id}/metrics LIST.
212
181
  seedRelatedOpParams(opmap, point, step)
213
182
 
214
183
  flow.step.push(step)
@@ -227,8 +196,6 @@ function seedRelatedOpParams(opmap: any, createPoint: any, step: ModelEntityFlow
227
196
  if (!param?.name) continue
228
197
  if (isEntityIdParam(point, param, opname as any)) continue
229
198
  if (step.match[param.name] !== undefined) continue
230
- // For renamed-from-id params on CREATE's chosen point we'd already
231
- // have set the snake-case origin; don't double-write.
232
199
  if ('id' === param.name) continue
233
200
  step.match[param.name] =
234
201
  param.name.replace(/_id/, '') + '01'
@@ -251,10 +218,6 @@ const listStep: MakeFlowStep = (
251
218
 
252
219
  each(point.args.params, (param: any) => {
253
220
  if ('id' === param.name) {
254
- // For LIST, `id` in the path is a parent's id renamed by apidef
255
- // (LIST doesn't address a single entity by id). Recover the original
256
- // snake_case name so test code references a real idmap entry rather
257
- // than landing on the bogus `id01` default.
258
221
  const origName = originalSnakeNameOfRenamedId(point)
259
222
  if (origName) {
260
223
  step.match[origName] = args.input?.[origName] ?? origName.replace(/_id/, '') + '01'
@@ -348,18 +311,6 @@ const removeStep: MakeFlowStep = (
348
311
  }
349
312
 
350
313
 
351
- // The field the generated basic-flow test WRITES, to prove an update took
352
- // effect. It must not be a route parameter of the update itself.
353
- //
354
- // The test mock builds its lookup selector out of the request data, so when
355
- // the marked field is also a path segment the selector asks for the record by
356
- // the value it is about to write — it matches nothing and the mock answers
357
- // 404, on a flow that is doing exactly what it was told to.
358
- //
359
- // trello's notification_channel_setting chose `channel`, the {channel} of
360
- // PUT /members/{member_id}/notificationsChannelSettings/{channel}, and its js,
361
- // php and ts suites all failed on it. Updating a routing key is a rename, not
362
- // a field update, and neither the mock nor the SDK models it as one.
363
314
  function firstTextField(ent: ModelEntity, op?: ModelOp) {
364
315
  const paramNames: Record<string, boolean> = {}
365
316
  each((op as any)?.points).forEach((pt: any) => {
@@ -1,18 +1,5 @@
1
1
  /* Copyright (c) 2024-2026 Voxgig, MIT License */
2
2
 
3
- // Render the GraphQL wire data onto each point: the complete operation
4
- // document, its variable bindings, the response unwrap path, and (for list
5
- // ops) the pagination descriptor.
6
- //
7
- // Documents are computed HERE, once, and stored in the model as strings.
8
- // The alternative — shipping structured selection data and assembling query
9
- // text inside every generated SDK — would mean one query assembler per
10
- // language target, all of which must stay semantically identical. One
11
- // renderer in apidef is the whole reason GraphQL support stays affordable
12
- // across the target matrix.
13
- //
14
- // Documents are rendered SINGLE-LINE with sorted selection fields, so the
15
- // emitted model is byte-stable and schema drift shows up in model diffs.
16
3
 
17
4
  import { each } from 'jostraca'
18
5
 
@@ -33,9 +20,6 @@ import type {
33
20
  import { deriveRetShape } from '../guide/graphql01'
34
21
 
35
22
 
36
- // Fields the default fragment never selects on a to-one relation: the stub
37
- // carries the id only, so the caller loads the related entity through its
38
- // own entity op.
39
23
  const REL_STUB = '{ id }'
40
24
 
41
25
 
@@ -108,9 +92,6 @@ function payloadScalarFields(typeName: string, def: any): string[] {
108
92
  }
109
93
 
110
94
 
111
- // Variable bindings for a root field: one per argument. `from` is the op
112
- // argument the value is read from; for the input-object argument that is the
113
- // request data itself.
114
95
  function buildVars(fielddef: any, def: any): ModelGraphqlVar[] {
115
96
  const args = fielddef?.args ?? []
116
97
 
@@ -139,7 +120,6 @@ function buildVars(fielddef: any, def: any): ModelGraphqlVar[] {
139
120
  }
140
121
 
141
122
 
142
- // `issue(id: $id, first: $first)` — argument list wired to variables.
143
123
  function argList(vars: ModelGraphqlVar[]): string {
144
124
  return 0 === vars.length ? '' :
145
125
  '(' + vars.map((v) => v.name + ': $' + v.name).join(', ') + ')'
@@ -158,7 +138,6 @@ function varDecl(vars: ModelGraphqlVar[]): string {
158
138
  }
159
139
 
160
140
 
161
- // Render one operation document, single-line.
162
141
  function renderDoc(
163
142
  opname: string,
164
143
  optype: string,
@@ -176,7 +155,6 @@ function renderDoc(
176
155
  ' fragment ' + fragName + ' on ' + fragType +
177
156
  ' { ' + fragFields.join(' ') + ' }' : '')
178
157
 
179
- // Collapse any accidental double spacing so the string is canonical.
180
158
  return doc.replace(/\s+/g, ' ').trim()
181
159
  }
182
160
 
@@ -220,7 +198,6 @@ const graphqlTransform: Transform = async function(
220
198
 
221
199
  const vars = buildVars(fielddef, def)
222
200
 
223
- // Selection shape and response unwrap both follow the return kind.
224
201
  let selection = fragSpread
225
202
  let respath = 'body.data.' + rootfield
226
203
 
@@ -235,12 +212,6 @@ const graphqlTransform: Transform = async function(
235
212
  selection = fragSpread
236
213
  }
237
214
  else if ('payload' === ret.kind && null == ret.entity) {
238
- // Entity-less payload: Linear's DeletePayload is entityId +
239
- // success + lastSyncId and nothing else. The classifier admits
240
- // these (the entity comes from the field name), so the renderer
241
- // must not fall through to the default `{ id }` spread — the
242
- // payload HAS no id, and the server rejects the whole document.
243
- // Select the payload's own scalars and unwrap to the payload.
244
215
  const own = payloadScalarFields(fielddef.type, def)
245
216
  selection = '{ ' + (0 < own.length ? own.join(' ') : '__typename') + ' }'
246
217
  respath = 'body.data.' + rootfield
@@ -256,17 +227,10 @@ const graphqlTransform: Transform = async function(
256
227
  respath = 'body.data.' + rootfield + '.' + ret.unwrap
257
228
  }
258
229
 
259
- // Distinct operation name per point. The action comes from the GUIDE,
260
- // not from mpoint.select: selectTransform runs after this stage, so
261
- // $action is not set yet, and without the suffix every action point
262
- // on an op would ship the same operation name (three PlanetUpdates),
263
- // which is what server logs, tracing and APM key on.
264
230
  const actionName = Object.keys((gfield as any)?.action ?? {})[0]
265
231
  const docname = pascal(entname) + pascal(opname) +
266
232
  (null != actionName ? pascal(actionName) : '')
267
233
 
268
- // GraphQL points ride the HTTP machinery: POST to the single
269
- // endpoint, no path segments. The document carries everything else.
270
234
  mpoint.kind = 'graphql'
271
235
  mpoint.method = 'POST'
272
236
  mpoint.segments = []
@@ -79,7 +79,6 @@ function collectOps(ctx: any, gent: GuideEntity) {
79
79
  ; (gent as any).opm$ = (gent as any).opm$ ?? {}
80
80
  each((gent as any).paths$, (pathdesc: PathDesc) => {
81
81
  each(pathdesc.op, (gop: GuidePathOp, opname: OpName) => {
82
- // Op-level opt-out; see the entity-level note in transform/entity.ts.
83
82
  if (!guideActive(gop)) {
84
83
  return
85
84
  }
@@ -152,15 +151,6 @@ function resolveRemove(opm: ModelOpMap, gent: GuideEntity): undefined | ModelOp
152
151
  function resolvePatch(opm: ModelOpMap, gent: GuideEntity): undefined | ModelOp {
153
152
  const opdesc = resolveOp('patch', gent)
154
153
 
155
- // If patch is actually update, make it update!
156
- //
157
- // That holds when there is no PUT update at all, and equally when every
158
- // PUT update point is an ACTION: a verb such as GitHub's `merge` borrows
159
- // the update slot (actions have no slot of their own) but is not the
160
- // entity's update. Leaving PATCH as `patch` there made the real update
161
- // unreachable, since no target emits a `patch` method, and routed a plain
162
- // update() to the verb. The action points join the promoted PATCH, and
163
- // `$action` selects them at call time.
164
154
  if (null != opdesc && (null == opm.update || onlyActionPaths(gent, 'update'))) {
165
155
  if (null != opm.update) {
166
156
  opdesc.points.push(...opm.update.points)
@@ -191,13 +181,6 @@ function resolveOp(opname: OpName, gent: GuideEntity): undefined | ModelOp {
191
181
  mop = {
192
182
  name: opname,
193
183
  points: opdesc.paths.map((p: PathDesc) => {
194
- // Renames already applied by entity.ts resolvePathList — re-applying
195
- // here corrupted paths for any spec where rename map maps an old
196
- // name to a value that another rename maps to a different new name
197
- // (e.g. gitlab `/groups/{id}/badges/{badge_id}` with rename
198
- // `{badge_id: 'id', id: 'project_id'}` ended up as
199
- // `/groups/{project_id}/badges/{project_id}` — the second pass
200
- // rewrote the freshly-renamed `{id}` into `{project_id}` again).
201
184
  const segments = p.segments
202
185
 
203
186
  const mpoint: ModelPoint = {
@@ -206,12 +189,6 @@ function resolveOp(opname: OpName, gent: GuideEntity): undefined | ModelOp {
206
189
  rename: p.rename,
207
190
  method: p.method,
208
191
  args: {},
209
- // Carry the per-path op transform (res `body.<entity>`, req
210
- // `{<entity>: reqdata}`) computed by the guide step
211
- // (heuristic01 ResolveTransform) onto the point. It lives on the
212
- // path's op, not on the op-map entry, so read p.op.transform.
213
- // Spread into a fresh object so the default-fill below never
214
- // mutates the shared guide op.transform across points.
215
192
  transform: { ...((p as any).op?.transform ?? {}) },
216
193
  select: {
217
194
  exist: []
@@ -14,7 +14,6 @@ import type {
14
14
  } from '../types'
15
15
 
16
16
  import type {
17
- // GuidePath,
18
17
  PathDesc,
19
18
  OpDesc,
20
19
  } from '../desc'
@@ -24,19 +23,9 @@ import type {
24
23
  } from '../model'
25
24
 
26
25
 
27
- // Guide* => from guide model
28
- // *Desc => internal working descriptiuon
29
- // *Def => API spec definition
30
- // Model* => Generated SDK Model
31
26
 
32
27
 
33
- // type GuideEntity = {
34
- // name: string,
35
- // path: Record<string, GuidePath>
36
28
 
37
- // paths$: PathDesc[]
38
- // opm$: Record<OpName, OpDesc>
39
- // }
40
29
 
41
30
 
42
31
  const topTransform = async function(
@@ -55,14 +44,6 @@ const topTransform = async function(
55
44
  // the docs generated from it) never carry an empty/degenerate description.
56
45
  kit.info.description = ensureDescription(kit.info)
57
46
 
58
- // Public APIs that declare NO authentication (no security schemes, no
59
- // top-level `security`, and no per-operation `security`) get an explicit
60
- // no-auth signal in the model. Downstream sdkgen reads it via
61
- // isAuthActive() (main.kit.info.auth === false) to suppress apikey/auth
62
- // code, docs and examples. Only the negative signal is emitted: when the
63
- // spec DOES declare auth we leave `auth` unset so the SDK's own config
64
- // (main.kit.config.auth) governs. Set AFTER stringifyInfoScalars so the
65
- // value stays a real boolean rather than the string "false".
66
47
  if (true === def.graphql) {
67
48
  // A GraphQL schema NEVER declares HTTP auth, so specDeclaresAuth would
68
49
  // report every secured GraphQL API (Linear included) as public and
@@ -80,8 +61,6 @@ const topTransform = async function(
80
61
  type: authopt.type ?? 'apiKey',
81
62
  in: authopt.in ?? 'header',
82
63
  name: authopt.name ?? 'Authorization',
83
- // '' means a raw credential with no prefix (Linear's style);
84
- // prepareAuth in generated SDKs already honours that.
85
64
  prefix: authopt.prefix ?? '',
86
65
  }
87
66
  }
@@ -98,12 +77,6 @@ const topTransform = async function(
98
77
  if (null != security) {
99
78
  kit.info.security = security
100
79
 
101
- // Record the ACCESS-TOKEN EXCHANGE, when the spec describes one. The
102
- // endpoint that issues credentials is not a resource and does not
103
- // become an entity (the guide deactivates it — see
104
- // guide/heuristic01.ts and ADR-002); these facts are how it survives
105
- // into the model instead, and they are exactly what sdkgen's
106
- // `secrets` feature needs to drive the exchange.
107
80
  const exchange = findAuthExchange(def)
108
81
  if (null != exchange) {
109
82
  kit.info.security.exchange = exchange
@@ -111,7 +84,6 @@ const topTransform = async function(
111
84
  }
112
85
  }
113
86
 
114
- // Swagger 2.0
115
87
  if (def.host) {
116
88
  kit.info.servers.push({
117
89
  url: (def.schemes?.[0] ?? 'https') + '://' + join([def.host, def.basePath], '/', true)
@@ -127,25 +99,15 @@ const topTransform = async function(
127
99
  if (!server || 'string' !== typeof server.url) continue
128
100
  const url: string = server.url.trim()
129
101
  if (url === '') continue
130
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) continue // already has scheme
131
- // `//host/path` is a protocol-relative URL — meaningless to a
132
- // backend SDK, treat as missing-scheme and default to https.
102
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url)) continue
133
103
  if (url.startsWith('//')) {
134
104
  server.url = 'https:' + url
135
105
  continue
136
106
  }
137
- // `/path` is path-only (relative to wherever the spec is served).
138
- // Leave it untouched; it's a valid OpenAPI form.
139
107
  if (url.startsWith('/')) continue
140
108
  server.url = 'https://' + url
141
109
  }
142
110
 
143
- // A usable SDK requires a base URL. OpenAPI 3 puts it in `servers[].url`;
144
- // Swagger 2 derives it from `host` + `basePath`. If neither yields a
145
- // non-empty url, the generated SDK has no way to issue requests, so fail
146
- // the apidef model build rather than emit broken code.
147
- // (For GraphQL the parser already synthesised servers[0] from the required
148
- // `endpoint` build option, so this check passes on the same terms.)
149
111
  const firstServerUrl: any = kit.info.servers?.[0]?.url
150
112
  if (null == firstServerUrl || '' === String(firstServerUrl).trim()) {
151
113
  throw new Error(
@@ -156,8 +118,6 @@ const topTransform = async function(
156
118
  )
157
119
  }
158
120
 
159
- // A short "what this API is" blurb and a canonical website link, for doc
160
- // generators. Only set when derivable, so downstream can gate on them.
161
121
  const summary = resolveSummary(def)
162
122
  if (null != summary) {
163
123
  kit.info.summary = summary
@@ -171,8 +131,6 @@ const topTransform = async function(
171
131
  }
172
132
 
173
133
 
174
- // True when the text carries at least one alphabetic character — i.e. it is
175
- // real prose rather than a placeholder like "." / "---" / whitespace.
176
134
  function hasLetters(text: string): boolean {
177
135
  return /[a-zA-Z]/.test(text)
178
136
  }
@@ -210,22 +168,18 @@ function resolveSummary(def: any): string | undefined {
210
168
  }
211
169
 
212
170
  const desc = 'string' === typeof info.description ? info.description : ''
213
- // Treat letterless prose (a bare "." placeholder, "---", …) as no summary
214
- // rather than surfacing it verbatim.
215
171
  if ('' === desc.trim() || !hasLetters(desc)) {
216
172
  return undefined
217
173
  }
218
174
 
219
175
  const lines = desc.split('\n')
220
176
  let i = 0
221
- // Skip leading blank lines, ATX headings (`# ...`) and setext underlines.
222
177
  while (i < lines.length &&
223
178
  ('' === lines[i].trim() ||
224
179
  /^\s*#{1,6}\s/.test(lines[i]) ||
225
180
  /^\s*(-{2,}|={2,})\s*$/.test(lines[i]))) {
226
181
  i++
227
182
  }
228
- // Take the first paragraph (up to the next blank line or heading).
229
183
  const para: string[] = []
230
184
  while (i < lines.length &&
231
185
  '' !== lines[i].trim() &&
@@ -238,12 +192,6 @@ function resolveSummary(def: any): string | undefined {
238
192
  }
239
193
 
240
194
 
241
- // A canonical link back to the API's own website, in priority order:
242
- // 1. externalDocs.url (the spec's explicit external link)
243
- // 2. info['x-logo'].href (redoc homepage link)
244
- // 3. homepage from the server (strip an api./developer./docs. subdomain)
245
- // 4. info.contact.url
246
- // 5. info.termsOfService
247
195
  function resolveWebsite(def: any, servers: any[]): string | undefined {
248
196
  const info = def?.info ?? {}
249
197
 
@@ -272,15 +220,6 @@ function homepageFromServer(url: any): string | undefined {
272
220
  const u = new URL(url.includes('://') ? url : 'https://' + url)
273
221
  let host = u.hostname
274
222
  if ('' === host || !host.includes('.')) return undefined
275
- // A templated server — `https://{instance}.dreamapply.com/api`, a per-tenant
276
- // API declaring `instance` as an OpenAPI server variable — is a legitimate
277
- // spec, but its host is not a resolvable address. Deriving a "website" from
278
- // it put `https://{instance}.dreamapply.com` into the README as a clickable
279
- // link that no browser can open. The URL parser accepts the braces, so the
280
- // placeholder has to be rejected explicitly.
281
- //
282
- // Only the WEBSITE is skipped: the server URL itself is still emitted, where
283
- // the variable is meaningful and the runtime substitutes it.
284
223
  if (/[{}]/.test(host)) return undefined
285
224
  host = host.replace(
286
225
  /^(api|api-[a-z0-9]+|apis|developer|developers|docs?|www)\./i, '')
@@ -297,24 +236,6 @@ function isHttpUrl(v: any): boolean {
297
236
  }
298
237
 
299
238
 
300
- // Describe the spec's PRIMARY security scheme as model facts
301
- // (info.security): scheme key, type, where the credential goes (in/name),
302
- // and the value prefix for Authorization-header credentials. The primary
303
- // scheme is the one named by the first top-level `security` requirement,
304
- // falling back to the first declared scheme. Returns null when nothing
305
- // usable is declared (the no-auth signal is handled separately).
306
- //
307
- // Prefix rules:
308
- // http basic/bearer -> 'Basic' / 'Bearer'
309
- // oauth2 / openIdConnect -> 'Bearer' (access token in Authorization)
310
- // apiKey in an Authorization header -> the prefix the API's own prose
311
- // documents (e.g. `Authorization: OAuth <key>`), else '' (raw). An
312
- // `apiKey` scheme means "send the credential as-is" — a `Bearer`/etc.
313
- // prefix is only implied by an `http`+`bearer` scheme or explicit
314
- // prose, so absent evidence the key goes in raw (e.g. The SMS Works'
315
- // `Authorization: <jwt>`). A user override is available via
316
- // config.auth.prefix.
317
- // apiKey in any other header/query/cookie -> '' (raw credential)
318
239
  function resolveSecurity(def: any): Record<string, string> | null {
319
240
  const schemes: Record<string, any> =
320
241
  def.components?.securitySchemes ?? def.securityDefinitions ?? {}
@@ -344,8 +265,6 @@ function resolveSecurity(def: any): Record<string, string> | null {
344
265
  }
345
266
 
346
267
  if ('http' === type) {
347
- // Swagger 2 `type: basic` has no `scheme`; OpenAPI 3 uses
348
- // `scheme: basic|bearer|...`.
349
268
  out.prefix = 'basic' === String(scheme.scheme ?? '').toLowerCase() ?
350
269
  'Basic' : 'Bearer'
351
270
  }
@@ -367,25 +286,12 @@ function resolveSecurity(def: any): Record<string, string> | null {
367
286
  findAuthPrefix(def.info?.description) ??
368
287
  ''
369
288
  }
370
- // else: raw credential in a named header/query/cookie — no prefix.
371
289
  }
372
290
 
373
291
  return out
374
292
  }
375
293
 
376
294
 
377
- // Find the spec's access-token exchange and describe it as model facts:
378
- // where it lives, and the field names it sends and answers with. Returns
379
- // null when the spec describes no exchange, which is the common case.
380
- //
381
- // `path` is RELATIVE to the server URL, with no leading slash, because the
382
- // server URL already carries whatever account or tenant segment the API
383
- // templates into it — an absolute path would drop that segment. sdkgen's
384
- // secrets feature resolves it against `options.base` for the same reason.
385
- //
386
- // Only the FIRST exchange is recorded. A spec describing two token
387
- // endpoints is describing two auth schemes, which is a bigger thing than a
388
- // field on info.security and is not guessed at here.
389
295
  function findAuthExchange(def: any): Record<string, string> | null {
390
296
  const secured = specSecuredByDefault(def)
391
297
  if (!secured) {
@@ -418,18 +324,6 @@ function findAuthExchange(def: any): Record<string, string> | null {
418
324
  }
419
325
 
420
326
 
421
- // Extract the credential prefix from a securityScheme's / info prose.
422
- // Three signals, in confidence order:
423
- // 1. An explicit `Authorization: <prefix> <cred>` line (any prefix word)
424
- // — e.g. Statuspage's `Authorization: OAuth 89a2...`. The prefix must
425
- // be a short word followed by something credential-shaped (a long
426
- // token, or a `<key>` / `{token}` / `$KEY` / `YOUR_...` placeholder),
427
- // so a bare `Authorization: 89a2...` doesn't match.
428
- // 2. A KNOWN scheme word (Bearer/OAuth/Token/Basic) shown as an example
429
- // prefix — `Example: Bearer eyJ...` (NoFrixion's shape).
430
- // 3. A KNOWN scheme word named as the scheme — `the Bearer scheme`,
431
- // `Bearer authentication`.
432
- // Returns null when nothing indicates a prefix (an apiKey then goes in raw).
433
327
  function findAuthPrefix(text: unknown): string | null {
434
328
  if ('string' !== typeof text || '' === text) {
435
329
  return null
@@ -441,14 +335,12 @@ function findAuthPrefix(text: unknown): string | null {
441
335
  return explicit[1]
442
336
  }
443
337
 
444
- // A known scheme word as an example prefix, then a credential-shaped tail.
445
338
  const example = text.match(
446
339
  /(?:example|e\.g\.)[:\s][^\n]{0,20}?\b(Bearer|OAuth2?|Token|Basic)\b[ \t]+(?:<[^>\n]+>|\{[^}\n]+\}|[A-Za-z0-9._~+/=-]{6,})/i)
447
340
  if (null != example) {
448
341
  return canonAuthScheme(example[1])
449
342
  }
450
343
 
451
- // A known scheme word named as the auth scheme.
452
344
  const named = text.match(
453
345
  /\b(Bearer|OAuth2?|Token|Basic)\b[ \t]+(?:scheme|authentication|auth\b|credentials?)/i)
454
346
  if (null != named) {
@@ -459,7 +351,6 @@ function findAuthPrefix(text: unknown): string | null {
459
351
  }
460
352
 
461
353
 
462
- // Canonical casing for a known scheme word (Bearer/OAuth/Token/Basic).
463
354
  function canonAuthScheme(word: string): string {
464
355
  const w = word.toLowerCase()
465
356
  if (w.startsWith('oauth')) return 'OAuth'
@@ -470,27 +361,18 @@ function canonAuthScheme(word: string): string {
470
361
  }
471
362
 
472
363
 
473
- // Does the spec declare any authentication? True if it defines security
474
- // schemes (OpenAPI 3 `components.securitySchemes` or Swagger 2
475
- // `securityDefinitions`), a top-level `security` requirement, or a
476
- // per-operation `security` requirement. Used to emit a no-auth signal
477
- // (info.auth: false) for fully public APIs.
478
364
  function specDeclaresAuth(def: any): boolean {
479
365
  if (null == def || 'object' !== typeof def) return false
480
366
 
481
367
  const nonEmptyObj = (v: any) =>
482
368
  null != v && 'object' === typeof v && Object.keys(v).length > 0
483
369
 
484
- // OpenAPI 3 security schemes.
485
370
  if (nonEmptyObj(def.components?.securitySchemes)) return true
486
371
 
487
- // Swagger 2 security definitions.
488
372
  if (nonEmptyObj(def.securityDefinitions)) return true
489
373
 
490
- // Top-level security requirement.
491
374
  if (Array.isArray(def.security) && def.security.length > 0) return true
492
375
 
493
- // Per-operation security requirement.
494
376
  const paths = def.paths
495
377
  if (paths && 'object' === typeof paths) {
496
378
  for (const pathItem of Object.values(paths)) {
@@ -508,13 +390,6 @@ function specDeclaresAuth(def: any): boolean {
508
390
  }
509
391
 
510
392
 
511
- // OpenAPI's `info` object (and the `servers` array) declares every scalar
512
- // leaf as a string. YAML/JSON parsers don't enforce that — `version: 2`
513
- // without quotes parses as the number 2, `version: true` as a boolean.
514
- // Apidef's downstream schema (apidef.aontu) unifies info fields as
515
- // `string`, so non-string scalars cause an aontu unify failure during
516
- // model resolution. Normalise scalar leaves to strings here, at the
517
- // model-build boundary, rather than relax the schema.
518
393
  function stringifyInfoScalars(node: any): any {
519
394
  if (null == node) return node
520
395
  if (Array.isArray(node)) return node.map(stringifyInfoScalars)
@@ -541,8 +416,3 @@ export {
541
416
  homepageFromServer,
542
417
  findAuthPrefix,
543
418
  }
544
-
545
-
546
- // export type {
547
- // GuideEntity,
548
- // }
package/src/transform.ts CHANGED
@@ -13,7 +13,6 @@ import { operationTransform } from './transform/operation'
13
13
  import { argsTransform } from './transform/args'
14
14
  import { selectTransform } from './transform/select'
15
15
  import { fieldTransform } from './transform/field'
16
- // import { manualTransform } from './transform/manual'
17
16
 
18
17
 
19
18
 
package/src/types.ts CHANGED
@@ -5,6 +5,8 @@ import * as Fs from 'node:fs'
5
5
  import { Pino, prettyPino } from '@voxgig/util'
6
6
  import { Shape, Open, Any } from 'shape'
7
7
 
8
+ import type { ResolvedSpec } from './resolved'
9
+
8
10
 
9
11
  const KIT = 'kit'
10
12
 
@@ -50,7 +52,6 @@ type ApiDefOptions = {
50
52
  }
51
53
 
52
54
 
53
- // Input definition format.
54
55
  type DefKind = 'OpenAPI' | 'GraphQL'
55
56
 
56
57
 
@@ -208,6 +209,8 @@ type ApiDefContext = {
208
209
  warn: any,
209
210
  metrics: Metrics,
210
211
  work: Record<string, any>
212
+
213
+ resolved?: ResolvedSpec
211
214
  }
212
215
 
213
216
 
@@ -237,7 +240,6 @@ type GuideControl = {}
237
240
  type GuideMetrics = {
238
241
  count: {
239
242
  path: number
240
- // Schema root fields classified (GraphQL guides; 0 for OpenAPI).
241
243
  field: number
242
244
  method: number
243
245
  entity: number
@@ -253,15 +255,6 @@ type GuideMetrics = {
253
255
 
254
256
 
255
257
  type GuideEntity = {
256
- // COMPOSITE IDENTITY CORRECTION, the guide's say over how an entity is
257
- // addressed. apidef infers a compound key from adjacent path parameters,
258
- // which is right far more often than not and cannot always be right:
259
- // `/…/artifacts/{artifact_id}/{archive_format}` reads as composite and is
260
- // not. Stating it here is the documented correction surface (ADR-002).
261
- //
262
- // `parts` names the compound key outright; `composite: false` says these
263
- // adjacent parameters are not one (the record still has a key); `sep`
264
- // changes the separator without restating the parts.
265
258
  id?: {
266
259
  parts?: string[]
267
260
  sep?: string
@@ -281,8 +274,6 @@ type GuideEntity = {
281
274
  // Why the heuristic deactivated it, so guide.aon reads as a record of a
282
275
  // decision rather than an unexplained `active: false`.
283
276
  why_inactive?: string
284
- // GraphQL guides key operations by schema root field instead of path;
285
- // the two branches are mutually exclusive per guide.
286
277
  field?: Record<string, GuidePath>
287
278
  path: Record<string, GuidePath>
288
279
  }