@pikku/openapi-parser 0.12.22 → 0.12.23
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/CHANGELOG.md +16 -0
- package/dist/auth-config.d.ts +53 -16
- package/dist/auth-config.js +43 -9
- package/dist/codegen.d.ts +1 -1
- package/dist/codegen.js +415 -376
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/parse-openapi.d.ts +31 -5
- package/dist/parse-openapi.js +198 -70
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { parseOpenAPISpec, computeContractHash, type ParsedSpec, } from './parse-openapi.js';
|
|
1
|
+
export { parseOpenAPISpec, computeContractHash, filterOperations, specCoverageWarning, detectLoginOperation, type ParsedSpec, type ParseOptions, type AuthType, type OperationFilter, type LoginOperation, } from './parse-openapi.js';
|
|
2
2
|
export { generateAddonFromOpenAPI } from './codegen.js';
|
|
3
3
|
export { loadAuthConfig, type AuthConfig, type DelegatedLoginConfig, } from './auth-config.js';
|
|
4
4
|
export { type NamedOperation } from './naming.js';
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { parseOpenAPISpec, computeContractHash, } from './parse-openapi.js';
|
|
1
|
+
export { parseOpenAPISpec, computeContractHash, filterOperations, specCoverageWarning, detectLoginOperation, } from './parse-openapi.js';
|
|
2
2
|
export { generateAddonFromOpenAPI } from './codegen.js';
|
|
3
3
|
export { loadAuthConfig, } from './auth-config.js';
|
|
4
4
|
export { createContext, schemaToZod, schemaVarName, sanitizeTypeName, } from './openapi-to-zod-schema.js';
|
package/dist/parse-openapi.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export interface ErrorResponse {
|
|
|
4
4
|
description: string;
|
|
5
5
|
}
|
|
6
6
|
export interface SecuritySchemeInfo {
|
|
7
|
-
type: 'oauth2' | 'http' | 'apiKey';
|
|
7
|
+
type: 'oauth2' | 'http' | 'apiKey' | 'openIdConnect';
|
|
8
8
|
scheme?: string;
|
|
9
9
|
bearerFormat?: string;
|
|
10
10
|
name?: string;
|
|
@@ -23,12 +23,17 @@ export interface ParsedSpec {
|
|
|
23
23
|
};
|
|
24
24
|
baseUrl: string;
|
|
25
25
|
serverUrls: string[];
|
|
26
|
-
authType:
|
|
26
|
+
authType: AuthType;
|
|
27
27
|
operations: ParsedOperation[];
|
|
28
28
|
componentSchemas: Record<string, OpenAPISchema>;
|
|
29
29
|
securitySchemes: Record<string, SecuritySchemeInfo>;
|
|
30
30
|
tagDescriptions: Record<string, string>;
|
|
31
31
|
}
|
|
32
|
+
export type AuthType = 'bearer' | 'oauth2' | 'apiKey' | 'basic' | 'none';
|
|
33
|
+
export interface ParseOptions {
|
|
34
|
+
/** Sent with the request when the spec is fetched from a URL. */
|
|
35
|
+
headers?: Record<string, string>;
|
|
36
|
+
}
|
|
32
37
|
export interface ParsedOperation {
|
|
33
38
|
operationId?: string;
|
|
34
39
|
method: string;
|
|
@@ -42,7 +47,9 @@ export interface ParsedOperation {
|
|
|
42
47
|
requestBody?: OpenAPISchema;
|
|
43
48
|
requestBodyDescription?: string;
|
|
44
49
|
requestBodyRequired?: boolean;
|
|
50
|
+
requestBodyMediaType?: string;
|
|
45
51
|
responseSchema?: OpenAPISchema;
|
|
52
|
+
responseMediaType?: string;
|
|
46
53
|
responseDescription?: string;
|
|
47
54
|
errorResponses: ErrorResponse[];
|
|
48
55
|
deprecated: boolean;
|
|
@@ -55,13 +62,32 @@ export interface ParsedParam {
|
|
|
55
62
|
example?: unknown;
|
|
56
63
|
}
|
|
57
64
|
/**
|
|
58
|
-
* Read and parse an OpenAPI spec from a file path.
|
|
59
|
-
*
|
|
65
|
+
* Read and parse an OpenAPI spec from a file path or an http(s) URL.
|
|
66
|
+
* JSON and YAML are both accepted, whatever the extension.
|
|
60
67
|
*/
|
|
61
|
-
export declare function parseOpenAPISpec(
|
|
68
|
+
export declare function parseOpenAPISpec(source: string, options?: ParseOptions): Promise<ParsedSpec>;
|
|
62
69
|
/**
|
|
63
70
|
* Compute a SHA-256 hash of the contract-relevant parts of a parsed spec.
|
|
64
71
|
* Excludes presentation fields (summary, description, tags, operationId)
|
|
65
72
|
* so that only structural API changes produce a different hash.
|
|
66
73
|
*/
|
|
67
74
|
export declare function computeContractHash(spec: ParsedSpec): string;
|
|
75
|
+
export interface OperationFilter {
|
|
76
|
+
tags?: string[];
|
|
77
|
+
include?: string[];
|
|
78
|
+
exclude?: string[];
|
|
79
|
+
}
|
|
80
|
+
export declare function filterOperations(spec: ParsedSpec, filter: OperationFilter): ParsedSpec;
|
|
81
|
+
/**
|
|
82
|
+
* Specs served by API explorers often hide every authenticated route until
|
|
83
|
+
* the request carries a key, so a spec that is nearly empty — or holds only
|
|
84
|
+
* its own login route — is a sign the real one was never fetched.
|
|
85
|
+
*/
|
|
86
|
+
export declare function specCoverageWarning(spec: ParsedSpec): string | undefined;
|
|
87
|
+
export interface LoginOperation {
|
|
88
|
+
method: string;
|
|
89
|
+
path: string;
|
|
90
|
+
tokenPath?: string;
|
|
91
|
+
}
|
|
92
|
+
/** A login route that trades credentials for a token, if the spec has one. */
|
|
93
|
+
export declare function detectLoginOperation(spec: ParsedSpec): LoginOperation | undefined;
|
package/dist/parse-openapi.js
CHANGED
|
@@ -5,18 +5,17 @@ import { createHash } from 'crypto';
|
|
|
5
5
|
import { readFile } from 'fs/promises';
|
|
6
6
|
import { parse as parseYAML } from 'yaml';
|
|
7
7
|
/**
|
|
8
|
-
* Read and parse an OpenAPI spec from a file path.
|
|
9
|
-
*
|
|
8
|
+
* Read and parse an OpenAPI spec from a file path or an http(s) URL.
|
|
9
|
+
* JSON and YAML are both accepted, whatever the extension.
|
|
10
10
|
*/
|
|
11
|
-
export async function parseOpenAPISpec(
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
11
|
+
export async function parseOpenAPISpec(source, options = {}) {
|
|
12
|
+
const isUrl = /^https?:\/\//i.test(source);
|
|
13
|
+
const content = isUrl
|
|
14
|
+
? await fetchSpec(source, options.headers ?? {})
|
|
15
|
+
: await readFile(source, 'utf-8');
|
|
16
|
+
const doc = content.trimStart().startsWith('{')
|
|
17
|
+
? JSON.parse(content)
|
|
18
|
+
: parseYAML(content);
|
|
20
19
|
// Validate spec version
|
|
21
20
|
const specVersion = doc.openapi ?? doc.swagger;
|
|
22
21
|
if (!specVersion) {
|
|
@@ -33,7 +32,7 @@ export async function parseOpenAPISpec(filePath) {
|
|
|
33
32
|
version: doc.info?.version ?? '1.0.0',
|
|
34
33
|
description: doc.info?.description,
|
|
35
34
|
};
|
|
36
|
-
const serverUrls = extractServerUrls(doc);
|
|
35
|
+
const serverUrls = extractServerUrls(doc, isUrl ? source : undefined);
|
|
37
36
|
const baseUrl = serverUrls[0] ?? '';
|
|
38
37
|
const authType = detectAuthType(doc);
|
|
39
38
|
const securitySchemes = extractSecuritySchemes(doc);
|
|
@@ -61,6 +60,8 @@ export async function parseOpenAPISpec(filePath) {
|
|
|
61
60
|
// OpenAPI 3.x: op.requestBody, Swagger 2.x: parameters[].in === 'body'
|
|
62
61
|
const body3 = op.requestBody;
|
|
63
62
|
const body2 = allParams.find((p) => p.in === 'body');
|
|
63
|
+
const body = extractRequestBody(op, allParams, doc);
|
|
64
|
+
const response = extractResponse(op, doc);
|
|
64
65
|
operations.push({
|
|
65
66
|
operationId: op.operationId,
|
|
66
67
|
method,
|
|
@@ -71,10 +72,12 @@ export async function parseOpenAPISpec(filePath) {
|
|
|
71
72
|
pathParams: extractParams(allParams, 'path'),
|
|
72
73
|
queryParams: extractParams(allParams, 'query'),
|
|
73
74
|
headerParams: extractParams(allParams, 'header'),
|
|
74
|
-
requestBody:
|
|
75
|
+
requestBody: body?.schema,
|
|
76
|
+
requestBodyMediaType: body?.mediaType,
|
|
75
77
|
requestBodyDescription: body3?.description ?? body2?.description,
|
|
76
|
-
requestBodyRequired: body3?.required ?? body2?.required,
|
|
77
|
-
responseSchema:
|
|
78
|
+
requestBodyRequired: body3?.required ?? body2?.required ?? body?.required,
|
|
79
|
+
responseSchema: response?.schema,
|
|
80
|
+
responseMediaType: response?.mediaType,
|
|
78
81
|
responseDescription: extractResponseDescription(op),
|
|
79
82
|
errorResponses: extractErrorResponses(op),
|
|
80
83
|
deprecated: false,
|
|
@@ -137,16 +140,44 @@ function resolveRefPath(ref, root) {
|
|
|
137
140
|
}
|
|
138
141
|
return current;
|
|
139
142
|
}
|
|
140
|
-
function
|
|
143
|
+
async function fetchSpec(url, headers) {
|
|
144
|
+
const response = await fetch(url, { headers });
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
const hint = response.status === 401 || response.status === 403
|
|
147
|
+
? ' — the spec needs credentials; pass them with --openapi-header "NAME: value"'
|
|
148
|
+
: '';
|
|
149
|
+
throw new Error(`Could not fetch the spec from ${url}: HTTP ${response.status}${hint}`);
|
|
150
|
+
}
|
|
151
|
+
return response.text();
|
|
152
|
+
}
|
|
153
|
+
const trimSlash = (url) => url.replace(/\/+$/, '');
|
|
154
|
+
function resolveServerUrl(server, specUrl) {
|
|
155
|
+
if (typeof server?.url !== 'string' || !server.url)
|
|
156
|
+
return undefined;
|
|
157
|
+
const url = server.url.replace(/\{([^}]+)\}/g, (match, name) => server.variables?.[name]?.default ?? match);
|
|
158
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(url))
|
|
159
|
+
return trimSlash(url);
|
|
160
|
+
if (specUrl)
|
|
161
|
+
return trimSlash(new URL(url, specUrl).toString());
|
|
162
|
+
return trimSlash(url);
|
|
163
|
+
}
|
|
164
|
+
function extractServerUrls(doc, specUrl) {
|
|
141
165
|
// OpenAPI 3.x
|
|
142
166
|
if (doc.servers && doc.servers.length > 0) {
|
|
143
|
-
return doc.servers
|
|
167
|
+
return doc.servers
|
|
168
|
+
.map((server) => resolveServerUrl(server, specUrl))
|
|
169
|
+
.filter((url) => !!url);
|
|
144
170
|
}
|
|
145
|
-
// Swagger 2.x
|
|
146
|
-
|
|
147
|
-
|
|
171
|
+
// Swagger 2.x: host defaults to the one serving the document
|
|
172
|
+
const host = doc.host ?? (specUrl ? new URL(specUrl).host : undefined);
|
|
173
|
+
if (host) {
|
|
174
|
+
const schemes = doc.schemes?.length
|
|
175
|
+
? doc.schemes
|
|
176
|
+
: specUrl
|
|
177
|
+
? [new URL(specUrl).protocol.replace(':', '')]
|
|
178
|
+
: ['https'];
|
|
148
179
|
const basePath = doc.basePath ?? '';
|
|
149
|
-
return schemes.map((scheme) => `${scheme}://${
|
|
180
|
+
return schemes.map((scheme) => trimSlash(`${scheme}://${host}${basePath}`));
|
|
150
181
|
}
|
|
151
182
|
return [];
|
|
152
183
|
}
|
|
@@ -155,10 +186,13 @@ function detectAuthType(doc) {
|
|
|
155
186
|
for (const scheme of Object.values(securitySchemes)) {
|
|
156
187
|
if (scheme.type === 'oauth2')
|
|
157
188
|
return 'oauth2';
|
|
158
|
-
if (scheme.type === 'http' && scheme.scheme === 'bearer')
|
|
189
|
+
if (scheme.type === 'http' && scheme.scheme?.toLowerCase() === 'bearer')
|
|
159
190
|
return 'bearer';
|
|
160
191
|
if (scheme.type === 'apiKey')
|
|
161
192
|
return 'apiKey';
|
|
193
|
+
if (scheme.type === 'basic' ||
|
|
194
|
+
(scheme.type === 'http' && scheme.scheme?.toLowerCase() === 'basic'))
|
|
195
|
+
return 'basic';
|
|
162
196
|
}
|
|
163
197
|
return 'none';
|
|
164
198
|
}
|
|
@@ -189,26 +223,49 @@ function extractParams(params, location) {
|
|
|
189
223
|
example: p.example,
|
|
190
224
|
}));
|
|
191
225
|
}
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
226
|
+
function pickContent(content) {
|
|
227
|
+
if (!content)
|
|
228
|
+
return undefined;
|
|
229
|
+
const mediaTypes = Object.keys(content);
|
|
230
|
+
const mediaType = mediaTypes.find((type) => /[/+]json\b/i.test(type) && content[type]?.schema) ?? mediaTypes.find((type) => content[type]?.schema);
|
|
231
|
+
if (!mediaType)
|
|
232
|
+
return undefined;
|
|
233
|
+
return { schema: content[mediaType].schema, mediaType };
|
|
234
|
+
}
|
|
235
|
+
function extractRequestBody(op, params, doc) {
|
|
236
|
+
// OpenAPI 3.x: op.requestBody.content[<media type>].schema
|
|
237
|
+
const fromContent = pickContent(op.requestBody?.content);
|
|
238
|
+
if (fromContent)
|
|
239
|
+
return fromContent;
|
|
207
240
|
// Swagger 2.x: parameters[].in === 'body' → schema
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
241
|
+
const consumes = op.consumes ?? doc.consumes ?? [];
|
|
242
|
+
const bodyParam = params.find((p) => p.in === 'body');
|
|
243
|
+
if (bodyParam?.schema) {
|
|
244
|
+
return {
|
|
245
|
+
schema: bodyParam.schema,
|
|
246
|
+
mediaType: consumes[0] ?? 'application/json',
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// Swagger 2.x: parameters[].in === 'formData' → one object body
|
|
250
|
+
const formParams = params.filter((p) => p.in === 'formData');
|
|
251
|
+
if (formParams.length > 0) {
|
|
252
|
+
const properties = {};
|
|
253
|
+
const required = [];
|
|
254
|
+
for (const p of formParams) {
|
|
255
|
+
properties[p.name] = {
|
|
256
|
+
...paramToSchema(p),
|
|
257
|
+
...(p.description ? { description: p.description } : {}),
|
|
258
|
+
};
|
|
259
|
+
if (p.required)
|
|
260
|
+
required.push(p.name);
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
schema: { type: 'object', properties, required },
|
|
264
|
+
mediaType: consumes.includes('multipart/form-data')
|
|
265
|
+
? 'multipart/form-data'
|
|
266
|
+
: 'application/x-www-form-urlencoded',
|
|
267
|
+
required: required.length > 0,
|
|
268
|
+
};
|
|
212
269
|
}
|
|
213
270
|
return undefined;
|
|
214
271
|
}
|
|
@@ -223,41 +280,38 @@ function extractResponseDescription(op) {
|
|
|
223
280
|
}
|
|
224
281
|
return undefined;
|
|
225
282
|
}
|
|
226
|
-
function
|
|
283
|
+
function extractResponse(op, doc) {
|
|
227
284
|
const responses = op.responses;
|
|
228
285
|
if (!responses)
|
|
229
286
|
return undefined;
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const resp = responses[code];
|
|
287
|
+
const produces = op.produces ?? doc.produces ?? [];
|
|
288
|
+
const fromResponse = (resp) => {
|
|
233
289
|
if (!resp)
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
290
|
+
return undefined;
|
|
291
|
+
const fromContent = pickContent(resp.content);
|
|
292
|
+
if (fromContent)
|
|
293
|
+
return fromContent;
|
|
294
|
+
if (resp.schema) {
|
|
295
|
+
return {
|
|
296
|
+
schema: resp.schema,
|
|
297
|
+
mediaType: produces.find((type) => /[/+]json\b/i.test(type)) ??
|
|
298
|
+
produces[0] ??
|
|
299
|
+
'application/json',
|
|
300
|
+
};
|
|
245
301
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
302
|
+
return undefined;
|
|
303
|
+
};
|
|
304
|
+
for (const code of ['200', '201', '202', '204']) {
|
|
305
|
+
const found = fromResponse(responses[code]);
|
|
306
|
+
if (found)
|
|
307
|
+
return found;
|
|
249
308
|
}
|
|
250
|
-
// Fallback: any 2xx
|
|
251
309
|
for (const [code, resp] of Object.entries(responses)) {
|
|
252
310
|
if (!code.startsWith('2'))
|
|
253
311
|
continue;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return jsonContent.schema;
|
|
258
|
-
}
|
|
259
|
-
if (resp.schema)
|
|
260
|
-
return resp.schema;
|
|
312
|
+
const found = fromResponse(resp);
|
|
313
|
+
if (found)
|
|
314
|
+
return found;
|
|
261
315
|
}
|
|
262
316
|
return undefined;
|
|
263
317
|
}
|
|
@@ -284,8 +338,11 @@ function extractSecuritySchemes(doc) {
|
|
|
284
338
|
const result = {};
|
|
285
339
|
for (const [name, scheme] of Object.entries(raw)) {
|
|
286
340
|
const info = {
|
|
287
|
-
type: scheme.type === '
|
|
341
|
+
type: scheme.type === 'basic' ? 'http' : scheme.type,
|
|
288
342
|
};
|
|
343
|
+
if (scheme.type === 'basic') {
|
|
344
|
+
info.scheme = 'basic';
|
|
345
|
+
}
|
|
289
346
|
if (scheme.type === 'http') {
|
|
290
347
|
info.scheme = scheme.scheme;
|
|
291
348
|
info.bearerFormat = scheme.bearerFormat;
|
|
@@ -296,10 +353,12 @@ function extractSecuritySchemes(doc) {
|
|
|
296
353
|
}
|
|
297
354
|
if (scheme.type === 'oauth2') {
|
|
298
355
|
// Extract flows — prefer authorizationCode, then implicit, then clientCredentials
|
|
299
|
-
const flows = scheme.flows ?? {};
|
|
300
|
-
const flow = flows.
|
|
356
|
+
const flows = scheme.flows ?? (scheme.flow ? { [scheme.flow]: scheme } : {});
|
|
357
|
+
const flow = flows.accessCode ??
|
|
358
|
+
flows.authorizationCode ??
|
|
301
359
|
flows.implicit ??
|
|
302
360
|
flows.clientCredentials ??
|
|
361
|
+
flows.application ??
|
|
303
362
|
flows.password;
|
|
304
363
|
if (flow) {
|
|
305
364
|
info.flows = {
|
|
@@ -371,3 +430,72 @@ function extractTagDescriptions(doc) {
|
|
|
371
430
|
}
|
|
372
431
|
return result;
|
|
373
432
|
}
|
|
433
|
+
const globToRegExp = (glob) => new RegExp(`^${glob
|
|
434
|
+
.split('*')
|
|
435
|
+
.map((part) => part.replace(/[.+?^${}()|[\]\\]/g, '\\$&'))
|
|
436
|
+
.join('.*')}$`, 'i');
|
|
437
|
+
/**
|
|
438
|
+
* `include` / `exclude` patterns match an operationId, a path, or
|
|
439
|
+
* `METHOD /path`, with `*` as a wildcard.
|
|
440
|
+
*/
|
|
441
|
+
function matchesAny(op, patterns) {
|
|
442
|
+
const candidates = [
|
|
443
|
+
op.operationId ?? '',
|
|
444
|
+
op.path,
|
|
445
|
+
`${op.method.toUpperCase()} ${op.path}`,
|
|
446
|
+
];
|
|
447
|
+
return patterns
|
|
448
|
+
.map(globToRegExp)
|
|
449
|
+
.some((pattern) => candidates.some((c) => pattern.test(c)));
|
|
450
|
+
}
|
|
451
|
+
export function filterOperations(spec, filter) {
|
|
452
|
+
const tags = filter.tags?.map((tag) => tag.toLowerCase());
|
|
453
|
+
const operations = spec.operations.filter((op) => (!tags?.length ||
|
|
454
|
+
op.tags.some((tag) => tags.includes(tag.toLowerCase()))) &&
|
|
455
|
+
(!filter.include?.length || matchesAny(op, filter.include)) &&
|
|
456
|
+
!(filter.exclude?.length && matchesAny(op, filter.exclude)));
|
|
457
|
+
return { ...spec, operations };
|
|
458
|
+
}
|
|
459
|
+
const AUTH_ROUTE = /(^|\/)(log-?in|log-?out|sign-?in|sign-?out|auth\w*|token|session|oauth2?)(\/|$)/i;
|
|
460
|
+
/**
|
|
461
|
+
* Specs served by API explorers often hide every authenticated route until
|
|
462
|
+
* the request carries a key, so a spec that is nearly empty — or holds only
|
|
463
|
+
* its own login route — is a sign the real one was never fetched.
|
|
464
|
+
*/
|
|
465
|
+
export function specCoverageWarning(spec) {
|
|
466
|
+
const count = spec.operations.length;
|
|
467
|
+
const onlyAuth = count > 0 && spec.operations.every((op) => AUTH_ROUTE.test(op.path));
|
|
468
|
+
if (count >= 5 && !onlyAuth)
|
|
469
|
+
return undefined;
|
|
470
|
+
const what = onlyAuth
|
|
471
|
+
? `only authentication routes (${spec.operations.map((op) => `${op.method.toUpperCase()} ${op.path}`).join(', ')})`
|
|
472
|
+
: `only ${count} operation${count === 1 ? '' : 's'}`;
|
|
473
|
+
return `The spec has ${what}. Many APIs publish their full spec only to an authenticated request — fetch it again with credentials (--openapi-header "NAME: value", or the key in the URL's query string if the API reads it there) before generating, or the addon will be missing most of the API.`;
|
|
474
|
+
}
|
|
475
|
+
const TOKEN_KEY = /^(access_?token|token|id_?token|jwt|api_?key|session_?token)$/i;
|
|
476
|
+
function findTokenPath(schema, prefix = '', depth = 0) {
|
|
477
|
+
if (!schema || typeof schema !== 'object' || depth > 3)
|
|
478
|
+
return undefined;
|
|
479
|
+
for (const [key, prop] of Object.entries(schema.properties ?? {})) {
|
|
480
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
481
|
+
if (TOKEN_KEY.test(key))
|
|
482
|
+
return path;
|
|
483
|
+
const nested = findTokenPath(prop, path, depth + 1);
|
|
484
|
+
if (nested)
|
|
485
|
+
return nested;
|
|
486
|
+
}
|
|
487
|
+
return undefined;
|
|
488
|
+
}
|
|
489
|
+
/** A login route that trades credentials for a token, if the spec has one. */
|
|
490
|
+
export function detectLoginOperation(spec) {
|
|
491
|
+
const candidates = spec.operations.filter((op) => op.method === 'post' &&
|
|
492
|
+
/(^|\/)(log-?in|sign-?in|auth(enticate)?|token|sessions?)$/i.test(op.path));
|
|
493
|
+
for (const op of candidates) {
|
|
494
|
+
return {
|
|
495
|
+
method: op.method,
|
|
496
|
+
path: op.path,
|
|
497
|
+
tokenPath: findTokenPath(op.responseSchema),
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|