@mondaydotcomorg/z2h-cli 0.2.0 → 0.3.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 (93) hide show
  1. package/dist/commands/create.d.ts.map +1 -1
  2. package/dist/commands/create.js +9 -2
  3. package/dist/commands/deploy.d.ts.map +1 -1
  4. package/dist/commands/deploy.js +19 -24
  5. package/dist/commands/generate-z2h-token.d.ts +16 -0
  6. package/dist/commands/generate-z2h-token.d.ts.map +1 -0
  7. package/dist/commands/generate-z2h-token.js +107 -0
  8. package/dist/constants.d.ts +8 -0
  9. package/dist/constants.d.ts.map +1 -1
  10. package/dist/constants.js +28 -0
  11. package/dist/esm/commands/create.d.ts.map +1 -1
  12. package/dist/esm/commands/create.mjs +9 -2
  13. package/dist/esm/commands/deploy.d.ts.map +1 -1
  14. package/dist/esm/commands/deploy.mjs +16 -21
  15. package/dist/esm/commands/generate-z2h-token.d.ts +16 -0
  16. package/dist/esm/commands/generate-z2h-token.d.ts.map +1 -0
  17. package/dist/esm/commands/generate-z2h-token.mjs +99 -0
  18. package/dist/esm/constants.d.ts +8 -0
  19. package/dist/esm/constants.d.ts.map +1 -1
  20. package/dist/esm/constants.mjs +21 -1
  21. package/dist/esm/index.mjs +15 -0
  22. package/dist/esm/util/auth/__tests__/auth-store.test.d.ts +2 -0
  23. package/dist/esm/util/auth/__tests__/auth-store.test.d.ts.map +1 -0
  24. package/dist/esm/util/auth/__tests__/broker-credential-provider.test.d.ts +2 -0
  25. package/dist/esm/util/auth/__tests__/broker-credential-provider.test.d.ts.map +1 -0
  26. package/dist/esm/util/auth/__tests__/loopback-server.test.d.ts +2 -0
  27. package/dist/esm/util/auth/__tests__/loopback-server.test.d.ts.map +1 -0
  28. package/dist/esm/util/auth/__tests__/write-auth.test.d.ts +2 -0
  29. package/dist/esm/util/auth/__tests__/write-auth.test.d.ts.map +1 -0
  30. package/dist/esm/util/auth/auth-store.d.ts +11 -0
  31. package/dist/esm/util/auth/auth-store.d.ts.map +1 -0
  32. package/dist/esm/util/auth/auth-store.mjs +29 -0
  33. package/dist/esm/util/auth/broker-credential-provider.d.ts +12 -0
  34. package/dist/esm/util/auth/broker-credential-provider.d.ts.map +1 -0
  35. package/dist/esm/util/auth/broker-credential-provider.mjs +76 -0
  36. package/dist/esm/util/auth/loopback-server.d.ts +12 -0
  37. package/dist/esm/util/auth/loopback-server.d.ts.map +1 -0
  38. package/dist/esm/util/auth/loopback-server.mjs +87 -0
  39. package/dist/esm/util/auth/write-auth.d.ts +11 -0
  40. package/dist/esm/util/auth/write-auth.d.ts.map +1 -0
  41. package/dist/esm/util/auth/write-auth.mjs +22 -0
  42. package/dist/esm/util/manifest.d.ts.map +1 -1
  43. package/dist/esm/util/manifest.mjs +2 -1
  44. package/dist/esm/util/s3/client.d.ts +7 -8
  45. package/dist/esm/util/s3/client.d.ts.map +1 -1
  46. package/dist/esm/util/s3/client.mjs +15 -30
  47. package/dist/esm/util/s3/upload.d.ts.map +1 -1
  48. package/dist/esm/util/s3/upload.mjs +2 -1
  49. package/dist/index.js +15 -0
  50. package/dist/util/auth/__tests__/auth-store.test.d.ts +2 -0
  51. package/dist/util/auth/__tests__/auth-store.test.d.ts.map +1 -0
  52. package/dist/util/auth/__tests__/broker-credential-provider.test.d.ts +2 -0
  53. package/dist/util/auth/__tests__/broker-credential-provider.test.d.ts.map +1 -0
  54. package/dist/util/auth/__tests__/loopback-server.test.d.ts +2 -0
  55. package/dist/util/auth/__tests__/loopback-server.test.d.ts.map +1 -0
  56. package/dist/util/auth/__tests__/write-auth.test.d.ts +2 -0
  57. package/dist/util/auth/__tests__/write-auth.test.d.ts.map +1 -0
  58. package/dist/util/auth/auth-store.d.ts +11 -0
  59. package/dist/util/auth/auth-store.d.ts.map +1 -0
  60. package/dist/util/auth/auth-store.js +39 -0
  61. package/dist/util/auth/broker-credential-provider.d.ts +12 -0
  62. package/dist/util/auth/broker-credential-provider.d.ts.map +1 -0
  63. package/dist/util/auth/broker-credential-provider.js +80 -0
  64. package/dist/util/auth/loopback-server.d.ts +12 -0
  65. package/dist/util/auth/loopback-server.d.ts.map +1 -0
  66. package/dist/util/auth/loopback-server.js +94 -0
  67. package/dist/util/auth/write-auth.d.ts +11 -0
  68. package/dist/util/auth/write-auth.d.ts.map +1 -0
  69. package/dist/util/auth/write-auth.js +26 -0
  70. package/dist/util/manifest.d.ts.map +1 -1
  71. package/dist/util/manifest.js +5 -4
  72. package/dist/util/s3/client.d.ts +7 -8
  73. package/dist/util/s3/client.d.ts.map +1 -1
  74. package/dist/util/s3/client.js +14 -36
  75. package/dist/util/s3/upload.d.ts.map +1 -1
  76. package/dist/util/s3/upload.js +2 -1
  77. package/package.json +10 -1
  78. package/src/commands/create.ts +11 -2
  79. package/src/commands/deploy.ts +18 -31
  80. package/src/commands/generate-z2h-token.ts +124 -0
  81. package/src/constants.ts +25 -0
  82. package/src/index.ts +17 -0
  83. package/src/util/auth/__tests__/auth-store.test.ts +50 -0
  84. package/src/util/auth/__tests__/broker-credential-provider.test.ts +115 -0
  85. package/src/util/auth/__tests__/loopback-server.test.ts +85 -0
  86. package/src/util/auth/__tests__/write-auth.test.ts +79 -0
  87. package/src/util/auth/auth-store.ts +31 -0
  88. package/src/util/auth/broker-credential-provider.ts +93 -0
  89. package/src/util/auth/loopback-server.ts +103 -0
  90. package/src/util/auth/write-auth.ts +24 -0
  91. package/src/util/manifest.ts +2 -1
  92. package/src/util/s3/client.ts +15 -32
  93. package/src/util/s3/upload.ts +2 -1
