jamdesk 1.1.223 → 1.1.225

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jamdesk",
3
- "version": "1.1.223",
3
+ "version": "1.1.225",
4
4
  "description": "CLI for Jamdesk — build, preview, and deploy documentation sites from MDX. Dev server with hot reload, 50+ components, OpenAPI support, AI search, and Mintlify migration",
5
5
  "keywords": [
6
6
  "jamdesk",
@@ -37,6 +37,8 @@
37
37
  * live documentation sites.
38
38
  */
39
39
 
40
+ import { transform } from '@babel/standalone';
41
+
40
42
  /** Name the runtime key guard is bound to inside the generated function. */
41
43
  export const KEY_GUARD_NAME = '__jdKey';
42
44
 
@@ -222,7 +224,11 @@ export function mdxEvalGuardPlugin({ types: t }: { types: any }) {
222
224
  // getBinding() — not hasBinding() — because hasBinding also reports
223
225
  // true for names merely *collected* as program globals, which is
224
226
  // exactly the case we need to reject.
225
- if (path.scope.getBinding(name)) return;
227
+ //
228
+ // An import binding does not count: the snippet compiler strips import
229
+ // lines after transpile, so at runtime the name is the host global.
230
+ const binding = path.scope.getBinding(name);
231
+ if (binding && binding.kind !== 'module') return;
226
232
  reject(`"${name}"`);
227
233
  },
228
234
 
@@ -322,3 +328,183 @@ export function mdxEvalGuardPlugin({ types: t }: { types: any }) {
322
328
  },
323
329
  };
324
330
  }
