@raisenow/tamaro-cli 1.9.0-dev.1 → 1.9.0-dev.3

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/README.md CHANGED
@@ -187,7 +187,7 @@ Deploy a pre-built _Tamaro Core_ or customer configuration bundle to AWS S3.
187
187
 
188
188
  - `--dryrun` – Displays the operations that would be performed without actually running them
189
189
  - `--tag <tag>` – Tag which should be used for the deployment of the bundle (default: "latest")
190
- - `--skip-epms-sync-only-use-in-emergencies-or-you-will-be-fired` – Skip the EPMS sync entirely. Only use this in emergencies.
190
+ - `--force-skip-epms-sync` – Skip the EPMS sync entirely. Only use this in emergencies.
191
191
 
192
192
  Make sure you have built the bundle before deploying it with this command, otherwise you may mistakenly deploy the
193
193
  bundle from a previous build.
package/dist/cli.js CHANGED
@@ -1,23 +1,23 @@
1
1
  #!/usr/bin/env node
2
- import { A as EPMS_AUTH_CLIENT_ID_STAGE, B as logSuccess, C as DEFAULT_PORT, D as EPMS_AUTH_BASE_URL_PROD, E as EPMS_API_BASE_URL_STAGE, F as runCommandSync, I as createTerminalLink, L as logCommand, M as HTTPS_KEY_FILE, N as halt, O as EPMS_AUTH_BASE_URL_STAGE, P as promptConfirmation, R as logDataTable, S as DEFAULT_HTTP_TIMEOUT, T as EPMS_API_BASE_URL_PROD, V as logTitle, _ as AWS_S3_BUCKET_TAMARO, a as getIfCoreFns, b as CACHE_DIR, c as getWidgetUuid, d as resolveBin, f as resolveCoreVersion, g as AWS_CLOUDFRONT_DISTRIBUTION_ID, h as AUTH_PORT, j as HTTPS_CRT_FILE, k as EPMS_AUTH_CLIENT_ID_PROD, l as resolveAccountUuidFromConfig, m as resolveOwn, n as assertEnvValid, o as getPaths, t as applyEnv, v as AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD, w as DEFAULT_TAG, x as CORE_CONFIG_NAME, y as AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_STAGE, z as logError } from "./env-JzPIlctP.js";
2
+ import { A as EPMS_OAUTH_CLIENT_ID, B as logInfo, C as DEFAULT_HTTP_TIMEOUT, D as EPMS_API_BASE_URL_STAGE, E as EPMS_API_BASE_URL_PROD, F as runCommandSync, H as logTitle, I as createTerminalLink, L as logCommand, M as HTTPS_KEY_FILE, N as halt, O as EPMS_AUTH_BASE_URL_PROD, P as promptConfirmation, R as logDataTable, S as CORE_CONFIG_NAME, T as DEFAULT_TAG, V as logSuccess, _ as AWS_CLOUDFRONT_DISTRIBUTION_ID, a as getIfCoreFns, b as AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_STAGE, c as getWidgetUuid, d as resolveBin, f as resolveCoreVersion, g as AUTH_PORT, h as resolveOwn, j as HTTPS_CRT_FILE, k as EPMS_AUTH_BASE_URL_STAGE, l as resolveAccountUuidFromConfig, m as resolveManagedByFromConfig, n as assertEnvValid, o as getPaths, t as applyEnv, v as AWS_S3_BUCKET_TAMARO, w as DEFAULT_PORT, x as CACHE_DIR, y as AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD, z as logError } from "./env-DZ3cWM6p.js";
3
3
  import { createCommand } from "commander";