@@ -0,0 +1,12 @@
1
+ export interface LoopbackHandle {
2
+ /** Fully-qualified URL the browser should POST to (e.g. http://127.0.0.1:53811/token). */
3
+ callbackUrl: string;
4
+ /**
5
+ * Resolves with the token once the page POSTs it back. Rejects on timeout
6
+ * or if the server is closed before a token arrives.
7
+ */
8
+ waitForToken(timeoutMs: number): Promise<string>;
9
+ close(): void;
10
+ }
11
+ export declare function startLoopbackServer(): Promise<LoopbackHandle>;
12
+ //# sourceMappingURL=loopback-server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loopback-server.d.ts","sourceRoot":"","sources":["../../../src/util/auth/loopback-server.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,cAAc;IAC7B,0FAA0F;IAC1F,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD,KAAK,IAAI,IAAI,CAAC;CACf;AAED,wBAAgB,mBAAmB,IAAI,OAAO,CAAC,cAAc,CAAC,CAkF7D"}
@@ -0,0 +1,94 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ const cors = require('cors');
4
+ const express = require('express');
5
+
6
+ const _interopDefault = e => e && e.__esModule ? e : { default: e };
7
+
8
+ const cors__default = /*#__PURE__*/_interopDefault(cors);
9
+ const express__default = /*#__PURE__*/_interopDefault(express);
10
+
11
+ const ALLOWED_ORIGINS = new Set(['https://bigbrain.me', 'https://bigbrainstaging.me']);
12
+ const CALLBACK_PATH = '/token';
13
+ const REQUEST_BODY_LIMIT = '16kb';
14
+ const PREFLIGHT_MAX_AGE_SECONDS = 600;
15
+ function startLoopbackServer() {
16
+ return new Promise((resolve, reject) => {
17
+ let resolveToken = null;
18
+ let rejectToken = null;
19
+ const app = express__default.default();
20
+ // Reflect the origin if it's one of ours; otherwise wildcard. The
21
+ // endpoint is loopback-bound and single-shot, so the CORS surface is
22
+ // moot — we keep allowlist behavior so the test contract stays exact.
23
+ app.use(cors__default.default({
24
+ origin: (origin, cb) => {
25
+ if (origin && ALLOWED_ORIGINS.has(origin)) {
26
+ cb(null, origin);
27
+ }
28
+ else {
29
+ cb(null, '*');
30
+ }
31
+ },
32
+ methods: ['POST', 'OPTIONS'],
33
+ allowedHeaders: ['Content-Type'],
34
+ maxAge: PREFLIGHT_MAX_AGE_SECONDS,
35
+ }));
36
+ app.use(express.json({ limit: REQUEST_BODY_LIMIT }));
37
+ app.post(CALLBACK_PATH, (req, res) => {
38
+ const body = req.body;
39
+ const token = typeof body?.token === 'string' ? body.token.trim() : '';
40
+ if (!token) {
41
+ res.status(400).json({ error: 'missing token' });
42
+ return;
43
+ }
44
+ // Ack the page so it can transition to "Token sent ✓" and auto-close.
45
+ // The bigbrain.me page never displays this body — it just inspects res.ok.
46
+ res.status(200).json({ ok: true });
47
+ if (resolveToken) {
48
+ const fn = resolveToken;
49
+ resolveToken = null;
50
+ rejectToken = null;
51
+ fn(token);
52
+ }
53
+ });
54
+ const server = app.listen(0, '127.0.0.1', () => {
55
+ const address = server.address();
56
+ if (!address || typeof address === 'string') {
57
+ server.close();
58
+ reject(new Error('Failed to determine loopback port'));
59
+ return;
60
+ }
61
+ resolve({
62
+ callbackUrl: `http://127.0.0.1:${address.port}${CALLBACK_PATH}`,
63
+ waitForToken(timeoutMs) {
64
+ return new Promise((res, rej) => {
65
+ resolveToken = res;
66
+ rejectToken = rej;
67
+ const timer = setTimeout(() => {
68
+ if (rejectToken) {
69
+ const reject = rejectToken;
70
+ resolveToken = null;
71
+ rejectToken = null;
72
+ reject(new Error('Loopback callback timed out'));
73
+ }
74
+ }, timeoutMs);
75
+ // Don't keep the event loop alive on the timer alone.
76
+ timer.unref?.();
77
+ });
78
+ },
79
+ close() {
80
+ if (rejectToken) {
81
+ const reject = rejectToken;
82
+ resolveToken = null;
83
+ rejectToken = null;
84
+ reject(new Error('Loopback server closed before token arrived'));
85
+ }
86
+ server.close();
87
+ },
88
+ });
89
+ });
90
+ server.on('error', reject);
91
+ });
92
+ }
93
+
94
+ exports.startLoopbackServer = startLoopbackServer;
@@ -0,0 +1,11 @@
1
+ import { type ZthAuth } from './auth-store';
2
+ export declare function writeZthAuth(auth: ZthAuth): Promise<{
3
+ filePath: string;
4
+ }>;
5
+ /**
6
+ * Three base64url segments separated by dots — a structural sanity check,
7
+ * not a real verification. Server-side validation happens at the STS broker.
8
+ */
9
+ export declare function validateJwtPattern(token: string): boolean;
10
+ export { Z2H_AUTH_FILE } from './auth-store';
11
+ //# sourceMappingURL=write-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write-auth.d.ts","sourceRoot":"","sources":["../../../src/util/auth/write-auth.ts"],"names":[],"mappings":"AAEA,OAAO,EAA+B,KAAK,OAAO,EAAE,MAAM,cAAc,CAAC;AAMzE,wBAAsB,YAAY,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAK/E;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEzD;AAED,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,26 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ const promises = require('node:fs/promises');
4
+ const fsExtra = require('fs-extra');
5
+ const util_auth_authStore = require('./auth-store.js');
6
+
7
+ // 0o600 — owner read/write only. The file holds a long-lived bearer token,
8
+ // so other local users on the machine must not be able to read it.
9
+ const OWNER_READ_WRITE_FILE_MODE = 0o600;
10
+ async function writeZthAuth(auth) {
11
+ await fsExtra.ensureDir(util_auth_authStore.Z2H_AUTH_DIR);
12
+ await fsExtra.writeFile(util_auth_authStore.Z2H_AUTH_FILE, `${JSON.stringify(auth, null, 2)}\n`);
13
+ await promises.chmod(util_auth_authStore.Z2H_AUTH_FILE, OWNER_READ_WRITE_FILE_MODE);
14
+ return { filePath: util_auth_authStore.Z2H_AUTH_FILE };
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
+ function validateJwtPattern(token) {
21
+ return /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token);
22
+ }
23
+
24
+ exports.Z2H_AUTH_FILE = util_auth_authStore.Z2H_AUTH_FILE;
25
+ exports.validateJwtPattern = validateJwtPattern;
26
+ exports.writeZthAuth = writeZthAuth;
@@ -1 +1 @@
1
- {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/util/manifest.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAErD,wBAAsB,YAAY,IAAI,OAAO,CAAC,QAAQ,CAAC,CAQtD;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,UAI1D;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,QAAQ,CAE3F;AAED,wBAAsB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAWrE"}
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/util/manifest.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;;GAIG;AACH,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,OAAO,CAAC;IACpB,oBAAoB,EAAE,OAAO,CAAC;IAC9B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,UAAU,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,aAAc,SAAQ,iBAAiB;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAErD,wBAAsB,YAAY,IAAI,OAAO,CAAC,QAAQ,CAAC,CAQtD;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,UAI1D;AAED,wBAAgB,WAAW,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,QAAQ,CAE3F;AAED,wBAAsB,aAAa,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAWrE"}
@@ -1,13 +1,14 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
 
3
3
  const clientS3 = require('@aws-sdk/client-s3');
4
+ const constants = require('../constants.js');
4
5
  const util_s3_client = require('./s3/client.js');
5
6
 
6
7
  async function readManifest() {
7
8
  // Read via the public CDN endpoint, no AWS auth required.
8
- const res = await fetch(util_s3_client.MANIFEST_READ_URL);
9
+ const res = await fetch(constants.MANIFEST_READ_URL);
9
10
  if (!res.ok) {
10
- throw new Error(`failed to read manifest from ${util_s3_client.MANIFEST_READ_URL}: HTTP ${res.status}`);
11
+ throw new Error(`failed to read manifest from ${constants.MANIFEST_READ_URL}: HTTP ${res.status}`);
11
12
  }
12
13
  return (await res.json());
13
14
  }
@@ -22,8 +23,8 @@ function upsertEntry(manifest, app, entry) {
22
23
  async function writeManifest(manifest) {
23
24
  const s3 = util_s3_client.getS3Client();
24
25
  await s3.send(new clientS3.PutObjectCommand({
25
- Bucket: util_s3_client.BUCKET_WRITE_NAME,
26
- Key: util_s3_client.MANIFEST_KEY,
26
+ Bucket: constants.BUCKET_WRITE_NAME,
27
+ Key: constants.MANIFEST_KEY,
27
28
  Body: JSON.stringify(manifest, null, 2),
28
29
  ContentType: 'application/json',
29
30
  CacheControl: 'no-cache',
@@ -1,11 +1,10 @@
1
1
  import { S3Client } from '@aws-sdk/client-s3';
2
- export declare const BUCKET_WRITE_NAME = "prod-use1-bigbrain-zth-mf-assets";
3
- export declare const REGION = "us-east-1";
4
- export declare const BUCKET_WRITE_URL = "https://prod-use1-bigbrain-zth-mf-assets.s3.us-east-1.amazonaws.com";
5
- export declare const BUCKET_READ_DNS_NAME = "bigbrain-zth-mf-assets";
6
- export declare const BUCKET_READ_DNS_URL = "https://bigbrain-zth-mf-assets.bigbrain.me";
7
- export declare const MANIFEST_KEY = "mf-assets";
8
- export declare const MANIFEST_READ_URL = "https://bigbrain-zth-mf-assets.bigbrain.me/mf-assets";
2
+ /**
3
+ * Initialize the per-process S3 client with broker-issued credentials scoped
4
+ * to the given app's S3 prefix. Must be called once before any `getS3Client`.
5
+ */
6
+ export declare function setupS3Client({ appName }: {
7
+ appName: string;
8
+ }): void;
9
9
  export declare function getS3Client(): S3Client;
10
- export declare function assertBucketExists(): Promise<void>;
11
10
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/util/s3/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,QAAQ,EAAsB,MAAM,oBAAoB,CAAC;AAErF,eAAO,MAAM,iBAAiB,qCAAqC,CAAC;AACpE,eAAO,MAAM,MAAM,cAAc,CAAC;AAClC,eAAO,MAAM,gBAAgB,wEAA4D,CAAC;AAE1F,eAAO,MAAM,oBAAoB,2BAA2B,CAAC;AAC7D,eAAO,MAAM,mBAAmB,+CAAgD,CAAC;AAKjF,eAAO,MAAM,YAAY,cAAc,CAAC;AACxC,eAAO,MAAM,iBAAiB,yDAA2C,CAAC;AAI1E,wBAAgB,WAAW,IAAI,QAAQ,CAKtC;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAexD"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/util/s3/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAM9C;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAKpE;AAED,wBAAgB,WAAW,IAAI,QAAQ,CAKtC"}
@@ -1,48 +1,26 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
2
 
3
3
  const clientS3 = require('@aws-sdk/client-s3');
4
+ const util_auth_brokerCredentialProvider = require('../auth/broker-credential-provider.js');
5
+ const constants = require('../../constants.js');
4
6
 
5
- const BUCKET_WRITE_NAME = 'prod-use1-bigbrain-zth-mf-assets';
6
- const REGION = 'us-east-1';
7
- const BUCKET_WRITE_URL = `https://${BUCKET_WRITE_NAME}.s3.${REGION}.amazonaws.com`;
8
- const BUCKET_READ_DNS_NAME = 'bigbrain-zth-mf-assets';
9
- const BUCKET_READ_DNS_URL = `https://${BUCKET_READ_DNS_NAME}.bigbrain.me`;
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
- const MANIFEST_KEY = 'mf-assets';
14
- const MANIFEST_READ_URL = `${BUCKET_READ_DNS_URL}/${MANIFEST_KEY}`;
15
7
  let client = null;
8
+ /**
9
+ * Initialize the per-process S3 client with broker-issued credentials scoped
10
+ * to the given app's S3 prefix. Must be called once before any `getS3Client`.
11
+ */
12
+ function setupS3Client({ appName }) {
13
+ client = new clientS3.S3Client({
14
+ region: constants.REGION,
15
+ credentials: util_auth_brokerCredentialProvider.brokerCredentialProvider({ appName }),
16
+ });
17
+ }
16
18
  function getS3Client() {
17
19
  if (!client) {
18
- client = new clientS3.S3Client({ region: REGION });
20
+ throw new Error('S3 client not initialized — call setupS3Client({ appName }) first.');
19
21
  }
20
22
  return client;
21
23
  }
22
- async function assertBucketExists() {
23
- try {
24
- await getS3Client().send(new clientS3.HeadBucketCommand({ Bucket: BUCKET_WRITE_NAME }));
25
- }
26
- catch (err) {
27
- if (err instanceof clientS3.S3ServiceException) {
28
- const status = err.$metadata.httpStatusCode;
29
- if (status === 404 || err.name === 'NotFound' || err.name === 'NoSuchBucket') {
30
- throw new Error(`S3 bucket "${BUCKET_WRITE_NAME}" does not exist (region: ${REGION}).`);
31
- }
32
- if (status === 403) {
33
- throw new Error(`No access to S3 bucket "${BUCKET_WRITE_NAME}" (region: ${REGION}). Check AWS credentials.`);
34
- }
35
- }
36
- throw err;
37
- }
38
- }
39
24
 
40
- exports.BUCKET_READ_DNS_NAME = BUCKET_READ_DNS_NAME;
41
- exports.BUCKET_READ_DNS_URL = BUCKET_READ_DNS_URL;
42
- exports.BUCKET_WRITE_NAME = BUCKET_WRITE_NAME;
43
- exports.BUCKET_WRITE_URL = BUCKET_WRITE_URL;
44
- exports.MANIFEST_KEY = MANIFEST_KEY;
45
- exports.MANIFEST_READ_URL = MANIFEST_READ_URL;
46
- exports.REGION = REGION;
47
- exports.assertBucketExists = assertBucketExists;
48
25
  exports.getS3Client = getS3Client;
26
+ exports.setupS3Client = setupS3Client;
@@ -1 +1 @@
1
- {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/util/s3/upload.ts"],"names":[],"mappings":"AAcA,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA0BtF"}
1
+ {"version":3,"file":"upload.d.ts","sourceRoot":"","sources":["../../../src/util/s3/upload.ts"],"names":[],"mappings":"AAeA,wBAAsB,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA0BtF"}
@@ -6,6 +6,7 @@ const fsExtra = require('fs-extra');
6
6
  const mimeTypes = require('mime-types');
7
7
  const pMap = require('p-map');
8
8
  const clientS3 = require('@aws-sdk/client-s3');
9
+ const constants = require('../../constants.js');
9
10
  const util_s3_client = require('./client.js');
10
11
 
11
12
  const _interopDefault = e => e && e.__esModule ? e : { default: e };
@@ -26,7 +27,7 @@ async function uploadDir(localDir, keyPrefix) {
26
27
  const contentType = mimeTypes.lookup(rel) || 'application/octet-stream';
27
28
  const key = `${keyPrefix}/${rel.split(path__default.default.sep).join('/')}`;
28
29
  await s3.send(new clientS3.PutObjectCommand({
29
- Bucket: util_s3_client.BUCKET_WRITE_NAME,
30
+ Bucket: constants.BUCKET_WRITE_NAME,
30
31
  Key: key,
31
32
  Body: body,
32
33
  ContentType: contentType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mondaydotcomorg/z2h-cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "bin": "./bin/z2h-cli.js",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -10,6 +10,11 @@
10
10
  "require": "./dist/index.js",
11
11
  "types": "./dist/index.d.ts"
12
12
  },
13
+ "./constants": {
14
+ "import": "./dist/esm/constants.mjs",
15
+ "require": "./dist/constants.js",
16
+ "types": "./dist/constants.d.ts"
17
+ },
13
18
  "./package.json": "./package.json"
14
19
  },
15
20
  "nx": {
@@ -35,7 +40,9 @@
35
40
  "@aws-sdk/client-s3": "^3.706.0",
36
41
  "chokidar": "^3.6.0",
37
42
  "commander": "^12.1.0",
43
+ "cors": "^2.8.6",
38
44
  "execa": "^9.5.1",
45
+ "express": "^5.2.1",
39
46
  "fs-extra": "^11.2.0",
40
47
  "mime-types": "^2.1.35",
41
48
  "p-map": "^7.0.2",
@@ -44,6 +51,8 @@
44
51
  },
45
52
  "devDependencies": {
46
53
  "@mondaydotcomorg/trident-library": "^1.2.7",
54
+ "@types/cors": "^2.8.19",
55
+ "@types/express": "^5.0.6",
47
56
  "@types/fs-extra": "^11.0.4",
48
57
  "@types/mime-types": "^2.1.4",
49
58
  "@types/node": "^22.19.0",
@@ -7,6 +7,7 @@ import { assertWorkspace } from '../util/workspace';
7
7
  import { getTemplatesDir, renderTemplate, APP_TEMPLATES, type AppTemplate } from '../util/templates';
8
8
  import { readManifest } from '../util/manifest';
9
9
  import { mfAppName } from '../util/app-name';
10
+ import { APP_NAME_PATTERN } from '../constants';
10
11
 
11
12
  export interface CreateOptions {
12
13
  template?: string;
@@ -14,7 +15,10 @@ export interface CreateOptions {
14
15
  json?: boolean;
15
16
  }
16
17
 
17
- const APP_NAME_RE = /^[a-z][a-z0-9-]{1,49}$/;
18
+ // Names reserved by the host MF (mf-bigbrain-zth) for platform pages under
19
+ // /bigbrain-vibe/<name>. Creating an app with one of these would shadow the
20
+ // platform page in the URL, so we refuse upfront.
21
+ const RESERVED_APP_NAMES = new Set(['generate-z2h-token']);
18
22
 
19
23
  /**
20
24
  * Convert kebab-case to PascalCase. `demo-app` → `DemoApp`.
@@ -47,7 +51,12 @@ export async function createCommand(appName: string, opts: CreateOptions): Promi
47
51
  const workspace = await assertWorkspace();
48
52
 
49
53
  // Validate app name.
50
- if (!APP_NAME_RE.test(appName)) {
54
+ if (RESERVED_APP_NAMES.has(appName)) {
55
+ throw new Error(
56
+ `App name "${appName}" is reserved by the Z2H host for a platform page under /bigbrain-vibe/${appName}. Pick a different name.`
57
+ );
58
+ }
59
+ if (!APP_NAME_PATTERN.test(appName)) {
51
60
  throw new Error(
52
61
  `App name "${appName}" is invalid. Use lowercase kebab-case (a-z, 0-9, -), ` +
53
62
  `2-50 chars, starting with a letter.`
@@ -1,8 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import os from 'node:os';
3
3
  import { pathExists, readJson } from 'fs-extra';
4
- import { execa } from 'execa';
5
- import { info, warn, output, setOutputMode } from '../util/logger';
4
+ import { info, output, setOutputMode } from '../util/logger';
6
5
  import { resolveConsumerPaths } from '../shadow/paths';
7
6
  import {
8
7
  nextVersion,
@@ -13,36 +12,22 @@ import {
13
12
  type ManifestEntry,
14
13
  } from '../util/manifest';
15
14
  import { uploadDir } from '../util/s3/upload';
16
- import { assertBucketExists, BUCKET_WRITE_NAME, BUCKET_READ_DNS_URL } from '../util/s3/client';
15
+ import { BUCKET_WRITE_NAME, BUCKET_READ_DNS_URL } from '../constants';
16
+ import { setupS3Client } from '../util/s3/client';
17
+ import { readZthAuth } from '../util/auth/auth-store';
17
18
  import { cleanAppName, mfAppName } from '../util/app-name';
18
19
  import { buildCommand } from './build';
19
20
 
20
- const APONO_BUNDLE = '<APONO_BUNDLE_NAME>'; // TODO: replace with actual bundle name once infra setup is complete
21
-
22
21
  export interface DeployOptions {
23
22
  dryRun?: boolean;
24
23
  json?: boolean;
25
24
  }
26
25
 
27
- async function requestAponoAccess(): Promise<void> {
28
- if (APONO_BUNDLE === '<APONO_BUNDLE_NAME>') {
29
- warn(
30
- `skipping Apono request — APONO_BUNDLE is still a placeholder ("${APONO_BUNDLE}"). Set it in deploy.ts before relying on auto-grant.`
31
- );
32
- return;
33
- }
34
- info(`requesting Apono access for bundle "${APONO_BUNDLE}"`);
35
- await execa('apono', ['bundles', 'grant', APONO_BUNDLE], { stdio: 'inherit' });
36
- }
37
-
38
26
  export async function deployCommand(opts: DeployOptions): Promise<void> {
39
27
  if (opts.json) {
40
28
  setOutputMode('json');
41
29
  }
42
30
 
43
- info('checking S3 bucket');
44
- await assertBucketExists();
45
-
46
31
  const paths = resolveConsumerPaths(process.cwd());
47
32
  const consumerPkg = (await readJson(paths.consumerPkg)) as { name?: string };
48
33
  if (!consumerPkg.name) {
@@ -50,6 +35,11 @@ export async function deployCommand(opts: DeployOptions): Promise<void> {
50
35
  }
51
36
  const appName = mfAppName(consumerPkg.name);
52
37
 
38
+ // Validate the user's deploy token is on disk before doing any expensive
39
+ // work. Throws Z2hAuthMissingError with a remediation hint if not.
40
+ await readZthAuth();
41
+ setupS3Client({ appName });
42
+
53
43
  info('reading current Z2H manifest');
54
44
  const manifest = await readManifest();
55
45
  const version = nextVersion(manifest, appName);
@@ -70,6 +60,14 @@ export async function deployCommand(opts: DeployOptions): Promise<void> {
70
60
  }
71
61
  const assetManifest = (await readJson(assetManifestPath)) as AssetManifestFile;
72
62
 
63
+ const deployedBy = os.userInfo().username;
64
+ const entry: ManifestEntry = {
65
+ ...assetManifest,
66
+ version,
67
+ url,
68
+ deployedBy,
69
+ };
70
+
73
71
  if (opts.dryRun) {
74
72
  output(`[z2h-cli] dry-run: would upload to s3://${BUCKET_WRITE_NAME}/${keyPrefix}/ as v${version}`, {
75
73
  ok: true,
@@ -79,22 +77,11 @@ export async function deployCommand(opts: DeployOptions): Promise<void> {
79
77
  bucket: BUCKET_WRITE_NAME,
80
78
  keyPrefix,
81
79
  url,
82
- entry: { ...assetManifest, version, url },
80
+ entry,
83
81
  });
84
82
  return;
85
83
  }
86
84
 
87
- await requestAponoAccess();
88
-
89
- const deployedBy = os.userInfo().username;
90
-
91
- const entry: ManifestEntry = {
92
- ...assetManifest,
93
- version,
94
- url,
95
- deployedBy,
96
- };
97
-
98
85
  info(`uploading build → s3://${BUCKET_WRITE_NAME}/${keyPrefix}/`);
99
86
  await uploadDir(paths.shadowBuild, keyPrefix);
100
87
 
@@ -0,0 +1,124 @@
1
+ import readline from 'node:readline/promises';
2
+ import { stdin, stdout } from 'node:process';
3
+ import { execa } from 'execa';
4
+ import { info, warn, output, setOutputMode } from '../util/logger';
5
+ import { writeZthAuth, validateJwtPattern } from '../util/auth/write-auth';
6
+ import { startLoopbackServer } from '../util/auth/loopback-server';
7
+
8
+ export const DEFAULT_TOKEN_PAGE_URL = 'https://bigbrain.me/bigbrain-vibe/generate-z2h-token';
9
+ export const LOOPBACK_TIMEOUT_MS = 120_000;
10
+
11
+ export interface GenerateZ2HTokenOptions {
12
+ /**
13
+ * Skip browser auto-open AND skip the loopback callback. Prints the URL
14
+ * for the user to open manually, then waits for them to paste the token
15
+ * into the prompt. Use for SSH sessions, CI, or when the loopback can't
16
+ * reach the CLI for any reason.
17
+ */
18
+ noBrowser?: boolean;
19
+ /** Override the page URL — useful when pointing at staging. */
20
+ url?: string;
21
+ json?: boolean;
22
+ }
23
+
24
+ function browserOpenCommand(): string {
25
+ switch (process.platform) {
26
+ case 'darwin':
27
+ return 'open';
28
+ case 'win32':
29
+ return 'start';
30
+ default:
31
+ return 'xdg-open';
32
+ }
33
+ }
34
+
35
+ async function openInBrowser(url: string): Promise<void> {
36
+ await execa(browserOpenCommand(), [url], { stdio: 'ignore' });
37
+ }
38
+
39
+ async function promptForToken(): Promise<string> {
40
+ const rl = readline.createInterface({ input: stdin, output: stdout });
41
+ try {
42
+ const raw = await rl.question('Paste your token here, then press Enter:\n> ');
43
+ return raw.trim();
44
+ } finally {
45
+ rl.close();
46
+ }
47
+ }
48
+
49
+ function withQueryParam(url: string, key: string, value: string): string {
50
+ const sep = url.includes('?') ? '&' : '?';
51
+ return `${url}${sep}${key}=${encodeURIComponent(value)}`;
52
+ }
53
+
54
+ async function runManualFlow(pageUrl: string): Promise<string> {
55
+ info(
56
+ `open this URL in your browser to mint a Z2H deploy token:\n` +
57
+ ` ${pageUrl}\n\n` +
58
+ `1. Click "Generate token" on the page.\n` +
59
+ `2. Copy the token shown.\n` +
60
+ `3. Paste it below and press Enter.\n`
61
+ );
62
+ const token = await promptForToken();
63
+ if (!token) {
64
+ throw new Error('No token entered.');
65
+ }
66
+ return token;
67
+ }
68
+
69
+ async function runLoopbackFlow(pageUrl: string): Promise<string | null> {
70
+ const loopback = await startLoopbackServer();
71
+ const fullUrl = withQueryParam(pageUrl, 'cli_callback', loopback.callbackUrl);
72
+
73
+ info('opening your browser to mint a Z2H deploy token');
74
+ info(` ${fullUrl}`);
75
+
76
+ try {
77
+ await openInBrowser(fullUrl);
78
+ } catch {
79
+ warn('could not auto-open the browser. please visit the URL above manually.');
80
+ }
81
+
82
+ info('waiting for the page to send the token (Ctrl+C to fall back to manual paste)…');
83
+
84
+ try {
85
+ return await loopback.waitForToken(LOOPBACK_TIMEOUT_MS);
86
+ } catch {
87
+ return null;
88
+ } finally {
89
+ loopback.close();
90
+ }
91
+ }
92
+
93
+ export async function generateZ2HTokenCommand(opts: GenerateZ2HTokenOptions = {}): Promise<void> {
94
+ if (opts.json) {
95
+ setOutputMode('json');
96
+ }
97
+
98
+ const pageUrl = opts.url ?? process.env.Z2H_TOKEN_PAGE_URL ?? DEFAULT_TOKEN_PAGE_URL;
99
+
100
+ // --no-browser bypasses both auto-open and the loopback round-trip — the
101
+ // user opens the URL themselves and pastes the token. Useful when the
102
+ // loopback can't be reached (SSH, CI, locked-down browsers).
103
+ let token: string | null = null;
104
+ if (!opts.noBrowser) {
105
+ token = await runLoopbackFlow(pageUrl);
106
+ if (!token) {
107
+ warn('loopback flow timed out or was cancelled — falling back to manual paste.');
108
+ }
109
+ }
110
+ if (!token) {
111
+ token = await runManualFlow(pageUrl);
112
+ }
113
+
114
+ if (!validateJwtPattern(token)) {
115
+ throw new Error('That does not look like a JWT (expected three dot-separated segments).');
116
+ }
117
+
118
+ const { filePath } = await writeZthAuth({ token });
119
+
120
+ output(`[z2h-cli] token saved ✅ ${filePath}`, {
121
+ ok: true,
122
+ filePath,
123
+ });
124
+ }
package/src/constants.ts CHANGED
@@ -38,3 +38,28 @@ export const YARNRC_LINES = [
38
38
  "npmAuthToken: '${NPM_TOKEN}'",
39
39
  "npmRegistryServer: 'https://registry.npmjs.org/'",
40
40
  ];
41
+
42
+ // ─────────── Z2H deploy bucket / contract ─────────
43
+ // Source of truth for the bucket layout. The CLI uses these when configuring
44
+ // the S3 client + uploading; bigbrain-zth's STS broker imports them via
45
+ // `@mondaydotcomorg/z2h-cli/constants` to scope the per-deploy session
46
+ // policy to the same bucket + manifest key.
47
+
48
+ export const BUCKET_WRITE_NAME = 'prod-use1-bigbrain-zth-mf-assets';
49
+ export const REGION = 'us-east-1';
50
+ export const BUCKET_WRITE_URL = `https://${BUCKET_WRITE_NAME}.s3.${REGION}.amazonaws.com`;
51
+
52
+ export const BUCKET_READ_DNS_NAME = 'bigbrain-zth-mf-assets';
53
+ export const BUCKET_READ_DNS_URL = `https://${BUCKET_READ_DNS_NAME}.bigbrain.me`;
54
+
55
+ // No `.json` extension — TridentSkeleton's `mfUrl` API appends `/mf-assets`
56
+ // to the bucket base when fetching, so the manifest must live at this exact
57
+ // key. See microfrontends/mf-bigbrain-zth/.../ZeroToHeroHost.tsx.
58
+ export const MANIFEST_KEY = 'mf-assets';
59
+ export const MANIFEST_READ_URL = `${BUCKET_READ_DNS_URL}/${MANIFEST_KEY}`;
60
+
61
+ // ─────────── App-name slug — backend & CLI both honor this ─────
62
+ // Lowercase kebab-case, must start with a letter, 2-50 chars total. The
63
+ // backend STS broker rejects anything else with 400; the CLI can validate
64
+ // upfront for friendlier errors.
65
+ export const APP_NAME_PATTERN = /^[a-z][a-z0-9-]{1,49}$/;
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { cleanCommand } from './commands/clean';
5
5
  import { deployCommand } from './commands/deploy';
6
6
  import { createWorkspaceCommand } from './commands/create-workspace';
7
7
  import { createCommand } from './commands/create';
8
+ import { generateZ2HTokenCommand } from './commands/generate-z2h-token';
8
9
  import { errorLog, fail } from './util/logger';
9
10
 
10
11
  async function main(): Promise<void> {
@@ -91,6 +92,22 @@ async function main(): Promise<void> {
91
92
  }
92
93
  });
93
94
 
95
+ program
96
+ .command('generate-z2h-token')
97
+ .description(
98
+ 'Open the bigbrain.me token page, capture the token via a loopback callback, and persist it to ~/.zth/auth.json'
99
+ )
100
+ .option('--no-browser', "don't auto-open the browser; print the URL and prompt for manual paste")
101
+ .option('--url <url>', 'Override the token page URL (e.g. for staging)')
102
+ .option('--json', 'emit structured JSON on stdout')
103
+ .action(async (opts: { noBrowser?: boolean; url?: string; json?: boolean }) => {
104
+ try {
105
+ await generateZ2HTokenCommand(opts);
106
+ } catch (err) {
107
+ fail(`generate-z2h-token failed: ${(err as Error).message}`, { command: 'generate-z2h-token' }, 1);
108
+ }
109
+ });
110
+
94
111
  program
95
112
  .command('create <app-name>')
96
113
  .description('Scaffold a new Z2H consumer app inside ~/zth-projects/')