@ggui-ai/cli 0.2.0-alpha.3 → 0.3.0-rc.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.
Files changed (80) hide show
  1. package/README.md +38 -7
  2. package/dist/api-client.d.ts +42 -1
  3. package/dist/api-client.d.ts.map +1 -1
  4. package/dist/api-client.js +30 -2
  5. package/dist/auth-keys.d.ts +1 -1
  6. package/dist/auth-keys.d.ts.map +1 -1
  7. package/dist/auth-keys.js +22 -7
  8. package/dist/auth-store.d.ts +1 -1
  9. package/dist/auth-store.d.ts.map +1 -1
  10. package/dist/cli.js +112 -30
  11. package/dist/config-push.d.ts +79 -0
  12. package/dist/config-push.d.ts.map +1 -0
  13. package/dist/config-push.js +301 -0
  14. package/dist/create-app-command.d.ts +17 -0
  15. package/dist/create-app-command.d.ts.map +1 -0
  16. package/dist/create-app-command.js +58 -0
  17. package/dist/deploy-command.d.ts +67 -0
  18. package/dist/deploy-command.d.ts.map +1 -0
  19. package/dist/deploy-command.js +386 -0
  20. package/dist/dev-command.d.ts +5 -1
  21. package/dist/dev-command.d.ts.map +1 -1
  22. package/dist/dev-command.js +31 -1
  23. package/dist/export-pool-command.d.ts +25 -0
  24. package/dist/export-pool-command.d.ts.map +1 -0
  25. package/dist/export-pool-command.js +116 -0
  26. package/dist/filesystem-seed-pool-source.d.ts +13 -0
  27. package/dist/filesystem-seed-pool-source.d.ts.map +1 -0
  28. package/dist/filesystem-seed-pool-source.js +22 -0
  29. package/dist/generation-probe.d.ts +53 -7
  30. package/dist/generation-probe.d.ts.map +1 -1
  31. package/dist/generation-probe.js +37 -1
  32. package/dist/internal/artifact-install.d.ts +4 -2
  33. package/dist/internal/artifact-install.d.ts.map +1 -1
  34. package/dist/internal/artifact-install.js +11 -10
  35. package/dist/internal/artifact-publish.d.ts +34 -31
  36. package/dist/internal/artifact-publish.d.ts.map +1 -1
  37. package/dist/internal/artifact-publish.js +133 -241
  38. package/dist/internal/artifact-search.d.ts +2 -28
  39. package/dist/internal/artifact-search.d.ts.map +1 -1
  40. package/dist/internal/artifact-search.js +5 -4
  41. package/dist/internal/auth-strategy.d.ts +21 -31
  42. package/dist/internal/auth-strategy.d.ts.map +1 -1
  43. package/dist/internal/auth-strategy.js +15 -23
  44. package/dist/internal/register-author-key.d.ts +7 -1
  45. package/dist/internal/register-author-key.d.ts.map +1 -1
  46. package/dist/internal/register-author-key.js +56 -19
  47. package/dist/mcp-backend.d.ts +25 -25
  48. package/dist/mcp-backend.d.ts.map +1 -1
  49. package/dist/mcp-backend.js +65 -61
  50. package/dist/paths.d.ts +3 -3
  51. package/dist/paths.js +3 -3
  52. package/dist/persistent-secrets.js +1 -1
  53. package/dist/persistent-stores.d.ts +2 -2
  54. package/dist/persistent-stores.d.ts.map +1 -1
  55. package/dist/persistent-stores.js +5 -5
  56. package/dist/pool-artifact.d.ts +19 -0
  57. package/dist/pool-artifact.d.ts.map +1 -0
  58. package/dist/pool-artifact.js +59 -0
  59. package/dist/provider-key-command.d.ts +51 -0
  60. package/dist/provider-key-command.d.ts.map +1 -0
  61. package/dist/provider-key-command.js +222 -0
  62. package/dist/push-command.d.ts +47 -0
  63. package/dist/push-command.d.ts.map +1 -0
  64. package/dist/push-command.js +230 -0
  65. package/dist/serve-command.d.ts +23 -19
  66. package/dist/serve-command.d.ts.map +1 -1
  67. package/dist/serve-command.js +13 -26
  68. package/dist/theme-command.js +0 -2
  69. package/dist/tunnel-provider.d.ts +8 -7
  70. package/dist/tunnel-provider.d.ts.map +1 -1
  71. package/package.json +25 -21
  72. package/dist/internal/auth-cache.d.ts +0 -40
  73. package/dist/internal/auth-cache.d.ts.map +0 -1
  74. package/dist/internal/auth-cache.js +0 -133
  75. package/dist/persistent-short-code-index.d.ts +0 -14
  76. package/dist/persistent-short-code-index.d.ts.map +0 -1
  77. package/dist/persistent-short-code-index.js +0 -39
  78. package/dist/spawn-cloudflared.d.ts +0 -61
  79. package/dist/spawn-cloudflared.d.ts.map +0 -1
  80. package/dist/spawn-cloudflared.js +0 -167