4
4
  import fs, { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
5
5
  import path from "node:path";
6
+ import stripIndent from "strip-indent";
6
7
  import { S3 } from "@aws-sdk/client-s3";
7
8
  import { execaCommandSync } from "execa";
8
9
  import prompts from "prompts";
9
- import stripIndent from "strip-indent";
10
10
  import columnify from "columnify";
11
- import notifier from "node-notifier";
11
+ import * as yaml from "js-yaml";
12
12
  import { z } from "zod";
13
+ import ky from "ky";
14
+ import { createServer } from "node:http";
15
+ import open from "open";
16
+ import notifier from "node-notifier";
13
17
  import { globSync } from "glob";
14
18
  import Handlebars from "handlebars";
15
19
  import helpers from "handlebars-helpers";
16
20
  import { config } from "dotenv";
17
- import * as yaml from "js-yaml";
18
- import ky from "ky";
19
- import { createServer } from "node:http";
20
- import open from "open";
21
21
  import { getPortPromise } from "portfinder";
22
22
  //#region src/lib/aws.ts
23
23
  const awsAuthenticate = (options) => {
@@ -114,6 +114,308 @@ const getConfigDeployments = async (configName) => {
114
114
  return deployments;
115
115
  };
116
116
  //#endregion
117
+ //#region src/lib/epms/auth/util.ts
118
+ const CachedTokenSchema = z.object({
119
+ expirationTime: z.number(),
120
+ token: z.string()
121
+ });
122
+ const AuthResponseSchema = z.object({
123
+ access_token: z.string(),
124
+ expires_in: z.number(),
125
+ token_type: z.string()
126
+ });
127
+ const getCacheFilePath = (key) => {
128
+ const cacheKey = Buffer.from(key).toString("base64url");
129
+ return path.join(CACHE_DIR, `token-${cacheKey}.json`);
130
+ };
131
+ const getTokenCacheKey = (baseUrl, clientId) => {
132
+ return `${baseUrl}|${clientId}`;
133
+ };
134
+ const readCachedToken = (key) => {
135
+ try {
136
+ const filePath = getCacheFilePath(key);
137
+ if (!existsSync(filePath)) return;
138
+ const data = JSON.parse(readFileSync(filePath, "utf8"));
139
+ const parsed = CachedTokenSchema.safeParse(data);
140
+ if (parsed.success) return parsed.data;
141
+ } catch {}
142
+ };
143
+ const writeCachedToken = (key, cached) => {
144
+ try {
145
+ const filePath = getCacheFilePath(key);
146
+ mkdirSync(path.dirname(filePath), { recursive: true });
147
+ writeFileSync(filePath, JSON.stringify(cached), { mode: 384 });
148
+ } catch {}
149
+ };
150
+ const removeCachedToken = (key) => {
151
+ try {
152
+ const filePath = getCacheFilePath(key);
153
+ if (existsSync(filePath)) writeFileSync(filePath, "", { flag: "w" });
154
+ } catch {}
155
+ };
156
+ //#endregion
157
+ //#region src/lib/epms/auth/apiClient.ts
158
+ const createAuthorizer = (baseUrl, clientId, clientSecret) => {
159
+ const apiUnauthorized = ky.extend({
160
+ headers: { "Access-Control-Allow-Origin": "*" },
161
+ prefixUrl: baseUrl,
162
+ retry: { limit: 3 },
163
+ timeout: DEFAULT_HTTP_TIMEOUT
164
+ });
165
+ let expirationTime = void 0;
166
+ let token = void 0;
167
+ const cacheKey = getTokenCacheKey(baseUrl, clientId);
168
+ const cached = readCachedToken(cacheKey);
169
+ if (cached) {
170
+ token = cached.token;
171
+ expirationTime = cached.expirationTime;
172
+ }
173
+ const authorize = async () => {
174
+ const isExpired = (expirationTime ?? 0) - Date.now() < 300 * 1e3;
175
+ if (token && !isExpired) return token;
176
+ const data = await apiUnauthorized.post("oauth2/token", { json: {
177
+ client_id: clientId,
178
+ client_secret: clientSecret,
179
+ grant_type: "client_credentials"
180
+ } }).json();
181
+ token = `${data.token_type} ${data.access_token}`;
182
+ expirationTime = Date.now() + data.expires_in * 1e3;
183
+ writeCachedToken(cacheKey, {
184
+ expirationTime,
185
+ token
186
+ });
187
+ return token;
188
+ };
189
+ return apiUnauthorized.extend({ hooks: { beforeRequest: [async (request) => {
190
+ const authToken = await authorize();
191
+ request.headers.set("Authorization", authToken);
192
+ }] } });
193
+ };
194
+ //#endregion
195
+ //#region src/lib/epms/auth/authCallback.html
196
+ var authCallback_default = "<!doctype html>\n<html>\n <head\n ><title>Authenticating</title></head\n >\n <body>\n <p>Authenticating</p>\n <script>\n const hash = window.location.hash.substring(1)\n const params = new URLSearchParams(hash)\n const data = {\n access_token: params.get('access_token'),\n token_type: params.get('token_type'),\n expires_in: parseInt(params.get('expires_in') || '0', 10),\n }\n fetch('/token', {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify(data),\n })\n .then(() => {\n document.querySelector('p').textContent =\n 'Authentication successful! You can close this tab.'\n })\n .catch(() => {\n document.querySelector('p').textContent =\n 'Authentication failed. Please try again.'\n })\n <\/script>\n </body>\n</html>\n";
197
+ //#endregion
198
+ //#region src/lib/epms/auth/browser.ts
199
+ /**
200
+ * The following authentication method is a browser-based OAuth flow.
201
+ * A very similar flow is used by tools like AWS CLI when running `aws sso login` or Claude Code.
202
+ *
203
+ * The flow works as follows:
204
+ * 1. The CLI starts a temporary local HTTP server that listens for the OAuth callback.
205
+ * 2. The CLI opens the user's default web browser and navigates to the EPMS authorization URL, passing the local server's callback URL as the redirect_uri.
206
+ * 3. The user authenticates in the browser, and the OAuth server redirects back to the local server with the authorization token.
207
+ * 4. The local server captures the token and resolves the promise, completing the authentication flow.
208
+ *
209
+ *
210
+ * We also implement a simple caching mechanism that stores the token in a file in the user's home directory.
211
+ * This allows us to reuse the token for subsequent requests until it expires, at which point we automatically trigger a new authentication flow.
212
+ */
213
+ const doBrowserOAuthFlow = (authBaseUrl, clientId, port) => {
214
+ return new Promise((resolve, reject) => {
215
+ const server = createServer((req, res) => {
216
+ if (req.method === "POST" && req.url === "/token") {
217
+ let body = "";
218
+ req.on("data", (chunk) => {
219
+ body += chunk.toString();
220
+ });
221
+ req.on("end", () => {
222
+ try {
223
+ const data = AuthResponseSchema.parse(JSON.parse(body));
224
+ res.writeHead(200);
225
+ res.end();
226
+ server.close();
227
+ resolve(data);
228
+ } catch (error) {
229
+ res.writeHead(400);
230
+ res.end();
231
+ reject(error instanceof Error ? error : new Error(String(error)));
232
+ }
233
+ });
234
+ } else {
235
+ res.writeHead(200, { "Content-Type": "text/html" });
236
+ res.end(authCallback_default);
237
+ }
238
+ });
239
+ server.on("error", reject);
240
+ server.listen(port, () => {
241
+ const redirectUri = `http://localhost:${port}`;
242
+ const authUrl = `${authBaseUrl}/oauth2/authorize?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=token`;
243
+ logInfo("\nℹ️ Log in with your super admin account in the browser to continue.");
244
+ console.log(`Opening browser for authentication...`);
245
+ console.log(createTerminalLink(authUrl, authUrl));
246
+ open(authUrl).catch((error) => {
247
+ reject(error instanceof Error ? error : new Error(String(error)));
248
+ });
249
+ });
250
+ });
251
+ };
252
+ const createBrowserAuthorizer = (authBaseUrl, clientId, epmsBaseUrl) => {
253
+ const apiBase = ky.extend({
254
+ prefixUrl: epmsBaseUrl,
255
+ retry: { limit: 3 },
256
+ timeout: DEFAULT_HTTP_TIMEOUT
257
+ });
258
+ let expirationTime = void 0;
259
+ let token = void 0;
260
+ const cacheKey = getTokenCacheKey(authBaseUrl, clientId);
261
+ const cached = readCachedToken(cacheKey);
262
+ if (cached) {
263
+ token = cached.token;
264
+ expirationTime = cached.expirationTime;
265
+ }
266
+ const resetAuth = () => {
267
+ token = void 0;
268
+ expirationTime = void 0;
269
+ removeCachedToken(cacheKey);
270
+ };
271
+ const authorize = async () => {
272
+ const isExpired = (expirationTime ?? 0) - Date.now() < 300 * 1e3;
273
+ if (token && !isExpired) return token;
274
+ const data = await doBrowserOAuthFlow(authBaseUrl, clientId, AUTH_PORT);
275
+ token = `${data.token_type} ${data.access_token}`;
276
+ expirationTime = Date.now() + data.expires_in * 1e3;
277
+ writeCachedToken(cacheKey, {
278
+ expirationTime,
279
+ token
280
+ });
281
+ return token;
282
+ };
283
+ return apiBase.extend({ hooks: {
284
+ afterResponse: [async (request, options, response) => {
285
+ if (response.status === 401) {
286
+ resetAuth();
287
+ const newToken = await authorize();
288
+ const newRequest = new Request(request, { headers: new Headers(request.headers) });
289
+ newRequest.headers.set("Authorization", newToken);
290
+ return ky(newRequest, options);
291
+ }
292
+ return response;
293
+ }],
294
+ beforeRequest: [async (request) => {
295
+ const authToken = await authorize();
296
+ request.headers.set("Authorization", authToken);
297
+ }]
298
+ } });
299
+ };
300
+ //#endregion
301
+ //#region src/lib/epms/myselfSchema.ts
302
+ const myselfSchema = z.object({
303
+ email: z.string(),
304
+ identity_roles: z.array(z.object({
305
+ identity_uuid: z.string(),
306
+ role: z.object({
307
+ name: z.string(),
308
+ uuid: z.string()
309
+ })
310
+ })),
311
+ uuid: z.string()
312
+ });
313
+ //#endregion
314
+ //#region src/lib/epms/epmsClient.ts
315
+ /**
316
+ * Manages authentication and API interactions with EPMS.
317
+ * It provides factory methods for creating instances based on different authentication flows (client credentials or browser-based) and handles token caching and renewal transparently.
318
+ */
319
+ var EpmsClient = class EpmsClient {
320
+ #cacheKey;
321
+ #epmsClient;
322
+ #authType;
323
+ /**
324
+ * Creates an instance of EpmsClient.
325
+ *
326
+ * @param authType The type of authentication used by the client.
327
+ * @param cacheKey They key used for caching the token so that the client can manage the cache (e.g. clear it on logout). It should be unique per EPMS environment and client ID.
328
+ * @param epmsClient The underlying KyInstance used for making API requests.
329
+ */
330
+ constructor(authType, cacheKey, epmsClient) {
331
+ this.#authType = authType;
332
+ this.#cacheKey = cacheKey;
333
+ this.#epmsClient = epmsClient;
334
+ }
335
+ static fromClientCredentials(baseUrl, clientId, clientSecret) {
336
+ return new EpmsClient("client_credentials", getTokenCacheKey(baseUrl, clientId), createAuthorizer(baseUrl, clientId, clientSecret));
337
+ }
338
+ static fromBrowserAuth(epmsBaseUrl, authBaseUrl, clientId) {
339
+ return new EpmsClient("browser", getTokenCacheKey(authBaseUrl, clientId), createBrowserAuthorizer(authBaseUrl, clientId, epmsBaseUrl));
340
+ }
341
+ get authType() {
342
+ return this.#authType;
343
+ }
344
+ logout() {
345
+ removeCachedToken(this.#cacheKey);
346
+ }
347
+ async myself() {
348
+ return this.#epmsClient.get("users/myself").json().then((response) => myselfSchema.parse(response));
349
+ }
350
+ async getOrganisationIdByAccountUuid(accountUuid) {
351
+ try {
352
+ const organisationUuid = (await this.#epmsClient.get(`accounts/${accountUuid}`).json()).organisation.uuid;
353
+ if (!organisationUuid) throw new Error("Organisation UUID not found in response");
354
+ return organisationUuid;
355
+ } catch (error) {
356
+ throw new Error(`Organisation UUID not found for account UUID: ${accountUuid}`, { cause: error });
357
+ }
358
+ }
359
+ async updateTamaro(configName, tag, json) {
360
+ return this.#epmsClient.put(`products/tamaro/${configName}/tags/${tag}`, { json }).json();
361
+ }
362
+ async archiveTamaro(configName) {
363
+ return this.#epmsClient.post(`products/tamaro/${configName}/archive`).json();
364
+ }
365
+ async unarchiveTamaro(configName) {
366
+ return this.#epmsClient.post(`products/tamaro/${configName}/unarchive`).json();
367
+ }
368
+ };
369
+ //#endregion
370
+ //#region src/lib/epms/helpers.ts
371
+ const epmsAuthenticate = (env) => {
372
+ const { authBaseUrl, baseUrl, clientId, clientSecret } = {
373
+ prod: {
374
+ authBaseUrl: EPMS_AUTH_BASE_URL_PROD,
375
+ baseUrl: EPMS_API_BASE_URL_PROD,
376
+ clientId: process.env.EPMS_CLIENT_ID,
377
+ clientSecret: process.env.EPMS_CLIENT_SECRET
378
+ },
379
+ stage: {
380
+ authBaseUrl: EPMS_AUTH_BASE_URL_STAGE,
381
+ baseUrl: EPMS_API_BASE_URL_STAGE,
382
+ clientId: process.env.EPMS_CLIENT_ID_STAGE,
383
+ clientSecret: process.env.EPMS_CLIENT_SECRET_STAGE
384
+ }
385
+ }[env];
386
+ if (clientId && clientSecret) return EpmsClient.fromClientCredentials(baseUrl, clientId, clientSecret);
387
+ return EpmsClient.fromBrowserAuth(baseUrl, authBaseUrl, EPMS_OAUTH_CLIENT_ID);
388
+ };
389
+ const loadTamaroMetadataFromConfig = (paths) => {
390
+ if (!("configYml" in paths)) {
391
+ halt("Fatal error, this should never happen");
392
+ throw new Error("Fatal error");
393
+ }
394
+ const configContent = yaml.load(readFileSync(paths.configYml, "utf8"));
395
+ const accountUuidStage = resolveAccountUuidFromConfig(configContent, "epms_stage");
396
+ return {
397
+ accountUuidProd: resolveAccountUuidFromConfig(configContent, "epms"),
398
+ accountUuidStage,
399
+ managedBy: resolveManagedByFromConfig(configContent)
400
+ };
401
+ };
402
+ const assertUserIsSuperAdmin = async (epmsClient) => {
403
+ if (epmsClient.authType === "client_credentials") return;
404
+ const myself = await epmsClient.myself();
405
+ if (!myself.identity_roles.some(({ role }) => role.name === "super_admin")) {
406
+ epmsClient.logout();
407
+ logError("\nYou must be a super admin to perform this action.");
408
+ logTitle("\nYour EPMS user details:");
409
+ logDataTable({
410
+ email: myself.email,
411
+ roles: myself.identity_roles.map(({ role }) => role.name).join(", "),
412
+ uuid: myself.uuid
413
+ });
414
+ logError("If this keeps happening, go to Configurator / Hub and logout before trying again.");
415
+ halt();
416
+ }
417
+ };
418
+ //#endregion
117
419
  //#region src/lib/notifier.ts
118
420
  const notify = (args) => {
119
421
  const { message = "", title } = args;
@@ -126,7 +428,7 @@ const notify = (args) => {
126
428
  };
127
429
  //#endregion
128
430
  //#region src/commands/archive.ts
129
- const archive = (options) => {
431
+ const archive = async (options) => {
130
432
  const { ifCore } = getIfCoreFns();
131
433
  assertIsNotCore$4(ifCore);
132
434
  assertIsNotArchived();
@@ -138,6 +440,7 @@ const archive = (options) => {
138
440
  assertArchiveTargetDoesNotExist(archiveTarget);
139
441
  assertOptionsValid$9(options);
140
442
  if (options.dryrun) logTitle("🧪 DRY RUN MODE - No actual changes will be made 🧪");
443
+ if (!ifCore() && !options.forceSkipEpmsSync) await archiveEpms(configName, !!options.dryrun);
141
444
  if (!options.dryrun) {
142
445
  if (!existsSync(archiveDir)) mkdirSync(archiveDir, { recursive: true });
143
446
  renameSync(cwd, archiveTarget);
@@ -150,6 +453,39 @@ const archive = (options) => {
150
453
  });
151
454
  });
152
455
  };
456
+ const archiveEpms = async (configName, dryrun) => {
457
+ const { ifCore } = getIfCoreFns();
458
+ const { accountUuidProd, accountUuidStage } = loadTamaroMetadataFromConfig(getPaths(ifCore));
459
+ if (!accountUuidProd && !accountUuidStage) halt(stripIndent(`
460
+ No EPMS account UUIDs found in config.yml.
461
+ Add them to your config.yml, for example:
462
+ epms:
463
+ account_uuid: <UUID>
464
+ epms_stage:
465
+ account_uuid: <UUID>
466
+
467
+ Or re-run with --force-skip-epms-sync to skip the EPMS sync entirely - ONLY use in emergencies / or if you know what you're doing.
468
+ `));
469
+ const environments = [{
470
+ accountUuid: accountUuidStage,
471
+ env: "stage",
472
+ label: "Stage"
473
+ }, {
474
+ accountUuid: accountUuidProd,
475
+ env: "prod",
476
+ label: "Prod"
477
+ }];
478
+ for (const { accountUuid, env, label } of environments) {
479
+ if (!accountUuid) continue;
480
+ const epmsClient = epmsAuthenticate(env);
481
+ await assertUserIsSuperAdmin(epmsClient);
482
+ logTitle(`\nArchiving EPMS ${label}: ${configName}`);
483
+ if (!dryrun) {
484
+ await epmsClient.archiveTamaro(configName);
485
+ logSuccess(`✅ EPMS ${label} entry archived.`);
486
+ } else logSuccess(`⏭️ EPMS ${label} entry would be archived (dry run).`);
487
+ }
488
+ };
153
489
  const assertOptionsValid$9 = (options) => {
154
490
  const { profile } = options;
155
491
  if (profile) assertProfileValid(profile);
@@ -354,211 +690,6 @@ const assertTagValid = (tag) => {
354
690
  if (!regex.test(tag)) halt(`Flag "--tag" has forbidden format. Allowed format: ${regex.toString()}.`);
355
691
  };
356
692
  //#endregion
357
- //#region src/lib/epms/auth/util.ts
358
- const CachedTokenSchema = z.object({
359
- expirationTime: z.number(),
360
- token: z.string()
361
- });
362
- const AuthResponseSchema = z.object({
363
- access_token: z.string(),
364
- expires_in: z.number(),
365
- token_type: z.string()
366
- });
367
- const getCacheFilePath = (baseUrl, clientId) => {
368
- const cacheKey = Buffer.from(`${baseUrl}|${clientId}`).toString("base64url");
369
- return path.join(CACHE_DIR, `token-${cacheKey}.json`);
370
- };
371
- const readCachedToken = (baseUrl, clientId) => {
372
- try {
373
- const filePath = getCacheFilePath(baseUrl, clientId);
374
- if (!existsSync(filePath)) return;
375
- const data = JSON.parse(readFileSync(filePath, "utf8"));
376
- const parsed = CachedTokenSchema.safeParse(data);
377
- if (parsed.success) return parsed.data;
378
- } catch {}
379
- };
380
- const writeCachedToken = (baseUrl, clientId, cached) => {
381
- try {
382
- const filePath = getCacheFilePath(baseUrl, clientId);
383
- mkdirSync(path.dirname(filePath), { recursive: true });
384
- writeFileSync(filePath, JSON.stringify(cached), { mode: 384 });
385
- } catch {}
386
- };
387
- const removeCachedToken = (baseUrl, clientId) => {
388
- try {
389
- const filePath = getCacheFilePath(baseUrl, clientId);
390
- if (existsSync(filePath)) writeFileSync(filePath, "", { flag: "w" });
391
- } catch {}
392
- };
393
- //#endregion
394
- //#region src/lib/epms/auth/apiClient.ts
395
- const createAuthorizer = (baseUrl, clientId, clientSecret) => {
396
- const apiUnauthorized = ky.extend({
397
- headers: { "Access-Control-Allow-Origin": "*" },
398
- prefixUrl: baseUrl,
399
- timeout: DEFAULT_HTTP_TIMEOUT
400
- });
401
- let expirationTime = void 0;
402
- let token = void 0;
403
- const cached = readCachedToken(baseUrl, clientId);
404
- if (cached) {
405
- token = cached.token;
406
- expirationTime = cached.expirationTime;
407
- }
408
- const authorize = async () => {
409
- const isExpired = (expirationTime ?? 0) - Date.now() < 300 * 1e3;
410
- if (token && !isExpired) return token;
411
- const data = await apiUnauthorized.post("oauth2/token", { json: {
412
- client_id: clientId,
413
- client_secret: clientSecret,
414
- grant_type: "client_credentials"
415
- } }).json();
416
- token = `${data.token_type} ${data.access_token}`;
417
- expirationTime = Date.now() + data.expires_in * 1e3;
418
- writeCachedToken(baseUrl, clientId, {
419
- expirationTime,
420
- token
421
- });
422
- return token;
423
- };
424
- return apiUnauthorized.extend({ hooks: { beforeRequest: [async (request) => {
425
- const authToken = await authorize();
426
- request.headers.set("Authorization", authToken);
427
- }] } });
428
- };
429
- //#endregion
430
- //#region src/lib/epms/auth/authCallback.html
431
- var authCallback_default = "<!doctype html>\n<html>\n <head\n ><title>Authenticating</title></head\n >\n <body>\n <p>Authenticating</p>\n <script>\n const hash = window.location.hash.substring(1)\n const params = new URLSearchParams(hash)\n const data = {\n access_token: params.get('access_token'),\n token_type: params.get('token_type'),\n expires_in: parseInt(params.get('expires_in') || '0', 10),\n }\n fetch('/token', {\n method: 'POST',\n headers: {'Content-Type': 'application/json'},\n body: JSON.stringify(data),\n })\n .then(() => {\n document.querySelector('p').textContent =\n 'Authentication successful! You can close this tab.'\n })\n .catch(() => {\n document.querySelector('p').textContent =\n 'Authentication failed. Please try again.'\n })\n <\/script>\n </body>\n</html>\n";
432
- //#endregion
433
- //#region src/lib/epms/auth/browser.ts
434
- /**
435
- * The following authentication method is a browser-based OAuth flow.
436
- * A very similar flow is used by tools like AWS CLI when running `aws sso login` or Claude Code.
437
- *
438
- * The flow works as follows:
439
- * 1. The CLI starts a temporary local HTTP server that listens for the OAuth callback.
440
- * 2. The CLI opens the user's default web browser and navigates to the EPMS authorization URL, passing the local server's callback URL as the redirect_uri.
441
- * 3. The user authenticates in the browser, and the OAuth server redirects back to the local server with the authorization token.
442
- * 4. The local server captures the token and resolves the promise, completing the authentication flow.
443
- *
444
- *
445
- * We also implement a simple caching mechanism that stores the token in a file in the user's home directory.
446
- * This allows us to reuse the token for subsequent requests until it expires, at which point we automatically trigger a new authentication flow.
447
- */
448
- const doBrowserOAuthFlow = (authBaseUrl, clientId, port) => {
449
- return new Promise((resolve, reject) => {
450
- const server = createServer((req, res) => {
451
- if (req.method === "POST" && req.url === "/token") {
452
- let body = "";
453
- req.on("data", (chunk) => {
454
- body += chunk.toString();
455
- });
456
- req.on("end", () => {
457
- try {
458
- const data = AuthResponseSchema.parse(JSON.parse(body));
459
- res.writeHead(200);
460
- res.end();
461
- server.close();
462
- resolve(data);
463
- } catch (error) {
464
- res.writeHead(400);
465
- res.end();
466
- reject(error instanceof Error ? error : new Error(String(error)));
467
- }
468
- });
469
- } else {
470
- res.writeHead(200, { "Content-Type": "text/html" });
471
- res.end(authCallback_default);
472
- }
473
- });
474
- server.on("error", reject);
475
- server.listen(port, () => {
476
- const redirectUri = `http://localhost:${port}`;
477
- const authUrl = `${authBaseUrl}/oauth2/authorize?client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=token`;
478
- console.log(`Opening browser for authentication: ${authUrl}`);
479
- open(authUrl).catch((error) => {
480
- reject(error instanceof Error ? error : new Error(String(error)));
481
- });
482
- });
483
- });
484
- };
485
- const createBrowserAuthorizer = (authBaseUrl, clientId, epmsBaseUrl) => {
486
- const apiBase = ky.extend({
487
- prefixUrl: epmsBaseUrl,
488
- timeout: DEFAULT_HTTP_TIMEOUT
489
- });
490
- let expirationTime = void 0;
491
- let token = void 0;
492
- const cacheBaseUrl = `browser|${authBaseUrl}`;
493
- const cached = readCachedToken(cacheBaseUrl, clientId);
494
- if (cached) {
495
- token = cached.token;
496
- expirationTime = cached.expirationTime;
497
- }
498
- const resetAuth = () => {
499
- token = void 0;
500
- expirationTime = void 0;
501
- removeCachedToken(cacheBaseUrl, clientId);
502
- };
503
- const authorize = async () => {
504
- const isExpired = (expirationTime ?? 0) - Date.now() < 300 * 1e3;
505
- if (token && !isExpired) return token;
506
- const data = await doBrowserOAuthFlow(authBaseUrl, clientId, AUTH_PORT);
507
- token = `${data.token_type} ${data.access_token}`;
508
- expirationTime = Date.now() + data.expires_in * 1e3;
509
- writeCachedToken(cacheBaseUrl, clientId, {
510
- expirationTime,
511
- token
512
- });
513
- return token;
514
- };
515
- return apiBase.extend({ hooks: {
516
- afterResponse: [async (request, options, response) => {
517
- if (response.status === 401) {
518
- resetAuth();
519
- const newToken = await authorize();
520
- const newRequest = new Request(request, { headers: new Headers(request.headers) });
521
- newRequest.headers.set("Authorization", newToken);
522
- return ky(newRequest, options);
523
- }
524
- return response;
525
- }],
526
- beforeRequest: [async (request) => {
527
- const authToken = await authorize();
528
- request.headers.set("Authorization", authToken);
529
- }]
530
- } });
531
- };
532
- //#endregion
533
- //#region src/lib/epms/client.ts
534
- var EpmsClient = class EpmsClient {
535
- #epmsClient;
536
- constructor(epmsClient) {
537
- this.#epmsClient = epmsClient;
538
- }
539
- static fromClientCredentials(baseUrl, clientId, clientSecret) {
540
- return new EpmsClient(createAuthorizer(baseUrl, clientId, clientSecret));
541
- }
542
- static fromBrowserAuth(epmsBaseUrl, authBaseUrl, clientId) {
543
- return new EpmsClient(createBrowserAuthorizer(authBaseUrl, clientId, epmsBaseUrl));
544
- }
545
- async getOrganisationIdByAccountUuid(accountUuid) {
546
- const query = {
547
- from: 0,
548
- query: { $and: [{ $term: { object_uuid: accountUuid } }, { $term: { object: "account" } }] },
549
- size: 1
550
- };
551
- const response = await this.#epmsClient.post("search/events", { json: query }).json();
552
- if (response.hits.length === 0) throw new Error(`No events found for account UUID: ${accountUuid}`);
553
- const organisationId = response.hits[0].organisation_uuid;
554
- if (!organisationId) throw new Error(`Organisation UUID not found for account UUID: ${accountUuid}`);
555
- return organisationId;
556
- }
557
- async updateTamaro(configName, tag, json) {
558
- return this.#epmsClient.put(`products/tamaro/${configName}/tags/${tag}`, { json }).json();
559
- }
560
- };
561
- //#endregion
562
693
  //#region src/commands/update-epms.ts
563
694
  const updateEpms = async (options) => {
564
695
  const { ifCore } = getIfCoreFns();
@@ -576,7 +707,11 @@ const updateEpms = async (options) => {
576
707
  const { dryrun, tag } = options;
577
708
  const configName = getWidgetUuid();
578
709
  loadEnv(paths);
579
- const { accountUuidsProd, accountUuidsStage } = loadAccountUuidsFromConfig(paths);
710
+ const { accountUuidProd, accountUuidStage, managedBy } = loadTamaroMetadataFromConfig(paths);
711
+ if (!accountUuidProd && !accountUuidStage) {
712
+ logTitle("No EPMS account UUIDs found in config.yml. Skipping EPMS update.");
713
+ return;
714
+ }
580
715
  if (dryrun) logTitle("🧪 DRY RUN MODE - No actual changes will be made 🧪");
581
716
  const configuredVersion = resolveCoreVersion();
582
717
  if (!configuredVersion) {
@@ -586,36 +721,29 @@ const updateEpms = async (options) => {
586
721
  const baseInfo = {
587
722
  configured_version: configuredVersion ?? "unknown",
588
723
  last_deployment: Math.trunc(Date.now() / 1e3),
724
+ managed_by: managedBy,
589
725
  read_only: false
590
726
  };
591
- for (const accountUuidStage of accountUuidsStage) if (accountUuidStage) {
592
- const epmsClient = epmsAuthenticate("stage");
593
- const organisationUuid = await epmsClient.getOrganisationIdByAccountUuid(accountUuidStage);
594
- const body = {
595
- ...baseInfo,
596
- account_uuid: accountUuidStage,
597
- organisation_uuid: organisationUuid
598
- };
599
- logTitle(`Updating EPMS Stage for tag "${tag}":`);
600
- logDataTable({
601
- name: configName,
602
- tag,
603
- ...body
604
- });
605
- if (!dryrun) {
606
- await epmsClient.updateTamaro(configName, tag, body);
607
- logSuccess("✅ EPMS Stage is updated.");
608
- } else logSuccess("⏭️ EPMS Stage would be updated (dry run).");
609
- }
610
- for (const accountUuidProd of accountUuidsProd) if (accountUuidProd) {
611
- const epmsClient = epmsAuthenticate("prod");
612
- const organisationUuid = await epmsClient.getOrganisationIdByAccountUuid(accountUuidProd);
613
- logTitle(`Updating EPMS Prod for tag "${tag}":`);
727
+ const environments = [{
728
+ accountUuid: accountUuidStage,
729
+ env: "stage",
730
+ label: "Stage"
731
+ }, {
732
+ accountUuid: accountUuidProd,
733
+ env: "prod",
734
+ label: "Prod"
735
+ }];
736
+ for (const { accountUuid, env, label } of environments) {
737
+ if (!accountUuid) continue;
738
+ const epmsClient = epmsAuthenticate(env);
739
+ await assertUserIsSuperAdmin(epmsClient);
740
+ const organisationUuid = await epmsClient.getOrganisationIdByAccountUuid(accountUuid);
614
741
  const body = {
615
742
  ...baseInfo,
616
- account_uuid: accountUuidProd,
743
+ account_uuid: accountUuid,
617
744
  organisation_uuid: organisationUuid
618
745
  };
746
+ logTitle(`\nUpdating EPMS ${label}:`);
619
747
  logDataTable({
620
748
  name: configName,
621
749
  tag,
@@ -623,34 +751,13 @@ const updateEpms = async (options) => {
623
751
  });
624
752
  if (!dryrun) {
625
753
  await epmsClient.updateTamaro(configName, tag, body);
626
- logSuccess("✅ EPMS Prod is updated.");
627
- } else logSuccess("⏭️ EPMS Prod would be updated (dry run).");
628
- }
629
- };
630
- const epmsAuthenticate = (env) => {
631
- if (env === "stage") {
632
- if (process.env.EPMS_CLIENT_ID_STAGE && process.env.EPMS_CLIENT_SECRET_STAGE) return EpmsClient.fromClientCredentials(EPMS_API_BASE_URL_STAGE, process.env.EPMS_CLIENT_ID_STAGE, process.env.EPMS_CLIENT_SECRET_STAGE);
633
- return EpmsClient.fromBrowserAuth(EPMS_API_BASE_URL_STAGE, EPMS_AUTH_BASE_URL_STAGE, EPMS_AUTH_CLIENT_ID_STAGE);
754
+ logSuccess(`✅ EPMS ${label} is updated.`);
755
+ } else logSuccess(`⏭️ EPMS ${label} would be updated (dry run).`);
634
756
  }
635
- if (process.env.EPMS_CLIENT_ID && process.env.EPMS_CLIENT_SECRET) return EpmsClient.fromClientCredentials(EPMS_API_BASE_URL_PROD, process.env.EPMS_CLIENT_ID, process.env.EPMS_CLIENT_SECRET);
636
- return EpmsClient.fromBrowserAuth(EPMS_API_BASE_URL_PROD, EPMS_AUTH_BASE_URL_PROD, EPMS_AUTH_CLIENT_ID_PROD);
637
- };
638
- const loadAccountUuidsFromConfig = (paths) => {
639
- if (!("configYml" in paths)) {
640
- halt("Fatal error, this should never happen");
641
- throw new Error("Fatal error");
642
- }
643
- const configContent = yaml.load(readFileSync(paths.configYml, "utf8"));
644
- const accountUuidsStage = resolveAccountUuidFromConfig(configContent, "epms_stage");
645
- return {
646
- accountUuidsProd: resolveAccountUuidFromConfig(configContent, "epms"),
647
- accountUuidsStage
648
- };
649
757
  };
650
758
  const loadEnv = (paths) => {
651
759
  const filePath = globSync(paths.appEnv).find((file) => path.basename(file) === ".env");
652
- if (!filePath) halt("No \".env\" file found. Are you in the folder of the Tamaro configuration?");
653
- config({
760
+ if (filePath) config({
654
761
  path: filePath,
655
762
  quiet: true
656
763
  });
@@ -730,8 +837,8 @@ const deploy = async (options) => {
730
837
  halt(error.stderr);
731
838
  }
732
839
  }
733
- if (options.skipEpmsSyncOnlyUseInEmergenciesOrYouWillBeFired) logTitle("⚠️ EPMS sync skipped. You better know what you are doing.");
734
- else await updateEpms(options);
840
+ if (options.forceSkipEpmsSync) logTitle("⚠️ EPMS sync skipped. You better know what you are doing.");
841
+ else if (!ifCore()) await updateEpms(options);
735
842
  const demoPage = `https://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}/index.html`;
736
843
  const entryPoint = `https://${AWS_S3_BUCKET_TAMARO}/${configName}/${tag}/${entryFilename}`;
737
844
  logTitle(`\nBundle for "${configName}" is deployed with tag "${tag}".`);
@@ -954,7 +1061,7 @@ const prepareFlags = async (options) => {
954
1061
  };
955
1062
  //#endregion
956
1063
  //#region src/commands/unarchive.ts
957
- const unarchive = (options) => {
1064
+ const unarchive = async (options) => {
958
1065
  const { ifCore } = getIfCoreFns({ allowArchived: true });
959
1066
  assertIsNotCore$1(ifCore);
960
1067
  assertIsArchived();
@@ -965,12 +1072,13 @@ const unarchive = (options) => {
965
1072
  const restoreTarget = path.resolve(configsDir, configName);
966
1073
  assertRestoreTargetDoesNotExist(restoreTarget);
967
1074
  assertOptionsValid$2(options);
1075
+ if (!ifCore() && !options.forceSkipEpmsSync) await unarchiveEpms(configName, !!options.dryrun);
968
1076
  if (options.dryrun) logTitle("🧪 DRY RUN MODE - No actual changes will be made 🧪");
969
1077
  if (!options.dryrun) {
970
1078
  renameSync(cwd, restoreTarget);
971
1079
  process.chdir(restoreTarget);
972
1080
  } else logTitle(`Would move ${cwd} → ${restoreTarget}`);
973
- logTitle(`✅ "${configName}" has been unarchived and moved to ${restoreTarget}`);
1081
+ logSuccess(`\n✅ "${configName}" has been unarchived and moved to ${restoreTarget}`);
974
1082
  process.on("exit", () => {
975
1083
  notify({
976
1084
  message: `"${configName}" has been unarchived.`,
@@ -978,6 +1086,39 @@ const unarchive = (options) => {
978
1086
  });
979
1087
  });
980
1088
  };
1089
+ const unarchiveEpms = async (configName, dryrun) => {
1090
+ const { ifCore } = getIfCoreFns({ allowArchived: true });
1091
+ const { accountUuidProd, accountUuidStage } = loadTamaroMetadataFromConfig(getPaths(ifCore));
1092
+ if (!accountUuidProd && !accountUuidStage) halt(stripIndent(`
1093
+ No EPMS account UUIDs found in config.yml.
1094
+ Add them to your config.yml, for example:
1095
+ epms:
1096
+ account_uuid: <UUID>
1097
+ epms_stage:
1098
+ account_uuid: <UUID>
1099
+
1100
+ Or re-run with --force-skip-epms-sync to skip the EPMS sync entirely - ONLY use in emergencies / or if you know what you're doing.
1101
+ `));
1102
+ const environments = [{
1103
+ accountUuid: accountUuidStage,
1104
+ env: "stage",
1105
+ label: "Stage"
1106
+ }, {
1107
+ accountUuid: accountUuidProd,
1108
+ env: "prod",
1109
+ label: "Prod"
1110
+ }];
1111
+ for (const { accountUuid, env, label } of environments) {
1112
+ if (!accountUuid) continue;
1113
+ const epmsClient = epmsAuthenticate(env);
1114
+ await assertUserIsSuperAdmin(epmsClient);
1115
+ logTitle(`\nUnarchiving EPMS ${label}: ${configName}`);
1116
+ if (!dryrun) {
1117
+ await epmsClient.unarchiveTamaro(configName);
1118
+ logSuccess(`✅ EPMS ${label} entry unarchived.`);
1119
+ } else logSuccess(`⏭️ EPMS ${label} entry would be unarchived (dry run).`);
1120
+ }
1121
+ };
981
1122
  const assertOptionsValid$2 = (options) => {
982
1123
  const { profile } = options;
983
1124
  if (profile) assertProfileValid(profile);
@@ -1128,7 +1269,7 @@ const promptBucketTamaroEmailConfig = async () => {
1128
1269
  //#endregion
1129
1270
  //#region package.json
1130
1271
  var name = "@raisenow/tamaro-cli";
1131
- var version = "1.9.0-dev.1";
1272
+ var version = "1.9.0-dev.3";
1132
1273
  //#endregion
1133
1274
  //#region src/cli.ts
1134
1275
  /**
@@ -1146,7 +1287,7 @@ const cli = createCommand().name(name).version(version, "-v, --version");
1146
1287
  cli.command("dev").description("Run the local development server for Tamaro Core or a particular customer configuration").option("--local-core", "Load Tamaro Core from localhost:1234 instead of the CDN", false).option("--https", "Let local web server serve Tamaro with SSL encryption", false).option("--port <port>", "Web server port", `${DEFAULT_PORT}`).option("--env <env>", "Environment (dev, stage, prod)").option("--nolint", "Disable ESLint", false).option("--debug", "Enable debug mode", false).action(async (options) => {
1147
1288
  await dev(options);
1148
1289
  });
1149
- cli.command("build").description("Build an optimised (minified) bundle of Tamaro Core or a customer configuration").option("--local-core", "Load Tamaro Core from localhost:1234 instead of the CDN", false).option("--analyze", "Generate bundle statistics to \"reports\" folder", false).option("--serve", "Run the generated bundle with a local web server", false).option("--https", "Let local web server serve Tamaro with SSL encryption", false).option("--port <port>", "Web server port", `${DEFAULT_PORT}`).option("--env <env>", "Environment (dev, stage, prod)").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--deploy", "Deploy Tamaro Core or a customer configuration bundle to AWS S3", false).option("--skip-epms-sync-only-use-in-emergencies-or-you-will-be-fired", "Skip the EPMS sync entirely. Only use this in emergencies.", false).option("--tag <tag>", "Tag which should be used for the deployment of the bundle", DEFAULT_TAG).option("--nolint", "Disable ESLint", false).option("--debug", "Enable debug mode", false).action(async (options) => {
1290
+ cli.command("build").description("Build an optimised (minified) bundle of Tamaro Core or a customer configuration").option("--local-core", "Load Tamaro Core from localhost:1234 instead of the CDN", false).option("--analyze", "Generate bundle statistics to \"reports\" folder", false).option("--serve", "Run the generated bundle with a local web server", false).option("--https", "Let local web server serve Tamaro with SSL encryption", false).option("--port <port>", "Web server port", `${DEFAULT_PORT}`).option("--env <env>", "Environment (dev, stage, prod)").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--deploy", "Deploy Tamaro Core or a customer configuration bundle to AWS S3", false).option("--force-skip-epms-sync", "Skip the EPMS sync entirely. Only use this in emergencies.", false).option("--tag <tag>", "Tag which should be used for the deployment of the bundle", DEFAULT_TAG).option("--nolint", "Disable ESLint", false).option("--debug", "Enable debug mode", false).action(async (options) => {
1150
1291
  await build(options);
1151
1292
  if (options.serve) await serve(options);
1152
1293
  if (options.deploy) await deploy(options);
@@ -1154,7 +1295,7 @@ cli.command("build").description("Build an optimised (minified) bundle of Tamaro
1154
1295
  cli.command("serve").description("Run a local web server for pre-built bundle").option("--port <port>", "Web server port", `${DEFAULT_PORT}`).option("--https", "Let local web server serve Tamaro with SSL encryption", false).action(async (options) => {
1155
1296
  await serve(options);
1156
1297
  });
1157
- cli.command("deploy").description("Deploy a pre-built Tamaro Core or customer configuration bundle to AWS S3").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--tag <tag>", "Tag which should be used for the deployment of the bundle", DEFAULT_TAG).option("--dryrun", "Displays the operations that would be performed without actually running them", false).option("--skip-epms-sync-only-use-in-emergencies-or-you-will-be-fired", "Skip the EPMS sync entirely. Only use this in emergencies.", false).action(async (options) => {
1298
+ cli.command("deploy").description("Deploy a pre-built Tamaro Core or customer configuration bundle to AWS S3").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--tag <tag>", "Tag which should be used for the deployment of the bundle", DEFAULT_TAG).option("--dryrun", "Displays the operations that would be performed without actually running them", false).option("--force-skip-epms-sync", "Skip the EPMS sync entirely. Only use this in emergencies.", false).action(async (options) => {
1158
1299
  await deploy(options);
1159
1300
  });
1160
1301
  cli.command("list-deployed").description("List deployments of Tamaro Core or a particular customer configuration").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--config <config>", "Configuration name (default: current customer configuration)").action(async (options) => {
@@ -1169,11 +1310,11 @@ cli.command("undeploy").description("Undeploy a Tamaro Core or customer configur
1169
1310
  cli.command("undeploy-email-config").description("Undeploy a widget's email configuration from AWS S3").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--bucket <bucket>", "AWS bucket for email configs").option("--stage", "Undeploy from stage environment (alternative to --bucket)").option("--prod", "Undeploy from prod environment (alternative to --bucket)").option("--dryrun", "Displays the operations that would be performed without actually running them", false).action(async (options) => {
1170
1311
  await undeployEmailConfig(options);
1171
1312
  });
1172
- cli.command("archive").description("Archive a customer configuration by moving it to the _archived folder").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--dryrun", "Displays the operations that would be performed without actually running them", false).action((options) => {
1173
- archive(options);
1313
+ cli.command("archive").description("Archive a customer configuration by moving it to the _archived folder").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--dryrun", "Displays the operations that would be performed without actually running them", false).option("--force-skip-epms-sync", "Skip EPMS archive sync (emergency use only)", false).action(async (options) => {
1314
+ await archive(options);
1174
1315
  });
1175
- cli.command("unarchive").description("Unarchive a customer configuration by restoring it from the _archived folder").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--dryrun", "Displays the operations that would be performed without actually running them", false).action((options) => {
1176
- unarchive(options);
1316
+ cli.command("unarchive").description("Unarchive a customer configuration by restoring it from the _archived folder").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--dryrun", "Displays the operations that would be performed without actually running them", false).option("--force-skip-epms-sync", "Skip EPMS unarchive sync (emergency use only)", false).action(async (options) => {
1317
+ await unarchive(options);
1177
1318
  });
1178
1319
  cli.command("update-epms").description("Update EPMS with the currently deployed widget tags.").option("--profile <profile>", "AWS profile").option("--ci", "CI environment", false).option("--dryrun", "Displays the operations that would be performed without actually running them", false).option("--tag <tag>", "Tag which should be set for the deployment", DEFAULT_TAG).action(async (options) => {
1179
1320
  await updateEpms(options);
@@ -1,9 +1,9 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { existsSync, realpathSync } from "node:fs";
3
3
  import path from "node:path";
4
+ import stripIndent from "strip-indent";
4
5
  import { execaCommandSync } from "execa";
5
6
  import prompts from "prompts";
6
- import stripIndent from "strip-indent";
7
7
  import chalk from "chalk";
8
8
  import columnify from "columnify";
9
9
  import envPaths from "env-paths";
@@ -22,6 +22,9 @@ const logError = (message) => {
22
22
  const logSuccess = (message) => {
23
23
  if (message) console.log(chalk.green(message));
24
24
  };
25
+ const logInfo = (message) => {
26
+ if (message) console.log(chalk.blue(message));
27
+ };
25
28
  const logCommand = (cmd) => {
26
29
  console.log(`\n${chalk.dim(stripIndent(cmd).trim())}\n`);
27
30
  };
@@ -66,16 +69,16 @@ const EPMS_API_BASE_URL_STAGE = "https://api.stage.mesos.raisenow.net";
66
69
  const EPMS_API_BASE_URL_PROD = "https://api.raisenow.io";
67
70
  const EPMS_AUTH_BASE_URL_STAGE = "https://login.stage.mesos.raisenow.net";
68
71
  const EPMS_AUTH_BASE_URL_PROD = "https://login.raisenow.com";
69
- const EPMS_AUTH_CLIENT_ID_STAGE = "luna-stage-local";
70
- const EPMS_AUTH_CLIENT_ID_PROD = "luna-local";
72
+ const EPMS_OAUTH_CLIENT_ID = "tamaro-cli";
71
73
  const CORE_CONFIG_NAME = "tamaro-core";
72
- const AUTH_PORT = 8080;
74
+ const AUTH_PORT = 4571;
73
75
  const AWS_CLOUDFRONT_DISTRIBUTION_ID = "EHJ1OM458YQ0I";
74
76
  const HTTPS_CRT_FILE = "localhost.crt";
75
77
  const HTTPS_KEY_FILE = "localhost.key";
76
78
  const CACHE_DIR = envPaths("tamaro-cli", { suffix: "" }).cache;
79
+ const TAMARO_VERSION_IN_URL_REGEX = /tamaro-core\/(.*)\/index.js/;
77
80
  //#endregion
78
- //#region node_modules/.pnpm/tsdown@0.21.8_synckit@0.11.11_typescript@6.0.2/node_modules/tsdown/esm-shims.js
81
+ //#region node_modules/.pnpm/tsdown@0.22.0_tsx@4.19.1_typescript@6.0.3_unrun@0.2.37_synckit@0.11.11_/node_modules/tsdown/esm-shims.js
79
82
  const getFilename = () => fileURLToPath(import.meta.url);
80
83
  const getDirname = () => path.dirname(getFilename());
81
84
  const __dirname = /* @__PURE__ */ getDirname();
@@ -199,27 +202,25 @@ const getIfCoreFns = ({ allowArchived = false } = {}) => {
199
202
  };
200
203
  const resolveAccountUuidFromConfig = (rawConfig, field) => {
201
204
  const epmsConfig = z.object({ [field]: z.record(z.string(), z.unknown()) }).safeParse(rawConfig);
202
- if (!epmsConfig.success) return [];
205
+ if (!epmsConfig.success) return;
203
206
  const configContent = epmsConfig.data[field];
204
207
  const accountUuid = configContent.account_mapping ?? configContent.account_uuid;
205
208
  const accountUuidValidation = z.uuid().safeParse(accountUuid);
206
- const accontUuidArrayValidation = z.array(z.uuid()).safeParse(accountUuid);
207
- if (!accountUuidValidation.success && !accontUuidArrayValidation.success) halt(stripIndent(`Could not extract EPMS account UUID(s) from config.yml. Please ensure that the "${field}" field is correctly formatted. Expected formats:
209
+ if (!accountUuidValidation.success) halt(stripIndent(`Could not extract EPMS account UUID from config.yml. Please ensure that the "${field}" field is correctly formatted. Expected formats:
208
210
  1. ${field}:
209
211
  account_uuid: <UUID>
210
-
212
+
211
213
  Alternatively, if the account_uuid is conditional using "if" / "then" / "else" statements, it must be provided using the "account_mapping" format:
212
214
  2. ${field}:
213
- account_mapping:
214
- - <UUID_1>
215
- - <UUID_2>
216
- - <UUID_N>
215
+ account_mapping: <UUID>
217
216
  `));
218
- if (accontUuidArrayValidation.success) return accontUuidArrayValidation.data;
219
- if (accountUuidValidation.success) return [accountUuidValidation.data];
220
- throw new Error("This should never happen");
217
+ return accountUuidValidation.data;
218
+ };
219
+ const resolveManagedByFromConfig = (rawConfig) => {
220
+ const managedByConfig = z.object({ managed_by: z.string().trim().min(1).max(255).optional() }).safeParse(rawConfig);
221
+ if (!managedByConfig.success) halt("Could not extract \"managed_by\" from config.yml. Please ensure it is a non-empty string.");
222
+ return managedByConfig.data?.managed_by ?? "RaiseNow";
221
223
  };
222
- const TAMARO_VERSION_IN_URL_REGEX = /tamaro-core\/(.*)\/index.js/;
223
224
  const extractTamaroVersionFromUrl = (url) => {
224
225
  const match = url.match(TAMARO_VERSION_IN_URL_REGEX);
225
226
  if (match) return match[1].toString();
@@ -343,4 +344,4 @@ const applyEnv = (env) => {
343
344
  if (filePath) config({ path: filePath });
344
345
  };
345
346
  //#endregion
346
- export { EPMS_AUTH_CLIENT_ID_STAGE as A, logSuccess as B, DEFAULT_PORT as C, EPMS_AUTH_BASE_URL_PROD as D, EPMS_API_BASE_URL_STAGE as E, runCommandSync as F, createTerminalLink as I, logCommand as L, HTTPS_KEY_FILE as M, halt as N, EPMS_AUTH_BASE_URL_STAGE as O, promptConfirmation as P, logDataTable as R, DEFAULT_HTTP_TIMEOUT as S, EPMS_API_BASE_URL_PROD as T, logTitle as V, AWS_S3_BUCKET_TAMARO as _, getIfCoreFns as a, CACHE_DIR as b, getWidgetUuid as c, resolveBin as d, resolveCoreVersion as f, AWS_CLOUDFRONT_DISTRIBUTION_ID as g, AUTH_PORT as h, extensions as i, HTTPS_CRT_FILE as j, EPMS_AUTH_CLIENT_ID_PROD as k, resolveAccountUuidFromConfig as l, resolveOwn as m, assertEnvValid as n, getPaths as o, resolveEslintPluginConfig as p, getEnvVars as r, getRelativePaths as s, applyEnv as t, resolveApp as u, AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD as v, DEFAULT_TAG as w, CORE_CONFIG_NAME as x, AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_STAGE as y, logError as z };
347
+ export { EPMS_OAUTH_CLIENT_ID as A, logInfo as B, DEFAULT_HTTP_TIMEOUT as C, EPMS_API_BASE_URL_STAGE as D, EPMS_API_BASE_URL_PROD as E, runCommandSync as F, logTitle as H, createTerminalLink as I, logCommand as L, HTTPS_KEY_FILE as M, halt as N, EPMS_AUTH_BASE_URL_PROD as O, promptConfirmation as P, logDataTable as R, CORE_CONFIG_NAME as S, DEFAULT_TAG as T, logSuccess as V, AWS_CLOUDFRONT_DISTRIBUTION_ID as _, getIfCoreFns as a, AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_STAGE as b, getWidgetUuid as c, resolveBin as d, resolveCoreVersion as f, AUTH_PORT as g, resolveOwn as h, extensions as i, HTTPS_CRT_FILE as j, EPMS_AUTH_BASE_URL_STAGE as k, resolveAccountUuidFromConfig as l, resolveManagedByFromConfig as m, assertEnvValid as n, getPaths as o, resolveEslintPluginConfig as p, getEnvVars as r, getRelativePaths as s, applyEnv as t, resolveApp as u, AWS_S3_BUCKET_TAMARO as v, DEFAULT_PORT as w, CACHE_DIR as x, AWS_S3_BUCKET_TAMARO_EMAIL_CONFIG_PROD as y, logError as z };
@@ -1,4 +1,4 @@
1
- import { M as HTTPS_KEY_FILE, R as logDataTable, V as logTitle, a as getIfCoreFns, i as extensions, j as HTTPS_CRT_FILE, o as getPaths, p as resolveEslintPluginConfig, r as getEnvVars, s as getRelativePaths, u as resolveApp } from "./env-JzPIlctP.js";
1
+ import { H as logTitle, M as HTTPS_KEY_FILE, R as logDataTable, a as getIfCoreFns, i as extensions, j as HTTPS_CRT_FILE, o as getPaths, p as resolveEslintPluginConfig, r as getEnvVars, s as getRelativePaths, u as resolveApp } from "./env-DZ3cWM6p.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";
@@ -164,6 +164,22 @@ const getWebpackConfig = (env) => {
164
164
  debug: ifDebug(),
165
165
  modules: false,
166
166
  useBuiltIns: "usage",
167
+ /**
168
+ * Exclude plugins that interfere with MobX flow inference via makeAutoObservable.
169
+ *
170
+ * MobX detects generator methods on the prototype and wraps them as `flow` automatically.
171
+ * Several Babel transforms break this detection:
172
+ *
173
+ * - transform-parameters: rewrites generator methods with default params (e.g. `*update(data, skip = false)`)
174
+ * into plain functions returning an IIFE generator. makeAutoObservable then sees a regular function
175
+ * and wraps it as `action` instead of `flow`.
176
+ *
177
+ * - async-to-generator / regenerator / async-generator-functions: downcompile async/generator syntax
178
+ * to state-machine helpers, again making the methods invisible to MobX flow inference.
179
+ *
180
+ * All of these transforms are unnecessary — our browser targets support default parameters,
181
+ * generators, and async functions natively.
182
+ */
167
183
  exclude: [
168
184
  "@babel/plugin-transform-async-to-generator",
169
185
  "@babel/plugin-transform-regenerator",
package/package.json CHANGED
@@ -1,15 +1,14 @@
1
1
  {
2
2
  "name": "@raisenow/tamaro-cli",
3
- "version": "1.9.0-dev.1",
3
+ "version": "1.9.0-dev.3",
4
4
  "author": {
5
5
  "name": "RaiseNow",
6
6
  "email": "development@raisenow.com"
7
7
  },
8
8
  "type": "module",
9
- "bin": "dist/cli.js",
10
9
  "engines": {
11
10
  "node": ">=22.22.2",
12
- "pnpm": ">=10.33.0",
11
+ "pnpm": ">=11.0.9",
13
12
  "npm": "please-use-pnpm",
14
13
  "yarn": "please-use-pnpm"
15
14
  },
@@ -18,7 +17,7 @@
18
17
  "@babel/cli": "^7.28.6",
19
18
  "@babel/core": "^7.29.0",
20
19
  "@babel/plugin-syntax-dynamic-import": "^7.8.3",
21
- "@babel/preset-env": "^7.29.2",
20
+ "@babel/preset-env": "^7.29.5",
22
21
  "@babel/preset-react": "^7.28.5",
23
22
  "@babel/preset-typescript": "^7.28.5",
24
23
  "@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
@@ -26,7 +25,7 @@
26
25
  "@types/columnify": "^1.5.4",
27
26
  "@types/handlebars-helpers": "^0.5.6",
28
27
  "@types/lodash": "^4.17.24",
29
- "@types/node": "^22.19.17",
28
+ "@types/node": "^22.19.18",
30
29
  "@types/node-notifier": "^8.0.5",
31
30
  "@types/prompts": "^2.4.9",
32
31
  "@types/resolve-bin": "^0.4.3",
@@ -54,7 +53,7 @@
54
53
  "handlebars": "^4.7.9",
55
54
  "handlebars-helpers": "^0.10.0",
56
55
  "html-loader": "^5.1.0",
57
- "html-webpack-plugin": "^5.6.6",
56
+ "html-webpack-plugin": "^5.6.7",
58
57
  "js-yaml": "^4.1.1",
59
58
  "json-loader": "^0.5.7",
60
59
  "ky": "^1.14.3",
@@ -63,7 +62,7 @@
63
62
  "node-notifier": "^10.0.1",
64
63
  "open": "^11.0.0",
65
64
  "portfinder": "^1.0.38",
66
- "postcss": "^8.5.9",
65
+ "postcss": "^8.5.14",
67
66
  "postcss-import": "^16.1.1",
68
67
  "postcss-loader": "^8.2.1",
69
68
  "postcss-nested": "^7.0.2",
@@ -71,16 +70,16 @@
71
70
  "react-refresh": "^0.18.0",
72
71
  "resolve-url-loader": "^5.0.0",
73
72
  "sass": "^1.99.0",
74
- "sass-loader": "^16.0.7",
73
+ "sass-loader": "^16.0.8",
75
74
  "strip-indent": "^4.1.1",
76
75
  "style-loader": "^4.0.0",
77
- "terser-webpack-plugin": "^5.4.0",
76
+ "terser-webpack-plugin": "^5.6.0",
78
77
  "tsconfig-paths-webpack-plugin": "^4.2.0",
79
- "tsdown": "^0.21.8",
80
- "typescript": "^6.0.2",
78
+ "tsdown": "^0.22.0",
79
+ "typescript": "^6.0.3",
81
80
  "url-loader": "^4.1.1",
82
- "vitest": "^4.1.4",
83
- "webpack": "^5.106.1",
81
+ "vitest": "^4.1.5",
82
+ "webpack": "^5.106.2",
84
83
  "webpack-cli": "^7.0.2",
85
84
  "webpack-config-utils": "^2.3.1",
86
85
  "webpack-dev-server": "^5.2.3",
@@ -90,11 +89,11 @@
90
89
  "devDependencies": {
91
90
  "@rnw-npm/eslint-config": "^2.0.0",
92
91
  "@rnw-npm/prettier-config": "^1.0.0",
92
+ "eslint": "^10.3.0",
93
93
  "@types/js-yaml": "^4.0.9",
94
- "eslint": "^10.2.0",
95
94
  "husky": "^9.1.7",
96
95
  "lint-staged": "^16.4.0",
97
- "prettier": "^3.8.2"
96
+ "prettier": "^3.8.3"
98
97
  },
99
98
  "prettier": "@rnw-npm/prettier-config",
100
99
  "lint-staged": {
@@ -111,5 +110,8 @@
111
110
  "lint": "DEBUG=eslint:eslint-helpers eslint .",
112
111
  "format": "prettier . --write --ignore-path ./.prettierignore",
113
112
  "test": "vitest"
113
+ },
114
+ "bin": {
115
+ "tamaro-cli": "dist/cli.js"
114
116
  }
115
117
  }
@@ -1,11 +1,11 @@
1
1
  minimumReleaseAge: 1440
2
-
3
2
  minimumReleaseAgeExclude:
4
3
  - '@raisenow/*'
5
4
  - '@rnw-npm/*'
6
-
7
- onlyBuiltDependencies:
8
- - core-js
9
- - core-js-pure
10
- - esbuild
11
- - unrs-resolver
5
+ engineStrict: true
6
+ allowBuilds:
7
+ core-js: true
8
+ core-js-pure: true
9
+ esbuild: true
10
+ highlight.js: true
11
+ unrs-resolver: true
package/eslint.config.ts DELETED
@@ -1,30 +0,0 @@
1
- import {
2
- baseConfig,
3
- defineConfig,
4
- GLOB_MARKDOWN,
5
- GLOB_SRC,
6
- OFF,
7
- } from '@rnw-npm/eslint-config'
8
-
9
- export default defineConfig([
10
- ...baseConfig,
11
-
12
- /**
13
- * "tamaro-cli" specific overrides.
14
- */
15
- {
16
- files: [GLOB_SRC],
17
- name: 'tamaro-cli/overrides/ts',
18
- rules: {
19
- 'jsdoc/convert-to-jsdoc-comments': OFF,
20
- 'no-useless-assignment': OFF,
21
- },
22
- },
23
- {
24
- files: [GLOB_MARKDOWN],
25
- name: 'tamaro-cli/overrides/md',
26
- rules: {
27
- 'markdown/no-multiple-h1': OFF,
28
- },
29
- },
30
- ])