azurajs 3.0.2 → 3.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/README.md +32 -0
  2. package/dist/IpResolver-BVgnGnpf.d.mts +5 -0
  3. package/dist/IpResolver-BVgnGnpf.d.ts +5 -0
  4. package/dist/SwaggerPlugin-C0UZTjaZ.d.ts +6 -0
  5. package/dist/SwaggerPlugin-wr9S4SRG.d.mts +6 -0
  6. package/dist/cookies/index.d.mts +7 -0
  7. package/dist/cookies/index.d.ts +7 -0
  8. package/dist/cookies/index.js +38 -0
  9. package/dist/cookies/index.js.map +1 -0
  10. package/dist/cookies/index.mjs +35 -0
  11. package/dist/cookies/index.mjs.map +1 -0
  12. package/dist/core/index.d.mts +18 -27
  13. package/dist/core/index.d.ts +18 -27
  14. package/dist/core/index.js +214 -14
  15. package/dist/core/index.js.map +1 -1
  16. package/dist/core/index.mjs +214 -14
  17. package/dist/core/index.mjs.map +1 -1
  18. package/dist/cors/index.d.mts +7 -0
  19. package/dist/cors/index.d.ts +7 -0
  20. package/dist/cors/index.js +52 -0
  21. package/dist/cors/index.js.map +1 -0
  22. package/dist/cors/index.mjs +50 -0
  23. package/dist/cors/index.mjs.map +1 -0
  24. package/dist/decorators/index.d.mts +2 -0
  25. package/dist/decorators/index.d.ts +2 -0
  26. package/dist/decorators/index.js +25 -0
  27. package/dist/decorators/index.js.map +1 -1
  28. package/dist/decorators/index.mjs +24 -1
  29. package/dist/decorators/index.mjs.map +1 -1
  30. package/dist/decorators-B6l3CbxC.d.ts +13 -0
  31. package/dist/decorators-D5nY109r.d.mts +13 -0
  32. package/dist/http-error/index.d.mts +18 -0
  33. package/dist/http-error/index.d.ts +18 -0
  34. package/dist/http-error/index.js +81 -0
  35. package/dist/http-error/index.js.map +1 -0
  36. package/dist/http-error/index.mjs +79 -0
  37. package/dist/http-error/index.mjs.map +1 -0
  38. package/dist/index-j6QGMhZU.d.mts +30 -0
  39. package/dist/index-tpPZS_UK.d.ts +30 -0
  40. package/dist/index.d.mts +16 -5
  41. package/dist/index.d.ts +16 -5
  42. package/dist/index.js +1178 -14
  43. package/dist/index.js.map +1 -1
  44. package/dist/index.mjs +1176 -15
  45. package/dist/index.mjs.map +1 -1
  46. package/dist/infra/index.d.mts +6 -0
  47. package/dist/infra/index.d.ts +6 -0
  48. package/dist/infra/index.js +162 -0
  49. package/dist/infra/index.js.map +1 -0
  50. package/dist/infra/index.mjs +159 -0
  51. package/dist/infra/index.mjs.map +1 -0
  52. package/dist/{Logger-iEQNVVSc.d.mts → logger/index.d.mts} +2 -1
  53. package/dist/{Logger-iEQNVVSc.d.ts → logger/index.d.ts} +2 -1
  54. package/dist/logger/index.js +123 -0
  55. package/dist/logger/index.js.map +1 -0
  56. package/dist/logger/index.mjs +120 -0
  57. package/dist/logger/index.mjs.map +1 -0
  58. package/dist/plugins/index.d.mts +8 -6
  59. package/dist/plugins/index.d.ts +8 -6
  60. package/dist/plugins/index.js +1101 -0
  61. package/dist/plugins/index.js.map +1 -1
  62. package/dist/plugins/index.mjs +1101 -1
  63. package/dist/plugins/index.mjs.map +1 -1
  64. package/dist/rate-limit/index.d.mts +7 -0
  65. package/dist/rate-limit/index.d.ts +7 -0
  66. package/dist/rate-limit/index.js +81 -0
  67. package/dist/rate-limit/index.js.map +1 -0
  68. package/dist/rate-limit/index.mjs +79 -0
  69. package/dist/rate-limit/index.mjs.map +1 -0
  70. package/dist/router/index.d.mts +4 -0
  71. package/dist/router/index.d.ts +4 -0
  72. package/dist/router/index.js +218 -0
  73. package/dist/router/index.js.map +1 -0
  74. package/dist/router/index.mjs +216 -0
  75. package/dist/router/index.mjs.map +1 -0
  76. package/dist/routes.type-DZO5VBW2.d.mts +58 -0
  77. package/dist/routes.type-DzHNkCag.d.ts +58 -0
  78. package/dist/swagger/index.d.mts +30 -0
  79. package/dist/swagger/index.d.ts +30 -0
  80. package/dist/swagger/index.js +1136 -0
  81. package/dist/swagger/index.js.map +1 -0
  82. package/dist/swagger/index.mjs +1126 -0
  83. package/dist/swagger/index.mjs.map +1 -0
  84. package/dist/swagger/swagger-ui-modern.html +894 -0
  85. package/dist/swagger.type-Bfn5nGR8.d.mts +42 -0
  86. package/dist/swagger.type-CfDbFCZC.d.ts +42 -0
  87. package/dist/types/index.d.mts +5 -58
  88. package/dist/types/index.d.ts +5 -58
  89. package/dist/utils/index.d.mts +7 -72
  90. package/dist/utils/index.d.ts +7 -72
  91. package/dist/validators/index.d.mts +48 -0
  92. package/dist/validators/index.d.ts +48 -0
  93. package/dist/validators/index.js +144 -0
  94. package/dist/validators/index.js.map +1 -0
  95. package/dist/validators/index.mjs +141 -0
  96. package/dist/validators/index.mjs.map +1 -0
  97. package/package.json +86 -2
  98. package/src/cookies/index.ts +1 -0
  99. package/src/core/index.ts +1 -0
  100. package/src/core/router.ts +26 -15
  101. package/src/core/server.ts +64 -14
  102. package/src/cors/index.ts +2 -0
  103. package/src/decorators/index.ts +2 -0
  104. package/src/http-error/index.ts +1 -0
  105. package/src/infra/index.ts +3 -0
  106. package/src/logger/index.ts +1 -0
  107. package/src/plugins/SwaggerPlugin.ts +45 -0
  108. package/src/plugins/index.ts +1 -0
  109. package/src/rate-limit/index.ts +2 -0
  110. package/src/router/index.ts +1 -0
  111. package/src/swagger/constants.ts +8 -0
  112. package/src/swagger/decorators.ts +35 -0
  113. package/src/swagger/index.ts +10 -0
  114. package/src/swagger/openapi-builder.ts +199 -0
  115. package/src/swagger/swagger-ui-html.ts +24 -0
  116. package/src/swagger/swagger-ui-modern.html +894 -0
  117. package/src/swagger/swagger-ui-template.ts +5 -0
  118. package/src/types/index.ts +7 -0
  119. package/src/types/swagger.type.ts +36 -0
  120. package/src/validators/index.ts +4 -0
package/dist/index.js CHANGED
@@ -187,31 +187,37 @@ var Router = class {
187
187
  return path;
188
188
  }
