@axiom-lattice/gateway 2.1.102 → 2.1.103
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/.turbo/turbo-build.log +11 -11
- package/CHANGELOG.md +12 -0
- package/dist/index.js +68 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{resources-3DGHISBZ.mjs → resources-NE6DFF5I.mjs} +68 -18
- package/dist/resources-NE6DFF5I.mjs.map +1 -0
- package/package.json +11 -8
- package/src/controllers/resources.ts +68 -21
- package/dist/resources-3DGHISBZ.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -8336,7 +8336,7 @@ var start = async (config) => {
|
|
|
8336
8336
|
logger4.info("Registered sandbox manager from env configuration");
|
|
8337
8337
|
}
|
|
8338
8338
|
try {
|
|
8339
|
-
const { ResourceController } = await import("./resources-
|
|
8339
|
+
const { ResourceController } = await import("./resources-NE6DFF5I.mjs");
|
|
8340
8340
|
const sharedResourceStore = getStoreLattice17("default", "sharedResource").store;
|
|
8341
8341
|
const cache = new TokenCache();
|
|
8342
8342
|
const resourceController = new ResourceController({
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-R4ZO3HZ3.mjs";
|
|
4
4
|
|
|
5
5
|
// src/controllers/resources.ts
|
|
6
|
+
import bcrypt from "bcryptjs";
|
|
6
7
|
import {
|
|
7
8
|
generateToken,
|
|
8
9
|
createSharePayload
|
|
@@ -31,8 +32,8 @@ var ResourceController = class {
|
|
|
31
32
|
try {
|
|
32
33
|
await this.deps.store.create({ ...payload, token });
|
|
33
34
|
this.deps.logger.info(
|
|
34
|
-
"
|
|
35
|
-
{ token,
|
|
35
|
+
"[share] created",
|
|
36
|
+
{ token, originalPath: body.resourcePath, normalizedPath: payload.address.resourcePath, volume: payload.address.volume, userId }
|
|
36
37
|
);
|
|
37
38
|
} catch {
|
|
38
39
|
return reply.status(500).send({ error: "Failed to create share" });
|
|
@@ -74,22 +75,44 @@ var ResourceController = class {
|
|
|
74
75
|
const params = request.params;
|
|
75
76
|
const { token } = params;
|
|
76
77
|
const subPath = params["*"] ?? "";
|
|
78
|
+
this.deps.logger.info("[share] resolve start", { token, subPath });
|
|
77
79
|
const cached = this.deps.cache.get(token);
|
|
78
80
|
if (cached && !cached.requiresUnlock) {
|
|
79
|
-
|
|
81
|
+
this.deps.logger.info("[share] cache hit", { token, volume: cached.address.volume, resourcePath: cached.address.resourcePath });
|
|
82
|
+
return this._serveFile(cached.address, token, subPath, cached.sandboxConfig ?? { tenantId: "default", workspaceId: "", projectId: "", assistantId: null }, reply);
|
|
80
83
|
}
|
|
84
|
+
this.deps.logger.info("[share] cache miss, querying DB", { token });
|
|
81
85
|
const record = await this.deps.store.findByToken(token);
|
|
82
|
-
if (!record || record.revoked)
|
|
86
|
+
if (!record || record.revoked) {
|
|
87
|
+
this.deps.logger.warn("[share] token not found or revoked", { token, found: !!record, revoked: record?.revoked });
|
|
88
|
+
return reply.status(404).send("Not found");
|
|
89
|
+
}
|
|
83
90
|
if (record.expiresAt && new Date(record.expiresAt) < /* @__PURE__ */ new Date()) {
|
|
91
|
+
this.deps.logger.info("[share] token expired", { token, expiresAt: record.expiresAt });
|
|
84
92
|
return reply.status(410).send("Expired");
|
|
85
93
|
}
|
|
86
|
-
|
|
94
|
+
this.deps.logger.info("[share] record found", {
|
|
95
|
+
token,
|
|
96
|
+
volume: record.address.volume,
|
|
97
|
+
resourcePath: record.address.resourcePath,
|
|
98
|
+
visibility: record.visibility,
|
|
99
|
+
workspaceId: record.workspaceId,
|
|
100
|
+
projectId: record.projectId
|
|
101
|
+
});
|
|
102
|
+
const isInternal = this._isInternalRequest(request);
|
|
103
|
+
if (!isInternal) {
|
|
87
104
|
if (record.visibility === "password") {
|
|
88
105
|
const unlocked = this._isUnlocked(request, token);
|
|
89
106
|
if (!unlocked) {
|
|
90
|
-
this.deps.
|
|
107
|
+
this.deps.logger.info("[share] password protected, returning password page", { token });
|
|
108
|
+
this.deps.cache.set(token, {
|
|
109
|
+
address: record.address,
|
|
110
|
+
requiresUnlock: true,
|
|
111
|
+
sandboxConfig: { tenantId: record.tenantId, workspaceId: record.workspaceId, projectId: record.projectId, assistantId: record.assistantId }
|
|
112
|
+
});
|
|
91
113
|
return reply.type("text/html").send(this._passwordPage(token));
|
|
92
114
|
}
|
|
115
|
+
this.deps.logger.info("[share] password unlocked", { token });
|
|
93
116
|
}
|
|
94
117
|
}
|
|
95
118
|
if (record.maxAccess !== null) {
|
|
@@ -99,11 +122,13 @@ var ResourceController = class {
|
|
|
99
122
|
this.deps.store.incrementAccess(token).catch(() => {
|
|
100
123
|
});
|
|
101
124
|
}
|
|
125
|
+
const sandboxConfig = { tenantId: record.tenantId, workspaceId: record.workspaceId, projectId: record.projectId, assistantId: record.assistantId };
|
|
102
126
|
this.deps.cache.set(token, {
|
|
103
127
|
address: record.address,
|
|
104
|
-
requiresUnlock: record.visibility === "password" && !
|
|
128
|
+
requiresUnlock: record.visibility === "password" && !isInternal,
|
|
129
|
+
sandboxConfig
|
|
105
130
|
});
|
|
106
|
-
return this._serveFile(record.address, token, subPath, reply);
|
|
131
|
+
return this._serveFile(record.address, token, subPath, sandboxConfig, reply);
|
|
107
132
|
}
|
|
108
133
|
async unlockShare(request, reply) {
|
|
109
134
|
const { token } = request.params;
|
|
@@ -112,7 +137,7 @@ var ResourceController = class {
|
|
|
112
137
|
if (!record || !record.passwordHash) {
|
|
113
138
|
return reply.status(404).send({ error: "Invalid request" });
|
|
114
139
|
}
|
|
115
|
-
const valid = password
|
|
140
|
+
const valid = await bcrypt.compare(password, record.passwordHash);
|
|
116
141
|
if (!valid) {
|
|
117
142
|
return reply.status(401).send({ error: "Incorrect password" });
|
|
118
143
|
}
|
|
@@ -130,20 +155,45 @@ var ResourceController = class {
|
|
|
130
155
|
const cookie = request.headers.cookie ?? "";
|
|
131
156
|
return cookie.includes(`share_unlock_${token}=1`);
|
|
132
157
|
}
|
|
133
|
-
async _serveFile(address, token, subPath, reply) {
|
|
158
|
+
async _serveFile(address, token, subPath, sandboxConfig, reply) {
|
|
134
159
|
const provider = this.deps.sandboxManager.getDefaultProvider();
|
|
135
160
|
const resolver = provider.getResourceResolver();
|
|
136
|
-
const
|
|
161
|
+
const sandboxPath = subPath ? this._resolveSafeSubPath(address.resourcePath, subPath) : address.resourcePath;
|
|
162
|
+
this.deps.logger.info("[share] serving file", {
|
|
163
|
+
token,
|
|
164
|
+
volume: address.volume,
|
|
165
|
+
sandboxPath,
|
|
166
|
+
subPath: subPath || "(none)",
|
|
167
|
+
tenantId: sandboxConfig.tenantId,
|
|
168
|
+
workspaceId: sandboxConfig.workspaceId,
|
|
169
|
+
projectId: sandboxConfig.projectId
|
|
170
|
+
});
|
|
137
171
|
let buf;
|
|
138
172
|
try {
|
|
139
|
-
buf = await resolver.resolve({ ...address, resourcePath:
|
|
173
|
+
buf = await resolver.resolve({ ...address, resourcePath: sandboxPath });
|
|
174
|
+
this.deps.logger.info("[share] resolved via volume FS", { token, size: buf.length });
|
|
140
175
|
} catch (err) {
|
|
141
|
-
this.deps.logger.warn(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
176
|
+
this.deps.logger.warn("[share] volume FS failed, trying sandbox fallback", {
|
|
177
|
+
token,
|
|
178
|
+
path: sandboxPath,
|
|
179
|
+
error: err.message
|
|
180
|
+
});
|
|
181
|
+
try {
|
|
182
|
+
const sandbox = await this.deps.sandboxManager.getSandboxFromConfig({
|
|
183
|
+
assistant_id: sandboxConfig.assistantId ?? "",
|
|
184
|
+
thread_id: "",
|
|
185
|
+
tenantId: sandboxConfig.tenantId,
|
|
186
|
+
workspaceId: sandboxConfig.workspaceId,
|
|
187
|
+
projectId: sandboxConfig.projectId
|
|
188
|
+
});
|
|
189
|
+
buf = await sandbox.file.downloadFile({ file: `/project/${sandboxPath}` });
|
|
190
|
+
this.deps.logger.info("[share] resolved via sandbox fallback", { token, size: buf.length });
|
|
191
|
+
} catch (err2) {
|
|
192
|
+
this.deps.logger.warn("[share] all resolution attempts failed", { token, resourcePath: sandboxPath, error: err2.message });
|
|
193
|
+
return reply.status(404).send("File not found");
|
|
194
|
+
}
|
|
146
195
|
}
|
|
196
|
+
const fullPath = sandboxPath;
|
|
147
197
|
const filename = fullPath.split("/").pop() || "download";
|
|
148
198
|
const isHtml = !subPath && /\.(html|htm)$/i.test(filename);
|
|
149
199
|
if (isHtml) {
|
|
@@ -180,4 +230,4 @@ var ResourceController = class {
|
|
|
180
230
|
export {
|
|
181
231
|
ResourceController
|
|
182
232
|
};
|
|
183
|
-
//# sourceMappingURL=resources-
|
|
233
|
+
//# sourceMappingURL=resources-NE6DFF5I.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/controllers/resources.ts"],"sourcesContent":["import { FastifyRequest, FastifyReply } from \"fastify\";\nimport type { ResourceAddress, SharedResourceStore } from \"@axiom-lattice/protocols\";\nimport bcrypt from \"bcryptjs\";\nimport {\n generateToken,\n createSharePayload,\n TokenCache,\n SandboxLatticeManager,\n} from \"@axiom-lattice/core\";\nimport { getContentTypeFromFilename } from \"../utils/mime\";\n\ninterface ResourceControllerDeps {\n store: SharedResourceStore;\n sandboxManager: SandboxLatticeManager;\n cache: TokenCache;\n logger: { info: (msg: string, obj?: object) => void; warn: (msg: string, obj?: object) => void; error: (msg: string, obj?: object) => void };\n}\n\nexport class ResourceController {\n constructor(private deps: ResourceControllerDeps) {}\n\n async createShare(request: FastifyRequest, reply: FastifyReply) {\n const userId = (request as any).user\n ? (request as any).user.id ?? \"unknown\"\n : \"unknown\";\n const tenantId = (request.headers[\"x-tenant-id\"] as string) || \"default\";\n const workspaceId = request.headers[\"x-workspace-id\"] as string;\n const projectId = request.headers[\"x-project-id\"] as string;\n const body = request.body as any;\n\n if (!workspaceId || !projectId) {\n return reply.status(400).send({ error: \"x-workspace-id and x-project-id headers required\" });\n }\n\n const payload = createSharePayload(\n tenantId,\n workspaceId,\n projectId,\n userId as string,\n body,\n );\n const token = generateToken();\n\n try {\n await this.deps.store.create({ ...payload, token });\n this.deps.logger.info(\n \"[share] created\",\n { token, originalPath: body.resourcePath, normalizedPath: payload.address.resourcePath, volume: payload.address.volume, userId },\n );\n } catch {\n return reply.status(500).send({ error: \"Failed to create share\" });\n }\n\n return reply.status(201).send({ token, url: `/s/${token}` });\n }\n\n async listShares(request: FastifyRequest, reply: FastifyReply) {\n const userId = (request as any).user\n ? (request as any).user.id ?? \"unknown\"\n : \"unknown\";\n const tenantId = (request.headers[\"x-tenant-id\"] as string) || \"default\";\n const shares = await this.deps.store.listByUser(tenantId, userId as string);\n return reply.send(shares);\n }\n\n async updateShare(request: FastifyRequest, reply: FastifyReply) {\n const { token } = request.params as unknown as { token: string };\n const userId = (request as any).user\n ? (request as any).user.id as string\n : undefined;\n const patch = request.body as any;\n\n const record = await this.deps.store.findByToken(token);\n if (!record || record.createdBy !== userId) {\n return reply.status(404).send({ error: \"Share not found\" });\n }\n\n await this.deps.store.update(token, patch);\n\n if (patch.revoked) {\n this.deps.cache.invalidate(token);\n }\n\n return reply.send({ ok: true });\n }\n\n async revokeShare(request: FastifyRequest, reply: FastifyReply) {\n const { token } = request.params as unknown as { token: string };\n const userId = (request as any).user\n ? (request as any).user.id as string\n : undefined;\n\n const record = await this.deps.store.findByToken(token);\n if (!record || record.createdBy !== userId) {\n return reply.status(404).send({ error: \"Share not found\" });\n }\n\n await this.deps.store.update(token, { revoked: true });\n this.deps.cache.invalidate(token);\n\n return reply.send({ ok: true });\n }\n\n async resolveResource(request: FastifyRequest, reply: FastifyReply) {\n const params = request.params as unknown as { token: string; \"*\"?: string };\n const { token } = params;\n const subPath = params[\"*\"] ?? \"\";\n\n this.deps.logger.info(\"[share] resolve start\", { token, subPath });\n\n // 1. Check cache\n const cached = this.deps.cache.get(token);\n if (cached && !cached.requiresUnlock) {\n this.deps.logger.info(\"[share] cache hit\", { token, volume: cached.address.volume, resourcePath: cached.address.resourcePath });\n return this._serveFile(cached.address, token, subPath, cached.sandboxConfig ?? { tenantId: \"default\", workspaceId: \"\", projectId: \"\", assistantId: null }, reply);\n }\n this.deps.logger.info(\"[share] cache miss, querying DB\", { token });\n\n // 2. Single DB lookup\n const record = await this.deps.store.findByToken(token);\n if (!record || record.revoked) {\n this.deps.logger.warn(\"[share] token not found or revoked\", { token, found: !!record, revoked: record?.revoked });\n return reply.status(404).send(\"Not found\");\n }\n if (record.expiresAt && new Date(record.expiresAt) < new Date()) {\n this.deps.logger.info(\"[share] token expired\", { token, expiresAt: record.expiresAt });\n return reply.status(410).send(\"Expired\");\n }\n\n this.deps.logger.info(\"[share] record found\", {\n token,\n volume: record.address.volume,\n resourcePath: record.address.resourcePath,\n visibility: record.visibility,\n workspaceId: record.workspaceId,\n projectId: record.projectId,\n });\n\n // 3. Auth\n const isInternal = this._isInternalRequest(request);\n if (!isInternal) {\n if (record.visibility === \"password\") {\n const unlocked = this._isUnlocked(request, token);\n if (!unlocked) {\n this.deps.logger.info(\"[share] password protected, returning password page\", { token });\n this.deps.cache.set(token, {\n address: record.address,\n requiresUnlock: true,\n sandboxConfig: { tenantId: record.tenantId, workspaceId: record.workspaceId, projectId: record.projectId, assistantId: record.assistantId },\n });\n return reply.type(\"text/html\").send(this._passwordPage(token));\n }\n this.deps.logger.info(\"[share] password unlocked\", { token });\n }\n }\n\n // 4. Access count\n if (record.maxAccess !== null) {\n const ok = await this.deps.store.atomicIncrementAccess(token);\n if (!ok) return reply.status(410).send(\"Access limit reached\");\n } else {\n this.deps.store.incrementAccess(token).catch(() => {});\n }\n\n // 5. Cache\n const sandboxConfig = { tenantId: record.tenantId, workspaceId: record.workspaceId, projectId: record.projectId, assistantId: record.assistantId };\n this.deps.cache.set(token, {\n address: record.address,\n requiresUnlock: record.visibility === \"password\" && !isInternal,\n sandboxConfig,\n });\n\n return this._serveFile(record.address, token, subPath, sandboxConfig, reply);\n }\n\n async unlockShare(request: FastifyRequest, reply: FastifyReply) {\n const { token } = request.params as unknown as { token: string };\n const { password } = request.body as unknown as { password: string };\n\n const record = await this.deps.store.findByToken(token);\n if (!record || !record.passwordHash) {\n return reply.status(404).send({ error: \"Invalid request\" });\n }\n\n // Plain text comparison for now — bcrypt will come in Task 12\n const valid = await bcrypt.compare(password, record.passwordHash!);\n if (!valid) {\n return reply.status(401).send({ error: \"Incorrect password\" });\n }\n\n reply.header(\n \"Set-Cookie\",\n `share_unlock_${token}=1; Max-Age=86400; Path=/s/${token}; SameSite=Lax; HttpOnly; Secure`,\n );\n return reply.send({ ok: true });\n }\n\n private _isInternalRequest(request: FastifyRequest): boolean {\n // Check for valid user session from auth middleware\n const user = (request as any).user;\n return !!user && !!user.id;\n }\n\n private _isUnlocked(request: FastifyRequest, token: string): boolean {\n const cookie = request.headers.cookie ?? \"\";\n return cookie.includes(`share_unlock_${token}=1`);\n }\n\n private async _serveFile(\n address: ResourceAddress,\n token: string,\n subPath: string,\n sandboxConfig: { tenantId: string; workspaceId: string; projectId: string; assistantId: string | null },\n reply: FastifyReply,\n ) {\n const provider = this.deps.sandboxManager.getDefaultProvider();\n const resolver = provider.getResourceResolver();\n const sandboxPath = subPath\n ? this._resolveSafeSubPath(address.resourcePath, subPath)\n : address.resourcePath;\n\n this.deps.logger.info(\"[share] serving file\", {\n token, volume: address.volume, sandboxPath, subPath: subPath || \"(none)\",\n tenantId: sandboxConfig.tenantId, workspaceId: sandboxConfig.workspaceId, projectId: sandboxConfig.projectId,\n });\n\n let buf: Buffer;\n try {\n // Try volume FS first\n buf = await resolver.resolve({ ...address, resourcePath: sandboxPath });\n this.deps.logger.info(\"[share] resolved via volume FS\", { token, size: buf.length });\n } catch (err) {\n this.deps.logger.warn(\"[share] volume FS failed, trying sandbox fallback\", {\n token, path: sandboxPath, error: (err as Error).message,\n });\n try {\n const sandbox = await this.deps.sandboxManager.getSandboxFromConfig({\n assistant_id: sandboxConfig.assistantId ?? \"\",\n thread_id: \"\",\n tenantId: sandboxConfig.tenantId,\n workspaceId: sandboxConfig.workspaceId,\n projectId: sandboxConfig.projectId,\n });\n buf = await sandbox.file.downloadFile({ file: `/project/${sandboxPath}` });\n this.deps.logger.info(\"[share] resolved via sandbox fallback\", { token, size: buf.length });\n } catch (err) {\n this.deps.logger.warn(\"[share] all resolution attempts failed\", { token, resourcePath: sandboxPath, error: (err as Error).message });\n return reply.status(404).send(\"File not found\");\n }\n }\n\n const fullPath = sandboxPath;\n\n const filename = fullPath.split(\"/\").pop() || \"download\";\n const isHtml = !subPath && /\\.(html|htm)$/i.test(filename);\n\n if (isHtml) {\n buf = this._injectBaseTag(buf, token);\n }\n\n const contentType = getContentTypeFromFilename(filename);\n\n return reply\n .status(200)\n .type(contentType)\n .header(\"Content-Disposition\", `inline; filename=\"${filename}\"`)\n .header(\"Access-Control-Allow-Origin\", \"*\")\n .send(buf);\n }\n\n private _resolveSafeSubPath(entryFile: string, subPath: string): string {\n if (!subPath) return entryFile;\n if (subPath.includes(\"..\")) throw new Error(\"Path traversal denied\");\n const entryDir = entryFile.replace(/[^/]+$/, \"\");\n return (entryDir + subPath).replace(/\\/+/g, \"/\");\n }\n\n private _injectBaseTag(buf: Buffer, token: string): Buffer {\n const html = buf.toString(\"utf-8\");\n if (/<base\\b/i.test(html)) return buf;\n const baseTag = `<base href=\"/s/${token}/\">`;\n if (html.includes(\"</head>\")) {\n return Buffer.from(html.replace(\"</head>\", `${baseTag}</head>`), \"utf-8\");\n }\n // No </head> — insert after <head> or <html> or after <!doctype>\n if (html.includes(\"<head>\")) {\n return Buffer.from(html.replace(\"<head>\", `<head>${baseTag}`), \"utf-8\");\n }\n if (html.includes(\"<html>\")) {\n return Buffer.from(html.replace(\"<html>\", `<html>${baseTag}`), \"utf-8\");\n }\n // Fallback: prepend (will be after <!doctype> if present since html starts with <!doctype)\n return Buffer.from(baseTag + html, \"utf-8\");\n }\n\n private _passwordPage(token: string): string {\n return `<!DOCTYPE html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Password Protected</title><style>body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f5f5f5}.card{background:#fff;padding:32px;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.1);width:100%;max-width:360px;text-align:center}h2{margin:0 0 8px;font-size:20px}p{margin:0 0 20px;color:#666;font-size:14px}input{width:100%;padding:10px 12px;border:1px solid #d9d9d9;border-radius:8px;font-size:14px;box-sizing:border-box;margin-bottom:12px}button{width:100%;padding:10px;background:#6366f1;color:#fff;border:none;border-radius:8px;font-size:14px;cursor:pointer}.error{color:#ef4444;font-size:13px;margin-top:8px;display:none}</style></head><body><div class=\"card\"><h2>Password Protected</h2><p>This share requires a password to access.</p><form id=\"f\"><input type=\"password\" id=\"p\" placeholder=\"Enter password\" required><button type=\"submit\">Unlock</button><div class=\"error\" id=\"e\">Incorrect password</div></form></div><script>document.getElementById('f').onsubmit=async(e)=>{e.preventDefault();const r=await fetch('/s/${token}/unlock',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:document.getElementById('p').value})});if(r.ok)location.reload();else document.getElementById('e').style.display='block'}</script></body></html>`;\n }\n}\n"],"mappings":"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","names":["err"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@axiom-lattice/gateway",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.103",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -25,28 +25,31 @@
|
|
|
25
25
|
"@fastify/swagger": "^9.5.1",
|
|
26
26
|
"@fastify/swagger-ui": "^5.2.3",
|
|
27
27
|
"@fastify/websocket": "^11.0.1",
|
|
28
|
-
"@larksuiteoapi/node-sdk": "^1.47.0",
|
|
29
28
|
"@langchain/core": "1.1.30",
|
|
30
29
|
"@langchain/langgraph": "1.0.4",
|
|
30
|
+
"@larksuiteoapi/node-sdk": "^1.47.0",
|
|
31
31
|
"@supabase/supabase-js": "^2.49.1",
|
|
32
|
+
"bcryptjs": "^3.0.3",
|
|
32
33
|
"dotenv": "^16.6.1",
|
|
33
34
|
"fastify": "^5.5.0",
|
|
34
35
|
"fastify-plugin": "^5.0.1",
|
|
35
36
|
"lodash": "^4.17.21",
|
|
37
|
+
"pg": "^8.11.0",
|
|
36
38
|
"pino": "^9.7.0",
|
|
37
39
|
"pino-pretty": "^13.0.0",
|
|
38
40
|
"pino-roll": "^3.1.0",
|
|
39
|
-
"pg": "^8.11.0",
|
|
40
41
|
"redis": "^5.0.1",
|
|
41
42
|
"uuid": "^9.0.1",
|
|
42
43
|
"zod": "3.25.76",
|
|
43
|
-
"@axiom-lattice/agent-eval": "2.1.
|
|
44
|
-
"@axiom-lattice/core": "2.1.
|
|
45
|
-
"@axiom-lattice/pg-stores": "1.0.
|
|
46
|
-
"@axiom-lattice/protocols": "2.1.
|
|
47
|
-
"@axiom-lattice/queue-redis": "1.0.
|
|
44
|
+
"@axiom-lattice/agent-eval": "2.1.85",
|
|
45
|
+
"@axiom-lattice/core": "2.1.91",
|
|
46
|
+
"@axiom-lattice/pg-stores": "1.0.82",
|
|
47
|
+
"@axiom-lattice/protocols": "2.1.46",
|
|
48
|
+
"@axiom-lattice/queue-redis": "1.0.45"
|
|
48
49
|
},
|
|
49
50
|
"devDependencies": {
|
|
51
|
+
"@types/bcrypt": "^6.0.0",
|
|
52
|
+
"@types/bcryptjs": "^3.0.0",
|
|
50
53
|
"@types/jest": "^29.5.14",
|
|
51
54
|
"@types/lodash": "^4.17.16",
|
|
52
55
|
"@types/node": "^20.19.13",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FastifyRequest, FastifyReply } from "fastify";
|
|
2
2
|
import type { ResourceAddress, SharedResourceStore } from "@axiom-lattice/protocols";
|
|
3
|
+
import bcrypt from "bcryptjs";
|
|
3
4
|
import {
|
|
4
5
|
generateToken,
|
|
5
6
|
createSharePayload,
|
|
@@ -43,8 +44,8 @@ export class ResourceController {
|
|
|
43
44
|
try {
|
|
44
45
|
await this.deps.store.create({ ...payload, token });
|
|
45
46
|
this.deps.logger.info(
|
|
46
|
-
"
|
|
47
|
-
{ token,
|
|
47
|
+
"[share] created",
|
|
48
|
+
{ token, originalPath: body.resourcePath, normalizedPath: payload.address.resourcePath, volume: payload.address.volume, userId },
|
|
48
49
|
);
|
|
49
50
|
} catch {
|
|
50
51
|
return reply.status(500).send({ error: "Failed to create share" });
|
|
@@ -105,28 +106,51 @@ export class ResourceController {
|
|
|
105
106
|
const { token } = params;
|
|
106
107
|
const subPath = params["*"] ?? "";
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
this.deps.logger.info("[share] resolve start", { token, subPath });
|
|
110
|
+
|
|
111
|
+
// 1. Check cache
|
|
109
112
|
const cached = this.deps.cache.get(token);
|
|
110
113
|
if (cached && !cached.requiresUnlock) {
|
|
111
|
-
|
|
114
|
+
this.deps.logger.info("[share] cache hit", { token, volume: cached.address.volume, resourcePath: cached.address.resourcePath });
|
|
115
|
+
return this._serveFile(cached.address, token, subPath, cached.sandboxConfig ?? { tenantId: "default", workspaceId: "", projectId: "", assistantId: null }, reply);
|
|
112
116
|
}
|
|
117
|
+
this.deps.logger.info("[share] cache miss, querying DB", { token });
|
|
113
118
|
|
|
114
119
|
// 2. Single DB lookup
|
|
115
120
|
const record = await this.deps.store.findByToken(token);
|
|
116
|
-
if (!record || record.revoked)
|
|
121
|
+
if (!record || record.revoked) {
|
|
122
|
+
this.deps.logger.warn("[share] token not found or revoked", { token, found: !!record, revoked: record?.revoked });
|
|
123
|
+
return reply.status(404).send("Not found");
|
|
124
|
+
}
|
|
117
125
|
if (record.expiresAt && new Date(record.expiresAt) < new Date()) {
|
|
126
|
+
this.deps.logger.info("[share] token expired", { token, expiresAt: record.expiresAt });
|
|
118
127
|
return reply.status(410).send("Expired");
|
|
119
128
|
}
|
|
120
129
|
|
|
121
|
-
|
|
122
|
-
|
|
130
|
+
this.deps.logger.info("[share] record found", {
|
|
131
|
+
token,
|
|
132
|
+
volume: record.address.volume,
|
|
133
|
+
resourcePath: record.address.resourcePath,
|
|
134
|
+
visibility: record.visibility,
|
|
135
|
+
workspaceId: record.workspaceId,
|
|
136
|
+
projectId: record.projectId,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// 3. Auth
|
|
140
|
+
const isInternal = this._isInternalRequest(request);
|
|
141
|
+
if (!isInternal) {
|
|
123
142
|
if (record.visibility === "password") {
|
|
124
143
|
const unlocked = this._isUnlocked(request, token);
|
|
125
144
|
if (!unlocked) {
|
|
126
|
-
|
|
127
|
-
this.deps.cache.set(token, {
|
|
145
|
+
this.deps.logger.info("[share] password protected, returning password page", { token });
|
|
146
|
+
this.deps.cache.set(token, {
|
|
147
|
+
address: record.address,
|
|
148
|
+
requiresUnlock: true,
|
|
149
|
+
sandboxConfig: { tenantId: record.tenantId, workspaceId: record.workspaceId, projectId: record.projectId, assistantId: record.assistantId },
|
|
150
|
+
});
|
|
128
151
|
return reply.type("text/html").send(this._passwordPage(token));
|
|
129
152
|
}
|
|
153
|
+
this.deps.logger.info("[share] password unlocked", { token });
|
|
130
154
|
}
|
|
131
155
|
}
|
|
132
156
|
|
|
@@ -138,13 +162,15 @@ export class ResourceController {
|
|
|
138
162
|
this.deps.store.incrementAccess(token).catch(() => {});
|
|
139
163
|
}
|
|
140
164
|
|
|
141
|
-
// 5. Cache
|
|
165
|
+
// 5. Cache
|
|
166
|
+
const sandboxConfig = { tenantId: record.tenantId, workspaceId: record.workspaceId, projectId: record.projectId, assistantId: record.assistantId };
|
|
142
167
|
this.deps.cache.set(token, {
|
|
143
168
|
address: record.address,
|
|
144
|
-
requiresUnlock: record.visibility === "password" && !
|
|
169
|
+
requiresUnlock: record.visibility === "password" && !isInternal,
|
|
170
|
+
sandboxConfig,
|
|
145
171
|
});
|
|
146
172
|
|
|
147
|
-
return this._serveFile(record.address, token, subPath, reply);
|
|
173
|
+
return this._serveFile(record.address, token, subPath, sandboxConfig, reply);
|
|
148
174
|
}
|
|
149
175
|
|
|
150
176
|
async unlockShare(request: FastifyRequest, reply: FastifyReply) {
|
|
@@ -157,7 +183,7 @@ export class ResourceController {
|
|
|
157
183
|
}
|
|
158
184
|
|
|
159
185
|
// Plain text comparison for now — bcrypt will come in Task 12
|
|
160
|
-
const valid = password
|
|
186
|
+
const valid = await bcrypt.compare(password, record.passwordHash!);
|
|
161
187
|
if (!valid) {
|
|
162
188
|
return reply.status(401).send({ error: "Incorrect password" });
|
|
163
189
|
}
|
|
@@ -184,26 +210,47 @@ export class ResourceController {
|
|
|
184
210
|
address: ResourceAddress,
|
|
185
211
|
token: string,
|
|
186
212
|
subPath: string,
|
|
213
|
+
sandboxConfig: { tenantId: string; workspaceId: string; projectId: string; assistantId: string | null },
|
|
187
214
|
reply: FastifyReply,
|
|
188
215
|
) {
|
|
189
216
|
const provider = this.deps.sandboxManager.getDefaultProvider();
|
|
190
217
|
const resolver = provider.getResourceResolver();
|
|
191
|
-
|
|
192
|
-
const fullPath = subPath
|
|
218
|
+
const sandboxPath = subPath
|
|
193
219
|
? this._resolveSafeSubPath(address.resourcePath, subPath)
|
|
194
220
|
: address.resourcePath;
|
|
195
221
|
|
|
222
|
+
this.deps.logger.info("[share] serving file", {
|
|
223
|
+
token, volume: address.volume, sandboxPath, subPath: subPath || "(none)",
|
|
224
|
+
tenantId: sandboxConfig.tenantId, workspaceId: sandboxConfig.workspaceId, projectId: sandboxConfig.projectId,
|
|
225
|
+
});
|
|
226
|
+
|
|
196
227
|
let buf: Buffer;
|
|
197
228
|
try {
|
|
198
|
-
|
|
229
|
+
// Try volume FS first
|
|
230
|
+
buf = await resolver.resolve({ ...address, resourcePath: sandboxPath });
|
|
231
|
+
this.deps.logger.info("[share] resolved via volume FS", { token, size: buf.length });
|
|
199
232
|
} catch (err) {
|
|
200
|
-
this.deps.logger.warn(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
233
|
+
this.deps.logger.warn("[share] volume FS failed, trying sandbox fallback", {
|
|
234
|
+
token, path: sandboxPath, error: (err as Error).message,
|
|
235
|
+
});
|
|
236
|
+
try {
|
|
237
|
+
const sandbox = await this.deps.sandboxManager.getSandboxFromConfig({
|
|
238
|
+
assistant_id: sandboxConfig.assistantId ?? "",
|
|
239
|
+
thread_id: "",
|
|
240
|
+
tenantId: sandboxConfig.tenantId,
|
|
241
|
+
workspaceId: sandboxConfig.workspaceId,
|
|
242
|
+
projectId: sandboxConfig.projectId,
|
|
243
|
+
});
|
|
244
|
+
buf = await sandbox.file.downloadFile({ file: `/project/${sandboxPath}` });
|
|
245
|
+
this.deps.logger.info("[share] resolved via sandbox fallback", { token, size: buf.length });
|
|
246
|
+
} catch (err) {
|
|
247
|
+
this.deps.logger.warn("[share] all resolution attempts failed", { token, resourcePath: sandboxPath, error: (err as Error).message });
|
|
248
|
+
return reply.status(404).send("File not found");
|
|
249
|
+
}
|
|
205
250
|
}
|
|
206
251
|
|
|
252
|
+
const fullPath = sandboxPath;
|
|
253
|
+
|
|
207
254
|
const filename = fullPath.split("/").pop() || "download";
|
|
208
255
|
const isHtml = !subPath && /\.(html|htm)$/i.test(filename);
|
|
209
256
|
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/controllers/resources.ts"],"sourcesContent":["import { FastifyRequest, FastifyReply } from \"fastify\";\nimport type { ResourceAddress, SharedResourceStore } from \"@axiom-lattice/protocols\";\nimport {\n generateToken,\n createSharePayload,\n TokenCache,\n SandboxLatticeManager,\n} from \"@axiom-lattice/core\";\nimport { getContentTypeFromFilename } from \"../utils/mime\";\n\ninterface ResourceControllerDeps {\n store: SharedResourceStore;\n sandboxManager: SandboxLatticeManager;\n cache: TokenCache;\n logger: { info: (msg: string, obj?: object) => void; warn: (msg: string, obj?: object) => void; error: (msg: string, obj?: object) => void };\n}\n\nexport class ResourceController {\n constructor(private deps: ResourceControllerDeps) {}\n\n async createShare(request: FastifyRequest, reply: FastifyReply) {\n const userId = (request as any).user\n ? (request as any).user.id ?? \"unknown\"\n : \"unknown\";\n const tenantId = (request.headers[\"x-tenant-id\"] as string) || \"default\";\n const workspaceId = request.headers[\"x-workspace-id\"] as string;\n const projectId = request.headers[\"x-project-id\"] as string;\n const body = request.body as any;\n\n if (!workspaceId || !projectId) {\n return reply.status(400).send({ error: \"x-workspace-id and x-project-id headers required\" });\n }\n\n const payload = createSharePayload(\n tenantId,\n workspaceId,\n projectId,\n userId as string,\n body,\n );\n const token = generateToken();\n\n try {\n await this.deps.store.create({ ...payload, token });\n this.deps.logger.info(\n \"Share created\",\n { token, resourcePath: body.resourcePath, userId },\n );\n } catch {\n return reply.status(500).send({ error: \"Failed to create share\" });\n }\n\n return reply.status(201).send({ token, url: `/s/${token}` });\n }\n\n async listShares(request: FastifyRequest, reply: FastifyReply) {\n const userId = (request as any).user\n ? (request as any).user.id ?? \"unknown\"\n : \"unknown\";\n const tenantId = (request.headers[\"x-tenant-id\"] as string) || \"default\";\n const shares = await this.deps.store.listByUser(tenantId, userId as string);\n return reply.send(shares);\n }\n\n async updateShare(request: FastifyRequest, reply: FastifyReply) {\n const { token } = request.params as unknown as { token: string };\n const userId = (request as any).user\n ? (request as any).user.id as string\n : undefined;\n const patch = request.body as any;\n\n const record = await this.deps.store.findByToken(token);\n if (!record || record.createdBy !== userId) {\n return reply.status(404).send({ error: \"Share not found\" });\n }\n\n await this.deps.store.update(token, patch);\n\n if (patch.revoked) {\n this.deps.cache.invalidate(token);\n }\n\n return reply.send({ ok: true });\n }\n\n async revokeShare(request: FastifyRequest, reply: FastifyReply) {\n const { token } = request.params as unknown as { token: string };\n const userId = (request as any).user\n ? (request as any).user.id as string\n : undefined;\n\n const record = await this.deps.store.findByToken(token);\n if (!record || record.createdBy !== userId) {\n return reply.status(404).send({ error: \"Share not found\" });\n }\n\n await this.deps.store.update(token, { revoked: true });\n this.deps.cache.invalidate(token);\n\n return reply.send({ ok: true });\n }\n\n async resolveResource(request: FastifyRequest, reply: FastifyReply) {\n const params = request.params as unknown as { token: string; \"*\"?: string };\n const { token } = params;\n const subPath = params[\"*\"] ?? \"\";\n\n // 1. Check cache (skip for password-protected — must re-check auth)\n const cached = this.deps.cache.get(token);\n if (cached && !cached.requiresUnlock) {\n return this._serveFile(cached.address, token, subPath, reply);\n }\n\n // 2. Single DB lookup\n const record = await this.deps.store.findByToken(token);\n if (!record || record.revoked) return reply.status(404).send(\"Not found\");\n if (record.expiresAt && new Date(record.expiresAt) < new Date()) {\n return reply.status(410).send(\"Expired\");\n }\n\n // 3. Auth for external visitors\n if (!this._isInternalRequest(request)) {\n if (record.visibility === \"password\") {\n const unlocked = this._isUnlocked(request, token);\n if (!unlocked) {\n // Cache as requiresUnlock so next hit rechecks\n this.deps.cache.set(token, { address: record.address, requiresUnlock: true });\n return reply.type(\"text/html\").send(this._passwordPage(token));\n }\n }\n }\n\n // 4. Access count\n if (record.maxAccess !== null) {\n const ok = await this.deps.store.atomicIncrementAccess(token);\n if (!ok) return reply.status(410).send(\"Access limit reached\");\n } else {\n this.deps.store.incrementAccess(token).catch(() => {});\n }\n\n // 5. Cache (public / internally accessed shares)\n this.deps.cache.set(token, {\n address: record.address,\n requiresUnlock: record.visibility === \"password\" && !this._isInternalRequest(request),\n });\n\n return this._serveFile(record.address, token, subPath, reply);\n }\n\n async unlockShare(request: FastifyRequest, reply: FastifyReply) {\n const { token } = request.params as unknown as { token: string };\n const { password } = request.body as unknown as { password: string };\n\n const record = await this.deps.store.findByToken(token);\n if (!record || !record.passwordHash) {\n return reply.status(404).send({ error: \"Invalid request\" });\n }\n\n // Plain text comparison for now — bcrypt will come in Task 12\n const valid = password === record.passwordHash;\n if (!valid) {\n return reply.status(401).send({ error: \"Incorrect password\" });\n }\n\n reply.header(\n \"Set-Cookie\",\n `share_unlock_${token}=1; Max-Age=86400; Path=/s/${token}; SameSite=Lax; HttpOnly; Secure`,\n );\n return reply.send({ ok: true });\n }\n\n private _isInternalRequest(request: FastifyRequest): boolean {\n // Check for valid user session from auth middleware\n const user = (request as any).user;\n return !!user && !!user.id;\n }\n\n private _isUnlocked(request: FastifyRequest, token: string): boolean {\n const cookie = request.headers.cookie ?? \"\";\n return cookie.includes(`share_unlock_${token}=1`);\n }\n\n private async _serveFile(\n address: ResourceAddress,\n token: string,\n subPath: string,\n reply: FastifyReply,\n ) {\n const provider = this.deps.sandboxManager.getDefaultProvider();\n const resolver = provider.getResourceResolver();\n\n const fullPath = subPath\n ? this._resolveSafeSubPath(address.resourcePath, subPath)\n : address.resourcePath;\n\n let buf: Buffer;\n try {\n buf = await resolver.resolve({ ...address, resourcePath: fullPath });\n } catch (err) {\n this.deps.logger.warn(\n \"Resource resolution failed\",\n { token, resourcePath: fullPath, error: (err as Error).message },\n );\n return reply.status(404).send(\"File not found\");\n }\n\n const filename = fullPath.split(\"/\").pop() || \"download\";\n const isHtml = !subPath && /\\.(html|htm)$/i.test(filename);\n\n if (isHtml) {\n buf = this._injectBaseTag(buf, token);\n }\n\n const contentType = getContentTypeFromFilename(filename);\n\n return reply\n .status(200)\n .type(contentType)\n .header(\"Content-Disposition\", `inline; filename=\"${filename}\"`)\n .header(\"Access-Control-Allow-Origin\", \"*\")\n .send(buf);\n }\n\n private _resolveSafeSubPath(entryFile: string, subPath: string): string {\n if (!subPath) return entryFile;\n if (subPath.includes(\"..\")) throw new Error(\"Path traversal denied\");\n const entryDir = entryFile.replace(/[^/]+$/, \"\");\n return (entryDir + subPath).replace(/\\/+/g, \"/\");\n }\n\n private _injectBaseTag(buf: Buffer, token: string): Buffer {\n const html = buf.toString(\"utf-8\");\n if (/<base\\b/i.test(html)) return buf;\n const baseTag = `<base href=\"/s/${token}/\">`;\n if (html.includes(\"</head>\")) {\n return Buffer.from(html.replace(\"</head>\", `${baseTag}</head>`), \"utf-8\");\n }\n // No </head> — insert after <head> or <html> or after <!doctype>\n if (html.includes(\"<head>\")) {\n return Buffer.from(html.replace(\"<head>\", `<head>${baseTag}`), \"utf-8\");\n }\n if (html.includes(\"<html>\")) {\n return Buffer.from(html.replace(\"<html>\", `<html>${baseTag}`), \"utf-8\");\n }\n // Fallback: prepend (will be after <!doctype> if present since html starts with <!doctype)\n return Buffer.from(baseTag + html, \"utf-8\");\n }\n\n private _passwordPage(token: string): string {\n return `<!DOCTYPE html><html><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>Password Protected</title><style>body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0;background:#f5f5f5}.card{background:#fff;padding:32px;border-radius:12px;box-shadow:0 2px 8px rgba(0,0,0,.1);width:100%;max-width:360px;text-align:center}h2{margin:0 0 8px;font-size:20px}p{margin:0 0 20px;color:#666;font-size:14px}input{width:100%;padding:10px 12px;border:1px solid #d9d9d9;border-radius:8px;font-size:14px;box-sizing:border-box;margin-bottom:12px}button{width:100%;padding:10px;background:#6366f1;color:#fff;border:none;border-radius:8px;font-size:14px;cursor:pointer}.error{color:#ef4444;font-size:13px;margin-top:8px;display:none}</style></head><body><div class=\"card\"><h2>Password Protected</h2><p>This share requires a password to access.</p><form id=\"f\"><input type=\"password\" id=\"p\" placeholder=\"Enter password\" required><button type=\"submit\">Unlock</button><div class=\"error\" id=\"e\">Incorrect password</div></form></div><script>document.getElementById('f').onsubmit=async(e)=>{e.preventDefault();const r=await fetch('/s/${token}/unlock',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:document.getElementById('p').value})});if(r.ok)location.reload();else document.getElementById('e').style.display='block'}</script></body></html>`;\n }\n}\n"],"mappings":"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","names":[]}
|