@foldrun/core 0.2.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.
Files changed (166) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +239 -0
  3. package/dist/index.d.ts +32 -0
  4. package/dist/index.js +43 -0
  5. package/dist/src/agent-tools.d.ts +31 -0
  6. package/dist/src/agent-tools.js +99 -0
  7. package/dist/src/agents-md.d.ts +9 -0
  8. package/dist/src/agents-md.js +83 -0
  9. package/dist/src/api-tools.d.ts +38 -0
  10. package/dist/src/api-tools.js +313 -0
  11. package/dist/src/approvals.d.ts +39 -0
  12. package/dist/src/approvals.js +103 -0
  13. package/dist/src/arrange.d.ts +8 -0
  14. package/dist/src/arrange.js +40 -0
  15. package/dist/src/catalog.d.ts +108 -0
  16. package/dist/src/catalog.js +255 -0
  17. package/dist/src/completions.d.ts +61 -0
  18. package/dist/src/completions.js +578 -0
  19. package/dist/src/confine.d.ts +31 -0
  20. package/dist/src/confine.js +298 -0
  21. package/dist/src/container.d.ts +30 -0
  22. package/dist/src/container.js +172 -0
  23. package/dist/src/context-tools.d.ts +81 -0
  24. package/dist/src/context-tools.js +347 -0
  25. package/dist/src/deploy.d.ts +100 -0
  26. package/dist/src/deploy.js +377 -0
  27. package/dist/src/egress.d.ts +75 -0
  28. package/dist/src/egress.js +133 -0
  29. package/dist/src/evals.d.ts +150 -0
  30. package/dist/src/evals.js +651 -0
  31. package/dist/src/flow-lint.d.ts +16 -0
  32. package/dist/src/flow-lint.js +177 -0
  33. package/dist/src/git-hook.d.ts +1 -0
  34. package/dist/src/git-hook.js +70 -0
  35. package/dist/src/git.d.ts +37 -0
  36. package/dist/src/git.js +101 -0
  37. package/dist/src/gitrepo.d.ts +95 -0
  38. package/dist/src/gitrepo.js +355 -0
  39. package/dist/src/history.d.ts +82 -0
  40. package/dist/src/history.js +259 -0
  41. package/dist/src/kinds.d.ts +95 -0
  42. package/dist/src/kinds.js +421 -0
  43. package/dist/src/library.d.ts +94 -0
  44. package/dist/src/library.js +341 -0
  45. package/dist/src/linkable.d.ts +20 -0
  46. package/dist/src/linkable.js +113 -0
  47. package/dist/src/model-probe.d.ts +21 -0
  48. package/dist/src/model-probe.js +81 -0
  49. package/dist/src/notify.d.ts +61 -0
  50. package/dist/src/notify.js +252 -0
  51. package/dist/src/oauth-presets.d.ts +13 -0
  52. package/dist/src/oauth-presets.js +39 -0
  53. package/dist/src/okf.d.ts +220 -0
  54. package/dist/src/okf.js +656 -0
  55. package/dist/src/openapi.d.ts +66 -0
  56. package/dist/src/openapi.js +342 -0
  57. package/dist/src/paths.d.ts +33 -0
  58. package/dist/src/paths.js +78 -0
  59. package/dist/src/platform.d.ts +32 -0
  60. package/dist/src/platform.js +48 -0
  61. package/dist/src/providers.d.ts +47 -0
  62. package/dist/src/providers.js +114 -0
  63. package/dist/src/refs.d.ts +20 -0
  64. package/dist/src/refs.js +78 -0
  65. package/dist/src/run-container.d.ts +227 -0
  66. package/dist/src/run-container.js +760 -0
  67. package/dist/src/runner.d.ts +241 -0
  68. package/dist/src/runner.js +3215 -0
  69. package/dist/src/runtime.d.ts +41 -0
  70. package/dist/src/runtime.js +357 -0
  71. package/dist/src/script-tools.d.ts +62 -0
  72. package/dist/src/script-tools.js +296 -0
  73. package/dist/src/secret-files.d.ts +12 -0
  74. package/dist/src/secret-files.js +92 -0
  75. package/dist/src/secrets.d.ts +147 -0
  76. package/dist/src/secrets.js +626 -0
  77. package/dist/src/starter.d.ts +21 -0
  78. package/dist/src/starter.js +399 -0
  79. package/dist/src/step-exec.d.ts +136 -0
  80. package/dist/src/step-exec.js +391 -0
  81. package/dist/src/storage.d.ts +123 -0
  82. package/dist/src/storage.js +640 -0
  83. package/dist/src/store.d.ts +1052 -0
  84. package/dist/src/store.js +2378 -0
  85. package/dist/src/tar.d.ts +30 -0
  86. package/dist/src/tar.js +111 -0
  87. package/dist/src/tool-names.d.ts +39 -0
  88. package/dist/src/tool-names.js +79 -0
  89. package/dist/src/tool-programs.d.ts +18 -0
  90. package/dist/src/tool-programs.js +52 -0
  91. package/dist/src/tool-test.d.ts +19 -0
  92. package/dist/src/tool-test.js +335 -0
  93. package/dist/src/translator.d.ts +159 -0
  94. package/dist/src/translator.js +986 -0
  95. package/dist/src/triggers.d.ts +60 -0
  96. package/dist/src/triggers.js +270 -0
  97. package/dist/src/webhook.d.ts +47 -0
  98. package/dist/src/webhook.js +148 -0
  99. package/index.ts +44 -0
  100. package/package.json +64 -0
  101. package/src/agent-tools.ts +138 -0
  102. package/src/agents-md.ts +81 -0
  103. package/src/api-tools.ts +389 -0
  104. package/src/approvals.ts +129 -0
  105. package/src/arrange.ts +42 -0
  106. package/src/catalog.ts +321 -0
  107. package/src/completions.ts +652 -0
  108. package/src/confine.ts +350 -0
  109. package/src/container.ts +220 -0
  110. package/src/context-tools.ts +435 -0
  111. package/src/deploy.ts +458 -0
  112. package/src/egress.ts +176 -0
  113. package/src/evals.ts +812 -0
  114. package/src/flow-lint.ts +215 -0
  115. package/src/git-hook.ts +73 -0
  116. package/src/git.ts +117 -0
  117. package/src/gitrepo.ts +391 -0
  118. package/src/history.ts +329 -0
  119. package/src/kinds.ts +507 -0
  120. package/src/library.ts +419 -0
  121. package/src/linkable.ts +123 -0
  122. package/src/model-probe.ts +112 -0
  123. package/src/notify.ts +281 -0
  124. package/src/oauth-presets.ts +54 -0
  125. package/src/okf.ts +768 -0
  126. package/src/openapi.ts +383 -0
  127. package/src/paths.ts +79 -0
  128. package/src/platform.ts +87 -0
  129. package/src/providers.ts +147 -0
  130. package/src/refs.ts +83 -0
  131. package/src/run-container.ts +883 -0
  132. package/src/runner.ts +3446 -0
  133. package/src/runtime.ts +404 -0
  134. package/src/script-tools.ts +383 -0
  135. package/src/secret-files.ts +101 -0
  136. package/src/secrets.ts +799 -0
  137. package/src/starter.ts +415 -0
  138. package/src/step-exec.ts +498 -0
  139. package/src/storage.ts +771 -0
  140. package/src/store.ts +2985 -0
  141. package/src/tar.ts +124 -0
  142. package/src/tool-names.ts +91 -0
  143. package/src/tool-programs.ts +65 -0
  144. package/src/tool-test.ts +401 -0
  145. package/src/translator.ts +1029 -0
  146. package/src/triggers.ts +287 -0
  147. package/src/webhook.ts +183 -0
  148. package/templates/blog-desk/AGENTS.md +13 -0
  149. package/templates/blog-desk/agents/researcher/agent.md +11 -0
  150. package/templates/blog-desk/agents/writer/agent.md +8 -0
  151. package/templates/blog-desk/evals/writer-quality.md +10 -0
  152. package/templates/blog-desk/flows/publish.md +10 -0
  153. package/templates/blog-desk/knowledge/house-style.md +9 -0
  154. package/templates/blog-desk/knowledge/index.md +9 -0
  155. package/templates/hello/AGENTS.md +18 -0
  156. package/templates/hello/agents/curator/agent.md +32 -0
  157. package/templates/hello/agents/notetaker/agent.md +24 -0
  158. package/templates/hello/evals/house-style.md +16 -0
  159. package/templates/hello/flows/memory-review.md +7 -0
  160. package/templates/hello/flows/note.md +7 -0
  161. package/templates/hello/knowledge/house-style.md +15 -0
  162. package/templates/hello/knowledge/index.md +9 -0
  163. package/templates/hello/skills/plain-english/SKILL.md +11 -0
  164. package/templates/hello/state/publishing.md +7 -0
  165. package/templates/hello/tools/wordcount/run.py +13 -0
  166. package/templates/hello/tools/wordcount/tool.md +22 -0
