@mondaydotcomorg/z2h-cli 0.2.0 → 0.3.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 (98) hide show
  1. package/dist/commands/create-workspace.js +3 -3
  2. package/dist/commands/create.d.ts.map +1 -1
  3. package/dist/commands/create.js +10 -3
  4. package/dist/commands/deploy.d.ts.map +1 -1
  5. package/dist/commands/deploy.js +19 -24
  6. package/dist/commands/generate-z2h-token.d.ts +16 -0
  7. package/dist/commands/generate-z2h-token.d.ts.map +1 -0
  8. package/dist/commands/generate-z2h-token.js +107 -0
  9. package/dist/constants.d.ts +8 -0
  10. package/dist/constants.d.ts.map +1 -1
  11. package/dist/constants.js +28 -0
  12. package/dist/esm/commands/create-workspace.mjs +3 -3
  13. package/dist/esm/commands/create.d.ts.map +1 -1
  14. package/dist/esm/commands/create.mjs +10 -3
  15. package/dist/esm/commands/deploy.d.ts.map +1 -1
  16. package/dist/esm/commands/deploy.mjs +16 -21
  17. package/dist/esm/commands/generate-z2h-token.d.ts +16 -0
  18. package/dist/esm/commands/generate-z2h-token.d.ts.map +1 -0
  19. package/dist/esm/commands/generate-z2h-token.mjs +99 -0
  20. package/dist/esm/constants.d.ts +8 -0
  21. package/dist/esm/constants.d.ts.map +1 -1
  22. package/dist/esm/constants.mjs +21 -1
  23. package/dist/esm/index.mjs +18 -1
  24. package/dist/esm/util/auth/__tests__/auth-store.test.d.ts +2 -0
  25. package/dist/esm/util/auth/__tests__/auth-store.test.d.ts.map +1 -0
  26. package/dist/esm/util/auth/__tests__/broker-credential-provider.test.d.ts +2 -0
  27. package/dist/esm/util/auth/__tests__/broker-credential-provider.test.d.ts.map +1 -0
  28. package/dist/esm/util/auth/__tests__/loopback-server.test.d.ts +2 -0
  29. package/dist/esm/util/auth/__tests__/loopback-server.test.d.ts.map +1 -0
  30. package/dist/esm/util/auth/__tests__/write-auth.test.d.ts +2 -0
  31. package/dist/esm/util/auth/__tests__/write-auth.test.d.ts.map +1 -0
  32. package/dist/esm/util/auth/auth-store.d.ts +11 -0
  33. package/dist/esm/util/auth/auth-store.d.ts.map +1 -0
  34. package/dist/esm/util/auth/auth-store.mjs +29 -0
  35. package/dist/esm/util/auth/broker-credential-provider.d.ts +12 -0
  36. package/dist/esm/util/auth/broker-credential-provider.d.ts.map +1 -0
  37. package/dist/esm/util/auth/broker-credential-provider.mjs +76 -0
  38. package/dist/esm/util/auth/loopback-server.d.ts +12 -0
  39. package/dist/esm/util/auth/loopback-server.d.ts.map +1 -0
  40. package/dist/esm/util/auth/loopback-server.mjs +87 -0
  41. package/dist/esm/util/auth/write-auth.d.ts +11 -0
  42. package/dist/esm/util/auth/write-auth.d.ts.map +1 -0
  43. package/dist/esm/util/auth/write-auth.mjs +22 -0
  44. package/dist/esm/util/manifest.d.ts.map +1 -1
  45. package/dist/esm/util/manifest.mjs +2 -1
  46. package/dist/esm/util/s3/client.d.ts +7 -8
  47. package/dist/esm/util/s3/client.d.ts.map +1 -1
  48. package/dist/esm/util/s3/client.mjs +15 -30
  49. package/dist/esm/util/s3/upload.d.ts.map +1 -1
  50. package/dist/esm/util/s3/upload.mjs +2 -1
  51. package/dist/index.js +19 -1
  52. package/dist/util/auth/__tests__/auth-store.test.d.ts +2 -0
  53. package/dist/util/auth/__tests__/auth-store.test.d.ts.map +1 -0
  54. package/dist/util/auth/__tests__/broker-credential-provider.test.d.ts +2 -0
  55. package/dist/util/auth/__tests__/broker-credential-provider.test.d.ts.map +1 -0
  56. package/dist/util/auth/__tests__/loopback-server.test.d.ts +2 -0
  57. package/dist/util/auth/__tests__/loopback-server.test.d.ts.map +1 -0
  58. package/dist/util/auth/__tests__/write-auth.test.d.ts +2 -0
  59. package/dist/util/auth/__tests__/write-auth.test.d.ts.map +1 -0
  60. package/dist/util/auth/auth-store.d.ts +11 -0
  61. package/dist/util/auth/auth-store.d.ts.map +1 -0
  62. package/dist/util/auth/auth-store.js +39 -0
  63. package/dist/util/auth/broker-credential-provider.d.ts +12 -0
  64. package/dist/util/auth/broker-credential-provider.d.ts.map +1 -0
  65. package/dist/util/auth/broker-credential-provider.js +80 -0
  66. package/dist/util/auth/loopback-server.d.ts +12 -0
  67. package/dist/util/auth/loopback-server.d.ts.map +1 -0
  68. package/dist/util/auth/loopback-server.js +94 -0
  69. package/dist/util/auth/write-auth.d.ts +11 -0
  70. package/dist/util/auth/write-auth.d.ts.map +1 -0
  71. package/dist/util/auth/write-auth.js +26 -0
  72. package/dist/util/manifest.d.ts.map +1 -1
  73. package/dist/util/manifest.js +5 -4
  74. package/dist/util/s3/client.d.ts +7 -8
  75. package/dist/util/s3/client.d.ts.map +1 -1
  76. package/dist/util/s3/client.js +14 -36
  77. package/dist/util/s3/upload.d.ts.map +1 -1
  78. package/dist/util/s3/upload.js +2 -1
  79. package/package.json +10 -1
  80. package/src/commands/__tests__/create-workspace.test.ts +1 -1
  81. package/src/commands/__tests__/create.test.ts +1 -5
  82. package/src/commands/create-workspace.ts +3 -3
  83. package/src/commands/create.ts +12 -3
  84. package/src/commands/deploy.ts +18 -31
  85. package/src/commands/generate-z2h-token.ts +124 -0
  86. package/src/constants.ts +25 -0
  87. package/src/index.ts +21 -1
  88. package/src/util/auth/__tests__/auth-store.test.ts +50 -0
  89. package/src/util/auth/__tests__/broker-credential-provider.test.ts +115 -0
  90. package/src/util/auth/__tests__/loopback-server.test.ts +85 -0
  91. package/src/util/auth/__tests__/write-auth.test.ts +79 -0
  92. package/src/util/auth/auth-store.ts +31 -0
  93. package/src/util/auth/broker-credential-provider.ts +93 -0
  94. package/src/util/auth/loopback-server.ts +103 -0
  95. package/src/util/auth/write-auth.ts +24 -0
  96. package/src/util/manifest.ts +2 -1
  97. package/src/util/s3/client.ts +15 -32
  98. package/src/util/s3/upload.ts +2 -1