189
189
  getAllRoutes() {
190
+ return this.getRouteDocuments().map(({ method, path }) => ({ method, path }));
191
+ }
192
+ /**
193
+ * Todas as rotas com metadados (ex.: OpenAPI) associados ao handler.
194
+ */
195
+ getRouteDocuments() {
190
196
  const routes = [];
191
- for (const [key] of this.staticRoutes) {
197
+ for (const [key, stored] of this.staticRoutes) {
192
198
  const [method, path] = key.split(":", 2);
193
- routes.push({ method, path });
199
+ routes.push({ method, path, meta: stored.meta });
194
200
  }
195
- this.collectRoutes(this.root, "", routes);
201
+ this.collectRoutesWithMeta(this.root, "", routes);
196
202
  return routes;
197
203
  }
198
- collectRoutes(node, prefix, routes) {
199
- for (const [method] of node.handlers) {
200
- routes.push({ method, path: prefix || "/" });
204
+ collectRoutesWithMeta(node, prefix, routes) {
205
+ for (const [method, stored] of node.handlers) {
206
+ routes.push({ method, path: prefix || "/", meta: stored.meta });
201
207
  }
202
208
  for (const [seg, child] of node.children) {
203
- this.collectRoutes(child, `${prefix}/${seg}`, routes);
209
+ this.collectRoutesWithMeta(child, `${prefix}/${seg}`, routes);
204
210
  }
205
211
  if (node.paramChild) {
206
- this.collectRoutes(
212
+ this.collectRoutesWithMeta(
207
213
  node.paramChild,
208
214
  `${prefix}/:${node.paramChild.paramName}`,
209
215
  routes
210
216
  );
211
217
  }
212
218
  if (node.wildcardHandler) {
213
- for (const [method] of node.wildcardHandler) {
214
- routes.push({ method, path: `${prefix}/*` });
219
+ for (const [method, stored] of node.wildcardHandler) {
220
+ routes.push({ method, path: `${prefix}/*`, meta: stored.meta });
215
221
  }
216
222
  }
217
223
  }
@@ -642,7 +648,174 @@ function clearCookieHeader(name, options = {}) {
642
648
  });
643
649
  }
644
650
 
651
+ // src/swagger/constants.ts
652
+ var API_TAGS_KEY = "azura:api-tags";
653
+ var METHOD_TAGS_KEY = "azura:swagger-method-tags";
654
+ var API_OPERATION_KEY = "azura:swagger-operation";
655
+
656
+ // src/swagger/openapi-builder.ts
657
+ function resolveInfo(options) {
658
+ return options.info ?? {
659
+ title: "AzuraJS API",
660
+ version: "1.0.0",
661
+ description: "Generated OpenAPI specification"
662
+ };
663
+ }
664
+ function toOpenApiPath(path) {
665
+ if (path === "/") return "/";
666
+ const mapped = path.split("/").map((seg) => {
667
+ if (seg === "*") return "{wildcard}";
668
+ return seg.startsWith(":") ? `{${seg.slice(1)}}` : seg;
669
+ }).join("/").replace(/\/{2,}/g, "/");
670
+ if (mapped.endsWith("/*")) {
671
+ return mapped.slice(0, -2) + "/{wildcard}";
672
+ }
673
+ return mapped;
674
+ }
675
+ function pathParametersFromTemplate(path) {
676
+ const re = /\{([^}]+)\}/g;
677
+ const out = [];
678
+ let m;
679
+ const seen = /* @__PURE__ */ new Set();
680
+ while ((m = re.exec(path)) !== null) {
681
+ const name = m[1];
682
+ if (seen.has(name)) continue;
683
+ seen.add(name);
684
+ out.push({
685
+ name,
686
+ in: "path",
687
+ required: true,
688
+ schema: { type: "string" }
689
+ });
690
+ }
691
+ return out;
692
+ }
693
+ function defaultResponses(meta) {
694
+ if (meta?.responses && Object.keys(meta.responses).length > 0) {
695
+ const out = {};
696
+ for (const [code, r] of Object.entries(meta.responses)) {
697
+ out[String(code)] = {
698
+ description: r.description,
699
+ ...r.schema ? { content: { "application/json": { schema: r.schema } } } : {}
700
+ };
701
+ }
702
+ return out;
703
+ }
704
+ return {
705
+ "200": {
706
+ description: "Successful response",
707
+ content: {
708
+ "application/json": {
709
+ schema: { type: "object", additionalProperties: true }
710
+ }
711
+ }
712
+ }
713
+ };
714
+ }
715
+ function mergeParameters(meta, pathParams) {
716
+ const fromMeta = meta?.parameters ?? [];
717
+ const names = new Set(pathParams.map((p) => p.name));
718
+ const rest = fromMeta.filter((p) => !(p.in === "path" && names.has(p.name)));
719
+ const mapped = fromMeta.filter((p) => p.in === "path").map((p) => ({
720
+ name: p.name,
721
+ in: "path",
722
+ required: p.required ?? true,
723
+ description: p.description,
724
+ schema: p.schema ?? { type: p.type ?? "string" }
725
+ }));
726
+ const pathMerged = [...pathParams];
727
+ for (const m of mapped) {
728
+ if (!pathMerged.find((x) => x.name === m.name)) pathMerged.push(m);
729
+ }
730
+ return [...pathMerged, ...rest.map((p) => mapParameter(p))];
731
+ }
732
+ function mapParameter(p) {
733
+ const base = {
734
+ name: p.name,
735
+ in: p.in,
736
+ required: p.required ?? (p.in === "path" ? true : false),
737
+ description: p.description
738
+ };
739
+ if (p.in === "body") {
740
+ return {
741
+ ...base,
742
+ content: {
743
+ "application/json": {
744
+ schema: p.schema ?? { type: "object" }
745
+ }
746
+ }
747
+ };
748
+ }
749
+ return {
750
+ ...base,
751
+ schema: p.schema ?? { type: p.type ?? "string" }
752
+ };
753
+ }
754
+ function operationIdFor(method, oPath) {
755
+ const slug = oPath.replace(/[/{}\-*]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
756
+ return `${method.toLowerCase()}_${slug || "root"}`;
757
+ }
758
+ function operationForMethod(method, oPath, meta) {
759
+ const op = {
760
+ operationId: operationIdFor(method, oPath),
761
+ summary: meta?.summary ?? `${method} ${oPath}`,
762
+ ...meta?.description ? { description: meta.description } : {},
763
+ ...meta?.deprecated ? { deprecated: true } : {},
764
+ ...meta?.tags?.length ? { tags: meta.tags } : {},
765
+ ...meta?.security?.length ? { security: meta.security } : {}
766
+ };
767
+ if (["POST", "PUT", "PATCH"].includes(method) && !meta?.parameters?.some((x) => x.in === "body")) {
768
+ op.requestBody = {
769
+ content: {
770
+ "application/json": {
771
+ schema: { type: "object", additionalProperties: true }
772
+ }
773
+ },
774
+ required: false
775
+ };
776
+ }
777
+ op.responses = defaultResponses(meta);
778
+ return op;
779
+ }
780
+ function buildOpenApiDocument(routes, options) {
781
+ const info = resolveInfo(options);
782
+ const prefix = options.pathPrefix ?? "";
783
+ const paths = {};
784
+ const seen = /* @__PURE__ */ new Set();
785
+ for (const r of routes) {
786
+ const full = prefix + r.path;
787
+ const oPath = toOpenApiPath(full);
788
+ const key = `${r.method}:${oPath}`;
789
+ if (seen.has(key)) continue;
790
+ seen.add(key);
791
+ const pathParams = pathParametersFromTemplate(oPath);
792
+ const op = operationForMethod(r.method, oPath, r.meta);
793
+ const parameters = mergeParameters(r.meta, pathParams);
794
+ if (parameters.length) op.parameters = parameters;
795
+ const methodLc = r.method.toLowerCase();
796
+ if (!paths[oPath]) paths[oPath] = {};
797
+ paths[oPath][methodLc] = op;
798
+ }
799
+ return {
800
+ openapi: "3.0.3",
801
+ info: {
802
+ title: info.title,
803
+ version: info.version,
804
+ ...info.description ? { description: info.description } : {},
805
+ ...info.contact ? { contact: info.contact } : {},
806
+ ...info.license ? { license: info.license } : {}
807
+ },
808
+ ...options.servers?.length ? { servers: options.servers } : {},
809
+ paths
810
+ };
811
+ }
812
+
645
813
  // src/core/server.ts
814
+ function isRouteDocumentMeta(o) {
815
+ if (!o || typeof o !== "object" || Array.isArray(o)) return false;
816
+ const x = o;
817
+ return "summary" in x || "description" in x || "tags" in x || "deprecated" in x || "parameters" in x || "responses" in x || "security" in x || "produces" in x || "consumes" in x;
818
+ }
646
819
  var CONTROLLER_META_KEY = "azura:controller";
647
820
  var ROUTES_META_KEY = "azura:routes";
648
821
  var PARAMS_META_KEY = "azura:params";
@@ -731,9 +904,19 @@ var AzuraServer = class {
731
904
  return this;
732
905
  }
733
906
  route(method, path, handlers) {
734
- const routeHandler = handlers[handlers.length - 1];
735
- const middlewares = handlers.slice(0, -1);
736
- this.router.add(method, path, routeHandler, middlewares);
907
+ let meta;
908
+ let list = handlers;
909
+ const last = list[list.length - 1];
910
+ if (list.length >= 2 && isRouteDocumentMeta(last)) {
911
+ meta = last;
912
+ list = list.slice(0, -1);
913
+ }
914
+ if (list.length === 0) {
915
+ throw new Error(`Route ${method} ${path} requires a handler function`);
916
+ }
917
+ const routeHandler = list[list.length - 1];
918
+ const middlewares = list.slice(0, -1);
919
+ this.router.add(method, path, routeHandler, middlewares, meta);
737
920
  return this;
738
921
  }
739
922
  register(...controllers) {
@@ -746,6 +929,7 @@ var AzuraServer = class {
746
929
  const instance = new Controller2();
747
930
  const prefix = Reflect.getMetadata?.(CONTROLLER_META_KEY, Controller2) ?? "";
748
931
  const controllerMiddlewares = Reflect.getMetadata?.("azura:middlewares", Controller2) ?? [];
932
+ const controllerTags = Reflect.getMetadata?.(API_TAGS_KEY, Controller2) ?? [];
749
933
  const prototype = Controller2.prototype;
750
934
  const propertyNames = Object.getOwnPropertyNames(prototype).filter(
751
935
  (p) => p !== "constructor"
@@ -761,6 +945,14 @@ var AzuraServer = class {
761
945
  const paramsMeta = Reflect.getMetadata?.(PARAMS_META_KEY, prototype, propertyKey) ?? [];
762
946
  const routeMiddlewares = Reflect.getMetadata?.("azura:middlewares", prototype, propertyKey) ?? [];
763
947
  const allMiddlewares = [...controllerMiddlewares, ...routeMiddlewares];
948
+ const methodTags = Reflect.getMetadata?.(METHOD_TAGS_KEY, prototype, propertyKey) ?? [];
949
+ const opExtra = Reflect.getMetadata?.(API_OPERATION_KEY, prototype, propertyKey) ?? {};
950
+ const tagList = [...controllerTags, ...methodTags, ...routeMeta.meta?.tags ?? []];
951
+ const mergedMeta = (() => {
952
+ const m = { ...routeMeta.meta, ...opExtra };
953
+ if (tagList.length) m.tags = tagList;
954
+ return Object.keys(m).length ? m : void 0;
955
+ })();
764
956
  const handler = async (req, res) => {
765
957
  const args = this.resolveParams(paramsMeta, req, res);
766
958
  const result = await instance[propertyKey](...args);
@@ -772,7 +964,7 @@ var AzuraServer = class {
772
964
  }
773
965
  }
774
966
  };
775
- this.router.add(routeMeta.method, fullPath, handler, allMiddlewares, routeMeta.meta);
967
+ this.router.add(routeMeta.method, fullPath, handler, allMiddlewares, mergedMeta);
776
968
  }
777
969
  }
778
970
  resolveParams(paramsMeta, req, res) {
@@ -1126,6 +1318,14 @@ var AzuraServer = class {
1126
1318
  getRoutes() {
1127
1319
  return this.router.getAllRoutes();
1128
1320
  }
1321
+ /** Rotas com metadados OpenAPI (para Swagger ou export manual). */
1322
+ getRouteDocuments() {
1323
+ return this.router.getRouteDocuments();
1324
+ }
1325
+ /** Gera documento OpenAPI 3.0 a partir das rotas registadas. */
1326
+ getOpenApiDocument(options = {}) {
1327
+ return buildOpenApiDocument(this.getRouteDocuments(), options);
1328
+ }
1129
1329
  };
1130
1330
 
1131
1331
  // src/decorators/Route.ts
@@ -1225,6 +1425,24 @@ function Meta(key, value) {
1225
1425
  };
1226
1426
  }
1227
1427
 
1428
+ // src/swagger/decorators.ts
1429
+ function ApiTags(...tags) {
1430
+ return function(target, propertyKey, _descriptor) {
1431
+ if (propertyKey === void 0) {
1432
+ Reflect.defineMetadata(API_TAGS_KEY, tags, target);
1433
+ } else {
1434
+ Reflect.defineMetadata(METHOD_TAGS_KEY, tags, target, String(propertyKey));
1435
+ }
1436
+ };
1437
+ }
1438
+ function ApiOperation(meta) {
1439
+ return function(target, propertyKey, _descriptor) {
1440
+ const key = String(propertyKey);
1441
+ const cur = Reflect.getMetadata?.(API_OPERATION_KEY, target, key) ?? {};
1442
+ Reflect.defineMetadata(API_OPERATION_KEY, { ...cur, ...meta }, target, key);
1443
+ };
1444
+ }
1445
+
1228
1446
  // src/plugins/CORSPlugin.ts
1229
1447
  var DEFAULT_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
1230
1448
  var DEFAULT_HEADERS = ["Content-Type", "Authorization", "Accept", "X-Requested-With"];
@@ -2258,6 +2476,949 @@ function bufferIndexOf(buf, search, offset) {
2258
2476
  return -1;
2259
2477
  }
2260
2478
 
2479
+ // src/swagger/swagger-ui-template.ts
2480
+ var SWAGGER_UI_MODERN_HTML = `<!DOCTYPE html>\r
2481
+ <html lang="en">\r
2482
+ \r
2483
+ <head>\r
2484
+ <meta charset="UTF-8" />\r
2485
+ <meta name="viewport" content="width=device-width,initial-scale=1.0" />\r
2486
+ <title>__PAGE_TITLE__</title>\r
2487
+ <link rel="preconnect" href="https://fonts.googleapis.com" />\r
2488
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />\r
2489
+ <link\r
2490
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap"\r
2491
+ rel="stylesheet" />\r
2492
+ <style>\r
2493
+ :root {\r
2494
+ --bg-body: #09090b;\r
2495
+ --bg-sidebar: #18181b;\r
2496
+ --bg-card: #18181b;\r
2497
+ --bg-card-hover: #27272a;\r
2498
+ --bg-input: #09090b;\r
2499
+ --border-color: #27272a;\r
2500
+ --primary-color: #6366f1;\r
2501
+ --primary-hover: #4f46e5;\r
2502
+ --text-primary: #f4f4f5;\r
2503
+ --text-secondary: #a1a1aa;\r
2504
+ --text-muted: #52525b;\r
2505
+ --success: #22c55e;\r
2506
+ --warning: #eab308;\r
2507
+ --danger: #ef4444;\r
2508
+ --info: #3b82f6;\r
2509
+ --font-sans: 'Inter', system-ui, -apple-system, sans-serif;\r
2510
+ --font-mono: 'JetBrains Mono', monospace;\r
2511
+ }\r
2512
+ \r
2513
+ * {\r
2514
+ margin: 0;\r
2515
+ padding: 0;\r
2516
+ box-sizing: border-box\r
2517
+ }\r
2518
+ \r
2519
+ body {\r
2520
+ font-family: var(--font-sans);\r
2521
+ background-color: var(--bg-body);\r
2522
+ color: var(--text-primary);\r
2523
+ line-height: 1.5;\r
2524
+ height: 100vh;\r
2525
+ overflow: hidden\r
2526
+ }\r
2527
+ \r
2528
+ ::-webkit-scrollbar {\r
2529
+ width: 6px;\r
2530
+ height: 6px\r
2531
+ }\r
2532
+ \r
2533
+ ::-webkit-scrollbar-track {\r
2534
+ background: transparent\r
2535
+ }\r
2536
+ \r
2537
+ ::-webkit-scrollbar-thumb {\r
2538
+ background: var(--border-color);\r
2539
+ border-radius: 3px\r
2540
+ }\r
2541
+ \r
2542
+ ::-webkit-scrollbar-thumb:hover {\r
2543
+ background: var(--text-muted)\r
2544
+ }\r
2545
+ \r
2546
+ .layout {\r
2547
+ display: flex;\r
2548
+ height: 100%\r
2549
+ }\r
2550
+ \r
2551
+ .sidebar {\r
2552
+ width: 300px;\r
2553
+ background: var(--bg-sidebar);\r
2554
+ border-right: 1px solid var(--border-color);\r
2555
+ display: flex;\r
2556
+ flex-direction: column;\r
2557
+ flex-shrink: 0\r
2558
+ }\r
2559
+ \r
2560
+ .sidebar-header {\r
2561
+ padding: 20px;\r
2562
+ border-bottom: 1px solid var(--border-color);\r
2563
+ background: var(--bg-sidebar)\r
2564
+ }\r
2565
+ \r
2566
+ .api-title {\r
2567
+ font-size: 1.25rem;\r
2568
+ font-weight: 700;\r
2569
+ color: var(--text-primary);\r
2570
+ margin-bottom: 4px\r
2571
+ }\r
2572
+ \r
2573
+ .api-version {\r
2574
+ font-size: 0.75rem;\r
2575
+ color: var(--primary-color);\r
2576
+ background: rgba(99, 102, 241, 0.1);\r
2577
+ padding: 2px 8px;\r
2578
+ border-radius: 12px;\r
2579
+ font-family: var(--font-mono)\r
2580
+ }\r
2581
+ \r
2582
+ .search-box {\r
2583
+ padding: 16px\r
2584
+ }\r
2585
+ \r
2586
+ .search-input {\r
2587
+ width: 100%;\r
2588
+ background: var(--bg-input);\r
2589
+ border: 1px solid var(--border-color);\r
2590
+ padding: 8px 12px;\r
2591
+ border-radius: 6px;\r
2592
+ color: var(--text-primary);\r
2593
+ font-size: 0.875rem;\r
2594
+ outline: none;\r
2595
+ transition: border-color 0.2s\r
2596
+ }\r
2597
+ \r
2598
+ .search-input:focus {\r
2599
+ border-color: var(--primary-color)\r
2600
+ }\r
2601
+ \r
2602
+ .nav-list {\r
2603
+ flex: 1;\r
2604
+ overflow-y: auto;\r
2605
+ padding: 0 16px 20px\r
2606
+ }\r
2607
+ \r
2608
+ .nav-group-title {\r
2609
+ font-size: 0.75rem;\r
2610
+ text-transform: uppercase;\r
2611
+ color: var(--text-muted);\r
2612
+ font-weight: 600;\r
2613
+ margin: 24px 0 8px;\r
2614
+ letter-spacing: 0.05em\r
2615
+ }\r
2616
+ \r
2617
+ .nav-item {\r
2618
+ display: flex;\r
2619
+ align-items: center;\r
2620
+ gap: 8px;\r
2621
+ padding: 8px;\r
2622
+ border-radius: 6px;\r
2623
+ cursor: pointer;\r
2624
+ transition: background 0.2s;\r
2625
+ margin-bottom: 2px\r
2626
+ }\r
2627
+ \r
2628
+ .nav-item:hover {\r
2629
+ background: var(--bg-card-hover)\r
2630
+ }\r
2631
+ \r
2632
+ .nav-item.active {\r
2633
+ background: rgba(99, 102, 241, 0.1)\r
2634
+ }\r
2635
+ \r
2636
+ .method-tag {\r
2637
+ font-family: var(--font-mono);\r
2638
+ font-size: 0.65rem;\r
2639
+ font-weight: 700;\r
2640
+ padding: 2px 6px;\r
2641
+ border-radius: 4px;\r
2642
+ width: 45px;\r
2643
+ text-align: center\r
2644
+ }\r
2645
+ \r
2646
+ .get {\r
2647
+ color: var(--success);\r
2648
+ background: rgba(34, 197, 94, 0.1)\r
2649
+ }\r
2650
+ \r
2651
+ .post {\r
2652
+ color: var(--primary-color);\r
2653
+ background: rgba(99, 102, 241, 0.1)\r
2654
+ }\r
2655
+ \r
2656
+ .put {\r
2657
+ color: var(--warning);\r
2658
+ background: rgba(234, 179, 8, 0.1)\r
2659
+ }\r
2660
+ \r
2661
+ .delete {\r
2662
+ color: var(--danger);\r
2663
+ background: rgba(239, 68, 68, 0.1)\r
2664
+ }\r
2665
+ \r
2666
+ .patch {\r
2667
+ color: var(--info);\r
2668
+ background: rgba(59, 130, 246, 0.1)\r
2669
+ }\r
2670
+ \r
2671
+ .nav-path {\r
2672
+ font-size: 0.8rem;\r
2673
+ color: var(--text-secondary);\r
2674
+ white-space: nowrap;\r
2675
+ overflow: hidden;\r
2676
+ text-overflow: ellipsis;\r
2677
+ font-family: var(--font-mono)\r
2678
+ }\r
2679
+ \r
2680
+ .main-content {\r
2681
+ flex: 1;\r
2682
+ overflow-y: auto;\r
2683
+ padding: 40px;\r
2684
+ scroll-behavior: smooth\r
2685
+ }\r
2686
+ \r
2687
+ .content-width {\r
2688
+ max-width: 1000px;\r
2689
+ margin: 0 auto\r
2690
+ }\r
2691
+ \r
2692
+ .info-section {\r
2693
+ margin-bottom: 40px\r
2694
+ }\r
2695
+ \r
2696
+ .info-title {\r
2697
+ font-size: 2.5rem;\r
2698
+ font-weight: 800;\r
2699
+ margin-bottom: 16px;\r
2700
+ letter-spacing: -0.02em\r
2701
+ }\r
2702
+ \r
2703
+ .info-desc {\r
2704
+ color: var(--text-secondary);\r
2705
+ font-size: 1.1rem;\r
2706
+ max-width: 700px\r
2707
+ }\r
2708
+ \r
2709
+ .server-section {\r
2710
+ background: var(--bg-card);\r
2711
+ border: 1px solid var(--border-color);\r
2712
+ padding: 20px;\r
2713
+ border-radius: 8px;\r
2714
+ margin-bottom: 40px\r
2715
+ }\r
2716
+ \r
2717
+ .server-label {\r
2718
+ display: block;\r
2719
+ font-size: 0.875rem;\r
2720
+ color: var(--text-muted);\r
2721
+ margin-bottom: 8px\r
2722
+ }\r
2723
+ \r
2724
+ .server-select {\r
2725
+ width: 100%;\r
2726
+ background: var(--bg-input);\r
2727
+ border: 1px solid var(--border-color);\r
2728
+ color: var(--text-primary);\r
2729
+ padding: 10px;\r
2730
+ border-radius: 6px;\r
2731
+ font-family: var(--font-mono);\r
2732
+ font-size: 0.9rem\r
2733
+ }\r
2734
+ \r
2735
+ .endpoint-card {\r
2736
+ background: var(--bg-card);\r
2737
+ border: 1px solid var(--border-color);\r
2738
+ border-radius: 8px;\r
2739
+ margin-bottom: 24px;\r
2740
+ overflow: hidden;\r
2741
+ transition: border-color 0.2s\r
2742
+ }\r
2743
+ \r
2744
+ .endpoint-card:hover {\r
2745
+ border-color: var(--text-muted)\r
2746
+ }\r
2747
+ \r
2748
+ .card-header {\r
2749
+ padding: 16px 24px;\r
2750
+ display: flex;\r
2751
+ align-items: center;\r
2752
+ gap: 16px;\r
2753
+ cursor: pointer;\r
2754
+ background: rgba(255, 255, 255, 0.02);\r
2755
+ user-select: none\r
2756
+ }\r
2757
+ \r
2758
+ .card-summary {\r
2759
+ flex: 1;\r
2760
+ font-weight: 500;\r
2761
+ font-size: 1rem\r
2762
+ }\r
2763
+ \r
2764
+ .card-path {\r
2765
+ font-family: var(--font-mono);\r
2766
+ font-size: 0.85rem;\r
2767
+ color: var(--text-muted)\r
2768
+ }\r
2769
+ \r
2770
+ .chevron {\r
2771
+ transition: transform 0.2s;\r
2772
+ color: var(--text-muted)\r
2773
+ }\r
2774
+ \r
2775
+ .card-header[aria-expanded="true"] .chevron {\r
2776
+ transform: rotate(180deg)\r
2777
+ }\r
2778
+ \r
2779
+ .card-body {\r
2780
+ border-top: 1px solid var(--border-color);\r
2781
+ padding: 24px;\r
2782
+ display: none\r
2783
+ }\r
2784
+ \r
2785
+ .card-header[aria-expanded="true"]+.card-body {\r
2786
+ display: block\r
2787
+ }\r
2788
+ \r
2789
+ .section-header {\r
2790
+ font-size: 0.75rem;\r
2791
+ text-transform: uppercase;\r
2792
+ color: var(--text-muted);\r
2793
+ font-weight: 700;\r
2794
+ margin: 24px 0 12px;\r
2795
+ letter-spacing: 0.05em\r
2796
+ }\r
2797
+ \r
2798
+ .section-header:first-child {\r
2799
+ margin-top: 0\r
2800
+ }\r
2801
+ \r
2802
+ .params-table {\r
2803
+ width: 100%;\r
2804
+ border-collapse: collapse;\r
2805
+ font-size: 0.9rem;\r
2806
+ margin-bottom: 20px\r
2807
+ }\r
2808
+ \r
2809
+ .params-table th {\r
2810
+ text-align: left;\r
2811
+ color: var(--text-muted);\r
2812
+ padding: 8px 12px;\r
2813
+ font-weight: 500;\r
2814
+ border-bottom: 1px solid var(--border-color);\r
2815
+ font-size: 0.8rem\r
2816
+ }\r
2817
+ \r
2818
+ .params-table td {\r
2819
+ padding: 12px;\r
2820
+ border-bottom: 1px solid var(--border-color);\r
2821
+ color: var(--text-secondary);\r
2822
+ vertical-align: top\r
2823
+ }\r
2824
+ \r
2825
+ .params-table tr:last-child td {\r
2826
+ border-bottom: none\r
2827
+ }\r
2828
+ \r
2829
+ .param-name {\r
2830
+ font-family: var(--font-mono);\r
2831
+ color: var(--text-primary);\r
2832
+ font-weight: 600\r
2833
+ }\r
2834
+ \r
2835
+ .param-req {\r
2836
+ color: var(--danger);\r
2837
+ font-size: 0.7rem;\r
2838
+ margin-left: 4px\r
2839
+ }\r
2840
+ \r
2841
+ .param-meta {\r
2842
+ font-size: 0.8rem;\r
2843
+ color: var(--text-muted);\r
2844
+ margin-top: 4px;\r
2845
+ font-family: var(--font-mono)\r
2846
+ }\r
2847
+ \r
2848
+ .code-block {\r
2849
+ background: #000;\r
2850
+ border: 1px solid var(--border-color);\r
2851
+ border-radius: 6px;\r
2852
+ padding: 16px;\r
2853
+ font-family: var(--font-mono);\r
2854
+ font-size: 0.85rem;\r
2855
+ overflow-x: auto;\r
2856
+ color: #d4d4d8\r
2857
+ }\r
2858
+ \r
2859
+ .try-it-box {\r
2860
+ background: rgba(99, 102, 241, 0.05);\r
2861
+ border: 1px solid rgba(99, 102, 241, 0.2);\r
2862
+ border-radius: 8px;\r
2863
+ padding: 20px;\r
2864
+ margin-top: 32px\r
2865
+ }\r
2866
+ \r
2867
+ .form-grid {\r
2868
+ display: grid;\r
2869
+ grid-template-columns: 1fr;\r
2870
+ gap: 16px;\r
2871
+ margin-bottom: 20px\r
2872
+ }\r
2873
+ \r
2874
+ .form-group label {\r
2875
+ display: block;\r
2876
+ font-size: 0.8rem;\r
2877
+ font-weight: 600;\r
2878
+ margin-bottom: 6px;\r
2879
+ color: var(--text-secondary)\r
2880
+ }\r
2881
+ \r
2882
+ .form-group input {\r
2883
+ width: 100%;\r
2884
+ background: var(--bg-body);\r
2885
+ border: 1px solid var(--border-color);\r
2886
+ padding: 8px 12px;\r
2887
+ border-radius: 4px;\r
2888
+ color: var(--text-primary);\r
2889
+ font-family: var(--font-mono);\r
2890
+ font-size: 0.85rem\r
2891
+ }\r
2892
+ \r
2893
+ .form-group input:focus {\r
2894
+ border-color: var(--primary-color);\r
2895
+ outline: none\r
2896
+ }\r
2897
+ \r
2898
+ .body-editor {\r
2899
+ width: 100%;\r
2900
+ height: 200px;\r
2901
+ background: var(--bg-body);\r
2902
+ border: 1px solid var(--border-color);\r
2903
+ border-radius: 6px;\r
2904
+ padding: 12px;\r
2905
+ color: var(--text-primary);\r
2906
+ font-family: var(--font-mono);\r
2907
+ font-size: 0.85rem;\r
2908
+ resize: vertical;\r
2909
+ margin-bottom: 16px\r
2910
+ }\r
2911
+ \r
2912
+ .action-btn {\r
2913
+ background: var(--primary-color);\r
2914
+ color: white;\r
2915
+ border: none;\r
2916
+ padding: 10px 20px;\r
2917
+ border-radius: 6px;\r
2918
+ font-weight: 600;\r
2919
+ cursor: pointer;\r
2920
+ font-size: 0.9rem;\r
2921
+ transition: background 0.2s;\r
2922
+ display: inline-flex;\r
2923
+ align-items: center;\r
2924
+ gap: 8px\r
2925
+ }\r
2926
+ \r
2927
+ .action-btn:hover {\r
2928
+ background: var(--primary-hover)\r
2929
+ }\r
2930
+ \r
2931
+ .action-btn:disabled {\r
2932
+ opacity: 0.6;\r
2933
+ cursor: not-allowed\r
2934
+ }\r
2935
+ \r
2936
+ .response-area {\r
2937
+ margin-top: 20px;\r
2938
+ border-top: 1px solid var(--border-color);\r
2939
+ padding-top: 20px\r
2940
+ }\r
2941
+ \r
2942
+ .status-badge {\r
2943
+ display: inline-block;\r
2944
+ padding: 4px 8px;\r
2945
+ border-radius: 4px;\r
2946
+ font-size: 0.8rem;\r
2947
+ font-weight: 700;\r
2948
+ margin-bottom: 10px;\r
2949
+ font-family: var(--font-mono)\r
2950
+ }\r
2951
+ \r
2952
+ .status-2xx {\r
2953
+ background: rgba(34, 197, 94, 0.2);\r
2954
+ color: var(--success)\r
2955
+ }\r
2956
+ \r
2957
+ .status-4xx,\r
2958
+ .status-5xx {\r
2959
+ background: rgba(239, 68, 68, 0.2);\r
2960
+ color: var(--danger)\r
2961
+ }\r
2962
+ \r
2963
+ .loader {\r
2964
+ width: 16px;\r
2965
+ height: 16px;\r
2966
+ border: 2px solid #ffffff;\r
2967
+ border-bottom-color: transparent;\r
2968
+ border-radius: 50%;\r
2969
+ display: inline-block;\r
2970
+ animation: rotation 1s linear infinite\r
2971
+ }\r
2972
+ \r
2973
+ @keyframes rotation {\r
2974
+ 0% {\r
2975
+ transform: rotate(0deg)\r
2976
+ }\r
2977
+ \r
2978
+ 100% {\r
2979
+ transform: rotate(360deg)\r
2980
+ }\r
2981
+ }\r
2982
+ \r
2983
+ @media (max-width:768px) {\r
2984
+ .layout {\r
2985
+ flex-direction: column;\r
2986
+ overflow: auto\r
2987
+ }\r
2988
+ \r
2989
+ .sidebar {\r
2990
+ width: 100%;\r
2991
+ height: auto;\r
2992
+ border-right: none;\r
2993
+ border-bottom: 1px solid var(--border-color)\r
2994
+ }\r
2995
+ \r
2996
+ .main-content {\r
2997
+ padding: 20px;\r
2998
+ overflow: visible\r
2999
+ }\r
3000
+ \r
3001
+ .nav-list {\r
3002
+ max-height: 300px\r
3003
+ }\r
3004
+ }\r
3005
+ </style>\r
3006
+ </head>\r
3007
+ \r
3008
+ <body>\r
3009
+ <div class="layout">\r
3010
+ <aside class="sidebar">\r
3011
+ <div class="sidebar-header">\r
3012
+ <div class="api-title" id="apiTitle">API Docs</div>\r
3013
+ <span class="api-version" id="apiVersion">v1.0.0</span>\r
3014
+ </div>\r
3015
+ <div class="search-box">\r
3016
+ <input type="text" class="search-input" id="searchNav" placeholder="Filter endpoints...">\r
3017
+ </div>\r
3018
+ <div class="nav-list" id="navList"></div>\r
3019
+ </aside>\r
3020
+ <main class="main-content">\r
3021
+ <div class="content-width">\r
3022
+ <div class="info-section">\r
3023
+ <h1 class="info-title" id="docTitle">API Reference</h1>\r
3024
+ <p class="info-desc" id="docDesc">Loading documentation...</p>\r
3025
+ </div>\r
3026
+ <div class="server-section">\r
3027
+ <label class="server-label">Base URL</label>\r
3028
+ <select class="server-select" id="serverUrl"></select>\r
3029
+ </div>\r
3030
+ <div id="endpointsContainer"></div>\r
3031
+ </div>\r
3032
+ </main>\r
3033
+ </div>\r
3034
+ \r
3035
+ <script>\r
3036
+ const state = { spec: null, server: '' };\r
3037
+ function sanitizeId(str) { return String(str).replace(/[^a-zA-Z0-9_-]/g, '_'); }\r
3038
+ async function init() {\r
3039
+ try {\r
3040
+ const res = await fetch("___AZURA_SPEC_URL_PLACEHOLDER___");\r
3041
+ if (!res.ok) throw new Error('Spec not found');\r
3042
+ state.spec = await res.json();\r
3043
+ renderHeader();\r
3044
+ renderServers();\r
3045
+ renderNavigation();\r
3046
+ renderEndpoints();\r
3047
+ setupSearch();\r
3048
+ attachGlobalListeners();\r
3049
+ } catch (err) {\r
3050
+ const descEl = document.getElementById('docDesc');\r
3051
+ descEl.textContent = 'Failed to load API definition. Check that the OpenAPI JSON endpoint is configured and reachable.';\r
3052
+ descEl.style.color = 'var(--danger)';\r
3053
+ }\r
3054
+ }\r
3055
+ \r
3056
+ function renderHeader() {\r
3057
+ const info = state.spec.info || {};\r
3058
+ document.title = \`\${info.title || 'API'} - Docs\`;\r
3059
+ document.getElementById('apiTitle').textContent = info.title || 'API';\r
3060
+ document.getElementById('apiVersion').textContent = info.version ? \`v\${info.version}\` : '';\r
3061
+ document.getElementById('docTitle').textContent = info.title || 'API Reference';\r
3062
+ document.getElementById('docDesc').textContent = info.description || 'No description provided.';\r
3063
+ }\r
3064
+ \r
3065
+ function renderServers() {\r
3066
+ const select = document.getElementById('serverUrl');\r
3067
+ const servers = state.spec.servers && state.spec.servers.length ? state.spec.servers : [{ url: window.location.origin, description: 'Current Origin' }];\r
3068
+ select.innerHTML = servers.map(s => \`<option value="\${s.url}">\${s.url}\${s.description ? \` (\${s.description})\` : ''}</option>\`).join('');\r
3069
+ state.server = servers[0].url;\r
3070
+ select.addEventListener('change', (e) => state.server = e.target.value);\r
3071
+ }\r
3072
+ \r
3073
+ function renderNavigation() {\r
3074
+ const nav = document.getElementById('navList');\r
3075
+ const tags = {};\r
3076
+ Object.entries(state.spec.paths || {}).forEach(([path, methods]) => {\r
3077
+ Object.entries(methods).forEach(([method, op]) => {\r
3078
+ const tag = (op.tags && op.tags[0]) ? op.tags[0] : 'General';\r
3079
+ if (!tags[tag]) tags[tag] = [];\r
3080
+ tags[tag].push({ path, method, op });\r
3081
+ });\r
3082
+ });\r
3083
+ \r
3084
+ let html = '';\r
3085
+ Object.entries(tags).forEach(([tag, items]) => {\r
3086
+ html += \`<div class="nav-group-title">\${tag}</div>\`;\r
3087
+ items.forEach(item => {\r
3088
+ const label = item.op.summary || item.path;\r
3089
+ const filter = \`\${item.method} \${item.path} \${label}\`.toLowerCase();\r
3090
+ const targetId = sanitizeId(\`\${item.method}-\${item.path}\`);\r
3091
+ html += \`<div class="nav-item" data-filter="\${escapeHtmlAttr(filter)}" data-target-id="\${escapeHtmlAttr(targetId)}">\r
3092
+ <span class="method-tag \${item.method.toLowerCase()}">\${item.method.toUpperCase()}</span>\r
3093
+ <span class="nav-path">\${escapeHtmlAttr(label)}</span>\r
3094
+ </div>\`;\r
3095
+ });\r
3096
+ });\r
3097
+ \r
3098
+ nav.innerHTML = html;\r
3099
+ nav.querySelectorAll('.nav-item').forEach(el => {\r
3100
+ el.addEventListener('click', () => {\r
3101
+ const tid = el.dataset.targetId;\r
3102
+ scrollEndpoint(tid);\r
3103
+ document.querySelectorAll('.nav-item').forEach(i => i.classList.toggle('active', i === el));\r
3104
+ });\r
3105
+ });\r
3106
+ }\r
3107
+ \r
3108
+ function renderEndpoints() {\r
3109
+ const container = document.getElementById('endpointsContainer');\r
3110
+ container.innerHTML = '';\r
3111
+ Object.entries(state.spec.paths || {}).forEach(([path, methods]) => {\r
3112
+ Object.entries(methods).forEach(([method, op]) => {\r
3113
+ container.appendChild(createEndpointCard(path, method, op));\r
3114
+ });\r
3115
+ });\r
3116
+ }\r
3117
+ \r
3118
+ function createEndpointCard(path, method, op) {\r
3119
+ const rawId = \`\${method}-\${path}\`;\r
3120
+ const id = sanitizeId(rawId);\r
3121
+ const card = document.createElement('div');\r
3122
+ card.className = 'endpoint-card';\r
3123
+ card.id = id;\r
3124
+ \r
3125
+ const descriptionHtml = op.description ? \`<p style="margin-bottom:20px;color:var(--text-secondary)">\${escapeHtml(op.description)}</p>\` : '';\r
3126
+ const paramsHtml = renderParameters(op.parameters);\r
3127
+ const requestBodyHtml = renderRequestBody(op.requestBody);\r
3128
+ const responsesHtml = renderResponses(op.responses);\r
3129
+ \r
3130
+ const inputsId = \`inputs_\${id}\`;\r
3131
+ const bodyId = \`body_\${id}\`;\r
3132
+ const tryButtonId = \`btn_\${id}\`;\r
3133
+ const inputsHtmlForTry = (op.parameters || []).map(p => {\r
3134
+ const placeholder = p.schema?.type || '';\r
3135
+ const required = p.required ? ' <span style="color:var(--danger)">*</span>' : '';\r
3136
+ return \`<div class="form-group">\r
3137
+ <label>\${escapeHtml(p.name)} <span style="font-weight:400;color:var(--text-muted)">(\${escapeHtml(p.in)})</span>\${required}</label>\r
3138
+ <input type="text" data-name="\${escapeHtmlAttr(p.name)}" data-in="\${escapeHtmlAttr(p.in)}" placeholder="\${escapeHtmlAttr(placeholder)}">\r
3139
+ </div>\`;\r
3140
+ }).join('');\r
3141
+ \r
3142
+ const bodyEditorHtml = op.requestBody ? \`<div class="form-group">\r
3143
+ <label>Request Body (JSON)</label>\r
3144
+ <textarea class="body-editor" id="\${bodyId}">\${getExampleBody(op.requestBody)}</textarea>\r
3145
+ </div>\` : '';\r
3146
+ \r
3147
+ card.innerHTML = \`\r
3148
+ <div class="card-header" aria-expanded="false">\r
3149
+ <span class="method-tag \${method.toLowerCase()}">\${method.toUpperCase()}</span>\r
3150
+ <span class="card-summary">\${escapeHtml(op.summary || path)}</span>\r
3151
+ <span class="card-path">\${escapeHtml(path)}</span>\r
3152
+ <svg class="chevron" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>\r
3153
+ </div>\r
3154
+ <div class="card-body">\r
3155
+ \${descriptionHtml}\r
3156
+ \${paramsHtml}\r
3157
+ \${requestBodyHtml}\r
3158
+ <div class="try-it-box">\r
3159
+ <div class="section-header">Try It Out</div>\r
3160
+ <div class="form-grid" id="\${inputsId}">\r
3161
+ \${inputsHtmlForTry}\r
3162
+ </div>\r
3163
+ \${bodyEditorHtml}\r
3164
+ <button class="action-btn" id="\${tryButtonId}" data-method="\${escapeHtmlAttr(method)}" data-path="\${escapeHtmlAttr(path)}" data-card-id="\${escapeHtmlAttr(id)}">\r
3165
+ <span>Execute Request</span>\r
3166
+ </button>\r
3167
+ <div class="response-area" style="display:none"></div>\r
3168
+ </div>\r
3169
+ \${responsesHtml}\r
3170
+ </div>\r
3171
+ \`;\r
3172
+ \r
3173
+ return card;\r
3174
+ }\r
3175
+ \r
3176
+ function renderParameters(params) {\r
3177
+ if (!params || params.length === 0) return '';\r
3178
+ return \`\r
3179
+ <div class="section-header">Parameters</div>\r
3180
+ <table class="params-table">\r
3181
+ <thead><tr><th>Name</th><th>Description</th></tr></thead>\r
3182
+ <tbody>\r
3183
+ \${params.map(p => \`\r
3184
+ <tr>\r
3185
+ <td>\r
3186
+ <div class="param-name">\${escapeHtml(p.name)}\${p.required ? '<span class="param-req">*</span>' : ''}</div>\r
3187
+ <div class="param-meta">\${escapeHtml(p.in)} &bull; \${escapeHtml(p.schema?.type || 'string')}</div>\r
3188
+ </td>\r
3189
+ <td>\${escapeHtml(p.description || '-')}</td>\r
3190
+ </tr>\r
3191
+ \`).join('')}\r
3192
+ </tbody>\r
3193
+ </table>\r
3194
+ \`;\r
3195
+ }\r
3196
+ \r
3197
+ function renderRequestBody(body) {\r
3198
+ if (!body) return '';\r
3199
+ const contentType = Object.keys(body.content || {})[0] || 'application/json';\r
3200
+ const schema = body.content && body.content[contentType] ? body.content[contentType].schema : null;\r
3201
+ return \`\r
3202
+ <div class="section-header">Request Body <span style="font-weight:400;text-transform:none;color:var(--text-secondary)">(\${escapeHtml(contentType)})</span></div>\r
3203
+ <div class="code-block">\${escapeHtml(JSON.stringify(generateExample(schema), null, 2))}</div>\r
3204
+ \`;\r
3205
+ }\r
3206
+ \r
3207
+ function renderResponses(responses) {\r
3208
+ if (!responses) return '';\r
3209
+ return \`\r
3210
+ <div class="section-header">Responses</div>\r
3211
+ \${Object.entries(responses).map(([status, res]) => \`\r
3212
+ <div style="margin-bottom:16px">\r
3213
+ <div style="font-weight:600;font-family:var(--font-mono);margin-bottom:4px">\r
3214
+ <span style="color:\${String(status).startsWith('2') ? 'var(--success)' : 'var(--danger)'}">\${escapeHtml(status)}</span>\r
3215
+ <span style="color:var(--text-secondary)">\${escapeHtml(res.description || '')}</span>\r
3216
+ </div>\r
3217
+ </div>\r
3218
+ \`).join('')}\r
3219
+ \`;\r
3220
+ }\r
3221
+ \r
3222
+ function getExampleBody(body) {\r
3223
+ if (!body || !body.content) return '{}';\r
3224
+ const contentType = Object.keys(body.content)[0];\r
3225
+ const schema = body.content[contentType].schema;\r
3226
+ try { return JSON.stringify(generateExample(schema), null, 2); } catch { return '{}'; }\r
3227
+ }\r
3228
+ \r
3229
+ function generateExample(schema) {\r
3230
+ if (!schema) return {};\r
3231
+ if (schema.example !== undefined) return schema.example;\r
3232
+ if (schema.$ref) {\r
3233
+ const ref = String(schema.$ref).replace(/^#\\/components\\/schemas\\//, '');\r
3234
+ const comp = state.spec.components && state.spec.components.schemas && state.spec.components.schemas[ref];\r
3235
+ if (comp) return generateExample(comp);\r
3236
+ return {};\r
3237
+ }\r
3238
+ if (schema.type === 'object' && schema.properties) {\r
3239
+ const obj = {};\r
3240
+ for (const [k, prop] of Object.entries(schema.properties)) {\r
3241
+ obj[k] = generateExample(prop);\r
3242
+ }\r
3243
+ return obj;\r
3244
+ }\r
3245
+ if (schema.type === 'array') return [generateExample(schema.items)];\r
3246
+ if (schema.type === 'string') return 'string';\r
3247
+ if (schema.type === 'integer' || schema.type === 'number') return 0;\r
3248
+ if (schema.type === 'boolean') return true;\r
3249
+ return null;\r
3250
+ }\r
3251
+ \r
3252
+ async function executeRequest(method, path, btn) {\r
3253
+ const cardId = btn.dataset.cardId;\r
3254
+ const card = document.getElementById(cardId);\r
3255
+ const inputs = card.querySelectorAll('input[data-name]');\r
3256
+ const bodyEditor = card.querySelector('.body-editor');\r
3257
+ const responseArea = card.querySelector('.response-area');\r
3258
+ const originalHtml = btn.innerHTML;\r
3259
+ btn.disabled = true;\r
3260
+ btn.innerHTML = '<span class="loader"></span> Executing...';\r
3261
+ responseArea.style.display = 'none';\r
3262
+ try {\r
3263
+ let url = (state.server || '') + path;\r
3264
+ const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json' };\r
3265
+ const queryParams = new URLSearchParams();\r
3266
+ inputs.forEach(input => {\r
3267
+ const value = input.value.trim();\r
3268
+ if (!value) return;\r
3269
+ const type = input.dataset.in;\r
3270
+ const name = input.dataset.name;\r
3271
+ if (type === 'path') {\r
3272
+ url = url.replace(\`{\${name}}\`, encodeURIComponent(value));\r
3273
+ } else if (type === 'query') {\r
3274
+ queryParams.append(name, value);\r
3275
+ } else if (type === 'header') {\r
3276
+ headers[name] = value;\r
3277
+ } else if (type === 'cookie') {\r
3278
+ // cookies handled as header for demo purposes\r
3279
+ headers['Cookie'] = (headers['Cookie'] ? headers['Cookie'] + '; ' : '') + \`\${name}=\${value}\`;\r
3280
+ }\r
3281
+ });\r
3282
+ const qs = queryParams.toString();\r
3283
+ if (qs) url += \`?\${qs}\`;\r
3284
+ const options = { method: method.toUpperCase(), headers };\r
3285
+ if (bodyEditor && ['POST', 'PUT', 'PATCH'].includes(method.toUpperCase())) {\r
3286
+ try {\r
3287
+ const parsed = JSON.parse(bodyEditor.value);\r
3288
+ options.body = JSON.stringify(parsed);\r
3289
+ } catch (e) {\r
3290
+ alert('Invalid JSON in Request Body');\r
3291
+ throw new Error('Invalid JSON');\r
3292
+ }\r
3293
+ }\r
3294
+ const start = performance.now();\r
3295
+ const res = await fetch(url, options);\r
3296
+ const duration = Math.round(performance.now() - start);\r
3297
+ let data;\r
3298
+ const ct = res.headers.get('content-type') || '';\r
3299
+ if (ct.includes('application/json')) data = await res.json(); else data = await res.text();\r
3300
+ const statusClass = res.ok ? 'status-2xx' : (String(res.status).startsWith('5') ? 'status-5xx' : 'status-4xx');\r
3301
+ responseArea.style.display = 'block';\r
3302
+ responseArea.innerHTML = \`\r
3303
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">\r
3304
+ <span class="status-badge \${statusClass}">\${res.status} \${escapeHtml(res.statusText)}</span>\r
3305
+ <span style="font-family:var(--font-mono);font-size:0.8rem;color:var(--text-muted)">\${duration}ms</span>\r
3306
+ </div>\r
3307
+ <div class="section-header">Response Body</div>\r
3308
+ <div class="code-block">\${escapeHtml(typeof data === 'object' ? JSON.stringify(data, null, 2) : String(data))}</div>\r
3309
+ <div class="section-header">Response Headers</div>\r
3310
+ <div class="code-block">\${escapeHtml(Array.from(res.headers.entries()).map(([k, v]) => \`\${k}: \${v}\`).join('\\\\n'))}</div>\r
3311
+ \`;\r
3312
+ } catch (err) {\r
3313
+ responseArea.style.display = 'block';\r
3314
+ responseArea.innerHTML = \`<div style="color:var(--danger);font-weight:600">Error: \${escapeHtml(err.message || String(err))}</div>\`;\r
3315
+ } finally {\r
3316
+ btn.disabled = false;\r
3317
+ btn.innerHTML = originalHtml;\r
3318
+ }\r
3319
+ }\r
3320
+ \r
3321
+ function toggleCard(header) {\r
3322
+ const isExpanded = header.getAttribute('aria-expanded') === 'true';\r
3323
+ header.setAttribute('aria-expanded', String(!isExpanded));\r
3324
+ }\r
3325
+ \r
3326
+ function scrollEndpoint(sanitizedId) {\r
3327
+ const el = document.getElementById(sanitizedId);\r
3328
+ if (!el) return;\r
3329
+ el.scrollIntoView({ behavior: 'smooth', block: 'start' });\r
3330
+ const header = el.querySelector('.card-header');\r
3331
+ if (header && header.getAttribute('aria-expanded') !== 'true') {\r
3332
+ header.setAttribute('aria-expanded', 'true');\r
3333
+ }\r
3334
+ }\r
3335
+ \r
3336
+ function setupSearch() {\r
3337
+ const input = document.getElementById('searchNav');\r
3338
+ input.addEventListener('input', (e) => {\r
3339
+ const val = e.target.value.toLowerCase();\r
3340
+ document.querySelectorAll('.nav-item').forEach(el => {\r
3341
+ const filter = el.dataset.filter || '';\r
3342
+ el.style.display = filter.includes(val) ? 'flex' : 'none';\r
3343
+ });\r
3344
+ });\r
3345
+ }\r
3346
+ \r
3347
+ function attachGlobalListeners() {\r
3348
+ document.querySelectorAll('.endpoint-card .card-header').forEach(header => {\r
3349
+ header.addEventListener('click', () => toggleCard(header));\r
3350
+ });\r
3351
+ document.querySelectorAll('.action-btn').forEach(btn => {\r
3352
+ btn.addEventListener('click', () => executeRequest(btn.dataset.method, btn.dataset.path, btn));\r
3353
+ });\r
3354
+ }\r
3355
+ \r
3356
+ function escapeHtml(str) {\r
3357
+ if (str === undefined || str === null) return '';\r
3358
+ return String(str)\r
3359
+ .replace(/&/g, '&amp;')\r
3360
+ .replace(/</g, '&lt;')\r
3361
+ .replace(/>/g, '&gt;')\r
3362
+ .replace(/"/g, '&quot;')\r
3363
+ .replace(/'/g, '&#39;');\r
3364
+ }\r
3365
+ function escapeHtmlAttr(str) {\r
3366
+ return escapeHtml(str).replace(/"/g, '&quot;').replace(/'/g, '&#39;');\r
3367
+ }\r
3368
+ \r
3369
+ init();\r
3370
+ </script>\r
3371
+ </body>\r
3372
+ \r
3373
+ </html>`;
3374
+
3375
+ // src/swagger/swagger-ui-html.ts
3376
+ function renderSwaggerUiPage(options) {
3377
+ const { title, specUrl } = options;
3378
+ return SWAGGER_UI_MODERN_HTML.replaceAll("__PAGE_TITLE__", escapeHtml(title)).replaceAll(
3379
+ '"___AZURA_SPEC_URL_PLACEHOLDER___"',
3380
+ JSON.stringify(specUrl)
3381
+ );
3382
+ }
3383
+ function escapeHtml(s) {
3384
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3385
+ }
3386
+
3387
+ // src/plugins/SwaggerPlugin.ts
3388
+ function SwaggerPlugin(options) {
3389
+ const {
3390
+ getDocuments,
3391
+ specPath = "/openapi.json",
3392
+ docsPath = "/docs",
3393
+ deepLinking = true,
3394
+ servers,
3395
+ pathPrefix,
3396
+ info
3397
+ } = options;
3398
+ const title = info?.title ?? "API";
3399
+ return async (ctx, next) => {
3400
+ const { req, res } = ctx;
3401
+ const path = req.pathname ?? "";
3402
+ if (path === specPath || path === `${specPath}/`) {
3403
+ const doc = buildOpenApiDocument(getDocuments(), {
3404
+ info: info ?? { title, version: "1.0.0" },
3405
+ servers,
3406
+ pathPrefix
3407
+ });
3408
+ res.json(doc);
3409
+ return;
3410
+ }
3411
+ if (path === docsPath || path === `${docsPath}/`) {
3412
+ const html = renderSwaggerUiPage({
3413
+ title,
3414
+ specUrl: specPath});
3415
+ res.html(html);
3416
+ return;
3417
+ }
3418
+ next();
3419
+ };
3420
+ }
3421
+
2261
3422
  // src/utils/validators/DTOValidator.ts
2262
3423
  var DTOValidator = class {
2263
3424
  static validate(data, schema) {
@@ -2536,6 +3697,8 @@ function LoggingMiddleware() {
2536
3697
  };
2537
3698
  }
2538
3699
 
3700
+ exports.ApiOperation = ApiOperation;
3701
+ exports.ApiTags = ApiTags;
2539
3702
  exports.AzuraServer = AzuraServer;
2540
3703
  exports.Body = Body;
2541
3704
  exports.CORSPlugin = CORSPlugin;
@@ -2580,6 +3743,7 @@ exports.SchemaValidator = SchemaValidator;
2580
3743
  exports.Session = Session;
2581
3744
  exports.SessionPlugin = SessionPlugin;
2582
3745
  exports.StaticPlugin = StaticPlugin;
3746
+ exports.SwaggerPlugin = SwaggerPlugin;
2583
3747
  exports.TimeoutPlugin = TimeoutPlugin;
2584
3748
  exports.UseMiddleware = UseMiddleware;
2585
3749
  exports.applyDecorators = applyDecorators;