veryfront 0.1.241 → 0.1.243

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.
Files changed (56) hide show
  1. package/esm/cli/templates/manifest.js +77 -77
  2. package/esm/deno.js +1 -1
  3. package/esm/src/agent/conversation-root-run-context.d.ts.map +1 -1
  4. package/esm/src/agent/conversation-root-run-context.js +2 -0
  5. package/esm/src/agent/conversation-run-context.d.ts +2 -0
  6. package/esm/src/agent/conversation-run-context.d.ts.map +1 -1
  7. package/esm/src/agent/durable.d.ts +23 -0
  8. package/esm/src/agent/durable.d.ts.map +1 -1
  9. package/esm/src/agent/durable.js +39 -0
  10. package/esm/src/agent/index.d.ts +1 -1
  11. package/esm/src/agent/index.d.ts.map +1 -1
  12. package/esm/src/agent/index.js +1 -1
  13. package/esm/src/oauth/handlers/callback-handler.d.ts +2 -2
  14. package/esm/src/oauth/handlers/callback-handler.d.ts.map +1 -1
  15. package/esm/src/oauth/handlers/callback-handler.js +17 -5
  16. package/esm/src/oauth/handlers/init-handler.d.ts +24 -4
  17. package/esm/src/oauth/handlers/init-handler.d.ts.map +1 -1
  18. package/esm/src/oauth/handlers/init-handler.js +47 -10
  19. package/esm/src/oauth/providers/base.d.ts +9 -2
  20. package/esm/src/oauth/providers/base.d.ts.map +1 -1
  21. package/esm/src/oauth/providers/base.js +12 -5
  22. package/esm/src/oauth/token-store/index.d.ts +1 -1
  23. package/esm/src/oauth/token-store/index.d.ts.map +1 -1
  24. package/esm/src/oauth/token-store/memory.d.ts +21 -9
  25. package/esm/src/oauth/token-store/memory.d.ts.map +1 -1
  26. package/esm/src/oauth/token-store/memory.js +42 -28
  27. package/esm/src/oauth/types.d.ts +33 -7
  28. package/esm/src/oauth/types.d.ts.map +1 -1
  29. package/esm/src/platform/compat/framework-source-resolver.d.ts.map +1 -1
  30. package/esm/src/platform/compat/framework-source-resolver.js +34 -0
  31. package/esm/src/routing/api/module-loader/loader.d.ts +11 -0
  32. package/esm/src/routing/api/module-loader/loader.d.ts.map +1 -1
  33. package/esm/src/routing/api/module-loader/loader.js +18 -2
  34. package/esm/src/server/handlers/dev/dashboard/api.d.ts.map +1 -1
  35. package/esm/src/server/handlers/dev/dashboard/api.js +34 -13
  36. package/esm/src/server/handlers/dev/files/esbuild-plugins.d.ts.map +1 -1
  37. package/esm/src/server/handlers/dev/files/esbuild-plugins.js +45 -4
  38. package/esm/src/utils/version-constant.d.ts +1 -1
  39. package/esm/src/utils/version-constant.js +1 -1
  40. package/package.json +1 -1
  41. package/src/cli/templates/manifest.js +77 -77
  42. package/src/deno.js +1 -1
  43. package/src/src/agent/conversation-root-run-context.ts +2 -0
  44. package/src/src/agent/durable.ts +60 -0
  45. package/src/src/agent/index.ts +3 -0
  46. package/src/src/oauth/handlers/callback-handler.ts +25 -8
  47. package/src/src/oauth/handlers/init-handler.ts +83 -15
  48. package/src/src/oauth/providers/base.ts +12 -5
  49. package/src/src/oauth/token-store/index.ts +1 -1
  50. package/src/src/oauth/token-store/memory.ts +48 -35
  51. package/src/src/oauth/types.ts +34 -7
  52. package/src/src/platform/compat/framework-source-resolver.ts +32 -0
  53. package/src/src/routing/api/module-loader/loader.ts +18 -2
  54. package/src/src/server/handlers/dev/dashboard/api.ts +32 -10
  55. package/src/src/server/handlers/dev/files/esbuild-plugins.ts +54 -5
  56. package/src/src/utils/version-constant.ts +1 -1