331
+
332
+ const PARAM_NAME = /^[A-Za-z_$][\w$]*$/;
333
+
334
+ /**
335
+ * The ONLY place tenant code becomes a function. It checks the exact source
336
+ * it is about to compile, then compiles that same string.
337
+ *
338
+ * Why a second check on top of mdxEvalGuardPlugin: the plugin runs on the
339
+ * author's source, and later steps change that code before it runs (the
340
+ * TypeScript preset erases `declare` statements, the snippet compiler strips
341
+ * import lines and rewrites exports). Scope facts from the earlier pass, like
342
+ * "this name is declared locally", need not hold for the code that runs.
343
+ * This check parses the final JavaScript, applies no transforms, and trusts
344
+ * nothing from the earlier pass. It rejects instead of rewriting, so the
345
+ * runtime key guard must already wrap every dynamic key.
346
+ */
347
+ export function compileGuardedFunction(
348
+ paramNames: string[],
349
+ body: string
350
+ ): (...args: unknown[]) => unknown {
351
+ const source = `${STRICT_MODE_PROLOGUE}\n${body}`;
352
+ assertExecutedCodeIsGuarded(paramNames, source);
353
+ // NOTE: new Function() is intentionally used here to compile tenant MDX
354
+ // components and snippets; `source` is exactly what was checked above.
355
+ return new Function(...paramNames, source) as (...args: unknown[]) => unknown;
356
+ }
357
+
358
+ function assertExecutedCodeIsGuarded(paramNames: string[], source: string): void {
359
+ for (const name of paramNames) {
360
+ if (!PARAM_NAME.test(name)) reject(`Parameter name "${name}"`);
361
+ }
362
+ if (!paramNames.includes(KEY_GUARD_NAME)) reject('Code without the key guard');
363
+
364
+ const wrapped = `(function (${paramNames.join(', ')}) {\n${source}\n});`;
365
+ try {
366
+ transform(wrapped, {
367
+ babelrc: false,
368
+ configFile: false,
369
+ code: false,
370
+ ast: false,
371
+ sourceType: 'script',
372
+ plugins: [executedCodeCheckPlugin(paramNames.length)],
373
+ });
374
+ } catch (error) {
375
+ if (error instanceof MdxEvalGuardError) throw error;
376
+ // Babel could not parse what new Function is about to compile. Refuse it
377
+ // rather than run code nothing has checked.
378
+ reject('Code that could not be checked');
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Check-only twin of mdxEvalGuardPlugin, for the final JavaScript. Same deny
384
+ * lists, but every rule that rewrote there rejects here.
385
+ */
386
+ function executedCodeCheckPlugin(paramCount: number) {
387
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
388
+ return function plugin(): any {
389
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
390
+ let wrapper: any = null;
391
+
392
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
393
+ const isGuardCall = (node: any) =>
394
+ node?.type === 'CallExpression' &&
395
+ node.callee?.type === 'Identifier' &&
396
+ node.callee.name === KEY_GUARD_NAME &&
397
+ node.arguments?.length === 1 &&
398
+ node.arguments[0].type !== 'SpreadElement';
399
+
400
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
401
+ const checkComputedKey = (key: any) => {
402
+ if (key?.type === 'StringLiteral') {
403
+ if (FORBIDDEN_PROPERTIES.has(key.value)) reject(`Property "${key.value}"`);
404
+ return;
405
+ }
406
+ if (key?.type === 'NumericLiteral') return;
407
+ if (isGuardCall(key)) return;
408
+ reject('Unguarded dynamic property access');
409
+ };
410
+
411
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
412
+ const checkMember = (path: any) => {
413
+ const { node } = path;
414
+ if (!node.computed) {
415
+ if (node.property?.type === 'Identifier' &&
416
+ FORBIDDEN_PROPERTIES.has(node.property.name)) {
417
+ reject(`Property "${node.property.name}"`);
418
+ }
419
+ return;
420
+ }
421
+ checkComputedKey(node.property);
422
+ };
423
+
424
+ return {
425
+ visitor: {
426
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
427
+ Program(path: any) {
428
+ // The source must parse as exactly the one wrapper function, so
429
+ // text that closes the wrapper early is refused, not checked as
430
+ // something new Function would never see.
431
+ const body = path.node.body;
432
+ const expr = body.length === 1 && body[0].type === 'ExpressionStatement'
433
+ ? body[0].expression : null;
434
+ if (expr?.type !== 'FunctionExpression' ||
435
+ expr.params.length !== paramCount ||
436
+ (path.node.directives?.length ?? 0) > 0) {
437
+ reject('Code that could not be checked');
438
+ }
439
+ wrapper = expr;
440
+ },
441
+
442
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
443
+ Identifier(path: any) {
444
+ const name = path.node.name;
445
+
446
+ if (name === KEY_GUARD_NAME) {
447
+ if (path.parentPath?.node === wrapper && path.listKey === 'params') return;
448
+ const call = path.parentPath;
449
+ if (call?.isCallExpression() && call.node.callee === path.node &&
450
+ isGuardCall(call.node)) {
451
+ const owner = call.parentPath;
452
+ const asMemberKey =
453
+ (owner?.isMemberExpression() || owner?.isOptionalMemberExpression()) &&
454
+ owner.node.computed && owner.node.property === call.node;
455
+ const asPatternKey =
456
+ owner?.isObjectProperty() && owner.node.computed &&
457
+ owner.node.key === call.node && owner.parentPath?.isObjectPattern();
458
+ const binding = path.scope.getBinding(name);
459
+ if ((asMemberKey || asPatternKey) && binding?.kind === 'param' &&
460
+ binding.scope.block === wrapper) {
461
+ return;
462
+ }
463
+ }
464
+ reject(`"${KEY_GUARD_NAME}" outside a guarded key`);
465
+ }
466
+
467
+ if (!FORBIDDEN_GLOBALS.has(name)) return;
468
+ if (!path.isReferencedIdentifier() && !path.isBindingIdentifier()) return;
469
+ const binding = path.scope.getBinding(name);
470
+ if (binding && binding.kind !== 'module') return;
471
+ reject(`"${name}"`);
472
+ },
473
+
474
+ MemberExpression: checkMember,
475
+ OptionalMemberExpression: checkMember,
476
+
477
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
478
+ ObjectProperty(path: any) {
479
+ const { node } = path;
480
+ const isPattern = path.parentPath?.isObjectPattern() === true;
481
+ if (node.computed) {
482
+ if (isPattern) checkComputedKey(node.key);
483
+ return;
484
+ }
485
+ const name = node.key?.type === 'Identifier' ? node.key.name :
486
+ node.key?.type === 'StringLiteral' ? node.key.value : null;
487
+ if (name === null) return;
488
+ if (isPattern ? FORBIDDEN_PROPERTIES.has(name) : name === '__proto__') {
489
+ reject(`Property "${name}"`);
490
+ }
491
+ },
492
+
493
+ ImportExpression() {
494
+ reject('Dynamic import()');
495
+ },
496
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
497
+ CallExpression(path: any) {
498
+ if (path.node.callee?.type === 'Import') reject('Dynamic import()');
499
+ },
500
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
501
+ MetaProperty(path: any) {
502
+ if (path.node.meta?.name === 'import') reject('import.meta');
503
+ },
504
+ WithStatement() {
505
+ reject('with statements');
506
+ },
507
+ },
508
+ };
509
+ };
510
+ }
@@ -30,7 +30,7 @@ import {
30
30
  mdxEvalGuardPlugin,
31
31
  guardPropertyKey,
32
32
  KEY_GUARD_NAME,
33
- STRICT_MODE_PROLOGUE,
33
+ compileGuardedFunction,
34
34
  } from './mdx-eval-guard';
35
35
 
36
36
  interface InlineExports {
@@ -130,24 +130,17 @@ export function compileInlineComponents(
130
130
 
131
131
  // Step 3: Create and evaluate the component
132
132
  // The component has access to React, jsx runtime, AND built-in MDX components
133
- // NOTE: new Function() is intentionally used here to compile user-provided MDX
134
- // This is similar to how MDX itself works - user content becomes executable code
133
+ // compileGuardedFunction checks the final JavaScript, then compiles it with
134
+ // new Function. This is similar to how MDX itself works - user content becomes executable code
135
135
  // Babel's automatic runtime uses _jsx/_jsxs as variable names (with underscore)
136
136
  //
137
137
  // IMPORTANT: React hooks are intentionally NOT provided here. Inline components
138
138
  // are server-rendered during Next.js static export, and hooks only work client-side.
139
139
  // Users who need hooks should use snippet files with 'use client' directive instead.
140
140
 
141
- const createComponent = new Function(
142
- 'React',
143
- '_jsx',
144
- '_jsxs',
145
- '_Fragment',
146
- KEY_GUARD_NAME, // runtime half of mdxEvalGuardPlugin — see mdx-eval-guard.ts
147
- ...builtInNames, // Note, Card, Icon, Tip, Warning, etc.
148
- // STRICT_MODE_PROLOGUE is load-bearing, not hygiene: without it this
149
- // body is sloppy-mode and `this` is globalThis. See mdx-eval-guard.ts.
150
- `${STRICT_MODE_PROLOGUE}\nreturn ${jsFunctionBody}`
141
+ const createComponent = compileGuardedFunction(
142
+ ['React', '_jsx', '_jsxs', '_Fragment', KEY_GUARD_NAME, ...builtInNames],
143
+ `return ${jsFunctionBody}`
151
144
  );
152
145
 
153
146
  const component = createComponent(
@@ -159,7 +152,7 @@ export function compileInlineComponents(
159
152
  ...builtInNames.map((n) => builtInComponents[n]) // Pass actual components
160
153
  );
161
154
 
162
- components[name] = component;
155
+ components[name] = component as AnyComponent;
163
156
  } catch (error) {
164
157
  const errorMessage = error instanceof Error ? error.message : String(error);
165
158
  errors.push(`Failed to compile inline component "${name}": ${errorMessage}`);
@@ -24,8 +24,8 @@ import {
24
24
  mdxEvalGuardPlugin,
25
25
  guardPropertyKey,
26
26
  KEY_GUARD_NAME,
27
- STRICT_MODE_PROLOGUE,
28
27
  MdxEvalGuardError,
28
+ compileGuardedFunction,
29
29
  } from './mdx-eval-guard';
30
30
  import { extractUtilityCandidates, MAX_UTILITY_CANDIDATES } from './user-utility-css';
31
31
  import { logger } from '../shared/logger';
@@ -355,17 +355,9 @@ function evaluateSnippetCode(
355
355
  builtInNames: string[],
356
356
  builtInValues: AnyComponent[]
357
357
  ): unknown {
358
- // NOTE: new Function() is intentionally used here to compile user-provided snippets
359
- const fn = new Function(
360
- 'React',
361
- '_jsx',
362
- '_jsxs',
363
- '_Fragment',
364
- KEY_GUARD_NAME, // runtime half of mdxEvalGuardPlugin — see mdx-eval-guard.ts
365
- ...builtInNames,
366
- // STRICT_MODE_PROLOGUE is load-bearing, not hygiene: without it this
367
- // body is sloppy-mode and `this` is globalThis. See mdx-eval-guard.ts.
368
- `${STRICT_MODE_PROLOGUE}\n${code}`
358
+ const fn = compileGuardedFunction(
359
+ ['React', '_jsx', '_jsxs', '_Fragment', KEY_GUARD_NAME, ...builtInNames],
360
+ code
369
361
  );
370
362
  return fn(React, jsxRuntime.jsx, jsxRuntime.jsxs, jsxRuntime.Fragment, guardPropertyKey, ...builtInValues);
371
363
  }