@helloleo/plugins 0.2.8 → 0.2.9

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
@@ -25568,28 +25568,51 @@ export function signInWithNetSuite(returnTo: string = window.location.href) {
25568
25568
  'VITE_WORKER_URL is not set. HelloLeo Cloud must be provisioned before sign-in works.',
25569
25569
  );
25570
25570
  }
25571
+ const workerOrigin = new URL(WORKER_URL).origin;
25571
25572
  const url = new URL(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/start\`);
25572
25573
  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;
25574
+ // popup=1 tells the Worker callback to postMessage the session id back to
25575
+ // THIS window (then close) instead of a #nssid= redirect.
25576
+ url.searchParams.set('popup', '1');
25577
+
25578
+ // Open OAuth in a POPUP -- not the iframe, not the top frame. NetSuite's OAuth
25579
+ // page is X-Frame-Options: DENY (can't load in an iframe), and navigating the
25580
+ // TOP frame replaces the HelloLeo preview and lands the session in a DIFFERENT
25581
+ // browsing context -- so back in the preview iframe (its own partitioned
25582
+ // sessionStorage) you still see the login even after signing in. A popup keeps
25583
+ // the app + preview intact and hands the token back to this exact context.
25584
+ const popup = window.open(
25585
+ url.toString(),
25586
+ 'netsuite-oauth',
25587
+ 'width=600,height=760,menubar=no,toolbar=no',
25588
+ );
25589
+
25590
+ if (!popup) {
25591
+ // Popup blocked -> fall back to top-frame navigation (works first-party / in
25592
+ // a standalone tab; the callback then uses the #nssid= redirect).
25593
+ try {
25594
+ if (window.top && window.top !== window) {
25595
+ window.top.location.href = url.toString();
25596
+ return;
25597
+ }
25598
+ } catch {
25599
+ /* top window not accessible */
25588
25600
  }
25589
- } catch {
25590
- /* top window not accessible \u2014 fall through to same-frame navigation */
25601
+ window.location.href = url.toString();
25602
+ return;
25591
25603
  }
25592
- window.location.href = url.toString();
25604
+
25605
+ const onMessage = (event: MessageEvent) => {
25606
+ if (event.origin !== workerOrigin) return;
25607
+ const data = event.data as { type?: string; nssid?: string } | null;
25608
+ if (!data || data.type !== 'netsuite-auth' || !data.nssid) return;
25609
+ try { sessionStorage.setItem(TOKEN_KEY, data.nssid); } catch {}
25610
+ window.removeEventListener('message', onMessage);
25611
+ try { popup.close(); } catch {}
25612
+ // Re-render as signed-in -- the auth provider re-checks /api/me on load.
25613
+ window.location.reload();
25614
+ };
25615
+ window.addEventListener('message', onMessage);
25593
25616
  }
25594
25617
 
25595
25618
  export interface NetSuiteSession {
@@ -25868,6 +25891,7 @@ var setupNetsuite = tool({
25868
25891
  "- Read the session anywhere with useNetSuiteAuth() from src/lib/auth.tsx.",
25869
25892
  "- Never replace AuthLayout with a password gate. Real OAuth is the only supported login.",
25870
25893
  "- 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).",
25894
+ "- 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
25895
  "",
25872
25896
  "Next: in src/routes/__root.tsx, import AuthLayout and wrap <Outlet /> with it. Keep any existing providers.",
25873
25897
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helloleo/plugins",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "OpenCode plugins for HelloLeo",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -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
  "",