@@ -0,0 +1,93 @@
1
+ import { readZthAuth } from './auth-store';
2
+ import type { AwsCredentialIdentity, AwsCredentialIdentityProvider } from '@aws-sdk/types';
3
+
4
+ export const DEFAULT_BROKER_BASE_URL = 'https://bigbrain-zth.bigbrain.me';
5
+ export const STS_BROKER_PATH = '/z2h-cli/sts/credentials';
6
+
7
+ // Refresh the cached credentials this many ms before they expire so the next
8
+ // AWS API call doesn't race the broker round-trip against a hard expiry.
9
+ const REFRESH_BUFFER_MS = 5 * 60 * 1000;
10
+
11
+ interface BrokerResponse {
12
+ accessKeyId?: string;
13
+ secretAccessKey?: string;
14
+ sessionToken?: string;
15
+ expiration?: string;
16
+ }
17
+
18
+ function brokerBaseUrl(): string {
19
+ return process.env.Z2H_BROKER_BASE_URL || DEFAULT_BROKER_BASE_URL;
20
+ }
21
+
22
+ async function fetchCredentialsOnce(appName: string): Promise<AwsCredentialIdentity> {
23
+ const { token } = await readZthAuth();
24
+ const url = `${brokerBaseUrl()}${STS_BROKER_PATH}`;
25
+
26
+ let res: Response;
27
+ try {
28
+ res = await fetch(url, {
29
+ method: 'POST',
30
+ headers: {
31
+ Authorization: `Bearer ${token}`,
32
+ 'Content-Type': 'application/json',
33
+ Accept: 'application/json',
34
+ },
35
+ body: JSON.stringify({ appName }),
36
+ });
37
+ } catch (err) {
38
+ const message = err instanceof Error ? err.message : String(err);
39
+ throw new Error(`Z2H deploy broker request failed: ${message}`);
40
+ }
41
+
42
+ if (res.status === 401 || res.status === 403) {
43
+ throw new Error(
44
+ `Z2H deploy token rejected by the broker (${res.status}). Refresh user token by running 'z2h-cli generate-z2h-token'.`
45
+ );
46
+ }
47
+ if (!res.ok) {
48
+ const text = await res.text();
49
+ throw new Error(`Z2H deploy broker returned ${res.status}: ${text.slice(0, 1024)}`);
50
+ }
51
+
52
+ const body = (await res.json()) as BrokerResponse;
53
+ if (!body.accessKeyId || !body.secretAccessKey || !body.sessionToken || !body.expiration) {
54
+ throw new Error('Z2H deploy broker response missing required credential fields');
55
+ }
56
+
57
+ return {
58
+ accessKeyId: body.accessKeyId,
59
+ secretAccessKey: body.secretAccessKey,
60
+ sessionToken: body.sessionToken,
61
+ expiration: new Date(body.expiration),
62
+ };
63
+ }
64
+
65
+ /**
66
+ * AWS SDK credential provider that fetches short-lived deploy credentials
67
+ * from the bigbrain-zth STS broker. Credentials are cached in memory until
68
+ * 5 minutes before expiry; concurrent calls share a single in-flight fetch.
69
+ */
70
+ export function brokerCredentialProvider({ appName }: { appName: string }): AwsCredentialIdentityProvider {
71
+ let cached: AwsCredentialIdentity | undefined;
72
+ let inflight: Promise<AwsCredentialIdentity> | undefined;
73
+
74
+ return async () => {
75
+ const now = Date.now();
76
+ if (cached && cached.expiration && cached.expiration.getTime() - now > REFRESH_BUFFER_MS) {
77
+ return cached;
78
+ }
79
+ if (inflight) {
80
+ return inflight;
81
+ }
82
+
83
+ inflight = fetchCredentialsOnce(appName)
84
+ .then(creds => {
85
+ cached = creds;
86
+ return creds;
87
+ })
88
+ .finally(() => {
89
+ inflight = undefined;
90
+ });
91
+ return inflight;
92
+ };
93
+ }
@@ -0,0 +1,103 @@
1
+ import cors from 'cors';
2
+ import express, { json } from 'express';
3
+ import type { AddressInfo } from 'node:net';
4
+
5
+ const ALLOWED_ORIGINS = new Set(['https://bigbrain.me', 'https://bigbrainstaging.me']);
6
+ const CALLBACK_PATH = '/token';
7
+ const REQUEST_BODY_LIMIT = '16kb';
8
+ const PREFLIGHT_MAX_AGE_SECONDS = 600;
9
+
10
+ export interface LoopbackHandle {
11
+ /** Fully-qualified URL the browser should POST to (e.g. http://127.0.0.1:53811/token). */
12
+ callbackUrl: string;
13
+ /**
14
+ * Resolves with the token once the page POSTs it back. Rejects on timeout
15
+ * or if the server is closed before a token arrives.
16
+ */
17
+ waitForToken(timeoutMs: number): Promise<string>;
18
+ close(): void;
19
+ }
20
+
21
+ export function startLoopbackServer(): Promise<LoopbackHandle> {
22
+ return new Promise((resolve, reject) => {
23
+ let resolveToken: ((token: string) => void) | null = null;
24
+ let rejectToken: ((err: Error) => void) | null = null;
25
+
26
+ const app = express();
27
+
28
+ // Reflect the origin if it's one of ours; otherwise wildcard. The
29
+ // endpoint is loopback-bound and single-shot, so the CORS surface is
30
+ // moot — we keep allowlist behavior so the test contract stays exact.
31
+ app.use(
32
+ cors({
33
+ origin: (origin, cb) => {
34
+ if (origin && ALLOWED_ORIGINS.has(origin)) {
35
+ cb(null, origin);
36
+ } else {
37
+ cb(null, '*');
38
+ }
39
+ },
40
+ methods: ['POST', 'OPTIONS'],
41
+ allowedHeaders: ['Content-Type'],
42
+ maxAge: PREFLIGHT_MAX_AGE_SECONDS,
43
+ })
44
+ );
45
+ app.use(json({ limit: REQUEST_BODY_LIMIT }));
46
+
47
+ app.post(CALLBACK_PATH, (req, res) => {
48
+ const body = req.body as { token?: unknown } | undefined;
49
+ const token = typeof body?.token === 'string' ? body.token.trim() : '';
50
+ if (!token) {
51
+ res.status(400).json({ error: 'missing token' });
52
+ return;
53
+ }
54
+ // Ack the page so it can transition to "Token sent ✓" and auto-close.
55
+ // The bigbrain.me page never displays this body — it just inspects res.ok.
56
+ res.status(200).json({ ok: true });
57
+ if (resolveToken) {
58
+ const fn = resolveToken;
59
+ resolveToken = null;
60
+ rejectToken = null;
61
+ fn(token);
62
+ }
63
+ });
64
+
65
+ const server = app.listen(0, '127.0.0.1', () => {
66
+ const address = server.address() as AddressInfo | null;
67
+ if (!address || typeof address === 'string') {
68
+ server.close();
69
+ reject(new Error('Failed to determine loopback port'));
70
+ return;
71
+ }
72
+ resolve({
73
+ callbackUrl: `http://127.0.0.1:${address.port}${CALLBACK_PATH}`,
74
+ waitForToken(timeoutMs) {
75
+ return new Promise<string>((res, rej) => {
76
+ resolveToken = res;
77
+ rejectToken = rej;
78
+ const timer = setTimeout(() => {
79
+ if (rejectToken) {
80
+ const reject = rejectToken;
81
+ resolveToken = null;
82
+ rejectToken = null;
83
+ reject(new Error('Loopback callback timed out'));
84
+ }
85
+ }, timeoutMs);
86
+ // Don't keep the event loop alive on the timer alone.
87
+ timer.unref?.();
88
+ });
89
+ },
90
+ close() {
91
+ if (rejectToken) {
92
+ const reject = rejectToken;
93
+ resolveToken = null;
94
+ rejectToken = null;
95
+ reject(new Error('Loopback server closed before token arrived'));
96
+ }
97
+ server.close();
98
+ },
99
+ });
100
+ });
101
+ server.on('error', reject);
102
+ });
103
+ }
@@ -0,0 +1,24 @@
1
+ import { chmod } from 'node:fs/promises';
2
+ import { ensureDir, writeFile } from 'fs-extra';
3
+ import { Z2H_AUTH_DIR, Z2H_AUTH_FILE, type ZthAuth } from './auth-store';
4
+
5
+ // 0o600 — owner read/write only. The file holds a long-lived bearer token,
6
+ // so other local users on the machine must not be able to read it.
7
+ const OWNER_READ_WRITE_FILE_MODE = 0o600;
8
+
9
+ export async function writeZthAuth(auth: ZthAuth): Promise<{ filePath: string }> {
10
+ await ensureDir(Z2H_AUTH_DIR);
11
+ await writeFile(Z2H_AUTH_FILE, `${JSON.stringify(auth, null, 2)}\n`);
12
+ await chmod(Z2H_AUTH_FILE, OWNER_READ_WRITE_FILE_MODE);
13
+ return { filePath: Z2H_AUTH_FILE };
14
+ }
15
+
16
+ /**
17
+ * Three base64url segments separated by dots — a structural sanity check,
18
+ * not a real verification. Server-side validation happens at the STS broker.
19
+ */
20
+ export function validateJwtPattern(token: string): boolean {
21
+ return /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token);
22
+ }
23
+
24
+ export { Z2H_AUTH_FILE } from './auth-store';
@@ -1,5 +1,6 @@
1
1
  import { PutObjectCommand } from '@aws-sdk/client-s3';
