@voxgig/apidef 8.0.2 → 8.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.
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,