@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 +72 -25
- package/package.json +1 -1
- package/src/tools/scaffold.ts +72 -25
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
|
|
12916
|
-
* 1. MCP
|
|
12917
|
-
*
|
|
12918
|
-
*
|
|
12919
|
-
*
|
|
12920
|
-
*
|
|
12921
|
-
*
|
|
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
|
|
12926
|
-
*
|
|
12927
|
-
*
|
|
12928
|
-
*
|
|
12929
|
-
*
|
|
12930
|
-
*
|
|
12931
|
-
*
|
|
12932
|
-
*
|
|
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
|
-
*
|
|
12935
|
-
*
|
|
12936
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12972
|
-
|
|
12973
|
-
|
|
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
package/src/tools/scaffold.ts
CHANGED
|
@@ -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
|
|
395
|
-
* 1. MCP
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
399
|
-
*
|
|
400
|
-
*
|
|
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
|
|
405
|
-
*
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
*
|
|
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
|
-
*
|
|
414
|
-
*
|
|
415
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
|