@foldrun/core 0.4.0 → 0.5.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 +3 -0
- package/dist/index.js +3 -0
- package/dist/src/account.d.ts +45 -0
- package/dist/src/account.js +130 -0
- package/dist/src/api-tools.d.ts +8 -0
- package/dist/src/api-tools.js +22 -4
- package/dist/src/browser-pod.d.ts +129 -0
- package/dist/src/browser-pod.js +284 -0
- package/dist/src/catalog.js +2 -1
- package/dist/src/clock.d.ts +49 -0
- package/dist/src/clock.js +121 -0
- package/dist/src/completions.js +17 -8
- package/dist/src/confine.d.ts +22 -0
- package/dist/src/confine.js +218 -3
- package/dist/src/deploy.d.ts +33 -0
- package/dist/src/deploy.js +145 -6
- package/dist/src/egress.d.ts +7 -0
- package/dist/src/egress.js +6 -0
- package/dist/src/eol.d.ts +2 -0
- package/dist/src/eol.js +7 -0
- package/dist/src/evals.d.ts +5 -0
- package/dist/src/evals.js +6 -2
- package/dist/src/flow-lint.d.ts +8 -0
- package/dist/src/flow-lint.js +223 -1
- package/dist/src/flow-patterns.d.ts +141 -0
- package/dist/src/flow-patterns.js +568 -0
- package/dist/src/gitrepo.d.ts +18 -0
- package/dist/src/gitrepo.js +49 -0
- package/dist/src/kinds.js +1 -1
- package/dist/src/language.d.ts +17 -0
- package/dist/src/language.js +55 -0
- package/dist/src/layout.d.ts +69 -0
- package/dist/src/layout.js +200 -0
- package/dist/src/library.d.ts +9 -0
- package/dist/src/library.js +30 -3
- package/dist/src/limits.d.ts +84 -0
- package/dist/src/limits.js +287 -0
- package/dist/src/locale.d.ts +47 -0
- package/dist/src/locale.js +177 -0
- package/dist/src/mail.d.ts +88 -0
- package/dist/src/mail.js +245 -0
- package/dist/src/model-probe.d.ts +3 -0
- package/dist/src/model-probe.js +1 -0
- package/dist/src/notify.d.ts +53 -7
- package/dist/src/notify.js +194 -126
- package/dist/src/openapi.js +5 -3
- package/dist/src/operator.d.ts +201 -0
- package/dist/src/operator.js +251 -0
- package/dist/src/paths.d.ts +28 -0
- package/dist/src/paths.js +55 -0
- package/dist/src/platform.d.ts +43 -3
- package/dist/src/platform.js +9 -1
- package/dist/src/provider-health.js +3 -2
- package/dist/src/providers.d.ts +481 -0
- package/dist/src/providers.js +1105 -6
- package/dist/src/refusal.d.ts +83 -0
- package/dist/src/refusal.js +281 -0
- package/dist/src/run-container.d.ts +149 -13
- package/dist/src/run-container.js +403 -42
- package/dist/src/runner.d.ts +142 -9
- package/dist/src/runner.js +1027 -122
- package/dist/src/runtime.d.ts +52 -0
- package/dist/src/runtime.js +493 -58
- package/dist/src/script-tools.d.ts +3 -2
- package/dist/src/script-tools.js +35 -3
- package/dist/src/secret-files.d.ts +6 -5
- package/dist/src/secret-files.js +24 -11
- package/dist/src/secret-health.d.ts +30 -6
- package/dist/src/secret-health.js +42 -20
- package/dist/src/secrets.d.ts +19 -0
- package/dist/src/secrets.js +155 -86
- package/dist/src/starter.d.ts +9 -0
- package/dist/src/starter.js +92 -11
- package/dist/src/step-exec.d.ts +61 -2
- package/dist/src/step-exec.js +414 -36
- package/dist/src/step-removal.d.ts +15 -0
- package/dist/src/step-removal.js +49 -0
- package/dist/src/storage.d.ts +23 -4
- package/dist/src/storage.js +125 -23
- package/dist/src/store.d.ts +218 -37
- package/dist/src/store.js +347 -60
- package/dist/src/subagents.d.ts +75 -0
- package/dist/src/subagents.js +149 -0
- package/dist/src/tool-names.d.ts +13 -1
- package/dist/src/tool-names.js +38 -11
- package/dist/src/tool-programs.d.ts +26 -0
- package/dist/src/tool-programs.js +172 -0
- package/dist/src/tool-test.js +104 -12
- package/dist/src/translator.d.ts +8 -2
- package/dist/src/translator.js +69 -23
- package/dist/src/trigger-log.d.ts +1 -1
- package/dist/src/triggers.d.ts +3 -0
- package/dist/src/triggers.js +10 -2
- package/dist/src/untrusted-fetch.d.ts +11 -0
- package/dist/src/untrusted-fetch.js +77 -0
- package/dist/src/web-actions.d.ts +91 -0
- package/dist/src/web-actions.js +199 -0
- package/dist/src/webhook.d.ts +4 -0
- package/dist/src/webhook.js +39 -0
- package/index.ts +3 -0
- package/package.json +3 -3
- package/src/account.ts +149 -0
- package/src/api-tools.ts +32 -4
- package/src/browser-pod.ts +313 -0
- package/src/catalog.ts +2 -1
- package/src/clock.ts +149 -0
- package/src/completions.ts +17 -8
- package/src/confine.ts +218 -3
- package/src/deploy.ts +137 -6
- package/src/egress.ts +13 -1
- package/src/eol.ts +7 -0
- package/src/evals.ts +11 -2
- package/src/flow-lint.ts +253 -1
- package/src/flow-patterns.ts +597 -0
- package/src/gitrepo.ts +48 -0
- package/src/kinds.ts +1 -1
- package/src/language.ts +62 -0
- package/src/layout.ts +228 -0
- package/src/library.ts +37 -3
- package/src/limits.ts +290 -0
- package/src/locale.ts +217 -0
- package/src/mail.ts +316 -0
- package/src/model-probe.ts +4 -0
- package/src/notify.ts +189 -103
- package/src/openapi.ts +5 -3
- package/src/operator.ts +300 -0
- package/src/paths.ts +54 -0
- package/src/platform.ts +52 -3
- package/src/provider-health.ts +3 -2
- package/src/providers.ts +1368 -6
- package/src/refusal.ts +320 -0
- package/src/run-container.ts +439 -43
- package/src/runner.ts +1050 -122
- package/src/runtime.ts +487 -55
- package/src/script-tools.ts +38 -5
- package/src/secret-files.ts +22 -10
- package/src/secret-health.ts +64 -19
- package/src/secrets.ts +149 -87
- package/src/starter.ts +93 -11
- package/src/step-exec.ts +429 -37
- package/src/step-removal.ts +59 -0
- package/src/storage.ts +141 -22
- package/src/store.ts +500 -68
- package/src/subagents.ts +189 -0
- package/src/tool-names.ts +40 -11
- package/src/tool-programs.ts +179 -0
- package/src/tool-test.ts +110 -12
- package/src/translator.ts +72 -22
- package/src/trigger-log.ts +1 -0
- package/src/triggers.ts +10 -2
- package/src/untrusted-fetch.ts +88 -0
- package/src/web-actions.ts +237 -0
- package/src/webhook.ts +41 -0
- package/templates/hello/agents/curator/agent.md +1 -1
- package/templates/hello/agents/notetaker/agent.md +1 -1
package/dist/src/mail.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
// Every email the platform sends, through one door.
|
|
2
|
+
//
|
|
3
|
+
// Mail used to leave from six places, each with its own fetch to Resend and
|
|
4
|
+
// none of them asking whether the person reading it wanted it. A person who
|
|
5
|
+
// was tired of "credits low" had one remedy — a filter — and a filter that
|
|
6
|
+
// bins the low-balance warning bins the payment failure beside it.
|
|
7
|
+
//
|
|
8
|
+
// Now each send names a CATEGORY, and the category decides two things:
|
|
9
|
+
//
|
|
10
|
+
// · whether the person may turn it off. Security, billing failures,
|
|
11
|
+
// invites and account notices may not: a password reset nobody receives
|
|
12
|
+
// is a locked-out person, and a payment failure nobody reads is an
|
|
13
|
+
// account that stops working with no warning. Everything else — run
|
|
14
|
+
// alerts, approval requests, low-balance warnings, product news — is the
|
|
15
|
+
// reader's to refuse, per workspace where the mail is about one.
|
|
16
|
+
//
|
|
17
|
+
// · whether it carries a one-click unsubscribe (RFC 8058): a
|
|
18
|
+
// `List-Unsubscribe` URL with a signed token and `List-Unsubscribe-Post:
|
|
19
|
+
// List-Unsubscribe=One-Click`, so the mail client's own "Unsubscribe"
|
|
20
|
+
// turns that category off without a login. Gmail and Yahoo require it
|
|
21
|
+
// of bulk senders; a person deserves it from anyone.
|
|
22
|
+
//
|
|
23
|
+
// The preference is asked at send time, here, through platform.mailPreference
|
|
24
|
+
// — the platform keeps them as records. A laptop has no preferences store and
|
|
25
|
+
// sends everything, which is what it always did.
|
|
26
|
+
import crypto from "node:crypto";
|
|
27
|
+
import { platform } from "./platform.js";
|
|
28
|
+
import { installKey, publicUrl } from "./webhook.js";
|
|
29
|
+
export const MAIL_CATEGORIES = [
|
|
30
|
+
{
|
|
31
|
+
id: "security",
|
|
32
|
+
label: "Security",
|
|
33
|
+
required: true,
|
|
34
|
+
perWorkspace: false,
|
|
35
|
+
description: "Password resets, email confirmations, a login address changed, an account made for you.",
|
|
36
|
+
why: "these are how you get back into your account and how you hear that someone else tried to",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "invites",
|
|
40
|
+
label: "Invites",
|
|
41
|
+
required: true,
|
|
42
|
+
perWorkspace: false,
|
|
43
|
+
description: "An invitation to join an account. Sent once, to an address someone typed.",
|
|
44
|
+
why: "an invite is a one-off message someone asked to send you, not a subscription",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: "billing",
|
|
48
|
+
label: "Billing failures",
|
|
49
|
+
required: true,
|
|
50
|
+
perWorkspace: false,
|
|
51
|
+
description: "A payment that failed, and the grace period before runs stop.",
|
|
52
|
+
why: "a failed payment you never hear about is an account that stops working without warning",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
id: "account",
|
|
56
|
+
label: "Account notices",
|
|
57
|
+
required: true,
|
|
58
|
+
perWorkspace: false,
|
|
59
|
+
description: "Something about the account stopped working on its own — a notification webhook switched off after days of failures, a close request.",
|
|
60
|
+
why: "each one is the only message saying a thing you rely on has stopped",
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: "run-alerts",
|
|
64
|
+
label: "Run alerts",
|
|
65
|
+
required: false,
|
|
66
|
+
perWorkspace: true,
|
|
67
|
+
description: "A run failed, finished, or was blocked; a flow was quarantined, a run passed its SLA, a budget is nearly spent — mail from a workspace's notify: email.",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "approvals",
|
|
71
|
+
label: "Approval requests",
|
|
72
|
+
required: false,
|
|
73
|
+
perWorkspace: true,
|
|
74
|
+
description: "A run is waiting for a person, with approve and reject links. Off means you decide from the dashboard.",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "low-balance",
|
|
78
|
+
label: "Low-balance warnings",
|
|
79
|
+
required: false,
|
|
80
|
+
perWorkspace: false,
|
|
81
|
+
description: "Credits are below the warning level. Runs still stop at $0 whether or not you are told.",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "product-updates",
|
|
85
|
+
label: "Product updates",
|
|
86
|
+
required: false,
|
|
87
|
+
perWorkspace: false,
|
|
88
|
+
description: "What changed in foldrun. Rare.",
|
|
89
|
+
},
|
|
90
|
+
];
|
|
91
|
+
export function mailCategory(id) {
|
|
92
|
+
return MAIL_CATEGORIES.find((c) => c.id === id) ?? null;
|
|
93
|
+
}
|
|
94
|
+
export function isRequiredCategory(id) {
|
|
95
|
+
return mailCategory(id)?.required ?? false;
|
|
96
|
+
}
|
|
97
|
+
// ------------------------------------------------------------ unsubscribe
|
|
98
|
+
/** How long a token in a mail keeps working. Long: a person unsubscribes
|
|
99
|
+
* from a mail they kept, often months later, and an unsubscribe link that
|
|
100
|
+
* has died is the one that sends them to the spam button instead. */
|
|
101
|
+
export const UNSUBSCRIBE_TTL_MS = (Number(process.env.FOLDRUN_UNSUBSCRIBE_TTL_DAYS) || 180) * 86400_000;
|
|
102
|
+
const b64 = (s) => Buffer.from(s, "utf8").toString("base64url");
|
|
103
|
+
const sign = (payload) => crypto.createHmac("sha256", installKey()).update(`unsubscribe:${payload}`).digest("base64url").slice(0, 32);
|
|
104
|
+
/**
|
|
105
|
+
* `<payload>.<signature>`: the payload is base64url JSON of who, what and
|
|
106
|
+
* until when; the signature is an HMAC under the install key, so the token
|
|
107
|
+
* is derived, never stored, and a key rotation kills every old one.
|
|
108
|
+
*/
|
|
109
|
+
export function unsubscribeToken(c, now = Date.now()) {
|
|
110
|
+
const claim = {
|
|
111
|
+
t: c.tenant,
|
|
112
|
+
e: c.email.trim().toLowerCase(),
|
|
113
|
+
c: c.category,
|
|
114
|
+
w: c.workspace ?? null,
|
|
115
|
+
x: Math.floor(c.expiresAt ?? now + UNSUBSCRIBE_TTL_MS),
|
|
116
|
+
};
|
|
117
|
+
const payload = b64(JSON.stringify(claim));
|
|
118
|
+
return `${payload}.${sign(payload)}`;
|
|
119
|
+
}
|
|
120
|
+
/** What a presented token says, or why it says nothing. */
|
|
121
|
+
export function readUnsubscribeToken(token, now = Date.now()) {
|
|
122
|
+
if (!token || typeof token !== "string")
|
|
123
|
+
return { error: "no token", status: 400 };
|
|
124
|
+
const [payload, sig, extra] = token.split(".");
|
|
125
|
+
if (!payload || !sig || extra !== undefined)
|
|
126
|
+
return { error: "invalid token", status: 401 };
|
|
127
|
+
const want = sign(payload);
|
|
128
|
+
if (sig.length !== want.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(want))) {
|
|
129
|
+
return { error: "invalid token", status: 401 };
|
|
130
|
+
}
|
|
131
|
+
let raw;
|
|
132
|
+
try {
|
|
133
|
+
raw = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { error: "invalid token", status: 401 };
|
|
137
|
+
}
|
|
138
|
+
if (typeof raw.t !== "string" || typeof raw.e !== "string" || typeof raw.c !== "string" || typeof raw.x !== "number") {
|
|
139
|
+
return { error: "invalid token", status: 401 };
|
|
140
|
+
}
|
|
141
|
+
const cat = mailCategory(raw.c);
|
|
142
|
+
if (!cat)
|
|
143
|
+
return { error: "invalid token", status: 401 };
|
|
144
|
+
if (raw.x < now)
|
|
145
|
+
return { error: "this unsubscribe link has expired — change what you receive from Profile → Notifications", status: 410 };
|
|
146
|
+
return {
|
|
147
|
+
claim: { tenant: raw.t, email: raw.e, category: cat.id, workspace: typeof raw.w === "string" ? raw.w : null, expiresAt: raw.x },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/** The page a person opens, and the URL a mail client POSTs to. Null when
|
|
151
|
+
* the install does not know its own address — then there is no link. */
|
|
152
|
+
export function unsubscribeLinks(claim, now = Date.now()) {
|
|
153
|
+
const base = publicUrl();
|
|
154
|
+
if (!base)
|
|
155
|
+
return null;
|
|
156
|
+
const token = encodeURIComponent(unsubscribeToken(claim, now));
|
|
157
|
+
return { page: `${base}/unsubscribe?token=${token}`, oneClick: `${base}/api/unsubscribe?token=${token}` };
|
|
158
|
+
}
|
|
159
|
+
/** The RFC 8058 pair, for a mail whose category may be turned off. */
|
|
160
|
+
export function unsubscribeHeaders(claim, now = Date.now()) {
|
|
161
|
+
const links = unsubscribeLinks(claim, now);
|
|
162
|
+
if (!links)
|
|
163
|
+
return {};
|
|
164
|
+
return { "List-Unsubscribe": `<${links.oneClick}>`, "List-Unsubscribe-Post": "List-Unsubscribe=One-Click" };
|
|
165
|
+
}
|
|
166
|
+
const recipients = (to) => (Array.isArray(to) ? to : to.split(","))
|
|
167
|
+
.map((s) => s.trim())
|
|
168
|
+
.filter(Boolean);
|
|
169
|
+
/** Does this person take this category of mail? Required mail: always. */
|
|
170
|
+
export async function mailWanted(tenant, email, category, workspace = null) {
|
|
171
|
+
if (isRequiredCategory(category))
|
|
172
|
+
return true;
|
|
173
|
+
try {
|
|
174
|
+
return await platform.mailPreference(tenant, email.trim().toLowerCase(), category, workspace);
|
|
175
|
+
}
|
|
176
|
+
catch (err) {
|
|
177
|
+
// A preferences store that cannot be read sends: a missed alert is
|
|
178
|
+
// worse than one unwanted mail, and the next send asks again.
|
|
179
|
+
console.error(`[foldrun] mail: preference for ${category} unreadable, sending:`, err instanceof Error ? err.message : err);
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Send one message, honouring each recipient's preference for its
|
|
185
|
+
* category. Optional mail is sent one recipient at a time so each copy
|
|
186
|
+
* carries that reader's own unsubscribe token; required mail goes as one.
|
|
187
|
+
* Never throws: a mail that fails is an `ok: false` with the reason.
|
|
188
|
+
*/
|
|
189
|
+
export async function sendMail(m) {
|
|
190
|
+
const all = recipients(m.to);
|
|
191
|
+
const required = isRequiredCategory(m.category);
|
|
192
|
+
const workspace = m.workspace ?? null;
|
|
193
|
+
const sent = [];
|
|
194
|
+
const suppressed = [];
|
|
195
|
+
let status;
|
|
196
|
+
let error;
|
|
197
|
+
const post = async (to, headers, footer) => {
|
|
198
|
+
try {
|
|
199
|
+
const res = await fetch("https://api.resend.com/emails", {
|
|
200
|
+
method: "POST",
|
|
201
|
+
headers: { authorization: `Bearer ${m.mail.key}`, "content-type": "application/json" },
|
|
202
|
+
body: JSON.stringify({
|
|
203
|
+
from: m.mail.from,
|
|
204
|
+
to: to.length === 1 ? to[0] : to,
|
|
205
|
+
subject: m.subject,
|
|
206
|
+
text: m.text + footer,
|
|
207
|
+
...(m.html ? { html: m.html } : {}),
|
|
208
|
+
...(Object.keys(headers).length ? { headers } : {}),
|
|
209
|
+
}),
|
|
210
|
+
signal: AbortSignal.timeout(8000),
|
|
211
|
+
});
|
|
212
|
+
status = res.status;
|
|
213
|
+
if (!res.ok) {
|
|
214
|
+
error = (await res.text().catch(() => "")).replace(/\s+/g, " ").slice(0, 300) || `HTTP ${res.status}`;
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
sent.push(...to);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
error = err instanceof Error ? err.message : String(err);
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
if (required) {
|
|
226
|
+
if (all.length)
|
|
227
|
+
await post(all, {}, "");
|
|
228
|
+
return { ok: all.length > 0 && sent.length === all.length, status, error, sent, suppressed };
|
|
229
|
+
}
|
|
230
|
+
for (const to of all) {
|
|
231
|
+
if (!(await mailWanted(m.tenant, to, m.category, workspace))) {
|
|
232
|
+
suppressed.push(to);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const claim = { tenant: m.tenant, email: to, category: m.category, workspace: mailCategory(m.category)?.perWorkspace ? workspace : null };
|
|
236
|
+
const links = unsubscribeLinks(claim);
|
|
237
|
+
const label = mailCategory(m.category)?.label.toLowerCase() ?? m.category;
|
|
238
|
+
const footer = links
|
|
239
|
+
? `\n\n—\nStop ${label}${claim.workspace ? ` from ${claim.workspace}` : ""}: ${links.page}\n`
|
|
240
|
+
: "";
|
|
241
|
+
await post([to], unsubscribeHeaders(claim), footer);
|
|
242
|
+
}
|
|
243
|
+
const wanted = all.length - suppressed.length;
|
|
244
|
+
return { ok: sent.length === wanted, status, error, sent, suppressed };
|
|
245
|
+
}
|
|
@@ -12,6 +12,9 @@ export interface ProbeReport {
|
|
|
12
12
|
durationMs: number;
|
|
13
13
|
/** The reply itself, for the human reading a failure. */
|
|
14
14
|
reply: string | null;
|
|
15
|
+
/** Why the last turn ended — end_turn on a healthy probe; max_tokens,
|
|
16
|
+
* refusal or model_context_window_exceeded say why one failed. */
|
|
17
|
+
stopReason: string | null;
|
|
15
18
|
}
|
|
16
19
|
/**
|
|
17
20
|
* Run the tool-loop probe against one model, through whatever endpoint the
|
package/dist/src/model-probe.js
CHANGED
package/dist/src/notify.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { type RunRecord } from "./store.ts";
|
|
2
2
|
/**
|
|
3
|
-
* The platform's own mail: an invite, a low balance.
|
|
3
|
+
* The platform's own mail: an invite, a password reset, a low balance.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
5
|
+
* An account that configured its own mail — RESEND_API_KEY and EMAIL_FROM
|
|
6
|
+
* on the account — gets ALL of its mail from that sender, these included:
|
|
7
|
+
* a business that sends from its own domain does not want a second, foreign
|
|
8
|
+
* address turning up in its people's inboxes (2026-09-17, an owner found
|
|
9
|
+
* foldrun.io mail beside their own and asked for none of it). Only an
|
|
10
|
+
* account with no sender of its own falls to the platform's key and sender
|
|
11
|
+
* (FOLDRUN_RESEND_API_KEY, FOLDRUN_EMAIL_FROM), so invites still work
|
|
12
|
+
* before anyone has set mail up.
|
|
11
13
|
*/
|
|
12
14
|
export declare function platformMail(tenant: string): {
|
|
13
15
|
key: string;
|
|
@@ -57,6 +59,50 @@ export declare function notifyConfig(tenant: string, workspace: string): NotifyC
|
|
|
57
59
|
* not check one is no worse off than before.
|
|
58
60
|
*/
|
|
59
61
|
export declare function signatureHeaders(tenant: string, workspace: string, config: NotifyConfig, body: string): Record<string, string>;
|
|
62
|
+
/** When each attempt is made, counted from the one before: at once, then
|
|
63
|
+
* 1m, 5m, 30m, 2h and 6h. Six attempts over about eight and a half hours,
|
|
64
|
+
* then the delivery is given up and stays in the log to be redelivered. */
|
|
65
|
+
export declare const WEBHOOK_BACKOFF_MS: readonly [0, 60000, number, number, number, number];
|
|
66
|
+
/** When attempt `n + 1` is due after attempt `n` (1-based) failed at
|
|
67
|
+
* `at`, or null when `n` was the last. */
|
|
68
|
+
export declare function nextAttemptAt(n: number, at: number): number | null;
|
|
69
|
+
/** The delivery id: sent as X-Foldrun-Delivery on every attempt of one
|
|
70
|
+
* delivery, redeliveries included, so a receiver dedupes on it. */
|
|
71
|
+
export declare function newDeliveryId(): string;
|
|
72
|
+
/** The URL with ${SECRET}s resolved, and how it may be shown: a Slack hook
|
|
73
|
+
* URL is itself a credential, so it is never echoed resolved. */
|
|
74
|
+
export declare function webhookTarget(tenant: string, workspace: string, config?: NotifyConfig | null): {
|
|
75
|
+
url: string | null;
|
|
76
|
+
shown: string;
|
|
77
|
+
};
|
|
78
|
+
export interface WebhookAttempt {
|
|
79
|
+
/** 1-based. */
|
|
80
|
+
n: number;
|
|
81
|
+
at: string;
|
|
82
|
+
/** The receiver's status, or null when none came back (a timeout, DNS). */
|
|
83
|
+
statusCode: number | null;
|
|
84
|
+
durationMs: number;
|
|
85
|
+
/** The first 500 characters of what the receiver answered. */
|
|
86
|
+
response: string | null;
|
|
87
|
+
error: string | null;
|
|
88
|
+
/** A person pressed redeliver or test; not part of the backoff. */
|
|
89
|
+
manual?: boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* One attempt at one delivery, against the workspace's notify: as it reads
|
|
93
|
+
* NOW — a URL fixed between attempts is the one the retry goes to. Signed
|
|
94
|
+
* afresh each time (the timestamp is inside the signature, so a stale
|
|
95
|
+
* signature is one a receiver should refuse). Never throws.
|
|
96
|
+
*/
|
|
97
|
+
export declare function attemptWebhook(tenant: string, workspace: string, d: {
|
|
98
|
+
id: string;
|
|
99
|
+
event: string;
|
|
100
|
+
body: string;
|
|
101
|
+
attempt: number;
|
|
102
|
+
manual?: boolean;
|
|
103
|
+
}): Promise<WebhookAttempt & {
|
|
104
|
+
ok: boolean;
|
|
105
|
+
}>;
|
|
60
106
|
/** One waiting step's pair of links. */
|
|
61
107
|
export interface StepLinks {
|
|
62
108
|
/** Index into run.steps. */
|