2
- import { BUCKET_WRITE_NAME, MANIFEST_KEY, MANIFEST_READ_URL, getS3Client } from './s3/client';
2
+ import { BUCKET_WRITE_NAME, MANIFEST_KEY, MANIFEST_READ_URL } from '../constants';
3
+ import { getS3Client } from './s3/client';
3
4
 
4
5
  export interface SharedDep {
5
6
  name?: string;
@@ -1,40 +1,23 @@
1
- import { HeadBucketCommand, S3Client, S3ServiceException } from '@aws-sdk/client-s3';
2
-
3
- export const BUCKET_WRITE_NAME = 'prod-use1-bigbrain-zth-mf-assets';
4
- export const REGION = 'us-east-1';
5
- export const BUCKET_WRITE_URL = `https://${BUCKET_WRITE_NAME}.s3.${REGION}.amazonaws.com`;
6
-
7
- export const BUCKET_READ_DNS_NAME = 'bigbrain-zth-mf-assets';
8
- export const BUCKET_READ_DNS_URL = `https://${BUCKET_READ_DNS_NAME}.bigbrain.me`;
9
-
10
- // No `.json` extension — TridentSkeleton's `mfUrl` API appends `/mf-assets`
11
- // to the bucket base when fetching, so the manifest must live at this exact
12
- // key. See microfrontends/mf-bigbrain-zth/.../ZeroToHeroHost.tsx.
13
- export const MANIFEST_KEY = 'mf-assets';
14
- export const MANIFEST_READ_URL = `${BUCKET_READ_DNS_URL}/${MANIFEST_KEY}`;
1
+ import { S3Client } from '@aws-sdk/client-s3';
2
+ import { brokerCredentialProvider } from '../auth/broker-credential-provider';
3
+ import { REGION } from '../../constants';
15
4
 
16
5
  let client: S3Client | null = null;
17
6
 
7
+ /**
8
+ * Initialize the per-process S3 client with broker-issued credentials scoped
9
+ * to the given app's S3 prefix. Must be called once before any `getS3Client`.
10
+ */
11
+ export function setupS3Client({ appName }: { appName: string }): void {
12
+ client = new S3Client({
13
+ region: REGION,
14
+ credentials: brokerCredentialProvider({ appName }),
15
+ });
16
+ }
17
+
18
18
  export function getS3Client(): S3Client {
19
19
  if (!client) {
20
- client = new S3Client({ region: REGION });
20
+ throw new Error('S3 client not initialized — call setupS3Client({ appName }) first.');
21
21
  }
22
22
  return client;
23
23
  }
24
-
25
- export async function assertBucketExists(): Promise<void> {
26
- try {
27
- await getS3Client().send(new HeadBucketCommand({ Bucket: BUCKET_WRITE_NAME }));
28
- } catch (err) {
29
- if (err instanceof S3ServiceException) {
30
- const status = err.$metadata.httpStatusCode;
31
- if (status === 404 || err.name === 'NotFound' || err.name === 'NoSuchBucket') {
32
- throw new Error(`S3 bucket "${BUCKET_WRITE_NAME}" does not exist (region: ${REGION}).`);
33
- }
34
- if (status === 403) {
35
- throw new Error(`No access to S3 bucket "${BUCKET_WRITE_NAME}" (region: ${REGION}). Check AWS credentials.`);
36
- }
37
- }
38
- throw err;
39
- }
40
- }
@@ -4,7 +4,8 @@ import { readFile } from 'fs-extra';
4
4
  import { lookup as lookupMime } from 'mime-types';
5
5
  import pMap from 'p-map';
6
6
  import { PutObjectCommand } from '@aws-sdk/client-s3';
7
- import { BUCKET_WRITE_NAME, getS3Client } from './client';
7
+ import { BUCKET_WRITE_NAME } from '../../constants';
8
+ import { getS3Client } from './client';
8
9
 
9
10
  // Each deploy uploads to a version-prefixed path (<app>/<version>/...), so
10
11
  // the URL itself is the cache key. Assets never need to be revalidated.