@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 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: projectUuidFromDir(dir)
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
- // The app usually renders inside the HelloLeo preview iframe. NetSuite's
25574
- // OAuth page sets X-Frame-Options: DENY, so navigating the iframe to the
25575
- // Worker (which 302s to NetSuite) ends up loading a page the browser
25576
- // refuses to render -- looks like "nothing happens" to the user. Navigate
25577
- // the top frame instead so the OAuth flow runs at the top level and the
25578
- // callback redirect lands on the same top frame, which then reads the
25579
- // #nssid= fragment. Use \`location.href =\` (NOT \`location.assign()\`):
25580
- // reading the \`.assign\` method off a cross-origin \`window.top.location\`
25581
- // throws a SecurityError, which would drop us into the same-frame fallback
25582
- // and load NetSuite inside the iframe (X-Frame-Options: DENY -> "refused to
25583
- // connect"). Assigning \`.href\` is a navigation, allowed cross-origin.
25584
- try {
25585
- if (window.top && window.top !== window) {
25586
- window.top.location.href = url.toString();
25587
- return;
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
- } catch {
25590
- /* top window not accessible \u2014 fall through to same-frame navigation */
25607
+ window.location.href = url.toString();
25608
+ return;
25591
25609
  }
25592
- window.location.href = url.toString();
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
- export const INTEGRATION_PROXY_URL =
43
- 'https://api.app.helloleo.dev/api/integration-proxy/{{LEO_PROJECT_UUID}}';
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helloleo/plugins",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "OpenCode plugins for HelloLeo",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -39,8 +39,10 @@
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
+
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;
@@ -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: projectUuidFromDir(dir),
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
- // The app usually renders inside the HelloLeo preview iframe. NetSuite's
80
- // OAuth page sets X-Frame-Options: DENY, so navigating the iframe to the
81
- // Worker (which 302s to NetSuite) ends up loading a page the browser
82
- // refuses to render -- looks like "nothing happens" to the user. Navigate
83
- // the top frame instead so the OAuth flow runs at the top level and the
84
- // callback redirect lands on the same top frame, which then reads the
85
- // #nssid= fragment. Use \`location.href =\` (NOT \`location.assign()\`):
86
- // reading the \`.assign\` method off a cross-origin \`window.top.location\`
87
- // throws a SecurityError, which would drop us into the same-frame fallback
88
- // and load NetSuite inside the iframe (X-Frame-Options: DENY -> "refused to
89
- // connect"). Assigning \`.href\` is a navigation, allowed cross-origin.
90
- try {
91
- if (window.top && window.top !== window) {
92
- window.top.location.href = url.toString();
93
- return;
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
- } catch {
96
- /* top window not accessible — fall through to same-frame navigation */
107
+ window.location.href = url.toString();
108
+ return;
97
109
  }
98
- window.location.href = url.toString();
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
  "",