@helloleo/plugins 0.2.29 → 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 +12533 -0
- package/dist/index.js +2135 -2033
- 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 +2 -2
- 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
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@helloleo/plugins",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.31-beta.0",
|
|
4
4
|
"description": "OpenCode plugins for HelloLeo",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"access": "public"
|
|
16
16
|
},
|
|
17
17
|
"scripts": {
|
|
18
|
-
"build": "rm -rf dist && bun build src/index.ts --outdir dist --target bun --format esm && bun run scripts/copy-templates.ts",
|
|
18
|
+
"build": "rm -rf dist && bun build src/index.ts src/cli.ts --outdir dist --target bun --format esm && bun run scripts/copy-templates.ts",
|
|
19
19
|
"format": "bunx --bun @biomejs/biome format --write src",
|
|
20
20
|
"lint": "bunx --bun @biomejs/biome lint --write src",
|
|
21
21
|
"check": "bunx --bun @biomejs/biome check --write src",
|
|
@@ -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
|