@wtfalch/foundry 0.33.1 → 0.35.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/bootstrap/bootstrap.d.ts +1 -1
- package/dist/bootstrap/bootstrap.js +49 -11
- package/dist/bootstrap/declare.d.ts +12 -5
- package/dist/bootstrap/declare.js +33 -18
- package/dist/bootstrap/versions.d.ts +24 -0
- package/dist/bootstrap/versions.js +46 -0
- package/dist/calls-sfu.d.ts +10 -0
- package/dist/calls-sfu.js +44 -3
- package/dist/index.d.ts +21 -8
- package/dist/index.js +11 -4
- package/dist/migrations/0051_org_services.sql +13 -0
- package/dist/migrations/0052_platform_mail_relay.sql +8 -0
- package/dist/migrations/0053_heartbeats_provider.sql +9 -0
- package/dist/migrations/0053_org_version_pin.sql +6 -0
- package/dist/org.d.ts +76 -12
- package/dist/org.js +136 -20
- package/dist/plan/backup-gate.d.ts +93 -0
- package/dist/plan/backup-gate.js +261 -0
- package/dist/plan/broker-entries.d.ts +9 -1
- package/dist/plan/broker-entries.js +20 -3
- package/dist/plan/build-record.d.ts +56 -0
- package/dist/plan/build-record.js +109 -0
- package/dist/plan/calls-matrix.d.ts +9 -0
- package/dist/plan/calls-matrix.js +18 -1
- package/dist/plan/cloudflared-connector.d.ts +7 -1
- package/dist/plan/cloudflared-connector.js +15 -2
- package/dist/plan/company-backups.d.ts +31 -0
- package/dist/plan/company-backups.js +101 -0
- package/dist/plan/deploy-health.d.ts +12 -2
- package/dist/plan/deploy-health.js +3 -1
- package/dist/plan/destinations.d.ts +73 -0
- package/dist/plan/destinations.js +217 -0
- package/dist/plan/diff.d.ts +46 -8
- package/dist/plan/diff.js +367 -126
- package/dist/plan/edge-access.d.ts +15 -0
- package/dist/plan/edge-access.js +21 -2
- package/dist/plan/executor.d.ts +276 -5
- package/dist/plan/executor.js +1111 -109
- package/dist/plan/health-check-settings.d.ts +28 -0
- package/dist/plan/health-check-settings.js +36 -0
- package/dist/plan/health-gaps.d.ts +33 -0
- package/dist/plan/health-gaps.js +89 -0
- package/dist/plan/health-targets.d.ts +1 -3
- package/dist/plan/health-targets.js +36 -36
- package/dist/plan/held-key-refs.d.ts +15 -0
- package/dist/plan/held-key-refs.js +15 -0
- package/dist/plan/hostnames.d.ts +10 -0
- package/dist/plan/hostnames.js +14 -0
- package/dist/plan/index.d.ts +19 -6
- package/dist/plan/index.js +10 -3
- package/dist/plan/live.d.ts +11 -2
- package/dist/plan/live.js +16 -2
- package/dist/plan/locked-hostnames.d.ts +7 -3
- package/dist/plan/locked-hostnames.js +25 -4
- package/dist/plan/login-host.d.ts +61 -12
- package/dist/plan/login-host.js +99 -11
- package/dist/plan/mail-relay.d.ts +139 -0
- package/dist/plan/mail-relay.js +400 -0
- package/dist/plan/network-move.d.ts +20 -0
- package/dist/plan/network-move.js +41 -0
- package/dist/plan/oikos-ingress.d.ts +111 -0
- package/dist/plan/oikos-ingress.js +231 -0
- package/dist/plan/org-names.d.ts +30 -0
- package/dist/plan/org-names.js +108 -0
- package/dist/plan/own-diff.d.ts +170 -0
- package/dist/plan/own-diff.js +440 -0
- package/dist/plan/own-dns.d.ts +6 -0
- package/dist/plan/own-dns.js +15 -0
- package/dist/plan/own-hostnames.d.ts +27 -0
- package/dist/plan/own-hostnames.js +24 -0
- package/dist/plan/own-limits.d.ts +213 -0
- package/dist/plan/own-limits.js +232 -0
- package/dist/plan/own-services.d.ts +193 -0
- package/dist/plan/own-services.js +169 -0
- package/dist/plan/own-sfu.d.ts +69 -0
- package/dist/plan/own-sfu.js +256 -0
- package/dist/plan/plan-apply.d.ts +121 -10
- package/dist/plan/plan-apply.js +357 -18
- package/dist/plan/platform-diff.d.ts +155 -11
- package/dist/plan/platform-diff.js +271 -66
- package/dist/plan/platform-org.d.ts +107 -4
- package/dist/plan/platform-org.js +175 -3
- package/dist/plan/platform-providers.d.ts +474 -81
- package/dist/plan/platform-providers.js +1407 -560
- package/dist/plan/proxy-labels.d.ts +57 -11
- package/dist/plan/proxy-labels.js +94 -14
- package/dist/plan/run-options.d.ts +19 -0
- package/dist/plan/run-options.js +46 -0
- package/dist/plan/sign-in-org.d.ts +1 -1
- package/dist/plan/sign-in-org.js +20 -2
- package/dist/plan/zitadel-login-policy.d.ts +1 -1
- package/dist/plan/zitadel-login-policy.js +77 -9
- package/dist/plan/zitadel-system-api.d.ts +22 -1
- package/dist/plan/zitadel-system-api.js +49 -8
- package/dist/platform.d.ts +40 -0
- package/dist/platform.js +16 -0
- package/dist/provision.d.ts +2 -0
- package/dist/provision.js +3 -1
- package/dist/slug.d.ts +3 -1
- package/dist/slug.js +26 -3
- package/dist/store/postgres.js +93 -2
- package/dist/store/schema.d.ts +87 -0
- package/dist/store/schema.js +9 -0
- package/package.json +3 -3
- package/versions.digests.json +11 -0
- package/versions.json +3 -1
|
@@ -8,22 +8,55 @@ export interface EntryLabelSpec {
|
|
|
8
8
|
wildcardDomain: string | null;
|
|
9
9
|
port: number;
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* The label every routed container carries, with the install's token as its value. Traefik is told
|
|
13
|
+
* (`--providers.docker.constraints=Label(`foundry.edge`,`<token>`)`) to route only containers that carry
|
|
14
|
+
* it, so a route an image declares in its own `LABEL`s, which cannot know the token, is ignored.
|
|
15
|
+
*/
|
|
16
|
+
export declare const EDGE_LABEL = "foundry.edge";
|
|
11
17
|
/**
|
|
12
18
|
* The scope key holding the hash of the label set last patched onto an
|
|
13
19
|
* entry's app. Present only for an entry that has had a custom label set.
|
|
14
20
|
*/
|
|
15
21
|
export declare function appLabelsAppliedScopeKey(entryKey: string): string;
|
|
22
|
+
/** The entry routes ({@link entryRoutesMarker}) the company tunnel's ingress was last written with. */
|
|
23
|
+
export declare const CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY = "cloudflare_tunnel_entry_routes";
|
|
24
|
+
/**
|
|
25
|
+
* The entry names in the platform's oikos.host zone ({@link entryOikosHostnames}) the company tunnel's records were
|
|
26
|
+
* last written for, comma-joined. Recorded only once every record is in place.
|
|
27
|
+
*/
|
|
28
|
+
export declare const CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY = "cloudflare_tunnel_oikos_hostnames";
|
|
29
|
+
/**
|
|
30
|
+
* The run label every customer hosting-entry container gets. A run label overrides the image's own
|
|
31
|
+
* `LABEL` of the same key, so Traefik ignores the container whatever its image says: a customer image
|
|
32
|
+
* cannot route a hostname it does not own. The company's tunnel reaches the container directly
|
|
33
|
+
* ({@link entryRoutes}); no Traefik router and no Coolify `domains` are involved.
|
|
34
|
+
*/
|
|
35
|
+
export declare const ENTRY_CONTAINER_LABELS = "traefik.enable=false";
|
|
36
|
+
/**
|
|
37
|
+
* What an entry's routing is built from: the names it is served at, the wildcard domain when it
|
|
38
|
+
* declares `'*'`, and the port its container listens on. Every entry has one, because every entry
|
|
39
|
+
* carries {@link ENTRY_CONTAINER_LABELS}. Coolify's default port for an app is 80.
|
|
40
|
+
*/
|
|
41
|
+
export declare function entryLabelSpec(org: DeclaredOrg, entry: HostingEntry): EntryLabelSpec;
|
|
42
|
+
/**
|
|
43
|
+
* The company tunnel's ingress for the hosting entries: each name, and the wildcard last, goes straight to
|
|
44
|
+
* `http://<container name>:<port>` on the company network, never through `coolify-proxy`. A wildcard
|
|
45
|
+
* entry gets a wildcard hostname rule; the first entry that declares `'*'` owns it, and the first entry
|
|
46
|
+
* that declares a name owns it. An entry with no name (internal) has no rule. This holds for a company
|
|
47
|
+
* without a domain too: its `<slug>.oikos.host` name is routed by the company's own tunnel
|
|
48
|
+
* ({@link entryOikosHostnames}), not by the platform's shared ingress.
|
|
49
|
+
*/
|
|
50
|
+
export declare function entryRoutes(org: DeclaredOrg): Array<{
|
|
51
|
+
hostname: string;
|
|
52
|
+
service: string;
|
|
53
|
+
}>;
|
|
54
|
+
/** The scope value recording the routes the tunnel step last wrote; the planner compares it to {@link entryRoutes}. */
|
|
55
|
+
export declare function entryRoutesMarker(org: DeclaredOrg): string;
|
|
16
56
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* set, so an entry with `'*'` carries its exact `Host` rules as well. An entry
|
|
20
|
-
* that already has a custom set (`hadLabels`) keeps one, without the wildcard
|
|
21
|
-
* rule, so a removed `'*'` stops matching and a later name change still
|
|
22
|
-
* reaches the app: an empty `custom_labels` is refused by Coolify's API.
|
|
23
|
-
* Coolify's default port for an app is 80.
|
|
57
|
+
* A short digest of the spec; the same spec always gives the same hash. The token is left out: the
|
|
58
|
+
* planner has none, and a changed token reaches a live app through the comparison of its live labels.
|
|
24
59
|
*/
|
|
25
|
-
export declare function entryLabelSpec(org: DeclaredOrg, entry: HostingEntry, hadLabels: boolean): EntryLabelSpec | null;
|
|
26
|
-
/** A short digest of the spec; the same spec always gives the same hash. */
|
|
27
60
|
export declare function entryLabelHash(spec: EntryLabelSpec): string;
|
|
28
61
|
/**
|
|
29
62
|
* The Traefik labels for an entry's app, one per line. Coolify reaches an app
|
|
@@ -32,11 +65,24 @@ export declare function entryLabelHash(spec: EntryLabelSpec): string;
|
|
|
32
65
|
* `Host(name) && PathPrefix(/)`, default priority. The wildcard is one
|
|
33
66
|
* `HostRegexp` router at `priority=1`, so any exact name, this app's or
|
|
34
67
|
* another's, wins over it. The regex matches one label (`[a-z0-9-]+`), the
|
|
35
|
-
* same depth Cloudflare's Universal certificate covers.
|
|
68
|
+
* same depth Cloudflare's Universal certificate covers. A set that routes
|
|
69
|
+
* anything carries {@link EDGE_LABEL}; the "no route" set does not need it.
|
|
36
70
|
*/
|
|
37
|
-
export declare function entryProxyLabels(appUuid: string, spec: EntryLabelSpec): string;
|
|
71
|
+
export declare function entryProxyLabels(appUuid: string, spec: EntryLabelSpec, token: string): string;
|
|
72
|
+
/** Whether a label set routes something but does not carry the token: Traefik would ignore it once the constraint is on. */
|
|
73
|
+
export declare function routesWithoutToken(labels: string, token: string): boolean;
|
|
38
74
|
/**
|
|
39
75
|
* Whether two label sets hold the same labels. Order and the `coolify.*`
|
|
40
76
|
* lines Coolify adds and strips itself do not count.
|
|
41
77
|
*/
|
|
42
78
|
export declare function sameLabelSets(a: string, b: string): boolean;
|
|
79
|
+
/**
|
|
80
|
+
* The entry names that live in the platform's `oikos.host` zone (a company with no domain of its own, or with
|
|
81
|
+
* `usesDomain` off): the names the company's tunnel gets a proxied CNAME for. Sorted.
|
|
82
|
+
*/
|
|
83
|
+
export declare function entryOikosHostnames(org: DeclaredOrg): string[];
|
|
84
|
+
/**
|
|
85
|
+
* Whether `name` is one the company may write in the platform's `oikos.host` zone: exactly its own
|
|
86
|
+
* `<slug>.oikos.host`. A company writes no other company's name, whatever an entry declares.
|
|
87
|
+
*/
|
|
88
|
+
export declare function ownsOikosName(org: DeclaredOrg, name: string): boolean;
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { entryHostnames, wildcardHostname } from './hostnames.js';
|
|
3
|
+
import { NO_DOMAIN_ZONE } from './own-hostnames.js';
|
|
4
|
+
import { appNameFor } from './platform-diff.js';
|
|
5
|
+
/**
|
|
6
|
+
* The label every routed container carries, with the install's token as its value. Traefik is told
|
|
7
|
+
* (`--providers.docker.constraints=Label(`foundry.edge`,`<token>`)`) to route only containers that carry
|
|
8
|
+
* it, so a route an image declares in its own `LABEL`s, which cannot know the token, is ignored.
|
|
9
|
+
*/
|
|
10
|
+
export const EDGE_LABEL = 'foundry.edge';
|
|
11
|
+
/** The token is a base64url string; anything else would not survive a label line or Traefik's constraint syntax. */
|
|
12
|
+
const TOKEN_SHAPE = /^[A-Za-z0-9_-]{32,128}$/;
|
|
13
|
+
function checkToken(token) {
|
|
14
|
+
if (!TOKEN_SHAPE.test(token))
|
|
15
|
+
throw new Error('proxy labels: the label token is malformed');
|
|
16
|
+
}
|
|
3
17
|
/**
|
|
4
18
|
* The scope key holding the hash of the label set last patched onto an
|
|
5
19
|
* entry's app. Present only for an entry that has had a custom label set.
|
|
@@ -7,26 +21,68 @@ import { entryHostnames, wildcardHostname } from './hostnames.js';
|
|
|
7
21
|
export function appLabelsAppliedScopeKey(entryKey) {
|
|
8
22
|
return `app_labels_applied.${entryKey}`;
|
|
9
23
|
}
|
|
24
|
+
/** The entry routes ({@link entryRoutesMarker}) the company tunnel's ingress was last written with. */
|
|
25
|
+
export const CLOUDFLARE_TUNNEL_ENTRY_ROUTES_SCOPE_KEY = 'cloudflare_tunnel_entry_routes';
|
|
10
26
|
/**
|
|
11
|
-
* The
|
|
12
|
-
*
|
|
13
|
-
* set, so an entry with `'*'` carries its exact `Host` rules as well. An entry
|
|
14
|
-
* that already has a custom set (`hadLabels`) keeps one, without the wildcard
|
|
15
|
-
* rule, so a removed `'*'` stops matching and a later name change still
|
|
16
|
-
* reaches the app: an empty `custom_labels` is refused by Coolify's API.
|
|
17
|
-
* Coolify's default port for an app is 80.
|
|
27
|
+
* The entry names in the platform's oikos.host zone ({@link entryOikosHostnames}) the company tunnel's records were
|
|
28
|
+
* last written for, comma-joined. Recorded only once every record is in place.
|
|
18
29
|
*/
|
|
19
|
-
export
|
|
30
|
+
export const CLOUDFLARE_TUNNEL_OIKOS_HOSTNAMES_SCOPE_KEY = 'cloudflare_tunnel_oikos_hostnames';
|
|
31
|
+
/**
|
|
32
|
+
* The run label every customer hosting-entry container gets. A run label overrides the image's own
|
|
33
|
+
* `LABEL` of the same key, so Traefik ignores the container whatever its image says: a customer image
|
|
34
|
+
* cannot route a hostname it does not own. The company's tunnel reaches the container directly
|
|
35
|
+
* ({@link entryRoutes}); no Traefik router and no Coolify `domains` are involved.
|
|
36
|
+
*/
|
|
37
|
+
export const ENTRY_CONTAINER_LABELS = 'traefik.enable=false';
|
|
38
|
+
/**
|
|
39
|
+
* What an entry's routing is built from: the names it is served at, the wildcard domain when it
|
|
40
|
+
* declares `'*'`, and the port its container listens on. Every entry has one, because every entry
|
|
41
|
+
* carries {@link ENTRY_CONTAINER_LABELS}. Coolify's default port for an app is 80.
|
|
42
|
+
*/
|
|
43
|
+
export function entryLabelSpec(org, entry) {
|
|
20
44
|
const wildcard = entry.hostnames.includes('*') ? wildcardHostname(org) : null;
|
|
21
|
-
if (!wildcard && !hadLabels)
|
|
22
|
-
return null;
|
|
23
45
|
return {
|
|
24
46
|
names: entryHostnames(org, entry),
|
|
25
47
|
wildcardDomain: wildcard ? wildcard.slice(2) : null,
|
|
26
48
|
port: entry.buildPack === 'static' ? 80 : (entry.port ?? 80),
|
|
27
49
|
};
|
|
28
50
|
}
|
|
29
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* The company tunnel's ingress for the hosting entries: each name, and the wildcard last, goes straight to
|
|
53
|
+
* `http://<container name>:<port>` on the company network, never through `coolify-proxy`. A wildcard
|
|
54
|
+
* entry gets a wildcard hostname rule; the first entry that declares `'*'` owns it, and the first entry
|
|
55
|
+
* that declares a name owns it. An entry with no name (internal) has no rule. This holds for a company
|
|
56
|
+
* without a domain too: its `<slug>.oikos.host` name is routed by the company's own tunnel
|
|
57
|
+
* ({@link entryOikosHostnames}), not by the platform's shared ingress.
|
|
58
|
+
*/
|
|
59
|
+
export function entryRoutes(org) {
|
|
60
|
+
if (!org.hosting.enabled)
|
|
61
|
+
return [];
|
|
62
|
+
const routes = [];
|
|
63
|
+
let wildcard;
|
|
64
|
+
for (const entry of org.hosting.entries) {
|
|
65
|
+
const service = `http://${appNameFor(org, entry)}:${entryLabelSpec(org, entry).port}`;
|
|
66
|
+
for (const hostname of entryHostnames(org, entry)) {
|
|
67
|
+
if (!routes.some((r) => r.hostname === hostname))
|
|
68
|
+
routes.push({ hostname, service });
|
|
69
|
+
}
|
|
70
|
+
const wildcardName = entry.hostnames.includes('*') ? wildcardHostname(org) : null;
|
|
71
|
+
if (wildcardName && !wildcard)
|
|
72
|
+
wildcard = { hostname: wildcardName, service };
|
|
73
|
+
}
|
|
74
|
+
if (wildcard)
|
|
75
|
+
routes.push(wildcard);
|
|
76
|
+
return routes;
|
|
77
|
+
}
|
|
78
|
+
/** The scope value recording the routes the tunnel step last wrote; the planner compares it to {@link entryRoutes}. */
|
|
79
|
+
export function entryRoutesMarker(org) {
|
|
80
|
+
return JSON.stringify(entryRoutes(org));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A short digest of the spec; the same spec always gives the same hash. The token is left out: the
|
|
84
|
+
* planner has none, and a changed token reaches a live app through the comparison of its live labels.
|
|
85
|
+
*/
|
|
30
86
|
export function entryLabelHash(spec) {
|
|
31
87
|
return createHash('sha256').update(JSON.stringify(spec)).digest('hex').slice(0, 16);
|
|
32
88
|
}
|
|
@@ -37,9 +93,11 @@ export function entryLabelHash(spec) {
|
|
|
37
93
|
* `Host(name) && PathPrefix(/)`, default priority. The wildcard is one
|
|
38
94
|
* `HostRegexp` router at `priority=1`, so any exact name, this app's or
|
|
39
95
|
* another's, wins over it. The regex matches one label (`[a-z0-9-]+`), the
|
|
40
|
-
* same depth Cloudflare's Universal certificate covers.
|
|
96
|
+
* same depth Cloudflare's Universal certificate covers. A set that routes
|
|
97
|
+
* anything carries {@link EDGE_LABEL}; the "no route" set does not need it.
|
|
41
98
|
*/
|
|
42
|
-
export function entryProxyLabels(appUuid, spec) {
|
|
99
|
+
export function entryProxyLabels(appUuid, spec, token) {
|
|
100
|
+
checkToken(token);
|
|
43
101
|
const labels = [];
|
|
44
102
|
const route = (id, rule, priority) => {
|
|
45
103
|
const router = `traefik.http.routers.${id}`;
|
|
@@ -56,9 +114,14 @@ export function entryProxyLabels(appUuid, spec) {
|
|
|
56
114
|
}
|
|
57
115
|
if (labels.length === 0)
|
|
58
116
|
return 'traefik.enable=false';
|
|
59
|
-
labels.push('traefik.enable=true', 'traefik.http.middlewares.gzip.compress=true');
|
|
117
|
+
labels.push('traefik.enable=true', 'traefik.http.middlewares.gzip.compress=true', `${EDGE_LABEL}=${token}`);
|
|
60
118
|
return labels.sort().join('\n');
|
|
61
119
|
}
|
|
120
|
+
/** Whether a label set routes something but does not carry the token: Traefik would ignore it once the constraint is on. */
|
|
121
|
+
export function routesWithoutToken(labels, token) {
|
|
122
|
+
const lines = labels.split(/\r\n|\n|\r/);
|
|
123
|
+
return lines.includes('traefik.enable=true') && !lines.includes(`${EDGE_LABEL}=${token}`);
|
|
124
|
+
}
|
|
62
125
|
/**
|
|
63
126
|
* Whether two label sets hold the same labels. Order and the `coolify.*`
|
|
64
127
|
* lines Coolify adds and strips itself do not count.
|
|
@@ -71,3 +134,20 @@ export function sameLabelSets(a, b) {
|
|
|
71
134
|
.join('\n');
|
|
72
135
|
return lines(a) === lines(b);
|
|
73
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* The entry names that live in the platform's `oikos.host` zone (a company with no domain of its own, or with
|
|
139
|
+
* `usesDomain` off): the names the company's tunnel gets a proxied CNAME for. Sorted.
|
|
140
|
+
*/
|
|
141
|
+
export function entryOikosHostnames(org) {
|
|
142
|
+
return entryRoutes(org)
|
|
143
|
+
.map((r) => r.hostname)
|
|
144
|
+
.filter((h) => h.endsWith(`.${NO_DOMAIN_ZONE}`))
|
|
145
|
+
.sort();
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Whether `name` is one the company may write in the platform's `oikos.host` zone: exactly its own
|
|
149
|
+
* `<slug>.oikos.host`. A company writes no other company's name, whatever an entry declares.
|
|
150
|
+
*/
|
|
151
|
+
export function ownsOikosName(org, name) {
|
|
152
|
+
return name.toLowerCase() === `${org.slug}.${NO_DOMAIN_ZONE}`;
|
|
153
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Linux capabilities of every container foundry makes through an application call, sent in
|
|
3
|
+
* Coolify's `custom_docker_run_options`. Coolify turns `--cap-drop`, `--cap-add` and `--security-opt`
|
|
4
|
+
* into compose keys; its parser reads a value up to the first dash, so `no-new-privileges:true` is
|
|
5
|
+
* not sent. A Coolify database has no such field and keeps Docker's defaults.
|
|
6
|
+
*/
|
|
7
|
+
/** What no foundry service needs: raw sockets, device nodes, file capabilities, chroot, audit writes. */
|
|
8
|
+
export declare const CAPS_TIER_A: readonly string[];
|
|
9
|
+
/** A customer's own app: nothing, then back what nginx, node and `gosu` style entrypoints need. */
|
|
10
|
+
export declare const CAPS_TIER_B: readonly string[];
|
|
11
|
+
export type CapabilityTier = 'A' | 'B';
|
|
12
|
+
/**
|
|
13
|
+
* `existing` with its capability options replaced by the tier's. Everything else in it (the
|
|
14
|
+
* `--entrypoint "..."`) stays as it was, so a step that sends its own entrypoint and the capability
|
|
15
|
+
* wrapper can both write the field. Running it twice gives the same string.
|
|
16
|
+
*/
|
|
17
|
+
export declare function withCapabilities(existing: string | null | undefined, tier: CapabilityTier): string;
|
|
18
|
+
/** Whether `existing` holds exactly the tier's capability options. */
|
|
19
|
+
export declare function hasCapabilities(existing: string | null | undefined, tier: CapabilityTier): boolean;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Linux capabilities of every container foundry makes through an application call, sent in
|
|
3
|
+
* Coolify's `custom_docker_run_options`. Coolify turns `--cap-drop`, `--cap-add` and `--security-opt`
|
|
4
|
+
* into compose keys; its parser reads a value up to the first dash, so `no-new-privileges:true` is
|
|
5
|
+
* not sent. A Coolify database has no such field and keeps Docker's defaults.
|
|
6
|
+
*/
|
|
7
|
+
/** What no foundry service needs: raw sockets, device nodes, file capabilities, chroot, audit writes. */
|
|
8
|
+
export const CAPS_TIER_A = [
|
|
9
|
+
'--cap-drop=NET_RAW',
|
|
10
|
+
'--cap-drop=MKNOD',
|
|
11
|
+
'--cap-drop=SETFCAP',
|
|
12
|
+
'--cap-drop=SYS_CHROOT',
|
|
13
|
+
'--cap-drop=AUDIT_WRITE',
|
|
14
|
+
];
|
|
15
|
+
/** A customer's own app: nothing, then back what nginx, node and `gosu` style entrypoints need. */
|
|
16
|
+
export const CAPS_TIER_B = [
|
|
17
|
+
'--cap-drop=ALL',
|
|
18
|
+
'--cap-add=CHOWN',
|
|
19
|
+
'--cap-add=DAC_OVERRIDE',
|
|
20
|
+
'--cap-add=FOWNER',
|
|
21
|
+
'--cap-add=SETGID',
|
|
22
|
+
'--cap-add=SETUID',
|
|
23
|
+
'--cap-add=NET_BIND_SERVICE',
|
|
24
|
+
];
|
|
25
|
+
const tokens = (tier) => tier === 'B' ? CAPS_TIER_B : CAPS_TIER_A;
|
|
26
|
+
/** One `--cap-drop` or `--cap-add` option, with the space or equals sign and its value. */
|
|
27
|
+
const CAP_OPTION = /\s*--cap-(?:drop|add)(?:=|\s+)\S+/g;
|
|
28
|
+
/** The cap options of a run options string, in the order they stand. */
|
|
29
|
+
function capOptionsOf(options) {
|
|
30
|
+
return (options ?? '').match(CAP_OPTION)?.map((o) => o.trim().replace(/\s+/, '=')) ?? [];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* `existing` with its capability options replaced by the tier's. Everything else in it (the
|
|
34
|
+
* `--entrypoint "..."`) stays as it was, so a step that sends its own entrypoint and the capability
|
|
35
|
+
* wrapper can both write the field. Running it twice gives the same string.
|
|
36
|
+
*/
|
|
37
|
+
export function withCapabilities(existing, tier) {
|
|
38
|
+
const rest = (existing ?? '').replace(CAP_OPTION, '').trim();
|
|
39
|
+
return [rest, ...tokens(tier)].filter(Boolean).join(' ');
|
|
40
|
+
}
|
|
41
|
+
/** Whether `existing` holds exactly the tier's capability options. */
|
|
42
|
+
export function hasCapabilities(existing, tier) {
|
|
43
|
+
const have = capOptionsOf(existing);
|
|
44
|
+
const want = tokens(tier);
|
|
45
|
+
return have.length === want.length && want.every((t) => have.includes(t));
|
|
46
|
+
}
|
|
@@ -23,7 +23,7 @@ export declare function signInOrgDomain(org: {
|
|
|
23
23
|
* idempotent. A failure after the add leaves the domain on the org but not
|
|
24
24
|
* primary, and nothing is recorded, so the next run repeats both.
|
|
25
25
|
*/
|
|
26
|
-
export declare function registerSignInDomain(zitadel: Pick<ZitadelPort, 'addOrgDomain' | 'setPrimaryOrgDomain'>, orgId: string, domain: string): Promise<void>;
|
|
26
|
+
export declare function registerSignInDomain(zitadel: Pick<ZitadelPort, 'addOrgDomain' | 'setPrimaryOrgDomain'>, orgId: string, domain: string, sleep?: (ms: number) => Promise<void>): Promise<void>;
|
|
27
27
|
/**
|
|
28
28
|
* {@link registerSignInDomain}, except that a refusal to make the domain
|
|
29
29
|
* primary (the sign-in server's domain policy wants proof) is recorded under
|
package/dist/plan/sign-in-org.js
CHANGED
|
@@ -21,6 +21,12 @@ export function signInDomainBlockedText(orgName, domain) {
|
|
|
21
21
|
function isPrimaryDomainRefused(error) {
|
|
22
22
|
return error instanceof Error && /is not verified/.test(error.message);
|
|
23
23
|
}
|
|
24
|
+
/** True for the providers' "domain is not on org" answer of `setPrimaryOrgDomain`. */
|
|
25
|
+
function isDomainNotListed(error) {
|
|
26
|
+
return error instanceof Error && /is not on org/.test(error.message);
|
|
27
|
+
}
|
|
28
|
+
const DOMAIN_LISTED_TRIES = 10;
|
|
29
|
+
const DOMAIN_LISTED_WAIT_MS = 500;
|
|
24
30
|
/** The domain an org's owners sign in on: its own, or `<slug>.oikos.host` when it has none. */
|
|
25
31
|
export function signInOrgDomain(org) {
|
|
26
32
|
return (org.domain ?? `${org.slug}.oikos.host`).toLowerCase();
|
|
@@ -30,9 +36,21 @@ export function signInOrgDomain(org) {
|
|
|
30
36
|
* idempotent. A failure after the add leaves the domain on the org but not
|
|
31
37
|
* primary, and nothing is recorded, so the next run repeats both.
|
|
32
38
|
*/
|
|
33
|
-
export async function registerSignInDomain(zitadel, orgId, domain) {
|
|
39
|
+
export async function registerSignInDomain(zitadel, orgId, domain, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))) {
|
|
34
40
|
await zitadel.addOrgDomain(orgId, domain);
|
|
35
|
-
|
|
41
|
+
// The sign-in server lists a domain a moment after it accepts the add, and
|
|
42
|
+
// `setPrimaryOrgDomain` reads that list first: ask again, for five seconds.
|
|
43
|
+
for (let attempt = 1;; attempt += 1) {
|
|
44
|
+
try {
|
|
45
|
+
await zitadel.setPrimaryOrgDomain(orgId, domain);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
if (attempt >= DOMAIN_LISTED_TRIES || !isDomainNotListed(error))
|
|
50
|
+
throw error;
|
|
51
|
+
await sleep(DOMAIN_LISTED_WAIT_MS);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
36
54
|
}
|
|
37
55
|
/**
|
|
38
56
|
* {@link registerSignInDomain}, except that a refusal to make the domain
|
|
@@ -20,4 +20,4 @@ export interface ZitadelLoginPolicyOptions {
|
|
|
20
20
|
* policy (POST), so a later change to the default cannot turn registration
|
|
21
21
|
* back on. A custom policy keeps its own factors and IdPs (PUT).
|
|
22
22
|
*/
|
|
23
|
-
export declare function createZitadelLoginPolicy(options: ZitadelLoginPolicyOptions): Pick<ZitadelPort, 'closeSelfRegistration'>;
|
|
23
|
+
export declare function createZitadelLoginPolicy(options: ZitadelLoginPolicyOptions): Pick<ZitadelPort, 'closeSelfRegistration' | 'enforceSecondFactor'>;
|
|
@@ -20,6 +20,21 @@ const LOGIN_POLICY_SETTINGS = [
|
|
|
20
20
|
'forceMfaLocalOnly',
|
|
21
21
|
];
|
|
22
22
|
const CALL_TIMEOUT_MS = 30_000;
|
|
23
|
+
/** What {@link ZitadelPort.enforceSecondFactor} sets. The two lists are exact: any other factor (email or SMS code) is removed. */
|
|
24
|
+
const SECOND_FACTOR_SETTINGS = {
|
|
25
|
+
forceMfa: true,
|
|
26
|
+
allowRegister: false,
|
|
27
|
+
allowExternalIdp: false,
|
|
28
|
+
allowUsernamePassword: true,
|
|
29
|
+
passwordlessType: 'PASSWORDLESS_TYPE_ALLOWED',
|
|
30
|
+
mfaInitSkipLifetime: '0s',
|
|
31
|
+
};
|
|
32
|
+
const SECOND_FACTORS = ['SECOND_FACTOR_TYPE_OTP', 'SECOND_FACTOR_TYPE_U2F'];
|
|
33
|
+
const MULTI_FACTORS = ['MULTI_FACTOR_TYPE_U2F_WITH_VERIFICATION'];
|
|
34
|
+
/** ZITADEL leaves false booleans and zero durations out of a read. */
|
|
35
|
+
const isZeroDuration = (value) => value === undefined || (typeof value === 'string' && /^0+(\.0+)?s$/.test(value));
|
|
36
|
+
const stringList = (value) => Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
|
|
37
|
+
const sameSet = (a, b) => a.length === b.length && b.every((v) => a.includes(v));
|
|
23
38
|
/**
|
|
24
39
|
* The real {@link ZitadelPort.closeSelfRegistration}: the mirror of
|
|
25
40
|
* `ZitadelClient.allowSelfRegistration` in `@wtfalch/providers`, with
|
|
@@ -35,8 +50,7 @@ const CALL_TIMEOUT_MS = 30_000;
|
|
|
35
50
|
*/
|
|
36
51
|
export function createZitadelLoginPolicy(options) {
|
|
37
52
|
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
38
|
-
async function call(method, orgId, body) {
|
|
39
|
-
const path = '/management/v1/policies/login';
|
|
53
|
+
async function call(method, orgId, body, path = '/management/v1/policies/login') {
|
|
40
54
|
const response = await fetchImpl(`${options.url}${path}`, {
|
|
41
55
|
method,
|
|
42
56
|
signal: AbortSignal.timeout(CALL_TIMEOUT_MS),
|
|
@@ -67,22 +81,76 @@ export function createZitadelLoginPolicy(options) {
|
|
|
67
81
|
}
|
|
68
82
|
return data;
|
|
69
83
|
}
|
|
84
|
+
/** Every setting of the policy in force, as the write bodies take them. */
|
|
85
|
+
const settingsOf = (policy) => {
|
|
86
|
+
const settings = {};
|
|
87
|
+
for (const key of LOGIN_POLICY_SETTINGS) {
|
|
88
|
+
if (policy[key] !== undefined)
|
|
89
|
+
settings[key] = policy[key];
|
|
90
|
+
}
|
|
91
|
+
return settings;
|
|
92
|
+
};
|
|
70
93
|
return {
|
|
94
|
+
async enforceSecondFactor(orgId) {
|
|
95
|
+
if (!orgId)
|
|
96
|
+
throw new Error('enforceSecondFactor: orgId is required');
|
|
97
|
+
const data = await call('GET', orgId, undefined);
|
|
98
|
+
const policy = data.policy ?? {};
|
|
99
|
+
const isDefault = data.isDefault === true || policy.isDefault === true;
|
|
100
|
+
const secondFactors = stringList(policy.secondFactors);
|
|
101
|
+
const multiFactors = stringList(policy.multiFactors);
|
|
102
|
+
const want = SECOND_FACTOR_SETTINGS;
|
|
103
|
+
const settingsDiffer = (policy.forceMfa === true) !== want.forceMfa ||
|
|
104
|
+
(policy.allowRegister === true) !== want.allowRegister ||
|
|
105
|
+
(policy.allowExternalIdp === true) !== want.allowExternalIdp ||
|
|
106
|
+
(policy.allowUsernamePassword === true) !== want.allowUsernamePassword ||
|
|
107
|
+
policy.passwordlessType !== want.passwordlessType ||
|
|
108
|
+
!isZeroDuration(policy.mfaInitSkipLifetime);
|
|
109
|
+
const factorsDiffer = !sameSet(secondFactors, SECOND_FACTORS) || !sameSet(multiFactors, MULTI_FACTORS);
|
|
110
|
+
if (!isDefault && !settingsDiffer && !factorsDiffer)
|
|
111
|
+
return { changed: false };
|
|
112
|
+
const settings = { ...settingsOf(policy), ...want };
|
|
113
|
+
if (isDefault) {
|
|
114
|
+
// The org's own policy, written whole: the factor lists go in the same body.
|
|
115
|
+
await call('POST', orgId, {
|
|
116
|
+
...settings,
|
|
117
|
+
secondFactors: SECOND_FACTORS,
|
|
118
|
+
multiFactors: MULTI_FACTORS,
|
|
119
|
+
idps: (policy.idps ?? []).map((idp) => ({
|
|
120
|
+
idpId: idp.idpId,
|
|
121
|
+
ownerType: 'IDP_OWNER_TYPE_SYSTEM',
|
|
122
|
+
})),
|
|
123
|
+
});
|
|
124
|
+
return { changed: true };
|
|
125
|
+
}
|
|
126
|
+
if (settingsDiffer)
|
|
127
|
+
await call('PUT', orgId, settings);
|
|
128
|
+
// Add before remove, so the org is never left with no second factor.
|
|
129
|
+
const factorPath = '/management/v1/policies/login';
|
|
130
|
+
for (const type of SECOND_FACTORS.filter((t) => !secondFactors.includes(t))) {
|
|
131
|
+
await call('POST', orgId, { type }, `${factorPath}/second_factors`);
|
|
132
|
+
}
|
|
133
|
+
for (const type of MULTI_FACTORS.filter((t) => !multiFactors.includes(t))) {
|
|
134
|
+
await call('POST', orgId, { type }, `${factorPath}/multi_factors`);
|
|
135
|
+
}
|
|
136
|
+
for (const type of secondFactors.filter((t) => !SECOND_FACTORS.includes(t))) {
|
|
137
|
+
await call('DELETE', orgId, undefined, `${factorPath}/second_factors/${encodeURIComponent(type)}`);
|
|
138
|
+
}
|
|
139
|
+
for (const type of multiFactors.filter((t) => !MULTI_FACTORS.includes(t))) {
|
|
140
|
+
await call('DELETE', orgId, undefined, `${factorPath}/multi_factors/${encodeURIComponent(type)}`);
|
|
141
|
+
}
|
|
142
|
+
return { changed: true };
|
|
143
|
+
},
|
|
71
144
|
async closeSelfRegistration(orgId) {
|
|
72
145
|
if (!orgId)
|
|
73
146
|
throw new Error('closeSelfRegistration: orgId is required');
|
|
74
147
|
const data = await call('GET', orgId, undefined);
|
|
75
148
|
const policy = data.policy ?? {};
|
|
76
149
|
const isDefault = data.isDefault === true || policy.isDefault === true;
|
|
77
|
-
if (!isDefault && policy.allowRegister
|
|
150
|
+
if (!isDefault && policy.allowRegister !== true) {
|
|
78
151
|
return { changed: false };
|
|
79
152
|
}
|
|
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;
|
|
153
|
+
const settings = { ...settingsOf(policy), allowRegister: false };
|
|
86
154
|
if (isDefault) {
|
|
87
155
|
await call('POST', orgId, {
|
|
88
156
|
...settings,
|
|
@@ -36,13 +36,32 @@ export declare class CreateInstanceError extends Error {
|
|
|
36
36
|
constructor(kind: 'rejected' | 'exists' | 'uncertain', message: string);
|
|
37
37
|
}
|
|
38
38
|
/** What a person does when an instance exists without a delivered bearer. Nothing is deleted automatically. */
|
|
39
|
-
export declare function instanceWithoutBearerMessage(hostname: string, detail: string): string;
|
|
39
|
+
export declare function instanceWithoutBearerMessage(hostname: string, detail: string, label?: string): string;
|
|
40
40
|
export interface CreateInstanceRuntime {
|
|
41
41
|
fetch?: typeof fetch;
|
|
42
42
|
now: () => number;
|
|
43
43
|
/** Fetch timeout for the one create call (default 30s). */
|
|
44
44
|
timeoutMs?: number;
|
|
45
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Waits until ZITADEL's system API answers, before the one create call. `/debug/healthz` and `/debug/ready`
|
|
48
|
+
* answer as soon as the HTTP server listens, but the gateway behind the system API reaches its gRPC server a
|
|
49
|
+
* moment later and answers 503 until then (seen: `POST /instances/_create` 503 about 100 ms after "server is
|
|
50
|
+
* listening"). A 503 on the create leaves the instance marked as possibly created, so the wait asks first with a
|
|
51
|
+
* read that changes nothing: `POST /system/v1/instances/_search`. Any answer other than a failed request or a
|
|
52
|
+
* 5xx counts (a 401 is the key being refused, which the create reports itself). Times out with an error that
|
|
53
|
+
* names the last answer.
|
|
54
|
+
*/
|
|
55
|
+
export declare function waitForSystemApi(input: {
|
|
56
|
+
customDomain: string;
|
|
57
|
+
privateKey: string;
|
|
58
|
+
label?: string;
|
|
59
|
+
}, runtime: CreateInstanceRuntime & {
|
|
60
|
+
sleep: (ms: number) => Promise<void>;
|
|
61
|
+
timeoutMs?: number;
|
|
62
|
+
intervalMs?: number;
|
|
63
|
+
requestTimeoutMs?: number;
|
|
64
|
+
}): Promise<void>;
|
|
46
65
|
/**
|
|
47
66
|
* Creates the platform's ZITADEL instance on `customDomain` with one machine
|
|
48
67
|
* owner and returns its id and the owner's PAT. Sends exactly one request,
|
|
@@ -54,6 +73,8 @@ export declare function createZitadelInstance(input: {
|
|
|
54
73
|
privateKey: string;
|
|
55
74
|
instanceName: string;
|
|
56
75
|
firstOrgName: string;
|
|
76
|
+
/** Names the target in error texts (default `platform:auth`). */
|
|
77
|
+
label?: string;
|
|
57
78
|
owner: {
|
|
58
79
|
userName: string;
|
|
59
80
|
name: string;
|
|
@@ -61,8 +61,48 @@ export class CreateInstanceError extends Error {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
/** What a person does when an instance exists without a delivered bearer. Nothing is deleted automatically. */
|
|
64
|
-
export function instanceWithoutBearerMessage(hostname, detail) {
|
|
65
|
-
return
|
|
64
|
+
export function instanceWithoutBearerMessage(hostname, detail, label = 'platform:auth') {
|
|
65
|
+
return `${label}: a ZITADEL instance may already exist for ${hostname} without a delivered bearer (${detail}). To recover, delete the instance with the system API (DELETE /system/v1/instances/{id}) or redeploy the ZITADEL database, remove the recorded key zitadel_platform_instance_undelivered, then apply again`;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Waits until ZITADEL's system API answers, before the one create call. `/debug/healthz` and `/debug/ready`
|
|
69
|
+
* answer as soon as the HTTP server listens, but the gateway behind the system API reaches its gRPC server a
|
|
70
|
+
* moment later and answers 503 until then (seen: `POST /instances/_create` 503 about 100 ms after "server is
|
|
71
|
+
* listening"). A 503 on the create leaves the instance marked as possibly created, so the wait asks first with a
|
|
72
|
+
* read that changes nothing: `POST /system/v1/instances/_search`. Any answer other than a failed request or a
|
|
73
|
+
* 5xx counts (a 401 is the key being refused, which the create reports itself). Times out with an error that
|
|
74
|
+
* names the last answer.
|
|
75
|
+
*/
|
|
76
|
+
export async function waitForSystemApi(input, runtime) {
|
|
77
|
+
const fetchImpl = runtime.fetch ?? globalThis.fetch;
|
|
78
|
+
const url = `https://${input.customDomain}`;
|
|
79
|
+
const timeout = runtime.timeoutMs ?? 300_000;
|
|
80
|
+
const interval = runtime.intervalMs ?? 2_000;
|
|
81
|
+
const deadline = runtime.now() + timeout;
|
|
82
|
+
let last = 'no answer';
|
|
83
|
+
for (;;) {
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetchImpl(`${url}/system/v1/instances/_search`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
signal: AbortSignal.timeout(runtime.requestTimeoutMs ?? 10_000),
|
|
88
|
+
headers: {
|
|
89
|
+
authorization: `Bearer ${signSystemApiJwt(input.privateKey, url, runtime.now())}`,
|
|
90
|
+
'content-type': 'application/json',
|
|
91
|
+
},
|
|
92
|
+
body: '{}',
|
|
93
|
+
});
|
|
94
|
+
if (response.status < 500)
|
|
95
|
+
return;
|
|
96
|
+
last = `HTTP ${response.status}`;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
last = 'the request failed';
|
|
100
|
+
}
|
|
101
|
+
if (runtime.now() >= deadline) {
|
|
102
|
+
throw new Error(`${input.label ?? 'platform:auth'}: ZITADEL's system API did not answer at ${url} within ${Math.round(timeout / 1000)}s (${last}); nothing was created, retry this apply`);
|
|
103
|
+
}
|
|
104
|
+
await runtime.sleep(interval);
|
|
105
|
+
}
|
|
66
106
|
}
|
|
67
107
|
/**
|
|
68
108
|
* Creates the platform's ZITADEL instance on `customDomain` with one machine
|
|
@@ -73,6 +113,7 @@ export function instanceWithoutBearerMessage(hostname, detail) {
|
|
|
73
113
|
export async function createZitadelInstance(input, runtime) {
|
|
74
114
|
const fetchImpl = runtime.fetch ?? globalThis.fetch;
|
|
75
115
|
const url = `https://${input.customDomain}`;
|
|
116
|
+
const label = input.label ?? 'platform:auth';
|
|
76
117
|
let response;
|
|
77
118
|
try {
|
|
78
119
|
response = await fetchImpl(`${url}/system/v1/instances/_create`, {
|
|
@@ -97,19 +138,19 @@ export async function createZitadelInstance(input, runtime) {
|
|
|
97
138
|
});
|
|
98
139
|
}
|
|
99
140
|
catch {
|
|
100
|
-
throw new CreateInstanceError('uncertain', instanceWithoutBearerMessage(input.customDomain, 'the create request timed out or failed'));
|
|
141
|
+
throw new CreateInstanceError('uncertain', instanceWithoutBearerMessage(input.customDomain, 'the create request timed out or failed', label));
|
|
101
142
|
}
|
|
102
143
|
if (response.status === 401) {
|
|
103
|
-
throw new CreateInstanceError('rejected',
|
|
144
|
+
throw new CreateInstanceError('rejected', `${label}: system API key rejected (HTTP 401); nothing was created`);
|
|
104
145
|
}
|
|
105
146
|
if (response.status === 409) {
|
|
106
|
-
throw new CreateInstanceError('exists', instanceWithoutBearerMessage(input.customDomain, 'ZITADEL answered 409, already exists'));
|
|
147
|
+
throw new CreateInstanceError('exists', instanceWithoutBearerMessage(input.customDomain, 'ZITADEL answered 409, already exists', label));
|
|
107
148
|
}
|
|
108
149
|
if (!response.ok) {
|
|
109
150
|
// A 4xx is a refusal before anything was created; a 5xx may not be.
|
|
110
151
|
throw new CreateInstanceError(response.status < 500 ? 'rejected' : 'uncertain', response.status < 500
|
|
111
|
-
?
|
|
112
|
-
: instanceWithoutBearerMessage(input.customDomain, `ZITADEL answered HTTP ${response.status}
|
|
152
|
+
? `${label}: ZITADEL refused to create the instance (HTTP ${response.status}); nothing was created`
|
|
153
|
+
: instanceWithoutBearerMessage(input.customDomain, `ZITADEL answered HTTP ${response.status}`, label));
|
|
113
154
|
}
|
|
114
155
|
let body = {};
|
|
115
156
|
try {
|
|
@@ -119,7 +160,7 @@ export async function createZitadelInstance(input, runtime) {
|
|
|
119
160
|
// Handled below.
|
|
120
161
|
}
|
|
121
162
|
if (!body.instanceId || !body.pat) {
|
|
122
|
-
throw new CreateInstanceError('uncertain', instanceWithoutBearerMessage(input.customDomain, 'the answer had no instance id or token'));
|
|
163
|
+
throw new CreateInstanceError('uncertain', instanceWithoutBearerMessage(input.customDomain, 'the answer had no instance id or token', label));
|
|
123
164
|
}
|
|
124
165
|
return { instanceId: body.instanceId, pat: body.pat };
|
|
125
166
|
}
|