jamdesk 1.1.21 → 1.1.23

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.
@@ -741,6 +741,31 @@ export interface SpellcheckConfig {
741
741
  ignore?: string[];
742
742
  }
743
743
 
744
+ /**
745
+ * Shared-password site protection config.
746
+ *
747
+ * Requires the password to be set in the Jamdesk dashboard; this block only
748
+ * controls declarative opt-in and public path exceptions.
749
+ */
750
+ export interface PasswordAuthConfig {
751
+ /** Opt in to password protection. Site is only gated when this is true AND a password has been set in the dashboard. */
752
+ enabled?: boolean;
753
+ /** Optional hint shown on the unlock page (e.g., "Ask your account manager"). Plain text, no HTML. */
754
+ hint?: string;
755
+ /** Paths or globs that bypass the password check. Supports '*' (one path segment) and '**' (recursive). */
756
+ public?: string[];
757
+ /** Exact paths that require authentication even when the whole site is not password-protected (specific-pages mode). */
758
+ private?: string[];
759
+ }
760
+
761
+ /**
762
+ * Auth configuration for access control. Currently supports shared-password
763
+ * protection; additional mechanisms may be added in future.
764
+ */
765
+ export interface AuthConfig {
766
+ password?: PasswordAuthConfig;
767
+ }
768
+
744
769
  /**
745
770
  * Image optimization configuration
746
771
  */
