jamdesk 1.1.203 → 1.1.205
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/dist/__tests__/integration/fix-scan-integration.test.js.map +1 -1
- package/dist/__tests__/integration/init.integration.test.js.map +1 -1
- package/dist/__tests__/integration/prepublish.integration.test.js.map +1 -1
- package/dist/__tests__/integration/validate.integration.test.js.map +1 -1
- package/dist/__tests__/unit/banner-art-sync.test.js.map +1 -1
- package/dist/__tests__/unit/deploy-templates.test.js.map +1 -1
- package/dist/__tests__/unit/deploy.test.js +106 -0
- package/dist/__tests__/unit/deploy.test.js.map +1 -1
- package/dist/__tests__/unit/deprecated-components-sync.test.js.map +1 -1
- package/dist/__tests__/unit/deps-sync.test.js.map +1 -1
- package/dist/__tests__/unit/dev-spinner-ownership.test.js.map +1 -1
- package/dist/__tests__/unit/docs-config.test.js.map +1 -1
- package/dist/__tests__/unit/fix-engine-sync.test.js.map +1 -1
- package/dist/__tests__/unit/frontmatter-sync.test.js.map +1 -1
- package/dist/__tests__/unit/heading-extractor-sync.test.js.map +1 -1
- package/dist/__tests__/unit/inert-config-keys-sync.test.js.map +1 -1
- package/dist/__tests__/unit/init.test.js.map +1 -1
- package/dist/__tests__/unit/install-banner-gating.test.js.map +1 -1
- package/dist/__tests__/unit/jwt-auth-config-drift.test.d.ts +22 -0
- package/dist/__tests__/unit/jwt-auth-config-drift.test.d.ts.map +1 -0
- package/dist/__tests__/unit/jwt-auth-config-drift.test.js +193 -0
- package/dist/__tests__/unit/jwt-auth-config-drift.test.js.map +1 -0
- package/dist/__tests__/unit/openapi-schema-patch-sync.test.js.map +1 -1
- package/dist/__tests__/unit/openapi-schema-runtime-patch.test.js +1 -1
- package/dist/__tests__/unit/openapi.test.js.map +1 -1
- package/dist/__tests__/unit/package-config.test.js.map +1 -1
- package/dist/__tests__/unit/project-favicon.test.js.map +1 -1
- package/dist/__tests__/unit/prompt-vendoring.test.js.map +1 -1
- package/dist/__tests__/unit/risky-expression-scanner-sync.test.js.map +1 -1
- package/dist/__tests__/unit/seo-ai-schema.test.js.map +1 -1
- package/dist/__tests__/unit/snippet-scanner-sync.test.js.map +1 -1
- package/dist/__tests__/unit/spec-generation-notice.test.js.map +1 -1
- package/dist/__tests__/unit/templates-consistency.test.js.map +1 -1
- package/dist/__tests__/unit/validate-inert-config-keys.test.js.map +1 -1
- package/dist/__tests__/unit/validate-jwt-auth.test.d.ts +2 -0
- package/dist/__tests__/unit/validate-jwt-auth.test.d.ts.map +1 -0
- package/dist/__tests__/unit/validate-jwt-auth.test.js +77 -0
- package/dist/__tests__/unit/validate-jwt-auth.test.js.map +1 -0
- package/dist/__tests__/unit/validate-password-warning.test.js.map +1 -1
- package/dist/__tests__/unit/vendored-sync.test.js +11 -0
- package/dist/__tests__/unit/vendored-sync.test.js.map +1 -1
- package/dist/commands/broken-links.js.map +1 -1
- package/dist/commands/clean.js.map +1 -1
- package/dist/commands/deploy/cloudflare.js.map +1 -1
- package/dist/commands/deploy.js +16 -0
- package/dist/commands/deploy.js.map +1 -1
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/auth.js.map +1 -1
- package/dist/lib/deps.d.ts +4 -4
- package/dist/lib/deps.d.ts.map +1 -1
- package/dist/lib/deps.js +15 -10
- package/dist/lib/deps.js.map +1 -1
- package/dist/lib/docs-config.d.ts +1 -0
- package/dist/lib/docs-config.d.ts.map +1 -1
- package/dist/lib/docs-config.js +49 -0
- package/dist/lib/docs-config.js.map +1 -1
- package/dist/lib/errors.d.ts.map +1 -1
- package/dist/lib/errors.js.map +1 -1
- package/dist/lib/find-project-dir.js.map +1 -1
- package/dist/lib/first-run-banner.js.map +1 -1
- package/dist/lib/mdx-validator.js.map +1 -1
- package/dist/lib/missing-description-scan.js.map +1 -1
- package/dist/utils/update-checker.js.map +1 -1
- package/package.json +9 -8
- package/vendored/app/(jd-system)/jd-inactive/BrandedInactive.tsx +1 -7
- package/vendored/app/(jd-system)/jd-inactive/page.tsx +2 -4
- package/vendored/app/(unlock)/jd/auth/callback/CallbackClient.tsx +33 -0
- package/vendored/app/(unlock)/jd/auth/callback/page.tsx +15 -0
- package/vendored/app/(unlock)/jd/unlock/page.tsx +29 -7
- package/vendored/app/api/jd/auth/callback/route.ts +103 -0
- package/vendored/app/api/jd/auth/logout/route.ts +38 -0
- package/vendored/app/api/jd/auth/me/route.ts +52 -0
- package/vendored/app/api/jd/unlock/route.ts +26 -2
- package/vendored/app/api/markdown-export/[project]/[...slug]/route.ts +65 -22
- package/vendored/app/api/raw-content/[...slug]/route.ts +21 -1
- package/vendored/app/layout.tsx +7 -0
- package/vendored/components/AIActionsMenu.tsx +19 -2
- package/vendored/components/mdx/OpenApiEndpoint.tsx +4 -0
- package/vendored/components/navigation/Header.tsx +5 -0
- package/vendored/components/navigation/LogoutButton.tsx +41 -0
- package/vendored/lib/auth-resolver.ts +125 -50
- package/vendored/lib/cached-redis.ts +28 -3
- package/vendored/lib/docs-types.ts +17 -1
- package/vendored/lib/glob-match.ts +54 -0
- package/vendored/lib/jwt-auth-config.ts +37 -0
- package/vendored/lib/jwt-session.ts +98 -0
- package/vendored/lib/language-utils.ts +14 -2
- package/vendored/lib/layout-helpers.tsx +4 -0
- package/vendored/lib/mdx-inline-components.ts +1 -1
- package/vendored/lib/middleware-helpers.ts +27 -6
- package/vendored/lib/nav-group-filter.ts +81 -0
- package/vendored/lib/public-paths-resolver.ts +164 -7
- package/vendored/lib/r2-public-storage-guard.ts +26 -4
- package/vendored/lib/redis.ts +20 -2
- package/vendored/lib/render-doc-page.tsx +76 -34
- package/vendored/lib/root-page-slug.ts +110 -0
- package/vendored/lib/same-origin.ts +38 -0
- package/vendored/lib/sanitize-from.ts +31 -1
- package/vendored/lib/session-hint.ts +11 -0
- package/vendored/lib/snippet-compiler-isr.ts +1 -1
- package/vendored/lib/snippet-loader-isr.ts +1 -1
- package/vendored/lib/validate-config.ts +14 -0
- package/vendored/lib/viewer-navigation.ts +45 -0
- package/vendored/lib/visibility.ts +65 -0
- package/vendored/schema/docs-schema.json +25 -0
- package/vendored/shared/auth-cookie.ts +98 -5
- package/vendored/workspace-package-lock.json +474 -183
|
@@ -78,7 +78,7 @@ import {
|
|
|
78
78
|
parseFrontmatter,
|
|
79
79
|
projectExists,
|
|
80
80
|
} from '@/lib/content-loader';
|
|
81
|
-
import { getBaseUrl, getBaseUrlFromConfig
|
|
81
|
+
import { getBaseUrl, getBaseUrlFromConfig } from '@/lib/page-isr-helpers';
|
|
82
82
|
import { prefixRelativeHref } from '@/lib/link-prefix-helpers';
|
|
83
83
|
import {
|
|
84
84
|
parseOpenApiFrontmatter,
|
|
@@ -93,10 +93,10 @@ import {
|
|
|
93
93
|
} from '@/lib/openapi';
|
|
94
94
|
import { classifyOpenApiLoadError } from '@/lib/openapi/classify-load-error';
|
|
95
95
|
import { findOperation, deriveOperationDescription, collectLocalSpecPaths } from '@/lib/openapi/operation-description';
|
|
96
|
-
import { extractLanguageFromPath,
|
|
96
|
+
import { extractLanguageFromPath, resolveLocaleFromPath } from '@/lib/language-utils';
|
|
97
97
|
import { parseLocalePath } from '@/lib/locale-fallback';
|
|
98
98
|
import { DocsLocaleProvider } from '@/components/mdx/docs-locale-context';
|
|
99
|
-
import {
|
|
99
|
+
import { needsSlugRewrite, resolveMarkdownFallbackPath, resolveSlug } from '@/lib/root-page-slug';
|
|
100
100
|
import { candidateSpecPaths } from '@/lib/openapi/lang-spec-path';
|
|
101
101
|
import { squeezeToPageDescription } from '@/lib/openapi/squeeze-page-description';
|
|
102
102
|
import { ApiEndpoint } from '@/components/mdx/ApiEndpoint';
|
|
@@ -106,6 +106,7 @@ import { withApiSpecDownload } from '@/lib/api-spec-menu-gate';
|
|
|
106
106
|
import { SnippetComponents } from '@/components/snippets/ProjectSnippets';
|
|
107
107
|
import { EmailSubscribe } from '@/components/mdx/EmailSubscribe';
|
|
108
108
|
import { resolveAutoNewsletter, mdxHasEmailSubscribe } from '@/lib/email-subscribe-autoplacement';
|
|
109
|
+
import { filterNavigationForViewer } from '@/lib/viewer-navigation';
|
|
109
110
|
|
|
110
111
|
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'TRACE';
|
|
111
112
|
|
|
@@ -231,13 +232,6 @@ function resolveAuth(
|
|
|
231
232
|
return endpoint ? deriveAuthFromSecurity(endpoint.security) : {};
|
|
232
233
|
}
|
|
233
234
|
|
|
234
|
-
function findFirstPage(config: DocsConfig, lang?: string): string {
|
|
235
|
-
const nav = config.navigation;
|
|
236
|
-
const langBlock = lang ? nav.languages?.find((l) => l.language === lang) : undefined;
|
|
237
|
-
const result = (langBlock && findFirstNavPage(langBlock)) || findFirstNavPage(nav);
|
|
238
|
-
return result ? result.replace(/^\//, '') : 'introduction';
|
|
239
|
-
}
|
|
240
|
-
|
|
241
235
|
/**
|
|
242
236
|
* Every locale this project declared in docs.json — the whitelist the localized
|
|
243
237
|
* component chrome (`resolveLocaleFromPath`) gates on. Empty for a project with
|
|
@@ -296,18 +290,6 @@ function fallbackLocales(config: DocsConfig): string[] {
|
|
|
296
290
|
return languages.filter((l) => l.language !== defaultLang).map((l) => l.language);
|
|
297
291
|
}
|
|
298
292
|
|
|
299
|
-
function needsSlugRewrite(slug: string[]): boolean {
|
|
300
|
-
return slug.length === 0 || (slug.length === 1 && isValidLanguageCode(slug[0]));
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function resolveSlug(normalizedSlug: string[], config: DocsConfig): string[] {
|
|
304
|
-
if (normalizedSlug.length === 0) return pathToSlug(findFirstPage(config));
|
|
305
|
-
if (normalizedSlug.length === 1 && isValidLanguageCode(normalizedSlug[0])) {
|
|
306
|
-
return pathToSlug(findFirstPage(config, normalizedSlug[0]));
|
|
307
|
-
}
|
|
308
|
-
return normalizedSlug;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
293
|
/**
|
|
312
294
|
* Resolve a page `<meta>`/og description from its OpenAPI operation when the
|
|
313
295
|
* page has no frontmatter `description`. Pure: the spec loader is injected so
|
|
@@ -451,17 +433,38 @@ export async function buildDocMetadata(input: RenderInput): Promise<Metadata> {
|
|
|
451
433
|
// Bare `/` or `/<lang>` roots render the first nav page but keep the alias URL;
|
|
452
434
|
// suppress hreflang on them (passed to buildSeoMetadata — see its isRootAlias
|
|
453
435
|
// doc for why).
|
|
454
|
-
const
|
|
455
|
-
const slug =
|
|
436
|
+
const isAliasSlug = needsSlugRewrite(normalizedSlug);
|
|
437
|
+
const slug = isAliasSlug
|
|
456
438
|
? resolveSlug(normalizedSlug, await configP)
|
|
457
439
|
: normalizedSlug;
|
|
458
|
-
const
|
|
440
|
+
const literalPath = slug.join('/');
|
|
459
441
|
|
|
460
|
-
const [
|
|
461
|
-
loader.getContent(
|
|
442
|
+
const [literalContents, config] = await Promise.all([
|
|
443
|
+
loader.getContent(literalPath).catch(() => null),
|
|
462
444
|
configP,
|
|
463
445
|
]);
|
|
464
446
|
|
|
447
|
+
// `<prefix>/index` is the site root spelled out — the proxy collapses `/` and
|
|
448
|
+
// `/index.md` to the page path `index`, so the markdown export has always
|
|
449
|
+
// answered it, while HTML 404'd because `/docs` yields the empty slug and
|
|
450
|
+
// `/docs/index` yields `['index']`. That asymmetry let an agent cite a URL
|
|
451
|
+
// that 404s for a human. Resolve it the way the .md route does — LITERAL
|
|
452
|
+
// FIRST, fallback second — so a tenant with a real `index.mdx` still reaches
|
|
453
|
+
// it here, and only a tenant without one falls through to the first nav page.
|
|
454
|
+
// A miss on any other path stays a 404: resolveMarkdownFallbackPath returns
|
|
455
|
+
// null for everything except the root sentinel and a bare language code.
|
|
456
|
+
const rootFallbackPath = literalContents
|
|
457
|
+
? null
|
|
458
|
+
: resolveMarkdownFallbackPath(literalPath, config);
|
|
459
|
+
const rootFallbackContents = rootFallbackPath
|
|
460
|
+
? await loader.getContent(rootFallbackPath).catch(() => null)
|
|
461
|
+
: null;
|
|
462
|
+
|
|
463
|
+
const pageContents = literalContents ?? rootFallbackContents;
|
|
464
|
+
const pagePath = rootFallbackContents ? rootFallbackPath! : literalPath;
|
|
465
|
+
// A fallback render IS a root alias: same page, same hreflang/JSON-LD shape.
|
|
466
|
+
const isRootAlias = isAliasSlug || rootFallbackContents !== null;
|
|
467
|
+
|
|
465
468
|
// Untranslated locale page → serve the English page's metadata for it (see
|
|
466
469
|
// lib/locale-fallback). This block MUST stay in lockstep with the identical
|
|
467
470
|
// one in renderDocPage: they are two Next entry points over ONE URL, and a
|
|
@@ -575,24 +578,46 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
|
|
|
575
578
|
const highlighterP = getHighlighter();
|
|
576
579
|
|
|
577
580
|
const normalizedSlug = normalizeSlugForContent(slugInput || [], hostAtDocs);
|
|
578
|
-
const
|
|
579
|
-
const slug =
|
|
581
|
+
const isAliasSlug = needsSlugRewrite(normalizedSlug);
|
|
582
|
+
const slug = isAliasSlug
|
|
580
583
|
? resolveSlug(normalizedSlug, await configP)
|
|
581
584
|
: normalizedSlug;
|
|
582
|
-
const
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
loader.getContent(pagePath).catch(() => null),
|
|
585
|
+
const literalPath = slug.join('/');
|
|
586
|
+
const [literalContents, loadedConfig, highlighter] = await Promise.all([
|
|
587
|
+
loader.getContent(literalPath).catch(() => null),
|
|
586
588
|
configP,
|
|
587
589
|
highlighterP,
|
|
588
590
|
]);
|
|
589
591
|
|
|
592
|
+
// `<prefix>/index` is the site root spelled out — the proxy collapses `/` and
|
|
593
|
+
// `/index.md` to the page path `index`, so the markdown export has always
|
|
594
|
+
// answered it, while HTML 404'd because `/docs` yields the empty slug and
|
|
595
|
+
// `/docs/index` yields `['index']`. That asymmetry let an agent cite a URL
|
|
596
|
+
// that 404s for a human. Resolve it the way the .md route does — LITERAL
|
|
597
|
+
// FIRST, fallback second — so a tenant with a real `index.mdx` still reaches
|
|
598
|
+
// it here, and only a tenant without one falls through to the first nav page.
|
|
599
|
+
// A miss on any other path stays a 404: resolveMarkdownFallbackPath returns
|
|
600
|
+
// null for everything except the root sentinel and a bare language code.
|
|
601
|
+
// Mirror of the block in buildDocMetadata — the two entry points must reach
|
|
602
|
+
// the same verdict on the same URL, or metadata and body disagree.
|
|
603
|
+
const rootFallbackPath = literalContents
|
|
604
|
+
? null
|
|
605
|
+
: resolveMarkdownFallbackPath(literalPath, loadedConfig);
|
|
606
|
+
const rootFallbackContents = rootFallbackPath
|
|
607
|
+
? await loader.getContent(rootFallbackPath).catch(() => null)
|
|
608
|
+
: null;
|
|
609
|
+
|
|
610
|
+
const pageContents = literalContents ?? rootFallbackContents;
|
|
611
|
+
const pagePath = rootFallbackContents ? rootFallbackPath! : literalPath;
|
|
612
|
+
const isRootAlias = isAliasSlug || rootFallbackContents !== null;
|
|
613
|
+
const currentLang = extractLanguageFromPath(`/${pagePath}`);
|
|
614
|
+
|
|
590
615
|
// Untranslated locale page → serve the English body with a notice instead of
|
|
591
616
|
// 404ing. Mirror of the block in buildDocMetadata — see the comment there for
|
|
592
617
|
// why both entry points must reach the same verdict on the same URL.
|
|
593
618
|
const localeFallback = pageContents
|
|
594
619
|
? null
|
|
595
|
-
: parseLocalePath(pagePath, fallbackLocales(
|
|
620
|
+
: parseLocalePath(pagePath, fallbackLocales(loadedConfig));
|
|
596
621
|
const fileContents = pageContents ?? (localeFallback
|
|
597
622
|
? await loader.getContent(localeFallback.englishPath).catch(() => null)
|
|
598
623
|
: null);
|
|
@@ -609,6 +634,17 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
|
|
|
609
634
|
});
|
|
610
635
|
}
|
|
611
636
|
|
|
637
|
+
// Same viewer filter the root layout applies: this config feeds the
|
|
638
|
+
// breadcrumb and the prev/next pager, which would otherwise name a
|
|
639
|
+
// group-restricted neighbour the edge 404s for this viewer.
|
|
640
|
+
let config = loadedConfig;
|
|
641
|
+
if (isIsr && projectSlug && requestHeaders && config.auth?.jwt?.enabled) {
|
|
642
|
+
config = {
|
|
643
|
+
...config,
|
|
644
|
+
navigation: await filterNavigationForViewer(projectSlug, config.navigation, requestHeaders),
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
612
648
|
const parsed = parseFrontmatter(fileContents);
|
|
613
649
|
const data = parsed.data as FrontmatterData;
|
|
614
650
|
const rawContent = parsed.content;
|
|
@@ -1091,6 +1127,10 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
|
|
|
1091
1127
|
|
|
1092
1128
|
const resolvedMdxAuth = resolveAuth(mdxEndpointData, config);
|
|
1093
1129
|
const resolvedOpenApiAuth = resolveAuth(openApiEndpointData, config);
|
|
1130
|
+
// Drives the playground's post-mount /_jd/api/auth/me fetch that pre-fills
|
|
1131
|
+
// auth/params from the signed-in session — only worth the round-trip when
|
|
1132
|
+
// JWT auth is actually opted into for this project.
|
|
1133
|
+
const jwtAuthEnabled = config.auth?.jwt?.enabled === true;
|
|
1094
1134
|
|
|
1095
1135
|
// Pass the RAW MDX content — preprocessMdx() runs stripSnippetImports(),
|
|
1096
1136
|
// which would erase the `import X from '/snippets/...'` lines that the
|
|
@@ -1144,6 +1184,7 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
|
|
|
1144
1184
|
serverUrl={fallbackServerUrl}
|
|
1145
1185
|
proxyEnabled={proxyEnabled}
|
|
1146
1186
|
languages={config.api?.examples?.languages}
|
|
1187
|
+
jwtAuthEnabled={jwtAuthEnabled}
|
|
1147
1188
|
/>
|
|
1148
1189
|
) : (
|
|
1149
1190
|
<ApiEndpoint
|
|
@@ -1166,6 +1207,7 @@ export async function renderDocPage(input: RenderInput): Promise<ReactElement> {
|
|
|
1166
1207
|
proxyEnabled={proxyEnabled}
|
|
1167
1208
|
languages={config.api?.examples?.languages}
|
|
1168
1209
|
descriptionAlreadyShown={descriptionAlreadyShown}
|
|
1210
|
+
jwtAuthEnabled={jwtAuthEnabled}
|
|
1169
1211
|
/>
|
|
1170
1212
|
)}
|
|
1171
1213
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root-alias resolution shared by the HTML renderer and the markdown export.
|
|
3
|
+
*
|
|
4
|
+
* `/` and `/<lang>` render the first navigation page IN PLACE — no redirect,
|
|
5
|
+
* the alias URL stays in the address bar. render-doc-page.tsx has done that
|
|
6
|
+
* for HTML since the alias URLs were introduced. The markdown export never
|
|
7
|
+
* did: the proxy collapses `/`, `/index.md`, `/docs` and `/docs/index.md` to
|
|
8
|
+
* the page path `index` (and `/fr` to `fr`), and both markdown routes fetched
|
|
9
|
+
* that path verbatim, so every tenant whose root page is not literally
|
|
10
|
+
* `index.mdx` 404'd its own home page to agents (found 2026-09-10 through
|
|
11
|
+
* afdocs' markdown-url-support probes). Both surfaces now resolve through
|
|
12
|
+
* this one module so they cannot drift again.
|
|
13
|
+
*
|
|
14
|
+
* isValidLanguageCode is `Object.hasOwn`, not `in` (tightened 2026-09-10), so
|
|
15
|
+
* `constructor` / `toString` / `hasOwnProperty` are ordinary page names on both
|
|
16
|
+
* surfaces: they fall through needsSlugRewrite to the literal path instead of
|
|
17
|
+
* resolving to the first nav page. Parity with HTML is the contract here — the
|
|
18
|
+
* helper is shared, so the two surfaces move together.
|
|
19
|
+
*/
|
|
20
|
+
import type { DocsConfig } from '@/lib/docs-types';
|
|
21
|
+
import { findFirstNavPage } from '@/lib/find-first-nav-page';
|
|
22
|
+
import { isValidLanguageCode } from '@/lib/language-utils';
|
|
23
|
+
import { pathToSlug } from '@/lib/page-isr-helpers';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The page path proxy.ts rewrites the site root to (`/`, `/index.md`, and the
|
|
27
|
+
* hostAtDocs `/docs` forms). Reference this, never the literal, in the routes.
|
|
28
|
+
*/
|
|
29
|
+
export const ROOT_MARKDOWN_SENTINEL = 'index';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* First leaf page of the navigation, without its leading slash. With `lang`,
|
|
33
|
+
* that language block's first page wins; an undeclared or empty block falls
|
|
34
|
+
* through to the whole nav. `'introduction'` is the historical default for a
|
|
35
|
+
* nav with no leaf page at all.
|
|
36
|
+
*/
|
|
37
|
+
export function findFirstPage(config: DocsConfig, lang?: string): string {
|
|
38
|
+
const nav = config.navigation;
|
|
39
|
+
const langBlock = lang ? nav.languages?.find((l) => l.language === lang) : undefined;
|
|
40
|
+
const result = (langBlock && findFirstNavPage(langBlock)) || findFirstNavPage(nav);
|
|
41
|
+
return result ? result.replace(/^\//, '') : 'introduction';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* `/` or a bare `/<lang>` — the two URL shapes that render a page they do not name.
|
|
46
|
+
*
|
|
47
|
+
* DELIBERATE, do not "tighten" this to the tenant's declared languages: the test
|
|
48
|
+
* is against the GLOBAL language list, so all ~34 codes alias the first nav page
|
|
49
|
+
* on every tenant, including ones that declare no languages at all
|
|
50
|
+
* (`acme.jamdesk.app/ru` and `/ru.md` both serve the English home page). That is
|
|
51
|
+
* `findFirstPage`'s documented contract — an undeclared language block falls
|
|
52
|
+
* through to the whole nav — and HTML has behaved this way since alias URLs were
|
|
53
|
+
* introduced; the markdown export was brought into line with it on purpose, so
|
|
54
|
+
* the two surfaces agree.
|
|
55
|
+
*
|
|
56
|
+
* Gating on `config.navigation.languages` would 404 a bookmarked `/en` on every
|
|
57
|
+
* monolingual tenant, and would have to change all three consumers at once
|
|
58
|
+
* (render-doc-page's two entry points and resolveMarkdownFallbackPath, which
|
|
59
|
+
* feeds both markdown routes). The cost of leaving it is agent-facing only —
|
|
60
|
+
* ~34 phantom URLs per tenant that all return the same page — and `.md` carries
|
|
61
|
+
* `noindex, nofollow`, so there is no search exposure. Revisit only with an
|
|
62
|
+
* owner decision, not as a cleanup.
|
|
63
|
+
*/
|
|
64
|
+
export function needsSlugRewrite(slug: string[]): boolean {
|
|
65
|
+
return slug.length === 0 || (slug.length === 1 && isValidLanguageCode(slug[0]));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* HTML-side resolution: the alias slug → the first page's slug segments;
|
|
70
|
+
* any other slug passes through untouched. `normalizedSlug[0]` is the language
|
|
71
|
+
* for a bare `/<lang>` and `undefined` for the empty root slug — which is
|
|
72
|
+
* exactly `findFirstPage`'s optional-lang contract, so the two alias shapes
|
|
73
|
+
* share one line and only `needsSlugRewrite` defines what an alias IS.
|
|
74
|
+
*/
|
|
75
|
+
export function resolveSlug(normalizedSlug: string[], config: DocsConfig): string[] {
|
|
76
|
+
if (!needsSlugRewrite(normalizedSlug)) return normalizedSlug;
|
|
77
|
+
return pathToSlug(findFirstPage(config, normalizedSlug[0]));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Markdown-side resolution for a page path that was NOT found in the content
|
|
82
|
+
* store. Only the two root aliases get a fallback — the `index` sentinel and a
|
|
83
|
+
* bare language code — and only when the first nav page is a different path.
|
|
84
|
+
* So a missing real page still 404s, and a project whose first page IS `index`
|
|
85
|
+
* never fetches the same key twice. Callers try the literal path first: a
|
|
86
|
+
* project with a real `index.mdx` keeps serving it, which is the one deliberate
|
|
87
|
+
* difference from the HTML root (which never consults `index.mdx`).
|
|
88
|
+
*
|
|
89
|
+
* @returns the page path to fetch instead, or null when there is no fallback
|
|
90
|
+
*/
|
|
91
|
+
export function resolveMarkdownFallbackPath(
|
|
92
|
+
requestedPath: string,
|
|
93
|
+
config: DocsConfig | null | undefined,
|
|
94
|
+
): string | null {
|
|
95
|
+
if (!config?.navigation) return null;
|
|
96
|
+
|
|
97
|
+
// Re-express the page path as the slug the HTML path would have seen, so the
|
|
98
|
+
// alias test and the resolution are the SAME code on both surfaces: `index`
|
|
99
|
+
// is the proxy's stand-in for the empty root slug, and a bare language code
|
|
100
|
+
// is already a single segment. Anything else — including a nested page and
|
|
101
|
+
// the empty string — fails needsSlugRewrite, so a real miss stays a 404.
|
|
102
|
+
const aliasSlug = requestedPath === ROOT_MARKDOWN_SENTINEL ? [] : [requestedPath];
|
|
103
|
+
if (!needsSlugRewrite(aliasSlug)) return null;
|
|
104
|
+
|
|
105
|
+
// resolveSlug brings pathToSlug's normalisation with it: a nav entry authored
|
|
106
|
+
// as "/introduction" or "introduction/" must become the clean R2 key, and the
|
|
107
|
+
// equality guard must compare clean to clean.
|
|
108
|
+
const normalized = resolveSlug(aliasSlug, config).join('/');
|
|
109
|
+
return normalized === requestedPath ? null : normalized;
|
|
110
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Same-origin guard for cookie-minting POST routes (JWT callback, password
|
|
3
|
+
* unlock). Both routes set a session cookie from a POST body, and a browser
|
|
4
|
+
* will happily send that POST cross-site — a page on evil.example.com can
|
|
5
|
+
* submit a text/plain form or fetch() to <docs-host>/api/jd/auth/callback,
|
|
6
|
+
* and the response's Set-Cookie lands in the victim's jar (login-CSRF). With
|
|
7
|
+
* the JWT route that means an attacker-chosen session, including groups and
|
|
8
|
+
* API-playground inputs the playground then auto-fills.
|
|
9
|
+
*
|
|
10
|
+
* Decision, in order:
|
|
11
|
+
* 1. `Sec-Fetch-Site` when present is authoritative (browsers set it and
|
|
12
|
+
* pages cannot forge it): `same-origin` and `none` (user-typed
|
|
13
|
+
* navigation) pass; `cross-site` and `same-site` fail — a sibling
|
|
14
|
+
* *.jamdesk.app subdomain is another tenant, so same-site is NOT ok.
|
|
15
|
+
* 2. Otherwise `Origin` when present must match the request host —
|
|
16
|
+
* the customer-forwarded public host first (x-jamdesk-forwarded-host),
|
|
17
|
+
* then Host — case-insensitively. `Origin: null` (sandboxed/opaque
|
|
18
|
+
* contexts) fails.
|
|
19
|
+
* 3. Neither header → allow. Non-browser clients (curl, the smoke harness)
|
|
20
|
+
* send neither, and every browser that can mount the attack sends at
|
|
21
|
+
* least one of them on a cross-site POST.
|
|
22
|
+
*/
|
|
23
|
+
export function isSameOriginRequest(req: Request): boolean {
|
|
24
|
+
const fetchSite = req.headers.get('sec-fetch-site');
|
|
25
|
+
if (fetchSite) {
|
|
26
|
+
return fetchSite === 'same-origin' || fetchSite === 'none';
|
|
27
|
+
}
|
|
28
|
+
const origin = req.headers.get('origin');
|
|
29
|
+
if (!origin) return true;
|
|
30
|
+
let originHost: string;
|
|
31
|
+
try {
|
|
32
|
+
originHost = new URL(origin).host;
|
|
33
|
+
} catch {
|
|
34
|
+
return false; // includes the literal "null"
|
|
35
|
+
}
|
|
36
|
+
const requestHost = req.headers.get('x-jamdesk-forwarded-host') || req.headers.get('host') || '';
|
|
37
|
+
return originHost.toLowerCase() === requestHost.toLowerCase();
|
|
38
|
+
}
|
|
@@ -29,8 +29,38 @@ export function sanitizeFrom(raw: string | null | undefined): string {
|
|
|
29
29
|
// form to catch percent-encoded slashes like /%2F%2Fevil.com → //evil.com.
|
|
30
30
|
const normalized = parsed.pathname + parsed.search + parsed.hash;
|
|
31
31
|
if (parsed.pathname.startsWith('//')) return '/';
|
|
32
|
-
|
|
32
|
+
// The URL parser passes malformed percent-sequences through verbatim
|
|
33
|
+
// ('/%', '/a%2'), where decodeURIComponent throws — if we can't decode we
|
|
34
|
+
// can't rule out an encoded //, so reject rather than 500.
|
|
35
|
+
let decodedPathname: string;
|
|
36
|
+
try {
|
|
37
|
+
decodedPathname = decodeURIComponent(parsed.pathname);
|
|
38
|
+
} catch {
|
|
39
|
+
return '/';
|
|
40
|
+
}
|
|
33
41
|
if (decodedPathname.startsWith('//')) return '/';
|
|
34
42
|
|
|
35
43
|
return normalized;
|
|
36
44
|
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Redirect sanitizer for the JWT auth callback. sanitizeFrom does the heavy
|
|
48
|
+
* lifting (same-site path only; rejects CRLF, backslashes, protocol-relative
|
|
49
|
+
* and percent-encoded // forms); this only adds the auth self-loop guards:
|
|
50
|
+
* the callback page itself, and the auth API routes — /api/jd/auth/logout is
|
|
51
|
+
* a GET that clears the cookie, so a crafted `redirect` would log the freshly
|
|
52
|
+
* minted session straight back out. Both the legacy paths and the /_jd
|
|
53
|
+
* aliases are served, so both prefixes are refused.
|
|
54
|
+
*/
|
|
55
|
+
const CALLBACK_REDIRECT_DENY_PREFIXES = ['/jd/auth/', '/_jd/auth/', '/api/jd/auth/', '/_jd/api/auth/'];
|
|
56
|
+
|
|
57
|
+
export function sanitizeCallbackRedirect(raw: unknown): string {
|
|
58
|
+
const path = sanitizeFrom(typeof raw === 'string' ? raw : null);
|
|
59
|
+
// Match on the DECODED path: sanitizeFrom keeps percent-encoding, and the
|
|
60
|
+
// router decodes before matching, so `/%61pi/jd/auth/logout` would reach
|
|
61
|
+
// logout while slipping past a literal prefix check.
|
|
62
|
+
let decoded = path;
|
|
63
|
+
try { decoded = decodeURIComponent(path); } catch { return '/'; }
|
|
64
|
+
if (CALLBACK_REDIRECT_DENY_PREFIXES.some((prefix) => decoded.startsWith(prefix))) return '/';
|
|
65
|
+
return path;
|
|
66
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `jd_authed=1` hint cookie is set by the JWT callback and cleared by
|
|
3
|
+
* logout. It is client-readable, carries nothing, and has no security value —
|
|
4
|
+
* the signed HttpOnly session cookie behind /_jd/api/auth/me is what's
|
|
5
|
+
* trusted. Its only job is to let client components skip the /me round-trip
|
|
6
|
+
* on anonymous views (every SEO landing and shared link would otherwise pay
|
|
7
|
+
* it). Shared by the header logout affordance and the API playground pre-fill.
|
|
8
|
+
*/
|
|
9
|
+
export function hasJwtSessionHint(cookieString: string): boolean {
|
|
10
|
+
return cookieString.split('; ').includes('jd_authed=1');
|
|
11
|
+
}
|
|
@@ -21,6 +21,7 @@ import JSON5 from 'json5';
|
|
|
21
21
|
import fs from 'fs';
|
|
22
22
|
import path from 'path';
|
|
23
23
|
import { BUILD_SERVICE_DIR } from './paths.js';
|
|
24
|
+
import { validateJwtAuth } from './jwt-auth-config.js';
|
|
24
25
|
|
|
25
26
|
// Types
|
|
26
27
|
export interface DocsConfig {
|
|
@@ -592,6 +593,19 @@ export async function validateConfig(
|
|
|
592
593
|
};
|
|
593
594
|
}
|
|
594
595
|
|
|
596
|
+
// validate-config.ts's local DocsConfig (theme optional, pre-schema-validation)
|
|
597
|
+
// is structurally close to but not assignable to docs-types.ts's DocsConfig
|
|
598
|
+
// (theme required) — validateJwtAuth only reads `config.auth`, which both
|
|
599
|
+
// shapes carry via their `[key: string]: unknown` index signatures.
|
|
600
|
+
const jwtErrors = validateJwtAuth(config as unknown as Parameters<typeof validateJwtAuth>[0]);
|
|
601
|
+
if (jwtErrors.length > 0) {
|
|
602
|
+
return {
|
|
603
|
+
valid: false,
|
|
604
|
+
error: `Invalid docs.json: ${jwtErrors[0]}`,
|
|
605
|
+
errorType: 'config_error',
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
|
|
595
609
|
// Try schema validation if available (non-blocking if schema not available)
|
|
596
610
|
try {
|
|
597
611
|
const schema = await getSchema();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { DocsConfig } from './docs-types';
|
|
2
|
+
import { resolveAuth } from './auth-resolver';
|
|
3
|
+
import { verifyAuthCookie } from '../shared/auth-cookie';
|
|
4
|
+
import { filterNavigationForGroups } from './nav-group-filter';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Drop group-restricted pages the viewer can't access from the navigation
|
|
8
|
+
* before it reaches the client. Server-only (reads Redis + the session
|
|
9
|
+
* cookie). Two consumers must agree: the root layout (sidebar/tabs/header
|
|
10
|
+
* resolve navigation from its config in the browser) and the page render
|
|
11
|
+
* (breadcrumb + prev/next pager). Without this every public page's RSC
|
|
12
|
+
* payload lists the titles and hrefs of pages the edge answers with a 404
|
|
13
|
+
* to out-of-group users — defeating the point of the 404.
|
|
14
|
+
*
|
|
15
|
+
* Group membership comes from the SERVER-VERIFIED v2 session cookie (same
|
|
16
|
+
* check as /_jd/api/auth/me), never the client-readable jd_authed hint.
|
|
17
|
+
* Cookie verification fails closed to "no groups" (over-hides). A resolver
|
|
18
|
+
* failure is different: the group map itself is unavailable, so the
|
|
19
|
+
* navigation is returned unfiltered rather than taking the page down — the
|
|
20
|
+
* edge gate 503s the request on the same outage, so this only renders when
|
|
21
|
+
* the edge and Node runtimes' 15s caches disagree.
|
|
22
|
+
*/
|
|
23
|
+
export async function filterNavigationForViewer(
|
|
24
|
+
slug: string,
|
|
25
|
+
navigation: DocsConfig['navigation'],
|
|
26
|
+
headersList: Headers,
|
|
27
|
+
): Promise<DocsConfig['navigation']> {
|
|
28
|
+
let auth: Awaited<ReturnType<typeof resolveAuth>> = null;
|
|
29
|
+
try { auth = await resolveAuth(slug); } catch { /* group map unavailable: unfiltered */ }
|
|
30
|
+
if (!auth || auth.authType !== 'jwt' || !auth.groupsPaths) return navigation;
|
|
31
|
+
if (Object.keys(auth.groupsPaths).length === 0) return navigation;
|
|
32
|
+
|
|
33
|
+
let userGroups: string[] = [];
|
|
34
|
+
const secret = process.env.EDGE_AUTH_SECRET;
|
|
35
|
+
const cookieHeader = headersList.get('cookie') || '';
|
|
36
|
+
const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)jd_auth_${slug}=([^;]+)`));
|
|
37
|
+
if (secret && match) {
|
|
38
|
+
const host = headersList.get('x-jamdesk-forwarded-host') || headersList.get('host') || '';
|
|
39
|
+
try {
|
|
40
|
+
const v = await verifyAuthCookie(match[1], { slug, host, version: auth.version }, secret);
|
|
41
|
+
if (v.valid && v.formatVersion === 'v2') userGroups = v.extras?.g ?? [];
|
|
42
|
+
} catch { /* fail closed: no groups */ }
|
|
43
|
+
}
|
|
44
|
+
return filterNavigationForGroups(navigation, auth.groupsPaths, userGroups);
|
|
45
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DocsConfig, NavigationConfig } from './docs-types.js';
|
|
2
2
|
import { RECURSE_KEYS } from './enhance-navigation.js';
|
|
3
|
+
import { MAX_GROUP_NAME_LEN } from './public-paths-resolver.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Result of walking the nav tree for hidden-state classification.
|
|
@@ -120,12 +121,24 @@ export interface VisibilityInputs {
|
|
|
120
121
|
navPaths: Set<string>;
|
|
121
122
|
hiddenNavPaths: Set<string>;
|
|
122
123
|
searchableNavPaths: Set<string>;
|
|
124
|
+
/**
|
|
125
|
+
* Slug (no leading slash, extension stripped — same convention as `path` and
|
|
126
|
+
* `navPaths`/`hiddenNavPaths`) → required groups, from
|
|
127
|
+
* lib/public-paths-resolver.ts's `collectGroupsPaths`. This map is what
|
|
128
|
+
* carries locale inheritance: a translated page (e.g. `fr/admin`) whose own
|
|
129
|
+
* frontmatter lacks `groups` still shows up here because collectGroupsPaths
|
|
130
|
+
* propagates the base page's restriction onto it. Optional/omitted (or
|
|
131
|
+
* empty, as in non-jwt auth modes) preserves the own-frontmatter-only
|
|
132
|
+
* fallback in `isGroupRestricted` below.
|
|
133
|
+
*/
|
|
134
|
+
groupsPaths?: Record<string, string[]>;
|
|
123
135
|
}
|
|
124
136
|
|
|
125
137
|
export interface PageFrontmatterForVisibility {
|
|
126
138
|
hidden?: boolean;
|
|
127
139
|
noindex?: boolean;
|
|
128
140
|
seo?: { noindex?: boolean };
|
|
141
|
+
groups?: unknown;
|
|
129
142
|
}
|
|
130
143
|
|
|
131
144
|
export interface PageVisibility {
|
|
@@ -135,17 +148,60 @@ export interface PageVisibility {
|
|
|
135
148
|
| 'orphan'
|
|
136
149
|
| 'frontmatter-hidden'
|
|
137
150
|
| 'frontmatter-noindex'
|
|
151
|
+
| 'frontmatter-groups'
|
|
138
152
|
| 'nav-hidden'
|
|
139
153
|
| 'override-indexing-all'
|
|
140
154
|
| 'override-indexHiddenPages';
|
|
141
155
|
}
|
|
142
156
|
|
|
157
|
+
/**
|
|
158
|
+
* True when frontmatter carries a real group restriction: an array with at
|
|
159
|
+
* least one VALID entry. Validity mirrors collectGroupsPaths' per-entry
|
|
160
|
+
* filter exactly (typeof === 'string', non-empty, <= MAX_GROUP_NAME_LEN) —
|
|
161
|
+
* so `groups: [123]` or `groups: ['x'.repeat(65)]` is judged unrestricted
|
|
162
|
+
* here too. Without this parity, a page whose only entries are junk would be
|
|
163
|
+
* fail-closed (hidden from search/sitemap/RAG) here while the runtime edge
|
|
164
|
+
* gate (which uses the same filter) fail-opens it as public — a page that's
|
|
165
|
+
* reachable but invisible, not the access-control divergence this guards
|
|
166
|
+
* against being read the other way around.
|
|
167
|
+
*/
|
|
168
|
+
export function hasGroupRestriction(frontmatter: unknown): boolean {
|
|
169
|
+
const groups = (frontmatter as { groups?: unknown } | undefined)?.groups;
|
|
170
|
+
if (!Array.isArray(groups)) return false;
|
|
171
|
+
return groups.some(
|
|
172
|
+
(g): g is string => typeof g === 'string' && g.length > 0 && g.length <= MAX_GROUP_NAME_LEN,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* True when a page is group-restricted, considering BOTH its own frontmatter
|
|
178
|
+
* and locale inheritance. `groupsPaths` (from collectGroupsPaths) carries
|
|
179
|
+
* restrictions forward onto translated variants that lack their own `groups`
|
|
180
|
+
* field — a translated page's frontmatter routinely drops fields the base
|
|
181
|
+
* page had, so checking `frontmatter` alone would silently reopen every
|
|
182
|
+
* localized variant of a restricted page. `path` must already be normalized
|
|
183
|
+
* to the same leading-slash-free convention as `groupsPaths`' keys and the
|
|
184
|
+
* rest of VisibilityInputs (navPaths etc.) — callers own that normalization.
|
|
185
|
+
*/
|
|
186
|
+
export function isGroupRestricted(
|
|
187
|
+
path: string,
|
|
188
|
+
frontmatter: unknown,
|
|
189
|
+
groupsPaths?: Record<string, string[]>,
|
|
190
|
+
): boolean {
|
|
191
|
+
if (hasGroupRestriction(frontmatter)) return true;
|
|
192
|
+
const inherited = groupsPaths?.[path];
|
|
193
|
+
return Array.isArray(inherited) && inherited.length > 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
143
196
|
/**
|
|
144
197
|
* The single function that decides whether a page is in the sitemap / llms.txt /
|
|
145
198
|
* llms-full.txt / search index. Same logic for all four artifacts.
|
|
146
199
|
*
|
|
147
200
|
* Excluded when ANY of:
|
|
148
201
|
* - frontmatter.noindex or frontmatter.seo.noindex
|
|
202
|
+
* - the page is group-restricted per isGroupRestricted (own frontmatter.groups
|
|
203
|
+
* OR inherited via inputs.groupsPaths — see that function) (access control —
|
|
204
|
+
* NEVER overrideable, not even by indexHiddenPages or indexing:'all')
|
|
149
205
|
* - frontmatter.hidden (UNLESS indexHiddenPages or indexing:'all')
|
|
150
206
|
* - path not in navigation (orphan) AND indexing !== 'all'
|
|
151
207
|
* - path in hiddenNavPaths AND NOT in searchableNavPaths (UNLESS indexHiddenPages or indexing:'all')
|
|
@@ -168,6 +224,15 @@ export function computePageVisibility(
|
|
|
168
224
|
return { inArtifacts: false, reason: 'frontmatter-noindex' };
|
|
169
225
|
}
|
|
170
226
|
|
|
227
|
+
// Group-restricted pages are access-controlled content: never list them in
|
|
228
|
+
// sitemap/llms/search artifacts, regardless of seo indexing overrides —
|
|
229
|
+
// those overrides express SEO preference, not permission to leak titles.
|
|
230
|
+
// isGroupRestricted also catches locale-inherited restriction (a translated
|
|
231
|
+
// page whose own frontmatter dropped `groups`) via inputs.groupsPaths.
|
|
232
|
+
if (isGroupRestricted(path, frontmatter, inputs.groupsPaths)) {
|
|
233
|
+
return { inArtifacts: false, reason: 'frontmatter-groups' };
|
|
234
|
+
}
|
|
235
|
+
|
|
171
236
|
// frontmatter.hidden wins over everything except noindex (checked above).
|
|
172
237
|
// Mintlify spec rule #7: page-level hidden always wins over parent searchable.
|
|
173
238
|
if (frontmatter.hidden === true && !override) {
|
|
@@ -1694,6 +1694,31 @@
|
|
|
1694
1694
|
"uniqueItems": true
|
|
1695
1695
|
}
|
|
1696
1696
|
}
|
|
1697
|
+
},
|
|
1698
|
+
"jwt": {
|
|
1699
|
+
"type": "object",
|
|
1700
|
+
"description": "JWT authentication. Generate the signing key in the Jamdesk dashboard; this block controls opt-in, your login URL, and public path exceptions.",
|
|
1701
|
+
"additionalProperties": false,
|
|
1702
|
+
"properties": {
|
|
1703
|
+
"enabled": {
|
|
1704
|
+
"type": "boolean",
|
|
1705
|
+
"description": "Require JWT authentication for the whole site."
|
|
1706
|
+
},
|
|
1707
|
+
"loginUrl": {
|
|
1708
|
+
"type": "string",
|
|
1709
|
+
"format": "uri",
|
|
1710
|
+
"description": "Absolute https:// URL of your login flow. Visitors are redirected here with ?redirect=<path>."
|
|
1711
|
+
},
|
|
1712
|
+
"public": {
|
|
1713
|
+
"type": "array",
|
|
1714
|
+
"maxItems": 100,
|
|
1715
|
+
"items": {
|
|
1716
|
+
"type": "string",
|
|
1717
|
+
"pattern": "^/[A-Za-z0-9._*/-]+$"
|
|
1718
|
+
},
|
|
1719
|
+
"description": "Paths or globs that bypass authentication. Supports '*' and '**'."
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1697
1722
|
}
|
|
1698
1723
|
}
|
|
1699
1724
|
},
|