@wtfalch/foundry 0.4.0 → 0.6.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/bootstrap/bootstrap.d.ts +111 -0
- package/dist/bootstrap/bootstrap.js +323 -0
- package/dist/bootstrap/declare.d.ts +36 -0
- package/dist/bootstrap/declare.js +99 -0
- package/dist/bootstrap/drive.d.ts +40 -0
- package/dist/bootstrap/drive.js +51 -0
- package/dist/bootstrap/index.d.ts +3 -0
- package/dist/bootstrap/index.js +1 -0
- package/dist/calls-sfu-deploy-support.d.ts +17 -6
- package/dist/calls-sfu-deploy-support.js +19 -7
- package/dist/calls-sfu-keys.d.ts +21 -0
- package/dist/calls-sfu-keys.js +27 -0
- package/dist/calls-sfu-ssh-adapter.d.ts +55 -0
- package/dist/calls-sfu-ssh-adapter.js +78 -0
- package/dist/calls-sfu-ssh-client.d.ts +49 -0
- package/dist/calls-sfu-ssh-client.js +82 -0
- package/dist/calls-sfu.d.ts +36 -12
- package/dist/calls-sfu.js +41 -9
- package/dist/index.d.ts +38 -7
- package/dist/index.js +20 -4
- package/dist/migrations/0016_platform.sql +61 -0
- package/dist/migrations/0017_requested_changes.sql +42 -0
- package/dist/migrations/0018_platform_apply.sql +19 -0
- package/dist/migrations/0019_mail_owner_mailbox.sql +15 -0
- package/dist/migrations/0020_mail_apps.sql +7 -0
- package/dist/migrations/0021_mail_role_mailboxes.sql +6 -0
- package/dist/org.d.ts +81 -0
- package/dist/org.js +96 -0
- package/dist/plan/agora.d.ts +77 -0
- package/dist/plan/agora.js +127 -0
- package/dist/plan/cloudflared-connector.d.ts +70 -0
- package/dist/plan/cloudflared-connector.js +106 -0
- package/dist/plan/diff.d.ts +20 -3
- package/dist/plan/diff.js +713 -115
- package/dist/plan/executor.d.ts +680 -172
- package/dist/plan/executor.js +755 -189
- package/dist/plan/index.d.ts +28 -5
- package/dist/plan/index.js +15 -2
- package/dist/plan/jobs-scheduler-http.d.ts +29 -0
- package/dist/plan/jobs-scheduler-http.js +50 -0
- package/dist/plan/live.d.ts +51 -2
- package/dist/plan/live.js +8 -0
- package/dist/plan/mail-copy.d.ts +125 -0
- package/dist/plan/mail-copy.js +166 -0
- package/dist/plan/mail-identity-allowlist.d.ts +50 -0
- package/dist/plan/mail-identity-allowlist.js +11 -0
- package/dist/plan/mail-identity.d.ts +68 -0
- package/dist/plan/mail-identity.js +17 -0
- package/dist/plan/plan-apply.d.ts +197 -3
- package/dist/plan/plan-apply.js +278 -9
- package/dist/plan/platform-diff.d.ts +487 -0
- package/dist/plan/platform-diff.js +1220 -0
- package/dist/plan/platform-org-ports.d.ts +50 -0
- package/dist/plan/platform-org-ports.js +70 -0
- package/dist/plan/platform-org.d.ts +121 -0
- package/dist/plan/platform-org.js +115 -0
- package/dist/plan/platform-providers.d.ts +615 -0
- package/dist/plan/platform-providers.js +4410 -0
- package/dist/plan/platform-sfu.d.ts +128 -0
- package/dist/plan/platform-sfu.js +142 -0
- package/dist/plan/providers-readers.js +22 -1
- package/dist/plan/zitadel-machine-user.d.ts +83 -0
- package/dist/plan/zitadel-machine-user.js +238 -0
- package/dist/plan.d.ts +6 -0
- package/dist/platform.d.ts +55 -0
- package/dist/platform.js +46 -0
- package/dist/requested-changes.d.ts +118 -0
- package/dist/requested-changes.js +125 -0
- package/dist/slug.js +15 -2
- package/dist/store/audit.js +4 -2
- package/dist/store/postgres.d.ts +29 -1
- package/dist/store/postgres.js +203 -4
- package/dist/store/schema.d.ts +755 -0
- package/dist/store/schema.js +70 -0
- package/dist/store.d.ts +53 -0
- package/package.json +1 -1
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform's `sfu` step (#170): the shared calls SFU (LiveKit +
|
|
3
|
+
* cloudflared, `../calls-sfu.ts`) applied from `applyPlatform` instead of by
|
|
4
|
+
* hand running `scripts/calls-sfu-deploy.mjs`. Mirrors #167's
|
|
5
|
+
* `platform-providers.ts` shape -- narrow ports, a host (archon) supplies the
|
|
6
|
+
* real implementation -- for the two things this step needs beyond
|
|
7
|
+
* `applyCallsSfu` itself:
|
|
8
|
+
*
|
|
9
|
+
* - {@link CallsSfuConnectionPort}: the SFU VM's address, SSH credentials and
|
|
10
|
+
* host key fingerprint. A human stores these as held keys ahead of time
|
|
11
|
+
* (#170's "Human steps") -- this package only ever reads them back, the
|
|
12
|
+
* same way #122's original script did from the environment.
|
|
13
|
+
* - {@link CallsSfuKeysPort}: a LiveKit API key/secret pair *per org*, as of
|
|
14
|
+
* #152 ("one shared LiveKit SFU, kept apart by per-company keys", estate
|
|
15
|
+
* DECISIONS.md 2026-09-28). `applyCallsSfuPlatformStep` takes the list of
|
|
16
|
+
* org slugs with calls enabled -- `platform-providers.ts`'s `executeSfu`
|
|
17
|
+
* builds it from `DeclaredOrgStore.list()` -- and, for each one, mints a
|
|
18
|
+
* pair the first time and delivers it to held keys; every apply after that
|
|
19
|
+
* reads the same pair back rather than minting a second one. A slug no
|
|
20
|
+
* longer in the list simply contributes no line to the next apply's
|
|
21
|
+
* `keys:` map (`../calls-sfu.ts`'s `livekitConfigYaml`) -- its held pair is
|
|
22
|
+
* left stored, not deleted, the same "park, don't delete" rule the rest of
|
|
23
|
+
* this planner follows. `executor.ts`'s `CallsSfuPort.resolveCredentials`
|
|
24
|
+
* (the org-level calls-service step) reads a specific org's own held pair
|
|
25
|
+
* back, by that org's slug. {@link PLATFORM_LIVEKIT_KEY_SLUG}'s own pair is
|
|
26
|
+
* always included too, even with zero orgs enabled, so `keys:` is never
|
|
27
|
+
* empty (round 2 of #152's review, finding 2).
|
|
28
|
+
*
|
|
29
|
+
* {@link CallsSfuSshConnector} is the third port: opening the actual SSH
|
|
30
|
+
* session. The real one is `calls-sfu-ssh-client.ts`'s `connectCallsSfuSsh`
|
|
31
|
+
* (over `ssh2`); a host passes it as `ssh`. {@link CallsSfuConnectionPort} and
|
|
32
|
+
* {@link CallsSfuKeysPort} stay host-implemented on purpose: they read and
|
|
33
|
+
* write held keys (`@wtfalch/keys`), which only the host (archon) can reach,
|
|
34
|
+
* the same way `CoolifyPort`/`CloudflarePort`/`ZitadelPort`'s real clients
|
|
35
|
+
* live outside this package. `calls-sfu-ssh-adapter.ts`'s {@link CallsSfuSshSession} is the narrow shape
|
|
36
|
+
* a test fakes instead.
|
|
37
|
+
*/
|
|
38
|
+
import { type LivekitCredentials } from '../calls-sfu-keys.js';
|
|
39
|
+
import { type CallsSfuSshSession } from '../calls-sfu-ssh-adapter.js';
|
|
40
|
+
/** The SFU VM's own address, SSH credentials and host key fingerprint -- held keys a human stored ahead of time. */
|
|
41
|
+
export interface CallsSfuConnection {
|
|
42
|
+
address: string;
|
|
43
|
+
sshUser: string;
|
|
44
|
+
sshPort: number;
|
|
45
|
+
sshPrivateKey: string;
|
|
46
|
+
/** `SHA256:...`, `hostKeyFingerprint`'s own format -- checked by `toCallsSfuHostPort`/`applyCallsSfuOverSession`. */
|
|
47
|
+
sshHostFingerprint: string;
|
|
48
|
+
/** #127's own held key, read back here rather than minted -- unrelated to the LiveKit pair this step mints. */
|
|
49
|
+
cloudflaredTunnelToken: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Read-only: `null` before a human has stored the SFU VM's connection
|
|
53
|
+
* details as held keys (#170's "Human steps"). This step never invents an
|
|
54
|
+
* address or a private key.
|
|
55
|
+
*/
|
|
56
|
+
export interface CallsSfuConnectionPort {
|
|
57
|
+
resolveConnection(): Promise<CallsSfuConnection | null>;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Where this step mints and stores each org's own LiveKit API key/secret
|
|
61
|
+
* pair, as held keys, keyed by the org's slug (#152). `resolveLivekitKeys`
|
|
62
|
+
* returns `null` for a slug with no pair stored yet; `deliverLivekitKeys` is
|
|
63
|
+
* called exactly once per slug, the first time that slug's pair comes back
|
|
64
|
+
* `null` -- mirrors `FoundryStepExecutorPorts.deliverSecret`'s own "mint
|
|
65
|
+
* once, deliver, never re-mint" shape (`zitadel:app`'s
|
|
66
|
+
* `createApiApp`/`deliverSecret` pair in `executor.ts`).
|
|
67
|
+
*
|
|
68
|
+
* `deliverLivekitKeys` must be create-if-absent at the store: the first
|
|
69
|
+
* write for a slug wins and a later one is a no-op, not an overwrite. Two
|
|
70
|
+
* concurrent applies can both see `resolveLivekitKeys` return `null` for the
|
|
71
|
+
* same new slug and each mint a different pair; create-if-absent is what
|
|
72
|
+
* lets {@link applyCallsSfuPlatformStep}'s post-deliver re-read (round 2 of
|
|
73
|
+
* #152's review) converge both applies on whichever pair the store actually
|
|
74
|
+
* kept, rather than each one writing its own locally-minted pair into
|
|
75
|
+
* `livekit.yaml`.
|
|
76
|
+
*/
|
|
77
|
+
export interface CallsSfuKeysPort {
|
|
78
|
+
resolveLivekitKeys(orgSlug: string): Promise<LivekitCredentials | null>;
|
|
79
|
+
deliverLivekitKeys(orgSlug: string, pair: LivekitCredentials): Promise<void>;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The key id the platform's own LiveKit pair is held under, alongside every
|
|
83
|
+
* org's own pair, in the same {@link CallsSfuKeysPort} (#152 round 2 review:
|
|
84
|
+
* a `keys:` map with no entries at all makes LiveKit refuse to start outside
|
|
85
|
+
* dev mode, so an estate with no org's calls turned on yet still needs one
|
|
86
|
+
* live key). `oikos` is reserved and can never be a real org's own slug
|
|
87
|
+
* (`../slug.ts`'s `RESERVED_SLUGS`), so this can never collide with an org's
|
|
88
|
+
* pair.
|
|
89
|
+
*/
|
|
90
|
+
export declare const PLATFORM_LIVEKIT_KEY_SLUG = "oikos";
|
|
91
|
+
/** Opens the actual SSH session; `connectCallsSfuSsh` is the real implementation. */
|
|
92
|
+
export interface CallsSfuSshConnector {
|
|
93
|
+
connect(options: {
|
|
94
|
+
address: string;
|
|
95
|
+
sshUser: string;
|
|
96
|
+
sshPort: number;
|
|
97
|
+
sshPrivateKey: string;
|
|
98
|
+
/** Verified inside the handshake, before authentication; the connector refuses a mismatch. */
|
|
99
|
+
sshHostFingerprint: string;
|
|
100
|
+
}): Promise<CallsSfuSshSession & {
|
|
101
|
+
close(): void;
|
|
102
|
+
}>;
|
|
103
|
+
}
|
|
104
|
+
export declare class MissingCallsSfuConnectionError extends Error {
|
|
105
|
+
constructor();
|
|
106
|
+
}
|
|
107
|
+
export interface CallsSfuPlatformDeps {
|
|
108
|
+
connection: CallsSfuConnectionPort;
|
|
109
|
+
keys: CallsSfuKeysPort;
|
|
110
|
+
ssh: CallsSfuSshConnector;
|
|
111
|
+
/** Overridable so a test never needs a real CSPRNG call to assert on the minted pair. */
|
|
112
|
+
mintLivekitCredentials?: () => LivekitCredentials;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Runs the platform's `sfu` step: resolve the connection, mint each enabled
|
|
116
|
+
* org's own LiveKit pair the first time (read it back on every apply after
|
|
117
|
+
* that), open the SSH session, and apply. `orgSlugs` is every org with calls
|
|
118
|
+
* enabled, in no required order -- sorted here, alongside the always-present
|
|
119
|
+
* {@link PLATFORM_LIVEKIT_KEY_SLUG}, so a re-run with the same set mints
|
|
120
|
+
* nothing new and builds the identical `keys:` map (`../calls-sfu.ts`'s
|
|
121
|
+
* `livekitConfigYaml` sorts it again regardless, but sorting before minting
|
|
122
|
+
* keeps this function's own behaviour independent of the order its caller
|
|
123
|
+
* happens to list orgs in). Every part of this is safe to re-run -- minting
|
|
124
|
+
* only happens once a given slug's stored pair comes back `null`, and
|
|
125
|
+
* `applyCallsSfuOverSession` is itself idempotent -- so a second apply with
|
|
126
|
+
* the same held keys and the same org list changes nothing.
|
|
127
|
+
*/
|
|
128
|
+
export declare function applyCallsSfuPlatformStep(deps: CallsSfuPlatformDeps, orgSlugs?: readonly string[]): Promise<void>;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform's `sfu` step (#170): the shared calls SFU (LiveKit +
|
|
3
|
+
* cloudflared, `../calls-sfu.ts`) applied from `applyPlatform` instead of by
|
|
4
|
+
* hand running `scripts/calls-sfu-deploy.mjs`. Mirrors #167's
|
|
5
|
+
* `platform-providers.ts` shape -- narrow ports, a host (archon) supplies the
|
|
6
|
+
* real implementation -- for the two things this step needs beyond
|
|
7
|
+
* `applyCallsSfu` itself:
|
|
8
|
+
*
|
|
9
|
+
* - {@link CallsSfuConnectionPort}: the SFU VM's address, SSH credentials and
|
|
10
|
+
* host key fingerprint. A human stores these as held keys ahead of time
|
|
11
|
+
* (#170's "Human steps") -- this package only ever reads them back, the
|
|
12
|
+
* same way #122's original script did from the environment.
|
|
13
|
+
* - {@link CallsSfuKeysPort}: a LiveKit API key/secret pair *per org*, as of
|
|
14
|
+
* #152 ("one shared LiveKit SFU, kept apart by per-company keys", estate
|
|
15
|
+
* DECISIONS.md 2026-09-28). `applyCallsSfuPlatformStep` takes the list of
|
|
16
|
+
* org slugs with calls enabled -- `platform-providers.ts`'s `executeSfu`
|
|
17
|
+
* builds it from `DeclaredOrgStore.list()` -- and, for each one, mints a
|
|
18
|
+
* pair the first time and delivers it to held keys; every apply after that
|
|
19
|
+
* reads the same pair back rather than minting a second one. A slug no
|
|
20
|
+
* longer in the list simply contributes no line to the next apply's
|
|
21
|
+
* `keys:` map (`../calls-sfu.ts`'s `livekitConfigYaml`) -- its held pair is
|
|
22
|
+
* left stored, not deleted, the same "park, don't delete" rule the rest of
|
|
23
|
+
* this planner follows. `executor.ts`'s `CallsSfuPort.resolveCredentials`
|
|
24
|
+
* (the org-level calls-service step) reads a specific org's own held pair
|
|
25
|
+
* back, by that org's slug. {@link PLATFORM_LIVEKIT_KEY_SLUG}'s own pair is
|
|
26
|
+
* always included too, even with zero orgs enabled, so `keys:` is never
|
|
27
|
+
* empty (round 2 of #152's review, finding 2).
|
|
28
|
+
*
|
|
29
|
+
* {@link CallsSfuSshConnector} is the third port: opening the actual SSH
|
|
30
|
+
* session. The real one is `calls-sfu-ssh-client.ts`'s `connectCallsSfuSsh`
|
|
31
|
+
* (over `ssh2`); a host passes it as `ssh`. {@link CallsSfuConnectionPort} and
|
|
32
|
+
* {@link CallsSfuKeysPort} stay host-implemented on purpose: they read and
|
|
33
|
+
* write held keys (`@wtfalch/keys`), which only the host (archon) can reach,
|
|
34
|
+
* the same way `CoolifyPort`/`CloudflarePort`/`ZitadelPort`'s real clients
|
|
35
|
+
* live outside this package. `calls-sfu-ssh-adapter.ts`'s {@link CallsSfuSshSession} is the narrow shape
|
|
36
|
+
* a test fakes instead.
|
|
37
|
+
*/
|
|
38
|
+
import { redactSecrets } from '../calls-sfu-deploy-support.js';
|
|
39
|
+
import { mintLivekitCredentials } from '../calls-sfu-keys.js';
|
|
40
|
+
import { applyCallsSfuOverSession } from '../calls-sfu-ssh-adapter.js';
|
|
41
|
+
/**
|
|
42
|
+
* The key id the platform's own LiveKit pair is held under, alongside every
|
|
43
|
+
* org's own pair, in the same {@link CallsSfuKeysPort} (#152 round 2 review:
|
|
44
|
+
* a `keys:` map with no entries at all makes LiveKit refuse to start outside
|
|
45
|
+
* dev mode, so an estate with no org's calls turned on yet still needs one
|
|
46
|
+
* live key). `oikos` is reserved and can never be a real org's own slug
|
|
47
|
+
* (`../slug.ts`'s `RESERVED_SLUGS`), so this can never collide with an org's
|
|
48
|
+
* pair.
|
|
49
|
+
*/
|
|
50
|
+
export const PLATFORM_LIVEKIT_KEY_SLUG = 'oikos';
|
|
51
|
+
export class MissingCallsSfuConnectionError extends Error {
|
|
52
|
+
constructor() {
|
|
53
|
+
super('No SSH connection details are held for the calls SFU. Store the SFU VM’s ' +
|
|
54
|
+
'SSH key and host key fingerprint as held keys before applying the ' +
|
|
55
|
+
'platform’s sfu step (#170’s "Human steps").');
|
|
56
|
+
this.name = 'MissingCallsSfuConnectionError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Runs the platform's `sfu` step: resolve the connection, mint each enabled
|
|
61
|
+
* org's own LiveKit pair the first time (read it back on every apply after
|
|
62
|
+
* that), open the SSH session, and apply. `orgSlugs` is every org with calls
|
|
63
|
+
* enabled, in no required order -- sorted here, alongside the always-present
|
|
64
|
+
* {@link PLATFORM_LIVEKIT_KEY_SLUG}, so a re-run with the same set mints
|
|
65
|
+
* nothing new and builds the identical `keys:` map (`../calls-sfu.ts`'s
|
|
66
|
+
* `livekitConfigYaml` sorts it again regardless, but sorting before minting
|
|
67
|
+
* keeps this function's own behaviour independent of the order its caller
|
|
68
|
+
* happens to list orgs in). Every part of this is safe to re-run -- minting
|
|
69
|
+
* only happens once a given slug's stored pair comes back `null`, and
|
|
70
|
+
* `applyCallsSfuOverSession` is itself idempotent -- so a second apply with
|
|
71
|
+
* the same held keys and the same org list changes nothing.
|
|
72
|
+
*/
|
|
73
|
+
export async function applyCallsSfuPlatformStep(deps, orgSlugs = []) {
|
|
74
|
+
const connection = await deps.connection.resolveConnection();
|
|
75
|
+
if (!connection)
|
|
76
|
+
throw new MissingCallsSfuConnectionError();
|
|
77
|
+
// Every value below can echo back in an error (remote stdout/stderr, an
|
|
78
|
+
// ssh2 connect error), and the message reaches the platform audit row, so
|
|
79
|
+
// it is redacted before it leaves this function.
|
|
80
|
+
const secrets = [connection.sshPrivateKey, connection.cloudflaredTunnelToken];
|
|
81
|
+
try {
|
|
82
|
+
const livekitKeys = {};
|
|
83
|
+
const slugs = new Set([PLATFORM_LIVEKIT_KEY_SLUG, ...orgSlugs]);
|
|
84
|
+
for (const orgSlug of [...slugs].sort()) {
|
|
85
|
+
let pair = await deps.keys.resolveLivekitKeys(orgSlug);
|
|
86
|
+
if (!pair) {
|
|
87
|
+
const minted = (deps.mintLivekitCredentials ?? mintLivekitCredentials)();
|
|
88
|
+
// Pushed before the delivery call below (not after): a delivery
|
|
89
|
+
// that throws with the freshly minted secret in its message must
|
|
90
|
+
// still find it in this list to redact it (round 2 of #152's
|
|
91
|
+
// review, finding 1).
|
|
92
|
+
secrets.push(minted.apiKey, minted.apiSecret);
|
|
93
|
+
await deps.keys.deliverLivekitKeys(orgSlug, minted);
|
|
94
|
+
// `deliverLivekitKeys` is create-if-absent (this interface's own
|
|
95
|
+
// doc comment), so a concurrent apply that minted first may already
|
|
96
|
+
// have won the write for this slug. Re-read rather than trusting
|
|
97
|
+
// the locally minted pair, so this apply's own `livekit.yaml`
|
|
98
|
+
// always matches whatever the store actually kept (round 2's
|
|
99
|
+
// finding 3).
|
|
100
|
+
pair = (await deps.keys.resolveLivekitKeys(orgSlug)) ?? minted;
|
|
101
|
+
if (pair !== minted)
|
|
102
|
+
secrets.push(pair.apiKey, pair.apiSecret);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
secrets.push(pair.apiKey, pair.apiSecret);
|
|
106
|
+
}
|
|
107
|
+
livekitKeys[pair.apiKey] = pair.apiSecret;
|
|
108
|
+
}
|
|
109
|
+
const session = await deps.ssh.connect({
|
|
110
|
+
address: connection.address,
|
|
111
|
+
sshUser: connection.sshUser,
|
|
112
|
+
sshPort: connection.sshPort,
|
|
113
|
+
sshPrivateKey: connection.sshPrivateKey,
|
|
114
|
+
sshHostFingerprint: connection.sshHostFingerprint,
|
|
115
|
+
});
|
|
116
|
+
try {
|
|
117
|
+
const config = {
|
|
118
|
+
address: connection.address,
|
|
119
|
+
livekitKeys,
|
|
120
|
+
cloudflaredTunnelToken: connection.cloudflaredTunnelToken,
|
|
121
|
+
};
|
|
122
|
+
await applyCallsSfuOverSession(session, config, connection.sshHostFingerprint);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
session.close();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
throw redactedError(error, secrets);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function redactedError(error, secrets) {
|
|
133
|
+
const original = error instanceof Error ? error : new Error(String(error));
|
|
134
|
+
const message = redactSecrets(original.message, secrets);
|
|
135
|
+
const stack = redactSecrets(original.stack ?? message, secrets);
|
|
136
|
+
if (message === original.message && stack === original.stack)
|
|
137
|
+
return original;
|
|
138
|
+
const clean = new Error(message);
|
|
139
|
+
clean.name = original.name;
|
|
140
|
+
clean.stack = stack;
|
|
141
|
+
return clean;
|
|
142
|
+
}
|
|
@@ -130,6 +130,26 @@ async function withLogVolumeConfig(client, resource, recorded) {
|
|
|
130
130
|
return resource;
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* #164: the org's own recorded Agora app's live `AGORA_*` env vars, as
|
|
135
|
+
* `config.env`, so `diff.ts`'s `planAgora` can compare them with what
|
|
136
|
+
* `executor.ts` would write. Gated on `recorded` mapping this exact uuid onto
|
|
137
|
+
* the `agora` kind, like {@link withLogVolumeConfig}. Unlike that read, a
|
|
138
|
+
* failure here is not swallowed: a plan that cannot see the live config must
|
|
139
|
+
* fail, not report it as missing.
|
|
140
|
+
*/
|
|
141
|
+
async function withAgoraEnvConfig(client, resource, recorded) {
|
|
142
|
+
if (resource.kind !== 'application' || !recorded[resource.key]?.startsWith('agora:')) {
|
|
143
|
+
return resource;
|
|
144
|
+
}
|
|
145
|
+
const envs = await client.listEnvs(resource.key);
|
|
146
|
+
const env = {};
|
|
147
|
+
for (const e of envs) {
|
|
148
|
+
if (!e.is_preview && e.key.startsWith('AGORA_'))
|
|
149
|
+
env[e.key] = e.value;
|
|
150
|
+
}
|
|
151
|
+
return { ...resource, config: { ...resource.config, env } };
|
|
152
|
+
}
|
|
133
153
|
/** `@wtfalch/providers`' own `LiveResource` (`resource.ts`) has no `system` field — foundry's adds it at this boundary. */
|
|
134
154
|
function toFoundryResources(system, resources) {
|
|
135
155
|
return resources.map((r) => ({ system, kind: r.kind, key: r.key, config: r.config }));
|
|
@@ -157,7 +177,8 @@ export class ProvidersCoolifyReader {
|
|
|
157
177
|
this.resolveRecordedIds(orgId),
|
|
158
178
|
]);
|
|
159
179
|
const withLogConfig = await Promise.all(toFoundryResources('coolify', resources).map((r) => withLogVolumeConfig(this.client, r, recorded)));
|
|
160
|
-
|
|
180
|
+
const withAgoraConfig = await Promise.all(withLogConfig.map((r) => withAgoraEnvConfig(this.client, r, recorded)));
|
|
181
|
+
return applyRecordedIds(withAgoraConfig, recorded);
|
|
161
182
|
}
|
|
162
183
|
}
|
|
163
184
|
/**
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ZitadelPort } from './executor.js';
|
|
2
|
+
/**
|
|
3
|
+
* #168: the real {@link ZitadelPort.mintAdminMachineUser}/
|
|
4
|
+
* {@link ZitadelPort.mintLoginClientMachineUser}. `@wtfalch/providers` has no
|
|
5
|
+
* machine-user support, so this speaks ZITADEL's v1 management and admin APIs
|
|
6
|
+
* directly, with the same request shape as providers' `ZitadelClient.call`
|
|
7
|
+
* (bearer token, JSON in and out, `x-zitadel-orgid` for the target org,
|
|
8
|
+
* optional instance-host headers, a typed error on a non-ok response). The
|
|
9
|
+
* calls follow ZITADEL's published API reference and have not been run
|
|
10
|
+
* against a live instance.
|
|
11
|
+
*
|
|
12
|
+
* 1. `POST /management/v1/users/_search` finds the machine user by user name.
|
|
13
|
+
* 2. `POST /management/v1/users/machine` creates it when absent.
|
|
14
|
+
* 3. #216: for the login-client role on an *existing* user, `POST
|
|
15
|
+
* /admin/v1/members/_search` reads its currently granted IAM roles, and
|
|
16
|
+
* refuses (rather than reusing) a user that holds anything other than
|
|
17
|
+
* exactly `IAM_LOGIN_CLIENT` -- see {@link ZitadelLoginClientRoleConflictError}.
|
|
18
|
+
* 4. `POST /admin/v1/members` grants the role (409 means already granted).
|
|
19
|
+
* 5. #216: for the login-client role on an existing user, `POST
|
|
20
|
+
* /management/v1/users/{id}/pats/_search` (round 2 of the second PR
|
|
21
|
+
* review: ZITADEL lists PATs with a POST and a JSON body, `{}` here --
|
|
22
|
+
* not the `GET` this file used until then, which a live instance
|
|
23
|
+
* rejects before the revoke sweep ever runs) lists every PAT it already
|
|
24
|
+
* holds, and `DELETE /management/v1/users/{id}/pats/{patId}` removes
|
|
25
|
+
* each one --
|
|
26
|
+
* before the new PAT is requested, so a rerun (a crash between a prior
|
|
27
|
+
* mint and the caller recording it, or every delivery retry failing)
|
|
28
|
+
* always leaves exactly one live token, never two. The admin role's PAT
|
|
29
|
+
* is never revoked this way, unchanged from before #216: nothing here
|
|
30
|
+
* decrypts with a stale admin PAT the way ZITADEL's own masterkey
|
|
31
|
+
* protects data encrypted under it, but this file still only revokes
|
|
32
|
+
* what the caller (`platform-providers.ts`) actually asked for. #216
|
|
33
|
+
* round 3: a 404 on one of these deletes (two concurrent mints listing
|
|
34
|
+
* and deleting the same already-gone PAT) is treated as already done,
|
|
35
|
+
* not an error -- the sweep's goal, that PAT is gone, already holds.
|
|
36
|
+
* 6. `POST /management/v1/users/{id}/pats` adds a token, which ZITADEL
|
|
37
|
+
* returns once, alongside the new PAT's own id (`tokenId`) -- #216
|
|
38
|
+
* round 2: {@link ZitadelPort.mintLoginClientMachineUser} returns that
|
|
39
|
+
* id (`patId`) so the caller can re-list PATs after delivery and detect
|
|
40
|
+
* a concurrent apply that revoked or replaced it (see
|
|
41
|
+
* {@link ZitadelPort.listLoginClientPats}'s own doc comment). The admin
|
|
42
|
+
* role's mint drops it before returning, unchanged from before #216
|
|
43
|
+
* round 2 -- nothing reads it there.
|
|
44
|
+
*
|
|
45
|
+
* `token` is the bearer for the new instance, so it must belong to a caller
|
|
46
|
+
* allowed to create users and instance members (for example the instance's
|
|
47
|
+
* bootstrap machine user). The minted token is returned to the caller and
|
|
48
|
+
* never logged or put in an error.
|
|
49
|
+
*/
|
|
50
|
+
export declare class ZitadelHttpError extends Error {
|
|
51
|
+
readonly status: number;
|
|
52
|
+
constructor(status: number, message: string);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* #216: thrown by {@link createZitadelAdminMinter}'s `mintLoginClientMachineUser`
|
|
56
|
+
* when a `foundry-login-client` user already exists but holds an IAM role
|
|
57
|
+
* other than exactly `IAM_LOGIN_CLIENT` -- for example `IAM_OWNER`. That user
|
|
58
|
+
* was not created by this method; granting it an additional role, or handing
|
|
59
|
+
* its PAT out as the login-client token, would widen access nobody asked
|
|
60
|
+
* for. Never caught and retried -- a human has to resolve the conflicting
|
|
61
|
+
* user by hand.
|
|
62
|
+
*
|
|
63
|
+
* #216 round 2: a user with *no* role at all is never refused this way --
|
|
64
|
+
* see `mintMachineUser`'s own `roles.length > 0 && ...` check. That is the
|
|
65
|
+
* crash window between `POST /management/v1/users/machine` creating the user
|
|
66
|
+
* and `POST /admin/v1/members` granting it the role; refusing it would turn
|
|
67
|
+
* an ordinary crash-and-rerun into a permanent, human-only block, which is
|
|
68
|
+
* exactly the bug class #216 exists to close.
|
|
69
|
+
*/
|
|
70
|
+
export declare class ZitadelLoginClientRoleConflictError extends Error {
|
|
71
|
+
readonly userId: string;
|
|
72
|
+
readonly roles: readonly string[];
|
|
73
|
+
constructor(userId: string, roles: readonly string[]);
|
|
74
|
+
}
|
|
75
|
+
export interface ZitadelMinterOptions {
|
|
76
|
+
/** The new instance's base URL, e.g. `https://auth.companyincluded.com`. */
|
|
77
|
+
url: string;
|
|
78
|
+
/** Bearer token allowed to create users and add instance members there. */
|
|
79
|
+
token: string;
|
|
80
|
+
fetchImpl?: typeof fetch;
|
|
81
|
+
instanceHost?: string;
|
|
82
|
+
}
|
|
83
|
+
export declare function createZitadelAdminMinter(options: ZitadelMinterOptions): Pick<ZitadelPort, 'mintAdminMachineUser' | 'mintLoginClientMachineUser' | 'listLoginClientPats'>;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #168: the real {@link ZitadelPort.mintAdminMachineUser}/
|
|
3
|
+
* {@link ZitadelPort.mintLoginClientMachineUser}. `@wtfalch/providers` has no
|
|
4
|
+
* machine-user support, so this speaks ZITADEL's v1 management and admin APIs
|
|
5
|
+
* directly, with the same request shape as providers' `ZitadelClient.call`
|
|
6
|
+
* (bearer token, JSON in and out, `x-zitadel-orgid` for the target org,
|
|
7
|
+
* optional instance-host headers, a typed error on a non-ok response). The
|
|
8
|
+
* calls follow ZITADEL's published API reference and have not been run
|
|
9
|
+
* against a live instance.
|
|
10
|
+
*
|
|
11
|
+
* 1. `POST /management/v1/users/_search` finds the machine user by user name.
|
|
12
|
+
* 2. `POST /management/v1/users/machine` creates it when absent.
|
|
13
|
+
* 3. #216: for the login-client role on an *existing* user, `POST
|
|
14
|
+
* /admin/v1/members/_search` reads its currently granted IAM roles, and
|
|
15
|
+
* refuses (rather than reusing) a user that holds anything other than
|
|
16
|
+
* exactly `IAM_LOGIN_CLIENT` -- see {@link ZitadelLoginClientRoleConflictError}.
|
|
17
|
+
* 4. `POST /admin/v1/members` grants the role (409 means already granted).
|
|
18
|
+
* 5. #216: for the login-client role on an existing user, `POST
|
|
19
|
+
* /management/v1/users/{id}/pats/_search` (round 2 of the second PR
|
|
20
|
+
* review: ZITADEL lists PATs with a POST and a JSON body, `{}` here --
|
|
21
|
+
* not the `GET` this file used until then, which a live instance
|
|
22
|
+
* rejects before the revoke sweep ever runs) lists every PAT it already
|
|
23
|
+
* holds, and `DELETE /management/v1/users/{id}/pats/{patId}` removes
|
|
24
|
+
* each one --
|
|
25
|
+
* before the new PAT is requested, so a rerun (a crash between a prior
|
|
26
|
+
* mint and the caller recording it, or every delivery retry failing)
|
|
27
|
+
* always leaves exactly one live token, never two. The admin role's PAT
|
|
28
|
+
* is never revoked this way, unchanged from before #216: nothing here
|
|
29
|
+
* decrypts with a stale admin PAT the way ZITADEL's own masterkey
|
|
30
|
+
* protects data encrypted under it, but this file still only revokes
|
|
31
|
+
* what the caller (`platform-providers.ts`) actually asked for. #216
|
|
32
|
+
* round 3: a 404 on one of these deletes (two concurrent mints listing
|
|
33
|
+
* and deleting the same already-gone PAT) is treated as already done,
|
|
34
|
+
* not an error -- the sweep's goal, that PAT is gone, already holds.
|
|
35
|
+
* 6. `POST /management/v1/users/{id}/pats` adds a token, which ZITADEL
|
|
36
|
+
* returns once, alongside the new PAT's own id (`tokenId`) -- #216
|
|
37
|
+
* round 2: {@link ZitadelPort.mintLoginClientMachineUser} returns that
|
|
38
|
+
* id (`patId`) so the caller can re-list PATs after delivery and detect
|
|
39
|
+
* a concurrent apply that revoked or replaced it (see
|
|
40
|
+
* {@link ZitadelPort.listLoginClientPats}'s own doc comment). The admin
|
|
41
|
+
* role's mint drops it before returning, unchanged from before #216
|
|
42
|
+
* round 2 -- nothing reads it there.
|
|
43
|
+
*
|
|
44
|
+
* `token` is the bearer for the new instance, so it must belong to a caller
|
|
45
|
+
* allowed to create users and instance members (for example the instance's
|
|
46
|
+
* bootstrap machine user). The minted token is returned to the caller and
|
|
47
|
+
* never logged or put in an error.
|
|
48
|
+
*/
|
|
49
|
+
export class ZitadelHttpError extends Error {
|
|
50
|
+
status;
|
|
51
|
+
constructor(status, message) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.status = status;
|
|
54
|
+
this.name = 'ZitadelHttpError';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* #216: thrown by {@link createZitadelAdminMinter}'s `mintLoginClientMachineUser`
|
|
59
|
+
* when a `foundry-login-client` user already exists but holds an IAM role
|
|
60
|
+
* other than exactly `IAM_LOGIN_CLIENT` -- for example `IAM_OWNER`. That user
|
|
61
|
+
* was not created by this method; granting it an additional role, or handing
|
|
62
|
+
* its PAT out as the login-client token, would widen access nobody asked
|
|
63
|
+
* for. Never caught and retried -- a human has to resolve the conflicting
|
|
64
|
+
* user by hand.
|
|
65
|
+
*
|
|
66
|
+
* #216 round 2: a user with *no* role at all is never refused this way --
|
|
67
|
+
* see `mintMachineUser`'s own `roles.length > 0 && ...` check. That is the
|
|
68
|
+
* crash window between `POST /management/v1/users/machine` creating the user
|
|
69
|
+
* and `POST /admin/v1/members` granting it the role; refusing it would turn
|
|
70
|
+
* an ordinary crash-and-rerun into a permanent, human-only block, which is
|
|
71
|
+
* exactly the bug class #216 exists to close.
|
|
72
|
+
*/
|
|
73
|
+
export class ZitadelLoginClientRoleConflictError extends Error {
|
|
74
|
+
userId;
|
|
75
|
+
roles;
|
|
76
|
+
constructor(userId, roles) {
|
|
77
|
+
super(`ZITADEL machine user ${userId} already exists with role(s) [${roles.join(', ') || 'none'}], not [IAM_LOGIN_CLIENT] -- refusing to reuse it as foundry's login-client user`);
|
|
78
|
+
this.userId = userId;
|
|
79
|
+
this.roles = roles;
|
|
80
|
+
this.name = 'ZitadelLoginClientRoleConflictError';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function createZitadelAdminMinter(options) {
|
|
84
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
85
|
+
async function call(method, path, body, orgId) {
|
|
86
|
+
const response = await fetchImpl(`${options.url}${path}`, {
|
|
87
|
+
method,
|
|
88
|
+
headers: {
|
|
89
|
+
authorization: `Bearer ${options.token}`,
|
|
90
|
+
'content-type': 'application/json',
|
|
91
|
+
...(options.instanceHost
|
|
92
|
+
? {
|
|
93
|
+
'x-zitadel-instance-host': options.instanceHost,
|
|
94
|
+
'x-forwarded-host': options.instanceHost,
|
|
95
|
+
'x-forwarded-proto': 'https',
|
|
96
|
+
}
|
|
97
|
+
: {}),
|
|
98
|
+
...(orgId ? { 'x-zitadel-orgid': orgId } : {}),
|
|
99
|
+
},
|
|
100
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
101
|
+
});
|
|
102
|
+
const text = await response.text();
|
|
103
|
+
let data = {};
|
|
104
|
+
try {
|
|
105
|
+
data = text ? JSON.parse(text) : {};
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Never echo the body: on a PAT call it could hold the token.
|
|
109
|
+
throw new ZitadelHttpError(response.status, `non-JSON response from ${path}`);
|
|
110
|
+
}
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new ZitadelHttpError(response.status, String(data.message ?? response.statusText));
|
|
113
|
+
}
|
|
114
|
+
return data;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* #216 round 2: every live PAT id on a machine user, for both the
|
|
118
|
+
* revoke-before-mint sweep below and {@link ZitadelPort.listLoginClientPats}
|
|
119
|
+
* -- one call, one place that reads it.
|
|
120
|
+
*
|
|
121
|
+
* #216 (second PR review, round 2): ZITADEL lists a user's PATs with `POST
|
|
122
|
+
* /management/v1/users/{id}/pats/_search` and a JSON body (`{}` -- no
|
|
123
|
+
* filter narrows the search), not `GET` on the plain collection route this
|
|
124
|
+
* function used until this fix. A `GET` here 404s/405s before the
|
|
125
|
+
* revoke-before-mint sweep below ever runs on a live instance, so a re-mint
|
|
126
|
+
* on an existing login-client user never got as far as revoking its old
|
|
127
|
+
* PAT; `authLoginClientDrifted` (`platform-providers.ts`) then swallowed
|
|
128
|
+
* that failure as "unknown, not drifted" (its own `try`/`catch`), silently
|
|
129
|
+
* masking the break.
|
|
130
|
+
*/
|
|
131
|
+
async function listPats(orgId, userId) {
|
|
132
|
+
const pats = await call('POST', `/management/v1/users/${userId}/pats/_search`, {}, orgId);
|
|
133
|
+
return (pats.result ?? [])
|
|
134
|
+
.map((p) => p.id)
|
|
135
|
+
.filter((id) => Boolean(id));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Shared by `mintAdminMachineUser` (`role: 'IAM_OWNER'`) and
|
|
139
|
+
* `mintLoginClientMachineUser` (`role: 'IAM_LOGIN_CLIENT'`) -- find or
|
|
140
|
+
* create the machine user by `userName`, grant `role`, then mint a fresh
|
|
141
|
+
* PAT. See this file's own top doc comment for the numbered call sequence
|
|
142
|
+
* and which of the `#216` steps (3 and 5) apply only to the login-client
|
|
143
|
+
* role, on an existing user, to keep the admin path's own behaviour
|
|
144
|
+
* unchanged.
|
|
145
|
+
*/
|
|
146
|
+
async function mintMachineUser(orgId, input, role) {
|
|
147
|
+
const found = await call('POST', '/management/v1/users/_search', {
|
|
148
|
+
queries: [
|
|
149
|
+
{ userNameQuery: { userName: input.userName, method: 'TEXT_QUERY_METHOD_EQUALS' } },
|
|
150
|
+
],
|
|
151
|
+
}, orgId);
|
|
152
|
+
const existing = found.result?.[0]?.id;
|
|
153
|
+
let userId = existing;
|
|
154
|
+
if (!userId) {
|
|
155
|
+
const created = await call('POST', '/management/v1/users/machine', {
|
|
156
|
+
userName: input.userName,
|
|
157
|
+
name: input.name,
|
|
158
|
+
accessTokenType: 'ACCESS_TOKEN_TYPE_BEARER',
|
|
159
|
+
}, orgId);
|
|
160
|
+
userId = typeof created.userId === 'string' ? created.userId : undefined;
|
|
161
|
+
}
|
|
162
|
+
if (!userId)
|
|
163
|
+
throw new Error('ZITADEL did not return the machine user id');
|
|
164
|
+
// #216: only the login-client role checks an existing user's roles
|
|
165
|
+
// before reusing it -- see this function's own doc comment for why the
|
|
166
|
+
// admin path stays as it was.
|
|
167
|
+
if (existing && role === 'IAM_LOGIN_CLIENT') {
|
|
168
|
+
const members = await call('POST', '/admin/v1/members/_search', {
|
|
169
|
+
queries: [{ userIdQuery: { userId } }],
|
|
170
|
+
});
|
|
171
|
+
const roles = (members.result ?? [])
|
|
172
|
+
.filter((m) => m.userId === userId)
|
|
173
|
+
.flatMap((m) => m.roles ?? []);
|
|
174
|
+
if (roles.length > 0 && !(roles.length === 1 && roles[0] === role)) {
|
|
175
|
+
throw new ZitadelLoginClientRoleConflictError(userId, roles);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
await call('POST', '/admin/v1/members', { userId, roles: [role] });
|
|
180
|
+
}
|
|
181
|
+
catch (e) {
|
|
182
|
+
if (!(e instanceof ZitadelHttpError && e.status === 409))
|
|
183
|
+
throw e;
|
|
184
|
+
}
|
|
185
|
+
// #216 (third PR review): true exactly when a live PAT existed on this
|
|
186
|
+
// user and this sweep revoked it -- never true on a first-ever mint
|
|
187
|
+
// (`existing` false, or an admin-role mint, which never revokes). The
|
|
188
|
+
// caller (`provisionPlatformAuth`) uses this to word a delivery-failure
|
|
189
|
+
// error correctly: revoking-then-failing leaves the apps with no live
|
|
190
|
+
// token at all, where a first mint that fails to deliver never had one.
|
|
191
|
+
let revokedPriorPat = false;
|
|
192
|
+
if (existing && role === 'IAM_LOGIN_CLIENT') {
|
|
193
|
+
// #216: revoke every PAT already on the user before minting a new one
|
|
194
|
+
// -- ZITADEL shows a PAT's value once, so a crash or a failed
|
|
195
|
+
// delivery between a prior mint and the caller recording it would
|
|
196
|
+
// otherwise leave that lost token live forever, and a rerun would
|
|
197
|
+
// mint a second one alongside it rather than replacing it.
|
|
198
|
+
const existingPats = await listPats(orgId, userId);
|
|
199
|
+
revokedPriorPat = existingPats.length > 0;
|
|
200
|
+
for (const patId of existingPats) {
|
|
201
|
+
try {
|
|
202
|
+
await call('DELETE', `/management/v1/users/${userId}/pats/${patId}`, undefined, orgId);
|
|
203
|
+
}
|
|
204
|
+
catch (e) {
|
|
205
|
+
// #216 round 3: two concurrent mints can both list the same
|
|
206
|
+
// existing PAT and both try to delete it -- the second DELETE
|
|
207
|
+
// finds it already gone. That is the outcome this sweep wants,
|
|
208
|
+
// not a failure; only a non-404 error (ZITADEL unreachable, a
|
|
209
|
+
// permissions problem) should still abort the mint.
|
|
210
|
+
if (!(e instanceof ZitadelHttpError && e.status === 404))
|
|
211
|
+
throw e;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const pat = await call('POST', `/management/v1/users/${userId}/pats`, {}, orgId);
|
|
216
|
+
if (typeof pat.token !== 'string' || pat.token === '') {
|
|
217
|
+
throw new Error(`ZITADEL returned no token for machine user ${userId}`);
|
|
218
|
+
}
|
|
219
|
+
const patId = typeof pat.tokenId === 'string' && pat.tokenId !== '' ? pat.tokenId : undefined;
|
|
220
|
+
// Only the login-client role's caller ever reads `patId` (the round-2
|
|
221
|
+
// concurrent-apply guard) -- the admin path never required a PAT id
|
|
222
|
+
// before #216 round 2 and still does not, so this never turns a response
|
|
223
|
+
// the admin path already accepted into a new failure.
|
|
224
|
+
if (role === 'IAM_LOGIN_CLIENT' && !patId) {
|
|
225
|
+
throw new Error(`ZITADEL returned no token id for machine user ${userId}`);
|
|
226
|
+
}
|
|
227
|
+
return { userId, token: pat.token, patId: patId ?? '', revokedPriorPat };
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
mintAdminMachineUser: (orgId, input) => mintMachineUser(orgId, input, 'IAM_OWNER').then(({ userId, token }) => ({ userId, token })),
|
|
231
|
+
mintLoginClientMachineUser: (orgId, input) => mintMachineUser(orgId, input, 'IAM_LOGIN_CLIENT'),
|
|
232
|
+
// #216 round 2: lets the caller (`platform-providers.ts`) re-check, after
|
|
233
|
+
// delivering a freshly minted token, that it is still live and alone on
|
|
234
|
+
// the user -- see `ZitadelPort.listLoginClientPats`'s own doc comment for
|
|
235
|
+
// the race this closes.
|
|
236
|
+
listLoginClientPats: (orgId, userId) => listPats(orgId, userId),
|
|
237
|
+
};
|
|
238
|
+
}
|
package/dist/plan.d.ts
CHANGED
|
@@ -35,6 +35,12 @@ export interface PlanStep {
|
|
|
35
35
|
*/
|
|
36
36
|
export interface UnmanagedItem {
|
|
37
37
|
description: string;
|
|
38
|
+
/**
|
|
39
|
+
* Set on a platform service that is blocked, not planned: the service's name
|
|
40
|
+
* (`auth`, `mail`, `integrations`, `files`, `ai`, `jobs`, `archon`, `sfu`).
|
|
41
|
+
* Absent on a live-but-undeclared resource.
|
|
42
|
+
*/
|
|
43
|
+
key?: string;
|
|
38
44
|
}
|
|
39
45
|
/**
|
|
40
46
|
* The diff between a {@link DeclaredOrg} and live reality, computed by the
|