@@ -815,6 +840,7 @@ export interface DocsConfig {
815
840
  analytics?: AnalyticsConfig;
816
841
  chat?: ChatConfig;
817
842
  spellcheck?: SpellcheckConfig;
843
+ auth?: AuthConfig;
818
844
  images?: ImagesConfig;
819
845
 
820
846
  // Mintlify compatibility fields (normalized at load time)
@@ -3,6 +3,11 @@ import type { DocsConfig } from './docs-types.js';
3
3
  /**
4
4
  * Highlights extracted from docs.json for dashboard display.
5
5
  * IMPORTANT: Keep in sync with DocsConfigHighlights in dashboard/hosting/app/lib/domain.ts
6
+ *
7
+ * `passwordProtection` is not in this interface: it can't be computed from
8
+ * docs.json alone (specific-pages mode is signalled by frontmatter), so
9
+ * build.ts writes the authoritative value after the auth-mode detector
10
+ * has seen both planes.
6
11
  */
7
12
  export interface ExtractedHighlights {
8
13
  siteName: string;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Path matcher for password-protection public-path lists.
3
+ *
4
+ * Splits each input list into a Set of literal paths plus an array of
5
+ * compiled regexes. The Set lookup short-circuits the regex loop for
6
+ * literal slugs, which dominate the inverted publicPaths list in
7
+ * specific-pages mode (where it can hold hundreds of literal slugs from
8
+ * the inverted page list).
9
+ *
10
+ * Compiled paths are cached on a WeakMap keyed by array reference, so
11
+ * call sites that reuse the same array (auth-resolver between requests
12
+ * on a warm edge worker, public-paths-resolver within a single build)
13
+ * amortize the regex-compile cost.
14
+ */
15
+
16
+ const GLOB_CHARS_RE = /[*?[\]]/;
17
+
18
+ function globToRegex(glob: string): RegExp {
19
+ // Escape regex metacharacters except * which is handled below.
20
+ const escaped = glob.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
21
+ // ** must be processed before * to avoid double-replacement.
22
+ const withRecursive = escaped.replace(/\*\*/g, '___DOUBLE_STAR___');
23
+ // Single * matches one path segment (no slashes).
24
+ const withSingle = withRecursive.replace(/\*/g, '[^/]*');
25
+ // ** matches any character including slashes.
26
+ const finalPattern = withSingle.replace(/___DOUBLE_STAR___/g, '.*');
27
+ return new RegExp(`^${finalPattern}$`);
28
+ }
29
+
30
+ interface CompiledPaths {
31
+ literals: Set<string>;
32
+ regexes: RegExp[];
33
+ }
34
+
35
+ const compiledCache = new WeakMap<string[], CompiledPaths>();
36
+
37
+ function compilePublicPaths(publicPaths: string[]): CompiledPaths {
38
+ const cached = compiledCache.get(publicPaths);
39
+ if (cached) return cached;
40
+
41
+ const literals = new Set<string>();
42
+ const regexes: RegExp[] = [];
43
+ for (const path of publicPaths) {
44
+ if (GLOB_CHARS_RE.test(path)) {
45
+ regexes.push(globToRegex(path));
46
+ } else {
47
+ literals.add(path);
48
+ }
49
+ }
50
+
51
+ const compiled: CompiledPaths = { literals, regexes };
52
+ compiledCache.set(publicPaths, compiled);
53
+ return compiled;
54
+ }
55
+
56
+ /**
57
+ * Check whether a pathname matches any of the public-path globs.
58
+ *
59
+ * For hostAtDocs projects, strips the `/docs` prefix before matching since
60
+ * authors write public paths relative to the site root. The strip only
61
+ * applies when `/docs` is an exact path or is followed by `/` — a look-alike
62
+ * prefix like `/docsfoo` must NOT be rewritten to `/foo`.
63
+ */
64
+ export function matchPublicPath(
65
+ pathname: string,
66
+ publicPaths: string[],
67
+ hostAtDocs: boolean,
68
+ ): boolean {
69
+ if (publicPaths.length === 0) return false;
70
+
71
+ let normalized = pathname;
72
+ if (hostAtDocs) {
73
+ if (pathname === '/docs') {
74
+ normalized = '/';
75
+ } else if (pathname.startsWith('/docs/')) {
76
+ normalized = pathname.slice(5); // '/docs/' → '/' (keep leading slash)
77
+ }
78
+ }
79
+
80
+ const { literals, regexes } = compilePublicPaths(publicPaths);
81
+ if (literals.has(normalized)) return true;
82
+ return regexes.some((re) => re.test(normalized));
83
+ }
@@ -19,6 +19,13 @@ import { getRedirects, matchRedirect, mergeQueryStrings, isInvalidDestination }
19
19
  import { ASSET_PREFIX } from './docs-types';
20
20
  import type { NextRequest } from 'next/server';
21
21
 
22
+ /**
23
+ * Project slug character set — lowercase alphanumeric + hyphens, must
24
+ * start with an alphanumeric. Used as a defense-in-depth guard against
25
+ * URL-encoded path traversal or Redis-key injection via crafted slugs.
26
+ */
27
+ export const VALID_SLUG_RE = /^[a-z0-9][a-z0-9-]*$/;
28
+
22
29
  /**
23
30
  * Result of project resolution.
24
31
  */
@@ -81,6 +88,25 @@ export async function handleProjectResolution(
81
88
  return { projectSlug: null, hostAtDocs: true, skip: true };
82
89
  }
83
90
 
91
+ // Dev-only fallback for localhost: `dev-project.cjs` sets PROJECT_NAME
92
+ // to the slug being served. With ISR_MODE=true the middleware normally
93
+ // resolves the slug from the hostname, but localhost never matches a
94
+ // *.jamdesk.app pattern. Allow a hostname override so developers can
95
+ // exercise the real auth gate locally. Hard-gated by NODE_ENV.
96
+ if (
97
+ process.env.NODE_ENV !== 'production' &&
98
+ process.env.PROJECT_NAME &&
99
+ (hostname.startsWith('localhost') || hostname.startsWith('127.0.0.1'))
100
+ ) {
101
+ const slug = process.env.PROJECT_NAME;
102
+ log('info', 'Dev localhost fallback: resolving project from PROJECT_NAME', {
103
+ hostname,
104
+ slug,
105
+ });
106
+ const config = await getProjectConfig(slug);
107
+ return { projectSlug: slug, hostAtDocs: config.hostAtDocs };
108
+ }
109
+
84
110
  log('info', 'Resolving project from hostname', { hostname });
85
111
 
86
112
  // Check if it's a Jamdesk subdomain first (fast path)
@@ -0,0 +1,227 @@
1
+ import type { DocsConfig } from './docs-types.js';
2
+ import { matchPublicPath } from './glob-match.js';
3
+
4
+ interface PageWithFrontmatter {
5
+ slug: string;
6
+ frontmatter: Record<string, unknown>;
7
+ }
8
+
9
+ interface Input {
10
+ docsConfig: DocsConfig;
11
+ pagesWithFrontmatter: PageWithFrontmatter[];
12
+ }
13
+
14
+ /**
15
+ * Walk the navigation tree and return the slug of the first leaf page.
16
+ * Matches the resolution used by `app/[[...slug]]/page.tsx:findFirstPage`
17
+ * so specific-pages mode can keep the root path public when the first
18
+ * nav page is public. Returns null if no leaf page is found.
19
+ */
20
+ export function findFirstNavPage(nav: unknown): string | null {
21
+ if (!nav || typeof nav !== 'object') return null;
22
+ const node = nav as Record<string, unknown>;
23
+ const pages = node.pages;
24
+
25
+ if (Array.isArray(pages)) {
26
+ for (const p of pages) {
27
+ if (typeof p === 'string' && p.length > 0) return '/' + p;
28
+ if (p && typeof p === 'object') {
29
+ const pageVal = (p as Record<string, unknown>).page;
30
+ if (typeof pageVal === 'string' && pageVal.length > 0) {
31
+ return '/' + pageVal;
32
+ }
33
+ const nested = findFirstNavPage(p);
34
+ if (nested) return nested;
35
+ }
36
+ }
37
+ // A group is an atomic container — exhaust its pages rather than
38
+ // falling through to the sibling-key loop below (which is for
39
+ // top-level nav shapes like tabs/anchors/languages, not group contents).
40
+ if ('group' in node) return null;
41
+ }
42
+
43
+ for (const key of ['groups', 'tabs', 'anchors', 'versions', 'languages']) {
44
+ const arr = node[key];
45
+ if (Array.isArray(arr)) {
46
+ for (const item of arr) {
47
+ const nested = findFirstNavPage(item);
48
+ if (nested) return nested;
49
+ }
50
+ }
51
+ }
52
+
53
+ return null;
54
+ }
55
+
56
+ function collectPublicGroupPages(
57
+ nav: unknown,
58
+ inPublicGroup: boolean = false,
59
+ out: string[] = []
60
+ ): string[] {
61
+ if (!nav || typeof nav !== 'object') return out;
62
+
63
+ const node = nav as Record<string, unknown>;
64
+
65
+ if ('group' in node && Array.isArray((node as any).pages)) {
66
+ const isPublic = inPublicGroup || node.public === true;
67
+ for (const p of (node as any).pages) {
68
+ if (typeof p === 'string') {
69
+ if (isPublic) out.push(`/${p}`);
70
+ } else {
71
+ collectPublicGroupPages(p, isPublic, out);
72
+ }
73
+ }
74
+ return out;
75
+ }
76
+
77
+ if (Array.isArray((node as any).pages)) {
78
+ for (const p of (node as any).pages) {
79
+ collectPublicGroupPages(p, inPublicGroup, out);
80
+ }
81
+ }
82
+
83
+ for (const key of ['groups', 'tabs', 'anchors', 'versions', 'languages']) {
84
+ const arr = (node as any)[key];
85
+ if (Array.isArray(arr)) {
86
+ for (const item of arr) {
87
+ collectPublicGroupPages(item, inPublicGroup, out);
88
+ }
89
+ }
90
+ }
91
+
92
+ return out;
93
+ }
94
+
95
+ /**
96
+ * Resolve the final list of public paths by merging three sources:
97
+ * 1. MDX frontmatter `public: true`
98
+ * 2. Navigation groups with `public: true` in docs.json
99
+ * 3. `auth.password.public[]` globs in docs.json
100
+ */
101
+ export function resolvePublicPaths(input: Input): string[] {
102
+ const set = new Set<string>();
103
+
104
+ for (const p of input.pagesWithFrontmatter) {
105
+ if (p.frontmatter.public === true) {
106
+ set.add(p.slug);
107
+ }
108
+ }
109
+
110
+ for (const path of collectPublicGroupPages(input.docsConfig.navigation)) {
111
+ set.add(path);
112
+ }
113
+
114
+ const globs = (input.docsConfig as any).auth?.password?.public;
115
+ if (Array.isArray(globs)) {
116
+ for (const g of globs) {
117
+ if (typeof g === 'string' && g.startsWith('/')) {
118
+ set.add(g);
119
+ }
120
+ }
121
+ }
122
+
123
+ return Array.from(set);
124
+ }
125
+
126
+ /**
127
+ * Collect paths marked private via:
128
+ * 1. MDX frontmatter `private: true`
129
+ * 2. `auth.password.private[]` exact paths in docs.json
130
+ *
131
+ * Public wins over private: if a page appears in (or matches a glob in) the
132
+ * public set, it stays public regardless of any private marker. We use
133
+ * matchPublicPath here so a glob like `/changelog/*` correctly shadows
134
+ * `/changelog/release-1` — without this, the page would land in the private
135
+ * list while the middleware actually serves it publicly via the glob match,
136
+ * and the dashboard would lie about which pages are gated.
137
+ */
138
+ export function collectPrivatePaths(input: Input): string[] {
139
+ const publicPaths = resolvePublicPaths(input);
140
+ const isPublic = (path: string): boolean =>
141
+ matchPublicPath(path, publicPaths, false);
142
+ const privateSet = new Set<string>();
143
+
144
+ for (const p of input.pagesWithFrontmatter) {
145
+ if (p.frontmatter.private === true && !isPublic(p.slug)) {
146
+ privateSet.add(p.slug);
147
+ }
148
+ }
149
+
150
+ const configPrivate = input.docsConfig.auth?.password?.private;
151
+ if (Array.isArray(configPrivate)) {
152
+ for (const entry of configPrivate) {
153
+ if (
154
+ typeof entry === 'string' &&
155
+ entry.startsWith('/') &&
156
+ !isPublic(entry)
157
+ ) {
158
+ privateSet.add(entry);
159
+ }
160
+ }
161
+ }
162
+
163
+ return Array.from(privateSet);
164
+ }
165
+
166
+ // ---------------------------------------------------------------------------
167
+ // detectAuthMode
168
+ // ---------------------------------------------------------------------------
169
+
170
+ export type AuthMode = 'off' | 'all' | 'specific';
171
+
172
+ export interface AuthModeResult {
173
+ mode: AuthMode;
174
+ publicPaths: string[];
175
+ privatePaths: string[];
176
+ }
177
+
178
+ /**
179
+ * Determine the effective auth mode from the docs config and page frontmatter.
180
+ *
181
+ * 1. `auth.password.enabled === true` → mode='all'
182
+ * 2. Private markers exist (frontmatter or config) → mode='specific'
183
+ * 3. Otherwise → mode='off'
184
+ */
185
+ export function detectAuthMode(input: Input): AuthModeResult {
186
+ const enabled = input.docsConfig.auth?.password?.enabled === true;
187
+
188
+ if (enabled) {
189
+ return {
190
+ mode: 'all',
191
+ publicPaths: resolvePublicPaths(input),
192
+ privatePaths: [],
193
+ };
194
+ }
195
+
196
+ const privatePaths = collectPrivatePaths(input);
197
+ if (privatePaths.length === 0) {
198
+ return { mode: 'off', publicPaths: [], privatePaths: [] };
199
+ }
200
+
201
+ // Specific mode: invert the page list (all pages minus private) and
202
+ // union with the explicit public set so non-MDX routes like
203
+ // /api/openapi survive the inversion.
204
+ const privateSet = new Set(privatePaths);
205
+ const publicSet = new Set<string>();
206
+ for (const p of input.pagesWithFrontmatter) {
207
+ if (!privateSet.has(p.slug)) publicSet.add(p.slug);
208
+ }
209
+ for (const path of resolvePublicPaths(input)) {
210
+ publicSet.add(path);
211
+ }
212
+
213
+ // The root path `/` is a routing artifact that resolves in-place to
214
+ // the first nav page. Without this, specific-pages mode would gate the
215
+ // homepage while the page it resolves to is reachable at its own URL —
216
+ // surprising UX and a signal that a password exists.
217
+ const firstPage = findFirstNavPage(input.docsConfig.navigation);
218
+ if (firstPage && !privateSet.has(firstPage)) {
219
+ publicSet.add('/');
220
+ }
221
+
222
+ return {
223
+ mode: 'specific',
224
+ publicPaths: Array.from(publicSet),
225
+ privatePaths,
226
+ };
227
+ }
@@ -21,3 +21,97 @@ export const redis = redisUrl && redisToken && redisUrl.startsWith('https://')
21
21
  export function isRedisConfigured(): boolean {
22
22
  return redis !== null;
23
23
  }
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Auth key helpers
27
+ // These are used by the build pipeline (build-service) to write/clear the
28
+ // auth configuration in Redis on every build.
29
+ // ---------------------------------------------------------------------------
30
+
31
+ /**
32
+ * Minimal Upstash REST command helper (fetch-based, edge-compatible).
33
+ * Avoids the @upstash/redis SDK for commands that need raw URL encoding.
34
+ */
35
+ async function upstashCommand(
36
+ kvUrl: string,
37
+ kvToken: string,
38
+ command: string,
39
+ ...args: string[]
40
+ ): Promise<unknown> {
41
+ const encodedArgs = args.map((arg) => encodeURIComponent(arg));
42
+ const url = `${kvUrl}/${command}/${encodedArgs.join('/')}`;
43
+ const response = await fetch(url, {
44
+ headers: { Authorization: `Bearer ${kvToken}` },
45
+ });
46
+ const bodyText = await response.text();
47
+ let data: any = null;
48
+ try {
49
+ data = bodyText ? JSON.parse(bodyText) : null;
50
+ } catch {
51
+ data = { result: bodyText };
52
+ }
53
+ if (!response.ok) {
54
+ throw new Error(data?.error || bodyText || `Upstash ${command} failed`);
55
+ }
56
+ return data?.result;
57
+ }
58
+
59
+ /**
60
+ * Public part of project auth: the docs.json enabled flag + the resolved list
61
+ * of paths that bypass the password check (from frontmatter `public: true`,
62
+ * docs.json groups with `public: true`, and `auth.password.public[]` globs).
63
+ * Written by the build pipeline after every build.
64
+ *
65
+ * The `enabled` flag is the docs.json truth-table source. Middleware only
66
+ * gates traffic when enabled=true AND a hash exists in projectAuthSecret.
67
+ */
68
+ export async function setProjectAuthPublic(
69
+ slug: string,
70
+ value: { enabled: boolean; publicPaths: unknown[] },
71
+ kvUrl?: string,
72
+ kvToken?: string
73
+ ): Promise<void> {
74
+ if (!kvUrl || !kvToken) return;
75
+ // Filter to strings here so the read path (auth-resolver) can trust
76
+ // the Redis shape and skip the per-request defensive filter — that
77
+ // filter creates a fresh array on every request and defeats the
78
+ // matchPublicPath WeakMap cache.
79
+ const publicPaths = value.publicPaths.filter(
80
+ (p): p is string => typeof p === 'string',
81
+ );
82
+ await upstashCommand(
83
+ kvUrl,
84
+ kvToken,
85
+ 'SET',
86
+ `projectAuthPublic:${slug}`,
87
+ JSON.stringify({ enabled: value.enabled, publicPaths }),
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Delete the secret part of project auth (disables gating for the project).
93
+ * Called when auth.password.enabled becomes false so the gate opens immediately
94
+ * without waiting for an admin to remove the hash manually.
95
+ */
96
+ export async function deleteProjectAuthSecret(
97
+ slug: string,
98
+ kvUrl?: string,
99
+ kvToken?: string
100
+ ): Promise<void> {
101
+ if (!kvUrl || !kvToken) return;
102
+ await upstashCommand(kvUrl, kvToken, 'DEL', `projectAuthSecret:${slug}`);
103
+ }
104
+
105
+ /**
106
+ * Delete a domain-scoped auth secret mirror.
107
+ * Called when auth.password.enabled becomes false to ensure no domain mirror
108
+ * keeps gating traffic after the project-level gate is removed.
109
+ */
110
+ export async function deleteDomainAuthSecret(
111
+ hostname: string,
112
+ kvUrl?: string,
113
+ kvToken?: string
114
+ ): Promise<void> {
115
+ if (!kvUrl || !kvToken) return;
116
+ await upstashCommand(kvUrl, kvToken, 'DEL', `domainAuthSecret:${hostname}`);
117
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Validate and sanitize a user-supplied "from" path used for post-unlock redirects.
3
+ *
4
+ * Parses the input against a placeholder base URL and requires the resulting
5
+ * origin to still be the placeholder. Rejects CRLF, backslashes, paths that
6
+ * don't start with /, and anything that changes origin after URL normalization.
7
+ * Works in both Edge and Node runtimes.
8
+ */
9
+ const PLACEHOLDER_BASE = 'https://placeholder.local';
10
+
11
+ export function sanitizeFrom(raw: string | null | undefined): string {
12
+ if (!raw) return '/';
13
+ if (/[\r\n]/.test(raw)) return '/';
14
+ if (!raw.startsWith('/')) return '/';
15
+ if (raw.includes('\\')) return '/';
16
+
17
+ let parsed: URL;
18
+ try {
19
+ parsed = new URL(raw, PLACEHOLDER_BASE);
20
+ } catch {
21
+ return '/';
22
+ }
23
+
24
+ if (parsed.origin !== PLACEHOLDER_BASE) return '/';
25
+
26
+ // After URL normalization, reject any path that starts with // — these are
27
+ // protocol-relative references that browsers may interpret as external URLs
28
+ // (e.g., /..//evil.com normalizes to //evil.com). Also check the decoded
29
+ // form to catch percent-encoded slashes like /%2F%2Fevil.com → //evil.com.
30
+ const normalized = parsed.pathname + parsed.search + parsed.hash;
31
+ if (parsed.pathname.startsWith('//')) return '/';
32
+ const decodedPathname = decodeURIComponent(parsed.pathname);
33
+ if (decodedPathname.startsWith('//')) return '/';
34
+
35
+ return normalized;
36
+ }
@@ -0,0 +1,6 @@
1
+ export default {
2
+ plugins: {
3
+ '@tailwindcss/postcss': {},
4
+ },
5
+ }
6
+
@@ -1545,6 +1545,50 @@
1545
1545
  "additionalProperties": false,
1546
1546
  "description": "Configuration for the jamdesk spellcheck command"
1547
1547
  },
1548
+ "auth": {
1549
+ "type": "object",
1550
+ "description": "Access control configuration for the docs site.",
1551
+ "additionalProperties": false,
1552
+ "properties": {
1553
+ "password": {
1554
+ "type": "object",
1555
+ "description": "Shared-password site protection. Requires the password to be set in the Jamdesk dashboard; this block only controls declarative opt-in and public exceptions.",
1556
+ "additionalProperties": false,
1557
+ "properties": {
1558
+ "enabled": {
1559
+ "type": "boolean",
1560
+ "description": "Opt in to password protection. Site is only gated when this is true AND a password has been set in the dashboard.",
1561
+ "default": false
1562
+ },
1563
+ "hint": {
1564
+ "type": "string",
1565
+ "description": "Optional hint shown on the unlock page (e.g., \"Ask your account manager\"). Plain text, no HTML.",
1566
+ "maxLength": 200
1567
+ },
1568
+ "public": {
1569
+ "type": "array",
1570
+ "description": "Paths or globs that bypass the password check. Supports '*' (one path segment) and '**' (recursive). A bare '/' is rejected so the password wall cannot be silently disabled.",
1571
+ "items": {
1572
+ "type": "string",
1573
+ "pattern": "^/[A-Za-z0-9._*/-]+$"
1574
+ },
1575
+ "maxItems": 100,
1576
+ "uniqueItems": true
1577
+ },
1578
+ "private": {
1579
+ "type": "array",
1580
+ "description": "Exact paths (starting with /) that require the password. When set without auth.password.enabled, the feature activates in 'specific pages' mode — only these paths are gated and every other page stays public. A page marked both public and private is treated as public.",
1581
+ "items": {
1582
+ "type": "string",
1583
+ "pattern": "^/[A-Za-z0-9._/-]+$"
1584
+ },
1585
+ "maxItems": 100,
1586
+ "uniqueItems": true
1587
+ }
1588
+ }
1589
+ }
1590
+ }
1591
+ },
1548
1592
  "images": {
1549
1593
  "type": "object",
1550
1594
  "description": "Image optimization settings for documentation builds",
@@ -1672,6 +1716,9 @@
1672
1716
  "spellcheck": {
1673
1717
  "$ref": "#/anyOf/0/properties/spellcheck"
1674
1718
  },
1719
+ "auth": {
1720
+ "$ref": "#/anyOf/0/properties/auth"
1721
+ },
1675
1722
  "images": {
1676
1723
  "$ref": "#/anyOf/0/properties/images"
1677
1724
  }
@@ -1791,6 +1838,9 @@
1791
1838
  "spellcheck": {
1792
1839
  "$ref": "#/anyOf/0/properties/spellcheck"
1793
1840
  },
1841
+ "auth": {
1842
+ "$ref": "#/anyOf/0/properties/auth"
1843
+ },
1794
1844
  "images": {
1795
1845
  "$ref": "#/anyOf/0/properties/images"
1796
1846
  }