@foir/sdk 0.28.0 → 0.30.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/ai.js ADDED
@@ -0,0 +1,566 @@
1
+ // src/ai/index.ts
2
+ import { asSchema } from "ai";
3
+
4
+ // src/data/agents.ts
5
+ async function uploadAgentFile(foir, options) {
6
+ const agents = foir.ns.agents;
7
+ if (typeof agents?.createAgentAttachment !== "function") {
8
+ throw new Error("[foir] uploadAgentFile: this client sends no agent files. Its key runs none of the project's agents, or the project has released none.");
9
+ }
10
+ const { blob, mediaType, filename } = fileParts(options.file);
11
+ const up = await agents.createAgentAttachment({
12
+ agent: options.agent,
13
+ filename,
14
+ mediaType,
15
+ size: blob.size,
16
+ ...options.visitorToken ? { visitorToken: options.visitorToken } : {}
17
+ });
18
+ const put = await (options.fetch ?? fetch)(up.uploadUrl, { method: "PUT", headers: { "content-type": mediaType }, body: blob });
19
+ if (!put.ok) throw new Error(`[foir] uploadAgentFile: ${filename} was not uploaded (${put.status}).`);
20
+ return up.attachment;
21
+ }
22
+ function fileParts(file) {
23
+ if (typeof Blob !== "undefined" && file instanceof Blob) {
24
+ const name = file.name;
25
+ return { blob: file, mediaType: file.type, filename: name || "file" };
26
+ }
27
+ const { data, mediaType, filename } = file;
28
+ const blob = data instanceof Blob ? data : new Blob([data], { type: mediaType });
29
+ return { blob, mediaType, filename: filename || "file" };
30
+ }
31
+ var VISITOR_KEY = "foir:agents:visitor";
32
+ function visitorToken(storage = browserStorage(), key = VISITOR_KEY) {
33
+ try {
34
+ const kept = storage?.getItem(key);
35
+ if (kept) return kept;
36
+ } catch {
37
+ }
38
+ const made = randomSecret();
39
+ try {
40
+ storage?.setItem(key, made);
41
+ } catch {
42
+ }
43
+ return made;
44
+ }
45
+ function browserStorage() {
46
+ try {
47
+ return typeof localStorage === "undefined" ? null : localStorage;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+ function randomSecret() {
53
+ const bytes = new Uint8Array(24);
54
+ globalThis.crypto.getRandomValues(bytes);
55
+ let out = "";
56
+ for (const b of bytes) out += b.toString(16).padStart(2, "0");
57
+ return out;
58
+ }
59
+
60
+ // src/ai/index.ts
61
+ var TOOL_KINDS = /* @__PURE__ */ new Set(["VERB", "OPERATION", "MCP_TOOL", "AGENT", "PAGE_TOOL"]);
62
+ var STOPS = /* @__PURE__ */ new Set(["COMPLETED", "FAILED", "SKIPPED", "CANCELLED", "HELD"]);
63
+ var FoirRunChunks = class {
64
+ /** `runs` names the page's tools the transport runs itself. */
65
+ constructor(runId, runs = () => false) {
66
+ this.runId = runId;
67
+ this.runs = runs;
68
+ }
69
+ runId;
70
+ runs;
71
+ tools = /* @__PURE__ */ new Map();
72
+ /** The last event shown, to follow the run again from. */
73
+ after = "";
74
+ textId = null;
75
+ reasoningId = null;
76
+ opened = false;
77
+ /** The run stopped for this stream's followers: it ended, or holds an act. */
78
+ stopped = false;
79
+ /** Calls to the page's tools waiting for the app's `addToolOutput`. */
80
+ pageCalls = /* @__PURE__ */ new Set();
81
+ /** Calls to the page's tools the transport runs and answers itself, until the run hears them. */
82
+ answering = /* @__PURE__ */ new Set();
83
+ /** Calls the transport is to run: taken with `takeCalls`. */
84
+ toRun = [];
85
+ /** Acts held for a decision, by anyone. */
86
+ held = /* @__PURE__ */ new Set();
87
+ /** The calls to the page's tools that arrived for the transport to run. */
88
+ takeCalls() {
89
+ const out = this.toRun;
90
+ this.toRun = [];
91
+ return out;
92
+ }
93
+ /** Starts a new stream of the same run, as after a decision. */
94
+ resume() {
95
+ this.opened = false;
96
+ this.stopped = false;
97
+ this.textId = null;
98
+ this.reasoningId = null;
99
+ }
100
+ open(out, fresh) {
101
+ if (this.opened) return;
102
+ this.opened = true;
103
+ if (fresh) out.push({ type: "start" });
104
+ out.push({ type: "start-step" });
105
+ }
106
+ closeText(out) {
107
+ if (this.textId) out.push({ type: "text-end", id: this.textId });
108
+ this.textId = null;
109
+ }
110
+ closeReasoning(out) {
111
+ if (this.reasoningId) out.push({ type: "reasoning-end", id: this.reasoningId });
112
+ this.reasoningId = null;
113
+ }
114
+ /** The chunks an event makes. `fresh` is a stream that starts a new message. */
115
+ of(event, fresh = true) {
116
+ const out = [];
117
+ if (event.seq) this.after = event.seq;
118
+ switch (event.kind) {
119
+ case "TEXT": {
120
+ if (!event.text) break;
121
+ this.open(out, fresh);
122
+ this.closeReasoning(out);
123
+ if (!this.textId) {
124
+ this.textId = `${this.runId}:t:${event.seq}`;
125
+ out.push({ type: "text-start", id: this.textId });
126
+ }
127
+ out.push({ type: "text-delta", id: this.textId, delta: event.text });
128
+ break;
129
+ }
130
+ case "REASONING": {
131
+ if (!event.text) break;
132
+ this.open(out, fresh);
133
+ if (!this.reasoningId) {
134
+ this.reasoningId = `${this.runId}:r:${event.seq}`;
135
+ out.push({ type: "reasoning-start", id: this.reasoningId });
136
+ }
137
+ out.push({ type: "reasoning-delta", id: this.reasoningId, delta: event.text });
138
+ break;
139
+ }
140
+ case "NODE": {
141
+ const node = event.node;
142
+ if (!node || !TOOL_KINDS.has(node.kind) || node.id === this.runId) break;
143
+ this.open(out, fresh);
144
+ this.closeText(out);
145
+ this.closeReasoning(out);
146
+ out.push(...this.toolChunks(node));
147
+ break;
148
+ }
149
+ case "DATA": {
150
+ if (!event.dataName) break;
151
+ this.open(out, fresh);
152
+ out.push({ type: `data-${event.dataName}`, id: `${this.runId}:d:${event.seq}`, data: event.data ?? null });
153
+ break;
154
+ }
155
+ case "STATUS": {
156
+ if (!event.status || !STOPS.has(event.status)) break;
157
+ if (event.status === "HELD" && this.answering.size > 0 && this.pageCalls.size === 0 && this.held.size === 0) break;
158
+ this.open(out, fresh);
159
+ this.closeText(out);
160
+ this.closeReasoning(out);
161
+ if (event.status === "FAILED") {
162
+ out.push({ type: "error", errorText: event.error || "The answer could not be finished." });
163
+ }
164
+ if (event.status === "SKIPPED" && event.answer && this.tools.size === 0) {
165
+ const id = `${this.runId}:refusal`;
166
+ out.push({ type: "text-start", id }, { type: "text-delta", id, delta: event.answer }, { type: "text-end", id });
167
+ }
168
+ out.push({ type: "finish-step" }, { type: "finish" });
169
+ this.stopped = true;
170
+ break;
171
+ }
172
+ default:
173
+ break;
174
+ }
175
+ return out;
176
+ }
177
+ toolChunks(node) {
178
+ const out = [];
179
+ const shown = this.tools.get(node.id);
180
+ if (shown === node.status) return out;
181
+ if (shown === void 0) {
182
+ const input = node.input?.args ?? node.input ?? {};
183
+ out.push({ type: "tool-input-available", toolCallId: node.id, toolName: node.key, input, dynamic: true });
184
+ }
185
+ this.tools.set(node.id, node.status);
186
+ if (node.kind === "PAGE_TOOL") return this.pageToolChunks(node, out);
187
+ if (node.status === "HELD" && node.approval) this.held.add(node.id);
188
+ else this.held.delete(node.id);
189
+ switch (node.status) {
190
+ case "COMPLETED":
191
+ out.push({ type: "tool-output-available", toolCallId: node.id, output: node.result ?? {}, dynamic: true });
192
+ break;
193
+ case "HELD":
194
+ if (node.approval?.approver === "CALLER" && node.approval.decision === "PENDING") {
195
+ out.push({ type: "tool-approval-request", approvalId: node.id, toolCallId: node.id });
196
+ } else if (node.approval) {
197
+ out.push({
198
+ type: "tool-output-available",
199
+ toolCallId: node.id,
200
+ output: { status: "held", approver: node.approval.approver, summary: node.approval.summary },
201
+ preliminary: true,
202
+ dynamic: true
203
+ });
204
+ }
205
+ break;
206
+ case "SKIPPED":
207
+ if (node.approval?.decision === "DENIED") out.push({ type: "tool-output-denied", toolCallId: node.id });
208
+ else out.push({ type: "tool-output-error", toolCallId: node.id, errorText: node.error || "Refused.", dynamic: true });
209
+ break;
210
+ case "FAILED":
211
+ case "CANCELLED":
212
+ out.push({ type: "tool-output-error", toolCallId: node.id, errorText: node.error || "Refused.", dynamic: true });
213
+ break;
214
+ default:
215
+ break;
216
+ }
217
+ return out;
218
+ }
219
+ /**
220
+ * A call to one of the page's tools. The transport runs one it has an
221
+ * `execute` for and shows what it answered; any other waits for the app's
222
+ * `addToolOutput`. One the page did not answer in time is an error.
223
+ */
224
+ pageToolChunks(node, out) {
225
+ const ours = this.runs(node.key);
226
+ if (node.status === "HELD") {
227
+ if (ours) {
228
+ this.answering.add(node.id);
229
+ this.toRun.push(node);
230
+ } else {
231
+ this.pageCalls.add(node.id);
232
+ }
233
+ return out;
234
+ }
235
+ this.pageCalls.delete(node.id);
236
+ const answered = this.answering.delete(node.id);
237
+ if (node.status === "COMPLETED" && answered) {
238
+ const result = node.result;
239
+ out.push(
240
+ { type: "tool-output-available", toolCallId: node.id, output: result?.result ?? null, dynamic: true },
241
+ { type: "finish-step" },
242
+ { type: "start-step" }
243
+ );
244
+ } else if (node.status === "FAILED" || node.status === "CANCELLED" || node.status === "SKIPPED") {
245
+ out.push({ type: "tool-output-error", toolCallId: node.id, errorText: node.error || "The page did not answer.", dynamic: true });
246
+ }
247
+ return out;
248
+ }
249
+ };
250
+ async function pageToolInput(tool, raw) {
251
+ if (!tool.inputSchema) return raw ?? {};
252
+ const checked = await asSchema(tool.inputSchema).validate?.(raw);
253
+ if (!checked) return raw;
254
+ if (!checked.success) throw checked.error;
255
+ return checked.value;
256
+ }
257
+ async function pageToolOffers(tools) {
258
+ return Promise.all(
259
+ Object.entries(tools).map(async ([name, tool]) => {
260
+ const offer = { name };
261
+ if (tool.description) offer.description = tool.description;
262
+ if (tool.inputSchema) offer.inputSchema = await asSchema(tool.inputSchema).jsonSchema;
263
+ return offer;
264
+ })
265
+ );
266
+ }
267
+ function asAnswer(value) {
268
+ if (value === void 0) return null;
269
+ try {
270
+ return JSON.parse(JSON.stringify(value));
271
+ } catch {
272
+ return { error: "The tool answered something that is not JSON." };
273
+ }
274
+ }
275
+ function lastUserText(messages) {
276
+ for (let i = messages.length - 1; i >= 0; i--) {
277
+ const m = messages[i];
278
+ if (m?.role !== "user") continue;
279
+ const text = m.parts.filter((p) => p.type === "text").map((p) => p.text).join("\n");
280
+ return { id: m.id, text };
281
+ }
282
+ return { id: "", text: "" };
283
+ }
284
+ function lastUserFiles(messages) {
285
+ for (let i = messages.length - 1; i >= 0; i--) {
286
+ const m = messages[i];
287
+ if (m?.role !== "user") continue;
288
+ return m.parts.filter((p) => p.type === "file").map((p) => ({ url: p.url, mediaType: p.mediaType, filename: p.filename }));
289
+ }
290
+ return [];
291
+ }
292
+ function lastUserTags(messages) {
293
+ for (let i = messages.length - 1; i >= 0; i--) {
294
+ const m = messages[i];
295
+ if (m?.role !== "user") continue;
296
+ const out = [];
297
+ for (const part of m.parts) {
298
+ const p = part;
299
+ if (p.type !== "data-tag" || !p.data) continue;
300
+ const str = (k) => typeof p.data?.[k] === "string" ? p.data[k] : void 0;
301
+ const kind = str("kind")?.toUpperCase();
302
+ const id = str("id");
303
+ if (!kind || !id) continue;
304
+ out.push({ kind, id, model: str("model"), field: str("field"), label: str("label") });
305
+ }
306
+ return out;
307
+ }
308
+ return [];
309
+ }
310
+ async function blobOf(url, mediaType) {
311
+ const resp = await fetch(url);
312
+ if (!resp.ok) throw new Error(`A file could not be read (${resp.status}).`);
313
+ const blob = await resp.blob();
314
+ return blob.type ? blob : new Blob([blob], { type: mediaType });
315
+ }
316
+ function decisionsIn(messages, sent) {
317
+ const last = messages[messages.length - 1];
318
+ if (last?.role !== "assistant") return [];
319
+ const out = [];
320
+ for (const part of last.parts) {
321
+ const p = part;
322
+ if (!(p.type === "dynamic-tool" || p.type.startsWith("tool-")) || p.state !== "approval-responded" || !p.approval) continue;
323
+ if (sent.has(p.approval.id)) continue;
324
+ out.push({ id: p.approval.id, approve: p.approval.approved === true, reason: p.approval.reason });
325
+ }
326
+ return out;
327
+ }
328
+ function pageAnswersIn(messages, waiting) {
329
+ const last = messages[messages.length - 1];
330
+ if (last?.role !== "assistant") return [];
331
+ const out = [];
332
+ for (const part of last.parts) {
333
+ const p = part;
334
+ if (p.type !== "dynamic-tool" || p.state !== "output-available" || !p.toolCallId || !waiting.has(p.toolCallId)) continue;
335
+ out.push({ id: p.toolCallId, output: p.output ?? null });
336
+ }
337
+ return out;
338
+ }
339
+ function agentsOf(client, caller) {
340
+ const found = client.ns.agents;
341
+ if (!found) {
342
+ const refuse = () => {
343
+ throw new Error(
344
+ `[foir] ${caller}: this client offers no agents. Its key runs none of the project's agents (agents:execute), or the project has released none.`
345
+ );
346
+ };
347
+ return {
348
+ startAgentRun: refuse,
349
+ onRun: refuse,
350
+ decideAgentApproval: refuse,
351
+ answerPageTool: refuse,
352
+ agentThread: refuse,
353
+ createAgentAttachment: refuse
354
+ };
355
+ }
356
+ return found;
357
+ }
358
+ function foirChatTransport(options) {
359
+ const { client, agent } = options;
360
+ const pageTools = options.pageTools ?? {};
361
+ const agents = agentsOf(client, "foirChatTransport");
362
+ let offers = null;
363
+ const offered = () => offers ??= pageToolOffers(pageTools);
364
+ const runsItself = (name) => typeof pageTools[name]?.execute === "function";
365
+ const visitor = options.visitorToken === void 0 ? visitorToken() : options.visitorToken ?? void 0;
366
+ const store = options.threads === void 0 ? browserStorage2() : options.threads;
367
+ const pageThreads = /* @__PURE__ */ new Map();
368
+ const runs = /* @__PURE__ */ new Map();
369
+ const lastRun = /* @__PURE__ */ new Map();
370
+ const decided = /* @__PURE__ */ new Set();
371
+ const uploaded = /* @__PURE__ */ new Map();
372
+ const threadKey = (chatId) => `foir:agents:thread:${agent}:${chatId}`;
373
+ const threadOf = (chatId) => {
374
+ const kept = pageThreads.get(chatId);
375
+ if (kept) return kept;
376
+ try {
377
+ return store?.getItem(threadKey(chatId)) ?? null;
378
+ } catch {
379
+ return null;
380
+ }
381
+ };
382
+ const keepThread = (chatId, threadId) => {
383
+ pageThreads.set(chatId, threadId);
384
+ try {
385
+ store?.setItem(threadKey(chatId), threadId);
386
+ } catch {
387
+ }
388
+ };
389
+ const chunksOf = (runId) => {
390
+ let c = runs.get(runId);
391
+ if (!c) {
392
+ c = new FoirRunChunks(runId, runsItself);
393
+ runs.set(runId, c);
394
+ }
395
+ return c;
396
+ };
397
+ const answerCall = async (node, signal) => {
398
+ const tool = pageTools[node.key];
399
+ let output;
400
+ try {
401
+ const raw = node.input?.args ?? node.input;
402
+ const input = await pageToolInput(tool, raw);
403
+ output = asAnswer(await tool.execute(input, { toolCallId: node.id, messages: [], abortSignal: signal }));
404
+ } catch (err) {
405
+ output = { error: err instanceof Error ? err.message : String(err) };
406
+ }
407
+ await agents.answerPageTool({ id: node.id, output, visitorToken: visitor });
408
+ };
409
+ const follow = (runId, fresh, signal) => {
410
+ const chunks = chunksOf(runId);
411
+ chunks.resume();
412
+ let sub = null;
413
+ let closed = false;
414
+ return new ReadableStream({
415
+ start(controller) {
416
+ const close = () => {
417
+ if (closed) return;
418
+ closed = true;
419
+ sub?.unsubscribe();
420
+ controller.close();
421
+ };
422
+ sub = agents.onRun({
423
+ id: runId,
424
+ after: chunks.after || void 0,
425
+ visitorToken: visitor,
426
+ onData: (event) => {
427
+ if (closed) return;
428
+ for (const c of chunks.of(event, fresh)) controller.enqueue(c);
429
+ for (const call of chunks.takeCalls()) {
430
+ answerCall(call, signal).catch((err) => {
431
+ chunks.answering.delete(call.id);
432
+ if (closed) return;
433
+ controller.enqueue({ type: "error", errorText: err instanceof Error ? err.message : String(err) });
434
+ close();
435
+ });
436
+ }
437
+ if (chunks.stopped) close();
438
+ },
439
+ onError: (err) => {
440
+ if (closed) return;
441
+ controller.enqueue({ type: "error", errorText: err instanceof Error ? err.message : String(err) });
442
+ close();
443
+ },
444
+ onComplete: close
445
+ });
446
+ signal?.addEventListener("abort", close, { once: true });
447
+ },
448
+ // The reader went away: stop following, and write nothing more to it.
449
+ cancel() {
450
+ closed = true;
451
+ sub?.unsubscribe();
452
+ }
453
+ });
454
+ };
455
+ return {
456
+ async sendMessages({ chatId, messages, abortSignal }) {
457
+ const decisions = decisionsIn(messages, decided);
458
+ const waiting = /* @__PURE__ */ new Set();
459
+ for (const c of runs.values()) for (const id2 of c.pageCalls) waiting.add(id2);
460
+ const answers = pageAnswersIn(messages, waiting);
461
+ if (decisions.length > 0 || answers.length > 0) {
462
+ for (const d of decisions) {
463
+ await agents.decideAgentApproval(d);
464
+ decided.add(d.id);
465
+ }
466
+ for (const a of answers) {
467
+ await agents.answerPageTool({ id: a.id, output: a.output, visitorToken: visitor });
468
+ for (const c of runs.values()) c.pageCalls.delete(a.id);
469
+ }
470
+ let runId = lastRun.get(chatId);
471
+ if (!runId) {
472
+ const threadId = threadOf(chatId);
473
+ const thread = threadId ? await agents.agentThread({ id: threadId, visitorToken: visitor }) : null;
474
+ runId = thread?.runningRunId ?? void 0;
475
+ }
476
+ if (!runId) throw new Error("The run these decisions were for could not be found.");
477
+ return follow(runId, false, abortSignal);
478
+ }
479
+ const { id, text } = lastUserText(messages);
480
+ let attachmentIds = id ? uploaded.get(id) : void 0;
481
+ if (!attachmentIds) {
482
+ const files = lastUserFiles(messages);
483
+ attachmentIds = [];
484
+ for (const f of files) {
485
+ const blob = await blobOf(f.url, f.mediaType);
486
+ const sent = await uploadAgentFile(client, {
487
+ agent,
488
+ file: { data: blob, mediaType: f.mediaType, filename: f.filename },
489
+ visitorToken: visitor
490
+ });
491
+ attachmentIds.push(sent.id);
492
+ }
493
+ if (id) uploaded.set(id, attachmentIds);
494
+ }
495
+ const tags = lastUserTags(messages);
496
+ const tools = await offered();
497
+ const run = await agents.startAgentRun({
498
+ agent,
499
+ message: text,
500
+ threadId: threadOf(chatId) ?? void 0,
501
+ idempotencyKey: id || void 0,
502
+ visitorToken: visitor,
503
+ ...attachmentIds.length > 0 ? { attachmentIds } : {},
504
+ ...tags.length > 0 ? { tags } : {},
505
+ ...tools.length > 0 ? { pageTools: tools } : {}
506
+ });
507
+ if (run.threadId) keepThread(chatId, run.threadId);
508
+ lastRun.set(chatId, run.id);
509
+ if (STOPS.has(run.status) && run.status !== "HELD") {
510
+ const answer = run.answer || run.error || "";
511
+ const textId = `${run.id}:answer`;
512
+ return new ReadableStream({
513
+ start(controller) {
514
+ controller.enqueue({ type: "start" });
515
+ if (answer) {
516
+ controller.enqueue({ type: "text-start", id: textId });
517
+ controller.enqueue({ type: "text-delta", id: textId, delta: answer });
518
+ controller.enqueue({ type: "text-end", id: textId });
519
+ }
520
+ controller.enqueue({ type: "finish" });
521
+ controller.close();
522
+ }
523
+ });
524
+ }
525
+ return follow(run.id, true, abortSignal);
526
+ },
527
+ async reconnectToStream({ chatId }) {
528
+ const threadId = threadOf(chatId);
529
+ if (!threadId) return null;
530
+ const thread = await agents.agentThread({ id: threadId, visitorToken: visitor });
531
+ if (!thread?.runningRunId) return null;
532
+ lastRun.set(chatId, thread.runningRunId);
533
+ return follow(thread.runningRunId, true);
534
+ }
535
+ };
536
+ }
537
+ async function foirChatMessages(client, threadId, options = {}) {
538
+ const visitor = options.visitorToken === void 0 ? visitorToken() : options.visitorToken ?? void 0;
539
+ const thread = await agentsOf(client, "foirChatMessages").agentThread({ id: threadId, visitorToken: visitor });
540
+ return (thread?.messages ?? []).map((m) => ({
541
+ id: m.id,
542
+ role: m.role === "USER" ? "user" : "assistant",
543
+ parts: (m.parts ?? []).map(asUIPart)
544
+ }));
545
+ }
546
+ function asUIPart(p) {
547
+ if (p.type !== "file") return p;
548
+ return {
549
+ type: "file",
550
+ mediaType: String(p.mediaType ?? ""),
551
+ url: String(p.url ?? ""),
552
+ ...typeof p.filename === "string" ? { filename: p.filename } : {}
553
+ };
554
+ }
555
+ function browserStorage2() {
556
+ try {
557
+ return typeof localStorage === "undefined" ? null : localStorage;
558
+ } catch {
559
+ return null;
560
+ }
561
+ }
562
+ export {
563
+ FoirRunChunks,
564
+ foirChatMessages,
565
+ foirChatTransport
566
+ };
@@ -43,11 +43,16 @@ export declare function login(env: FoirEnv, vars: {
43
43
  email: string;
44
44
  password: string;
45
45
  }): Promise<AuthResult>;
46
- /** Register a new user with email + password. */
46
+ /**
47
+ * Start an account with email + password. The address is mailed a code, and
48
+ * signing in with it ({@link loginWithOTP}) proves the address and finishes
49
+ * the account. Answered the same whether or not the address has an account
50
+ * already, or the project takes nobody new: its holder is told by email.
51
+ */
47
52
  export declare function register(env: FoirEnv, vars: {
48
53
  email: string;
49
54
  password: string;
50
- }): Promise<AuthResult>;
55
+ }): Promise<boolean>;
51
56
  /** Request a one-time password be emailed to the user. */
