@softr/sdk 0.0.93-alpha.0 → 0.0.93-alpha.1
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/action.d.ts +46 -1
- package/dist/action.js +38 -1
- package/dist/auth/server.js +1 -1
- package/dist/core/email.d.ts +57 -0
- package/dist/core/email.js +77 -0
- package/dist/datasource/record.js +3 -12
- package/dist/default-error-overlay.d.ts +2 -2
- package/dist/index.d.ts +3 -4
- package/dist/index.js +2 -4
- package/dist/mcp.d.ts +3 -16
- package/dist/mcp.js +18 -61
- package/dist/package.js +1 -1
- package/dist/pwa.d.ts +2 -2
- package/dist/server.d.ts +2 -1
- package/dist/server.js +2 -1
- package/package.json +1 -1
- package/skills/softr-sdk/SKILL.md +10 -8
- package/skills/softr-sdk/ai/SKILL.md +10 -10
- package/skills/softr-sdk/auth/SKILL.md +1 -1
- package/skills/softr-sdk/charts-and-metrics/SKILL.md +1 -1
- package/skills/softr-sdk/current-user/SKILL.md +1 -1
- package/skills/softr-sdk/email/SKILL.md +123 -0
- package/skills/softr-sdk/file-uploads/SKILL.md +1 -1
- package/skills/softr-sdk/formatting/SKILL.md +1 -1
- package/skills/softr-sdk/integrations/SKILL.md +3 -1
- package/skills/softr-sdk/mcp/SKILL.md +1 -16
- package/skills/softr-sdk/pwa/SKILL.md +1 -1
- package/skills/softr-sdk/record-mutations/SKILL.md +1 -1
- package/skills/softr-sdk/record-queries/SKILL.md +1 -1
- package/skills/softr-sdk/region-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/routes-and-visibility/SKILL.md +1 -1
- package/skills/softr-sdk/workflows/SKILL.md +10 -8
- package/dist/agent/agent-fns.d.ts +0 -30
- package/dist/agent/agent-fns.js +0 -215
- package/dist/agent/use-agent-chat.d.ts +0 -106
- package/dist/agent/use-agent-chat.js +0 -486
- package/skills/softr-sdk/agent-chat/SKILL.md +0 -110
package/dist/action.d.ts
CHANGED
|
@@ -234,6 +234,51 @@ type IntegrationDefinition<TId extends string> = {
|
|
|
234
234
|
* const res = await listEvents({ query: { timeMin: now, singleEvents: "true" } })
|
|
235
235
|
*/
|
|
236
236
|
declare function defineIntegration<const TId extends string>(definition: IntegrationDefinition<TId>): IntegrationAction<TId>;
|
|
237
|
+
type EmailInputs = {
|
|
238
|
+
senderId?: string;
|
|
239
|
+
to?: ReadonlyArray<string>;
|
|
240
|
+
cc?: ReadonlyArray<string>;
|
|
241
|
+
bcc?: ReadonlyArray<string>;
|
|
242
|
+
replyTo?: string;
|
|
243
|
+
};
|
|
244
|
+
type EmailResource = {
|
|
245
|
+
type: "WORKFLOW_STEP";
|
|
246
|
+
stepType: "SOFTR_SEND_EMAIL";
|
|
247
|
+
inputs: EmailInputs;
|
|
248
|
+
};
|
|
249
|
+
type EmailAction<TId extends string = string> = Action<EmailResource> & {
|
|
250
|
+
id: TId;
|
|
251
|
+
};
|
|
252
|
+
type EmailDefinition<TId extends string> = EmailInputs & {
|
|
253
|
+
id: TId;
|
|
254
|
+
permissions: ActionPermissions;
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* Declares an email action: who may send it, and the fields every email it sends keeps whatever the
|
|
258
|
+
* call says. Softr sends it from the app's email sender and bills the workspace's workflow credits.
|
|
259
|
+
*
|
|
260
|
+
* - `to`, `cc`, `bcc`: addresses. An element may be a whole `{LOGGED_IN_USER:EMAIL}` or
|
|
261
|
+
* `{LOGGED_IN_USER:<field>}` placeholder, filled in for the signed-in sender.
|
|
262
|
+
* - `replyTo`: an address or one such placeholder.
|
|
263
|
+
* - `senderId`: one of the workspace's email senders, for an email that must come from that address.
|
|
264
|
+
* Left out, the app's sender is used. The call can never choose the sender.
|
|
265
|
+
*
|
|
266
|
+
* A field declared here wins over the call's, and declaring `to` also ignores the call's `cc` and
|
|
267
|
+
* `bcc`. Fix the recipients whenever the use case allows,
|
|
268
|
+
* because anyone in the audience can otherwise send any email to anyone.
|
|
269
|
+
*
|
|
270
|
+
* Pass the returned object to `email.send()` at runtime.
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* const notifyOwner = defineEmailAction({
|
|
274
|
+
* id: "notifyOwner",
|
|
275
|
+
* to: ["owner@acme.com"],
|
|
276
|
+
* permissions: { userGroupTypes: ["NON_LOGGED_IN_USERS", "LOGGED_IN_USERS"] },
|
|
277
|
+
* })
|
|
278
|
+
*
|
|
279
|
+
* await email.send(notifyOwner, { subject: "New submission", body: html, replyTo: data.email })
|
|
280
|
+
*/
|
|
281
|
+
declare function defineEmailAction<const TId extends string>(definition: EmailDefinition<TId>): EmailAction<TId>;
|
|
237
282
|
type AiResource = {
|
|
238
283
|
type: "SOFTR_AI";
|
|
239
284
|
model: string;
|
|
@@ -268,4 +313,4 @@ type AiDefinition<TId extends string> = {
|
|
|
268
313
|
*/
|
|
269
314
|
declare function defineAiAction<const TId extends string>(definition: AiDefinition<TId>): AiAction<TId>;
|
|
270
315
|
//#endregion
|
|
271
|
-
export { ActionData, ActionPermissions, ActionRecord, ActionShape, AiAction, DatasourceAction, FieldTypeOf, IntegrationAction, ReadOf, ReadShape, SoftrDatasources, WorkflowAction, defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow };
|
|
316
|
+
export { ActionData, ActionPermissions, ActionRecord, ActionShape, AiAction, DatasourceAction, EmailAction, FieldTypeOf, IntegrationAction, ReadOf, ReadShape, SoftrDatasources, WorkflowAction, defineAiAction, defineEmailAction, defineIntegration, defineMutation, defineQuery, defineWorkflow };
|
package/dist/action.js
CHANGED
|
@@ -50,6 +50,43 @@ function defineIntegration(definition) {
|
|
|
50
50
|
permissions
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Declares an email action: who may send it, and the fields every email it sends keeps whatever the
|
|
55
|
+
* call says. Softr sends it from the app's email sender and bills the workspace's workflow credits.
|
|
56
|
+
*
|
|
57
|
+
* - `to`, `cc`, `bcc`: addresses. An element may be a whole `{LOGGED_IN_USER:EMAIL}` or
|
|
58
|
+
* `{LOGGED_IN_USER:<field>}` placeholder, filled in for the signed-in sender.
|
|
59
|
+
* - `replyTo`: an address or one such placeholder.
|
|
60
|
+
* - `senderId`: one of the workspace's email senders, for an email that must come from that address.
|
|
61
|
+
* Left out, the app's sender is used. The call can never choose the sender.
|
|
62
|
+
*
|
|
63
|
+
* A field declared here wins over the call's, and declaring `to` also ignores the call's `cc` and
|
|
64
|
+
* `bcc`. Fix the recipients whenever the use case allows,
|
|
65
|
+
* because anyone in the audience can otherwise send any email to anyone.
|
|
66
|
+
*
|
|
67
|
+
* Pass the returned object to `email.send()` at runtime.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* const notifyOwner = defineEmailAction({
|
|
71
|
+
* id: "notifyOwner",
|
|
72
|
+
* to: ["owner@acme.com"],
|
|
73
|
+
* permissions: { userGroupTypes: ["NON_LOGGED_IN_USERS", "LOGGED_IN_USERS"] },
|
|
74
|
+
* })
|
|
75
|
+
*
|
|
76
|
+
* await email.send(notifyOwner, { subject: "New submission", body: html, replyTo: data.email })
|
|
77
|
+
*/
|
|
78
|
+
function defineEmailAction(definition) {
|
|
79
|
+
const { id, permissions,...inputs } = definition;
|
|
80
|
+
return {
|
|
81
|
+
id,
|
|
82
|
+
resource: {
|
|
83
|
+
type: "WORKFLOW_STEP",
|
|
84
|
+
stepType: "SOFTR_SEND_EMAIL",
|
|
85
|
+
inputs
|
|
86
|
+
},
|
|
87
|
+
permissions
|
|
88
|
+
};
|
|
89
|
+
}
|
|
53
90
|
function isDatasourceAction(value) {
|
|
54
91
|
if (typeof value !== "object" || value === null) return false;
|
|
55
92
|
const candidate = value;
|
|
@@ -69,4 +106,4 @@ function defineAiAction(definition) {
|
|
|
69
106
|
}
|
|
70
107
|
|
|
71
108
|
//#endregion
|
|
72
|
-
export { defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow, isDatasourceAction };
|
|
109
|
+
export { defineAiAction, defineEmailAction, defineIntegration, defineMutation, defineQuery, defineWorkflow, isDatasourceAction };
|
package/dist/auth/server.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { settings } from "./methods/settings.js";
|
|
2
|
-
import { currentUserRow } from "./current-user-row.js";
|
|
3
2
|
import { confirmAuthenticatorSetup, removeAuthenticator, resetAuthenticator, sendAuthenticatorResetCode, skipAuthenticatorSetup, startAuthenticatorSetup, verifyAuthenticator } from "./methods/authenticator.js";
|
|
4
3
|
import { sendSignInCode, signInWithCode } from "./methods/code.js";
|
|
5
4
|
import { signInWithMagicLink } from "./methods/magicLink.js";
|
|
@@ -8,6 +7,7 @@ import { checkPasswordLink, requestPasswordReset, setPasswordWithLink } from "./
|
|
|
8
7
|
import { confirmSignUp, signUp } from "./methods/signup.js";
|
|
9
8
|
import { signInWithGoogle } from "./methods/social.js";
|
|
10
9
|
import { removeAvatar, updateProfile } from "./methods/profile.js";
|
|
10
|
+
import { currentUserRow } from "./current-user-row.js";
|
|
11
11
|
import "@tanstack/react-start/server-only";
|
|
12
12
|
|
|
13
13
|
//#region src/auth/server.ts
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { EmailAction } from "../action.js";
|
|
2
|
+
|
|
3
|
+
//#region src/core/email.d.ts
|
|
4
|
+
/** A file the email carries, by the URL the app holds it under. */
|
|
5
|
+
interface EmailAttachment {
|
|
6
|
+
name: string;
|
|
7
|
+
/** The URL of a file uploaded to this app with `upload`. */
|
|
8
|
+
url: string;
|
|
9
|
+
}
|
|
10
|
+
interface EmailSendInit {
|
|
11
|
+
/** Ignored when the action declares its own. */
|
|
12
|
+
to?: ReadonlyArray<string>;
|
|
13
|
+
/** Ignored when the action declares its own `cc` or `to`. */
|
|
14
|
+
cc?: ReadonlyArray<string>;
|
|
15
|
+
/** Ignored when the action declares its own `bcc` or `to`. */
|
|
16
|
+
bcc?: ReadonlyArray<string>;
|
|
17
|
+
/** Ignored when the action declares its own. */
|
|
18
|
+
replyTo?: string;
|
|
19
|
+
subject: string;
|
|
20
|
+
/** Plain text or HTML. */
|
|
21
|
+
body: string;
|
|
22
|
+
/** Files uploaded to this app with `upload`; any other URL is refused. */
|
|
23
|
+
attachments?: ReadonlyArray<EmailAttachment>;
|
|
24
|
+
/**
|
|
25
|
+
* The same key sends the email once, however often the call is repeated. Pass one derived from
|
|
26
|
+
* what the email is about, such as the id of the record it announces, when the call may be retried.
|
|
27
|
+
* At most 128 characters. Left out, every call gets a fresh one.
|
|
28
|
+
*/
|
|
29
|
+
idempotencyKey?: string;
|
|
30
|
+
}
|
|
31
|
+
interface EmailSent {
|
|
32
|
+
/** Null when the mail provider did not report one; the email was still sent. */
|
|
33
|
+
messageId: string | null;
|
|
34
|
+
}
|
|
35
|
+
declare const EMAIL_ERROR_CODES: readonly ["NOT_FOUND", "INVALID_INPUT", "UNAVAILABLE", "TIMEOUT"];
|
|
36
|
+
type EmailErrorCode = (typeof EMAIL_ERROR_CODES)[number];
|
|
37
|
+
/**
|
|
38
|
+
* Thrown when Softr did not send the email. `code` says why:
|
|
39
|
+
*
|
|
40
|
+
* - `NOT_FOUND`: no such email action, or the caller is not in its audience.
|
|
41
|
+
* - `INVALID_INPUT`: a missing subject or body, an entry that is not exactly one address, more than 50 recipients, an attachment that is not one of the app's uploads, or an idempotency key over 128 characters.
|
|
42
|
+
* - `UNAVAILABLE`: Softr could not send it, for example with no workflow credits left, past the Softr fallback
|
|
43
|
+
* address's daily limit or with an unconfirmed sender, or could not be reached.
|
|
44
|
+
* - `TIMEOUT`: no answer in time. It may still have been sent; retry with the same idempotency key.
|
|
45
|
+
*/
|
|
46
|
+
declare class EmailError extends Error {
|
|
47
|
+
readonly status: number;
|
|
48
|
+
readonly code: EmailErrorCode;
|
|
49
|
+
constructor(status: number, code: EmailErrorCode, message: string);
|
|
50
|
+
}
|
|
51
|
+
/** Sends one email through the action. Never retries by itself. */
|
|
52
|
+
declare function sendEmail(action: EmailAction, init: EmailSendInit): Promise<EmailSent>;
|
|
53
|
+
declare const email: {
|
|
54
|
+
send: typeof sendEmail;
|
|
55
|
+
};
|
|
56
|
+
//#endregion
|
|
57
|
+
export { EmailAttachment, EmailError, EmailErrorCode, EmailSendInit, EmailSent, email };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { assert } from "../assertions.js";
|
|
2
|
+
import { softrApiUrl } from "../softr-api.js";
|
|
3
|
+
import { fetch } from "../fetch.js";
|
|
4
|
+
import { sessionConfig } from "../auth/config.js";
|
|
5
|
+
import { readSessionToken } from "../auth/session.js";
|
|
6
|
+
|
|
7
|
+
//#region src/core/email.ts
|
|
8
|
+
/** Longer than Softr waits for the send itself, so its own timeout answers first. */
|
|
9
|
+
const TIMEOUT_MS = 12e4;
|
|
10
|
+
const EMAIL_ERROR_CODES = [
|
|
11
|
+
"NOT_FOUND",
|
|
12
|
+
"INVALID_INPUT",
|
|
13
|
+
"UNAVAILABLE",
|
|
14
|
+
"TIMEOUT"
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* Thrown when Softr did not send the email. `code` says why:
|
|
18
|
+
*
|
|
19
|
+
* - `NOT_FOUND`: no such email action, or the caller is not in its audience.
|
|
20
|
+
* - `INVALID_INPUT`: a missing subject or body, an entry that is not exactly one address, more than 50 recipients, an attachment that is not one of the app's uploads, or an idempotency key over 128 characters.
|
|
21
|
+
* - `UNAVAILABLE`: Softr could not send it, for example with no workflow credits left, past the Softr fallback
|
|
22
|
+
* address's daily limit or with an unconfirmed sender, or could not be reached.
|
|
23
|
+
* - `TIMEOUT`: no answer in time. It may still have been sent; retry with the same idempotency key.
|
|
24
|
+
*/
|
|
25
|
+
var EmailError = class extends Error {
|
|
26
|
+
status;
|
|
27
|
+
code;
|
|
28
|
+
constructor(status, code, message) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "EmailError";
|
|
31
|
+
this.status = status;
|
|
32
|
+
this.code = code;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/** Sends one email through the action. Never retries by itself. */
|
|
36
|
+
async function sendEmail(action, init) {
|
|
37
|
+
const { to, cc, bcc, replyTo, subject, body, attachments } = init;
|
|
38
|
+
const sessionToken = readSessionToken();
|
|
39
|
+
let res;
|
|
40
|
+
try {
|
|
41
|
+
res = await fetch(`${softrApiUrl("SOFTR_PLATFORM_API_URL")}/v1/vibe/applications/${sessionConfig().applicationId}/actions/${action.id}/step`, {
|
|
42
|
+
method: "POST",
|
|
43
|
+
headers: {
|
|
44
|
+
...sessionToken ? { Authorization: `Bearer ${sessionToken}` } : {},
|
|
45
|
+
"Content-Type": "application/json",
|
|
46
|
+
"Idempotency-Key": init.idempotencyKey ?? crypto.randomUUID()
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify({ input: {
|
|
49
|
+
to,
|
|
50
|
+
cc,
|
|
51
|
+
bcc,
|
|
52
|
+
replyTo,
|
|
53
|
+
subject,
|
|
54
|
+
body,
|
|
55
|
+
attachments
|
|
56
|
+
} }),
|
|
57
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
58
|
+
});
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof DOMException && error.name === "TimeoutError") throw new EmailError(504, "TIMEOUT", `Email "${action.id}" got no answer in time. It may still have been sent.`);
|
|
61
|
+
throw new EmailError(502, "UNAVAILABLE", `Email "${action.id}" could not reach Softr: ${error instanceof Error ? error.message : String(error)}`);
|
|
62
|
+
}
|
|
63
|
+
if (res.ok) {
|
|
64
|
+
const sent = await res.json();
|
|
65
|
+
assert(typeof sent === "object" && sent !== null && "messageId" in sent && (typeof sent.messageId === "string" || sent.messageId === null), "the send answered without a messageId");
|
|
66
|
+
return { messageId: sent.messageId };
|
|
67
|
+
}
|
|
68
|
+
const failure = await res.json().catch(() => null);
|
|
69
|
+
const known = typeof failure === "object" && failure !== null ? failure : {};
|
|
70
|
+
const code = ("code" in known ? EMAIL_ERROR_CODES.find((errorCode) => errorCode === known.code) : void 0) ?? (res.status === 404 ? "NOT_FOUND" : res.status === 400 ? "INVALID_INPUT" : "UNAVAILABLE");
|
|
71
|
+
const message = "message" in known && typeof known.message === "string" && known.message !== "" ? known.message : `Email "${action.id}" failed with HTTP ${res.status}`;
|
|
72
|
+
throw new EmailError(res.status, code, message);
|
|
73
|
+
}
|
|
74
|
+
const email = { send: sendEmail };
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
export { EmailError, email };
|
|
@@ -30,15 +30,6 @@ function refererHeader() {
|
|
|
30
30
|
const referer = getRequestHeader("referer");
|
|
31
31
|
return { Referer: referer !== void 0 && URL.parse(referer)?.origin === page.origin ? referer : page.href };
|
|
32
32
|
}
|
|
33
|
-
/** A datasource call the service refused. `status` is its HTTP status. */
|
|
34
|
-
var DatasourceError = class extends Error {
|
|
35
|
-
status;
|
|
36
|
-
constructor(message, status) {
|
|
37
|
-
super(message);
|
|
38
|
-
this.name = "DatasourceError";
|
|
39
|
-
this.status = status;
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
33
|
async function describeFailure(res) {
|
|
43
34
|
const body = await res.json().catch(() => null);
|
|
44
35
|
const message = typeof body === "object" && body !== null && "message" in body ? body.message : null;
|
|
@@ -244,7 +235,7 @@ async function updateRecord(action, options) {
|
|
|
244
235
|
},
|
|
245
236
|
body: JSON.stringify({ fields: rawFields })
|
|
246
237
|
});
|
|
247
|
-
if (!res.ok) throw new
|
|
238
|
+
if (!res.ok) throw new Error(`datasource update failed for action "${actionId}": ${await describeFailure(res)}`);
|
|
248
239
|
const record = await res.json();
|
|
249
240
|
return {
|
|
250
241
|
id: record.id,
|
|
@@ -266,7 +257,7 @@ async function deleteRecord(action, options) {
|
|
|
266
257
|
...refererHeader()
|
|
267
258
|
}
|
|
268
259
|
});
|
|
269
|
-
if (!res.ok) throw new
|
|
260
|
+
if (!res.ok) throw new Error(`datasource delete failed for action "${actionId}": ${await describeFailure(res)}`);
|
|
270
261
|
}
|
|
271
262
|
function resolveTarget(subject, operation) {
|
|
272
263
|
const declaredBy = operation === "read" ? "defineQuery" : "defineMutation";
|
|
@@ -290,4 +281,4 @@ function dealiasData(data, select) {
|
|
|
290
281
|
}
|
|
291
282
|
|
|
292
283
|
//#endregion
|
|
293
|
-
export {
|
|
284
|
+
export { createRecord, deleteRecord, describeFailure, dsUrl, getRecord, listRecords, refererHeader, resolveTarget, updateRecord };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react6 from "react";
|
|
2
2
|
|
|
3
3
|
//#region src/default-error-overlay.d.ts
|
|
4
4
|
type Props = {
|
|
@@ -8,6 +8,6 @@ type Props = {
|
|
|
8
8
|
declare function DefaultErrorOverlay({
|
|
9
9
|
error,
|
|
10
10
|
reset
|
|
11
|
-
}: Props):
|
|
11
|
+
}: Props): react6.JSX.Element;
|
|
12
12
|
//#endregion
|
|
13
13
|
export { DefaultErrorOverlay };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { SoftrUserGroupId, SoftrUserGroups, SoftrUserProperties, User } from "./auth/user.js";
|
|
2
|
-
import { ActionPermissions, SoftrDatasources, defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
2
|
+
import { ActionPermissions, SoftrDatasources, defineAiAction, defineEmailAction, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
3
3
|
import { FileAttachment } from "./core/file-upload.js";
|
|
4
4
|
import { FileItem, useFileUpload } from "./core/use-file-upload.js";
|
|
5
5
|
import { AiAnswer, AiFile, AiGenerateInit, AiMessage, AiUsage } from "./core/ai.js";
|
|
6
|
+
import { EmailAttachment, EmailSendInit, EmailSent } from "./core/email.js";
|
|
6
7
|
import { DateOptions, DateParts, DateSeparator, DateValue, FixedDateFormat, FormattedDate, Formatting, FormattingScope, WeekDay, formatDate, formatDateTime, formatNumber, useFormatting } from "./formatting.js";
|
|
7
8
|
import { SoftrContext } from "./auth/context.js";
|
|
8
9
|
import { AuthSettings } from "./auth/platform.js";
|
|
@@ -17,7 +18,5 @@ import { useOverrides } from "./core/use-overrides.js";
|
|
|
17
18
|
import { useUserTimezone } from "./timezone.js";
|
|
18
19
|
import { PwaInstall, PwaPlatform, SoftrPwaHead, usePwaInstall } from "./pwa.js";
|
|
19
20
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
20
|
-
import { Agent, AgentChatSummary, listAgents } from "./agent/agent-fns.js";
|
|
21
|
-
import { AgentBlock, AgentChat, AgentChatOptions, AgentMessage, AgentQuestion, AgentTodo, AgentToolCall, useAgentChat } from "./agent/use-agent-chat.js";
|
|
22
21
|
import { Audience } from "./route/audience.js";
|
|
23
|
-
export { type ActionPermissions, type
|
|
22
|
+
export { type ActionPermissions, type AiAnswer, type AiFile, type AiGenerateInit, type AiMessage, type AiUsage, Audience, type AuthOptions, type AuthSettings, AuthSettingsProvider, type DateOptions, type DateParts, type DateSeparator, type DateValue, DefaultErrorOverlay, type EmailAttachment, type EmailSendInit, type EmailSent, type FieldShape, type FileAttachment, type FileItem, type FixedDateFormat, FormattedDate, type Formatting, FormattingScope, type Page, type PageSettings, type PwaInstall, type PwaPlatform, type SoftrContext, type SoftrDatasources, type SoftrFieldRegistry, SoftrPwaHead, type SoftrUserGroupId, type SoftrUserGroups, type SoftrUserProperties, type User, type WeekDay, assertRouteAccess, checkAccess, defineAiAction, defineEmailAction, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { defineAiAction, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
1
|
+
import { defineAiAction, defineEmailAction, defineIntegration, defineMutation, defineQuery, defineWorkflow } from "./action.js";
|
|
2
2
|
import { useFileUpload } from "./core/use-file-upload.js";
|
|
3
3
|
import { checkAccess, defineVisibility, isAuthorized } from "./route/visibility.js";
|
|
4
4
|
import { assertRouteAccess, guardRoute, isAccessError, isAlreadyAuthenticatedError, isForbiddenError, isUnauthenticatedError } from "./route/route.js";
|
|
@@ -12,8 +12,6 @@ import { fieldShape, formatDuration, formatField, formatValue, registerSoftrFiel
|
|
|
12
12
|
import { useOverrides } from "./core/use-overrides.js";
|
|
13
13
|
import { useUserTimezone } from "./timezone.js";
|
|
14
14
|
import { DefaultErrorOverlay } from "./default-error-overlay.js";
|
|
15
|
-
import { listAgents } from "./agent/agent-fns.js";
|
|
16
|
-
import { useAgentChat } from "./agent/use-agent-chat.js";
|
|
17
15
|
import { Audience } from "./route/audience.js";
|
|
18
16
|
|
|
19
|
-
export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineAiAction, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError,
|
|
17
|
+
export { Audience, AuthSettingsProvider, DefaultErrorOverlay, FormattedDate, FormattingScope, SoftrPwaHead, assertRouteAccess, checkAccess, defineAiAction, defineEmailAction, defineIntegration, defineMutation, definePage, defineQuery, defineVisibility, defineWorkflow, fieldShape, formatDate, formatDateTime, formatDuration, formatField, formatNumber, formatValue, getAuthSettings, getSoftrContext, guardRoute, initialSoftrContext, isAccessError, isAlreadyAuthenticatedError, isAuthorized, isForbiddenError, isUnauthenticatedError, pageHead, registerSoftrFields, softrAuthMiddleware, softrMcpMiddleware, toReturnPath, useAuthSettings, useCurrentUser, useFileUpload, useFormatting, useOverrides, usePwaInstall, useUserTimezone };
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { DatasourceAction
|
|
1
|
+
import { DatasourceAction } from "./action.js";
|
|
2
2
|
import * as _tanstack_react_start7 from "@tanstack/react-start";
|
|
3
3
|
|
|
4
4
|
//#region src/mcp.d.ts
|
|
5
5
|
|
|
6
|
-
type SoftrMcpOptions
|
|
6
|
+
type SoftrMcpOptions = {
|
|
7
7
|
/**
|
|
8
8
|
* The actions to expose as tools, from `defineQuery` and `defineMutation`.
|
|
9
9
|
*
|
|
@@ -12,19 +12,6 @@ type SoftrMcpOptions<TUsers extends ReadShape = ReadShape> = {
|
|
|
12
12
|
* decide who may run the ones that are listed.
|
|
13
13
|
*/
|
|
14
14
|
actions?: Array<DatasourceAction>;
|
|
15
|
-
/**
|
|
16
|
-
* Fills the owner of a person's record from who is calling, the way the app's
|
|
17
|
-
* pages do with `auth.currentUserRow`. The field is left out of the tool, so
|
|
18
|
-
* the assistant can't leave it wrong or name someone else.
|
|
19
|
-
*/
|
|
20
|
-
owner?: SoftrMcpOwner<TUsers>;
|
|
21
|
-
};
|
|
22
|
-
type SoftrMcpOwner<TUsers extends ReadShape = ReadShape> = {
|
|
23
|
-
/** The people table, and its email field, to find the caller's row in. */
|
|
24
|
-
users: DatasourceAction<TUsers>;
|
|
25
|
-
email: keyof TUsers["fields"] & string;
|
|
26
|
-
/** Listed create or update action id to the field that links to the caller's row. */
|
|
27
|
-
fields: Record<string, string>;
|
|
28
15
|
};
|
|
29
16
|
/**
|
|
30
17
|
* Serves the MCP endpoint and its OAuth flow from the app's own origin.
|
|
@@ -44,6 +31,6 @@ type SoftrMcpOwner<TUsers extends ReadShape = ReadShape> = {
|
|
|
44
31
|
* ],
|
|
45
32
|
* }))
|
|
46
33
|
*/
|
|
47
|
-
declare function softrMcpMiddleware
|
|
34
|
+
declare function softrMcpMiddleware(options?: SoftrMcpOptions): _tanstack_react_start7.RequestMiddlewareAfterServer<{}, undefined, undefined>;
|
|
48
35
|
//#endregion
|
|
49
36
|
export { softrMcpMiddleware };
|
package/dist/mcp.js
CHANGED
|
@@ -5,10 +5,8 @@ import { softrConfig } from "./softr-config.js";
|
|
|
5
5
|
import { sessionConfig } from "./auth/config.js";
|
|
6
6
|
import { readSessionToken, withSessionToken } from "./auth/session.js";
|
|
7
7
|
import { isDatasourceAction } from "./action.js";
|
|
8
|
-
import { DatasourceError } from "./datasource/record.js";
|
|
9
8
|
import { makePlatform } from "./auth/platform.js";
|
|
10
9
|
import { ds } from "./datasource/datasource.js";
|
|
11
|
-
import { currentUserRow } from "./auth/current-user-row.js";
|
|
12
10
|
import { z } from "zod";
|
|
13
11
|
import { createMiddleware } from "@tanstack/react-start";
|
|
14
12
|
|
|
@@ -50,14 +48,6 @@ function softrMcpMiddleware(options = {}) {
|
|
|
50
48
|
assert(!actions.has(value.id), `two actions share the id "${value.id}"`);
|
|
51
49
|
actions.set(value.id, value);
|
|
52
50
|
}
|
|
53
|
-
const owner = options.owner;
|
|
54
|
-
for (const [actionId, field] of Object.entries(owner?.fields ?? {})) {
|
|
55
|
-
const action = actions.get(actionId);
|
|
56
|
-
assert(action, `owner names "${actionId}", which is not a listed action`);
|
|
57
|
-
const { operation, fields } = action.resource;
|
|
58
|
-
assert(operation === "create" || operation === "update", `owner fills "${actionId}", which is a ${operation}`);
|
|
59
|
-
assert(field in (fields ?? {}), `"${actionId}" has no field "${field}" for the owner`);
|
|
60
|
-
}
|
|
61
51
|
return createMiddleware({ type: "request" }).server(async ({ next, request }) => {
|
|
62
52
|
const url = new URL(request.url);
|
|
63
53
|
const origin = url.origin;
|
|
@@ -81,7 +71,7 @@ function softrMcpMiddleware(options = {}) {
|
|
|
81
71
|
});
|
|
82
72
|
case AUTHORIZE_PATH: return await authorize(request, login);
|
|
83
73
|
case TOKEN_PATH: return await issueToken(request);
|
|
84
|
-
case MCP_PATH: return await rpc(request, origin, actions
|
|
74
|
+
case MCP_PATH: return await rpc(request, origin, actions);
|
|
85
75
|
default: return await next();
|
|
86
76
|
}
|
|
87
77
|
});
|
|
@@ -254,8 +244,7 @@ const WHOAMI = {
|
|
|
254
244
|
type: "object",
|
|
255
245
|
properties: {},
|
|
256
246
|
additionalProperties: false
|
|
257
|
-
}
|
|
258
|
-
annotations: { readOnlyHint: true }
|
|
247
|
+
}
|
|
259
248
|
};
|
|
260
249
|
/** How many records a read returns when the caller does not say. */
|
|
261
250
|
const DEFAULT_LIMIT = 20;
|
|
@@ -268,16 +257,14 @@ const MAX_LIMIT = 100;
|
|
|
268
257
|
* that only ever reads one datasource exposes one read, even though its
|
|
269
258
|
* credentials would allow more.
|
|
270
259
|
*/
|
|
271
|
-
function describeTool(action
|
|
260
|
+
function describeTool(action) {
|
|
272
261
|
const { operation, fields } = action.resource;
|
|
273
262
|
const names = Object.keys(fields ?? {});
|
|
274
|
-
const
|
|
275
|
-
const fieldProps = Object.fromEntries(writable.map((name) => [name, { description: `The "${name}" field.` }]));
|
|
263
|
+
const fieldProps = Object.fromEntries(names.map((name) => [name, { description: `The "${name}" field.` }]));
|
|
276
264
|
switch (operation) {
|
|
277
265
|
case "read": return {
|
|
278
266
|
name: action.id,
|
|
279
267
|
description: `Read records from "${action.id}". Returns the fields ${names.join(", ")}. Only records the signed-in user may see come back.`,
|
|
280
|
-
annotations: { readOnlyHint: true },
|
|
281
268
|
inputSchema: {
|
|
282
269
|
type: "object",
|
|
283
270
|
properties: {
|
|
@@ -298,7 +285,7 @@ function describeTool(action, ownerField) {
|
|
|
298
285
|
};
|
|
299
286
|
case "create": return {
|
|
300
287
|
name: action.id,
|
|
301
|
-
description: `Create a record with "${action.id}". Writable fields: ${
|
|
288
|
+
description: `Create a record with "${action.id}". Writable fields: ${names.join(", ")}.`,
|
|
302
289
|
inputSchema: {
|
|
303
290
|
type: "object",
|
|
304
291
|
properties: fieldProps,
|
|
@@ -307,7 +294,7 @@ function describeTool(action, ownerField) {
|
|
|
307
294
|
};
|
|
308
295
|
case "update": return {
|
|
309
296
|
name: action.id,
|
|
310
|
-
description: `Update a record with "${action.id}". Writable fields: ${
|
|
297
|
+
description: `Update a record with "${action.id}". Writable fields: ${names.join(", ")}.`,
|
|
311
298
|
inputSchema: {
|
|
312
299
|
type: "object",
|
|
313
300
|
properties: {
|
|
@@ -343,7 +330,7 @@ function describeTool(action, ownerField) {
|
|
|
343
330
|
* visitor's user groups exactly as it would a click in the app. Nothing here
|
|
344
331
|
* decides what is allowed.
|
|
345
332
|
*/
|
|
346
|
-
async function runTool(action, args
|
|
333
|
+
async function runTool(action, args) {
|
|
347
334
|
const typed = action;
|
|
348
335
|
switch (action.resource.operation) {
|
|
349
336
|
case "read": {
|
|
@@ -355,21 +342,13 @@ async function runTool(action, args, owner) {
|
|
|
355
342
|
nextOffset: result$1.nextOffset
|
|
356
343
|
};
|
|
357
344
|
}
|
|
358
|
-
case "create": {
|
|
359
|
-
const data = writableFields(action, args, owner?.field);
|
|
360
|
-
if (owner !== null) {
|
|
361
|
-
const row = await owner.row();
|
|
362
|
-
if (row === null) throw new Error("The visitor has no row in the people table, so this record can't be created for them.");
|
|
363
|
-
data[owner.field] = { id: row.id };
|
|
364
|
-
}
|
|
365
|
-
return await ds.create(typed, { data });
|
|
366
|
-
}
|
|
345
|
+
case "create": return await ds.create(typed, { data: writableFields(action, args) });
|
|
367
346
|
case "update": {
|
|
368
347
|
const { id,...data } = args;
|
|
369
348
|
assert(typeof id === "string" && id !== "", "update needs a record id");
|
|
370
349
|
return await ds.update(typed, {
|
|
371
350
|
id,
|
|
372
|
-
data: writableFields(action, data
|
|
351
|
+
data: writableFields(action, data)
|
|
373
352
|
});
|
|
374
353
|
}
|
|
375
354
|
case "delete": {
|
|
@@ -380,19 +359,8 @@ async function runTool(action, args, owner) {
|
|
|
380
359
|
}
|
|
381
360
|
}
|
|
382
361
|
}
|
|
383
|
-
function ownerFieldOf(owner, actionId) {
|
|
384
|
-
return owner !== void 0 && Object.hasOwn(owner.fields, actionId) ? owner.fields[actionId] : void 0;
|
|
385
|
-
}
|
|
386
|
-
function ownerFill(owner, actionId, sessionToken) {
|
|
387
|
-
const field = ownerFieldOf(owner, actionId);
|
|
388
|
-
if (owner === void 0 || field === void 0) return null;
|
|
389
|
-
return {
|
|
390
|
-
field,
|
|
391
|
-
row: async () => currentUserRow(await resolveUser(sessionToken), owner.users, owner.email)
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
362
|
/**
|
|
395
|
-
* Only the fields the action declared
|
|
363
|
+
* Only the fields the action declared.
|
|
396
364
|
*
|
|
397
365
|
* `additionalProperties: false` in the tool schema is a hint to the model, not
|
|
398
366
|
* a check — a client can send whatever it likes. An unlisted key reaches the
|
|
@@ -400,18 +368,9 @@ function ownerFill(owner, actionId, sessionToken) {
|
|
|
400
368
|
* straight through. So the action's field list gets enforced here, or it isn't
|
|
401
369
|
* a boundary at all.
|
|
402
370
|
*/
|
|
403
|
-
function writableFields(action, args
|
|
371
|
+
function writableFields(action, args) {
|
|
404
372
|
const allowed = action.resource.fields ?? {};
|
|
405
|
-
return Object.fromEntries(Object.entries(args).filter(([name]) => name in allowed
|
|
406
|
-
}
|
|
407
|
-
/**
|
|
408
|
-
* The error as the assistant should read it. A row rule that keeps the caller
|
|
409
|
-
* from changing a record answers 404, the same as a deleted record, so a bare
|
|
410
|
-
* "not found" would leave the assistant guessing.
|
|
411
|
-
*/
|
|
412
|
-
function describeError(error) {
|
|
413
|
-
if (error instanceof DatasourceError && error.status === 404) return "Refused: this record doesn't exist, or the visitor's user group may not change it. If it still shows in a read, it is a permission. Tell the visitor they don't have access to this action, and that the app's builder can grant it.";
|
|
414
|
-
return error instanceof Error ? error.message : String(error);
|
|
373
|
+
return Object.fromEntries(Object.entries(args).filter(([name]) => name in allowed));
|
|
415
374
|
}
|
|
416
375
|
function clampLimit(raw) {
|
|
417
376
|
if (typeof raw !== "number" || !Number.isInteger(raw)) return DEFAULT_LIMIT;
|
|
@@ -421,15 +380,13 @@ function clampOffset(raw) {
|
|
|
421
380
|
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) return null;
|
|
422
381
|
return raw;
|
|
423
382
|
}
|
|
424
|
-
async function rpc(request, origin, actions
|
|
383
|
+
async function rpc(request, origin, actions) {
|
|
425
384
|
if (request.method !== "POST") return methodNotAllowed("POST");
|
|
426
385
|
const token = bearerToken(request);
|
|
427
386
|
if (token === null) return unauthorized(origin);
|
|
428
387
|
const session = await platformSession(token);
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
else if (session.refused && await resolveUser(token) !== null) sessionToken = token;
|
|
432
|
-
else return session.refused ? unauthorized(origin) : unavailable();
|
|
388
|
+
if (!session.ok) return session.refused ? unauthorized(origin) : unavailable();
|
|
389
|
+
const { sessionToken } = session;
|
|
433
390
|
const body = await readJson(request);
|
|
434
391
|
const message = z.object({
|
|
435
392
|
id: z.union([z.string(), z.number()]).optional(),
|
|
@@ -458,7 +415,7 @@ async function rpc(request, origin, actions, owner) {
|
|
|
458
415
|
}
|
|
459
416
|
});
|
|
460
417
|
}
|
|
461
|
-
case "tools/list": return result(id, { tools: [WHOAMI, ...[...actions.values()].map(
|
|
418
|
+
case "tools/list": return result(id, { tools: [WHOAMI, ...[...actions.values()].map(describeTool)] });
|
|
462
419
|
case "tools/call": {
|
|
463
420
|
const name = params?.name;
|
|
464
421
|
const args = typeof params?.arguments === "object" && params.arguments !== null ? params.arguments : {};
|
|
@@ -470,13 +427,13 @@ async function rpc(request, origin, actions, owner) {
|
|
|
470
427
|
const action = typeof name === "string" ? actions.get(name) : void 0;
|
|
471
428
|
if (action === void 0) return failure(id, -32602, `Unknown tool: ${String(name)}`);
|
|
472
429
|
try {
|
|
473
|
-
return result(id, toolResult(await withSessionToken(sessionToken, () => runTool(action, args
|
|
430
|
+
return result(id, toolResult(await withSessionToken(sessionToken, () => runTool(action, args))));
|
|
474
431
|
} catch (error) {
|
|
475
432
|
return result(id, {
|
|
476
433
|
isError: true,
|
|
477
434
|
content: [{
|
|
478
435
|
type: "text",
|
|
479
|
-
text:
|
|
436
|
+
text: error instanceof Error ? error.message : String(error)
|
|
480
437
|
}]
|
|
481
438
|
});
|
|
482
439
|
}
|
package/dist/package.js
CHANGED
package/dist/pwa.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import * as
|
|
1
|
+
import * as react4 from "react";
|
|
2
2
|
import "@tanstack/react-start";
|
|
3
3
|
|
|
4
4
|
//#region src/pwa.d.ts
|
|
@@ -23,7 +23,7 @@ import "@tanstack/react-start";
|
|
|
23
23
|
* )
|
|
24
24
|
* }
|
|
25
25
|
*/
|
|
26
|
-
declare function SoftrPwaHead():
|
|
26
|
+
declare function SoftrPwaHead(): react4.FunctionComponentElement<react4.FragmentProps>;
|
|
27
27
|
type PwaPlatform = "ios" | "android" | "desktop";
|
|
28
28
|
type PwaInstall = {
|
|
29
29
|
available: false;
|
package/dist/server.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { upload } from "./core/file-upload.js";
|
|
2
2
|
import { AiError, ai } from "./core/ai.js";
|
|
3
|
+
import { EmailError, EmailErrorCode, email } from "./core/email.js";
|
|
3
4
|
import { auth } from "./auth/server.js";
|
|
4
5
|
import { FieldChoice, RecordOption, RecordOptions } from "./core/options.js";
|
|
5
6
|
import { ds } from "./datasource/datasource.js";
|
|
6
7
|
import { workflow } from "./core/workflow.js";
|
|
7
8
|
import { integration } from "./core/integration.js";
|
|
8
9
|
import "@tanstack/react-start/server-only";
|
|
9
|
-
export { AiError, type FieldChoice, type RecordOption, type RecordOptions, ai, auth, ds, integration, upload, workflow };
|
|
10
|
+
export { AiError, EmailError, type EmailErrorCode, type FieldChoice, type RecordOption, type RecordOptions, ai, auth, ds, email, integration, upload, workflow };
|
package/dist/server.js
CHANGED
|
@@ -4,6 +4,7 @@ import { auth } from "./auth/server.js";
|
|
|
4
4
|
import { workflow } from "./core/workflow.js";
|
|
5
5
|
import { integration } from "./core/integration.js";
|
|
6
6
|
import { AiError, ai } from "./core/ai.js";
|
|
7
|
+
import { EmailError, email } from "./core/email.js";
|
|
7
8
|
import "@tanstack/react-start/server-only";
|
|
8
9
|
|
|
9
|
-
export { AiError, ai, auth, ds, integration, upload, workflow };
|
|
10
|
+
export { AiError, EmailError, ai, auth, ds, email, integration, upload, workflow };
|