@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.
- package/dist/commands/create-workspace.js +3 -3
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +10 -3
- package/dist/commands/deploy.d.ts.map +1 -1
- package/dist/commands/deploy.js +19 -24
- package/dist/commands/generate-z2h-token.d.ts +16 -0
- package/dist/commands/generate-z2h-token.d.ts.map +1 -0
- package/dist/commands/generate-z2h-token.js +107 -0
- package/dist/constants.d.ts +8 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +28 -0
- package/dist/esm/commands/create-workspace.mjs +3 -3
- package/dist/esm/commands/create.d.ts.map +1 -1
- package/dist/esm/commands/create.mjs +10 -3
- package/dist/esm/commands/deploy.d.ts.map +1 -1
- package/dist/esm/commands/deploy.mjs +16 -21
- package/dist/esm/commands/generate-z2h-token.d.ts +16 -0
- package/dist/esm/commands/generate-z2h-token.d.ts.map +1 -0
- package/dist/esm/commands/generate-z2h-token.mjs +99 -0
- package/dist/esm/constants.d.ts +8 -0
- package/dist/esm/constants.d.ts.map +1 -1
- package/dist/esm/constants.mjs +21 -1
- package/dist/esm/index.mjs +18 -1
- package/dist/esm/util/auth/__tests__/auth-store.test.d.ts +2 -0
- package/dist/esm/util/auth/__tests__/auth-store.test.d.ts.map +1 -0
- package/dist/esm/util/auth/__tests__/broker-credential-provider.test.d.ts +2 -0
- package/dist/esm/util/auth/__tests__/broker-credential-provider.test.d.ts.map +1 -0
- package/dist/esm/util/auth/__tests__/loopback-server.test.d.ts +2 -0
- package/dist/esm/util/auth/__tests__/loopback-server.test.d.ts.map +1 -0
- package/dist/esm/util/auth/__tests__/write-auth.test.d.ts +2 -0
- package/dist/esm/util/auth/__tests__/write-auth.test.d.ts.map +1 -0
- package/dist/esm/util/auth/auth-store.d.ts +11 -0
- package/dist/esm/util/auth/auth-store.d.ts.map +1 -0
- package/dist/esm/util/auth/auth-store.mjs +29 -0
- package/dist/esm/util/auth/broker-credential-provider.d.ts +12 -0
- package/dist/esm/util/auth/broker-credential-provider.d.ts.map +1 -0
- package/dist/esm/util/auth/broker-credential-provider.mjs +76 -0
- package/dist/esm/util/auth/loopback-server.d.ts +12 -0
- package/dist/esm/util/auth/loopback-server.d.ts.map +1 -0
- package/dist/esm/util/auth/loopback-server.mjs +87 -0
- package/dist/esm/util/auth/write-auth.d.ts +11 -0
- package/dist/esm/util/auth/write-auth.d.ts.map +1 -0
- package/dist/esm/util/auth/write-auth.mjs +22 -0
- package/dist/esm/util/manifest.d.ts.map +1 -1
- package/dist/esm/util/manifest.mjs +2 -1
- package/dist/esm/util/s3/client.d.ts +7 -8
- package/dist/esm/util/s3/client.d.ts.map +1 -1
- package/dist/esm/util/s3/client.mjs +15 -30
- package/dist/esm/util/s3/upload.d.ts.map +1 -1
- package/dist/esm/util/s3/upload.mjs +2 -1
- package/dist/index.js +19 -1
- package/dist/util/auth/__tests__/auth-store.test.d.ts +2 -0
- package/dist/util/auth/__tests__/auth-store.test.d.ts.map +1 -0
- package/dist/util/auth/__tests__/broker-credential-provider.test.d.ts +2 -0
- package/dist/util/auth/__tests__/broker-credential-provider.test.d.ts.map +1 -0
- package/dist/util/auth/__tests__/loopback-server.test.d.ts +2 -0
- package/dist/util/auth/__tests__/loopback-server.test.d.ts.map +1 -0
- package/dist/util/auth/__tests__/write-auth.test.d.ts +2 -0
- package/dist/util/auth/__tests__/write-auth.test.d.ts.map +1 -0
- package/dist/util/auth/auth-store.d.ts +11 -0
- package/dist/util/auth/auth-store.d.ts.map +1 -0
- package/dist/util/auth/auth-store.js +39 -0
- package/dist/util/auth/broker-credential-provider.d.ts +12 -0
- package/dist/util/auth/broker-credential-provider.d.ts.map +1 -0
- package/dist/util/auth/broker-credential-provider.js +80 -0
- package/dist/util/auth/loopback-server.d.ts +12 -0
- package/dist/util/auth/loopback-server.d.ts.map +1 -0
- package/dist/util/auth/loopback-server.js +94 -0
- package/dist/util/auth/write-auth.d.ts +11 -0
- package/dist/util/auth/write-auth.d.ts.map +1 -0
- package/dist/util/auth/write-auth.js +26 -0
- package/dist/util/manifest.d.ts.map +1 -1
- package/dist/util/manifest.js +5 -4
- package/dist/util/s3/client.d.ts +7 -8
- package/dist/util/s3/client.d.ts.map +1 -1
- package/dist/util/s3/client.js +14 -36
- package/dist/util/s3/upload.d.ts.map +1 -1
- package/dist/util/s3/upload.js +2 -1
- package/package.json +10 -1
- package/src/commands/__tests__/create-workspace.test.ts +1 -1
- package/src/commands/__tests__/create.test.ts +1 -5
- package/src/commands/create-workspace.ts +3 -3
- package/src/commands/create.ts +12 -3
- package/src/commands/deploy.ts +18 -31
- package/src/commands/generate-z2h-token.ts +124 -0
- package/src/constants.ts +25 -0
- package/src/index.ts +21 -1
- package/src/util/auth/__tests__/auth-store.test.ts +50 -0
- package/src/util/auth/__tests__/broker-credential-provider.test.ts +115 -0
- package/src/util/auth/__tests__/loopback-server.test.ts +85 -0
- package/src/util/auth/__tests__/write-auth.test.ts +79 -0
- package/src/util/auth/auth-store.ts +31 -0
- package/src/util/auth/broker-credential-provider.ts +93 -0
- package/src/util/auth/loopback-server.ts +103 -0
- package/src/util/auth/write-auth.ts +24 -0
- package/src/util/manifest.ts +2 -1
- package/src/util/s3/client.ts +15 -32
- package/src/util/s3/upload.ts +2 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const fsExtra = require('fs-extra');
|
|
6
|
+
|
|
7
|
+
const _interopDefault = e => e && e.__esModule ? e : { default: e };
|
|
8
|
+
|
|
9
|
+
const path__default = /*#__PURE__*/_interopDefault(path);
|
|
10
|
+
const os__default = /*#__PURE__*/_interopDefault(os);
|
|
11
|
+
|
|
12
|
+
// npm-style convention: per-user config under ~/.zth (mirrors ~/.npmrc, ~/.aws/).
|
|
13
|
+
// Kept out of the workspace dir so accidental `git init` in ~/zth-projects can't
|
|
14
|
+
// stage it.
|
|
15
|
+
const Z2H_AUTH_DIR = path__default.default.join(os__default.default.homedir(), '.zth');
|
|
16
|
+
const Z2H_AUTH_FILE = path__default.default.join(Z2H_AUTH_DIR, 'auth.json');
|
|
17
|
+
class Z2hAuthMissingError extends Error {
|
|
18
|
+
authFilePath;
|
|
19
|
+
constructor(authFilePath) {
|
|
20
|
+
super(`No Z2H deploy token found at ${authFilePath}. Please run 'z2h-cli generate-z2h-token' command.`);
|
|
21
|
+
this.authFilePath = authFilePath;
|
|
22
|
+
this.name = 'Z2hAuthMissingError';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function readZthAuth() {
|
|
26
|
+
if (!(await fsExtra.pathExists(Z2H_AUTH_FILE))) {
|
|
27
|
+
throw new Z2hAuthMissingError(Z2H_AUTH_FILE);
|
|
28
|
+
}
|
|
29
|
+
const parsed = (await fsExtra.readJson(Z2H_AUTH_FILE));
|
|
30
|
+
if (!parsed.token) {
|
|
31
|
+
throw new Error(`${Z2H_AUTH_FILE} is missing a "token" string field.`);
|
|
32
|
+
}
|
|
33
|
+
return { token: parsed.token };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
exports.Z2H_AUTH_DIR = Z2H_AUTH_DIR;
|
|
37
|
+
exports.Z2H_AUTH_FILE = Z2H_AUTH_FILE;
|
|
38
|
+
exports.Z2hAuthMissingError = Z2hAuthMissingError;
|
|
39
|
+
exports.readZthAuth = readZthAuth;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AwsCredentialIdentityProvider } from '@aws-sdk/types';
|
|
2
|
+
export declare const DEFAULT_BROKER_BASE_URL = "https://bigbrain-zth.bigbrain.me";
|
|
3
|
+
export declare const STS_BROKER_PATH = "/z2h-cli/sts/credentials";
|
|
4
|
+
/**
|
|
5
|
+
* AWS SDK credential provider that fetches short-lived deploy credentials
|
|
6
|
+
* from the bigbrain-zth STS broker. Credentials are cached in memory until
|
|
7
|
+
* 5 minutes before expiry; concurrent calls share a single in-flight fetch.
|
|
8
|
+
*/
|
|
9
|
+
export declare function brokerCredentialProvider({ appName }: {
|
|
10
|
+
appName: string;
|
|
11
|
+
}): AwsCredentialIdentityProvider;
|
|
12
|
+
//# sourceMappingURL=broker-credential-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"broker-credential-provider.d.ts","sourceRoot":"","sources":["../../../src/util/auth/broker-credential-provider.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAyB,6BAA6B,EAAE,MAAM,gBAAgB,CAAC;AAE3F,eAAO,MAAM,uBAAuB,qCAAqC,CAAC;AAC1E,eAAO,MAAM,eAAe,6BAA6B,CAAC;AA4D1D;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,6BAA6B,CAuBxG"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
|
|
3
|
+
const util_auth_authStore = require('./auth-store.js');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_BROKER_BASE_URL = 'https://bigbrain-zth.bigbrain.me';
|
|
6
|
+
const STS_BROKER_PATH = '/z2h-cli/sts/credentials';
|
|
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
|
+
function brokerBaseUrl() {
|
|
11
|
+
return process.env.Z2H_BROKER_BASE_URL || DEFAULT_BROKER_BASE_URL;
|
|
12
|
+
}
|
|
13
|
+
async function fetchCredentialsOnce(appName) {
|
|
14
|
+
const { token } = await util_auth_authStore.readZthAuth();
|
|
15
|
+
const url = `${brokerBaseUrl()}${STS_BROKER_PATH}`;
|
|
16
|
+
let res;
|
|
17
|
+
try {
|
|
18
|
+
res = await fetch(url, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
Authorization: `Bearer ${token}`,
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
Accept: 'application/json',
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify({ appName }),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
30
|
+
throw new Error(`Z2H deploy broker request failed: ${message}`);
|
|
31
|
+
}
|
|
32
|
+
if (res.status === 401 || res.status === 403) {
|
|
33
|
+
throw new Error(`Z2H deploy token rejected by the broker (${res.status}). Refresh user token by running 'z2h-cli generate-z2h-token'.`);
|
|
34
|
+
}
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const text = await res.text();
|
|
37
|
+
throw new Error(`Z2H deploy broker returned ${res.status}: ${text.slice(0, 1024)}`);
|
|
38
|
+
}
|
|
39
|
+
const body = (await res.json());
|
|
40
|
+
if (!body.accessKeyId || !body.secretAccessKey || !body.sessionToken || !body.expiration) {
|
|
41
|
+
throw new Error('Z2H deploy broker response missing required credential fields');
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
accessKeyId: body.accessKeyId,
|
|
45
|
+
secretAccessKey: body.secretAccessKey,
|
|
46
|
+
sessionToken: body.sessionToken,
|
|
47
|
+
expiration: new Date(body.expiration),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* AWS SDK credential provider that fetches short-lived deploy credentials
|
|
52
|
+
* from the bigbrain-zth STS broker. Credentials are cached in memory until
|
|
53
|
+
* 5 minutes before expiry; concurrent calls share a single in-flight fetch.
|
|
54
|
+
*/
|
|
55
|
+
function brokerCredentialProvider({ appName }) {
|
|
56
|
+
let cached;
|
|
57
|
+
let inflight;
|
|
58
|
+
return async () => {
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
if (cached && cached.expiration && cached.expiration.getTime() - now > REFRESH_BUFFER_MS) {
|
|
61
|
+
return cached;
|
|
62
|
+
}
|
|
63
|
+
if (inflight) {
|
|
64
|
+
return inflight;
|
|
65
|
+
}
|
|
66
|
+
inflight = fetchCredentialsOnce(appName)
|
|
67
|
+
.then(creds => {
|
|
68
|
+
cached = creds;
|
|
69
|
+
return creds;
|
|
70
|
+
})
|
|
71
|
+
.finally(() => {
|
|
72
|
+
inflight = undefined;
|
|
73
|
+
});
|
|
74
|
+
return inflight;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
exports.DEFAULT_BROKER_BASE_URL = DEFAULT_BROKER_BASE_URL;
|
|
79
|
+
exports.STS_BROKER_PATH = STS_BROKER_PATH;
|
|
80
|
+
exports.brokerCredentialProvider = brokerCredentialProvider;
|
|
@@ -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":"
|
|
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"}
|
package/dist/util/manifest.js
CHANGED
|
@@ -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(
|
|
9
|
+
const res = await fetch(constants.MANIFEST_READ_URL);
|
|
9
10
|
if (!res.ok) {
|
|
10
|
-
throw new Error(`failed to read manifest from ${
|
|
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:
|
|
26
|
-
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',
|
package/dist/util/s3/client.d.ts
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { S3Client } from '@aws-sdk/client-s3';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export declare
|
|
7
|
-
|
|
8
|
-
|
|
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,
|
|
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"}
|
package/dist/util/s3/client.js
CHANGED
|
@@ -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
|
|
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":"
|
|
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"}
|
package/dist/util/s3/upload.js
CHANGED
|
@@ -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:
|
|
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.
|
|
3
|
+
"version": "0.3.1",
|
|
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",
|
|
@@ -57,7 +57,7 @@ describe('create-workspace', () => {
|
|
|
57
57
|
await ensureDir(tmp);
|
|
58
58
|
await writeJson(path.join(tmp, 'somefile.json'), { hello: 'world' });
|
|
59
59
|
|
|
60
|
-
await expect(createWorkspaceCommand({})).rejects.toThrow(/
|
|
60
|
+
await expect(createWorkspaceCommand({})).rejects.toThrow(/Workspace already exists/);
|
|
61
61
|
expect(execa).not.toHaveBeenCalled();
|
|
62
62
|
});
|
|
63
63
|
|
|
@@ -77,11 +77,7 @@ describe('create', () => {
|
|
|
77
77
|
expect(content).toContain('export default function DemoApp()');
|
|
78
78
|
expect(content).not.toContain('__APP_COMPONENT__');
|
|
79
79
|
|
|
80
|
-
expect(execa).toHaveBeenCalledWith(
|
|
81
|
-
'yarn',
|
|
82
|
-
expect.arrayContaining(['install']),
|
|
83
|
-
expect.objectContaining({ cwd: tmp })
|
|
84
|
-
);
|
|
80
|
+
expect(execa).toHaveBeenCalledWith('yarn', ['install'], expect.any(Object));
|
|
85
81
|
});
|
|
86
82
|
|
|
87
83
|
it('scaffolds an html-embed app and copies the html file', async () => {
|
|
@@ -38,7 +38,7 @@ export async function createWorkspaceCommand(opts: CreateWorkspaceOptions): Prom
|
|
|
38
38
|
if (await pathExists(home)) {
|
|
39
39
|
const entries = await readdir(home);
|
|
40
40
|
if (entries.length > 0) {
|
|
41
|
-
throw new Error(
|
|
41
|
+
throw new Error(`Workspace already exists at ${home}. Remove it and try again.`);
|
|
42
42
|
}
|
|
43
43
|
} else {
|
|
44
44
|
await ensureDir(home);
|
|
@@ -72,13 +72,13 @@ export async function createWorkspaceCommand(opts: CreateWorkspaceOptions): Prom
|
|
|
72
72
|
),
|
|
73
73
|
]);
|
|
74
74
|
|
|
75
|
-
info(`
|
|
75
|
+
info(`setting up your Z2H workspace — this may take a minute...`);
|
|
76
76
|
await execa('yarn', ['install'], {
|
|
77
77
|
cwd: home,
|
|
78
78
|
stdio: 'inherit',
|
|
79
79
|
});
|
|
80
80
|
|
|
81
|
-
output(`
|
|
81
|
+
output(`Your workspace is ready! 🚀 Apps will live at ${home}`, {
|
|
82
82
|
ok: true,
|
|
83
83
|
workspace: home,
|
|
84
84
|
z2hCliVersion: cliVersion,
|
package/src/commands/create.ts
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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.`
|
|
@@ -106,7 +115,7 @@ export async function createCommand(appName: string, opts: CreateOptions): Promi
|
|
|
106
115
|
await copy(opts.htmlFile, path.join(appDir, 'src', 'page.html'));
|
|
107
116
|
}
|
|
108
117
|
|
|
109
|
-
info(`installing
|
|
118
|
+
info(`installing dependencies for ${appName}...`);
|
|
110
119
|
await execa('yarn', ['install'], {
|
|
111
120
|
cwd: workspace,
|
|
112
121
|
stdio: 'inherit',
|