@@ -20,11 +20,11 @@
20
20
  * platform=neutral) with manifest.peerDeps + react family
21
21
  * hoisted to externals.
22
22
  * 4. Authenticate — either `--auth=bearer` (explicit token for self-
23
- * hosted / local registries) OR the default hosted-auth flow via
24
- * the configured registry. The hosted-auth implementation here
25
- * talks to the cloud registry's hosted identity provider, but
26
- * the public-surface naming stays vendor-neutral (see
27
- * `./auth-strategy.ts`).
23
+ * hosted / local registries) OR the default: reuse the session
24
+ * `ggui login` already stored at `~/.ggui/auth.json`, refreshing
25
+ * the access token via `POST /v1/auth/refresh` when it has
26
+ * expired (see `./auth-strategy.ts` for the routing and
27
+ * `../auth-store.ts` for the on-disk document).
28
28
  * 5. Conformance preflight — POST `/conformance/check` with the
29
29
  * manifest + bundle text. Fail-fast so a malformed bundle
30
30
  * doesn't hit S3.
@@ -42,7 +42,7 @@
42
42
  * **HTTP contract (locked).**
43
43
  *
44
44
  * POST <registry>/publish
45
- * Headers: `Authorization: Bearer <jwt>`, `Content-Type: application/json`
45
+ * Headers: `Authorization: Bearer <token>`, `Content-Type: application/json`
46
46
  * Body : { manifest, bundle?, bundleSha384?, signature }
47
47
  * 201 : { artifactId, version, manifestUrl, bundleUrl?, signatureUrl?, installCommand }
48
48
  * 400 : { code: 'version_exists' | 'unknown_key' | 'conformance_failed' | … , message }
@@ -50,26 +50,23 @@
50
50
  * 501 : endpoint stubbed
51
51
  *
52
52
  * POST <registry>/conformance/check
53
- * Headers: `Authorization: Bearer <jwt>`, `Content-Type: application/json`
53
+ * Headers: `Authorization: Bearer <token>`, `Content-Type: application/json`
54
54
  * Body : { manifest, bundle? }
55
55
  * 200 : { ok: true }
56
56
  * 200 : { ok: false, issues: [{code, message, path?}] }
57
57
  * 5xx : transient — caller retries or surfaces
58
58
  */
59
59
  import { build as esbuild } from 'esbuild';
60
- // Cloud-internal hosted-auth implementation. The vendor SDK is named
61
- // here because this is the leaf implementation file the OSS-facing
62
- // `acquireHostedAuthJwt` wraps; the public surface (CLI flags, help
63
- // text, README, the auth-strategy module) is vendor-neutral.
64
- import { CognitoIdentityProviderClient, InitiateAuthCommand, } from '@aws-sdk/client-cognito-identity-provider';
65
60
  import { existsSync, readFileSync } from 'node:fs';
66
61
  import { dirname, isAbsolute, join, resolve } from 'node:path';
67
62
  import { URL } from 'node:url';
68
63
  import { GGUI_BLUEPRINT_JSON_FILENAME, GGUI_GADGET_JSON_FILENAME, parseBlueprintManifest, parseGadgetManifest, } from '@ggui-ai/artifact-manifest';
