@jtl-software/create-cloud-app 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +123 -15
  2. package/package.json +7 -7
package/dist/index.js CHANGED
@@ -23,16 +23,32 @@ import pc from "picocolors";
23
23
  var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
24
24
  var CALLBACK_PORT = 9876;
25
25
  var REDIRECT_URI = `http://localhost:${CALLBACK_PORT}/callback`;
26
- var DEFAULT_SCOPE = "openid offline_access";
27
26
  var DEFAULT_API_HOST = "https://api.jtl-cloud.com";
28
- var DEFAULT_AUTH_HOST = "https://auth.jtl-cloud.com";
27
+ var DEFAULT_AUTH_HOST = "https://id.jtl-cloud.com";
29
28
  var DEFAULT_HUB_HOST = "https://hub.jtl-cloud.com";
30
29
  var DEFAULT_PARTNER_HOST = "https://partner.jtl-cloud.com";
31
30
  var CLIENT_IDS_BY_AUTH_HOST = {
32
- "auth.jtl-cloud.com": "b6aca875-c9a3-4010-9fd7-7f8d2e0b0333",
33
- "auth.dev.jtl-cloud.com": "9c230937-6a52-48d6-9a39-e97edd8ef511",
34
- "auth.qa.jtl-cloud.com": "fdf8ec1d-b0d3-4f16-9f29-a2ebbbf62951"
31
+ // Registered as a native client in
32
+ // zitadel-terraform (services/cloud-apps-cli.yaml), which is what trusts the
33
+ // loopback redirect below. These are client ids, not the Zitadel app ids that
34
+ // appear in terraform apply logs - the authorize endpoint rejects those.
35
+ "id.jtl-cloud.com": "386095063207346992",
36
+ "id.dev.jtl-cloud.com": "385952106948949981",
37
+ "id.qa.jtl-cloud.com": "386094749154581156"
35
38
  };
39
+ var ZITADEL_PROJECT_IDS = {
40
+ "id.dev.jtl-cloud.com": "380736094687225619",
41
+ "id.qa.jtl-cloud.com": "383246500387367093",
42
+ "id.jtl-cloud.com": "383246859386235061"
43
+ };
44
+ function defaultScopeFor(authBaseUrl) {
45
+ const projectId = ZITADEL_PROJECT_IDS[new URL(authBaseUrl).hostname];
46
+ const scopes = ["openid", "profile", "email", "offline_access"];
47
+ if (projectId) {
48
+ scopes.push(`urn:zitadel:iam:org:project:id:${projectId}:aud`);
49
+ }
50
+ return scopes.join(" ");
51
+ }
36
52
  var TOKEN_EXPIRY_MARGIN_S = 60;
