apidocly 1.0.3 → 1.0.5

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 CHANGED
@@ -3,14 +3,13 @@
3
3
  [![npm version](https://img.shields.io/npm/v/apidocly.svg)](https://www.npmjs.com/package/apidocly)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/apidocly.svg)](https://www.npmjs.com/package/apidocly)
5
5
 
6
- API documentation generator with shadcn-style dark UI and password protection.
6
+ API documentation generator with dark UI and password protection.
7
7
 
8
8
  A modern, dark-themed package that generates pure HTML/CSS/JS documentation.
9
9
 
10
10
  ## Features
11
11
 
12
12
  - **apidoc-compatible**: Uses the same annotation syntax as apidoc.js
13
- - **Shadcn Dark UI**: Beautiful, modern dark theme inspired by shadcn/ui
14
13
  - **OpenAPI Export**: Automatically generates OpenAPI 3.0 JSON for AI tools and code generators
15
14
  - **Password Protection**: Optional client-side password protection with AES-256-GCM encryption
16
15
  - **Zero Dependencies Output**: Generated docs are pure HTML/CSS/JS - no frameworks required
@@ -84,11 +83,13 @@ Create `apidocly.json` in your project root:
84
83
  "passwordMessage": "Enter password to view documentation",
85
84
  "header": {
86
85
  "title": "Introduction",
87
- "filename": "header.md"
86
+ "filename": "header.md",
87
+ "collapsed": false
88
88
  },
89
89
  "footer": {
90
90
  "title": "Footer",
91
- "filename": "footer.md"
91
+ "filename": "footer.md",
92
+ "collapsed": true
92
93
  }
93
94
  }
94
95
  ```
@@ -38,7 +38,8 @@ async function generateDocs(apiData, config, outputPath, options = {}) {
38
38
  }
39
39
 
40
40
  // Always save version history for version switching feature
41
- saveVersionHistory(apiData, config, outputPath, verbose);
41
+ const plainPassword = password ? getPlainPassword(password) : null;
42
+ saveVersionHistory(apiData, config, outputPath, verbose, plainPassword);
42
43
 
43
44
  generateApiDataFile(apiData, outputPath, password, authData);
44
45
 
@@ -229,7 +230,7 @@ function escapeHtml(str) {
229
230
  .replace(/'/g, ''');
230
231
  }
231
232
 
232
- function saveVersionHistory(apiData, config, outputPath, verbose) {
233
+ function saveVersionHistory(apiData, config, outputPath, verbose, password = null) {
233
234
  const versionsDir = path.join(outputPath, 'versions');
234
235
 
235
236
  if (!fs.existsSync(versionsDir)) {
@@ -294,18 +295,32 @@ function saveVersionHistory(apiData, config, outputPath, verbose) {
294
295
  }
295
296
  };
296
297
 
297
- fs.writeFileSync(versionPath, JSON.stringify(versionData, null, 2), 'utf8');
298
+ // If password is provided, encrypt the version file
299
+ if (password) {
300
+ const encryptedVersionData = encryptData(versionData, password);
301
+ fs.writeFileSync(versionPath, encryptedVersionData, 'utf8');
302
+ } else {
303
+ fs.writeFileSync(versionPath, JSON.stringify(versionData, null, 2), 'utf8');
304
+ }
298
305
 
299
- // Save versions index
306
+ // Save versions index (not encrypted - only contains metadata, no API data)
300
307
  fs.writeFileSync(versionsIndexPath, JSON.stringify(versionsIndex, null, 2), 'utf8');
301
308
 
302
309
  // Load all version data to embed in versions.js (for file:// protocol support)
310
+ // When encrypted, we store the encrypted strings directly
303
311
  const allVersionData = {};
304
312
  for (const v of versionsIndex) {
305
313
  const vPath = path.join(versionsDir, v.filename);
306
314
  if (fs.existsSync(vPath)) {
307
315
  try {
308
- allVersionData[v.filename] = JSON.parse(fs.readFileSync(vPath, 'utf8'));
316
+ const fileContent = fs.readFileSync(vPath, 'utf8');
317
+ if (password) {
318
+ // Store encrypted string directly
319
+ allVersionData[v.filename] = fileContent;
320
+ } else {
321
+ // Store parsed JSON
322
+ allVersionData[v.filename] = JSON.parse(fileContent);
323
+ }
309
324
  } catch (e) {
310
325
  // Skip if can't read
311
326
  }
@@ -313,13 +328,14 @@ function saveVersionHistory(apiData, config, outputPath, verbose) {
313
328
  }
314
329
 
315
330
  // Generate versions.js for frontend
316
- // Include withCompare setting and embedded version data for file:// protocol support
331
+ // Include withCompare setting, encrypted flag, and embedded version data
317
332
  const withCompare = config.template && config.template.withCompare ? true : false;
318
- const versionsJs = `var apiVersions=${JSON.stringify(versionsIndex)};var apiVersionsConfig={withCompare:${withCompare}};var apiVersionsData=${JSON.stringify(allVersionData)};`;
333
+ const isEncrypted = !!password;
334
+ const versionsJs = `var apiVersions=${JSON.stringify(versionsIndex)};var apiVersionsConfig={withCompare:${withCompare},encrypted:${isEncrypted}};var apiVersionsData=${JSON.stringify(allVersionData)};`;
319
335
  fs.writeFileSync(path.join(outputPath, 'versions.js'), versionsJs, 'utf8');
320
336
 
321
337
  if (verbose) {
322
- console.log(` Saved version ${version} to history (${versionsIndex.length} versions total)`);
338
+ console.log(` Saved version ${version} to history (${versionsIndex.length} versions total)${password ? ' (encrypted)' : ''}`);
323
339
  }
324
340
  }
325
341
 
package/package.json CHANGED
@@ -1,46 +1,46 @@
1
- {
2
- "name": "apidocly",
3
- "version": "1.0.3",
4
- "description": "API documentation generator with beautiful dark UI, password protection, and OpenAPI/Rest client export.",
5
- "main": "lib/index.js",
6
- "bin": {
7
- "apidocly": "bin/apidocly.js"
8
- },
9
- "scripts": {
10
- "test": "echo \"No tests yet\" && exit 0"
11
- },
12
- "keywords": [
13
- "api",
14
- "documentation",
15
- "generator",
16
- "apidoc",
17
- "openapi",
18
- "swagger",
19
- "dark-mode",
20
- "rest-api",
21
- "api-docs",
22
- "jsdoc",
23
- "cli"
24
- ],
25
- "author": "",
26
- "license": "MIT",
27
- "homepage": "https://apidocly.com",
28
- "bugs": {
29
- "url": "https://apidocly.com"
30
- },
31
- "dependencies": {
32
- "commander": "^11.1.0",
33
- "glob": "11.1.0",
34
- "terser": "^5.37.0"
35
- },
36
- "engines": {
37
- "node": ">=14.0.0"
38
- },
39
- "files": [
40
- "bin",
41
- "lib",
42
- "template",
43
- "README.md",
44
- "LICENSE"
45
- ]
46
- }
1
+ {
2
+ "name": "apidocly",
3
+ "version": "1.0.5",
4
+ "description": "API documentation generator with beautiful dark UI, password protection, and OpenAPI/Rest client export.",
5
+ "main": "lib/index.js",
6
+ "bin": {
7
+ "apidocly": "bin/apidocly.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"No tests yet\" && exit 0"
11
+ },
12
+ "keywords": [
13
+ "api",
14
+ "documentation",
15
+ "generator",
16
+ "apidoc",
17
+ "openapi",
18
+ "swagger",
19
+ "dark-mode",
20
+ "rest-api",
21
+ "api-docs",
22
+ "jsdoc",
23
+ "cli"
24
+ ],
25
+ "author": "",
26
+ "license": "MIT",
27
+ "homepage": "https://apidocly.com",
28
+ "bugs": {
29
+ "url": "https://apidocly.com"
30
+ },
31
+ "dependencies": {
32
+ "commander": "^11.1.0",
33
+ "glob": "11.1.0",
34
+ "terser": "^5.37.0"
35
+ },
36
+ "engines": {
37
+ "node": ">=14.0.0"
38
+ },
39
+ "files": [
40
+ "bin",
41
+ "lib",
42
+ "template",
43
+ "README.md",
44
+ "LICENSE"
45
+ ]
46
+ }
@@ -810,6 +810,190 @@ a:hover {
810
810
  backdrop-filter: blur(10px);
811
811
  }
812
812
 
813
+ /* Documentation Header Block (collapsible) */
814
+ .doc-header-block {
815
+ margin: 0 0 1.5rem;
816
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.02) 0%, rgba(255, 255, 255, 0.01) 100%);
817
+ border: 1px solid var(--border);
818
+ border-radius: var(--radius-xl);
819
+ overflow: hidden;
820
+ backdrop-filter: blur(10px);
821
+ }
822
+
823
+ .doc-header-block.hidden {
824
+ display: none;
825
+ }
826
+
827
+ .doc-header-toggle {
828
+ display: flex;
829
+ align-items: center;
830
+ justify-content: space-between;
831
+ padding: 1.25rem 1.5rem;
832
+ background: rgba(255, 255, 255, 0.02);
833
+ border-bottom: 1px solid var(--border);
834
+ cursor: pointer;
835
+ user-select: none;
836
+ transition: background-color 0.2s;
837
+ }
838
+
839
+ .doc-header-toggle:hover {
840
+ background: rgba(255, 255, 255, 0.04);
841
+ }
842
+
843
+ .doc-header-block.collapsed .doc-header-toggle {
844
+ border-bottom: none;
845
+ }
846
+
847
+ .doc-header-toggle-title {
848
+ font-size: 0.9375rem;
849
+ font-weight: 600;
850
+ color: var(--foreground);
851
+ }
852
+
853
+ .doc-header-chevron {
854
+ transition: transform 0.2s;
855
+ color: var(--muted-foreground);
856
+ flex-shrink: 0;
857
+ }
858
+
859
+ .doc-header-block.collapsed .doc-header-chevron {
860
+ transform: rotate(-90deg);
861
+ }
862
+
863
+ .doc-header-block.collapsed .doc-header-content {
864
+ display: none;
865
+ }
866
+
867
+ /* Documentation Header Content (from markdown) */
868
+ .doc-header-content {
869
+ padding: 1.25rem 1.5rem;
870
+ color: var(--muted-foreground);
871
+ font-size: 0.9375rem;
872
+ line-height: 1.7;
873
+ }
874
+
875
+ .doc-header-content > *:first-child {
876
+ margin-top: 0;
877
+ }
878
+
879
+ .doc-header-content > *:last-child {
880
+ margin-bottom: 0;
881
+ }
882
+
883
+ .doc-header-content h1,
884
+ .doc-header-content h2,
885
+ .doc-header-content h3,
886
+ .doc-header-content h4 {
887
+ color: var(--foreground);
888
+ margin-top: 1.25rem;
889
+ margin-bottom: 0.75rem;
890
+ }
891
+
892
+ .doc-header-content h1 { font-size: 1.5rem; }
893
+ .doc-header-content h2 { font-size: 1.25rem; }
894
+ .doc-header-content h3 { font-size: 1.125rem; }
895
+ .doc-header-content h4 { font-size: 1rem; }
896
+
897
+ .doc-header-content p {
898
+ margin-bottom: 0.75rem;
899
+ }
900
+
901
+ .doc-header-content ul,
902
+ .doc-header-content ol {
903
+ margin: 0.75rem 0;
904
+ padding-left: 1.5rem;
905
+ }
906
+
907
+ .doc-header-content li {
908
+ margin-bottom: 0.375rem;
909
+ }
910
+
911
+ .doc-header-content li > ul,
912
+ .doc-header-content li > ol {
913
+ margin-top: 0.375rem;
914
+ margin-bottom: 0;
915
+ }
916
+
917
+ .doc-header-content blockquote {
918
+ margin: 1rem 0;
919
+ padding: 0.75rem 1rem;
920
+ border-left: 3px solid var(--primary);
921
+ background: rgba(255, 255, 255, 0.02);
922
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
923
+ }
924
+
925
+ .doc-header-content blockquote p {
926
+ margin: 0;
927
+ }
928
+
929
+ .doc-header-content table {
930
+ width: 100%;
931
+ border-collapse: collapse;
932
+ margin: 1rem 0;
933
+ font-size: 0.875rem;
934
+ }
935
+
936
+ .doc-header-content th,
937
+ .doc-header-content td {
938
+ padding: 0.625rem 0.75rem;
939
+ border: 1px solid var(--border);
940
+ text-align: left;
941
+ }
942
+
943
+ .doc-header-content th {
944
+ background: var(--secondary);
945
+ color: var(--foreground);
946
+ font-weight: 600;
947
+ }
948
+
949
+ .doc-header-content tr:nth-child(even) td {
950
+ background: rgba(255, 255, 255, 0.02);
951
+ }
952
+
953
+ .doc-header-content hr {
954
+ margin: 1.5rem 0;
955
+ border: none;
956
+ border-top: 1px solid var(--border);
957
+ }
958
+
959
+ .doc-header-content code {
960
+ background: rgba(255, 255, 255, 0.05);
961
+ padding: 0.15rem 0.4rem;
962
+ border-radius: var(--radius-sm);
963
+ font-family: var(--font-mono);
964
+ font-size: 0.875em;
965
+ color: var(--primary);
966
+ }
967
+
968
+ .doc-header-content pre {
969
+ background: rgba(0, 0, 0, 0.3);
970
+ border: 1px solid var(--border);
971
+ border-radius: var(--radius);
972
+ padding: 1rem;
973
+ margin: 1rem 0;
974
+ overflow-x: auto;
975
+ }
976
+
977
+ .doc-header-content pre code {
978
+ background: none;
979
+ padding: 0;
980
+ color: var(--foreground);
981
+ }
982
+
983
+ .doc-header-content a {
984
+ color: var(--primary);
985
+ text-decoration: none;
986
+ }
987
+
988
+ .doc-header-content a:hover {
989
+ text-decoration: underline;
990
+ }
991
+
992
+ .doc-header-content strong {
993
+ color: var(--foreground);
994
+ font-weight: 600;
995
+ }
996
+
813
997
  /* Section Header */
814
998
  .section-header {
815
999
  display: flex;
@@ -157,6 +157,17 @@
157
157
  </div>
158
158
  </div>
159
159
 
160
+ <!-- Header Content (from markdown) - collapsible, separate block -->
161
+ <div class="doc-header-block hidden" id="doc-header-block">
162
+ <div class="doc-header-toggle" onclick="toggleDocHeader()">
163
+ <span class="doc-header-toggle-title" id="doc-header-toggle-title">Documentation</span>
164
+ <svg class="doc-header-chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
165
+ <path d="m6 9 6 6 6-6"></path>
166
+ </svg>
167
+ </div>
168
+ <div class="doc-header-content" id="doc-header-content"></div>
169
+ </div>
170
+
160
171
  <!-- Content Container -->
161
172
  <div class="content-container" id="content-container">
162
173
  <!-- Groups View (default) -->
@@ -197,11 +197,47 @@
197
197
  initEnvironmentSelector();
198
198
  initVersionComparison();
199
199
  updateStats();
200
+ renderHeaderContent();
200
201
  renderGroups();
201
202
  setupEventListeners();
202
203
  handleInitialHash();
203
204
  }
204
205
 
206
+ // Render header content from markdown
207
+ function renderHeaderContent() {
208
+ const headerBlock = document.getElementById('doc-header-block');
209
+ const headerContent = document.getElementById('doc-header-content');
210
+ const headerToggleTitle = document.getElementById('doc-header-toggle-title');
211
+ if (!headerBlock || !headerContent) return;
212
+
213
+ if (apiData.header && apiData.header.content) {
214
+ // Set the toggle title
215
+ if (headerToggleTitle && apiData.header.title) {
216
+ headerToggleTitle.textContent = apiData.header.title;
217
+ }
218
+ // Render markdown content
219
+ headerContent.innerHTML = marked.parse(apiData.header.content);
220
+ headerBlock.classList.remove('hidden');
221
+
222
+ // Determine collapsed state: localStorage overrides config default
223
+ const savedState = localStorage.getItem('apidocly-header-collapsed');
224
+ let isCollapsed;
225
+ if (savedState !== null) {
226
+ // User has manually toggled, use their preference
227
+ isCollapsed = savedState === 'true';
228
+ } else {
229
+ // Use config default (collapsed: true means start collapsed)
230
+ isCollapsed = apiData.header.collapsed !== false;
231
+ }
232
+
233
+ if (isCollapsed) {
234
+ headerBlock.classList.add('collapsed');
235
+ }
236
+ } else {
237
+ headerBlock.classList.add('hidden');
238
+ }
239
+ }
240
+
205
241
  // Environment Selector
206
242
  function initEnvironmentSelector() {
207
243
  const environments = apiData.project.environments || [];
@@ -2028,6 +2064,15 @@
2028
2064
  }
2029
2065
 
2030
2066
  // Global functions
2067
+ window.toggleDocHeader = function() {
2068
+ const block = document.getElementById('doc-header-block');
2069
+ if (block) {
2070
+ block.classList.toggle('collapsed');
2071
+ // Save state to localStorage
2072
+ localStorage.setItem('apidocly-header-collapsed', block.classList.contains('collapsed'));
2073
+ }
2074
+ };
2075
+
2031
2076
  window.toggleExample = function(header) {
2032
2077
  const block = header.closest('.example-block');
2033
2078
  block.classList.toggle('collapsed');
@@ -2667,8 +2712,28 @@
2667
2712
 
2668
2713
  // Check if version data is embedded in versions.js (for file:// protocol support)
2669
2714
  if (typeof apiVersionsData !== 'undefined' && apiVersionsData[filename]) {
2670
- versionCache[filename] = apiVersionsData[filename];
2671
- return apiVersionsData[filename];
2715
+ const versionContent = apiVersionsData[filename];
2716
+
2717
+ // Check if data is encrypted
2718
+ if (typeof apiVersionsConfig !== 'undefined' && apiVersionsConfig.encrypted) {
2719
+ // Decrypt using the stored password from auth module
2720
+ const storedPwd = sessionStorage.getItem('apidocly-pwd');
2721
+ if (!storedPwd) {
2722
+ throw new Error('Cannot decrypt version data: no password available');
2723
+ }
2724
+ try {
2725
+ const decrypted = await window.apiDoclyAuth.decryptData(versionContent, storedPwd);
2726
+ versionCache[filename] = decrypted;
2727
+ return decrypted;
2728
+ } catch (e) {
2729
+ console.error('Failed to decrypt version data:', e);
2730
+ throw new Error('Failed to decrypt version data');
2731
+ }
2732
+ } else {
2733
+ // Not encrypted, use directly
2734
+ versionCache[filename] = versionContent;
2735
+ return versionContent;
2736
+ }
2672
2737
  }
2673
2738
 
2674
2739
  // Fall back to fetch (for http:// protocol)
@@ -2679,7 +2744,20 @@
2679
2744
  throw new Error('Failed to load version: ' + filename + ' (HTTP ' + response.status + ')');
2680
2745
  }
2681
2746
 
2682
- const data = await response.json();
2747
+ // Check if response is encrypted (starts with base64 data, not JSON)
2748
+ const text = await response.text();
2749
+ let data;
2750
+
2751
+ if (typeof apiVersionsConfig !== 'undefined' && apiVersionsConfig.encrypted) {
2752
+ const storedPwd = sessionStorage.getItem('apidocly-pwd');
2753
+ if (!storedPwd) {
2754
+ throw new Error('Cannot decrypt version data: no password available');
2755
+ }
2756
+ data = await window.apiDoclyAuth.decryptData(text, storedPwd);
2757
+ } else {
2758
+ data = JSON.parse(text);
2759
+ }
2760
+
2683
2761
  versionCache[filename] = data;
2684
2762
  return data;
2685
2763
  } catch (error) {