jamdesk 1.1.205 → 1.1.206
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__/unit/turbopack-loader-config-drift.test.d.ts +20 -0
- package/dist/__tests__/unit/turbopack-loader-config-drift.test.d.ts.map +1 -0
- package/dist/__tests__/unit/turbopack-loader-config-drift.test.js +79 -0
- package/dist/__tests__/unit/turbopack-loader-config-drift.test.js.map +1 -0
- package/package.json +1 -1
- package/vendored/app/[[...slug]]/page.tsx +1 -102
- package/vendored/app/api/jd/auth/logout/route.ts +36 -4
- package/vendored/app/api/jd/unlock/route.ts +1 -4
- package/vendored/lib/auth-plane-write-warning.ts +84 -0
- package/vendored/lib/jwt-key-build-warning.ts +38 -0
- package/vendored/lib/mdx-import-scan.ts +110 -0
- package/vendored/lib/unlock-audit.ts +10 -0
- package/vendored/next.config.js +32 -0
- package/vendored/scripts/turbopack-js-to-ts-loader.cjs +51 -0
- package/vendored/workspace-package-lock.json +33 -33
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drift guard for the Turbopack `.js`-specifier workaround.
|
|
3
|
+
*
|
|
4
|
+
* `build-service/next.config.mjs` applies `scripts/turbopack-js-to-ts-loader.cjs`
|
|
5
|
+
* to every `*.ts`/`*.tsx` because Turbopack has no equivalent of webpack's
|
|
6
|
+
* `resolve.extensionAlias` and throws a hard "Module not found" on some
|
|
7
|
+
* `./foo.js` imports whose real source is `./foo.ts` — the convention this
|
|
8
|
+
* repo uses everywhere (tsconfig `moduleResolution: "bundler"`).
|
|
9
|
+
*
|
|
10
|
+
* The CLI vendors the SAME source tree, including the jwt-session subtree that
|
|
11
|
+
* provoked the original failure, and compiles it with Turbopack in
|
|
12
|
+
* `jamdesk dev`. So the CLI config needs the same rule, and the loader has to
|
|
13
|
+
* be vendored or the rule points at a file that is not in the workspace.
|
|
14
|
+
*
|
|
15
|
+
* CLAUDE.md already requires that a build-service next.config change be
|
|
16
|
+
* mirrored into the CLI config by hand. This test is what makes that rule
|
|
17
|
+
* enforceable instead of aspirational.
|
|
18
|
+
*/
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=turbopack-loader-config-drift.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turbopack-loader-config-drift.test.d.ts","sourceRoot":"","sources":["../../../src/__tests__/unit/turbopack-loader-config-drift.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drift guard for the Turbopack `.js`-specifier workaround.
|
|
3
|
+
*
|
|
4
|
+
* `build-service/next.config.mjs` applies `scripts/turbopack-js-to-ts-loader.cjs`
|
|
5
|
+
* to every `*.ts`/`*.tsx` because Turbopack has no equivalent of webpack's
|
|
6
|
+
* `resolve.extensionAlias` and throws a hard "Module not found" on some
|
|
7
|
+
* `./foo.js` imports whose real source is `./foo.ts` — the convention this
|
|
8
|
+
* repo uses everywhere (tsconfig `moduleResolution: "bundler"`).
|
|
9
|
+
*
|
|
10
|
+
* The CLI vendors the SAME source tree, including the jwt-session subtree that
|
|
11
|
+
* provoked the original failure, and compiles it with Turbopack in
|
|
12
|
+
* `jamdesk dev`. So the CLI config needs the same rule, and the loader has to
|
|
13
|
+
* be vendored or the rule points at a file that is not in the workspace.
|
|
14
|
+
*
|
|
15
|
+
* CLAUDE.md already requires that a build-service next.config change be
|
|
16
|
+
* mirrored into the CLI config by hand. This test is what makes that rule
|
|
17
|
+
* enforceable instead of aspirational.
|
|
18
|
+
*/
|
|
19
|
+
import { describe, it, expect } from 'vitest';
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { fileURLToPath } from 'node:url';
|
|
23
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const CLI_ROOT = path.join(__dirname, '../../..');
|
|
25
|
+
const BUILD_SERVICE = path.join(CLI_ROOT, '../build-service');
|
|
26
|
+
const LOADER_BASENAME = 'turbopack-js-to-ts-loader.cjs';
|
|
27
|
+
const CONFIGS = {
|
|
28
|
+
'build-service/next.config.mjs': path.join(BUILD_SERVICE, 'next.config.mjs'),
|
|
29
|
+
'cli/config/next.config.js': path.join(CLI_ROOT, 'config/next.config.js'),
|
|
30
|
+
'cli/vendored/next.config.js': path.join(CLI_ROOT, 'vendored/next.config.js'),
|
|
31
|
+
};
|
|
32
|
+
function read(p) {
|
|
33
|
+
return fs.readFileSync(p, 'utf8');
|
|
34
|
+
}
|
|
35
|
+
/** Comments stripped, so a prose edit never fails the guard. */
|
|
36
|
+
function code(src) {
|
|
37
|
+
return src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
|
38
|
+
}
|
|
39
|
+
describe('Turbopack loader rule is mirrored across every Next config', () => {
|
|
40
|
+
for (const [label, file] of Object.entries(CONFIGS)) {
|
|
41
|
+
it(`${label} applies the loader to *.ts and *.tsx`, () => {
|
|
42
|
+
const src = code(read(file));
|
|
43
|
+
expect(src, `${label} is missing a turbopack.rules block`).toMatch(/rules\s*:\s*\{/);
|
|
44
|
+
for (const glob of ['*.ts', '*.tsx']) {
|
|
45
|
+
const re = new RegExp(`'\\${glob}'\\s*:\\s*\\{\\s*loaders\\s*:\\s*\\[\\s*'\\./scripts/${LOADER_BASENAME.replace(/\./g, '\\.')}'`);
|
|
46
|
+
expect(src, `${label} does not route ${glob} through ${LOADER_BASENAME}`).toMatch(re);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
it(`${label} keeps the webpack extensionAlias fallback`, () => {
|
|
50
|
+
// `jamdesk dev --webpack` is an advertised escape hatch (src/index.ts),
|
|
51
|
+
// and webpack needs the alias for exactly the same imports.
|
|
52
|
+
const src = code(read(file));
|
|
53
|
+
expect(src, `${label} is missing resolve.extensionAlias`).toMatch(/extensionAlias/);
|
|
54
|
+
expect(src).toMatch(/'\.js'\s*:\s*\[\s*'\.ts'\s*,\s*'\.tsx'\s*,\s*'\.js'\s*\]/);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
describe('the loader itself is present wherever a config points at it', () => {
|
|
59
|
+
const canonical = path.join(BUILD_SERVICE, 'scripts', LOADER_BASENAME);
|
|
60
|
+
const vendored = path.join(CLI_ROOT, 'vendored/scripts', LOADER_BASENAME);
|
|
61
|
+
it('exists in build-service/scripts', () => {
|
|
62
|
+
expect(fs.existsSync(canonical), `${canonical} is missing`).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
it('is vendored into the CLI workspace tree', () => {
|
|
65
|
+
// dev.ts copies vendored/ wholesale into the workspace, so vendored/scripts
|
|
66
|
+
// becomes <workspace>/scripts — the path the rule resolves against.
|
|
67
|
+
expect(fs.existsSync(vendored), `${vendored} is missing; add it to safeScripts in cli/scripts/vendor.js`).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
it('is byte-identical to the canonical copy', () => {
|
|
70
|
+
expect(read(vendored)).toBe(read(canonical));
|
|
71
|
+
});
|
|
72
|
+
it('is on the vendor allowlist, so re-vendoring keeps it', () => {
|
|
73
|
+
// The copy loop in vendor.js is existsSync-guarded, so an omission here is
|
|
74
|
+
// silent: vendoring succeeds and the CLI fails at compile time instead.
|
|
75
|
+
const vendorScript = read(path.join(CLI_ROOT, 'scripts/vendor.js'));
|
|
76
|
+
expect(vendorScript).toContain(LOADER_BASENAME);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
//# sourceMappingURL=turbopack-loader-config-drift.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turbopack-loader-config-drift.test.js","sourceRoot":"","sources":["../../../src/__tests__/unit/turbopack-loader-config-drift.test.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jamdesk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.206",
|
|
4
4
|
"description": "CLI for Jamdesk \u2014 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",
|
|
@@ -5,10 +5,7 @@
|
|
|
5
5
|
// non-ISR local dev). The render body lives in lib/render-doc-page.tsx.
|
|
6
6
|
import { headers } from 'next/headers';
|
|
7
7
|
import { after } from 'next/server';
|
|
8
|
-
import fs from 'fs';
|
|
9
|
-
import path from 'path';
|
|
10
8
|
import type { Metadata } from 'next';
|
|
11
|
-
import { getContentDir } from '@/lib/docs';
|
|
12
9
|
import {
|
|
13
10
|
isIsrMode,
|
|
14
11
|
getProjectFromRequest,
|
|
@@ -18,7 +15,7 @@ import {
|
|
|
18
15
|
import { renderDocPage, buildDocMetadata, type RenderInput } from '@/lib/render-doc-page';
|
|
19
16
|
import { readNewsletterDisplay } from '@/lib/newsletter-display';
|
|
20
17
|
import { withR2OpsContext, emitR2OpsSummary } from '@/lib/r2-content';
|
|
21
|
-
import {
|
|
18
|
+
import { getAllDocPaths } from '@/lib/mdx-import-scan';
|
|
22
19
|
|
|
23
20
|
export const dynamic = 'force-dynamic';
|
|
24
21
|
export const dynamicParams = true;
|
|
@@ -30,104 +27,6 @@ interface PageProps {
|
|
|
30
27
|
}>;
|
|
31
28
|
}
|
|
32
29
|
|
|
33
|
-
// Mirror of the CLI's detector regex in cli/src/lib/relative-mdx-imports.ts.
|
|
34
|
-
// Duplicated intentionally — build-service uses bundler module resolution
|
|
35
|
-
// and shouldn't reach into cli/src/. Keep the regex itself in sync with
|
|
36
|
-
// that file. Two intentional simplifications vs the CLI version:
|
|
37
|
-
// - no `g` flag (boolean test, not iteration)
|
|
38
|
-
// - fence stripping replaces matches with empty string instead of
|
|
39
|
-
// blank lines of equal count — line numbers don't matter here, only
|
|
40
|
-
// a yes/no skip decision (the CLI preserves them for warning text).
|
|
41
|
-
const PARENT_RELATIVE_MDX_IMPORT_RE =
|
|
42
|
-
/^[ \t]*import\s+(?:type\s+)?[\w$*{}, \n\r]+\s+from\s+["']\.{1,2}\/[^"']+\.mdx["']\s*;?/m;
|
|
43
|
-
|
|
44
|
-
const FENCED_CODE_BLOCK_RE =
|
|
45
|
-
/^( *)(```+|~~~+)[^\n]*\n([\s\S]*?)\n\1\2\s*$/gm;
|
|
46
|
-
|
|
47
|
-
// Cache for pageHasRelativeMdxImport, keyed by absolute file path,
|
|
48
|
-
// invalidated when the file's mtime changes. `generateStaticParams` runs
|
|
49
|
-
// on every nav in `jamdesk dev`, and the regex-test reads the FULL file
|
|
50
|
-
// (per fix 7205c17c). For dodo (~200 MDX × 70-94 KB) that's 14-19 MB of
|
|
51
|
-
// disk I/O per nav. Cache hit reduces it to a directory walk + statSync.
|
|
52
|
-
//
|
|
53
|
-
// In production ISR mode generateStaticParams returns [] before any of
|
|
54
|
-
// this runs (see isIsrMode early-return), so the cache is dev-only in
|
|
55
|
-
// practice. The cache is unbounded — entries for deleted files persist
|
|
56
|
-
// until process exit. For a dev-only ~200-file working set this is
|
|
57
|
-
// trivial (a few KB). If this pattern is reused in a long-running ISR
|
|
58
|
-
// context in the future, add LRU eviction or rebuild-on-walk.
|
|
59
|
-
const mdxImportCache = new Map<string, { mtimeMs: number; hasImport: boolean }>();
|
|
60
|
-
|
|
61
|
-
export function pageHasRelativeMdxImport(filePath: string, mtimeMs?: number): boolean {
|
|
62
|
-
const cached = mdxImportCache.get(filePath);
|
|
63
|
-
if (cached && mtimeMs !== undefined && cached.mtimeMs === mtimeMs) {
|
|
64
|
-
return cached.hasImport;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
let hasImport: boolean;
|
|
68
|
-
try {
|
|
69
|
-
// Read the full file — MDX imports can appear at any top-level position
|
|
70
|
-
// (after a long prose intro or table of contents), and a slice was
|
|
71
|
-
// missing real imports past byte ~8192 in 70-94 KB customer pages.
|
|
72
|
-
const content = readMdxFile(filePath);
|
|
73
|
-
// Strip fenced code blocks so documentation examples (e.g.
|
|
74
|
-
// ```mdx\nimport X from "../snippets/foo.mdx";\n```) don't false-trigger.
|
|
75
|
-
hasImport = PARENT_RELATIVE_MDX_IMPORT_RE.test(content.replace(FENCED_CODE_BLOCK_RE, ''));
|
|
76
|
-
} catch {
|
|
77
|
-
hasImport = false;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (mtimeMs !== undefined) {
|
|
81
|
-
mdxImportCache.set(filePath, { mtimeMs, hasImport });
|
|
82
|
-
}
|
|
83
|
-
return hasImport;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** Test-only: clear the per-file MDX-import cache between test cases. */
|
|
87
|
-
export function _resetMdxImportCacheForTest(): void {
|
|
88
|
-
if (process.env.NODE_ENV === 'production') return;
|
|
89
|
-
mdxImportCache.clear();
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
interface CollectedPaths {
|
|
93
|
-
supported: string[];
|
|
94
|
-
skipped: string[];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function getAllDocPaths(): CollectedPaths {
|
|
98
|
-
const contentDir = getContentDir();
|
|
99
|
-
const supported: string[] = [];
|
|
100
|
-
const skipped: string[] = [];
|
|
101
|
-
|
|
102
|
-
function traverseDir(dir: string, basePath: string = '') {
|
|
103
|
-
if (!fs.existsSync(dir)) return;
|
|
104
|
-
const files = fs.readdirSync(dir);
|
|
105
|
-
for (const file of files) {
|
|
106
|
-
if (file.startsWith('.')) continue;
|
|
107
|
-
const filePath = path.join(dir, file);
|
|
108
|
-
let stat;
|
|
109
|
-
try {
|
|
110
|
-
stat = fs.statSync(filePath);
|
|
111
|
-
} catch {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
if (stat.isDirectory()) {
|
|
115
|
-
traverseDir(filePath, path.join(basePath, file));
|
|
116
|
-
} else if (file.endsWith('.mdx')) {
|
|
117
|
-
const slug = path.join(basePath, file.replace(/\.mdx$/, ''));
|
|
118
|
-
if (pageHasRelativeMdxImport(filePath, stat.mtimeMs)) {
|
|
119
|
-
skipped.push(slug);
|
|
120
|
-
} else {
|
|
121
|
-
supported.push(slug);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
traverseDir(contentDir);
|
|
128
|
-
return { supported, skipped };
|
|
129
|
-
}
|
|
130
|
-
|
|
131
30
|
// In `jamdesk dev`, Next.js calls generateStaticParams during initial
|
|
132
31
|
// compile AND for each lazily-compiled route (every page click). Without
|
|
133
32
|
// a guard the same multi-line warning floods the dev console after every
|
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
* route's redirects: never `NextResponse.redirect(new URL('/', req.url))`,
|
|
11
11
|
* since `req.url`'s host is the internal rewrite host, not the customer's
|
|
12
12
|
* public host behind a reverse proxy.
|
|
13
|
+
*
|
|
14
|
+
* Cross-site protection: clearing a cookie from a GET means any page on the
|
|
15
|
+
* web could log a reader out by pointing an <img> or <iframe> at this URL.
|
|
16
|
+
* We do NOT use `isSameOriginRequest` here, unlike the cookie-MINTING routes.
|
|
17
|
+
* That helper rejects `cross-site`, and a perfectly legitimate 'Log out of
|
|
18
|
+
* docs' link on the customer's own app is cross-site. The attack and the
|
|
19
|
+
* legitimate link differ by Sec-Fetch-Dest, not by site: a forced logout is
|
|
20
|
+
* always a sub-resource load, a real logout is always a document navigation.
|
|
21
|
+
* So we clear only for `document` (plus clients that send no Sec-Fetch at
|
|
22
|
+
* all, i.e. curl and pre-2020 browsers, which cannot be attacked this way).
|
|
23
|
+
* A suppressed logout still 303s — the route's contract is that it never
|
|
24
|
+
* fails loudly — it just leaves the session alone.
|
|
13
25
|
*/
|
|
14
26
|
|
|
15
27
|
import { resolveAuth } from '@/lib/auth-resolver';
|
|
@@ -17,6 +29,23 @@ import { resolveAuth } from '@/lib/auth-resolver';
|
|
|
17
29
|
export const runtime = 'nodejs';
|
|
18
30
|
export const dynamic = 'force-dynamic';
|
|
19
31
|
|
|
32
|
+
/**
|
|
33
|
+
* True when this GET is a real top-level navigation, so acting on it is the
|
|
34
|
+
* reader's own doing. Sec-Fetch-Dest is browser-set and page-unforgeable.
|
|
35
|
+
* Absent means a non-browser client, which cannot be the victim of a
|
|
36
|
+
* cross-site sub-resource load, so we act.
|
|
37
|
+
*
|
|
38
|
+
* Prefetch is excluded separately: a speculative fetch of the logout link
|
|
39
|
+
* carries `Sec-Fetch-Dest: document` but no reader ever clicked it.
|
|
40
|
+
*/
|
|
41
|
+
function isUserInitiatedNavigation(req: Request): boolean {
|
|
42
|
+
const purpose = req.headers.get('sec-purpose') || '';
|
|
43
|
+
if (purpose.includes('prefetch')) return false;
|
|
44
|
+
const dest = req.headers.get('sec-fetch-dest');
|
|
45
|
+
if (!dest) return true;
|
|
46
|
+
return dest === 'document';
|
|
47
|
+
}
|
|
48
|
+
|
|
20
49
|
export async function GET(req: Request): Promise<Response> {
|
|
21
50
|
const slug = req.headers.get('x-project-slug');
|
|
22
51
|
// Relative '/' works for the same-site fallback; the customer loginUrl is
|
|
@@ -29,10 +58,13 @@ export async function GET(req: Request): Promise<Response> {
|
|
|
29
58
|
} catch { /* logout must always succeed; fall back to '/' */ }
|
|
30
59
|
}
|
|
31
60
|
const headers = new Headers({ Location: location, 'Cache-Control': 'no-store' });
|
|
32
|
-
if (
|
|
33
|
-
|
|
61
|
+
if (isUserInitiatedNavigation(req)) {
|
|
62
|
+
if (slug) {
|
|
63
|
+
headers.append('Set-Cookie', `jd_auth_${slug}=; Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=0`);
|
|
64
|
+
}
|
|
65
|
+
// Clear the client-readable hint the callback set (Task 8) alongside it,
|
|
66
|
+
// so the header never advertises a session the cookie no longer backs.
|
|
67
|
+
headers.append('Set-Cookie', 'jd_authed=; Path=/; Secure; SameSite=Lax; Max-Age=0');
|
|
34
68
|
}
|
|
35
|
-
// Always clear the client-readable hint the callback set (Task 8).
|
|
36
|
-
headers.append('Set-Cookie', 'jd_authed=; Path=/; Secure; SameSite=Lax; Max-Age=0');
|
|
37
69
|
return new Response(null, { status: 303, headers });
|
|
38
70
|
}
|
|
@@ -24,6 +24,7 @@ import { sanitizeFrom } from '@/lib/sanitize-from';
|
|
|
24
24
|
import { VALID_SLUG_RE } from '@/lib/middleware-helpers';
|
|
25
25
|
import { signAuthCookie } from '@/shared/auth-cookie';
|
|
26
26
|
import { secretsEqual } from '@/lib/crypto-helpers';
|
|
27
|
+
import { MASTER_AUDIT_TAG } from '@/lib/unlock-audit';
|
|
27
28
|
import { isSameOriginRequest } from '@/lib/same-origin';
|
|
28
29
|
|
|
29
30
|
const scryptAsync = promisify(scrypt) as (
|
|
@@ -68,10 +69,6 @@ const MASTER_COOKIE_MAX_AGE = 60 * 60;
|
|
|
68
69
|
// an attacker rotates slugs to evade the per-slug limit.
|
|
69
70
|
const MASTER_PASSWORD_MIN_LENGTH = 32;
|
|
70
71
|
|
|
71
|
-
// Audit log tag emitted on every successful master-password unlock. Exported
|
|
72
|
-
// so tests can assert exact format without duplicating the literal.
|
|
73
|
-
export const MASTER_AUDIT_TAG = '[unlock] master-password used';
|
|
74
|
-
|
|
75
72
|
// Pre-compute a dummy hash at module load time so we can always run scrypt
|
|
76
73
|
// even when no hash is stored, preventing timing oracles that reveal whether
|
|
77
74
|
// a project has password protection configured.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { BuildWarning } from '../shared/status-reporter.js'; // vendored copy — NOT '../../shared'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Warn when a gated project finished a build without its auth plane written.
|
|
5
|
+
*
|
|
6
|
+
* `setProjectAuthPublic` is the only thing that puts a project's gate config
|
|
7
|
+
* where the edge can read it, and in build.ts it sits inside a try whose catch
|
|
8
|
+
* logs `Failed to write projectAuthPublic to Redis (non-fatal)` and lets the
|
|
9
|
+
* build go green. That non-fatal choice is right for availability and wrong for
|
|
10
|
+
* visibility: `resolveAuth` returns null when the key is absent, and a null
|
|
11
|
+
* resolve means `applyAuthGate` returns null, which is NO GATE. So the failure
|
|
12
|
+
* mode of a dropped write is a fully public site reported as a successful build.
|
|
13
|
+
*
|
|
14
|
+
* Two ways in, neither of them a misconfiguration:
|
|
15
|
+
* - Upstash is briefly unavailable during the write.
|
|
16
|
+
* - The payload outgrows what `upstashCommand` can put in a URL path. It
|
|
17
|
+
* encodes the whole value into the path, and a few hundred group-restricted
|
|
18
|
+
* pages is tens of KB of URL, so this one arrives through ordinary growth on
|
|
19
|
+
* a project that has been fine for months.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately mode-agnostic. The fail-open lives in `resolveAuth` returning
|
|
22
|
+
* null, which happens before authType is ever consulted, so a password tenant
|
|
23
|
+
* loses its gate exactly as a jwt tenant does. Silent when `enabled` is false,
|
|
24
|
+
* where serving publicly is the stated intent.
|
|
25
|
+
*
|
|
26
|
+
* This warns rather than fails the build. Failing would be the safer default on
|
|
27
|
+
* the ordering alone (the write precedes the R2 upload, so a hard fail would
|
|
28
|
+
* stop unprotected content from publishing at all), but it converts every
|
|
29
|
+
* transient Upstash blip into a broken customer build. That tradeoff is a
|
|
30
|
+
* product call, not a build-service one.
|
|
31
|
+
*/
|
|
32
|
+
export function buildAuthPlaneWriteWarning(input: {
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
writeFailed: boolean;
|
|
35
|
+
}): BuildWarning | null {
|
|
36
|
+
if (!input.enabled || !input.writeFailed) return null;
|
|
37
|
+
return {
|
|
38
|
+
// Same reuse, and the same reasoning, as buildJwtKeyWarning: this routing
|
|
39
|
+
// (emailed, counted, badged) is what the warning needs, and a dedicated
|
|
40
|
+
// BuildWarningType would mean the six-place sync chain for no difference.
|
|
41
|
+
type: 'invalid_openapi_spec',
|
|
42
|
+
file: 'docs.json',
|
|
43
|
+
message:
|
|
44
|
+
'This project is password or JWT protected, but the build could not publish its access rules, ' +
|
|
45
|
+
'so the site is currently readable by anyone. Rebuild to retry. ' +
|
|
46
|
+
'If it keeps failing, the access rules may have grown too large to store and support can raise it.',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Does docs.json itself declare a gate? Read BEFORE the auth try block, so the
|
|
52
|
+
* warning above survives a throw inside it.
|
|
53
|
+
*
|
|
54
|
+
* The gap this closes: `authPlaneGated` used to be assigned only from
|
|
55
|
+
* `effectiveEnabled`, ~19 lines into the try, after `pageInfos.map` and
|
|
56
|
+
* `detectAuthMode`. Anything throwing in that window skipped both the
|
|
57
|
+
* assignment AND `setProjectAuthPublic`, so the site went public and the
|
|
58
|
+
* warning was suppressed by the same throw. That window is reachable from
|
|
59
|
+
* user-supplied config: `collectGroupsPaths` does
|
|
60
|
+
* `(navigation?.languages ?? []).map(...)`, and `??` only guards null and
|
|
61
|
+
* undefined, so a `languages` that is a string throws.
|
|
62
|
+
*
|
|
63
|
+
* Mirrors `detectAuthMode`'s own test exactly (`?.enabled === true`), never
|
|
64
|
+
* truthiness — 'false' and 0 are not gates there and must not be gates here,
|
|
65
|
+
* or the warning fires on a site that is public by intent.
|
|
66
|
+
*
|
|
67
|
+
* MUST NOT THROW. Optional chaining is safe against any non-null value, which
|
|
68
|
+
* is the point: docs.json is user JSON and `auth` can be a string, an array or
|
|
69
|
+
* a number. Callers combine this monotonically (`a || b`) with the in-try
|
|
70
|
+
* value, so it can only ever turn the warning on.
|
|
71
|
+
*
|
|
72
|
+
* Residual, named rather than hidden: `specific` mode is frontmatter-derived
|
|
73
|
+
* (`collectPrivatePaths`), so a tenant gated only by per-page `private`
|
|
74
|
+
* frontmatter reads false here. A throw before `detectAuthMode` on such a
|
|
75
|
+
* tenant still goes unwarned. Covering it would mean replicating the
|
|
76
|
+
* frontmatter walk outside the try, which reintroduces the throw it guards.
|
|
77
|
+
*/
|
|
78
|
+
export function configDeclaresGate(docsConfig: unknown): boolean {
|
|
79
|
+
const auth = (docsConfig as { auth?: unknown } | null | undefined)?.auth as {
|
|
80
|
+
password?: { enabled?: unknown };
|
|
81
|
+
jwt?: { enabled?: unknown };
|
|
82
|
+
} | undefined;
|
|
83
|
+
return auth?.password?.enabled === true || auth?.jwt?.enabled === true;
|
|
84
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { BuildWarning } from '../shared/status-reporter.js'; // vendored copy — NOT '../../shared'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Warn when a project declares JWT authentication but has no signing key.
|
|
5
|
+
*
|
|
6
|
+
* The truth table in auth-resolver.ts is explicit: `authType === 'jwt'` with a
|
|
7
|
+
* missing or malformed `projectJwtSecret` means NO GATE. That is the correct
|
|
8
|
+
* fail-safe for a key that was cleared deliberately, but it is also reachable
|
|
9
|
+
* by accident: commit `auth.jwt.enabled: true`, publish, and never click
|
|
10
|
+
* Generate signing key. The build then succeeds, the site serves every page to
|
|
11
|
+
* the public, and nothing says so. The dashboard card does report it, but a
|
|
12
|
+
* customer who publishes from git has no reason to open that page.
|
|
13
|
+
*
|
|
14
|
+
* Silent for `enabled: false`, where a public site is the stated intent, and
|
|
15
|
+
* silent for password tenants, whose own secret has its own row in that table.
|
|
16
|
+
* A password tenant can legitimately still hold a JWT key from a half-finished
|
|
17
|
+
* migration; coexistence is by design and only `authType` decides.
|
|
18
|
+
*/
|
|
19
|
+
export function buildJwtKeyWarning(input: {
|
|
20
|
+
authType: 'password' | 'jwt';
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
hasSigningKey: boolean;
|
|
23
|
+
}): BuildWarning | null {
|
|
24
|
+
if (input.authType !== 'jwt' || !input.enabled || input.hasSigningKey) return null;
|
|
25
|
+
return {
|
|
26
|
+
// Reuses invalid_openapi_spec deliberately: it already routes the way this
|
|
27
|
+
// needs — emailed, counted, badged, not filed as a suggestion — and a
|
|
28
|
+
// dedicated BuildWarningType would mean editing the six-place sync chain
|
|
29
|
+
// plus EMAILABLE_TYPES for no behavioural difference. Same reasoning, and
|
|
30
|
+
// the same call, as the spec-expansion warnings in build.ts.
|
|
31
|
+
type: 'invalid_openapi_spec',
|
|
32
|
+
// docs.json is where the opt-in lives and what an author edits to change it.
|
|
33
|
+
file: 'docs.json',
|
|
34
|
+
message:
|
|
35
|
+
'auth.jwt.enabled is true but this project has no signing key, so nothing is gated and every page is publicly readable. ' +
|
|
36
|
+
'Generate an Ed25519 signing key under Settings, or set auth.jwt.enabled to false if the site is meant to be public.',
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// Extracted from app/[[...slug]]/page.tsx: Next.js's route/page typegen
|
|
2
|
+
// requires that a page.tsx export ONLY the known page-convention symbols
|
|
3
|
+
// (default, generateStaticParams, generateMetadata, dynamic, ...) — any
|
|
4
|
+
// other export fails `tsc`'s check of .next/types/app/**/page.ts with
|
|
5
|
+
// TS2344 ("does not satisfy the constraint '{ [x: string]: never }'").
|
|
6
|
+
// These helpers are used internally by generateStaticParams but also need
|
|
7
|
+
// to be unit-testable, so they live here instead and are imported (not
|
|
8
|
+
// re-exported) by page.tsx.
|
|
9
|
+
import fs from 'fs';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { getContentDir } from '@/lib/docs';
|
|
12
|
+
import { readFileSync as readMdxFile } from '@/lib/fs-readfile';
|
|
13
|
+
|
|
14
|
+
// Mirror of the CLI's detector regex in cli/src/lib/relative-mdx-imports.ts.
|
|
15
|
+
// Duplicated intentionally — build-service uses bundler module resolution
|
|
16
|
+
// and shouldn't reach into cli/src/. Keep the regex itself in sync with
|
|
17
|
+
// that file. Two intentional simplifications vs the CLI version:
|
|
18
|
+
// - no `g` flag (boolean test, not iteration)
|
|
19
|
+
// - fence stripping replaces matches with empty string instead of
|
|
20
|
+
// blank lines of equal count — line numbers don't matter here, only
|
|
21
|
+
// a yes/no skip decision (the CLI preserves them for warning text).
|
|
22
|
+
const PARENT_RELATIVE_MDX_IMPORT_RE =
|
|
23
|
+
/^[ \t]*import\s+(?:type\s+)?[\w$*{}, \n\r]+\s+from\s+["']\.{1,2}\/[^"']+\.mdx["']\s*;?/m;
|
|
24
|
+
|
|
25
|
+
const FENCED_CODE_BLOCK_RE =
|
|
26
|
+
/^( *)(```+|~~~+)[^\n]*\n([\s\S]*?)\n\1\2\s*$/gm;
|
|
27
|
+
|
|
28
|
+
// Cache for pageHasRelativeMdxImport, keyed by absolute file path,
|
|
29
|
+
// invalidated when the file's mtime changes. `generateStaticParams` runs
|
|
30
|
+
// on every nav in `jamdesk dev`, and the regex-test reads the FULL file
|
|
31
|
+
// (per fix 7205c17c). For dodo (~200 MDX × 70-94 KB) that's 14-19 MB of
|
|
32
|
+
// disk I/O per nav. Cache hit reduces it to a directory walk + statSync.
|
|
33
|
+
//
|
|
34
|
+
// In production ISR mode generateStaticParams returns [] before any of
|
|
35
|
+
// this runs (see isIsrMode early-return), so the cache is dev-only in
|
|
36
|
+
// practice. The cache is unbounded — entries for deleted files persist
|
|
37
|
+
// until process exit. For a dev-only ~200-file working set this is
|
|
38
|
+
// trivial (a few KB). If this pattern is reused in a long-running ISR
|
|
39
|
+
// context in the future, add LRU eviction or rebuild-on-walk.
|
|
40
|
+
const mdxImportCache = new Map<string, { mtimeMs: number; hasImport: boolean }>();
|
|
41
|
+
|
|
42
|
+
export function pageHasRelativeMdxImport(filePath: string, mtimeMs?: number): boolean {
|
|
43
|
+
const cached = mdxImportCache.get(filePath);
|
|
44
|
+
if (cached && mtimeMs !== undefined && cached.mtimeMs === mtimeMs) {
|
|
45
|
+
return cached.hasImport;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let hasImport: boolean;
|
|
49
|
+
try {
|
|
50
|
+
// Read the full file — MDX imports can appear at any top-level position
|
|
51
|
+
// (after a long prose intro or table of contents), and a slice was
|
|
52
|
+
// missing real imports past byte ~8192 in 70-94 KB customer pages.
|
|
53
|
+
const content = readMdxFile(filePath);
|
|
54
|
+
// Strip fenced code blocks so documentation examples (e.g.
|
|
55
|
+
// ```mdx\nimport X from "../snippets/foo.mdx";\n```) don't false-trigger.
|
|
56
|
+
hasImport = PARENT_RELATIVE_MDX_IMPORT_RE.test(content.replace(FENCED_CODE_BLOCK_RE, ''));
|
|
57
|
+
} catch {
|
|
58
|
+
hasImport = false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (mtimeMs !== undefined) {
|
|
62
|
+
mdxImportCache.set(filePath, { mtimeMs, hasImport });
|
|
63
|
+
}
|
|
64
|
+
return hasImport;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Test-only: clear the per-file MDX-import cache between test cases. */
|
|
68
|
+
export function _resetMdxImportCacheForTest(): void {
|
|
69
|
+
if (process.env.NODE_ENV === 'production') return;
|
|
70
|
+
mdxImportCache.clear();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface CollectedPaths {
|
|
74
|
+
supported: string[];
|
|
75
|
+
skipped: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function getAllDocPaths(): CollectedPaths {
|
|
79
|
+
const contentDir = getContentDir();
|
|
80
|
+
const supported: string[] = [];
|
|
81
|
+
const skipped: string[] = [];
|
|
82
|
+
|
|
83
|
+
function traverseDir(dir: string, basePath: string = '') {
|
|
84
|
+
if (!fs.existsSync(dir)) return;
|
|
85
|
+
const files = fs.readdirSync(dir);
|
|
86
|
+
for (const file of files) {
|
|
87
|
+
if (file.startsWith('.')) continue;
|
|
88
|
+
const filePath = path.join(dir, file);
|
|
89
|
+
let stat;
|
|
90
|
+
try {
|
|
91
|
+
stat = fs.statSync(filePath);
|
|
92
|
+
} catch {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (stat.isDirectory()) {
|
|
96
|
+
traverseDir(filePath, path.join(basePath, file));
|
|
97
|
+
} else if (file.endsWith('.mdx')) {
|
|
98
|
+
const slug = path.join(basePath, file.replace(/\.mdx$/, ''));
|
|
99
|
+
if (pageHasRelativeMdxImport(filePath, stat.mtimeMs)) {
|
|
100
|
+
skipped.push(slug);
|
|
101
|
+
} else {
|
|
102
|
+
supported.push(slug);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
traverseDir(contentDir);
|
|
109
|
+
return { supported, skipped };
|
|
110
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Extracted from app/api/jd/unlock/route.ts: Next.js's route typegen requires
|
|
2
|
+
// that a route.ts export ONLY the known route-convention symbols (GET, POST,
|
|
3
|
+
// runtime, dynamic, ...) — any other export fails `tsc`'s check of
|
|
4
|
+
// .next/types/app/**/route.ts with TS2344 ("does not satisfy the constraint
|
|
5
|
+
// '{ [x: string]: never }'"). This constant is used by the route handler but
|
|
6
|
+
// also needs to be assertable-against in tests, so it lives here instead.
|
|
7
|
+
|
|
8
|
+
// Audit log tag emitted on every successful master-password unlock. Exported
|
|
9
|
+
// so tests can assert exact format without duplicating the literal.
|
|
10
|
+
export const MASTER_AUDIT_TAG = '[unlock] master-password used';
|
package/vendored/next.config.js
CHANGED
|
@@ -70,6 +70,25 @@ const nextConfig = {
|
|
|
70
70
|
'../build/polyfills/polyfill-module': './lib/empty-polyfill.js',
|
|
71
71
|
'next/dist/build/polyfills/polyfill-module': './lib/empty-polyfill.js',
|
|
72
72
|
},
|
|
73
|
+
// Mirrors build-service/next.config.mjs. Turbopack has no equivalent of
|
|
74
|
+
// webpack's resolve.extensionAlias (https://github.com/vercel/next.js/issues/82945),
|
|
75
|
+
// so a relative `./foo.js` import whose real source is `./foo.ts` can fail
|
|
76
|
+
// to resolve. That is the convention every file in vendored/ uses, 120
|
|
77
|
+
// relative `.js` specifiers of it, including the lib/jwt-session.ts ->
|
|
78
|
+
// lib/public-paths-resolver.ts chain that made build-service fail
|
|
79
|
+
// deterministically. The workspace compiles the same tree, so it needs
|
|
80
|
+
// the same rule. Exempt from this file's keep-it-minimal rule: the
|
|
81
|
+
// 80s-compile warning above is about turbopack.root and resolution
|
|
82
|
+
// scope, and this is one regex pass over source text.
|
|
83
|
+
//
|
|
84
|
+
// vendored/ is copied wholesale into the workspace, so vendored/scripts
|
|
85
|
+
// lands at <workspace>/scripts and this relative path resolves. The
|
|
86
|
+
// loader is on the safeScripts allowlist in cli/scripts/vendor.js;
|
|
87
|
+
// turbopack-loader-config-drift.test.ts pins all of it together.
|
|
88
|
+
rules: {
|
|
89
|
+
'*.ts': { loaders: ['./scripts/turbopack-js-to-ts-loader.cjs'] },
|
|
90
|
+
'*.tsx': { loaders: ['./scripts/turbopack-js-to-ts-loader.cjs'] },
|
|
91
|
+
},
|
|
73
92
|
},
|
|
74
93
|
// Critical for fast builds
|
|
75
94
|
experimental: {
|
|
@@ -87,6 +106,19 @@ const nextConfig = {
|
|
|
87
106
|
'@radix-ui/react-tooltip',
|
|
88
107
|
],
|
|
89
108
|
},
|
|
109
|
+
// `jamdesk dev --webpack` is an advertised fallback (src/index.ts), and
|
|
110
|
+
// webpack needs the same `.js`-means-`.ts` mapping the Turbopack rule above
|
|
111
|
+
// provides. Without it the escape hatch fails on the imports the escape
|
|
112
|
+
// hatch exists to work around.
|
|
113
|
+
webpack: (config) => {
|
|
114
|
+
config.resolve.extensionAlias = {
|
|
115
|
+
...config.resolve.extensionAlias,
|
|
116
|
+
'.js': ['.ts', '.tsx', '.js'],
|
|
117
|
+
'.mjs': ['.mts', '.mjs'],
|
|
118
|
+
'.cjs': ['.cts', '.cjs'],
|
|
119
|
+
}
|
|
120
|
+
return config
|
|
121
|
+
},
|
|
90
122
|
}
|
|
91
123
|
|
|
92
124
|
export default nextConfig
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turbopack build-time source transform: strips the `.js` suffix off
|
|
3
|
+
* RELATIVE import/export/dynamic-import specifiers before Turbopack's own
|
|
4
|
+
* resolver sees them.
|
|
5
|
+
*
|
|
6
|
+
* Why this exists: this project's tsconfig.json sets
|
|
7
|
+
* `moduleResolution: "bundler"`, the TypeScript convention where source
|
|
8
|
+
* files are `.ts` but every relative import of them is written with a
|
|
9
|
+
* `.js` suffix (e.g. `import { x } from './foo.js'` for a file that is
|
|
10
|
+
* actually `./foo.ts` on disk). webpack supports this natively via
|
|
11
|
+
* `resolve.extensionAlias` (wired up in next.config.mjs's `webpack()`
|
|
12
|
+
* function). Turbopack has NO equivalent, documented, built-in support for
|
|
13
|
+
* this as of Next.js 16.3.x — see https://github.com/vercel/next.js/issues/82945
|
|
14
|
+
* ("Turbopack: support importing .ts/.tsx via .js extension"), which is
|
|
15
|
+
* open/unfixed upstream. In practice Turbopack resolves the VAST MAJORITY
|
|
16
|
+
* of this project's `.js`-suffixed relative imports fine through some
|
|
17
|
+
* internal inference, but was observed to throw a hard "Module not found"
|
|
18
|
+
* for specific files/subtrees (e.g. the whole lib/jwt-session.ts ->
|
|
19
|
+
* lib/public-paths-resolver.ts -> lib/{docs-types,glob-match,
|
|
20
|
+
* find-first-nav-page,language-utils}.ts chain added by the jwt docs-auth
|
|
21
|
+
* feature) — confirmed via a standalone unrs-resolver probe, and via
|
|
22
|
+
* removing the `.js` suffix by hand (which fixes resolution one hop at a
|
|
23
|
+
* time, confirming the failure is the extension-alias gap, not a real
|
|
24
|
+
* missing file). Rather than rewrite this project's whole import style
|
|
25
|
+
* (pervasive, deliberate convention — see tsconfig.json's
|
|
26
|
+
* `moduleResolution: "bundler"` and CLAUDE.md), this loader rewrites the
|
|
27
|
+
* `.js` -> (nothing) at Turbopack-compile time only. TypeScript itself,
|
|
28
|
+
* `tsc --noEmit`, vitest, and webpack builds are all UNAFFECTED (this file
|
|
29
|
+
* is wired into next.config.mjs's `turbopack.rules` only).
|
|
30
|
+
*
|
|
31
|
+
* Scoped to RELATIVE specifiers only (`./x.js`, `../x.js`) — never bare
|
|
32
|
+
* package specifiers — so a legitimate deep import of an npm package's own
|
|
33
|
+
* `.js` file (e.g. a subpath export) is never touched.
|
|
34
|
+
*
|
|
35
|
+
* Community-sourced workaround for the same open issue, adapted to be
|
|
36
|
+
* relative-imports-only (the referenced version was unscoped):
|
|
37
|
+
* https://github.com/vercel/next.js/issues/82945#issuecomment-3269958583
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
// Matches: (import|export) ... from '<spec>'; and dynamic import('<spec>')
|
|
41
|
+
// where <spec> starts with ./ or ../ and ends in .js/.jsx/.mjs, capturing
|
|
42
|
+
// the quote character so it round-trips exactly.
|
|
43
|
+
const RELATIVE_JS_SPECIFIER_RE =
|
|
44
|
+
/((?:from\s+|import\()\s*)(["'])(\.\.?\/[^"']+?)\.(m?js|jsx)\2/g;
|
|
45
|
+
|
|
46
|
+
module.exports = function stripRelativeJsExtensions(source) {
|
|
47
|
+
return source.replace(
|
|
48
|
+
RELATIVE_JS_SPECIFIER_RE,
|
|
49
|
+
(_match, prefix, quote, specPath) => `${prefix}${quote}${specPath}${quote}`,
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -82,13 +82,13 @@
|
|
|
82
82
|
}
|
|
83
83
|
},
|
|
84
84
|
"node_modules/@antfu/install-pkg": {
|
|
85
|
-
"version": "2.0
|
|
86
|
-
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-2.0.
|
|
87
|
-
"integrity": "sha512-
|
|
85
|
+
"version": "2.1.0",
|
|
86
|
+
"resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-2.1.0.tgz",
|
|
87
|
+
"integrity": "sha512-sdg9NxU3zR4Mnawfbc/x6GB5Wf17WYud5qOuEuxXjaKpYpMkISSJEjItGebXJ2bQ4DIcly4NYH23mtkGJjvKUw==",
|
|
88
88
|
"license": "MIT",
|
|
89
89
|
"dependencies": {
|
|
90
|
-
"package-manager-detector": "^1.
|
|
91
|
-
"tinyexec": "^1.
|
|
90
|
+
"package-manager-detector": "^1.8.0",
|
|
91
|
+
"tinyexec": "^1.3.1"
|
|
92
92
|
},
|
|
93
93
|
"funding": {
|
|
94
94
|
"url": "https://github.com/sponsors/antfu"
|
|
@@ -1974,9 +1974,9 @@
|
|
|
1974
1974
|
"license": "MIT"
|
|
1975
1975
|
},
|
|
1976
1976
|
"node_modules/@types/d3-selection": {
|
|
1977
|
-
"version": "3.0.
|
|
1978
|
-
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.
|
|
1979
|
-
"integrity": "sha512-
|
|
1977
|
+
"version": "3.0.12",
|
|
1978
|
+
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.12.tgz",
|
|
1979
|
+
"integrity": "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==",
|
|
1980
1980
|
"license": "MIT"
|
|
1981
1981
|
},
|
|
1982
1982
|
"node_modules/@types/d3-shape": {
|
|
@@ -2119,9 +2119,9 @@
|
|
|
2119
2119
|
}
|
|
2120
2120
|
},
|
|
2121
2121
|
"node_modules/@types/node": {
|
|
2122
|
-
"version": "26.
|
|
2123
|
-
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.
|
|
2124
|
-
"integrity": "sha512-
|
|
2122
|
+
"version": "26.6.1",
|
|
2123
|
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.1.tgz",
|
|
2124
|
+
"integrity": "sha512-VqGJBMCtdhqkBUCcBLvywI0NJ+KLuVzgNnlBUNFOQjqVxzo2lxLUNg1DSey8+u2u6ktswSAxg+s68QLzWHNOuA==",
|
|
2125
2125
|
"license": "MIT",
|
|
2126
2126
|
"dependencies": {
|
|
2127
2127
|
"undici-types": "~8.9.0"
|
|
@@ -2277,9 +2277,9 @@
|
|
|
2277
2277
|
}
|
|
2278
2278
|
},
|
|
2279
2279
|
"node_modules/autoprefixer": {
|
|
2280
|
-
"version": "10.6.
|
|
2281
|
-
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.
|
|
2282
|
-
"integrity": "sha512-
|
|
2280
|
+
"version": "10.6.1",
|
|
2281
|
+
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.1.tgz",
|
|
2282
|
+
"integrity": "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==",
|
|
2283
2283
|
"funding": [
|
|
2284
2284
|
{
|
|
2285
2285
|
"type": "opencollective",
|
|
@@ -2332,9 +2332,9 @@
|
|
|
2332
2332
|
}
|
|
2333
2333
|
},
|
|
2334
2334
|
"node_modules/baseline-browser-mapping": {
|
|
2335
|
-
"version": "2.11.
|
|
2336
|
-
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.
|
|
2337
|
-
"integrity": "sha512-
|
|
2335
|
+
"version": "2.11.24",
|
|
2336
|
+
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.24.tgz",
|
|
2337
|
+
"integrity": "sha512-hYrgxie335U08WqICoGqKRzV1HFXv6zdxwJE4ekCb80CM9a0SVVsN4QPwT67RraRo+9h8IATk6uxHJw7QSkdOg==",
|
|
2338
2338
|
"license": "Apache-2.0",
|
|
2339
2339
|
"bin": {
|
|
2340
2340
|
"baseline-browser-mapping": "dist/cli.cjs"
|
|
@@ -2344,9 +2344,9 @@
|
|
|
2344
2344
|
}
|
|
2345
2345
|
},
|
|
2346
2346
|
"node_modules/brace-expansion": {
|
|
2347
|
-
"version": "5.0.
|
|
2348
|
-
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.
|
|
2349
|
-
"integrity": "sha512-
|
|
2347
|
+
"version": "5.0.12",
|
|
2348
|
+
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
|
|
2349
|
+
"integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
|
|
2350
2350
|
"license": "MIT",
|
|
2351
2351
|
"dependencies": {
|
|
2352
2352
|
"balanced-match": "^4.0.2"
|
|
@@ -2356,9 +2356,9 @@
|
|
|
2356
2356
|
}
|
|
2357
2357
|
},
|
|
2358
2358
|
"node_modules/browserslist": {
|
|
2359
|
-
"version": "4.
|
|
2360
|
-
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.
|
|
2361
|
-
"integrity": "sha512-
|
|
2359
|
+
"version": "4.29.0",
|
|
2360
|
+
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.0.tgz",
|
|
2361
|
+
"integrity": "sha512-3GSvyjvDI4Dur1Meg2BekJquu5uF+9R9a1+5M1Mde192eZoXbeXjzgOsgqPS2V8D5wrrip0gR5Hf/GhWQ9ZzaA==",
|
|
2362
2362
|
"funding": [
|
|
2363
2363
|
{
|
|
2364
2364
|
"type": "opencollective",
|
|
@@ -2375,11 +2375,11 @@
|
|
|
2375
2375
|
],
|
|
2376
2376
|
"license": "MIT",
|
|
2377
2377
|
"dependencies": {
|
|
2378
|
-
"baseline-browser-mapping": "^2.11.
|
|
2378
|
+
"baseline-browser-mapping": "^2.11.23",
|
|
2379
2379
|
"caniuse-lite": "^1.0.30001810",
|
|
2380
|
-
"electron-to-chromium": "^1.5.
|
|
2381
|
-
"node-releases": "^2.0.
|
|
2382
|
-
"update-browserslist-db": "^1.3.
|
|
2380
|
+
"electron-to-chromium": "^1.5.427",
|
|
2381
|
+
"node-releases": "^2.0.55",
|
|
2382
|
+
"update-browserslist-db": "^1.3.3"
|
|
2383
2383
|
},
|
|
2384
2384
|
"bin": {
|
|
2385
2385
|
"browserslist": "cli.js"
|
|
@@ -3132,9 +3132,9 @@
|
|
|
3132
3132
|
"license": "MIT"
|
|
3133
3133
|
},
|
|
3134
3134
|
"node_modules/electron-to-chromium": {
|
|
3135
|
-
"version": "1.5.
|
|
3136
|
-
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.
|
|
3137
|
-
"integrity": "sha512
|
|
3135
|
+
"version": "1.5.429",
|
|
3136
|
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.429.tgz",
|
|
3137
|
+
"integrity": "sha512-/1ENIE3cx4HTIx4IfPZFaOunJmsrSVTnj6coXoRVbiJUbkeTyFkJvBeWGkdgh08OhFbxYLMT1kbkwFVSarq6Ow==",
|
|
3138
3138
|
"license": "ISC"
|
|
3139
3139
|
},
|
|
3140
3140
|
"node_modules/empathic": {
|
|
@@ -3365,9 +3365,9 @@
|
|
|
3365
3365
|
"license": "MIT"
|
|
3366
3366
|
},
|
|
3367
3367
|
"node_modules/fast-uri": {
|
|
3368
|
-
"version": "3.1.
|
|
3369
|
-
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.
|
|
3370
|
-
"integrity": "sha512-
|
|
3368
|
+
"version": "3.1.8",
|
|
3369
|
+
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz",
|
|
3370
|
+
"integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==",
|
|
3371
3371
|
"funding": [
|
|
3372
3372
|
{
|
|
3373
3373
|
"type": "github",
|