package/src/notify.ts ADDED
@@ -0,0 +1,281 @@
1
+ // Telling a person what their agents did while they were not looking.
2
+ //
3
+ // An agent that runs when the laptop is closed needs a way to say "I
4
+ // finished", "I failed", and above all "I am waiting for you" — a parked
5
+ // approval nobody hears about is a run that never happens. The channel is a
6
+ // webhook out: one URL, declared in AGENTS.md frontmatter, POSTed a small
7
+ // JSON body. Slack, Discord, ntfy, a phone-push relay and a plain inbox
8
+ // service all speak that; building any one of them natively would just be
9
+ // this plus vendor formatting.
10
+ //
11
+ // notify:
12
+ // url: ${SLACK_WEBHOOK_URL} # a secret name, or a literal URL
13
+ // events: [failed, awaiting-approval, completed]
14
+ //
15
+ // Or, for people who live in an inbox rather than a channel:
16
+ //
17
+ // notify:
18
+ // email: ops@example.com # sent via the RESEND_API_KEY connection
19
+ // events: [failed, completed]
20
+ //
21
+ // Email is the one vendor worth speaking natively: the platform already
22
+ // holds a Resend connection for its email tool, and "just email me" is the
23
+ // notification default for most of the world. Everything else stays a
24
+ // webhook — one URL covers Slack, Discord, ntfy and every relay.
25
+ //
26
+ // Workspace config replaces account config whole, like provider: — a URL is
27
+ // a destination, and merging two destinations sends half your alerts to a
28
+ // channel you stopped reading.
29
+ //
30
+ // Defaults: failed and awaiting-approval. Completed is opt-in — a schedule
31
+ // that works is the quiet kind of good news, and a channel that pings on
32
+ // every success gets muted, which un-pings the failures too.
33
+
34
+ import { readAgentsMd } from "./runner.ts";
35
+ import { accountDir, workspaceDir, runCost, type RunRecord } from "./store.ts";
36
+ import { getSecret } from "./secrets.ts";
37
+ import { approveToken, publicUrl } from "./webhook.ts";
38
+
39
+ /**
40
+ * The platform's own mail: an invite, a low balance.
41
+ *
42
+ * These are from foldrun, about the platform, to the account's owner — so
43
+ * they go through the platform's Resend key and sender (FOLDRUN_RESEND_API_KEY,
44
+ * FOLDRUN_EMAIL_FROM), never through anything the customer configured.
45
+ *
46
+ * Without a platform key — the CLI on a laptop, a test — the account's own
47
+ * key and EMAIL_FROM are the fallback.
48
+ */
49
+ export function platformMail(tenant: string): { key: string; from: string } | null {
50
+ const key = process.env.FOLDRUN_RESEND_API_KEY;
51
+ if (key) return { key, from: process.env.FOLDRUN_EMAIL_FROM || "foldrun <hello@foldrun.io>" };
52
+ return accountMail(tenant);
53
+ }
54
+
55
+ /** The account's own Resend key and sender, or null when it has none. */
56
+ export function accountMail(tenant: string): { key: string; from: string } | null {
57
+ const own = getSecret(tenant, "RESEND_API_KEY");
58
+ if (!own) return null;
59
+ const from = getSecret(tenant, "EMAIL_FROM")?.value?.trim() || "foldrun <onboarding@resend.dev>";
60
+ return { key: own.value, from };
61
+ }
62
+
63
+ /**
64
+ * A run notification is the account's mail, not the platform's: it is a
65
+ * desk telling its owner what it found, and the owner chose the sender —
66
+ * their RESEND_API_KEY and EMAIL_FROM on the account, the same connection
67
+ * their agents' `email` tool uses. So it comes from the address they
68
+ * expect, under their own domain's reputation and their own inbox rules;
69
+ * 2026-09-06, every notification from the platform's address was in the
70
+ * owner's bin. The platform's key is the fallback for an account that set
71
+ * none, so `notify:` works before anyone has configured mail at all.
72
+ */
73
+ export function notifyMail(tenant: string): { key: string; from: string } | null {
74
+ return accountMail(tenant) ?? platformMail(tenant);
75
+ }
76
+
77
+ export interface NotifyConfig {
78
+ url?: string;
79
+ email?: string;
80
+ events: string[];
81
+ }
82
+
83
+ const DEFAULT_EVENTS = ["failed", "awaiting-approval"];
84
+
85
+ export function notifyConfig(tenant: string, workspace: string): NotifyConfig | null {
86
+ const raw =
87
+ (readAgentsMd(workspaceDir(tenant, workspace))?.data.notify as unknown) ??
88
+ (readAgentsMd(accountDir(tenant))?.data.notify as unknown);
89
+ if (!raw) return null;
90
+ if (typeof raw === "string") {
91
+ // A bare string is whichever destination it looks like.
92
+ return raw.includes("@") && !raw.includes("/")
93
+ ? { email: raw, events: DEFAULT_EVENTS }
94
+ : { url: raw, events: DEFAULT_EVENTS };
95
+ }
96
+ if (typeof raw === "object" && raw !== null) {
97
+ const o = raw as { url?: unknown; email?: unknown; events?: unknown };
98
+ const url = typeof o.url === "string" ? o.url : undefined;
99
+ const email = typeof o.email === "string" ? o.email : undefined;
100
+ if (!url && !email) return null;
101
+ return {
102
+ url,
103
+ email,
104
+ events: Array.isArray(o.events) && o.events.length ? o.events.map(String) : DEFAULT_EVENTS,
105
+ };
106
+ }
107
+ return null;
108
+ }
109
+
110
+ // Said once per process, not once per run: the fix is one env edit, and a
111
+ // line per parked run would bury the failures around it.
112
+ let warnedNoPublicUrl = false;
113
+
114
+ /**
115
+ * The approve/reject links for a run waiting on a person, or null when the
116
+ * install cannot say where it lives. A notification that only says "waiting
117
+ * for you" sends the reader to find a laptop; one that carries the decision
118
+ * lets them make it where they read it.
119
+ *
120
+ * The reject link goes to the same page with the choice pre-selected — a
121
+ * GET must not decide anything, because inbox link-checkers follow links.
122
+ */
123
+ export function approvalLinks(
124
+ tenant: string,
125
+ workspace: string,
126
+ runId: string,
127
+ ): { approveUrl: string; rejectUrl: string } | null {
128
+ const base = publicUrl();
129
+ if (!base) {
130
+ if (!warnedNoPublicUrl) {
131
+ warnedNoPublicUrl = true;
132
+ console.error(
133
+ "[foldrun] notify: approval links need FOLDRUN_PUBLIC_URL (the install's public origin); sending without them",
134
+ );
135
+ }
136
+ return null;
137
+ }
138
+ const approveUrl = `${base}/api/approve/${tenant}/${workspace}/${runId}?token=${approveToken(tenant, workspace, runId)}`;
139
+ return { approveUrl, rejectUrl: `${approveUrl}&decision=reject` };
140
+ }
141
+
142
+ /**
143
+ * Fire the webhook for a run's state, if the workspace asked to hear about
144
+ * it. Failures are logged and swallowed — a broken Slack hook must never
145
+ * fail the run it is reporting on.
146
+ */
147
+ /** Flows whose completion is nobody's news: eval cases and adhoc runs. */
148
+ export function isQuietFlow(flow: string): boolean {
149
+ return /^(eval|adhoc):/.test(flow);
150
+ }
151
+
152
+ export async function sendRunNotification(
153
+ tenant: string,
154
+ workspace: string,
155
+ run: RunRecord,
156
+ ): Promise<boolean> {
157
+ const config = notifyConfig(tenant, workspace);
158
+ if (!config || !config.events.includes(run.status)) return false;
159
+ // A test is not news. An eval case or an adhoc single-agent run is started
160
+ // by a person who is watching it, and a desk that emails "completed" for
161
+ // every one of them buries the weekly verdict under two hundred of these —
162
+ // on 2026-09-06 the reader's inbox rule had sent the lot to the bin, the
163
+ // real reports with them. Failures and gates still send: a person asked.
164
+ if (run.status === "completed" && isQuietFlow(run.flow)) return false;
165
+
166
+ const failed = run.steps.filter((s) => s.status === "failed").map((s) => s.agent);
167
+ const waitingSteps = run.steps.filter((s) => s.status === "awaiting-approval");
168
+ const waiting = waitingSteps.map((s) => s.agent);
169
+ // A park on `wait: event` is not a question for the reader: the message
170
+ // says what the run is waiting for, so nobody hunts for a button that
171
+ // the outside world is meant to press.
172
+ const onEvent = waitingSteps.length > 0 && waitingSteps.every((s) => s.waitFor === "event");
173
+ const headline =
174
+ run.status === "completed"
175
+ ? `✓ ${run.flow} completed`
176
+ : run.status === "failed"
177
+ ? `✗ ${run.flow} failed${failed.length ? ` at ${failed.join(", ")}` : ""}`
178
+ : onEvent
179
+ ? `⏳ ${run.flow} is waiting for an external event${waiting.length ? ` (${waiting.join(", ")})` : ""}`
180
+ : `⏸ ${run.flow} is waiting for your approval${waiting.length ? ` (${waiting.join(", ")})` : ""}`;
181
+
182
+ // What the run concluded, if it concluded anything. This is the whole point
183
+ // of the message: "✓ health completed · $0.42" tells you a thing ran and
184
+ // what it cost, and nothing at all about what it found. A scheduled desk
185
+ // reporting only its own existence is a desk nobody reads by week three.
186
+ const summary = run.summary?.trim() || null;
187
+
188
+ const links = run.status === "awaiting-approval" ? approvalLinks(tenant, workspace, run.id) : null;
189
+ // At a gate the reader is being asked something; the question goes in the
190
+ // mail, and so does what the run has concluded so far — the last finished
191
+ // step's first line, which is the proposal's own summary ("… ·
192
+ // TARGETS-PROPOSAL: 1 change"). Without these a gate email said only
193
+ // that a step was waiting, and the decision meant opening the dashboard.
194
+ const asks = waitingSteps.map((s) => s.ask?.trim()).filter((a): a is string => Boolean(a));
195
+ const soFar =
196
+ run.status === "awaiting-approval" && !summary
197
+ ? [...run.steps].reverse().find((s) => s.status === "completed" && s.result?.trim())?.result?.trim().split("\n")[0] ?? null
198
+ : null;
199
+
200
+ const body = {
201
+ // `text` is what Slack-shaped receivers render; the rest is for anything
202
+ // that wants the data instead of the sentence.
203
+ text: summary
204
+ ? `${headline} — ${summary} · ${workspace}/${run.id} · $${runCost(run).toFixed(4)}`
205
+ : `${headline} — ${workspace}/${run.id} · $${runCost(run).toFixed(4)}`,
206
+ workspace,
207
+ runId: run.id,
208
+ flow: run.flow,
209
+ status: run.status,
210
+ summary,
211
+ costUsd: runCost(run),
212
+ startedAt: run.startedAt,
213
+ finishedAt: run.finishedAt,
214
+ ...(links ?? {}),
215
+ };
216
+
217
+ try {
218
+ if (config.email) {
219
+ const mail = notifyMail(tenant);
220
+ if (!mail) {
221
+ console.error(`[foldrun] notify: email configured but no mail credential — set RESEND_API_KEY (and EMAIL_FROM) on the account ${tenant}, or FOLDRUN_RESEND_API_KEY on the platform`);
222
+ return false;
223
+ }
224
+ const res = await fetch("https://api.resend.com/emails", {
225
+ method: "POST",
226
+ headers: { authorization: `Bearer ${mail.key}`, "content-type": "application/json" },
227
+ body: JSON.stringify({
228
+ from: mail.from,
229
+ to: config.email,
230
+ // The subject is the headline and what the run concluded, and
231
+ // nothing else. Ids and costs belong in the body: a subject line is
232
+ // read in a list, where the only useful question it can answer is
233
+ // whether this one needs opening.
234
+ subject: summary ? `${headline} — ${summary}`.slice(0, 160) : headline,
235
+ text:
236
+ `${headline}\n` +
237
+ (summary ? `\n${summary}\n` : soFar ? `\n${soFar}\n` : "") +
238
+ (asks.length ? `\n${asks.map((a) => `Question: ${a}`).join("\n")}\n` : "") +
239
+ `\nworkspace: ${workspace}\nrun: ${run.id}\nflow: ${run.flow}\n` +
240
+ `cost: $${runCost(run).toFixed(4)}\nstarted: ${run.startedAt}\nfinished: ${run.finishedAt ?? "-"}\n` +
241
+ (links ? `\nApprove: ${links.approveUrl}\nReject: ${links.rejectUrl}\n` : ""),
242
+ }),
243
+ signal: AbortSignal.timeout(8000),
244
+ });
245
+ if (!res.ok) {
246
+ // Resend's body says WHY — "domain not verified", "can only send to
247
+ // your own address" — and a status alone sent someone to the wrong
248
+ // dashboard for twenty minutes on 2026-09-06.
249
+ const why = (await res.text().catch(() => "")).replace(/\s+/g, " ").slice(0, 300);
250
+ console.error(`[foldrun] notify email: ${tenant}/${workspace} → HTTP ${res.status} ${why}`);
251
+ return false;
252
+ }
253
+ return true;
254
+ }
255
+
256
+ // ${SECRET} so the Slack URL — itself a credential — can live in the
257
+ // vault instead of in a file that gets committed.
258
+ const url = (config.url ?? "").replace(/\$\{([A-Z][A-Z0-9_]*)\}/g, (whole, name) => {
259
+ const hit = getSecret(tenant, name, workspace);
260
+ return hit ? hit.value : whole;
261
+ });
262
+ if (!url || url.includes("${")) {
263
+ console.error(`[foldrun] notify: secret in URL not set for ${tenant}/${workspace}`);
264
+ return false;
265
+ }
266
+ const res = await fetch(url, {
267
+ method: "POST",
268
+ headers: { "content-type": "application/json" },
269
+ body: JSON.stringify(body),
270
+ signal: AbortSignal.timeout(5000),
271
+ });
272
+ if (!res.ok) {
273
+ console.error(`[foldrun] notify: ${tenant}/${workspace} → HTTP ${res.status}`);
274
+ return false;
275
+ }
276
+ return true;
277
+ } catch (err) {
278
+ console.error(`[foldrun] notify: ${tenant}/${workspace} →`, err instanceof Error ? err.message : err);
279
+ return false;
280
+ }
281
+ }
@@ -0,0 +1,54 @@
1
+ // Prefills for the common OAuth providers: URLs and quirks, never
2
+ // credentials. One list, read by the CLI (`foldrun connect`), the dashboard's
3
+ // Connections form and the platform's consent callback, so a provider's
4
+ // refresh-token quirk is written down once.
5
+ //
6
+ // What stays with the account is the OAuth *client* (id + secret): a consent
7
+ // screen belongs to a registered app, so the developer brings those two
8
+ // values from the provider's console and everything else is here.
9
+
10
+ export interface OAuthPreset {
11
+ authorize_url: string;
12
+ token_url: string;
13
+ /** Extra authorize-URL params (Google: access_type=offline&prompt=consent). */
14
+ authorize_extra?: Record<string, string>;
15
+ /** Scopes to start from — every provider names them differently, and a
16
+ * form with the right shape in it is worth more than a blank field. */
17
+ scopes_example?: string;
18
+ /** The one thing about this provider that costs an afternoon to learn. */
19
+ hint: string;
20
+ }
21
+
22
+ export const OAUTH_PRESETS: Record<string, OAuthPreset> = {
23
+ google: {
24
+ authorize_url: "https://accounts.google.com/o/oauth2/v2/auth",
25
+ token_url: "https://oauth2.googleapis.com/token",
26
+ authorize_extra: { access_type: "offline", prompt: "consent" },
27
+ scopes_example: "https://www.googleapis.com/auth/adwords",
28
+ hint: "Scopes are full URLs. Offline access is requested for you, so a refresh token comes back.",
29
+ },
30
+ github: {
31
+ authorize_url: "https://github.com/login/oauth/authorize",
32
+ token_url: "https://github.com/login/oauth/access_token",
33
+ scopes_example: "repo read:org",
34
+ hint: "GitHub issues long-lived tokens and no refresh token — stored as a static secret.",
35
+ },
36
+ microsoft: {
37
+ authorize_url: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
38
+ token_url: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
39
+ scopes_example: "offline_access https://graph.microsoft.com/.default",
40
+ hint: "Include offline_access in the scopes or no refresh token is issued.",
41
+ },
42
+ linkedin: {
43
+ authorize_url: "https://www.linkedin.com/oauth/v2/authorization",
44
+ token_url: "https://www.linkedin.com/oauth/v2/accessToken",
45
+ scopes_example: "openid profile r_organization_admin r_organization_social w_organization_social",
46
+ hint:
47
+ "Posting as a company page needs w_organization_social and a page admin signing in; r_organization_admin lets the token list the pages it may post to. " +
48
+ "Access tokens last 60 days; a refresh token is issued only to apps LinkedIn has enabled for it, " +
49
+ "otherwise the token is stored as is and you connect again when it expires. " +
50
+ "The redirect URL must match what the app registers exactly, and only localhost may be plain http.",
51
+ },
52
+ };
53
+
54
+ export const OAUTH_PRESET_NAMES = Object.keys(OAUTH_PRESETS);