@brass-build/cli 0.1.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 (70) hide show
  1. package/AGENTS.md +170 -0
  2. package/CHANGELOG.md +12 -0
  3. package/LICENSE +21 -0
  4. package/README.md +172 -0
  5. package/dist/api.d.ts +73 -0
  6. package/dist/api.d.ts.map +1 -0
  7. package/dist/api.js +97 -0
  8. package/dist/api.js.map +1 -0
  9. package/dist/args.d.ts +10 -0
  10. package/dist/args.d.ts.map +1 -0
  11. package/dist/args.js +94 -0
  12. package/dist/args.js.map +1 -0
  13. package/dist/auth.d.ts +5 -0
  14. package/dist/auth.d.ts.map +1 -0
  15. package/dist/auth.js +12 -0
  16. package/dist/auth.js.map +1 -0
  17. package/dist/bin/brass.d.ts +3 -0
  18. package/dist/bin/brass.d.ts.map +1 -0
  19. package/dist/bin/brass.js +11 -0
  20. package/dist/bin/brass.js.map +1 -0
  21. package/dist/cli.d.ts +4 -0
  22. package/dist/cli.d.ts.map +1 -0
  23. package/dist/cli.js +364 -0
  24. package/dist/cli.js.map +1 -0
  25. package/dist/commands.d.ts +94 -0
  26. package/dist/commands.d.ts.map +1 -0
  27. package/dist/commands.js +559 -0
  28. package/dist/commands.js.map +1 -0
  29. package/dist/config.d.ts +40 -0
  30. package/dist/config.d.ts.map +1 -0
  31. package/dist/config.js +76 -0
  32. package/dist/config.js.map +1 -0
  33. package/dist/log.d.ts +9 -0
  34. package/dist/log.d.ts.map +1 -0
  35. package/dist/log.js +32 -0
  36. package/dist/log.js.map +1 -0
  37. package/dist/login.d.ts +21 -0
  38. package/dist/login.d.ts.map +1 -0
  39. package/dist/login.js +158 -0
  40. package/dist/login.js.map +1 -0
  41. package/dist/project.d.ts +32 -0
  42. package/dist/project.d.ts.map +1 -0
  43. package/dist/project.js +129 -0
  44. package/dist/project.js.map +1 -0
  45. package/dist/session.d.ts +47 -0
  46. package/dist/session.d.ts.map +1 -0
  47. package/dist/session.js +224 -0
  48. package/dist/session.js.map +1 -0
  49. package/dist/store.d.ts +17 -0
  50. package/dist/store.d.ts.map +1 -0
  51. package/dist/store.js +90 -0
  52. package/dist/store.js.map +1 -0
  53. package/dist/version.d.ts +3 -0
  54. package/dist/version.d.ts.map +1 -0
  55. package/dist/version.js +8 -0
  56. package/dist/version.js.map +1 -0
  57. package/package.json +42 -0
  58. package/src/api.ts +195 -0
  59. package/src/args.ts +107 -0
  60. package/src/auth.ts +16 -0
  61. package/src/bin/brass.ts +11 -0
  62. package/src/cli.ts +422 -0
  63. package/src/commands.ts +864 -0
  64. package/src/config.ts +132 -0
  65. package/src/log.ts +41 -0
  66. package/src/login.ts +211 -0
  67. package/src/project.ts +176 -0
  68. package/src/session.ts +319 -0
  69. package/src/store.ts +123 -0
  70. package/src/version.ts +8 -0
