@wtfalch/foundry 0.76.1 → 0.77.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/app-canary-worker.d.ts +35 -0
- package/dist/app-canary-worker.js +56 -0
- package/dist/audit/connector.d.ts +6 -2
- package/dist/audit/connector.js +3 -2
- package/dist/audit/evidence.d.ts +4 -0
- package/dist/audit/evidence.js +14 -2
- package/dist/audit/host-store.d.ts +58 -0
- package/dist/audit/host-store.js +147 -0
- package/dist/audit/host.d.ts +53 -0
- package/dist/audit/host.js +294 -0
- package/dist/audit/index.d.ts +2 -0
- package/dist/audit/index.js +2 -0
- package/dist/audit/registry.d.ts +14 -0
- package/dist/audit/sealer-role.d.ts +12 -0
- package/dist/audit/sealer-role.js +26 -2
- package/dist/audit/tick.d.ts +10 -2
- package/dist/audit/tick.js +15 -2
- package/dist/catalogue/copy-en.js +5 -0
- package/dist/catalogue/integrations.js +5 -0
- package/dist/entry-rules.d.ts +35 -0
- package/dist/entry-rules.js +35 -0
- package/dist/hosted-label.js +2 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +6 -1
- package/dist/migrations/0072_audit_host.sql +46 -0
- package/dist/migrations/0074_platform_app_canary.sql +11 -0
- package/dist/modules/ai.js +15 -1
- package/dist/modules/assistants.js +3 -0
- package/dist/modules/email.d.ts +6 -1
- package/dist/modules/email.js +88 -5
- package/dist/modules/files.js +79 -2
- package/dist/modules/index.js +7 -0
- package/dist/modules/jobs.d.ts +5 -1
- package/dist/modules/jobs.js +74 -4
- package/dist/modules/reporting.d.ts +9 -0
- package/dist/modules/reporting.js +29 -0
- package/dist/modules/sign-in.js +20 -2
- package/dist/modules/types.d.ts +2 -0
- package/dist/modules/workspaces.js +3 -0
- package/dist/org.d.ts +114 -13
- package/dist/org.js +233 -20
- package/dist/plan/ai-boule-key-apply.d.ts +65 -0
- package/dist/plan/ai-boule-key-apply.js +169 -0
- package/dist/plan/ai-boule-key.d.ts +89 -0
- package/dist/plan/ai-boule-key.js +195 -0
- package/dist/plan/analytics.d.ts +8 -1
- package/dist/plan/analytics.js +8 -1
- package/dist/plan/app-canary-apply.d.ts +49 -0
- package/dist/plan/app-canary-apply.js +232 -0
- package/dist/plan/app-canary.d.ts +44 -0
- package/dist/plan/app-canary.js +110 -0
- package/dist/plan/blue-green-apply.d.ts +6 -2
- package/dist/plan/blue-green-apply.js +14 -3
- package/dist/plan/broker-entries.js +20 -5
- package/dist/plan/company-backups.js +2 -0
- package/dist/plan/diff.d.ts +22 -0
- package/dist/plan/diff.js +231 -18
- package/dist/plan/entry-files-key.d.ts +39 -3
- package/dist/plan/entry-files-key.js +103 -6
- package/dist/plan/entry-jobs-boule-key-apply.d.ts +65 -0
- package/dist/plan/entry-jobs-boule-key-apply.js +192 -0
- package/dist/plan/entry-jobs-boule-key.d.ts +59 -0
- package/dist/plan/entry-jobs-boule-key.js +141 -0
- package/dist/plan/entry-jobs.d.ts +2 -0
- package/dist/plan/entry-reporting-key.d.ts +36 -0
- package/dist/plan/entry-reporting-key.js +136 -0
- package/dist/plan/executor.d.ts +108 -1
- package/dist/plan/executor.js +669 -23
- package/dist/plan/files-boule-key-apply.d.ts +73 -0
- package/dist/plan/files-boule-key-apply.js +223 -0
- package/dist/plan/files-boule-key.d.ts +92 -0
- package/dist/plan/files-boule-key.js +175 -0
- package/dist/plan/hostnames.js +4 -0
- package/dist/plan/index.d.ts +4 -0
- package/dist/plan/index.js +2 -0
- package/dist/plan/jobs-scheduler-http.d.ts +13 -0
- package/dist/plan/jobs-scheduler-http.js +39 -0
- package/dist/plan/live.d.ts +1 -1
- package/dist/plan/locked-hostnames.d.ts +1 -1
- package/dist/plan/locked-hostnames.js +4 -1
- package/dist/plan/login-host.js +2 -2
- package/dist/plan/org-names.js +3 -0
- package/dist/plan/own-diff.js +3 -2
- package/dist/plan/own-jobs-route.d.ts +5 -2
- package/dist/plan/own-jobs-route.js +6 -2
- package/dist/plan/own-limits.d.ts +6 -0
- package/dist/plan/own-limits.js +1 -0
- package/dist/plan/plan-apply.d.ts +24 -0
- package/dist/plan/plan-apply.js +39 -4
- package/dist/plan/platform-diff.d.ts +2 -0
- package/dist/plan/platform-diff.js +15 -1
- package/dist/plan/platform-providers.d.ts +9 -0
- package/dist/plan/platform-providers.js +21 -0
- package/dist/plan/providers-readers.js +25 -1
- package/dist/plan/registered-app-env.d.ts +31 -0
- package/dist/plan/registered-app-env.js +63 -0
- package/dist/plan/registered-app.d.ts +121 -0
- package/dist/plan/registered-app.js +262 -0
- package/dist/plan/release-host.js +2 -1
- package/dist/plan/reporting-boule.d.ts +18 -0
- package/dist/plan/reporting-boule.js +44 -0
- package/dist/plan/reporting.d.ts +90 -0
- package/dist/plan/reporting.js +135 -0
- package/dist/plan/stage-env.js +1 -0
- package/dist/plan/stage-isolation.d.ts +3 -1
- package/dist/plan/stage-isolation.js +20 -2
- package/dist/plan/stage-services.d.ts +3 -1
- package/dist/plan/stage-services.js +8 -1
- package/dist/plan/stage-sink.d.ts +2 -2
- package/dist/plan/stage-sink.js +2 -2
- package/dist/plan/workspace-client.d.ts +1 -1
- package/dist/plan/workspace-client.js +2 -1
- package/dist/plan/workspace-sign-in.d.ts +1 -1
- package/dist/plan/workspace-sign-in.js +39 -7
- package/dist/platform.d.ts +25 -0
- package/dist/platform.js +20 -0
- package/dist/service-labels.d.ts +8 -0
- package/dist/service-labels.js +8 -0
- package/dist/slug.d.ts +1 -1
- package/dist/slug.js +5 -2
- package/dist/store/postgres.js +13 -0
- package/dist/store/schema.d.ts +485 -2
- package/dist/store/schema.js +38 -1
- package/dist/worker-app-config.js +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type FilesBouleWant } from './files-boule-key.js';
|
|
2
|
+
/** Every call this module makes (files, held keys, Coolify) is cut off after this long. */
|
|
3
|
+
export declare const FILES_BOULE_CALL_TIMEOUT_MS = 30000;
|
|
4
|
+
/** How many times Boule's key is tried against the held keys. */
|
|
5
|
+
export declare const FILES_BOULE_HOLD_ATTEMPTS = 3;
|
|
6
|
+
/** The two files calls this needs, as `FilesOrganisationPort` offers them. */
|
|
7
|
+
export interface FilesBouleFilesPort {
|
|
8
|
+
issueKeyBelow(organisationId: string, minterId: string, input: {
|
|
9
|
+
name: string;
|
|
10
|
+
kind: 'api_key' | 'agent' | 'service';
|
|
11
|
+
permissions: string[];
|
|
12
|
+
expiresDays: number;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
id: string;
|
|
15
|
+
secret: string;
|
|
16
|
+
expiresAt: string;
|
|
17
|
+
}>;
|
|
18
|
+
revokeOrganisationKey(organisationId: string, keyId: string): Promise<unknown>;
|
|
19
|
+
}
|
|
20
|
+
export interface FilesBouleKeyDeps {
|
|
21
|
+
files: FilesBouleFilesPort;
|
|
22
|
+
heldKeys: {
|
|
23
|
+
resolve(ref: string): Promise<string | null | undefined>;
|
|
24
|
+
put(ref: string, value: string): Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
coolify: {
|
|
27
|
+
bulkUpsertEnvs(appId: string, envs: Array<{
|
|
28
|
+
key: string;
|
|
29
|
+
value: string;
|
|
30
|
+
literal?: boolean;
|
|
31
|
+
}>): Promise<unknown>;
|
|
32
|
+
listEnvs(appId: string): Promise<Array<{
|
|
33
|
+
uuid?: string;
|
|
34
|
+
key: string;
|
|
35
|
+
value: string;
|
|
36
|
+
is_preview: boolean;
|
|
37
|
+
}>>;
|
|
38
|
+
deleteEnv(appId: string, envUuid: string): Promise<unknown>;
|
|
39
|
+
restart(appId: string): Promise<unknown>;
|
|
40
|
+
};
|
|
41
|
+
org: {
|
|
42
|
+
id: string;
|
|
43
|
+
slug: string;
|
|
44
|
+
};
|
|
45
|
+
/** Boule's Coolify app, when it is recorded. */
|
|
46
|
+
bouleAppId: string | undefined;
|
|
47
|
+
recorded: () => Readonly<Record<string, string>>;
|
|
48
|
+
record: (key: string, value: string) => void;
|
|
49
|
+
forget: (key: string) => void;
|
|
50
|
+
persist: () => Promise<void>;
|
|
51
|
+
/** Registers a value for redaction before anything else can see it. */
|
|
52
|
+
secret: (value: string) => void;
|
|
53
|
+
now?: () => number;
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
}
|
|
56
|
+
/** What `ensureFilesBouleKey` is given from the plan. */
|
|
57
|
+
export interface FilesBouleStep extends FilesBouleWant {
|
|
58
|
+
mode: 'mint' | 'renew' | 'deliver';
|
|
59
|
+
name: string;
|
|
60
|
+
expiresDays: number;
|
|
61
|
+
permissions: string[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Mints (or reuses the held) Boule key and writes `FILES_URL` and `FILES_BOULE_KEYS` to Boule's env. See
|
|
65
|
+
* `files-boule-key.ts` for the order. Errors name the key id and the ref, never the key.
|
|
66
|
+
*/
|
|
67
|
+
export declare function ensureFilesBouleKey(deps: FilesBouleKeyDeps, step: FilesBouleStep): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* Revokes Boule's key once no entry uses files (a held one, one minted and never held, and any a renewal
|
|
70
|
+
* left), takes `FILES_URL` and `FILES_BOULE_KEYS` out of Boule's env and restarts Boule, then clears the
|
|
71
|
+
* records. A key files no longer knows counts as revoked.
|
|
72
|
+
*/
|
|
73
|
+
export declare function retireFilesBouleKey(deps: FilesBouleKeyDeps): Promise<void>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { FILES_KEY_RENEW_WINDOW_DAYS, parseIdList } from './entry-files-key.js';
|
|
2
|
+
import { FILES_BOULE_APPLIED_SCOPE_KEY, FILES_BOULE_EXPIRES_SCOPE_KEY, FILES_BOULE_KEYS_ENV, FILES_BOULE_KEY_SCOPE_KEY, FILES_BOULE_MINTER_SCOPE_KEY, FILES_BOULE_PREVIOUS_SCOPE_KEY, FILES_BOULE_UNDELIVERED_SCOPE_KEY, FILES_BOULE_UNREVOKED_SCOPE_KEY, FILES_BOULE_URL_ENV, filesBouleKeyRef, } from './files-boule-key.js';
|
|
3
|
+
/** Every call this module makes (files, held keys, Coolify) is cut off after this long. */
|
|
4
|
+
export const FILES_BOULE_CALL_TIMEOUT_MS = 30_000;
|
|
5
|
+
/** How many times Boule's key is tried against the held keys. */
|
|
6
|
+
export const FILES_BOULE_HOLD_ATTEMPTS = 3;
|
|
7
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
8
|
+
async function timed(work, ms) {
|
|
9
|
+
let timer;
|
|
10
|
+
try {
|
|
11
|
+
return await Promise.race([
|
|
12
|
+
work(),
|
|
13
|
+
new Promise((_, reject) => {
|
|
14
|
+
timer = setTimeout(() => reject(new Error('timeout')), ms);
|
|
15
|
+
}),
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
clearTimeout(timer);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const statusOf = (error) => {
|
|
23
|
+
const status = error?.status;
|
|
24
|
+
return typeof status === 'number' ? ` (${status})` : '';
|
|
25
|
+
};
|
|
26
|
+
const isNotFound = (error) => error?.status === 404;
|
|
27
|
+
/**
|
|
28
|
+
* Mints (or reuses the held) Boule key and writes `FILES_URL` and `FILES_BOULE_KEYS` to Boule's env. See
|
|
29
|
+
* `files-boule-key.ts` for the order. Errors name the key id and the ref, never the key.
|
|
30
|
+
*/
|
|
31
|
+
export async function ensureFilesBouleKey(deps, step) {
|
|
32
|
+
const ms = deps.timeoutMs ?? FILES_BOULE_CALL_TIMEOUT_MS;
|
|
33
|
+
const now = deps.now ?? Date.now;
|
|
34
|
+
const at = `files:boule-key for ${deps.org.slug}`;
|
|
35
|
+
const ref = filesBouleKeyRef(deps.org.id);
|
|
36
|
+
if (!deps.bouleAppId)
|
|
37
|
+
throw new Error(`${at}: Boule is not recorded yet`);
|
|
38
|
+
const bouleAppId = deps.bouleAppId;
|
|
39
|
+
const recorded = deps.recorded();
|
|
40
|
+
const undelivered = recorded[FILES_BOULE_UNDELIVERED_SCOPE_KEY];
|
|
41
|
+
let guard = recorded[FILES_BOULE_KEY_SCOPE_KEY];
|
|
42
|
+
if (undelivered && undelivered !== guard) {
|
|
43
|
+
throw new Error(`${at}: key ${undelivered} was minted but never held, and files shows a key once; revoke it in files and remove the recorded key ${FILES_BOULE_UNDELIVERED_SCOPE_KEY}`);
|
|
44
|
+
}
|
|
45
|
+
const rootId = recorded[`files:key:${deps.org.id}`];
|
|
46
|
+
let key = null;
|
|
47
|
+
if (guard && step.mode === 'renew') {
|
|
48
|
+
// The key in Boule's env stays until the new one is written; it is revoked after.
|
|
49
|
+
const previous = parseIdList(recorded[FILES_BOULE_PREVIOUS_SCOPE_KEY]);
|
|
50
|
+
if (!previous.includes(guard))
|
|
51
|
+
previous.push(guard);
|
|
52
|
+
deps.record(FILES_BOULE_PREVIOUS_SCOPE_KEY, JSON.stringify(previous));
|
|
53
|
+
deps.forget(FILES_BOULE_KEY_SCOPE_KEY);
|
|
54
|
+
deps.forget(FILES_BOULE_UNDELIVERED_SCOPE_KEY);
|
|
55
|
+
await deps.persist();
|
|
56
|
+
guard = undefined;
|
|
57
|
+
}
|
|
58
|
+
else if (guard) {
|
|
59
|
+
try {
|
|
60
|
+
key = (await timed(() => deps.heldKeys.resolve(ref), ms)) || null;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
throw new Error(`${at}: the held key ${ref} could not be read`);
|
|
64
|
+
}
|
|
65
|
+
if (key) {
|
|
66
|
+
deps.secret(key);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
// The held value is gone: the key Boule had is revoked before a new one is minted.
|
|
70
|
+
try {
|
|
71
|
+
await timed(() => deps.files.revokeOrganisationKey(deps.org.id, guard), ms);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (!isNotFound(error)) {
|
|
75
|
+
throw new Error(`${at}: the held key ${ref} is missing and key ${guard} could not be revoked before a new one is minted; the next apply tries again`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
deps.forget(FILES_BOULE_KEY_SCOPE_KEY);
|
|
79
|
+
deps.forget(FILES_BOULE_UNDELIVERED_SCOPE_KEY);
|
|
80
|
+
await deps.persist();
|
|
81
|
+
guard = undefined;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (!key) {
|
|
85
|
+
if (!rootId)
|
|
86
|
+
throw new Error(`${at}: the org's own files key is not recorded yet`);
|
|
87
|
+
// files refuses a key that outlives its minter. A minute is held back so the call still lands inside it.
|
|
88
|
+
const rootExpires = Date.parse(recorded.files_key_expires ?? '');
|
|
89
|
+
if (Number.isNaN(rootExpires)) {
|
|
90
|
+
throw new Error(`${at}: the org's own files key expiry is not recorded`);
|
|
91
|
+
}
|
|
92
|
+
const expiresDays = Math.min(step.expiresDays, Math.floor((rootExpires - now() - 60_000) / DAY_MS));
|
|
93
|
+
if (expiresDays < FILES_KEY_RENEW_WINDOW_DAYS) {
|
|
94
|
+
throw new Error(`${at}: the org's own files key leaves under ${FILES_KEY_RENEW_WINDOW_DAYS} days; renew it first`);
|
|
95
|
+
}
|
|
96
|
+
let minted;
|
|
97
|
+
try {
|
|
98
|
+
minted = await timed(() => deps.files.issueKeyBelow(deps.org.id, rootId, {
|
|
99
|
+
name: step.name,
|
|
100
|
+
kind: 'service',
|
|
101
|
+
permissions: [...step.permissions],
|
|
102
|
+
expiresDays,
|
|
103
|
+
}), ms);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
throw new Error(`${at}: files refused or did not answer the key${statusOf(error)}`);
|
|
107
|
+
}
|
|
108
|
+
if (typeof minted?.secret === 'string' && minted.secret)
|
|
109
|
+
deps.secret(minted.secret);
|
|
110
|
+
if (typeof minted?.id !== 'string' || !minted.id) {
|
|
111
|
+
throw new Error(`${at}: files answered the key with no id`);
|
|
112
|
+
}
|
|
113
|
+
deps.record(FILES_BOULE_UNDELIVERED_SCOPE_KEY, minted.id);
|
|
114
|
+
await deps.persist();
|
|
115
|
+
if (typeof minted.secret !== 'string' || !minted.secret) {
|
|
116
|
+
throw new Error(`${at}: key ${minted.id} was minted but files returned no key, which it shows once; revoke it in files and remove the recorded key ${FILES_BOULE_UNDELIVERED_SCOPE_KEY}`);
|
|
117
|
+
}
|
|
118
|
+
let stored = false;
|
|
119
|
+
for (let attempt = 1; attempt <= FILES_BOULE_HOLD_ATTEMPTS && !stored; attempt++) {
|
|
120
|
+
try {
|
|
121
|
+
await timed(() => deps.heldKeys.put(ref, minted.secret), ms);
|
|
122
|
+
stored = true;
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
// Retried; the error is dropped in case it echoes the key.
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!stored) {
|
|
129
|
+
throw new Error(`${at}: key ${minted.id} was minted but could not be held at ${ref} after ${FILES_BOULE_HOLD_ATTEMPTS} attempts; revoke it in files and remove the recorded key ${FILES_BOULE_UNDELIVERED_SCOPE_KEY}`);
|
|
130
|
+
}
|
|
131
|
+
deps.record(FILES_BOULE_KEY_SCOPE_KEY, minted.id);
|
|
132
|
+
deps.record(FILES_BOULE_MINTER_SCOPE_KEY, rootId);
|
|
133
|
+
deps.record(FILES_BOULE_EXPIRES_SCOPE_KEY, minted.expiresAt);
|
|
134
|
+
await deps.persist();
|
|
135
|
+
key = minted.secret;
|
|
136
|
+
}
|
|
137
|
+
const value = {
|
|
138
|
+
live: { organisationId: step.organisationId, key, apps: step.apps },
|
|
139
|
+
};
|
|
140
|
+
try {
|
|
141
|
+
await timed(() => deps.coolify.bulkUpsertEnvs(bouleAppId, [
|
|
142
|
+
{ key: FILES_BOULE_URL_ENV, value: step.url, literal: true },
|
|
143
|
+
{ key: FILES_BOULE_KEYS_ENV, value: JSON.stringify(value), literal: true },
|
|
144
|
+
]), ms);
|
|
145
|
+
await timed(() => deps.coolify.restart(bouleAppId), ms);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// The error is dropped in case Coolify echoes the request body, which holds the key.
|
|
149
|
+
throw new Error(`${at}: the key is held at ${ref} but could not be written to Boule's env; the next apply writes it again`);
|
|
150
|
+
}
|
|
151
|
+
deps.forget(FILES_BOULE_UNDELIVERED_SCOPE_KEY);
|
|
152
|
+
deps.record(FILES_BOULE_APPLIED_SCOPE_KEY, JSON.stringify({ url: step.url, organisationId: step.organisationId, apps: step.apps }));
|
|
153
|
+
// The keys a renewal replaced: Boule no longer holds them. A failed revoke is listed, not retried.
|
|
154
|
+
const previous = parseIdList(deps.recorded()[FILES_BOULE_PREVIOUS_SCOPE_KEY]);
|
|
155
|
+
if (previous.length > 0) {
|
|
156
|
+
const unrevoked = parseIdList(deps.recorded()[FILES_BOULE_UNREVOKED_SCOPE_KEY]);
|
|
157
|
+
for (const id of previous) {
|
|
158
|
+
try {
|
|
159
|
+
await timed(() => deps.files.revokeOrganisationKey(deps.org.id, id), ms);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
if (!isNotFound(error) && !unrevoked.includes(id))
|
|
163
|
+
unrevoked.push(id);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
deps.record(FILES_BOULE_UNREVOKED_SCOPE_KEY, unrevoked.length ? JSON.stringify(unrevoked) : '');
|
|
167
|
+
deps.forget(FILES_BOULE_PREVIOUS_SCOPE_KEY);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Revokes Boule's key once no entry uses files (a held one, one minted and never held, and any a renewal
|
|
172
|
+
* left), takes `FILES_URL` and `FILES_BOULE_KEYS` out of Boule's env and restarts Boule, then clears the
|
|
173
|
+
* records. A key files no longer knows counts as revoked.
|
|
174
|
+
*/
|
|
175
|
+
export async function retireFilesBouleKey(deps) {
|
|
176
|
+
const ms = deps.timeoutMs ?? FILES_BOULE_CALL_TIMEOUT_MS;
|
|
177
|
+
const at = `files:boule-key for ${deps.org.slug}`;
|
|
178
|
+
const recorded = deps.recorded();
|
|
179
|
+
const ids = [
|
|
180
|
+
...new Set([
|
|
181
|
+
recorded[FILES_BOULE_KEY_SCOPE_KEY],
|
|
182
|
+
recorded[FILES_BOULE_UNDELIVERED_SCOPE_KEY],
|
|
183
|
+
...parseIdList(recorded[FILES_BOULE_PREVIOUS_SCOPE_KEY]),
|
|
184
|
+
].filter((id) => Boolean(id))),
|
|
185
|
+
];
|
|
186
|
+
for (const id of ids) {
|
|
187
|
+
try {
|
|
188
|
+
await timed(() => deps.files.revokeOrganisationKey(deps.org.id, id), ms);
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (!isNotFound(error)) {
|
|
192
|
+
throw new Error(`${at}: key ${id} could not be revoked; the next apply tries again`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (deps.bouleAppId) {
|
|
197
|
+
const appId = deps.bouleAppId;
|
|
198
|
+
try {
|
|
199
|
+
const rows = await timed(() => deps.coolify.listEnvs(appId), ms);
|
|
200
|
+
const gone = rows.filter((e) => !e.is_preview &&
|
|
201
|
+
e.uuid &&
|
|
202
|
+
(e.key === FILES_BOULE_KEYS_ENV || e.key === FILES_BOULE_URL_ENV));
|
|
203
|
+
for (const row of gone) {
|
|
204
|
+
await timed(() => deps.coolify.deleteEnv(appId, row.uuid), ms);
|
|
205
|
+
}
|
|
206
|
+
if (gone.length > 0)
|
|
207
|
+
await timed(() => deps.coolify.restart(appId), ms);
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
throw new Error(`${at}: the key is revoked but could not be taken out of Boule's env; the next apply tries again`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const name of [
|
|
214
|
+
FILES_BOULE_KEY_SCOPE_KEY,
|
|
215
|
+
FILES_BOULE_UNDELIVERED_SCOPE_KEY,
|
|
216
|
+
FILES_BOULE_MINTER_SCOPE_KEY,
|
|
217
|
+
FILES_BOULE_EXPIRES_SCOPE_KEY,
|
|
218
|
+
FILES_BOULE_APPLIED_SCOPE_KEY,
|
|
219
|
+
FILES_BOULE_PREVIOUS_SCOPE_KEY,
|
|
220
|
+
]) {
|
|
221
|
+
deps.forget(name);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { DeclaredOrg } from '../org.js';
|
|
2
|
+
import type { DraftStep } from './diff.js';
|
|
3
|
+
export { entryCollectionName, entryCollectionNames, entryFilesCollectionsScopeKey, FILES_BOULE_KEY_SCOPE_KEY, FILES_BOULE_MINTER_SCOPE_KEY, FILES_BOULE_PREVIOUS_SCOPE_KEY, FILES_BOULE_UNDELIVERED_SCOPE_KEY, knownCollectionNames, } from './entry-files-key.js';
|
|
4
|
+
import type { OrgPlatformReadiness } from './platform-org.js';
|
|
5
|
+
/**
|
|
6
|
+
* foundry#1121: Boule's files key, and each app's collections named apart.
|
|
7
|
+
*
|
|
8
|
+
* **Boule's key.** Boule's Files page (boule#651) reads one app's report through files' `/v1/manage/report/*`
|
|
9
|
+
* routes (files#162) and stops its share links. One `files:boule-key` step, keyed by the org id, planned
|
|
10
|
+
* after Boule and after the entries' own files keys, while any entry declares `uses.files`:
|
|
11
|
+
*
|
|
12
|
+
* 1. mints a key below the org's own files key (`issueKeyBelow`, kind `service`) holding
|
|
13
|
+
* {@link FILES_BOULE_PERMISSIONS}; files shows it once;
|
|
14
|
+
* 2. records its id as `files_boule_key_undelivered` and persists it before anything else;
|
|
15
|
+
* 3. holds the key at `files:boule-key:<orgId>:live` (3 tries), then records the id as `files_boule_key` (the
|
|
16
|
+
* held guard), the org key it is below as `files_boule_key_minter` and its expiry, and persists them;
|
|
17
|
+
* 4. writes `FILES_URL` and `FILES_BOULE_KEYS` (JSON, see {@link FilesBouleKeys}) to Boule's env and restarts
|
|
18
|
+
* Boule;
|
|
19
|
+
* 5. only then clears the undelivered marker, records what it wrote as `files_boule_key_applied` (no key in
|
|
20
|
+
* it) and revokes the key it replaced.
|
|
21
|
+
*
|
|
22
|
+
* A marker equal to the guard is a delivery that did not finish: the step is planned again and reuses the held
|
|
23
|
+
* key. A marker without it (minted, never held) is blocked and never minted again. A guard whose held value is
|
|
24
|
+
* gone is repaired by the step itself: that key is revoked first, then a new one is minted. A renewal (under 30
|
|
25
|
+
* days left, or below an org key that was replaced) moves the guard to `files_boule_key_previous` and persists
|
|
26
|
+
* that before it mints, so the old key stays in Boule's env until the new one is written, and is revoked after.
|
|
27
|
+
* The org key's own `revoke-previous` waits until Boule's key is below the new org key too.
|
|
28
|
+
*
|
|
29
|
+
* Removing the last `uses.files` plans the retire form (`destroys: true`): revoke the key (held, or minted and
|
|
30
|
+
* never held), take both names out of Boule's env, restart Boule, clear the records. The held value stays (the
|
|
31
|
+
* held store has no delete); it is never read again without a guard.
|
|
32
|
+
*
|
|
33
|
+
* **Collections per app.** files makes a collection by name, so two entries declaring `INVOICES` used to share
|
|
34
|
+
* one and read each other's files. A collection is now named `<entry key>.<LABEL>` (an entry key holds no
|
|
35
|
+
* `.`, a label holds none either, so two apps never get one name). A collection an entry already had keeps
|
|
36
|
+
* its old name, so its files stay where they are: the first apply after this reads the labels the entry's
|
|
37
|
+
* last applied marker names and keeps each as its own name. The names are recorded per entry as
|
|
38
|
+
* `files_entry_collections_<entry>` (JSON, label to name, every name the entry ever had, so a label removed
|
|
39
|
+
* and declared again comes back to its files). The app's env is unchanged (`FILES_COLLECTION_<LABEL>_ID` holds
|
|
40
|
+
* the collection's id); Boule gets the label-to-name map per app in `FILES_BOULE_KEYS`.
|
|
41
|
+
*
|
|
42
|
+
* **Not here: the workspace copy.** files has no notion of a copy, and foundry gives each workspace an
|
|
43
|
+
* organisation of its own per entry (`<org id>-stage-<entry>`, stage services), holding no collections (stage
|
|
44
|
+
* keeps none of production's `FILES_COLLECTION_*`). There is no one workspace organisation for Boule's `work`
|
|
45
|
+
* key to read, so `FILES_BOULE_KEYS` carries `live` only.
|
|
46
|
+
*
|
|
47
|
+
* The key reaches the held keys and Boule's env and nothing else: not a plan, a log, an error or a recorded id.
|
|
48
|
+
*/
|
|
49
|
+
/** What Boule's key holds: read the reports (`files.usage:read`, `files.files:read`, `files.shares:read`) and stop a share (`files.files:share`). */
|
|
50
|
+
export declare const FILES_BOULE_PERMISSIONS: readonly ["files.usage:read", "files.files:read", "files.shares:read", "files.files:share"];
|
|
51
|
+
/** Boule's env names. */
|
|
52
|
+
export declare const FILES_BOULE_KEYS_ENV = "FILES_BOULE_KEYS";
|
|
53
|
+
export declare const FILES_BOULE_URL_ENV = "FILES_URL";
|
|
54
|
+
/** The ISO time Boule's key expires. */
|
|
55
|
+
export declare const FILES_BOULE_EXPIRES_SCOPE_KEY = "files_boule_key_expires";
|
|
56
|
+
/** What Boule's env was last written with (`{ url, apps }`); never a key. */
|
|
57
|
+
export declare const FILES_BOULE_APPLIED_SCOPE_KEY = "files_boule_key_applied";
|
|
58
|
+
/** Ids of replaced Boule keys whose revoke failed, as a JSON array. They expire on their own; the plan lists each. */
|
|
59
|
+
export declare const FILES_BOULE_UNREVOKED_SCOPE_KEY = "files_boule_key_unrevoked";
|
|
60
|
+
/** The held-key ref of Boule's key for the company's own files organisation. */
|
|
61
|
+
export declare const filesBouleKeyRef: (orgId: string) => string;
|
|
62
|
+
export declare const filesBouleKeyName: (org: DeclaredOrg) => string;
|
|
63
|
+
/**
|
|
64
|
+
* `FILES_BOULE_KEYS` as Boule reads it: one entry per copy. `apps` is, per entry key, the app's collections,
|
|
65
|
+
* label to the collection's name in files (pass the names as `collection=`).
|
|
66
|
+
*/
|
|
67
|
+
export interface FilesBouleKeys {
|
|
68
|
+
live: {
|
|
69
|
+
organisationId: string;
|
|
70
|
+
key: string;
|
|
71
|
+
apps: Record<string, Record<string, string>>;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
export interface FilesBouleWant {
|
|
75
|
+
url: string;
|
|
76
|
+
organisationId: string;
|
|
77
|
+
apps: Record<string, Record<string, string>>;
|
|
78
|
+
}
|
|
79
|
+
export type FilesBouleMode = 'mint' | 'renew' | 'deliver' | 'retire';
|
|
80
|
+
type Blocked = Array<{
|
|
81
|
+
description: string;
|
|
82
|
+
key: string;
|
|
83
|
+
}>;
|
|
84
|
+
/**
|
|
85
|
+
* Boule's key step, or nothing. Blocked, not planned (rule 2), while the held keys cannot hold the key, files'
|
|
86
|
+
* address is not recorded, Boule or the org's own files key is not recorded or planned, or after a mint whose
|
|
87
|
+
* key was never held: a second mint would leave two live keys.
|
|
88
|
+
*/
|
|
89
|
+
export declare function planFilesBouleKey(org: DeclaredOrg, recordedIds: Readonly<Record<string, string>>, plannedSteps: readonly DraftStep[], readiness: OrgPlatformReadiness, now: number): {
|
|
90
|
+
steps: DraftStep[];
|
|
91
|
+
blocked: Blocked;
|
|
92
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { FILES_BOULE_KEY_SCOPE_KEY, FILES_BOULE_MINTER_SCOPE_KEY, FILES_BOULE_PREVIOUS_SCOPE_KEY, FILES_BOULE_UNDELIVERED_SCOPE_KEY, FILES_KEY_DAYS, FILES_KEY_RENEW_WINDOW_DAYS, entryCollectionNames, parseIdList, } from './entry-files-key.js';
|
|
2
|
+
export { entryCollectionName, entryCollectionNames, entryFilesCollectionsScopeKey, FILES_BOULE_KEY_SCOPE_KEY, FILES_BOULE_MINTER_SCOPE_KEY, FILES_BOULE_PREVIOUS_SCOPE_KEY, FILES_BOULE_UNDELIVERED_SCOPE_KEY, knownCollectionNames, } from './entry-files-key.js';
|
|
3
|
+
/**
|
|
4
|
+
* foundry#1121: Boule's files key, and each app's collections named apart.
|
|
5
|
+
*
|
|
6
|
+
* **Boule's key.** Boule's Files page (boule#651) reads one app's report through files' `/v1/manage/report/*`
|
|
7
|
+
* routes (files#162) and stops its share links. One `files:boule-key` step, keyed by the org id, planned
|
|
8
|
+
* after Boule and after the entries' own files keys, while any entry declares `uses.files`:
|
|
9
|
+
*
|
|
10
|
+
* 1. mints a key below the org's own files key (`issueKeyBelow`, kind `service`) holding
|
|
11
|
+
* {@link FILES_BOULE_PERMISSIONS}; files shows it once;
|
|
12
|
+
* 2. records its id as `files_boule_key_undelivered` and persists it before anything else;
|
|
13
|
+
* 3. holds the key at `files:boule-key:<orgId>:live` (3 tries), then records the id as `files_boule_key` (the
|
|
14
|
+
* held guard), the org key it is below as `files_boule_key_minter` and its expiry, and persists them;
|
|
15
|
+
* 4. writes `FILES_URL` and `FILES_BOULE_KEYS` (JSON, see {@link FilesBouleKeys}) to Boule's env and restarts
|
|
16
|
+
* Boule;
|
|
17
|
+
* 5. only then clears the undelivered marker, records what it wrote as `files_boule_key_applied` (no key in
|
|
18
|
+
* it) and revokes the key it replaced.
|
|
19
|
+
*
|
|
20
|
+
* A marker equal to the guard is a delivery that did not finish: the step is planned again and reuses the held
|
|
21
|
+
* key. A marker without it (minted, never held) is blocked and never minted again. A guard whose held value is
|
|
22
|
+
* gone is repaired by the step itself: that key is revoked first, then a new one is minted. A renewal (under 30
|
|
23
|
+
* days left, or below an org key that was replaced) moves the guard to `files_boule_key_previous` and persists
|
|
24
|
+
* that before it mints, so the old key stays in Boule's env until the new one is written, and is revoked after.
|
|
25
|
+
* The org key's own `revoke-previous` waits until Boule's key is below the new org key too.
|
|
26
|
+
*
|
|
27
|
+
* Removing the last `uses.files` plans the retire form (`destroys: true`): revoke the key (held, or minted and
|
|
28
|
+
* never held), take both names out of Boule's env, restart Boule, clear the records. The held value stays (the
|
|
29
|
+
* held store has no delete); it is never read again without a guard.
|
|
30
|
+
*
|
|
31
|
+
* **Collections per app.** files makes a collection by name, so two entries declaring `INVOICES` used to share
|
|
32
|
+
* one and read each other's files. A collection is now named `<entry key>.<LABEL>` (an entry key holds no
|
|
33
|
+
* `.`, a label holds none either, so two apps never get one name). A collection an entry already had keeps
|
|
34
|
+
* its old name, so its files stay where they are: the first apply after this reads the labels the entry's
|
|
35
|
+
* last applied marker names and keeps each as its own name. The names are recorded per entry as
|
|
36
|
+
* `files_entry_collections_<entry>` (JSON, label to name, every name the entry ever had, so a label removed
|
|
37
|
+
* and declared again comes back to its files). The app's env is unchanged (`FILES_COLLECTION_<LABEL>_ID` holds
|
|
38
|
+
* the collection's id); Boule gets the label-to-name map per app in `FILES_BOULE_KEYS`.
|
|
39
|
+
*
|
|
40
|
+
* **Not here: the workspace copy.** files has no notion of a copy, and foundry gives each workspace an
|
|
41
|
+
* organisation of its own per entry (`<org id>-stage-<entry>`, stage services), holding no collections (stage
|
|
42
|
+
* keeps none of production's `FILES_COLLECTION_*`). There is no one workspace organisation for Boule's `work`
|
|
43
|
+
* key to read, so `FILES_BOULE_KEYS` carries `live` only.
|
|
44
|
+
*
|
|
45
|
+
* The key reaches the held keys and Boule's env and nothing else: not a plan, a log, an error or a recorded id.
|
|
46
|
+
*/
|
|
47
|
+
/** What Boule's key holds: read the reports (`files.usage:read`, `files.files:read`, `files.shares:read`) and stop a share (`files.files:share`). */
|
|
48
|
+
export const FILES_BOULE_PERMISSIONS = [
|
|
49
|
+
'files.usage:read',
|
|
50
|
+
'files.files:read',
|
|
51
|
+
'files.shares:read',
|
|
52
|
+
'files.files:share',
|
|
53
|
+
];
|
|
54
|
+
/** Boule's env names. */
|
|
55
|
+
export const FILES_BOULE_KEYS_ENV = 'FILES_BOULE_KEYS';
|
|
56
|
+
export const FILES_BOULE_URL_ENV = 'FILES_URL';
|
|
57
|
+
/** The ISO time Boule's key expires. */
|
|
58
|
+
export const FILES_BOULE_EXPIRES_SCOPE_KEY = 'files_boule_key_expires';
|
|
59
|
+
/** What Boule's env was last written with (`{ url, apps }`); never a key. */
|
|
60
|
+
export const FILES_BOULE_APPLIED_SCOPE_KEY = 'files_boule_key_applied';
|
|
61
|
+
/** Ids of replaced Boule keys whose revoke failed, as a JSON array. They expire on their own; the plan lists each. */
|
|
62
|
+
export const FILES_BOULE_UNREVOKED_SCOPE_KEY = 'files_boule_key_unrevoked';
|
|
63
|
+
/** The held-key ref of Boule's key for the company's own files organisation. */
|
|
64
|
+
export const filesBouleKeyRef = (orgId) => `files:boule-key:${orgId}:live`;
|
|
65
|
+
export const filesBouleKeyName = (org) => `${org.slug}-boule-files`;
|
|
66
|
+
/**
|
|
67
|
+
* Boule's key step, or nothing. Blocked, not planned (rule 2), while the held keys cannot hold the key, files'
|
|
68
|
+
* address is not recorded, Boule or the org's own files key is not recorded or planned, or after a mint whose
|
|
69
|
+
* key was never held: a second mint would leave two live keys.
|
|
70
|
+
*/
|
|
71
|
+
export function planFilesBouleKey(org, recordedIds, plannedSteps, readiness, now) {
|
|
72
|
+
const steps = [];
|
|
73
|
+
const blocked = [];
|
|
74
|
+
const key = `files:boule-key:${org.id}`;
|
|
75
|
+
const at = `${org.slug}'s Boule files key`;
|
|
76
|
+
const block = (why) => blocked.push({ description: `${at} is blocked: ${why}`, key });
|
|
77
|
+
const planned = (system, kind, k) => plannedSteps.some((p) => p.action.system === system && p.action.kind === kind && p.action.key === k);
|
|
78
|
+
for (const id of parseIdList(recordedIds[FILES_BOULE_UNREVOKED_SCOPE_KEY])) {
|
|
79
|
+
blocked.push({
|
|
80
|
+
description: `warning: ${org.slug}'s replaced Boule files key ${id} stays live until it expires; its revoke failed`,
|
|
81
|
+
key: `${key}:unrevoked:${id}`,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const entries = org.hosting.enabled ? org.hosting.entries.filter((e) => e.uses?.files) : [];
|
|
85
|
+
const undelivered = recordedIds[FILES_BOULE_UNDELIVERED_SCOPE_KEY];
|
|
86
|
+
const guard = recordedIds[FILES_BOULE_KEY_SCOPE_KEY];
|
|
87
|
+
const previous = parseIdList(recordedIds[FILES_BOULE_PREVIOUS_SCOPE_KEY]);
|
|
88
|
+
if (entries.length === 0) {
|
|
89
|
+
if (!readiness.filesReady)
|
|
90
|
+
return { steps, blocked };
|
|
91
|
+
if (guard || undelivered || previous.length > 0) {
|
|
92
|
+
steps.push({
|
|
93
|
+
action: { system: 'files', kind: 'boule-key', key: org.id, desired: { mode: 'retire' } },
|
|
94
|
+
step: {
|
|
95
|
+
description: `revoke Boule's files key for ${org.slug} and take FILES_URL and FILES_BOULE_KEYS out of Boule's env, restart Boule: no entry uses files`,
|
|
96
|
+
destroys: true,
|
|
97
|
+
migrates: false,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return { steps, blocked };
|
|
102
|
+
}
|
|
103
|
+
// `files:organisation` and the org key's own item report a files host that is not ready.
|
|
104
|
+
if (!readiness.filesReady)
|
|
105
|
+
return { steps, blocked };
|
|
106
|
+
if (undelivered && undelivered !== guard) {
|
|
107
|
+
block(`key ${undelivered} was minted but never held, and files shows a key once; revoke it in files and remove the recorded key ${FILES_BOULE_UNDELIVERED_SCOPE_KEY}`);
|
|
108
|
+
return { steps, blocked };
|
|
109
|
+
}
|
|
110
|
+
if (readiness.canHoldSecrets !== true) {
|
|
111
|
+
block("the host's held keys cannot hold Boule's key");
|
|
112
|
+
return { steps, blocked };
|
|
113
|
+
}
|
|
114
|
+
if (!readiness.filesUrl) {
|
|
115
|
+
block("the platform has not recorded files' address yet");
|
|
116
|
+
return { steps, blocked };
|
|
117
|
+
}
|
|
118
|
+
if (!recordedIds[`coolify:boule:${org.id}`] && !planned('coolify', 'boule', org.id)) {
|
|
119
|
+
block("the company's Boule is not recorded yet");
|
|
120
|
+
return { steps, blocked };
|
|
121
|
+
}
|
|
122
|
+
const rootKeyId = recordedIds[`files:key:${org.id}`];
|
|
123
|
+
const rootPlanned = plannedSteps.find((p) => p.action.system === 'files' && p.action.kind === 'key' && p.action.key === org.id);
|
|
124
|
+
const rootMode = rootPlanned?.action.desired?.mode;
|
|
125
|
+
const rootRenewing = rootMode === 'renew';
|
|
126
|
+
if (!rootKeyId && rootMode !== 'mint') {
|
|
127
|
+
block("the org's own files key is not recorded yet");
|
|
128
|
+
return { steps, blocked };
|
|
129
|
+
}
|
|
130
|
+
const want = {
|
|
131
|
+
url: readiness.filesUrl,
|
|
132
|
+
organisationId: org.id,
|
|
133
|
+
apps: Object.fromEntries(entries.map((e) => [e.key, entryCollectionNames(e, recordedIds)])),
|
|
134
|
+
};
|
|
135
|
+
let mode = null;
|
|
136
|
+
if (!guard) {
|
|
137
|
+
mode = 'mint';
|
|
138
|
+
}
|
|
139
|
+
else if (rootRenewing ||
|
|
140
|
+
recordedIds[FILES_BOULE_MINTER_SCOPE_KEY] !== rootKeyId ||
|
|
141
|
+
!(Date.parse(recordedIds[FILES_BOULE_EXPIRES_SCOPE_KEY] ?? '') - now >=
|
|
142
|
+
FILES_KEY_RENEW_WINDOW_DAYS * DAY_MS)) {
|
|
143
|
+
mode = 'renew';
|
|
144
|
+
}
|
|
145
|
+
else if (undelivered === guard ||
|
|
146
|
+
previous.length > 0 ||
|
|
147
|
+
recordedIds[FILES_BOULE_APPLIED_SCOPE_KEY] !== JSON.stringify(want)) {
|
|
148
|
+
mode = 'deliver';
|
|
149
|
+
}
|
|
150
|
+
if (!mode)
|
|
151
|
+
return { steps, blocked };
|
|
152
|
+
steps.push({
|
|
153
|
+
action: {
|
|
154
|
+
system: 'files',
|
|
155
|
+
kind: 'boule-key',
|
|
156
|
+
key: org.id,
|
|
157
|
+
desired: {
|
|
158
|
+
mode,
|
|
159
|
+
name: filesBouleKeyName(org),
|
|
160
|
+
expiresDays: FILES_KEY_DAYS,
|
|
161
|
+
permissions: [...FILES_BOULE_PERMISSIONS],
|
|
162
|
+
...want,
|
|
163
|
+
},
|
|
164
|
+
},
|
|
165
|
+
step: {
|
|
166
|
+
description: mode === 'deliver'
|
|
167
|
+
? `write Boule's files key for ${org.slug} and its apps' collections to Boule's FILES_BOULE_KEYS, restart Boule`
|
|
168
|
+
: `${mode === 'mint' ? 'mint' : 'renew'} Boule's files key for ${org.slug} below the org's own key, hold it, write FILES_URL and FILES_BOULE_KEYS to Boule's env, restart Boule`,
|
|
169
|
+
destroys: false,
|
|
170
|
+
migrates: false,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
return { steps, blocked };
|
|
174
|
+
}
|
|
175
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
package/dist/plan/hostnames.js
CHANGED
|
@@ -4,6 +4,7 @@ import { workerAppHostnameOf } from '../worker-app-config.js';
|
|
|
4
4
|
import { agoraHostnameFor } from './agora.js';
|
|
5
5
|
import { analyticsHostnameFor, analyticsWanted } from './analytics.js';
|
|
6
6
|
import { bouleHostnameFor } from './boule.js';
|
|
7
|
+
import { reportingHostnameFor, reportingWanted } from './reporting.js';
|
|
7
8
|
import { workspaceWorkerParent } from './workspace-worker.js';
|
|
8
9
|
/**
|
|
9
10
|
* The full names an entry is served at. With `hosting.usesDomain` off, or no
|
|
@@ -158,6 +159,9 @@ export function tunnelIngressHostnames(org, loginHost = null) {
|
|
|
158
159
|
// The analytics backend's route, only while something uses it.
|
|
159
160
|
if (analyticsWanted(org))
|
|
160
161
|
hostnames.push(analyticsHostnameFor(org));
|
|
162
|
+
// The reporting backend's route, the same way.
|
|
163
|
+
if (reportingWanted(org))
|
|
164
|
+
hostnames.push(reportingHostnameFor(org));
|
|
161
165
|
}
|
|
162
166
|
const share = filesShareHostname(org);
|
|
163
167
|
if (share)
|
package/dist/plan/index.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export type { KeysRegistrarPort } from './keys-registrar.js';
|
|
2
2
|
export type { AnalyticsPort } from './analytics.js';
|
|
3
3
|
export { httpAnalytics } from './analytics.js';
|
|
4
|
+
export type { ReportingPort } from './reporting.js';
|
|
5
|
+
export { httpReporting } from './reporting.js';
|
|
6
|
+
export { httpRegistrarApi } from './registered-app.js';
|
|
7
|
+
export type { RegistrarApiPort } from './registered-app.js';
|
|
4
8
|
export type { LiveResource, ZitadelReader, CoolifyReader, CloudflareReader, FilesReader, AiReader, CmsReader, LiveReaders, PlatformCloudflareReader, PlatformCoolifyReader, PlatformLiveReaders, } from './live.js';
|
|
5
9
|
export { readLiveResources, readLivePlatformResources } from './live.js';
|
|
6
10
|
export type { StepAction, PlannedStep, PlanDetail, OrgPostgresPlan } from './diff.js';
|
package/dist/plan/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { httpAnalytics } from './analytics.js';
|
|
2
|
+
export { httpReporting } from './reporting.js';
|
|
3
|
+
export { httpRegistrarApi } from './registered-app.js';
|
|
2
4
|
export { readLiveResources, readLivePlatformResources } from './live.js';
|
|
3
5
|
export { computePlanDetail, toPlan } from './diff.js';
|
|
4
6
|
export { stepSystemOf } from './step-system.js';
|
|
@@ -26,6 +26,19 @@ export declare class HttpJobsSchedulerClient implements JobsSchedulerPort {
|
|
|
26
26
|
secret?: string;
|
|
27
27
|
syncKey?: string;
|
|
28
28
|
}>;
|
|
29
|
+
/**
|
|
30
|
+
* foundry#1119: `POST /admin/projects/<name>/keys` (jobs#79). Answers `201 { id, key, ... }`, the key shown
|
|
31
|
+
* once. A refusal throws with the status only: no response text reaches the error.
|
|
32
|
+
*/
|
|
33
|
+
mintProjectKey(project: string, grant: {
|
|
34
|
+
principal: string;
|
|
35
|
+
scopes: readonly string[];
|
|
36
|
+
}): Promise<{
|
|
37
|
+
id: string;
|
|
38
|
+
key: string;
|
|
39
|
+
}>;
|
|
40
|
+
/** `DELETE /admin/projects/<name>/keys/<id>`. A refusal throws with the status only (404: no such key). */
|
|
41
|
+
revokeProjectKey(project: string, keyId: string): Promise<void>;
|
|
29
42
|
/** `DELETE /admin/projects/<name>`. A refusal throws with the status only. */
|
|
30
43
|
deleteProject(name: string): Promise<void>;
|
|
31
44
|
}
|