69
64
  import { canonicalJson, derivePublicKeyId, generateEd25519Keypair, publicKeyFromPrivate, signBundleEd25519, signBundleSigstore, } from '@ggui-ai/gadget-signing';
65
+ import { PROTOCOL_VERSION } from '@ggui-ai/protocol';
70
66
  import { resolveOidcToken, OidcResolutionError, } from './oidc-token.js';
71
67
  import { sha384 } from '@noble/hashes/sha512.js';
72
- import { isTokenFresh, loadRegistryToken, saveRegistryToken, } from './auth-cache.js';
68
+ import { saveAuthSession, tryLoadAuthSession, } from '../auth-store.js';
69
+ import { ApiError, postAuthRefresh } from '../api-client.js';
73
70
  import { getPrivateKeyPath, hasKeypair, readPrivateKey, scopeOf, writePrivateKey, } from './key-store.js';
74
71
  import { acquireAuthToken, parseAuthFlags, AUTH_HELP_FRAGMENT, } from './auth-strategy.js';
75
72
  /**
@@ -224,8 +221,21 @@ export async function runArtifactPublish(opts) {
224
221
  }
225
222
  return { ok: false, exitCode: 1, error: manifestRes.error };
226
223
  }
227
- const { manifest, manifestPath } = manifestRes;
228
- // ---- step 2a: enforce verb / manifest kind alignment ----
224
+ const { manifestPath } = manifestRes;
225
+ // ---- step 2a: stamp the persistence-contract era (blueprints) ----
226
+ // `generatorProtocolVersion` is the import-gate era stamp (mirrors
227
+ // `PortableBlueprint`): deployments that feed installed blueprints
228
+ // into a matcher pool REQUIRE it and drop unstamped rows loudly.
229
+ // The SIGNING toolchain owns the claim — it validates the manifest
230
+ // against its own protocol era and signs the stamped bytes — so the
231
+ // stamp is set here, BEFORE the signature payload is computed, and
232
+ // overwrites any hand-authored value (an author cannot truthfully
233
+ // claim a different toolchain's era). Gadgets never enter a matcher
234
+ // pool; they stay unstamped.
235
+ const manifest = manifestRes.manifest.kind === 'blueprint'
236
+ ? { ...manifestRes.manifest, generatorProtocolVersion: PROTOCOL_VERSION }
237
+ : manifestRes.manifest;
238
+ // ---- step 2b: enforce verb / manifest kind alignment ----
229
239
  if (manifest.kind !== opts.kind) {
230
240
  const foundFile = manifest.kind === 'blueprint'
231
241
  ? GGUI_BLUEPRINT_JSON_FILENAME
@@ -259,34 +269,26 @@ export async function runArtifactPublish(opts) {
259
269
  // ---- step 4: authenticate ----
260
270
  // Two paths:
261
271
  // --auth=bearer → token from flag or GGUI_REGISTRY_TOKEN
262
- // default → hosted auth via the configured registry (cloud)
263
- let jwt;
272
+ // default → the `ggui login` session stored at ~/.ggui/auth.json
273
+ let token;
264
274
  const authFlags = opts.auth ?? {};
275
+ const authKind = authFlags.auth === 'bearer' ? 'bearer' : 'session';
265
276
  try {
266
- jwt = await acquireAuthToken({
277
+ token = await acquireAuthToken({
267
278
  flags: authFlags,
268
279
  env: process.env,
269
- cwd,
270
- registryUrl,
271
- acquireHostedAuthJwt: async (deps) => {
272
- const hosted = await acquireHostedAuthJwt({
273
- registryUrl: deps.registryUrl,
274
- env: deps.env,
275
- cwd: deps.cwd,
276
- hostedAuthClient: opts.hostedAuthClient,
277
- prompt: opts.prompt,
278
- now,
279
- });
280
- if (!hosted.ok) {
281
- throw new HostedAuthError(hosted.error);
280
+ acquireSessionToken: async () => {
281
+ const session = await acquireLoginSessionToken({ now, fetchImpl });
282
+ if (!session.ok) {
283
+ throw new SessionAuthError(session.error);
282
284
  }
283
- stdout(`auth: ${hosted.fromCache ? 'cached' : 'fresh'} JWT for ${hosted.token.username}\n`);
284
- return hosted.token.idToken;
285
+ stdout(`auth: ggui login session${session.refreshed ? ' (refreshed)' : ''}\n`);
286
+ return session.accessToken;
285
287
  },
286
288
  });
287
289
  }
288
290
  catch (err) {
289
- if (err instanceof HostedAuthError) {
291
+ if (err instanceof SessionAuthError) {
290
292
  stderr(`${verb}: ${err.payload.message}\n`);
291
293
  return { ok: false, exitCode: 1, error: err.payload };
292
294
  }
@@ -294,18 +296,24 @@ export async function runArtifactPublish(opts) {
294
296
  stderr(`${verb}: ${msg}\n`);
295
297
  return { ok: false, exitCode: 1, error: { code: 'auth_failed', message: msg } };
296
298
  }
297
- if (authFlags.auth === 'bearer') {
298
- stdout(`auth: bearer token (length=${jwt.length})\n`);
299
+ if (authKind === 'bearer') {
300
+ stdout(`auth: bearer token (length=${token.length})\n`);
299
301
  }
300
302
  // ---- step 5: conformance preflight ----
301
303
  const preflightRes = await runConformancePreflight({
302
304
  registryUrl,
303
- jwt,
305
+ token,
306
+ authKind,
304
307
  manifest,
305
308
  bundleBytes,
306
309
  fetchImpl,
307
310
  });
308
311
  if (!preflightRes.ok) {
312
+ if (preflightRes.error.code === 'auth_failed') {
313
+ // 401 from the registry — an auth problem, not a conformance one.
314
+ stderr(`${verb}: ${preflightRes.error.message}\n`);
315
+ return { ok: false, exitCode: 1, error: preflightRes.error };
316
+ }
309
317
  stderr(`${verb}: conformance preflight failed\n`);
310
318
  if (preflightRes.error.code === 'conformance_failed' && preflightRes.error.issues) {
311
319
  for (const issue of preflightRes.error.issues) {
@@ -402,7 +410,8 @@ export async function runArtifactPublish(opts) {
402
410
  }
403
411
  const publishRes = await postPublish({
404
412
  registryUrl,
405
- jwt,
413
+ token,
414
+ authKind,
406
415
  manifest,
407
416
  bundleBytes,
408
417
  bundleSha384,
@@ -625,240 +634,105 @@ async function bundleGadget(manifest, manifestDir) {
625
634
  }
626
635
  }
627
636
  /**
628
- * Sentinel error that lets the bearer/hosted strategy router surface
637
+ * Sentinel error that lets the bearer/session strategy router surface
629
638
  * a structured `PublishError` without leaking through `throw any`.
630
639
  */
