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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +124 -15
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -23,16 +23,33 @@ 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
+ scopes.push("urn:zitadel:iam:org:project:id:zitadel:aud");
50
+ }
51
+ return scopes.join(" ");
52
+ }
36
53
  var TOKEN_EXPIRY_MARGIN_S = 60;
37
54
  function tokenCacheFile() {
38
55
  return process.env.JTL_CLI_TOKEN_CACHE_FILE ?? path.join(os.homedir(), ".config", "jtl-cli", "tokens.json");
@@ -237,8 +254,33 @@ Opening browser to sign in.`);
237
254
  });
238
255
  });
239
256
  }
240
- async function postTokenRequest(authBaseUrl, body) {
241
- const res = await fetch(`${authBaseUrl}/oauth2/token`, {
257
+ var oidcMetadataCache = /* @__PURE__ */ new Map();
258
+ async function discoverOidc(authBaseUrl) {
259
+ const cached = oidcMetadataCache.get(authBaseUrl);
260
+ if (cached) return cached;
261
+ const pending = (async () => {
262
+ const url = `${authBaseUrl}/.well-known/openid-configuration`;
263
+ const res = await fetch(url, { headers: { Accept: "application/json" } });
264
+ if (!res.ok) {
265
+ throw new Error(`OIDC discovery failed: ${url} returned ${res.status}`);
266
+ }
267
+ const doc = await res.json();
268
+ if (!doc.authorization_endpoint || !doc.token_endpoint) {
269
+ throw new Error(
270
+ `OIDC discovery at ${url} is missing authorization_endpoint or token_endpoint`
271
+ );
272
+ }
273
+ return {
274
+ authorization_endpoint: doc.authorization_endpoint,
275
+ token_endpoint: doc.token_endpoint
276
+ };
277
+ })();
278
+ oidcMetadataCache.set(authBaseUrl, pending);
279
+ pending.catch(() => oidcMetadataCache.delete(authBaseUrl));
280
+ return pending;
281
+ }
282
+ async function postTokenRequest(tokenEndpoint, body) {
283
+ const res = await fetch(tokenEndpoint, {
242
284
  method: "POST",
243
285
  headers: {
244
286
  "Content-Type": "application/x-www-form-urlencoded",
@@ -262,8 +304,9 @@ async function postTokenRequest(authBaseUrl, body) {
262
304
  return parsed;
263
305
  }
264
306
  async function exchangeCodeForToken(authBaseUrl, clientId, code, verifier) {
307
+ const { token_endpoint } = await discoverOidc(authBaseUrl);
265
308
  return postTokenRequest(
266
- authBaseUrl,
309
+ token_endpoint,
267
310
  new URLSearchParams({
268
311
  grant_type: "authorization_code",
269
312
  code,
@@ -274,8 +317,9 @@ async function exchangeCodeForToken(authBaseUrl, clientId, code, verifier) {
274
317
  );
275
318
  }
276
319
  async function refreshAccessToken(authBaseUrl, clientId, refreshToken) {
320
+ const { token_endpoint } = await discoverOidc(authBaseUrl);
277
321
  return postTokenRequest(
278
- authBaseUrl,
322
+ token_endpoint,
279
323
  new URLSearchParams({
280
324
  grant_type: "refresh_token",
281
325
  refresh_token: refreshToken,
@@ -312,7 +356,8 @@ async function acquireAccessToken(opts) {
312
356
  }
313
357
  const pkce = generatePkce();
314
358
  const state = base64url(crypto.randomBytes(16));
315
- const authorizeUrl = new URL("/oauth2/auth", auth);
359
+ const { authorization_endpoint } = await discoverOidc(auth);
360
+ const authorizeUrl = new URL(authorization_endpoint);
316
361
  authorizeUrl.searchParams.set("response_type", "code");
317
362
  authorizeUrl.searchParams.set("client_id", clientId);
318
363
  authorizeUrl.searchParams.set("redirect_uri", REDIRECT_URI);
@@ -320,7 +365,6 @@ async function acquireAccessToken(opts) {
320
365
  authorizeUrl.searchParams.set("state", state);
321
366
  authorizeUrl.searchParams.set("code_challenge", pkce.challenge);
322
367
  authorizeUrl.searchParams.set("code_challenge_method", "S256");
323
- authorizeUrl.searchParams.set("prompt", "login");
324
368
  spinner4?.start("Waiting for browser login");
325
369
  try {
326
370
  const { code, verifier } = await captureAuthCode(
@@ -357,8 +401,13 @@ async function apiCall(url, init) {
357
401
  return body;
358
402
  }
359
403
  async function fetchTenants(apiBaseUrl, jwt) {
404
+ const fromToken = tenantsFromClaims(decodeJwtClaims(jwt));
405
+ if (fromToken.length > 0) return fromToken;
360
406
  const tenants = await apiCall(`${apiBaseUrl}/account/tenants`, {
361
- headers: { Authorization: `Bearer ${jwt}`, Accept: "application/json" }
407
+ headers: {
408
+ Authorization: `Bearer ${apiTokenFor(jwt)}`,
409
+ Accept: "application/json"
410
+ }
362
411
  });
363
412
  if (!Array.isArray(tenants)) {
364
413
  throw new Error(
@@ -367,6 +416,62 @@ async function fetchTenants(apiBaseUrl, jwt) {
367
416
  }
368
417
  return tenants;
369
418
  }
419
+ function apiTokenFor(jwt) {
420
+ const bridged = decodeJwtClaims(jwt)[ORY_TOKEN_CLAIM];
421
+ return typeof bridged === "string" && bridged ? bridged : jwt;
422
+ }
423
+ function decodeJwtClaims(jwt) {
424
+ const payload = jwt.split(".")[1];
425
+ if (!payload) return {};
426
+ try {
427
+ const json = Buffer.from(payload, "base64url").toString("utf-8");
428
+ const parsed = JSON.parse(json);
429
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
430
+ } catch {
431
+ return {};
432
+ }
433
+ }
434
+ var ORY_TOKEN_CLAIM = "urn:jtl:ory_token";
435
+ var TENANTS_CLAIM = "urn:jtl:tenants";
436
+ var TENANT_ID_CLAIM = "urn:jtl:tenant_id";
437
+ var PROJECT_ROLES_CLAIM = "urn:zitadel:iam:org:project:roles";
438
+ function tenantsFromClaims(claims) {
439
+ const tenants = /* @__PURE__ */ new Map();
440
+ const explicit = claims[TENANTS_CLAIM];
441
+ if (Array.isArray(explicit)) {
442
+ for (const entry of explicit) {
443
+ if (typeof entry !== "object" || entry === null) continue;
444
+ const { id, slug, name } = entry;
445
+ if (typeof id === "string" && id) {
446
+ tenants.set(id, {
447
+ id,
448
+ slug: slug ?? id,
449
+ name: name ?? slug ?? id
450
+ });
451
+ }
452
+ }
453
+ }
454
+ const roles = claims[PROJECT_ROLES_CLAIM];
455
+ if (tenants.size === 0 && typeof roles === "object" && roles !== null) {
456
+ for (const orgs of Object.values(roles)) {
457
+ if (typeof orgs !== "object" || orgs === null) continue;
458
+ for (const [orgId, domain] of Object.entries(
459
+ orgs
460
+ )) {
461
+ if (tenants.has(orgId)) continue;
462
+ const slug = typeof domain === "string" ? domain.split(".")[0] : orgId;
463
+ tenants.set(orgId, { id: orgId, slug, name: slug });
464
+ }
465
+ }
466
+ }
467
+ const ordered = [...tenants.values()];
468
+ const active = claims[TENANT_ID_CLAIM];
469
+ if (typeof active === "string") {
470
+ const index = ordered.findIndex((tenant) => tenant.id === active);
471
+ if (index > 0) ordered.unshift(...ordered.splice(index, 1));
472
+ }
473
+ return ordered;
474
+ }
370
475
  function validateHost(input, flag) {
371
476
  let url;
372
477
  try {
@@ -408,7 +513,7 @@ function resolveCommonOptions(values) {
408
513
  hub,
409
514
  partner,
410
515
  clientId,
411
- scope: values.scope ?? DEFAULT_SCOPE,
516
+ scope: values.scope ?? defaultScopeFor(auth),
412
517
  reauth: values.reauth ?? false,
413
518
  yes: values.yes ?? false,
414
519
  tenant: values.tenant
@@ -906,10 +1011,12 @@ async function register(argv = [], runOpts = {}) {
906
1011
  } catch (err) {
907
1012
  bail(err.message);
908
1013
  }
1014
+ const identityJwt = jwt;
1015
+ jwt = apiTokenFor(jwt);
909
1016
  spinner4.start("Fetching tenants");
910
1017
  let tenants;
911
1018
  try {
912
- tenants = await fetchTenants(opts.api, jwt);
1019
+ tenants = await fetchTenants(opts.api, identityJwt);
913
1020
  spinner4.stop(
914
1021
  `Found ${tenants.length} tenant${tenants.length === 1 ? "" : "s"}`
915
1022
  );
@@ -1188,10 +1295,12 @@ async function listing(argv = [], runOpts = {}) {
1188
1295
  } catch (err) {
1189
1296
  bail(err.message);
1190
1297
  }
1298
+ const identityJwt = jwt;
1299
+ jwt = apiTokenFor(jwt);
1191
1300
  spinner4.start("Fetching tenants");
1192
1301
  let tenants;
1193
1302
  try {
1194
- tenants = await fetchTenants(opts.api, jwt);
1303
+ tenants = await fetchTenants(opts.api, identityJwt);
1195
1304
  spinner4.stop(
1196
1305
  `Found ${tenants.length} tenant${tenants.length === 1 ? "" : "s"}`
1197
1306
  );
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.0",
4
4
  "type": "module",
5
5
  "description": "CLI tool for scaffolding JTL Platform cloud apps",
6
6
  "bin": {
@@ -21,7 +21,7 @@
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
27
  "posthog-node": "^5.45.2"