37
53
  function tokenCacheFile() {
38
54
  return process.env.JTL_CLI_TOKEN_CACHE_FILE ?? path.join(os.homedir(), ".config", "jtl-cli", "tokens.json");
@@ -237,8 +253,33 @@ Opening browser to sign in.`);
237
253
  });
238
254
  });
239
255
  }
240
- async function postTokenRequest(authBaseUrl, body) {
241
- const res = await fetch(`${authBaseUrl}/oauth2/token`, {
256
+ var oidcMetadataCache = /* @__PURE__ */ new Map();
257
+ async function discoverOidc(authBaseUrl) {
258
+ const cached = oidcMetadataCache.get(authBaseUrl);
259
+ if (cached) return cached;
260
+ const pending = (async () => {
261
+ const url = `${authBaseUrl}/.well-known/openid-configuration`;
262
+ const res = await fetch(url, { headers: { Accept: "application/json" } });
263
+ if (!res.ok) {
264
+ throw new Error(`OIDC discovery failed: ${url} returned ${res.status}`);
265
+ }
266
+ const doc = await res.json();
267
+ if (!doc.authorization_endpoint || !doc.token_endpoint) {
268
+ throw new Error(
269
+ `OIDC discovery at ${url} is missing authorization_endpoint or token_endpoint`
270
+ );
271
+ }
272
+ return {
273
+ authorization_endpoint: doc.authorization_endpoint,
274
+ token_endpoint: doc.token_endpoint
275
+ };
276
+ })();
277
+ oidcMetadataCache.set(authBaseUrl, pending);
278
+ pending.catch(() => oidcMetadataCache.delete(authBaseUrl));
279
+ return pending;
280
+ }
281
+ async function postTokenRequest(tokenEndpoint, body) {
282
+ const res = await fetch(tokenEndpoint, {
242
283
  method: "POST",
243
284
  headers: {
244
285
  "Content-Type": "application/x-www-form-urlencoded",
@@ -262,8 +303,9 @@ async function postTokenRequest(authBaseUrl, body) {
262
303
  return parsed;
263
304
  }
264
305
  async function exchangeCodeForToken(authBaseUrl, clientId, code, verifier) {
306
+ const { token_endpoint } = await discoverOidc(authBaseUrl);
265
307
  return postTokenRequest(
266
- authBaseUrl,
308
+ token_endpoint,
267
309
  new URLSearchParams({
268
310
  grant_type: "authorization_code",
269
311
  code,
@@ -274,8 +316,9 @@ async function exchangeCodeForToken(authBaseUrl, clientId, code, verifier) {
274
316
  );
275
317
  }
276
318
  async function refreshAccessToken(authBaseUrl, clientId, refreshToken) {
319
+ const { token_endpoint } = await discoverOidc(authBaseUrl);
277
320
  return postTokenRequest(
278
- authBaseUrl,
321
+ token_endpoint,
279
322
  new URLSearchParams({
280
323
  grant_type: "refresh_token",
281
324
  refresh_token: refreshToken,
@@ -312,7 +355,8 @@ async function acquireAccessToken(opts) {
312
355
  }
313
356
  const pkce = generatePkce();
314
357
  const state = base64url(crypto.randomBytes(16));
315
- const authorizeUrl = new URL("/oauth2/auth", auth);
358
+ const { authorization_endpoint } = await discoverOidc(auth);
359
+ const authorizeUrl = new URL(authorization_endpoint);
316
360
  authorizeUrl.searchParams.set("response_type", "code");
317
361
  authorizeUrl.searchParams.set("client_id", clientId);
318
362
  authorizeUrl.searchParams.set("redirect_uri", REDIRECT_URI);
@@ -320,7 +364,6 @@ async function acquireAccessToken(opts) {
320
364
  authorizeUrl.searchParams.set("state", state);
321
365
  authorizeUrl.searchParams.set("code_challenge", pkce.challenge);
322
366
  authorizeUrl.searchParams.set("code_challenge_method", "S256");
323
- authorizeUrl.searchParams.set("prompt", "login");
324
367
  spinner4?.start("Waiting for browser login");
325
368
  try {
326
369
  const { code, verifier } = await captureAuthCode(
@@ -357,8 +400,13 @@ async function apiCall(url, init) {
357
400
  return body;
358
401
  }
359
402
  async function fetchTenants(apiBaseUrl, jwt) {
403
+ const fromToken = tenantsFromClaims(decodeJwtClaims(jwt));
404
+ if (fromToken.length > 0) return fromToken;
360
405
  const tenants = await apiCall(`${apiBaseUrl}/account/tenants`, {
361
- headers: { Authorization: `Bearer ${jwt}`, Accept: "application/json" }
406
+ headers: {
407
+ Authorization: `Bearer ${apiTokenFor(jwt)}`,
408
+ Accept: "application/json"
409
+ }
362
410
  });
363
411
  if (!Array.isArray(tenants)) {
364
412
  throw new Error(
@@ -367,6 +415,62 @@ async function fetchTenants(apiBaseUrl, jwt) {
367
415
  }
368
416
  return tenants;
369
417
  }
418
+ function apiTokenFor(jwt) {
419
+ const bridged = decodeJwtClaims(jwt)[ORY_TOKEN_CLAIM];
420
+ return typeof bridged === "string" && bridged ? bridged : jwt;
421
+ }
422
+ function decodeJwtClaims(jwt) {
423
+ const payload = jwt.split(".")[1];
424
+ if (!payload) return {};
425
+ try {
426
+ const json = Buffer.from(payload, "base64url").toString("utf-8");
427
+ const parsed = JSON.parse(json);
428
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
429
+ } catch {
430
+ return {};
431
+ }
432
+ }
433
+ var ORY_TOKEN_CLAIM = "urn:jtl:ory_token";
434
+ var TENANTS_CLAIM = "urn:jtl:tenants";
435
+ var TENANT_ID_CLAIM = "urn:jtl:tenant_id";
436
+ var PROJECT_ROLES_CLAIM = "urn:zitadel:iam:org:project:roles";
437
+ function tenantsFromClaims(claims) {
438
+ const tenants = /* @__PURE__ */ new Map();
439
+ const explicit = claims[TENANTS_CLAIM];
440
+ if (Array.isArray(explicit)) {
441
+ for (const entry of explicit) {
442
+ if (typeof entry !== "object" || entry === null) continue;
443
+ const { id, slug, name } = entry;
444
+ if (typeof id === "string" && id) {
445
+ tenants.set(id, {
446
+ id,
447
+ slug: slug ?? id,
448
+ name: name ?? slug ?? id
449
+ });
450
+ }
451
+ }
452
+ }
453
+ const roles = claims[PROJECT_ROLES_CLAIM];
454
+ if (tenants.size === 0 && typeof roles === "object" && roles !== null) {
455
+ for (const orgs of Object.values(roles)) {
456
+ if (typeof orgs !== "object" || orgs === null) continue;
457
+ for (const [orgId, domain] of Object.entries(
458
+ orgs
459
+ )) {
460
+ if (tenants.has(orgId)) continue;
461
+ const slug = typeof domain === "string" ? domain.split(".")[0] : orgId;
462
+ tenants.set(orgId, { id: orgId, slug, name: slug });
463
+ }
464
+ }
465
+ }
466
+ const ordered = [...tenants.values()];
467
+ const active = claims[TENANT_ID_CLAIM];
468
+ if (typeof active === "string") {
469
+ const index = ordered.findIndex((tenant) => tenant.id === active);
470
+ if (index > 0) ordered.unshift(...ordered.splice(index, 1));
471
+ }
472
+ return ordered;
473
+ }
370
474
  function validateHost(input, flag) {
371
475
  let url;
372
476
  try {
@@ -408,7 +512,7 @@ function resolveCommonOptions(values) {
408
512
  hub,
409
513
  partner,
410
514
  clientId,
411
- scope: values.scope ?? DEFAULT_SCOPE,
515
+ scope: values.scope ?? defaultScopeFor(auth),
412
516
  reauth: values.reauth ?? false,
413
517
  yes: values.yes ?? false,
414
518
  tenant: values.tenant
@@ -906,10 +1010,12 @@ async function register(argv = [], runOpts = {}) {
906
1010
  } catch (err) {
907
1011
  bail(err.message);
908
1012
  }
1013
+ const identityJwt = jwt;
1014
+ jwt = apiTokenFor(jwt);
909
1015
  spinner4.start("Fetching tenants");
910
1016
  let tenants;
911
1017
  try {
912
- tenants = await fetchTenants(opts.api, jwt);
1018
+ tenants = await fetchTenants(opts.api, identityJwt);
913
1019
  spinner4.stop(
914
1020
  `Found ${tenants.length} tenant${tenants.length === 1 ? "" : "s"}`
915
1021
  );
@@ -1188,10 +1294,12 @@ async function listing(argv = [], runOpts = {}) {
1188
1294
  } catch (err) {
1189
1295
  bail(err.message);
1190
1296
  }
1297
+ const identityJwt = jwt;
1298
+ jwt = apiTokenFor(jwt);
1191
1299
  spinner4.start("Fetching tenants");
1192
1300
  let tenants;
1193
1301
  try {
1194
- tenants = await fetchTenants(opts.api, jwt);
1302
+ tenants = await fetchTenants(opts.api, identityJwt);
1195
1303
  spinner4.stop(
1196
1304
  `Found ${tenants.length} tenant${tenants.length === 1 ? "" : "s"}`
1197
1305
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jtl-software/create-cloud-app",
3
- "version": "0.2.2",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "CLI tool for scaffolding JTL Platform cloud apps",
6
6
  "bin": {
@@ -17,20 +17,20 @@
17
17
  "dev": "tsx src/index.ts"
18
18
  },
19
19
  "dependencies": {
20
- "@clack/prompts": "^1.6.0",
20
+ "@clack/prompts": "^1.7.0",
21
21
  "@jtl-software/cloud-app-template-backend-dotnet": "0.0.12",
22
22
  "@jtl-software/cloud-app-template-backend-node": "0.0.15",
23
23
  "@jtl-software/cloud-app-template-backend-php": "0.1.1",
24
- "@jtl-software/cloud-app-template-frontend-react": "0.0.17",
24
+ "@jtl-software/cloud-app-template-frontend-react": "0.0.18",
25
25
  "@jtl-software/cloud-app-template-shared": "0.1.1",
26
26
  "picocolors": "^1.1.1",
27
- "posthog-node": "^5.45.2"
27
+ "posthog-node": "^5.48.1"
28
28
  },
29
29
  "devDependencies": {
30
- "@types/node": "^26.0.0",
31
- "msw": "^2.14.6",
30
+ "@types/node": "^26.2.0",
31
+ "msw": "^2.15.0",
32
32
  "tsup": "^8.5.1",
33
- "tsx": "^4.22.4",
33
+ "tsx": "^4.23.12",
34
34
  "typescript": "^6.0.3"
35
35
  },
36
36
  "publishConfig": {