@helloleo/plugins 0.2.30 → 0.2.31-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +2 -1
- package/dist/index.js +106 -45
- package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/bun.lock +2 -2
- package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/bunfig.toml +1 -1
- package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/package.json +1 -1
- package/dist/templates/tanstack-start-2026-09-29/src/lib/proxy.server.ts +204 -0
- package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/vite.config.ts +5 -10
- package/package.json +1 -1
- package/dist/templates/tanstack-start-2026-09-11/src/lib/proxy.ts +0 -182
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/components.json +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/drizzle/meta/_journal.json +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/drizzle.config.ts +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/error-component.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/accordion.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/alert-dialog.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/alert.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/aspect-ratio.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/avatar.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/badge.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/breadcrumb.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/button.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/calendar.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/card.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/carousel.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/chart.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/checkbox.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/collapsible.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/command.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/context-menu.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/dialog.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/drawer.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/dropdown-menu.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/form.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/hover-card.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/input-otp.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/input.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/label.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/menubar.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/navigation-menu.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/pagination.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/popover.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/progress.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/radio-group.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/resizable.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/scroll-area.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/select.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/separator.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/sheet.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/sidebar.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/skeleton.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/slider.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/sonner.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/switch.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/table.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/tabs.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/textarea.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/toggle-group.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/toggle.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/components/ui/tooltip.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/db/index.ts +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/db/schema.ts +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/global.d.ts +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/hooks/use-mobile.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/lib/cache.ts +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/lib/kv.ts +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/lib/storage.ts +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/lib/utils.ts +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/routeTree.gen.ts +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/router.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/routes/README.md +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/routes/__root.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/routes/api/health.ts +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/routes/index.tsx +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/server.ts +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/styles.css +0 -0
- /package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/tsconfig.json +0 -0
package/dist/cli.js
CHANGED
|
@@ -12367,7 +12367,7 @@ async function renderTemplate(templateId, vars, destDir) {
|
|
|
12367
12367
|
}
|
|
12368
12368
|
|
|
12369
12369
|
// src/tools/scaffold.ts
|
|
12370
|
-
var SCAFFOLD_VERSION = "tanstack-start-2026-09-
|
|
12370
|
+
var SCAFFOLD_VERSION = "tanstack-start-2026-09-29";
|
|
12371
12371
|
function resolveTemplateId() {
|
|
12372
12372
|
return SCAFFOLD_VERSION;
|
|
12373
12373
|
}
|
|
@@ -12427,6 +12427,7 @@ var SCAFFOLD_NOTES = [
|
|
|
12427
12427
|
"- The database schema lives in src/db/schema.ts and is the single source of truth. Migration files under drizzle/ are generated and applied automatically. Never create, edit or delete them.",
|
|
12428
12428
|
"- Never rename existing tables or columns in schema.ts. Renames cannot be auto-migrated: no migration file is generated, the database keeps the old name, and every query against the new name fails at runtime with a 500. To reshape data, add a new column or table, write to it, and migrate old values in app code (e.g. fall back to the old column on read).",
|
|
12429
12429
|
"- Persistence goes through @helloleo/runtime exclusively: database via #/db (see src/db/index.ts), key-value store via #/lib/kv (see src/lib/kv.ts), file storage via #/lib/storage (see src/lib/storage.ts -- uploads/downloads flow through your API routes; presigned URLs are NOT available), env/secrets via env from @helloleo/runtime. Read those files before server-side data work. Never install another DB/storage client, never import from cloudflare:workers, all of it is server-side only.",
|
|
12430
|
+
"- Connected integrations (Odoo, Airtable, ...) go through src/lib/proxy.server.ts (callIntegration, uploadFile). Read its header first. It is server-only: import it from server routes under src/routes/api/, never from components. One route per operation; never write a route that forwards integration, endpoint, model, method or a raw body from the request.",
|
|
12430
12431
|
"- Add routes as files in src/routes/. Do not default export route components. Routing and layout conventions are documented in src/routes/README.md, read it before adding routes.",
|
|
12431
12432
|
"- shadcn/ui components are already in src/components/ui.",
|
|
12432
12433
|
"- Design system: style exclusively with the token utilities, never hardcoded colors. To change the look, edit the token values in src/styles.css (light and dark blocks).",
|
package/dist/index.js
CHANGED
|
@@ -14,6 +14,33 @@ var __export = (target, all) => {
|
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
// src/hooks/image-expiry.ts
|
|
18
|
+
var KEEP_TURNS = 3;
|
|
19
|
+
var EXPIRED_IMAGE_NOTE = "[A screenshot the user attached here was removed from context because the conversation has moved on. Do not refer to it. If you still need it, ask the user to share it again.]";
|
|
20
|
+
function isUserTurn(m) {
|
|
21
|
+
return m.info.role === "user" && m.parts.some((p) => p.type === "text" && !p.synthetic && !p.ignored);
|
|
22
|
+
}
|
|
23
|
+
var ImageExpiryPlugin = async () => ({
|
|
24
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
25
|
+
let laterTurns = 0;
|
|
26
|
+
for (let i = output.messages.length - 1;i >= 0; i--) {
|
|
27
|
+
const m = output.messages[i];
|
|
28
|
+
const age = laterTurns;
|
|
29
|
+
if (isUserTurn(m))
|
|
30
|
+
laterTurns++;
|
|
31
|
+
if (m.info.role !== "user" || age < KEEP_TURNS)
|
|
32
|
+
continue;
|
|
33
|
+
m.parts = m.parts.map((p) => p.type === "file" && p.mime.startsWith("image/") ? {
|
|
34
|
+
id: p.id,
|
|
35
|
+
sessionID: p.sessionID,
|
|
36
|
+
messageID: p.messageID,
|
|
37
|
+
type: "text",
|
|
38
|
+
text: EXPIRED_IMAGE_NOTE,
|
|
39
|
+
synthetic: true
|
|
40
|
+
} : p);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
17
44
|
// src/hooks/syntax-check.ts
|
|
18
45
|
import { readFileSync, statSync } from "fs";
|
|
19
46
|
import { relative } from "path";
|
|
@@ -116,6 +143,9 @@ var SyntaxCheckPlugin = async ({ worktree, directory }) => {
|
|
|
116
143
|
}
|
|
117
144
|
};
|
|
118
145
|
};
|
|
146
|
+
// src/tools/inspect-http.ts
|
|
147
|
+
import path from "path";
|
|
148
|
+
|
|
119
149
|
// node_modules/zod/v4/classic/external.js
|
|
120
150
|
var exports_external = {};
|
|
121
151
|
__export(exports_external, {
|
|
@@ -12490,6 +12520,11 @@ var inspectHttp = tool({
|
|
|
12490
12520
|
Origin: "https://app.helloleo.dev",
|
|
12491
12521
|
...args.headers
|
|
12492
12522
|
};
|
|
12523
|
+
if (!hasHeader(requestHeaders, "authorization") && isOwnProxyUrl(url, path.basename(context.directory))) {
|
|
12524
|
+
const key = await readWorkspaceEnv(context.directory, "HELLOLEO_API_KEY");
|
|
12525
|
+
if (key)
|
|
12526
|
+
requestHeaders.Authorization = `Bearer ${key}`;
|
|
12527
|
+
}
|
|
12493
12528
|
if (METHODS_AUTO_JSON.has(args.method) && args.body !== undefined && !hasHeader(requestHeaders, "content-type")) {
|
|
12494
12529
|
requestHeaders["Content-Type"] = "application/json";
|
|
12495
12530
|
}
|
|
@@ -12568,6 +12603,30 @@ function isPrivateHost(hostname) {
|
|
|
12568
12603
|
const base = findHostBase(hostname);
|
|
12569
12604
|
return base !== null && hosts[base].private;
|
|
12570
12605
|
}
|
|
12606
|
+
function isOwnProxyUrl(url, projectUuid) {
|
|
12607
|
+
if (!projectUuid || !isPrivateHost(url.hostname))
|
|
12608
|
+
return false;
|
|
12609
|
+
const pathname = url.pathname.replace(/\/+$/, "");
|
|
12610
|
+
if (pathname === `/api/integration-proxy/${projectUuid}`)
|
|
12611
|
+
return true;
|
|
12612
|
+
const host = url.hostname.toLowerCase();
|
|
12613
|
+
const isProxyHost = host === "proxy.helloleo.dev" || host.endsWith(".proxy.helloleo.dev");
|
|
12614
|
+
return isProxyHost && pathname === `/${projectUuid}`;
|
|
12615
|
+
}
|
|
12616
|
+
async function readWorkspaceEnv(dir, key) {
|
|
12617
|
+
try {
|
|
12618
|
+
const text = await Bun.file(path.join(dir, ".env")).text();
|
|
12619
|
+
for (const line of text.split(`
|
|
12620
|
+
`)) {
|
|
12621
|
+
const match = line.match(/^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
|
12622
|
+
if (match?.[1] === key) {
|
|
12623
|
+
const value = match[2].trim().replace(/^["']|["']$/g, "");
|
|
12624
|
+
return value || null;
|
|
12625
|
+
}
|
|
12626
|
+
}
|
|
12627
|
+
} catch {}
|
|
12628
|
+
return null;
|
|
12629
|
+
}
|
|
12571
12630
|
function hasHeader(headers, name) {
|
|
12572
12631
|
const target = name.toLowerCase();
|
|
12573
12632
|
for (const key of Object.keys(headers)) {
|
|
@@ -12625,23 +12684,23 @@ var InspectHttpPlugin = async () => {
|
|
|
12625
12684
|
};
|
|
12626
12685
|
// src/tools/remove.ts
|
|
12627
12686
|
import { unlink } from "fs/promises";
|
|
12628
|
-
import
|
|
12687
|
+
import path2 from "path";
|
|
12629
12688
|
var remove = tool({
|
|
12630
12689
|
description: "Remove a file from the project. The file path must be relative to the project root. " + "This tool will ask for permission before deleting. " + "Cannot delete files outside the project boundary.",
|
|
12631
12690
|
args: {
|
|
12632
12691
|
filePath: tool.schema.string().describe("Relative path to the file to remove (e.g. src/App.tsx)")
|
|
12633
12692
|
},
|
|
12634
12693
|
async execute(args, context) {
|
|
12635
|
-
const resolved =
|
|
12636
|
-
const normalized =
|
|
12637
|
-
if (!normalized.startsWith(context.directory +
|
|
12694
|
+
const resolved = path2.resolve(context.directory, args.filePath);
|
|
12695
|
+
const normalized = path2.normalize(resolved);
|
|
12696
|
+
if (!normalized.startsWith(context.directory + path2.sep) && normalized !== context.directory) {
|
|
12638
12697
|
throw new Error(`Path "${args.filePath}" resolves outside the project boundary. Deletion blocked.`);
|
|
12639
12698
|
}
|
|
12640
12699
|
const file = Bun.file(resolved);
|
|
12641
12700
|
if (!await file.exists()) {
|
|
12642
12701
|
throw new Error(`File not found: ${args.filePath}`);
|
|
12643
12702
|
}
|
|
12644
|
-
const relative =
|
|
12703
|
+
const relative = path2.relative(context.directory, resolved);
|
|
12645
12704
|
await context.ask({
|
|
12646
12705
|
permission: "remove",
|
|
12647
12706
|
patterns: [resolved],
|
|
@@ -12659,15 +12718,15 @@ var RemovePlugin = async () => {
|
|
|
12659
12718
|
};
|
|
12660
12719
|
// src/tools/scaffold.ts
|
|
12661
12720
|
import { mkdir } from "fs/promises";
|
|
12662
|
-
import
|
|
12721
|
+
import path4 from "path";
|
|
12663
12722
|
// src/template-render.ts
|
|
12664
|
-
import
|
|
12723
|
+
import path3 from "path";
|
|
12665
12724
|
var {Glob } = globalThis.Bun;
|
|
12666
|
-
var TEMPLATES_ROOT =
|
|
12725
|
+
var TEMPLATES_ROOT = path3.join(import.meta.dir, "templates");
|
|
12667
12726
|
var TOKEN_RE = /\{\{(LEO_[A-Z0-9_]+)\}\}/g;
|
|
12668
12727
|
var SKIP = /(^|\/)(node_modules|dist|dist-ssr|\.tanstack|\.output|bun\.lockb|package-lock\.json|yarn\.lock|pnpm-lock\.yaml)(\/|$)/;
|
|
12669
12728
|
async function renderTemplate(templateId, vars, destDir) {
|
|
12670
|
-
const root =
|
|
12729
|
+
const root = path3.join(TEMPLATES_ROOT, templateId);
|
|
12671
12730
|
const glob = new Glob("**/*");
|
|
12672
12731
|
const created = [];
|
|
12673
12732
|
for await (const rel of glob.scan({
|
|
@@ -12677,21 +12736,21 @@ async function renderTemplate(templateId, vars, destDir) {
|
|
|
12677
12736
|
})) {
|
|
12678
12737
|
if (SKIP.test(rel))
|
|
12679
12738
|
continue;
|
|
12680
|
-
const raw = await Bun.file(
|
|
12739
|
+
const raw = await Bun.file(path3.join(root, rel)).text();
|
|
12681
12740
|
const content = raw.replace(TOKEN_RE, (_m, key) => vars[key] ?? "");
|
|
12682
|
-
await Bun.write(
|
|
12741
|
+
await Bun.write(path3.join(destDir, rel), content);
|
|
12683
12742
|
created.push(rel);
|
|
12684
12743
|
}
|
|
12685
12744
|
return created.sort();
|
|
12686
12745
|
}
|
|
12687
12746
|
|
|
12688
12747
|
// src/tools/scaffold.ts
|
|
12689
|
-
var SCAFFOLD_VERSION = "tanstack-start-2026-09-
|
|
12748
|
+
var SCAFFOLD_VERSION = "tanstack-start-2026-09-29";
|
|
12690
12749
|
function resolveTemplateId() {
|
|
12691
12750
|
return SCAFFOLD_VERSION;
|
|
12692
12751
|
}
|
|
12693
12752
|
function projectUuidFromDir(dir) {
|
|
12694
|
-
return
|
|
12753
|
+
return path4.basename(dir);
|
|
12695
12754
|
}
|
|
12696
12755
|
function integrationProxyBase() {
|
|
12697
12756
|
return (process.env.PLUGINS_INTEGRATION_PROXY_URL || "https://api.app.helloleo.dev").replace(/\/+$/, "");
|
|
@@ -12713,10 +12772,10 @@ class AlreadyScaffoldedError extends Error {
|
|
|
12713
12772
|
}
|
|
12714
12773
|
}
|
|
12715
12774
|
async function existingScaffold(dir) {
|
|
12716
|
-
if (!await Bun.file(
|
|
12775
|
+
if (!await Bun.file(path4.join(dir, "package.json")).exists())
|
|
12717
12776
|
return null;
|
|
12718
12777
|
try {
|
|
12719
|
-
const meta = await Bun.file(
|
|
12778
|
+
const meta = await Bun.file(path4.join(dir, ".leo", "project.json")).json();
|
|
12720
12779
|
return typeof meta?.scaffold === "string" ? meta.scaffold : "legacy";
|
|
12721
12780
|
} catch {
|
|
12722
12781
|
return "legacy";
|
|
@@ -12735,9 +12794,9 @@ async function writeBaseScaffold({ name, description }, dir) {
|
|
|
12735
12794
|
LEO_PROJECT_UUID: projectUuid,
|
|
12736
12795
|
LEO_INTEGRATION_PROXY_URL: `${integrationProxyBase()}/api/integration-proxy/${projectUuid}`
|
|
12737
12796
|
}, dir);
|
|
12738
|
-
await mkdir(
|
|
12739
|
-
const leoPath =
|
|
12740
|
-
await Bun.write(
|
|
12797
|
+
await mkdir(path4.join(dir, ".leo"), { recursive: true });
|
|
12798
|
+
const leoPath = path4.join(".leo", "project.json");
|
|
12799
|
+
await Bun.write(path4.join(dir, leoPath), JSON.stringify({ $schema: 1, scaffold: templateId }, null, 2));
|
|
12741
12800
|
return [...created, leoPath];
|
|
12742
12801
|
}
|
|
12743
12802
|
var SCAFFOLD_NOTES = [
|
|
@@ -12746,6 +12805,7 @@ var SCAFFOLD_NOTES = [
|
|
|
12746
12805
|
"- The database schema lives in src/db/schema.ts and is the single source of truth. Migration files under drizzle/ are generated and applied automatically. Never create, edit or delete them.",
|
|
12747
12806
|
"- Never rename existing tables or columns in schema.ts. Renames cannot be auto-migrated: no migration file is generated, the database keeps the old name, and every query against the new name fails at runtime with a 500. To reshape data, add a new column or table, write to it, and migrate old values in app code (e.g. fall back to the old column on read).",
|
|
12748
12807
|
"- Persistence goes through @helloleo/runtime exclusively: database via #/db (see src/db/index.ts), key-value store via #/lib/kv (see src/lib/kv.ts), file storage via #/lib/storage (see src/lib/storage.ts -- uploads/downloads flow through your API routes; presigned URLs are NOT available), env/secrets via env from @helloleo/runtime. Read those files before server-side data work. Never install another DB/storage client, never import from cloudflare:workers, all of it is server-side only.",
|
|
12808
|
+
"- Connected integrations (Odoo, Airtable, ...) go through src/lib/proxy.server.ts (callIntegration, uploadFile). Read its header first. It is server-only: import it from server routes under src/routes/api/, never from components. One route per operation; never write a route that forwards integration, endpoint, model, method or a raw body from the request.",
|
|
12749
12809
|
"- Add routes as files in src/routes/. Do not default export route components. Routing and layout conventions are documented in src/routes/README.md, read it before adding routes.",
|
|
12750
12810
|
"- shadcn/ui components are already in src/components/ui.",
|
|
12751
12811
|
"- Design system: style exclusively with the token utilities, never hardcoded colors. To change the look, edit the token values in src/styles.css (light and dark blocks).",
|
|
@@ -12800,10 +12860,10 @@ var ScaffoldPlugin = async () => {
|
|
|
12800
12860
|
};
|
|
12801
12861
|
// src/tools/setup-netsuite.ts
|
|
12802
12862
|
import { mkdir as mkdir2, readFile } from "fs/promises";
|
|
12803
|
-
import
|
|
12863
|
+
import path5 from "path";
|
|
12804
12864
|
async function readScaffoldId(dir) {
|
|
12805
12865
|
try {
|
|
12806
|
-
const meta = await Bun.file(
|
|
12866
|
+
const meta = await Bun.file(path5.join(dir, ".leo", "project.json")).json();
|
|
12807
12867
|
return typeof meta?.scaffold === "string" ? meta.scaffold : null;
|
|
12808
12868
|
} catch {
|
|
12809
12869
|
return null;
|
|
@@ -12814,7 +12874,7 @@ async function isStartScaffold(dir) {
|
|
|
12814
12874
|
if (scaffoldId)
|
|
12815
12875
|
return scaffoldId.startsWith("tanstack-start");
|
|
12816
12876
|
try {
|
|
12817
|
-
const pkg = await Bun.file(
|
|
12877
|
+
const pkg = await Bun.file(path5.join(dir, "package.json")).json();
|
|
12818
12878
|
return Boolean(pkg?.dependencies?.["@tanstack/react-start"] ?? pkg?.devDependencies?.["@tanstack/react-start"]);
|
|
12819
12879
|
} catch {
|
|
12820
12880
|
return false;
|
|
@@ -13975,18 +14035,18 @@ function Gate({ children }: { children: ReactNode }) {
|
|
|
13975
14035
|
`;
|
|
13976
14036
|
}
|
|
13977
14037
|
async function executeStart(dir) {
|
|
13978
|
-
await mkdir2(
|
|
13979
|
-
await mkdir2(
|
|
14038
|
+
await mkdir2(path5.join(dir, "src", "lib"), { recursive: true });
|
|
14039
|
+
await mkdir2(path5.join(dir, "src", "components", "auth"), {
|
|
13980
14040
|
recursive: true
|
|
13981
14041
|
});
|
|
13982
|
-
await mkdir2(
|
|
14042
|
+
await mkdir2(path5.join(dir, "src", "routes", "auth", "netsuite"), {
|
|
13983
14043
|
recursive: true
|
|
13984
14044
|
});
|
|
13985
|
-
await mkdir2(
|
|
14045
|
+
await mkdir2(path5.join(dir, "src", "routes", "api"), { recursive: true });
|
|
13986
14046
|
const schemaImports = `import { sql } from 'drizzle-orm'
|
|
13987
14047
|
import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
13988
14048
|
`;
|
|
13989
|
-
const schemaPath =
|
|
14049
|
+
const schemaPath = path5.join(dir, "src", "db", "schema.ts");
|
|
13990
14050
|
let schemaAppended = false;
|
|
13991
14051
|
try {
|
|
13992
14052
|
const current = await readFile(schemaPath, "utf8");
|
|
@@ -14000,41 +14060,41 @@ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
|
14000
14060
|
schemaAppended = true;
|
|
14001
14061
|
}
|
|
14002
14062
|
const files = [
|
|
14003
|
-
[
|
|
14063
|
+
[path5.join("src", "lib", "netsuite-server.ts"), netsuiteServerTs()],
|
|
14004
14064
|
[
|
|
14005
|
-
|
|
14065
|
+
path5.join("src", "routes", "auth", "netsuite", "start.ts"),
|
|
14006
14066
|
startRouteTs()
|
|
14007
14067
|
],
|
|
14008
14068
|
[
|
|
14009
|
-
|
|
14069
|
+
path5.join("src", "routes", "auth", "netsuite", "callback.ts"),
|
|
14010
14070
|
callbackRouteTs()
|
|
14011
14071
|
],
|
|
14012
14072
|
[
|
|
14013
|
-
|
|
14073
|
+
path5.join("src", "routes", "auth", "netsuite", "logout.ts"),
|
|
14014
14074
|
logoutRouteTs()
|
|
14015
14075
|
],
|
|
14016
14076
|
[
|
|
14017
|
-
|
|
14077
|
+
path5.join("src", "routes", "auth", "netsuite", "config.ts"),
|
|
14018
14078
|
configRouteTs()
|
|
14019
14079
|
],
|
|
14020
|
-
[
|
|
14021
|
-
[
|
|
14022
|
-
[
|
|
14080
|
+
[path5.join("src", "routes", "api", "me.ts"), meRouteTs()],
|
|
14081
|
+
[path5.join("src", "lib", "netsuite.ts"), netsuiteClientTs()],
|
|
14082
|
+
[path5.join("src", "lib", "auth.tsx"), startAuthTsx()],
|
|
14023
14083
|
[
|
|
14024
|
-
|
|
14084
|
+
path5.join("src", "components", "auth", "AuthLayout.tsx"),
|
|
14025
14085
|
startAuthLayoutTsx()
|
|
14026
14086
|
],
|
|
14027
14087
|
[
|
|
14028
|
-
|
|
14088
|
+
path5.join("src", "components", "auth", "LoginPage.tsx"),
|
|
14029
14089
|
startLoginPageTsx()
|
|
14030
14090
|
],
|
|
14031
14091
|
[
|
|
14032
|
-
|
|
14092
|
+
path5.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
|
|
14033
14093
|
startSignInButtonTsx()
|
|
14034
14094
|
]
|
|
14035
14095
|
];
|
|
14036
14096
|
for (const [filePath, content] of files) {
|
|
14037
|
-
await Bun.write(
|
|
14097
|
+
await Bun.write(path5.join(dir, filePath), content);
|
|
14038
14098
|
}
|
|
14039
14099
|
return [
|
|
14040
14100
|
`Wrote ${files.length} NetSuite files${schemaAppended ? " + appended src/db/schema.ts" : ""}:`,
|
|
@@ -14083,27 +14143,27 @@ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
|
14083
14143
|
`);
|
|
14084
14144
|
}
|
|
14085
14145
|
async function executeSpa(dir) {
|
|
14086
|
-
await mkdir2(
|
|
14146
|
+
await mkdir2(path5.join(dir, "src", "components", "auth"), {
|
|
14087
14147
|
recursive: true
|
|
14088
14148
|
});
|
|
14089
14149
|
const nsFiles = [
|
|
14090
|
-
[
|
|
14091
|
-
[
|
|
14150
|
+
[path5.join("src", "lib", "netsuite.ts"), spaNetsuiteTs()],
|
|
14151
|
+
[path5.join("src", "lib", "auth.tsx"), spaAuthTsx()],
|
|
14092
14152
|
[
|
|
14093
|
-
|
|
14153
|
+
path5.join("src", "components", "auth", "AuthLayout.tsx"),
|
|
14094
14154
|
spaAuthLayoutTsx()
|
|
14095
14155
|
],
|
|
14096
14156
|
[
|
|
14097
|
-
|
|
14157
|
+
path5.join("src", "components", "auth", "LoginPage.tsx"),
|
|
14098
14158
|
spaLoginPageTsx()
|
|
14099
14159
|
],
|
|
14100
14160
|
[
|
|
14101
|
-
|
|
14161
|
+
path5.join("src", "components", "auth", "SignInWithNetSuite.tsx"),
|
|
14102
14162
|
spaSignInButtonTsx()
|
|
14103
14163
|
]
|
|
14104
14164
|
];
|
|
14105
14165
|
for (const [filePath, content] of nsFiles) {
|
|
14106
|
-
await Bun.write(
|
|
14166
|
+
await Bun.write(path5.join(dir, filePath), content);
|
|
14107
14167
|
}
|
|
14108
14168
|
return [
|
|
14109
14169
|
`Wrote ${nsFiles.length} NetSuite OAuth files (all additive, nothing overwritten):`,
|
|
@@ -14148,6 +14208,7 @@ var SetupNetsuitePlugin = async () => {
|
|
|
14148
14208
|
};
|
|
14149
14209
|
};
|
|
14150
14210
|
export {
|
|
14211
|
+
ImageExpiryPlugin,
|
|
14151
14212
|
InspectHttpPlugin,
|
|
14152
14213
|
RemovePlugin,
|
|
14153
14214
|
ScaffoldPlugin,
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
"devDependencies": {
|
|
63
63
|
"@cloudflare/vite-plugin": "1.54.4",
|
|
64
64
|
"@cloudflare/workers-types": "5.20260908.1",
|
|
65
|
-
"@helloleo/vite-config": "0.1.
|
|
65
|
+
"@helloleo/vite-config": "0.1.7",
|
|
66
66
|
"@libsql/client": "0.17.4",
|
|
67
67
|
"@tailwindcss/typography": "0.5.20",
|
|
68
68
|
"@tanstack/router-cli": "1.167.34",
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
|
|
207
207
|
"@helloleo/runtime": ["@helloleo/runtime@0.1.6", "", { "dependencies": { "aws4fetch": "^1.0.20" }, "peerDependencies": { "@libsql/client": "*", "drizzle-orm": "^0.45.2" }, "optionalPeers": ["@libsql/client"] }, "sha512-oH5v5cZ0xMB8/IOg3VsJ2OpoG9Bod6D8UXjR2IGjFKAwx3vj62phit+3daJ9Wwpn3EuX4+BHs84swLfsOW3reg=="],
|
|
208
208
|
|
|
209
|
-
"@helloleo/vite-config": ["@helloleo/vite-config@0.1.
|
|
209
|
+
"@helloleo/vite-config": ["@helloleo/vite-config@0.1.7", "", { "peerDependencies": { "@cloudflare/vite-plugin": "*", "@tailwindcss/vite": "*", "@tanstack/react-start": "*", "@tanstack/router-plugin": "*", "@vitejs/plugin-react": "*", "vite": "*" } }, "sha512-couH35PuS5qN11tYvEJBl/ccdoP0Zec3KaUwc5vMGeihNd82k2bRqIAZGEGqAF2YJPjGr74mK/0vglCGDIT8Aw=="],
|
|
210
210
|
|
|
211
211
|
"@hookform/resolvers": ["@hookform/resolvers@5.9.1", "", { "dependencies": { "@standard-schema/utils": "^0.3.0" }, "peerDependencies": { "@sinclair/typebox": ">=0.25.24", "@standard-schema/spec": "^1.0.0", "@typeschema/main": ">=0.13.7", "@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0", "ajv": "^8.12.0", "ajv-errors": "^3.0.0", "ajv-formats": "^2.1.1", "arktype": "^2.0.0", "ata-validator": "^1.2.0", "class-transformer": ">=0.4.0", "class-validator": ">=0.12.0", "computed-types": "^1.0.0", "effect": "^3.10.3", "fluentvalidation-ts": "^3.0.0", "fp-ts": "^2.7.0", "io-ts": "^2.0.0", "joi": "^17.0.0 || ^18.0.0", "nope-validator": ">=0.12.0", "react-hook-form": "^7.55.0", "superstruct": ">=0.12.0", "typanion": "^3.3.2", "valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc", "vest": ">=6.0.0", "yup": "^1.0.0", "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["@sinclair/typebox", "@standard-schema/spec", "@typeschema/main", "@vinejs/vine", "ajv", "ajv-errors", "ajv-formats", "arktype", "ata-validator", "class-transformer", "class-validator", "computed-types", "effect", "fluentvalidation-ts", "fp-ts", "io-ts", "joi", "nope-validator", "superstruct", "typanion", "valibot", "vest", "yup", "zod"] }, "sha512-7b7vsbraJxKgjVSA1Nur9tLwj539WGJUBLA7QNvXnFoT2pM5Z7G+6rlukk4B2/QrTZy6huRtH6wKeESPKuIr6w=="],
|
|
212
212
|
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"devDependencies": {
|
|
73
73
|
"@cloudflare/vite-plugin": "1.54.4",
|
|
74
74
|
"@cloudflare/workers-types": "5.20260908.1",
|
|
75
|
-
"@helloleo/vite-config": "0.1.
|
|
75
|
+
"@helloleo/vite-config": "0.1.7",
|
|
76
76
|
"@libsql/client": "0.17.4",
|
|
77
77
|
"@tailwindcss/typography": "0.5.20",
|
|
78
78
|
"drizzle-kit": "0.31.10",
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import '@tanstack/react-start/server-only'
|
|
2
|
+
import { env } from '@helloleo/runtime'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* HelloLeo Integration Proxy: server-side helper for connected integrations.
|
|
6
|
+
*
|
|
7
|
+
* All third-party APIs (Odoo, Airtable, HubSpot, Stripe, Notion, Gladia,
|
|
8
|
+
* Microsoft 365, Klaviyo, Supabase, DimoMaint, etc.) are reachable through
|
|
9
|
+
* this single proxy. The proxy holds the credentials the user configured in
|
|
10
|
+
* Project Settings -- you NEVER pass an integration's API key yourself, and you
|
|
11
|
+
* NEVER call third-party hosts directly.
|
|
12
|
+
*
|
|
13
|
+
* SERVER-ONLY. Every call carries this project's HELLOLEO_API_KEY, which only
|
|
14
|
+
* exists on the server. Importing this file from browser code fails the build
|
|
15
|
+
* (the import above, plus the .server.ts name). Call it from a server route
|
|
16
|
+
* under src/routes/api/ and fetch that route from the page with react-query:
|
|
17
|
+
*
|
|
18
|
+
* // src/routes/api/products.ts
|
|
19
|
+
* import { createFileRoute } from '@tanstack/react-router'
|
|
20
|
+
* import { callIntegration } from '#/lib/proxy.server'
|
|
21
|
+
*
|
|
22
|
+
* export const Route = createFileRoute('/api/products')({
|
|
23
|
+
* server: {
|
|
24
|
+
* handlers: {
|
|
25
|
+
* GET: async () => {
|
|
26
|
+
* const r = await callIntegration({
|
|
27
|
+
* integration: 'odoo',
|
|
28
|
+
* endpoint: '/jsonrpc',
|
|
29
|
+
* body: {
|
|
30
|
+
* model: 'product.template',
|
|
31
|
+
* method: 'search_read',
|
|
32
|
+
* args: [[['type', '=', 'consu']]],
|
|
33
|
+
* kwargs: { fields: ['name', 'list_price'], limit: 50 },
|
|
34
|
+
* },
|
|
35
|
+
* })
|
|
36
|
+
* if (!r.success) return Response.json({ error: r.data }, { status: 502 })
|
|
37
|
+
* return Response.json(r.data?.result ?? [])
|
|
38
|
+
* },
|
|
39
|
+
* },
|
|
40
|
+
* },
|
|
41
|
+
* })
|
|
42
|
+
*
|
|
43
|
+
* One route per operation the app needs. NEVER write a route that forwards
|
|
44
|
+
* `integration`, `endpoint`, `model`, `method` or a raw body from the request
|
|
45
|
+
* to callIntegration: that hands every visitor the user's full integration
|
|
46
|
+
* access, with this project's key attached.
|
|
47
|
+
*
|
|
48
|
+
* Integration slugs:
|
|
49
|
+
* Use the exact slug from the Enabled MCP list with the "pipedream-"
|
|
50
|
+
* prefix removed. e.g. "pipedream-gorgias_oauth" -> "gorgias_oauth".
|
|
51
|
+
* Native integrations (odoo, airtable) keep their plain name.
|
|
52
|
+
*
|
|
53
|
+
* Response shape (always wrapped):
|
|
54
|
+
* { success: boolean, status: number, data: <provider payload>, docs: string }
|
|
55
|
+
*
|
|
56
|
+
* - `status` is the UPSTREAM status, not the proxy's.
|
|
57
|
+
* - `success` is false whenever that status is >= 400.
|
|
58
|
+
* - `data` is null when the upstream returned no body.
|
|
59
|
+
* - `docs` links the proxy's own reference.
|
|
60
|
+
*
|
|
61
|
+
* Unwrap "data" by provider:
|
|
62
|
+
* odoo -> data.result
|
|
63
|
+
* airtable -> data.records
|
|
64
|
+
* notion -> data.results
|
|
65
|
+
* microsoft 365 -> data.value
|
|
66
|
+
* klaviyo -> data.data
|
|
67
|
+
* supabase / dimomaint -> data
|
|
68
|
+
*
|
|
69
|
+
* A 401 with code "api_key_missing" or "api_key_invalid" comes from the
|
|
70
|
+
* proxy itself: the call ran without this project's key. Its `llm` field
|
|
71
|
+
* says what to change.
|
|
72
|
+
*
|
|
73
|
+
* Pagination: put query params directly in the endpoint string,
|
|
74
|
+
* e.g. endpoint: '/api/tickets?cursor=xyz&per_page=100'.
|
|
75
|
+
* Read pagination metadata from the response to build the next call.
|
|
76
|
+
* Never .filter() large lists in the page -- paginate in the route.
|
|
77
|
+
*
|
|
78
|
+
* Sending files:
|
|
79
|
+
* Most APIs take a file as base64 inside the normal JSON body. Odoo is the
|
|
80
|
+
* common case: ir.attachment.create with the bytes in `datas` and BOTH
|
|
81
|
+
* `res_model` and `res_id` set, or the file is stored but appears on no
|
|
82
|
+
* record. Nothing special is needed for those.
|
|
83
|
+
*
|
|
84
|
+
* Some upload endpoints accept only multipart/form-data and answer JSON with
|
|
85
|
+
* HTTP 415. For those set bodyEncoding: 'multipart' (see uploadFile below).
|
|
86
|
+
* Never set a Content-Type header for multipart, the proxy lets fetch
|
|
87
|
+
* generate the boundary. Not accepted for odoo, which has no multipart API.
|
|
88
|
+
*
|
|
89
|
+
* Either way the browser POSTs the file to one of the app's own routes as
|
|
90
|
+
* FormData; the route reads it with `await request.formData()` and encodes
|
|
91
|
+
* it with fileToBase64 below.
|
|
92
|
+
*
|
|
93
|
+
* Secrets for non-integration APIs (e.g. raw OpenAI): store them in the
|
|
94
|
+
* project's Variables and read them via `env` from @helloleo/runtime, in
|
|
95
|
+
* server code only.
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
export const INTEGRATION_PROXY_URL = '{{LEO_INTEGRATION_PROXY_URL}}'
|
|
99
|
+
|
|
100
|
+
// This project's key, issued by HelloLeo per target and injected into the
|
|
101
|
+
// server env: the preview holds one value, the published app another. The
|
|
102
|
+
// proxy reads which one it got to pick the integration connection, so the
|
|
103
|
+
// published app talks to production and the preview to development with no
|
|
104
|
+
// flag to set. Not in @helloleo/runtime's RuntimeEnv, hence the local shape.
|
|
105
|
+
const platformEnv = env as unknown as { HELLOLEO_API_KEY?: string }
|
|
106
|
+
|
|
107
|
+
function proxyHeaders(): Record<string, string> {
|
|
108
|
+
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
|
|
109
|
+
const key = platformEnv.HELLOLEO_API_KEY
|
|
110
|
+
if (key) headers.Authorization = `Bearer ${key}`
|
|
111
|
+
return headers
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface MultipartFile {
|
|
115
|
+
/** Form field name the upstream expects, e.g. 'content' or 'file'. */
|
|
116
|
+
name: string
|
|
117
|
+
filename: string
|
|
118
|
+
contentType?: string
|
|
119
|
+
/** File bytes, base64-encoded (no data: prefix). */
|
|
120
|
+
dataBase64: string
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface MultipartBody {
|
|
124
|
+
fields?: Record<string, string>
|
|
125
|
+
files?: MultipartFile[]
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ProxyRequest {
|
|
129
|
+
integration: string
|
|
130
|
+
endpoint: string
|
|
131
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
132
|
+
body?: unknown
|
|
133
|
+
headers?: Record<string, string>
|
|
134
|
+
/** Omit for JSON. 'multipart' when the upstream upload endpoint rejects JSON with 415. */
|
|
135
|
+
bodyEncoding?: 'json' | 'multipart'
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface ProxyResponse<T = unknown> {
|
|
139
|
+
success: boolean
|
|
140
|
+
/** The UPSTREAM status, not the proxy's. */
|
|
141
|
+
status: number
|
|
142
|
+
/** Null when the upstream returned no body. */
|
|
143
|
+
data: T | null
|
|
144
|
+
/** The proxy's own reference, on every response. */
|
|
145
|
+
docs: string
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function callIntegration<T = unknown>(
|
|
149
|
+
req: ProxyRequest,
|
|
150
|
+
): Promise<ProxyResponse<T>> {
|
|
151
|
+
if (req.integration.toLowerCase() === 'netsuite') {
|
|
152
|
+
throw new Error(
|
|
153
|
+
"NetSuite is not proxied -- call it from this app's own /auth/netsuite + " +
|
|
154
|
+
"/api server routes with the signed-in user's session, not callIntegration().",
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
const res = await fetch(INTEGRATION_PROXY_URL, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: proxyHeaders(),
|
|
160
|
+
body: JSON.stringify({ method: 'POST', ...req }),
|
|
161
|
+
})
|
|
162
|
+
return res.json()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Upload a file to an integration whose endpoint requires multipart/form-data
|
|
167
|
+
* (Zoho WorkDrive and similar). For Odoo, do NOT use this -- create an
|
|
168
|
+
* ir.attachment with the bytes base64-encoded in `datas` instead.
|
|
169
|
+
*/
|
|
170
|
+
export async function uploadFile<T = unknown>(
|
|
171
|
+
req: Omit<ProxyRequest, 'body' | 'bodyEncoding'> & { body: MultipartBody },
|
|
172
|
+
): Promise<ProxyResponse<T>> {
|
|
173
|
+
return callIntegration<T>({ method: 'POST', ...req, bodyEncoding: 'multipart' })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function fileToBase64(file: Blob): Promise<string> {
|
|
177
|
+
const bytes = new Uint8Array(await file.arrayBuffer())
|
|
178
|
+
let binary = ''
|
|
179
|
+
for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i])
|
|
180
|
+
return btoa(binary)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/* Example -- multipart upload (Zoho WorkDrive), inside a POST route handler:
|
|
184
|
+
*
|
|
185
|
+
* POST: async ({ request }) => {
|
|
186
|
+
* const form = await request.formData()
|
|
187
|
+
* const file = form.get('file') as File
|
|
188
|
+
* const r = await uploadFile({
|
|
189
|
+
* integration: 'zoho_workdrive',
|
|
190
|
+
* endpoint: '/api/v1/upload',
|
|
191
|
+
* body: {
|
|
192
|
+
* fields: { parent_id: FOLDER_ID, 'override-name-exist': 'true' },
|
|
193
|
+
* files: [{
|
|
194
|
+
* name: 'content',
|
|
195
|
+
* filename: file.name,
|
|
196
|
+
* contentType: file.type,
|
|
197
|
+
* dataBase64: await fileToBase64(file),
|
|
198
|
+
* }],
|
|
199
|
+
* },
|
|
200
|
+
* })
|
|
201
|
+
* if (!r.success) return Response.json({ error: r.data }, { status: 502 })
|
|
202
|
+
* return Response.json(r.data)
|
|
203
|
+
* }
|
|
204
|
+
*/
|
package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/vite.config.ts
RENAMED
|
@@ -4,14 +4,9 @@ import { defineConfig } from '@helloleo/vite-config'
|
|
|
4
4
|
// aliases `cloudflare:workers` -> @helloleo/runtime/cf-shim (libsql .dev.db for
|
|
5
5
|
// D1+KV, local disk for R2) and runs plain Vite SSR; at build it adds
|
|
6
6
|
// @cloudflare/vite-plugin to emit a workerd Worker bound to wrangler.jsonc. It
|
|
7
|
-
// also binds host:true/port:5173 itself (>=0.1.2)
|
|
7
|
+
// also binds host:true/port:5173 itself (>=0.1.2) and blocks *.server.* files
|
|
8
|
+
// from the client bundle (>=0.1.7).
|
|
8
9
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
export default defineConfig(
|
|
12
|
-
vite: {
|
|
13
|
-
server: {
|
|
14
|
-
hmr: { clientPort: 443, protocol: 'wss' },
|
|
15
|
-
},
|
|
16
|
-
},
|
|
17
|
-
})
|
|
10
|
+
// No HMR override: with no clientPort set, the Vite client connects back to the
|
|
11
|
+
// page's own origin, which is wss://…:443 behind the preview proxy.
|
|
12
|
+
export default defineConfig()
|
package/package.json
CHANGED
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* HelloLeo Integration Proxy: runtime helper for connected integrations.
|
|
3
|
-
*
|
|
4
|
-
* All third-party APIs (Odoo, Airtable, HubSpot, Stripe, Notion, Gladia,
|
|
5
|
-
* Microsoft 365, Klaviyo, Supabase, DimoMaint, etc.) are reachable through
|
|
6
|
-
* this single proxy. The proxy handles auth and CORS using credentials the
|
|
7
|
-
* user already configured in Project Settings -- you NEVER pass an API key
|
|
8
|
-
* from frontend code, and you NEVER call third-party hosts directly.
|
|
9
|
-
*
|
|
10
|
-
* Two modes:
|
|
11
|
-
* A) Frontend-only project -> import callIntegration() from this file.
|
|
12
|
-
* B) HelloLeo Cloud project -> route via Worker fetch(env.INTEGRATION_PROXY).
|
|
13
|
-
* Use Cloud whenever the project needs backend logic, automations,
|
|
14
|
-
* webhooks, scheduled jobs, or AI agents. Keep secrets server-side.
|
|
15
|
-
*
|
|
16
|
-
* Integration slugs:
|
|
17
|
-
* Use the exact slug from the Enabled MCP list with the "pipedream-"
|
|
18
|
-
* prefix removed. e.g. "pipedream-gorgias_oauth" -> "gorgias_oauth".
|
|
19
|
-
* Native integrations (odoo, airtable) keep their plain name.
|
|
20
|
-
*
|
|
21
|
-
* Response shape (always wrapped):
|
|
22
|
-
* { success: boolean, status: number, data: <provider payload>, docs: string }
|
|
23
|
-
*
|
|
24
|
-
* - `status` is the UPSTREAM status, not the proxy's.
|
|
25
|
-
* - `success` is false whenever that status is >= 400.
|
|
26
|
-
* - `data` is null when the upstream returned no body.
|
|
27
|
-
* - `docs` links the proxy's own reference.
|
|
28
|
-
*
|
|
29
|
-
* Unwrap "data" by provider:
|
|
30
|
-
* odoo -> data.result
|
|
31
|
-
* airtable -> data.records
|
|
32
|
-
* notion -> data.results
|
|
33
|
-
* microsoft 365 -> data.value
|
|
34
|
-
* klaviyo -> data.data
|
|
35
|
-
* supabase / dimomaint -> data
|
|
36
|
-
*
|
|
37
|
-
* Pagination: put query params directly in the endpoint string,
|
|
38
|
-
* e.g. endpoint: '/api/tickets?cursor=xyz&per_page=100'.
|
|
39
|
-
* Read pagination metadata from the response to build the next call.
|
|
40
|
-
* Never .filter() large lists in the frontend -- paginate server-side.
|
|
41
|
-
*
|
|
42
|
-
* Sending files:
|
|
43
|
-
* Most APIs take a file as base64 inside the normal JSON body. Odoo is the
|
|
44
|
-
* common case: ir.attachment.create with the bytes in `datas` and BOTH
|
|
45
|
-
* `res_model` and `res_id` set, or the file is stored but appears on no
|
|
46
|
-
* record. Nothing special is needed for those.
|
|
47
|
-
*
|
|
48
|
-
* Some upload endpoints accept only multipart/form-data and answer JSON with
|
|
49
|
-
* HTTP 415. For those set bodyEncoding: 'multipart' (see uploadFile below).
|
|
50
|
-
* Never set a Content-Type header for multipart, the proxy lets fetch
|
|
51
|
-
* generate the boundary. Not accepted for odoo, which has no multipart API.
|
|
52
|
-
*
|
|
53
|
-
* Secrets for non-integration APIs (e.g. raw OpenAI): store them in
|
|
54
|
-
* HelloLeo Cloud -> Settings -> Cloud -> Secrets, and read via env.SECRET_NAME
|
|
55
|
-
* from a Worker. Never embed secrets in frontend code.
|
|
56
|
-
*/
|
|
57
|
-
|
|
58
|
-
export const INTEGRATION_PROXY_URL = '{{LEO_INTEGRATION_PROXY_URL}}';
|
|
59
|
-
|
|
60
|
-
// Multi-environment integrations: the published HelloLeo Cloud Worker has
|
|
61
|
-
// HELLOLEO_ENV=production injected at publish. Forwarding `?env=production`
|
|
62
|
-
// makes the proxy resolve credentials from the project's PRODUCTION integration
|
|
63
|
-
// connection (e.g. your production Odoo) instead of the development one, so a
|
|
64
|
-
// published app talks to production while the preview keeps talking to staging.
|
|
65
|
-
//
|
|
66
|
-
// Reads the flag from the server runtime (Cloudflare Worker / Node) when
|
|
67
|
-
// available; in the browser it's absent and we send no param — the proxy then
|
|
68
|
-
// derives the environment from the request origin, so client-side calls from
|
|
69
|
-
// the published app still resolve to production. Server-side calls (SSR / Cloud
|
|
70
|
-
// Worker) carry no browser origin, which is exactly why the explicit param
|
|
71
|
-
// matters there.
|
|
72
|
-
function proxyUrl(): string {
|
|
73
|
-
// Read via globalThis only (no bare `process` reference) so this compiles in
|
|
74
|
-
// any scaffolded project regardless of whether node types are present.
|
|
75
|
-
const g = globalThis as {
|
|
76
|
-
HELLOLEO_ENV?: string;
|
|
77
|
-
process?: { env?: Record<string, string | undefined> };
|
|
78
|
-
};
|
|
79
|
-
const env = g.HELLOLEO_ENV ?? g.process?.env?.HELLOLEO_ENV;
|
|
80
|
-
return env === 'production' ? `${INTEGRATION_PROXY_URL}?env=production` : INTEGRATION_PROXY_URL;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface MultipartFile {
|
|
84
|
-
/** Form field name the upstream expects, e.g. 'content' or 'file'. */
|
|
85
|
-
name: string;
|
|
86
|
-
filename: string;
|
|
87
|
-
contentType?: string;
|
|
88
|
-
/** File bytes, base64-encoded (no data: prefix). */
|
|
89
|
-
dataBase64: string;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface MultipartBody {
|
|
93
|
-
fields?: Record<string, string>;
|
|
94
|
-
files?: MultipartFile[];
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export interface ProxyRequest {
|
|
98
|
-
integration: string;
|
|
99
|
-
endpoint: string;
|
|
100
|
-
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
|
|
101
|
-
body?: unknown;
|
|
102
|
-
headers?: Record<string, string>;
|
|
103
|
-
/** Omit for JSON. 'multipart' when the upstream upload endpoint rejects JSON with 415. */
|
|
104
|
-
bodyEncoding?: 'json' | 'multipart';
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export interface ProxyResponse<T = unknown> {
|
|
108
|
-
success: boolean;
|
|
109
|
-
/** The UPSTREAM status, not the proxy's. */
|
|
110
|
-
status: number;
|
|
111
|
-
/** Null when the upstream returned no body. */
|
|
112
|
-
data: T | null;
|
|
113
|
-
/** The proxy's own reference, on every response. */
|
|
114
|
-
docs: string;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export async function callIntegration<T = unknown>(
|
|
118
|
-
req: ProxyRequest,
|
|
119
|
-
): Promise<ProxyResponse<T>> {
|
|
120
|
-
if (req.integration.toLowerCase() === 'netsuite') {
|
|
121
|
-
throw new Error(
|
|
122
|
-
'NetSuite is not proxied -- call it from this app\'s own /auth/netsuite + ' +
|
|
123
|
-
'/api server routes with the signed-in user\'s session, not callIntegration().',
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
const res = await fetch(proxyUrl(), {
|
|
127
|
-
method: 'POST',
|
|
128
|
-
headers: { 'Content-Type': 'application/json' },
|
|
129
|
-
body: JSON.stringify({ method: 'POST', ...req }),
|
|
130
|
-
});
|
|
131
|
-
return res.json();
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Upload a file to an integration whose endpoint requires multipart/form-data
|
|
136
|
-
* (Zoho WorkDrive and similar). For Odoo, do NOT use this -- create an
|
|
137
|
-
* ir.attachment with the bytes base64-encoded in `datas` instead.
|
|
138
|
-
*/
|
|
139
|
-
export async function uploadFile<T = unknown>(
|
|
140
|
-
req: Omit<ProxyRequest, 'body' | 'bodyEncoding'> & { body: MultipartBody },
|
|
141
|
-
): Promise<ProxyResponse<T>> {
|
|
142
|
-
return callIntegration<T>({ method: 'POST', ...req, bodyEncoding: 'multipart' });
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
export async function fileToBase64(file: Blob): Promise<string> {
|
|
146
|
-
const bytes = new Uint8Array(await file.arrayBuffer());
|
|
147
|
-
let binary = '';
|
|
148
|
-
for (let i = 0; i < bytes.length; i += 1) binary += String.fromCharCode(bytes[i]);
|
|
149
|
-
return btoa(binary);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/* Example -- Odoo search_read:
|
|
153
|
-
*
|
|
154
|
-
* const r = await callIntegration({
|
|
155
|
-
* integration: 'odoo',
|
|
156
|
-
* endpoint: '/jsonrpc',
|
|
157
|
-
* body: {
|
|
158
|
-
* model: 'product.template',
|
|
159
|
-
* method: 'search_read',
|
|
160
|
-
* args: [[['type', '=', 'consu']]],
|
|
161
|
-
* kwargs: { fields: ['name', 'list_price'], limit: 50 },
|
|
162
|
-
* },
|
|
163
|
-
* });
|
|
164
|
-
* const products = r.data?.result ?? [];
|
|
165
|
-
*
|
|
166
|
-
* Example -- multipart upload (Zoho WorkDrive):
|
|
167
|
-
*
|
|
168
|
-
* const r = await uploadFile({
|
|
169
|
-
* integration: 'zoho_workdrive',
|
|
170
|
-
* endpoint: '/api/v1/upload',
|
|
171
|
-
* body: {
|
|
172
|
-
* fields: { parent_id: folderId, 'override-name-exist': 'true' },
|
|
173
|
-
* files: [{
|
|
174
|
-
* name: 'content',
|
|
175
|
-
* filename: file.name,
|
|
176
|
-
* contentType: file.type,
|
|
177
|
-
* dataBase64: await fileToBase64(file),
|
|
178
|
-
* }],
|
|
179
|
-
* },
|
|
180
|
-
* });
|
|
181
|
-
* if (!r.success) throw new Error(String(r.data));
|
|
182
|
-
*/
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/components.json
RENAMED
|
File without changes
|
|
File without changes
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/drizzle.config.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/db/index.ts
RENAMED
|
File without changes
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/db/schema.ts
RENAMED
|
File without changes
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/global.d.ts
RENAMED
|
File without changes
|
|
File without changes
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/lib/cache.ts
RENAMED
|
File without changes
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/lib/kv.ts
RENAMED
|
File without changes
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/lib/storage.ts
RENAMED
|
File without changes
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/lib/utils.ts
RENAMED
|
File without changes
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/routeTree.gen.ts
RENAMED
|
File without changes
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/router.tsx
RENAMED
|
File without changes
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/routes/README.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/routes/index.tsx
RENAMED
|
File without changes
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/server.ts
RENAMED
|
File without changes
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/src/styles.css
RENAMED
|
File without changes
|
/package/dist/templates/{tanstack-start-2026-09-11 → tanstack-start-2026-09-29}/tsconfig.json
RENAMED
|
File without changes
|