@wtfalch/foundry 0.37.0 → 0.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/plan/backup-gate.d.ts +3 -3
- package/dist/plan/backup-gate.js +9 -4
- package/dist/plan/company-backups.d.ts +7 -2
- package/dist/plan/company-backups.js +13 -9
- package/dist/plan/diff.js +4 -0
- package/dist/plan/executor.js +7 -1
- package/dist/plan/index.d.ts +2 -1
- package/dist/plan/index.js +1 -0
- package/dist/plan/mail-store.d.ts +34 -0
- package/dist/plan/mail-store.js +49 -0
- package/dist/plan/own-diff.js +15 -4
- package/dist/plan/own-services.d.ts +1 -0
- package/dist/plan/own-services.js +2 -0
- package/dist/plan/plan-apply.js +7 -3
- package/dist/plan/platform-diff.d.ts +30 -0
- package/dist/plan/platform-diff.js +62 -7
- package/dist/plan/platform-org.d.ts +15 -0
- package/dist/plan/platform-org.js +17 -1
- package/dist/plan/platform-providers.d.ts +23 -1
- package/dist/plan/platform-providers.js +176 -14
- package/dist/plan/uptime-worker.d.ts +20 -1
- package/dist/plan/uptime-worker.js +121 -3
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
2
|
import { EdgeApiError } from '@wtfalch/edge';
|
|
3
3
|
import { uptimeArtifactUrl } from '../platform.js';
|
|
4
|
-
import { PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SECRETS_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SHA256_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_TAG_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_URL_REF, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_KV_TITLE, PLATFORM_UPTIME_OPTIONAL_SECRETS, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_UPTIME_TOKEN_UNDELIVERED_SCOPE_KEY, uptimeAccessTokenConflict, uptimeSecretNames, uptimeVarsDigest, } from './platform-diff.js';
|
|
4
|
+
import { PLATFORM_EDGE_INTERNAL_URL_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_CLIENT_ID_REF, PLATFORM_UPTIME_ACCESS_CLIENT_SECRET_REF, PLATFORM_UPTIME_ACCESS_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_ACCESS_TOKEN_NAME, PLATFORM_UPTIME_ACCESS_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_HOSTNAME_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SECRETS_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_SHA256_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_TAG_SCOPE_KEY, PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY, PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, PLATFORM_UPTIME_CHAT_TOKEN_REF, PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY, PLATFORM_UPTIME_HEARTBEAT_URL_REF, PLATFORM_UPTIME_KEY, PLATFORM_UPTIME_KV_TITLE, PLATFORM_UPTIME_OPTIONAL_SECRETS, PLATFORM_UPTIME_REMOVED_SCOPE_KEY, PLATFORM_UPTIME_SCRIPT_NAME, PLATFORM_UPTIME_TOKEN_ID_SCOPE_KEY, PLATFORM_UPTIME_TOKEN_REF, PLATFORM_UPTIME_TOKEN_UNDELIVERED_SCOPE_KEY, uptimeAccessTokenConflict, uptimeEffectiveVars, uptimeSecretNames, uptimeVarsDigest, } from './platform-diff.js';
|
|
5
5
|
/** The Worker's cron trigger, from Archon's `workers/uptime/manifest.json`. */
|
|
6
6
|
const UPTIME_CRONS = ['* * * * *'];
|
|
7
7
|
/** The date Archon's own Worker config used. */
|
|
@@ -15,6 +15,84 @@ export const UPTIME_OPTIONAL_VARS = [
|
|
|
15
15
|
'ALERT_EMAIL_FROM',
|
|
16
16
|
'ALERT_EMAIL_TO',
|
|
17
17
|
];
|
|
18
|
+
/** How long one call to read chat-service's token or to ask it for the bot may take. */
|
|
19
|
+
const CHAT_CALL_TIMEOUT_MS = 10_000;
|
|
20
|
+
/** The bot's localpart in chat-service (`@uptime-bot:<server>`). */
|
|
21
|
+
const CHAT_BOT_LOCALPART = 'uptime-bot';
|
|
22
|
+
/** Runs `work` and gives up after `ms`; the error names `what` only. */
|
|
23
|
+
async function withinMs(ms, what, work) {
|
|
24
|
+
let timer;
|
|
25
|
+
try {
|
|
26
|
+
return await Promise.race([
|
|
27
|
+
work,
|
|
28
|
+
new Promise((_, reject) => {
|
|
29
|
+
timer = setTimeout(() => reject(new Error(`platform:uptime: ${what} did not answer within ${ms / 1000}s`)), ms);
|
|
30
|
+
}),
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* `PUT /_chat/internal/bots/uptime-bot` on the company's Agora host. Idempotent on the chat-service
|
|
39
|
+
* side: a repeat returns the room and no token unless `rotate` is set, so calling with `rotate` again
|
|
40
|
+
* revokes the previous token. Every message here is fixed text: the response body, the bearer and the
|
|
41
|
+
* token never reach an error.
|
|
42
|
+
*/
|
|
43
|
+
async function callChatBot(chat, owner, rotate) {
|
|
44
|
+
const timeoutMs = chat.timeoutMs ?? CHAT_CALL_TIMEOUT_MS;
|
|
45
|
+
let envs;
|
|
46
|
+
try {
|
|
47
|
+
envs = await withinMs(timeoutMs, 'reading the chat-service app', chat.coolify.listEnvs(owner.appId));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// The cause can carry a response body.
|
|
51
|
+
throw new Error('platform:uptime: the chat-service app could not be read');
|
|
52
|
+
}
|
|
53
|
+
const bearer = envs.find((e) => e.key === 'CHAT_INTERNAL_TOKEN' && !e.is_preview)?.value;
|
|
54
|
+
if (typeof bearer !== 'string' || !/^\S{32,}$/.test(bearer)) {
|
|
55
|
+
throw new Error('platform:uptime: chat-service holds no usable CHAT_INTERNAL_TOKEN');
|
|
56
|
+
}
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
59
|
+
try {
|
|
60
|
+
const response = await (chat.fetch ?? fetch)(`https://${owner.agoraHost}/_chat/internal/bots/${CHAT_BOT_LOCALPART}`, {
|
|
61
|
+
method: 'PUT',
|
|
62
|
+
headers: { authorization: `Bearer ${bearer}`, 'content-type': 'application/json' },
|
|
63
|
+
body: JSON.stringify({
|
|
64
|
+
displayName: 'Platform alerts',
|
|
65
|
+
room: { name: 'platform-alerts' },
|
|
66
|
+
members: [...owner.subjects],
|
|
67
|
+
...(rotate ? { rotate: true } : {}),
|
|
68
|
+
}),
|
|
69
|
+
redirect: 'error',
|
|
70
|
+
signal: controller.signal,
|
|
71
|
+
});
|
|
72
|
+
if (response.status !== 200) {
|
|
73
|
+
throw new Error(`platform:uptime: chat-service refused the bot request (HTTP ${response.status})`);
|
|
74
|
+
}
|
|
75
|
+
const body = (await response.json());
|
|
76
|
+
const { userId, roomId, accessToken } = body ?? {};
|
|
77
|
+
if (typeof userId !== 'string' ||
|
|
78
|
+
!userId ||
|
|
79
|
+
typeof roomId !== 'string' ||
|
|
80
|
+
!roomId ||
|
|
81
|
+
(accessToken !== undefined && (typeof accessToken !== 'string' || !accessToken))) {
|
|
82
|
+
throw new Error('platform:uptime: chat-service answered the bot request with an unusable body');
|
|
83
|
+
}
|
|
84
|
+
return { userId, roomId, ...(accessToken ? { accessToken } : {}) };
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
// Our own messages pass; anything else (a network error, a bad JSON body) is replaced.
|
|
88
|
+
if (error instanceof Error && error.message.startsWith('platform:uptime: '))
|
|
89
|
+
throw error;
|
|
90
|
+
throw new Error('platform:uptime: chat-service did not answer the bot request');
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
18
96
|
/**
|
|
19
97
|
* What is held now: the optional secrets, and `UPTIME_HEARTBEAT_URL` (a ping
|
|
20
98
|
* URL that works as a capability, so a secret too), name to value. Callers
|
|
@@ -108,11 +186,51 @@ export async function provisionPlatformUptime(context, deps) {
|
|
|
108
186
|
}
|
|
109
187
|
const hostname = platform.archon.hostname;
|
|
110
188
|
const held = await heldUptimeValues(deps);
|
|
189
|
+
// The chat bot comes before the upload: its room is one of the Worker's variables. A call that fails
|
|
190
|
+
// stops the step here, and the next apply makes it again (the marker below says whether a token may
|
|
191
|
+
// already have been minted).
|
|
192
|
+
const ownerChat = context.owner?.chat ?? null;
|
|
193
|
+
let chatRoomId = recorded[PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY] || '';
|
|
194
|
+
if (deps.chat && ownerChat && ownerChat.subjects.length > 0) {
|
|
195
|
+
const lost = recorded[PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY];
|
|
196
|
+
if (held.CHAT_ACCESS_TOKEN) {
|
|
197
|
+
if (lost) {
|
|
198
|
+
context.recordScopeId(PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY, '');
|
|
199
|
+
await context.persist?.();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
// Recorded and persisted before the route is called, because the call is what mints the token.
|
|
204
|
+
if (!lost) {
|
|
205
|
+
context.recordScopeId(PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY, randomUUID());
|
|
206
|
+
await context.persist?.();
|
|
207
|
+
}
|
|
208
|
+
// A marker, or a room already made, means the bot exists and its token is not held: rotate.
|
|
209
|
+
const rotate = Boolean(lost) || Boolean(chatRoomId);
|
|
210
|
+
let bot = await callChatBot(deps.chat, ownerChat, rotate);
|
|
211
|
+
if (!bot.accessToken && !rotate)
|
|
212
|
+
bot = await callChatBot(deps.chat, ownerChat, true);
|
|
213
|
+
const chatToken = bot.accessToken;
|
|
214
|
+
if (!chatToken) {
|
|
215
|
+
throw new Error('platform:uptime: chat-service returned no token for the bot, even when asked to rotate it');
|
|
216
|
+
}
|
|
217
|
+
chatRoomId = bot.roomId;
|
|
218
|
+
context.recordScopeId(PLATFORM_UPTIME_CHAT_ROOM_ID_SCOPE_KEY, chatRoomId);
|
|
219
|
+
await context.persist?.();
|
|
220
|
+
await attempt('holding the chat bot token', () => deps.deliverSecret(PLATFORM_UPTIME_CHAT_TOKEN_REF, chatToken));
|
|
221
|
+
context.recordScopeId(PLATFORM_UPTIME_CHAT_UNDELIVERED_SCOPE_KEY, '');
|
|
222
|
+
await context.persist?.();
|
|
223
|
+
held.CHAT_ACCESS_TOKEN = chatToken;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const workerVars = uptimeEffectiveVars(deps.vars, deps.chat && ownerChat && chatRoomId
|
|
227
|
+
? { homeserverUrl: `https://${ownerChat.agoraHost}`, roomId: chatRoomId }
|
|
228
|
+
: null);
|
|
111
229
|
const bindings = [
|
|
112
230
|
{ type: 'kv_namespace', name: 'UPTIME_KV', title: PLATFORM_UPTIME_KV_TITLE },
|
|
113
231
|
{ type: 'plain_text', name: 'ARCHON_BASE_URL', text: `https://${hostname}` },
|
|
114
232
|
...UPTIME_OPTIONAL_VARS.flatMap((name) => {
|
|
115
|
-
const text =
|
|
233
|
+
const text = workerVars[name];
|
|
116
234
|
return text ? [{ type: 'plain_text', name, text }] : [];
|
|
117
235
|
}),
|
|
118
236
|
];
|
|
@@ -223,7 +341,7 @@ export async function provisionPlatformUptime(context, deps) {
|
|
|
223
341
|
context.recordScopeId(PLATFORM_UPTIME_APPLIED_TAG_SCOPE_KEY, uptime.tag);
|
|
224
342
|
context.recordScopeId(PLATFORM_UPTIME_APPLIED_SHA256_SCOPE_KEY, uptime.sha256);
|
|
225
343
|
context.recordScopeId(PLATFORM_UPTIME_APPLIED_HOSTNAME_SCOPE_KEY, hostname);
|
|
226
|
-
context.recordScopeId(PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY, uptimeVarsDigest(
|
|
344
|
+
context.recordScopeId(PLATFORM_UPTIME_APPLIED_VARS_SCOPE_KEY, uptimeVarsDigest(workerVars));
|
|
227
345
|
context.recordScopeId(PLATFORM_UPTIME_APPLIED_SECRETS_SCOPE_KEY, uptimeSecretNames(Object.keys(held)));
|
|
228
346
|
return { resourceId: PLATFORM_UPTIME_SCRIPT_NAME };
|
|
229
347
|
}
|