@jtl-software/create-cloud-app 0.2.1 → 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.
- package/dist/index.js +124 -15
- package/package.json +3 -3
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://
|
|
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
|
+
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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: {
|
|
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 ??
|
|
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,
|
|
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,
|
|
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.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI tool for scaffolding JTL Platform cloud apps",
|
|
6
6
|
"bin": {
|
|
@@ -21,8 +21,8 @@
|
|
|
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.
|
|
25
|
-
"@jtl-software/cloud-app-template-shared": "0.1.
|
|
24
|
+
"@jtl-software/cloud-app-template-frontend-react": "0.0.18",
|
|
25
|
+
"@jtl-software/cloud-app-template-shared": "0.1.1",
|
|
26
26
|
"picocolors": "^1.1.1",
|
|
27
27
|
"posthog-node": "^5.45.2"
|
|
28
28
|
},
|