@helloleo/plugins 0.2.20 → 0.2.21

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
@@ -25613,7 +25613,9 @@ function netsuiteServerTs() {
25613
25613
  * Config is read from the Cloudflare env binding via @helloleo/runtime.
25614
25614
  * Set NETSUITE_ACCOUNT_ID / _CLIENT_ID / _CLIENT_SECRET (secret optional for a
25615
25615
  * PKCE public client) and optional PUBLIC_APP_URL as HelloLeo Cloud secrets
25616
- * (prod) or dev vars (preview). They never reach the browser.
25616
+ * (prod) or dev vars (preview). NETSUITE_BROKER_CALLBACK (written by the
25617
+ * HelloLeo wizard) switches the redirect URI to the shared OAuth broker so
25618
+ * one URI serves every app/environment. They never reach the browser.
25617
25619
  */
25618
25620
  import { eq } from 'drizzle-orm'
25619
25621
  import { env as runtimeEnv } from '@helloleo/runtime'
@@ -25624,6 +25626,7 @@ interface NsEnv {
25624
25626
  NETSUITE_ACCOUNT_ID?: string
25625
25627
  NETSUITE_CLIENT_ID?: string
25626
25628
  NETSUITE_CLIENT_SECRET?: string
25629
+ NETSUITE_BROKER_CALLBACK?: string
25627
25630
  PUBLIC_APP_URL?: string
25628
25631
  HELLOLEO_URL?: string
25629
25632
  }
@@ -25684,8 +25687,47 @@ export function appOrigin(request: Request) {
25684
25687
  url.host
25685
25688
  return \`\${proto}://\${hostHeader}\`
25686
25689
  }
25690
+ /**
25691
+ * HelloLeo's shared OAuth broker (integration-proxy Worker). When
25692
+ * NETSUITE_BROKER_CALLBACK is set (the platform writes it to the project env),
25693
+ * customers register that ONE stable URI on their Integration Record instead
25694
+ * of one URI per app/environment: the authorize redirect goes to the broker,
25695
+ * the broker 302s the callback back to THIS app (looked up by the state
25696
+ * nonce), and the token exchange passes the same broker URI. Unset (self-host
25697
+ * / standalone), the app falls back to its own origin's callback.
25698
+ */
25699
+ export function brokerCallback(): string | null {
25700
+ const raw = nsEnv.NETSUITE_BROKER_CALLBACK
25701
+ if (!raw) return null
25702
+ try {
25703
+ const u = new URL(raw)
25704
+ return u.protocol === 'https:' ? u.toString() : null
25705
+ } catch {
25706
+ return null
25707
+ }
25708
+ }
25709
+
25687
25710
  export function redirectUri(request: Request) {
25688
- return \`\${appOrigin(request)}/auth/netsuite/callback\`
25711
+ return brokerCallback() ?? \`\${appOrigin(request)}/auth/netsuite/callback\`
25712
+ }
25713
+
25714
+ /**
25715
+ * Tell the broker where to send this flow's callback: nonce -> this app's
25716
+ * origin. MUST succeed before redirecting to NetSuite, otherwise the callback
25717
+ * dead-ends at the broker. No-op when the broker isn't configured.
25718
+ */
25719
+ export async function registerWithBroker(request: Request, flowId: string): Promise<void> {
25720
+ const callback = brokerCallback()
25721
+ if (!callback) return
25722
+ const registerUrl = new URL('/auth/netsuite/register', callback)
25723
+ const res = await fetch(registerUrl.toString(), {
25724
+ method: 'POST',
25725
+ headers: { 'Content-Type': 'application/json' },
25726
+ body: JSON.stringify({ nonce: flowId, target: appOrigin(request) }),
25727
+ })
25728
+ if (!res.ok) {
25729
+ throw new Error(\`NetSuite broker registration failed (HTTP \${res.status}) -- cannot start sign-in\`)
25730
+ }
25689
25731
  }
25690
25732
 
25691
25733
  // Coerce ?return_to= to our OWN origin. The callback appends the session bearer
@@ -25849,6 +25891,7 @@ import {
25849
25891
  authorizeUrl,
25850
25892
  pkce,
25851
25893
  redirectUri,
25894
+ registerWithBroker,
25852
25895
  requireConfig,
25853
25896
  safeReturnTo,
25854
25897
  SCOPE,
@@ -25866,6 +25909,10 @@ export const Route = createFileRoute('/auth/netsuite/start')({
25866
25909
 
25867
25910
  const { verifier, challenge } = await pkce()
25868
25911
  const flowId = crypto.randomUUID()
25912
+ // With the shared broker, NetSuite redirects to the broker's fixed
25913
+ // callback and the broker forwards here -- it must know this flow's
25914
+ // target before the user leaves for NetSuite.
25915
+ await registerWithBroker(request, flowId)
25869
25916
  await db.insert(netsuiteFlows).values({ flowId, codeVerifier: verifier, returnTo })
25870
25917
 
25871
25918
  const authorize = new URL(authorizeUrl(accountId))
@@ -26728,8 +26775,8 @@ import { integer, sqliteTable, text } from 'drizzle-orm/sqlite-core'
26728
26775
  "CONFIG -- NETSUITE_ACCOUNT_ID, NETSUITE_CLIENT_ID, NETSUITE_CLIENT_SECRET (secret optional for PKCE public clients). Never hardcode; never put in code. Same var names in every environment, different SOURCE:",
26729
26776
  "- DEV (Fly preview): read from the project .env. HelloLeo writes them there (the setup wizard, or the env tooling). In dev the app uses .env -- do NOT tell the user to use HelloLeo Cloud here.",
26730
26777
  "- PROD (published Worker): the SAME names live as HelloLeo Cloud secrets; the publish step promotes them from .env onto the Worker automatically. Same code path (@helloleo/runtime env), just a different source per environment.",
26731
- "- PUBLIC_APP_URL / HELLOLEO_URL are set by the platform per environment; don't hardcode them.",
26732
- "- REDIRECT URI -- never guess or hardcode it. It is derived server-side and auto-corrects behind the preview proxy. Read the exact value from GET /auth/netsuite/config (or getNetSuiteRedirectUri() on the client) and tell the user to register THAT on Integration Record #2 (scope: rest_webservices). ONE redirect URI per record: register exactly the value /auth/netsuite/config returns for the environment the user signs into. If login must work in BOTH the preview and the published app, use a separate Integration Record per environment (same env var names, different NETSUITE_CLIENT_ID/_SECRET values per environment) -- do not stack URLs on one record.",
26778
+ "- PUBLIC_APP_URL / HELLOLEO_URL / NETSUITE_BROKER_CALLBACK are set by the platform per environment; don't hardcode them.",
26779
+ "- REDIRECT URI -- never guess or hardcode it. When NETSUITE_BROKER_CALLBACK is set (the HelloLeo wizard writes it), the redirect URI is HelloLeo's shared OAuth broker: ONE stable URI the user registers ONCE on Integration Record #2 (scope: rest_webservices), valid for both the preview and the published app -- the broker routes each callback back to this app. Without it, the app falls back to its own origin's callback. Either way, read the exact value from GET /auth/netsuite/config (or getNetSuiteRedirectUri() on the client) and tell the user to register THAT.",
26733
26780
  "- Never replace <AuthLayout> with a password gate. Real OAuth is the only supported login."
26734
26781
  ].join(`
26735
26782
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@helloleo/plugins",
3
- "version": "0.2.20",
3
+ "version": "0.2.21",
4
4
  "description": "OpenCode plugins for HelloLeo",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -107,7 +107,9 @@ function netsuiteServerTs(): string {
107
107
  * Config is read from the Cloudflare env binding via @helloleo/runtime.
108
108
  * Set NETSUITE_ACCOUNT_ID / _CLIENT_ID / _CLIENT_SECRET (secret optional for a
109
109
  * PKCE public client) and optional PUBLIC_APP_URL as HelloLeo Cloud secrets
110
- * (prod) or dev vars (preview). They never reach the browser.
110
+ * (prod) or dev vars (preview). NETSUITE_BROKER_CALLBACK (written by the
111
+ * HelloLeo wizard) switches the redirect URI to the shared OAuth broker so
112
+ * one URI serves every app/environment. They never reach the browser.
111
113
  */
112
114
  import { eq } from 'drizzle-orm'
113
115
  import { env as runtimeEnv } from '@helloleo/runtime'
@@ -118,6 +120,7 @@ interface NsEnv {
118
120
  NETSUITE_ACCOUNT_ID?: string
119
121
  NETSUITE_CLIENT_ID?: string
120
122
  NETSUITE_CLIENT_SECRET?: string
123
+ NETSUITE_BROKER_CALLBACK?: string
121
124
  PUBLIC_APP_URL?: string
122
125
  HELLOLEO_URL?: string
123
126
  }
@@ -178,8 +181,47 @@ export function appOrigin(request: Request) {
178
181
  url.host
179
182
  return \`\${proto}://\${hostHeader}\`
180
183
  }
184
+ /**
185
+ * HelloLeo's shared OAuth broker (integration-proxy Worker). When
186
+ * NETSUITE_BROKER_CALLBACK is set (the platform writes it to the project env),
187
+ * customers register that ONE stable URI on their Integration Record instead
188
+ * of one URI per app/environment: the authorize redirect goes to the broker,
189
+ * the broker 302s the callback back to THIS app (looked up by the state
190
+ * nonce), and the token exchange passes the same broker URI. Unset (self-host
191
+ * / standalone), the app falls back to its own origin's callback.
192
+ */
193
+ export function brokerCallback(): string | null {
194
+ const raw = nsEnv.NETSUITE_BROKER_CALLBACK
195
+ if (!raw) return null
196
+ try {
197
+ const u = new URL(raw)
198
+ return u.protocol === 'https:' ? u.toString() : null
199
+ } catch {
200
+ return null
201
+ }
202
+ }
203
+
181
204
  export function redirectUri(request: Request) {
182
- return \`\${appOrigin(request)}/auth/netsuite/callback\`
205
+ return brokerCallback() ?? \`\${appOrigin(request)}/auth/netsuite/callback\`
206
+ }
207
+
208
+ /**
209
+ * Tell the broker where to send this flow's callback: nonce -> this app's
210
+ * origin. MUST succeed before redirecting to NetSuite, otherwise the callback
211
+ * dead-ends at the broker. No-op when the broker isn't configured.
212
+ */
213
+ export async function registerWithBroker(request: Request, flowId: string): Promise<void> {
214
+ const callback = brokerCallback()
215
+ if (!callback) return
216
+ const registerUrl = new URL('/auth/netsuite/register', callback)
217
+ const res = await fetch(registerUrl.toString(), {
218
+ method: 'POST',
219
+ headers: { 'Content-Type': 'application/json' },
220
+ body: JSON.stringify({ nonce: flowId, target: appOrigin(request) }),
221
+ })
222
+ if (!res.ok) {
223
+ throw new Error(\`NetSuite broker registration failed (HTTP \${res.status}) -- cannot start sign-in\`)
224
+ }
183
225
  }
184
226
 
185
227
  // Coerce ?return_to= to our OWN origin. The callback appends the session bearer
@@ -345,6 +387,7 @@ import {
345
387
  authorizeUrl,
346
388
  pkce,
347
389
  redirectUri,
390
+ registerWithBroker,
348
391
  requireConfig,
349
392
  safeReturnTo,
350
393
  SCOPE,
@@ -362,6 +405,10 @@ export const Route = createFileRoute('/auth/netsuite/start')({
362
405
 
363
406
  const { verifier, challenge } = await pkce()
364
407
  const flowId = crypto.randomUUID()
408
+ // With the shared broker, NetSuite redirects to the broker's fixed
409
+ // callback and the broker forwards here -- it must know this flow's
410
+ // target before the user leaves for NetSuite.
411
+ await registerWithBroker(request, flowId)
365
412
  await db.insert(netsuiteFlows).values({ flowId, codeVerifier: verifier, returnTo })
366
413
 
367
414
  const authorize = new URL(authorizeUrl(accountId))
@@ -1260,8 +1307,8 @@ async function executeStart(dir: string): Promise<string> {
1260
1307
  "CONFIG -- NETSUITE_ACCOUNT_ID, NETSUITE_CLIENT_ID, NETSUITE_CLIENT_SECRET (secret optional for PKCE public clients). Never hardcode; never put in code. Same var names in every environment, different SOURCE:",
1261
1308
  "- DEV (Fly preview): read from the project .env. HelloLeo writes them there (the setup wizard, or the env tooling). In dev the app uses .env -- do NOT tell the user to use HelloLeo Cloud here.",
1262
1309
  "- PROD (published Worker): the SAME names live as HelloLeo Cloud secrets; the publish step promotes them from .env onto the Worker automatically. Same code path (@helloleo/runtime env), just a different source per environment.",
1263
- "- PUBLIC_APP_URL / HELLOLEO_URL are set by the platform per environment; don't hardcode them.",
1264
- "- REDIRECT URI -- never guess or hardcode it. It is derived server-side and auto-corrects behind the preview proxy. Read the exact value from GET /auth/netsuite/config (or getNetSuiteRedirectUri() on the client) and tell the user to register THAT on Integration Record #2 (scope: rest_webservices). ONE redirect URI per record: register exactly the value /auth/netsuite/config returns for the environment the user signs into. If login must work in BOTH the preview and the published app, use a separate Integration Record per environment (same env var names, different NETSUITE_CLIENT_ID/_SECRET values per environment) -- do not stack URLs on one record.",
1310
+ "- PUBLIC_APP_URL / HELLOLEO_URL / NETSUITE_BROKER_CALLBACK are set by the platform per environment; don't hardcode them.",
1311
+ "- REDIRECT URI -- never guess or hardcode it. When NETSUITE_BROKER_CALLBACK is set (the HelloLeo wizard writes it), the redirect URI is HelloLeo's shared OAuth broker: ONE stable URI the user registers ONCE on Integration Record #2 (scope: rest_webservices), valid for both the preview and the published app -- the broker routes each callback back to this app. Without it, the app falls back to its own origin's callback. Either way, read the exact value from GET /auth/netsuite/config (or getNetSuiteRedirectUri() on the client) and tell the user to register THAT.",
1265
1312
  "- Never replace <AuthLayout> with a password gate. Real OAuth is the only supported login.",
1266
1313
  ].join("\n");
1267
1314
  }