raffel 1.1.74 → 1.1.76
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/README.md +7 -0
- package/dist/docs/docs-state.d.ts +2 -0
- package/dist/docs/docs-state.d.ts.map +1 -1
- package/dist/docs/docs-state.js +1 -0
- package/dist/docs/docs-state.js.map +1 -1
- package/dist/docs/generators/http-generator.d.ts +1 -1
- package/dist/docs/generators/http-generator.js +2 -2
- package/dist/docs/generators/http-generator.js.map +1 -1
- package/dist/docs/generators/usd-generator.d.ts +5 -1
- package/dist/docs/generators/usd-generator.d.ts.map +1 -1
- package/dist/docs/generators/usd-generator.js +5 -1
- package/dist/docs/generators/usd-generator.js.map +1 -1
- package/dist/docs/index.d.ts +1 -1
- package/dist/docs/index.d.ts.map +1 -1
- package/dist/docs/index.js +1 -1
- package/dist/docs/index.js.map +1 -1
- package/dist/docs/try-it-proxy.d.ts +16 -0
- package/dist/docs/try-it-proxy.d.ts.map +1 -0
- package/dist/docs/try-it-proxy.js +138 -0
- package/dist/docs/try-it-proxy.js.map +1 -0
- package/dist/docs/ui/assets/raffel-docs.css +266 -10
- package/dist/docs/ui/assets/raffel-docs.js +1167 -49
- package/dist/docs/ui/client-script/index.d.ts +1 -1
- package/dist/docs/ui/client-script/index.d.ts.map +1 -1
- package/dist/docs/ui/client-script/index.js +4 -2
- package/dist/docs/ui/client-script/index.js.map +1 -1
- package/dist/docs/ui/html-builder.d.ts.map +1 -1
- package/dist/docs/ui/html-builder.js +58 -2
- package/dist/docs/ui/html-builder.js.map +1 -1
- package/dist/docs/ui/response-examples.d.ts +10 -0
- package/dist/docs/ui/response-examples.d.ts.map +1 -0
- package/dist/docs/ui/response-examples.js +113 -0
- package/dist/docs/ui/response-examples.js.map +1 -0
- package/dist/docs/ui/runtime/index.js +1167 -49
- package/dist/docs/ui/runtime/index.js.map +1 -1
- package/dist/docs/ui/style-sections/schema-code.d.ts +1 -1
- package/dist/docs/ui/style-sections/schema-code.d.ts.map +1 -1
- package/dist/docs/ui/style-sections/schema-code.js +249 -0
- package/dist/docs/ui/style-sections/schema-code.js.map +1 -1
- package/dist/docs/ui/style-sections/shell.d.ts.map +1 -1
- package/dist/docs/ui/style-sections/shell.js +17 -10
- package/dist/docs/ui/style-sections/shell.js.map +1 -1
- package/dist/docs/ui/types.d.ts +23 -2
- package/dist/docs/ui/types.d.ts.map +1 -1
- package/dist/docs/usd-middleware.d.ts +23 -2
- package/dist/docs/usd-middleware.d.ts.map +1 -1
- package/dist/docs/usd-middleware.js +142 -6
- package/dist/docs/usd-middleware.js.map +1 -1
- package/dist/http/route-table.d.ts.map +1 -1
- package/dist/http/route-table.js +19 -0
- package/dist/http/route-table.js.map +1 -1
- package/dist/server/builder/execution-http-docs.d.ts.map +1 -1
- package/dist/server/builder/execution-http-docs.js +35 -3
- package/dist/server/builder/execution-http-docs.js.map +1 -1
- package/dist/server/builder.d.ts.map +1 -1
- package/dist/server/builder.js +4 -1
- package/dist/server/builder.js.map +1 -1
- package/dist/server/rest-middleware.d.ts +1 -1
- package/dist/server/rest-middleware.d.ts.map +1 -1
- package/dist/server/rest-middleware.js +36 -16
- package/dist/server/rest-middleware.js.map +1 -1
- package/dist/server/types/lifecycle-docs-types.d.ts +13 -2
- package/dist/server/types/lifecycle-docs-types.d.ts.map +1 -1
- package/dist/ui/docs/docs-state.d.ts +2 -0
- package/dist/ui/docs/docs-state.d.ts.map +1 -1
- package/dist/ui/docs/generators/http-generator.d.ts +1 -1
- package/dist/ui/docs/generators/usd-generator.d.ts +5 -1
- package/dist/ui/docs/generators/usd-generator.d.ts.map +1 -1
- package/dist/ui/docs/index.d.ts +1 -1
- package/dist/ui/docs/index.d.ts.map +1 -1
- package/dist/ui/docs/try-it-proxy.d.ts +16 -0
- package/dist/ui/docs/try-it-proxy.d.ts.map +1 -0
- package/dist/ui/docs/ui/client-script/index.d.ts +1 -1
- package/dist/ui/docs/ui/client-script/index.d.ts.map +1 -1
- package/dist/ui/docs/ui/html-builder.d.ts.map +1 -1
- package/dist/ui/docs/ui/response-examples.d.ts +10 -0
- package/dist/ui/docs/ui/response-examples.d.ts.map +1 -0
- package/dist/ui/docs/ui/style-sections/schema-code.d.ts +1 -1
- package/dist/ui/docs/ui/style-sections/schema-code.d.ts.map +1 -1
- package/dist/ui/docs/ui/style-sections/shell.d.ts.map +1 -1
- package/dist/ui/docs/ui/types.d.ts +23 -2
- package/dist/ui/docs/ui/types.d.ts.map +1 -1
- package/dist/ui/docs/usd-middleware.d.ts +23 -2
- package/dist/ui/docs/usd-middleware.d.ts.map +1 -1
- package/dist/ui/usd/export/openapi.d.ts.map +1 -1
- package/dist/ui/usd/index.d.ts +1 -1
- package/dist/ui/usd/index.d.ts.map +1 -1
- package/dist/ui/usd/spec/types.d.ts +43 -0
- package/dist/ui/usd/spec/types.d.ts.map +1 -1
- package/dist/ui/usd/validator/schema.d.ts +40 -0
- package/dist/ui/usd/validator/schema.d.ts.map +1 -1
- package/dist/usd/export/openapi.d.ts.map +1 -1
- package/dist/usd/export/openapi.js +4 -1
- package/dist/usd/export/openapi.js.map +1 -1
- package/dist/usd/index.d.ts +1 -1
- package/dist/usd/index.d.ts.map +1 -1
- package/dist/usd/index.js.map +1 -1
- package/dist/usd/spec/types.d.ts +43 -0
- package/dist/usd/spec/types.d.ts.map +1 -1
- package/dist/usd/spec/types.js.map +1 -1
- package/dist/usd/validator/schema.d.ts +40 -0
- package/dist/usd/validator/schema.d.ts.map +1 -1
- package/dist/usd/validator/schema.js +21 -0
- package/dist/usd/validator/schema.js.map +1 -1
- package/package.json +2 -1
|
@@ -32,6 +32,8 @@ const footerMarkdown = data.footerMarkdown ?? null;
|
|
|
32
32
|
const tocConfig = data.tocConfig ?? {};
|
|
33
33
|
const markdownConfig = data.markdownConfig ?? {};
|
|
34
34
|
const mermaidConfig = data.mermaidConfig ?? { enabled: true, src: 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js', viewer: true };
|
|
35
|
+
const serializedResponseExamples = data.responseExamples ?? {};
|
|
36
|
+
const tryItConfig = data.tryIt ?? { enabled: false, mode: 'direct' };
|
|
35
37
|
let mermaidLoadPromise = null;
|
|
36
38
|
const docsRepoConfig = (data.docsRepoConfig ?? null);
|
|
37
39
|
const breadcrumbsConfig = (data.breadcrumbsConfig && typeof data.breadcrumbsConfig === 'object')
|
|
@@ -39,6 +41,7 @@ const breadcrumbsConfig = (data.breadcrumbsConfig && typeof data.breadcrumbsConf
|
|
|
39
41
|
: { enabled: true, hideOnHome: true };
|
|
40
42
|
const pageNavConfig = data.pageNavConfig ?? { enabled: true, hide: [] };
|
|
41
43
|
const xUsd = spec['x-usd'] ?? {};
|
|
44
|
+
const authenticationConfig = spec['x-usd-authentication'] ?? {};
|
|
42
45
|
const { websocket: wsSpec = {}, graphql: graphqlSpec = {}, streams: streamsSpec = {}, jsonrpc: jsonrpcSpec = {}, grpc: grpcSpec = {}, tcp: tcpSpec = {}, udp: udpSpec = {} } = xUsd;
|
|
43
46
|
const docsRouteBase = String(xUsd.documentation?.routeBase ?? '').replace(/^#/, '').replace(/\/+$/, '');
|
|
44
47
|
const protocolData = detectProtocols();
|
|
@@ -52,6 +55,8 @@ const protocolRank = (name) => {
|
|
|
52
55
|
};
|
|
53
56
|
const protocols = Object.keys(protocolData).sort((a, b) => protocolRank(a) - protocolRank(b));
|
|
54
57
|
let activeProtocol = protocols[0] ?? 'http';
|
|
58
|
+
const environments = resolveEnvironments(spec.servers);
|
|
59
|
+
let selectedEnvironmentUrl = inferEnvironmentUrl(environments);
|
|
55
60
|
let searchQuery = '';
|
|
56
61
|
let routeState = parseRouteHash();
|
|
57
62
|
let activePagePath = resolveDocsAlias(routeState.pagePath);
|
|
@@ -859,6 +864,22 @@ function detectProtocols() {
|
|
|
859
864
|
out.udp = Object.keys(udpSpec.endpoints).length;
|
|
860
865
|
return out;
|
|
861
866
|
}
|
|
867
|
+
function mergePathItemParameters(pathParameters, operationParameters) {
|
|
868
|
+
const resolvedParameters = (parameters) => (Array.isArray(parameters) ? parameters.map(parameter => resolveSchema(parameter) ?? parameter) : []);
|
|
869
|
+
const inherited = resolvedParameters(pathParameters);
|
|
870
|
+
const declared = resolvedParameters(operationParameters);
|
|
871
|
+
const declaredKeys = new Set(declared.map(parameter => {
|
|
872
|
+
const resolved = resolveSchema(parameter);
|
|
873
|
+
return `${String(resolved?.in ?? '')}\0${String(resolved?.name ?? '')}`;
|
|
874
|
+
}));
|
|
875
|
+
return [
|
|
876
|
+
...inherited.filter(parameter => {
|
|
877
|
+
const resolved = resolveSchema(parameter);
|
|
878
|
+
return !declaredKeys.has(`${String(resolved?.in ?? '')}\0${String(resolved?.name ?? '')}`);
|
|
879
|
+
}),
|
|
880
|
+
...declared,
|
|
881
|
+
];
|
|
882
|
+
}
|
|
862
883
|
function getEndpointsForProtocol(protocol) {
|
|
863
884
|
const endpoints = [];
|
|
864
885
|
let id = 0;
|
|
@@ -876,7 +897,10 @@ function getEndpointsForProtocol(protocol) {
|
|
|
876
897
|
for (const [method, operation] of Object.entries(methods ?? {})) {
|
|
877
898
|
if (!['get', 'post', 'put', 'patch', 'delete'].includes(method))
|
|
878
899
|
continue;
|
|
879
|
-
add(path, method.toUpperCase(),
|
|
900
|
+
add(path, method.toUpperCase(), {
|
|
901
|
+
...operation,
|
|
902
|
+
parameters: mergePathItemParameters(methods.parameters, operation.parameters),
|
|
903
|
+
});
|
|
880
904
|
}
|
|
881
905
|
}
|
|
882
906
|
}
|
|
@@ -1202,11 +1226,19 @@ function appendSidebarGroup(nav, title, items) {
|
|
|
1202
1226
|
group.appendChild(itemContainer);
|
|
1203
1227
|
nav.appendChild(group);
|
|
1204
1228
|
}
|
|
1205
|
-
function generateExampleFromSchema(schema) {
|
|
1229
|
+
function generateExampleFromSchema(schema, refStack = new Set()) {
|
|
1206
1230
|
if (!schema)
|
|
1207
1231
|
return null;
|
|
1208
|
-
if (schema.$ref)
|
|
1209
|
-
|
|
1232
|
+
if (schema.$ref) {
|
|
1233
|
+
const pointer = String(schema.$ref);
|
|
1234
|
+
const name = pointer.split('/').pop() || pointer;
|
|
1235
|
+
if (refStack.has(pointer))
|
|
1236
|
+
return `Recursive schema: ${name}`;
|
|
1237
|
+
const resolved = resolveSchema(schema);
|
|
1238
|
+
if (resolved === schema)
|
|
1239
|
+
return `Unresolved schema reference: ${name}`;
|
|
1240
|
+
return generateExampleFromSchema(resolved, new Set([...refStack, pointer]));
|
|
1241
|
+
}
|
|
1210
1242
|
if (schema.example !== undefined)
|
|
1211
1243
|
return schema.example;
|
|
1212
1244
|
if (schema.default !== undefined)
|
|
@@ -1218,20 +1250,79 @@ function generateExampleFromSchema(schema) {
|
|
|
1218
1250
|
case 'number': return 0;
|
|
1219
1251
|
case 'integer': return 0;
|
|
1220
1252
|
case 'boolean': return true;
|
|
1221
|
-
case 'array': return schema.items ? [generateExampleFromSchema(schema.items)] : [];
|
|
1253
|
+
case 'array': return schema.items ? [generateExampleFromSchema(schema.items, refStack)] : [];
|
|
1222
1254
|
case 'object':
|
|
1223
1255
|
if (!schema.properties)
|
|
1224
1256
|
return {};
|
|
1225
1257
|
const obj = {};
|
|
1226
1258
|
Object.entries(schema.properties).forEach(([key, prop]) => {
|
|
1227
|
-
obj[key] = generateExampleFromSchema(prop);
|
|
1259
|
+
obj[key] = generateExampleFromSchema(prop, refStack);
|
|
1228
1260
|
});
|
|
1229
1261
|
return obj;
|
|
1230
1262
|
default: return null;
|
|
1231
1263
|
}
|
|
1232
1264
|
}
|
|
1265
|
+
function resolveEnvironments(servers) {
|
|
1266
|
+
const resolved = [];
|
|
1267
|
+
const list = Array.isArray(servers) && servers.length > 0
|
|
1268
|
+
? servers
|
|
1269
|
+
: [{ url: win.location?.origin ?? 'http://localhost:3000', description: 'Current origin' }];
|
|
1270
|
+
list.forEach((server, serverIndex) => {
|
|
1271
|
+
const template = String(server?.url ?? '');
|
|
1272
|
+
const variables = server?.variables && typeof server.variables === 'object' ? server.variables : {};
|
|
1273
|
+
let combinations = [{}];
|
|
1274
|
+
for (const [name, definition] of Object.entries(variables)) {
|
|
1275
|
+
const values = Array.isArray(definition?.enum) && definition.enum.length > 0
|
|
1276
|
+
? definition.enum.map(String)
|
|
1277
|
+
: [String(definition?.default ?? '')];
|
|
1278
|
+
combinations = combinations.flatMap(current => values.map(value => ({ ...current, [name]: value }))).slice(0, 64);
|
|
1279
|
+
}
|
|
1280
|
+
combinations.forEach((values, variantIndex) => {
|
|
1281
|
+
const expandedUrl = Object.entries(values).reduce((current, [name, value]) => current.replaceAll(`{${name}}`, encodeURIComponent(value)), template);
|
|
1282
|
+
const url = String(safeRuntimeUrl(expandedUrl)?.toString() ?? expandedUrl).replace(/\/$/, '');
|
|
1283
|
+
const described = String(server?.description ?? '');
|
|
1284
|
+
const label = Object.entries(values).reduce((current, [name, value]) => current.replaceAll(`{${name}}`, value), described) || Object.values(values).join(' / ') || `Server ${serverIndex + 1}`;
|
|
1285
|
+
resolved.push({ id: `${serverIndex}:${variantIndex}`, label, url, variables: values });
|
|
1286
|
+
});
|
|
1287
|
+
});
|
|
1288
|
+
return resolved;
|
|
1289
|
+
}
|
|
1290
|
+
function inferEnvironmentUrl(options) {
|
|
1291
|
+
if (options.length === 0)
|
|
1292
|
+
return String(win.location?.origin ?? 'http://localhost:3000');
|
|
1293
|
+
const current = safeRuntimeUrl(String(win.location?.href ?? ''));
|
|
1294
|
+
if (!current)
|
|
1295
|
+
return options[0].url;
|
|
1296
|
+
const scored = options.map((environment, index) => {
|
|
1297
|
+
const candidate = safeRuntimeUrl(environment.url);
|
|
1298
|
+
if (!candidate)
|
|
1299
|
+
return { environment, score: -index };
|
|
1300
|
+
let score = -index;
|
|
1301
|
+
if (candidate.origin === current.origin)
|
|
1302
|
+
score += 1000;
|
|
1303
|
+
if (current.href.startsWith(candidate.href.replace(/\/$/, '')))
|
|
1304
|
+
score += 100;
|
|
1305
|
+
if (candidate.hostname === current.hostname)
|
|
1306
|
+
score += 50;
|
|
1307
|
+
score += candidate.pathname === '/' ? 0 : candidate.pathname.length;
|
|
1308
|
+
return { environment, score };
|
|
1309
|
+
});
|
|
1310
|
+
scored.sort((a, b) => b.score - a.score);
|
|
1311
|
+
return scored[0].environment.url;
|
|
1312
|
+
}
|
|
1313
|
+
function safeRuntimeUrl(value) {
|
|
1314
|
+
try {
|
|
1315
|
+
return new URL(value, win.location?.origin ?? 'http://localhost:3000');
|
|
1316
|
+
}
|
|
1317
|
+
catch {
|
|
1318
|
+
return null;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
function selectedEnvironment() {
|
|
1322
|
+
return environments.find(environment => environment.url === selectedEnvironmentUrl) ?? environments[0];
|
|
1323
|
+
}
|
|
1233
1324
|
function httpBaseUrl() {
|
|
1234
|
-
return String(spec?.servers?.[0]?.url ?? 'http://localhost:3000').replace(/\/$/, '');
|
|
1325
|
+
return String(selectedEnvironment()?.url ?? spec?.servers?.[0]?.url ?? 'http://localhost:3000').replace(/\/$/, '');
|
|
1235
1326
|
}
|
|
1236
1327
|
// Collect JSON-Schema validation constraints as readable chips.
|
|
1237
1328
|
function schemaConstraintChips(schema) {
|
|
@@ -1320,47 +1411,176 @@ function pyLiteral(value) {
|
|
|
1320
1411
|
}
|
|
1321
1412
|
return 'None';
|
|
1322
1413
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1414
|
+
function parameterSample(parameter) {
|
|
1415
|
+
const schema = parameter?.schema ?? {};
|
|
1416
|
+
if (parameter?.example !== undefined)
|
|
1417
|
+
return parameter.example;
|
|
1418
|
+
if (schema.example !== undefined)
|
|
1419
|
+
return schema.example;
|
|
1420
|
+
if (schema.default !== undefined)
|
|
1421
|
+
return schema.default;
|
|
1422
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0)
|
|
1423
|
+
return schema.enum[0];
|
|
1424
|
+
return parameter?.required ? generateExampleFromSchema(schema) : undefined;
|
|
1425
|
+
}
|
|
1426
|
+
function serializeParameter(value) {
|
|
1427
|
+
if (Array.isArray(value))
|
|
1428
|
+
return value.map(item => serializeParameter(item)).join(',');
|
|
1429
|
+
if (value && typeof value === 'object')
|
|
1430
|
+
return JSON.stringify(value);
|
|
1431
|
+
return String(value ?? '');
|
|
1432
|
+
}
|
|
1433
|
+
function hasStoredCredential(schemeName) {
|
|
1434
|
+
const scheme = spec?.components?.securitySchemes?.[schemeName];
|
|
1435
|
+
const credential = readStoredCredential(schemeName);
|
|
1436
|
+
if (!scheme)
|
|
1437
|
+
return false;
|
|
1438
|
+
if (scheme.type === 'http' && String(scheme.scheme).toLowerCase() === 'bearer')
|
|
1439
|
+
return Boolean(credential.accessToken);
|
|
1440
|
+
if (scheme.type === 'http' && String(scheme.scheme).toLowerCase() === 'basic')
|
|
1441
|
+
return Boolean(credential.username || credential.password);
|
|
1442
|
+
if (scheme.type === 'oauth2' || scheme.type === 'openIdConnect')
|
|
1443
|
+
return Boolean(credential.accessToken);
|
|
1444
|
+
if (scheme.type === 'apiKey')
|
|
1445
|
+
return Boolean(credential.apiKey);
|
|
1446
|
+
return false;
|
|
1447
|
+
}
|
|
1448
|
+
function selectedSecurityRequirement(operation) {
|
|
1449
|
+
const requirements = operation.security === undefined ? (spec.security ?? []) : operation.security;
|
|
1450
|
+
if (!Array.isArray(requirements) || requirements.length === 0)
|
|
1451
|
+
return null;
|
|
1452
|
+
return requirements.find(requirement => (requirement &&
|
|
1453
|
+
typeof requirement === 'object' &&
|
|
1454
|
+
Object.keys(requirement).every(hasStoredCredential))) ?? requirements[0];
|
|
1455
|
+
}
|
|
1456
|
+
function buildStructuredHttpRequest(endpoint, operation) {
|
|
1457
|
+
const method = String(endpoint.method || 'GET').toUpperCase();
|
|
1458
|
+
const requestContent = operation.requestBody?.content;
|
|
1459
|
+
const body = requestContent ? operationRequestExample(operation) : null;
|
|
1460
|
+
const headers = {};
|
|
1461
|
+
let url = `${httpBaseUrl()}${endpoint.path || '/'}`;
|
|
1462
|
+
if (body !== null && body !== undefined && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
|
|
1463
|
+
headers['Content-Type'] = 'application/json';
|
|
1464
|
+
}
|
|
1465
|
+
const query = new URLSearchParams();
|
|
1466
|
+
const cookies = [];
|
|
1467
|
+
for (const parameter of operation.parameters ?? []) {
|
|
1468
|
+
const sample = parameterSample(parameter);
|
|
1469
|
+
if (sample === undefined)
|
|
1470
|
+
continue;
|
|
1471
|
+
const serialized = serializeParameter(sample);
|
|
1472
|
+
if (parameter.in === 'path') {
|
|
1473
|
+
url = url.replaceAll(`{${parameter.name}}`, encodeURIComponent(serialized)).replaceAll(`:${parameter.name}`, encodeURIComponent(serialized));
|
|
1474
|
+
}
|
|
1475
|
+
else if (parameter.in === 'query') {
|
|
1476
|
+
query.set(parameter.name, serialized);
|
|
1477
|
+
}
|
|
1478
|
+
else if (parameter.in === 'header') {
|
|
1479
|
+
headers[parameter.name] = serialized;
|
|
1480
|
+
}
|
|
1481
|
+
else if (parameter.in === 'cookie') {
|
|
1482
|
+
cookies.push(`${parameter.name}=${encodeURIComponent(serialized)}`);
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
const queryText = query.toString();
|
|
1486
|
+
if (queryText)
|
|
1487
|
+
url += `${url.includes('?') ? '&' : '?'}${queryText}`;
|
|
1488
|
+
if (cookies.length > 0)
|
|
1489
|
+
headers.Cookie = cookies.join('; ');
|
|
1490
|
+
const requirement = selectedSecurityRequirement(operation);
|
|
1491
|
+
if (requirement && typeof requirement === 'object') {
|
|
1492
|
+
for (const schemeName of Object.keys(requirement)) {
|
|
1493
|
+
const scheme = spec?.components?.securitySchemes?.[schemeName];
|
|
1494
|
+
const credential = readStoredCredential(schemeName);
|
|
1495
|
+
if (!scheme)
|
|
1496
|
+
continue;
|
|
1497
|
+
if (scheme.type === 'http' && String(scheme.scheme).toLowerCase() === 'bearer' && credential.accessToken) {
|
|
1498
|
+
headers.Authorization = `Bearer ${credential.accessToken}`;
|
|
1499
|
+
}
|
|
1500
|
+
else if (scheme.type === 'http' && String(scheme.scheme).toLowerCase() === 'basic' && credential.username) {
|
|
1501
|
+
const encoded = typeof win.btoa === 'function'
|
|
1502
|
+
? win.btoa(`${credential.username}:${credential.password ?? ''}`)
|
|
1503
|
+
: `${credential.username}:${credential.password ?? ''}`;
|
|
1504
|
+
headers.Authorization = `Basic ${encoded}`;
|
|
1505
|
+
}
|
|
1506
|
+
else if ((scheme.type === 'oauth2' || scheme.type === 'openIdConnect') && credential.accessToken) {
|
|
1507
|
+
headers.Authorization = `${credential.tokenType || 'Bearer'} ${credential.accessToken}`;
|
|
1508
|
+
}
|
|
1509
|
+
else if (scheme.type === 'apiKey' && credential.apiKey) {
|
|
1510
|
+
if (scheme.in === 'query') {
|
|
1511
|
+
const parsed = safeRuntimeUrl(url);
|
|
1512
|
+
if (parsed) {
|
|
1513
|
+
parsed.searchParams.set(scheme.name || schemeName, credential.apiKey);
|
|
1514
|
+
url = parsed.toString();
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
else if (scheme.in === 'cookie') {
|
|
1518
|
+
headers.Cookie = `${scheme.name || schemeName}=${credential.apiKey}`;
|
|
1519
|
+
}
|
|
1520
|
+
else {
|
|
1521
|
+
headers[scheme.name || schemeName] = credential.apiKey;
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
return { method, url, headers, body };
|
|
1527
|
+
}
|
|
1528
|
+
// Build display-only request samples. Execution always uses the structured
|
|
1529
|
+
// fetch request model, never one of these language snippets.
|
|
1530
|
+
function buildHttpSamples(request) {
|
|
1531
|
+
const { method, url, body } = request;
|
|
1325
1532
|
const hasBody = body !== null && body !== undefined && ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method);
|
|
1326
1533
|
const jsonPretty = hasBody ? JSON.stringify(body, null, 2) : '';
|
|
1327
1534
|
const samples = {};
|
|
1328
1535
|
let curl = `curl -X ${method} "${url}"`;
|
|
1536
|
+
Object.entries(request.headers).forEach(([name, value]) => { curl += ` \\\n -H ${JSON.stringify(`${name}: ${value}`)}`; });
|
|
1329
1537
|
if (hasBody)
|
|
1330
|
-
curl += ` \\\n -
|
|
1538
|
+
curl += ` \\\n -d '${JSON.stringify(body)}'`;
|
|
1331
1539
|
samples.curl = curl;
|
|
1332
1540
|
let js = `const res = await fetch(${JSON.stringify(url)}, {\n method: ${JSON.stringify(method)},`;
|
|
1541
|
+
if (Object.keys(request.headers).length > 0)
|
|
1542
|
+
js += `\n headers: ${JSON.stringify(request.headers, null, 2).split('\n').join('\n ')},`;
|
|
1333
1543
|
if (hasBody)
|
|
1334
|
-
js += `\n
|
|
1544
|
+
js += `\n body: JSON.stringify(${jsonPretty.split('\n').join('\n ')}),`;
|
|
1335
1545
|
js += `\n});\nconst data = await res.json();\nconsole.log(data);`;
|
|
1336
|
-
samples.
|
|
1546
|
+
samples.typescript = js;
|
|
1337
1547
|
let py = `import requests\n\nres = requests.${method.toLowerCase()}(\n ${JSON.stringify(url)},`;
|
|
1548
|
+
if (Object.keys(request.headers).length > 0)
|
|
1549
|
+
py += `\n headers=${JSON.stringify(request.headers)},`;
|
|
1338
1550
|
if (hasBody)
|
|
1339
1551
|
py += `\n json=${pyLiteral(body)},`;
|
|
1340
1552
|
py += `\n)\nprint(res.json())`;
|
|
1341
1553
|
samples.python = py;
|
|
1342
1554
|
let rust = `use reqwest::Client;\n\nlet client = Client::new();\nlet res = client\n .${method.toLowerCase()}(${JSON.stringify(url)})`;
|
|
1555
|
+
Object.entries(request.headers).forEach(([name, value]) => { rust += `\n .header(${JSON.stringify(name)}, ${JSON.stringify(value)})`; });
|
|
1343
1556
|
if (hasBody)
|
|
1344
1557
|
rust += `\n .json(&serde_json::json!(${jsonPretty.split('\n').join('\n ')}))`;
|
|
1345
1558
|
rust += `\n .send()\n .await?;\nlet body = res.text().await?;\nprintln!("{}", body);`;
|
|
1346
1559
|
samples.rust = rust;
|
|
1560
|
+
const goImports = hasBody ? '"net/http"\n "strings"' : '"net/http"';
|
|
1561
|
+
const goBody = hasBody ? `strings.NewReader(${JSON.stringify(JSON.stringify(body))})` : 'nil';
|
|
1562
|
+
let go = `package main\n\nimport (\n ${goImports}\n)\n\nfunc main() {\n req, err := http.NewRequest(${JSON.stringify(method)}, ${JSON.stringify(url)}, ${goBody})\n if err != nil { panic(err) }`;
|
|
1563
|
+
Object.entries(request.headers).forEach(([name, value]) => { go += `\n req.Header.Set(${JSON.stringify(name)}, ${JSON.stringify(value)})`; });
|
|
1564
|
+
go += `\n res, err := http.DefaultClient.Do(req)\n if err != nil { panic(err) }\n defer res.Body.Close()\n}`;
|
|
1565
|
+
samples.go = go;
|
|
1347
1566
|
return samples;
|
|
1348
1567
|
}
|
|
1349
1568
|
function renderCodeExamples(endpoint, data) {
|
|
1350
|
-
const
|
|
1351
|
-
const
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1569
|
+
const samples = buildHttpSamples(buildStructuredHttpRequest(endpoint, data));
|
|
1570
|
+
const langs = [
|
|
1571
|
+
['curl', 'cURL', 'bash'],
|
|
1572
|
+
['typescript', 'TypeScript', 'typescript'],
|
|
1573
|
+
['rust', 'Rust', 'rust'],
|
|
1574
|
+
['python', 'Python', 'python'],
|
|
1575
|
+
['go', 'Go', 'go'],
|
|
1576
|
+
];
|
|
1357
1577
|
const wrap = doc.createElement('div');
|
|
1358
1578
|
wrap.className = 'http-code-samples';
|
|
1359
1579
|
const tabs = doc.createElement('div');
|
|
1360
1580
|
tabs.className = 'code-tabs';
|
|
1361
1581
|
const contents = doc.createElement('div');
|
|
1362
1582
|
contents.className = 'code-contents';
|
|
1363
|
-
langs.forEach(([key, label], index) => {
|
|
1583
|
+
langs.forEach(([key, label, language], index) => {
|
|
1364
1584
|
const tab = doc.createElement('button');
|
|
1365
1585
|
tab.type = 'button';
|
|
1366
1586
|
tab.className = `code-tab${index === 0 ? ' active' : ''}`;
|
|
@@ -1369,7 +1589,10 @@ function renderCodeExamples(endpoint, data) {
|
|
|
1369
1589
|
content.className = `code-content${index === 0 ? ' active' : ''}`;
|
|
1370
1590
|
const pre = doc.createElement('pre');
|
|
1371
1591
|
pre.className = 'http-code-sample-pre';
|
|
1372
|
-
|
|
1592
|
+
const code = doc.createElement('code');
|
|
1593
|
+
code.className = `language-${language}`;
|
|
1594
|
+
code.textContent = samples[key];
|
|
1595
|
+
pre.appendChild(code);
|
|
1373
1596
|
content.appendChild(pre);
|
|
1374
1597
|
tab.onclick = () => {
|
|
1375
1598
|
tabs.querySelectorAll('.code-tab').forEach((t) => t.classList.remove('active'));
|
|
@@ -1400,6 +1623,71 @@ function renderCodeExamples(endpoint, data) {
|
|
|
1400
1623
|
wrap.appendChild(contents);
|
|
1401
1624
|
return wrap;
|
|
1402
1625
|
}
|
|
1626
|
+
async function executeStructuredHttpRequest(endpoint, operation, button, result) {
|
|
1627
|
+
button.disabled = true;
|
|
1628
|
+
result.textContent = 'Sending request…';
|
|
1629
|
+
try {
|
|
1630
|
+
await refreshCredentialsBeforeRequest(operation);
|
|
1631
|
+
const request = buildStructuredHttpRequest(endpoint, operation);
|
|
1632
|
+
let status;
|
|
1633
|
+
let statusText;
|
|
1634
|
+
let responseHeaders = {};
|
|
1635
|
+
let responseBody;
|
|
1636
|
+
if (tryItConfig.mode === 'proxy') {
|
|
1637
|
+
const proxyResponse = await win.fetch?.(tryItConfig.proxyUrl, {
|
|
1638
|
+
method: 'POST',
|
|
1639
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1640
|
+
body: JSON.stringify(request),
|
|
1641
|
+
});
|
|
1642
|
+
if (!proxyResponse)
|
|
1643
|
+
throw new Error('Fetch is not available in this browser.');
|
|
1644
|
+
const payload = await proxyResponse.json();
|
|
1645
|
+
if (!proxyResponse.ok)
|
|
1646
|
+
throw new Error(payload?.detail || payload?.message || `Proxy failed with ${proxyResponse.status}.`);
|
|
1647
|
+
status = payload.status;
|
|
1648
|
+
statusText = payload.statusText ?? '';
|
|
1649
|
+
responseHeaders = payload.headers ?? {};
|
|
1650
|
+
responseBody = payload.body ?? '';
|
|
1651
|
+
}
|
|
1652
|
+
else {
|
|
1653
|
+
const response = await win.fetch?.(request.url, {
|
|
1654
|
+
method: request.method,
|
|
1655
|
+
headers: request.headers,
|
|
1656
|
+
body: request.body === null || request.body === undefined ? undefined : JSON.stringify(request.body),
|
|
1657
|
+
credentials: 'include',
|
|
1658
|
+
});
|
|
1659
|
+
if (!response)
|
|
1660
|
+
throw new Error('Fetch is not available in this browser.');
|
|
1661
|
+
status = response.status;
|
|
1662
|
+
statusText = response.statusText;
|
|
1663
|
+
response.headers?.forEach?.((value, name) => { responseHeaders[name] = value; });
|
|
1664
|
+
responseBody = await response.text();
|
|
1665
|
+
}
|
|
1666
|
+
const headerText = Object.entries(responseHeaders).map(([name, value]) => `${name}: ${value}`).join('\n');
|
|
1667
|
+
result.textContent = `${status} ${statusText}`.trim() + `${headerText ? `\n${headerText}` : ''}${responseBody ? `\n\n${responseBody}` : ''}`;
|
|
1668
|
+
}
|
|
1669
|
+
catch (error) {
|
|
1670
|
+
result.textContent = `Request failed: ${String(error?.message ?? error)}`;
|
|
1671
|
+
}
|
|
1672
|
+
finally {
|
|
1673
|
+
button.disabled = false;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
function renderHttpExecutor(endpoint, operation) {
|
|
1677
|
+
const wrapper = doc.createElement('div');
|
|
1678
|
+
wrapper.className = 'http-try';
|
|
1679
|
+
const button = doc.createElement('button');
|
|
1680
|
+
button.type = 'button';
|
|
1681
|
+
button.className = 'http-try-run';
|
|
1682
|
+
button.textContent = 'Run request';
|
|
1683
|
+
const result = doc.createElement('pre');
|
|
1684
|
+
result.className = 'http-try-result';
|
|
1685
|
+
result.textContent = tryItConfig.mode === 'proxy' ? 'Ready · server proxy' : 'Ready · browser fetch';
|
|
1686
|
+
button.onclick = () => { void executeStructuredHttpRequest(endpoint, operation, button, result); };
|
|
1687
|
+
wrapper.appendChild(button);
|
|
1688
|
+
wrapper.appendChild(result);
|
|
1689
|
+
return wrapper;
|
|
1690
|
+
}
|
|
1403
1691
|
// Append a JSON example block to a container.
|
|
1404
1692
|
// Syntax-highlight a JSON value into HTML using the .sample-json token classes.
|
|
1405
1693
|
function highlightJsonHtml(value) {
|
|
@@ -1420,7 +1708,7 @@ function highlightJsonHtml(value) {
|
|
|
1420
1708
|
}
|
|
1421
1709
|
// Right-panel response samples: one status tab per response, each showing the
|
|
1422
1710
|
// generated JSON example (colorized) — ReDoc's response sample switcher.
|
|
1423
|
-
function renderResponseSamples(responses) {
|
|
1711
|
+
function renderResponseSamples(responses, endpoint) {
|
|
1424
1712
|
const wrap = doc.createElement('div');
|
|
1425
1713
|
const tabs = doc.createElement('div');
|
|
1426
1714
|
tabs.className = 'sample-tabs';
|
|
@@ -1438,22 +1726,46 @@ function renderResponseSamples(responses) {
|
|
|
1438
1726
|
tab.textContent = status;
|
|
1439
1727
|
const content = doc.createElement('div');
|
|
1440
1728
|
content.className = `sample-content${index === 0 ? ' active' : ''}`;
|
|
1441
|
-
const
|
|
1442
|
-
const
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1729
|
+
const operationKey = `${String(endpoint.method).toUpperCase()} ${endpoint.path}`;
|
|
1730
|
+
const mediaEntries = Object.entries(resp.content ?? {});
|
|
1731
|
+
mediaEntries.forEach(([mediaType, media]) => {
|
|
1732
|
+
const mediaBlock = doc.createElement('div');
|
|
1733
|
+
mediaBlock.className = 'response-media-sample';
|
|
1734
|
+
const typeLine = doc.createElement('div');
|
|
1735
|
+
typeLine.className = 'sample-content-type';
|
|
1736
|
+
typeLine.textContent = mediaType;
|
|
1737
|
+
mediaBlock.appendChild(typeLine);
|
|
1738
|
+
const prepared = serializedResponseExamples?.[operationKey]?.[status]?.[mediaType];
|
|
1739
|
+
const isToonl = mediaType.toLowerCase().includes('toonl');
|
|
1740
|
+
const examples = prepared?.length
|
|
1741
|
+
? prepared
|
|
1742
|
+
: isToonl
|
|
1743
|
+
? [{ name: 'example required', language: 'text', value: 'TOONL examples require an explicit array of flat object records.', error: true }]
|
|
1744
|
+
: media?.schema
|
|
1745
|
+
? [{ name: 'generated', language: 'json', value: JSON.stringify(generateExampleFromSchema(media.schema), null, 2) }]
|
|
1746
|
+
: [];
|
|
1747
|
+
examples.forEach(example => {
|
|
1748
|
+
const label = doc.createElement('div');
|
|
1749
|
+
label.className = 'response-example-name';
|
|
1750
|
+
label.textContent = example.summary || example.name;
|
|
1751
|
+
mediaBlock.appendChild(label);
|
|
1752
|
+
const pre = doc.createElement('pre');
|
|
1753
|
+
pre.className = `sample-code${example.error ? ' sample-code-error' : ''}`;
|
|
1754
|
+
const code = doc.createElement('code');
|
|
1755
|
+
code.className = `language-${example.language || 'text'}`;
|
|
1756
|
+
code.textContent = example.value;
|
|
1757
|
+
pre.appendChild(code);
|
|
1758
|
+
mediaBlock.appendChild(pre);
|
|
1759
|
+
});
|
|
1760
|
+
if (examples.length === 0) {
|
|
1761
|
+
const empty = doc.createElement('div');
|
|
1762
|
+
empty.className = 'no-example';
|
|
1763
|
+
empty.textContent = 'No example provided.';
|
|
1764
|
+
mediaBlock.appendChild(empty);
|
|
1450
1765
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
content.appendChild(pre);
|
|
1455
|
-
}
|
|
1456
|
-
else {
|
|
1766
|
+
content.appendChild(mediaBlock);
|
|
1767
|
+
});
|
|
1768
|
+
if (mediaEntries.length === 0) {
|
|
1457
1769
|
const empty = doc.createElement('div');
|
|
1458
1770
|
empty.className = 'no-example';
|
|
1459
1771
|
empty.textContent = resp.description || 'No response body.';
|
|
@@ -1475,6 +1787,7 @@ function renderResponseSamples(responses) {
|
|
|
1475
1787
|
function renderSchemaTree(parent, schema, depth = 0) {
|
|
1476
1788
|
if (!schema)
|
|
1477
1789
|
return;
|
|
1790
|
+
schema = resolveSchema(schema);
|
|
1478
1791
|
const div = doc.createElement('div');
|
|
1479
1792
|
div.style.paddingLeft = depth > 0 ? `${depth * 12}px` : '0';
|
|
1480
1793
|
div.style.marginTop = depth === 0 ? '8px' : '0';
|
|
@@ -1482,7 +1795,7 @@ function renderSchemaTree(parent, schema, depth = 0) {
|
|
|
1482
1795
|
const refName = String(schema.$ref).split('/').pop();
|
|
1483
1796
|
const row = doc.createElement('div');
|
|
1484
1797
|
row.style.padding = '4px 0';
|
|
1485
|
-
row.innerHTML = `<span style="color: #888; font-style: italic;"
|
|
1798
|
+
row.innerHTML = `<span style="color: #888; font-style: italic;">Unresolved schema reference: ${esc(refName)}</span>`;
|
|
1486
1799
|
div.appendChild(row);
|
|
1487
1800
|
}
|
|
1488
1801
|
else if (schema.type === 'object' && schema.properties) {
|
|
@@ -1603,16 +1916,44 @@ function renderResponseAccordion(status, resp, openByDefault) {
|
|
|
1603
1916
|
body.appendChild(block);
|
|
1604
1917
|
}
|
|
1605
1918
|
const content = resp.content;
|
|
1606
|
-
const contentType
|
|
1607
|
-
const schema = contentType ? content[contentType]?.schema : null;
|
|
1608
|
-
if (schema) {
|
|
1919
|
+
for (const [contentType, media] of Object.entries(content ?? {})) {
|
|
1609
1920
|
const block = doc.createElement('div');
|
|
1610
1921
|
block.className = 'response-block';
|
|
1611
1922
|
const sub = doc.createElement('div');
|
|
1612
1923
|
sub.className = 'response-subhead';
|
|
1613
|
-
sub.textContent = `Response Body
|
|
1924
|
+
sub.textContent = `Response Body · ${contentType}`;
|
|
1614
1925
|
block.appendChild(sub);
|
|
1615
|
-
|
|
1926
|
+
if (media?.schema)
|
|
1927
|
+
renderSchemaTree(block, media.schema);
|
|
1928
|
+
else {
|
|
1929
|
+
const note = doc.createElement('div');
|
|
1930
|
+
note.className = 'response-desc-only';
|
|
1931
|
+
note.textContent = media?.examples || 'example' in (media ?? {}) ? 'Example available' : 'No schema provided.';
|
|
1932
|
+
block.appendChild(note);
|
|
1933
|
+
}
|
|
1934
|
+
body.appendChild(block);
|
|
1935
|
+
}
|
|
1936
|
+
if (resp.links && Object.keys(resp.links).length > 0) {
|
|
1937
|
+
const block = doc.createElement('div');
|
|
1938
|
+
block.className = 'response-block response-links';
|
|
1939
|
+
const sub = doc.createElement('div');
|
|
1940
|
+
sub.className = 'response-subhead';
|
|
1941
|
+
sub.textContent = 'Response links';
|
|
1942
|
+
block.appendChild(sub);
|
|
1943
|
+
Object.entries(resp.links).forEach(([name, rawLink]) => {
|
|
1944
|
+
const link = resolveSchema(rawLink);
|
|
1945
|
+
const card = doc.createElement('div');
|
|
1946
|
+
card.className = 'response-link';
|
|
1947
|
+
const target = link?.operationId || link?.operationRef || 'Linked operation';
|
|
1948
|
+
card.innerHTML = `<strong>${esc(name)}</strong><code>${esc(target)}</code>${link?.description ? `<span>${esc(link.description)}</span>` : ''}`;
|
|
1949
|
+
if (link?.parameters && Object.keys(link.parameters).length > 0) {
|
|
1950
|
+
const parameters = doc.createElement('pre');
|
|
1951
|
+
parameters.className = 'response-link-parameters';
|
|
1952
|
+
parameters.textContent = JSON.stringify(link.parameters, null, 2);
|
|
1953
|
+
card.appendChild(parameters);
|
|
1954
|
+
}
|
|
1955
|
+
block.appendChild(card);
|
|
1956
|
+
});
|
|
1616
1957
|
body.appendChild(block);
|
|
1617
1958
|
}
|
|
1618
1959
|
if (!body.children.length) {
|
|
@@ -1626,6 +1967,51 @@ function renderResponseAccordion(status, resp, openByDefault) {
|
|
|
1626
1967
|
acc.appendChild(body);
|
|
1627
1968
|
return acc;
|
|
1628
1969
|
}
|
|
1970
|
+
const OPENAPI_HTTP_METHODS = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
|
|
1971
|
+
function appendPathItemContract(container, expression, rawPathItem) {
|
|
1972
|
+
const pathItem = resolveSchema(rawPathItem);
|
|
1973
|
+
const contract = doc.createElement('div');
|
|
1974
|
+
contract.className = 'async-contract';
|
|
1975
|
+
const expressionLine = doc.createElement('code');
|
|
1976
|
+
expressionLine.className = 'async-contract-expression';
|
|
1977
|
+
expressionLine.textContent = expression;
|
|
1978
|
+
contract.appendChild(expressionLine);
|
|
1979
|
+
OPENAPI_HTTP_METHODS.forEach(method => {
|
|
1980
|
+
const operation = pathItem?.[method];
|
|
1981
|
+
if (!operation)
|
|
1982
|
+
return;
|
|
1983
|
+
const row = doc.createElement('div');
|
|
1984
|
+
row.className = 'async-contract-operation';
|
|
1985
|
+
const requestTypes = Object.keys(operation.requestBody?.content ?? {});
|
|
1986
|
+
const statuses = Object.keys(operation.responses ?? {});
|
|
1987
|
+
row.innerHTML = `<span class="badge badge-${method}">${method.toUpperCase()}</span><strong>${esc(operation.summary || operation.operationId || expression)}</strong>${requestTypes.length ? `<span>body: ${esc(requestTypes.join(', '))}</span>` : ''}${statuses.length ? `<span>responses: ${esc(statuses.join(', '))}</span>` : ''}`;
|
|
1988
|
+
contract.appendChild(row);
|
|
1989
|
+
});
|
|
1990
|
+
container.appendChild(contract);
|
|
1991
|
+
}
|
|
1992
|
+
function renderAsyncContracts(title, entries, callbacks = false) {
|
|
1993
|
+
if (!entries || Object.keys(entries).length === 0)
|
|
1994
|
+
return null;
|
|
1995
|
+
const section = doc.createElement('section');
|
|
1996
|
+
section.className = `async-contracts ${title.toLowerCase()}-section`;
|
|
1997
|
+
section.innerHTML = `<div class="subsection-label">${esc(title.toUpperCase())}</div><h2>${esc(title)}</h2><p class="async-contract-note">Read-only contract. Raffel never executes callbacks or webhooks from this page.</p>`;
|
|
1998
|
+
Object.entries(entries).forEach(([name, rawEntry]) => {
|
|
1999
|
+
const entry = resolveSchema(rawEntry);
|
|
2000
|
+
const group = doc.createElement('div');
|
|
2001
|
+
group.className = 'async-contract-group';
|
|
2002
|
+
const heading = doc.createElement('h3');
|
|
2003
|
+
heading.textContent = name;
|
|
2004
|
+
group.appendChild(heading);
|
|
2005
|
+
if (callbacks) {
|
|
2006
|
+
Object.entries(entry ?? {}).forEach(([expression, pathItem]) => appendPathItemContract(group, expression, pathItem));
|
|
2007
|
+
}
|
|
2008
|
+
else {
|
|
2009
|
+
appendPathItemContract(group, name, entry);
|
|
2010
|
+
}
|
|
2011
|
+
section.appendChild(group);
|
|
2012
|
+
});
|
|
2013
|
+
return section;
|
|
2014
|
+
}
|
|
1629
2015
|
function renderEndpointDetails(endpoint) {
|
|
1630
2016
|
const container = doc.createElement('div');
|
|
1631
2017
|
container.className = 'endpoint-details';
|
|
@@ -1669,6 +2055,32 @@ function renderEndpointDetails(endpoint) {
|
|
|
1669
2055
|
const bodyContent = reqBody.content[contentType];
|
|
1670
2056
|
if (bodyContent?.schema)
|
|
1671
2057
|
renderSchemaTree(section, bodyContent.schema);
|
|
2058
|
+
const requestExamples = Object.entries(bodyContent?.examples ?? {});
|
|
2059
|
+
if (requestExamples.length > 0 || bodyContent && 'example' in bodyContent) {
|
|
2060
|
+
const examplesTitle = doc.createElement('div');
|
|
2061
|
+
examplesTitle.className = 'response-subhead request-examples-title';
|
|
2062
|
+
examplesTitle.textContent = 'Request examples';
|
|
2063
|
+
section.appendChild(examplesTitle);
|
|
2064
|
+
const examples = requestExamples.length > 0
|
|
2065
|
+
? requestExamples
|
|
2066
|
+
: [['example', { value: bodyContent.example }]];
|
|
2067
|
+
examples.forEach(([name, example]) => {
|
|
2068
|
+
const wrapper = doc.createElement('div');
|
|
2069
|
+
wrapper.className = 'request-example';
|
|
2070
|
+
const label = doc.createElement('div');
|
|
2071
|
+
label.className = 'response-example-name';
|
|
2072
|
+
label.textContent = example?.summary || name;
|
|
2073
|
+
const pre = doc.createElement('pre');
|
|
2074
|
+
pre.className = 'sample-code';
|
|
2075
|
+
const code = doc.createElement('code');
|
|
2076
|
+
code.className = 'language-json';
|
|
2077
|
+
code.textContent = JSON.stringify(example?.value, null, 2);
|
|
2078
|
+
pre.appendChild(code);
|
|
2079
|
+
wrapper.appendChild(label);
|
|
2080
|
+
wrapper.appendChild(pre);
|
|
2081
|
+
section.appendChild(wrapper);
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
1672
2084
|
left.appendChild(section);
|
|
1673
2085
|
}
|
|
1674
2086
|
const responses = data.responses;
|
|
@@ -1691,18 +2103,23 @@ function renderEndpointDetails(endpoint) {
|
|
|
1691
2103
|
}
|
|
1692
2104
|
left.appendChild(section);
|
|
1693
2105
|
}
|
|
1694
|
-
|
|
2106
|
+
const callbacks = renderAsyncContracts('Callbacks', data.callbacks ?? {}, true);
|
|
2107
|
+
if (callbacks)
|
|
2108
|
+
left.appendChild(callbacks);
|
|
2109
|
+
// Right column: request samples (cURL / TypeScript / Rust / Python / Go).
|
|
1695
2110
|
const samplesSection = doc.createElement('div');
|
|
1696
2111
|
samplesSection.className = 'endpoint-right-section';
|
|
1697
2112
|
samplesSection.innerHTML = '<div class="endpoint-right-header">Request samples</div>';
|
|
1698
2113
|
samplesSection.appendChild(renderCodeExamples(endpoint, data));
|
|
2114
|
+
if (tryItConfig.enabled)
|
|
2115
|
+
samplesSection.appendChild(renderHttpExecutor(endpoint, data));
|
|
1699
2116
|
right.appendChild(samplesSection);
|
|
1700
2117
|
// Right column: response samples (JSON examples per status code).
|
|
1701
2118
|
if (responses && Object.keys(responses).length > 0) {
|
|
1702
2119
|
const respSamples = doc.createElement('div');
|
|
1703
2120
|
respSamples.className = 'endpoint-right-section';
|
|
1704
2121
|
respSamples.innerHTML = '<div class="endpoint-right-header">Response samples</div>';
|
|
1705
|
-
respSamples.appendChild(renderResponseSamples(responses));
|
|
2122
|
+
respSamples.appendChild(renderResponseSamples(responses, endpoint));
|
|
1706
2123
|
right.appendChild(respSamples);
|
|
1707
2124
|
}
|
|
1708
2125
|
content.appendChild(left);
|
|
@@ -1789,8 +2206,13 @@ function resolveMessagePayload(message) {
|
|
|
1789
2206
|
function resolveSchema(schema) {
|
|
1790
2207
|
if (!schema)
|
|
1791
2208
|
return null;
|
|
1792
|
-
if (schema.$ref)
|
|
1793
|
-
|
|
2209
|
+
if (schema.$ref) {
|
|
2210
|
+
const resolved = resolveRef(schema.$ref);
|
|
2211
|
+
if (!resolved || typeof resolved !== 'object')
|
|
2212
|
+
return schema;
|
|
2213
|
+
const { $ref: _ref, ...siblings } = schema;
|
|
2214
|
+
return { ...resolved, ...siblings };
|
|
2215
|
+
}
|
|
1794
2216
|
return schema;
|
|
1795
2217
|
}
|
|
1796
2218
|
function resolveRef(ref) {
|
|
@@ -1799,6 +2221,683 @@ function resolveRef(ref) {
|
|
|
1799
2221
|
return null;
|
|
1800
2222
|
return pointer.slice(2).split('/').map(part => part.replace(/~1/g, '/').replace(/~0/g, '~')).reduce((current, part) => current?.[part], spec);
|
|
1801
2223
|
}
|
|
2224
|
+
function credentialStorageKey(schemeName) {
|
|
2225
|
+
return `raffel-docs:auth:${encodeURIComponent(selectedEnvironmentUrl)}:${encodeURIComponent(schemeName)}`;
|
|
2226
|
+
}
|
|
2227
|
+
function readStoredCredential(schemeName) {
|
|
2228
|
+
try {
|
|
2229
|
+
const value = win.sessionStorage?.getItem?.(credentialStorageKey(schemeName));
|
|
2230
|
+
return value ? JSON.parse(value) : {};
|
|
2231
|
+
}
|
|
2232
|
+
catch {
|
|
2233
|
+
return {};
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
function storeCredential(schemeName, value) {
|
|
2237
|
+
try {
|
|
2238
|
+
win.sessionStorage?.setItem?.(credentialStorageKey(schemeName), JSON.stringify(value));
|
|
2239
|
+
}
|
|
2240
|
+
catch { /* unavailable storage */ }
|
|
2241
|
+
}
|
|
2242
|
+
function authSchemeKind(scheme) {
|
|
2243
|
+
if (scheme?.type === 'http' && String(scheme.scheme).toLowerCase() === 'bearer')
|
|
2244
|
+
return 'Bearer token';
|
|
2245
|
+
if (scheme?.type === 'http' && String(scheme.scheme).toLowerCase() === 'basic')
|
|
2246
|
+
return 'HTTP Basic';
|
|
2247
|
+
if (scheme?.type === 'apiKey')
|
|
2248
|
+
return 'API key';
|
|
2249
|
+
if (scheme?.type === 'oauth2')
|
|
2250
|
+
return 'OAuth 2.0';
|
|
2251
|
+
if (scheme?.type === 'openIdConnect')
|
|
2252
|
+
return 'OpenID Connect';
|
|
2253
|
+
if (scheme?.type === 'mutualTLS')
|
|
2254
|
+
return 'Mutual TLS';
|
|
2255
|
+
return String(scheme?.type ?? 'Authentication');
|
|
2256
|
+
}
|
|
2257
|
+
function findHttpOperation(operationId) {
|
|
2258
|
+
for (const [path, pathItem] of Object.entries(spec.paths ?? {})) {
|
|
2259
|
+
for (const [method, operation] of Object.entries(pathItem ?? {})) {
|
|
2260
|
+
if (operation?.operationId === operationId)
|
|
2261
|
+
return { path, method: method.toUpperCase(), operation };
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
return null;
|
|
2265
|
+
}
|
|
2266
|
+
function operationRequestExample(operation) {
|
|
2267
|
+
const media = Object.values(operation?.requestBody?.content ?? {})[0];
|
|
2268
|
+
if (!media)
|
|
2269
|
+
return {};
|
|
2270
|
+
if ('example' in media)
|
|
2271
|
+
return media.example;
|
|
2272
|
+
const named = Object.values(media.examples ?? {})[0];
|
|
2273
|
+
if (named && typeof named === 'object' && 'value' in named)
|
|
2274
|
+
return named.value;
|
|
2275
|
+
return generateExampleFromSchema(media.schema) ?? {};
|
|
2276
|
+
}
|
|
2277
|
+
function readJsonPointer(value, pointer) {
|
|
2278
|
+
const path = String(pointer ?? '');
|
|
2279
|
+
if (path === '')
|
|
2280
|
+
return value;
|
|
2281
|
+
if (!path.startsWith('/'))
|
|
2282
|
+
return undefined;
|
|
2283
|
+
return path.slice(1).split('/').map(part => part.replace(/~1/g, '/').replace(/~0/g, '~')).reduce((current, part) => current?.[part], value);
|
|
2284
|
+
}
|
|
2285
|
+
function tokenExpiry(response, pointers) {
|
|
2286
|
+
const expiresAt = readJsonPointer(response, pointers?.expiresAt);
|
|
2287
|
+
if (expiresAt !== undefined) {
|
|
2288
|
+
if (typeof expiresAt === 'number')
|
|
2289
|
+
return expiresAt > 1e12 ? expiresAt : expiresAt * 1000;
|
|
2290
|
+
const parsed = Date.parse(String(expiresAt));
|
|
2291
|
+
if (Number.isFinite(parsed))
|
|
2292
|
+
return parsed;
|
|
2293
|
+
}
|
|
2294
|
+
const expiresIn = Number(readJsonPointer(response, pointers?.expiresIn));
|
|
2295
|
+
return Number.isFinite(expiresIn) && expiresIn > 0 ? Date.now() + expiresIn * 1000 : undefined;
|
|
2296
|
+
}
|
|
2297
|
+
async function executeJsonHttpRequest(url, init = {}) {
|
|
2298
|
+
if (tryItConfig.mode === 'proxy') {
|
|
2299
|
+
const proxyResponse = await win.fetch?.(tryItConfig.proxyUrl, {
|
|
2300
|
+
method: 'POST',
|
|
2301
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2302
|
+
body: JSON.stringify({
|
|
2303
|
+
url,
|
|
2304
|
+
method: String(init.method ?? 'GET').toUpperCase(),
|
|
2305
|
+
headers: init.headers ?? {},
|
|
2306
|
+
body: init.body,
|
|
2307
|
+
}),
|
|
2308
|
+
});
|
|
2309
|
+
if (!proxyResponse)
|
|
2310
|
+
throw new Error('Fetch is not available in this browser.');
|
|
2311
|
+
const envelope = await proxyResponse.json();
|
|
2312
|
+
if (!proxyResponse.ok) {
|
|
2313
|
+
throw new Error(envelope?.detail || envelope?.message || `Proxy failed with ${proxyResponse.status}.`);
|
|
2314
|
+
}
|
|
2315
|
+
const status = Number(envelope?.status ?? 502);
|
|
2316
|
+
let payload = {};
|
|
2317
|
+
try {
|
|
2318
|
+
payload = JSON.parse(String(envelope?.body ?? ''));
|
|
2319
|
+
}
|
|
2320
|
+
catch { /* callers report a missing token or discovery field */ }
|
|
2321
|
+
return { ok: status >= 200 && status < 300, status, payload };
|
|
2322
|
+
}
|
|
2323
|
+
const response = await win.fetch?.(url, init);
|
|
2324
|
+
if (!response)
|
|
2325
|
+
throw new Error('Fetch is not available in this browser.');
|
|
2326
|
+
return { ok: response.ok, status: response.status, payload: await response.json() };
|
|
2327
|
+
}
|
|
2328
|
+
async function requestOperationToken(schemeName, recipe, operation, rawBody, status) {
|
|
2329
|
+
try {
|
|
2330
|
+
status.textContent = 'Requesting token…';
|
|
2331
|
+
const parsedBody = rawBody.trim() ? JSON.parse(rawBody) : undefined;
|
|
2332
|
+
const headers = parsedBody === undefined ? {} : { 'Content-Type': 'application/json' };
|
|
2333
|
+
const response = await executeJsonHttpRequest(`${httpBaseUrl()}${operation.path}`, {
|
|
2334
|
+
method: operation.method,
|
|
2335
|
+
headers,
|
|
2336
|
+
body: parsedBody === undefined ? undefined : JSON.stringify(parsedBody),
|
|
2337
|
+
});
|
|
2338
|
+
const payload = response.payload;
|
|
2339
|
+
if (!response.ok)
|
|
2340
|
+
throw new Error(payload?.message || `Token request failed with ${response.status}.`);
|
|
2341
|
+
const accessToken = readJsonPointer(payload, recipe.tokenPointers?.accessToken);
|
|
2342
|
+
if (accessToken === undefined || accessToken === null || accessToken === '') {
|
|
2343
|
+
throw new Error('The access-token JSON Pointer did not match the response.');
|
|
2344
|
+
}
|
|
2345
|
+
storeCredential(schemeName, {
|
|
2346
|
+
accessToken: String(accessToken),
|
|
2347
|
+
refreshToken: readJsonPointer(payload, recipe.tokenPointers?.refreshToken),
|
|
2348
|
+
expiresAt: tokenExpiry(payload, recipe.tokenPointers),
|
|
2349
|
+
tokenType: readJsonPointer(payload, recipe.tokenPointers?.tokenType) ?? 'Bearer',
|
|
2350
|
+
});
|
|
2351
|
+
render();
|
|
2352
|
+
}
|
|
2353
|
+
catch (error) {
|
|
2354
|
+
status.textContent = String(error?.message ?? error);
|
|
2355
|
+
status.classList.add('auth-status-error');
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
function oauthFlowLabel(flowName) {
|
|
2359
|
+
if (flowName === 'authorizationCode')
|
|
2360
|
+
return 'Authorization code';
|
|
2361
|
+
if (flowName === 'clientCredentials')
|
|
2362
|
+
return 'Client credentials';
|
|
2363
|
+
if (flowName === 'password')
|
|
2364
|
+
return 'Resource owner password';
|
|
2365
|
+
return 'Implicit';
|
|
2366
|
+
}
|
|
2367
|
+
function oauthScopeValue(flow) {
|
|
2368
|
+
return Object.keys(flow?.scopes ?? {}).join(' ');
|
|
2369
|
+
}
|
|
2370
|
+
async function requestOAuthToken(schemeName, flowName, flow, fieldset, status) {
|
|
2371
|
+
try {
|
|
2372
|
+
const value = (name) => String(fieldset.querySelector(`input[name="${name}"]`)?.value ?? '');
|
|
2373
|
+
const body = new URLSearchParams();
|
|
2374
|
+
body.set('grant_type', flowName === 'clientCredentials' ? 'client_credentials' : 'password');
|
|
2375
|
+
if (value('clientId'))
|
|
2376
|
+
body.set('client_id', value('clientId'));
|
|
2377
|
+
if (value('clientSecret'))
|
|
2378
|
+
body.set('client_secret', value('clientSecret'));
|
|
2379
|
+
if (value('scopes'))
|
|
2380
|
+
body.set('scope', value('scopes'));
|
|
2381
|
+
if (flowName === 'password') {
|
|
2382
|
+
body.set('username', value('username'));
|
|
2383
|
+
body.set('password', value('password'));
|
|
2384
|
+
}
|
|
2385
|
+
status.textContent = 'Requesting token…';
|
|
2386
|
+
const response = await executeJsonHttpRequest(flow.tokenUrl, {
|
|
2387
|
+
method: 'POST',
|
|
2388
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
2389
|
+
body: body.toString(),
|
|
2390
|
+
});
|
|
2391
|
+
const payload = response.payload;
|
|
2392
|
+
if (!response.ok)
|
|
2393
|
+
throw new Error(payload?.error_description || payload?.error || `Token request failed with ${response.status}.`);
|
|
2394
|
+
if (!payload.access_token)
|
|
2395
|
+
throw new Error('The token response did not include access_token.');
|
|
2396
|
+
storeCredential(schemeName, {
|
|
2397
|
+
accessToken: String(payload.access_token),
|
|
2398
|
+
refreshToken: payload.refresh_token,
|
|
2399
|
+
expiresAt: Number(payload.expires_in) > 0 ? Date.now() + Number(payload.expires_in) * 1000 : undefined,
|
|
2400
|
+
tokenType: payload.token_type || 'Bearer',
|
|
2401
|
+
clientId: value('clientId'),
|
|
2402
|
+
clientSecret: value('clientSecret'),
|
|
2403
|
+
scopes: value('scopes'),
|
|
2404
|
+
tokenUrl: flow.tokenUrl,
|
|
2405
|
+
refreshUrl: flow.refreshUrl,
|
|
2406
|
+
});
|
|
2407
|
+
render();
|
|
2408
|
+
}
|
|
2409
|
+
catch (error) {
|
|
2410
|
+
status.textContent = String(error?.message ?? error);
|
|
2411
|
+
status.classList.add('auth-status-error');
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
function randomOAuthState() {
|
|
2415
|
+
const values = new Uint32Array(4);
|
|
2416
|
+
try {
|
|
2417
|
+
(globalThis.crypto ?? win.crypto)?.getRandomValues?.(values);
|
|
2418
|
+
}
|
|
2419
|
+
catch {
|
|
2420
|
+
values[0] = Date.now();
|
|
2421
|
+
}
|
|
2422
|
+
return Array.from(values).map(value => value.toString(16)).join('') || String(Date.now());
|
|
2423
|
+
}
|
|
2424
|
+
function base64Url(bytes) {
|
|
2425
|
+
let binary = '';
|
|
2426
|
+
bytes.forEach(byte => { binary += String.fromCharCode(byte); });
|
|
2427
|
+
return win.btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
|
2428
|
+
}
|
|
2429
|
+
function randomOAuthVerifier() {
|
|
2430
|
+
const values = new Uint8Array(32);
|
|
2431
|
+
const cryptoApi = globalThis.crypto ?? win.crypto;
|
|
2432
|
+
try {
|
|
2433
|
+
cryptoApi?.getRandomValues?.(values);
|
|
2434
|
+
}
|
|
2435
|
+
catch { /* use the state-based fallback below */ }
|
|
2436
|
+
if (values.every(value => value === 0)) {
|
|
2437
|
+
const fallback = `${randomOAuthState()}${randomOAuthState()}`;
|
|
2438
|
+
values.forEach((_, index) => { values[index] = fallback.charCodeAt(index % fallback.length); });
|
|
2439
|
+
}
|
|
2440
|
+
return base64Url(values);
|
|
2441
|
+
}
|
|
2442
|
+
async function oauthCodeChallenge(verifier) {
|
|
2443
|
+
const cryptoApi = globalThis.crypto ?? win.crypto;
|
|
2444
|
+
if (!cryptoApi?.subtle?.digest)
|
|
2445
|
+
return { challenge: verifier, method: 'plain' };
|
|
2446
|
+
try {
|
|
2447
|
+
const bytes = Uint8Array.from(verifier, character => character.charCodeAt(0));
|
|
2448
|
+
const digest = await cryptoApi.subtle.digest('SHA-256', bytes);
|
|
2449
|
+
return { challenge: base64Url(new Uint8Array(digest)), method: 'S256' };
|
|
2450
|
+
}
|
|
2451
|
+
catch {
|
|
2452
|
+
return { challenge: verifier, method: 'plain' };
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
async function startOAuthAuthorization(schemeName, flowName, flow, fieldset) {
|
|
2456
|
+
const value = (name) => String(fieldset.querySelector(`input[name="${name}"]`)?.value ?? '');
|
|
2457
|
+
const redirectUri = `${String(win.location?.href ?? '').split('#')[0].split('?')[0]}?raffel_oauth_callback=1`;
|
|
2458
|
+
const state = randomOAuthState();
|
|
2459
|
+
const authorization = new URL(flow.authorizationUrl);
|
|
2460
|
+
const popup = win.open?.('', 'raffel-oauth', 'popup,width=540,height=720');
|
|
2461
|
+
authorization.searchParams.set('response_type', flowName === 'implicit' ? 'token' : 'code');
|
|
2462
|
+
authorization.searchParams.set('client_id', value('clientId'));
|
|
2463
|
+
authorization.searchParams.set('redirect_uri', redirectUri);
|
|
2464
|
+
authorization.searchParams.set('state', state);
|
|
2465
|
+
if (value('scopes'))
|
|
2466
|
+
authorization.searchParams.set('scope', value('scopes'));
|
|
2467
|
+
let codeVerifier;
|
|
2468
|
+
if (flowName !== 'implicit') {
|
|
2469
|
+
codeVerifier = randomOAuthVerifier();
|
|
2470
|
+
const pkce = await oauthCodeChallenge(codeVerifier);
|
|
2471
|
+
authorization.searchParams.set('code_challenge', pkce.challenge);
|
|
2472
|
+
authorization.searchParams.set('code_challenge_method', pkce.method);
|
|
2473
|
+
}
|
|
2474
|
+
try {
|
|
2475
|
+
win.sessionStorage?.setItem?.(`${credentialStorageKey(schemeName)}:pending`, JSON.stringify({
|
|
2476
|
+
schemeName, flowName, state, redirectUri, tokenUrl: flow.tokenUrl,
|
|
2477
|
+
clientId: value('clientId'), clientSecret: value('clientSecret'), scopes: value('scopes'),
|
|
2478
|
+
codeVerifier,
|
|
2479
|
+
}));
|
|
2480
|
+
}
|
|
2481
|
+
catch { /* unavailable storage */ }
|
|
2482
|
+
if (popup) {
|
|
2483
|
+
if (typeof popup.location?.replace === 'function')
|
|
2484
|
+
popup.location.replace(authorization.toString());
|
|
2485
|
+
else
|
|
2486
|
+
popup.location = authorization.toString();
|
|
2487
|
+
}
|
|
2488
|
+
else {
|
|
2489
|
+
win.location?.assign?.(authorization.toString());
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
function appendOAuthFlowFields(form, schemeName, scheme, stored) {
|
|
2493
|
+
Object.entries(scheme.flows ?? {}).forEach(([flowName, flow]) => {
|
|
2494
|
+
const fieldset = doc.createElement('fieldset');
|
|
2495
|
+
fieldset.className = 'auth-oauth-flow';
|
|
2496
|
+
fieldset.dataset.oauthFlow = flowName;
|
|
2497
|
+
const legend = doc.createElement('legend');
|
|
2498
|
+
legend.textContent = oauthFlowLabel(flowName);
|
|
2499
|
+
fieldset.appendChild(legend);
|
|
2500
|
+
fieldset.appendChild(createAuthInput('clientId', 'Client ID', 'text', stored.clientId ?? ''));
|
|
2501
|
+
if (flowName !== 'implicit')
|
|
2502
|
+
fieldset.appendChild(createAuthInput('clientSecret', 'Client secret', 'password', stored.clientSecret ?? ''));
|
|
2503
|
+
if (flowName === 'password') {
|
|
2504
|
+
fieldset.appendChild(createAuthInput('username', 'Username'));
|
|
2505
|
+
fieldset.appendChild(createAuthInput('password', 'Password', 'password'));
|
|
2506
|
+
}
|
|
2507
|
+
fieldset.appendChild(createAuthInput('scopes', 'Scopes', 'text', stored.scopes ?? oauthScopeValue(flow)));
|
|
2508
|
+
const actions = doc.createElement('div');
|
|
2509
|
+
actions.className = 'auth-actions';
|
|
2510
|
+
const button = doc.createElement('button');
|
|
2511
|
+
button.type = 'button';
|
|
2512
|
+
const status = doc.createElement('span');
|
|
2513
|
+
status.className = 'auth-status';
|
|
2514
|
+
status.textContent = stored.accessToken ? 'Ready' : 'Not authenticated';
|
|
2515
|
+
if (flowName === 'clientCredentials' || flowName === 'password') {
|
|
2516
|
+
button.className = 'auth-oauth-token';
|
|
2517
|
+
button.textContent = 'Request token';
|
|
2518
|
+
button.onclick = () => { void requestOAuthToken(schemeName, flowName, flow, fieldset, status); };
|
|
2519
|
+
}
|
|
2520
|
+
else {
|
|
2521
|
+
button.className = 'auth-oauth-authorize';
|
|
2522
|
+
button.textContent = 'Authorize in popup';
|
|
2523
|
+
button.onclick = () => { void startOAuthAuthorization(schemeName, flowName, flow, fieldset); };
|
|
2524
|
+
}
|
|
2525
|
+
actions.appendChild(button);
|
|
2526
|
+
actions.appendChild(status);
|
|
2527
|
+
fieldset.appendChild(actions);
|
|
2528
|
+
form.appendChild(fieldset);
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
function appendOidcFields(form, schemeName, scheme, stored) {
|
|
2532
|
+
const fieldset = doc.createElement('fieldset');
|
|
2533
|
+
fieldset.className = 'auth-oauth-flow';
|
|
2534
|
+
fieldset.dataset.oauthFlow = 'oidc';
|
|
2535
|
+
const legend = doc.createElement('legend');
|
|
2536
|
+
legend.textContent = 'OpenID discovery';
|
|
2537
|
+
fieldset.appendChild(legend);
|
|
2538
|
+
fieldset.appendChild(createAuthInput('clientId', 'Client ID', 'text', stored.clientId ?? ''));
|
|
2539
|
+
fieldset.appendChild(createAuthInput('clientSecret', 'Client secret', 'password', stored.clientSecret ?? ''));
|
|
2540
|
+
fieldset.appendChild(createAuthInput('scopes', 'Scopes', 'text', stored.scopes ?? 'openid profile'));
|
|
2541
|
+
const actions = doc.createElement('div');
|
|
2542
|
+
actions.className = 'auth-actions';
|
|
2543
|
+
const button = doc.createElement('button');
|
|
2544
|
+
button.type = 'button';
|
|
2545
|
+
button.className = 'auth-oidc-authorize';
|
|
2546
|
+
button.textContent = 'Discover and authorize';
|
|
2547
|
+
const status = doc.createElement('span');
|
|
2548
|
+
status.className = 'auth-status';
|
|
2549
|
+
status.textContent = stored.accessToken ? 'Ready' : 'Not authenticated';
|
|
2550
|
+
button.onclick = async () => {
|
|
2551
|
+
try {
|
|
2552
|
+
status.textContent = 'Loading discovery…';
|
|
2553
|
+
const response = await executeJsonHttpRequest(scheme.openIdConnectUrl);
|
|
2554
|
+
if (!response.ok)
|
|
2555
|
+
throw new Error('OpenID discovery failed.');
|
|
2556
|
+
const discovery = response.payload;
|
|
2557
|
+
await startOAuthAuthorization(schemeName, 'authorizationCode', {
|
|
2558
|
+
authorizationUrl: discovery.authorization_endpoint,
|
|
2559
|
+
tokenUrl: discovery.token_endpoint,
|
|
2560
|
+
scopes: {},
|
|
2561
|
+
}, fieldset);
|
|
2562
|
+
status.textContent = 'Authorization opened';
|
|
2563
|
+
}
|
|
2564
|
+
catch (error) {
|
|
2565
|
+
status.textContent = String(error?.message ?? error);
|
|
2566
|
+
status.classList.add('auth-status-error');
|
|
2567
|
+
}
|
|
2568
|
+
};
|
|
2569
|
+
actions.appendChild(button);
|
|
2570
|
+
actions.appendChild(status);
|
|
2571
|
+
fieldset.appendChild(actions);
|
|
2572
|
+
form.appendChild(fieldset);
|
|
2573
|
+
}
|
|
2574
|
+
function oauthCallbackParams() {
|
|
2575
|
+
const query = new URLSearchParams(String(win.location?.search ?? ''));
|
|
2576
|
+
const fragment = new URLSearchParams(String(win.location?.hash ?? '').replace(/^#/, ''));
|
|
2577
|
+
if (query.has('code') || query.has('error'))
|
|
2578
|
+
return query;
|
|
2579
|
+
if (fragment.has('access_token') || fragment.has('error'))
|
|
2580
|
+
return fragment;
|
|
2581
|
+
return null;
|
|
2582
|
+
}
|
|
2583
|
+
function findPendingOAuth(state) {
|
|
2584
|
+
if (!state)
|
|
2585
|
+
return null;
|
|
2586
|
+
for (const schemeName of Object.keys(spec?.components?.securitySchemes ?? {})) {
|
|
2587
|
+
try {
|
|
2588
|
+
const key = `${credentialStorageKey(schemeName)}:pending`;
|
|
2589
|
+
const raw = win.sessionStorage?.getItem?.(key);
|
|
2590
|
+
if (!raw)
|
|
2591
|
+
continue;
|
|
2592
|
+
const pending = JSON.parse(raw);
|
|
2593
|
+
if (pending.state === state)
|
|
2594
|
+
return { ...pending, key };
|
|
2595
|
+
}
|
|
2596
|
+
catch { /* ignore malformed pending state */ }
|
|
2597
|
+
}
|
|
2598
|
+
return null;
|
|
2599
|
+
}
|
|
2600
|
+
async function finishOAuthAuthorization(params) {
|
|
2601
|
+
const pending = findPendingOAuth(params.get('state'));
|
|
2602
|
+
if (!pending)
|
|
2603
|
+
return;
|
|
2604
|
+
if (params.get('error'))
|
|
2605
|
+
throw new Error(params.get('error_description') || params.get('error') || 'Authorization failed.');
|
|
2606
|
+
let payload = Object.fromEntries(params.entries());
|
|
2607
|
+
if (!payload.access_token && params.get('code')) {
|
|
2608
|
+
if (!pending.tokenUrl)
|
|
2609
|
+
throw new Error('The authorization-code flow has no tokenUrl.');
|
|
2610
|
+
const body = new URLSearchParams({
|
|
2611
|
+
grant_type: 'authorization_code',
|
|
2612
|
+
code: String(params.get('code')),
|
|
2613
|
+
redirect_uri: pending.redirectUri,
|
|
2614
|
+
client_id: pending.clientId,
|
|
2615
|
+
});
|
|
2616
|
+
if (pending.codeVerifier)
|
|
2617
|
+
body.set('code_verifier', pending.codeVerifier);
|
|
2618
|
+
if (pending.clientSecret)
|
|
2619
|
+
body.set('client_secret', pending.clientSecret);
|
|
2620
|
+
const response = await executeJsonHttpRequest(pending.tokenUrl, {
|
|
2621
|
+
method: 'POST',
|
|
2622
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
2623
|
+
body: body.toString(),
|
|
2624
|
+
});
|
|
2625
|
+
payload = response.payload;
|
|
2626
|
+
if (!response.ok)
|
|
2627
|
+
throw new Error(payload?.error_description || payload?.error || 'Token exchange failed.');
|
|
2628
|
+
}
|
|
2629
|
+
if (!payload.access_token)
|
|
2630
|
+
throw new Error('The authorization response did not include an access token.');
|
|
2631
|
+
storeCredential(pending.schemeName, {
|
|
2632
|
+
accessToken: String(payload.access_token),
|
|
2633
|
+
refreshToken: payload.refresh_token,
|
|
2634
|
+
expiresAt: Number(payload.expires_in) > 0 ? Date.now() + Number(payload.expires_in) * 1000 : undefined,
|
|
2635
|
+
tokenType: payload.token_type || 'Bearer',
|
|
2636
|
+
clientId: pending.clientId,
|
|
2637
|
+
clientSecret: pending.clientSecret,
|
|
2638
|
+
scopes: pending.scopes,
|
|
2639
|
+
tokenUrl: pending.tokenUrl,
|
|
2640
|
+
});
|
|
2641
|
+
try {
|
|
2642
|
+
win.sessionStorage?.removeItem?.(pending.key);
|
|
2643
|
+
}
|
|
2644
|
+
catch { /* unavailable storage */ }
|
|
2645
|
+
render();
|
|
2646
|
+
}
|
|
2647
|
+
function clearOAuthCallbackLocation() {
|
|
2648
|
+
const current = safeRuntimeUrl(String(win.location?.href ?? ''));
|
|
2649
|
+
if (!current)
|
|
2650
|
+
return;
|
|
2651
|
+
['raffel_oauth_callback', 'code', 'state', 'error', 'error_description'].forEach(name => current.searchParams.delete(name));
|
|
2652
|
+
const fragment = new URLSearchParams(current.hash.replace(/^#/, ''));
|
|
2653
|
+
if (fragment.has('access_token') || fragment.has('error'))
|
|
2654
|
+
current.hash = '';
|
|
2655
|
+
win.history?.replaceState?.(null, '', `${current.pathname}${current.search}${current.hash}`);
|
|
2656
|
+
}
|
|
2657
|
+
function installOAuthCallback() {
|
|
2658
|
+
const params = oauthCallbackParams();
|
|
2659
|
+
if (params) {
|
|
2660
|
+
if (win.opener && win.opener !== win) {
|
|
2661
|
+
;
|
|
2662
|
+
win.opener.postMessage({ type: 'raffel-oauth-callback', params: params.toString() }, win.location?.origin);
|
|
2663
|
+
win.close?.();
|
|
2664
|
+
}
|
|
2665
|
+
else {
|
|
2666
|
+
void finishOAuthAuthorization(params).catch(() => { }).finally(clearOAuthCallbackLocation);
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
win.addEventListener?.('message', (event) => {
|
|
2670
|
+
if (event?.origin !== win.location?.origin || event?.data?.type !== 'raffel-oauth-callback')
|
|
2671
|
+
return;
|
|
2672
|
+
void finishOAuthAuthorization(new URLSearchParams(String(event.data.params ?? ''))).catch(() => { });
|
|
2673
|
+
});
|
|
2674
|
+
}
|
|
2675
|
+
function securitySchemeNames(operation) {
|
|
2676
|
+
const requirement = selectedSecurityRequirement(operation);
|
|
2677
|
+
return requirement && typeof requirement === 'object' ? Object.keys(requirement) : [];
|
|
2678
|
+
}
|
|
2679
|
+
function substituteRefreshToken(value, refreshToken) {
|
|
2680
|
+
if (value === '$refreshToken')
|
|
2681
|
+
return refreshToken;
|
|
2682
|
+
if (Array.isArray(value))
|
|
2683
|
+
return value.map(item => substituteRefreshToken(item, refreshToken));
|
|
2684
|
+
if (value && typeof value === 'object') {
|
|
2685
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, substituteRefreshToken(item, refreshToken)]));
|
|
2686
|
+
}
|
|
2687
|
+
return value;
|
|
2688
|
+
}
|
|
2689
|
+
async function refreshCredential(schemeName) {
|
|
2690
|
+
const credential = readStoredCredential(schemeName);
|
|
2691
|
+
if (!credential.accessToken || !credential.refreshToken || !credential.expiresAt)
|
|
2692
|
+
return;
|
|
2693
|
+
if (Number(credential.expiresAt) > Date.now() + 30_000)
|
|
2694
|
+
return;
|
|
2695
|
+
const recipe = authenticationConfig?.schemes?.[schemeName];
|
|
2696
|
+
if (recipe?.strategy === 'operation' && recipe.refreshOperationId) {
|
|
2697
|
+
const operation = findHttpOperation(recipe.refreshOperationId);
|
|
2698
|
+
if (!operation)
|
|
2699
|
+
throw new Error(`Refresh operation ${recipe.refreshOperationId} was not found.`);
|
|
2700
|
+
const body = substituteRefreshToken(recipe.refreshRequestBody ?? { refreshToken: '$refreshToken' }, credential.refreshToken);
|
|
2701
|
+
const response = await executeJsonHttpRequest(`${httpBaseUrl()}${operation.path}`, {
|
|
2702
|
+
method: operation.method,
|
|
2703
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2704
|
+
body: JSON.stringify(body),
|
|
2705
|
+
});
|
|
2706
|
+
const payload = response.payload;
|
|
2707
|
+
if (!response.ok)
|
|
2708
|
+
throw new Error(payload?.message || `Token refresh failed with ${response.status}.`);
|
|
2709
|
+
const accessToken = readJsonPointer(payload, recipe.tokenPointers?.accessToken);
|
|
2710
|
+
if (!accessToken)
|
|
2711
|
+
throw new Error('The refreshed access-token JSON Pointer did not match the response.');
|
|
2712
|
+
storeCredential(schemeName, {
|
|
2713
|
+
...credential,
|
|
2714
|
+
accessToken: String(accessToken),
|
|
2715
|
+
refreshToken: readJsonPointer(payload, recipe.tokenPointers?.refreshToken) ?? credential.refreshToken,
|
|
2716
|
+
expiresAt: tokenExpiry(payload, recipe.tokenPointers),
|
|
2717
|
+
tokenType: readJsonPointer(payload, recipe.tokenPointers?.tokenType) ?? credential.tokenType ?? 'Bearer',
|
|
2718
|
+
});
|
|
2719
|
+
return;
|
|
2720
|
+
}
|
|
2721
|
+
const scheme = spec?.components?.securitySchemes?.[schemeName];
|
|
2722
|
+
if (scheme?.type === 'oauth2' || scheme?.type === 'openIdConnect') {
|
|
2723
|
+
const tokenUrl = credential.refreshUrl || credential.tokenUrl;
|
|
2724
|
+
if (!tokenUrl)
|
|
2725
|
+
return;
|
|
2726
|
+
const body = new URLSearchParams({ grant_type: 'refresh_token', refresh_token: credential.refreshToken });
|
|
2727
|
+
if (credential.clientId)
|
|
2728
|
+
body.set('client_id', credential.clientId);
|
|
2729
|
+
if (credential.clientSecret)
|
|
2730
|
+
body.set('client_secret', credential.clientSecret);
|
|
2731
|
+
const response = await executeJsonHttpRequest(tokenUrl, {
|
|
2732
|
+
method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString(),
|
|
2733
|
+
});
|
|
2734
|
+
const payload = response.payload;
|
|
2735
|
+
if (!response.ok || !payload.access_token)
|
|
2736
|
+
throw new Error(payload?.error_description || payload?.error || 'Token refresh failed.');
|
|
2737
|
+
storeCredential(schemeName, {
|
|
2738
|
+
...credential,
|
|
2739
|
+
accessToken: String(payload.access_token),
|
|
2740
|
+
refreshToken: payload.refresh_token ?? credential.refreshToken,
|
|
2741
|
+
expiresAt: Number(payload.expires_in) > 0 ? Date.now() + Number(payload.expires_in) * 1000 : undefined,
|
|
2742
|
+
tokenType: payload.token_type ?? credential.tokenType ?? 'Bearer',
|
|
2743
|
+
});
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
async function refreshCredentialsBeforeRequest(operation) {
|
|
2747
|
+
for (const schemeName of securitySchemeNames(operation))
|
|
2748
|
+
await refreshCredential(schemeName);
|
|
2749
|
+
}
|
|
2750
|
+
function createAuthInput(name, labelText, type = 'text', value = '') {
|
|
2751
|
+
const label = doc.createElement('label');
|
|
2752
|
+
label.className = 'auth-field';
|
|
2753
|
+
const caption = doc.createElement('span');
|
|
2754
|
+
caption.className = 'auth-field-label';
|
|
2755
|
+
caption.textContent = labelText;
|
|
2756
|
+
const input = doc.createElement('input');
|
|
2757
|
+
input.className = 'auth-input';
|
|
2758
|
+
input.name = name;
|
|
2759
|
+
input.type = type;
|
|
2760
|
+
input.autocomplete = type === 'password' ? 'off' : 'on';
|
|
2761
|
+
input.value = value;
|
|
2762
|
+
label.appendChild(caption);
|
|
2763
|
+
label.appendChild(input);
|
|
2764
|
+
return label;
|
|
2765
|
+
}
|
|
2766
|
+
function renderAuthenticationSection() {
|
|
2767
|
+
const schemes = spec?.components?.securitySchemes ?? {};
|
|
2768
|
+
if (!schemes || Object.keys(schemes).length === 0)
|
|
2769
|
+
return null;
|
|
2770
|
+
const section = doc.createElement('section');
|
|
2771
|
+
section.className = 'authentication-section';
|
|
2772
|
+
section.id = 'authentication';
|
|
2773
|
+
const heading = doc.createElement('div');
|
|
2774
|
+
heading.className = 'authentication-header';
|
|
2775
|
+
heading.innerHTML = '<div><div class="subsection-label">AUTHENTICATION</div><h2>Authentication</h2><p>Credentials saved here are shared with every protected route in this environment.</p></div>';
|
|
2776
|
+
section.appendChild(heading);
|
|
2777
|
+
const environmentField = doc.createElement('label');
|
|
2778
|
+
environmentField.className = 'auth-environment';
|
|
2779
|
+
environmentField.innerHTML = '<span class="auth-field-label">Current environment</span>';
|
|
2780
|
+
const select = doc.createElement('select');
|
|
2781
|
+
select.className = 'auth-environment-select';
|
|
2782
|
+
environments.forEach(environment => {
|
|
2783
|
+
const option = doc.createElement('option');
|
|
2784
|
+
option.value = environment.url;
|
|
2785
|
+
option.textContent = `${environment.label} — ${environment.url}`;
|
|
2786
|
+
if (environment.url === selectedEnvironmentUrl)
|
|
2787
|
+
option.setAttribute('selected', '');
|
|
2788
|
+
select.appendChild(option);
|
|
2789
|
+
});
|
|
2790
|
+
select.value = selectedEnvironmentUrl;
|
|
2791
|
+
select.onchange = () => {
|
|
2792
|
+
selectedEnvironmentUrl = select.value;
|
|
2793
|
+
render();
|
|
2794
|
+
};
|
|
2795
|
+
environmentField.appendChild(select);
|
|
2796
|
+
section.appendChild(environmentField);
|
|
2797
|
+
const cards = doc.createElement('div');
|
|
2798
|
+
cards.className = 'auth-schemes';
|
|
2799
|
+
Object.entries(schemes).forEach(([schemeName, scheme]) => {
|
|
2800
|
+
const card = doc.createElement('article');
|
|
2801
|
+
card.className = 'auth-scheme';
|
|
2802
|
+
card.dataset.scheme = schemeName;
|
|
2803
|
+
const stored = readStoredCredential(schemeName);
|
|
2804
|
+
const title = doc.createElement('div');
|
|
2805
|
+
title.className = 'auth-scheme-title';
|
|
2806
|
+
title.innerHTML = `<strong>${esc(schemeName)}</strong><span>${esc(authSchemeKind(scheme))}</span>`;
|
|
2807
|
+
card.appendChild(title);
|
|
2808
|
+
if (scheme.description) {
|
|
2809
|
+
const description = doc.createElement('p');
|
|
2810
|
+
description.className = 'auth-scheme-description';
|
|
2811
|
+
description.textContent = scheme.description;
|
|
2812
|
+
card.appendChild(description);
|
|
2813
|
+
}
|
|
2814
|
+
const form = doc.createElement('div');
|
|
2815
|
+
form.className = 'auth-form';
|
|
2816
|
+
const recipe = authenticationConfig?.schemes?.[schemeName];
|
|
2817
|
+
if (recipe?.strategy === 'operation') {
|
|
2818
|
+
const operation = findHttpOperation(recipe.operationId);
|
|
2819
|
+
if (!operation) {
|
|
2820
|
+
const error = doc.createElement('div');
|
|
2821
|
+
error.className = 'auth-status auth-status-error';
|
|
2822
|
+
error.textContent = `Operation ${recipe.operationId} was not found.`;
|
|
2823
|
+
form.appendChild(error);
|
|
2824
|
+
}
|
|
2825
|
+
else {
|
|
2826
|
+
const label = doc.createElement('label');
|
|
2827
|
+
label.className = 'auth-field';
|
|
2828
|
+
label.innerHTML = `<span class="auth-field-label">Request body · ${esc(operation.method)} ${esc(operation.path)}</span>`;
|
|
2829
|
+
const textarea = doc.createElement('textarea');
|
|
2830
|
+
textarea.className = 'auth-operation-body';
|
|
2831
|
+
textarea.value = JSON.stringify(recipe.requestBody ?? operationRequestExample(operation.operation), null, 2);
|
|
2832
|
+
label.appendChild(textarea);
|
|
2833
|
+
form.appendChild(label);
|
|
2834
|
+
const actions = doc.createElement('div');
|
|
2835
|
+
actions.className = 'auth-actions';
|
|
2836
|
+
const requestToken = doc.createElement('button');
|
|
2837
|
+
requestToken.type = 'button';
|
|
2838
|
+
requestToken.className = 'auth-request-token';
|
|
2839
|
+
requestToken.textContent = stored.accessToken ? 'Request new token' : 'Request token';
|
|
2840
|
+
const status = doc.createElement('span');
|
|
2841
|
+
status.className = 'auth-status';
|
|
2842
|
+
status.textContent = stored.accessToken ? 'Ready' : 'Not authenticated';
|
|
2843
|
+
requestToken.onclick = () => { void requestOperationToken(schemeName, recipe, operation, textarea.value, status); };
|
|
2844
|
+
actions.appendChild(requestToken);
|
|
2845
|
+
actions.appendChild(status);
|
|
2846
|
+
form.appendChild(actions);
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
2849
|
+
else if (scheme.type === 'http' && String(scheme.scheme).toLowerCase() === 'bearer') {
|
|
2850
|
+
form.appendChild(createAuthInput('accessToken', 'Bearer token', 'password', stored.accessToken ?? ''));
|
|
2851
|
+
}
|
|
2852
|
+
else if (scheme.type === 'http' && String(scheme.scheme).toLowerCase() === 'basic') {
|
|
2853
|
+
form.appendChild(createAuthInput('username', 'Username', 'text', stored.username ?? ''));
|
|
2854
|
+
form.appendChild(createAuthInput('password', 'Password', 'password', stored.password ?? ''));
|
|
2855
|
+
}
|
|
2856
|
+
else if (scheme.type === 'apiKey') {
|
|
2857
|
+
form.appendChild(createAuthInput('apiKey', scheme.name || 'API key', 'password', stored.apiKey ?? ''));
|
|
2858
|
+
const location = doc.createElement('div');
|
|
2859
|
+
location.className = 'auth-scheme-meta';
|
|
2860
|
+
location.textContent = `${scheme.in ?? 'header'} · ${scheme.name ?? schemeName}`;
|
|
2861
|
+
form.appendChild(location);
|
|
2862
|
+
}
|
|
2863
|
+
else if (scheme.type === 'oauth2') {
|
|
2864
|
+
appendOAuthFlowFields(form, schemeName, scheme, stored);
|
|
2865
|
+
}
|
|
2866
|
+
else if (scheme.type === 'openIdConnect') {
|
|
2867
|
+
appendOidcFields(form, schemeName, scheme, stored);
|
|
2868
|
+
}
|
|
2869
|
+
else {
|
|
2870
|
+
const note = doc.createElement('div');
|
|
2871
|
+
note.className = 'auth-scheme-meta';
|
|
2872
|
+
note.textContent = 'This authentication method must be configured by the HTTP client.';
|
|
2873
|
+
form.appendChild(note);
|
|
2874
|
+
}
|
|
2875
|
+
if (!recipe && ['http', 'apiKey'].includes(scheme.type)) {
|
|
2876
|
+
const save = doc.createElement('button');
|
|
2877
|
+
save.type = 'button';
|
|
2878
|
+
save.className = 'auth-save';
|
|
2879
|
+
save.textContent = stored.accessToken || stored.apiKey || stored.username ? 'Update credential' : 'Use credential';
|
|
2880
|
+
const status = doc.createElement('span');
|
|
2881
|
+
status.className = 'auth-status';
|
|
2882
|
+
status.textContent = stored.accessToken || stored.apiKey || stored.username ? 'Ready' : 'Not configured';
|
|
2883
|
+
save.onclick = () => {
|
|
2884
|
+
const credential = {};
|
|
2885
|
+
form.querySelectorAll('input[name]').forEach((input) => { credential[input.name] = input.value; });
|
|
2886
|
+
storeCredential(schemeName, credential);
|
|
2887
|
+
render();
|
|
2888
|
+
};
|
|
2889
|
+
const actions = doc.createElement('div');
|
|
2890
|
+
actions.className = 'auth-actions';
|
|
2891
|
+
actions.appendChild(save);
|
|
2892
|
+
actions.appendChild(status);
|
|
2893
|
+
form.appendChild(actions);
|
|
2894
|
+
}
|
|
2895
|
+
card.appendChild(form);
|
|
2896
|
+
cards.appendChild(card);
|
|
2897
|
+
});
|
|
2898
|
+
section.appendChild(cards);
|
|
2899
|
+
return section;
|
|
2900
|
+
}
|
|
1802
2901
|
function renderContent() {
|
|
1803
2902
|
const main = byId('mainContent');
|
|
1804
2903
|
if (!main)
|
|
@@ -1832,6 +2931,12 @@ function renderContent() {
|
|
|
1832
2931
|
// mid-search, in which case the search results take the surface.
|
|
1833
2932
|
if (!searchQuery) {
|
|
1834
2933
|
main.appendChild(renderDocsOverview());
|
|
2934
|
+
const authentication = renderAuthenticationSection();
|
|
2935
|
+
if (authentication)
|
|
2936
|
+
main.appendChild(authentication);
|
|
2937
|
+
const webhooks = renderAsyncContracts('Webhooks', spec.webhooks ?? {});
|
|
2938
|
+
if (webhooks)
|
|
2939
|
+
main.appendChild(webhooks);
|
|
1835
2940
|
}
|
|
1836
2941
|
if (searchQuery)
|
|
1837
2942
|
renderDocsSearch(main);
|
|
@@ -1842,7 +2947,9 @@ function renderContent() {
|
|
|
1842
2947
|
const section = doc.createElement('section');
|
|
1843
2948
|
section.className = 'endpoint-section';
|
|
1844
2949
|
section.id = endpoint.id;
|
|
1845
|
-
|
|
2950
|
+
const endpointData = (endpoint.data ?? {});
|
|
2951
|
+
const deprecated = endpointData.deprecated ? '<span class="endpoint-deprecated">Deprecated</span>' : '';
|
|
2952
|
+
section.innerHTML = `<div class="endpoint-header"><div><div class="endpoint-method-path"><span class="badge badge-${esc(endpoint.method.toLowerCase())}">${esc(endpoint.method)}</span><span class="endpoint-path">${esc(endpoint.path)}</span>${deprecated}</div><h2 class="endpoint-title">${esc(endpoint.summary ?? endpoint.path)}</h2>${endpoint.description ? `<div class="endpoint-description markdown-content">${parseMarkdown(endpoint.description)}</div>` : ''}</div></div>`;
|
|
1846
2953
|
section.appendChild(renderEndpointDetails(endpoint));
|
|
1847
2954
|
main.appendChild(section);
|
|
1848
2955
|
}
|
|
@@ -1894,7 +3001,17 @@ function renderDocsOverview() {
|
|
|
1894
3001
|
const desc = s?.description
|
|
1895
3002
|
? `<span class="docs-overview-server-desc">${esc(String(s.description))}</span>`
|
|
1896
3003
|
: '';
|
|
1897
|
-
|
|
3004
|
+
const variables = Object.entries(s?.variables ?? {}).map(([name, definition]) => {
|
|
3005
|
+
const allowed = Array.isArray(definition?.enum) && definition.enum.length > 0
|
|
3006
|
+
? `<span>allowed: ${esc(definition.enum.join(', '))}</span>`
|
|
3007
|
+
: '';
|
|
3008
|
+
const description = definition?.description ? `<span>${esc(definition.description)}</span>` : '';
|
|
3009
|
+
return `<li><code>${esc(name)}</code><span>default: ${esc(definition?.default ?? '')}</span>${allowed}${description}</li>`;
|
|
3010
|
+
}).join('');
|
|
3011
|
+
const variableBlock = variables
|
|
3012
|
+
? `<div class="docs-overview-server-variables"><strong>Server variables</strong><ul>${variables}</ul></div>`
|
|
3013
|
+
: '';
|
|
3014
|
+
return `<li class="docs-overview-server"><code class="docs-overview-server-url">${url}</code>${desc}${variableBlock}</li>`;
|
|
1898
3015
|
})
|
|
1899
3016
|
.join('');
|
|
1900
3017
|
section.innerHTML = `<h2 class="docs-overview-subtitle">${servers.length > 1 ? 'Servers' : 'Server'}</h2><ul class="docs-overview-server-list">${rows}</ul>`;
|
|
@@ -2605,6 +3722,7 @@ function init() {
|
|
|
2605
3722
|
const storedTheme = win.localStorage?.getItem?.(themeStorageKey);
|
|
2606
3723
|
if (storedTheme === 'auto' || storedTheme === 'dark' || storedTheme === 'light' || storedTheme === 'custom')
|
|
2607
3724
|
doc.documentElement.setAttribute('data-theme', storedTheme);
|
|
3725
|
+
installOAuthCallback();
|
|
2608
3726
|
installDocsPluginApi();
|
|
2609
3727
|
if (activePagePath && activePagePath !== routeState.pagePath) {
|
|
2610
3728
|
win.history?.replaceState?.(null, '', routeToHash(activePagePath, activeHeadingId));
|