@helloleo/plugins 0.2.8 → 0.2.10
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/index.js +49 -19
- package/dist/templates/tanstack-router-2026-06-15/src/lib/proxy.ts +4 -2
- package/dist/templates/tanstack-start-2026-06-18/src/lib/proxy.ts +1 -2
- package/package.json +1 -1
- package/src/templates/tanstack-router-2026-06-15/src/lib/proxy.ts +4 -2
- package/src/templates/tanstack-start-2026-06-18/src/lib/proxy.ts +1 -2
- package/src/tools/scaffold.ts +18 -1
- package/src/tools/setup-netsuite.ts +42 -18
package/dist/index.js
CHANGED
|
@@ -24915,6 +24915,10 @@ function resolveTemplateId() {
|
|
|
24915
24915
|
function projectUuidFromDir(dir) {
|
|
24916
24916
|
return path3.basename(dir);
|
|
24917
24917
|
}
|
|
24918
|
+
function integrationProxyUrl(projectUuid) {
|
|
24919
|
+
const base = (process.env.PLUGINS_INTEGRATION_PROXY_URL || "https://api.app.helloleo.dev").replace(/\/+$/, "");
|
|
24920
|
+
return `${base}/api/integration-proxy/${projectUuid}`;
|
|
24921
|
+
}
|
|
24918
24922
|
var scaffoldOptionsSchema = exports_external2.object({
|
|
24919
24923
|
name: exports_external2.string().describe("Human-readable name for SEO, e.g. 'Shipping Platform'"),
|
|
24920
24924
|
description: exports_external2.string().describe("One sentence describing the app. Used as the meta description.")
|
|
@@ -24924,11 +24928,13 @@ function slugify(name) {
|
|
|
24924
24928
|
}
|
|
24925
24929
|
async function writeBaseScaffold({ name, description }, dir) {
|
|
24926
24930
|
const templateId = resolveTemplateId();
|
|
24931
|
+
const projectUuid = projectUuidFromDir(dir);
|
|
24927
24932
|
const created = await renderTemplate(templateId, {
|
|
24928
24933
|
LEO_NAME: name,
|
|
24929
24934
|
LEO_SLUG: slugify(name),
|
|
24930
24935
|
LEO_DESCRIPTION: description,
|
|
24931
|
-
LEO_PROJECT_UUID:
|
|
24936
|
+
LEO_PROJECT_UUID: projectUuid,
|
|
24937
|
+
LEO_INTEGRATION_PROXY_URL: integrationProxyUrl(projectUuid)
|
|
24932
24938
|
}, dir);
|
|
24933
24939
|
await mkdir(path3.join(dir, ".leo"), { recursive: true });
|
|
24934
24940
|
const leoPath = path3.join(".leo", "project.json");
|
|
@@ -25568,28 +25574,51 @@ export function signInWithNetSuite(returnTo: string = window.location.href) {
|
|
|
25568
25574
|
'VITE_WORKER_URL is not set. HelloLeo Cloud must be provisioned before sign-in works.',
|
|
25569
25575
|
);
|
|
25570
25576
|
}
|
|
25577
|
+
const workerOrigin = new URL(WORKER_URL).origin;
|
|
25571
25578
|
const url = new URL(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/start\`);
|
|
25572
25579
|
url.searchParams.set('return_to', returnTo);
|
|
25573
|
-
//
|
|
25574
|
-
//
|
|
25575
|
-
|
|
25576
|
-
|
|
25577
|
-
//
|
|
25578
|
-
//
|
|
25579
|
-
//
|
|
25580
|
-
//
|
|
25581
|
-
//
|
|
25582
|
-
//
|
|
25583
|
-
|
|
25584
|
-
|
|
25585
|
-
|
|
25586
|
-
|
|
25587
|
-
|
|
25580
|
+
// popup=1 tells the Worker callback to postMessage the session id back to
|
|
25581
|
+
// THIS window (then close) instead of a #nssid= redirect.
|
|
25582
|
+
url.searchParams.set('popup', '1');
|
|
25583
|
+
|
|
25584
|
+
// Open OAuth in a POPUP -- not the iframe, not the top frame. NetSuite's OAuth
|
|
25585
|
+
// page is X-Frame-Options: DENY (can't load in an iframe), and navigating the
|
|
25586
|
+
// TOP frame replaces the HelloLeo preview and lands the session in a DIFFERENT
|
|
25587
|
+
// browsing context -- so back in the preview iframe (its own partitioned
|
|
25588
|
+
// sessionStorage) you still see the login even after signing in. A popup keeps
|
|
25589
|
+
// the app + preview intact and hands the token back to this exact context.
|
|
25590
|
+
const popup = window.open(
|
|
25591
|
+
url.toString(),
|
|
25592
|
+
'netsuite-oauth',
|
|
25593
|
+
'width=600,height=760,menubar=no,toolbar=no',
|
|
25594
|
+
);
|
|
25595
|
+
|
|
25596
|
+
if (!popup) {
|
|
25597
|
+
// Popup blocked -> fall back to top-frame navigation (works first-party / in
|
|
25598
|
+
// a standalone tab; the callback then uses the #nssid= redirect).
|
|
25599
|
+
try {
|
|
25600
|
+
if (window.top && window.top !== window) {
|
|
25601
|
+
window.top.location.href = url.toString();
|
|
25602
|
+
return;
|
|
25603
|
+
}
|
|
25604
|
+
} catch {
|
|
25605
|
+
/* top window not accessible */
|
|
25588
25606
|
}
|
|
25589
|
-
|
|
25590
|
-
|
|
25607
|
+
window.location.href = url.toString();
|
|
25608
|
+
return;
|
|
25591
25609
|
}
|
|
25592
|
-
|
|
25610
|
+
|
|
25611
|
+
const onMessage = (event: MessageEvent) => {
|
|
25612
|
+
if (event.origin !== workerOrigin) return;
|
|
25613
|
+
const data = event.data as { type?: string; nssid?: string } | null;
|
|
25614
|
+
if (!data || data.type !== 'netsuite-auth' || !data.nssid) return;
|
|
25615
|
+
try { sessionStorage.setItem(TOKEN_KEY, data.nssid); } catch {}
|
|
25616
|
+
window.removeEventListener('message', onMessage);
|
|
25617
|
+
try { popup.close(); } catch {}
|
|
25618
|
+
// Re-render as signed-in -- the auth provider re-checks /api/me on load.
|
|
25619
|
+
window.location.reload();
|
|
25620
|
+
};
|
|
25621
|
+
window.addEventListener('message', onMessage);
|
|
25593
25622
|
}
|
|
25594
25623
|
|
|
25595
25624
|
export interface NetSuiteSession {
|
|
@@ -25868,6 +25897,7 @@ var setupNetsuite = tool({
|
|
|
25868
25897
|
"- Read the session anywhere with useNetSuiteAuth() from src/lib/auth.tsx.",
|
|
25869
25898
|
"- Never replace AuthLayout with a password gate. Real OAuth is the only supported login.",
|
|
25870
25899
|
"- The auth worker (deployed via the cloudflare-backend MCP) serves /auth/netsuite/start, /auth/netsuite/callback, /api/me and /auth/netsuite/logout. It reads env.NETSUITE_ACCOUNT_ID, _CLIENT_ID and _CLIENT_SECRET. VITE_WORKER_URL must be set (HelloLeo Cloud seeds it).",
|
|
25900
|
+
"- POPUP FLOW (required so login works inside the HelloLeo preview iframe): the client opens /auth/netsuite/start in a popup with ?popup=1. The worker MUST carry that flag through OAuth (persist it on the flow row in /start, read it back in /callback via state). In /auth/netsuite/callback, when the flow was popup, DO NOT redirect to return_to#nssid \u2014 instead return an HTML page whose script does: window.opener.postMessage({ type: 'netsuite-auth', nssid: SID }, '*'); window.close(); (fall back to the return_to#nssid redirect only when there is no opener / not a popup). Without this, the preview iframe (partitioned sessionStorage) keeps showing the login after sign-in.",
|
|
25871
25901
|
"",
|
|
25872
25902
|
"Next: in src/routes/__root.tsx, import AuthLayout and wrap <Outlet /> with it. Keep any existing providers.",
|
|
25873
25903
|
"",
|
|
@@ -39,8 +39,10 @@
|
|
|
39
39
|
* from a Worker. Never embed secrets in frontend code.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
|
|
43
|
+
export const INTEGRATION_PROXY_URL = '{{LEO_INTEGRATION_PROXY_URL}}';
|
|
44
|
+
|
|
45
|
+
export const BACKEND_URL = (import.meta.env.VITE_WORKER_URL as string | undefined) ?? '';
|
|
44
46
|
|
|
45
47
|
export interface ProxyRequest {
|
|
46
48
|
integration: string;
|
|
@@ -39,8 +39,7 @@
|
|
|
39
39
|
* from a Worker. Never embed secrets in frontend code.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
export const INTEGRATION_PROXY_URL =
|
|
43
|
-
'https://api.app.helloleo.dev/api/integration-proxy/{{LEO_PROJECT_UUID}}';
|
|
42
|
+
export const INTEGRATION_PROXY_URL = '{{LEO_INTEGRATION_PROXY_URL}}';
|
|
44
43
|
|
|
45
44
|
export interface ProxyRequest {
|
|
46
45
|
integration: string;
|
package/package.json
CHANGED
|
@@ -39,8 +39,10 @@
|
|
|
39
39
|
* from a Worker. Never embed secrets in frontend code.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
|
|
43
|
+
export const INTEGRATION_PROXY_URL = '{{LEO_INTEGRATION_PROXY_URL}}';
|
|
44
|
+
|
|
45
|
+
export const BACKEND_URL = (import.meta.env.VITE_WORKER_URL as string | undefined) ?? '';
|
|
44
46
|
|
|
45
47
|
export interface ProxyRequest {
|
|
46
48
|
integration: string;
|
|
@@ -39,8 +39,7 @@
|
|
|
39
39
|
* from a Worker. Never embed secrets in frontend code.
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
|
-
export const INTEGRATION_PROXY_URL =
|
|
43
|
-
'https://api.app.helloleo.dev/api/integration-proxy/{{LEO_PROJECT_UUID}}';
|
|
42
|
+
export const INTEGRATION_PROXY_URL = '{{LEO_INTEGRATION_PROXY_URL}}';
|
|
44
43
|
|
|
45
44
|
export interface ProxyRequest {
|
|
46
45
|
integration: string;
|
package/src/tools/scaffold.ts
CHANGED
|
@@ -51,6 +51,21 @@ export function projectUuidFromDir(dir: string): string {
|
|
|
51
51
|
return path.basename(dir);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Integration-proxy base URL, provided by the opencode container via
|
|
56
|
+
* PLUGINS_INTEGRATION_PROXY_URL (like SCAFFOLD_VERSION) so each deployment
|
|
57
|
+
* (dev | preprod | prod) bakes its own proxy host into proxy.ts at scaffold
|
|
58
|
+
* time -- previews and published apps use the same value. Unset falls back
|
|
59
|
+
* to prod so a missing env var can never break production scaffolds.
|
|
60
|
+
*/
|
|
61
|
+
export function integrationProxyUrl(projectUuid: string): string {
|
|
62
|
+
const base = (
|
|
63
|
+
process.env.PLUGINS_INTEGRATION_PROXY_URL || "https://api.app.helloleo.dev"
|
|
64
|
+
).replace(/\/+$/, "");
|
|
65
|
+
|
|
66
|
+
return `${base}/api/integration-proxy/${projectUuid}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
const scaffoldOptionsSchema = z.object({
|
|
55
70
|
name: z
|
|
56
71
|
.string()
|
|
@@ -92,13 +107,15 @@ export async function writeBaseScaffold(
|
|
|
92
107
|
dir: string,
|
|
93
108
|
): Promise<string[]> {
|
|
94
109
|
const templateId = resolveTemplateId();
|
|
110
|
+
const projectUuid = projectUuidFromDir(dir);
|
|
95
111
|
const created = await renderTemplate(
|
|
96
112
|
templateId,
|
|
97
113
|
{
|
|
98
114
|
LEO_NAME: name,
|
|
99
115
|
LEO_SLUG: slugify(name),
|
|
100
116
|
LEO_DESCRIPTION: description,
|
|
101
|
-
LEO_PROJECT_UUID:
|
|
117
|
+
LEO_PROJECT_UUID: projectUuid,
|
|
118
|
+
LEO_INTEGRATION_PROXY_URL: integrationProxyUrl(projectUuid),
|
|
102
119
|
},
|
|
103
120
|
dir,
|
|
104
121
|
);
|
|
@@ -74,28 +74,51 @@ export function signInWithNetSuite(returnTo: string = window.location.href) {
|
|
|
74
74
|
'VITE_WORKER_URL is not set. HelloLeo Cloud must be provisioned before sign-in works.',
|
|
75
75
|
);
|
|
76
76
|
}
|
|
77
|
+
const workerOrigin = new URL(WORKER_URL).origin;
|
|
77
78
|
const url = new URL(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/start\`);
|
|
78
79
|
url.searchParams.set('return_to', returnTo);
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
80
|
+
// popup=1 tells the Worker callback to postMessage the session id back to
|
|
81
|
+
// THIS window (then close) instead of a #nssid= redirect.
|
|
82
|
+
url.searchParams.set('popup', '1');
|
|
83
|
+
|
|
84
|
+
// Open OAuth in a POPUP -- not the iframe, not the top frame. NetSuite's OAuth
|
|
85
|
+
// page is X-Frame-Options: DENY (can't load in an iframe), and navigating the
|
|
86
|
+
// TOP frame replaces the HelloLeo preview and lands the session in a DIFFERENT
|
|
87
|
+
// browsing context -- so back in the preview iframe (its own partitioned
|
|
88
|
+
// sessionStorage) you still see the login even after signing in. A popup keeps
|
|
89
|
+
// the app + preview intact and hands the token back to this exact context.
|
|
90
|
+
const popup = window.open(
|
|
91
|
+
url.toString(),
|
|
92
|
+
'netsuite-oauth',
|
|
93
|
+
'width=600,height=760,menubar=no,toolbar=no',
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
if (!popup) {
|
|
97
|
+
// Popup blocked -> fall back to top-frame navigation (works first-party / in
|
|
98
|
+
// a standalone tab; the callback then uses the #nssid= redirect).
|
|
99
|
+
try {
|
|
100
|
+
if (window.top && window.top !== window) {
|
|
101
|
+
window.top.location.href = url.toString();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
/* top window not accessible */
|
|
94
106
|
}
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
window.location.href = url.toString();
|
|
108
|
+
return;
|
|
97
109
|
}
|
|
98
|
-
|
|
110
|
+
|
|
111
|
+
const onMessage = (event: MessageEvent) => {
|
|
112
|
+
if (event.origin !== workerOrigin) return;
|
|
113
|
+
const data = event.data as { type?: string; nssid?: string } | null;
|
|
114
|
+
if (!data || data.type !== 'netsuite-auth' || !data.nssid) return;
|
|
115
|
+
try { sessionStorage.setItem(TOKEN_KEY, data.nssid); } catch {}
|
|
116
|
+
window.removeEventListener('message', onMessage);
|
|
117
|
+
try { popup.close(); } catch {}
|
|
118
|
+
// Re-render as signed-in -- the auth provider re-checks /api/me on load.
|
|
119
|
+
window.location.reload();
|
|
120
|
+
};
|
|
121
|
+
window.addEventListener('message', onMessage);
|
|
99
122
|
}
|
|
100
123
|
|
|
101
124
|
export interface NetSuiteSession {
|
|
@@ -388,6 +411,7 @@ export const setupNetsuite = tool({
|
|
|
388
411
|
"- Read the session anywhere with useNetSuiteAuth() from src/lib/auth.tsx.",
|
|
389
412
|
"- Never replace AuthLayout with a password gate. Real OAuth is the only supported login.",
|
|
390
413
|
"- The auth worker (deployed via the cloudflare-backend MCP) serves /auth/netsuite/start, /auth/netsuite/callback, /api/me and /auth/netsuite/logout. It reads env.NETSUITE_ACCOUNT_ID, _CLIENT_ID and _CLIENT_SECRET. VITE_WORKER_URL must be set (HelloLeo Cloud seeds it).",
|
|
414
|
+
"- POPUP FLOW (required so login works inside the HelloLeo preview iframe): the client opens /auth/netsuite/start in a popup with ?popup=1. The worker MUST carry that flag through OAuth (persist it on the flow row in /start, read it back in /callback via state). In /auth/netsuite/callback, when the flow was popup, DO NOT redirect to return_to#nssid — instead return an HTML page whose script does: window.opener.postMessage({ type: 'netsuite-auth', nssid: SID }, '*'); window.close(); (fall back to the return_to#nssid redirect only when there is no opener / not a popup). Without this, the preview iframe (partitioned sessionStorage) keeps showing the login after sign-in.",
|
|
391
415
|
"",
|
|
392
416
|
"Next: in src/routes/__root.tsx, import AuthLayout and wrap <Outlet /> with it. Keep any existing providers.",
|
|
393
417
|
"",
|