jamdesk 1.1.164 → 1.1.166

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jamdesk",
3
- "version": "1.1.164",
3
+ "version": "1.1.166",
4
4
  "description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
5
5
  "keywords": [
6
6
  "jamdesk",
@@ -120,6 +120,79 @@ export function parseErrorDetails(
120
120
  const { pageToFileMap, discoveryHint, repoFullName } = opts;
121
121
  const lowerOutput = output.toLowerCase();
122
122
 
123
+ // R2 upload failure — our storage provider erred, never the customer's
124
+ // content. putWithRetry (scripts/upload-content-to-r2.ts) tags every
125
+ // failed put with this prefix; by the time we see it here the docs
126
+ // compiled fine and only the CDN upload failed. Checked before the
127
+ // generic timeout/OOM classifiers because R2 error text often contains
128
+ // "timeout". Transient indicators mirror isRetryableR2PutError's
129
+ // message heuristics — only those become `temporary` (= silently
130
+ // auto-retried); anything else (403 auth, missing bucket, misconfig)
131
+ // is platform breakage where a retry won't help and the customer +
132
+ // ops must hear about it immediately.
133
+ if (
134
+ phase === 'r2_upload' &&
135
+ (message.includes('R2 put failed') || lowerOutput.includes('r2 put failed'))
136
+ ) {
137
+ const r2Text = `${message} ${lowerOutput}`.toLowerCase();
138
+ const R2_TRANSIENT_INDICATORS = [
139
+ // Authoritative signal from putWithRetry (scripts/upload-content-to-r2.ts):
140
+ // its terminal catch appends "[retry envelope exhausted]" ONLY when the
141
+ // failure was retried the full 5 attempts by status code / error name /
142
+ // $retryable — a richer classification than the message-substring list
143
+ // below. It captures transient failures whose text matches none of those
144
+ // substrings (429 "Too Many Requests", SlowDown "Please reduce your
145
+ // request rate."). Checked first; the substrings below stay as a fallback
146
+ // for the SIGTERM/legacy paths that don't carry the marker.
147
+ 'retry envelope exhausted',
148
+ 'internal error',
149
+ 'please try again',
150
+ 'timeout',
151
+ // Bare Node socket errors ("connect ETIMEDOUT 1.2.3.4:443") do NOT
152
+ // contain the substring "timeout" (note the extra "d") — listed
153
+ // separately, mirroring isRetryableR2PutError which checks both.
154
+ 'etimedout',
155
+ 'econnreset',
156
+ 'socket hang up',
157
+ 'throttl',
158
+ 'slow down',
159
+ 'service unavailable',
160
+ // 502s can surface as a bare "Bad Gateway" body with none of the
161
+ // texts above — putWithRetry retries them by status code, so an
162
+ // exhaustion with this text is transient too.
163
+ 'bad gateway',
164
+ // Matches putWithRetry's defensive post-loop fallback message
165
+ // ("R2 put retry loop exhausted"). That fallback is currently
166
+ // unreachable — the final attempt always rethrows the tagged
167
+ // original error — but match it anyway in case the loop structure
168
+ // changes; exhausting retries implies the error was transient.
169
+ 'retry loop exhausted',
170
+ ];
171
+ if (R2_TRANSIENT_INDICATORS.some((s) => r2Text.includes(s))) {
172
+ return {
173
+ type: 'temporary',
174
+ message: 'Content upload failed',
175
+ details:
176
+ 'Your documentation built successfully, but uploading it to our CDN hit a ' +
177
+ 'temporary storage error on our side. Your content and repository are fine.',
178
+ suggestion:
179
+ "We'll retry this build automatically in a few minutes — no action needed; " +
180
+ 'if a later build succeeded, this failure is already resolved. ' +
181
+ 'If it keeps failing, click "Try Again" or contact support.',
182
+ };
183
+ }
184
+ return {
185
+ type: 'build_error',
186
+ message: 'Content upload failed',
187
+ details:
188
+ 'Uploading your documentation to our CDN failed with a storage error on our ' +
189
+ 'side. Your content and repository are fine.',
190
+ suggestion:
191
+ 'This does not look like a passing glitch, so we have not scheduled an ' +
192
+ 'automatic retry. Please contact support and quote the reference below.',
193
+ };
194
+ }
195
+
123
196
  // Extract error source information upfront - used by multiple error types
124
197
  const errorSource = extractErrorSource(output, pageToFileMap);
125
198
 
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Auto-retry eligibility + customer-facing copy for the graceful-shutdown
3
+ * (SIGTERM/SIGINT) "Build interrupted" failure write in server.ts.
4
+ *
5
+ * Extracted as a pure seam (same rationale as lib/child-process.ts):
6
+ * server.ts starts the HTTP listener and registers the signal handlers at
7
+ * import time, so this logic is unit-tested here instead of through a full
8
+ * server import driving a real signal.
9
+ *
10
+ * Mirrors the reportFailure contract in firestore-reporter.ts exactly —
11
+ * Tasks 4/5 consume `autoRetryEligible === true` as meaning this rule
12
+ * regardless of which write path produced it:
13
+ * - autoRetriedFrom set → NOT eligible (once-only), and the suggestion is
14
+ * rewritten to honest "we already retried" copy: this failure is the one
15
+ * notification the customer receives during a persistent outage, so it
16
+ * must not imply another retry is coming.
17
+ * - tarballKey set → NOT eligible (CLI/CI already saw the failure via
18
+ * non-zero exit; silently publishing later would contradict their
19
+ * recorded result).
20
+ * - JD_AUTO_RETRY_DISABLED=1 → NOT eligible (producer-side kill switch).
21
+ * - snap === null (doc read failed) → NOT eligible, generic copy
22
+ * (fail-closed, legacy behavior).
23
+ */
24
+
25
+ export interface InterruptBuildSnap {
26
+ get(field: string): unknown;
27
+ }
28
+
29
+ export function computeInterruptRetryFields(
30
+ snap: InterruptBuildSnap | null,
31
+ errorRef: string,
32
+ env: Record<string, string | undefined> = process.env,
33
+ ): { autoRetryEligible: boolean; errorSuggestion: string } {
34
+ const isAutoRetryBuild = Boolean(snap?.get('autoRetriedFrom'));
35
+ const autoRetryEligible =
36
+ snap !== null &&
37
+ env.JD_AUTO_RETRY_DISABLED !== '1' &&
38
+ !isAutoRetryBuild &&
39
+ !snap.get('tarballKey');
40
+
41
+ const reference = `\n\nIf you need help, provide this reference: ${errorRef}`;
42
+ const errorSuggestion = isAutoRetryBuild
43
+ ? 'We already retried this build automatically and it failed again — ' +
44
+ 'this looks like a longer outage on our side. Please try again in a ' +
45
+ 'few minutes, or contact support if it persists.' +
46
+ reference
47
+ : `This is temporary and should resolve itself. Please try again in a few minutes.${reference}`;
48
+
49
+ return { autoRetryEligible, errorSuggestion };
50
+ }
@@ -684,14 +684,23 @@ const TRUSTED_PROXY_HEADERS = [
684
684
  'x-jd-project-name',
685
685
  'x-jd-project-logo',
686
686
  // Canonical override: middleware writes this when serving *.jamdesk.app
687
- // directly for a hostAtDocs project that has a registered custom
688
- // domain. The page render path uses it to emit the public-face canonical
689
- // instead of the upstream subdomain URL.
687
+ // directly for a project (ANY hosting mode) whose registered custom
688
+ // domain passed domain-side validation (domain:<host> mapping points back
689
+ // at the slug + domainStatus active). The page render path uses it to
690
+ // emit the public-face canonical instead of the upstream subdomain URL.
690
691
  'x-jd-canonical-host',
691
- // Set when *.jamdesk.app serves a hostAtDocs project that has NOT
692
- // registered a custom domain yet page emits robots: noindex so the
693
- // upstream URL doesn't compete with the (yet-to-arrive) public face
694
- // in search results.
692
+ // The registered domain's OWN serving mode ('true' | 'false', from
693
+ // domainCfg:<host>) decides whether the canonical URL carries the
694
+ // /docs prefix. Only meaningful alongside x-jd-canonical-host; absent
695
+ // for legacy domains without a domainCfg record (page falls back to the
696
+ // projectCfg-derived hostAtDocs).
697
+ 'x-jd-canonical-at-docs',
698
+ // Set when *.jamdesk.app serves a hostAtDocs project with no
699
+ // domain-validated custom domain (none registered, or the projectCfg
700
+ // mirror failed domain-side validation) — page emits robots: noindex so
701
+ // the upstream URL doesn't compete with the public face in search
702
+ // results. NEVER set for hostAtDocs=false tenants without a domain:
703
+ // their subdomain IS the public site.
695
704
  'x-jd-noindex',
696
705
  ] as const;
697
706
 
@@ -703,15 +712,25 @@ const TRUSTED_PROXY_HEADERS = [
703
712
  export interface BuildProjectHeadersOptions {
704
713
  /**
705
714
  * Public-face canonical host. Set when serving *.jamdesk.app directly
706
- * for a hostAtDocs project that has a registered custom domain — the
707
- * page render path emits this host in <link rel="canonical"> instead
708
- * of the upstream subdomain.
715
+ * for a project (any hosting mode) whose registered custom domain passed
716
+ * domain-side validation — the page render path emits this host in
717
+ * <link rel="canonical"> instead of the upstream subdomain.
709
718
  */
710
719
  canonicalHost?: string;
720
+ /**
721
+ * The registered domain's OWN serving mode (domainCfg:<host>.hostAtDocs).
722
+ * Decides whether the canonical URL carries the /docs prefix — the
723
+ * projectCfg-side hostAtDocs describes how the SUBDOMAIN serves, which
724
+ * can diverge from the domain's live mode. undefined = legacy domain
725
+ * with no domainCfg record; the page falls back to the projectCfg-derived
726
+ * hostAtDocs. Only emitted when canonicalHost is set.
727
+ */
728
+ canonicalAtDocs?: boolean;
711
729
  /**
712
730
  * When true, emit `x-jd-noindex: true` so the page emits a robots
713
- * noindex tag. Used for *.jamdesk.app subdomains of hostAtDocs
714
- * projects without a custom domain yet.
731
+ * noindex tag. Used for *.jamdesk.app subdomains of hostAtDocs projects
732
+ * without a domain-validated custom domain. Never set for
733
+ * hostAtDocs=false tenants — their subdomain is the public site.
715
734
  */
716
735
  noindex?: boolean;
717
736
  }
@@ -724,6 +743,8 @@ export interface BuildProjectHeadersOptions {
724
743
  * - x-host-at-docs — whether docs are mounted at /docs
725
744
  * - x-jd-language — locale code if the path starts with one (e.g. /fr/...)
726
745
  * - x-jd-canonical-host — public-face host (opts.canonicalHost)
746
+ * - x-jd-canonical-at-docs — domain's own /docs mode (opts.canonicalAtDocs;
747
+ * only alongside x-jd-canonical-host)
727
748
  * - x-jd-noindex — "true" when opts.noindex is set
728
749
  *
729
750
  * Strips any client-supplied copies of those headers from the inbound
@@ -763,6 +784,10 @@ export function buildProjectHeaders(
763
784
 
764
785
  if (opts.canonicalHost) {
765
786
  newHeaders.set('x-jd-canonical-host', opts.canonicalHost);
787
+ // Only meaningful with a canonical host; getBaseUrl ignores it otherwise.
788
+ if (opts.canonicalAtDocs !== undefined) {
789
+ newHeaders.set('x-jd-canonical-at-docs', opts.canonicalAtDocs ? 'true' : 'false');
790
+ }
766
791
  }
767
792
  if (opts.noindex) {
768
793
  newHeaders.set('x-jd-noindex', 'true');
@@ -132,14 +132,21 @@ export function parseCacheKey(cacheKey: string): { projectSlug: string; pagePath
132
132
  *
133
133
  * Header priority (highest first):
134
134
  * 1. x-jd-canonical-host — internal override set by middleware when a
135
- * hostAtDocs project is served directly via *.jamdesk.app and has a
136
- * registered customDomain. Forces the canonical to the public face.
135
+ * project (any hosting mode) is served directly via *.jamdesk.app and
136
+ * its registered customDomain passed domain-side validation. Forces
137
+ * the canonical to the public face. When present, the companion
138
+ * x-jd-canonical-at-docs header (the DOMAIN's own serving mode, from
139
+ * domainCfg:<host>) overrides the hostAtDocs param for the /docs
140
+ * prefix decision — projectCfg's hostAtDocs describes how the
141
+ * subdomain serves, which can diverge from the live domain mode.
142
+ * Companion header absent (legacy domains without a domainCfg record)
143
+ * → the hostAtDocs param decides, as before.
137
144
  * 2. x-jamdesk-forwarded-host — set by the Cloudflare Worker proxy
138
145
  * (e.g. jamdesk.com → forwarded `jamdesk.com`).
139
146
  * 3. host header — direct request hostname.
140
147
  * 4. Subdomain fallback `<slug>.jamdesk.app` (no headers available).
141
148
  *
142
- * Both override headers are in TRUSTED_PROXY_HEADERS so a client can't
149
+ * All override headers are in TRUSTED_PROXY_HEADERS so a client can't
143
150
  * spoof them.
144
151
  *
145
152
  * When hostAtDocs=true, includes /docs path prefix (forwarded-host and
@@ -157,7 +164,13 @@ export function getBaseUrl(headers: Headers, projectSlug: string, hostAtDocs = f
157
164
 
158
165
  if (host) {
159
166
  const hostname = host.split(':')[0];
160
- return hostAtDocs ? `https://${hostname}/docs` : `https://${hostname}`;
167
+ // Domain-mode override: only consulted when the canonical-host override
168
+ // is active (the companion header is only ever set alongside it).
169
+ const canonicalAtDocs = canonicalHost
170
+ ? headers.get('x-jd-canonical-at-docs')
171
+ : null;
172
+ const atDocs = canonicalAtDocs !== null ? canonicalAtDocs === 'true' : hostAtDocs;
173
+ return atDocs ? `https://${hostname}/docs` : `https://${hostname}`;
161
174
  }
162
175
 
163
176
  // Fallback to subdomain URL (subdomains always serve at root, never /docs)
@@ -256,10 +256,11 @@ export async function buildDocMetadata(input: RenderInput): Promise<Metadata> {
256
256
  const { slug: slugInput, projectSlug, hostAtDocs, requestHeaders } = input;
257
257
 
258
258
  // Middleware sets `x-jd-noindex: true` when serving a hostAtDocs project
259
- // directly via *.jamdesk.app and the project has no registered custom
260
- // domain yet. The upstream subdomain shouldn't compete with the (yet-to-
261
- // arrive) public face in search results emit robots noindex so Google
262
- // skips it. See proxy.ts → projectHeaderOptsForCanonical for the source.
259
+ // directly via *.jamdesk.app and the project has no domain-validated
260
+ // custom domain (none registered, or the registration is stale/inactive).
261
+ // The upstream subdomain shouldn't compete with the public face in search
262
+ // results — emit robots noindex so Google skips it. See proxy.ts →
263
+ // projectHeaderOptsForCanonical for the source.
263
264
  const noindexHeader = requestHeaders?.get('x-jd-noindex') === 'true';
264
265
 
265
266
  if (isIsrMode()) {
@@ -43,10 +43,11 @@ const CONTENT_TYPES: Record<string, string> = {
43
43
  * have no business being served from a non-canonical host.
44
44
  *
45
45
  * `x-jd-noindex: true` is set by `proxy.ts` (`projectHeaderOptsForCanonical`)
46
- * when a project uses `hostAtDocs` WITHOUT a custom domain — i.e. its public
47
- * face is the raw `<slug>.jamdesk.app` subdomain with no canonical elsewhere.
48
- * Once a custom domain is registered, the header is replaced by a canonical-
49
- * host override and noindex is no longer set.
46
+ * when a project uses `hostAtDocs` WITHOUT a domain-validated custom domain
47
+ * — i.e. its public face is the raw `<slug>.jamdesk.app` subdomain with no
48
+ * canonical elsewhere. Once a custom domain is registered AND active, the
49
+ * header is replaced by a canonical-host override and noindex is no longer
50
+ * set.
50
51
  *
51
52
  * `robots.txt` is handled separately — it returns a Disallow-all directive,
52
53
  * not a 404, so crawlers get an explicit "don't crawl" signal.
@@ -395,9 +395,6 @@
395
395
  "cpu": [
396
396
  "arm"
397
397
  ],
398
- "libc": [
399
- "glibc"
400
- ],
401
398
  "license": "LGPL-3.0-or-later",
402
399
  "optional": true,
403
400
  "os": [
@@ -414,9 +411,6 @@
414
411
  "cpu": [
415
412
  "arm64"
416
413
  ],
417
- "libc": [
418
- "glibc"
419
- ],
420
414
  "license": "LGPL-3.0-or-later",
421
415
  "optional": true,
422
416
  "os": [
@@ -433,9 +427,6 @@
433
427
  "cpu": [
434
428
  "ppc64"
435
429
  ],
436
- "libc": [
437
- "glibc"
438
- ],
439
430
  "license": "LGPL-3.0-or-later",
440
431
  "optional": true,
441
432
  "os": [
@@ -452,9 +443,6 @@
452
443
  "cpu": [
453
444
  "riscv64"
454
445
  ],
455
- "libc": [
456
- "glibc"
457
- ],
458
446
  "license": "LGPL-3.0-or-later",
459
447
  "optional": true,
460
448
  "os": [
@@ -471,9 +459,6 @@
471
459
  "cpu": [
472
460
  "s390x"
473
461
  ],
474
- "libc": [
475
- "glibc"
476
- ],
477
462
  "license": "LGPL-3.0-or-later",
478
463
  "optional": true,
479
464
  "os": [
@@ -490,9 +475,6 @@
490
475
  "cpu": [
491
476
  "x64"
492
477
  ],
493
- "libc": [
494
- "glibc"
495
- ],
496
478
  "license": "LGPL-3.0-or-later",
497
479
  "optional": true,
498
480
  "os": [
@@ -509,9 +491,6 @@
509
491
  "cpu": [
510
492
  "arm64"
511
493
  ],
512
- "libc": [
513
- "musl"
514
- ],
515
494
  "license": "LGPL-3.0-or-later",
516
495
  "optional": true,
517
496
  "os": [
@@ -528,9 +507,6 @@
528
507
  "cpu": [
529
508
  "x64"
530
509
  ],
531
- "libc": [
532
- "musl"
533
- ],
534
510
  "license": "LGPL-3.0-or-later",
535
511
  "optional": true,
536
512
  "os": [
@@ -547,9 +523,6 @@
547
523
  "cpu": [
548
524
  "arm"
549
525
  ],
550
- "libc": [
551
- "glibc"
552
- ],
553
526
  "license": "Apache-2.0",
554
527
  "optional": true,
555
528
  "os": [
@@ -572,9 +545,6 @@
572
545
  "cpu": [
573
546
  "arm64"
574
547
  ],
575
- "libc": [
576
- "glibc"
577
- ],
578
548
  "license": "Apache-2.0",
579
549
  "optional": true,
580
550
  "os": [
@@ -597,9 +567,6 @@
597
567
  "cpu": [
598
568
  "ppc64"
599
569
  ],
600
- "libc": [
601
- "glibc"
602
- ],
603
570
  "license": "Apache-2.0",
604
571
  "optional": true,
605
572
  "os": [
@@ -622,9 +589,6 @@
622
589
  "cpu": [
623
590
  "riscv64"
624
591
  ],
625
- "libc": [
626
- "glibc"
627
- ],
628
592
  "license": "Apache-2.0",
629
593
  "optional": true,
630
594
  "os": [
@@ -647,9 +611,6 @@
647
611
  "cpu": [
648
612
  "s390x"
649
613
  ],
650
- "libc": [
651
- "glibc"
652
- ],
653
614
  "license": "Apache-2.0",
654
615
  "optional": true,
655
616
  "os": [
@@ -672,9 +633,6 @@
672
633
  "cpu": [
673
634
  "x64"
674
635
  ],
675
- "libc": [
676
- "glibc"
677
- ],
678
636
  "license": "Apache-2.0",
679
637
  "optional": true,
680
638
  "os": [
@@ -697,9 +655,6 @@
697
655
  "cpu": [
698
656
  "arm64"
699
657
  ],
700
- "libc": [
701
- "musl"
702
- ],
703
658
  "license": "Apache-2.0",
704
659
  "optional": true,
705
660
  "os": [
@@ -722,9 +677,6 @@
722
677
  "cpu": [
723
678
  "x64"
724
679
  ],
725
- "libc": [
726
- "musl"
727
- ],
728
680
  "license": "Apache-2.0",
729
681
  "optional": true,
730
682
  "os": [
@@ -999,9 +951,6 @@
999
951
  "cpu": [
1000
952
  "arm64"
1001
953
  ],
1002
- "libc": [
1003
- "glibc"
1004
- ],
1005
954
  "license": "MIT",
1006
955
  "optional": true,
1007
956
  "os": [
@@ -1018,9 +967,6 @@
1018
967
  "cpu": [
1019
968
  "arm64"
1020
969
  ],
1021
- "libc": [
1022
- "musl"
1023
- ],
1024
970
  "license": "MIT",
1025
971
  "optional": true,
1026
972
  "os": [
@@ -1037,9 +983,6 @@
1037
983
  "cpu": [
1038
984
  "x64"
1039
985
  ],
1040
- "libc": [
1041
- "glibc"
1042
- ],
1043
986
  "license": "MIT",
1044
987
  "optional": true,
1045
988
  "os": [
@@ -1056,9 +999,6 @@
1056
999
  "cpu": [
1057
1000
  "x64"
1058
1001
  ],
1059
- "libc": [
1060
- "musl"
1061
- ],
1062
1002
  "license": "MIT",
1063
1003
  "optional": true,
1064
1004
  "os": [
@@ -1398,9 +1338,6 @@
1398
1338
  "cpu": [
1399
1339
  "arm64"
1400
1340
  ],
1401
- "libc": [
1402
- "glibc"
1403
- ],
1404
1341
  "license": "MIT",
1405
1342
  "optional": true,
1406
1343
  "os": [
@@ -1417,9 +1354,6 @@
1417
1354
  "cpu": [
1418
1355
  "arm64"
1419
1356
  ],
1420
- "libc": [
1421
- "musl"
1422
- ],
1423
1357
  "license": "MIT",
1424
1358
  "optional": true,
1425
1359
  "os": [
@@ -1436,9 +1370,6 @@
1436
1370
  "cpu": [
1437
1371
  "x64"
1438
1372
  ],
1439
- "libc": [
1440
- "glibc"
1441
- ],
1442
1373
  "license": "MIT",
1443
1374
  "optional": true,
1444
1375
  "os": [
@@ -1455,9 +1386,6 @@
1455
1386
  "cpu": [
1456
1387
  "x64"
1457
1388
  ],
1458
- "libc": [
1459
- "musl"
1460
- ],
1461
1389
  "license": "MIT",
1462
1390
  "optional": true,
1463
1391
  "os": [
@@ -3959,9 +3887,6 @@
3959
3887
  "cpu": [
3960
3888
  "arm64"
3961
3889
  ],
3962
- "libc": [
3963
- "glibc"
3964
- ],
3965
3890
  "license": "MPL-2.0",
3966
3891
  "optional": true,
3967
3892
  "os": [
@@ -3982,9 +3907,6 @@
3982
3907
  "cpu": [
3983
3908
  "arm64"
3984
3909
  ],
3985
- "libc": [
3986
- "musl"
3987
- ],
3988
3910
  "license": "MPL-2.0",
3989
3911
  "optional": true,
3990
3912
  "os": [
@@ -4005,9 +3927,6 @@
4005
3927
  "cpu": [
4006
3928
  "x64"
4007
3929
  ],
4008
- "libc": [
4009
- "glibc"
4010
- ],
4011
3930
  "license": "MPL-2.0",
4012
3931
  "optional": true,
4013
3932
  "os": [
@@ -4028,9 +3947,6 @@
4028
3947
  "cpu": [
4029
3948
  "x64"
4030
3949
  ],
4031
- "libc": [
4032
- "musl"
4033
- ],
4034
3950
  "license": "MPL-2.0",
4035
3951
  "optional": true,
4036
3952
  "os": [