@@ -0,0 +1,224 @@
1
+ // Browser sign-in for the CLI, and the session-backed auth provider it
2
+ // produces. `brass login` runs the RFC 8628 device-authorization grant: the
3
+ // CLI mints a grant, opens the browser to the approval page with the user
4
+ // code prefilled (and prints the URL + code as a fallback for a headless
5
+ // box), then polls until the human approves. Approval mints the same tokens
6
+ // `/refresh` returns, including the opaque `session_token` the CLI stores to
7
+ // keep refreshing. The CLI's OAuth client is the seeded
8
+ // `brass_app_internal_cli`; its redirect allowlist is the loopback wildcards,
9
+ // which the `/refresh` Origin gate checks the CLI's portless 127.0.0.1 origin
10
+ // against on every later token refresh.
11
+ import { spawn } from 'node:child_process';
12
+ import { BrassApiError } from './api.js';
13
+ // The seeded CLI OAuth client (see infra/lib/api-stack.ts). Stable across
14
+ // environments, so one CLI build signs in against prod or dev.
15
+ export const CLI_APP_ID = 'brass_app_internal_cli';
16
+ // The Origin the CLI presents on every `/refresh` call (a command refreshing
17
+ // its access token from the stored session). The CLI app's loopback wildcard
18
+ // allowlist matches any 127.0.0.1 origin, port ignored, so this portless
19
+ // loopback origin passes the `/refresh` Origin gate.
20
+ const REFRESH_ORIGIN = 'http://127.0.0.1';
21
+ const realSleep = (ms) => new Promise((r) => setTimeout(r, ms));
22
+ // Refresh an access token at `/refresh` from the stored session pointer
23
+ // (`sid`). A form-encoded body keeps this a CORS simple request (the auth API
24
+ // serves no preflight); the Origin header is required by the endpoint's
25
+ // allowlist and satisfied by a loopback origin.
26
+ export async function postRefresh(authBaseUrl, sid, origin = REFRESH_ORIGIN, now = Date.now()) {
27
+ const body = new URLSearchParams({ sid });
28
+ const url = `${authBaseUrl}/refresh?${new URLSearchParams({ app_id: CLI_APP_ID }).toString()}`;
29
+ let response;
30
+ try {
31
+ response = await fetch(url, {
32
+ method: 'POST',
33
+ headers: { 'content-type': 'application/x-www-form-urlencoded', origin },
34
+ body,
35
+ });
36
+ }
37
+ catch (cause) {
38
+ throw new BrassApiError(0, `Network error reaching ${authBaseUrl}: ${String(cause)}`);
39
+ }
40
+ if (!response.ok) {
41
+ // Carry the status, and name what a 401 means. Every command reaches the
42
+ // data API through this refresh, so this error is what the caller
43
+ // classifies the whole attempt by, and a status-free error here is
44
+ // indistinguishable from a request that never completed: that is what
45
+ // turns an expired session into a report of an unreachable API.
46
+ throw new BrassApiError(response.status, response.status === 401
47
+ ? `Your Brass sign-in is no longer valid (401). Run 'brass login' to sign in again.`
48
+ : `Sign-in exchange failed (${response.status}).`);
49
+ }
50
+ const json = (await response.json());
51
+ return {
52
+ accessToken: json.access_token,
53
+ idToken: json.id_token,
54
+ // Default the lifetime when the server omits it: `undefined * 1000` is
55
+ // `NaN`, which makes `expiresAt` NaN and the `Date.now() >= expiresAt`
56
+ // refresh guard permanently false, so the in-process token would never
57
+ // re-refresh. `pollDeviceToken` already guards the same field.
58
+ expiresAt: now + (json.expires_in ?? 3600) * 1000,
59
+ ...(json.session_token ? { sessionToken: json.session_token } : {}),
60
+ };
61
+ }
62
+ // Decode the email claim from a Cognito id token, best-effort (used only for
63
+ // the "Signed in as ..." confirmation; never for authorization).
64
+ export function decodeEmail(idToken) {
65
+ const part = idToken.split('.')[1];
66
+ if (part === undefined)
67
+ return undefined;
68
+ try {
69
+ const claims = JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
70
+ return typeof claims.email === 'string' ? claims.email : undefined;
71
+ }
72
+ catch {
73
+ return undefined;
74
+ }
75
+ }
76
+ export async function deviceAuthorize(authBaseUrl, now = Date.now()) {
77
+ const body = new URLSearchParams({ app_id: CLI_APP_ID });
78
+ let response;
79
+ try {
80
+ response = await fetch(`${authBaseUrl}/device/authorize`, {
81
+ method: 'POST',
82
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
83
+ body,
84
+ });
85
+ }
86
+ catch (cause) {
87
+ throw new Error(`Network error reaching ${authBaseUrl}: ${String(cause)}`);
88
+ }
89
+ if (!response.ok)
90
+ throw new Error(`Device sign-in could not start (${response.status})`);
91
+ const json = (await response.json());
92
+ return {
93
+ deviceCode: json.device_code,
94
+ userCode: json.user_code,
95
+ verificationUri: json.verification_uri,
96
+ ...(json.verification_uri_complete
97
+ ? { verificationUriComplete: json.verification_uri_complete }
98
+ : {}),
99
+ // Guard both fields the poll loop depends on. The server always sends
100
+ // them, but a missing `interval` makes `sleep(NaN * 1000)` a zero-delay
101
+ // tight poll of /device/token, and a missing `expires_in` makes the
102
+ // `now() >= expiresAt` deadline check always false, so the loop never
103
+ // times out. The token-exchange path already guards `expires_in` the same
104
+ // way. RFC 8628 defaults: 5s poll interval, 600s grant lifetime.
105
+ intervalSeconds: json.interval ?? 5,
106
+ expiresAt: now + (json.expires_in ?? 600) * 1000,
107
+ };
108
+ }
109
+ export async function pollDeviceTokenOnce(authBaseUrl, deviceCode, now = Date.now()) {
110
+ const body = new URLSearchParams({ device_code: deviceCode, app_id: CLI_APP_ID });
111
+ let response;
112
+ try {
113
+ response = await fetch(`${authBaseUrl}/device/token`, {
114
+ method: 'POST',
115
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
116
+ body,
117
+ });
118
+ }
119
+ catch {
120
+ // A transient network error mid-poll: the device grant is still valid
121
+ // server-side, so report "still pending" and let the caller poll again
122
+ // rather than aborting a sign-in a retry would complete.
123
+ // (`deviceAuthorize` / `postRefresh` guard their fetches the same way.)
124
+ return { state: 'pending' };
125
+ }
126
+ const json = (await response.json().catch(() => ({})));
127
+ if (response.ok && json.access_token && json.id_token) {
128
+ return {
129
+ state: 'approved',
130
+ tokens: {
131
+ accessToken: json.access_token,
132
+ idToken: json.id_token,
133
+ expiresAt: now + (json.expires_in ?? 3600) * 1000,
134
+ ...(json.session_token ? { sessionToken: json.session_token } : {}),
135
+ },
136
+ };
137
+ }
138
+ // The user declined: the one terminal refusal.
139
+ if (json.error === 'access_denied')
140
+ return { state: 'denied' };
141
+ // RFC 8628 §3.5: polling too fast. The caller backs off.
142
+ if (json.error === 'slow_down')
143
+ return { state: 'slow_down' };
144
+ // Everything else — `authorization_pending`, a transient non-2xx (5xx/429),
145
+ // a non-JSON body, or an error code this pinned CLI does not recognize — is
146
+ // "keep waiting". The caller's deadline check is the single terminal bound,
147
+ // so an unknown error or a server blip does not abort a sign-in the user is
148
+ // one poll away from completing; a genuinely expired grant simply ends
149
+ // there as a clean timeout.
150
+ return { state: 'pending' };
151
+ }
152
+ // Poll `/device/token` until the grant is approved, or reject on denial /
153
+ // expiry. Honors the server-supplied interval; treats `authorization_pending`
154
+ // as "keep waiting".
155
+ export async function pollDeviceToken(authBaseUrl, auth, opts = {}) {
156
+ const sleep = opts.sleep ?? realSleep;
157
+ const now = opts.now ?? (() => Date.now());
158
+ // Server-supplied poll interval, bumped on `slow_down` per RFC 8628 §3.5.
159
+ let intervalSeconds = auth.intervalSeconds;
160
+ for (;;) {
161
+ await sleep(intervalSeconds * 1000);
162
+ if (now() >= auth.expiresAt)
163
+ throw new Error('Device sign-in timed out. Run `brass login` again.');
164
+ const outcome = await pollDeviceTokenOnce(authBaseUrl, auth.deviceCode, now());
165
+ if (outcome.state === 'approved')
166
+ return outcome.tokens;
167
+ if (outcome.state === 'denied')
168
+ throw new Error('Sign-in was denied.');
169
+ if (outcome.state === 'slow_down')
170
+ intervalSeconds += 5;
171
+ }
172
+ }
173
+ export async function loginDevice(options) {
174
+ const auth = await deviceAuthorize(options.authBaseUrl);
175
+ (options.onPrompt ?? defaultDevicePrompt)(auth);
176
+ (options.openBrowser ?? openBrowser)(auth.verificationUriComplete ?? auth.verificationUri);
177
+ const tokens = await pollDeviceToken(options.authBaseUrl, auth, options.sleep ? { sleep: options.sleep } : {});
178
+ if (!tokens.sessionToken)
179
+ throw new Error('Device sign-in did not return a session token.');
180
+ return { sessionToken: tokens.sessionToken, email: decodeEmail(tokens.idToken) };
181
+ }
182
+ function defaultDevicePrompt(auth) {
183
+ const target = auth.verificationUriComplete ?? auth.verificationUri;
184
+ process.stderr.write('\nOpening your browser to approve this sign-in.\n' +
185
+ `If it doesn't open, go to:\n ${target}\n` +
186
+ `and confirm the code: ${auth.userCode}\n` +
187
+ '\nWaiting for approval...\n');
188
+ }
189
+ function openBrowser(url) {
190
+ const platform = process.platform;
191
+ const [cmd, args] = platform === 'darwin'
192
+ ? ['open', [url]]
193
+ : platform === 'win32'
194
+ ? ['cmd', ['/c', 'start', '', url]]
195
+ : ['xdg-open', [url]];
196
+ try {
197
+ const child = spawn(cmd, args, { detached: true, stdio: 'ignore' });
198
+ // A missing opener (`xdg-open` absent on a headless box) surfaces ENOENT
199
+ // as an ASYNC 'error' event, not a throw; an unhandled one crashes the
200
+ // process. Swallow it: the URL is already printed, so the user opens it
201
+ // themselves.
202
+ child.on('error', () => { });
203
+ child.unref();
204
+ }
205
+ catch {
206
+ // The URL was already printed; a failed auto-open is not fatal.
207
+ }
208
+ }
209
+ // An auth provider backed by a stored login session: refresh the access
210
+ // token on first use and whenever it is within the skew window of expiry,
211
+ // caching it in memory for the process's lifetime.
212
+ export function sessionAuth(authBaseUrl, sid) {
213
+ let cached = null;
214
+ const SKEW_MS = 60 * 1000;
215
+ return {
216
+ async headers() {
217
+ if (cached === null || Date.now() >= cached.expiresAt - SKEW_MS) {
218
+ cached = await postRefresh(authBaseUrl, sid);
219
+ }
220
+ return { authorization: `Bearer ${cached.accessToken}`, 'x-id-token': cached.idToken };
221
+ },
222
+ };
223
+ }
224
+ //# sourceMappingURL=session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.js","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ import type { CredentialsFile, Profile, StoredCredential } from './config.js';
2
+ export declare function credentialsFilePath(env?: NodeJS.ProcessEnv): string;
3
+ export declare function readCredentialsFile(env?: NodeJS.ProcessEnv): Promise<CredentialsFile | null>;
4
+ export declare function writeStoredCredential(target: Profile, credential: StoredCredential | null, env?: NodeJS.ProcessEnv): Promise<void>;
5
+ export interface PendingLogin {
6
+ authBaseUrl: string;
7
+ deviceCode: string;
8
+ userCode: string;
9
+ verificationUri: string;
10
+ verificationUriComplete?: string;
11
+ intervalSeconds: number;
12
+ expiresAt: number;
13
+ }
14
+ export declare function pendingLoginFilePath(env?: NodeJS.ProcessEnv): string;
15
+ export declare function readPendingLogin(profile: Profile, env?: NodeJS.ProcessEnv): Promise<PendingLogin | null>;
16
+ export declare function writePendingLogin(profile: Profile, pending: PendingLogin | null, env?: NodeJS.ProcessEnv): Promise<void>;
17
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAM9E,wBAAgB,mBAAmB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAIhF;AAED,wBAAsB,mBAAmB,CACvC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CASjC;AAKD,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,OAAO,EACf,UAAU,EAAE,gBAAgB,GAAG,IAAI,EACnC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,IAAI,CAAC,CAMf;AAKD,MAAM,WAAW,YAAY;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;CACnB;AAUD,wBAAgB,oBAAoB,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAEjF;AAaD,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,OAAO,EAChB,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAY9B;AAKD,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,YAAY,GAAG,IAAI,EAC5B,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,OAAO,CAAC,IAAI,CAAC,CASf"}
package/dist/store.js ADDED
@@ -0,0 +1,90 @@
1
+ import { readFile, writeFile, mkdir, chmod } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ // Where `brass login` persists its credential, and where
5
+ // `publish` / `schema` / `whoami` look for it when no `--token` /
6
+ // `BRASS_SERVICE_TOKEN` was given. Honors `XDG_CONFIG_HOME`, else
7
+ // `~/.config/brass/credentials.json`.
8
+ export function credentialsFilePath(env = process.env) {
9
+ const base = env['XDG_CONFIG_HOME'];
10
+ const root = base && base.trim() !== '' ? base : join(homedir(), '.config');
11
+ return join(root, 'brass', 'credentials.json');
12
+ }
13
+ export async function readCredentialsFile(env = process.env) {
14
+ try {
15
+ const raw = await readFile(credentialsFilePath(env), 'utf8');
16
+ const parsed = JSON.parse(raw);
17
+ if (parsed.version !== 1 || typeof parsed.credentials !== 'object')
18
+ return null;
19
+ return parsed;
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ // Merge one profile's stored credential into the file (0600 so the session
26
+ // pointer is not world-readable), or clear it when `credential` is null.
27
+ // Preserves the other env's entry.
28
+ export async function writeStoredCredential(target, credential, env = process.env) {
29
+ const existing = (await readCredentialsFile(env)) ?? { version: 1, credentials: {} };
30
+ const credentials = { ...existing.credentials };
31
+ if (credential === null)
32
+ delete credentials[target];
33
+ else
34
+ credentials[target] = credential;
35
+ await writeOwnerOnlyJson(credentialsFilePath(env), { version: 1, credentials });
36
+ }
37
+ // Sibling of the credentials file: the in-flight sign-in per profile. Kept
38
+ // out of credentials.json so starting a new sign-in never disturbs a stored
39
+ // session until the new grant is approved.
40
+ export function pendingLoginFilePath(env = process.env) {
41
+ return join(dirname(credentialsFilePath(env)), 'pending-login.json');
42
+ }
43
+ async function readPendingLoginsFile(env) {
44
+ try {
45
+ const raw = await readFile(pendingLoginFilePath(env), 'utf8');
46
+ const parsed = JSON.parse(raw);
47
+ if (parsed.version !== 1 || typeof parsed.pending !== 'object')
48
+ return null;
49
+ return parsed;
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ export async function readPendingLogin(profile, env = process.env) {
56
+ const file = await readPendingLoginsFile(env);
57
+ const entry = file?.pending[profile];
58
+ if (entry === undefined ||
59
+ typeof entry.authBaseUrl !== 'string' ||
60
+ typeof entry.deviceCode !== 'string' ||
61
+ typeof entry.expiresAt !== 'number') {
62
+ return null;
63
+ }
64
+ return entry;
65
+ }
66
+ // Merge one profile's pending grant into the file (0600: the device code is
67
+ // redeemable for tokens once the human approves), or clear it when `pending`
68
+ // is null. Preserves the other profiles' entries.
69
+ export async function writePendingLogin(profile, pending, env = process.env) {
70
+ const existing = (await readPendingLoginsFile(env)) ?? { version: 1, pending: {} };
71
+ const entries = { ...existing.pending };
72
+ if (pending === null)
73
+ delete entries[profile];
74
+ else
75
+ entries[profile] = pending;
76
+ await writeOwnerOnlyJson(pendingLoginFilePath(env), {
77
+ version: 1,
78
+ pending: entries,
79
+ });
80
+ }
81
+ async function writeOwnerOnlyJson(path, payload) {
82
+ await mkdir(dirname(path), { recursive: true });
83
+ await writeFile(path, `${JSON.stringify(payload, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
84
+ // writeFile's `mode` applies only when it CREATES the file; an existing
85
+ // file (an older CLI wrote it, or a permissive umask) keeps its old perms.
86
+ // chmod unconditionally so a rewrite tightens a file that is already too
87
+ // open, rather than leaving the secret group/world-readable.
88
+ await chmod(path, 0o600);
89
+ }
90
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export declare const VERSION = "0.1.0";
2
+ export declare const CLI_CLIENT_ID = "cli/0.1.0";
3
+ //# sourceMappingURL=version.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,OAAO,UAAU,CAAC;AAE/B,eAAO,MAAM,aAAa,cAAmB,CAAC"}
@@ -0,0 +1,8 @@
1
+ // The package version the CLI reports on every data-API call (the
2
+ // `x-brass-client` header; a Node process has no CORS so a header is
3
+ // free here, unlike the SDK's query-param channel). Lets the platform
4
+ // measure the CLI version distribution, in particular pinned CI copies.
5
+ // A unit test pins this to package.json's version.
6
+ export const VERSION = '0.1.0';
7
+ export const CLI_CLIENT_ID = `cli/${VERSION}`;
8
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,qEAAqE;AACrE,sEAAsE;AACtE,wEAAwE;AACxE,mDAAmD;AACnD,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC;AAE/B,MAAM,CAAC,MAAM,aAAa,GAAG,OAAO,OAAO,EAAE,CAAC"}
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@brass-build/cli",
3
+ "version": "0.1.0",
4
+ "description": "Brass command-line tool: publish apps and pull schemas from a terminal or CI",
5
+ "type": "module",
6
+ "bin": {
7
+ "brass": "./dist/bin/brass.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "src",
12
+ "!src/**/__tests__/**",
13
+ "README.md",
14
+ "AGENTS.md",
15
+ "CHANGELOG.md",
16
+ "LICENSE"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc -b",
20
+ "type-check": "tsc -b",
21
+ "prepublishOnly": "tsc -b --force"
22
+ },
23
+ "dependencies": {
24
+ "fflate": "^0.8.3"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^24.12.2",
28
+ "typescript": "~6.0.0"
29
+ },
30
+ "engines": {
31
+ "node": "^20.19.0 || >=22.12.0"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/brass-build/cli.git"
39
+ },
40
+ "homepage": "https://brass.build",
41
+ "license": "MIT"
42
+ }
package/src/api.ts ADDED
@@ -0,0 +1,195 @@
1
+ // The HTTP client the commands share: a bearer-authenticated wrapper over
2
+ // the Brass data API, plus the narrow wire types the CLI reads. Wire shapes
3
+ // are declared here (not imported from `@brass-build/client`) so the CLI
4
+ // stays a standalone package with no browser-SDK dependency; the shapes it
5
+ // depends on are small and stable (the `/apps` + hosting surface).
6
+
7
+ import type { AuthProvider } from './auth.js';
8
+ import { CLI_CLIENT_ID } from './version.js';
9
+
10
+ // A schema manifest as it rides on the wire (a document's resolved schema
11
+ // and an app's `schema` declaration): the artifact's family word plus each
12
+ // stream's JSON Schema. The CLI never interprets it, only transports it
13
+ // verbatim.
14
+ export interface BrassSchemaManifest {
15
+ family: string;
16
+ streams: Record<string, Record<string, unknown>>;
17
+ }
18
+
19
+ export type AppVisibility = 'private' | 'invitee_visible' | 'public';
20
+
21
+ export interface AppDetail {
22
+ app_id: string;
23
+ name: string;
24
+ owner_organization_id?: string;
25
+ visibility?: AppVisibility;
26
+ }
27
+
28
+ export interface HostingStatus {
29
+ enabled: boolean;
30
+ slug: string | null;
31
+ url: string | null;
32
+ deployed: boolean;
33
+ active_version: string | null;
34
+ // Whether the edge gates the bundle behind the app's audience. A public
35
+ // showcase turns this off so the bundle is world-loadable.
36
+ require_access?: boolean;
37
+ }
38
+
39
+ export interface HostingUploadUrl {
40
+ upload_url: string;
41
+ version_id: string;
42
+ expires_in: number;
43
+ }
44
+
45
+ export type HostingVersionStatus = 'pending' | 'ready' | 'failed';
46
+
47
+ export interface HostingVersion {
48
+ version_id: string;
49
+ status: HostingVersionStatus;
50
+ failure_reason?: string;
51
+ active: boolean;
52
+ // The deterministic content hash the publishing client recorded for this
53
+ // version's bundle (absent on versions uploaded before hashing was added).
54
+ // `publish` compares it against the local bundle to skip an unchanged
55
+ // re-upload.
56
+ content_hash?: string;
57
+ }
58
+
59
+ // What a document holds: its streams and the contract of each. A document
60
+ // is a set of streams, not a representation, so this carries no family; the
61
+ // document's TYPE is the `schema_type` on its detail response, which is what
62
+ // `schema pull` assembles the app manifest's `family` from.
63
+ export interface DocumentStreams {
64
+ // `schema` is absent on a stream the document holds whose contract does
65
+ // not resolve: nobody published one under that name, or this caller is
66
+ // outside the publisher's read audience.
67
+ streams: { name: string; schema?: Record<string, unknown> }[];
68
+ // Set when the document holds no records yet, so the streams are the ones
69
+ // the importing app says it will produce. Still the right thing to build
70
+ // against; the CLI transports it either way.
71
+ predicted?: true;
72
+ }
73
+
74
+ export interface DocumentTypeSummary {
75
+ schema_type?: string;
76
+ }
77
+
78
+ // An organization's agentic-coding instructions (its AGENTS.md / CLAUDE.md
79
+ // body), from `GET /organizations/:id/agent-instructions`. `content` is empty
80
+ // when the org has never set them.
81
+ export interface AgentInstructionsResponse {
82
+ content: string;
83
+ updated_at?: string;
84
+ updated_by?: string;
85
+ }
86
+
87
+ // The narrow slice of `GET /organizations` the CLI reads: enough to resolve a
88
+ // single-org caller's org id and to name the orgs when the caller must pick.
89
+ export interface OrganizationSummary {
90
+ organization_id: string;
91
+ name: string;
92
+ }
93
+
94
+ export interface RefreshCapabilitiesResponse {
95
+ app_id: string;
96
+ manifest_origin: string;
97
+ opens: string[];
98
+ warnings?: string[];
99
+ }
100
+
101
+ // One error type for every API failure, carrying the HTTP status and the
102
+ // server-authored message so the CLI can render it verbatim (the API returns
103
+ // `{ error: string }` on the 4xx/5xx ladder).
104
+ export class BrassApiError extends Error {
105
+ readonly status: number;
106
+ constructor(status: number, message: string) {
107
+ super(message);
108
+ this.name = 'BrassApiError';
109
+ this.status = status;
110
+ }
111
+ }
112
+
113
+ export class BrassApi {
114
+ private readonly apiBaseUrl: string;
115
+ private readonly auth: AuthProvider;
116
+
117
+ constructor(apiBaseUrl: string, auth: AuthProvider) {
118
+ this.apiBaseUrl = apiBaseUrl;
119
+ this.auth = auth;
120
+ }
121
+
122
+ get<T>(path: string): Promise<T> {
123
+ return this.request<T>('GET', path);
124
+ }
125
+ post<T>(path: string, body?: unknown): Promise<T> {
126
+ return this.request<T>('POST', path, body);
127
+ }
128
+ patch<T>(path: string, body?: unknown): Promise<T> {
129
+ return this.request<T>('PATCH', path, body);
130
+ }
131
+
132
+ private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
133
+ const headers: Record<string, string> = {
134
+ ...(await this.auth.headers()),
135
+ // Version telemetry; the api Lambda logs it so pinned CI copies
136
+ // stay visible in the deployed-version distribution.
137
+ 'x-brass-client': CLI_CLIENT_ID,
138
+ };
139
+ let init: RequestInit = { method, headers };
140
+ if (body !== undefined) {
141
+ headers['content-type'] = 'application/json';
142
+ init = { ...init, body: JSON.stringify(body) };
143
+ }
144
+ let response: Response;
145
+ try {
146
+ response = await fetch(`${this.apiBaseUrl}${path}`, init);
147
+ } catch (cause) {
148
+ throw new BrassApiError(0, `Network error reaching ${this.apiBaseUrl}: ${String(cause)}`);
149
+ }
150
+ if (!response.ok) {
151
+ throw new BrassApiError(response.status, await errorMessage(response));
152
+ }
153
+ if (response.status === 204) return undefined as T;
154
+ return (await response.json()) as T;
155
+ }
156
+ }
157
+
158
+ // Upload bytes to a presigned S3 URL. Deliberately carries NO Brass headers
159
+ // and no bearer: the presigned signature pins an exact header set, and a
160
+ // stray `authorization` header would break it (the same rule the SDK's
161
+ // `api-headers` module enforces on the document path).
162
+ export async function putPresigned(
163
+ url: string,
164
+ bytes: Uint8Array,
165
+ contentType: string,
166
+ ): Promise<void> {
167
+ let response: Response;
168
+ try {
169
+ response = await fetch(url, {
170
+ method: 'PUT',
171
+ headers: { 'content-type': contentType },
172
+ // fflate allocates a plain ArrayBuffer, so narrowing the generic off
173
+ // `ArrayBufferLike` is sound and satisfies fetch's `BufferSource`
174
+ // (which pins `ArrayBufferView<ArrayBuffer>`) across lib configs.
175
+ body: bytes as Uint8Array<ArrayBuffer>,
176
+ });
177
+ } catch (cause) {
178
+ throw new BrassApiError(0, `Network error uploading the bundle: ${String(cause)}`);
179
+ }
180
+ if (!response.ok) {
181
+ throw new BrassApiError(response.status, `Bundle upload failed (${response.status})`);
182
+ }
183
+ }
184
+
185
+ // Pull the server's `{ error }` message off a failed response, falling back
186
+ // to the bare status when the body is not the expected shape.
187
+ async function errorMessage(response: Response): Promise<string> {
188
+ try {
189
+ const body = (await response.json()) as { error?: unknown };
190
+ if (typeof body.error === 'string' && body.error !== '') return body.error;
191
+ } catch {
192
+ // Non-JSON body (an edge / gateway error); fall through to the status.
193
+ }
194
+ return `Request failed with status ${response.status}`;
195
+ }