@ggui-ai/cli 0.1.0-rc.1

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 (117) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +96 -0
  3. package/dist/agent-resolution.d.ts +41 -0
  4. package/dist/agent-resolution.d.ts.map +1 -0
  5. package/dist/agent-resolution.js +80 -0
  6. package/dist/api-client.d.ts +59 -0
  7. package/dist/api-client.d.ts.map +1 -0
  8. package/dist/api-client.js +145 -0
  9. package/dist/auth-keys.d.ts +10 -0
  10. package/dist/auth-keys.d.ts.map +1 -0
  11. package/dist/auth-keys.js +382 -0
  12. package/dist/auth-login.d.ts +11 -0
  13. package/dist/auth-login.d.ts.map +1 -0
  14. package/dist/auth-login.js +231 -0
  15. package/dist/auth-logout.d.ts +3 -0
  16. package/dist/auth-logout.d.ts.map +1 -0
  17. package/dist/auth-logout.js +32 -0
  18. package/dist/auth-store.d.ts +67 -0
  19. package/dist/auth-store.d.ts.map +1 -0
  20. package/dist/auth-store.js +150 -0
  21. package/dist/auth-whoami.d.ts +3 -0
  22. package/dist/auth-whoami.d.ts.map +1 -0
  23. package/dist/auth-whoami.js +52 -0
  24. package/dist/blueprint-command.d.ts +3 -0
  25. package/dist/blueprint-command.d.ts.map +1 -0
  26. package/dist/blueprint-command.js +203 -0
  27. package/dist/blueprint-create.d.ts +71 -0
  28. package/dist/blueprint-create.d.ts.map +1 -0
  29. package/dist/blueprint-create.js +377 -0
  30. package/dist/byok-resolver.d.ts +141 -0
  31. package/dist/byok-resolver.d.ts.map +1 -0
  32. package/dist/byok-resolver.js +89 -0
  33. package/dist/cli.d.ts +3 -0
  34. package/dist/cli.d.ts.map +1 -0
  35. package/dist/cli.js +1066 -0
  36. package/dist/dev-command.d.ts +169 -0
  37. package/dist/dev-command.d.ts.map +1 -0
  38. package/dist/dev-command.js +222 -0
  39. package/dist/gadget-command.d.ts +3 -0
  40. package/dist/gadget-command.d.ts.map +1 -0
  41. package/dist/gadget-command.js +181 -0
  42. package/dist/gadget-create.d.ts +106 -0
  43. package/dist/gadget-create.d.ts.map +1 -0
  44. package/dist/gadget-create.js +492 -0
  45. package/dist/generation-probe.d.ts +135 -0
  46. package/dist/generation-probe.d.ts.map +1 -0
  47. package/dist/generation-probe.js +132 -0
  48. package/dist/internal/artifact-install.d.ts +150 -0
  49. package/dist/internal/artifact-install.d.ts.map +1 -0
  50. package/dist/internal/artifact-install.js +1019 -0
  51. package/dist/internal/artifact-publish.d.ts +204 -0
  52. package/dist/internal/artifact-publish.d.ts.map +1 -0
  53. package/dist/internal/artifact-publish.js +1141 -0
  54. package/dist/internal/artifact-search.d.ts +155 -0
  55. package/dist/internal/artifact-search.d.ts.map +1 -0
  56. package/dist/internal/artifact-search.js +539 -0
  57. package/dist/internal/artifact-uninstall.d.ts +30 -0
  58. package/dist/internal/artifact-uninstall.d.ts.map +1 -0
  59. package/dist/internal/artifact-uninstall.js +205 -0
  60. package/dist/internal/auth-cache.d.ts +40 -0
  61. package/dist/internal/auth-cache.d.ts.map +1 -0
  62. package/dist/internal/auth-cache.js +133 -0
  63. package/dist/internal/auth-strategy.d.ts +89 -0
  64. package/dist/internal/auth-strategy.d.ts.map +1 -0
  65. package/dist/internal/auth-strategy.js +132 -0
  66. package/dist/internal/ggui-json.d.ts +53 -0
  67. package/dist/internal/ggui-json.d.ts.map +1 -0
  68. package/dist/internal/ggui-json.js +92 -0
  69. package/dist/internal/key-store.d.ts +43 -0
  70. package/dist/internal/key-store.d.ts.map +1 -0
  71. package/dist/internal/key-store.js +128 -0
  72. package/dist/internal/oidc-token.d.ts +52 -0
  73. package/dist/internal/oidc-token.d.ts.map +1 -0
  74. package/dist/internal/oidc-token.js +351 -0
  75. package/dist/internal/register-author-key.d.ts +44 -0
  76. package/dist/internal/register-author-key.d.ts.map +1 -0
  77. package/dist/internal/register-author-key.js +212 -0
  78. package/dist/local-keys.d.ts +47 -0
  79. package/dist/local-keys.d.ts.map +1 -0
  80. package/dist/local-keys.js +115 -0
  81. package/dist/mcp-backend.d.ts +356 -0
  82. package/dist/mcp-backend.d.ts.map +1 -0
  83. package/dist/mcp-backend.js +668 -0
  84. package/dist/paths.d.ts +77 -0
  85. package/dist/paths.d.ts.map +1 -0
  86. package/dist/paths.js +119 -0
  87. package/dist/persistent-secrets.d.ts +28 -0
  88. package/dist/persistent-secrets.d.ts.map +1 -0
  89. package/dist/persistent-secrets.js +94 -0
  90. package/dist/persistent-short-code-index.d.ts +14 -0
  91. package/dist/persistent-short-code-index.d.ts.map +1 -0
  92. package/dist/persistent-short-code-index.js +39 -0
  93. package/dist/persistent-stores.d.ts +12 -0
  94. package/dist/persistent-stores.d.ts.map +1 -0
  95. package/dist/persistent-stores.js +42 -0
  96. package/dist/serve-command.d.ts +500 -0
  97. package/dist/serve-command.d.ts.map +1 -0
  98. package/dist/serve-command.js +616 -0
  99. package/dist/serve-fallback.d.ts +64 -0
  100. package/dist/serve-fallback.d.ts.map +1 -0
  101. package/dist/serve-fallback.js +122 -0
  102. package/dist/spawn-cloudflared.d.ts +61 -0
  103. package/dist/spawn-cloudflared.d.ts.map +1 -0
  104. package/dist/spawn-cloudflared.js +167 -0
  105. package/dist/theme-command.d.ts +4 -0
  106. package/dist/theme-command.d.ts.map +1 -0
  107. package/dist/theme-command.js +154 -0
  108. package/dist/theme-file-uploader.d.ts +8 -0
  109. package/dist/theme-file-uploader.d.ts.map +1 -0
  110. package/dist/theme-file-uploader.js +58 -0
  111. package/dist/theme-writer.d.ts +8 -0
  112. package/dist/theme-writer.d.ts.map +1 -0
  113. package/dist/theme-writer.js +103 -0
  114. package/dist/tunnel-provider.d.ts +273 -0
  115. package/dist/tunnel-provider.d.ts.map +1 -0
  116. package/dist/tunnel-provider.js +142 -0
  117. package/package.json +81 -0
