@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.
- package/dist/index.js +123 -15
- 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://
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: {
|
|
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 ??
|
|
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,
|
|
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
27
|
+
"posthog-node": "^5.48.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@types/node": "^26.
|
|
31
|
-
"msw": "^2.
|
|
30
|
+
"@types/node": "^26.2.0",
|
|
31
|
+
"msw": "^2.15.0",
|
|
32
32
|
"tsup": "^8.5.1",
|
|
33
|
-
"tsx": "^4.
|
|
33
|
+
"tsx": "^4.23.12",
|
|
34
34
|
"typescript": "^6.0.3"
|
|
35
35
|
},
|
|
36
36
|
"publishConfig": {
|