@dashai/cli 0.8.1 → 0.10.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
@@ -256,53 +256,185 @@ async function revokeApiKey(installationId, keyId, options = {}) {
256
256
  options
257
257
  );
258
258
  }
259
- async function createTable(installationId, body, options = {}) {
259
+ async function getOrigins(installationId, options = {}) {
260
+ return apiRequest(
261
+ "GET",
262
+ `/api/installations/${installationId}/origins`,
263
+ void 0,
264
+ options
265
+ );
266
+ }
267
+ async function setOrigins(installationId, allowedOrigins, options = {}) {
268
+ return apiRequest(
269
+ "PUT",
270
+ `/api/installations/${installationId}/origins`,
271
+ { allowed_origins: allowedOrigins },
272
+ options
273
+ );
274
+ }
275
+ async function createTable(body, options = {}) {
260
276
  return apiRequest(
261
277
  "POST",
262
- `/api/installations/${installationId}/schema/tables`,
278
+ `/api/module-api/schema/tables`,
263
279
  body,
264
280
  options
265
281
  );
266
282
  }
267
- async function dropTable(installationId, tableSlug, options = {}) {
283
+ async function dropTable(tableSlug, options = {}) {
268
284
  return apiRequest(
269
285
  "DELETE",
270
- `/api/installations/${installationId}/schema/tables/${tableSlug}`,
286
+ `/api/module-api/schema/tables/${tableSlug}`,
271
287
  void 0,
272
288
  options
273
289
  );
274
290
  }
275
- async function addField(installationId, body, options = {}) {
291
+ async function addField(body, options = {}) {
276
292
  return apiRequest(
277
293
  "POST",
278
- `/api/installations/${installationId}/schema/fields`,
294
+ `/api/module-api/schema/fields`,
279
295
  body,
280
296
  options
281
297
  );
282
298
  }
283
- async function updateField(installationId, tableSlug, fieldSlug, body, options = {}) {
299
+ async function updateField(tableSlug, fieldSlug, body, options = {}) {
284
300
  return apiRequest(
285
301
  "PATCH",
286
- `/api/installations/${installationId}/schema/fields/${tableSlug}/${fieldSlug}`,
302
+ `/api/module-api/schema/fields/${tableSlug}/${fieldSlug}`,
287
303
  body,
288
304
  options
289
305
  );
290
306
  }
291
- async function getInstallationSchema(installationId, options = {}) {
307
+ async function getInstallationSchema(options = {}) {
292
308
  return apiRequest(
293
309
  "GET",
294
- `/api/installations/${installationId}/schema`,
310
+ `/api/module-api/schema`,
295
311
  void 0,
296
312
  options
297
313
  );
298
314
  }
299
- async function removeField(installationId, tableSlug, fieldSlug, options = {}) {
315
+ async function removeField(tableSlug, fieldSlug, options = {}) {
300
316
  return apiRequest(
301
317
  "DELETE",
302
- `/api/installations/${installationId}/schema/fields/${tableSlug}/${fieldSlug}`,
318
+ `/api/module-api/schema/fields/${tableSlug}/${fieldSlug}`,
319
+ void 0,
320
+ options
321
+ );
322
+ }
323
+ async function getInstallationDependencies(installationId, options = {}) {
324
+ return apiRequest(
325
+ "GET",
326
+ `/api/installations/${installationId}/dependencies`,
327
+ void 0,
328
+ options
329
+ );
330
+ }
331
+ async function bindDependency(installationId, providerSlug, body = {}, options = {}) {
332
+ return apiRequest(
333
+ "POST",
334
+ `/api/installations/${installationId}/dependencies/${providerSlug}/bind`,
335
+ body,
336
+ options
337
+ );
338
+ }
339
+ async function unbindDependency(installationId, providerSlug, options = {}) {
340
+ return apiRequest(
341
+ "POST",
342
+ `/api/installations/${installationId}/dependencies/${providerSlug}/unbind`,
343
+ void 0,
344
+ options
345
+ );
346
+ }
347
+ async function getInstallationBindings(installationId, options = {}) {
348
+ return apiRequest(
349
+ "GET",
350
+ `/api/installations/${installationId}/bindings`,
351
+ void 0,
352
+ options
353
+ );
354
+ }
355
+ async function getBindingCandidates(installationId, usesSlug, options = {}) {
356
+ return apiRequest(
357
+ "GET",
358
+ `/api/installations/${installationId}/bindings/${encodeURIComponent(usesSlug)}/candidates`,
359
+ void 0,
360
+ options
361
+ );
362
+ }
363
+ async function putBinding(installationId, usesSlug, body, options = {}) {
364
+ return apiRequest(
365
+ "PUT",
366
+ `/api/installations/${installationId}/bindings/${encodeURIComponent(usesSlug)}`,
367
+ body,
368
+ options
369
+ );
370
+ }
371
+ async function unbindBinding(installationId, usesSlug, options = {}) {
372
+ return apiRequest(
373
+ "POST",
374
+ `/api/installations/${installationId}/bindings/${encodeURIComponent(usesSlug)}/unbind`,
375
+ void 0,
376
+ options
377
+ );
378
+ }
379
+ async function declineBinding(installationId, usesSlug, options = {}) {
380
+ return apiRequest(
381
+ "POST",
382
+ `/api/installations/${installationId}/bindings/${encodeURIComponent(usesSlug)}/decline`,
383
+ void 0,
384
+ options
385
+ );
386
+ }
387
+ function slugifyFieldName(name) {
388
+ return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
389
+ }
390
+ async function introspectWorkspaceTable(_installationId, tableNameOrId, options = {}) {
391
+ const ref = String(tableNameOrId).trim();
392
+ const asNumber = Number(ref);
393
+ if (!Number.isInteger(asNumber) || asNumber <= 0 || String(asNumber) !== ref) {
394
+ const message = `"${ref}" is not a numeric table id. Pass the workspace table's numeric id (find it in the Dashwise UI URL, or via \`GET /api/tables/application/:applicationId\`). Resolving a table by name is not yet supported by this command.`;
395
+ throw new ApiError(0, { code: "NON_NUMERIC_TABLE_REF", message }, message);
396
+ }
397
+ const table = await apiRequest(
398
+ "GET",
399
+ `/api/tables/${asNumber}`,
303
400
  void 0,
304
401
  options
305
402
  );
403
+ const rawFields = await apiRequest(
404
+ "GET",
405
+ `/api/fields/table/${asNumber}`,
406
+ void 0,
407
+ options
408
+ );
409
+ const fields = rawFields.map((f) => ({
410
+ id: f.id,
411
+ slug: slugifyFieldName(f.name),
412
+ name: f.name,
413
+ type: f.type,
414
+ ...f.is_required !== void 0 ? { required: f.is_required } : {}
415
+ }));
416
+ return {
417
+ table_id: table.id,
418
+ name: table.name,
419
+ slug: slugifyFieldName(table.name),
420
+ fields
421
+ };
422
+ }
423
+ async function getModuleBySlug(slug, options = {}) {
424
+ return apiRequest(
425
+ "GET",
426
+ `/api/modules/by-slug/${encodeURIComponent(slug)}`,
427
+ void 0,
428
+ options
429
+ );
430
+ }
431
+ async function applyDraftManifest(moduleId, manifest, options = {}) {
432
+ return apiRequest(
433
+ "PUT",
434
+ `/api/modules/${moduleId}/draft/manifest`,
435
+ manifest,
436
+ options
437
+ );
306
438
  }
307
439
  var ApiError;
308
440
  var init_api_client = __esm({
@@ -326,38 +458,6 @@ var init_api_client = __esm({
326
458
  };
327
459
  }
328
460
  });
329
- function manifestPath(dir) {
330
- return join(resolve(dir ?? process.cwd()), "module.json");
331
- }
332
- function readManifest(dir) {
333
- const path = manifestPath(dir);
334
- if (!existsSync(path)) return null;
335
- const raw = readFileSync(path, "utf-8");
336
- try {
337
- return JSON.parse(raw);
338
- } catch (err) {
339
- throw new Error(
340
- `Failed to parse ${path}: ${err.message}. Fix or delete the file and re-run.`
341
- );
342
- }
343
- }
344
- function writeManifest(manifest, dir) {
345
- const path = manifestPath(dir);
346
- writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
347
- }
348
- function readManifestOrThrow(dir) {
349
- const manifest = readManifest(dir);
350
- if (!manifest) {
351
- throw new Error(
352
- `No module.json found in ${resolve(dir ?? process.cwd())}. Run \`dashwise module init\` first or pass \`--dir <path>\`.`
353
- );
354
- }
355
- return manifest;
356
- }
357
- var init_manifest_io = __esm({
358
- "src/lib/manifest-io.ts"() {
359
- }
360
- });
361
461
  function success(msg) {
362
462
  process.stderr.write(`${pc.green("\u2713")} ${msg}
363
463
  `);
@@ -393,6 +493,38 @@ var init_output = __esm({
393
493
  "src/lib/output.ts"() {
394
494
  }
395
495
  });
496
+ function manifestPath(dir) {
497
+ return join(resolve(dir ?? process.cwd()), "module.json");
498
+ }
499
+ function readManifest(dir) {
500
+ const path = manifestPath(dir);
501
+ if (!existsSync(path)) return null;
502
+ const raw = readFileSync(path, "utf-8");
503
+ try {
504
+ return JSON.parse(raw);
505
+ } catch (err) {
506
+ throw new Error(
507
+ `Failed to parse ${path}: ${err.message}. Fix or delete the file and re-run.`
508
+ );
509
+ }
510
+ }
511
+ function writeManifest(manifest, dir) {
512
+ const path = manifestPath(dir);
513
+ writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
514
+ }
515
+ function readManifestOrThrow(dir) {
516
+ const manifest = readManifest(dir);
517
+ if (!manifest) {
518
+ throw new Error(
519
+ `No module.json found in ${resolve(dir ?? process.cwd())}. Run \`dashwise module init\` first or pass \`--dir <path>\`.`
520
+ );
521
+ }
522
+ return manifest;
523
+ }
524
+ var init_manifest_io = __esm({
525
+ "src/lib/manifest-io.ts"() {
526
+ }
527
+ });
396
528
 
397
529
  // src/lib/scaffold.ts
398
530
  function isDeployTarget(value) {
@@ -409,7 +541,19 @@ function manifestJson(opts) {
409
541
  },
410
542
  module_kind: kind,
411
543
  owns: {
412
- tables: []
544
+ tables: [
545
+ {
546
+ // NOTE: no `name` key here — the backend manifest validator
547
+ // (POST /api/installations/dev) is strict and rejects unrecognized
548
+ // table keys, and it derives the display name from the slug. A
549
+ // `name` here would fail provisioning's table materialization (#4).
550
+ slug: "items",
551
+ fields: [
552
+ { slug: "title", type: "text" },
553
+ { slug: "done", type: "boolean" }
554
+ ]
555
+ }
556
+ ]
413
557
  }
414
558
  };
415
559
  if (kind === "custom") {
@@ -440,7 +584,7 @@ function packageJson(opts) {
440
584
  start: "next start"
441
585
  },
442
586
  dependencies: {
443
- "@dashai/sdk": "^0.8.0",
587
+ "@dashai/sdk": "^2.0.0",
444
588
  next: "^15.0.0",
445
589
  react: "^19.0.0",
446
590
  "react-dom": "^19.0.0"
@@ -485,9 +629,27 @@ function tsconfigJson() {
485
629
  }
486
630
  function nextConfig() {
487
631
  return `/** @type {import("next").NextConfig} */
632
+ //
633
+ // Phase 2 two-var contract: the deploy env supplies exactly
634
+ // DASHWISE_API_URL + DASHWISE_API_KEY. The browser needs the API URL
635
+ // too (the SDK's <AuthProvider> calls /api/auth/sessions/me on it), so
636
+ // we DERIVE NEXT_PUBLIC_DASHWISE_API_URL here from DASHWISE_API_URL at
637
+ // build/start time \u2014 you never set the NEXT_PUBLIC_ var yourself. If
638
+ // you already have one set (e.g. a custom setup), it wins.
639
+ const apiUrl = process.env.DASHWISE_API_URL;
640
+
488
641
  const nextConfig = {
642
+ // Required for the platform-hosted (per-install Lambda) runtime: the
643
+ // injected wrapper probes [.next/standalone/server.js, server.js] and
644
+ // exit(1)s the cold start if neither exists. Without this the built
645
+ // Lambda FATALs. Harmless for local \`next dev\` / \`next start\`.
646
+ output: 'standalone',
489
647
  typescript: { ignoreBuildErrors: true },
490
648
  eslint: { ignoreDuringBuilds: true },
649
+ env: {
650
+ NEXT_PUBLIC_DASHWISE_API_URL:
651
+ process.env.NEXT_PUBLIC_DASHWISE_API_URL ?? apiUrl,
652
+ },
491
653
  };
492
654
  export default nextConfig;
493
655
  `;
@@ -531,16 +693,18 @@ function gitignore() {
531
693
  function envLocalExample(apiUrl = SCAFFOLD_DEFAULT_API_URL) {
532
694
  return [
533
695
  "# Copy to .env.local + fill in real values.",
534
- "# These vars are auto-injected by `dashwise dev` (the unified dev loop)",
535
- "# from ~/.config/dashwise/auth.json \u2014 you only need to edit this file if",
536
- "# you want to point at a non-default backend or run `next dev` standalone.",
696
+ "# Phase 2 two-var contract: exactly these two vars. `dashwise dev`",
697
+ "# auto-injects both from ~/.config/dashwise/auth.json \u2014 you only need",
698
+ "# to edit this file if you point at a non-default backend or run",
699
+ "# `next dev` standalone.",
700
+ "#",
701
+ "# The browser needs the API URL too; next.config derives",
702
+ "# NEXT_PUBLIC_DASHWISE_API_URL from DASHWISE_API_URL, so you never set",
703
+ "# it yourself.",
537
704
  `DASHWISE_API_URL=${apiUrl}`,
538
- "# Client-side mirror \u2014 Next.js inlines NEXT_PUBLIC_* into the browser",
539
- "# bundle so the SDK's <AuthProvider> can hit `/api/auth/sessions/me`",
540
- "# on the right origin. Should match DASHWISE_API_URL above.",
541
- `NEXT_PUBLIC_DASHWISE_API_URL=${apiUrl}`,
542
- "DASHWISE_API_TOKEN=",
543
- "DASHWISE_INSTALLATION_ID=local",
705
+ "# Scoped `dwk_` API key \u2014 mint with `dashwise api-keys create <name>`",
706
+ "# for deploys; `dashwise dev` injects the local-dev key automatically.",
707
+ "DASHWISE_API_KEY=",
544
708
  ""
545
709
  ].join("\n");
546
710
  }
@@ -548,24 +712,22 @@ function envLocal(apiUrl = SCAFFOLD_DEFAULT_API_URL) {
548
712
  return [
549
713
  "# Local dev config \u2014 auto-generated by `dashwise module init`.",
550
714
  `# Backend URL pinned to your logged-in CLI config (${apiUrl}). Gitignored;`,
551
- "# safe to edit. `dashwise dev` (the unified loop) auto-injects",
552
- "# DASHWISE_API_TOKEN from ~/.config/dashwise/auth.json AND",
553
- "# DASHWISE_DEV_SESSION_ID from the dev session it registers \u2014 so",
554
- "# you typically don't need to set either here. Change DASHWISE_API_URL",
555
- "# (and NEXT_PUBLIC_DASHWISE_API_URL to match) to point at a different",
556
- "# backend.",
715
+ "# safe to edit. `dashwise dev` (the unified loop) auto-injects the",
716
+ "# Phase 2 two-var contract \u2014 DASHWISE_API_URL + the per-module `dwk_`",
717
+ "# DASHWISE_API_KEY \u2014 from ~/.config/dashwise/auth.json, so you",
718
+ "# typically don't need to set either here.",
557
719
  "#",
558
- "# DASHWISE_INSTALLATION_ID is only needed AFTER you publish + install",
559
- "# the module in a workspace; until then dev mode (DASHWISE_DEV_SESSION_ID,",
560
- "# auto-set by `dashwise dev`) is the auth path.",
720
+ "# The browser needs the API URL too; next.config derives",
721
+ "# NEXT_PUBLIC_DASHWISE_API_URL from DASHWISE_API_URL automatically.",
722
+ "# There is no DASHWISE_INSTALLATION_ID \u2014 the API key resolves the",
723
+ "# install server-side.",
561
724
  `DASHWISE_API_URL=${apiUrl}`,
562
- "# Client-side mirror (must match DASHWISE_API_URL). Next.js inlines",
563
- "# NEXT_PUBLIC_* into the browser bundle so the SDK's AuthProvider can",
564
- "# call /api/auth/sessions/me on the right origin instead of bouncing",
565
- "# off the local module server with a 404.",
566
- `NEXT_PUBLIC_DASHWISE_API_URL=${apiUrl}`,
567
- "DASHWISE_API_TOKEN=",
568
- "DASHWISE_INSTALLATION_ID=",
725
+ "# Scoped `dwk_` key. Commented out on purpose: `dashwise dev` injects",
726
+ "# the local-dev key from ~/.config/dashwise/auth.json, and an empty",
727
+ "# `DASHWISE_API_KEY=` here would SHADOW that (and make a bare `next dev`",
728
+ "# fail with a confusing empty-key error). For a standalone `next dev`,",
729
+ "# uncomment + paste a key from `dashwise api-keys create <name>`.",
730
+ "# DASHWISE_API_KEY=",
569
731
  ""
570
732
  ].join("\n");
571
733
  }
@@ -687,7 +849,7 @@ npm run dev:next # next dev on :3000 (terminal 2)
687
849
  - **\`app/page.tsx\`** \u2014 public landing (allowlisted in \`middleware.ts\`).
688
850
  - **\`app/dashboard/page.tsx\`** \u2014 Server Component requiring a valid session via \`getServerSession()\`.
689
851
  - **\`app/api/auth/callback/route.ts\`** \u2014 exchanges the SSO one-time code for a session cookie (uses \`exchangeCode()\` from the SDK).
690
- - **\`app/api/auth/sign-in/route.ts\`** \u2014 entry point that redirects to the backend's \`/sso/start\` with the right \`return_to\` + \`client_id\`.
852
+ - **\`app/api/auth/sign-in/route.ts\`** \u2014 one entry point that calls the backend's \`POST /api/module-api/sso/start-url\` (authenticated with your \`dwk_\` key) and 302s the browser to the returned \`redirect_url\`. Same path in dev + prod.
691
853
  - **\`app/api/auth/sign-out/route.ts\`** \u2014 clears the session cookie + tells the backend to revoke.
692
854
 
693
855
  ## Imports
@@ -736,25 +898,23 @@ the canonical block-discovery flow.
736
898
 
737
899
  ## Deploying elsewhere (custom domain)
738
900
 
739
- The scaffolded SSO flow is wired for DashWise's own infrastructure
740
- (\`m-<id>.dashwise.net\`). If you want to host this module on your own
741
- domain (e.g. \`mymodule.example.com\`), there are two options:
742
-
743
- 1. **Wait for custom-domain support (in design).** A future \`--callback-origin\`
744
- flag on \`dashwise module init\` plus a \`module_oauth_clients\` allowlist will
745
- let modules run at arbitrary domains while still using DashWise SSO. Tracked
746
- in [\`modules-author-experience-v1.md\`](https://github.com/sharmaintuize/dashwise-devops-docs/blob/main/plans/modules-author-experience-v1.md) \xA73.
747
- 2. **Bypass SSO + use a Workspace API Token today.** Replace the SSO middleware
748
- with a token-check middleware that reads \`Authorization: Token <key>\` from
749
- each request. Tokens are created at \`<dashwise-dashboard>/settings/tokens\`
750
- and scoped to a workspace. The SDK accepts these via the standard
751
- \`DASHWISE_API_TOKEN\` env var. You'll lose the per-user session model
752
- (every request runs as the token's owner) but gain the freedom to deploy
753
- anywhere.
754
-
755
- The CLI's \`dashwise dev\` command auto-injects the developer's CLI token (also
756
- a bearer token) into local dev \u2014 so local development works regardless of
757
- which path you pick for production.
901
+ The scaffolded SSO flow works on any domain. The \`/api/auth/sign-in\`
902
+ route sends its own public origin as the \`callback_url\` to the backend;
903
+ the backend accepts it as long as the origin is on your installation's
904
+ allowlist. Local dev (\`http://localhost:<port>\`) is allowlisted
905
+ automatically for \`kind='local'\` installs.
906
+
907
+ For a deployed custom domain (e.g. \`https://mymodule.example.com\`), add
908
+ it to the allowlist:
909
+
910
+ \`\`\`bash
911
+ dashwise origins add https://mymodule.example.com
912
+ dashwise origins list # confirm
913
+ \`\`\`
914
+
915
+ That's it \u2014 no middleware changes, no separate token scheme. The single
916
+ \`dwk_\` \`DASHWISE_API_KEY\` drives both the data plane and the SSO
917
+ start-url call.
758
918
  `;
759
919
  }
760
920
  function middlewareTs() {
@@ -796,7 +956,10 @@ export default function RootLayout({ children }: { children: React.ReactNode })
796
956
  enableSystem
797
957
  disableTransitionOnChange
798
958
  >
799
- <AuthProvider>{children}</AuthProvider>
959
+ {/* sessionEndpoint points at the same-origin /api/auth/session
960
+ route so the host-scoped session cookie is actually sent \u2014
961
+ a cross-origin fetch to the API would drop it (D8). */}
962
+ <AuthProvider sessionEndpoint="/api/auth/session">{children}</AuthProvider>
800
963
  </ThemeProvider>
801
964
  </body>
802
965
  </html>
@@ -1001,73 +1164,102 @@ function Row({ label, value }: { label: string; value: string }) {
1001
1164
  `;
1002
1165
  }
1003
1166
  function appApiAuthSignInTs() {
1004
- return `// /api/auth/sign-in \u2014 kick off the SSO flow.
1005
- //
1006
- // Reads \`?return_to=\` from the caller (defaults to '/'). Branches
1007
- // on env:
1167
+ return `// /api/auth/sign-in \u2014 kick off the SSO flow (one path, dev + prod).
1008
1168
  //
1009
- // - DASHWISE_DEV_SESSION_ID set (set automatically by
1010
- // \`dashwise dev\`): hits /api/auth/dev/sso/start with the dev
1011
- // session id. No installation_id required \u2014 the module isn't
1012
- // installed in a workspace yet.
1169
+ // Reads \`?return_to=\` from the caller (defaults to '/'), then asks the
1170
+ // DashWise backend for a redirect URL by calling the module-API
1171
+ // server-side start endpoint:
1013
1172
  //
1014
- // - DASHWISE_INSTALLATION_ID set (production / installed-module
1015
- // path): hits /api/auth/sso/start with the installation id.
1173
+ // POST /api/module-api/sso/start-url
1174
+ // Authorization: Bearer <DASHWISE_API_KEY> (the module's dwk_ key)
1175
+ // { "callback_url": "<this module>/api/auth/callback?return_to=..." }
1016
1176
  //
1017
- // In both cases the backend mints an exchange code + 302s back to
1018
- // \`/api/auth/callback?code=...\` where we set the session cookie.
1177
+ // The backend resolves the install from the key (no installation id in
1178
+ // the request), validates the callback_url origin against the install's
1179
+ // allowlist (kind='local' installs implicitly allow localhost), and
1180
+ // returns { redirect_url, expires_in_seconds }. We 302 the browser to
1181
+ // redirect_url, which runs the existing main-app SSO flow and ends in a
1182
+ // 302 back to callback_url?code=<one-time>. The /api/auth/callback route
1183
+ // exchanges that code for a session cookie.
1019
1184
  //
1020
- // Unauthenticated browser hits to either backend endpoint 302 to
1021
- // the DashWise FE \`/login?return_to=...\` automatically \u2014 you don't
1022
- // need to handle the "user not logged in" case here.
1185
+ // No dev-vs-prod branching, no installation id, no dev-session id \u2014 the
1186
+ // single dwk_ key drives both local dev and production.
1023
1187
  import { NextResponse } from 'next/server';
1024
1188
  import { getPublicOrigin } from '@/lib/request-origin';
1025
1189
 
1026
- export function GET(request: Request) {
1190
+ export async function GET(request: Request) {
1027
1191
  const url = new URL(request.url);
1028
1192
  const returnTo = url.searchParams.get('return_to') ?? '/';
1029
1193
  const apiBaseUrl = process.env.DASHWISE_API_URL;
1030
- const installationId = process.env.DASHWISE_INSTALLATION_ID;
1031
- const devSessionId = process.env.DASHWISE_DEV_SESSION_ID;
1194
+ const apiKey = process.env.DASHWISE_API_KEY;
1032
1195
 
1033
1196
  if (!apiBaseUrl) {
1034
- return new NextResponse(
1035
- 'DASHWISE_API_URL must be set in .env.local',
1036
- { status: 500 },
1037
- );
1197
+ return new NextResponse('DASHWISE_API_URL must be set in the environment', {
1198
+ status: 500,
1199
+ });
1038
1200
  }
1039
- if (!installationId && !devSessionId) {
1201
+ if (!apiKey) {
1040
1202
  return new NextResponse(
1041
- 'Either DASHWISE_INSTALLATION_ID (installed module) or DASHWISE_DEV_SESSION_ID ' +
1042
- '(local dev via \`dashwise dev\`) must be set. Run \`dashwise dev\` to register ' +
1043
- 'a dev session, or set DASHWISE_INSTALLATION_ID after publishing your module.',
1203
+ 'DASHWISE_API_KEY must be set in the environment. Run \`dashwise dev\` ' +
1204
+ '(local) or paste a key from \`dashwise api-keys create\` (deployed).',
1044
1205
  { status: 500 },
1045
1206
  );
1046
1207
  }
1047
1208
 
1048
- // Absolute URL for return_to so the backend knows which host to
1049
- // bounce back to (the callback path is fixed at /api/auth/callback
1050
- // by SDK convention). \`getPublicOrigin\` reads \`X-Forwarded-Host\`
1051
- // when present so the redirect URL points at the public domain,
1052
- // not the internal Node origin behind reverse proxies.
1209
+ // Absolute callback URL so the backend knows which host to bounce back
1210
+ // to. \`getPublicOrigin\` reads \`X-Forwarded-Host\` when present so the
1211
+ // URL points at the public domain, not the internal Node origin behind
1212
+ // reverse proxies (Railway/Vercel/Fly). The callback path is fixed at
1213
+ // /api/auth/callback by SDK convention; we tuck \`return_to\` onto it so
1214
+ // the callback route can resume the user's original destination.
1053
1215
  const moduleOrigin = getPublicOrigin(request);
1054
- const absoluteReturnTo = \`\${moduleOrigin}\${returnTo}\`;
1055
-
1056
- let ssoStart;
1057
- if (devSessionId) {
1058
- // Dev mode wins when both are set \u2014 \`dashwise dev\` injects
1059
- // DASHWISE_DEV_SESSION_ID into the child env, which overrides
1060
- // whatever was in .env.local. The dev session id keys back to
1061
- // the dev's workspace + callback origin allowlist.
1062
- ssoStart = new URL('/api/auth/dev/sso/start', apiBaseUrl);
1063
- ssoStart.searchParams.set('return_to', absoluteReturnTo);
1064
- ssoStart.searchParams.set('dev_session_id', devSessionId);
1065
- } else {
1066
- ssoStart = new URL('/api/auth/sso/start', apiBaseUrl);
1067
- ssoStart.searchParams.set('return_to', absoluteReturnTo);
1068
- ssoStart.searchParams.set('client_id', installationId);
1216
+ const callback = new URL('/api/auth/callback', moduleOrigin);
1217
+ callback.searchParams.set('return_to', returnTo);
1218
+
1219
+ let res: Response;
1220
+ try {
1221
+ res = await fetch(\`\${apiBaseUrl}/api/module-api/sso/start-url\`, {
1222
+ method: 'POST',
1223
+ headers: {
1224
+ 'Content-Type': 'application/json',
1225
+ Accept: 'application/json',
1226
+ Authorization: \`Bearer \${apiKey}\`,
1227
+ },
1228
+ body: JSON.stringify({ callback_url: callback.toString() }),
1229
+ });
1230
+ } 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);
1235
+ }
1236
+
1237
+ if (!res.ok) {
1238
+ // The backend rejected the start (bad key, callback origin not
1239
+ // allowlisted, install missing). Surface a real error page instead
1240
+ // of a raw 4xx.
1241
+ const body = (await res.json().catch(() => null)) as
1242
+ | { code?: string; message?: string }
1243
+ | 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);
1069
1248
  }
1070
- return NextResponse.redirect(ssoStart);
1249
+
1250
+ const body = (await res.json().catch(() => null)) as
1251
+ | { redirect_url?: string }
1252
+ | null;
1253
+ 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',
1259
+ );
1260
+ return NextResponse.redirect(target);
1261
+ }
1262
+ return NextResponse.redirect(body.redirect_url);
1071
1263
  }
1072
1264
  `;
1073
1265
  }
@@ -1139,6 +1331,65 @@ export async function POST(request: Request) {
1139
1331
  }
1140
1332
  `;
1141
1333
  }
1334
+ function appApiItemsRouteTs() {
1335
+ return `// Example data-plane route \u2014 lists + creates rows in the seeded
1336
+ // \`items\` table via the generated typed SDK. Delete this (and the
1337
+ // \`items\` table in module.json) once you add your own tables.
1338
+ //
1339
+ // \`db\` comes from @dashai/generated, regenerated on every module.json
1340
+ // save by \`dashwise dev\`. Run it (or \`dashwise module generate\`) first.
1341
+ import { NextResponse } from 'next/server';
1342
+ import { db } from '@dashai/generated';
1343
+
1344
+ export async function GET() {
1345
+ const page = await db.items.list({ limit: 50 });
1346
+ return NextResponse.json(page.rows);
1347
+ }
1348
+
1349
+ export async function POST(request: Request) {
1350
+ const body = (await request.json().catch(() => null)) as
1351
+ | { title?: unknown; done?: unknown }
1352
+ | null;
1353
+ const title = typeof body?.title === 'string' ? body.title : '';
1354
+ if (!title) {
1355
+ return NextResponse.json(
1356
+ { error: 'title is required (string)' },
1357
+ { status: 400 },
1358
+ );
1359
+ }
1360
+ const done = typeof body?.done === 'boolean' ? body.done : false;
1361
+ const row = await db.items.create({ title, done });
1362
+ return NextResponse.json(row, { status: 201 });
1363
+ }
1364
+ `;
1365
+ }
1366
+ function appApiAuthSessionTs() {
1367
+ return `// /api/auth/session \u2014 SAME-ORIGIN session probe for client components.
1368
+ //
1369
+ // D8: the browser's <AuthProvider> can't read the session by calling the
1370
+ // cross-origin DashWise API directly, because the \`dashwise.session\`
1371
+ // cookie is host-scoped to THIS module's origin and is never sent to
1372
+ // \`api.dashwise.net\`. So <AuthProvider sessionEndpoint="/api/auth/session">
1373
+ // hits this route (same origin \u2192 the cookie IS sent); we validate it
1374
+ // server-side via \`getServerSession()\` (which forwards the cookie to the
1375
+ // API for us) and return the fully-enriched session \u2014 or \`null\` \u2014 as JSON.
1376
+ //
1377
+ // \`getServerSession()\` already merges the RBAC slice, so the client
1378
+ // receives \`rbacRoles\` / \`rbacPermissions\` and skips a second fetch.
1379
+ import { NextResponse } from 'next/server';
1380
+ import { getServerSession } from '@dashai/sdk/auth/server';
1381
+
1382
+ export async function GET() {
1383
+ const session = await getServerSession();
1384
+ // Always 200 \u2014 \`null\` is a valid "not signed in" answer the client
1385
+ // maps to \`unauthenticated\`. \`no-store\` so a stale signed-in body is
1386
+ // never served to a signed-out user after sign-out.
1387
+ return NextResponse.json(session, {
1388
+ headers: { 'Cache-Control': 'no-store' },
1389
+ });
1390
+ }
1391
+ `;
1392
+ }
1142
1393
  function packageJsonCustom(opts) {
1143
1394
  return JSON.stringify(
1144
1395
  {
@@ -1154,7 +1405,7 @@ function packageJsonCustom(opts) {
1154
1405
  start: "next start"
1155
1406
  },
1156
1407
  dependencies: {
1157
- "@dashai/sdk": "^0.8.0",
1408
+ "@dashai/sdk": "^2.0.0",
1158
1409
  next: "^15.0.0",
1159
1410
  react: "^19.0.0",
1160
1411
  "react-dom": "^19.0.0",
@@ -1504,7 +1755,7 @@ const ERROR_MESSAGES: Record<string, string> = {
1504
1755
  server_error:
1505
1756
  'The DashWise sign-in server returned an internal error. This is usually transient \u2014 try again in a moment.',
1506
1757
  invalid_request:
1507
- 'The sign-in request was malformed. This is almost always a configuration bug \u2014 check that DASHWISE_API_URL and DASHWISE_INSTALLATION_ID are set correctly.',
1758
+ 'The sign-in request was malformed. This is almost always a configuration bug \u2014 check that DASHWISE_API_URL and DASHWISE_API_KEY are set correctly and the callback origin is allowlisted (run "dashwise origins add <url>").',
1508
1759
  missing_code:
1509
1760
  'The callback URL was missing the expected exchange code. This usually means the SSO flow was interrupted.',
1510
1761
  exchange_failed:
@@ -1577,7 +1828,14 @@ function escapeHtml(s) {
1577
1828
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1578
1829
  }
1579
1830
  function isValidSlug(slug) {
1580
- return /^[a-z][a-z0-9_-]*$/.test(slug);
1831
+ return MODULE_SLUG_REGEX.test(slug);
1832
+ }
1833
+ function slugValidationMessage(slug) {
1834
+ if (slug.includes("_")) {
1835
+ const suggestion = slug.replace(/_/g, "-").replace(/^-+|-+$/g, "");
1836
+ return `Slug "${slug}" uses underscores \u2014 module slugs must be kebab-case (letters, digits, hyphens; e.g. "${suggestion || "my-module"}"). Rename it and retry.`;
1837
+ }
1838
+ return `Slug "${slug}" must be kebab-case \u2014 match /^[a-z][a-z0-9-]*[a-z0-9]$/ (a leading letter, then letters/digits/hyphens, no trailing hyphen).`;
1581
1839
  }
1582
1840
  function claudeMd(opts) {
1583
1841
  const isCustom = (opts.kind ?? "hand_authored") === "custom";
@@ -1660,17 +1918,43 @@ await db.<table>.create({ /* fields */ });
1660
1918
  const stats = await qb.from('<table>').where({ /* ... */ }).execute();
1661
1919
  \`\`\`
1662
1920
 
1663
- The data plane authenticates via the per-module runtime token cached
1664
- in \`~/.config/dashwise/auth.json#modules[${opts.slug}]\` after
1665
- \`dashwise init\` provisions the dev install. \`dashwise dev\` injects
1666
- this token into the Next.js child env automatically.
1921
+ The data plane authenticates via the per-module scoped \`dwk_\` API key
1922
+ cached in \`~/.config/dashwise/auth.json#modules[${opts.slug}]\` after
1923
+ \`dashwise init\` provisions the dev install. \`dashwise dev\` injects it
1924
+ as \`DASHWISE_API_KEY\` into the Next.js child env automatically. The
1925
+ key binds the install server-side \u2014 there is no \`DASHWISE_INSTALLATION_ID\`.
1926
+
1927
+ ### Own tables vs. the shared Data Hub
1928
+
1929
+ The dev key \`dashwise init\` mints gives you **full access to your
1930
+ module's OWN tables** (\`owns.tables\` in \`module.json\`) through \`db\` /
1931
+ \`qb\` \u2014 no grant setup needed. That covers the common case.
1932
+
1933
+ The **shared workspace Data Hub** (other tables in the workspace, reached
1934
+ via \`datahub.table(<id>)\`) is separate: it is default-deny and returns
1935
+ **403** with the dev key until you mint a key that carries an explicit
1936
+ Data Hub grant. Do that with:
1937
+
1938
+ \`\`\`
1939
+ # read a specific Data Hub table:
1940
+ dashwise api-keys create datahub-read --grant datahub:table=<tableId>:read
1941
+ # or a whole database (application):
1942
+ dashwise api-keys create datahub-rw --grant datahub:app=<appId>:read,create,update
1943
+ \`\`\`
1944
+
1945
+ Paste the printed key as \`DASHWISE_API_KEY\` (or, for local dev, set it
1946
+ before \`dashwise dev\` so it wins over the auto-injected own-tables key).
1947
+ So: \`db\`/\`qb\` on your own tables works out of the box; \`datahub\` needs a
1948
+ granted key.
1667
1949
 
1668
1950
  ## Auth in dev mode
1669
1951
 
1670
- \`dashwise dev\` registers a dev-mode SSO session with the backend
1671
- and injects \`DASHWISE_DEV_SESSION_ID\` so workspace members can sign
1672
- into the locally-running module via DashWise SSO \u2014 no installed
1673
- \`installation_id\` required.
1952
+ \`dashwise dev\` registers a dev-mode SSO session with the backend so
1953
+ workspace members can sign into the locally-running module via DashWise
1954
+ SSO. The scaffold's \`/api/auth/sign-in\` route is a single path: it
1955
+ calls \`POST /api/module-api/sso/start-url\` with your \`dwk_\` key and
1956
+ follows the returned \`redirect_url\` \u2014 the same code runs in dev and in
1957
+ production, with no installation id or dev-session id in the request.
1674
1958
 
1675
1959
  In Server Components: \`await getServerSession()\` (from
1676
1960
  \`@dashai/sdk/auth/server\`).
@@ -1727,13 +2011,14 @@ what tables it owns.
1727
2011
 
1728
2012
  | Table | Columns | Notes |
1729
2013
  |---|---|---|
1730
- | (none yet) | run \`dashwise table create <slug> --field <slug:type>\` to add one | |
2014
+ | \`items\` | \`title:text\`, \`done:boolean\` | seeded example \u2014 edit or replace; \`dashwise table create <slug> --field <slug:type>\` adds more |
1731
2015
 
1732
2016
  ## API / contract
1733
2017
 
1734
2018
  | Method | Path | Auth | Purpose |
1735
2019
  |---|---|---|---|
1736
- | (none yet) | | | |
2020
+ | GET | \`/api/items\` | dwk_ (server-side) | list \`items\` rows via \`db.items.list()\` |
2021
+ | POST | \`/api/items\` | dwk_ (server-side) | create an \`items\` row via \`db.items.create()\` |
1737
2022
 
1738
2023
  ## Business rules
1739
2024
 
@@ -1849,11 +2134,12 @@ primary_region = "iad"
1849
2134
  memory_mb = 256
1850
2135
  `;
1851
2136
  }
1852
- var DEPLOY_TARGETS, SCAFFOLD_DEFAULT_API_URL;
2137
+ var DEPLOY_TARGETS, SCAFFOLD_DEFAULT_API_URL, MODULE_SLUG_REGEX;
1853
2138
  var init_scaffold = __esm({
1854
2139
  "src/lib/scaffold.ts"() {
1855
2140
  DEPLOY_TARGETS = ["railway", "vercel", "fly"];
1856
2141
  SCAFFOLD_DEFAULT_API_URL = "http://localhost:3000";
2142
+ MODULE_SLUG_REGEX = /^[a-z]([a-z0-9-]*[a-z0-9])?$/;
1857
2143
  }
1858
2144
  });
1859
2145
  async function selectWorkspace(opts) {
@@ -1921,7 +2207,7 @@ async function moduleInitCommand(slugArg, options) {
1921
2207
  const answer = await text({
1922
2208
  message: "Module slug?",
1923
2209
  placeholder: "tasks",
1924
- validate: (v) => isValidSlug(v) ? void 0 : "Slug must match /^[a-z][a-z0-9_-]*$/"
2210
+ validate: (v) => isValidSlug(v) ? void 0 : slugValidationMessage(v)
1925
2211
  });
1926
2212
  if (isCancel(answer)) {
1927
2213
  fail("Aborted.");
@@ -1934,7 +2220,7 @@ async function moduleInitCommand(slugArg, options) {
1934
2220
  return 1;
1935
2221
  }
1936
2222
  if (!isValidSlug(slug)) {
1937
- fail(`Invalid slug ${code(slug)} \u2014 must match /^[a-z][a-z0-9_-]*$/`);
2223
+ fail(slugValidationMessage(slug));
1938
2224
  return 1;
1939
2225
  }
1940
2226
  const name = options.name ?? humanizeSlug(slug);
@@ -1982,7 +2268,11 @@ async function moduleInitCommand(slugArg, options) {
1982
2268
  // discipline + SDK pointers loaded into context from byte one.
1983
2269
  ["CLAUDE.md", claudeMd(scaffoldOpts)],
1984
2270
  ["spec.md", specMd(scaffoldOpts)],
1985
- ["agent-log.md", agentLogMd(scaffoldOpts)]
2271
+ ["agent-log.md", agentLogMd(scaffoldOpts)],
2272
+ // Example data-plane route (both flavors) — exercises the generated
2273
+ // SDK against the seeded `items` table. Ships for simple + custom
2274
+ // since both produce `@dashai/generated`.
2275
+ ["app/api/items/route.ts", appApiItemsRouteTs()]
1986
2276
  ];
1987
2277
  if (kind === "custom") {
1988
2278
  files.push(
@@ -2002,7 +2292,10 @@ async function moduleInitCommand(slugArg, options) {
2002
2292
  ["app/sign-in-error/page.tsx", appSignInErrorPageTsx()],
2003
2293
  ["app/api/auth/sign-in/route.ts", appApiAuthSignInTs()],
2004
2294
  ["app/api/auth/callback/route.ts", appApiAuthCallbackTs()],
2005
- ["app/api/auth/sign-out/route.ts", appApiAuthSignOutTs()]
2295
+ ["app/api/auth/sign-out/route.ts", appApiAuthSignOutTs()],
2296
+ // D8: same-origin session probe that <AuthProvider> hits so the
2297
+ // host-scoped session cookie is actually sent.
2298
+ ["app/api/auth/session/route.ts", appApiAuthSessionTs()]
2006
2299
  );
2007
2300
  } else {
2008
2301
  files.push(
@@ -2038,18 +2331,40 @@ async function moduleInitCommand(slugArg, options) {
2038
2331
  let devInstall = null;
2039
2332
  const wantsProvision = options.provision !== false;
2040
2333
  if (wantsProvision) {
2334
+ const manifest = (() => {
2335
+ try {
2336
+ return readManifest(dir) ?? void 0;
2337
+ } catch {
2338
+ return void 0;
2339
+ }
2340
+ })();
2041
2341
  devInstall = await tryProvisionDevInstall({
2042
2342
  slug,
2043
2343
  name,
2044
2344
  description: options.description,
2045
2345
  workspaceFlag: options.workspace,
2046
- moduleKind: kind
2346
+ moduleKind: kind,
2347
+ ...manifest !== void 0 ? { manifest } : {}
2047
2348
  });
2048
2349
  } else {
2049
2350
  info(
2050
2351
  `Skipping provisioning (${code("--no-provision")}). ${dim("Run with provisioning later to enable `dashwise dev` data-plane.")}`
2051
2352
  );
2052
2353
  }
2354
+ const keyCached = !wantsProvision || readModuleEntry(slug)?.apiKey ? true : false;
2355
+ if (wantsProvision && !keyCached) {
2356
+ fail(
2357
+ `NO API KEY WAS CACHED for ${code(slug)} \u2014 the scaffold is written, but ${code("dashwise dev")} will run with NO token (data-plane + sign-in will fail).`
2358
+ );
2359
+ info(
2360
+ `Fix it by re-running provisioning once the cause above is resolved:
2361
+ ${code(`dashwise init ${slug} --force --workspace <your-workspace-slug>`)}
2362
+ Confirm success: the line ${code("\u2713 Provisioned dev install id=\u2026 API key cached")} must appear,
2363
+ and ${code("cat ~/.config/dashwise/auth.json")} must show a non-empty ${code(`modules.${slug}.apiKey`)} starting with ${code("dwk_")}.`
2364
+ );
2365
+ outro("Scaffold written, but provisioning failed \u2014 see above.");
2366
+ return 2;
2367
+ }
2053
2368
  info(`Next steps:`);
2054
2369
  process.stdout.write(`
2055
2370
  ${dim("1.")} ${code(`cd ${dir}`)}
@@ -2061,7 +2376,7 @@ async function moduleInitCommand(slugArg, options) {
2061
2376
  ${dim(" dashwise module dev")} ${dim("# watch + auto-regenerate")}
2062
2377
  ${dim(" npm run dev:next # next dev on :3000)")}
2063
2378
  ${devInstall ? `
2064
- ${dim(`Dev installation id=${devInstall.installation.id}; runtime token expires ${devInstall.runtime_token_expires_at}.`)}
2379
+ ${dim(`Dev installation id=${devInstall.installation.id}; API key expires ${devInstall.api_key_expires_at}.`)}
2065
2380
  ` : ""}`);
2066
2381
  outro("Happy authoring.");
2067
2382
  return 0;
@@ -2078,6 +2393,20 @@ async function tryProvisionDevInstall(args) {
2078
2393
  warn("CLI config disappeared mid-init. Skipping provisioning.");
2079
2394
  return null;
2080
2395
  }
2396
+ const targetApiUrl = resolveApiUrl();
2397
+ info(`Backend: ${code(targetApiUrl)}`);
2398
+ const hostOf = (u) => {
2399
+ try {
2400
+ return new URL(u).host;
2401
+ } catch {
2402
+ return u;
2403
+ }
2404
+ };
2405
+ if (hostOf(targetApiUrl) !== hostOf(cfg.apiUrl)) {
2406
+ warn(
2407
+ `Your saved login is for ${code(cfg.apiUrl)} but this run targets ${code(targetApiUrl)}. Provisioning uses the saved token, which the target backend will reject (401). Log in against the target first: ${code(`dashwise login --api-url ${targetApiUrl}`)}.`
2408
+ );
2409
+ }
2081
2410
  let workspace2;
2082
2411
  try {
2083
2412
  workspace2 = await selectWorkspace({
@@ -2093,42 +2422,60 @@ async function tryProvisionDevInstall(args) {
2093
2422
  info(
2094
2423
  `Provisioning dev install in workspace ${code(workspace2.slug)} (id=${workspace2.id})\u2026`
2095
2424
  );
2425
+ const baseBody = {
2426
+ slug: args.slug,
2427
+ name: args.name,
2428
+ ...args.description !== void 0 ? { description: args.description } : {},
2429
+ workspaceId: workspace2.id,
2430
+ ...args.moduleKind !== void 0 ? { moduleKind: args.moduleKind } : {}
2431
+ };
2432
+ const reqOpts = { baseUrl: resolveApiUrl(), auth: { token: cfg.token } };
2096
2433
  let response;
2097
2434
  try {
2098
2435
  response = await createDevInstallation(
2099
- {
2100
- slug: args.slug,
2101
- name: args.name,
2102
- ...args.description !== void 0 ? { description: args.description } : {},
2103
- workspaceId: workspace2.id,
2104
- ...args.moduleKind !== void 0 ? { moduleKind: args.moduleKind } : {}
2105
- },
2106
- { baseUrl: resolveApiUrl(), auth: { token: cfg.token } }
2436
+ { ...baseBody, ...args.manifest !== void 0 ? { manifest: args.manifest } : {} },
2437
+ reqOpts
2107
2438
  );
2108
2439
  } catch (err) {
2109
- if (err instanceof ApiError) {
2440
+ if (err instanceof ApiError && err.code === "MANIFEST_INVALID" && args.manifest !== void 0) {
2441
+ warn(
2442
+ `Couldn't pre-materialize the seeded table(s) (${err.message}). Provisioning without them; add tables later with ${code("dashwise table create")}.`
2443
+ );
2444
+ try {
2445
+ response = await createDevInstallation(baseBody, reqOpts);
2446
+ } catch (err2) {
2447
+ if (err2 instanceof ApiError) {
2448
+ surfaceProvisionApiError(err2, args.slug);
2449
+ return null;
2450
+ }
2451
+ warn(`Provisioning failed: ${err2.message}`);
2452
+ return null;
2453
+ }
2454
+ } else if (err instanceof ApiError) {
2110
2455
  surfaceProvisionApiError(err, args.slug);
2111
2456
  return null;
2457
+ } else {
2458
+ warn(`Provisioning failed: ${err.message}`);
2459
+ return null;
2112
2460
  }
2113
- warn(`Provisioning failed: ${err.message}`);
2114
- return null;
2115
2461
  }
2116
2462
  try {
2117
2463
  upsertModuleEntry(args.slug, {
2118
- runtimeToken: response.runtime_token,
2464
+ apiKey: response.api_key,
2465
+ apiKeyId: response.api_key_id,
2119
2466
  installationId: response.installation.id,
2120
- expiresAt: response.runtime_token_expires_at
2467
+ expiresAt: response.api_key_expires_at
2121
2468
  });
2122
2469
  } catch (err) {
2123
2470
  warn(
2124
- `Provisioned successfully (installation id=${response.installation.id}) but failed to cache the runtime token: ${err.message}
2125
- Runtime token (save this manually): ${response.runtime_token}
2126
- Token expires: ${response.runtime_token_expires_at}`
2471
+ `Provisioned successfully (installation id=${response.installation.id}) but failed to cache the API key: ${err.message}
2472
+ API key (save this manually): ${response.api_key}
2473
+ Key expires: ${response.api_key_expires_at}`
2127
2474
  );
2128
2475
  return response;
2129
2476
  }
2130
2477
  success(
2131
- `Provisioned dev install id=${response.installation.id}. Runtime token cached in your CLI config.`
2478
+ `Provisioned dev install id=${response.installation.id}. API key cached in your CLI config.` + (response.tables_applied > 0 ? ` ${response.tables_applied} seeded table(s) materialized.` : "")
2132
2479
  );
2133
2480
  return response;
2134
2481
  }
@@ -2173,6 +2520,7 @@ var init_init = __esm({
2173
2520
  init_output();
2174
2521
  init_api_client();
2175
2522
  init_config();
2523
+ init_manifest_io();
2176
2524
  init_workspaces();
2177
2525
  }
2178
2526
  });
@@ -2197,6 +2545,7 @@ function loadSchemaCommandContext(opts) {
2197
2545
  `No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
2198
2546
  );
2199
2547
  }
2548
+ const token = resolveSchemaAuthToken(entry, slug);
2200
2549
  return {
2201
2550
  projectRoot,
2202
2551
  manifestPath: manifestPath2,
@@ -2207,10 +2556,16 @@ function loadSchemaCommandContext(opts) {
2207
2556
  entry,
2208
2557
  requestOptions: {
2209
2558
  baseUrl: resolveApiUrl(),
2210
- auth: { token: entry.runtimeToken }
2559
+ auth: { token }
2211
2560
  }
2212
2561
  };
2213
2562
  }
2563
+ function resolveSchemaAuthToken(entry, slug) {
2564
+ if (entry.apiKey) return entry.apiKey;
2565
+ throw new Error(
2566
+ `The cached credential for "${slug}" is a legacy runtime token, which the schema endpoints no longer accept. Run \`dashwise dev\` once (it mints a dwk_ API key and updates your config), or \`dashwise init ${slug} --force\` to re-provision.`
2567
+ );
2568
+ }
2214
2569
  var init_schema_command_context = __esm({
2215
2570
  "src/lib/schema-command-context.ts"() {
2216
2571
  init_config();
@@ -2549,7 +2904,6 @@ async function tableCreateCommand(slug, options) {
2549
2904
  info(`Creating table ${code(slug)} in installation ${ctx.entry.installationId}\u2026`);
2550
2905
  try {
2551
2906
  const result = await createTable(
2552
- ctx.entry.installationId,
2553
2907
  { slug, fields },
2554
2908
  ctx.requestOptions
2555
2909
  );
@@ -2563,10 +2917,7 @@ async function tableCreateCommand(slug, options) {
2563
2917
  }
2564
2918
  async function syncLocalManifest(ctx) {
2565
2919
  try {
2566
- const { manifest } = await getInstallationSchema(
2567
- ctx.entry.installationId,
2568
- ctx.requestOptions
2569
- );
2920
+ const { manifest } = await getInstallationSchema(ctx.requestOptions);
2570
2921
  writeManifest(manifest, ctx.projectRoot);
2571
2922
  return 0;
2572
2923
  } catch (err) {
@@ -2599,17 +2950,17 @@ function surfaceFailure(err) {
2599
2950
  break;
2600
2951
  case "UNAUTHENTICATED":
2601
2952
  fail(
2602
- `Runtime token rejected by backend. The cached token may be expired \u2014 re-run \`dashwise init <slug> --force\` to mint a fresh one.`
2953
+ `API key rejected by backend. The cached key may be expired or revoked \u2014 re-run \`dashwise init <slug> --force\` to mint a fresh one.`
2603
2954
  );
2604
2955
  break;
2605
- case "SANDBOX_TOKEN_NOT_ACCEPTED":
2956
+ case "SCHEMA_LOCKED":
2606
2957
  fail(
2607
- `Schema-mutation endpoints require a local-dev runtime token (kind=local). The cached token is the wrong kind \u2014 re-run \`dashwise init <slug> --force\`.`
2958
+ `Schema mutations require a local-dev install (kind=local). The cached API key belongs to a non-local install \u2014 schema is locked. Run \`dashwise init <slug> --force\` for a local dev install.`
2608
2959
  );
2609
2960
  break;
2610
2961
  case "INSTALL_MISMATCH":
2611
2962
  fail(
2612
- `Runtime token is bound to a different installation than the URL. The cached install id may be stale \u2014 re-run \`dashwise init <slug> --force\`.`
2963
+ `API key is bound to a different installation than the URL. The cached install id may be stale \u2014 re-run \`dashwise init <slug> --force\`.`
2613
2964
  );
2614
2965
  break;
2615
2966
  default:
@@ -2651,11 +3002,7 @@ async function tableDeleteCommand(slug, options) {
2651
3002
  }
2652
3003
  info(`Dropping table ${code(slug)} from installation ${ctx.entry.installationId}\u2026`);
2653
3004
  try {
2654
- const result = await dropTable(
2655
- ctx.entry.installationId,
2656
- slug,
2657
- ctx.requestOptions
2658
- );
3005
+ const result = await dropTable(slug, ctx.requestOptions);
2659
3006
  success(
2660
3007
  `Dropped table ${code(slug)} (manifest_version=${result.manifest_version}). Physical table soft-deleted; recoverable for 30 days via the dashboard's trash.`
2661
3008
  );
@@ -2723,7 +3070,6 @@ async function fieldAddCommand(tableSlug, fieldSpec, options) {
2723
3070
  );
2724
3071
  try {
2725
3072
  const result = await addField(
2726
- ctx.entry.installationId,
2727
3073
  { table_slug: tableSlug, field },
2728
3074
  ctx.requestOptions
2729
3075
  );
@@ -2769,7 +3115,6 @@ async function fieldRemoveCommand(tableSlug, fieldSlug, options) {
2769
3115
  );
2770
3116
  try {
2771
3117
  const result = await removeField(
2772
- ctx.entry.installationId,
2773
3118
  tableSlug,
2774
3119
  fieldSlug,
2775
3120
  ctx.requestOptions
@@ -2823,7 +3168,6 @@ async function fieldSetTypeCommand(tableSlug, fieldSlug, newType, options) {
2823
3168
  );
2824
3169
  try {
2825
3170
  const result = await updateField(
2826
- ctx.entry.installationId,
2827
3171
  tableSlug,
2828
3172
  fieldSlug,
2829
3173
  { type: newType },
@@ -2849,14 +3193,84 @@ var init_set_type = __esm({
2849
3193
 
2850
3194
  // src/commands/dev.ts
2851
3195
  init_api_client();
2852
- init_config();
2853
- init_manifest_io();
2854
3196
 
2855
- // src/lib/manifest-poll.ts
3197
+ // src/lib/connect-status.ts
2856
3198
  init_api_client();
2857
- function startManifestPollLoop(opts) {
3199
+ init_output();
3200
+ async function reportConnectStatus(opts) {
3201
+ const [deps, bindings] = await Promise.all([
3202
+ safeGet(
3203
+ () => getInstallationDependencies(opts.installationId, opts.requestOptions)
3204
+ ),
3205
+ safeGet(
3206
+ () => getInstallationBindings(opts.installationId, opts.requestOptions)
3207
+ )
3208
+ ]);
3209
+ if (deps === null && bindings === null) return;
3210
+ const depList = deps ?? [];
3211
+ const bindList = bindings ?? [];
3212
+ if (depList.length === 0 && bindList.length === 0) return;
3213
+ const unboundDeps = depList.filter((d) => d.status === "unbound").length;
3214
+ const unboundUses = bindList.filter((b) => b.status === "unbound").length;
3215
+ info(
3216
+ `Connect status for ${code(opts.moduleSlug)} ${dim(
3217
+ `(${depList.length} dep${depList.length === 1 ? "" : "s"}, ${bindList.length} uses slot${bindList.length === 1 ? "" : "s"})`
3218
+ )}:`
3219
+ );
3220
+ for (const d of depList) renderDep(d);
3221
+ for (const b of bindList) renderBinding(b);
3222
+ const hints = [];
3223
+ if (unboundDeps > 0) {
3224
+ hints.push(
3225
+ `${code("dashwise deps bind <provider-slug>")} once the provider is installed`
3226
+ );
3227
+ }
3228
+ if (unboundUses > 0) {
3229
+ hints.push(`${code("dashwise bind <uses-slug> --table <name>")} to connect a table`);
3230
+ }
3231
+ if (hints.length > 0) {
3232
+ info(`Connect parked items with ${hints.join("; ")}.`);
3233
+ }
3234
+ out("");
3235
+ }
3236
+ function renderDep(d) {
3237
+ if (d.status === "resolved") {
3238
+ const providerLine = d.provider ? ` ${dim(`\u2192 install #${d.provider.installation_id} @ v${d.provider.version}`)}` : "";
3239
+ out(` ${pc.green("\u25CF resolved")} ${dim("dep")} ${code(d.slug)}@${d.range ?? "*"}${providerLine}`);
3240
+ } else {
3241
+ out(
3242
+ ` ${pc.yellow("\u25CB unbound")} ${dim("dep")} ${code(d.slug)}@${d.range ?? "*"} ${dim(`reason: ${d.reason ?? "(unspecified)"}`)}`
3243
+ );
3244
+ }
3245
+ }
3246
+ function renderBinding(b) {
3247
+ if (b.status === "bound") {
3248
+ const tableLine = b.table ? ` ${dim(`\u2192 ${b.table.name} (id ${b.table.id})`)}` : "";
3249
+ out(` ${pc.green("\u25CF bound")} ${dim("uses")} ${code(b.uses_slug)}${tableLine}`);
3250
+ } else if (b.status === "declined") {
3251
+ out(` ${pc.dim("\u2298 declined")} ${dim("uses")} ${code(b.uses_slug)}`);
3252
+ } else {
3253
+ out(
3254
+ ` ${pc.yellow("\u25CB unbound")} ${dim("uses")} ${code(b.uses_slug)} ${dim(`reason: ${b.reason ?? "(unspecified)"}`)}`
3255
+ );
3256
+ }
3257
+ }
3258
+ async function safeGet(fn) {
3259
+ try {
3260
+ return await fn();
3261
+ } catch (err) {
3262
+ return null;
3263
+ }
3264
+ }
3265
+
3266
+ // src/commands/dev.ts
3267
+ init_config();
3268
+ init_manifest_io();
3269
+
3270
+ // src/lib/manifest-poll.ts
3271
+ init_api_client();
3272
+ function startManifestPollLoop(opts) {
2858
3273
  const {
2859
- installationId,
2860
3274
  manifestPath: manifestPath2,
2861
3275
  requestOptions,
2862
3276
  intervalMs,
@@ -2869,10 +3283,7 @@ function startManifestPollLoop(opts) {
2869
3283
  const tick = async () => {
2870
3284
  if (stopped) return;
2871
3285
  try {
2872
- const { manifest } = await getInstallationSchema(
2873
- installationId,
2874
- requestOptions
2875
- );
3286
+ const { manifest } = await getInstallationSchema(requestOptions);
2876
3287
  const remoteJson = JSON.stringify(manifest, null, 2) + "\n";
2877
3288
  const currentLocal = existsSync(manifestPath2) ? readFileSync(manifestPath2, "utf-8") : "";
2878
3289
  if (remoteJson === currentLocal) {
@@ -2967,16 +3378,15 @@ async function devCommand(options) {
2967
3378
  }
2968
3379
  const explicitInstallationId = options.installationId ?? process.env.DASHWISE_INSTALLATION_ID;
2969
3380
  const reuseDevSessionId = process.env.DASHWISE_DEV_SESSION_ID;
2970
- const moduleEntry = manifestSlug !== void 0 ? readModuleEntry(manifestSlug) : null;
2971
- const runtimeTokenEnv = process.env.DASHWISE_RUNTIME_TOKEN ?? moduleEntry?.runtimeToken;
3381
+ let moduleEntry = manifestSlug !== void 0 ? readModuleEntry(manifestSlug) : null;
3382
+ const healed = manifestSlug !== void 0 ? await resolveModuleApiKey(manifestSlug, moduleEntry, apiUrl, apiToken) : null;
3383
+ if (healed) moduleEntry = healed;
3384
+ const apiKeyEnv = process.env.DASHWISE_API_KEY ?? moduleEntry?.apiKey;
2972
3385
  let installationIdEnv;
2973
- let devSessionIdEnv;
2974
3386
  let revokeDevSession;
2975
3387
  if (explicitInstallationId && explicitInstallationId !== "local") {
2976
3388
  installationIdEnv = explicitInstallationId;
2977
- } else if (reuseDevSessionId) {
2978
- devSessionIdEnv = reuseDevSessionId;
2979
- } else {
3389
+ } else if (reuseDevSessionId) ; else {
2980
3390
  let workspaceContext;
2981
3391
  try {
2982
3392
  workspaceContext = await resolveTargetWorkspace(apiUrl, apiToken, options.workspace);
@@ -2990,7 +3400,6 @@ async function devCommand(options) {
2990
3400
  target_workspace_id: workspaceContext.id,
2991
3401
  callback_origins: callbackOrigins
2992
3402
  });
2993
- devSessionIdEnv = session.id;
2994
3403
  revokeDevSession = () => revokeDevSessionBestEffort(apiUrl, apiToken, session.id);
2995
3404
  info(` ${dim("dev session")} ${session.id}`);
2996
3405
  info(` ${dim("workspace")} ${workspaceContext.name} (id=${workspaceContext.id}, slug=${workspaceContext.slug})`);
@@ -3005,43 +3414,44 @@ async function devCommand(options) {
3005
3414
  return 1;
3006
3415
  }
3007
3416
  }
3008
- const childInstallationId = installationIdEnv ?? (runtimeTokenEnv !== void 0 ? "sandbox" : "");
3009
3417
  const childEnv = {
3010
3418
  ...process.env,
3011
3419
  DASHWISE_API_URL: apiUrl,
3012
- // HH-CC-FUP-3: client-side mirror. Next.js inlines NEXT_PUBLIC_*
3013
- // env vars into the browser bundle at build time (or, in dev, at
3014
- // process-start time). Without this, the SDK's <AuthProvider> hits
3015
- // `/api/auth/sessions/me` on its own origin (localhost:<port>),
3016
- // 404s, and stays unauthenticated. We mirror DASHWISE_API_URL so
3017
- // the client + server agree on the backend origin without the
3018
- // module author having to touch .env.local.
3019
3420
  NEXT_PUBLIC_DASHWISE_API_URL: apiUrl,
3020
- DASHWISE_API_TOKEN: apiToken,
3021
- DASHWISE_INSTALLATION_ID: childInstallationId,
3022
- ...devSessionIdEnv !== void 0 ? { DASHWISE_DEV_SESSION_ID: devSessionIdEnv } : {},
3023
- ...runtimeTokenEnv !== void 0 ? { DASHWISE_RUNTIME_TOKEN: runtimeTokenEnv } : {}
3421
+ ...apiKeyEnv !== void 0 ? { DASHWISE_API_KEY: apiKeyEnv } : {}
3024
3422
  };
3025
3423
  info(`Dev loop starting \u2014 Ctrl-C to stop.`);
3026
3424
  info(` ${dim("api")} ${apiUrl}`);
3027
3425
  if (installationIdEnv !== void 0) {
3028
3426
  info(` ${dim("installation")} ${installationIdEnv} (prod-mode SSO)`);
3029
3427
  } else {
3030
- info(` ${dim("mode")} dev (DASHWISE_DEV_SESSION_ID injected)`);
3428
+ info(` ${dim("mode")} dev (dev SSO session registered)`);
3031
3429
  }
3032
- info(` ${dim("token")} ${maskedToken(apiToken)} ${dim(`(from ${apiToken === savedConfig?.token ? "~/.config/dashwise/auth.json" : "$DASHWISE_API_TOKEN"})`)}`);
3033
- if (runtimeTokenEnv !== void 0) {
3034
- const source = process.env.DASHWISE_RUNTIME_TOKEN === runtimeTokenEnv ? "$DASHWISE_RUNTIME_TOKEN" : `~/.config/dashwise/auth.json#modules[${manifestSlug}]`;
3430
+ info(` ${dim("cli token")} ${maskedToken(apiToken)} ${dim(`(from ${apiToken === savedConfig?.token ? "~/.config/dashwise/auth.json" : "$DASHWISE_API_TOKEN"})`)}`);
3431
+ if (apiKeyEnv !== void 0) {
3432
+ const source = process.env.DASHWISE_API_KEY === apiKeyEnv ? "$DASHWISE_API_KEY" : `~/.config/dashwise/auth.json#modules[${manifestSlug}]`;
3035
3433
  const installId = moduleEntry?.installationId;
3036
3434
  info(
3037
- ` ${dim("runtime")} ${maskedToken(runtimeTokenEnv)} ${dim(`(from ${source}${installId ? `, install id=${installId}` : ""})`)}`
3435
+ ` ${dim("api key")} ${maskedToken(apiKeyEnv)} ${dim(`(from ${source}${installId ? `, install id=${installId}` : ""})`)}`
3038
3436
  );
3039
3437
  } else if (manifestSlug !== void 0 && installationIdEnv === void 0) {
3040
- warn(
3041
- `No runtime token cached for module ${code(manifestSlug)}. Data-plane calls (db, qb) will fail with UnauthenticatedError. Run ${code(`dashwise init ${manifestSlug} --force`)} to provision a dev install + cache the token.`
3438
+ fail(
3439
+ `No ${code("dwk_")} API key for module ${code(manifestSlug)} \u2014 the module will run with NO token.`
3440
+ );
3441
+ info(
3442
+ ` Data-plane calls (db, qb) AND sign-in will fail until you provision a dev install:
3443
+ ${code(`dashwise init ${manifestSlug} --force --workspace <your-workspace-slug>`)}
3444
+ (Ensure you're logged into the right backend: ${code(`dashwise login --api-url ${apiUrl}`)}.)`
3042
3445
  );
3043
3446
  }
3044
3447
  info("");
3448
+ if (moduleEntry !== null && manifestSlug !== void 0) {
3449
+ await reportConnectStatus({
3450
+ installationId: moduleEntry.installationId,
3451
+ moduleSlug: manifestSlug,
3452
+ requestOptions: { baseUrl: apiUrl, auth: { token: apiToken } }
3453
+ });
3454
+ }
3045
3455
  const children = [];
3046
3456
  if (!options.nextOnly) {
3047
3457
  const sdkChild = spawnSdkWatcher(projectRoot, childEnv);
@@ -3056,18 +3466,19 @@ async function devCommand(options) {
3056
3466
  pipeWithPrefix(child.stderr, process.stderr, prefix);
3057
3467
  }
3058
3468
  let manifestPoll = null;
3059
- const enablePoll = !options.noRemotePoll && moduleEntry !== null && manifestSlug !== void 0;
3060
- if (enablePoll) {
3469
+ const pollEntry = moduleEntry;
3470
+ const enablePoll = !options.noRemotePoll && pollEntry !== null && apiKeyEnv !== void 0 && manifestSlug !== void 0;
3471
+ if (enablePoll && pollEntry !== null) {
3061
3472
  const intervalSec = Math.max(
3062
3473
  MIN_REMOTE_POLL_INTERVAL_SEC,
3063
3474
  options.remotePollIntervalSec ?? DEFAULT_REMOTE_POLL_INTERVAL_SEC
3064
3475
  );
3065
3476
  manifestPoll = startManifestPollLoop({
3066
- installationId: moduleEntry.installationId,
3067
3477
  manifestPath: manifestPath2,
3068
3478
  requestOptions: {
3069
3479
  baseUrl: apiUrl,
3070
- auth: { token: moduleEntry.runtimeToken }
3480
+ // The `dwk_` key resolves the install server-side.
3481
+ auth: { token: apiKeyEnv }
3071
3482
  },
3072
3483
  intervalMs: intervalSec * 1e3,
3073
3484
  onUpdate: (msg) => {
@@ -3200,6 +3611,51 @@ function maskedToken(token) {
3200
3611
  const tail = token.slice(-4);
3201
3612
  return `${prefix}\u2026${tail}`;
3202
3613
  }
3614
+ async function resolveModuleApiKey(slug, entry, apiUrl, cliBearer) {
3615
+ if (process.env.DASHWISE_API_KEY) return null;
3616
+ if (!entry) return null;
3617
+ if (entry.apiKey) return null;
3618
+ if (!entry.installationId) return null;
3619
+ info(
3620
+ `No usable ${code("dwk_")} API key cached for ${code(slug)} \u2014 minting a fresh one against dev install ${entry.installationId} (a stale CLI can cache an empty key)\u2026`
3621
+ );
3622
+ let minted;
3623
+ try {
3624
+ minted = await createApiKey(
3625
+ entry.installationId,
3626
+ { name: "local-dev" },
3627
+ { baseUrl: apiUrl, auth: { token: cliBearer } }
3628
+ );
3629
+ } catch (err) {
3630
+ if (err instanceof ApiError && err.code === "INSTALLATION_NOT_FOUND") {
3631
+ warn(
3632
+ `Could not upgrade credentials for ${code(slug)}: its dev install no longer exists. Run ${code(`dashwise init ${slug} --force`)} to re-provision (a clean re-register).`
3633
+ );
3634
+ return null;
3635
+ }
3636
+ const reason = err instanceof ApiError ? `${err.code}: ${err.message}` : err.message;
3637
+ warn(
3638
+ `Could not upgrade credentials for ${code(slug)} (${reason}). Data-plane calls may fail; run ${code(`dashwise init ${slug} --force`)} to refresh.`
3639
+ );
3640
+ return null;
3641
+ }
3642
+ const upgraded = {
3643
+ apiKey: minted.raw_key,
3644
+ apiKeyId: minted.api_key.id,
3645
+ installationId: entry.installationId,
3646
+ expiresAt: minted.api_key.expires_at ?? entry.expiresAt
3647
+ };
3648
+ try {
3649
+ upsertModuleEntry(slug, upgraded);
3650
+ } catch (err) {
3651
+ warn(
3652
+ `Minted a new API key for ${code(slug)} but failed to cache it: ${err.message}
3653
+ API key (save manually): ${minted.raw_key}`
3654
+ );
3655
+ }
3656
+ success(`Upgraded ${code(slug)} to a dwk_ API key (id=${minted.api_key.id}).`);
3657
+ return upgraded;
3658
+ }
3203
3659
  function computeCallbackOrigins(port, extra) {
3204
3660
  const def = `http://localhost:${port}`;
3205
3661
  return Array.from(/* @__PURE__ */ new Set([def, ...extra]));
@@ -3887,7 +4343,7 @@ function isPlausibleRepoUrl(url) {
3887
4343
 
3888
4344
  // src/commands/module/convert.ts
3889
4345
  init_scaffold();
3890
- var SDK_DEP_RANGE = "^0.8.0";
4346
+ var SDK_DEP_RANGE = "^2.0.0";
3891
4347
  var SCAN_DIRS = ["app", "src", "pages", "lib", "components"];
3892
4348
  var SCAN_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs"];
3893
4349
  var MAX_SCAN_FILES = 2e3;
@@ -3943,6 +4399,7 @@ function convertModule(opts) {
3943
4399
  writeIfMissing("app/api/auth/sign-in/route.ts", appApiAuthSignInTs());
3944
4400
  writeIfMissing("app/api/auth/callback/route.ts", appApiAuthCallbackTs());
3945
4401
  writeIfMissing("app/api/auth/sign-out/route.ts", appApiAuthSignOutTs());
4402
+ writeIfMissing("app/api/auth/session/route.ts", appApiAuthSessionTs());
3946
4403
  writeIfMissing("app/sign-in-error/page.tsx", appSignInErrorPageTsx());
3947
4404
  writeIfMissing(".env.local.example", envLocalExample());
3948
4405
  const layoutFile = [
@@ -3954,7 +4411,7 @@ function convertModule(opts) {
3954
4411
  writeIfMissing("app/layout.tsx", appLayoutCustomTsx(sopts));
3955
4412
  } else if (!readFileSync(join(dir, layoutFile), "utf-8").includes("AuthProvider")) {
3956
4413
  warnings.push(
3957
- `${layoutFile}: wrap your tree in <AuthProvider> from "@dashai/sdk/auth/client" so client components can read the session.`
4414
+ `${layoutFile}: wrap your tree in <AuthProvider sessionEndpoint="/api/auth/session"> from "@dashai/sdk/auth/client" so client components can read the session (the same-origin session route was scaffolded for you).`
3958
4415
  );
3959
4416
  } else {
3960
4417
  skipped.push(layoutFile);
@@ -4236,6 +4693,7 @@ async function moduleExportCommand(opts) {
4236
4693
  init_config();
4237
4694
  init_output();
4238
4695
  init_manifest_io();
4696
+ init_scaffold();
4239
4697
  init_workspaces();
4240
4698
  init_api_client();
4241
4699
  var MODULE_KINDS = ["ai_builder", "hand_authored", "custom"];
@@ -4258,6 +4716,13 @@ async function moduleRegisterCommand(opts) {
4258
4716
  fail("module.json is missing `module.slug`.");
4259
4717
  return 1;
4260
4718
  }
4719
+ if (!isValidSlug(slug)) {
4720
+ fail(
4721
+ `${slugValidationMessage(slug)}
4722
+ Fix ${code("module.slug")} in module.json, then re-run ${code("dashwise module register")}.`
4723
+ );
4724
+ return 1;
4725
+ }
4261
4726
  const name = opts.name ?? (typeof manifest.module?.name === "string" ? manifest.module.name : slug);
4262
4727
  const moduleKind = MODULE_KINDS.includes(
4263
4728
  manifest.module_kind
@@ -4295,7 +4760,12 @@ async function moduleRegisterCommand(opts) {
4295
4760
  name,
4296
4761
  ...description !== void 0 ? { description } : {},
4297
4762
  workspaceId: workspace2.id,
4298
- ...moduleKind !== void 0 ? { moduleKind } : {}
4763
+ ...moduleKind !== void 0 ? { moduleKind } : {},
4764
+ // P1-C C2: push the whole local manifest so the backend
4765
+ // materializes owns.tables into the fresh draft. `init` omits
4766
+ // this (scaffolds empty); `register` sends it because the local
4767
+ // module.json is the source of truth for an already-authored app.
4768
+ manifest
4299
4769
  },
4300
4770
  { baseUrl: resolveApiUrl(), auth: { token: cfg.token } }
4301
4771
  );
@@ -4307,6 +4777,13 @@ async function moduleRegisterCommand(opts) {
4307
4777
  );
4308
4778
  } else if (err.code === "FORBIDDEN") {
4309
4779
  fail(`Not a member of workspace "${workspace2.slug}". Join it, then retry.`);
4780
+ } else if (err.code === "MANIFEST_INVALID") {
4781
+ const issues = extractManifestIssues(err);
4782
+ fail(
4783
+ `Your module.json failed validation:
4784
+ ${issues}
4785
+ Fix the issues above (or run ${code("dashwise module validate")} for the full report) and retry.`
4786
+ );
4310
4787
  } else {
4311
4788
  fail(`Registration failed (${err.code || err.status}): ${err.message}`);
4312
4789
  }
@@ -4317,26 +4794,49 @@ async function moduleRegisterCommand(opts) {
4317
4794
  }
4318
4795
  try {
4319
4796
  upsertModuleEntry(slug, {
4320
- runtimeToken: response.runtime_token,
4797
+ apiKey: response.api_key,
4798
+ apiKeyId: response.api_key_id,
4321
4799
  installationId: response.installation.id,
4322
- expiresAt: response.runtime_token_expires_at
4800
+ expiresAt: response.api_key_expires_at
4323
4801
  });
4324
4802
  } catch (err) {
4325
4803
  warn(
4326
- `Registered (installation id=${response.installation.id}) but failed to cache the runtime token: ${err.message}
4327
- Runtime token (save this manually): ${response.runtime_token}
4328
- Expires: ${response.runtime_token_expires_at}`
4804
+ `Registered (installation id=${response.installation.id}) but failed to cache the API key: ${err.message}
4805
+ API key (save this manually): ${response.api_key}
4806
+ Expires: ${response.api_key_expires_at}`
4329
4807
  );
4330
4808
  return 0;
4331
4809
  }
4810
+ const tablesApplied = response.tables_applied ?? 0;
4332
4811
  success(
4333
- `Registered "${name}" \u2192 module #${response.module.id}, dev install #${response.installation.id}. Runtime token cached.`
4812
+ `Registered "${name}" \u2192 module #${response.module.id}, dev install #${response.installation.id}. API key cached.`
4334
4813
  );
4814
+ if (tablesApplied > 0) {
4815
+ info(
4816
+ `Materialized ${code(String(tablesApplied))} ${tablesApplied === 1 ? "table" : "tables"} from your module.json into the dev install.`
4817
+ );
4818
+ }
4335
4819
  info(
4336
- `Next: ${code("dashwise dev")} to run it against DashWise, or ${code("dashwise module publish")} to deploy. Add tables with ${code("dashwise table create")}.`
4820
+ `Next: ${code("dashwise dev")} to run it against DashWise, or ${code("dashwise module publish")} to deploy.${tablesApplied === 0 ? ` Add tables with ${code("dashwise table create")}.` : ""}`
4337
4821
  );
4338
4822
  return 0;
4339
4823
  }
4824
+ function extractManifestIssues(err) {
4825
+ const issues = err.context?.issues;
4826
+ if (Array.isArray(issues) && issues.length > 0) {
4827
+ return issues.map((issue) => {
4828
+ if (typeof issue === "string") return ` - ${issue}`;
4829
+ if (issue && typeof issue === "object") {
4830
+ const o = issue;
4831
+ const path = typeof o.path === "string" ? `${o.path}: ` : "";
4832
+ const message = typeof o.message === "string" ? o.message : JSON.stringify(issue);
4833
+ return ` - ${path}${message}`;
4834
+ }
4835
+ return ` - ${String(issue)}`;
4836
+ }).join("\n");
4837
+ }
4838
+ return ` - ${err.message}`;
4839
+ }
4340
4840
 
4341
4841
  // src/commands/module/generate.ts
4342
4842
  init_api_client();
@@ -5174,6 +5674,7 @@ function readIssues(context) {
5174
5674
 
5175
5675
  // src/commands/deps/add.ts
5176
5676
  init_api_client();
5677
+ init_config();
5177
5678
  init_manifest_io();
5178
5679
  init_output();
5179
5680
  var MIN_PURPOSE_LEN = 10;
@@ -5309,9 +5810,59 @@ async function depsAddCommand(providerSlug, options) {
5309
5810
  );
5310
5811
  info(`Reads: ${entry.reads.map((r) => `${code(r.table)} (${r.fields.join(", ")})`).join("; ")}`);
5311
5812
  info(`Run ${code("dashwise module generate")} to refresh typed wrappers.`);
5813
+ await reportConnectStatus2(manifest.module?.slug, providerSlug);
5312
5814
  if (isInteractive) outro("Done.");
5313
5815
  return 0;
5314
5816
  }
5817
+ async function reportConnectStatus2(consumerSlug, providerSlug) {
5818
+ if (!consumerSlug) return;
5819
+ let cfg;
5820
+ let entry;
5821
+ try {
5822
+ cfg = readConfig();
5823
+ entry = readModuleEntry(consumerSlug);
5824
+ } catch {
5825
+ cfg = null;
5826
+ entry = null;
5827
+ }
5828
+ if (!cfg || !entry) {
5829
+ info(
5830
+ `Once provisioned (${code("dashwise init")}) + pushed (${code("dashwise dev")}), run ${code("dashwise deps")} to see whether ${code(providerSlug)} resolves or parks.`
5831
+ );
5832
+ return;
5833
+ }
5834
+ const requestOptions = {
5835
+ baseUrl: resolveApiUrl(),
5836
+ auth: { token: cfg.token }
5837
+ };
5838
+ try {
5839
+ const deps = await getInstallationDependencies(entry.installationId, requestOptions);
5840
+ const match = deps.find((d) => d.slug === providerSlug);
5841
+ if (!match) {
5842
+ info(
5843
+ `${code(providerSlug)} isn't reconciled server-side yet. Push the manifest (${code("dashwise dev")} watches + applies it) \u2014 then ${code("dashwise deps")} shows its resolve/park status.`
5844
+ );
5845
+ return;
5846
+ }
5847
+ if (match.status === "resolved") {
5848
+ const providerLine = match.provider != null ? ` (${dim(`provider install #${match.provider.installation_id} @ v${match.provider.version}`)})` : "";
5849
+ out(` ${pc.green("\u25CF resolved")}${providerLine}`);
5850
+ } else {
5851
+ out(` ${pc.yellow("\u25CB unbound")} ${dim("reason:")} ${match.reason ?? dim("(unspecified)")}`);
5852
+ info(
5853
+ `Declared but not connected. Install the provider in this workspace, then ${code(`dashwise deps bind ${providerSlug}`)} \u2014 reads return ${code("DEP_UNBOUND")} until it's bound.`
5854
+ );
5855
+ }
5856
+ } catch (err) {
5857
+ if (err instanceof ApiError && err.status === 401) {
5858
+ info(`(Skipped connect-status check \u2014 CLI token stale; run ${code("dashwise login")} to refresh.)`);
5859
+ } else {
5860
+ info(
5861
+ `(Couldn't fetch connect status right now \u2014 run ${code("dashwise deps")} later to see whether ${code(providerSlug)} resolves or parks.)`
5862
+ );
5863
+ }
5864
+ }
5865
+ }
5315
5866
  async function collectInteractive(providerSlug, summary, resolvedVersion, options) {
5316
5867
  const reads = [];
5317
5868
  if (summary && summary.exposes && summary.exposes.length > 0) {
@@ -5730,96 +6281,7 @@ async function depsRemoveCommand(providerSlug, options) {
5730
6281
  return 0;
5731
6282
  }
5732
6283
 
5733
- // src/commands/pull.ts
5734
- init_api_client();
5735
- init_manifest_io();
5736
- init_schema_command_context();
5737
- init_output();
5738
- async function pullCommand(options) {
5739
- let ctx;
5740
- try {
5741
- ctx = loadSchemaCommandContext({
5742
- ...options.dir !== void 0 ? { dir: options.dir } : {}
5743
- });
5744
- } catch (err) {
5745
- fail(err.message);
5746
- return 1;
5747
- }
5748
- info(`Pulling manifest for installation ${ctx.entry.installationId}\u2026`);
5749
- let remoteManifest;
5750
- try {
5751
- const response = await getInstallationSchema(
5752
- ctx.entry.installationId,
5753
- ctx.requestOptions
5754
- );
5755
- remoteManifest = response.manifest;
5756
- } catch (err) {
5757
- return surfacePullFailure(err);
5758
- }
5759
- const localJson = JSON.stringify(ctx.manifest, null, 2) + "\n";
5760
- const remoteJson = JSON.stringify(remoteManifest, null, 2) + "\n";
5761
- if (localJson === remoteJson) {
5762
- success("Already in sync \u2014 local module.json matches backend.");
5763
- return 0;
5764
- }
5765
- const localBytes = Buffer.byteLength(localJson, "utf8");
5766
- const remoteBytes = Buffer.byteLength(remoteJson, "utf8");
5767
- const delta = remoteBytes - localBytes;
5768
- const deltaStr = delta === 0 ? "0 bytes" : `${delta > 0 ? "+" : ""}${delta} bytes`;
5769
- if (options.dryRun) {
5770
- info(
5771
- `Drift detected (${deltaStr}). ${code("--dry-run")} \u2014 not writing.`
5772
- );
5773
- info(`Run ${code("dashwise pull")} without ${code("--dry-run")} to apply.`);
5774
- return 0;
5775
- }
5776
- try {
5777
- writeManifest(remoteManifest, ctx.projectRoot);
5778
- } catch (err) {
5779
- fail(`Failed to write ${manifestPath(ctx.projectRoot)}: ${err.message}`);
5780
- return 1;
5781
- }
5782
- success(
5783
- `Synced module.json from backend (${deltaStr}). ${dim(`Run \`dashwise module generate\` to refresh typed SDK if your watcher isn't running.`)}`
5784
- );
5785
- return 0;
5786
- }
5787
- function surfacePullFailure(err) {
5788
- if (err instanceof ApiError) {
5789
- switch (err.code) {
5790
- case "UNAUTHENTICATED":
5791
- fail(
5792
- `Runtime token rejected. The cached token for this module may be expired \u2014 re-run \`dashwise init <slug> --force\` to mint a fresh one.`
5793
- );
5794
- break;
5795
- case "SANDBOX_TOKEN_NOT_ACCEPTED":
5796
- fail(
5797
- `Schema-read endpoint requires a local-dev runtime token (kind=local). The cached token is the wrong kind \u2014 re-run \`dashwise init <slug> --force\`.`
5798
- );
5799
- break;
5800
- case "INSTALL_MISMATCH":
5801
- fail(
5802
- `Runtime token is bound to a different installation than expected. The cached install id may be stale \u2014 re-run \`dashwise init <slug> --force\`.`
5803
- );
5804
- break;
5805
- case "INSTALLATION_NOT_FOUND":
5806
- fail(
5807
- `The backend has no record of this installation. It may have been destroyed (GC, manual cleanup). Re-run \`dashwise init <slug>\` to provision a fresh one.`
5808
- );
5809
- break;
5810
- default:
5811
- fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
5812
- }
5813
- return 1;
5814
- }
5815
- fail(`Pull failed: ${err.message}`);
5816
- return 1;
5817
- }
5818
-
5819
- // src/bin.ts
5820
- init_scaffold();
5821
-
5822
- // src/commands/api-keys.ts
6284
+ // src/commands/deps/connect.ts
5823
6285
  init_api_client();
5824
6286
  init_config();
5825
6287
  init_manifest_io();
@@ -5828,9 +6290,7 @@ function loadContext(opts) {
5828
6290
  const manifest = readManifestOrThrow(opts.dir);
5829
6291
  const slug = manifest.module?.slug;
5830
6292
  if (!slug) {
5831
- throw new Error(
5832
- `module.json has no module.slug \u2014 the manifest is malformed.`
5833
- );
6293
+ throw new Error("module.json has no module.slug \u2014 the manifest is malformed.");
5834
6294
  }
5835
6295
  const cfg = readConfig();
5836
6296
  if (!cfg) {
@@ -5851,7 +6311,12 @@ function loadContext(opts) {
5851
6311
  }
5852
6312
  };
5853
6313
  }
5854
- async function apiKeysListCommand(options) {
6314
+ function statusChip(status) {
6315
+ if (status === "resolved") return pc.green("\u25CF resolved");
6316
+ if (status === "unbound") return pc.yellow("\u25CB unbound");
6317
+ return pc.dim(`? ${status}`);
6318
+ }
6319
+ async function depsConnectListCommand(options) {
5855
6320
  let ctx;
5856
6321
  try {
5857
6322
  ctx = loadContext(options);
@@ -5859,40 +6324,50 @@ async function apiKeysListCommand(options) {
5859
6324
  fail(err.message);
5860
6325
  return 1;
5861
6326
  }
5862
- let keys;
6327
+ let deps;
5863
6328
  try {
5864
- const resp = await listApiKeys(ctx.installationId, ctx.requestOptions);
5865
- keys = resp.api_keys;
6329
+ deps = await getInstallationDependencies(ctx.installationId, ctx.requestOptions);
5866
6330
  } catch (err) {
5867
- return surfaceApiKeysError(err);
6331
+ return surfaceDepError(err);
5868
6332
  }
5869
- const filtered = options.all ? keys : keys.filter((k) => k.revoked_at === null);
5870
6333
  if (options.json) {
5871
- process.stdout.write(`${JSON.stringify(filtered, null, 2)}
5872
- `);
6334
+ out(JSON.stringify(deps, null, 2));
5873
6335
  return 0;
5874
6336
  }
5875
- if (filtered.length === 0) {
5876
- info(
5877
- options.all ? `No API keys (active or revoked) for ${code(ctx.slug)}.` : `No active API keys for ${code(ctx.slug)}. Run ${code("dashwise api-keys create <name>")} to mint one, or ${code("dashwise api-keys list --all")} to include revoked.`
5878
- );
6337
+ if (deps.length === 0) {
6338
+ info(`No cross-module dependencies declared for ${code(ctx.slug)}.`);
6339
+ info(`Add one with ${code("dashwise deps add <provider-slug>")}.`);
5879
6340
  return 0;
5880
6341
  }
5881
- info(`API keys for ${code(ctx.slug)} (install id=${ctx.installationId}):`);
5882
- for (const k of filtered) {
5883
- const status = formatKeyStatus(k);
5884
- const lastUsed = k.last_used_at ? `last used ${formatRelative(new Date(k.last_used_at))}` : "never used";
5885
- process.stdout.write(
5886
- ` ${code(`#${k.id}`)} ${k.name} ${dim(`dwk_${k.key_prefix}\u2026`)} ${status} ${dim(`(${lastUsed})`)}
5887
- `
6342
+ const unbound = deps.filter((d) => d.status === "unbound").length;
6343
+ info(
6344
+ `Cross-module dependencies for ${code(ctx.slug)} ${dim(
6345
+ `(${deps.length} declared${unbound > 0 ? `, ${unbound} unbound` : ""})`
6346
+ )}:`
6347
+ );
6348
+ out("");
6349
+ for (const dep of deps) {
6350
+ out(` ${statusChip(dep.status)} ${code(dep.slug)}@${dep.range ?? "*"}`);
6351
+ if (dep.status === "resolved" && dep.provider) {
6352
+ out(
6353
+ ` ${dim("provider:")} install #${dep.provider.installation_id} @ v${dep.provider.version}`
6354
+ );
6355
+ }
6356
+ if (dep.status === "unbound") {
6357
+ out(` ${dim("reason:")} ${dep.reason ?? dim("(unspecified)")}`);
6358
+ }
6359
+ out("");
6360
+ }
6361
+ if (unbound > 0) {
6362
+ info(
6363
+ `Connect a parked dep with ${code("dashwise deps bind <provider-slug>")} once its provider is installed.`
5888
6364
  );
5889
6365
  }
5890
6366
  return 0;
5891
6367
  }
5892
- async function apiKeysCreateCommand(nameArg, options) {
5893
- const name = nameArg ?? options.name;
5894
- if (!name) {
5895
- fail("Usage: `dashwise api-keys create <name>` (e.g. `dashwise api-keys create production`)");
6368
+ async function depsBindCommand(providerSlug, options) {
6369
+ if (!providerSlug || typeof providerSlug !== "string") {
6370
+ fail("Usage: `dashwise deps bind <provider-slug> [--installation <id>]`");
5896
6371
  return 1;
5897
6372
  }
5898
6373
  let ctx;
@@ -5902,23 +6377,1091 @@ async function apiKeysCreateCommand(nameArg, options) {
5902
6377
  fail(err.message);
5903
6378
  return 1;
5904
6379
  }
5905
- const body = { name };
5906
- if (options.scopes !== void 0) {
5907
- body.scopes = options.scopes.split(",").map((s) => s.trim()).filter(Boolean);
6380
+ try {
6381
+ const result = await bindDependency(
6382
+ ctx.installationId,
6383
+ providerSlug,
6384
+ options.installation !== void 0 ? { provider_installation_id: options.installation } : {},
6385
+ ctx.requestOptions
6386
+ );
6387
+ const providerLine = result.provider != null ? ` \u2192 install #${result.provider.installation_id} @ v${result.provider.version}` : "";
6388
+ success(`Bound ${code(providerSlug)} for ${code(ctx.slug)}${providerLine}.`);
6389
+ info("Runtime reads against this dependency will now succeed.");
6390
+ return 0;
6391
+ } catch (err) {
6392
+ return surfaceBindError(err, providerSlug, ctx.slug);
5908
6393
  }
5909
- if (options.expiresAt !== void 0) {
5910
- body.expires_at = options.expiresAt;
6394
+ }
6395
+ async function depsUnbindCommand(providerSlug, options) {
6396
+ if (!providerSlug || typeof providerSlug !== "string") {
6397
+ fail("Usage: `dashwise deps unbind <provider-slug>`");
6398
+ return 1;
5911
6399
  }
5912
- let resp;
6400
+ let ctx;
5913
6401
  try {
5914
- resp = await createApiKey(ctx.installationId, body, ctx.requestOptions);
6402
+ ctx = loadContext(options);
5915
6403
  } catch (err) {
5916
- return surfaceApiKeysError(err);
6404
+ fail(err.message);
6405
+ return 1;
5917
6406
  }
5918
- if (options.rawOnly) {
5919
- process.stderr.write(
5920
- `Created API key #${resp.api_key.id} (${resp.api_key.name}) for install ${ctx.installationId}.
5921
- `
6407
+ try {
6408
+ await unbindDependency(ctx.installationId, providerSlug, ctx.requestOptions);
6409
+ success(`Unbound ${code(providerSlug)} for ${code(ctx.slug)}.`);
6410
+ warn(
6411
+ `Runtime reads against this dependency now return ${code("DEP_UNBOUND")} until you ${code("dashwise deps bind")} it again.`
6412
+ );
6413
+ return 0;
6414
+ } catch (err) {
6415
+ return surfaceDepError(err);
6416
+ }
6417
+ }
6418
+ function surfaceBindError(err, providerSlug, consumerSlug) {
6419
+ if (err instanceof ApiError) {
6420
+ switch (err.code) {
6421
+ case "MISSING_DEPENDENCY":
6422
+ fail(
6423
+ `No installation of ${code(providerSlug)} found in ${code(consumerSlug)}'s workspace. Install the provider module first, then re-run ${code(`dashwise deps bind ${providerSlug}`)}.`
6424
+ );
6425
+ return 1;
6426
+ case "DEPENDENCY_VERSION_MISMATCH":
6427
+ fail(
6428
+ `The installed ${code(providerSlug)} version is outside your declared range (${err.message}). Update the range in module.json (${code(`dashwise deps add ${providerSlug}`)}) or upgrade the provider install.`
6429
+ );
6430
+ return 1;
6431
+ case "PROVIDER_VERSION_RECORD_MISSING":
6432
+ fail(
6433
+ `The provider ${code(providerSlug)}'s version record is missing (${err.message}). This is a provider-side data issue \u2014 the provider may need to re-publish.`
6434
+ );
6435
+ return 1;
6436
+ case "EXPOSES_MISSING":
6437
+ fail(
6438
+ `${code(providerSlug)} no longer exposes a table/field your dependency reads (${err.message}). Trim your ${code("reads[]")} or ask the provider to re-expose it.`
6439
+ );
6440
+ return 1;
6441
+ case "EXPOSES_PRIVATE":
6442
+ fail(
6443
+ `${code(providerSlug)} exposes the requested surface as ${code("private")} \u2014 it can't be read by depending modules (${err.message}).`
6444
+ );
6445
+ return 1;
6446
+ case "ACTION_NOT_EXPOSED":
6447
+ fail(
6448
+ `A cross-module action your dependency declares isn't exposed by ${code(providerSlug)} (${err.message}). Remove it from your dep's ${code("actions[]")} or ask the provider to expose it.`
6449
+ );
6450
+ return 1;
6451
+ case "PROVIDER_NOT_PRODUCTION":
6452
+ fail(
6453
+ `A production install can only bind to a production provider install; the available ${code(providerSlug)} install isn't production (${err.message}).`
6454
+ );
6455
+ return 1;
6456
+ case "DEPENDENCY_NOT_DECLARED":
6457
+ fail(
6458
+ `${code(consumerSlug)}'s module.json doesn't declare a dependency on ${code(providerSlug)}. Add it with ${code(`dashwise deps add ${providerSlug}`)} + push the manifest first.`
6459
+ );
6460
+ return 1;
6461
+ default:
6462
+ fail(
6463
+ `Could not bind ${code(providerSlug)}: ${err.code}${err.message ? ` \u2014 ${err.message}` : ""}. The dependency stays unbound.`
6464
+ );
6465
+ return 1;
6466
+ }
6467
+ }
6468
+ fail(`Bind failed: ${err.message}`);
6469
+ return 1;
6470
+ }
6471
+ function surfaceDepError(err) {
6472
+ if (err instanceof ApiError) {
6473
+ switch (err.code) {
6474
+ case "UNAUTHENTICATED":
6475
+ fail(
6476
+ `Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then retry.`
6477
+ );
6478
+ return 1;
6479
+ case "NOT_WORKSPACE_MEMBER":
6480
+ fail(
6481
+ `You're not a member of this installation's workspace. Dependency management requires workspace membership.`
6482
+ );
6483
+ return 1;
6484
+ case "NOT_WORKSPACE_ADMIN":
6485
+ case "FORBIDDEN":
6486
+ fail(
6487
+ `Not authorized. Binding / unbinding dependencies requires workspace-admin.`
6488
+ );
6489
+ return 1;
6490
+ case "INSTALLATION_NOT_FOUND":
6491
+ fail(
6492
+ `Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
6493
+ );
6494
+ return 1;
6495
+ case "DEPENDENCY_NOT_DECLARED":
6496
+ fail(
6497
+ `No such dependency declared in this module's manifest. Run ${code("dashwise deps")} to see declared deps.`
6498
+ );
6499
+ return 1;
6500
+ default:
6501
+ fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
6502
+ return 1;
6503
+ }
6504
+ }
6505
+ fail(`Dependency operation failed: ${err.message}`);
6506
+ return 1;
6507
+ }
6508
+
6509
+ // src/commands/uses/bind.ts
6510
+ init_api_client();
6511
+ init_config();
6512
+ init_manifest_io();
6513
+ init_output();
6514
+ function loadContext2(opts) {
6515
+ const manifest = readManifestOrThrow(opts.dir);
6516
+ const slug = manifest.module?.slug;
6517
+ if (!slug) {
6518
+ throw new Error("module.json has no module.slug \u2014 the manifest is malformed.");
6519
+ }
6520
+ const cfg = readConfig();
6521
+ if (!cfg) {
6522
+ throw new Error("Not logged in. Run `dashwise login` first.");
6523
+ }
6524
+ const entry = readModuleEntry(slug);
6525
+ if (!entry) {
6526
+ throw new Error(
6527
+ `No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
6528
+ );
6529
+ }
6530
+ return {
6531
+ installationId: entry.installationId,
6532
+ slug,
6533
+ ...opts.dir !== void 0 ? { dir: opts.dir } : {},
6534
+ requestOptions: {
6535
+ baseUrl: resolveApiUrl(),
6536
+ auth: { token: cfg.token }
6537
+ }
6538
+ };
6539
+ }
6540
+ function statusChip2(status) {
6541
+ if (status === "bound") return pc.green("\u25CF bound");
6542
+ if (status === "unbound") return pc.yellow("\u25CB unbound");
6543
+ if (status === "declined") return pc.dim("\u2298 declined");
6544
+ return pc.dim(`? ${status}`);
6545
+ }
6546
+ function opsLine(ops) {
6547
+ return ops.length > 0 ? ops.join(", ") : dim("(none)");
6548
+ }
6549
+ async function bindListCommand(options) {
6550
+ let ctx;
6551
+ try {
6552
+ ctx = loadContext2(options);
6553
+ } catch (err) {
6554
+ fail(err.message);
6555
+ return 1;
6556
+ }
6557
+ let bindings;
6558
+ try {
6559
+ bindings = await getInstallationBindings(ctx.installationId, ctx.requestOptions);
6560
+ } catch (err) {
6561
+ return surfaceBindingError(err);
6562
+ }
6563
+ if (options.json) {
6564
+ out(JSON.stringify(bindings, null, 2));
6565
+ return 0;
6566
+ }
6567
+ if (bindings.length === 0) {
6568
+ info(`No workspace-table ${code("uses")} slots declared for ${code(ctx.slug)}.`);
6569
+ info(
6570
+ `Declare one by hand in ${code("module.json#uses.tables")}, or on-ramp an existing table with ${code("dashwise uses add --from-table <name>")}.`
6571
+ );
6572
+ return 0;
6573
+ }
6574
+ const unbound = bindings.filter((b) => b.status === "unbound").length;
6575
+ info(
6576
+ `Workspace-table bindings for ${code(ctx.slug)} ${dim(
6577
+ `(${bindings.length} declared${unbound > 0 ? `, ${unbound} unbound` : ""})`
6578
+ )}:`
6579
+ );
6580
+ out("");
6581
+ for (const b of bindings) {
6582
+ const enforced = b.ops.filter((o) => (b.consented_ops ?? []).includes(o));
6583
+ const opsSummary = b.status === "bound" && enforced.length !== b.ops.length ? `${opsLine(enforced)} ${dim(`(declared ${opsLine(b.ops)}; re-bind to consent to the rest)`)}` : opsLine(b.ops);
6584
+ out(` ${statusChip2(b.status)} ${code(b.uses_slug)} ${dim(`[${opsSummary}]`)}`);
6585
+ if (b.purpose) {
6586
+ out(` ${dim("purpose:")} ${b.purpose}`);
6587
+ }
6588
+ if (b.status === "bound" && b.table) {
6589
+ out(` ${dim("table:")} ${b.table.name} ${dim(`(id ${b.table.id})`)}`);
6590
+ const mapped = b.field_map ? Object.keys(b.field_map).length : 0;
6591
+ if (mapped > 0) {
6592
+ out(` ${dim("fields:")} ${mapped} mapped`);
6593
+ }
6594
+ }
6595
+ if (b.status === "unbound") {
6596
+ out(` ${dim("reason:")} ${b.reason ?? dim("(unspecified)")}`);
6597
+ }
6598
+ if (b.missing_fields && b.missing_fields.length > 0) {
6599
+ out(
6600
+ ` ${pc.yellow("missing:")} ${b.missing_fields.join(", ")} ${dim("(required field(s) not mapped)")}`
6601
+ );
6602
+ }
6603
+ out("");
6604
+ }
6605
+ if (unbound > 0) {
6606
+ info(
6607
+ `Connect a parked slot with ${code("dashwise bind <uses-slug> --table <name>")} (or ${code("--create")} to materialize a fresh table).`
6608
+ );
6609
+ }
6610
+ return 0;
6611
+ }
6612
+ async function bindCommand(usesSlug, options) {
6613
+ if (!usesSlug || typeof usesSlug !== "string") {
6614
+ fail(
6615
+ "Usage: `dashwise bind <uses-slug> [--table <name-or-id>] [--map <usesField>=<physicalFieldName>...]`, `--create`, or `--decline`."
6616
+ );
6617
+ return 1;
6618
+ }
6619
+ const modes = [options.create, options.decline, options.table !== void 0].filter(
6620
+ Boolean
6621
+ ).length;
6622
+ if (modes > 1) {
6623
+ fail(
6624
+ `Pass at most one of ${code("--table")}, ${code("--create")}, ${code("--decline")} \u2014 they select different bind modes.`
6625
+ );
6626
+ return 1;
6627
+ }
6628
+ if (options.decline && (options.map ?? []).length > 0) {
6629
+ fail(`${code("--map")} makes no sense with ${code("--decline")} (nothing is being bound).`);
6630
+ return 1;
6631
+ }
6632
+ let ctx;
6633
+ try {
6634
+ ctx = loadContext2(options);
6635
+ } catch (err) {
6636
+ fail(err.message);
6637
+ return 1;
6638
+ }
6639
+ if (options.decline) {
6640
+ return declineSlot(ctx, usesSlug);
6641
+ }
6642
+ if (options.create) {
6643
+ return bindViaCreate(ctx, usesSlug);
6644
+ }
6645
+ let mapOverrides;
6646
+ try {
6647
+ mapOverrides = parseMapOverrides(options.map);
6648
+ } catch (err) {
6649
+ fail(err.message);
6650
+ return 1;
6651
+ }
6652
+ let candidates;
6653
+ try {
6654
+ candidates = await getBindingCandidates(
6655
+ ctx.installationId,
6656
+ usesSlug,
6657
+ ctx.requestOptions
6658
+ );
6659
+ } catch (err) {
6660
+ return surfaceBindingError(err);
6661
+ }
6662
+ const chosen = await chooseCandidate(candidates, usesSlug, options.table);
6663
+ if (chosen === "cancelled") {
6664
+ info("Cancelled.");
6665
+ return 130;
6666
+ }
6667
+ if (chosen === null) {
6668
+ return 1;
6669
+ }
6670
+ let fieldMap;
6671
+ try {
6672
+ fieldMap = await buildFieldMap(ctx, chosen, mapOverrides);
6673
+ } catch (err) {
6674
+ if (err instanceof ApiError) return surfaceBindingError(err);
6675
+ fail(err.message);
6676
+ return 1;
6677
+ }
6678
+ let result;
6679
+ try {
6680
+ result = await putBinding(
6681
+ ctx.installationId,
6682
+ usesSlug,
6683
+ fieldMap !== void 0 ? { table_id: chosen.table_id, field_map: fieldMap } : { table_id: chosen.table_id },
6684
+ ctx.requestOptions
6685
+ );
6686
+ } catch (err) {
6687
+ return surfacePutBindingError(err, usesSlug, ctx.slug);
6688
+ }
6689
+ reportBoundOk(result, ctx.slug);
6690
+ return 0;
6691
+ }
6692
+ async function chooseCandidate(candidates, usesSlug, tableFlag) {
6693
+ if (candidates.length === 0) {
6694
+ fail(
6695
+ `No workspace table can satisfy ${code(usesSlug)} yet. Create a matching table (or ${code(`dashwise bind ${usesSlug} --create`)} to materialize one from the declared fields).`
6696
+ );
6697
+ return null;
6698
+ }
6699
+ if (tableFlag !== void 0) {
6700
+ const match = matchCandidate(candidates, tableFlag);
6701
+ if (!match) {
6702
+ const list2 = candidates.map((c2) => ` - ${c2.name} ${dim(`(id ${c2.table_id}, score ${c2.match_score})`)}`).join("\n");
6703
+ fail(
6704
+ `No candidate table matching ${code(tableFlag)} for ${code(usesSlug)}. Candidates:
6705
+ ${list2}`
6706
+ );
6707
+ return null;
6708
+ }
6709
+ return match;
6710
+ }
6711
+ if (candidates.length === 1) {
6712
+ return candidates[0];
6713
+ }
6714
+ if (process.stdout.isTTY) {
6715
+ const choice = await select({
6716
+ message: `Which workspace table should ${usesSlug} bind to?`,
6717
+ options: candidates.map((c2) => ({
6718
+ value: String(c2.table_id),
6719
+ label: c2.name,
6720
+ hint: c2.missing_required.length > 0 ? `id=${c2.table_id}, score=${c2.match_score}, missing: ${c2.missing_required.join(", ")}` : `id=${c2.table_id}, score=${c2.match_score}, full coverage`
6721
+ }))
6722
+ });
6723
+ if (isCancel(choice)) return "cancelled";
6724
+ return candidates.find((c2) => String(c2.table_id) === choice) ?? null;
6725
+ }
6726
+ const list = candidates.map((c2) => ` - ${c2.name} ${dim(`(id ${c2.table_id}, score ${c2.match_score})`)}`).join("\n");
6727
+ fail(
6728
+ `${candidates.length} candidate tables for ${code(usesSlug)}. Re-run with ${code("--table <name-or-id>")} to pick one:
6729
+ ${list}`
6730
+ );
6731
+ return null;
6732
+ }
6733
+ function matchCandidate(candidates, flag) {
6734
+ const asNumber = Number.parseInt(flag, 10);
6735
+ const isNumeric = !Number.isNaN(asNumber) && String(asNumber) === flag;
6736
+ if (isNumeric) {
6737
+ return candidates.find((c2) => c2.table_id === asNumber);
6738
+ }
6739
+ return candidates.find((c2) => c2.name === flag);
6740
+ }
6741
+ async function buildFieldMap(ctx, candidate, overrides) {
6742
+ const overrideKeys = Object.keys(overrides);
6743
+ if (overrideKeys.length === 0) {
6744
+ return Object.keys(candidate.auto_field_map).length > 0 ? { ...candidate.auto_field_map } : void 0;
6745
+ }
6746
+ const table = await introspectWorkspaceTable(
6747
+ ctx.installationId,
6748
+ String(candidate.table_id),
6749
+ ctx.requestOptions
6750
+ );
6751
+ const byName = /* @__PURE__ */ new Map();
6752
+ for (const f of table.fields) {
6753
+ byName.set(f.slug, f.id);
6754
+ if (f.name) byName.set(f.name, f.id);
6755
+ }
6756
+ const merged = { ...candidate.auto_field_map };
6757
+ for (const [usesField, physicalName] of Object.entries(overrides)) {
6758
+ const id = byName.get(physicalName);
6759
+ if (id === void 0) {
6760
+ const available = table.fields.map((f) => f.name && f.name !== f.slug ? `${f.slug} (${f.name})` : f.slug).join(", ");
6761
+ throw new Error(
6762
+ `--map ${usesField}=${physicalName}: table "${table.name}" has no field named "${physicalName}". Available: ${available}.`
6763
+ );
6764
+ }
6765
+ merged[usesField] = id;
6766
+ }
6767
+ return merged;
6768
+ }
6769
+ function parseMapOverrides(specs) {
6770
+ const out2 = {};
6771
+ for (const spec of specs ?? []) {
6772
+ const eq = spec.indexOf("=");
6773
+ if (eq <= 0 || eq === spec.length - 1) {
6774
+ throw new Error(
6775
+ `Invalid --map "${spec}". Expected \`<usesField>=<physicalFieldName>\` (e.g. \`--map name=full_name\`).`
6776
+ );
6777
+ }
6778
+ const usesField = spec.slice(0, eq).trim();
6779
+ const physicalName = spec.slice(eq + 1).trim();
6780
+ if (!usesField || !physicalName) {
6781
+ throw new Error(`Invalid --map "${spec}": both sides must be non-empty.`);
6782
+ }
6783
+ if (out2[usesField] !== void 0) {
6784
+ throw new Error(`Duplicate --map for uses field "${usesField}".`);
6785
+ }
6786
+ out2[usesField] = physicalName;
6787
+ }
6788
+ return out2;
6789
+ }
6790
+ async function bindViaCreate(ctx, usesSlug) {
6791
+ const slot = findUsesSlot(ctx, usesSlug);
6792
+ if (slot && slot.if_missing !== void 0 && slot.if_missing !== "create") {
6793
+ warn(
6794
+ `${code(usesSlug)} declares ${code(`if_missing: "${slot.if_missing}"`)}, not ${code('"create"')} \u2014 the backend may reject ${code("--create")}. Set it to ${code("create")} in module.json if you want a table materialized.`
6795
+ );
6796
+ }
6797
+ let result;
6798
+ try {
6799
+ result = await putBinding(
6800
+ ctx.installationId,
6801
+ usesSlug,
6802
+ { create: true },
6803
+ ctx.requestOptions
6804
+ );
6805
+ } catch (err) {
6806
+ return surfacePutBindingError(err, usesSlug, ctx.slug);
6807
+ }
6808
+ if (result.table) {
6809
+ success(
6810
+ `Materialized ${code(result.table.name)} ${dim(`(id ${result.table.id})`)} and bound ${code(usesSlug)} for ${code(ctx.slug)}.`
6811
+ );
6812
+ info(
6813
+ `The new table is a plain user-owned Data Hub table \u2014 it stays in the workspace when the module is uninstalled.`
6814
+ );
6815
+ } else {
6816
+ success(`Bound ${code(usesSlug)} for ${code(ctx.slug)} to a freshly-created table.`);
6817
+ }
6818
+ return 0;
6819
+ }
6820
+ async function declineSlot(ctx, usesSlug) {
6821
+ try {
6822
+ await declineBinding(ctx.installationId, usesSlug, ctx.requestOptions);
6823
+ success(`Declined ${code(usesSlug)} for ${code(ctx.slug)} \u2014 the module runs without it.`);
6824
+ return 0;
6825
+ } catch (err) {
6826
+ return surfacePutBindingError(err, usesSlug, ctx.slug);
6827
+ }
6828
+ }
6829
+ function findUsesSlot(ctx, usesSlug) {
6830
+ try {
6831
+ const manifest = readManifestOrThrow(ctx.dir);
6832
+ return manifest.uses?.tables?.find((t) => t.slug === usesSlug);
6833
+ } catch {
6834
+ return void 0;
6835
+ }
6836
+ }
6837
+ function reportBoundOk(result, moduleSlug) {
6838
+ const tableLine = result.table ? ` \u2192 ${result.table.name} ${dim(`(id ${result.table.id})`)}` : "";
6839
+ success(`Bound ${code(result.uses_slug)} for ${code(moduleSlug)}${tableLine}.`);
6840
+ const consented = result.consented_ops ?? [];
6841
+ info(
6842
+ `Consented ops: ${opsLine(consented)}. Runtime reads/writes against this slot now succeed.`
6843
+ );
6844
+ if (result.missing_fields && result.missing_fields.length > 0) {
6845
+ warn(
6846
+ `Still unmapped required field(s): ${result.missing_fields.join(", ")}. Re-run with ${code("--map <usesField>=<physicalFieldName>")} to map them.`
6847
+ );
6848
+ }
6849
+ }
6850
+ async function unbindCommand(usesSlug, options) {
6851
+ if (!usesSlug || typeof usesSlug !== "string") {
6852
+ fail("Usage: `dashwise unbind <uses-slug>`");
6853
+ return 1;
6854
+ }
6855
+ let ctx;
6856
+ try {
6857
+ ctx = loadContext2(options);
6858
+ } catch (err) {
6859
+ fail(err.message);
6860
+ return 1;
6861
+ }
6862
+ try {
6863
+ await unbindBinding(ctx.installationId, usesSlug, ctx.requestOptions);
6864
+ success(`Unbound ${code(usesSlug)} for ${code(ctx.slug)}.`);
6865
+ warn(
6866
+ `Runtime reads/writes against this slot now return ${code("USES_UNBOUND")} until you ${code("dashwise bind")} it again.`
6867
+ );
6868
+ return 0;
6869
+ } catch (err) {
6870
+ return surfaceBindingError(err);
6871
+ }
6872
+ }
6873
+ function surfacePutBindingError(err, usesSlug, moduleSlug) {
6874
+ if (err instanceof ApiError) {
6875
+ switch (err.code) {
6876
+ case "TABLE_NOT_IN_WORKSPACE":
6877
+ fail(
6878
+ `The chosen table isn't in ${code(moduleSlug)}'s workspace (${err.message}). A ${code("uses")} binding can only target a table the install's workspace owns.`
6879
+ );
6880
+ return 1;
6881
+ case "TABLE_IS_MODULE_MANAGED":
6882
+ fail(
6883
+ `That table is module-managed \u2014 ${code("uses")} bindings target plain WORKSPACE tables only. Reach a module's own tables via ${code("dashwise deps")} instead.`
6884
+ );
6885
+ return 1;
6886
+ case "MISSING_REQUIRED_FIELDS":
6887
+ fail(
6888
+ `The table is missing required field(s) the ${code(usesSlug)} contract needs (${err.message}). Map them explicitly with ${code("--map <usesField>=<physicalFieldName>")}, or pick a table that has them.`
6889
+ );
6890
+ return 1;
6891
+ case "FIELD_TYPE_INCOMPATIBLE":
6892
+ fail(
6893
+ `A mapped physical field's type is incompatible with the declared abstract type (${err.message}). Map to a compatible field with ${code("--map")}.`
6894
+ );
6895
+ return 1;
6896
+ case "FIELD_NOT_FOUND":
6897
+ fail(
6898
+ `A ${code("--map")} referenced a physical field that doesn't exist on the table (${err.message}). Check the field name.`
6899
+ );
6900
+ return 1;
6901
+ case "DECLINE_NOT_ALLOWED":
6902
+ fail(
6903
+ `${code(usesSlug)} is a required need \u2014 it can't be declined. Bind it to a table (${code(`dashwise bind ${usesSlug} --table <name>`)}) or set ${code('if_missing: "optional"')} in module.json.`
6904
+ );
6905
+ return 1;
6906
+ case "USES_SLOT_NOT_DECLARED":
6907
+ case "BINDING_NOT_FOUND":
6908
+ fail(
6909
+ `${code(moduleSlug)}'s module.json doesn't declare a ${code("uses")} slot named ${code(usesSlug)}. Add it (or ${code("dashwise uses add --from-table <name>")}) + push the manifest first.`
6910
+ );
6911
+ return 1;
6912
+ case "NOT_WORKSPACE_ADMIN":
6913
+ case "FORBIDDEN":
6914
+ fail(`Not authorized. Binding / unbinding / declining requires workspace-admin.`);
6915
+ return 1;
6916
+ default:
6917
+ fail(
6918
+ `Could not bind ${code(usesSlug)}: ${err.code}${err.message ? ` \u2014 ${err.message}` : ""}. The binding is unchanged.`
6919
+ );
6920
+ return 1;
6921
+ }
6922
+ }
6923
+ fail(`Bind failed: ${err.message}`);
6924
+ return 1;
6925
+ }
6926
+ function surfaceBindingError(err) {
6927
+ if (err instanceof ApiError) {
6928
+ switch (err.code) {
6929
+ case "UNAUTHENTICATED":
6930
+ fail(
6931
+ `Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then retry.`
6932
+ );
6933
+ return 1;
6934
+ case "NOT_WORKSPACE_MEMBER":
6935
+ fail(
6936
+ `You're not a member of this installation's workspace. Binding management requires workspace membership.`
6937
+ );
6938
+ return 1;
6939
+ case "NOT_WORKSPACE_ADMIN":
6940
+ case "FORBIDDEN":
6941
+ fail(`Not authorized. Binding / unbinding requires workspace-admin.`);
6942
+ return 1;
6943
+ case "INSTALLATION_NOT_FOUND":
6944
+ fail(
6945
+ `Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
6946
+ );
6947
+ return 1;
6948
+ case "USES_SLOT_NOT_DECLARED":
6949
+ case "BINDING_NOT_FOUND":
6950
+ fail(
6951
+ `No such ${code("uses")} slot declared in this module's manifest. Run ${code("dashwise bind")} to see declared slots.`
6952
+ );
6953
+ return 1;
6954
+ default:
6955
+ fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
6956
+ return 1;
6957
+ }
6958
+ }
6959
+ fail(`Binding operation failed: ${err.message}`);
6960
+ return 1;
6961
+ }
6962
+
6963
+ // src/commands/uses/add.ts
6964
+ init_api_client();
6965
+ init_config();
6966
+ init_manifest_io();
6967
+
6968
+ // src/lib/uses-types.ts
6969
+ var PHYSICAL_TO_ABSTRACT = {
6970
+ text: "text",
6971
+ long_text: "long_text",
6972
+ number: "number",
6973
+ rating: "number",
6974
+ boolean: "boolean",
6975
+ // A date-with-time column has physical registry type `date` (with
6976
+ // `config.includeTime`), NOT a distinct `datetime` type — the backend
6977
+ // registry has no `datetime` key. So introspection only ever reports
6978
+ // `date` here; there is no `datetime` physical entry to fold.
6979
+ date: "date",
6980
+ email: "email",
6981
+ url: "url",
6982
+ single_select: "single_select",
6983
+ multi_select: "multi_select"
6984
+ };
6985
+ function abstractTypeForPhysical(physicalType) {
6986
+ return PHYSICAL_TO_ABSTRACT[physicalType] ?? null;
6987
+ }
6988
+ var USES_OPERATIONS = ["create", "read", "update", "delete"];
6989
+
6990
+ // src/commands/uses/add.ts
6991
+ init_output();
6992
+ async function usesAddCommand(options) {
6993
+ if (!options.fromTable) {
6994
+ fail("Usage: `dashwise uses add --from-table <name-or-id>`");
6995
+ return 1;
6996
+ }
6997
+ let ops;
6998
+ try {
6999
+ ops = parseOps(options.ops);
7000
+ } catch (err) {
7001
+ fail(err.message);
7002
+ return 1;
7003
+ }
7004
+ let ctx;
7005
+ try {
7006
+ ctx = loadContext3(options);
7007
+ } catch (err) {
7008
+ fail(err.message);
7009
+ return 1;
7010
+ }
7011
+ let table;
7012
+ try {
7013
+ table = await introspectWorkspaceTable(
7014
+ ctx.installationId,
7015
+ options.fromTable,
7016
+ ctx.requestOptions
7017
+ );
7018
+ } catch (err) {
7019
+ return surfaceIntrospectError(err, options.fromTable);
7020
+ }
7021
+ const slotSlug = options.slug ?? slugify(table.name);
7022
+ if (!isKebab(slotSlug)) {
7023
+ fail(
7024
+ `Generated slot slug ${code(slotSlug)} isn't kebab-case. Pass ${code("--slug <kebab-slug>")} explicitly.`
7025
+ );
7026
+ return 1;
7027
+ }
7028
+ const { fields, skipped } = generateFields(table.fields);
7029
+ if (fields.length === 0) {
7030
+ fail(
7031
+ `Table ${code(table.name)} has no fields representable as a v1 ${code("uses")} contract (all are link_row / formula / ai / file). Nothing to declare.`
7032
+ );
7033
+ return 1;
7034
+ }
7035
+ const slot = {
7036
+ slug: slotSlug,
7037
+ ops,
7038
+ fields,
7039
+ if_missing: "require"
7040
+ };
7041
+ if (options.purpose) slot.purpose = options.purpose;
7042
+ info(
7043
+ `Generated a ${code("uses")} contract ${code(slotSlug)} from table ${code(table.name)} ${dim(`(id ${table.table_id})`)}:`
7044
+ );
7045
+ out(` ${dim("ops:")} ${ops.join(", ")}`);
7046
+ for (const f of fields) {
7047
+ const req = f.required ? dim(" (required)") : "";
7048
+ out(` ${dim("field:")} ${code(f.slug)} \u2192 ${f.types.join(" | ")}${req}`);
7049
+ }
7050
+ if (skipped.length > 0) {
7051
+ warn(
7052
+ `Skipped ${skipped.length} field(s) not representable in a v1 uses contract: ${skipped.join(", ")}.`
7053
+ );
7054
+ }
7055
+ const uses = ctx.manifest.uses ?? { tables: [] };
7056
+ const tables = uses.tables ?? [];
7057
+ const existingIdx = tables.findIndex((t) => t.slug === slotSlug);
7058
+ if (existingIdx >= 0 && !options.yes) {
7059
+ fail(
7060
+ `A ${code("uses")} slot ${code(slotSlug)} already exists in module.json. Re-run with ${code("--yes")} to replace it, or pass a different ${code("--slug")}.`
7061
+ );
7062
+ return 1;
7063
+ }
7064
+ if (existingIdx >= 0) {
7065
+ tables[existingIdx] = slot;
7066
+ } else {
7067
+ tables.push(slot);
7068
+ }
7069
+ uses.tables = tables;
7070
+ ctx.manifest.uses = uses;
7071
+ try {
7072
+ writeManifest(ctx.manifest, ctx.dir);
7073
+ } catch (err) {
7074
+ fail(`Could not write manifest: ${err.message}`);
7075
+ return 1;
7076
+ }
7077
+ success(
7078
+ `${existingIdx >= 0 ? "Replaced" : "Added"} ${code(`uses.tables[${slotSlug}]`)} in module.json.`
7079
+ );
7080
+ const pushed = await pushManifest(ctx);
7081
+ if (!pushed) {
7082
+ info(
7083
+ `The manifest edit is saved locally. Push it (${code("dashwise dev")} watches + applies, or re-run once the backend is reachable), then ${code(`dashwise bind ${slotSlug} --table ${table.table_id}`)}.`
7084
+ );
7085
+ return 1;
7086
+ }
7087
+ if (options.noBind) {
7088
+ info(
7089
+ `Skipped auto-bind (${code("--no-bind")}). Connect it when ready: ${code(`dashwise bind ${slotSlug} --table ${table.table_id}`)}.`
7090
+ );
7091
+ return 0;
7092
+ }
7093
+ info(`Binding ${code(slotSlug)} to ${code(table.name)}\u2026`);
7094
+ const bindExit = await bindCommand(slotSlug, {
7095
+ ...ctx.dir !== void 0 ? { dir: ctx.dir } : {},
7096
+ table: String(table.table_id)
7097
+ });
7098
+ if (bindExit !== 0) {
7099
+ warn(
7100
+ `The ${code("uses")} slot was declared + pushed, but the auto-bind didn't complete. Retry with ${code(`dashwise bind ${slotSlug} --table ${table.table_id}`)}.`
7101
+ );
7102
+ return bindExit;
7103
+ }
7104
+ return 0;
7105
+ }
7106
+ function loadContext3(opts) {
7107
+ const manifest = readManifestOrThrow(opts.dir);
7108
+ const moduleSlug = manifest.module?.slug;
7109
+ if (!moduleSlug) {
7110
+ throw new Error("module.json has no module.slug \u2014 the manifest is malformed.");
7111
+ }
7112
+ const cfg = readConfig();
7113
+ if (!cfg) {
7114
+ throw new Error("Not logged in. Run `dashwise login` first.");
7115
+ }
7116
+ const entry = readModuleEntry(moduleSlug);
7117
+ if (!entry) {
7118
+ throw new Error(
7119
+ `No dev install cached for module "${moduleSlug}". Run \`dashwise init ${moduleSlug} --force\` to provision one.`
7120
+ );
7121
+ }
7122
+ return {
7123
+ installationId: entry.installationId,
7124
+ moduleSlug,
7125
+ ...opts.dir !== void 0 ? { dir: opts.dir } : {},
7126
+ manifest,
7127
+ requestOptions: {
7128
+ baseUrl: resolveApiUrl(),
7129
+ auth: { token: cfg.token }
7130
+ }
7131
+ };
7132
+ }
7133
+ async function pushManifest(ctx) {
7134
+ let moduleId;
7135
+ try {
7136
+ const mod = await getModuleBySlug(ctx.moduleSlug, ctx.requestOptions);
7137
+ moduleId = mod.id;
7138
+ } catch (err) {
7139
+ if (err instanceof ApiError && err.status === 404) {
7140
+ warn(
7141
+ `Module ${code(ctx.moduleSlug)} isn't registered on the backend yet. Run ${code("dashwise module register")} (or ${code("dashwise init")}) before on-ramping a table.`
7142
+ );
7143
+ return false;
7144
+ }
7145
+ warn(`Couldn't resolve the module id to push the manifest: ${err.message}`);
7146
+ return false;
7147
+ }
7148
+ try {
7149
+ await applyDraftManifest(
7150
+ moduleId,
7151
+ ctx.manifest,
7152
+ ctx.requestOptions
7153
+ );
7154
+ success("Pushed the manifest \u2014 the backend parked the new slot, ready to bind.");
7155
+ return true;
7156
+ } catch (err) {
7157
+ if (err instanceof ApiError) {
7158
+ if (err.code === "MANIFEST_INVALID" || err.status === 400) {
7159
+ fail(`Manifest rejected by the backend validator: ${err.message}`);
7160
+ const issues = extractIssues(err.context);
7161
+ for (const issue of issues) out(` ${dim("-")} ${issue}`);
7162
+ } else {
7163
+ fail(`Manifest push failed: ${err.code} ${err.status} \u2014 ${err.message}`);
7164
+ }
7165
+ } else {
7166
+ fail(`Manifest push failed: ${err.message}`);
7167
+ }
7168
+ return false;
7169
+ }
7170
+ }
7171
+ function extractIssues(context) {
7172
+ if (!context) return [];
7173
+ const raw = context.issues;
7174
+ if (!Array.isArray(raw)) return [];
7175
+ return raw.map((i) => {
7176
+ if (typeof i === "string") return i;
7177
+ if (i && typeof i === "object") {
7178
+ const o = i;
7179
+ const path = typeof o.path === "string" ? o.path : "";
7180
+ const message = typeof o.message === "string" ? o.message : JSON.stringify(i);
7181
+ return path ? `${path}: ${message}` : message;
7182
+ }
7183
+ return String(i);
7184
+ });
7185
+ }
7186
+ function generateFields(physical) {
7187
+ const fields = [];
7188
+ const skipped = [];
7189
+ for (const f of physical) {
7190
+ const abstract = abstractTypeForPhysical(f.type);
7191
+ if (abstract === null) {
7192
+ skipped.push(`${f.slug} (${f.type})`);
7193
+ continue;
7194
+ }
7195
+ const entry = { slug: f.slug, types: [abstract] };
7196
+ if (f.required) entry.required = true;
7197
+ fields.push(entry);
7198
+ }
7199
+ return { fields, skipped };
7200
+ }
7201
+ function parseOps(opsCsv) {
7202
+ if (opsCsv === void 0) return ["read"];
7203
+ const parsed = opsCsv.split(",").map((o) => o.trim()).filter(Boolean);
7204
+ if (parsed.length === 0) {
7205
+ throw new Error(`--ops must list at least one of ${USES_OPERATIONS.join(", ")}.`);
7206
+ }
7207
+ const seen = /* @__PURE__ */ new Set();
7208
+ for (const op of parsed) {
7209
+ if (!USES_OPERATIONS.includes(op)) {
7210
+ throw new Error(
7211
+ `Invalid --ops value "${op}". Allowed: ${USES_OPERATIONS.join(", ")}.`
7212
+ );
7213
+ }
7214
+ seen.add(op);
7215
+ }
7216
+ return USES_OPERATIONS.filter((o) => seen.has(o));
7217
+ }
7218
+ function slugify(name) {
7219
+ return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
7220
+ }
7221
+ var KEBAB_REGEX = /^[a-z][a-z0-9-]*[a-z0-9]$/;
7222
+ function isKebab(slug) {
7223
+ return KEBAB_REGEX.test(slug);
7224
+ }
7225
+ function surfaceIntrospectError(err, tableRef) {
7226
+ if (err instanceof ApiError) {
7227
+ switch (err.code) {
7228
+ case "NON_NUMERIC_TABLE_REF":
7229
+ fail(err.message);
7230
+ return 1;
7231
+ case "NOT_FOUND":
7232
+ fail(
7233
+ `No workspace table with id ${code(tableRef)} is visible to you. Check the id (find it in the Dashwise UI URL, or via ${code("GET /api/tables/application/:applicationId")}).`
7234
+ );
7235
+ return 1;
7236
+ case "TABLE_IS_MODULE_MANAGED":
7237
+ fail(
7238
+ `Table ${code(tableRef)} is module-managed \u2014 ${code("uses")} is for plain WORKSPACE tables. Reach a module's own tables via ${code("dashwise deps")} instead.`
7239
+ );
7240
+ return 1;
7241
+ case "UNAUTHENTICATED":
7242
+ fail(
7243
+ `Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then retry.`
7244
+ );
7245
+ return 1;
7246
+ case "NOT_WORKSPACE_MEMBER":
7247
+ case "FORBIDDEN":
7248
+ fail(`You're not a member of the workspace that owns table ${code(tableRef)}.`);
7249
+ return 1;
7250
+ default:
7251
+ fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
7252
+ return 1;
7253
+ }
7254
+ }
7255
+ fail(`Table introspection failed: ${err.message}`);
7256
+ return 1;
7257
+ }
7258
+
7259
+ // src/commands/pull.ts
7260
+ init_api_client();
7261
+ init_manifest_io();
7262
+ init_schema_command_context();
7263
+ init_output();
7264
+ async function pullCommand(options) {
7265
+ let ctx;
7266
+ try {
7267
+ ctx = loadSchemaCommandContext({
7268
+ ...options.dir !== void 0 ? { dir: options.dir } : {}
7269
+ });
7270
+ } catch (err) {
7271
+ fail(err.message);
7272
+ return 1;
7273
+ }
7274
+ info(`Pulling manifest for installation ${ctx.entry.installationId}\u2026`);
7275
+ let remoteManifest;
7276
+ try {
7277
+ const response = await getInstallationSchema(ctx.requestOptions);
7278
+ remoteManifest = response.manifest;
7279
+ } catch (err) {
7280
+ return surfacePullFailure(err);
7281
+ }
7282
+ const localJson = JSON.stringify(ctx.manifest, null, 2) + "\n";
7283
+ const remoteJson = JSON.stringify(remoteManifest, null, 2) + "\n";
7284
+ if (localJson === remoteJson) {
7285
+ success("Already in sync \u2014 local module.json matches backend.");
7286
+ return 0;
7287
+ }
7288
+ const localBytes = Buffer.byteLength(localJson, "utf8");
7289
+ const remoteBytes = Buffer.byteLength(remoteJson, "utf8");
7290
+ const delta = remoteBytes - localBytes;
7291
+ const deltaStr = delta === 0 ? "0 bytes" : `${delta > 0 ? "+" : ""}${delta} bytes`;
7292
+ if (options.dryRun) {
7293
+ info(
7294
+ `Drift detected (${deltaStr}). ${code("--dry-run")} \u2014 not writing.`
7295
+ );
7296
+ info(`Run ${code("dashwise pull")} without ${code("--dry-run")} to apply.`);
7297
+ return 0;
7298
+ }
7299
+ try {
7300
+ writeManifest(remoteManifest, ctx.projectRoot);
7301
+ } catch (err) {
7302
+ fail(`Failed to write ${manifestPath(ctx.projectRoot)}: ${err.message}`);
7303
+ return 1;
7304
+ }
7305
+ success(
7306
+ `Synced module.json from backend (${deltaStr}). ${dim(`Run \`dashwise module generate\` to refresh typed SDK if your watcher isn't running.`)}`
7307
+ );
7308
+ return 0;
7309
+ }
7310
+ function surfacePullFailure(err) {
7311
+ if (err instanceof ApiError) {
7312
+ switch (err.code) {
7313
+ case "UNAUTHENTICATED":
7314
+ fail(
7315
+ `Runtime token rejected. The cached token for this module may be expired \u2014 re-run \`dashwise init <slug> --force\` to mint a fresh one.`
7316
+ );
7317
+ break;
7318
+ case "SANDBOX_TOKEN_NOT_ACCEPTED":
7319
+ fail(
7320
+ `Schema-read endpoint requires a local-dev runtime token (kind=local). The cached token is the wrong kind \u2014 re-run \`dashwise init <slug> --force\`.`
7321
+ );
7322
+ break;
7323
+ case "INSTALL_MISMATCH":
7324
+ fail(
7325
+ `Runtime token is bound to a different installation than expected. The cached install id may be stale \u2014 re-run \`dashwise init <slug> --force\`.`
7326
+ );
7327
+ break;
7328
+ case "INSTALLATION_NOT_FOUND":
7329
+ fail(
7330
+ `The backend has no record of this installation. It may have been destroyed (GC, manual cleanup). Re-run \`dashwise init <slug>\` to provision a fresh one.`
7331
+ );
7332
+ break;
7333
+ default:
7334
+ fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
7335
+ }
7336
+ return 1;
7337
+ }
7338
+ fail(`Pull failed: ${err.message}`);
7339
+ return 1;
7340
+ }
7341
+
7342
+ // src/bin.ts
7343
+ init_scaffold();
7344
+
7345
+ // src/commands/api-keys.ts
7346
+ init_api_client();
7347
+ init_config();
7348
+ init_manifest_io();
7349
+ init_output();
7350
+ function loadContext4(opts) {
7351
+ const manifest = readManifestOrThrow(opts.dir);
7352
+ const slug = manifest.module?.slug;
7353
+ if (!slug) {
7354
+ throw new Error(
7355
+ `module.json has no module.slug \u2014 the manifest is malformed.`
7356
+ );
7357
+ }
7358
+ const cfg = readConfig();
7359
+ if (!cfg) {
7360
+ throw new Error("Not logged in. Run `dashwise login` first.");
7361
+ }
7362
+ const entry = readModuleEntry(slug);
7363
+ if (!entry) {
7364
+ throw new Error(
7365
+ `No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
7366
+ );
7367
+ }
7368
+ return {
7369
+ installationId: entry.installationId,
7370
+ slug,
7371
+ requestOptions: {
7372
+ baseUrl: resolveApiUrl(),
7373
+ auth: { token: cfg.token }
7374
+ }
7375
+ };
7376
+ }
7377
+ async function apiKeysListCommand(options) {
7378
+ let ctx;
7379
+ try {
7380
+ ctx = loadContext4(options);
7381
+ } catch (err) {
7382
+ fail(err.message);
7383
+ return 1;
7384
+ }
7385
+ let keys;
7386
+ try {
7387
+ const resp = await listApiKeys(ctx.installationId, ctx.requestOptions);
7388
+ keys = resp.api_keys;
7389
+ } catch (err) {
7390
+ return surfaceApiKeysError(err);
7391
+ }
7392
+ const filtered = options.all ? keys : keys.filter((k) => k.revoked_at === null);
7393
+ if (options.json) {
7394
+ process.stdout.write(`${JSON.stringify(filtered, null, 2)}
7395
+ `);
7396
+ return 0;
7397
+ }
7398
+ if (filtered.length === 0) {
7399
+ info(
7400
+ options.all ? `No API keys (active or revoked) for ${code(ctx.slug)}.` : `No active API keys for ${code(ctx.slug)}. Run ${code("dashwise api-keys create <name>")} to mint one, or ${code("dashwise api-keys list --all")} to include revoked.`
7401
+ );
7402
+ return 0;
7403
+ }
7404
+ info(`API keys for ${code(ctx.slug)} (install id=${ctx.installationId}):`);
7405
+ for (const k of filtered) {
7406
+ const status = formatKeyStatus(k);
7407
+ const lastUsed = k.last_used_at ? `last used ${formatRelative(new Date(k.last_used_at))}` : "never used";
7408
+ process.stdout.write(
7409
+ ` ${code(`#${k.id}`)} ${k.name} ${dim(`dwk_${k.key_prefix}\u2026`)} ${status} ${dim(`(${lastUsed})`)}
7410
+ `
7411
+ );
7412
+ const grantLines = formatGrants(k.grants);
7413
+ if (grantLines.length > 0) {
7414
+ for (const line of grantLines) {
7415
+ process.stdout.write(` ${dim(line)}
7416
+ `);
7417
+ }
7418
+ } else if (k.grants !== void 0) {
7419
+ process.stdout.write(` ${dim("full install access (no grants)")}
7420
+ `);
7421
+ }
7422
+ }
7423
+ return 0;
7424
+ }
7425
+ async function apiKeysCreateCommand(nameArg, options) {
7426
+ const name = nameArg ?? options.name;
7427
+ if (!name) {
7428
+ fail("Usage: `dashwise api-keys create <name>` (e.g. `dashwise api-keys create production`)");
7429
+ return 1;
7430
+ }
7431
+ let grants;
7432
+ try {
7433
+ grants = parseGrants(options.grant);
7434
+ } catch (err) {
7435
+ fail(err.message);
7436
+ return 1;
7437
+ }
7438
+ let expiresAt;
7439
+ try {
7440
+ expiresAt = resolveExpiresAt(options.expires);
7441
+ } catch (err) {
7442
+ fail(err.message);
7443
+ return 1;
7444
+ }
7445
+ let ctx;
7446
+ try {
7447
+ ctx = loadContext4(options);
7448
+ } catch (err) {
7449
+ fail(err.message);
7450
+ return 1;
7451
+ }
7452
+ const body = { name };
7453
+ if (grants !== void 0) body.grants = grants;
7454
+ if (expiresAt !== void 0) body.expires_at = expiresAt;
7455
+ let resp;
7456
+ try {
7457
+ resp = await createApiKey(ctx.installationId, body, ctx.requestOptions);
7458
+ } catch (err) {
7459
+ return surfaceApiKeysError(err);
7460
+ }
7461
+ if (options.rawOnly) {
7462
+ process.stderr.write(
7463
+ `Created API key #${resp.api_key.id} (${resp.api_key.name}) for install ${ctx.installationId}.
7464
+ `
5922
7465
  );
5923
7466
  process.stdout.write(`${resp.raw_key}
5924
7467
  `);
@@ -5927,6 +7470,13 @@ async function apiKeysCreateCommand(nameArg, options) {
5927
7470
  success(
5928
7471
  `Created API key ${code(`#${resp.api_key.id}`)} (${resp.api_key.name}) for ${code(ctx.slug)}.`
5929
7472
  );
7473
+ const grantLines = formatGrants(resp.api_key.grants);
7474
+ if (grantLines.length > 0) {
7475
+ info(` ${dim("scope:")} scoped to`);
7476
+ for (const line of grantLines) info(` ${line}`);
7477
+ } else {
7478
+ info(` ${dim("scope:")} full install access (no grants)`);
7479
+ }
5930
7480
  info("");
5931
7481
  warn(
5932
7482
  "This is the ONLY time the full key is displayed. Save it now \u2014 the backend stores a hash, not the plaintext."
@@ -5935,14 +7485,9 @@ async function apiKeysCreateCommand(nameArg, options) {
5935
7485
  process.stdout.write(` ${code(resp.raw_key)}
5936
7486
  `);
5937
7487
  info("");
5938
- info(`Use in a deployed module's env:`);
7488
+ info(`Use in a deployed module's env (the two-var contract):`);
7489
+ info(` ${code(`DASHWISE_API_URL=${resolveApiUrl()}`)}`);
5939
7490
  info(` ${code(`DASHWISE_API_KEY=${resp.raw_key.slice(0, 12)}\u2026`)}`);
5940
- info(
5941
- ` ${code(`DASHWISE_INSTALLATION_ID=${ctx.installationId}`)}`
5942
- );
5943
- info(
5944
- ` ${code(`DASHWISE_API_URL=${resolveApiUrl()}`)}`
5945
- );
5946
7491
  return 0;
5947
7492
  }
5948
7493
  async function apiKeysRevokeCommand(keyIdArg, options) {
@@ -5957,7 +7502,7 @@ async function apiKeysRevokeCommand(keyIdArg, options) {
5957
7502
  }
5958
7503
  let ctx;
5959
7504
  try {
5960
- ctx = loadContext(options);
7505
+ ctx = loadContext4(options);
5961
7506
  } catch (err) {
5962
7507
  fail(err.message);
5963
7508
  return 1;
@@ -5991,6 +7536,17 @@ function surfaceApiKeysError(err) {
5991
7536
  `You're not a member of this installation's workspace. API-key management requires workspace membership.`
5992
7537
  );
5993
7538
  return 1;
7539
+ case "NOT_WORKSPACE_ADMIN":
7540
+ case "FORBIDDEN":
7541
+ fail(
7542
+ `Not authorized. Minting scoped keys (with --grant) or keys on non-local installs requires workspace-admin; members can mint full-access keys on local dev installs only.`
7543
+ );
7544
+ return 1;
7545
+ case "INVALID_GRANT":
7546
+ fail(
7547
+ `A --grant referenced a table/app the backend rejected (${err.message}). Data Hub grants need a table or app that exists in this workspace; module grants need a table your manifest owns.`
7548
+ );
7549
+ return 1;
5994
7550
  case "INSTALLATION_NOT_FOUND":
5995
7551
  fail(
5996
7552
  `Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
@@ -6009,6 +7565,117 @@ function surfaceApiKeysError(err) {
6009
7565
  fail(`API-key operation failed: ${err.message}`);
6010
7566
  return 1;
6011
7567
  }
7568
+ var GRANT_OPERATIONS = ["create", "read", "update", "delete"];
7569
+ function parseGrants(specs) {
7570
+ if (!specs || specs.length === 0) return void 0;
7571
+ return specs.map((spec) => parseGrantSpec(spec));
7572
+ }
7573
+ function parseGrantSpec(spec) {
7574
+ const raw = spec.trim();
7575
+ const parts = raw.split(":");
7576
+ const source = parts[0];
7577
+ if (source === "module") {
7578
+ if (parts.length !== 3) {
7579
+ throw new Error(
7580
+ `Invalid --grant "${spec}". Module grants are \`module:<tableSlug>:<ops>\` (e.g. \`module:tasks:read,create\`).`
7581
+ );
7582
+ }
7583
+ const tableSlug = parts[1];
7584
+ if (!tableSlug) {
7585
+ throw new Error(`Invalid --grant "${spec}": missing table slug.`);
7586
+ }
7587
+ return {
7588
+ source: "module",
7589
+ tableSlug,
7590
+ operations: parseOps2(parts[2], spec)
7591
+ };
7592
+ }
7593
+ if (source === "datahub") {
7594
+ const scope = parts[1];
7595
+ if (!scope) {
7596
+ throw new Error(
7597
+ `Invalid --grant "${spec}". Data Hub grants are \`datahub:table=<id>:<ops>\`, \`datahub:app=<id>:<ops>\`, or \`datahub:workspace:<ops>\`.`
7598
+ );
7599
+ }
7600
+ if (scope === "workspace") {
7601
+ if (parts.length !== 3) {
7602
+ throw new Error(
7603
+ `Invalid --grant "${spec}". Workspace-wide Data Hub grants are \`datahub:workspace:<ops>\`.`
7604
+ );
7605
+ }
7606
+ return { source: "datahub", operations: parseOps2(parts[2], spec) };
7607
+ }
7608
+ const eq = scope.indexOf("=");
7609
+ const scopeKind = eq === -1 ? scope : scope.slice(0, eq);
7610
+ const scopeVal = eq === -1 ? "" : scope.slice(eq + 1);
7611
+ if (scopeKind !== "table" && scopeKind !== "app" || scopeVal === "") {
7612
+ throw new Error(
7613
+ `Invalid --grant "${spec}". Data Hub scope must be \`table=<id>\`, \`app=<id>\`, or \`workspace\`.`
7614
+ );
7615
+ }
7616
+ if (parts.length !== 3) {
7617
+ throw new Error(
7618
+ `Invalid --grant "${spec}". Data Hub grants are \`datahub:${scopeKind}=<id>:<ops>\`.`
7619
+ );
7620
+ }
7621
+ const id = Number.parseInt(scopeVal, 10);
7622
+ if (!Number.isInteger(id) || id <= 0 || String(id) !== scopeVal) {
7623
+ throw new Error(
7624
+ `Invalid --grant "${spec}": ${scopeKind} id must be a positive integer (got "${scopeVal}").`
7625
+ );
7626
+ }
7627
+ const operations = parseOps2(parts[2], spec);
7628
+ return scopeKind === "table" ? { source: "datahub", tableId: id, operations } : { source: "datahub", applicationId: id, operations };
7629
+ }
7630
+ throw new Error(
7631
+ `Invalid --grant "${spec}": unknown source "${source}". Use \`module:\u2026\` or \`datahub:\u2026\`.`
7632
+ );
7633
+ }
7634
+ function parseOps2(opsCsv, spec) {
7635
+ const ops = opsCsv.split(",").map((o) => o.trim()).filter(Boolean);
7636
+ if (ops.length === 0) {
7637
+ throw new Error(
7638
+ `Invalid --grant "${spec}": at least one operation is required (${GRANT_OPERATIONS.join(", ")}).`
7639
+ );
7640
+ }
7641
+ const seen = /* @__PURE__ */ new Set();
7642
+ for (const op of ops) {
7643
+ if (!GRANT_OPERATIONS.includes(op)) {
7644
+ throw new Error(
7645
+ `Invalid --grant "${spec}": unknown operation "${op}". Allowed: ${GRANT_OPERATIONS.join(", ")}.`
7646
+ );
7647
+ }
7648
+ seen.add(op);
7649
+ }
7650
+ return GRANT_OPERATIONS.filter((op) => seen.has(op));
7651
+ }
7652
+ function resolveExpiresAt(days) {
7653
+ if (days === void 0) return void 0;
7654
+ if (!Number.isFinite(days) || days <= 0) {
7655
+ throw new Error(`--expires must be a positive number of days (got ${days}).`);
7656
+ }
7657
+ return new Date(Date.now() + days * 24 * 60 * 60 * 1e3).toISOString();
7658
+ }
7659
+ function formatGrants(grants) {
7660
+ if (!grants || grants.length === 0) return [];
7661
+ const byScope = /* @__PURE__ */ new Map();
7662
+ for (const g of grants) {
7663
+ const label = grantScopeLabel(g);
7664
+ const existing = byScope.get(label);
7665
+ if (existing) existing.ops.add(g.operation);
7666
+ else byScope.set(label, { label, ops: /* @__PURE__ */ new Set([g.operation]) });
7667
+ }
7668
+ return [...byScope.values()].map((s) => {
7669
+ const ops = GRANT_OPERATIONS.filter((o) => s.ops.has(o));
7670
+ return `${s.label}: ${ops.join(", ")}`;
7671
+ });
7672
+ }
7673
+ function grantScopeLabel(g) {
7674
+ if (g.source === "module") return `module ${g.table_slug ?? "?"}`;
7675
+ if (g.table_id != null) return `datahub table#${g.table_id}`;
7676
+ if (g.application_id != null) return `datahub app#${g.application_id}`;
7677
+ return "datahub workspace";
7678
+ }
6012
7679
  function formatKeyStatus(k) {
6013
7680
  if (k.revoked_at) return dim("[revoked]");
6014
7681
  if (k.expires_at && new Date(k.expires_at).getTime() <= Date.now()) {
@@ -6028,6 +7695,190 @@ function formatRelative(d) {
6028
7695
  return `${day}d ago`;
6029
7696
  }
6030
7697
 
7698
+ // src/commands/origins.ts
7699
+ init_api_client();
7700
+ init_config();
7701
+ init_manifest_io();
7702
+ init_output();
7703
+ function loadContext5(opts) {
7704
+ const manifest = readManifestOrThrow(opts.dir);
7705
+ const slug = manifest.module?.slug;
7706
+ if (!slug) {
7707
+ throw new Error(
7708
+ `module.json has no module.slug \u2014 the manifest is malformed.`
7709
+ );
7710
+ }
7711
+ const cfg = readConfig();
7712
+ if (!cfg) {
7713
+ throw new Error("Not logged in. Run `dashwise login` first.");
7714
+ }
7715
+ const entry = readModuleEntry(slug);
7716
+ if (!entry) {
7717
+ throw new Error(
7718
+ `No dev install cached for module "${slug}". Run \`dashwise init ${slug} --force\` to provision one.`
7719
+ );
7720
+ }
7721
+ return {
7722
+ installationId: entry.installationId,
7723
+ slug,
7724
+ requestOptions: {
7725
+ baseUrl: resolveApiUrl(),
7726
+ auth: { token: cfg.token }
7727
+ }
7728
+ };
7729
+ }
7730
+ function normalizeOrigin(raw) {
7731
+ const trimmed = raw.trim();
7732
+ try {
7733
+ return new URL(trimmed).origin;
7734
+ } catch {
7735
+ return trimmed;
7736
+ }
7737
+ }
7738
+ async function originsListCommand(options) {
7739
+ let ctx;
7740
+ try {
7741
+ ctx = loadContext5(options);
7742
+ } catch (err) {
7743
+ fail(err.message);
7744
+ return 1;
7745
+ }
7746
+ let origins;
7747
+ try {
7748
+ const resp = await getOrigins(ctx.installationId, ctx.requestOptions);
7749
+ origins = resp.allowed_origins;
7750
+ } catch (err) {
7751
+ return surfaceOriginsError(err);
7752
+ }
7753
+ if (options.json) {
7754
+ process.stdout.write(`${JSON.stringify(origins, null, 2)}
7755
+ `);
7756
+ return 0;
7757
+ }
7758
+ info(
7759
+ `SSO callback origins for ${code(ctx.slug)} ${dim(
7760
+ "(localhost is always allowed for local dev installs)"
7761
+ )}:`
7762
+ );
7763
+ if (origins.length === 0) {
7764
+ info(` ${dim("(none configured \u2014 only implicit localhost applies)")}`);
7765
+ info("");
7766
+ info(
7767
+ `Add a deployed domain with ${code("dashwise origins add https://your-domain")}.`
7768
+ );
7769
+ return 0;
7770
+ }
7771
+ for (const o of origins) {
7772
+ process.stdout.write(` ${o}
7773
+ `);
7774
+ }
7775
+ return 0;
7776
+ }
7777
+ async function originsAddCommand(urlArg, options) {
7778
+ if (!urlArg) {
7779
+ fail("Usage: `dashwise origins add <url>` (e.g. `dashwise origins add https://app.example.com`)");
7780
+ return 1;
7781
+ }
7782
+ const origin = normalizeOrigin(urlArg);
7783
+ let ctx;
7784
+ try {
7785
+ ctx = loadContext5(options);
7786
+ } catch (err) {
7787
+ fail(err.message);
7788
+ return 1;
7789
+ }
7790
+ let current;
7791
+ try {
7792
+ current = (await getOrigins(ctx.installationId, ctx.requestOptions)).allowed_origins;
7793
+ } catch (err) {
7794
+ return surfaceOriginsError(err);
7795
+ }
7796
+ if (current.some((o) => normalizeOrigin(o) === origin)) {
7797
+ info(`${code(origin)} is already allowlisted for ${code(ctx.slug)}. Nothing to do.`);
7798
+ return 0;
7799
+ }
7800
+ const next = [...current, origin];
7801
+ try {
7802
+ const resp = await setOrigins(ctx.installationId, next, ctx.requestOptions);
7803
+ success(`Added ${code(origin)} to the SSO callback allowlist for ${code(ctx.slug)}.`);
7804
+ info(` ${dim(`${resp.allowed_origins.length} origin(s) now configured.`)}`);
7805
+ } catch (err) {
7806
+ return surfaceOriginsError(err);
7807
+ }
7808
+ return 0;
7809
+ }
7810
+ async function originsRemoveCommand(urlArg, options) {
7811
+ if (!urlArg) {
7812
+ fail("Usage: `dashwise origins remove <url>`");
7813
+ return 1;
7814
+ }
7815
+ const origin = normalizeOrigin(urlArg);
7816
+ let ctx;
7817
+ try {
7818
+ ctx = loadContext5(options);
7819
+ } catch (err) {
7820
+ fail(err.message);
7821
+ return 1;
7822
+ }
7823
+ let current;
7824
+ try {
7825
+ current = (await getOrigins(ctx.installationId, ctx.requestOptions)).allowed_origins;
7826
+ } catch (err) {
7827
+ return surfaceOriginsError(err);
7828
+ }
7829
+ const next = current.filter((o) => normalizeOrigin(o) !== origin);
7830
+ if (next.length === current.length) {
7831
+ fail(
7832
+ `${code(origin)} is not in the allowlist for ${code(ctx.slug)}. Run ${code("dashwise origins list")} to see current entries.`
7833
+ );
7834
+ return 1;
7835
+ }
7836
+ try {
7837
+ await setOrigins(ctx.installationId, next, ctx.requestOptions);
7838
+ success(`Removed ${code(origin)} from the SSO callback allowlist for ${code(ctx.slug)}.`);
7839
+ } catch (err) {
7840
+ return surfaceOriginsError(err);
7841
+ }
7842
+ return 0;
7843
+ }
7844
+ function surfaceOriginsError(err) {
7845
+ if (err instanceof ApiError) {
7846
+ switch (err.code) {
7847
+ case "UNAUTHENTICATED":
7848
+ fail(
7849
+ `Auth rejected by backend. Your CLI token may be stale \u2014 run ${code("dashwise login")} then retry.`
7850
+ );
7851
+ return 1;
7852
+ case "NOT_WORKSPACE_MEMBER":
7853
+ fail(
7854
+ `You're not a member of this installation's workspace. Origins management requires workspace membership.`
7855
+ );
7856
+ return 1;
7857
+ case "NOT_WORKSPACE_ADMIN":
7858
+ case "FORBIDDEN":
7859
+ fail(
7860
+ `Not authorized. Managing SSO callback origins requires workspace-admin.`
7861
+ );
7862
+ return 1;
7863
+ case "INVALID_ORIGIN":
7864
+ fail(
7865
+ `The backend rejected an origin (${err.message}). Non-localhost origins must be absolute https:// URLs.`
7866
+ );
7867
+ return 1;
7868
+ case "INSTALLATION_NOT_FOUND":
7869
+ fail(
7870
+ `Installation not found. It may have been destroyed; re-provision via ${code("dashwise init <slug>")}.`
7871
+ );
7872
+ return 1;
7873
+ default:
7874
+ fail(`Backend rejected: ${err.code} ${err.status} \u2014 ${err.message}`);
7875
+ return 1;
7876
+ }
7877
+ }
7878
+ fail(`Origins operation failed: ${err.message}`);
7879
+ return 1;
7880
+ }
7881
+
6031
7882
  // src/commands/env-export.ts
6032
7883
  init_api_client();
6033
7884
  init_config();
@@ -6117,16 +7968,6 @@ async function envExportCommand(options) {
6117
7968
  value: apiUrl,
6118
7969
  comment: "# DashWise backend URL \u2014 same value the CLI is logged into."
6119
7970
  },
6120
- {
6121
- key: "NEXT_PUBLIC_DASHWISE_API_URL",
6122
- value: apiUrl,
6123
- comment: "# Client-side mirror \u2014 Next.js inlines NEXT_PUBLIC_* into the browser bundle so <AuthProvider> hits the right origin."
6124
- },
6125
- {
6126
- key: "DASHWISE_INSTALLATION_ID",
6127
- value: String(installationId),
6128
- comment: `# Per-workspace install id for module "${slug}".`
6129
- },
6130
7971
  {
6131
7972
  key: "DASHWISE_API_KEY",
6132
7973
  value: apiKey,
@@ -6747,6 +8588,12 @@ program.name("dashwise").description("DashWise CLI \u2014 log in, author modules
6747
8588
  "--api-url <url>",
6748
8589
  "Override DashWise backend URL (default: $DASHWISE_API_URL or saved config or http://localhost:3000)"
6749
8590
  );
8591
+ program.hook("preAction", () => {
8592
+ const apiUrl = program.opts().apiUrl?.trim();
8593
+ if (apiUrl) {
8594
+ process.env.DASHWISE_API_URL = apiUrl.replace(/\/+$/, "");
8595
+ }
8596
+ });
6750
8597
  program.command("login").description("Sign in via the device-code flow.").option("--no-prompt", 'Skip the "open browser" prompt; just print the URL.').option("--name <name>", "Suggested name for the minted token (default: dashwise-cli).").action(async (cmdOpts) => {
6751
8598
  const apiUrl = program.opts().apiUrl;
6752
8599
  const exit = await loginCommand({
@@ -6947,8 +8794,15 @@ moduleCmd.command("clone <slug>").description("Clone a published module locally.
6947
8794
  });
6948
8795
  }
6949
8796
  );
6950
- var depsCmd = program.command("deps").description("Cross-module dependency commands (manipulate module.json#dependencies).");
6951
- depsCmd.command("list").description("List declared cross-module dependencies.").option("--dir <path>", "Project root (default: current directory).").option("--json", "Emit JSON to stdout instead of human-readable columns.").action(async (cmdOpts) => {
8797
+ var depsCmd = program.command("deps").description(
8798
+ "Cross-module dependency commands. Bare `dashwise deps` shows the live connect status (resolved / unbound) of each declared dep on your installation; the subcommands manage module.json#dependencies (add / remove / list / check) and the connect lifecycle (bind / unbind)."
8799
+ ).option("--dir <path>", "Project root (default: current directory).").option("--json", "Emit the connect-status list as JSON instead of the human-readable view.").action(async (cmdOpts) => {
8800
+ process.exitCode = await depsConnectListCommand({
8801
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
8802
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
8803
+ });
8804
+ });
8805
+ depsCmd.command("list").description("List declared cross-module dependencies (offline local module.json read).").option("--dir <path>", "Project root (default: current directory).").option("--json", "Emit JSON to stdout instead of human-readable columns.").action(async (cmdOpts) => {
6952
8806
  process.exitCode = await depsListCommand({
6953
8807
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
6954
8808
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
@@ -6980,6 +8834,93 @@ depsCmd.command("check").description("Verify each declared dep still resolves to
6980
8834
  ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
6981
8835
  });
6982
8836
  });
8837
+ depsCmd.command("bind <provider-slug>").description(
8838
+ "(Re-)connect a parked dependency to its provider installation. Re-runs resolution + snapshots the contract; on success runtime reads succeed. Fails (the dep stays unbound) with the park reason if the provider isn't installed / the version doesn't match / a table/action isn't exposed. Workspace-admin."
8839
+ ).option("--dir <path>", "Project root (default: current directory).").option(
8840
+ "--installation <id>",
8841
+ "Provider installation id to bind to (disambiguates when the workspace has more than one candidate install of the provider).",
8842
+ parseIntOption
8843
+ ).action(
8844
+ async (providerSlug, cmdOpts) => {
8845
+ process.exitCode = await depsBindCommand(providerSlug, {
8846
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
8847
+ ...cmdOpts.installation !== void 0 ? { installation: cmdOpts.installation } : {}
8848
+ });
8849
+ }
8850
+ );
8851
+ depsCmd.command("unbind <provider-slug>").description(
8852
+ "Disconnect a bound dependency: NULLs the provider ref + contract snapshot and parks the dep (reason MANUALLY_UNBOUND). Runtime reads then return DEP_UNBOUND until you bind it again. Workspace-admin."
8853
+ ).option("--dir <path>", "Project root (default: current directory).").action(async (providerSlug, cmdOpts) => {
8854
+ process.exitCode = await depsUnbindCommand(providerSlug, {
8855
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
8856
+ });
8857
+ });
8858
+ program.command("bind [uses-slug]").description(
8859
+ "Connect the module's declared workspace-table (`uses`) slots to real tables. Bare `dashwise bind` lists every slot's status (bound / unbound / declined) with reasons + missing fields. `dashwise bind <uses-slug> [--table <name-or-id>]` binds one (interactive pick when the match is ambiguous in a TTY); `--map <usesField>=<physicalFieldName>` overrides field mappings; `--create` materializes a fresh table; `--decline` declines an optional-needs slot. Bind/unbind/decline are workspace-admin."
8860
+ ).option("--dir <path>", "Project root (default: current directory).").option("--json", "With no slug: emit the binding-status list as JSON.").option(
8861
+ "--table <name-or-id>",
8862
+ "Workspace table (name or numeric id) to bind the slot to. Use for scripts / to skip the interactive pick."
8863
+ ).option(
8864
+ "--map <usesField=physicalFieldName>",
8865
+ "Override a field mapping (repeatable). The server auto-completes unmapped fields by name match.",
8866
+ collectRepeated,
8867
+ []
8868
+ ).option("--create", "Materialize a fresh Data Hub table from the declared fields + bind to it.").option("--decline", "Decline this slot (optional-needs only; the module runs without it).").action(
8869
+ async (usesSlug, cmdOpts) => {
8870
+ if (usesSlug === void 0) {
8871
+ process.exitCode = await bindListCommand({
8872
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
8873
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
8874
+ });
8875
+ return;
8876
+ }
8877
+ process.exitCode = await bindCommand(usesSlug, {
8878
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
8879
+ ...cmdOpts.table !== void 0 ? { table: cmdOpts.table } : {},
8880
+ ...cmdOpts.map !== void 0 && cmdOpts.map.length > 0 ? { map: cmdOpts.map } : {},
8881
+ ...cmdOpts.create !== void 0 ? { create: cmdOpts.create } : {},
8882
+ ...cmdOpts.decline !== void 0 ? { decline: cmdOpts.decline } : {}
8883
+ });
8884
+ }
8885
+ );
8886
+ program.command("unbind <uses-slug>").description(
8887
+ "Disconnect a bound workspace-table (`uses`) slot: NULLs the table ref + field map and parks the slot (reason MANUALLY_UNBOUND). Runtime reads/writes then return USES_UNBOUND until you bind it again. Workspace-admin."
8888
+ ).option("--dir <path>", "Project root (default: current directory).").action(async (usesSlug, cmdOpts) => {
8889
+ process.exitCode = await unbindCommand(usesSlug, {
8890
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
8891
+ });
8892
+ });
8893
+ var usesCmd = program.command("uses").description("Workspace-table (`uses`) contract commands.");
8894
+ usesCmd.command("add").description(
8895
+ 'On-ramp an existing workspace table: introspect it, generate the `uses.tables` manifest entry (fields + abstract types, read op by default), write module.json, push (draft-apply reconciles + parks), then bind the new slot to that table. One command from "I have an Employees table" to typed access.'
8896
+ ).option("--dir <path>", "Project root (default: current directory).").requiredOption(
8897
+ "--from-table <id>",
8898
+ "The existing workspace table to generate a `uses` contract from \u2014 its NUMERIC id (find it in the Dashwise UI URL). Resolving by table name isn't wired yet."
8899
+ ).option(
8900
+ "--slug <slug>",
8901
+ "Override the generated `uses` slot slug (default: slugified table name)."
8902
+ ).option(
8903
+ "--ops <list>",
8904
+ "Comma-separated operations to declare (create,read,update,delete). Default: read."
8905
+ ).option(
8906
+ "--purpose <text>",
8907
+ "Optional author-facing explanation, shown to admins at bind/consent."
8908
+ ).option(
8909
+ "--no-bind",
8910
+ "Only write + push the manifest entry; don't attempt to bind it to the source table."
8911
+ ).option("-y, --yes", "Skip the overwrite-existing-slot confirmation prompt.").action(
8912
+ async (cmdOpts) => {
8913
+ process.exitCode = await usesAddCommand({
8914
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
8915
+ fromTable: cmdOpts.fromTable,
8916
+ ...cmdOpts.slug !== void 0 ? { slug: cmdOpts.slug } : {},
8917
+ ...cmdOpts.ops !== void 0 ? { ops: cmdOpts.ops } : {},
8918
+ ...cmdOpts.purpose !== void 0 ? { purpose: cmdOpts.purpose } : {},
8919
+ ...cmdOpts.bind === false ? { noBind: true } : {},
8920
+ ...cmdOpts.yes !== void 0 ? { yes: cmdOpts.yes } : {}
8921
+ });
8922
+ }
8923
+ );
6983
8924
  program.command("query").description(
6984
8925
  'Run a typed query against the local module installation via @dashai/sdk/query. Pass one of `--inline "<expr>"`, `--file <path>`, or `--repl`. `qb`, `deps`, `db` are pre-bound to the local module\'s generated SDK.'
6985
8926
  ).option("--dir <path>", "Project root containing `module.json` (default: current directory).").option("--inline <expression>", "Evaluate a one-liner. e.g., `qb.tasks.where({done:false}).count()`.").option("--file <path>", "Dynamic-import a .mjs/.js file. Default export = Query | AggQuery | Promise | value.").option("--repl", "Open an interactive Node REPL with the generated SDK pre-bound. Top-level await supported.").option("--json", "Emit compact JSON (default: pretty-printed).").option(
@@ -7024,11 +8965,14 @@ apiKeysCmd.command("list").description(
7024
8965
  apiKeysCmd.command("create [name]").description(
7025
8966
  "Mint a new API key. The plaintext key is displayed ONCE \u2014 save it then."
7026
8967
  ).option("--dir <path>", "Project root (default: current directory).").option("--name <name>", "Override the positional name arg (e.g. for scripted use).").option(
7027
- "--scopes <csv>",
7028
- "Comma-separated scope strings (e.g. `runtime:read,runtime:write`). Default: full install access."
8968
+ "--grant <spec>",
8969
+ "Scoped grant (repeatable). `module:<tableSlug>:<ops>`, `datahub:table=<id>:<ops>`, `datahub:app=<id>:<ops>`, or `datahub:workspace:<ops>` where <ops> is a comma list of create,read,update,delete. Any grant mints a scoped (default-deny) key (workspace-admin required). Omit for a full-access key.",
8970
+ collectRepeated,
8971
+ []
7029
8972
  ).option(
7030
- "--expires-at <iso>",
7031
- "ISO-8601 expiry timestamp (e.g. 2026-12-31T23:59:59Z). Default: never expires."
8973
+ "--expires <days>",
8974
+ "Days until the key expires (converted to an absolute timestamp). Default: never expires.",
8975
+ parseIntOption
7032
8976
  ).option(
7033
8977
  "--raw-only",
7034
8978
  "Print only the plaintext key on stdout; chatter goes to stderr. Useful for shell capture: `KEY=$(dashwise api-keys create prod --raw-only)`"
@@ -7037,8 +8981,8 @@ apiKeysCmd.command("create [name]").description(
7037
8981
  process.exitCode = await apiKeysCreateCommand(nameArg, {
7038
8982
  ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
7039
8983
  ...cmdOpts.name !== void 0 ? { name: cmdOpts.name } : {},
7040
- ...cmdOpts.scopes !== void 0 ? { scopes: cmdOpts.scopes } : {},
7041
- ...cmdOpts.expiresAt !== void 0 ? { expiresAt: cmdOpts.expiresAt } : {},
8984
+ ...cmdOpts.grant !== void 0 && cmdOpts.grant.length > 0 ? { grant: cmdOpts.grant } : {},
8985
+ ...cmdOpts.expires !== void 0 ? { expires: cmdOpts.expires } : {},
7042
8986
  ...cmdOpts.rawOnly !== void 0 ? { rawOnly: cmdOpts.rawOnly } : {}
7043
8987
  });
7044
8988
  }
@@ -7053,6 +8997,27 @@ apiKeysCmd.command("revoke [key-id]").description(
7053
8997
  });
7054
8998
  }
7055
8999
  );
9000
+ var originsCmd = program.command("origins").description(
9001
+ "Manage the current module install's SSO callback-origin allowlist. Local dev (localhost) is always allowed; add deployed domains here."
9002
+ );
9003
+ originsCmd.command("list").description("Show the SSO callback origins configured for the current module install.").option("--dir <path>", "Project root (default: current directory).").option("--json", "Emit a JSON array instead of the human-friendly list.").action(async (cmdOpts) => {
9004
+ process.exitCode = await originsListCommand({
9005
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {},
9006
+ ...cmdOpts.json !== void 0 ? { json: cmdOpts.json } : {}
9007
+ });
9008
+ });
9009
+ originsCmd.command("add <url>").description(
9010
+ "Add an SSO callback origin (e.g. https://app.example.com). Idempotent; non-localhost origins must be https://."
9011
+ ).option("--dir <path>", "Project root (default: current directory).").action(async (url, cmdOpts) => {
9012
+ process.exitCode = await originsAddCommand(url, {
9013
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
9014
+ });
9015
+ });
9016
+ originsCmd.command("remove <url>").description("Remove an SSO callback origin from the allowlist.").option("--dir <path>", "Project root (default: current directory).").action(async (url, cmdOpts) => {
9017
+ process.exitCode = await originsRemoveCommand(url, {
9018
+ ...cmdOpts.dir !== void 0 ? { dir: cmdOpts.dir } : {}
9019
+ });
9020
+ });
7056
9021
  var envCmd = program.command("env").description("Environment-variable helpers for deploying a module.");
7057
9022
  envCmd.command("export").description(
7058
9023
  "Emit the env vars a deployed module needs to authenticate against DashWise. By default emits .env-style KEY=value lines; --format = env | json | railway | vercel | fly to wrap in platform-native syntax. Use --new-key <name> to mint + inline an API key in one step."
@@ -7266,7 +9231,7 @@ fieldCmd.command("set-type <table-slug> <field-slug> <new-type>").description(
7266
9231
  }
7267
9232
  })();
7268
9233
  function getVersion() {
7269
- return "0.8.1";
9234
+ return "0.10.0";
7270
9235
  }
7271
9236
  function parseIntOption(value) {
7272
9237
  const n = Number.parseInt(value, 10);