@wtfalch/foundry 0.29.0 → 0.31.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/calls-sfu-ssh-client.d.ts +11 -0
- package/dist/calls-sfu-ssh-client.js +65 -11
- package/dist/index.d.ts +7 -5
- package/dist/index.js +4 -3
- package/dist/org.d.ts +88 -0
- package/dist/org.js +68 -0
- package/dist/plan/agora.d.ts +13 -0
- package/dist/plan/agora.js +15 -0
- package/dist/plan/chat-matrix.d.ts +31 -2
- package/dist/plan/chat-matrix.js +40 -6
- package/dist/plan/diff.d.ts +45 -2
- package/dist/plan/diff.js +162 -10
- package/dist/plan/executor.d.ts +93 -0
- package/dist/plan/executor.js +345 -9
- package/dist/plan/index.d.ts +5 -2
- package/dist/plan/index.js +3 -1
- package/dist/plan/plan-apply.js +1 -0
- package/dist/plan/platform-diff.d.ts +18 -0
- package/dist/plan/platform-diff.js +35 -5
- package/dist/plan/platform-org.d.ts +2 -0
- package/dist/plan/platform-org.js +4 -1
- package/dist/plan/platform-providers.d.ts +1 -1
- package/dist/plan/platform-providers.js +85 -23
- package/dist/plan/providers-readers.d.ts +14 -1
- package/dist/plan/providers-readers.js +25 -2
- package/dist/plan/step-system.js +1 -0
- package/dist/plan/version-status.d.ts +200 -0
- package/dist/plan/version-status.js +213 -0
- package/dist/plan/zitadel-login-policy.d.ts +23 -0
- package/dist/plan/zitadel-login-policy.js +103 -0
- package/dist/plan/zitadel-machine-user.d.ts +1 -1
- package/dist/plan/zitadel-machine-user.js +34 -0
- package/dist/platform.d.ts +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
/**
|
|
3
|
+
* Extracts every `@wtfalch/*` package's resolved version out of a pnpm
|
|
4
|
+
* lockfile's text, without parsing YAML in general.
|
|
5
|
+
*
|
|
6
|
+
* pnpm lockfile v9 (`lockfileVersion: '9.0'`, this estate's own) writes each
|
|
7
|
+
* resolved package as a two-space-indented, single-quoted
|
|
8
|
+
* `'<name>@<version>'` key in its `packages:` section, and again --
|
|
9
|
+
* peer-qualified, e.g. `'<name>@<version>(<peer>@<peer-version>...)'` -- in
|
|
10
|
+
* its `snapshots:` section. Both carry the same base version for a given
|
|
11
|
+
* package, so reading either (or both, as this does) and keeping the last
|
|
12
|
+
* value seen per package name is lossless for what this exists to compare:
|
|
13
|
+
* *which version* an app is on, never which peer graph it resolved to.
|
|
14
|
+
* Verified against this very repo's own `pnpm-lock.yaml`.
|
|
15
|
+
*/
|
|
16
|
+
export function wtfalchVersionsFromPnpmLockfile(lockfile) {
|
|
17
|
+
const PACKAGE_LINE = /^ {2}'(@wtfalch\/[^@']+)@([^'()]+)(?:\(.*\))?':$/;
|
|
18
|
+
const versions = {};
|
|
19
|
+
for (const line of lockfile.split('\n')) {
|
|
20
|
+
const match = PACKAGE_LINE.exec(line);
|
|
21
|
+
const name = match?.[1];
|
|
22
|
+
const version = match?.[2];
|
|
23
|
+
if (name !== undefined && version !== undefined) {
|
|
24
|
+
versions[name] = version;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return versions;
|
|
28
|
+
}
|
|
29
|
+
/** `pnpm-lock.yaml` at the repo root -- every repo in this estate pins with pnpm (root `CLAUDE.md`). */
|
|
30
|
+
const DEFAULT_LOCKFILE_PATH = 'pnpm-lock.yaml';
|
|
31
|
+
/**
|
|
32
|
+
* Reads every named app's deployed sha and the `@wtfalch/*` versions its
|
|
33
|
+
* lockfile pins at that sha, then flags which packages have drifted --
|
|
34
|
+
* across the named apps, or against a declared version when the caller
|
|
35
|
+
* passes one (#263's "Done when": two orgs on different `@wtfalch/auth`
|
|
36
|
+
* versions is flagged).
|
|
37
|
+
*
|
|
38
|
+
* One app's read failing (its sha cannot be found, or its lockfile fetch
|
|
39
|
+
* errors) is recorded on that app alone (`OrgAppVersionStatus.error`) and
|
|
40
|
+
* never stops another app's read, or the drift computed from whichever
|
|
41
|
+
* apps did resolve.
|
|
42
|
+
*/
|
|
43
|
+
export async function readPlatformVersionStatus(apps, ports, options = {}) {
|
|
44
|
+
const lockfilePath = options.lockfilePath ?? DEFAULT_LOCKFILE_PATH;
|
|
45
|
+
const statuses = await Promise.all(apps.map((app) => readOneAppVersionStatus(app, ports, lockfilePath)));
|
|
46
|
+
return {
|
|
47
|
+
apps: statuses,
|
|
48
|
+
packageDrift: computeWtfalchPackageDrift(statuses, options.declaredVersions ?? {}),
|
|
49
|
+
platformVersionChanges: options.platformVersionChanges
|
|
50
|
+
? [...options.platformVersionChanges]
|
|
51
|
+
: [],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
async function readOneAppVersionStatus(app, ports, lockfilePath) {
|
|
55
|
+
const base = {
|
|
56
|
+
orgId: app.orgId,
|
|
57
|
+
orgSlug: app.orgSlug,
|
|
58
|
+
entryKey: app.entryKey,
|
|
59
|
+
repo: app.repo,
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
const deployedSha = await ports.coolify.deployedSha(app.coolifyApplicationUuid);
|
|
63
|
+
if (!deployedSha) {
|
|
64
|
+
return { ...base, deployedSha: null, wtfalchVersions: null, error: null };
|
|
65
|
+
}
|
|
66
|
+
const lockfile = await ports.github.getFileContents(app.repo, lockfilePath, deployedSha);
|
|
67
|
+
if (lockfile === null) {
|
|
68
|
+
return {
|
|
69
|
+
...base,
|
|
70
|
+
deployedSha,
|
|
71
|
+
wtfalchVersions: null,
|
|
72
|
+
error: `${lockfilePath} not found in ${app.repo} at ${deployedSha}`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
...base,
|
|
77
|
+
deployedSha,
|
|
78
|
+
wtfalchVersions: wtfalchVersionsFromPnpmLockfile(lockfile),
|
|
79
|
+
error: null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return {
|
|
84
|
+
...base,
|
|
85
|
+
deployedSha: null,
|
|
86
|
+
wtfalchVersions: null,
|
|
87
|
+
error: `read failed: ${error instanceof Error ? error.name : 'unknown error'}`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function computeWtfalchPackageDrift(statuses, declaredVersions) {
|
|
92
|
+
const byPackage = new Map();
|
|
93
|
+
for (const status of statuses) {
|
|
94
|
+
if (!status.wtfalchVersions)
|
|
95
|
+
continue;
|
|
96
|
+
for (const [pkg, version] of Object.entries(status.wtfalchVersions)) {
|
|
97
|
+
const versions = byPackage.get(pkg) ?? new Map();
|
|
98
|
+
byPackage.set(pkg, versions);
|
|
99
|
+
const slugs = versions.get(version) ?? new Set();
|
|
100
|
+
versions.set(version, slugs);
|
|
101
|
+
slugs.add(status.orgSlug);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// A package only named in `declaredVersions`, with no app running it yet,
|
|
105
|
+
// is still worth a row: it is the simplest drift there is -- declared,
|
|
106
|
+
// and nobody on it.
|
|
107
|
+
for (const pkg of Object.keys(declaredVersions)) {
|
|
108
|
+
if (!byPackage.has(pkg))
|
|
109
|
+
byPackage.set(pkg, new Map());
|
|
110
|
+
}
|
|
111
|
+
const packages = [...byPackage.keys()].sort();
|
|
112
|
+
return packages.map((pkg) => {
|
|
113
|
+
const versions = byPackage.get(pkg) ?? new Map();
|
|
114
|
+
const versionsBySlug = {};
|
|
115
|
+
for (const [version, slugs] of versions) {
|
|
116
|
+
versionsBySlug[version] = [...slugs].sort();
|
|
117
|
+
}
|
|
118
|
+
const declaredVersion = declaredVersions[pkg];
|
|
119
|
+
const distinct = new Set(versions.keys());
|
|
120
|
+
if (declaredVersion !== undefined)
|
|
121
|
+
distinct.add(declaredVersion);
|
|
122
|
+
return {
|
|
123
|
+
package: pkg,
|
|
124
|
+
versionsBySlug,
|
|
125
|
+
...(declaredVersion !== undefined ? { declaredVersion } : {}),
|
|
126
|
+
drifted: distinct.size > 1,
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* The real {@link StatusCoolifyPort}, over `@wtfalch/providers/coolify`'s
|
|
132
|
+
* `CoolifyClient` (Choice A, #263's "Decide"). Narrowed to the one method
|
|
133
|
+
* this needs, the same `Pick`-shaped narrowing nothing else in this package
|
|
134
|
+
* happened to need before -- `CoolifyClient` itself is never imported as a
|
|
135
|
+
* type here, so a fake in a test needs no part of its surface this doesn't use.
|
|
136
|
+
*/
|
|
137
|
+
export function providersStatusCoolifyPort(client) {
|
|
138
|
+
return {
|
|
139
|
+
async deployedSha(applicationUuid) {
|
|
140
|
+
const deployments = await client.listApplicationDeployments(applicationUuid);
|
|
141
|
+
const finished = deployments.find((d) => d.status === 'finished');
|
|
142
|
+
return finished?.commit ?? null;
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const GH_TIMEOUT_MS = 30_000;
|
|
147
|
+
const REPO_PATTERN = /^[\w.-]+\/[\w.-]+$/;
|
|
148
|
+
const REF_PATTERN = /^[\w./-]+$/;
|
|
149
|
+
/**
|
|
150
|
+
* The default {@link GhExec}: shells out to the real `gh` CLI, exactly the
|
|
151
|
+
* dependency `@wtfalch/providers/github`'s own `GitHubClient` already takes
|
|
152
|
+
* on. Real process I/O, left untested here the same as `ssh2`'s `Client` in
|
|
153
|
+
* `scripts/calls-sfu-deploy.mjs` -- {@link ghCliStatusGithubPort}'s own
|
|
154
|
+
* decision logic (404 -> `null`, base64 decode) is what is unit-tested,
|
|
155
|
+
* against a fake `GhExec`.
|
|
156
|
+
*/
|
|
157
|
+
function spawnGh(args) {
|
|
158
|
+
return new Promise((resolve, reject) => {
|
|
159
|
+
execFile('gh', args, { maxBuffer: 32 * 1024 * 1024, timeout: GH_TIMEOUT_MS }, (error, stdout, stderr) => {
|
|
160
|
+
if (error) {
|
|
161
|
+
reject(Object.assign(error, { stdout, stderr }));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
resolve({ stdout, stderr });
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* The real {@link StatusGithubPort}, built on `gh api repos/{repo}/contents/{path}`
|
|
170
|
+
* the way GitHub's own REST API serves a file's contents at a ref --
|
|
171
|
+
* `{ content: '<base64>' }` on success, a non-zero exit with `404` in `gh`'s
|
|
172
|
+
* own error text when `path` or `ref` doesn't exist (checked against a real
|
|
173
|
+
* `gh` CLI, not GitHub's docs, matching this package's `coolify.ts`'s own
|
|
174
|
+
* "verified against a live instance, not the docs" standard for an API this
|
|
175
|
+
* cannot run against the network in CI).
|
|
176
|
+
*/
|
|
177
|
+
export function ghCliStatusGithubPort(exec = spawnGh) {
|
|
178
|
+
return {
|
|
179
|
+
async getFileContents(repo, path, ref) {
|
|
180
|
+
if (!REPO_PATTERN.test(repo) || !REF_PATTERN.test(ref) || path.includes('..')) {
|
|
181
|
+
throw new Error('invalid repo, path or ref');
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
const { stdout } = await exec([
|
|
185
|
+
'api',
|
|
186
|
+
`repos/${repo}/contents/${path}`,
|
|
187
|
+
'--method',
|
|
188
|
+
'GET',
|
|
189
|
+
'-f',
|
|
190
|
+
`ref=${ref}`,
|
|
191
|
+
'--jq',
|
|
192
|
+
'.content',
|
|
193
|
+
]);
|
|
194
|
+
const base64 = stdout.trim();
|
|
195
|
+
if (!base64 || base64 === 'null')
|
|
196
|
+
return null;
|
|
197
|
+
return Buffer.from(base64, 'base64').toString('utf8');
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
if (isGhNotFoundError(error))
|
|
201
|
+
return null;
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
function isGhNotFoundError(error) {
|
|
208
|
+
const record = error;
|
|
209
|
+
const text = [record?.message, record?.stderr]
|
|
210
|
+
.filter((value) => typeof value === 'string')
|
|
211
|
+
.join('\n');
|
|
212
|
+
return /\b404\b|not found/i.test(text);
|
|
213
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ZitadelPort } from './executor.js';
|
|
2
|
+
export interface ZitadelLoginPolicyOptions {
|
|
3
|
+
/** The ZITADEL instance's base URL. */
|
|
4
|
+
url: string;
|
|
5
|
+
/** Bearer token allowed to read and write an org's login policy. */
|
|
6
|
+
token: string;
|
|
7
|
+
fetchImpl?: typeof fetch;
|
|
8
|
+
instanceHost?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The real {@link ZitadelPort.closeSelfRegistration}: the mirror of
|
|
12
|
+
* `ZitadelClient.allowSelfRegistration` in `@wtfalch/providers`, with
|
|
13
|
+
* `allowRegister` forced off instead of on. It speaks ZITADEL's v1 management
|
|
14
|
+
* API (`GET`/`POST`/`PUT /management/v1/policies/login`, org-scoped by
|
|
15
|
+
* `x-zitadel-orgid`) and has not been run against a live instance.
|
|
16
|
+
*
|
|
17
|
+
* Neither write merges: a field left out resets to false or empty. So every
|
|
18
|
+
* setting of the policy in force is copied back and only `allowRegister`
|
|
19
|
+
* changes. An org that inherits the instance default always gets its own
|
|
20
|
+
* policy (POST), so a later change to the default cannot turn registration
|
|
21
|
+
* back on. A custom policy keeps its own factors and IdPs (PUT).
|
|
22
|
+
*/
|
|
23
|
+
export declare function createZitadelLoginPolicy(options: ZitadelLoginPolicyOptions): Pick<ZitadelPort, 'closeSelfRegistration'>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ZitadelHttpError } from './zitadel-machine-user.js';
|
|
2
|
+
/** The settings both AddCustomLoginPolicy and UpdateCustomLoginPolicy accept. */
|
|
3
|
+
const LOGIN_POLICY_SETTINGS = [
|
|
4
|
+
'allowUsernamePassword',
|
|
5
|
+
'allowRegister',
|
|
6
|
+
'allowExternalIdp',
|
|
7
|
+
'forceMfa',
|
|
8
|
+
'passwordlessType',
|
|
9
|
+
'hidePasswordReset',
|
|
10
|
+
'ignoreUnknownUsernames',
|
|
11
|
+
'defaultRedirectUri',
|
|
12
|
+
'passwordCheckLifetime',
|
|
13
|
+
'externalLoginCheckLifetime',
|
|
14
|
+
'mfaInitSkipLifetime',
|
|
15
|
+
'secondFactorCheckLifetime',
|
|
16
|
+
'multiFactorCheckLifetime',
|
|
17
|
+
'allowDomainDiscovery',
|
|
18
|
+
'disableLoginWithEmail',
|
|
19
|
+
'disableLoginWithPhone',
|
|
20
|
+
'forceMfaLocalOnly',
|
|
21
|
+
];
|
|
22
|
+
const CALL_TIMEOUT_MS = 30_000;
|
|
23
|
+
/**
|
|
24
|
+
* The real {@link ZitadelPort.closeSelfRegistration}: the mirror of
|
|
25
|
+
* `ZitadelClient.allowSelfRegistration` in `@wtfalch/providers`, with
|
|
26
|
+
* `allowRegister` forced off instead of on. It speaks ZITADEL's v1 management
|
|
27
|
+
* API (`GET`/`POST`/`PUT /management/v1/policies/login`, org-scoped by
|
|
28
|
+
* `x-zitadel-orgid`) and has not been run against a live instance.
|
|
29
|
+
*
|
|
30
|
+
* Neither write merges: a field left out resets to false or empty. So every
|
|
31
|
+
* setting of the policy in force is copied back and only `allowRegister`
|
|
32
|
+
* changes. An org that inherits the instance default always gets its own
|
|
33
|
+
* policy (POST), so a later change to the default cannot turn registration
|
|
34
|
+
* back on. A custom policy keeps its own factors and IdPs (PUT).
|
|
35
|
+
*/
|
|
36
|
+
export function createZitadelLoginPolicy(options) {
|
|
37
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
38
|
+
async function call(method, orgId, body) {
|
|
39
|
+
const path = '/management/v1/policies/login';
|
|
40
|
+
const response = await fetchImpl(`${options.url}${path}`, {
|
|
41
|
+
method,
|
|
42
|
+
signal: AbortSignal.timeout(CALL_TIMEOUT_MS),
|
|
43
|
+
headers: {
|
|
44
|
+
authorization: `Bearer ${options.token}`,
|
|
45
|
+
'content-type': 'application/json',
|
|
46
|
+
'x-zitadel-orgid': orgId,
|
|
47
|
+
...(options.instanceHost
|
|
48
|
+
? {
|
|
49
|
+
'x-zitadel-instance-host': options.instanceHost,
|
|
50
|
+
'x-forwarded-host': options.instanceHost,
|
|
51
|
+
'x-forwarded-proto': 'https',
|
|
52
|
+
}
|
|
53
|
+
: {}),
|
|
54
|
+
},
|
|
55
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
56
|
+
});
|
|
57
|
+
const text = await response.text();
|
|
58
|
+
let data = {};
|
|
59
|
+
try {
|
|
60
|
+
data = text ? JSON.parse(text) : {};
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new ZitadelHttpError(response.status, `non-JSON response from ${path}`);
|
|
64
|
+
}
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw new ZitadelHttpError(response.status, String(data.message ?? response.statusText), typeof data.code === 'number' ? data.code : undefined);
|
|
67
|
+
}
|
|
68
|
+
return data;
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
async closeSelfRegistration(orgId) {
|
|
72
|
+
if (!orgId)
|
|
73
|
+
throw new Error('closeSelfRegistration: orgId is required');
|
|
74
|
+
const data = await call('GET', orgId, undefined);
|
|
75
|
+
const policy = data.policy ?? {};
|
|
76
|
+
const isDefault = data.isDefault === true || policy.isDefault === true;
|
|
77
|
+
if (!isDefault && policy.allowRegister === false) {
|
|
78
|
+
return { changed: false };
|
|
79
|
+
}
|
|
80
|
+
const settings = {};
|
|
81
|
+
for (const key of LOGIN_POLICY_SETTINGS) {
|
|
82
|
+
if (policy[key] !== undefined)
|
|
83
|
+
settings[key] = policy[key];
|
|
84
|
+
}
|
|
85
|
+
settings.allowRegister = false;
|
|
86
|
+
if (isDefault) {
|
|
87
|
+
await call('POST', orgId, {
|
|
88
|
+
...settings,
|
|
89
|
+
secondFactors: policy.secondFactors ?? [],
|
|
90
|
+
multiFactors: policy.multiFactors ?? [],
|
|
91
|
+
idps: (policy.idps ?? []).map((idp) => ({
|
|
92
|
+
idpId: idp.idpId,
|
|
93
|
+
ownerType: 'IDP_OWNER_TYPE_SYSTEM',
|
|
94
|
+
})),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
await call('PUT', orgId, settings);
|
|
99
|
+
}
|
|
100
|
+
return { changed: true };
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -84,4 +84,4 @@ export interface ZitadelMinterOptions {
|
|
|
84
84
|
fetchImpl?: typeof fetch;
|
|
85
85
|
instanceHost?: string;
|
|
86
86
|
}
|
|
87
|
-
export declare function createZitadelAdminMinter(options: ZitadelMinterOptions): Pick<ZitadelPort, 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats'>;
|
|
87
|
+
export declare function createZitadelAdminMinter(options: ZitadelMinterOptions): Pick<ZitadelPort, 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats' | 'reserveUserName'>;
|
|
@@ -230,6 +230,39 @@ export function createZitadelAdminMinter(options) {
|
|
|
230
230
|
}
|
|
231
231
|
return { userId, token: pat.token, patId: patId ?? '', revokedPriorPat };
|
|
232
232
|
}
|
|
233
|
+
/**
|
|
234
|
+
* Find-or-create only: the first two steps of the sequence above, with no
|
|
235
|
+
* IAM role grant and no PAT. The user holds no secret; it exists so
|
|
236
|
+
* ZITADEL's per-org username uniqueness refuses the name to anyone later.
|
|
237
|
+
*/
|
|
238
|
+
async function reserveUserName(orgId, userName) {
|
|
239
|
+
if (!orgId || !userName.trim()) {
|
|
240
|
+
throw new Error('reserveUserName: orgId and userName are required');
|
|
241
|
+
}
|
|
242
|
+
const found = await call('POST', '/management/v1/users/_search', {
|
|
243
|
+
queries: [
|
|
244
|
+
{
|
|
245
|
+
userNameQuery: {
|
|
246
|
+
userName,
|
|
247
|
+
method: 'TEXT_QUERY_METHOD_EQUALS_IGNORE_CASE',
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
],
|
|
251
|
+
}, orgId);
|
|
252
|
+
const existing = found.result?.[0]?.id;
|
|
253
|
+
if (existing)
|
|
254
|
+
return { userId: existing };
|
|
255
|
+
const created = await call('POST', '/management/v1/users/machine', {
|
|
256
|
+
userName,
|
|
257
|
+
name: `Foundry (reserved: ${userName})`,
|
|
258
|
+
accessTokenType: 'ACCESS_TOKEN_TYPE_BEARER',
|
|
259
|
+
}, orgId);
|
|
260
|
+
const userId = typeof created.userId === 'string' ? created.userId : undefined;
|
|
261
|
+
if (!userId) {
|
|
262
|
+
throw new Error(`ZITADEL did not return the machine user id reserving username ${userName}`);
|
|
263
|
+
}
|
|
264
|
+
return { userId };
|
|
265
|
+
}
|
|
233
266
|
return {
|
|
234
267
|
mintAdminMachineUser: (orgId, input) => mintMachineUser(orgId, input, 'IAM_OWNER').then(({ userId, token }) => ({ userId, token })),
|
|
235
268
|
mintLoginClientMachineUser: (orgId, input) => mintMachineUser(orgId, input, 'IAM_LOGIN_CLIENT'),
|
|
@@ -238,5 +271,6 @@ export function createZitadelAdminMinter(options) {
|
|
|
238
271
|
// the user -- see `ZitadelPort.listLoginClientPats`'s own doc comment for
|
|
239
272
|
// the race this closes.
|
|
240
273
|
listLoginClientPats: (orgId, userId) => listPats(orgId, userId),
|
|
274
|
+
reserveUserName,
|
|
241
275
|
};
|
|
242
276
|
}
|
package/dist/platform.d.ts
CHANGED
|
@@ -95,7 +95,7 @@ export interface DeclaredPlatform {
|
|
|
95
95
|
/**
|
|
96
96
|
* #540: a self-hosted heartbeat server (Healthchecks) for the platform,
|
|
97
97
|
* for now -- whether it stays the platform's heartbeat server long-term,
|
|
98
|
-
* instead of or alongside the hand-set-up
|
|
98
|
+
* instead of or alongside the hand-set-up heartbeat instance, and
|
|
99
99
|
* what happens to that instance's own data, are both still open "Decide"
|
|
100
100
|
* items on issue #540's own thread, not settled by this field. `gitRef`
|
|
101
101
|
* is the Docker image tag it runs (the same overload `mail.gitRef`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/foundry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.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",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"typecheck": "tsc --noEmit",
|
|
35
35
|
"test": "vitest run",
|
|
36
36
|
"db:provision": "node scripts/foundry-db-provision.mjs",
|
|
37
|
-
"calls-sfu:deploy": "node scripts/calls-sfu-deploy.mjs"
|
|
37
|
+
"calls-sfu:deploy": "node scripts/calls-sfu-deploy.mjs",
|
|
38
|
+
"status": "node scripts/status.mjs"
|
|
38
39
|
},
|
|
39
40
|
"peerDependencies": {
|
|
40
41
|
"@wtfalch/providers": ">=0.7.4",
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
"@wtfalch/audit": "0.6.0",
|
|
45
46
|
"@wtfalch/auth": "0.16.0",
|
|
46
47
|
"@wtfalch/edge": "0.12.0",
|
|
47
|
-
"@wtfalch/email": "^0.
|
|
48
|
+
"@wtfalch/email": "^0.20.0",
|
|
48
49
|
"@wtfalch/providers": "0.7.5",
|
|
49
50
|
"ssh2": "1.17.0",
|
|
50
51
|
"zod": "4.6.5"
|