631
- class HostedAuthError extends Error {
640
+ class SessionAuthError extends Error {
632
641
  payload;
633
642
  constructor(payload) {
634
643
  super(payload.message);
635
644
  this.payload = payload;
636
645
  }
637
646
  }
638
- export async function acquireHostedAuthJwt(opts) {
639
- // 1. Cached?
640
- const cached = loadRegistryToken(opts.registryUrl);
641
- if (cached && cached.registry === opts.registryUrl && isTokenFresh(cached, opts.now())) {
642
- return { ok: true, token: cached, fromCache: true };
643
- }
644
- // 2. Need auth config (Cognito pool id + client id). Personal
645
- // credentials never go in source-controlled ggui.json: auth config
646
- // lives in env vars or a user-level secret store. No fallback to
647
- // project config.
648
- const authConfig = resolveCognitoConfig(opts.env);
649
- if (!authConfig) {
647
+ /** Safety margin (seconds) before access-token expiry at which the CLI
648
+ * proactively refreshes instead of racing a boundary rejection. */
649
+ const ACCESS_TOKEN_FRESHNESS_MARGIN_SECONDS = 60;
650
+ /**
651
+ * Resolve the `ggui login` session token for registry calls.
652
+ *
653
+ * 1. Load `~/.ggui/auth.json` (written by `ggui login`). Missing /
654
+ * malformed → `auth_config_missing` telling the operator to either
655
+ * `ggui login` or use `--auth=bearer`.
656
+ * 2. Fresh access token → return it as-is.
657
+ * 3. Expired access token + live refresh token → `POST
658
+ * <session.endpoint>/v1/auth/refresh`, persist the rotated session,
659
+ * return the new access token.
660
+ * 4. Expired refresh token (or refresh rejected) → `auth_failed`
661
+ * telling the operator to `ggui login` again.
662
+ */
663
+ export async function acquireLoginSessionToken(opts) {
664
+ const session = tryLoadAuthSession();
665
+ if (!session) {
650
666
  return {
651
667
  ok: false,
652
668
  error: {
653
669
  code: 'auth_config_missing',
654
- message: 'registry hosted-auth config missing. Either:\n' +
655
- ' - pass --auth=bearer --token <token> (for self-hosted / local registries), or\n' +
656
- ' - set GGUI_REGISTRY_COGNITO_POOL_ID + GGUI_REGISTRY_COGNITO_APP_CLIENT_ID in your environment.',
670
+ message: 'no registry credentials found. Either:\n' +
671
+ ' - run `ggui login` to sign in, or\n' +
672
+ ' - pass --auth=bearer --token <token> (or set GGUI_REGISTRY_TOKEN) for self-hosted / local registries.',
657
673
  },
658
674
  };
659
675
  }
660
- // 3. Refresh-token path (cache exists but expired)
661
- if (cached && cached.refreshToken) {
662
- const refreshed = await tryRefresh({
663
- ...(opts.hostedAuthClient ? { hostedAuthClient: opts.hostedAuthClient } : {}),
664
- authConfig,
665
- refreshToken: cached.refreshToken,
666
- registryUrl: opts.registryUrl,
667
- username: cached.username,
668
- now: opts.now,
669
- });
670
- if (refreshed.ok) {
671
- saveRegistryToken(refreshed.token);
672
- return { ok: true, token: refreshed.token, fromCache: false };
673
- }
674
- // refresh failed — fall through to password flow
676
+ const nowSeconds = opts.now();
677
+ if (session.accessExpiresAt - nowSeconds > ACCESS_TOKEN_FRESHNESS_MARGIN_SECONDS) {
678
+ return { ok: true, accessToken: session.accessToken, refreshed: false };
675
679
  }
676
- // 4. Password flow
677
- const promptFn = opts.prompt ?? defaultPrompt;
678
- let username;
679
- let password;
680
- try {
681
- username = await promptFn('Registry username: ');
682
- password = await promptFn('Registry password: ', { hidden: true });
683
- }
684
- catch (err) {
680
+ if (session.refreshExpiresAt <= nowSeconds) {
685
681
  return {
686
682
  ok: false,
687
683
  error: {
688
684
  code: 'auth_failed',
689
- message: `prompt failed: ${err instanceof Error ? err.message : String(err)}`,
685
+ message: 'login session expired. Run `ggui login` again.',
690
686
  },
691
687
  };
692
688
  }
693
- const idp = opts.hostedAuthClient ?? new CognitoIdentityProviderClient({});
694
- const auth = await runUserPasswordAuth({
695
- hostedAuthClient: idp,
696
- authConfig,
697
- username,
698
- password,
699
- });
700
- if (!auth.ok) {
701
- return { ok: false, error: { code: 'auth_failed', message: auth.error } };
702
- }
703
- const token = {
704
- version: 1,
705
- registry: opts.registryUrl,
706
- idToken: auth.idToken,
707
- accessToken: auth.accessToken,
708
- refreshToken: auth.refreshToken,
709
- expiresAt: opts.now() + auth.expiresIn,
710
- username,
711
- writtenAt: new Date().toISOString(),
712
- };
713
- saveRegistryToken(token);
714
- return { ok: true, token, fromCache: false };
715
- }
716
- /**
717
- * Resolve Cognito auth config from environment variables only.
718
- *
719
- * `ggui.json` is source-controlled, so personal credentials never go
720
- * in git. The publish CLI's auth config comes exclusively from the
721
- * canonical env-var pair:
722
- *
723
- * `GGUI_REGISTRY_COGNITO_POOL_ID` + `GGUI_REGISTRY_COGNITO_APP_CLIENT_ID`
724
- *
725
- * The `GGUI_REGISTRY_` prefix matches other registry-scoped vars
726
- * (`GGUI_REGISTRY_TOKEN`, `GGUI_REGISTRY`). There is deliberately no
727
- * unprefixed `GGUI_COGNITO_*` fallback — a second accepted form would
728
- * re-introduce env-var ambiguity.
729
- *
730
- * No project-config fallback. Operators who want per-project auth
731
- * pinning use a per-project `.envrc` (direnv) or shell wrapper.
732
- */
733
- function resolveCognitoConfig(env) {
734
- const poolId = env['GGUI_REGISTRY_COGNITO_POOL_ID'];
735
- const appClientId = env['GGUI_REGISTRY_COGNITO_APP_CLIENT_ID'];
736
- if (poolId && appClientId) {
737
- return { poolId, appClientId };
738
- }
739
- return null;
740
- }
741
- async function runUserPasswordAuth(opts) {
742
- const input = {
743
- AuthFlow: 'USER_PASSWORD_AUTH',
744
- ClientId: opts.authConfig.appClientId,
745
- AuthParameters: {
746
- USERNAME: opts.username,
747
- PASSWORD: opts.password,
748
- },
749
- };
689
+ let tokens;
750
690
  try {
751
- const cmd = new InitiateAuthCommand(input);
752
- const res = await opts.hostedAuthClient.send(cmd);
753
- return extractAuthResult(res);
691
+ tokens = await postAuthRefresh(session.endpoint, session.refreshToken, opts.fetchImpl);
754
692
  }
755
693
  catch (err) {
694
+ if (err instanceof ApiError && (err.status === 400 || err.status === 401)) {
695
+ return {
696
+ ok: false,
697
+ error: {
698
+ code: 'auth_failed',
699
+ message: 'login session expired or was revoked. Run `ggui login` again.',
700
+ },
701
+ };
702
+ }
756
703
  return {
757
704
  ok: false,
758
- error: err instanceof Error ? err.message : String(err),
759
- };
760
- }
761
- }
762
- async function tryRefresh(opts) {
763
- const idp = opts.hostedAuthClient ?? new CognitoIdentityProviderClient({});
764
- const input = {
765
- AuthFlow: 'REFRESH_TOKEN_AUTH',
766
- ClientId: opts.authConfig.appClientId,
767
- AuthParameters: {
768
- REFRESH_TOKEN: opts.refreshToken,
769
- },
770
- };
771
- try {
772
- const res = await idp.send(new InitiateAuthCommand(input));
773
- const extracted = extractAuthResult(res);
774
- if (!extracted.ok)
775
- return { ok: false, error: extracted.error };
776
- // Cognito's REFRESH_TOKEN flow returns a new RefreshToken only
777
- // when rotation is enabled on the pool. When absent, reuse the
778
- // existing refresh token (still valid until its TTL).
779
- const refreshToken = extracted.refreshToken.length > 0 ? extracted.refreshToken : opts.refreshToken;
780
- return {
781
- ok: true,
782
- token: {
783
- version: 1,
784
- registry: opts.registryUrl,
785
- idToken: extracted.idToken,
786
- accessToken: extracted.accessToken,
787
- refreshToken,
788
- expiresAt: opts.now() + extracted.expiresIn,
789
- username: opts.username,
790
- writtenAt: new Date().toISOString(),
705
+ error: {
706
+ code: 'auth_failed',
707
+ message: `token refresh failed: ${err instanceof Error ? err.message : String(err)}`,
791
708
  },
792
709
  };
793
710
  }
794
- catch (err) {
795
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
796
- }
797
- }
798
- function extractAuthResult(res) {
799
- const result = res.AuthenticationResult;
800
- if (!result) {
801
- return {
802
- ok: false,
803
- error: `Cognito did not return AuthenticationResult (challenge=${res.ChallengeName ?? 'none'})`,
804
- };
805
- }
806
- if (typeof result.IdToken !== 'string' ||
807
- typeof result.AccessToken !== 'string' ||
808
- typeof result.ExpiresIn !== 'number') {
809
- return {
810
- ok: false,
811
- error: 'Cognito AuthenticationResult missing IdToken/AccessToken/ExpiresIn',
812
- };
813
- }
814
- return {
815
- ok: true,
816
- idToken: result.IdToken,
817
- accessToken: result.AccessToken,
818
- refreshToken: typeof result.RefreshToken === 'string' ? result.RefreshToken : '',
819
- expiresIn: result.ExpiresIn,
711
+ const refreshedSession = {
712
+ ...session,
713
+ accessToken: tokens.access_token,
714
+ accessExpiresAt: nowSeconds + tokens.expires_in,
715
+ refreshToken: tokens.refresh_token,
716
+ writtenAt: new Date().toISOString(),
820
717
  };
718
+ saveAuthSession(refreshedSession);
719
+ return { ok: true, accessToken: refreshedSession.accessToken, refreshed: true };
821
720
  }
822
- async function defaultPrompt(label, opts = {}) {
823
- // Minimal stdin reader. For hidden mode we toggle the TTY's `echo`
824
- // off. The dependency-injected `prompt` in `runGadgetPublish` is
825
- // the test seam — this is the fallback for real CLI use.
826
- process.stdout.write(label);
827
- const stdin = process.stdin;
828
- if (!stdin.isTTY) {
829
- throw new Error('stdin is not a TTY — pass --registry / set GGUI_REGISTRY and use a non-interactive auth path');
830
- }
831
- stdin.setEncoding('utf8');
832
- if (opts.hidden) {
833
- stdin.setRawMode(true);
834
- }
835
- let buf = '';
836
- for await (const chunk of stdin) {
837
- for (const ch of chunk) {
838
- if (ch === '\r' || ch === '\n') {
839
- if (opts.hidden)
840
- stdin.setRawMode(false);
841
- process.stdout.write('\n');
842
- return buf;
843
- }
844
- if (ch === '') {
845
- // Ctrl-C — propagate via throw so the caller exits cleanly.
846
- if (opts.hidden)
847
- stdin.setRawMode(false);
848
- throw new Error('prompt cancelled');
849
- }
850
- if (ch === '' || ch === '\b') {
851
- buf = buf.slice(0, -1);
852
- continue;
853
- }
854
- buf += ch;
855
- if (!opts.hidden)
856
- process.stdout.write(ch);
857
- }
858
- }
859
- if (opts.hidden)
860
- stdin.setRawMode(false);
861
- return buf;
721
+ /**
722
+ * Operator-facing diagnosis for an HTTP 401 from the registry,
723
+ * specialized on which credential was sent. Used by both the
724
+ * conformance preflight and the publish POST.
725
+ */
726
+ function describeRegistryAuthRejection(authKind, path) {
727
+ if (authKind === 'bearer') {
728
+ return (`registry rejected the bearer token (HTTP 401 from ${path}). ` +
729
+ 'Check that --token / GGUI_REGISTRY_TOKEN matches the publish token the registry is configured with.');
730
+ }
731
+ return ('registry rejected the `ggui login` session token (HTTP 401 from ' +
732
+ `${path}). Re-run \`ggui login\` — the session may have expired ` +
733
+ 'or been revoked. For self-hosted registries that authenticate ' +
734
+ 'with a static publish token instead of login sessions, pass ' +
735
+ '--auth=bearer --token <token> (or set GGUI_REGISTRY_TOKEN).');
862
736
  }
863
737
  async function runConformancePreflight(opts) {
864
738
  const url = `${opts.registryUrl}/conformance/check`;
@@ -874,7 +748,7 @@ async function runConformancePreflight(opts) {
874
748
  method: 'POST',
875
749
  headers: {
876
750
  'Content-Type': 'application/json',
877
- Authorization: `Bearer ${opts.jwt}`,
751
+ Authorization: `Bearer ${opts.token}`,
878
752
  },
879
753
  body,
880
754
  });
@@ -888,6 +762,15 @@ async function runConformancePreflight(opts) {
888
762
  },
889
763
  };
890
764
  }
765
+ if (res.status === 401) {
766
+ return {
767
+ ok: false,
768
+ error: {
769
+ code: 'auth_failed',
770
+ message: describeRegistryAuthRejection(opts.authKind, '/conformance/check'),
771
+ },
772
+ };
773
+ }
891
774
  if (!res.ok) {
892
775
  let text = '';
893
776
  try {
@@ -1043,7 +926,7 @@ async function postPublish(opts) {
1043
926
  method: 'POST',
1044
927
  headers: {
1045
928
  'Content-Type': 'application/json',
1046
- Authorization: `Bearer ${opts.jwt}`,
929
+ Authorization: `Bearer ${opts.token}`,
1047
930
  },
1048
931
  body,
1049
932
  });
@@ -1057,6 +940,15 @@ async function postPublish(opts) {
1057
940
  },
1058
941
  };
1059
942
  }
943
+ if (res.status === 401) {
944
+ return {
945
+ ok: false,
946
+ error: {
947
+ code: 'auth_failed',
948
+ message: describeRegistryAuthRejection(opts.authKind, '/publish'),
949
+ },
950
+ };
951
+ }
1060
952
  if (res.status === 501) {
1061
953
  return {
1062
954
  ok: false,
@@ -1,31 +1,5 @@
1
- /**
2
- * Artifact-kind discriminator. Mirrors the `gadgetManifestSchema` /
3
- * `blueprintManifestSchema` discriminators in
4
- * `@ggui-ai/artifact-manifest`. Inlined here because the manifest
5
- * package does not currently export a named `ArtifactKind` type — when
6
- * it does, swap to a structural `import type` from there.
7
- */
8
- export type ArtifactKind = 'gadget' | 'blueprint';
9
- /**
10
- * One row of `GET /search?…` results. Mirrors the registry's
11
- * `SearchResultEntry` response type.
12
- */
13
- export interface SearchResultEntry {
14
- readonly artifactId: string;
15
- readonly latestVersion: string;
16
- readonly kind: ArtifactKind;
17
- readonly description?: string;
18
- readonly tags?: readonly string[];
19
- readonly publishedAt: string;
20
- }
21
- /**
22
- * `GET /search?…` response body. `nextCursor` is an opaque base64
23
- * string the client roundtrips verbatim — never inspected client-side.
24
- */
25
- export interface SearchResponse {
26
- readonly results: readonly SearchResultEntry[];
27
- readonly nextCursor?: string;
28
- }
1
+ import { type ArtifactKind, type SearchResponse, type SearchResultEntry } from '@ggui-ai/registry-core';
2
+ export type { ArtifactKind, SearchResponse, SearchResultEntry };
29
3
  /**
30
4
  * Flag bundle accepted by {@link runArtifactSearch}. Mirrors the CLI's
31
5
  * positional + flag surface. Built either from `process.argv` (via
@@ -1 +1 @@
1
- {"version":3,"file":"artifact-search.d.ts","sourceRoot":"","sources":["../../src/internal/artifact-search.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"artifact-search.d.ts","sourceRoot":"","sources":["../../src/internal/artifact-search.ts"],"names":[],"mappings":"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"}
@@ -18,13 +18,14 @@
18
18
  * Returns a `SearchOutput` discriminated union; the CLI driver in
19
19
  * `gadget-command.ts` renders copy + picks the exit code.
20
20
  *
21
- * The wire shapes below mirror the registry's HTTP response types.
22
- * Adding fields is a minor change; removing or renaming a field is
23
- * breaking.
21
+ * The wire shapes (`SearchResultEntry` / `SearchResponse` /
22
+ * `ArtifactKind`) are imported from `@ggui-ai/registry-core` — the
23
+ * canonical owner of the registry HTTP response types — and
24
+ * re-exported for this module's consumers.
24
25
  */
25
26
  import { existsSync, readFileSync } from 'node:fs';
26
27
  import { dirname, join, resolve } from 'node:path';
27
- import { SEARCH_ERROR_CODES } from '@ggui-ai/registry-core';
28
+ import { SEARCH_ERROR_CODES, } from '@ggui-ai/registry-core';
28
29
  /**
29
30
  * Filename constant for the project manifest. Duplicated from
30
31
  * `@ggui-ai/project-config` rather than imported to keep this