@mastra/factory 0.2.2-alpha.0 → 0.2.2-alpha.2
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/CHANGELOG.md +36 -0
- package/dist/auth.d.ts +2 -2
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +401 -314
- package/dist/auth.js.map +1 -1
- package/dist/capabilities/connection.js +1 -1
- package/dist/capabilities/intake.js +1 -1
- package/dist/capabilities/version-control.js +1 -1
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +441 -39455
- package/dist/factory.js.map +1 -1
- package/dist/index.js +2 -39466
- package/dist/integrations/base.js +1 -1
- package/dist/integrations/github/config.js +36 -22
- package/dist/integrations/github/config.js.map +1 -1
- package/dist/integrations/github/factory-session.js +33 -37
- package/dist/integrations/github/factory-session.js.map +1 -1
- package/dist/integrations/github/integration.js +1037 -3624
- package/dist/integrations/github/integration.js.map +1 -1
- package/dist/integrations/github/issue-triage.js +71 -71
- package/dist/integrations/github/issue-triage.js.map +1 -1
- package/dist/integrations/github/pat.js +47 -36
- package/dist/integrations/github/pat.js.map +1 -1
- package/dist/integrations/github/provenance.js +72 -101
- package/dist/integrations/github/provenance.js.map +1 -1
- package/dist/integrations/github/routes.d.ts.map +1 -1
- package/dist/integrations/github/routes.js +1272 -1833
- package/dist/integrations/github/routes.js.map +1 -1
- package/dist/integrations/github/rules.js +203 -471
- package/dist/integrations/github/rules.js.map +1 -1
- package/dist/integrations/github/sandbox.d.ts +2 -0
- package/dist/integrations/github/sandbox.d.ts.map +1 -1
- package/dist/integrations/github/sandbox.js +401 -326
- package/dist/integrations/github/sandbox.js.map +1 -1
- package/dist/integrations/github/session-subscriptions.js +165 -223
- package/dist/integrations/github/session-subscriptions.js.map +1 -1
- package/dist/integrations/github/subscriptions.js +43 -51
- package/dist/integrations/github/subscriptions.js.map +1 -1
- package/dist/integrations/github/token-refresh.js +15 -18
- package/dist/integrations/github/token-refresh.js.map +1 -1
- package/dist/integrations/github/webhook.js +359 -343
- package/dist/integrations/github/webhook.js.map +1 -1
- package/dist/integrations/linear/agent-tools.js +77 -100
- package/dist/integrations/linear/agent-tools.js.map +1 -1
- package/dist/integrations/linear/integration.js +611 -1309
- package/dist/integrations/linear/integration.js.map +1 -1
- package/dist/integrations/linear/routes.js +227 -230
- package/dist/integrations/linear/routes.js.map +1 -1
- package/dist/integrations/linear/rules.js +126 -346
- package/dist/integrations/linear/rules.js.map +1 -1
- package/dist/integrations/linear/storage.js +1 -1
- package/dist/integrations/platform/api-client.js +131 -135
- package/dist/integrations/platform/api-client.js.map +1 -1
- package/dist/integrations/platform/github/event-worker.js +275 -573
- package/dist/integrations/platform/github/event-worker.js.map +1 -1
- package/dist/integrations/platform/github/integration.d.ts.map +1 -1
- package/dist/integrations/platform/github/integration.js +845 -3942
- package/dist/integrations/platform/github/integration.js.map +1 -1
- package/dist/integrations/platform/linear/integration.js +457 -1309
- package/dist/integrations/platform/linear/integration.js.map +1 -1
- package/dist/integrations/workos/integration.js +80 -85
- package/dist/integrations/workos/integration.js.map +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +933 -915
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/custom-provider-source.js +110 -93
- package/dist/routes/custom-provider-source.js.map +1 -1
- package/dist/routes/fs.js +401 -349
- package/dist/routes/fs.js.map +1 -1
- package/dist/routes/intake.js +165 -165
- package/dist/routes/intake.js.map +1 -1
- package/dist/routes/oauth.js +447 -25166
- package/dist/routes/oauth.js.map +1 -1
- package/dist/routes/projects.js +437 -376
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/provider-credentials.js +76 -58
- package/dist/routes/provider-credentials.js.map +1 -1
- package/dist/routes/route.js +12 -8
- package/dist/routes/route.js.map +1 -1
- package/dist/routes/skills.js +160 -173
- package/dist/routes/skills.js.map +1 -1
- package/dist/routes/surface.js +250 -28618
- package/dist/routes/surface.js.map +1 -1
- package/dist/routes/tenant-credentials.js +141 -166
- package/dist/routes/tenant-credentials.js.map +1 -1
- package/dist/routes/work-items.js +654 -862
- package/dist/routes/work-items.js.map +1 -1
- package/dist/rules/binding-context.js +23 -35
- package/dist/rules/binding-context.js.map +1 -1
- package/dist/rules/defaults.js +200 -309
- package/dist/rules/defaults.js.map +1 -1
- package/dist/rules/dispatcher.d.ts.map +1 -1
- package/dist/rules/dispatcher.js +438 -677
- package/dist/rules/dispatcher.js.map +1 -1
- package/dist/rules/index.js +5 -599
- package/dist/rules/processor.js +344 -596
- package/dist/rules/processor.js.map +1 -1
- package/dist/rules/resolve.js +21 -18
- package/dist/rules/resolve.js.map +1 -1
- package/dist/rules/start-coordinator.js +168 -157
- package/dist/rules/start-coordinator.js.map +1 -1
- package/dist/rules/tools.js +50 -85
- package/dist/rules/tools.js.map +1 -1
- package/dist/rules/transition-service.js +183 -454
- package/dist/rules/transition-service.js.map +1 -1
- package/dist/rules/types.js +33 -29
- package/dist/rules/types.js.map +1 -1
- package/dist/rules/validation.js +266 -281
- package/dist/rules/validation.js.map +1 -1
- package/dist/sandbox/fleet.js +272 -250
- package/dist/sandbox/fleet.js.map +1 -1
- package/dist/sandbox/reattach.js +13 -6
- package/dist/sandbox/reattach.js.map +1 -1
- package/dist/server-error.js +12 -16
- package/dist/server-error.js.map +1 -1
- package/dist/skills/service.js +30 -35
- package/dist/skills/service.js.map +1 -1
- package/dist/spa-static.d.ts.map +1 -1
- package/dist/spa-static.js +91 -64
- package/dist/spa-static.js.map +1 -1
- package/dist/state-signing.js +73 -44
- package/dist/state-signing.js.map +1 -1
- package/dist/storage/domains/audit/agent-audit.js +60 -59
- package/dist/storage/domains/audit/agent-audit.js.map +1 -1
- package/dist/storage/domains/audit/base.js +169 -112
- package/dist/storage/domains/audit/base.js.map +1 -1
- package/dist/storage/domains/audit/domain.js +155 -153
- package/dist/storage/domains/audit/domain.js.map +1 -1
- package/dist/storage/domains/credentials/base.js +259 -243
- package/dist/storage/domains/credentials/base.js.map +1 -1
- package/dist/storage/domains/custom-providers/base.js +138 -143
- package/dist/storage/domains/custom-providers/base.js.map +1 -1
- package/dist/storage/domains/intake/base.js +75 -57
- package/dist/storage/domains/intake/base.js.map +1 -1
- package/dist/storage/domains/integrations/base.js +310 -245
- package/dist/storage/domains/integrations/base.js.map +1 -1
- package/dist/storage/domains/memory-settings/base.js +117 -102
- package/dist/storage/domains/memory-settings/base.js.map +1 -1
- package/dist/storage/domains/model-packs/base.js +89 -88
- package/dist/storage/domains/model-packs/base.js.map +1 -1
- package/dist/storage/domains/projects/base.js +97 -92
- package/dist/storage/domains/projects/base.js.map +1 -1
- package/dist/storage/domains/queue-health/base.js +111 -76
- package/dist/storage/domains/queue-health/base.js.map +1 -1
- package/dist/storage/domains/source-control/base.d.ts +9 -0
- package/dist/storage/domains/source-control/base.d.ts.map +1 -1
- package/dist/storage/domains/source-control/base.js +777 -702
- package/dist/storage/domains/source-control/base.js.map +1 -1
- package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
- package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
- package/dist/storage/domains/source-control/inmemory.js +322 -344
- package/dist/storage/domains/source-control/inmemory.js.map +1 -1
- package/dist/storage/domains/work-items/base.d.ts.map +1 -1
- package/dist/storage/domains/work-items/base.js +1266 -1132
- package/dist/storage/domains/work-items/base.js.map +1 -1
- package/dist/storage/domains/work-items/metrics.js +202 -184
- package/dist/storage/domains/work-items/metrics.js.map +1 -1
- package/dist/timing.d.ts +15 -0
- package/dist/timing.d.ts.map +1 -0
- package/dist/timing.js +34 -0
- package/dist/timing.js.map +1 -0
- package/dist/workspace.d.ts +2 -3
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +236 -438
- package/dist/workspace.js.map +1 -1
- package/factory-skills/factory-review/SKILL.md +9 -2
- package/package.json +9 -9
- package/dist/capabilities/connection.js.map +0 -1
- package/dist/capabilities/intake.js.map +0 -1
- package/dist/capabilities/version-control.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrations/base.js.map +0 -1
- package/dist/integrations/linear/storage.js.map +0 -1
- package/dist/rules/index.js.map +0 -1
package/dist/routes/fs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/routes/fs.ts"],"sourcesContent":["import { lstat, open, readdir, realpath, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { isAbsolute, join, posix as posixPath, resolve, sep } from 'node:path';\n\nimport { SandboxFilesystem } from '@mastra/code-sdk/agents/sandbox-filesystem';\nimport { detectProject, getResourceIdOverride } from '@mastra/code-sdk/utils/project';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { SandboxFleet } from '../sandbox/fleet.js';\nimport type { SourceControlSession } from '../storage/domains/source-control/base.js';\nimport type { RouteAuth } from './route.js';\n\n/**\n * Server-side directory browser for the web project picker.\n *\n * The browser cannot read absolute filesystem paths (the File System Access API\n * only exposes a directory *name*), so the picker must ask the server — which\n * does have filesystem access — to enumerate directories. The result is real\n * absolute paths the user can select without typing.\n *\n * All access is confined to a configured `root` (default: the user's home\n * directory). Requests that try to escape the root via `..` or symlinks are\n * clamped back to the root.\n */\n\nexport interface DirectoryEntry {\n name: string;\n /** Absolute path to the entry. */\n path: string;\n}\n\nexport interface DirectoryListing {\n /** The allowed root; clients cannot browse above this. */\n root: string;\n /** The absolute path that was listed. */\n path: string;\n /** Parent directory path, or null when `path` is the root. */\n parent: string | null;\n /** Subdirectories of `path` (directories only, sorted, hidden excluded). */\n entries: DirectoryEntry[];\n}\n\nexport interface WorkspaceRenderedEntry {\n name: string;\n /** Path relative to the configured rendered root. */\n path: string;\n type: 'file' | 'directory';\n size: number;\n updatedAt: string;\n}\n\nexport interface WorkspaceRenderedListing {\n /** The confined workspace/project root. */\n workspacePath: string;\n /** Configured workspace-relative rendered root, e.g. `.artifacts`. */\n root: string;\n /** The confined absolute path for the rendered root. */\n rootPath: string;\n entries: WorkspaceRenderedEntry[];\n}\n\nexport interface WorkspaceFile {\n /** The confined workspace/project root. */\n workspacePath: string;\n /** Workspace-relative file path. */\n path: string;\n name: string;\n size: number;\n updatedAt: string;\n contentType: 'text' | 'unsupported';\n content?: string;\n truncated?: boolean;\n}\n\nexport type ArtifactEntry = WorkspaceRenderedEntry;\n\nexport interface ArtifactListing {\n /** The confined workspace/project root. */\n rootPath: string;\n /** The workspace artifact directory. */\n artifactsPath: string;\n entries: ArtifactEntry[];\n}\n\nconst MAX_TEXT_FILE_BYTES = 512 * 1024;\nconst TEXT_DECODER = new TextDecoder('utf-8', { fatal: true });\nconst APPROVED_RENDERED_ROOTS = new Set(['.artifacts']);\n\n/** Erase a route handler's path-parameterized context to a plain `Context`. */\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\n/** Resolve the browsable root, defaulting to the user's home directory. */\nexport function resolveFsRoot(root?: string): string {\n return resolve(root && root.trim() ? root : homedir());\n}\n\n/** True when `candidate` is `root` or nested under it. */\nfunction isWithinRoot(candidate: string, root: string): boolean {\n if (candidate === root) return true;\n const rootWithSep = root.endsWith(sep) ? root : root + sep;\n return candidate.startsWith(rootWithSep);\n}\n\nasync function realOrResolved(path: string): Promise<string> {\n try {\n return await realpath(path);\n } catch {\n return path;\n }\n}\n\n/**\n * Resolve a path's real location (following symlinks) and confirm it stays\n * within `root`. Returns the real path when confined, or `null` when it escapes\n * the root or does not exist. Used so a symlink inside the root that points\n * outside it cannot be browsed or selected.\n */\nasync function realPathWithinRoot(candidate: string, root: string): Promise<string | null> {\n try {\n const real = await realpath(candidate);\n return isWithinRoot(real, root) ? real : null;\n } catch {\n return null;\n }\n}\n\nfunction assertRelativePath(path: string, label: string): string {\n const trimmed = path.trim();\n if (!trimmed) throw new Error(`Missing required query param: ${label}`);\n if (isAbsolute(trimmed)) throw new Error(`${label} must be relative`);\n if (trimmed.split(/[\\\\/]+/).includes('..')) throw new Error(`${label} escapes workspace`);\n const normalized = resolve('/', trimmed).slice(1);\n if (!normalized || normalized === '..' || normalized.startsWith(`..${sep}`))\n throw new Error(`${label} escapes workspace`);\n return normalized;\n}\n\nfunction assertApprovedRenderedRoot(renderedRoot: string): string {\n const safeRoot = assertRelativePath(renderedRoot, 'root');\n if (!APPROVED_RENDERED_ROOTS.has(safeRoot)) throw new Error('Root is not approved for rendered workspace access');\n return safeRoot;\n}\n\nasync function confinedWorkspacePath(\n root: string,\n workspacePath: string,\n): Promise<{ resolvedRoot: string; workspace: string }> {\n const resolvedRoot = await realOrResolved(resolveFsRoot(root));\n const candidate = isAbsolute(workspacePath) ? resolve(workspacePath) : resolve(resolvedRoot, workspacePath);\n const workspace = await realPathWithinRoot(candidate, resolvedRoot);\n if (!workspace) throw new Error('Path is outside the browsable root');\n return { resolvedRoot, workspace };\n}\n\nasync function confinedWorkspaceRelativePath(\n root: string,\n workspacePath: string,\n relativePath: string,\n): Promise<{ workspace: string; path: string; relativePath: string }> {\n const safeRelativePath = assertRelativePath(relativePath, 'path');\n const { workspace } = await confinedWorkspacePath(root, workspacePath);\n const candidate = resolve(workspace, safeRelativePath);\n if (!isWithinRoot(candidate, workspace)) throw new Error('Path escapes workspace');\n const confinedPath = await realPathWithinRoot(candidate, workspace);\n if (!confinedPath) throw new Error('Path is outside the workspace');\n return { workspace, path: confinedPath, relativePath: safeRelativePath };\n}\n\n/**\n * List the directories inside `requestedPath`, confined to `root`. An absent or\n * out-of-root path is clamped to the root, so the worst a malicious client can\n * do is browse within the allowed root.\n */\nexport async function listDirectory(root: string, requestedPath?: string): Promise<DirectoryListing> {\n // Resolve the root through symlinks so all confinement checks compare real\n // paths; a symlink that escapes the root is then reliably detectable.\n const resolvedRoot = await realOrResolved(resolveFsRoot(root));\n\n let target = resolvedRoot;\n if (requestedPath && requestedPath.trim()) {\n const candidate = isAbsolute(requestedPath) ? resolve(requestedPath) : resolve(resolvedRoot, requestedPath);\n // Follow symlinks and re-confirm the real target stays within the root.\n target = (await realPathWithinRoot(candidate, resolvedRoot)) ?? resolvedRoot;\n }\n\n // Confirm the target is a real directory; fall back to root otherwise.\n try {\n const info = await stat(target);\n if (!info.isDirectory()) target = resolvedRoot;\n } catch {\n target = resolvedRoot;\n }\n\n const dirents = await readdir(target, { withFileTypes: true });\n const entries: DirectoryEntry[] = [];\n for (const dirent of dirents) {\n if (dirent.name.startsWith('.')) continue; // skip dotfiles/dirs\n const entryPath = join(target, dirent.name);\n let isDir = dirent.isDirectory();\n if (dirent.isSymbolicLink()) {\n // Only surface symlinks whose real target is a directory inside the root,\n // so a link pointing outside the root can't be browsed or selected.\n const real = await realPathWithinRoot(entryPath, resolvedRoot);\n isDir = real ? (await stat(real).catch(() => null))?.isDirectory() === true : false;\n }\n if (isDir) entries.push({ name: dirent.name, path: entryPath });\n }\n entries.sort((a, b) => a.name.localeCompare(b.name));\n\n const parent = target === resolvedRoot ? null : resolve(target, '..');\n\n return { root: resolvedRoot, path: target, parent, entries };\n}\n\nasync function listRenderedEntries(rootPath: string, currentPath = rootPath): Promise<WorkspaceRenderedEntry[]> {\n const dirents = await readdir(currentPath, { withFileTypes: true });\n const entries: WorkspaceRenderedEntry[] = [];\n\n for (const dirent of dirents) {\n const entryPath = join(currentPath, dirent.name);\n const info = await lstat(entryPath);\n const relativePath = entryPath.slice(rootPath.length + 1);\n\n if (info.isDirectory()) {\n entries.push({\n name: dirent.name,\n path: relativePath,\n type: 'directory',\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n });\n entries.push(...(await listRenderedEntries(rootPath, entryPath)));\n continue;\n }\n\n if (info.isFile()) {\n entries.push({\n name: dirent.name,\n path: relativePath,\n type: 'file',\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n });\n }\n }\n\n return entries.sort((a, b) => a.path.localeCompare(b.path));\n}\n\nexport async function listWorkspaceRenderedPath(\n root: string,\n workspacePath: string,\n renderedRoot: string,\n): Promise<WorkspaceRenderedListing> {\n const safeRoot = assertApprovedRenderedRoot(renderedRoot);\n const { workspace } = await confinedWorkspacePath(root, workspacePath);\n const renderedPath = resolve(workspace, safeRoot);\n if (!isWithinRoot(renderedPath, workspace)) throw new Error('Root escapes workspace');\n\n const confinedRootPath = await realPathWithinRoot(renderedPath, workspace);\n if (!confinedRootPath) return { workspacePath: workspace, root: safeRoot, rootPath: renderedPath, entries: [] };\n\n const info = await stat(confinedRootPath);\n if (!info.isDirectory()) return { workspacePath: workspace, root: safeRoot, rootPath: confinedRootPath, entries: [] };\n\n return {\n workspacePath: workspace,\n root: safeRoot,\n rootPath: confinedRootPath,\n entries: await listRenderedEntries(confinedRootPath),\n };\n}\n\nexport async function readWorkspaceFile(root: string, workspacePath: string, path: string): Promise<WorkspaceFile> {\n const safePath = assertRelativePath(path, 'path');\n const relativeRoot = safePath.split('/')[0] ?? '';\n assertApprovedRenderedRoot(relativeRoot);\n const {\n workspace,\n path: confinedPath,\n relativePath,\n } = await confinedWorkspaceRelativePath(root, workspacePath, path);\n const info = await lstat(confinedPath);\n if (info.isDirectory()) throw new Error('Path is a directory');\n if (!info.isFile()) throw new Error('Unsupported file type');\n\n const bytesToRead = Math.min(info.size, MAX_TEXT_FILE_BYTES);\n const contentBuffer = Buffer.alloc(bytesToRead);\n const handle = await open(confinedPath, 'r');\n try {\n await handle.read(contentBuffer, 0, bytesToRead, 0);\n } finally {\n await handle.close();\n }\n\n try {\n const content = TEXT_DECODER.decode(contentBuffer);\n return {\n workspacePath: workspace,\n path: relativePath,\n name: relativePath.split('/').pop() ?? relativePath,\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n contentType: 'text',\n content,\n truncated: info.size > MAX_TEXT_FILE_BYTES,\n };\n } catch {\n return {\n workspacePath: workspace,\n path: relativePath,\n name: relativePath.split('/').pop() ?? relativePath,\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n contentType: 'unsupported',\n };\n }\n}\n\nexport async function listArtifacts(root: string, workspacePath: string): Promise<ArtifactListing> {\n const listing = await listWorkspaceRenderedPath(root, workspacePath, '.artifacts');\n return {\n rootPath: listing.workspacePath,\n artifactsPath: listing.rootPath,\n entries: listing.entries,\n };\n}\n\n// ── Session-backed workspace access ──────────────────────────────────────────\n//\n// The web UI identifies a Factory session workspace by its session id (a UUID),\n// not by a server-local filesystem path — the session's files live inside the\n// session's sandbox (a remote VM on deployed factories). These helpers resolve\n// the session, enforce that the caller owns it, reattach to its sandbox, and\n// serve the approved rendered roots through `SandboxFilesystem`.\n\n/** Dependencies for resolving a `workspacePath` that is a Factory session id. */\nexport interface SessionFsDeps {\n auth: RouteAuth;\n fleet: SandboxFleet;\n sessions: { getBySessionId(sessionId: string): Promise<SourceControlSession | null> };\n}\n\n/**\n * Resolve a `workspacePath` query param as a Factory session id. Returns the\n * session when one exists and the caller owns it, `null` when no session\n * matches (the caller should fall back to local-path handling), and throws\n * when a session exists but belongs to another tenant.\n */\nasync function resolveAuthorizedSession(\n c: Context,\n deps: SessionFsDeps | undefined,\n workspacePath: string,\n): Promise<SourceControlSession | null> {\n if (!deps) return null;\n const session = await deps.sessions.getBySessionId(workspacePath);\n if (!session) return null;\n if (deps.auth.enabled()) {\n await deps.auth.ensureUser(c);\n const tenant = deps.auth.tenant(c);\n if (!tenant || tenant.orgId !== session.orgId || tenant.userId !== session.userId) {\n throw new Error('Session is not available to the current user');\n }\n }\n return session;\n}\n\ninterface SessionSandboxHandle {\n sandbox: { executeCommand(command: string, args?: string[], options?: { timeout?: number }): Promise<unknown> };\n filesystem: SandboxFilesystem;\n workdir: string;\n}\n\n/**\n * Reattach to the session's sandbox and wrap its workdir in a\n * `SandboxFilesystem`. Returns `null` when the session has no provisioned\n * sandbox yet (nothing materialized → nothing to list), or when the sandbox\n * can no longer be reattached (e.g. torn down by the provider's idle GC).\n * This is a passive read path, so it never re-provisions: the session's\n * filesystem is preserved in its provider checkpoint and comes back the next\n * time the workspace is actually opened (e.g. by sending a message).\n */\nasync function sessionSandbox(\n fleet: SandboxFleet,\n session: SourceControlSession,\n): Promise<SessionSandboxHandle | null> {\n if (!fleet.enabled || !session.sandboxId || !session.sandboxWorkdir) return null;\n let sandbox: Awaited<ReturnType<SandboxFleet['reattachSandbox']>>;\n try {\n sandbox = await fleet.reattachSandbox(session.sandboxId, { workingDirectory: session.sandboxWorkdir });\n } catch {\n // Sandbox is gone (idle GC) or unreachable. Degrade to an empty view\n // rather than surfacing a 500 from a file-viewer panel.\n return null;\n }\n return {\n sandbox,\n filesystem: new SandboxFilesystem({ sandbox, workdir: session.sandboxWorkdir }),\n workdir: session.sandboxWorkdir,\n };\n}\n\n/** List an approved rendered root inside a Factory session's sandbox workdir. */\nexport async function listSessionRenderedPath(\n fleet: SandboxFleet,\n session: SourceControlSession,\n renderedRoot: string,\n): Promise<WorkspaceRenderedListing> {\n const safeRoot = assertApprovedRenderedRoot(renderedRoot);\n const rootPath = posixPath.join(session.sandboxWorkdir ?? '', safeRoot);\n const empty: WorkspaceRenderedListing = { workspacePath: session.sessionId, root: safeRoot, rootPath, entries: [] };\n\n const handle = await sessionSandbox(fleet, session);\n if (!handle) return empty;\n\n // One round trip: emit \"type\\tsize\\tmtime\\tpath\" per entry. `safeRoot` comes\n // from a fixed allowlist so interpolating it (quoted) is safe.\n const quotedRoot = `'${rootPath.replace(/'/g, `'\\\\''`)}'`;\n const result = (await handle.sandbox.executeCommand(\n 'sh',\n [\n '-c',\n `test -d ${quotedRoot} && find ${quotedRoot} -mindepth 1 -printf '%y\\\\t%s\\\\t%T@\\\\t%p\\\\n' 2>/dev/null || true`,\n ],\n { timeout: 30_000 },\n )) as { exitCode: number; stdout: string };\n if (result.exitCode !== 0) return empty;\n\n const entries: WorkspaceRenderedEntry[] = [];\n for (const line of result.stdout.split('\\n')) {\n if (!line) continue;\n const [type, sizeStr, mtimeStr, ...pathParts] = line.split('\\t');\n const fullPath = pathParts.join('\\t');\n if (!fullPath || !fullPath.startsWith(`${rootPath}/`)) continue;\n const relativePath = fullPath.slice(rootPath.length + 1);\n entries.push({\n name: posixPath.basename(relativePath),\n path: relativePath,\n type: type === 'd' ? 'directory' : 'file',\n size: type === 'd' ? 0 : Number(sizeStr) || 0,\n updatedAt: new Date((Number(mtimeStr) || 0) * 1000).toISOString(),\n });\n }\n entries.sort((a, b) => a.path.localeCompare(b.path));\n\n return { workspacePath: session.sessionId, root: safeRoot, rootPath, entries };\n}\n\n/** Read a file under an approved rendered root inside a session's sandbox. */\nexport async function readSessionWorkspaceFile(\n fleet: SandboxFleet,\n session: SourceControlSession,\n path: string,\n): Promise<WorkspaceFile> {\n const safePath = assertRelativePath(path, 'path');\n assertApprovedRenderedRoot(safePath.split('/')[0] ?? '');\n\n const handle = await sessionSandbox(fleet, session);\n if (!handle) throw new Error('Session workspace is not available');\n const { filesystem } = handle;\n const info = await filesystem.stat(safePath);\n if (info.type === 'directory') throw new Error('Path is a directory');\n\n const buffer = (await filesystem.readFile(safePath)) as Buffer;\n const truncated = buffer.length > MAX_TEXT_FILE_BYTES;\n const base = {\n workspacePath: session.sessionId,\n path: safePath,\n name: posixPath.basename(safePath),\n size: buffer.length,\n updatedAt: info.modifiedAt.toISOString(),\n };\n try {\n const content = TEXT_DECODER.decode(truncated ? buffer.subarray(0, MAX_TEXT_FILE_BYTES) : buffer);\n return { ...base, contentType: 'text', content, truncated };\n } catch {\n return { ...base, contentType: 'unsupported' };\n }\n}\n\nexport interface ResolvedCodebase {\n /**\n * The resourceId the TUI would use for this path — derived identically so a\n * project opened in the terminal and in the web app resolve to the SAME\n * session (and therefore the same threads).\n */\n resourceId: string;\n name: string;\n rootPath: string;\n gitUrl?: string;\n gitBranch?: string;\n}\n\n/**\n * Resolve a project path to the same resourceId the TUI uses. Mirrors\n * `createMastraCode`: detect the project, then apply any resourceId override\n * (MASTRA_RESOURCE_ID env var or `.mastracode/database.json`). This is the\n * shared continuity point — start in the TUI, continue on the web, same path\n * → same resourceId → same session.\n */\nexport function resolveCodebase(projectPath: string): ResolvedCodebase {\n const info = detectProject(projectPath);\n const override = getResourceIdOverride(info.rootPath);\n return {\n resourceId: override ?? info.resourceId,\n name: info.name,\n rootPath: info.rootPath,\n gitUrl: info.gitUrl,\n gitBranch: info.gitBranch,\n };\n}\n\n/**\n * Build the web filesystem routes as Mastra `apiRoutes`:\n * - `GET /web/fs/list?path=...` — browse directories (confined to root)\n * - `GET /web/codebase/resolve?path=...` — TUI-compatible codebase resourceId\n */\nexport function buildFsRoutes(options: { root?: string; sessionFs?: SessionFsDeps } = {}): ApiRoute[] {\n const root = resolveFsRoot(options.root);\n const sessionFs = options.sessionFs;\n\n return [\n registerApiRoute('/web/fs/list', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const path = c.req.query('path');\n try {\n const listing = await listDirectory(root, path);\n return c.json(listing);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n },\n }),\n registerApiRoute('/web/artifacts/list', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const path = c.req.query('path');\n if (!path) return c.json({ error: 'Missing required query param: path' }, 400);\n try {\n return c.json(await listArtifacts(root, path));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status = message === 'Path is outside the browsable root' ? 403 : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/workspace/rendered/list', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const workspacePath = c.req.query('workspacePath');\n const renderedRoot = c.req.query('root');\n if (!workspacePath) return c.json({ error: 'Missing required query param: workspacePath' }, 400);\n if (!renderedRoot) return c.json({ error: 'Missing required query param: root' }, 400);\n try {\n const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);\n if (session && sessionFs) {\n return c.json(await listSessionRenderedPath(sessionFs.fleet, session, renderedRoot));\n }\n return c.json(await listWorkspaceRenderedPath(root, workspacePath, renderedRoot));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status =\n message.includes('outside') ||\n message.includes('relative') ||\n message.includes('escapes') ||\n message.includes('not approved') ||\n message.includes('not available')\n ? 403\n : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/workspace/file', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const workspacePath = c.req.query('workspacePath');\n const path = c.req.query('path');\n if (!workspacePath) return c.json({ error: 'Missing required query param: workspacePath' }, 400);\n if (!path) return c.json({ error: 'Missing required query param: path' }, 400);\n try {\n const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);\n if (session && sessionFs) {\n return c.json(await readSessionWorkspaceFile(sessionFs.fleet, session, path));\n }\n return c.json(await readWorkspaceFile(root, workspacePath, path));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status =\n message.includes('outside') ||\n message.includes('relative') ||\n message.includes('escapes') ||\n message.includes('not approved') ||\n message.includes('not available')\n ? 403\n : message.includes('directory')\n ? 400\n : message.includes('not found')\n ? 404\n : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/codebase/resolve', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const path = c.req.query('path');\n if (!path) return c.json({ error: 'Missing required query param: path' }, 400);\n // Confine resolution to the browsable root (following symlinks), so this\n // endpoint can't be used to probe arbitrary filesystem paths. The web UI\n // only ever resolves directories the user picked via the root-confined\n // browser, so legitimate requests are always within the root.\n const confined = await realPathWithinRoot(isAbsolute(path) ? resolve(path) : resolve(root, path), root);\n if (!confined) return c.json({ error: 'Path is outside the browsable root' }, 403);\n try {\n return c.json(resolveCodebase(confined));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n },\n }),\n ];\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"file":"fs.js","names":["posixPath"],"sources":["../../src/routes/fs.ts"],"sourcesContent":["import { lstat, open, readdir, realpath, stat } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { isAbsolute, join, posix as posixPath, resolve, sep } from 'node:path';\n\nimport { SandboxFilesystem } from '@mastra/code-sdk/agents/sandbox-filesystem';\nimport { detectProject, getResourceIdOverride } from '@mastra/code-sdk/utils/project';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { SandboxFleet } from '../sandbox/fleet.js';\nimport type { SourceControlSession } from '../storage/domains/source-control/base.js';\nimport type { RouteAuth } from './route.js';\n\n/**\n * Server-side directory browser for the web project picker.\n *\n * The browser cannot read absolute filesystem paths (the File System Access API\n * only exposes a directory *name*), so the picker must ask the server — which\n * does have filesystem access — to enumerate directories. The result is real\n * absolute paths the user can select without typing.\n *\n * All access is confined to a configured `root` (default: the user's home\n * directory). Requests that try to escape the root via `..` or symlinks are\n * clamped back to the root.\n */\n\nexport interface DirectoryEntry {\n name: string;\n /** Absolute path to the entry. */\n path: string;\n}\n\nexport interface DirectoryListing {\n /** The allowed root; clients cannot browse above this. */\n root: string;\n /** The absolute path that was listed. */\n path: string;\n /** Parent directory path, or null when `path` is the root. */\n parent: string | null;\n /** Subdirectories of `path` (directories only, sorted, hidden excluded). */\n entries: DirectoryEntry[];\n}\n\nexport interface WorkspaceRenderedEntry {\n name: string;\n /** Path relative to the configured rendered root. */\n path: string;\n type: 'file' | 'directory';\n size: number;\n updatedAt: string;\n}\n\nexport interface WorkspaceRenderedListing {\n /** The confined workspace/project root. */\n workspacePath: string;\n /** Configured workspace-relative rendered root, e.g. `.artifacts`. */\n root: string;\n /** The confined absolute path for the rendered root. */\n rootPath: string;\n entries: WorkspaceRenderedEntry[];\n}\n\nexport interface WorkspaceFile {\n /** The confined workspace/project root. */\n workspacePath: string;\n /** Workspace-relative file path. */\n path: string;\n name: string;\n size: number;\n updatedAt: string;\n contentType: 'text' | 'unsupported';\n content?: string;\n truncated?: boolean;\n}\n\nexport type ArtifactEntry = WorkspaceRenderedEntry;\n\nexport interface ArtifactListing {\n /** The confined workspace/project root. */\n rootPath: string;\n /** The workspace artifact directory. */\n artifactsPath: string;\n entries: ArtifactEntry[];\n}\n\nconst MAX_TEXT_FILE_BYTES = 512 * 1024;\nconst TEXT_DECODER = new TextDecoder('utf-8', { fatal: true });\nconst APPROVED_RENDERED_ROOTS = new Set(['.artifacts']);\n\n/** Erase a route handler's path-parameterized context to a plain `Context`. */\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\n/** Resolve the browsable root, defaulting to the user's home directory. */\nexport function resolveFsRoot(root?: string): string {\n return resolve(root && root.trim() ? root : homedir());\n}\n\n/** True when `candidate` is `root` or nested under it. */\nfunction isWithinRoot(candidate: string, root: string): boolean {\n if (candidate === root) return true;\n const rootWithSep = root.endsWith(sep) ? root : root + sep;\n return candidate.startsWith(rootWithSep);\n}\n\nasync function realOrResolved(path: string): Promise<string> {\n try {\n return await realpath(path);\n } catch {\n return path;\n }\n}\n\n/**\n * Resolve a path's real location (following symlinks) and confirm it stays\n * within `root`. Returns the real path when confined, or `null` when it escapes\n * the root or does not exist. Used so a symlink inside the root that points\n * outside it cannot be browsed or selected.\n */\nasync function realPathWithinRoot(candidate: string, root: string): Promise<string | null> {\n try {\n const real = await realpath(candidate);\n return isWithinRoot(real, root) ? real : null;\n } catch {\n return null;\n }\n}\n\nfunction assertRelativePath(path: string, label: string): string {\n const trimmed = path.trim();\n if (!trimmed) throw new Error(`Missing required query param: ${label}`);\n if (isAbsolute(trimmed)) throw new Error(`${label} must be relative`);\n if (trimmed.split(/[\\\\/]+/).includes('..')) throw new Error(`${label} escapes workspace`);\n const normalized = resolve('/', trimmed).slice(1);\n if (!normalized || normalized === '..' || normalized.startsWith(`..${sep}`))\n throw new Error(`${label} escapes workspace`);\n return normalized;\n}\n\nfunction assertApprovedRenderedRoot(renderedRoot: string): string {\n const safeRoot = assertRelativePath(renderedRoot, 'root');\n if (!APPROVED_RENDERED_ROOTS.has(safeRoot)) throw new Error('Root is not approved for rendered workspace access');\n return safeRoot;\n}\n\nasync function confinedWorkspacePath(\n root: string,\n workspacePath: string,\n): Promise<{ resolvedRoot: string; workspace: string }> {\n const resolvedRoot = await realOrResolved(resolveFsRoot(root));\n const candidate = isAbsolute(workspacePath) ? resolve(workspacePath) : resolve(resolvedRoot, workspacePath);\n const workspace = await realPathWithinRoot(candidate, resolvedRoot);\n if (!workspace) throw new Error('Path is outside the browsable root');\n return { resolvedRoot, workspace };\n}\n\nasync function confinedWorkspaceRelativePath(\n root: string,\n workspacePath: string,\n relativePath: string,\n): Promise<{ workspace: string; path: string; relativePath: string }> {\n const safeRelativePath = assertRelativePath(relativePath, 'path');\n const { workspace } = await confinedWorkspacePath(root, workspacePath);\n const candidate = resolve(workspace, safeRelativePath);\n if (!isWithinRoot(candidate, workspace)) throw new Error('Path escapes workspace');\n const confinedPath = await realPathWithinRoot(candidate, workspace);\n if (!confinedPath) throw new Error('Path is outside the workspace');\n return { workspace, path: confinedPath, relativePath: safeRelativePath };\n}\n\n/**\n * List the directories inside `requestedPath`, confined to `root`. An absent or\n * out-of-root path is clamped to the root, so the worst a malicious client can\n * do is browse within the allowed root.\n */\nexport async function listDirectory(root: string, requestedPath?: string): Promise<DirectoryListing> {\n // Resolve the root through symlinks so all confinement checks compare real\n // paths; a symlink that escapes the root is then reliably detectable.\n const resolvedRoot = await realOrResolved(resolveFsRoot(root));\n\n let target = resolvedRoot;\n if (requestedPath && requestedPath.trim()) {\n const candidate = isAbsolute(requestedPath) ? resolve(requestedPath) : resolve(resolvedRoot, requestedPath);\n // Follow symlinks and re-confirm the real target stays within the root.\n target = (await realPathWithinRoot(candidate, resolvedRoot)) ?? resolvedRoot;\n }\n\n // Confirm the target is a real directory; fall back to root otherwise.\n try {\n const info = await stat(target);\n if (!info.isDirectory()) target = resolvedRoot;\n } catch {\n target = resolvedRoot;\n }\n\n const dirents = await readdir(target, { withFileTypes: true });\n const entries: DirectoryEntry[] = [];\n for (const dirent of dirents) {\n if (dirent.name.startsWith('.')) continue; // skip dotfiles/dirs\n const entryPath = join(target, dirent.name);\n let isDir = dirent.isDirectory();\n if (dirent.isSymbolicLink()) {\n // Only surface symlinks whose real target is a directory inside the root,\n // so a link pointing outside the root can't be browsed or selected.\n const real = await realPathWithinRoot(entryPath, resolvedRoot);\n isDir = real ? (await stat(real).catch(() => null))?.isDirectory() === true : false;\n }\n if (isDir) entries.push({ name: dirent.name, path: entryPath });\n }\n entries.sort((a, b) => a.name.localeCompare(b.name));\n\n const parent = target === resolvedRoot ? null : resolve(target, '..');\n\n return { root: resolvedRoot, path: target, parent, entries };\n}\n\nasync function listRenderedEntries(rootPath: string, currentPath = rootPath): Promise<WorkspaceRenderedEntry[]> {\n const dirents = await readdir(currentPath, { withFileTypes: true });\n const entries: WorkspaceRenderedEntry[] = [];\n\n for (const dirent of dirents) {\n const entryPath = join(currentPath, dirent.name);\n const info = await lstat(entryPath);\n const relativePath = entryPath.slice(rootPath.length + 1);\n\n if (info.isDirectory()) {\n entries.push({\n name: dirent.name,\n path: relativePath,\n type: 'directory',\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n });\n entries.push(...(await listRenderedEntries(rootPath, entryPath)));\n continue;\n }\n\n if (info.isFile()) {\n entries.push({\n name: dirent.name,\n path: relativePath,\n type: 'file',\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n });\n }\n }\n\n return entries.sort((a, b) => a.path.localeCompare(b.path));\n}\n\nexport async function listWorkspaceRenderedPath(\n root: string,\n workspacePath: string,\n renderedRoot: string,\n): Promise<WorkspaceRenderedListing> {\n const safeRoot = assertApprovedRenderedRoot(renderedRoot);\n const { workspace } = await confinedWorkspacePath(root, workspacePath);\n const renderedPath = resolve(workspace, safeRoot);\n if (!isWithinRoot(renderedPath, workspace)) throw new Error('Root escapes workspace');\n\n const confinedRootPath = await realPathWithinRoot(renderedPath, workspace);\n if (!confinedRootPath) return { workspacePath: workspace, root: safeRoot, rootPath: renderedPath, entries: [] };\n\n const info = await stat(confinedRootPath);\n if (!info.isDirectory()) return { workspacePath: workspace, root: safeRoot, rootPath: confinedRootPath, entries: [] };\n\n return {\n workspacePath: workspace,\n root: safeRoot,\n rootPath: confinedRootPath,\n entries: await listRenderedEntries(confinedRootPath),\n };\n}\n\nexport async function readWorkspaceFile(root: string, workspacePath: string, path: string): Promise<WorkspaceFile> {\n const safePath = assertRelativePath(path, 'path');\n const relativeRoot = safePath.split('/')[0] ?? '';\n assertApprovedRenderedRoot(relativeRoot);\n const {\n workspace,\n path: confinedPath,\n relativePath,\n } = await confinedWorkspaceRelativePath(root, workspacePath, path);\n const info = await lstat(confinedPath);\n if (info.isDirectory()) throw new Error('Path is a directory');\n if (!info.isFile()) throw new Error('Unsupported file type');\n\n const bytesToRead = Math.min(info.size, MAX_TEXT_FILE_BYTES);\n const contentBuffer = Buffer.alloc(bytesToRead);\n const handle = await open(confinedPath, 'r');\n try {\n await handle.read(contentBuffer, 0, bytesToRead, 0);\n } finally {\n await handle.close();\n }\n\n try {\n const content = TEXT_DECODER.decode(contentBuffer);\n return {\n workspacePath: workspace,\n path: relativePath,\n name: relativePath.split('/').pop() ?? relativePath,\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n contentType: 'text',\n content,\n truncated: info.size > MAX_TEXT_FILE_BYTES,\n };\n } catch {\n return {\n workspacePath: workspace,\n path: relativePath,\n name: relativePath.split('/').pop() ?? relativePath,\n size: info.size,\n updatedAt: info.mtime.toISOString(),\n contentType: 'unsupported',\n };\n }\n}\n\nexport async function listArtifacts(root: string, workspacePath: string): Promise<ArtifactListing> {\n const listing = await listWorkspaceRenderedPath(root, workspacePath, '.artifacts');\n return {\n rootPath: listing.workspacePath,\n artifactsPath: listing.rootPath,\n entries: listing.entries,\n };\n}\n\n// ── Session-backed workspace access ──────────────────────────────────────────\n//\n// The web UI identifies a Factory session workspace by its session id (a UUID),\n// not by a server-local filesystem path — the session's files live inside the\n// session's sandbox (a remote VM on deployed factories). These helpers resolve\n// the session, enforce that the caller owns it, reattach to its sandbox, and\n// serve the approved rendered roots through `SandboxFilesystem`.\n\n/** Dependencies for resolving a `workspacePath` that is a Factory session id. */\nexport interface SessionFsDeps {\n auth: RouteAuth;\n fleet: SandboxFleet;\n sessions: { getBySessionId(sessionId: string): Promise<SourceControlSession | null> };\n}\n\n/**\n * Resolve a `workspacePath` query param as a Factory session id. Returns the\n * session when one exists and the caller owns it, `null` when no session\n * matches (the caller should fall back to local-path handling), and throws\n * when a session exists but belongs to another tenant.\n */\nasync function resolveAuthorizedSession(\n c: Context,\n deps: SessionFsDeps | undefined,\n workspacePath: string,\n): Promise<SourceControlSession | null> {\n if (!deps) return null;\n const session = await deps.sessions.getBySessionId(workspacePath);\n if (!session) return null;\n if (deps.auth.enabled()) {\n await deps.auth.ensureUser(c);\n const tenant = deps.auth.tenant(c);\n if (!tenant || tenant.orgId !== session.orgId || tenant.userId !== session.userId) {\n throw new Error('Session is not available to the current user');\n }\n }\n return session;\n}\n\ninterface SessionSandboxHandle {\n sandbox: { executeCommand(command: string, args?: string[], options?: { timeout?: number }): Promise<unknown> };\n filesystem: SandboxFilesystem;\n workdir: string;\n}\n\n/**\n * Reattach to the session's sandbox and wrap its workdir in a\n * `SandboxFilesystem`. Returns `null` when the session has no provisioned\n * sandbox yet (nothing materialized → nothing to list), or when the sandbox\n * can no longer be reattached (e.g. torn down by the provider's idle GC).\n * This is a passive read path, so it never re-provisions: the session's\n * filesystem is preserved in its provider checkpoint and comes back the next\n * time the workspace is actually opened (e.g. by sending a message).\n */\nasync function sessionSandbox(\n fleet: SandboxFleet,\n session: SourceControlSession,\n): Promise<SessionSandboxHandle | null> {\n if (!fleet.enabled || !session.sandboxId || !session.sandboxWorkdir) return null;\n let sandbox: Awaited<ReturnType<SandboxFleet['reattachSandbox']>>;\n try {\n sandbox = await fleet.reattachSandbox(session.sandboxId, { workingDirectory: session.sandboxWorkdir });\n } catch {\n // Sandbox is gone (idle GC) or unreachable. Degrade to an empty view\n // rather than surfacing a 500 from a file-viewer panel.\n return null;\n }\n return {\n sandbox,\n filesystem: new SandboxFilesystem({ sandbox, workdir: session.sandboxWorkdir }),\n workdir: session.sandboxWorkdir,\n };\n}\n\n/** List an approved rendered root inside a Factory session's sandbox workdir. */\nexport async function listSessionRenderedPath(\n fleet: SandboxFleet,\n session: SourceControlSession,\n renderedRoot: string,\n): Promise<WorkspaceRenderedListing> {\n const safeRoot = assertApprovedRenderedRoot(renderedRoot);\n const rootPath = posixPath.join(session.sandboxWorkdir ?? '', safeRoot);\n const empty: WorkspaceRenderedListing = { workspacePath: session.sessionId, root: safeRoot, rootPath, entries: [] };\n\n const handle = await sessionSandbox(fleet, session);\n if (!handle) return empty;\n\n // One round trip: emit \"type\\tsize\\tmtime\\tpath\" per entry. `safeRoot` comes\n // from a fixed allowlist so interpolating it (quoted) is safe.\n const quotedRoot = `'${rootPath.replace(/'/g, `'\\\\''`)}'`;\n const result = (await handle.sandbox.executeCommand(\n 'sh',\n [\n '-c',\n `test -d ${quotedRoot} && find ${quotedRoot} -mindepth 1 -printf '%y\\\\t%s\\\\t%T@\\\\t%p\\\\n' 2>/dev/null || true`,\n ],\n { timeout: 30_000 },\n )) as { exitCode: number; stdout: string };\n if (result.exitCode !== 0) return empty;\n\n const entries: WorkspaceRenderedEntry[] = [];\n for (const line of result.stdout.split('\\n')) {\n if (!line) continue;\n const [type, sizeStr, mtimeStr, ...pathParts] = line.split('\\t');\n const fullPath = pathParts.join('\\t');\n if (!fullPath || !fullPath.startsWith(`${rootPath}/`)) continue;\n const relativePath = fullPath.slice(rootPath.length + 1);\n entries.push({\n name: posixPath.basename(relativePath),\n path: relativePath,\n type: type === 'd' ? 'directory' : 'file',\n size: type === 'd' ? 0 : Number(sizeStr) || 0,\n updatedAt: new Date((Number(mtimeStr) || 0) * 1000).toISOString(),\n });\n }\n entries.sort((a, b) => a.path.localeCompare(b.path));\n\n return { workspacePath: session.sessionId, root: safeRoot, rootPath, entries };\n}\n\n/** Read a file under an approved rendered root inside a session's sandbox. */\nexport async function readSessionWorkspaceFile(\n fleet: SandboxFleet,\n session: SourceControlSession,\n path: string,\n): Promise<WorkspaceFile> {\n const safePath = assertRelativePath(path, 'path');\n assertApprovedRenderedRoot(safePath.split('/')[0] ?? '');\n\n const handle = await sessionSandbox(fleet, session);\n if (!handle) throw new Error('Session workspace is not available');\n const { filesystem } = handle;\n const info = await filesystem.stat(safePath);\n if (info.type === 'directory') throw new Error('Path is a directory');\n\n const buffer = (await filesystem.readFile(safePath)) as Buffer;\n const truncated = buffer.length > MAX_TEXT_FILE_BYTES;\n const base = {\n workspacePath: session.sessionId,\n path: safePath,\n name: posixPath.basename(safePath),\n size: buffer.length,\n updatedAt: info.modifiedAt.toISOString(),\n };\n try {\n const content = TEXT_DECODER.decode(truncated ? buffer.subarray(0, MAX_TEXT_FILE_BYTES) : buffer);\n return { ...base, contentType: 'text', content, truncated };\n } catch {\n return { ...base, contentType: 'unsupported' };\n }\n}\n\nexport interface ResolvedCodebase {\n /**\n * The resourceId the TUI would use for this path — derived identically so a\n * project opened in the terminal and in the web app resolve to the SAME\n * session (and therefore the same threads).\n */\n resourceId: string;\n name: string;\n rootPath: string;\n gitUrl?: string;\n gitBranch?: string;\n}\n\n/**\n * Resolve a project path to the same resourceId the TUI uses. Mirrors\n * `createMastraCode`: detect the project, then apply any resourceId override\n * (MASTRA_RESOURCE_ID env var or `.mastracode/database.json`). This is the\n * shared continuity point — start in the TUI, continue on the web, same path\n * → same resourceId → same session.\n */\nexport function resolveCodebase(projectPath: string): ResolvedCodebase {\n const info = detectProject(projectPath);\n const override = getResourceIdOverride(info.rootPath);\n return {\n resourceId: override ?? info.resourceId,\n name: info.name,\n rootPath: info.rootPath,\n gitUrl: info.gitUrl,\n gitBranch: info.gitBranch,\n };\n}\n\n/**\n * Build the web filesystem routes as Mastra `apiRoutes`:\n * - `GET /web/fs/list?path=...` — browse directories (confined to root)\n * - `GET /web/codebase/resolve?path=...` — TUI-compatible codebase resourceId\n */\nexport function buildFsRoutes(options: { root?: string; sessionFs?: SessionFsDeps } = {}): ApiRoute[] {\n const root = resolveFsRoot(options.root);\n const sessionFs = options.sessionFs;\n\n return [\n registerApiRoute('/web/fs/list', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const path = c.req.query('path');\n try {\n const listing = await listDirectory(root, path);\n return c.json(listing);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n },\n }),\n registerApiRoute('/web/artifacts/list', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const path = c.req.query('path');\n if (!path) return c.json({ error: 'Missing required query param: path' }, 400);\n try {\n return c.json(await listArtifacts(root, path));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status = message === 'Path is outside the browsable root' ? 403 : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/workspace/rendered/list', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const workspacePath = c.req.query('workspacePath');\n const renderedRoot = c.req.query('root');\n if (!workspacePath) return c.json({ error: 'Missing required query param: workspacePath' }, 400);\n if (!renderedRoot) return c.json({ error: 'Missing required query param: root' }, 400);\n try {\n const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);\n if (session && sessionFs) {\n return c.json(await listSessionRenderedPath(sessionFs.fleet, session, renderedRoot));\n }\n return c.json(await listWorkspaceRenderedPath(root, workspacePath, renderedRoot));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status =\n message.includes('outside') ||\n message.includes('relative') ||\n message.includes('escapes') ||\n message.includes('not approved') ||\n message.includes('not available')\n ? 403\n : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/workspace/file', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const workspacePath = c.req.query('workspacePath');\n const path = c.req.query('path');\n if (!workspacePath) return c.json({ error: 'Missing required query param: workspacePath' }, 400);\n if (!path) return c.json({ error: 'Missing required query param: path' }, 400);\n try {\n const session = await resolveAuthorizedSession(loose(c), sessionFs, workspacePath);\n if (session && sessionFs) {\n return c.json(await readSessionWorkspaceFile(sessionFs.fleet, session, path));\n }\n return c.json(await readWorkspaceFile(root, workspacePath, path));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const status =\n message.includes('outside') ||\n message.includes('relative') ||\n message.includes('escapes') ||\n message.includes('not approved') ||\n message.includes('not available')\n ? 403\n : message.includes('directory')\n ? 400\n : message.includes('not found')\n ? 404\n : 500;\n return c.json({ error: message }, status);\n }\n },\n }),\n registerApiRoute('/web/codebase/resolve', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const path = c.req.query('path');\n if (!path) return c.json({ error: 'Missing required query param: path' }, 400);\n // Confine resolution to the browsable root (following symlinks), so this\n // endpoint can't be used to probe arbitrary filesystem paths. The web UI\n // only ever resolves directories the user picked via the root-confined\n // browser, so legitimate requests are always within the root.\n const confined = await realPathWithinRoot(isAbsolute(path) ? resolve(path) : resolve(root, path), root);\n if (!confined) return c.json({ error: 'Path is outside the browsable root' }, 403);\n try {\n return c.json(resolveCodebase(confined));\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return c.json({ error: message }, 500);\n }\n },\n }),\n ];\n}\n"],"mappings":";;;;;;;AAsFA,MAAM,sBAAsB,MAAM;AAClC,MAAM,eAAe,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC;AAC7D,MAAM,0CAA0B,IAAI,IAAI,CAAC,YAAY,CAAC;;AAGtD,SAAS,MAAM,GAAqB;CAClC,OAAO;AACT;;AAGA,SAAgB,cAAc,MAAuB;CACnD,OAAO,QAAQ,QAAQ,KAAK,KAAK,IAAI,OAAO,QAAQ,CAAC;AACvD;;AAGA,SAAS,aAAa,WAAmB,MAAuB;CAC9D,IAAI,cAAc,MAAM,OAAO;CAC/B,MAAM,cAAc,KAAK,SAAS,GAAG,IAAI,OAAO,OAAO;CACvD,OAAO,UAAU,WAAW,WAAW;AACzC;AAEA,eAAe,eAAe,MAA+B;CAC3D,IAAI;EACF,OAAO,MAAM,SAAS,IAAI;CAC5B,QAAQ;EACN,OAAO;CACT;AACF;;;;;;;AAQA,eAAe,mBAAmB,WAAmB,MAAsC;CACzF,IAAI;EACF,MAAM,OAAO,MAAM,SAAS,SAAS;EACrC,OAAO,aAAa,MAAM,IAAI,IAAI,OAAO;CAC3C,QAAQ;EACN,OAAO;CACT;AACF;AAEA,SAAS,mBAAmB,MAAc,OAAuB;CAC/D,MAAM,UAAU,KAAK,KAAK;CAC1B,IAAI,CAAC,SAAS,MAAM,IAAI,MAAM,iCAAiC,OAAO;CACtE,IAAI,WAAW,OAAO,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,kBAAkB;CACpE,IAAI,QAAQ,MAAM,QAAQ,CAAC,CAAC,SAAS,IAAI,GAAG,MAAM,IAAI,MAAM,GAAG,MAAM,mBAAmB;CACxF,MAAM,aAAa,QAAQ,KAAK,OAAO,CAAC,CAAC,MAAM,CAAC;CAChD,IAAI,CAAC,cAAc,eAAe,QAAQ,WAAW,WAAW,KAAK,KAAK,GACxE,MAAM,IAAI,MAAM,GAAG,MAAM,mBAAmB;CAC9C,OAAO;AACT;AAEA,SAAS,2BAA2B,cAA8B;CAChE,MAAM,WAAW,mBAAmB,cAAc,MAAM;CACxD,IAAI,CAAC,wBAAwB,IAAI,QAAQ,GAAG,MAAM,IAAI,MAAM,oDAAoD;CAChH,OAAO;AACT;AAEA,eAAe,sBACb,MACA,eACsD;CACtD,MAAM,eAAe,MAAM,eAAe,cAAc,IAAI,CAAC;CAE7D,MAAM,YAAY,MAAM,mBADN,WAAW,aAAa,IAAI,QAAQ,aAAa,IAAI,QAAQ,cAAc,aAAa,GACpD,YAAY;CAClE,IAAI,CAAC,WAAW,MAAM,IAAI,MAAM,oCAAoC;CACpE,OAAO;EAAE;EAAc;CAAU;AACnC;AAEA,eAAe,8BACb,MACA,eACA,cACoE;CACpE,MAAM,mBAAmB,mBAAmB,cAAc,MAAM;CAChE,MAAM,EAAE,cAAc,MAAM,sBAAsB,MAAM,aAAa;CACrE,MAAM,YAAY,QAAQ,WAAW,gBAAgB;CACrD,IAAI,CAAC,aAAa,WAAW,SAAS,GAAG,MAAM,IAAI,MAAM,wBAAwB;CACjF,MAAM,eAAe,MAAM,mBAAmB,WAAW,SAAS;CAClE,IAAI,CAAC,cAAc,MAAM,IAAI,MAAM,+BAA+B;CAClE,OAAO;EAAE;EAAW,MAAM;EAAc,cAAc;CAAiB;AACzE;;;;;;AAOA,eAAsB,cAAc,MAAc,eAAmD;CAGnG,MAAM,eAAe,MAAM,eAAe,cAAc,IAAI,CAAC;CAE7D,IAAI,SAAS;CACb,IAAI,iBAAiB,cAAc,KAAK,GAGtC,SAAU,MAAM,mBAFE,WAAW,aAAa,IAAI,QAAQ,aAAa,IAAI,QAAQ,cAAc,aAAa,GAE5D,YAAY,KAAM;CAIlE,IAAI;EAEF,IAAI,EAAC,MADc,KAAK,MAAM,EAAA,CACpB,YAAY,GAAG,SAAS;CACpC,QAAQ;EACN,SAAS;CACX;CAEA,MAAM,UAAU,MAAM,QAAQ,QAAQ,EAAE,eAAe,KAAK,CAAC;CAC7D,MAAM,UAA4B,CAAC;CACnC,KAAK,MAAM,UAAU,SAAS;EAC5B,IAAI,OAAO,KAAK,WAAW,GAAG,GAAG;EACjC,MAAM,YAAY,KAAK,QAAQ,OAAO,IAAI;EAC1C,IAAI,QAAQ,OAAO,YAAY;EAC/B,IAAI,OAAO,eAAe,GAAG;GAG3B,MAAM,OAAO,MAAM,mBAAmB,WAAW,YAAY;GAC7D,QAAQ,QAAQ,MAAM,KAAK,IAAI,CAAC,CAAC,YAAY,IAAI,EAAA,EAAI,YAAY,MAAM,OAAO;EAChF;EACA,IAAI,OAAO,QAAQ,KAAK;GAAE,MAAM,OAAO;GAAM,MAAM;EAAU,CAAC;CAChE;CACA,QAAQ,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;CAEnD,MAAM,SAAS,WAAW,eAAe,OAAO,QAAQ,QAAQ,IAAI;CAEpE,OAAO;EAAE,MAAM;EAAc,MAAM;EAAQ;EAAQ;CAAQ;AAC7D;AAEA,eAAe,oBAAoB,UAAkB,cAAc,UAA6C;CAC9G,MAAM,UAAU,MAAM,QAAQ,aAAa,EAAE,eAAe,KAAK,CAAC;CAClE,MAAM,UAAoC,CAAC;CAE3C,KAAK,MAAM,UAAU,SAAS;EAC5B,MAAM,YAAY,KAAK,aAAa,OAAO,IAAI;EAC/C,MAAM,OAAO,MAAM,MAAM,SAAS;EAClC,MAAM,eAAe,UAAU,MAAM,SAAS,SAAS,CAAC;EAExD,IAAI,KAAK,YAAY,GAAG;GACtB,QAAQ,KAAK;IACX,MAAM,OAAO;IACb,MAAM;IACN,MAAM;IACN,MAAM,KAAK;IACX,WAAW,KAAK,MAAM,YAAY;GACpC,CAAC;GACD,QAAQ,KAAK,GAAI,MAAM,oBAAoB,UAAU,SAAS,CAAE;GAChE;EACF;EAEA,IAAI,KAAK,OAAO,GACd,QAAQ,KAAK;GACX,MAAM,OAAO;GACb,MAAM;GACN,MAAM;GACN,MAAM,KAAK;GACX,WAAW,KAAK,MAAM,YAAY;EACpC,CAAC;CAEL;CAEA,OAAO,QAAQ,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;AAC5D;AAEA,eAAsB,0BACpB,MACA,eACA,cACmC;CACnC,MAAM,WAAW,2BAA2B,YAAY;CACxD,MAAM,EAAE,cAAc,MAAM,sBAAsB,MAAM,aAAa;CACrE,MAAM,eAAe,QAAQ,WAAW,QAAQ;CAChD,IAAI,CAAC,aAAa,cAAc,SAAS,GAAG,MAAM,IAAI,MAAM,wBAAwB;CAEpF,MAAM,mBAAmB,MAAM,mBAAmB,cAAc,SAAS;CACzE,IAAI,CAAC,kBAAkB,OAAO;EAAE,eAAe;EAAW,MAAM;EAAU,UAAU;EAAc,SAAS,CAAC;CAAE;CAG9G,IAAI,EAAC,MADc,KAAK,gBAAgB,EAAA,CAC9B,YAAY,GAAG,OAAO;EAAE,eAAe;EAAW,MAAM;EAAU,UAAU;EAAkB,SAAS,CAAC;CAAE;CAEpH,OAAO;EACL,eAAe;EACf,MAAM;EACN,UAAU;EACV,SAAS,MAAM,oBAAoB,gBAAgB;CACrD;AACF;AAEA,eAAsB,kBAAkB,MAAc,eAAuB,MAAsC;CAGjH,2BAFiB,mBAAmB,MAAM,MACd,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,EACR;CACvC,MAAM,EACJ,WACA,MAAM,cACN,iBACE,MAAM,8BAA8B,MAAM,eAAe,IAAI;CACjE,MAAM,OAAO,MAAM,MAAM,YAAY;CACrC,IAAI,KAAK,YAAY,GAAG,MAAM,IAAI,MAAM,qBAAqB;CAC7D,IAAI,CAAC,KAAK,OAAO,GAAG,MAAM,IAAI,MAAM,uBAAuB;CAE3D,MAAM,cAAc,KAAK,IAAI,KAAK,MAAM,mBAAmB;CAC3D,MAAM,gBAAgB,OAAO,MAAM,WAAW;CAC9C,MAAM,SAAS,MAAM,KAAK,cAAc,GAAG;CAC3C,IAAI;EACF,MAAM,OAAO,KAAK,eAAe,GAAG,aAAa,CAAC;CACpD,UAAU;EACR,MAAM,OAAO,MAAM;CACrB;CAEA,IAAI;EACF,MAAM,UAAU,aAAa,OAAO,aAAa;EACjD,OAAO;GACL,eAAe;GACf,MAAM;GACN,MAAM,aAAa,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;GACvC,MAAM,KAAK;GACX,WAAW,KAAK,MAAM,YAAY;GAClC,aAAa;GACb;GACA,WAAW,KAAK,OAAO;EACzB;CACF,QAAQ;EACN,OAAO;GACL,eAAe;GACf,MAAM;GACN,MAAM,aAAa,MAAM,GAAG,CAAC,CAAC,IAAI,KAAK;GACvC,MAAM,KAAK;GACX,WAAW,KAAK,MAAM,YAAY;GAClC,aAAa;EACf;CACF;AACF;AAEA,eAAsB,cAAc,MAAc,eAAiD;CACjG,MAAM,UAAU,MAAM,0BAA0B,MAAM,eAAe,YAAY;CACjF,OAAO;EACL,UAAU,QAAQ;EAClB,eAAe,QAAQ;EACvB,SAAS,QAAQ;CACnB;AACF;;;;;;;AAuBA,eAAe,yBACb,GACA,MACA,eACsC;CACtC,IAAI,CAAC,MAAM,OAAO;CAClB,MAAM,UAAU,MAAM,KAAK,SAAS,eAAe,aAAa;CAChE,IAAI,CAAC,SAAS,OAAO;CACrB,IAAI,KAAK,KAAK,QAAQ,GAAG;EACvB,MAAM,KAAK,KAAK,WAAW,CAAC;EAC5B,MAAM,SAAS,KAAK,KAAK,OAAO,CAAC;EACjC,IAAI,CAAC,UAAU,OAAO,UAAU,QAAQ,SAAS,OAAO,WAAW,QAAQ,QACzE,MAAM,IAAI,MAAM,8CAA8C;CAElE;CACA,OAAO;AACT;;;;;;;;;;AAiBA,eAAe,eACb,OACA,SACsC;CACtC,IAAI,CAAC,MAAM,WAAW,CAAC,QAAQ,aAAa,CAAC,QAAQ,gBAAgB,OAAO;CAC5E,IAAI;CACJ,IAAI;EACF,UAAU,MAAM,MAAM,gBAAgB,QAAQ,WAAW,EAAE,kBAAkB,QAAQ,eAAe,CAAC;CACvG,QAAQ;EAGN,OAAO;CACT;CACA,OAAO;EACL;EACA,YAAY,IAAI,kBAAkB;GAAE;GAAS,SAAS,QAAQ;EAAe,CAAC;EAC9E,SAAS,QAAQ;CACnB;AACF;;AAGA,eAAsB,wBACpB,OACA,SACA,cACmC;CACnC,MAAM,WAAW,2BAA2B,YAAY;CACxD,MAAM,WAAWA,MAAU,KAAK,QAAQ,kBAAkB,IAAI,QAAQ;CACtE,MAAM,QAAkC;EAAE,eAAe,QAAQ;EAAW,MAAM;EAAU;EAAU,SAAS,CAAC;CAAE;CAElH,MAAM,SAAS,MAAM,eAAe,OAAO,OAAO;CAClD,IAAI,CAAC,QAAQ,OAAO;CAIpB,MAAM,aAAa,IAAI,SAAS,QAAQ,MAAM,OAAO,EAAE;CACvD,MAAM,SAAU,MAAM,OAAO,QAAQ,eACnC,MACA,CACE,MACA,WAAW,WAAW,WAAW,WAAW,iEAC9C,GACA,EAAE,SAAS,IAAO,CACpB;CACA,IAAI,OAAO,aAAa,GAAG,OAAO;CAElC,MAAM,UAAoC,CAAC;CAC3C,KAAK,MAAM,QAAQ,OAAO,OAAO,MAAM,IAAI,GAAG;EAC5C,IAAI,CAAC,MAAM;EACX,MAAM,CAAC,MAAM,SAAS,UAAU,GAAG,aAAa,KAAK,MAAM,GAAI;EAC/D,MAAM,WAAW,UAAU,KAAK,GAAI;EACpC,IAAI,CAAC,YAAY,CAAC,SAAS,WAAW,GAAG,SAAS,EAAE,GAAG;EACvD,MAAM,eAAe,SAAS,MAAM,SAAS,SAAS,CAAC;EACvD,QAAQ,KAAK;GACX,MAAMA,MAAU,SAAS,YAAY;GACrC,MAAM;GACN,MAAM,SAAS,MAAM,cAAc;GACnC,MAAM,SAAS,MAAM,IAAI,OAAO,OAAO,KAAK;GAC5C,4BAAW,IAAI,MAAM,OAAO,QAAQ,KAAK,KAAK,GAAI,EAAA,CAAE,YAAY;EAClE,CAAC;CACH;CACA,QAAQ,MAAM,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CAAC;CAEnD,OAAO;EAAE,eAAe,QAAQ;EAAW,MAAM;EAAU;EAAU;CAAQ;AAC/E;;AAGA,eAAsB,yBACpB,OACA,SACA,MACwB;CACxB,MAAM,WAAW,mBAAmB,MAAM,MAAM;CAChD,2BAA2B,SAAS,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE;CAEvD,MAAM,SAAS,MAAM,eAAe,OAAO,OAAO;CAClD,IAAI,CAAC,QAAQ,MAAM,IAAI,MAAM,oCAAoC;CACjE,MAAM,EAAE,eAAe;CACvB,MAAM,OAAO,MAAM,WAAW,KAAK,QAAQ;CAC3C,IAAI,KAAK,SAAS,aAAa,MAAM,IAAI,MAAM,qBAAqB;CAEpE,MAAM,SAAU,MAAM,WAAW,SAAS,QAAQ;CAClD,MAAM,YAAY,OAAO,SAAS;CAClC,MAAM,OAAO;EACX,eAAe,QAAQ;EACvB,MAAM;EACN,MAAMA,MAAU,SAAS,QAAQ;EACjC,MAAM,OAAO;EACb,WAAW,KAAK,WAAW,YAAY;CACzC;CACA,IAAI;EACF,MAAM,UAAU,aAAa,OAAO,YAAY,OAAO,SAAS,GAAG,mBAAmB,IAAI,MAAM;EAChG,OAAO;GAAE,GAAG;GAAM,aAAa;GAAQ;GAAS;EAAU;CAC5D,QAAQ;EACN,OAAO;GAAE,GAAG;GAAM,aAAa;EAAc;CAC/C;AACF;;;;;;;;AAsBA,SAAgB,gBAAgB,aAAuC;CACrE,MAAM,OAAO,cAAc,WAAW;CAEtC,OAAO;EACL,YAFe,sBAAsB,KAAK,QAEvB,KAAK,KAAK;EAC7B,MAAM,KAAK;EACX,UAAU,KAAK;EACf,QAAQ,KAAK;EACb,WAAW,KAAK;CAClB;AACF;;;;;;AAOA,SAAgB,cAAc,UAAwD,CAAC,GAAe;CACpG,MAAM,OAAO,cAAc,QAAQ,IAAI;CACvC,MAAM,YAAY,QAAQ;CAE1B,OAAO;EACL,iBAAiB,gBAAgB;GAC/B,QAAQ;GACR,cAAc;GACd,SAAS,OAAM,MAAK;IAClB,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;IAC/B,IAAI;KACF,MAAM,UAAU,MAAM,cAAc,MAAM,IAAI;KAC9C,OAAO,EAAE,KAAK,OAAO;IACvB,SAAS,OAAO;KACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;KACrE,OAAO,EAAE,KAAK,EAAE,OAAO,QAAQ,GAAG,GAAG;IACvC;GACF;EACF,CAAC;EACD,iBAAiB,uBAAuB;GACtC,QAAQ;GACR,cAAc;GACd,SAAS,OAAM,MAAK;IAClB,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;IAC/B,IAAI,CAAC,MAAM,OAAO,EAAE,KAAK,EAAE,OAAO,qCAAqC,GAAG,GAAG;IAC7E,IAAI;KACF,OAAO,EAAE,KAAK,MAAM,cAAc,MAAM,IAAI,CAAC;IAC/C,SAAS,OAAO;KACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;KACrE,MAAM,SAAS,YAAY,uCAAuC,MAAM;KACxE,OAAO,EAAE,KAAK,EAAE,OAAO,QAAQ,GAAG,MAAM;IAC1C;GACF;EACF,CAAC;EACD,iBAAiB,gCAAgC;GAC/C,QAAQ;GACR,cAAc;GACd,SAAS,OAAM,MAAK;IAClB,MAAM,gBAAgB,EAAE,IAAI,MAAM,eAAe;IACjD,MAAM,eAAe,EAAE,IAAI,MAAM,MAAM;IACvC,IAAI,CAAC,eAAe,OAAO,EAAE,KAAK,EAAE,OAAO,8CAA8C,GAAG,GAAG;IAC/F,IAAI,CAAC,cAAc,OAAO,EAAE,KAAK,EAAE,OAAO,qCAAqC,GAAG,GAAG;IACrF,IAAI;KACF,MAAM,UAAU,MAAM,yBAAyB,MAAM,CAAC,GAAG,WAAW,aAAa;KACjF,IAAI,WAAW,WACb,OAAO,EAAE,KAAK,MAAM,wBAAwB,UAAU,OAAO,SAAS,YAAY,CAAC;KAErF,OAAO,EAAE,KAAK,MAAM,0BAA0B,MAAM,eAAe,YAAY,CAAC;IAClF,SAAS,OAAO;KACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;KACrE,MAAM,SACJ,QAAQ,SAAS,SAAS,KAC1B,QAAQ,SAAS,UAAU,KAC3B,QAAQ,SAAS,SAAS,KAC1B,QAAQ,SAAS,cAAc,KAC/B,QAAQ,SAAS,eAAe,IAC5B,MACA;KACN,OAAO,EAAE,KAAK,EAAE,OAAO,QAAQ,GAAG,MAAM;IAC1C;GACF;EACF,CAAC;EACD,iBAAiB,uBAAuB;GACtC,QAAQ;GACR,cAAc;GACd,SAAS,OAAM,MAAK;IAClB,MAAM,gBAAgB,EAAE,IAAI,MAAM,eAAe;IACjD,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;IAC/B,IAAI,CAAC,eAAe,OAAO,EAAE,KAAK,EAAE,OAAO,8CAA8C,GAAG,GAAG;IAC/F,IAAI,CAAC,MAAM,OAAO,EAAE,KAAK,EAAE,OAAO,qCAAqC,GAAG,GAAG;IAC7E,IAAI;KACF,MAAM,UAAU,MAAM,yBAAyB,MAAM,CAAC,GAAG,WAAW,aAAa;KACjF,IAAI,WAAW,WACb,OAAO,EAAE,KAAK,MAAM,yBAAyB,UAAU,OAAO,SAAS,IAAI,CAAC;KAE9E,OAAO,EAAE,KAAK,MAAM,kBAAkB,MAAM,eAAe,IAAI,CAAC;IAClE,SAAS,OAAO;KACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;KACrE,MAAM,SACJ,QAAQ,SAAS,SAAS,KAC1B,QAAQ,SAAS,UAAU,KAC3B,QAAQ,SAAS,SAAS,KAC1B,QAAQ,SAAS,cAAc,KAC/B,QAAQ,SAAS,eAAe,IAC5B,MACA,QAAQ,SAAS,WAAW,IAC1B,MACA,QAAQ,SAAS,WAAW,IAC1B,MACA;KACV,OAAO,EAAE,KAAK,EAAE,OAAO,QAAQ,GAAG,MAAM;IAC1C;GACF;EACF,CAAC;EACD,iBAAiB,yBAAyB;GACxC,QAAQ;GACR,cAAc;GACd,SAAS,OAAM,MAAK;IAClB,MAAM,OAAO,EAAE,IAAI,MAAM,MAAM;IAC/B,IAAI,CAAC,MAAM,OAAO,EAAE,KAAK,EAAE,OAAO,qCAAqC,GAAG,GAAG;IAK7E,MAAM,WAAW,MAAM,mBAAmB,WAAW,IAAI,IAAI,QAAQ,IAAI,IAAI,QAAQ,MAAM,IAAI,GAAG,IAAI;IACtG,IAAI,CAAC,UAAU,OAAO,EAAE,KAAK,EAAE,OAAO,qCAAqC,GAAG,GAAG;IACjF,IAAI;KACF,OAAO,EAAE,KAAK,gBAAgB,QAAQ,CAAC;IACzC,SAAS,OAAO;KACd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;KACrE,OAAO,EAAE,KAAK,EAAE,OAAO,QAAQ,GAAG,GAAG;IACvC;GACF;EACF,CAAC;CACH;AACF"}
|
package/dist/routes/intake.js
CHANGED
|
@@ -1,179 +1,179 @@
|
|
|
1
|
-
|
|
1
|
+
import { Route } from "./route.js";
|
|
2
2
|
import { registerApiRoute } from "@mastra/core/server";
|
|
3
|
-
|
|
4
|
-
// src/routes/route.ts
|
|
5
|
-
var Route = class {
|
|
6
|
-
deps;
|
|
7
|
-
constructor(deps) {
|
|
8
|
-
this.deps = deps;
|
|
9
|
-
}
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
// src/routes/intake.ts
|
|
3
|
+
//#region src/routes/intake.ts
|
|
13
4
|
function loose(c) {
|
|
14
|
-
|
|
5
|
+
return c;
|
|
15
6
|
}
|
|
16
7
|
function sanitizeIdList(value) {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
8
|
+
if (value === null) return null;
|
|
9
|
+
if (!Array.isArray(value) || value.length > 200) return void 0;
|
|
10
|
+
const ids = value.filter((item) => typeof item === "string" && item.length > 0 && item.length <= 256);
|
|
11
|
+
return ids.length === value.length && new Set(ids).size === ids.length ? ids : void 0;
|
|
21
12
|
}
|
|
13
|
+
/** Validate a request body into an intake config, rejecting unknown shapes. */
|
|
22
14
|
function parseIntakeConfig(body) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
15
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return null;
|
|
16
|
+
const entries = Object.entries(body);
|
|
17
|
+
if (entries.length > 50) return null;
|
|
18
|
+
const config = {};
|
|
19
|
+
for (const [integrationId, value] of entries) {
|
|
20
|
+
if (!integrationId || integrationId.length > 128 || typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
21
|
+
const selection = value;
|
|
22
|
+
if (typeof selection.enabled !== "boolean") return null;
|
|
23
|
+
const sourceIds = sanitizeIdList(selection.sourceIds ?? null);
|
|
24
|
+
if (sourceIds === void 0) return null;
|
|
25
|
+
config[integrationId] = {
|
|
26
|
+
enabled: selection.enabled,
|
|
27
|
+
sourceIds
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return config;
|
|
38
31
|
}
|
|
39
32
|
function encodeCursor(cursors) {
|
|
40
|
-
|
|
33
|
+
return Object.keys(cursors).length > 0 ? Buffer.from(JSON.stringify(cursors)).toString("base64url") : null;
|
|
41
34
|
}
|
|
42
35
|
function decodeCursor(value) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
36
|
+
if (!value) return {};
|
|
37
|
+
try {
|
|
38
|
+
const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
39
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
|
|
40
|
+
const entries = Object.entries(parsed);
|
|
41
|
+
if (entries.some(([key, cursor]) => !key || typeof cursor !== "string")) return null;
|
|
42
|
+
return Object.fromEntries(entries);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
53
46
|
}
|
|
54
47
|
var IntakeRoutes = class extends Route {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
48
|
+
async #resolveTenant(c) {
|
|
49
|
+
await this.deps.auth.ensureUser(c);
|
|
50
|
+
const tenant = this.deps.auth.tenant(c);
|
|
51
|
+
if (!tenant) return { response: c.json({ error: "unauthorized" }, 401) };
|
|
52
|
+
if (!tenant.orgId) return { response: c.json({
|
|
53
|
+
error: "organization_required",
|
|
54
|
+
message: "Intake configuration requires an organization."
|
|
55
|
+
}, 403) };
|
|
56
|
+
return {
|
|
57
|
+
orgId: tenant.orgId,
|
|
58
|
+
userId: tenant.userId
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
routes() {
|
|
62
|
+
const { audit, intake, integrations = [] } = this.deps;
|
|
63
|
+
const integrationIds = integrations.map((integration) => integration.id);
|
|
64
|
+
return [
|
|
65
|
+
registerApiRoute("/web/intake/config", {
|
|
66
|
+
method: "GET",
|
|
67
|
+
requiresAuth: false,
|
|
68
|
+
handler: async (c) => {
|
|
69
|
+
const tenant = await this.#resolveTenant(loose(c));
|
|
70
|
+
if ("response" in tenant) return tenant.response;
|
|
71
|
+
await intake.ensureReady();
|
|
72
|
+
const config = await intake.getConfig({
|
|
73
|
+
...tenant,
|
|
74
|
+
integrationIds
|
|
75
|
+
});
|
|
76
|
+
return c.json({ config });
|
|
77
|
+
}
|
|
78
|
+
}),
|
|
79
|
+
registerApiRoute("/web/intake/config", {
|
|
80
|
+
method: "PUT",
|
|
81
|
+
requiresAuth: false,
|
|
82
|
+
handler: async (c) => {
|
|
83
|
+
const tenant = await this.#resolveTenant(loose(c));
|
|
84
|
+
if ("response" in tenant) return tenant.response;
|
|
85
|
+
let body;
|
|
86
|
+
try {
|
|
87
|
+
body = await c.req.json();
|
|
88
|
+
} catch {
|
|
89
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
90
|
+
}
|
|
91
|
+
const config = parseIntakeConfig(body);
|
|
92
|
+
if (!config || Object.keys(config).some((integrationId) => !integrationIds.includes(integrationId))) return c.json({ error: "invalid_config" }, 400);
|
|
93
|
+
await intake.ensureReady();
|
|
94
|
+
await intake.saveConfig({
|
|
95
|
+
...tenant,
|
|
96
|
+
config
|
|
97
|
+
});
|
|
98
|
+
await audit.emit({
|
|
99
|
+
context: loose(c),
|
|
100
|
+
input: {
|
|
101
|
+
action: "factory.intake.config_updated",
|
|
102
|
+
targets: [{
|
|
103
|
+
type: "intake_config",
|
|
104
|
+
id: tenant.orgId
|
|
105
|
+
}],
|
|
106
|
+
metadata: Object.fromEntries(Object.entries(config).map(([integrationId, selection]) => [integrationId, {
|
|
107
|
+
enabled: selection.enabled,
|
|
108
|
+
sources: selection.sourceIds?.length ?? null
|
|
109
|
+
}]))
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
return c.json({ config });
|
|
113
|
+
}
|
|
114
|
+
}),
|
|
115
|
+
registerApiRoute("/web/intake/sources", {
|
|
116
|
+
method: "GET",
|
|
117
|
+
requiresAuth: false,
|
|
118
|
+
handler: async (c) => {
|
|
119
|
+
const tenant = await this.#resolveTenant(loose(c));
|
|
120
|
+
if ("response" in tenant) return tenant.response;
|
|
121
|
+
const pages = await Promise.all(integrations.map(async (integration) => ({
|
|
122
|
+
integrationId: integration.id,
|
|
123
|
+
sources: await integration.intake.listSources(tenant)
|
|
124
|
+
})));
|
|
125
|
+
return c.json({ sources: pages.flatMap((page) => page.sources.map((source) => ({
|
|
126
|
+
integrationId: page.integrationId,
|
|
127
|
+
...source
|
|
128
|
+
}))) });
|
|
129
|
+
}
|
|
130
|
+
}),
|
|
131
|
+
registerApiRoute("/web/intake/items", {
|
|
132
|
+
method: "GET",
|
|
133
|
+
requiresAuth: false,
|
|
134
|
+
handler: async (c) => {
|
|
135
|
+
const tenant = await this.#resolveTenant(loose(c));
|
|
136
|
+
if ("response" in tenant) return tenant.response;
|
|
137
|
+
const cursors = decodeCursor(c.req.query("cursor"));
|
|
138
|
+
if (!cursors) return c.json({ error: "invalid_cursor" }, 400);
|
|
139
|
+
await intake.ensureReady();
|
|
140
|
+
const config = await intake.getConfig({
|
|
141
|
+
...tenant,
|
|
142
|
+
integrationIds
|
|
143
|
+
});
|
|
144
|
+
const items = [];
|
|
145
|
+
const nextCursors = {};
|
|
146
|
+
for (const integration of integrations) {
|
|
147
|
+
const selection = config[integration.id];
|
|
148
|
+
if (!selection?.enabled || !selection.sourceIds?.length) continue;
|
|
149
|
+
const page = await integration.intake.listItems({
|
|
150
|
+
...tenant,
|
|
151
|
+
sourceIds: selection.sourceIds,
|
|
152
|
+
...cursors[integration.id] ? { cursor: cursors[integration.id] } : {}
|
|
153
|
+
});
|
|
154
|
+
items.push(...page.items.map((item) => {
|
|
155
|
+
const { source, ...candidate } = item;
|
|
156
|
+
return {
|
|
157
|
+
...candidate,
|
|
158
|
+
integrationId: integration.id,
|
|
159
|
+
externalSource: {
|
|
160
|
+
integrationId: integration.id,
|
|
161
|
+
...source
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
}));
|
|
165
|
+
if (page.nextCursor) nextCursors[integration.id] = page.nextCursor;
|
|
166
|
+
}
|
|
167
|
+
return c.json({
|
|
168
|
+
items,
|
|
169
|
+
nextCursor: encodeCursor(nextCursors)
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
];
|
|
174
|
+
}
|
|
178
175
|
};
|
|
176
|
+
//#endregion
|
|
177
|
+
export { IntakeRoutes, parseIntakeConfig };
|
|
178
|
+
|
|
179
179
|
//# sourceMappingURL=intake.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/routes/intake.ts","../../src/routes/route.ts"],"sourcesContent":["import type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { Intake, IntakeItem } from '../capabilities/intake.js';\nimport type { AuditEmitter } from '../storage/domains/audit/domain.js';\nimport type { IntakeConfig, IntakeStorage } from '../storage/domains/intake/base.js';\nimport type { RouteDependencies } from './route.js';\nimport { Route } from './route.js';\n\nexport interface IntakeIntegration {\n id: string;\n intake: Pick<Intake, 'listSources' | 'listItems'>;\n}\n\ninterface AggregatedIntakeItem extends Omit<IntakeItem, 'source'> {\n integrationId: string;\n externalSource: {\n integrationId: string;\n type: string;\n externalId: string;\n url?: string;\n };\n}\n\nexport interface IntakeRoutesDeps extends RouteDependencies {\n audit: AuditEmitter;\n /** Intake selection domain handle. */\n intake: IntakeStorage;\n integrations?: IntakeIntegration[];\n}\n\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\nfunction sanitizeIdList(value: unknown): string[] | null | undefined {\n if (value === null) return null;\n if (!Array.isArray(value) || value.length > 200) return undefined;\n const ids = value.filter((item): item is string => typeof item === 'string' && item.length > 0 && item.length <= 256);\n return ids.length === value.length && new Set(ids).size === ids.length ? ids : undefined;\n}\n\n/** Validate a request body into an intake config, rejecting unknown shapes. */\nexport function parseIntakeConfig(body: unknown): IntakeConfig | null {\n if (typeof body !== 'object' || body === null || Array.isArray(body)) return null;\n const entries = Object.entries(body);\n if (entries.length > 50) return null;\n\n const config: IntakeConfig = {};\n for (const [integrationId, value] of entries) {\n if (\n !integrationId ||\n integrationId.length > 128 ||\n typeof value !== 'object' ||\n value === null ||\n Array.isArray(value)\n ) {\n return null;\n }\n const selection = value as { enabled?: unknown; sourceIds?: unknown };\n if (typeof selection.enabled !== 'boolean') return null;\n const sourceIds = sanitizeIdList(selection.sourceIds ?? null);\n if (sourceIds === undefined) return null;\n config[integrationId] = { enabled: selection.enabled, sourceIds };\n }\n return config;\n}\n\nfunction encodeCursor(cursors: Record<string, string>): string | null {\n return Object.keys(cursors).length > 0 ? Buffer.from(JSON.stringify(cursors)).toString('base64url') : null;\n}\n\nfunction decodeCursor(value: string | undefined): Record<string, string> | null {\n if (!value) return {};\n try {\n const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8')) as unknown;\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return null;\n const entries = Object.entries(parsed);\n if (entries.some(([key, cursor]) => !key || typeof cursor !== 'string')) return null;\n return Object.fromEntries(entries) as Record<string, string>;\n } catch {\n return null;\n }\n}\n\nexport class IntakeRoutes extends Route<IntakeRoutesDeps> {\n async #resolveTenant(c: Context): Promise<{ orgId: string; userId: string } | { response: Response }> {\n await this.deps.auth.ensureUser(c);\n const tenant = this.deps.auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json(\n { error: 'organization_required', message: 'Intake configuration requires an organization.' },\n 403,\n ),\n };\n }\n return { orgId: tenant.orgId, userId: tenant.userId };\n }\n\n routes(): ApiRoute[] {\n const { audit, intake, integrations = [] } = this.deps;\n const integrationIds = integrations.map(integration => integration.id);\n\n return [\n registerApiRoute('/web/intake/config', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const tenant = await this.#resolveTenant(loose(c));\n if ('response' in tenant) return tenant.response;\n await intake.ensureReady();\n const config = await intake.getConfig({ ...tenant, integrationIds });\n return c.json({ config });\n },\n }),\n registerApiRoute('/web/intake/config', {\n method: 'PUT',\n requiresAuth: false,\n handler: async c => {\n const tenant = await this.#resolveTenant(loose(c));\n if ('response' in tenant) return tenant.response;\n\n let body: unknown;\n try {\n body = await c.req.json();\n } catch {\n return c.json({ error: 'Invalid JSON body' }, 400);\n }\n const config = parseIntakeConfig(body);\n if (!config || Object.keys(config).some(integrationId => !integrationIds.includes(integrationId))) {\n return c.json({ error: 'invalid_config' }, 400);\n }\n\n await intake.ensureReady();\n await intake.saveConfig({ ...tenant, config });\n await audit.emit({\n context: loose(c),\n input: {\n action: 'factory.intake.config_updated',\n targets: [{ type: 'intake_config', id: tenant.orgId }],\n metadata: Object.fromEntries(\n Object.entries(config).map(([integrationId, selection]) => [\n integrationId,\n { enabled: selection.enabled, sources: selection.sourceIds?.length ?? null },\n ]),\n ),\n },\n });\n return c.json({ config });\n },\n }),\n registerApiRoute('/web/intake/sources', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const tenant = await this.#resolveTenant(loose(c));\n if ('response' in tenant) return tenant.response;\n const pages = await Promise.all(\n integrations.map(async integration => ({\n integrationId: integration.id,\n sources: await integration.intake.listSources(tenant),\n })),\n );\n return c.json({\n sources: pages.flatMap(page =>\n page.sources.map(source => ({ integrationId: page.integrationId, ...source })),\n ),\n });\n },\n }),\n registerApiRoute('/web/intake/items', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const tenant = await this.#resolveTenant(loose(c));\n if ('response' in tenant) return tenant.response;\n const cursors = decodeCursor(c.req.query('cursor'));\n if (!cursors) return c.json({ error: 'invalid_cursor' }, 400);\n\n await intake.ensureReady();\n const config = await intake.getConfig({ ...tenant, integrationIds });\n const items: AggregatedIntakeItem[] = [];\n const nextCursors: Record<string, string> = {};\n for (const integration of integrations) {\n const selection = config[integration.id];\n if (!selection?.enabled || !selection.sourceIds?.length) continue;\n const page = await integration.intake.listItems({\n ...tenant,\n sourceIds: selection.sourceIds,\n ...(cursors[integration.id] ? { cursor: cursors[integration.id] } : {}),\n });\n items.push(\n ...page.items.map(item => {\n const { source, ...candidate } = item;\n return {\n ...candidate,\n integrationId: integration.id,\n externalSource: { integrationId: integration.id, ...source },\n };\n }),\n );\n if (page.nextCursor) nextCursors[integration.id] = page.nextCursor;\n }\n return c.json({ items, nextCursor: encodeCursor(nextCursors) });\n },\n }),\n ];\n }\n}\n","/**\n * Base class for factory route modules.\n *\n * Route modules build Mastra `apiRoutes` from injected dependencies instead of\n * reaching into host globals. The host server (e.g. `mastracode/web`) supplies\n * the auth seam and storage domain handles at construction time, so the routes\n * stay portable and testable with fakes.\n */\n\nimport type { ApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\n/**\n * The auth surface factory routes need, implemented by the host server.\n *\n * Local (no-auth) deployments implement this with a stub where `enabled()`\n * returns `false` and `tenant()` returns `undefined` — routes then take their\n * single-user local paths.\n */\nexport interface RouteAuth {\n /** Whether an auth provider is active (tenant mode). */\n enabled(): boolean;\n /**\n * Resolve (and cache) the signed-in user for the request, if any. Must be\n * called before `tenant()` so the request context is populated.\n */\n ensureUser(c: Context): Promise<unknown>;\n /** Tenant identity for the request, when signed in. */\n tenant(c: Context): { orgId?: string; userId: string } | undefined;\n /** Fail-closed check that the caller administers the given organization. */\n isOrganizationAdmin(c: Context, organizationId: string): Promise<boolean>;\n}\n\n/** Dependencies shared by every factory route module. */\nexport interface RouteDependencies {\n auth: RouteAuth;\n}\n\n/**\n * A route module: constructed once at boot with its dependencies, then asked\n * for the `ApiRoute[]` it serves.\n */\nexport abstract class Route<TDeps extends RouteDependencies = RouteDependencies> {\n protected readonly deps: TDeps;\n\n constructor(deps: TDeps) {\n this.deps = deps;\n }\n\n /** Build the Mastra `apiRoutes` served by this module. */\n abstract routes(): ApiRoute[];\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"file":"intake.js","names":["#resolveTenant"],"sources":["../../src/routes/intake.ts"],"sourcesContent":["import type { ApiRoute } from '@mastra/core/server';\nimport { registerApiRoute } from '@mastra/core/server';\nimport type { Context } from 'hono';\n\nimport type { Intake, IntakeItem } from '../capabilities/intake.js';\nimport type { AuditEmitter } from '../storage/domains/audit/domain.js';\nimport type { IntakeConfig, IntakeStorage } from '../storage/domains/intake/base.js';\nimport type { RouteDependencies } from './route.js';\nimport { Route } from './route.js';\n\nexport interface IntakeIntegration {\n id: string;\n intake: Pick<Intake, 'listSources' | 'listItems'>;\n}\n\ninterface AggregatedIntakeItem extends Omit<IntakeItem, 'source'> {\n integrationId: string;\n externalSource: {\n integrationId: string;\n type: string;\n externalId: string;\n url?: string;\n };\n}\n\nexport interface IntakeRoutesDeps extends RouteDependencies {\n audit: AuditEmitter;\n /** Intake selection domain handle. */\n intake: IntakeStorage;\n integrations?: IntakeIntegration[];\n}\n\nfunction loose(c: unknown): Context {\n return c as Context;\n}\n\nfunction sanitizeIdList(value: unknown): string[] | null | undefined {\n if (value === null) return null;\n if (!Array.isArray(value) || value.length > 200) return undefined;\n const ids = value.filter((item): item is string => typeof item === 'string' && item.length > 0 && item.length <= 256);\n return ids.length === value.length && new Set(ids).size === ids.length ? ids : undefined;\n}\n\n/** Validate a request body into an intake config, rejecting unknown shapes. */\nexport function parseIntakeConfig(body: unknown): IntakeConfig | null {\n if (typeof body !== 'object' || body === null || Array.isArray(body)) return null;\n const entries = Object.entries(body);\n if (entries.length > 50) return null;\n\n const config: IntakeConfig = {};\n for (const [integrationId, value] of entries) {\n if (\n !integrationId ||\n integrationId.length > 128 ||\n typeof value !== 'object' ||\n value === null ||\n Array.isArray(value)\n ) {\n return null;\n }\n const selection = value as { enabled?: unknown; sourceIds?: unknown };\n if (typeof selection.enabled !== 'boolean') return null;\n const sourceIds = sanitizeIdList(selection.sourceIds ?? null);\n if (sourceIds === undefined) return null;\n config[integrationId] = { enabled: selection.enabled, sourceIds };\n }\n return config;\n}\n\nfunction encodeCursor(cursors: Record<string, string>): string | null {\n return Object.keys(cursors).length > 0 ? Buffer.from(JSON.stringify(cursors)).toString('base64url') : null;\n}\n\nfunction decodeCursor(value: string | undefined): Record<string, string> | null {\n if (!value) return {};\n try {\n const parsed = JSON.parse(Buffer.from(value, 'base64url').toString('utf8')) as unknown;\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) return null;\n const entries = Object.entries(parsed);\n if (entries.some(([key, cursor]) => !key || typeof cursor !== 'string')) return null;\n return Object.fromEntries(entries) as Record<string, string>;\n } catch {\n return null;\n }\n}\n\nexport class IntakeRoutes extends Route<IntakeRoutesDeps> {\n async #resolveTenant(c: Context): Promise<{ orgId: string; userId: string } | { response: Response }> {\n await this.deps.auth.ensureUser(c);\n const tenant = this.deps.auth.tenant(c);\n if (!tenant) return { response: c.json({ error: 'unauthorized' }, 401) };\n if (!tenant.orgId) {\n return {\n response: c.json(\n { error: 'organization_required', message: 'Intake configuration requires an organization.' },\n 403,\n ),\n };\n }\n return { orgId: tenant.orgId, userId: tenant.userId };\n }\n\n routes(): ApiRoute[] {\n const { audit, intake, integrations = [] } = this.deps;\n const integrationIds = integrations.map(integration => integration.id);\n\n return [\n registerApiRoute('/web/intake/config', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const tenant = await this.#resolveTenant(loose(c));\n if ('response' in tenant) return tenant.response;\n await intake.ensureReady();\n const config = await intake.getConfig({ ...tenant, integrationIds });\n return c.json({ config });\n },\n }),\n registerApiRoute('/web/intake/config', {\n method: 'PUT',\n requiresAuth: false,\n handler: async c => {\n const tenant = await this.#resolveTenant(loose(c));\n if ('response' in tenant) return tenant.response;\n\n let body: unknown;\n try {\n body = await c.req.json();\n } catch {\n return c.json({ error: 'Invalid JSON body' }, 400);\n }\n const config = parseIntakeConfig(body);\n if (!config || Object.keys(config).some(integrationId => !integrationIds.includes(integrationId))) {\n return c.json({ error: 'invalid_config' }, 400);\n }\n\n await intake.ensureReady();\n await intake.saveConfig({ ...tenant, config });\n await audit.emit({\n context: loose(c),\n input: {\n action: 'factory.intake.config_updated',\n targets: [{ type: 'intake_config', id: tenant.orgId }],\n metadata: Object.fromEntries(\n Object.entries(config).map(([integrationId, selection]) => [\n integrationId,\n { enabled: selection.enabled, sources: selection.sourceIds?.length ?? null },\n ]),\n ),\n },\n });\n return c.json({ config });\n },\n }),\n registerApiRoute('/web/intake/sources', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const tenant = await this.#resolveTenant(loose(c));\n if ('response' in tenant) return tenant.response;\n const pages = await Promise.all(\n integrations.map(async integration => ({\n integrationId: integration.id,\n sources: await integration.intake.listSources(tenant),\n })),\n );\n return c.json({\n sources: pages.flatMap(page =>\n page.sources.map(source => ({ integrationId: page.integrationId, ...source })),\n ),\n });\n },\n }),\n registerApiRoute('/web/intake/items', {\n method: 'GET',\n requiresAuth: false,\n handler: async c => {\n const tenant = await this.#resolveTenant(loose(c));\n if ('response' in tenant) return tenant.response;\n const cursors = decodeCursor(c.req.query('cursor'));\n if (!cursors) return c.json({ error: 'invalid_cursor' }, 400);\n\n await intake.ensureReady();\n const config = await intake.getConfig({ ...tenant, integrationIds });\n const items: AggregatedIntakeItem[] = [];\n const nextCursors: Record<string, string> = {};\n for (const integration of integrations) {\n const selection = config[integration.id];\n if (!selection?.enabled || !selection.sourceIds?.length) continue;\n const page = await integration.intake.listItems({\n ...tenant,\n sourceIds: selection.sourceIds,\n ...(cursors[integration.id] ? { cursor: cursors[integration.id] } : {}),\n });\n items.push(\n ...page.items.map(item => {\n const { source, ...candidate } = item;\n return {\n ...candidate,\n integrationId: integration.id,\n externalSource: { integrationId: integration.id, ...source },\n };\n }),\n );\n if (page.nextCursor) nextCursors[integration.id] = page.nextCursor;\n }\n return c.json({ items, nextCursor: encodeCursor(nextCursors) });\n },\n }),\n ];\n }\n}\n"],"mappings":";;;AAgCA,SAAS,MAAM,GAAqB;CAClC,OAAO;AACT;AAEA,SAAS,eAAe,OAA6C;CACnE,IAAI,UAAU,MAAM,OAAO;CAC3B,IAAI,CAAC,MAAM,QAAQ,KAAK,KAAK,MAAM,SAAS,KAAK,OAAO,KAAA;CACxD,MAAM,MAAM,MAAM,QAAQ,SAAyB,OAAO,SAAS,YAAY,KAAK,SAAS,KAAK,KAAK,UAAU,GAAG;CACpH,OAAO,IAAI,WAAW,MAAM,UAAU,IAAI,IAAI,GAAG,CAAC,CAAC,SAAS,IAAI,SAAS,MAAM,KAAA;AACjF;;AAGA,SAAgB,kBAAkB,MAAoC;CACpE,IAAI,OAAO,SAAS,YAAY,SAAS,QAAQ,MAAM,QAAQ,IAAI,GAAG,OAAO;CAC7E,MAAM,UAAU,OAAO,QAAQ,IAAI;CACnC,IAAI,QAAQ,SAAS,IAAI,OAAO;CAEhC,MAAM,SAAuB,CAAC;CAC9B,KAAK,MAAM,CAAC,eAAe,UAAU,SAAS;EAC5C,IACE,CAAC,iBACD,cAAc,SAAS,OACvB,OAAO,UAAU,YACjB,UAAU,QACV,MAAM,QAAQ,KAAK,GAEnB,OAAO;EAET,MAAM,YAAY;EAClB,IAAI,OAAO,UAAU,YAAY,WAAW,OAAO;EACnD,MAAM,YAAY,eAAe,UAAU,aAAa,IAAI;EAC5D,IAAI,cAAc,KAAA,GAAW,OAAO;EACpC,OAAO,iBAAiB;GAAE,SAAS,UAAU;GAAS;EAAU;CAClE;CACA,OAAO;AACT;AAEA,SAAS,aAAa,SAAgD;CACpE,OAAO,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,IAAI,OAAO,KAAK,KAAK,UAAU,OAAO,CAAC,CAAC,CAAC,SAAS,WAAW,IAAI;AACxG;AAEA,SAAS,aAAa,OAA0D;CAC9E,IAAI,CAAC,OAAO,OAAO,CAAC;CACpB,IAAI;EACF,MAAM,SAAS,KAAK,MAAM,OAAO,KAAK,OAAO,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC;EAC1E,IAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,MAAM,QAAQ,MAAM,GAAG,OAAO;EACnF,MAAM,UAAU,OAAO,QAAQ,MAAM;EACrC,IAAI,QAAQ,MAAM,CAAC,KAAK,YAAY,CAAC,OAAO,OAAO,WAAW,QAAQ,GAAG,OAAO;EAChF,OAAO,OAAO,YAAY,OAAO;CACnC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,IAAa,eAAb,cAAkC,MAAwB;CACxD,MAAMA,eAAe,GAAiF;EACpG,MAAM,KAAK,KAAK,KAAK,WAAW,CAAC;EACjC,MAAM,SAAS,KAAK,KAAK,KAAK,OAAO,CAAC;EACtC,IAAI,CAAC,QAAQ,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,OAAO,eAAe,GAAG,GAAG,EAAE;EACvE,IAAI,CAAC,OAAO,OACV,OAAO,EACL,UAAU,EAAE,KACV;GAAE,OAAO;GAAyB,SAAS;EAAiD,GAC5F,GACF,EACF;EAEF,OAAO;GAAE,OAAO,OAAO;GAAO,QAAQ,OAAO;EAAO;CACtD;CAEA,SAAqB;EACnB,MAAM,EAAE,OAAO,QAAQ,eAAe,CAAC,MAAM,KAAK;EAClD,MAAM,iBAAiB,aAAa,KAAI,gBAAe,YAAY,EAAE;EAErE,OAAO;GACL,iBAAiB,sBAAsB;IACrC,QAAQ;IACR,cAAc;IACd,SAAS,OAAM,MAAK;KAClB,MAAM,SAAS,MAAM,KAAKA,eAAe,MAAM,CAAC,CAAC;KACjD,IAAI,cAAc,QAAQ,OAAO,OAAO;KACxC,MAAM,OAAO,YAAY;KACzB,MAAM,SAAS,MAAM,OAAO,UAAU;MAAE,GAAG;MAAQ;KAAe,CAAC;KACnE,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC;IAC1B;GACF,CAAC;GACD,iBAAiB,sBAAsB;IACrC,QAAQ;IACR,cAAc;IACd,SAAS,OAAM,MAAK;KAClB,MAAM,SAAS,MAAM,KAAKA,eAAe,MAAM,CAAC,CAAC;KACjD,IAAI,cAAc,QAAQ,OAAO,OAAO;KAExC,IAAI;KACJ,IAAI;MACF,OAAO,MAAM,EAAE,IAAI,KAAK;KAC1B,QAAQ;MACN,OAAO,EAAE,KAAK,EAAE,OAAO,oBAAoB,GAAG,GAAG;KACnD;KACA,MAAM,SAAS,kBAAkB,IAAI;KACrC,IAAI,CAAC,UAAU,OAAO,KAAK,MAAM,CAAC,CAAC,MAAK,kBAAiB,CAAC,eAAe,SAAS,aAAa,CAAC,GAC9F,OAAO,EAAE,KAAK,EAAE,OAAO,iBAAiB,GAAG,GAAG;KAGhD,MAAM,OAAO,YAAY;KACzB,MAAM,OAAO,WAAW;MAAE,GAAG;MAAQ;KAAO,CAAC;KAC7C,MAAM,MAAM,KAAK;MACf,SAAS,MAAM,CAAC;MAChB,OAAO;OACL,QAAQ;OACR,SAAS,CAAC;QAAE,MAAM;QAAiB,IAAI,OAAO;OAAM,CAAC;OACrD,UAAU,OAAO,YACf,OAAO,QAAQ,MAAM,CAAC,CAAC,KAAK,CAAC,eAAe,eAAe,CACzD,eACA;QAAE,SAAS,UAAU;QAAS,SAAS,UAAU,WAAW,UAAU;OAAK,CAC7E,CAAC,CACH;MACF;KACF,CAAC;KACD,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC;IAC1B;GACF,CAAC;GACD,iBAAiB,uBAAuB;IACtC,QAAQ;IACR,cAAc;IACd,SAAS,OAAM,MAAK;KAClB,MAAM,SAAS,MAAM,KAAKA,eAAe,MAAM,CAAC,CAAC;KACjD,IAAI,cAAc,QAAQ,OAAO,OAAO;KACxC,MAAM,QAAQ,MAAM,QAAQ,IAC1B,aAAa,IAAI,OAAM,iBAAgB;MACrC,eAAe,YAAY;MAC3B,SAAS,MAAM,YAAY,OAAO,YAAY,MAAM;KACtD,EAAE,CACJ;KACA,OAAO,EAAE,KAAK,EACZ,SAAS,MAAM,SAAQ,SACrB,KAAK,QAAQ,KAAI,YAAW;MAAE,eAAe,KAAK;MAAe,GAAG;KAAO,EAAE,CAC/E,EACF,CAAC;IACH;GACF,CAAC;GACD,iBAAiB,qBAAqB;IACpC,QAAQ;IACR,cAAc;IACd,SAAS,OAAM,MAAK;KAClB,MAAM,SAAS,MAAM,KAAKA,eAAe,MAAM,CAAC,CAAC;KACjD,IAAI,cAAc,QAAQ,OAAO,OAAO;KACxC,MAAM,UAAU,aAAa,EAAE,IAAI,MAAM,QAAQ,CAAC;KAClD,IAAI,CAAC,SAAS,OAAO,EAAE,KAAK,EAAE,OAAO,iBAAiB,GAAG,GAAG;KAE5D,MAAM,OAAO,YAAY;KACzB,MAAM,SAAS,MAAM,OAAO,UAAU;MAAE,GAAG;MAAQ;KAAe,CAAC;KACnE,MAAM,QAAgC,CAAC;KACvC,MAAM,cAAsC,CAAC;KAC7C,KAAK,MAAM,eAAe,cAAc;MACtC,MAAM,YAAY,OAAO,YAAY;MACrC,IAAI,CAAC,WAAW,WAAW,CAAC,UAAU,WAAW,QAAQ;MACzD,MAAM,OAAO,MAAM,YAAY,OAAO,UAAU;OAC9C,GAAG;OACH,WAAW,UAAU;OACrB,GAAI,QAAQ,YAAY,MAAM,EAAE,QAAQ,QAAQ,YAAY,IAAI,IAAI,CAAC;MACvE,CAAC;MACD,MAAM,KACJ,GAAG,KAAK,MAAM,KAAI,SAAQ;OACxB,MAAM,EAAE,QAAQ,GAAG,cAAc;OACjC,OAAO;QACL,GAAG;QACH,eAAe,YAAY;QAC3B,gBAAgB;SAAE,eAAe,YAAY;SAAI,GAAG;QAAO;OAC7D;MACF,CAAC,CACH;MACA,IAAI,KAAK,YAAY,YAAY,YAAY,MAAM,KAAK;KAC1D;KACA,OAAO,EAAE,KAAK;MAAE;MAAO,YAAY,aAAa,WAAW;KAAE,CAAC;IAChE;GACF,CAAC;EACH;CACF;AACF"}
|