@@ -0,0 +1,351 @@
1
+ /**
2
+ * OIDC token resolution chain for sigstore signing.
3
+ *
4
+ * Public-gadget publishing signs with sigstore's keyless flow — Fulcio
5
+ * exchanges a short-lived OIDC identity-token for an ephemeral signing
6
+ * cert, then Rekor anchors the resulting signature in the transparency
7
+ * log. The CLI's job is to *get an OIDC token* from one of four sources;
8
+ * the actual sign call lives behind `signBundleSigstore` (gadget-signing).
9
+ *
10
+ * Precedence (highest first):
11
+ *
12
+ * 1. `--identity-token <jwt>` flag (matches cosign convention).
13
+ * 2. `GGUI_OIDC_TOKEN` env var.
14
+ * 3. GitHub Actions ambient OIDC — when `ACTIONS_ID_TOKEN_REQUEST_TOKEN`
15
+ * + `ACTIONS_ID_TOKEN_REQUEST_URL` are present, GET the token from
16
+ * the request URL with the request token as bearer + `audience=sigstore`.
17
+ * 4. Interactive browser flow — only when stdout is a TTY. Prints a
18
+ * sigstore-issuer URL, listens on `127.0.0.1:<rand>`, waits for the
19
+ * OAuth redirect carrying the access_token. If we can't bind a
20
+ * local port we fall back to asking the user to paste the token
21
+ * manually.
22
+ *
23
+ * If none of the four work → throws with a clear error directing the
24
+ * operator at the available knobs.
25
+ *
26
+ * **Tests.** The resolver is fully dependency-injectable: the env-var
27
+ * shape, TTY-ness, and (for the GH Actions branch) the fetch function
28
+ * are passed in. Interactive flow has a short-circuit (`interactive: false`)
29
+ * the tests use to avoid spinning up a real OAuth listener.
30
+ */
31
+ import { createServer } from 'node:http';
32
+ import { randomBytes, createHash } from 'node:crypto';
33
+ /**
34
+ * Sentinel error thrown when no OIDC source produces a usable token.
35
+ * Carries a `code` discriminant so callers can map back into a
36
+ * structured `PublishError`.
37
+ */
38
+ export class OidcResolutionError extends Error {
39
+ code;
40
+ constructor(code, message) {
41
+ super(message);
42
+ this.code = code;
43
+ this.name = 'OidcResolutionError';
44
+ }
45
+ }
46
+ // ---------------------------------------------------------------------------
47
+ // Resolver
48
+ // ---------------------------------------------------------------------------
49
+ /** Default sigstore public-good issuer. Matches cosign's default. */
50
+ const DEFAULT_ISSUER = 'https://oauth2.sigstore.dev/auth';
51
+ /** Public OAuth client id for the sigstore public-good issuer. */
52
+ const SIGSTORE_OAUTH_CLIENT_ID = 'sigstore';
53
+ export async function resolveOidcToken(opts) {
54
+ // 1. --identity-token flag (highest precedence).
55
+ if (opts.identityTokenFlag && opts.identityTokenFlag.length > 0) {
56
+ return { token: opts.identityTokenFlag, source: 'flag' };
57
+ }
58
+ // 2. GGUI_OIDC_TOKEN env var.
59
+ const envToken = opts.env['GGUI_OIDC_TOKEN'];
60
+ if (typeof envToken === 'string' && envToken.length > 0) {
61
+ return { token: envToken, source: 'env' };
62
+ }
63
+ // 3. GitHub Actions ambient OIDC.
64
+ const ghRequestToken = opts.env['ACTIONS_ID_TOKEN_REQUEST_TOKEN'];
65
+ const ghRequestUrl = opts.env['ACTIONS_ID_TOKEN_REQUEST_URL'];
66
+ if (typeof ghRequestToken === 'string' &&
67
+ ghRequestToken.length > 0 &&
68
+ typeof ghRequestUrl === 'string' &&
69
+ ghRequestUrl.length > 0) {
70
+ const token = await fetchGitHubActionsToken({
71
+ requestToken: ghRequestToken,
72
+ requestUrl: ghRequestUrl,
73
+ fetchImpl: opts.fetch ?? globalThis.fetch.bind(globalThis),
74
+ });
75
+ return { token, source: 'github-actions' };
76
+ }
77
+ // 4. Interactive browser flow — TTY-gated.
78
+ if (opts.isTty && opts.interactive !== false) {
79
+ const token = await runInteractiveFlow({
80
+ issuer: opts.issuer ?? DEFAULT_ISSUER,
81
+ stdout: opts.stdout ?? ((s) => void process.stdout.write(s)),
82
+ prompt: opts.prompt,
83
+ openBrowser: opts.openBrowser,
84
+ });
85
+ return { token, source: 'interactive' };
86
+ }
87
+ throw new OidcResolutionError('no_token_available', 'no OIDC token available. Provide one of:\n' +
88
+ ' --identity-token <jwt>\n' +
89
+ ' GGUI_OIDC_TOKEN env var\n' +
90
+ ' ACTIONS_ID_TOKEN_REQUEST_TOKEN + ACTIONS_ID_TOKEN_REQUEST_URL (GitHub Actions)\n' +
91
+ ' run from a TTY for the interactive browser flow');
92
+ }
93
+ // ---------------------------------------------------------------------------
94
+ // GitHub Actions ambient OIDC
95
+ //
96
+ // Standard pattern from the actions/toolkit docs:
97
+ // GET <ACTIONS_ID_TOKEN_REQUEST_URL>&audience=sigstore
98
+ // Authorization: bearer <ACTIONS_ID_TOKEN_REQUEST_TOKEN>
99
+ // → { value: "<jwt>" }
100
+ // ---------------------------------------------------------------------------
101
+ async function fetchGitHubActionsToken(opts) {
102
+ // Audience query parameter — sigstore's Fulcio expects `sigstore`.
103
+ const url = appendAudience(opts.requestUrl, 'sigstore');
104
+ let res;
105
+ try {
106
+ res = await opts.fetchImpl(url, {
107
+ method: 'GET',
108
+ headers: {
109
+ Authorization: `bearer ${opts.requestToken}`,
110
+ Accept: 'application/json; api-version=2.0',
111
+ },
112
+ });
113
+ }
114
+ catch (err) {
115
+ throw new OidcResolutionError('github_actions_fetch_failed', `GitHub Actions OIDC fetch failed: ${err instanceof Error ? err.message : String(err)}`);
116
+ }
117
+ if (!res.ok) {
118
+ let body = '';
119
+ try {
120
+ body = await res.text();
121
+ }
122
+ catch {
123
+ // ignore
124
+ }
125
+ throw new OidcResolutionError('github_actions_fetch_failed', `GitHub Actions OIDC fetch returned HTTP ${res.status}${body ? `: ${body.slice(0, 200)}` : ''}`);
126
+ }
127
+ let parsed;
128
+ try {
129
+ parsed = await res.json();
130
+ }
131
+ catch (err) {
132
+ throw new OidcResolutionError('github_actions_fetch_failed', `GitHub Actions OIDC response was not JSON: ${err instanceof Error ? err.message : String(err)}`);
133
+ }
134
+ if (typeof parsed !== 'object' ||
135
+ parsed === null ||
136
+ typeof parsed.value !== 'string') {
137
+ throw new OidcResolutionError('github_actions_fetch_failed', 'GitHub Actions OIDC response missing `value` string');
138
+ }
139
+ const value = parsed.value;
140
+ if (value.length === 0) {
141
+ throw new OidcResolutionError('github_actions_fetch_failed', 'GitHub Actions OIDC `value` was empty');
142
+ }
143
+ return value;
144
+ }
145
+ /**
146
+ * Append `audience=<aud>` to a URL, handling whether one is already
147
+ * present. The Actions request URL ships with `?api-version=...` already.
148
+ */
149
+ function appendAudience(url, audience) {
150
+ const sep = url.includes('?') ? '&' : '?';
151
+ return `${url}${sep}audience=${encodeURIComponent(audience)}`;
152
+ }
153
+ // ---------------------------------------------------------------------------
154
+ // Interactive browser flow — PKCE OAuth against the sigstore public-good
155
+ // issuer.
156
+ //
157
+ // Flow:
158
+ // 1. Bind a local HTTP listener on 127.0.0.1:<random>.
159
+ // 2. Build the issuer URL with redirect_uri = http://127.0.0.1:<port>/callback.
160
+ // 3. Print the URL + (try to) open the user's browser. They sign in
161
+ // against the configured IdP (Google/Microsoft/GitHub via Dex).
162
+ // 4. The issuer redirects back to our listener with `?code=...&state=...`.
163
+ // 5. We exchange the code for an id_token via the issuer's /token endpoint.
164
+ //
165
+ // If binding the listener fails (sandboxed env, no localhost network),
166
+ // fall back to the manual-paste path: print the URL with `redirect_uri=urn:ietf:wg:oauth:2.0:oob`
167
+ // and prompt the operator to paste the token directly.
168
+ // ---------------------------------------------------------------------------
169
+ async function runInteractiveFlow(opts) {
170
+ let listener;
171
+ try {
172
+ listener = await startCallbackListener();
173
+ }
174
+ catch {
175
+ listener = undefined;
176
+ }
177
+ // PKCE pair — code_verifier kept locally, code_challenge sent to the
178
+ // issuer. The fallback (manual-paste) doesn't need them but generating
179
+ // them unconditionally keeps the two branches symmetrical.
180
+ const codeVerifier = base64UrlEncode(randomBytes(32));
181
+ const codeChallenge = base64UrlEncode(createHash('sha256').update(codeVerifier).digest());
182
+ const state = base64UrlEncode(randomBytes(16));
183
+ if (listener) {
184
+ const redirectUri = `http://127.0.0.1:${listener.port}/callback`;
185
+ const authUrl = buildAuthUrl({
186
+ issuer: opts.issuer,
187
+ redirectUri,
188
+ codeChallenge,
189
+ state,
190
+ });
191
+ opts.stdout(`\nOpen this URL in your browser to authenticate:\n ${authUrl}\n\n`);
192
+ if (opts.openBrowser) {
193
+ try {
194
+ await opts.openBrowser(authUrl);
195
+ }
196
+ catch {
197
+ // ignore — printed URL is enough
198
+ }
199
+ }
200
+ let callback;
201
+ try {
202
+ callback = await listener.waitForCallback();
203
+ }
204
+ catch (err) {
205
+ throw new OidcResolutionError('interactive_failed', `interactive callback failed: ${err instanceof Error ? err.message : String(err)}`);
206
+ }
207
+ finally {
208
+ listener.close();
209
+ }
210
+ if (callback.state !== state) {
211
+ throw new OidcResolutionError('interactive_failed', 'OAuth state mismatch — possible CSRF; aborting.');
212
+ }
213
+ return await exchangeCodeForToken({
214
+ issuer: opts.issuer,
215
+ code: callback.code,
216
+ codeVerifier,
217
+ redirectUri,
218
+ });
219
+ }
220
+ // Manual-paste fallback — couldn't bind a local port.
221
+ const oobRedirect = 'urn:ietf:wg:oauth:2.0:oob';
222
+ const authUrl = buildAuthUrl({
223
+ issuer: opts.issuer,
224
+ redirectUri: oobRedirect,
225
+ codeChallenge,
226
+ state,
227
+ });
228
+ opts.stdout(`\nUnable to bind a local OAuth listener.\n` +
229
+ `Open this URL in your browser, authenticate, and paste the resulting token below:\n ${authUrl}\n\n`);
230
+ const promptFn = opts.prompt;
231
+ if (!promptFn) {
232
+ throw new OidcResolutionError('interactive_failed', 'interactive fallback requires an injected prompt function (or a free localhost port)');
233
+ }
234
+ const pasted = await promptFn('OIDC token: ');
235
+ if (!pasted || pasted.length === 0) {
236
+ throw new OidcResolutionError('interactive_failed', 'no token provided');
237
+ }
238
+ return pasted.trim();
239
+ }
240
+ function startCallbackListener() {
241
+ return new Promise((resolveStart, rejectStart) => {
242
+ let resolveCb;
243
+ let rejectCb;
244
+ const server = createServer((req, res) => {
245
+ if (!req.url) {
246
+ res.statusCode = 400;
247
+ res.end('missing url');
248
+ return;
249
+ }
250
+ const u = new URL(req.url, `http://127.0.0.1`);
251
+ const code = u.searchParams.get('code');
252
+ const state = u.searchParams.get('state');
253
+ const error = u.searchParams.get('error');
254
+ if (error) {
255
+ res.statusCode = 400;
256
+ res.end(`error: ${error}`);
257
+ rejectCb?.(new Error(`issuer returned error=${error}`));
258
+ return;
259
+ }
260
+ if (!code || !state) {
261
+ res.statusCode = 400;
262
+ res.end('missing code/state');
263
+ return;
264
+ }
265
+ res.statusCode = 200;
266
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
267
+ res.end('<html><body><p>Authentication complete. You can close this tab.</p></body></html>');
268
+ resolveCb?.({ code, state });
269
+ });
270
+ server.once('error', (err) => rejectStart(err));
271
+ server.listen(0, '127.0.0.1', () => {
272
+ const addr = server.address();
273
+ if (!addr || typeof addr.port !== 'number') {
274
+ rejectStart(new Error('failed to bind callback listener'));
275
+ return;
276
+ }
277
+ const port = addr.port;
278
+ resolveStart({
279
+ port,
280
+ waitForCallback() {
281
+ return new Promise((resolveInner, rejectInner) => {
282
+ resolveCb = resolveInner;
283
+ rejectCb = rejectInner;
284
+ });
285
+ },
286
+ close() {
287
+ server.close();
288
+ },
289
+ });
290
+ });
291
+ });
292
+ }
293
+ function buildAuthUrl(opts) {
294
+ const params = new URLSearchParams({
295
+ response_type: 'code',
296
+ client_id: SIGSTORE_OAUTH_CLIENT_ID,
297
+ scope: 'openid email',
298
+ redirect_uri: opts.redirectUri,
299
+ code_challenge: opts.codeChallenge,
300
+ code_challenge_method: 'S256',
301
+ state: opts.state,
302
+ });
303
+ return `${opts.issuer}/auth?${params.toString()}`;
304
+ }
305
+ async function exchangeCodeForToken(opts) {
306
+ const body = new URLSearchParams({
307
+ grant_type: 'authorization_code',
308
+ code: opts.code,
309
+ code_verifier: opts.codeVerifier,
310
+ client_id: SIGSTORE_OAUTH_CLIENT_ID,
311
+ redirect_uri: opts.redirectUri,
312
+ });
313
+ let res;
314
+ try {
315
+ res = await fetch(`${opts.issuer}/token`, {
316
+ method: 'POST',
317
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
318
+ body: body.toString(),
319
+ });
320
+ }
321
+ catch (err) {
322
+ throw new OidcResolutionError('interactive_failed', `token exchange failed: ${err instanceof Error ? err.message : String(err)}`);
323
+ }
324
+ if (!res.ok) {
325
+ let text = '';
326
+ try {
327
+ text = await res.text();
328
+ }
329
+ catch {
330
+ // ignore
331
+ }
332
+ throw new OidcResolutionError('interactive_failed', `token exchange HTTP ${res.status}${text ? `: ${text.slice(0, 200)}` : ''}`);
333
+ }
334
+ let parsed;
335
+ try {
336
+ parsed = await res.json();
337
+ }
338
+ catch (err) {
339
+ throw new OidcResolutionError('interactive_failed', `token exchange response was not JSON: ${err instanceof Error ? err.message : String(err)}`);
340
+ }
341
+ if (typeof parsed !== 'object' ||
342
+ parsed === null ||
343
+ typeof parsed.id_token !== 'string') {
344
+ throw new OidcResolutionError('interactive_failed', 'token exchange response missing `id_token` string');
345
+ }
346
+ return parsed.id_token;
347
+ }
348
+ function base64UrlEncode(buf) {
349
+ const b = Buffer.isBuffer(buf) ? buf : Buffer.from(buf);
350
+ return b.toString('base64').replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_');
351
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Outcome discriminator. The `ok: false` `code` union surfaces specific
3
+ * HTTP failure modes so callers (notably a future `--json` flag) can
4
+ * branch on the registry's closed error set without parsing the human-
5
+ * readable message:
6
+ *
7
+ * - `unauthorized` ← HTTP 401 (missing/invalid Cognito JWT).
8
+ * - `invalid_request` ← HTTP 400 (malformed body or wrong key length).
9
+ * - `key_conflict` ← HTTP 409 (collision against an existing row).
10
+ * - `http-error` ← any other non-2xx (5xx or unexpected status).
11
+ *
12
+ * The body's `error` field on a structured error response is read first
13
+ * (it carries the registry's authoritative {@link RegisterAuthorKeyErrorCode});
14
+ * status-code mapping is the fallback when the body omits it. The
15
+ * human-readable `message` always flows through from the body when
16
+ * present.
17
+ */
18
+ export type RegisterKeyOutcome = {
19
+ readonly ok: true;
20
+ readonly status: 200 | 201;
21
+ readonly registryUrl: string;
22
+ readonly subject: string;
23
+ readonly keyId: string;
24
+ } | {
25
+ readonly ok: false;
26
+ readonly code: 'no-registry' | 'invalid-registry' | 'no-keypair' | 'auth_failed' | 'auth_config_missing' | 'network-error' | 'unauthorized' | 'invalid_request' | 'key_conflict' | 'http-error' | 'bad-response';
27
+ readonly message: string;
28
+ };
29
+ export interface RunRegisterKeyFlags {
30
+ readonly scope: string;
31
+ readonly registry?: string;
32
+ }
33
+ export interface RunRegisterKeyDeps {
34
+ readonly cwd: string;
35
+ readonly env: NodeJS.ProcessEnv;
36
+ readonly fetch: typeof fetch;
37
+ readonly now: () => number;
38
+ }
39
+ /**
40
+ * Run the register flow end-to-end. Never throws — every error path
41
+ * returns a structured outcome.
42
+ */
43
+ export declare function runRegisterAuthorKey(flags: RunRegisterKeyFlags, deps: RunRegisterKeyDeps): Promise<RegisterKeyOutcome>;
44
+ //# sourceMappingURL=register-author-key.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register-author-key.d.ts","sourceRoot":"","sources":["../../src/internal/register-author-key.ts"],"names":[],"mappings":"AA2BA;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,MAAM,EAAE,GAAG,GAAG,GAAG,CAAC;IAC3B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,IAAI,EACT,aAAa,GACb,kBAAkB,GAClB,YAAY,GACZ,aAAa,GACb,qBAAqB,GACrB,eAAe,GACf,cAAc,GACd,iBAAiB,GACjB,cAAc,GACd,YAAY,GACZ,cAAc,CAAC;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEN,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,OAAO,KAAK,CAAC;IAC7B,QAAQ,CAAC,GAAG,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,mBAAmB,EAC1B,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,kBAAkB,CAAC,CA0J7B"}
@@ -0,0 +1,212 @@
1
+ /**
2
+ * `ggui keys register`.
3
+ *
4
+ * Register a per-scope Ed25519 public key with the marketplace
5
+ * registry's `POST /author-keys` endpoint. Replaces the operator-owned
6
+ * `aws dynamodb put-item` workflow that previously required raw IAM
7
+ * credentials + registry table knowledge.
8
+ *
9
+ * The publisher's identity is the Cognito JWT's `sub` (server-side);
10
+ * the body carries only `publicKeyBase64`. `keyId` is derived from the
11
+ * public-key bytes on the server, so re-running with the same key is
12
+ * idempotent (200 OK).
13
+ *
14
+ * Auth + registry-URL resolution match the publish flow (so the
15
+ * cached token / `GGUI_REGISTRY` env / `ggui.json#registry` chain
16
+ * works the same way operators are used to).
17
+ */
18
+ import { readFileSync } from 'node:fs';
19
+ import { REGISTER_AUTHOR_KEY_ERROR_CODES } from '@ggui-ai/registry-core';
20
+ import { acquireHostedAuthJwt, } from './artifact-publish.js';
21
+ import { resolveRegistryUrl } from './artifact-search.js';
22
+ import { getPublicKeyPath, hasKeypair } from './key-store.js';
23
+ /**
24
+ * Run the register flow end-to-end. Never throws — every error path
25
+ * returns a structured outcome.
26
+ */
27
+ export async function runRegisterAuthorKey(flags, deps) {
28
+ // 1. Registry URL — same resolver as publish/search.
29
+ const resolved = resolveRegistryUrl({
30
+ ...(flags.registry !== undefined ? { flag: flags.registry } : {}),
31
+ cwd: deps.cwd,
32
+ env: {
33
+ GGUI_REGISTRY: typeof deps.env['GGUI_REGISTRY'] === 'string' ? deps.env['GGUI_REGISTRY'] : undefined,
34
+ },
35
+ });
36
+ if ('error' in resolved) {
37
+ const isNoRegistry = resolved.error.startsWith('no registry');
38
+ return {
39
+ ok: false,
40
+ code: isNoRegistry ? 'no-registry' : 'invalid-registry',
41
+ message: resolved.error,
42
+ };
43
+ }
44
+ // 2. Read the local public key. Same per-scope keystore the publish
45
+ // flow auto-generates on first publish — operators run `ggui keys
46
+ // register` AFTER `ggui blueprint publish` / `ggui gadget publish`
47
+ // generates the keypair locally + the registry rejects with
48
+ // `unknown_key`.
49
+ if (!hasKeypair(flags.scope)) {
50
+ return {
51
+ ok: false,
52
+ code: 'no-keypair',
53
+ message: `no keypair found for scope ${flags.scope}. Run \`ggui blueprint publish\` or ` +
54
+ `\`ggui gadget publish\` under this scope first — the publish flow generates the ` +
55
+ `keypair on first run.`,
56
+ };
57
+ }
58
+ const pubPath = getPublicKeyPath(flags.scope);
59
+ let publicKeyBytes;
60
+ try {
61
+ publicKeyBytes = readFileSync(pubPath);
62
+ }
63
+ catch (err) {
64
+ return {
65
+ ok: false,
66
+ code: 'no-keypair',
67
+ message: `failed to read public key at ${pubPath}: ${err instanceof Error ? err.message : String(err)}`,
68
+ };
69
+ }
70
+ if (publicKeyBytes.length !== 32) {
71
+ return {
72
+ ok: false,
73
+ code: 'no-keypair',
74
+ message: `public key at ${pubPath} is not a 32-byte Ed25519 key (got ${publicKeyBytes.length} bytes)`,
75
+ };
76
+ }
77
+ const publicKeyBase64 = publicKeyBytes.toString('base64');
78
+ // 3. Hosted-auth Cognito JWT — same chain as publish.
79
+ const auth = await acquireHostedAuthJwt({
80
+ registryUrl: resolved.url,
81
+ env: deps.env,
82
+ cwd: deps.cwd,
83
+ now: deps.now,
84
+ });
85
+ if (!auth.ok) {
86
+ return {
87
+ ok: false,
88
+ code: auth.error.code,
89
+ message: auth.error.message,
90
+ };
91
+ }
92
+ // 4. POST /author-keys
93
+ const url = `${resolved.url}/author-keys`;
94
+ let res;
95
+ try {
96
+ res = await deps.fetch(url, {
97
+ method: 'POST',
98
+ headers: {
99
+ 'content-type': 'application/json',
100
+ authorization: `Bearer ${auth.token.idToken}`,
101
+ },
102
+ body: JSON.stringify({ publicKeyBase64 }),
103
+ });
104
+ }
105
+ catch (err) {
106
+ return {
107
+ ok: false,
108
+ code: 'network-error',
109
+ message: `failed to reach registry at ${resolved.url}: ${err instanceof Error ? err.message : String(err)}`,
110
+ };
111
+ }
112
+ let body;
113
+ try {
114
+ body = await res.json();
115
+ }
116
+ catch {
117
+ return {
118
+ ok: false,
119
+ code: 'bad-response',
120
+ message: `registry responded with HTTP ${res.status} but body was not valid JSON`,
121
+ };
122
+ }
123
+ if (res.status === 200 || res.status === 201) {
124
+ if (body === null || typeof body !== 'object') {
125
+ return {
126
+ ok: false,
127
+ code: 'bad-response',
128
+ message: `registry returned ${res.status} with malformed body`,
129
+ };
130
+ }
131
+ const okBody = body;
132
+ if (typeof okBody.subject !== 'string' || typeof okBody.keyId !== 'string') {
133
+ return {
134
+ ok: false,
135
+ code: 'bad-response',
136
+ message: `registry returned ${res.status} with missing subject/keyId fields`,
137
+ };
138
+ }
139
+ return {
140
+ ok: true,
141
+ status: res.status,
142
+ registryUrl: resolved.url,
143
+ subject: okBody.subject,
144
+ keyId: okBody.keyId,
145
+ };
146
+ }
147
+ // Defend against `null` / non-object JSON (e.g. `body === null` or
148
+ // `body === "string-literal"`) — accessing `.error`/`.message` on
149
+ // those would throw. Treat them like the "no structured body" case.
150
+ const errBody = body !== null && typeof body === 'object'
151
+ ? body
152
+ : {};
153
+ const errMsg = typeof errBody.message === 'string'
154
+ ? errBody.message
155
+ : `registry returned HTTP ${res.status}`;
156
+ // Prefer the body's `error` discriminator (closed enum from registry-
157
+ // core's `REGISTER_AUTHOR_KEY_ERROR_CODES`) over status-code mapping —
158
+ // the body is authoritative when present + well-formed. Status-code
159
+ // mapping is the fallback when the body omits `error`.
160
+ const bodyError = mapBodyErrorToCode(errBody.error);
161
+ if (bodyError !== undefined) {
162
+ return { ok: false, code: bodyError, message: errMsg };
163
+ }
164
+ return {
165
+ ok: false,
166
+ code: mapStatusToCode(res.status),
167
+ message: errMsg,
168
+ };
169
+ }
170
+ /**
171
+ * Map the registry's structured `error` discriminator to the CLI's
172
+ * outcome `code`. Returns `undefined` when the body doesn't carry a
173
+ * recognised code so the caller can fall back to status-code mapping.
174
+ * The `server_error` value from the registry deliberately maps to the
175
+ * CLI's generic `http-error` bucket — the CLI doesn't distinguish 500-
176
+ * shaped responses from other non-2xx statuses.
177
+ */
178
+ function mapBodyErrorToCode(raw) {
179
+ if (typeof raw !== 'string')
180
+ return undefined;
181
+ // Defend against a future registry-core enum addition leaking through
182
+ // — only map values the CLI knows about.
183
+ if (!REGISTER_AUTHOR_KEY_ERROR_CODES.includes(raw)) {
184
+ return undefined;
185
+ }
186
+ switch (raw) {
187
+ case 'unauthorized':
188
+ return 'unauthorized';
189
+ case 'invalid_request':
190
+ return 'invalid_request';
191
+ case 'key_conflict':
192
+ return 'key_conflict';
193
+ case 'server_error':
194
+ return 'http-error';
195
+ default:
196
+ return undefined;
197
+ }
198
+ }
199
+ /**
200
+ * Map an HTTP status to the CLI outcome `code` when the body didn't
201
+ * carry a structured `error` discriminator. Everything outside the
202
+ * three explicit buckets collapses to `http-error`.
203
+ */
204
+ function mapStatusToCode(status) {
205
+ if (status === 401)
206
+ return 'unauthorized';
207
+ if (status === 400)
208
+ return 'invalid_request';
209
+ if (status === 409)
210
+ return 'key_conflict';
211
+ return 'http-error';
212
+ }
@@ -0,0 +1,47 @@
1
+ interface LocalKeyRow {
2
+ pairingId: string;
3
+ deviceName: string;
4
+ createdAt: number;
5
+ lastUsedAt?: number;
6
+ lastRemoteAddress?: string;
7
+ token: string;
8
+ }
9
+ interface LocalKeysState {
10
+ v: 1;
11
+ pairings: LocalKeyRow[];
12
+ idCounter: number;
13
+ }
14
+ /**
15
+ * Read + parse the keys file, or return empty state when the file is
16
+ * missing / empty (first-boot path). Throws on malformed JSON or
17
+ * unsupported schema — loud failure beats silent overwrite.
18
+ */
19
+ export declare function loadLocalKeys(path: string): LocalKeysState;
20
+ /**
21
+ * Atomic write — temp file + rename, `0600` perms, parent dir created
22
+ * with `0700` if missing. Mirrors `InMemoryPairingService.persistToDisk`
23
+ * so the two writers can't disagree on file shape.
24
+ */
25
+ export declare function saveLocalKeys(path: string, state: LocalKeysState): void;
26
+ /**
27
+ * Mint a new `ggui_user_*` token and append it to `state`. Returns
28
+ * the appended row (caller surfaces the plaintext token to the
29
+ * operator — one-time reveal, exactly like cloud-side `keys create`).
30
+ */
31
+ export declare function mintLocalKey(state: LocalKeysState, opts: {
32
+ readonly deviceName: string;
33
+ readonly now?: () => number;
34
+ }): LocalKeyRow;
35
+ /**
36
+ * Remove the row with `pairingId`. Idempotent — revoking a missing
37
+ * id is not an error. Returns `true` when a row was removed.
38
+ */
39
+ export declare function revokeLocalKey(state: LocalKeysState, pairingId: string): boolean;
40
+ /**
41
+ * Render a fixed-width table to stdout. Columns chosen to match the
42
+ * cloud-side `keys list` shape so operators recognize the layout
43
+ * regardless of which mode they're in.
44
+ */
45
+ export declare function formatLocalKeyTable(state: LocalKeysState): string;
46
+ export {};
47
+ //# sourceMappingURL=local-keys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-keys.d.ts","sourceRoot":"","sources":["../src/local-keys.ts"],"names":[],"mappings":"AA4BA,UAAU,WAAW;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,cAAc;IACtB,CAAC,EAAE,CAAC,CAAC;IACL,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAe1D;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,GAAG,IAAI,CAqBvE;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,cAAc,EACrB,IAAI,EAAE;IAAE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAAE,GACjE,WAAW,CAYb;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAIhF;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAkBjE"}