@@ -1,5 +1,12 @@
1
- /** How long an OAuth state nonce remains valid (10 minutes). */
2
- const STATE_EXPIRATION_MS = 10 * 60 * 1_000;
1
+ /** State expiry window: reject any state older than this (10 minutes). */
2
+ const STATE_EXPIRY_MS = 10 * 60 * 1_000;
3
+ /**
4
+ * In-memory TokenStore keyed by `(serviceId, userId)`.
5
+ *
6
+ * Suitable for development and tests. For production use a persistent store
7
+ * (Redis, Postgres, ...) keyed the same way. Never share a single slot per
8
+ * service across users — see VULN-AUTH-2.
9
+ */
3
10
  export class MemoryTokenStore {
4
11
  tokens = new Map();
5
12
  states = new Map();
@@ -7,48 +14,55 @@ export class MemoryTokenStore {
7
14
  constructor(projectId = "default") {
8
15
  this.projectId = projectId;
9
16
  }
10
- scopedKey(serviceId) {
11
- return `${this.projectId}:${serviceId}`;
17
+ scopedKey(serviceId, userId) {
18
+ return `${this.projectId}:${serviceId}:${userId}`;
12
19
  }
13
- async getTokens(serviceId) {
14
- return this.tokens.get(this.scopedKey(serviceId)) ?? null;
20
+ getTokens(serviceId, userId) {
21
+ return Promise.resolve(this.tokens.get(this.scopedKey(serviceId, userId)) ?? null);
15
22
  }
16
- async setTokens(serviceId, tokens) {
17
- this.tokens.set(this.scopedKey(serviceId), tokens);
23
+ setTokens(serviceId, userId, tokens) {
24
+ this.tokens.set(this.scopedKey(serviceId, userId), tokens);
25
+ return Promise.resolve();
18
26
  }
19
- async clearTokens(serviceId) {
20
- this.tokens.delete(this.scopedKey(serviceId));
27
+ clearTokens(serviceId, userId) {
28
+ this.tokens.delete(this.scopedKey(serviceId, userId));
29
+ return Promise.resolve();
21
30
  }
22
- async getState(state) {
23
- const storedState = this.states.get(state);
24
- if (!storedState)
25
- return null;
26
- if (Date.now() - storedState.createdAt > STATE_EXPIRATION_MS) {
27
- this.states.delete(state);
28
- return null;
29
- }
30
- return storedState;
31
- }
32
- async setState(storedState) {
33
- this.states.set(storedState.state, storedState);
31
+ setState(state, meta) {
32
+ this.states.set(state, meta);
34
33
  this.cleanupExpiredStates();
34
+ return Promise.resolve();
35
35
  }
36
- async clearState(state) {
36
+ /**
37
+ * Atomically read and delete state (one-shot). Returns null for unknown or
38
+ * expired entries. Expired entries are removed on read.
39
+ */
40
+ consumeState(state) {
41
+ const meta = this.states.get(state);
42
+ if (!meta)
43
+ return Promise.resolve(null);
37
44
  this.states.delete(state);
45
+ if (Date.now() - meta.createdAt > STATE_EXPIRY_MS) {
46
+ return Promise.resolve(null);
47
+ }
48
+ return Promise.resolve(meta);
38
49
  }
39
50
  cleanupExpiredStates() {
40
51
  const now = Date.now();
41
- for (const [state, storedState] of this.states) {
42
- if (now - storedState.createdAt > STATE_EXPIRATION_MS) {
52
+ for (const [state, meta] of this.states) {
53
+ if (now - meta.createdAt > STATE_EXPIRY_MS) {
43
54
  this.states.delete(state);
44
55
  }
45
56
  }
46
57
  }
58
+ /** List connected slots as `${serviceId}:${userId}` strings (test/debug aid). */
47
59
  getConnectedServices() {
48
- return [...this.tokens.keys()];
60
+ const prefix = `${this.projectId}:`;
61
+ return [...this.tokens.keys()].map((key) => key.startsWith(prefix) ? key.slice(prefix.length) : key);
49
62
  }
50
- isConnected(serviceId) {
51
- const tokens = this.tokens.get(this.scopedKey(serviceId));
63
+ /** Whether a given user has usable tokens for a service. */
64
+ isConnected(serviceId, userId) {
65
+ const tokens = this.tokens.get(this.scopedKey(serviceId, userId));
52
66
  if (!tokens)
53
67
  return false;
54
68
  const isExpired = tokens.expiresAt != null && Date.now() > tokens.expiresAt;
@@ -1,11 +1,37 @@
1
1
  export type { AuthorizationUrlOptions, OAuthProviderConfig, OAuthServiceConfig, OAuthState, OAuthTokens, TokenExchangeOptions, TokenExchangeResult, } from "./schemas/index.js";
2
- import type { OAuthState, OAuthTokens } from "./schemas/index.js";
2
+ import type { OAuthTokens } from "./schemas/index.js";
3
+ /**
4
+ * Persisted OAuth state row. Created when init handler starts a flow and
5
+ * consumed exactly once by the callback handler.
6
+ *
7
+ * `userId` binds the flow to the authenticated user who initiated it so the
8
+ * resulting tokens are stored in that user's slot (not a shared one).
9
+ */
10
+ export interface StoredOAuthState {
11
+ userId: string;
12
+ serviceId: string;
13
+ codeVerifier?: string;
14
+ redirectUri?: string;
15
+ scopes?: string[];
16
+ createdAt: number;
17
+ metadata?: Record<string, unknown>;
18
+ }
19
+ /**
20
+ * TokenStore is keyed by `(serviceId, userId)` — tokens are per-user.
21
+ *
22
+ * Using a single-slot-per-service store is a vulnerability: the last OAuth
23
+ * completion overwrites all others, so an attacker who starts and finishes
24
+ * an OAuth flow with their own account can cause server-side code to act
25
+ * on the attacker's account. Callers MUST pass `userId` from authenticated
26
+ * session context.
27
+ */
3
28
  export interface TokenStore {
4
- getTokens(serviceId: string): Promise<OAuthTokens | null>;
5
- setTokens(serviceId: string, tokens: OAuthTokens): Promise<void>;
6
- clearTokens(serviceId: string): Promise<void>;
7
- getState(state: string): Promise<OAuthState | null>;
8
- setState(state: OAuthState): Promise<void>;
9
- clearState(state: string): Promise<void>;
29
+ getTokens(serviceId: string, userId: string): Promise<OAuthTokens | null>;
30
+ setTokens(serviceId: string, userId: string, tokens: OAuthTokens): Promise<void>;
31
+ clearTokens(serviceId: string, userId: string): Promise<void>;
32
+ /** Persist a new OAuth state row for the initiating user. */
33
+ setState(state: string, meta: StoredOAuthState): Promise<void>;
34
+ /** Atomically read and delete state. Returns null if unknown/expired. */
35
+ consumeState(state: string): Promise<StoredOAuthState | null>;
10
36
  }
11
37
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/src/oauth/types.ts"],"names":[],"mappings":"AACA,YAAY,EACV,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,EAClB,UAAU,EACV,WAAW,EACX,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAElE,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC1D,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9C,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACpD,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1C"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/src/oauth/types.ts"],"names":[],"mappings":"AACA,YAAY,EACV,uBAAuB,EACvB,mBAAmB,EACnB,kBAAkB,EAClB,UAAU,EACV,WAAW,EACX,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEtD;;;;;;GAMG;AACH,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,UAAU;IACzB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAAC;IAC1E,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjF,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9D,6DAA6D;IAC7D,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/D,yEAAyE;IACzE,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAAC,CAAC;CAC/D"}
@@ -1 +1 @@
1
- {"version":3,"file":"framework-source-resolver.d.ts","sourceRoot":"","sources":["../../../../src/src/platform/compat/framework-source-resolver.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAIpD,eAAO,MAAM,cAAc,QAA4C,CAAC;AACxE,eAAO,MAAM,iBAAiB,QAA8B,CAAC;AAC7D,eAAO,MAAM,0BAA0B,QAAgD,CAAC;AAExF,eAAO,MAAM,mCAAmC,6HAatC,CAAC;AAEX,MAAM,WAAW,yBAAyB;IACxC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iCAAiC;IAChD,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,2CAA2C;IAC1D,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,wBAAgB,4BAA4B,CAAC,eAAe,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CAarF;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAG3D;AAuCD,wBAAsB,0BAA0B,CAC9C,sBAAsB,EAAE,MAAM,EAC9B,OAAO,GAAE,iCAAsC,GAC9C,OAAO,CAAC,2BAA2B,GAAG,IAAI,CAAC,CA+B7C;AAED,wBAAsB,oCAAoC,CACxD,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE,2CAAgD,GACxD,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA6CxB"}
1
+ {"version":3,"file":"framework-source-resolver.d.ts","sourceRoot":"","sources":["../../../../src/src/platform/compat/framework-source-resolver.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AA2BpD,eAAO,MAAM,cAAc,QAA4C,CAAC;AACxE,eAAO,MAAM,iBAAiB,QAA8B,CAAC;AAC7D,eAAO,MAAM,0BAA0B,QAAgD,CAAC;AAExF,eAAO,MAAM,mCAAmC,6HAatC,CAAC;AAEX,MAAM,WAAW,yBAAyB;IACxC,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACvC;AAED,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iCAAiC;IAChD,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,MAAM,WAAW,2CAA2C;IAC1D,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED,wBAAgB,4BAA4B,CAAC,eAAe,GAAE,MAAM,EAAO,GAAG,MAAM,EAAE,CAarF;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAG3D;AAuCD,wBAAsB,0BAA0B,CAC9C,sBAAsB,EAAE,MAAM,EAC9B,OAAO,GAAE,iCAAsC,GAC9C,OAAO,CAAC,2BAA2B,GAAG,IAAI,CAAC,CAwC7C;AAED,wBAAsB,oCAAoC,CACxD,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE,2CAAgD,GACxD,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CA6CxB"}
@@ -1,6 +1,31 @@
1
1
  import { join } from "./path/index.js";
2
+ import { isWithinDirectory } from "../../security/path-validation.js";
2
3
  import { createFileSystem } from "./fs.js";
3
4
  import { getFrameworkRoot, getFrameworkRootFromMeta } from "./vfs-paths.js";
5
+ /**
6
+ * Reject candidate paths that contain traversal indicators — plain `..`,
7
+ * NUL, or any percent-encoded variant (including multiply-encoded forms such
8
+ * as `%252e` or `%25252e`). The public `/_vf_modules/...` route reaches this
9
+ * resolver, so a malicious basePath like
10
+ * `_veryfront/%2e%2e%2fsecret.ts` would otherwise be joined with the
11
+ * framework lookupDir and escape it.
12
+ */
13
+ function hasDangerousSegments(candidate) {
14
+ if (candidate.includes("\0"))
15
+ return true;
16
+ // Plain-text traversal (post URL-decode).
17
+ if (/(^|[/\\])\.\.([/\\]|$)/.test(candidate))
18
+ return true;
19
+ // Any occurrence of a percent sign is treated as suspicious: this resolver
20
+ // is called with inputs taken from URL path segments which have already
21
+ // been decoded once upstream. A lingering `%` means the attacker
22
+ // double-encoded the input, or that decoding missed a sequence — either
23
+ // way, refuse to probe the filesystem. Framework source paths never
24
+ // legitimately contain `%`.
25
+ if (candidate.includes("%"))
26
+ return true;
27
+ return false;
28
+ }
4
29
  export const FRAMEWORK_ROOT = getFrameworkRootFromMeta(globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url);
5
30
  export const FRAMEWORK_SRC_DIR = join(FRAMEWORK_ROOT, "src");
6
31
  export const FRAMEWORK_EMBEDDED_SRC_DIR = join(FRAMEWORK_ROOT, "dist", "framework-src");
@@ -67,6 +92,11 @@ async function findExistingFrameworkCandidate(candidatePath, options = {}) {
67
92
  return null;
68
93
  }
69
94
  export async function resolveFrameworkSourcePath(relativePathWithoutExt, options = {}) {
95
+ // VULN-FS-3: Reject any candidate containing traversal indicators
96
+ // (plain or percent-encoded) before joining with the framework lookup dir.
97
+ // The public /_vf_modules/... route reaches this function with user input.
98
+ if (hasDangerousSegments(relativePathWithoutExt))
99
+ return null;
70
100
  const fs = options.fileSystem ?? createFileSystem();
71
101
  const lookupDirs = getFrameworkSourceLookupDirs(options.extraLookupDirs);
72
102
  const extensions = options.extensions ?? DEFAULT_FRAMEWORK_SOURCE_EXTENSIONS;
@@ -78,6 +108,10 @@ export async function resolveFrameworkSourcePath(relativePathWithoutExt, options
78
108
  for (const candidate of candidates) {
79
109
  for (const ext of extensions) {
80
110
  const candidatePath = join(lookupDir, candidate + ext);
111
+ // Defence in depth: even if the candidate passed the textual gate
112
+ // above, confirm the joined path is physically within the lookup dir.
113
+ if (!isWithinDirectory(lookupDir, candidatePath))
114
+ continue;
81
115
  try {
82
116
  const stat = await fs.stat(candidatePath);
83
117
  if (stat.isFile) {
@@ -1,6 +1,17 @@
1
1
  import type { APIRoute, LoadModuleOptions } from "./types.js";
2
2
  import type { FileSystem } from "../../../platform/compat/fs.js";
3
3
  export { toCjsDestructureBindings } from "./loader-helpers.js";
4
+ /**
5
+ * Generates a CJS module loader shim for compiled Deno binaries.
6
+ *
7
+ * In compiled binaries, `createRequire()` can resolve module paths and load
8
+ * built-in modules (fs, path, etc.), but cannot load CJS files from disk
9
+ * (loadMaybeCjs fails with "path not found"). This shim works around that
10
+ * limitation by using `Deno.readTextFileSync` to read CJS files and
11
+ * `new Function` to evaluate them in a proper CJS wrapper with require,
12
+ * exports, module, __filename, and __dirname bindings.
13
+ */
14
+ export declare function generateCompiledBinaryRequireShim(projectDir: string): string;
4
15
  export declare function loadHandlerModule(options: LoadModuleOptions): Promise<APIRoute | null>;
5
16
  export declare function getNodeExternalPackagesToResolve(userDeps: Map<string, string>): string[];
6
17
  export declare function resolveNodePackageToFileUrl(projectDir: string, packageName: string, fs: FileSystem, pathToFileURL: typeof import("node:url").pathToFileURL): Promise<string | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../../../src/src/routing/api/module-loader/loader.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAI9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAkBjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAgI/D,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAyBtF;AAuaD,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,CAUxF;AAED,wBAAsB,2BAA2B,CAC/C,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,UAAU,EACd,aAAa,EAAE,cAAc,UAAU,EAAE,aAAa,GACrD,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoBxB;AAED,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,UAAU,GACb,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAW9C;AAED,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,UAAU,EACd,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,OAAO,CAAC,MAAM,CAAC,CA6EjB;AAED,wBAAgB,qCAAqC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqB1E;AAED,wBAAgB,0CAA0C,CACxD,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,MAAM,CAoDR;AAED,wBAAsB,+BAA+B,CACnD,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,UAAU,EACd,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,OAAO,CAAC,MAAM,CAAC,CA0BjB;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgBlE"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../../../src/src/routing/api/module-loader/loader.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAI9D,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AAkBjE,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAqD/D;;;;;;;;;GASG;AACH,wBAAgB,iCAAiC,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA+E5E;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAyBtF;AAuaD,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,EAAE,CAUxF;AAED,wBAAsB,2BAA2B,CAC/C,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,EACnB,EAAE,EAAE,UAAU,EACd,aAAa,EAAE,cAAc,UAAU,EAAE,aAAa,GACrD,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAoBxB;AAED,wBAAsB,uBAAuB,CAC3C,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,UAAU,GACb,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAW9C;AAED,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,UAAU,EACd,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,OAAO,CAAC,MAAM,CAAC,CA6EjB;AAED,wBAAgB,qCAAqC,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAqB1E;AAED,wBAAgB,0CAA0C,CACxD,IAAI,EAAE,MAAM,EACZ,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,MAAM,CAoDR;AAED,wBAAsB,+BAA+B,CACnD,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,UAAU,EACd,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,OAAO,CAAC,MAAM,CAAC,CA0BjB;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAgBlE"}
@@ -73,7 +73,7 @@ async function readProjectDependencies(projectDir, fs) {
73
73
  * `new Function` to evaluate them in a proper CJS wrapper with require,
74
74
  * exports, module, __filename, and __dirname bindings.
75
75
  */
76
- function generateCompiledBinaryRequireShim(projectDir) {
76
+ export function generateCompiledBinaryRequireShim(projectDir) {
77
77
  const builtinSet = JSON.stringify(NODE_BUILTINS);
78
78
  const safeProjectDir = JSON.stringify(projectDir + "/package.json");
79
79
  const safeProjectRoot = JSON.stringify(pathHelper.resolve(projectDir));
@@ -83,6 +83,12 @@ import { dirname as __vf_dirname, resolve as __vf_resolve } from "node:path";
83
83
  var __vf_builtinRequire = __vf_createRequire(${safeProjectDir});
84
84
  var __vf_builtinSet = new Set(${builtinSet});
85
85
  var __vf_projectRoot = ${safeProjectRoot};
86
+ // VULN-FS-5: Canonicalize the project root so containment checks using
87
+ // Deno.realPathSync(resolved) compare canonical-vs-canonical. Without this,
88
+ // when the project itself is opened via a symlink, the realpath'd resolved
89
+ // module path has a different prefix than the non-canonical projectRoot and
90
+ // legitimate dependencies would be rejected.
91
+ try { __vf_projectRoot = Deno.realPathSync(__vf_projectRoot); } catch (_) { /* expected: projectRoot may not exist at shim init in some environments */ }
86
92
  var __vf_cache = Object.create(null);
87
93
  function __vf_assertContained(resolved) {
88
94
  var norm = __vf_resolve(resolved).replace(/\\\\/g, "/");
@@ -105,10 +111,20 @@ function __vf_loadCjs(id, parentDir) {
105
111
  try { Deno.statSync(resolved + exts[i]); resolved += exts[i]; break; } catch (_) { /* expected: probing file extensions */ }
106
112
  }
107
113
  }
108
- __vf_assertContained(resolved);
109
114
  } else {
110
115
  resolved = __vf_builtinRequire.resolve(id);
111
116
  }
117
+ // VULN-FS-5: Always assert containment after resolution (both branches),
118
+ // then re-canonicalize via realPathSync to resist symlinked node_modules
119
+ // entries that could point outside the project root.
120
+ __vf_assertContained(resolved);
121
+ try {
122
+ var real = Deno.realPathSync(resolved);
123
+ __vf_assertContained(real);
124
+ resolved = real;
125
+ } catch (_) {
126
+ /* expected: realPathSync fails for non-existent paths — assertContained above already held */
127
+ }
112
128
  if (resolved in __vf_cache) return __vf_cache[resolved];
113
129
  var code = Deno.readTextFileSync(resolved);
114
130
  if (resolved.endsWith(".json")) {
@@ -1 +1 @@
1
- {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../../../../src/src/server/handlers/dev/dashboard/api.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AA2CrD,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,QAAQ,GAAG,IAAI,CAgE5C"}
1
+ {"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../../../../src/src/server/handlers/dev/dashboard/api.ts"],"names":[],"mappings":"AA4BA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAwDrD,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,QAAQ,GAAG,IAAI,CAgE5C"}
@@ -17,16 +17,29 @@ import { isRSCEnabled } from "../../../../utils/feature-flags.js";
17
17
  import { getEnvironmentConfig } from "../../../../config/environment-config.js";
18
18
  import { getErrorCollector } from "../../../../observability/error-collector.js";
19
19
  import { getLogBuffer } from "../../../../observability/log-buffer.js";
20
+ import { validatePathSync } from "../../../../security/index.js";
20
21
  import { ReloadNotifier } from "../../../reload-notifier.js";
21
22
  const WORKFLOW_EXECUTION_TIMEOUT_MS = 30_000;
22
- /** Validate a relative path for directory traversal and null bytes.
23
- * Note: searchParams.get() already percent-decodes, so no extra decoding needed. */
24
- function isValidRelativePath(path) {
25
- if (path.includes("\0"))
26
- return false;
27
- if (path.includes(".."))
28
- return false;
29
- return true;
23
+ /**
24
+ * Validate a relative path against the project directory.
25
+ *
26
+ * Uses `validatePathSync` in strict mode (rejects absolute paths, null bytes,
27
+ * `..` traversal, and any resolved path that escapes `baseDir`).
28
+ *
29
+ * Note: `searchParams.get()` already percent-decodes; no extra decoding needed
30
+ * (double-decoding would itself be a vulnerability).
31
+ *
32
+ * Returns the canonicalized absolute path on success, or `null` when invalid.
33
+ */
34
+ function validateRelativePath(path, projectDir) {
35
+ const result = validatePathSync(path, {
36
+ baseDir: projectDir,
37
+ allowAbsolute: false,
38
+ level: "strict",
39
+ });
40
+ if (!result.valid || !result.canonicalPath)
41
+ return null;
42
+ return result.canonicalPath;
30
43
  }
31
44
  const JSON_HEADERS = {
32
45
  "Content-Type": "application/json",
@@ -353,9 +366,16 @@ async function handleListFiles(req, ctx) {
353
366
  if (!projectDir)
354
367
  return errorResponse("No project directory configured", 500);
355
368
  const relativePath = new URL(req.url).searchParams.get("path") ?? "";
356
- if (!isValidRelativePath(relativePath))
357
- return errorResponse("Invalid path", 400);
358
- const fullPath = relativePath ? `${projectDir}/${relativePath}` : projectDir;
369
+ let fullPath;
370
+ if (relativePath === "") {
371
+ fullPath = projectDir;
372
+ }
373
+ else {
374
+ const canonical = validateRelativePath(relativePath, projectDir);
375
+ if (canonical === null)
376
+ return errorResponse("Invalid path", 400);
377
+ fullPath = canonical;
378
+ }
359
379
  try {
360
380
  const files = [];
361
381
  for await (const entry of adapter.fs.readDir(fullPath)) {
@@ -386,10 +406,11 @@ async function handleReadFileContent(req, ctx) {
386
406
  const relativePath = new URL(req.url).searchParams.get("path") ?? "";
387
407
  if (!relativePath)
388
408
  return errorResponse("path parameter is required", 400);
389
- if (!isValidRelativePath(relativePath))
409
+ const canonical = validateRelativePath(relativePath, projectDir);
410
+ if (canonical === null)
390
411
  return errorResponse("Invalid path", 400);
391
412
  try {
392
- const content = await adapter.fs.readFile(`${projectDir}/${relativePath}`);
413
+ const content = await adapter.fs.readFile(canonical);
393
414
  const extension = relativePath.split(".").pop() ?? "";
394
415
  if (!TEXT_EXTENSIONS.has(extension.toLowerCase())) {
395
416
  return jsonResponse({
@@ -1 +1 @@
1
- {"version":3,"file":"esbuild-plugins.d.ts","sourceRoot":"","sources":["../../../../../../src/src/server/handlers/dev/files/esbuild-plugins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA6B,MAAM,EAAe,MAAM,SAAS,CAAC;AAG9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAG5E,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,sCAAsC,CAAC;AAkB9C,gFAAgF;AAChF,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM,CA6C1F;AAKD,UAAU,yBAAyB;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AA+DD,kFAAkF;AAClF,wBAAgB,wBAAwB,CACtC,OAAO,GAAE,yBAAyB,GAAG,OAAe,GACnD,MAAM,CAqER;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAUjD"}
1
+ {"version":3,"file":"esbuild-plugins.d.ts","sourceRoot":"","sources":["../../../../../../src/src/server/handlers/dev/files/esbuild-plugins.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA6B,MAAM,EAAe,MAAM,SAAS,CAAC;AAG9E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAQ5E,OAAO,EAGL,KAAK,eAAe,EACrB,MAAM,sCAAsC,CAAC;AAkB9C,gFAAgF;AAChF,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,MAAM,CAyF1F;AAKD,UAAU,yBAAyB;IACjC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AA+DD,kFAAkF;AAClF,wBAAgB,wBAAwB,CACtC,OAAO,GAAE,yBAAyB,GAAG,OAAe,GACnD,MAAM,CAqER;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAUjD"}
@@ -1,5 +1,5 @@
1
1
  import { NETWORK_ERROR } from "../../../../errors/index.js";
2
- import { getDirectory, joinPath } from "../../../../utils/path-utils.js";
2
+ import { getDirectory, isWithinDirectory, joinPath, normalizePath, } from "../../../../utils/path-utils.js";
3
3
  import { computeIntegrity, createLockfileManager, } from "../../../../utils/import-lockfile.js";
4
4
  import { importMapOwnsSpecifier, mergeBrowserImportMapImports, } from "../../../../utils/import-map.js";
5
5
  import { serverLogger } from "../../../../utils/logger/index.js";
@@ -20,16 +20,40 @@ export function createRelativeFsPlugin(projectDir, adapter) {
20
20
  setup(build) {
21
21
  const exts = [".tsx", ".ts", ".jsx", ".js", ".mjs"];
22
22
  build.onResolve({ filter: /^(\.?\.?\/|\/)\/*/ }, async (args) => {
23
- const basedir = args.importer ? getDirectory(args.importer) : projectDir;
24
- const candidate = args.path.startsWith("/")
23
+ // VULN-FS-6: NUL bytes are never legitimate in module paths.
24
+ if (args.path.includes("\0")) {
25
+ return {
26
+ errors: [{ text: `Import path contains NUL byte: ${args.path}`, location: null }],
27
+ };
28
+ }
29
+ const basedir = args.resolveDir ||
30
+ (args.importer ? getDirectory(args.importer) : projectDir);
31
+ // normalizePath collapses `./` and `foo/../` segments produced by
32
+ // `joinPath` so downstream `adapter.fs.stat` lookups match the file
33
+ // system's canonical key. Still inside the containment check below.
34
+ const candidate = normalizePath(args.path.startsWith("/")
25
35
  ? joinPath(projectDir, args.path)
26
- : joinPath(basedir, args.path);
36
+ : joinPath(basedir, args.path));
37
+ // VULN-FS-6: refuse anything that, after joining, escapes the project
38
+ // root. esbuild plugins fire per-import; an entry file with
39
+ // `import "../../../../etc/hostname"` would otherwise embed the file.
40
+ if (!isWithinDirectory(projectDir, candidate)) {
41
+ return {
42
+ errors: [{
43
+ text: `Import escapes project directory: ${args.path}`,
44
+ location: null,
45
+ }],
46
+ };
47
+ }
27
48
  const candidates = [candidate];
28
49
  for (const ext of exts)
29
50
  candidates.push(candidate + ext);
30
51
  for (const ext of exts)
31
52
  candidates.push(joinPath(candidate, `index${ext}`));
32
53
  for (const f of candidates) {
54
+ // Defence in depth: each extension probe must also stay inside.
55
+ if (!isWithinDirectory(projectDir, f))
56
+ continue;
33
57
  try {
34
58
  const st = await adapter.fs.stat(f);
35
59
  if (st.isFile)
@@ -42,6 +66,23 @@ export function createRelativeFsPlugin(projectDir, adapter) {
42
66
  return undefined;
43
67
  });
44
68
  build.onLoad({ filter: /\.(tsx?|jsx?|mjs)$/ }, async (args) => {
69
+ // VULN-FS-6: belt-and-braces — reject any onLoad call whose path
70
+ // escapes the project root or carries a NUL byte. onResolve already
71
+ // gates this, but esbuild can call onLoad with paths produced by
72
+ // other plugins or namespaces.
73
+ if (args.path.includes("\0")) {
74
+ return {
75
+ errors: [{ text: `Load path contains NUL byte: ${args.path}`, location: null }],
76
+ };
77
+ }
78
+ if (!isWithinDirectory(projectDir, args.path)) {
79
+ return {
80
+ errors: [{
81
+ text: `Load path escapes project directory: ${args.path}`,
82
+ location: null,
83
+ }],
84
+ };
85
+ }
45
86
  try {
46
87
  const contents = await adapter.fs.readFile(args.path);
47
88
  return { contents, loader: getLoaderForPath(args.path) };
@@ -1,2 +1,2 @@
1
- export declare const VERSION = "0.1.241";
1
+ export declare const VERSION = "0.1.243";
2
2
  //# sourceMappingURL=version-constant.d.ts.map
@@ -1,3 +1,3 @@
1
1
  // Keep in sync with deno.json version.
2
2
  // scripts/release.ts updates this constant during releases.
3
- export const VERSION = "0.1.241";
3
+ export const VERSION = "0.1.243";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "veryfront",
3
- "version": "0.1.241",
3
+ "version": "0.1.243",
4
4
  "description": "The simplest way to build AI-powered apps",
5
5
  "keywords": [
6
6
  "react",