@helloleo/plugins 0.1.8 → 0.1.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
@@ -12910,35 +12910,58 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
12910
12910
  }
12911
12911
  function netsuiteTs() {
12912
12912
  return `/*
12913
- * NetSuite OAuth client helper.
12913
+ * NetSuite OAuth client helper (bearer-token-in-fragment).
12914
12914
  *
12915
- * NetSuite uses a TWO-Integration-Record setup:
12916
- * 1. MCP record (scope: mcp) -- used by HelloLeo at build time
12917
- * to introspect the schema. Configured in Project Settings, not here.
12918
- * 2. Runtime record (scope: rest_webservices) -- used by THIS app to sign
12919
- * end-users in. Credentials are stored as HelloLeo Cloud secrets on the
12920
- * project's Worker (env.NETSUITE_ACCOUNT_ID, env.NETSUITE_CLIENT_ID,
12921
- * env.NETSUITE_CLIENT_SECRET). They never reach the browser.
12915
+ * Two NetSuite Integration Records back this:
12916
+ * 1. MCP (scope: mcp) -- used by HelloLeo at build time.
12917
+ * 2. Runtime (scope: rest_webservices) -- used here at sign-in time.
12918
+ * Runtime credentials live as HelloLeo Cloud secrets on the project's Worker
12919
+ * (env.NETSUITE_ACCOUNT_ID / _CLIENT_ID / _CLIENT_SECRET). They never reach
12920
+ * the browser. Cookies are NOT used -- frontend \u2194 Worker are cross-site
12921
+ * (e.g. *.helloleo.dev <-> *.workers.dev) and Chrome partitions / drops
12922
+ * third-party cookies in the HelloLeo preview iframe.
12922
12923
  *
12923
12924
  * Flow:
12924
12925
  * 1. User clicks "Sign in with NetSuite".
12925
- * 2. Browser hits the project's Worker at /auth/netsuite/start, which
12926
- * redirects to NetSuite's OAuth authorise endpoint with PKCE.
12927
- * 3. NetSuite redirects back to /auth/netsuite/callback on the Worker.
12928
- * The Worker exchanges the code for an access + refresh token, stores
12929
- * them in D1 keyed by a session id, and sets a session cookie.
12930
- * 4. The frontend calls /api/me on the Worker to read the signed-in user.
12931
- * 5. All NetSuite REST calls flow through the Worker so tokens stay
12932
- * server-side.
12926
+ * 2. Browser navigates to the Worker /auth/netsuite/start.
12927
+ * 3. Worker redirects to NetSuite's OAuth UI with PKCE.
12928
+ * 4. NetSuite redirects back to the Worker /auth/netsuite/callback.
12929
+ * 5. Worker exchanges the code, mints a session id, then redirects to
12930
+ * <return_to>#nssid=<sid>. URL fragments stay in the browser only.
12931
+ * 6. This module reads the fragment on load, stashes the sid in
12932
+ * sessionStorage, and sends it as Authorization: Bearer on every
12933
+ * Worker call.
12933
12934
  *
12934
- * IMPORTANT: NEVER use a hardcoded password or sessionStorage flag as the
12935
- * login gate -- the NetSuite OAuth flow above is the only acceptable
12936
- * authentication for a NetSuite project.
12935
+ * NEVER use a hardcoded password gate. NEVER use sessionStorage as the
12936
+ * authoritative auth state -- the only acceptable check is /api/me on the
12937
+ * Worker. sessionStorage here is just a cache of the bearer token.
12937
12938
  */
12938
12939
 
12939
12940
  export const WORKER_URL = import.meta.env.VITE_WORKER_URL ?? '';
12941
+ const TOKEN_KEY = 'nssid';
12940
12942
 
12941
- export function signInWithNetSuite(returnTo: string = window.location.pathname) {
12943
+ // Capture-and-strip the #nssid=... fragment as early as possible so URLs
12944
+ // shown to the user stay clean. Runs once at module load.
12945
+ (function bootstrapTokenFromHash() {
12946
+ if (typeof window === 'undefined') return;
12947
+ const hash = window.location.hash.replace(/^#/, '');
12948
+ if (!hash) return;
12949
+ const params = new URLSearchParams(hash);
12950
+ const sid = params.get(TOKEN_KEY);
12951
+ if (sid) {
12952
+ try { sessionStorage.setItem(TOKEN_KEY, sid); } catch {}
12953
+ params.delete(TOKEN_KEY);
12954
+ const remaining = params.toString();
12955
+ const newHash = remaining ? '#' + remaining : '';
12956
+ window.history.replaceState(null, '', window.location.pathname + window.location.search + newHash);
12957
+ }
12958
+ })();
12959
+
12960
+ export function getSessionToken(): string | null {
12961
+ try { return sessionStorage.getItem(TOKEN_KEY); } catch { return null; }
12962
+ }
12963
+
12964
+ export function signInWithNetSuite(returnTo: string = window.location.href) {
12942
12965
  if (!WORKER_URL) {
12943
12966
  throw new Error(
12944
12967
  'VITE_WORKER_URL is not set. HelloLeo Cloud must be provisioned before sign-in works.',
@@ -12958,8 +12981,10 @@ export interface NetSuiteSession {
12958
12981
 
12959
12982
  export async function getNetSuiteSession(): Promise<NetSuiteSession> {
12960
12983
  if (!WORKER_URL) return { authenticated: false };
12984
+ const sid = getSessionToken();
12985
+ if (!sid) return { authenticated: false };
12961
12986
  const res = await fetch(\`\${WORKER_URL.replace(/\\/$/, '')}/api/me\`, {
12962
- credentials: 'include',
12987
+ headers: { Authorization: 'Bearer ' + sid },
12963
12988
  });
12964
12989
  if (!res.ok) return { authenticated: false };
12965
12990
  const data = await res.json();
@@ -12968,12 +12993,34 @@ export async function getNetSuiteSession(): Promise<NetSuiteSession> {
12968
12993
 
12969
12994
  export async function signOutNetSuite(): Promise<void> {
12970
12995
  if (!WORKER_URL) return;
12971
- await fetch(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/logout\`, {
12972
- method: 'POST',
12973
- credentials: 'include',
12974
- });
12996
+ const sid = getSessionToken();
12997
+ if (sid) {
12998
+ try {
12999
+ await fetch(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/logout\`, {
13000
+ method: 'POST',
13001
+ headers: { Authorization: 'Bearer ' + sid },
13002
+ });
13003
+ } catch {}
13004
+ }
13005
+ try { sessionStorage.removeItem(TOKEN_KEY); } catch {}
12975
13006
  window.location.reload();
12976
13007
  }
13008
+
13009
+ /**
13010
+ * Helper for ALL other Worker calls (your dashboard endpoints, etc.).
13011
+ * Always go through this instead of bare fetch -- it injects the bearer
13012
+ * token and routes through the Worker so NetSuite tokens stay server-side.
13013
+ */
13014
+ export async function workerFetch(pathOrUrl: string, init: RequestInit = {}): Promise<Response> {
13015
+ if (!WORKER_URL) throw new Error('VITE_WORKER_URL is not set');
13016
+ const url = pathOrUrl.startsWith('http')
13017
+ ? pathOrUrl
13018
+ : \`\${WORKER_URL.replace(/\\/$/, '')}\${pathOrUrl.startsWith('/') ? '' : '/'}\${pathOrUrl}\`;
13019
+ const sid = getSessionToken();
13020
+ const headers = new Headers(init.headers ?? {});
13021
+ if (sid) headers.set('Authorization', 'Bearer ' + sid);
13022
+ return fetch(url, { ...init, headers });
13023
+ }
12977
13024
  `;
12978
13025
  }
