@voxgig/apidef 8.0.3 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builder/flow/flowHeuristic01.js +1 -1
- package/dist/builder/flow/flowHeuristic01.js.map +1 -1
- package/dist/desc.d.ts +6 -2
- package/dist/guide/guide.js +17 -0
- package/dist/guide/guide.js.map +1 -1
- package/dist/guide/heuristic01.js +24 -1
- package/dist/guide/heuristic01.js.map +1 -1
- package/dist/model.d.ts +6 -2
- package/dist/transform/entity.js +38 -12
- package/dist/transform/entity.js.map +1 -1
- package/dist/transform/flowstep.js +6 -7
- package/dist/transform/flowstep.js.map +1 -1
- package/dist/transform/graphql.js +2 -2
- package/dist/transform/graphql.js.map +1 -1
- package/dist/transform/operation.js +3 -3
- package/dist/transform/operation.js.map +1 -1
- package/dist/transform/top.js +48 -0
- package/dist/transform/top.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.js.map +1 -1
- package/dist/utility.d.ts +6 -1
- package/dist/utility.js +102 -0
- package/dist/utility.js.map +1 -1
- package/model/apidef.aon +20 -2
- package/package.json +1 -1
- package/src/builder/flow/flowHeuristic01.ts +1 -1
- package/src/desc.ts +12 -1
- package/src/guide/guide.ts +19 -0
- package/src/guide/heuristic01.ts +31 -2
- package/src/model.ts +11 -2
- package/src/transform/entity.ts +40 -13
- package/src/transform/flowstep.ts +5 -6
- package/src/transform/graphql.ts +2 -2
- package/src/transform/operation.ts +3 -3
- package/src/transform/top.ts +58 -1
- package/src/types.ts +7 -0
- package/src/utility.ts +127 -0
package/src/transform/top.ts
CHANGED
|
@@ -3,7 +3,9 @@ import { join } from '@voxgig/struct'
|
|
|
3
3
|
|
|
4
4
|
import { KIT } from '../types'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
firstSentence, authExchangeOp, specSecuredByDefault, sortedEntries,
|
|
8
|
+
} from '../utility'
|
|
7
9
|
|
|
8
10
|
import type { TransformResult } from '../transform'
|
|
9
11
|
|
|
@@ -95,6 +97,17 @@ const topTransform = async function(
|
|
|
95
97
|
const security = resolveSecurity(def)
|
|
96
98
|
if (null != security) {
|
|
97
99
|
kit.info.security = security
|
|
100
|
+
|
|
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
|
+
const exchange = findAuthExchange(def)
|
|
108
|
+
if (null != exchange) {
|
|
109
|
+
kit.info.security.exchange = exchange
|
|
110
|
+
}
|
|
98
111
|
}
|
|
99
112
|
}
|
|
100
113
|
|
|
@@ -361,6 +374,50 @@ function resolveSecurity(def: any): Record<string, string> | null {
|
|
|
361
374
|
}
|
|
362
375
|
|
|
363
376
|
|
|
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
|
+
function findAuthExchange(def: any): Record<string, string> | null {
|
|
390
|
+
const secured = specSecuredByDefault(def)
|
|
391
|
+
if (!secured) {
|
|
392
|
+
return null
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for (const [pathStr, pdef] of sortedEntries(def.paths ?? {})) {
|
|
396
|
+
for (const [methodName, mdef] of sortedEntries(pdef as any)) {
|
|
397
|
+
const found = authExchangeOp(
|
|
398
|
+
{ ...(mdef as any), method: methodName.toUpperCase() }, secured)
|
|
399
|
+
|
|
400
|
+
if (null != found) {
|
|
401
|
+
const out: Record<string, string> = {
|
|
402
|
+
path: String(pathStr).replace(/^\/+/, ''),
|
|
403
|
+
method: methodName.toUpperCase(),
|
|
404
|
+
response: found.response,
|
|
405
|
+
}
|
|
406
|
+
// Only when the heuristic actually recognised the credential field.
|
|
407
|
+
// Absent, sdkgen keeps its own documented default rather than
|
|
408
|
+
// carrying a guess that reads like a fact.
|
|
409
|
+
if (null != found.request) {
|
|
410
|
+
out.request = found.request
|
|
411
|
+
}
|
|
412
|
+
return out
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return null
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
364
421
|
// Extract the credential prefix from a securityScheme's / info prose.
|
|
365
422
|
// Three signals, in confidence order:
|
|
366
423
|
// 1. An explicit `Authorization: <prefix> <cred>` line (any prefix word)
|
package/src/types.ts
CHANGED
|
@@ -255,6 +255,13 @@ type GuideMetrics = {
|
|
|
255
255
|
type GuideEntity = {
|
|
256
256
|
name: string
|
|
257
257
|
orig: string
|
|
258
|
+
// `false` drops the entity downstream (transform/entity.ts). Emitted by
|
|
259
|
+
// the heuristic for an access-token exchange, and editable in guide.aon —
|
|
260
|
+
// which is the ONLY correction surface (ADR-002).
|
|
261
|
+
active?: boolean
|
|
262
|
+
// Why the heuristic deactivated it, so guide.aon reads as a record of a
|
|
263
|
+
// decision rather than an unexplained `active: false`.
|
|
264
|
+
why_inactive?: string
|
|
258
265
|
// GraphQL guides key operations by schema root field instead of path;
|
|
259
266
|
// the two branches are mutually exclusive per guide.
|
|
260
267
|
field?: Record<string, GuidePath>
|
package/src/utility.ts
CHANGED
|
@@ -1243,6 +1243,131 @@ function guideActive(node: any): boolean {
|
|
|
1243
1243
|
}
|
|
1244
1244
|
|
|
1245
1245
|
|
|
1246
|
+
// An API's ACCESS-TOKEN EXCHANGE is not a resource, and must not become an
|
|
1247
|
+
// entity. The shape apidef looks for is the one every such endpoint has:
|
|
1248
|
+
//
|
|
1249
|
+
// 1. The spec as a whole is SECURED (a top-level `security` requirement).
|
|
1250
|
+
// Without that, a per-operation `security: []` clears nothing and
|
|
1251
|
+
// carries no signal at all.
|
|
1252
|
+
// 2. The operation clears that requirement with its own `security: []` —
|
|
1253
|
+
// it is the one call a client can make before it holds a credential,
|
|
1254
|
+
// because it is what issues them.
|
|
1255
|
+
// 3. It is a POST. A credential exchange writes; a GET that happens to
|
|
1256
|
+
// return a field called `token` is far likelier to be a resource.
|
|
1257
|
+
// 4. Its success response carries a TOKEN-shaped field.
|
|
1258
|
+
//
|
|
1259
|
+
// All four together, or it is a resource. There is deliberately no vendor
|
|
1260
|
+
// extension and no overlay to say otherwise (ADR-002): a spec apidef does
|
|
1261
|
+
// not control cannot be annotated anyway, and a heuristic that can be
|
|
1262
|
+
// corrected in guide.aon needs no second correction surface.
|
|
1263
|
+
//
|
|
1264
|
+
// Returns the field names the exchange uses, which is what sdkgen's
|
|
1265
|
+
// `secrets` feature needs to drive it, or null when this is a resource.
|
|
1266
|
+
const AUTH_TOKEN_FIELDS = [
|
|
1267
|
+
'access_token', 'accessToken', 'access-token',
|
|
1268
|
+
'id_token', 'idToken',
|
|
1269
|
+
'token', 'jwt',
|
|
1270
|
+
]
|
|
1271
|
+
|
|
1272
|
+
// What the exchange SENDS. Optional: an operation answering with an access
|
|
1273
|
+
// token is an exchange whether or not apidef recognises the credential it
|
|
1274
|
+
// was bought with, and sdkgen carries its own default for the field name.
|
|
1275
|
+
const AUTH_CREDENTIAL_FIELDS = [
|
|
1276
|
+
'refresh_token', 'refreshToken', 'refresh-token',
|
|
1277
|
+
'client_secret', 'clientSecret',
|
|
1278
|
+
'assertion', 'grant_type', 'grantType',
|
|
1279
|
+
'api_key', 'apiKey', 'apikey',
|
|
1280
|
+
'password', 'code',
|
|
1281
|
+
]
|
|
1282
|
+
|
|
1283
|
+
function authExchangeOp(
|
|
1284
|
+
op: any,
|
|
1285
|
+
specSecured: boolean
|
|
1286
|
+
): { request: string | null, response: string } | null {
|
|
1287
|
+
if (true !== specSecured) {
|
|
1288
|
+
return null
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// An empty ARRAY, specifically. `security` absent means "inherit the
|
|
1292
|
+
// global requirement"; `security: []` means "no credential needed here".
|
|
1293
|
+
if (!Array.isArray(op?.security) || 0 !== op.security.length) {
|
|
1294
|
+
return null
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
if ('POST' !== String(op?.method ?? '').toUpperCase()) {
|
|
1298
|
+
return null
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const response = firstFieldMatch(
|
|
1302
|
+
schemaProps(successResponseSchema(op?.responses)), AUTH_TOKEN_FIELDS)
|
|
1303
|
+
|
|
1304
|
+
if (null == response) {
|
|
1305
|
+
return null
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
const request = firstFieldMatch(
|
|
1309
|
+
schemaProps(requestBodySchema(op?.requestBody)), AUTH_CREDENTIAL_FIELDS)
|
|
1310
|
+
|
|
1311
|
+
return { request, response }
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
|
|
1315
|
+
// Does the spec require a credential by default? Only a non-empty top-level
|
|
1316
|
+
// `security` makes a per-operation `security: []` meaningful.
|
|
1317
|
+
function specSecuredByDefault(def: any): boolean {
|
|
1318
|
+
return Array.isArray(def?.security) && 0 < def.security.length
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
// The 2xx body schema, OpenAPI 3 (`content`) or Swagger 2 (`schema`).
|
|
1323
|
+
function successResponseSchema(responses: any): any {
|
|
1324
|
+
const res = responses?.['200'] ?? responses?.[200] ??
|
|
1325
|
+
responses?.['201'] ?? responses?.[201]
|
|
1326
|
+
if (null == res) {
|
|
1327
|
+
return null
|
|
1328
|
+
}
|
|
1329
|
+
return res.content?.['application/json']?.schema ?? res.schema ?? null
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
function requestBodySchema(requestBody: any): any {
|
|
1334
|
+
if (null == requestBody) {
|
|
1335
|
+
return null
|
|
1336
|
+
}
|
|
1337
|
+
return requestBody.content?.['application/json']?.schema ?? null
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
function schemaProps(schema: any): string[] {
|
|
1342
|
+
const props = schema?.properties
|
|
1343
|
+
if (null == props || 'object' !== typeof props) {
|
|
1344
|
+
return []
|
|
1345
|
+
}
|
|
1346
|
+
return Object.keys(props)
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
|
|
1350
|
+
// First name in `names` that the schema declares, compared case-insensitively
|
|
1351
|
+
// so `Access_Token` matches `access_token`. Ordered by the CANDIDATE list, not
|
|
1352
|
+
// by declaration order, so `access_token` wins over a sibling `token`.
|
|
1353
|
+
function firstFieldMatch(props: string[], names: string[]): string | null {
|
|
1354
|
+
const lower = new Map<string, string>()
|
|
1355
|
+
for (const p of props) {
|
|
1356
|
+
const k = p.toLowerCase()
|
|
1357
|
+
if (!lower.has(k)) {
|
|
1358
|
+
lower.set(k, p)
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
for (const name of names) {
|
|
1362
|
+
const hit = lower.get(name.toLowerCase())
|
|
1363
|
+
if (null != hit) {
|
|
1364
|
+
return hit
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
return null
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
|
|
1246
1371
|
function cleanComponentName(
|
|
1247
1372
|
name: string,
|
|
1248
1373
|
isKnownCmp?: (canonizedRemainder: string) => boolean
|
|
@@ -1826,6 +1951,8 @@ export {
|
|
|
1826
1951
|
transliterate,
|
|
1827
1952
|
cleanComponentName,
|
|
1828
1953
|
guideActive,
|
|
1954
|
+
authExchangeOp,
|
|
1955
|
+
specSecuredByDefault,
|
|
1829
1956
|
ensureMinEntityName,
|
|
1830
1957
|
inferFieldType,
|
|
1831
1958
|
normalizeFieldName,
|