52
57
  export declare function requestOTP(env: FoirEnv, email: string): Promise<boolean>;
53
58
  /** Complete an OTP login. */
@@ -194,7 +199,11 @@ export declare function logout(env: FoirEnv, options?: AuthOpOptions): Promise<b
194
199
  * Irreversible. `password` is required for an account that holds one. An
195
200
  * account with none sets `confirm`, on a sign-in made in the last ten minutes:
196
201
  * an older one is refused, and the user signs in again. The server enforces
197
- * the re-auth gate and fails closed.
202
+ * the re-auth gate and fails closed. The answer counts the records and files
203
+ * the user owns, which are destroyed just after it.
204
+ *
205
+ * This does NOT clear local session state: {@link deleteMyAccountAndSignOut}
206
+ * is the pairing that does both.
198
207
  */
199
208
  export declare function deleteMyAccount(env: FoirEnv, vars: {
200
209
  confirm: boolean;
@@ -444,11 +453,15 @@ export declare function signInWithPassword(adapter: SessionAdapter, env: FoirEnv
444
453
  email: string;
445
454
  password: string;
446
455
  }): Promise<FoirUser>;
447
- /** Register with email + password and persist the session. */
448
- export declare function signUpWithPassword(adapter: SessionAdapter, env: FoirEnv, vars: {
456
+ /**
457
+ * Start a sign-up with email + password: the address is mailed a code, and
458
+ * {@link signInWithOTP} with it finishes the account and persists the
459
+ * session. Nobody is signed in until then.
460
+ */
461
+ export declare function signUpWithPassword(env: FoirEnv, vars: {
449
462
  email: string;
450
463
  password: string;
451
- }): Promise<FoirUser>;
464
+ }): Promise<void>;
452
465
  /** Complete an OTP login and persist the session. */
453
466
  export declare function signInWithOTP(adapter: SessionAdapter, env: FoirEnv, vars: {
454
467
  email: string;
@@ -463,6 +476,16 @@ export declare function resetPasswordAndSignIn(adapter: SessionAdapter, env: Foi
463
476
  token: string;
464
477
  newPassword: string;
465
478
  }): Promise<FoirUser>;
479
+ /**
480
+ * Delete the signed-in user's account with the session's token, then clear the
481
+ * session, which the deletion ended server-side. A refusal (a wrong password,
482
+ * a sign-in too old to confirm with) leaves the session as it was, since the
483
+ * account is not deleted.
484
+ */
485
+ export declare function deleteMyAccountAndSignOut(adapter: SessionAdapter, env: FoirEnv, vars: {
486
+ confirm: boolean;
487
+ password?: string;
488
+ }): Promise<DeleteAccountResult>;
466
489
  /**
467
490
  * End the session on both sides: revoke it server-side, then clear local state.
468
491
  *