12979
13026
  function signInButtonTsx() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helloleo/plugins",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "OpenCode plugins for HelloLeo",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -389,35 +389,58 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
389
389
 
390
390
  function netsuiteTs() {
391
391
  return `/*
392
- * NetSuite OAuth client helper.
392
+ * NetSuite OAuth client helper (bearer-token-in-fragment).
393
393
  *
394
- * NetSuite uses a TWO-Integration-Record setup:
395
- * 1. MCP record (scope: mcp) -- used by HelloLeo at build time
396
- * to introspect the schema. Configured in Project Settings, not here.
397
- * 2. Runtime record (scope: rest_webservices) -- used by THIS app to sign
398
- * end-users in. Credentials are stored as HelloLeo Cloud secrets on the
399
- * project's Worker (env.NETSUITE_ACCOUNT_ID, env.NETSUITE_CLIENT_ID,
400
- * env.NETSUITE_CLIENT_SECRET). They never reach the browser.
394
+ * Two NetSuite Integration Records back this:
395
+ * 1. MCP (scope: mcp) -- used by HelloLeo at build time.
396
+ * 2. Runtime (scope: rest_webservices) -- used here at sign-in time.
397
+ * Runtime credentials live as HelloLeo Cloud secrets on the project's Worker
398
+ * (env.NETSUITE_ACCOUNT_ID / _CLIENT_ID / _CLIENT_SECRET). They never reach
399
+ * the browser. Cookies are NOT used -- frontend ↔ Worker are cross-site
400
+ * (e.g. *.helloleo.dev <-> *.workers.dev) and Chrome partitions / drops
401
+ * third-party cookies in the HelloLeo preview iframe.
401
402
  *
402
403
  * Flow:
403
404
  * 1. User clicks "Sign in with NetSuite".
404
- * 2. Browser hits the project's Worker at /auth/netsuite/start, which
405
- * redirects to NetSuite's OAuth authorise endpoint with PKCE.
406
- * 3. NetSuite redirects back to /auth/netsuite/callback on the Worker.
407
- * The Worker exchanges the code for an access + refresh token, stores
408
- * them in D1 keyed by a session id, and sets a session cookie.
409
- * 4. The frontend calls /api/me on the Worker to read the signed-in user.
410
- * 5. All NetSuite REST calls flow through the Worker so tokens stay
411
- * server-side.
405
+ * 2. Browser navigates to the Worker /auth/netsuite/start.
406
+ * 3. Worker redirects to NetSuite's OAuth UI with PKCE.
407
+ * 4. NetSuite redirects back to the Worker /auth/netsuite/callback.
408
+ * 5. Worker exchanges the code, mints a session id, then redirects to
409
+ * <return_to>#nssid=<sid>. URL fragments stay in the browser only.
410
+ * 6. This module reads the fragment on load, stashes the sid in
411
+ * sessionStorage, and sends it as Authorization: Bearer on every
412
+ * Worker call.
412
413
  *
413
- * IMPORTANT: NEVER use a hardcoded password or sessionStorage flag as the
414
- * login gate -- the NetSuite OAuth flow above is the only acceptable
415
- * authentication for a NetSuite project.
414
+ * NEVER use a hardcoded password gate. NEVER use sessionStorage as the
415
+ * authoritative auth state -- the only acceptable check is /api/me on the
416
+ * Worker. sessionStorage here is just a cache of the bearer token.
416
417
  */
417
418
 
418
419
  export const WORKER_URL = import.meta.env.VITE_WORKER_URL ?? '';
420
+ const TOKEN_KEY = 'nssid';
421
+
422
+ // Capture-and-strip the #nssid=... fragment as early as possible so URLs
423
+ // shown to the user stay clean. Runs once at module load.
424
+ (function bootstrapTokenFromHash() {
425
+ if (typeof window === 'undefined') return;
426
+ const hash = window.location.hash.replace(/^#/, '');
427
+ if (!hash) return;
428
+ const params = new URLSearchParams(hash);
429
+ const sid = params.get(TOKEN_KEY);
430
+ if (sid) {
431
+ try { sessionStorage.setItem(TOKEN_KEY, sid); } catch {}
432
+ params.delete(TOKEN_KEY);
433
+ const remaining = params.toString();
434
+ const newHash = remaining ? '#' + remaining : '';
435
+ window.history.replaceState(null, '', window.location.pathname + window.location.search + newHash);
436
+ }
437
+ })();
438
+
439
+ export function getSessionToken(): string | null {
440
+ try { return sessionStorage.getItem(TOKEN_KEY); } catch { return null; }
441
+ }
419
442
 
420
- export function signInWithNetSuite(returnTo: string = window.location.pathname) {
443
+ export function signInWithNetSuite(returnTo: string = window.location.href) {
421
444
  if (!WORKER_URL) {
422
445
  throw new Error(
423
446
  'VITE_WORKER_URL is not set. HelloLeo Cloud must be provisioned before sign-in works.',
@@ -437,8 +460,10 @@ export interface NetSuiteSession {
437
460
 
438
461
  export async function getNetSuiteSession(): Promise<NetSuiteSession> {
439
462
  if (!WORKER_URL) return { authenticated: false };
463
+ const sid = getSessionToken();
464
+ if (!sid) return { authenticated: false };
440
465
  const res = await fetch(\`\${WORKER_URL.replace(/\\/$/, '')}/api/me\`, {
441
- credentials: 'include',
466
+ headers: { Authorization: 'Bearer ' + sid },
442
467
  });
443
468
  if (!res.ok) return { authenticated: false };
444
469
  const data = await res.json();
@@ -447,12 +472,34 @@ export async function getNetSuiteSession(): Promise<NetSuiteSession> {
447
472
 
448
473
  export async function signOutNetSuite(): Promise<void> {
449
474
  if (!WORKER_URL) return;
450
- await fetch(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/logout\`, {
451
- method: 'POST',
452
- credentials: 'include',
453
- });
475
+ const sid = getSessionToken();
476
+ if (sid) {
477
+ try {
478
+ await fetch(\`\${WORKER_URL.replace(/\\/$/, '')}/auth/netsuite/logout\`, {
479
+ method: 'POST',
480
+ headers: { Authorization: 'Bearer ' + sid },
481
+ });
482
+ } catch {}
483
+ }
484
+ try { sessionStorage.removeItem(TOKEN_KEY); } catch {}
454
485
  window.location.reload();
455
486
  }
487
+
488
+ /**
489
+ * Helper for ALL other Worker calls (your dashboard endpoints, etc.).
490
+ * Always go through this instead of bare fetch -- it injects the bearer
491
+ * token and routes through the Worker so NetSuite tokens stay server-side.
492
+ */
493
+ export async function workerFetch(pathOrUrl: string, init: RequestInit = {}): Promise<Response> {
494
+ if (!WORKER_URL) throw new Error('VITE_WORKER_URL is not set');
495
+ const url = pathOrUrl.startsWith('http')
496
+ ? pathOrUrl
497
+ : \`\${WORKER_URL.replace(/\\/$/, '')}\${pathOrUrl.startsWith('/') ? '' : '/'}\${pathOrUrl}\`;
498
+ const sid = getSessionToken();
499
+ const headers = new Headers(init.headers ?? {});
500
+ if (sid) headers.set('Authorization', 'Bearer ' + sid);
501
+ return fetch(url, { ...init, headers });
502
+ }
456
503
  `;
457
504
  }
458
505