@dashai/cli 0.10.0 → 0.11.0

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/bin.js CHANGED
@@ -929,6 +929,15 @@ export default withAuth({
929
929
  // redirect-loop trying to re-authenticate.
930
930
  publicPaths: ['/', '/about', '/sign-in-error'],
931
931
  loginPath: '/api/auth/sign-in',
932
+ // Silent sign-in: when a signed-in DashWise user opens the module through
933
+ // the platform (in-app embed or the module subdomain), the SSO handshake
934
+ // completes with zero clicks \u2014 they land straight in the app, landing page
935
+ // included. It never touches:
936
+ // - anonymous visitors on public installs (no platform-session hint),
937
+ // - local \`dashwise dev\` (no platform proxy \u2192 no hint header) \u2014 the
938
+ // landing's "Sign in with DashWise" button stays your dev flow,
939
+ // - anything within 60s of a failed silent attempt (no redirect loops).
940
+ autoSignIn: true,
932
941
  });
933
942
 
934
943
  export const config = {
@@ -970,11 +979,13 @@ export default function RootLayout({ children }: { children: React.ReactNode })
970
979
  function appPageCustomTsx(opts) {
971
980
  return `// Public landing page \u2014 listed in middleware.ts publicPaths.
972
981
  //
973
- // First-run experience for unauthenticated visitors: hero with the
974
- // module name + tagline, a "Sign in with DashWise" button that kicks
975
- // off the SSO flow (via /api/auth/sign-in), and a feature card
976
- // describing what the module does. After signing in, the user lands
977
- // on /dashboard (Server Component, session-validated).
982
+ // NOTE: signed-in DashWise users usually never see this page \u2014 the
983
+ // middleware's \`autoSignIn\` signs them in silently and they land on the
984
+ // page they asked for. This landing serves anonymous visitors (public
985
+ // installs), local \`dashwise dev\`, and the rare failed silent attempt \u2014
986
+ // its "Sign in with DashWise" button is the manual fallback that kicks
987
+ // off the same SSO flow (via /api/auth/sign-in). After signing in, the
988
+ // user lands on /dashboard (Server Component, session-validated).
978
989
  //
979
990
  // Styling: shadcn/ui primitives (\`<Button>\`, \`<Card>\`) on Tailwind 4
980
991
  // theme tokens. Add more blocks via \`npx shadcn add @shadcn-studio/...\`
@@ -1190,6 +1201,11 @@ import { getPublicOrigin } from '@/lib/request-origin';
1190
1201
  export async function GET(request: Request) {
1191
1202
  const url = new URL(request.url);
1192
1203
  const returnTo = url.searchParams.get('return_to') ?? '/';
1204
+ // \`auto=1\` marks a SILENT attempt (the middleware's autoSignIn redirect,
1205
+ // no user click). Its failures degrade to the public landing page \u2014 where
1206
+ // the manual "Sign in with DashWise" button lives \u2014 instead of the error
1207
+ // page: the user never asked for anything, so don't show them an error.
1208
+ const isAuto = url.searchParams.get('auto') === '1';
1193
1209
  const apiBaseUrl = process.env.DASHWISE_API_URL;
1194
1210
  const apiKey = process.env.DASHWISE_API_KEY;
1195
1211
 
@@ -1216,6 +1232,16 @@ export async function GET(request: Request) {
1216
1232
  const callback = new URL('/api/auth/callback', moduleOrigin);
1217
1233
  callback.searchParams.set('return_to', returnTo);
1218
1234
 
1235
+ // Where a failed start lands: the error page for user-initiated sign-ins,
1236
+ // the landing page for silent (auto) attempts.
1237
+ const failTarget = (code: string, description?: string): URL => {
1238
+ if (isAuto) return new URL('/', moduleOrigin);
1239
+ const target = new URL('/sign-in-error', moduleOrigin);
1240
+ target.searchParams.set('code', code);
1241
+ if (description) target.searchParams.set('description', description);
1242
+ return target;
1243
+ };
1244
+
1219
1245
  let res: Response;
1220
1246
  try {
1221
1247
  res = await fetch(\`\${apiBaseUrl}/api/module-api/sso/start-url\`, {
@@ -1228,10 +1254,9 @@ export async function GET(request: Request) {
1228
1254
  body: JSON.stringify({ callback_url: callback.toString() }),
1229
1255
  });
1230
1256
  } catch (err) {
1231
- const target = new URL('/sign-in-error', moduleOrigin);
1232
- target.searchParams.set('code', 'server_error');
1233
- target.searchParams.set('description', (err as Error).message);
1234
- return NextResponse.redirect(target);
1257
+ return NextResponse.redirect(
1258
+ failTarget('server_error', (err as Error).message),
1259
+ );
1235
1260
  }
1236
1261
 
1237
1262
  if (!res.ok) {
@@ -1241,23 +1266,18 @@ export async function GET(request: Request) {
1241
1266
  const body = (await res.json().catch(() => null)) as
1242
1267
  | { code?: string; message?: string }
1243
1268
  | null;
1244
- const target = new URL('/sign-in-error', moduleOrigin);
1245
- target.searchParams.set('code', body?.code ?? 'invalid_request');
1246
- if (body?.message) target.searchParams.set('description', body.message);
1247
- return NextResponse.redirect(target);
1269
+ return NextResponse.redirect(
1270
+ failTarget(body?.code ?? 'invalid_request', body?.message),
1271
+ );
1248
1272
  }
1249
1273
 
1250
1274
  const body = (await res.json().catch(() => null)) as
1251
1275
  | { redirect_url?: string }
1252
1276
  | null;
1253
1277
  if (!body?.redirect_url) {
1254
- const target = new URL('/sign-in-error', moduleOrigin);
1255
- target.searchParams.set('code', 'server_error');
1256
- target.searchParams.set(
1257
- 'description',
1258
- 'start-url response missing redirect_url',
1278
+ return NextResponse.redirect(
1279
+ failTarget('server_error', 'start-url response missing redirect_url'),
1259
1280
  );
1260
- return NextResponse.redirect(target);
1261
1281
  }
1262
1282
  return NextResponse.redirect(body.redirect_url);
1263
1283
  }
@@ -1293,7 +1313,13 @@ export async function GET(request: Request) {
1293
1313
  return NextResponse.redirect(target);
1294
1314
  }
1295
1315
  const code = url.searchParams.get('code');
1296
- const returnTo = url.searchParams.get('return_to') ?? '/';
1316
+ // Only ever redirect to a ROOT-RELATIVE, same-module path. \`return_to\`
1317
+ // rides through the SSO round-trip unvalidated (the backend checks only the
1318
+ // callback ORIGIN, not this sub-param), so an absolute (\`https://evil\`),
1319
+ // protocol-relative (\`//evil\`), or control-char-smuggled value would be a
1320
+ // post-auth open redirect off the module host. Reject anything that isn't a
1321
+ // clean root-relative path.
1322
+ const returnTo = safeReturnTo(url.searchParams.get('return_to'));
1297
1323
  if (!code) {
1298
1324
  const target = new URL('/sign-in-error', publicOrigin);
1299
1325
  target.searchParams.set('code', 'missing_code');
@@ -1309,6 +1335,20 @@ export async function GET(request: Request) {
1309
1335
  }
1310
1336
  return NextResponse.redirect(new URL(returnTo, publicOrigin));
1311
1337
  }
1338
+
1339
+ /** Root-relative in-module paths only \u2014 defends against open-redirect via
1340
+ * a crafted \`return_to\`. Rejects absolute/protocol-relative/backslash and
1341
+ * any ASCII control char (which WHATWG URL parsing strips, defeating a naive
1342
+ * prefix check). Falls back to '/'. */
1343
+ function safeReturnTo(raw: string | null): string {
1344
+ if (!raw || raw[0] !== '/') return '/';
1345
+ if (raw.startsWith('//') || raw.startsWith('/\\\\')) return '/';
1346
+ for (let i = 0; i < raw.length; i++) {
1347
+ const c = raw.charCodeAt(i);
1348
+ if (c < 0x20 || c === 0x7f) return '/';
1349
+ }
1350
+ return raw;
1351
+ }
1312
1352
  `;
1313
1353
  }
1314
1354
  function appApiAuthSignOutTs() {
@@ -9231,7 +9271,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
9231
9271
  }
9232
9272
  })();
9233
9273
  function getVersion() {
9234
- return "0.10.0";
9274
+ return "0.11.0";
9235
9275
  }
9236
9276
  function parseIntOption(value) {
9237
9277
  const n = Number.parseInt(value, 10);