@wtfalch/foundry 0.71.0 → 0.72.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.d.ts +1 -1
- package/dist/modules/sign-in.js +81 -9
- package/dist/modules/types.d.ts +2 -0
- package/dist/org.d.ts +18 -1
- package/dist/org.js +40 -0
- package/dist/plan/boule.d.ts +15 -0
- package/dist/plan/boule.js +13 -0
- package/dist/plan/broker-registrar-token.d.ts +69 -0
- package/dist/plan/broker-registrar-token.js +215 -0
- package/dist/plan/broker-registrar.d.ts +2 -0
- package/dist/plan/broker-registrar.js +5 -1
- package/dist/plan/diff.d.ts +13 -0
- package/dist/plan/diff.js +26 -5
- package/dist/plan/executor.d.ts +66 -0
- package/dist/plan/executor.js +136 -8
- package/dist/plan/health-check-settings.d.ts +13 -0
- package/dist/plan/health-check-settings.js +17 -0
- package/dist/plan/own-diff.d.ts +32 -0
- package/dist/plan/own-diff.js +67 -10
- package/dist/plan/own-hostnames.d.ts +8 -0
- package/dist/plan/own-hostnames.js +14 -0
- package/dist/plan/own-services.d.ts +7 -0
- package/dist/plan/own-services.js +2 -0
- package/dist/plan/plan-apply.d.ts +15 -0
- package/dist/plan/plan-apply.js +21 -0
- package/dist/plan/platform-diff.d.ts +9 -5
- package/dist/plan/platform-diff.js +16 -10
- package/dist/plan/platform-providers.d.ts +24 -2
- package/dist/plan/platform-providers.js +155 -12
- package/dist/plan/signin-mail.d.ts +18 -5
- package/dist/plan/signin-mail.js +14 -6
- package/dist/plan/zitadel-login-policy.d.ts +1 -1
- package/dist/plan/zitadel-login-policy.js +72 -46
- package/dist/plan/zitadel-machine-user.d.ts +14 -1
- package/dist/plan/zitadel-machine-user.js +96 -13
- package/package.json +3 -3
|
@@ -86,6 +86,14 @@ export class ZitadelLoginClientRoleConflictError extends Error {
|
|
|
86
86
|
}
|
|
87
87
|
/** The read-only organisation role a directory reader holds. It lists the organisation's users and changes nothing. */
|
|
88
88
|
export const ORG_DIRECTORY_READER_ROLE = 'ORG_OWNER_VIEWER';
|
|
89
|
+
/**
|
|
90
|
+
* Boule removes a customer with this credential (boule#495). ZITADEL's narrowest built-in organisation role
|
|
91
|
+
* that holds `user.delete` is `ORG_USER_MANAGER`; it cannot be cut down to delete alone (it also lets the
|
|
92
|
+
* holder change users in that organisation). It is granted in the customers organisation only, never in the
|
|
93
|
+
* company's.
|
|
94
|
+
*/
|
|
95
|
+
export const ORG_DIRECTORY_USER_MANAGER_ROLE = 'ORG_USER_MANAGER';
|
|
96
|
+
const ORG_DIRECTORY_ROLES = [ORG_DIRECTORY_READER_ROLE, ORG_DIRECTORY_USER_MANAGER_ROLE];
|
|
89
97
|
/**
|
|
90
98
|
* C3: thrown by `mintOrgDirectoryReader` when the user already exists with an organisation role other
|
|
91
99
|
* than the read-only one. That user was not made by foundry; granting it more or handing out its PAT
|
|
@@ -95,12 +103,20 @@ export class ZitadelDirectoryRoleConflictError extends Error {
|
|
|
95
103
|
userId;
|
|
96
104
|
roles;
|
|
97
105
|
constructor(userId, roles) {
|
|
98
|
-
super(`ZITADEL machine user ${userId} already exists with organisation role(s) [${roles.join(', ') || 'none'}], not only [${
|
|
106
|
+
super(`ZITADEL machine user ${userId} already exists with organisation role(s) [${roles.join(', ') || 'none'}], not only [${ORG_DIRECTORY_ROLES.join(', ')}] -- refusing to reuse it as a directory reader`);
|
|
99
107
|
this.userId = userId;
|
|
100
108
|
this.roles = roles;
|
|
101
109
|
this.name = 'ZitadelDirectoryRoleConflictError';
|
|
102
110
|
}
|
|
103
111
|
}
|
|
112
|
+
/** Each call to ZITADEL gives up after this long. */
|
|
113
|
+
const CALL_TIMEOUT_MS = 30_000;
|
|
114
|
+
/**
|
|
115
|
+
* The organisation role of the broker's registrar user in each organisation it serves: it may make
|
|
116
|
+
* projects there, and a project it makes it owns. No member, user or login-policy rights, which
|
|
117
|
+
* `ORG_OWNER` would add.
|
|
118
|
+
*/
|
|
119
|
+
export const REGISTRAR_ROLE = 'ORG_PROJECT_CREATOR';
|
|
104
120
|
export function createZitadelAdminMinter(options) {
|
|
105
121
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
106
122
|
async function call(method, path, body, orgId) {
|
|
@@ -119,6 +135,7 @@ export function createZitadelAdminMinter(options) {
|
|
|
119
135
|
...(orgId ? { 'x-zitadel-orgid': orgId } : {}),
|
|
120
136
|
},
|
|
121
137
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
138
|
+
signal: AbortSignal.timeout(CALL_TIMEOUT_MS),
|
|
122
139
|
});
|
|
123
140
|
const text = await response.text();
|
|
124
141
|
let data = {};
|
|
@@ -306,21 +323,12 @@ export function createZitadelAdminMinter(options) {
|
|
|
306
323
|
if (!userId)
|
|
307
324
|
throw new Error('ZITADEL did not return the machine user id');
|
|
308
325
|
if (existing) {
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
.filter((m) => m.userId === userId)
|
|
312
|
-
.flatMap((m) => m.roles ?? []);
|
|
313
|
-
if (roles.some((role) => role !== ORG_DIRECTORY_READER_ROLE)) {
|
|
326
|
+
const roles = await memberRoles(orgId, userId);
|
|
327
|
+
if (roles.some((role) => !ORG_DIRECTORY_ROLES.includes(role))) {
|
|
314
328
|
throw new ZitadelDirectoryRoleConflictError(userId, roles);
|
|
315
329
|
}
|
|
316
330
|
}
|
|
317
|
-
|
|
318
|
-
await call('POST', '/management/v1/orgs/me/members', { userId, roles: [ORG_DIRECTORY_READER_ROLE] }, orgId);
|
|
319
|
-
}
|
|
320
|
-
catch (e) {
|
|
321
|
-
if (!(e instanceof ZitadelHttpError && e.status === 409))
|
|
322
|
-
throw e;
|
|
323
|
-
}
|
|
331
|
+
await ensureOrgDirectoryRoles(orgId, userId);
|
|
324
332
|
await revokeAllPats(orgId, userId);
|
|
325
333
|
const pat = await call('POST', `/management/v1/users/${userId}/pats`, {}, orgId);
|
|
326
334
|
const patId = typeof pat.tokenId === 'string' && pat.tokenId !== '' ? pat.tokenId : undefined;
|
|
@@ -329,6 +337,78 @@ export function createZitadelAdminMinter(options) {
|
|
|
329
337
|
}
|
|
330
338
|
return { userId, token: pat.token, patId };
|
|
331
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* The broker's registrar user: a machine user of its own in `orgIds[0]` (found by `userName`, else made)
|
|
342
|
+
* holding {@link REGISTRAR_ROLE} in every org of `orgIds`, each grant added only when the org does not
|
|
343
|
+
* have it yet (a 409 is the same). Mints no token, so it is safe to repeat whenever an org joins the
|
|
344
|
+
* list. Calls follow ZITADEL's published v1 management API and have not been run against a live
|
|
345
|
+
* instance.
|
|
346
|
+
*/
|
|
347
|
+
async function ensureRegistrarUser(orgIds, input) {
|
|
348
|
+
const home = orgIds[0];
|
|
349
|
+
if (!home)
|
|
350
|
+
throw new Error('the registrar user needs at least one organisation');
|
|
351
|
+
const found = await call('POST', '/management/v1/users/_search', {
|
|
352
|
+
queries: [
|
|
353
|
+
{ userNameQuery: { userName: input.userName, method: 'TEXT_QUERY_METHOD_EQUALS' } },
|
|
354
|
+
],
|
|
355
|
+
}, home);
|
|
356
|
+
let userId = found.result?.[0]?.id;
|
|
357
|
+
if (!userId) {
|
|
358
|
+
const created = await call('POST', '/management/v1/users/machine', {
|
|
359
|
+
userName: input.userName,
|
|
360
|
+
name: input.name,
|
|
361
|
+
accessTokenType: 'ACCESS_TOKEN_TYPE_BEARER',
|
|
362
|
+
}, home);
|
|
363
|
+
userId = typeof created.userId === 'string' ? created.userId : undefined;
|
|
364
|
+
}
|
|
365
|
+
if (!userId)
|
|
366
|
+
throw new Error('ZITADEL did not return the machine user id');
|
|
367
|
+
for (const orgId of new Set(orgIds)) {
|
|
368
|
+
try {
|
|
369
|
+
await call('POST', '/management/v1/orgs/me/members', { userId, roles: [REGISTRAR_ROLE] }, orgId);
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
if (!(e instanceof ZitadelHttpError && e.status === 409))
|
|
373
|
+
throw e;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return { userId };
|
|
377
|
+
}
|
|
378
|
+
/** A fresh token for the registrar user; every token it holds is revoked first, so one stays live. */
|
|
379
|
+
async function mintRegistrarToken(homeOrgId, userId) {
|
|
380
|
+
await revokeAllPats(homeOrgId, userId);
|
|
381
|
+
const pat = await call('POST', `/management/v1/users/${userId}/pats`, {}, homeOrgId);
|
|
382
|
+
const patId = typeof pat.tokenId === 'string' && pat.tokenId !== '' ? pat.tokenId : undefined;
|
|
383
|
+
if (typeof pat.token !== 'string' || pat.token === '' || !patId) {
|
|
384
|
+
throw new Error(`ZITADEL returned no token for machine user ${userId}`);
|
|
385
|
+
}
|
|
386
|
+
return { token: pat.token, patId };
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* The directory user's roles in one organisation: {@link ORG_DIRECTORY_ROLES}, set whether it is a member
|
|
390
|
+
* yet or not (a 409 on the add means it is, and the roles are then replaced with a `PUT`). Calls follow
|
|
391
|
+
* ZITADEL's published v1 management API and have not been run against a live instance.
|
|
392
|
+
*/
|
|
393
|
+
async function ensureOrgDirectoryRoles(orgId, userId) {
|
|
394
|
+
try {
|
|
395
|
+
await call('POST', '/management/v1/orgs/me/members', { userId, roles: ORG_DIRECTORY_ROLES }, orgId);
|
|
396
|
+
}
|
|
397
|
+
catch (e) {
|
|
398
|
+
if (!(e instanceof ZitadelHttpError && e.status === 409))
|
|
399
|
+
throw e;
|
|
400
|
+
// The PUT replaces every role, so the roles a person added stay: ours join the existing set.
|
|
401
|
+
const existing = await memberRoles(orgId, userId);
|
|
402
|
+
await call('PUT', `/management/v1/orgs/me/members/${userId}`, { roles: [...existing, ...ORG_DIRECTORY_ROLES.filter((r) => !existing.includes(r))] }, orgId);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
/** The organisation roles one user holds in an organisation. */
|
|
406
|
+
async function memberRoles(orgId, userId) {
|
|
407
|
+
const members = await call('POST', '/management/v1/orgs/me/members/_search', { queries: [{ userIdQuery: { userId } }] }, orgId);
|
|
408
|
+
return (members.result ?? [])
|
|
409
|
+
.filter((m) => m.userId === userId)
|
|
410
|
+
.flatMap((m) => m.roles ?? []);
|
|
411
|
+
}
|
|
332
412
|
/** Deletes every PAT of a user; a PAT already gone (404) is the outcome wanted. */
|
|
333
413
|
async function revokeAllPats(orgId, userId) {
|
|
334
414
|
for (const patId of await listPats(orgId, userId)) {
|
|
@@ -342,7 +422,10 @@ export function createZitadelAdminMinter(options) {
|
|
|
342
422
|
}
|
|
343
423
|
}
|
|
344
424
|
return {
|
|
425
|
+
ensureRegistrarUser,
|
|
426
|
+
mintRegistrarToken,
|
|
345
427
|
mintOrgDirectoryReader,
|
|
428
|
+
ensureOrgDirectoryRoles,
|
|
346
429
|
revokeOrgDirectoryReader: revokeAllPats,
|
|
347
430
|
mintAdminMachineUser: (orgId, input) => mintMachineUser(orgId, input, 'IAM_OWNER').then(({ userId, token }) => ({ userId, token })),
|
|
348
431
|
mintLoginClientMachineUser: (orgId, input) => mintMachineUser(orgId, input, 'IAM_LOGIN_CLIENT'),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/foundry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.72.0",
|
|
4
4
|
"description": "Foundry's SDK: the declared org's types, the plan and apply contracts, and the state store's interface.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"status": "node scripts/status.mjs"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@wtfalch/providers": ">=0.
|
|
57
|
+
"@wtfalch/providers": ">=0.16.0",
|
|
58
58
|
"drizzle-orm": ">=0.45.2"
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"@wtfalch/auth": "^0.26.0",
|
|
64
64
|
"@wtfalch/edge": "0.23.0",
|
|
65
65
|
"@wtfalch/email": "^0.20.0",
|
|
66
|
-
"@wtfalch/providers": "0.
|
|
66
|
+
"@wtfalch/providers": "0.16.0",
|
|
67
67
|
"bcryptjs": "3.0.3",
|
|
68
68
|
"ssh2": "1.17.0",
|
|
69
69
|
"zod": "4.6.5"
|