@agent-native/core 0.125.0 → 0.126.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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +8 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/server/email-template.ts +13 -0
- package/corpus/core/src/server/email.ts +40 -4
- package/corpus/core/src/sharing/actions/share-resource.ts +55 -5
- package/corpus/core/src/sharing/registry.ts +41 -1
- package/corpus/templates/assets/actions/edit-image.ts +31 -18
- package/corpus/templates/assets/actions/refine-image.ts +43 -29
- package/corpus/templates/assets/actions/restyle-image.ts +30 -19
- package/corpus/templates/assets/actions/variant-slots.ts +26 -0
- package/corpus/templates/assets/app/components/generation/GenerationResults.tsx +85 -17
- package/corpus/templates/clips/changelog/2026-07-27-share-notification-emails-now-include-a-clickable-recording-.md +6 -0
- package/corpus/templates/clips/server/db/index.ts +28 -6
- package/corpus/templates/clips/server/lib/share-email-hero.ts +105 -0
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +100 -12
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +17 -0
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +169 -22
- package/dist/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/email-template.d.ts +6 -0
- package/dist/server/email-template.d.ts.map +1 -1
- package/dist/server/email-template.js +6 -0
- package/dist/server/email-template.js.map +1 -1
- package/dist/server/email.d.ts +7 -0
- package/dist/server/email.d.ts.map +1 -1
- package/dist/server/email.js +31 -6
- package/dist/server/email.js.map +1 -1
- package/dist/sharing/actions/share-resource.d.ts.map +1 -1
- package/dist/sharing/actions/share-resource.js +49 -5
- package/dist/sharing/actions/share-resource.js.map +1 -1
- package/dist/sharing/registry.d.ts +40 -1
- package/dist/sharing/registry.d.ts.map +1 -1
- package/dist/sharing/registry.js.map +1 -1
- package/package.json +1 -1
- package/src/server/email-template.ts +13 -0
- package/src/server/email.ts +40 -4
- package/src/sharing/actions/share-resource.ts +55 -5
- package/src/sharing/registry.ts +41 -1
package/dist/server/email.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email.js","sourceRoot":"","sources":["../../src/server/email.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Email transport for system emails (password resets, invitations, notifications).\n *\n * Providers are selected by scoped secrets:\n * RESEND_API_KEY — https://resend.com\n * SENDGRID_API_KEY — https://sendgrid.com\n * EMAIL_FROM — \"Name <addr@domain>\" (optional; defaults to Resend's sandbox)\n *\n * With neither provider configured, `sendEmail` logs the message to the console\n * so the reset-password flow still works end-to-end for local development.\n */\n\nimport { FAVICON_PNG_BASE64 } from \"../assets/branding/favicon-base64.js\";\nimport { resolveSecret } from \"./credential-provider.js\";\nimport { AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID } from \"./email-template.js\";\n\nexport type EmailProvider = \"resend\" | \"sendgrid\" | \"dev\";\n\nexport interface EmailAttachment {\n filename: string;\n content: string | Buffer;\n contentType?: string;\n contentId?: string;\n disposition?: \"attachment\" | \"inline\";\n}\n\nexport interface SendEmailArgs {\n to: string;\n subject: string;\n html: string;\n text?: string;\n from?: string;\n cc?: string | string[];\n replyTo?: string;\n inReplyTo?: string;\n references?: string;\n attachments?: EmailAttachment[];\n timeoutMs?: number;\n}\n\nlet cachedAgentNativeLogo: Buffer | undefined;\n\nfunction getAgentNativeLogoAttachment(): EmailAttachment {\n cachedAgentNativeLogo ??= Buffer.from(FAVICON_PNG_BASE64, \"base64\");\n return {\n filename: \"agent-native-logo.png\",\n content: cachedAgentNativeLogo,\n contentType: \"image/png\",\n contentId: AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID,\n disposition: \"inline\",\n };\n}\n\nfunction resolveAttachments(\n args: SendEmailArgs,\n): EmailAttachment[] | undefined {\n if (!args.html.includes(`cid:${AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID}`)) {\n return args.attachments;\n }\n if (\n args.attachments?.some(\n (attachment) =>\n attachment.contentId === AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID,\n )\n ) {\n return args.attachments;\n }\n return [...(args.attachments ?? []), getAgentNativeLogoAttachment()];\n}\n\ninterface EmailTransportConfig {\n provider: EmailProvider;\n resendApiKey?: string;\n sendgridApiKey?: string;\n from?: string;\n}\n\nasync function resolveEmailTransport(): Promise<EmailTransportConfig> {\n const [resendApiKey, sendgridApiKey, from] = await Promise.all([\n resolveSecret(\"RESEND_API_KEY\"),\n resolveSecret(\"SENDGRID_API_KEY\"),\n resolveSecret(\"EMAIL_FROM\"),\n ]);\n const resolvedFrom = from ?? undefined;\n if (resendApiKey) {\n return {\n provider: \"resend\",\n resendApiKey,\n from: resolvedFrom,\n };\n }\n if (sendgridApiKey) {\n return {\n provider: \"sendgrid\",\n sendgridApiKey,\n from: resolvedFrom,\n };\n }\n return { provider: \"dev\", from: resolvedFrom };\n}\n\nexport async function isEmailConfigured(): Promise<boolean> {\n return (await resolveEmailTransport()).provider !== \"dev\";\n}\n\nexport async function getEmailProvider(): Promise<EmailProvider> {\n return (await resolveEmailTransport()).provider;\n}\n\nfunction getFromAddress(\n config: EmailTransportConfig,\n override?: string,\n): string {\n const explicit = override || config.from;\n if (explicit) return explicit;\n // Resend lets unverified accounts send from its sandbox domain; SendGrid\n // does not, so falling back there would cause silent 403s at runtime.\n if (config.provider === \"sendgrid\") {\n throw new Error(\n \"EMAIL_FROM is required when using SendGrid — save it as a verified sender address.\",\n );\n }\n return \"Agent Native <onboarding@resend.dev>\";\n}\n\nasync function sendEmailWithSignal(\n args: SendEmailArgs,\n signal?: AbortSignal,\n): Promise<void> {\n const config = await resolveEmailTransport();\n signal?.throwIfAborted();\n const provider = config.provider;\n const from = getFromAddress(config, args.from);\n const attachments = resolveAttachments(args);\n\n if (provider === \"resend\") {\n const payload: Record<string, unknown> = {\n from,\n to: args.to,\n subject: args.subject,\n html: args.html,\n text: args.text,\n };\n if (args.cc) payload.cc = Array.isArray(args.cc) ? args.cc : [args.cc];\n if (args.replyTo) payload.reply_to = args.replyTo;\n if (attachments?.length) {\n payload.attachments = attachments.map((a) => ({\n filename: a.filename,\n content:\n typeof a.content === \"string\"\n ? a.content\n : a.content.toString(\"base64\"),\n content_type: a.contentType,\n content_id: a.contentId,\n }));\n }\n const headers: Record<string, string> = {};\n if (args.inReplyTo) headers[\"In-Reply-To\"] = args.inReplyTo;\n if (args.references) headers[\"References\"] = args.references;\n if (Object.keys(headers).length) payload.headers = headers;\n\n const res = await fetch(\"https://api.resend.com/emails\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${config.resendApiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n signal,\n });\n if (!res.ok) {\n const body = await res.text().catch(() => \"\");\n throw new Error(`Resend error ${res.status}: ${body}`);\n }\n return;\n }\n\n if (provider === \"sendgrid\") {\n const personalization: Record<string, unknown> = {\n to: [{ email: args.to }],\n };\n if (args.cc) {\n const ccList = Array.isArray(args.cc) ? args.cc : [args.cc];\n personalization.cc = ccList.map((email) => ({ email }));\n }\n\n const sgPayload: Record<string, unknown> = {\n personalizations: [personalization],\n from: parseSendGridFrom(from),\n subject: args.subject,\n content: [\n ...(args.text ? [{ type: \"text/plain\", value: args.text }] : []),\n { type: \"text/html\", value: args.html },\n ],\n };\n if (args.replyTo) sgPayload.reply_to = parseSendGridFrom(args.replyTo);\n const sgHeaders: Record<string, string> = {};\n if (args.inReplyTo) sgHeaders[\"In-Reply-To\"] = args.inReplyTo;\n if (args.references) sgHeaders[\"References\"] = args.references;\n if (Object.keys(sgHeaders).length) sgPayload.headers = sgHeaders;\n if (attachments?.length) {\n sgPayload.attachments = attachments.map((a) => ({\n filename: a.filename,\n content:\n typeof a.content === \"string\"\n ? Buffer.from(a.content).toString(\"base64\")\n : a.content.toString(\"base64\"),\n type: a.contentType,\n disposition: a.disposition ?? (a.contentId ? \"inline\" : undefined),\n content_id: a.contentId,\n }));\n }\n\n const res = await fetch(\"https://api.sendgrid.com/v3/mail/send\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${config.sendgridApiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(sgPayload),\n signal,\n });\n if (!res.ok) {\n const body = await res.text().catch(() => \"\");\n throw new Error(`SendGrid error ${res.status}: ${body}`);\n }\n return;\n }\n\n // Dev fallback — no provider configured. Logging the full body exposes\n // reset tokens, so only do it outside production. In production, refuse\n // to send rather than silently leaking secrets to logs.\n if (process.env.NODE_ENV === \"production\") {\n throw new Error(\n \"No email provider configured. Save RESEND_API_KEY or SENDGRID_API_KEY in settings.\",\n );\n }\n console.log(\n `\\n[agent-native:email] No email provider configured. ` +\n `Save RESEND_API_KEY or SENDGRID_API_KEY in settings to send real emails.\\n` +\n `---\\nTo: ${args.to}\\nFrom: ${from}\\nSubject: ${args.subject}\\n\\n` +\n `${args.text || stripHtml(args.html)}\\n---\\n`,\n );\n}\n\nexport async function sendEmail(args: SendEmailArgs): Promise<void> {\n const requestedTimeoutMs = Number(args.timeoutMs);\n if (!Number.isFinite(requestedTimeoutMs) || requestedTimeoutMs <= 0) {\n return sendEmailWithSignal(args);\n }\n\n const timeoutMs = Math.floor(requestedTimeoutMs);\n const controller = new AbortController();\n const timeoutError = new Error(`Email send timed out after ${timeoutMs}ms`);\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n await Promise.race([\n sendEmailWithSignal(args, controller.signal),\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => {\n controller.abort(timeoutError);\n reject(timeoutError);\n }, timeoutMs);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nfunction parseSendGridFrom(from: string): { email: string; name?: string } {\n const m = from.match(/^\\s*(.*?)\\s*<(.+)>\\s*$/);\n if (m && m[2]) return { name: m[1] || undefined, email: m[2] };\n return { email: from.trim() };\n}\n\nfunction stripHtml(html: string): string {\n return html\n .replace(/<br\\s*\\/?>/gi, \"\\n\")\n .replace(/<\\/p>/gi, \"\\n\\n\")\n .replace(/<[^>]+>/g, \"\")\n .replace(/ /g, \" \")\n .trim();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"email.js","sourceRoot":"","sources":["../../src/server/email.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Email transport for system emails (password resets, invitations, notifications).\n *\n * Providers are selected by scoped secrets:\n * RESEND_API_KEY — https://resend.com\n * SENDGRID_API_KEY — https://sendgrid.com\n * EMAIL_FROM — \"Name <addr@domain>\" (optional; defaults to Resend's sandbox)\n *\n * With neither provider configured, `sendEmail` logs the message to the console\n * so the reset-password flow still works end-to-end for local development.\n */\n\nimport { FAVICON_PNG_BASE64 } from \"../assets/branding/favicon-base64.js\";\nimport { resolveSecret } from \"./credential-provider.js\";\nimport { AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID } from \"./email-template.js\";\n\nexport type EmailProvider = \"resend\" | \"sendgrid\" | \"dev\";\n\nexport interface EmailAttachment {\n filename: string;\n content: string | Buffer;\n contentType?: string;\n contentId?: string;\n disposition?: \"attachment\" | \"inline\";\n}\n\nexport interface SendEmailArgs {\n to: string;\n subject: string;\n html: string;\n text?: string;\n from?: string;\n /**\n * Display-name-only override. Keeps the configured (domain-verified) sending\n * address and just changes the name shown to the recipient, e.g.\n * \"Alice via Clips\". Ignored when `from` is set. Prefer this over `from` for\n * per-user senders: putting a user's own address in `From` breaks SPF/DKIM.\n */\n fromName?: string;\n cc?: string | string[];\n replyTo?: string;\n inReplyTo?: string;\n references?: string;\n attachments?: EmailAttachment[];\n timeoutMs?: number;\n}\n\nlet cachedAgentNativeLogo: Buffer | undefined;\n\nfunction getAgentNativeLogoAttachment(): EmailAttachment {\n cachedAgentNativeLogo ??= Buffer.from(FAVICON_PNG_BASE64, \"base64\");\n return {\n filename: \"agent-native-logo.png\",\n content: cachedAgentNativeLogo,\n contentType: \"image/png\",\n contentId: AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID,\n disposition: \"inline\",\n };\n}\n\nfunction resolveAttachments(\n args: SendEmailArgs,\n): EmailAttachment[] | undefined {\n if (!args.html.includes(`cid:${AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID}`)) {\n return args.attachments;\n }\n if (\n args.attachments?.some(\n (attachment) =>\n attachment.contentId === AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID,\n )\n ) {\n return args.attachments;\n }\n return [...(args.attachments ?? []), getAgentNativeLogoAttachment()];\n}\n\ninterface EmailTransportConfig {\n provider: EmailProvider;\n resendApiKey?: string;\n sendgridApiKey?: string;\n from?: string;\n}\n\nasync function resolveEmailTransport(): Promise<EmailTransportConfig> {\n const [resendApiKey, sendgridApiKey, from] = await Promise.all([\n resolveSecret(\"RESEND_API_KEY\"),\n resolveSecret(\"SENDGRID_API_KEY\"),\n resolveSecret(\"EMAIL_FROM\"),\n ]);\n const resolvedFrom = from ?? undefined;\n if (resendApiKey) {\n return {\n provider: \"resend\",\n resendApiKey,\n from: resolvedFrom,\n };\n }\n if (sendgridApiKey) {\n return {\n provider: \"sendgrid\",\n sendgridApiKey,\n from: resolvedFrom,\n };\n }\n return { provider: \"dev\", from: resolvedFrom };\n}\n\nexport async function isEmailConfigured(): Promise<boolean> {\n return (await resolveEmailTransport()).provider !== \"dev\";\n}\n\nexport async function getEmailProvider(): Promise<EmailProvider> {\n return (await resolveEmailTransport()).provider;\n}\n\nfunction getFromAddress(\n config: EmailTransportConfig,\n override?: string,\n fromName?: string,\n): string {\n if (override) return override;\n const base = config.from ?? defaultFromAddress(config);\n return fromName ? withDisplayName(base, fromName) : base;\n}\n\nfunction defaultFromAddress(config: EmailTransportConfig): string {\n // Resend lets unverified accounts send from its sandbox domain; SendGrid\n // does not, so falling back there would cause silent 403s at runtime.\n if (config.provider === \"sendgrid\") {\n throw new Error(\n \"EMAIL_FROM is required when using SendGrid — save it as a verified sender address.\",\n );\n }\n return \"Agent Native <onboarding@resend.dev>\";\n}\n\n/**\n * Swap the display name while keeping the verified address. The name is\n * sanitized and quoted because it lands in a header: CR/LF would allow header\n * injection, and quotes/angle brackets would break address parsing.\n */\nfunction withDisplayName(from: string, name: string): string {\n const safe = name\n .replace(/[\\r\\n\"<>\\\\]/g, \" \")\n .replace(/\\s+/g, \" \")\n .trim();\n if (!safe) return from;\n const address = from.match(/<([^>]+)>/)?.[1]?.trim() ?? from.trim();\n return `\"${safe}\" <${address}>`;\n}\n\nasync function sendEmailWithSignal(\n args: SendEmailArgs,\n signal?: AbortSignal,\n): Promise<void> {\n const config = await resolveEmailTransport();\n signal?.throwIfAborted();\n const provider = config.provider;\n const from = getFromAddress(config, args.from, args.fromName);\n const attachments = resolveAttachments(args);\n\n if (provider === \"resend\") {\n const payload: Record<string, unknown> = {\n from,\n to: args.to,\n subject: args.subject,\n html: args.html,\n text: args.text,\n };\n if (args.cc) payload.cc = Array.isArray(args.cc) ? args.cc : [args.cc];\n if (args.replyTo) payload.reply_to = args.replyTo;\n if (attachments?.length) {\n payload.attachments = attachments.map((a) => ({\n filename: a.filename,\n content:\n typeof a.content === \"string\"\n ? a.content\n : a.content.toString(\"base64\"),\n content_type: a.contentType,\n content_id: a.contentId,\n }));\n }\n const headers: Record<string, string> = {};\n if (args.inReplyTo) headers[\"In-Reply-To\"] = args.inReplyTo;\n if (args.references) headers[\"References\"] = args.references;\n if (Object.keys(headers).length) payload.headers = headers;\n\n const res = await fetch(\"https://api.resend.com/emails\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${config.resendApiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(payload),\n signal,\n });\n if (!res.ok) {\n const body = await res.text().catch(() => \"\");\n throw new Error(`Resend error ${res.status}: ${body}`);\n }\n return;\n }\n\n if (provider === \"sendgrid\") {\n const personalization: Record<string, unknown> = {\n to: [{ email: args.to }],\n };\n if (args.cc) {\n const ccList = Array.isArray(args.cc) ? args.cc : [args.cc];\n personalization.cc = ccList.map((email) => ({ email }));\n }\n\n const sgPayload: Record<string, unknown> = {\n personalizations: [personalization],\n from: parseSendGridFrom(from),\n subject: args.subject,\n content: [\n ...(args.text ? [{ type: \"text/plain\", value: args.text }] : []),\n { type: \"text/html\", value: args.html },\n ],\n };\n if (args.replyTo) sgPayload.reply_to = parseSendGridFrom(args.replyTo);\n const sgHeaders: Record<string, string> = {};\n if (args.inReplyTo) sgHeaders[\"In-Reply-To\"] = args.inReplyTo;\n if (args.references) sgHeaders[\"References\"] = args.references;\n if (Object.keys(sgHeaders).length) sgPayload.headers = sgHeaders;\n if (attachments?.length) {\n sgPayload.attachments = attachments.map((a) => ({\n filename: a.filename,\n content:\n typeof a.content === \"string\"\n ? Buffer.from(a.content).toString(\"base64\")\n : a.content.toString(\"base64\"),\n type: a.contentType,\n disposition: a.disposition ?? (a.contentId ? \"inline\" : undefined),\n content_id: a.contentId,\n }));\n }\n\n const res = await fetch(\"https://api.sendgrid.com/v3/mail/send\", {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${config.sendgridApiKey}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(sgPayload),\n signal,\n });\n if (!res.ok) {\n const body = await res.text().catch(() => \"\");\n throw new Error(`SendGrid error ${res.status}: ${body}`);\n }\n return;\n }\n\n // Dev fallback — no provider configured. Logging the full body exposes\n // reset tokens, so only do it outside production. In production, refuse\n // to send rather than silently leaking secrets to logs.\n if (process.env.NODE_ENV === \"production\") {\n throw new Error(\n \"No email provider configured. Save RESEND_API_KEY or SENDGRID_API_KEY in settings.\",\n );\n }\n console.log(\n `\\n[agent-native:email] No email provider configured. ` +\n `Save RESEND_API_KEY or SENDGRID_API_KEY in settings to send real emails.\\n` +\n `---\\nTo: ${args.to}\\nFrom: ${from}\\nSubject: ${args.subject}\\n\\n` +\n `${args.text || stripHtml(args.html)}\\n---\\n`,\n );\n}\n\nexport async function sendEmail(args: SendEmailArgs): Promise<void> {\n const requestedTimeoutMs = Number(args.timeoutMs);\n if (!Number.isFinite(requestedTimeoutMs) || requestedTimeoutMs <= 0) {\n return sendEmailWithSignal(args);\n }\n\n const timeoutMs = Math.floor(requestedTimeoutMs);\n const controller = new AbortController();\n const timeoutError = new Error(`Email send timed out after ${timeoutMs}ms`);\n let timeout: ReturnType<typeof setTimeout> | undefined;\n try {\n await Promise.race([\n sendEmailWithSignal(args, controller.signal),\n new Promise<never>((_, reject) => {\n timeout = setTimeout(() => {\n controller.abort(timeoutError);\n reject(timeoutError);\n }, timeoutMs);\n }),\n ]);\n } finally {\n if (timeout) clearTimeout(timeout);\n }\n}\n\nfunction parseSendGridFrom(from: string): { email: string; name?: string } {\n const m = from.match(/^\\s*(.*?)\\s*<(.+)>\\s*$/);\n if (m && m[2]) return { name: unquoteDisplayName(m[1]), email: m[2] };\n return { email: from.trim() };\n}\n\nfunction unquoteDisplayName(name: string): string | undefined {\n const trimmed = name.trim();\n const unquoted =\n trimmed.startsWith('\"') && trimmed.endsWith('\"')\n ? trimmed.slice(1, -1).replace(/\\\\(.)/g, \"$1\")\n : trimmed;\n return unquoted || undefined;\n}\n\nfunction stripHtml(html: string): string {\n return html\n .replace(/<br\\s*\\/?>/gi, \"\\n\")\n .replace(/<\\/p>/gi, \"\\n\\n\")\n .replace(/<[^>]+>/g, \"\")\n .replace(/ /g, \" \")\n .trim();\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"share-resource.d.ts","sourceRoot":"","sources":["../../../src/sharing/actions/share-resource.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"share-resource.d.ts","sourceRoot":"","sources":["../../../src/sharing/actions/share-resource.ts"],"names":[],"mappings":"AAkBA,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAazD;AAwCD,wBAAgB,2BAA2B,CACzC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,MAAM,SAAwB,GAC7B,MAAM,CAOR"}
|
|
@@ -7,6 +7,7 @@ import { renderEmail, emailStrong } from "../../server/email-template.js";
|
|
|
7
7
|
import { sendEmail, isEmailConfigured } from "../../server/email.js";
|
|
8
8
|
import { invalidateCollabAccessCache } from "../../server/poll.js";
|
|
9
9
|
import { getRequestUserEmail } from "../../server/request-context.js";
|
|
10
|
+
import { getUserProfile } from "../../user-profile/store.js";
|
|
10
11
|
import { assertAccess, ForbiddenError } from "../access.js";
|
|
11
12
|
import { requireShareableResource } from "../registry.js";
|
|
12
13
|
import { getExtensionShareChangeTargets, notifyExtensionShareChanged, } from "./extension-change.js";
|
|
@@ -227,18 +228,53 @@ export default defineAction({
|
|
|
227
228
|
const notificationUrl = resolveShareNotificationUrl(args.resourceUrl, resourcePath, appUrl);
|
|
228
229
|
const appName = process.env.APP_NAME || process.env.VITE_APP_NAME || "Agent Native";
|
|
229
230
|
let brandLogoUrl;
|
|
230
|
-
if (reg.
|
|
231
|
+
if (reg.getLogoUrl) {
|
|
231
232
|
try {
|
|
232
|
-
brandLogoUrl =
|
|
233
|
-
(await reg.getShareEmailLogoUrl(resource)) ?? undefined;
|
|
233
|
+
brandLogoUrl = (await reg.getLogoUrl(resource)) ?? undefined;
|
|
234
234
|
}
|
|
235
235
|
catch (err) {
|
|
236
236
|
console.error("[share-resource] brand logo resolver failed; using default logo:", err);
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
|
+
let brandName = appName;
|
|
240
|
+
if (reg.getBrandName) {
|
|
241
|
+
try {
|
|
242
|
+
brandName = (await reg.getBrandName(resource))?.trim() || appName;
|
|
243
|
+
}
|
|
244
|
+
catch (err) {
|
|
245
|
+
console.error("[share-resource] brand name resolver failed; using app name:", err);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
let fromName;
|
|
249
|
+
let replyTo;
|
|
250
|
+
if (reg.getSender) {
|
|
251
|
+
try {
|
|
252
|
+
const sender = await reg.getSender(resource, {
|
|
253
|
+
sender: await getUserProfile(actor),
|
|
254
|
+
});
|
|
255
|
+
fromName = sender?.fromName?.trim() || undefined;
|
|
256
|
+
replyTo = sender?.replyTo?.trim() || undefined;
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
console.error("[share-resource] sender resolver failed; using default sender:", err);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
let heroHtml;
|
|
263
|
+
if (reg.getHeroHtml) {
|
|
264
|
+
try {
|
|
265
|
+
heroHtml =
|
|
266
|
+
(await reg.getHeroHtml(resource, {
|
|
267
|
+
href: notificationUrl,
|
|
268
|
+
alt: resourceTitle,
|
|
269
|
+
})) ?? undefined;
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
console.error("[share-resource] hero html resolver failed; omitting preview:", err);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
239
275
|
const subject = `${actor} shared "${resourceTitle}" with you on ${appName}`;
|
|
240
276
|
const { html, text } = renderEmail({
|
|
241
|
-
brandName
|
|
277
|
+
brandName,
|
|
242
278
|
brandLogoUrl,
|
|
243
279
|
preheader: subject,
|
|
244
280
|
heading: "You've been given access",
|
|
@@ -246,10 +282,18 @@ export default defineAction({
|
|
|
246
282
|
`${emailStrong(actor)} has shared the ${reg.displayName} ${emailStrong(resourceTitle)} with you as a ${emailStrong(args.role)}.`,
|
|
247
283
|
`Use the button below to open it. If prompted, sign in with ${emailStrong(principalId)}.`,
|
|
248
284
|
],
|
|
285
|
+
heroHtml,
|
|
249
286
|
cta: { label: `Open ${reg.displayName}`, url: notificationUrl },
|
|
250
287
|
footer: `You received this because ${actor} granted you ${args.role} access.`,
|
|
251
288
|
});
|
|
252
|
-
await sendEmail({
|
|
289
|
+
await sendEmail({
|
|
290
|
+
to: principalId,
|
|
291
|
+
subject,
|
|
292
|
+
html,
|
|
293
|
+
text,
|
|
294
|
+
fromName,
|
|
295
|
+
replyTo,
|
|
296
|
+
});
|
|
253
297
|
}
|
|
254
298
|
catch (err) {
|
|
255
299
|
console.error("[share-resource] failed to send share notification:", err);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"share-resource.js","sourceRoot":"","sources":["../../../src/sharing/actions/share-resource.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq, sql, type SQL } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { defineAction } from \"../../action.js\";\nimport { getDbExec } from \"../../db/client.js\";\nimport { getAppProductionUrl } from \"../../server/app-url.js\";\nimport { renderEmail, emailStrong } from \"../../server/email-template.js\";\nimport { sendEmail, isEmailConfigured } from \"../../server/email.js\";\nimport { invalidateCollabAccessCache } from \"../../server/poll.js\";\nimport { getRequestUserEmail } from \"../../server/request-context.js\";\nimport { assertAccess, ForbiddenError } from \"../access.js\";\nimport { requireShareableResource } from \"../registry.js\";\nimport {\n getExtensionShareChangeTargets,\n notifyExtensionShareChanged,\n} from \"./extension-change.js\";\n\nexport function isSyntheticQaEmail(email: string): boolean {\n const trimmed = email.trim().toLowerCase();\n const at = trimmed.lastIndexOf(\"@\");\n if (at <= 0) return false;\n const local = trimmed.slice(0, at);\n const domain = trimmed.slice(at + 1);\n return (\n local.includes(\"+qa\") &&\n (domain === \"example.test\" ||\n domain.endsWith(\".test\") ||\n domain === \"example.invalid\" ||\n domain.endsWith(\".invalid\"))\n );\n}\n\nfunction appPath(path: string): string {\n if (!path.startsWith(\"/\")) return path;\n const raw = process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH || \"\";\n const base = raw.trim().replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n if (!base) return path;\n const normalizedBase = `/${base}`;\n if (path === normalizedBase || path.startsWith(`${normalizedBase}/`)) {\n return path;\n }\n return `${normalizedBase}${path}`;\n}\n\nfunction safeNotificationUrl(value: string, appUrl: string): string | null {\n const trimmed = value.trim();\n if (!trimmed) return null;\n\n try {\n const base = new URL(appUrl);\n if (trimmed.startsWith(\"/\")) {\n const path = appPath(trimmed);\n const basePath = base.pathname.replace(/\\/+$/, \"\");\n const alreadyIncludesBase =\n basePath && basePath !== \"/\" && path.startsWith(`${basePath}/`);\n const joined = alreadyIncludesBase\n ? `${base.origin}${path}`\n : `${appUrl.replace(/\\/+$/, \"\")}${path}`;\n return new URL(joined).toString();\n }\n\n const url = new URL(trimmed);\n if (![\"http:\", \"https:\"].includes(url.protocol)) return null;\n if (url.origin !== base.origin) return null;\n return url.toString();\n } catch {\n return null;\n }\n}\n\nexport function resolveShareNotificationUrl(\n explicitUrl: string | undefined,\n fallbackPath: string | undefined,\n appUrl = getAppProductionUrl(),\n): string {\n for (const candidate of [explicitUrl, fallbackPath]) {\n if (!candidate) continue;\n const url = safeNotificationUrl(candidate, appUrl);\n if (url) return url;\n }\n return appUrl;\n}\n\nfunction nanoid(size = 12): string {\n const chars =\n \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let id = \"\";\n const bytes = crypto.getRandomValues(new Uint8Array(size));\n for (const byte of bytes) id += chars[byte % chars.length];\n return id;\n}\n\nfunction normalizePrincipalId(\n principalType: \"user\" | \"org\",\n principalId: string,\n): string {\n return principalType === \"user\"\n ? principalId.trim().toLowerCase()\n : principalId;\n}\n\nfunction isEmailPrincipalId(value: string): boolean {\n return /^[^\\s@]+@[^\\s@]+$/.test(value.trim());\n}\n\nfunction principalIdMatches(\n sharesTable: any,\n principalType: \"user\" | \"org\",\n principalId: string,\n): SQL {\n return principalType === \"user\"\n ? sql`lower(${sharesTable.principalId}) = ${principalId}`\n : eq(sharesTable.principalId, principalId);\n}\n\n/**\n * Returns true if the given email is either an active member of `orgId` or\n * has a pending invitation to `orgId`. Used by resources whose registration\n * sets `requireOrgMemberForUserShares` (currently extensions) to refuse\n * cross-org user shares.\n *\n * Both `org_members` and `org_invitations` store email case-insensitively\n * via `LOWER()` in the rest of the framework, so we follow the same\n * convention here.\n */\nasync function isOrgMemberOrInvited(\n orgId: string,\n email: string,\n): Promise<boolean> {\n const lower = email.trim().toLowerCase();\n if (!lower || !orgId) return false;\n const client = getDbExec();\n const member = await client.execute({\n sql: `SELECT 1 FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,\n args: [orgId, lower],\n });\n if (member.rows.length > 0) return true;\n const invited = await client.execute({\n sql: `SELECT 1 FROM org_invitations WHERE org_id = ? AND LOWER(email) = ? AND status = 'pending' LIMIT 1`,\n args: [orgId, lower],\n });\n return invited.rows.length > 0;\n}\n\nexport default defineAction({\n description:\n \"Grant a user or org access to a shareable resource. Owner or admin role required.\",\n // (audit H5) Sharing-grant operations are admin-tier and let a caller\n // expand who can read/write a resource. Refuse from the tools iframe\n // bridge so a malicious shared tool can't silently re-share its\n // viewer's resources to an attacker-controlled email.\n toolCallable: false,\n schema: z.object({\n resourceType: z\n .string()\n .describe(\"Registered resource type, e.g. 'document', 'form'.\"),\n resourceId: z.string().describe(\"Id of the resource to share.\"),\n principalType: z\n .enum([\"user\", \"org\"])\n .describe(\"'user' for an individual, 'org' for a whole organization.\"),\n principalId: z\n .string()\n .describe(\"Email (user) or org id (org) of the principal.\"),\n role: z\n .enum([\"viewer\", \"editor\", \"admin\"])\n .default(\"viewer\")\n .describe(\"Role to grant.\"),\n notify: z\n .boolean()\n .default(true)\n .describe(\n \"Whether to email the user about a new individual share. Defaults to true.\",\n ),\n resourceUrl: z\n .string()\n .optional()\n .describe(\n \"Optional app-relative or same-origin URL recipients should open. External origins are ignored.\",\n ),\n }),\n run: async (args) => {\n const reg = requireShareableResource(args.resourceType);\n const access = await assertAccess(\n args.resourceType,\n args.resourceId,\n \"admin\",\n );\n const actor = getRequestUserEmail();\n if (!actor) throw new ForbiddenError(\"Not signed in\");\n const principalId = normalizePrincipalId(\n args.principalType,\n args.principalId,\n );\n if (args.principalType === \"user\" && !isEmailPrincipalId(principalId)) {\n throw new Error(\n \"User shares must use an email address, not an internal user id.\",\n );\n }\n const beforeExtensionTargets = await getExtensionShareChangeTargets(\n args.resourceType,\n args.resourceId,\n );\n\n if (reg.requireOrgMemberForUserShares) {\n const resourceOrgId = access.resource?.orgId as string | undefined | null;\n if (!resourceOrgId) {\n throw new ForbiddenError(\n `${reg.displayName} can only be shared from within an organization. Create or join an organization first.`,\n );\n }\n if (args.principalType === \"user\") {\n const ok = await isOrgMemberOrInvited(resourceOrgId, principalId);\n if (!ok) {\n throw new ForbiddenError(\n `${principalId} is not in your organization. Invite them to the organization first, then share.`,\n );\n }\n } else if (args.principalType === \"org\") {\n // Cross-org org shares would let an outside org's members run\n // extension code in the viewer's auth context — the same threat\n // model that blocks public + cross-org user shares. Pin org-\n // principal shares to the resource's own org.\n if (principalId !== resourceOrgId) {\n throw new ForbiddenError(\n `${reg.displayName} can only be shared with its own organization, not a different one.`,\n );\n }\n }\n }\n\n const db = reg.getDb() as any;\n const [existing] = await db\n .select()\n .from(reg.sharesTable)\n .where(\n and(\n eq(reg.sharesTable.resourceId, args.resourceId),\n eq(reg.sharesTable.principalType, args.principalType),\n principalIdMatches(reg.sharesTable, args.principalType, principalId),\n ),\n );\n\n if (existing) {\n await db\n .update(reg.sharesTable)\n .set({ role: args.role })\n .where(eq(reg.sharesTable.id, existing.id));\n invalidateCollabAccessCache(args.resourceType, args.resourceId);\n await notifyExtensionShareChanged(\n args.resourceType,\n args.resourceId,\n beforeExtensionTargets,\n );\n return { id: existing.id, updated: true };\n }\n\n const id = nanoid();\n await db.insert(reg.sharesTable).values({\n id,\n resourceId: args.resourceId,\n principalType: args.principalType,\n principalId,\n role: args.role,\n createdBy: actor,\n createdAt: new Date().toISOString(),\n });\n invalidateCollabAccessCache(args.resourceType, args.resourceId);\n await notifyExtensionShareChanged(\n args.resourceType,\n args.resourceId,\n beforeExtensionTargets,\n );\n\n if (\n args.notify !== false &&\n args.principalType === \"user\" &&\n (await isEmailConfigured()) &&\n !isSyntheticQaEmail(principalId)\n ) {\n try {\n const titleCol = reg.titleColumn ?? \"title\";\n const [resource] = await db\n .select()\n .from(reg.resourceTable)\n .where(eq(reg.resourceTable.id, args.resourceId));\n const resourceTitle: string =\n (resource?.[titleCol] as string | undefined) ?? args.resourceType;\n const appUrl = getAppProductionUrl();\n const resourcePath =\n resource && reg.getResourcePath\n ? reg.getResourcePath(resource)\n : undefined;\n const notificationUrl = resolveShareNotificationUrl(\n args.resourceUrl,\n resourcePath,\n appUrl,\n );\n const appName =\n process.env.APP_NAME || process.env.VITE_APP_NAME || \"Agent Native\";\n let brandLogoUrl: string | undefined;\n if (reg.getShareEmailLogoUrl) {\n try {\n brandLogoUrl =\n (await reg.getShareEmailLogoUrl(resource)) ?? undefined;\n } catch (err) {\n console.error(\n \"[share-resource] brand logo resolver failed; using default logo:\",\n err,\n );\n }\n }\n const subject = `${actor} shared \"${resourceTitle}\" with you on ${appName}`;\n const { html, text } = renderEmail({\n brandName: appName,\n brandLogoUrl,\n preheader: subject,\n heading: \"You've been given access\",\n paragraphs: [\n `${emailStrong(actor)} has shared the ${reg.displayName} ${emailStrong(resourceTitle)} with you as a ${emailStrong(args.role)}.`,\n `Use the button below to open it. If prompted, sign in with ${emailStrong(principalId)}.`,\n ],\n cta: { label: `Open ${reg.displayName}`, url: notificationUrl },\n footer: `You received this because ${actor} granted you ${args.role} access.`,\n });\n await sendEmail({ to: principalId, subject, html, text });\n } catch (err) {\n console.error(\n \"[share-resource] failed to send share notification:\",\n err,\n );\n }\n }\n\n return { id, updated: false };\n },\n});\n"]}
|
|
1
|
+
{"version":3,"file":"share-resource.js","sourceRoot":"","sources":["../../../src/sharing/actions/share-resource.ts"],"names":[],"mappings":"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","sourcesContent":["import { and, eq, sql, type SQL } from \"drizzle-orm\";\nimport { z } from \"zod\";\n\nimport { defineAction } from \"../../action.js\";\nimport { getDbExec } from \"../../db/client.js\";\nimport { getAppProductionUrl } from \"../../server/app-url.js\";\nimport { renderEmail, emailStrong } from \"../../server/email-template.js\";\nimport { sendEmail, isEmailConfigured } from \"../../server/email.js\";\nimport { invalidateCollabAccessCache } from \"../../server/poll.js\";\nimport { getRequestUserEmail } from \"../../server/request-context.js\";\nimport { getUserProfile } from \"../../user-profile/store.js\";\nimport { assertAccess, ForbiddenError } from \"../access.js\";\nimport { requireShareableResource } from \"../registry.js\";\nimport {\n getExtensionShareChangeTargets,\n notifyExtensionShareChanged,\n} from \"./extension-change.js\";\n\nexport function isSyntheticQaEmail(email: string): boolean {\n const trimmed = email.trim().toLowerCase();\n const at = trimmed.lastIndexOf(\"@\");\n if (at <= 0) return false;\n const local = trimmed.slice(0, at);\n const domain = trimmed.slice(at + 1);\n return (\n local.includes(\"+qa\") &&\n (domain === \"example.test\" ||\n domain.endsWith(\".test\") ||\n domain === \"example.invalid\" ||\n domain.endsWith(\".invalid\"))\n );\n}\n\nfunction appPath(path: string): string {\n if (!path.startsWith(\"/\")) return path;\n const raw = process.env.VITE_APP_BASE_PATH || process.env.APP_BASE_PATH || \"\";\n const base = raw.trim().replace(/^\\/+/, \"\").replace(/\\/+$/, \"\");\n if (!base) return path;\n const normalizedBase = `/${base}`;\n if (path === normalizedBase || path.startsWith(`${normalizedBase}/`)) {\n return path;\n }\n return `${normalizedBase}${path}`;\n}\n\nfunction safeNotificationUrl(value: string, appUrl: string): string | null {\n const trimmed = value.trim();\n if (!trimmed) return null;\n\n try {\n const base = new URL(appUrl);\n if (trimmed.startsWith(\"/\")) {\n const path = appPath(trimmed);\n const basePath = base.pathname.replace(/\\/+$/, \"\");\n const alreadyIncludesBase =\n basePath && basePath !== \"/\" && path.startsWith(`${basePath}/`);\n const joined = alreadyIncludesBase\n ? `${base.origin}${path}`\n : `${appUrl.replace(/\\/+$/, \"\")}${path}`;\n return new URL(joined).toString();\n }\n\n const url = new URL(trimmed);\n if (![\"http:\", \"https:\"].includes(url.protocol)) return null;\n if (url.origin !== base.origin) return null;\n return url.toString();\n } catch {\n return null;\n }\n}\n\nexport function resolveShareNotificationUrl(\n explicitUrl: string | undefined,\n fallbackPath: string | undefined,\n appUrl = getAppProductionUrl(),\n): string {\n for (const candidate of [explicitUrl, fallbackPath]) {\n if (!candidate) continue;\n const url = safeNotificationUrl(candidate, appUrl);\n if (url) return url;\n }\n return appUrl;\n}\n\nfunction nanoid(size = 12): string {\n const chars =\n \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\";\n let id = \"\";\n const bytes = crypto.getRandomValues(new Uint8Array(size));\n for (const byte of bytes) id += chars[byte % chars.length];\n return id;\n}\n\nfunction normalizePrincipalId(\n principalType: \"user\" | \"org\",\n principalId: string,\n): string {\n return principalType === \"user\"\n ? principalId.trim().toLowerCase()\n : principalId;\n}\n\nfunction isEmailPrincipalId(value: string): boolean {\n return /^[^\\s@]+@[^\\s@]+$/.test(value.trim());\n}\n\nfunction principalIdMatches(\n sharesTable: any,\n principalType: \"user\" | \"org\",\n principalId: string,\n): SQL {\n return principalType === \"user\"\n ? sql`lower(${sharesTable.principalId}) = ${principalId}`\n : eq(sharesTable.principalId, principalId);\n}\n\n/**\n * Returns true if the given email is either an active member of `orgId` or\n * has a pending invitation to `orgId`. Used by resources whose registration\n * sets `requireOrgMemberForUserShares` (currently extensions) to refuse\n * cross-org user shares.\n *\n * Both `org_members` and `org_invitations` store email case-insensitively\n * via `LOWER()` in the rest of the framework, so we follow the same\n * convention here.\n */\nasync function isOrgMemberOrInvited(\n orgId: string,\n email: string,\n): Promise<boolean> {\n const lower = email.trim().toLowerCase();\n if (!lower || !orgId) return false;\n const client = getDbExec();\n const member = await client.execute({\n sql: `SELECT 1 FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,\n args: [orgId, lower],\n });\n if (member.rows.length > 0) return true;\n const invited = await client.execute({\n sql: `SELECT 1 FROM org_invitations WHERE org_id = ? AND LOWER(email) = ? AND status = 'pending' LIMIT 1`,\n args: [orgId, lower],\n });\n return invited.rows.length > 0;\n}\n\nexport default defineAction({\n description:\n \"Grant a user or org access to a shareable resource. Owner or admin role required.\",\n // (audit H5) Sharing-grant operations are admin-tier and let a caller\n // expand who can read/write a resource. Refuse from the tools iframe\n // bridge so a malicious shared tool can't silently re-share its\n // viewer's resources to an attacker-controlled email.\n toolCallable: false,\n schema: z.object({\n resourceType: z\n .string()\n .describe(\"Registered resource type, e.g. 'document', 'form'.\"),\n resourceId: z.string().describe(\"Id of the resource to share.\"),\n principalType: z\n .enum([\"user\", \"org\"])\n .describe(\"'user' for an individual, 'org' for a whole organization.\"),\n principalId: z\n .string()\n .describe(\"Email (user) or org id (org) of the principal.\"),\n role: z\n .enum([\"viewer\", \"editor\", \"admin\"])\n .default(\"viewer\")\n .describe(\"Role to grant.\"),\n notify: z\n .boolean()\n .default(true)\n .describe(\n \"Whether to email the user about a new individual share. Defaults to true.\",\n ),\n resourceUrl: z\n .string()\n .optional()\n .describe(\n \"Optional app-relative or same-origin URL recipients should open. External origins are ignored.\",\n ),\n }),\n run: async (args) => {\n const reg = requireShareableResource(args.resourceType);\n const access = await assertAccess(\n args.resourceType,\n args.resourceId,\n \"admin\",\n );\n const actor = getRequestUserEmail();\n if (!actor) throw new ForbiddenError(\"Not signed in\");\n const principalId = normalizePrincipalId(\n args.principalType,\n args.principalId,\n );\n if (args.principalType === \"user\" && !isEmailPrincipalId(principalId)) {\n throw new Error(\n \"User shares must use an email address, not an internal user id.\",\n );\n }\n const beforeExtensionTargets = await getExtensionShareChangeTargets(\n args.resourceType,\n args.resourceId,\n );\n\n if (reg.requireOrgMemberForUserShares) {\n const resourceOrgId = access.resource?.orgId as string | undefined | null;\n if (!resourceOrgId) {\n throw new ForbiddenError(\n `${reg.displayName} can only be shared from within an organization. Create or join an organization first.`,\n );\n }\n if (args.principalType === \"user\") {\n const ok = await isOrgMemberOrInvited(resourceOrgId, principalId);\n if (!ok) {\n throw new ForbiddenError(\n `${principalId} is not in your organization. Invite them to the organization first, then share.`,\n );\n }\n } else if (args.principalType === \"org\") {\n // Cross-org org shares would let an outside org's members run\n // extension code in the viewer's auth context — the same threat\n // model that blocks public + cross-org user shares. Pin org-\n // principal shares to the resource's own org.\n if (principalId !== resourceOrgId) {\n throw new ForbiddenError(\n `${reg.displayName} can only be shared with its own organization, not a different one.`,\n );\n }\n }\n }\n\n const db = reg.getDb() as any;\n const [existing] = await db\n .select()\n .from(reg.sharesTable)\n .where(\n and(\n eq(reg.sharesTable.resourceId, args.resourceId),\n eq(reg.sharesTable.principalType, args.principalType),\n principalIdMatches(reg.sharesTable, args.principalType, principalId),\n ),\n );\n\n if (existing) {\n await db\n .update(reg.sharesTable)\n .set({ role: args.role })\n .where(eq(reg.sharesTable.id, existing.id));\n invalidateCollabAccessCache(args.resourceType, args.resourceId);\n await notifyExtensionShareChanged(\n args.resourceType,\n args.resourceId,\n beforeExtensionTargets,\n );\n return { id: existing.id, updated: true };\n }\n\n const id = nanoid();\n await db.insert(reg.sharesTable).values({\n id,\n resourceId: args.resourceId,\n principalType: args.principalType,\n principalId,\n role: args.role,\n createdBy: actor,\n createdAt: new Date().toISOString(),\n });\n invalidateCollabAccessCache(args.resourceType, args.resourceId);\n await notifyExtensionShareChanged(\n args.resourceType,\n args.resourceId,\n beforeExtensionTargets,\n );\n\n if (\n args.notify !== false &&\n args.principalType === \"user\" &&\n (await isEmailConfigured()) &&\n !isSyntheticQaEmail(principalId)\n ) {\n try {\n const titleCol = reg.titleColumn ?? \"title\";\n const [resource] = await db\n .select()\n .from(reg.resourceTable)\n .where(eq(reg.resourceTable.id, args.resourceId));\n const resourceTitle: string =\n (resource?.[titleCol] as string | undefined) ?? args.resourceType;\n const appUrl = getAppProductionUrl();\n const resourcePath =\n resource && reg.getResourcePath\n ? reg.getResourcePath(resource)\n : undefined;\n const notificationUrl = resolveShareNotificationUrl(\n args.resourceUrl,\n resourcePath,\n appUrl,\n );\n const appName =\n process.env.APP_NAME || process.env.VITE_APP_NAME || \"Agent Native\";\n let brandLogoUrl: string | undefined;\n if (reg.getLogoUrl) {\n try {\n brandLogoUrl = (await reg.getLogoUrl(resource)) ?? undefined;\n } catch (err) {\n console.error(\n \"[share-resource] brand logo resolver failed; using default logo:\",\n err,\n );\n }\n }\n let brandName = appName;\n if (reg.getBrandName) {\n try {\n brandName = (await reg.getBrandName(resource))?.trim() || appName;\n } catch (err) {\n console.error(\n \"[share-resource] brand name resolver failed; using app name:\",\n err,\n );\n }\n }\n let fromName: string | undefined;\n let replyTo: string | undefined;\n if (reg.getSender) {\n try {\n const sender = await reg.getSender(resource, {\n sender: await getUserProfile(actor),\n });\n fromName = sender?.fromName?.trim() || undefined;\n replyTo = sender?.replyTo?.trim() || undefined;\n } catch (err) {\n console.error(\n \"[share-resource] sender resolver failed; using default sender:\",\n err,\n );\n }\n }\n let heroHtml: string | undefined;\n if (reg.getHeroHtml) {\n try {\n heroHtml =\n (await reg.getHeroHtml(resource, {\n href: notificationUrl,\n alt: resourceTitle,\n })) ?? undefined;\n } catch (err) {\n console.error(\n \"[share-resource] hero html resolver failed; omitting preview:\",\n err,\n );\n }\n }\n const subject = `${actor} shared \"${resourceTitle}\" with you on ${appName}`;\n const { html, text } = renderEmail({\n brandName,\n brandLogoUrl,\n preheader: subject,\n heading: \"You've been given access\",\n paragraphs: [\n `${emailStrong(actor)} has shared the ${reg.displayName} ${emailStrong(resourceTitle)} with you as a ${emailStrong(args.role)}.`,\n `Use the button below to open it. If prompted, sign in with ${emailStrong(principalId)}.`,\n ],\n heroHtml,\n cta: { label: `Open ${reg.displayName}`, url: notificationUrl },\n footer: `You received this because ${actor} granted you ${args.role} access.`,\n });\n await sendEmail({\n to: principalId,\n subject,\n html,\n text,\n fromName,\n replyTo,\n });\n } catch (err) {\n console.error(\n \"[share-resource] failed to send share notification:\",\n err,\n );\n }\n }\n\n return { id, updated: false };\n },\n});\n"]}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* titleColumn: "title",
|
|
17
17
|
* });
|
|
18
18
|
*/
|
|
19
|
+
import type { UserProfile } from "../user-profile/shared.js";
|
|
19
20
|
export interface ShareableResourceRegistration {
|
|
20
21
|
/** Stable identifier used across actions, UI, and analytics. e.g. "document". */
|
|
21
22
|
type: string;
|
|
@@ -41,7 +42,45 @@ export interface ShareableResourceRegistration {
|
|
|
41
42
|
* (e.g. the sharing org's own logo), or undefined to fall back. Receives the
|
|
42
43
|
* resource row being shared so the template can scope the logo to its org.
|
|
43
44
|
*/
|
|
44
|
-
|
|
45
|
+
getLogoUrl?: (resource: any) => string | undefined | Promise<string | undefined>;
|
|
46
|
+
/**
|
|
47
|
+
* Optional resolver for the brand name shown beside the logo in the
|
|
48
|
+
* share-notification email header. Return a display name (e.g. the sharing
|
|
49
|
+
* org's name) to override the app name, or undefined to keep the default.
|
|
50
|
+
* Receives the resource row being shared.
|
|
51
|
+
*/
|
|
52
|
+
getBrandName?: (resource: any) => string | undefined | Promise<string | undefined>;
|
|
53
|
+
/**
|
|
54
|
+
* Optional resolver for who the share-notification email appears to come
|
|
55
|
+
* from. `fromName` changes only the display name and keeps the configured,
|
|
56
|
+
* domain-verified sending address (e.g. "Alice via Clips"); `replyTo` routes
|
|
57
|
+
* replies to the sharer. `ctx.sender` is the account doing the sharing, so the
|
|
58
|
+
* template decides how to present them (name, email, avatar).
|
|
59
|
+
*
|
|
60
|
+
* Do not put a user's address in the sending address itself — SPF/DKIM sign
|
|
61
|
+
* the app's domain, so recipients would bounce or spam-filter it.
|
|
62
|
+
*/
|
|
63
|
+
getSender?: (resource: any, ctx: {
|
|
64
|
+
sender: UserProfile;
|
|
65
|
+
}) => {
|
|
66
|
+
fromName?: string;
|
|
67
|
+
replyTo?: string;
|
|
68
|
+
} | undefined | Promise<{
|
|
69
|
+
fromName?: string;
|
|
70
|
+
replyTo?: string;
|
|
71
|
+
} | undefined>;
|
|
72
|
+
/**
|
|
73
|
+
* Optional resolver for a preview block shown above the CTA in the
|
|
74
|
+
* share-notification email — e.g. a recording thumbnail with a play badge.
|
|
75
|
+
* Return trusted HTML (built by template code, dynamic values escaped) that
|
|
76
|
+
* is injected verbatim, or undefined to omit it. `ctx.href` is the resolved
|
|
77
|
+
* link to the resource; `ctx.alt` is its title. The template owns the entire
|
|
78
|
+
* preview markup so the generic share action stays app-agnostic.
|
|
79
|
+
*/
|
|
80
|
+
getHeroHtml?: (resource: any, ctx: {
|
|
81
|
+
href: string;
|
|
82
|
+
alt?: string;
|
|
83
|
+
}) => string | undefined | Promise<string | undefined>;
|
|
45
84
|
/**
|
|
46
85
|
* Drizzle DB accessor from the template's server/db/index.ts. Required —
|
|
47
86
|
* the framework-level share actions and access helpers call this to reach
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/sharing/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,6BAA6B;IAC5C,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,aAAa,EAAE,GAAG,CAAC;IACnB,qDAAqD;IACrD,WAAW,EAAE,GAAG,CAAC;IACjB,+DAA+D;IAC/D,WAAW,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,MAAM,GAAG,SAAS,CAAC;IACxD;;;;;OAKG;IACH,
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/sharing/registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/sharing/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AA+HH,0EAA0E;AAC1E,6EAA6E;AAC7E,8EAA8E;AAC9E,4EAA4E;AAC5E,2EAA2E;AAC3E,0EAA0E;AAC1E,8EAA8E;AAC9E,+EAA+E;AAC/E,MAAM,YAAY,GAAG,mCAAmC,CAAC;AAEzD,MAAM,cAAc,GAClB,UAAiB,CAAC;AAEpB,SAAS,aAAa;IACpB,OAAO,CACL,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM;QAC/B,OAAO,CAAC,GAAG,CAAC,MAAM,KAAK,MAAM;QAC7B,OAAO,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG,CAC3B,CAAC;AACJ,CAAC;AAED,SAAS,4BAA4B;IACnC,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;IACtC,OAAO,oEAAoE,CAAC,IAAI,CAC9E,KAAK,CACN,CAAC;AACJ,CAAC;AAED,SAAS,WAAW;IAClB,IAAI,CAAC,GAAG,cAAc,CAAC,YAAY,CAAC,CAAC;IACrC,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,CAAC,GAAG,IAAI,GAAG,EAAyC,CAAC;QACrD,cAAc,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,KAAoC;IAEpC,IAAI,CAAC,aAAa,EAAE,IAAI,4BAA4B,EAAE;QAAE,OAAO;IAC/D,WAAW,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,IAAY;IAEZ,OAAO,WAAW,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,IAAY;IAEZ,MAAM,GAAG,GAAG,WAAW,EAAE,CAAC;IAC1B,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,qCAAqC,IAAI,gDAAgD,CAC1F,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,OAAO,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;AAC5C,CAAC","sourcesContent":["/**\n * Registry of shareable resources.\n *\n * Each template registers its ownable resource(s) once on module load so the\n * framework-level share actions (`share-resource`, `list-resource-shares`,\n * etc.) can dispatch to the correct tables.\n *\n * import { registerShareableResource } from \"@agent-native/core/sharing\";\n * import * as schema from \"./schema.js\";\n *\n * registerShareableResource({\n * type: \"document\",\n * resourceTable: schema.documents,\n * sharesTable: schema.documentShares,\n * displayName: \"Document\",\n * titleColumn: \"title\",\n * });\n */\n\nexport interface ShareableResourceRegistration {\n /** Stable identifier used across actions, UI, and analytics. e.g. \"document\". */\n type: string;\n /** Drizzle table for the parent resource (must have ownableColumns()). */\n resourceTable: any;\n /** Drizzle table produced by createSharesTable(). */\n sharesTable: any;\n /** Human-readable singular label shown in the share dialog. */\n displayName: string;\n /**\n * Column on the resource table that holds a human-readable title for\n * display in the share UI. Default: \"title\".\n */\n titleColumn?: string;\n /**\n * Optional app-relative path to this resource. Used by share notifications\n * when the caller does not pass a more specific resourceUrl.\n */\n getResourcePath?: (resource: any) => string | undefined;\n /**\n * Optional resolver for the share-notification email's brand logo. Return an\n * absolute `https://` URL to override the default embedded Agent Native logo\n * (e.g. the sharing org's own logo), or undefined to fall back. Receives the\n * resource row being shared so the template can scope the logo to its org.\n */\n getShareEmailLogoUrl?: (\n resource: any,\n ) => string | undefined | Promise<string | undefined>;\n /**\n * Drizzle DB accessor from the template's server/db/index.ts. Required —\n * the framework-level share actions and access helpers call this to reach\n * the right DB instance (schema is template-specific).\n */\n getDb: () => any;\n /**\n * When `false`, `visibility: \"public\"` is rejected by `set-resource-visibility`,\n * and `accessFilter` / `resolveAccess` treat any stored public row as private\n * (defense in depth — only owner + explicit shares grant access).\n *\n * Use this for resources that execute code or expose privileged data and must\n * never be reachable by a random authenticated user. Extensions set this:\n * extension HTML runs inside an iframe that calls actions / DB / proxied APIs\n * as the *viewer*, so a public extension would be arbitrary code with the\n * viewer's credentials.\n *\n * Default: `true` (matches the historical behavior — most resources can be public).\n */\n allowPublic?: boolean;\n /**\n * Optional role granted by a public-by-link read. Most resources should omit\n * this so public visibility remains viewer-only. Use narrowly for local or\n * otherwise constrained resources where the resource owner intentionally wants\n * unauthenticated link holders to do more than view.\n *\n * When this is a function, it may read arbitrary fields on the resource row\n * (not just the ownership/visibility columns). Because of that,\n * `resolveAccess`/`assertAccess`'s opt-in `{ skipResourceBody: true }`\n * projected load (see `access.ts`) is automatically ignored for this\n * registration and always loads the full row instead — a fixed role string\n * has no such requirement and is compatible with the projection.\n */\n publicAccessRole?:\n | \"viewer\"\n | \"editor\"\n | \"admin\"\n | ((\n resource: any,\n ctx: { userEmail?: string; orgId?: string },\n ) =>\n | \"viewer\"\n | \"editor\"\n | \"admin\"\n | Promise<\"viewer\" | \"editor\" | \"admin\">);\n /**\n * When `true`, individual user shares (`principalType: \"user\"`) must target\n * an email that is already a member of the same org as the resource, OR has\n * a pending invitation to that org. Cross-org user shares are rejected.\n *\n * Pair with `allowPublic: false` for resources that need a hard \"this org\n * only\" trust boundary. Extensions set this so a malicious caller can't\n * widen reach by sharing a code-executing extension to an outsider email.\n *\n * Default: `false` (matches the historical behavior — any email can be granted).\n */\n requireOrgMemberForUserShares?: boolean;\n /**\n * Optional per-resource access-context adapter. Most resources should use the\n * request user/org unchanged. Templates with an intentional alternate local\n * identity can normalize here so the generic framework sharing actions and\n * access helpers stay in sync with template-owned actions.\n */\n resolveAccessContext?: (ctx: { userEmail?: string; orgId?: string }) => {\n userEmail?: string;\n orgId?: string;\n };\n /**\n * When true, direct ownership is recognized by owner_email regardless of the\n * caller's active org. Use this only for resource types where the template's\n * own actions already treat owner_email as the cross-org authority and list\n * views add their own org filters.\n *\n * Default: `false`.\n */\n ownerAccessIgnoresOrg?: boolean;\n /**\n * Optional external-agent read handoff. When set, the framework-level\n * `create-agent-resource-link` action can mint a short-lived, read-only\n * `agent_access` URL for this resource. The context endpoint is owned by the\n * template so it can expose the same intentionally shareable shape as the\n * public page, not a generic raw database row.\n */\n agentReadable?:\n | false\n | {\n /** Token scope. Include the app name to avoid cross-app collisions. */\n resourceKind: string;\n /** App-relative JSON endpoint that accepts `id` + `agent_access`. */\n getContextPath: (resource: any) => string | undefined;\n /** Optional override for the page URL. Defaults to getResourcePath. */\n getPagePath?: (resource: any) => string | undefined;\n /** Optional override for the default two-hour token lifetime. */\n ttlSeconds?: number;\n };\n}\n\n// Stash the registry on globalThis so it survives SSR bundle duplication.\n// Vite SSR's `noExternal: /^(?!node:)/` policy means @agent-native/core gets\n// inlined into every server bundle that imports it — and each bundle gets its\n// own module-level state. A plain `new Map()` here would create one Map per\n// bundle, so the template's `registerShareableResource()` (called from the\n// Nitro plugin graph) wouldn't be visible to the framework's auto-mounted\n// share-resource action (loaded via `import(\"../sharing/actions/...js\")` in a\n// different module instance). Using globalThis collapses them back to one Map.\nconst REGISTRY_KEY = \"__agentNativeShareableResources__\";\ntype RegistryStore = Map<string, ShareableResourceRegistration>;\nconst globalRegistry: { [K in typeof REGISTRY_KEY]?: RegistryStore } =\n globalThis as any;\n\nfunction isTestRuntime(): boolean {\n return (\n process.env.NODE_ENV === \"test\" ||\n process.env.VITEST === \"true\" ||\n process.env.VITEST === \"1\"\n );\n}\n\nfunction registrationCameFromTestFile(): boolean {\n const stack = new Error().stack ?? \"\";\n return /[./\\\\](?:[^/\\\\]+[.-])(?:test|spec)\\.[cm]?[jt]sx?(?::\\d+)?(?::\\d+)?/.test(\n stack,\n );\n}\n\nfunction getRegistry(): RegistryStore {\n let r = globalRegistry[REGISTRY_KEY];\n if (!r) {\n r = new Map<string, ShareableResourceRegistration>();\n globalRegistry[REGISTRY_KEY] = r;\n }\n return r;\n}\n\nexport function registerShareableResource(\n entry: ShareableResourceRegistration,\n): void {\n if (!isTestRuntime() && registrationCameFromTestFile()) return;\n getRegistry().set(entry.type, entry);\n}\n\nexport function getShareableResource(\n type: string,\n): ShareableResourceRegistration | undefined {\n return getRegistry().get(type);\n}\n\nexport function requireShareableResource(\n type: string,\n): ShareableResourceRegistration {\n const reg = getRegistry();\n const entry = reg.get(type);\n if (!entry) {\n throw new Error(\n `Unknown shareable resource type: \"${type}\". Did you forget registerShareableResource()?`,\n );\n }\n return entry;\n}\n\nexport function listShareableResources(): ShareableResourceRegistration[] {\n return Array.from(getRegistry().values());\n}\n"]}
|
|
1
|
+
{"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/sharing/registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAuKH,0EAA0E;AAC1E,6EAA6E;AAC7E,8EAA8E;AAC9E,4EAA4E;AAC5E,2EAA2E;AAC3E,0EAA0E;AAC1E,8EAA8E;AAC9E,+EAA+E;AAC/E,MAAM,YAAY,GAAG,mCAAmC,CAAC;AAEzD,MAAM,cAAc,GAClB,UAAiB,CAAC;AAEpB,SAAS,aAAa;IACpB,OAAO,CACL,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,MAAM;QAC/B,OAAO,CAAC,GAAG,CAAC,MAAM,KAAK,MAAM;QAC7B,OAAO,CAAC,GAAG,CAAC,MAAM,KAAK,GAAG,CAC3B,CAAC;AACJ,CAAC;AAED,SAAS,4BAA4B;IACnC,MAAM,KAAK,GAAG,IAAI,KAAK,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;IACtC,OAAO,oEAAoE,CAAC,IAAI,CAC9E,KAAK,CACN,CAAC;AACJ,CAAC;AAED,SAAS,WAAW;IAClB,IAAI,CAAC,GAAG,cAAc,CAAC,YAAY,CAAC,CAAC;IACrC,IAAI,CAAC,CAAC,EAAE,CAAC;QACP,CAAC,GAAG,IAAI,GAAG,EAAyC,CAAC;QACrD,cAAc,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,MAAM,UAAU,yBAAyB,CACvC,KAAoC;IAEpC,IAAI,CAAC,aAAa,EAAE,IAAI,4BAA4B,EAAE;QAAE,OAAO;IAC/D,WAAW,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,oBAAoB,CAClC,IAAY;IAEZ,OAAO,WAAW,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,wBAAwB,CACtC,IAAY;IAEZ,MAAM,GAAG,GAAG,WAAW,EAAE,CAAC;IAC1B,MAAM,KAAK,GAAG,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC5B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CACb,qCAAqC,IAAI,gDAAgD,CAC1F,CAAC;IACJ,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,OAAO,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,MAAM,EAAE,CAAC,CAAC;AAC5C,CAAC","sourcesContent":["/**\n * Registry of shareable resources.\n *\n * Each template registers its ownable resource(s) once on module load so the\n * framework-level share actions (`share-resource`, `list-resource-shares`,\n * etc.) can dispatch to the correct tables.\n *\n * import { registerShareableResource } from \"@agent-native/core/sharing\";\n * import * as schema from \"./schema.js\";\n *\n * registerShareableResource({\n * type: \"document\",\n * resourceTable: schema.documents,\n * sharesTable: schema.documentShares,\n * displayName: \"Document\",\n * titleColumn: \"title\",\n * });\n */\n\nimport type { UserProfile } from \"../user-profile/shared.js\";\n\nexport interface ShareableResourceRegistration {\n /** Stable identifier used across actions, UI, and analytics. e.g. \"document\". */\n type: string;\n /** Drizzle table for the parent resource (must have ownableColumns()). */\n resourceTable: any;\n /** Drizzle table produced by createSharesTable(). */\n sharesTable: any;\n /** Human-readable singular label shown in the share dialog. */\n displayName: string;\n /**\n * Column on the resource table that holds a human-readable title for\n * display in the share UI. Default: \"title\".\n */\n titleColumn?: string;\n /**\n * Optional app-relative path to this resource. Used by share notifications\n * when the caller does not pass a more specific resourceUrl.\n */\n getResourcePath?: (resource: any) => string | undefined;\n /**\n * Optional resolver for the share-notification email's brand logo. Return an\n * absolute `https://` URL to override the default embedded Agent Native logo\n * (e.g. the sharing org's own logo), or undefined to fall back. Receives the\n * resource row being shared so the template can scope the logo to its org.\n */\n getLogoUrl?: (\n resource: any,\n ) => string | undefined | Promise<string | undefined>;\n /**\n * Optional resolver for the brand name shown beside the logo in the\n * share-notification email header. Return a display name (e.g. the sharing\n * org's name) to override the app name, or undefined to keep the default.\n * Receives the resource row being shared.\n */\n getBrandName?: (\n resource: any,\n ) => string | undefined | Promise<string | undefined>;\n /**\n * Optional resolver for who the share-notification email appears to come\n * from. `fromName` changes only the display name and keeps the configured,\n * domain-verified sending address (e.g. \"Alice via Clips\"); `replyTo` routes\n * replies to the sharer. `ctx.sender` is the account doing the sharing, so the\n * template decides how to present them (name, email, avatar).\n *\n * Do not put a user's address in the sending address itself — SPF/DKIM sign\n * the app's domain, so recipients would bounce or spam-filter it.\n */\n getSender?: (\n resource: any,\n ctx: { sender: UserProfile },\n ) =>\n | { fromName?: string; replyTo?: string }\n | undefined\n | Promise<{ fromName?: string; replyTo?: string } | undefined>;\n /**\n * Optional resolver for a preview block shown above the CTA in the\n * share-notification email — e.g. a recording thumbnail with a play badge.\n * Return trusted HTML (built by template code, dynamic values escaped) that\n * is injected verbatim, or undefined to omit it. `ctx.href` is the resolved\n * link to the resource; `ctx.alt` is its title. The template owns the entire\n * preview markup so the generic share action stays app-agnostic.\n */\n getHeroHtml?: (\n resource: any,\n ctx: { href: string; alt?: string },\n ) => string | undefined | Promise<string | undefined>;\n /**\n * Drizzle DB accessor from the template's server/db/index.ts. Required —\n * the framework-level share actions and access helpers call this to reach\n * the right DB instance (schema is template-specific).\n */\n getDb: () => any;\n /**\n * When `false`, `visibility: \"public\"` is rejected by `set-resource-visibility`,\n * and `accessFilter` / `resolveAccess` treat any stored public row as private\n * (defense in depth — only owner + explicit shares grant access).\n *\n * Use this for resources that execute code or expose privileged data and must\n * never be reachable by a random authenticated user. Extensions set this:\n * extension HTML runs inside an iframe that calls actions / DB / proxied APIs\n * as the *viewer*, so a public extension would be arbitrary code with the\n * viewer's credentials.\n *\n * Default: `true` (matches the historical behavior — most resources can be public).\n */\n allowPublic?: boolean;\n /**\n * Optional role granted by a public-by-link read. Most resources should omit\n * this so public visibility remains viewer-only. Use narrowly for local or\n * otherwise constrained resources where the resource owner intentionally wants\n * unauthenticated link holders to do more than view.\n *\n * When this is a function, it may read arbitrary fields on the resource row\n * (not just the ownership/visibility columns). Because of that,\n * `resolveAccess`/`assertAccess`'s opt-in `{ skipResourceBody: true }`\n * projected load (see `access.ts`) is automatically ignored for this\n * registration and always loads the full row instead — a fixed role string\n * has no such requirement and is compatible with the projection.\n */\n publicAccessRole?:\n | \"viewer\"\n | \"editor\"\n | \"admin\"\n | ((\n resource: any,\n ctx: { userEmail?: string; orgId?: string },\n ) =>\n | \"viewer\"\n | \"editor\"\n | \"admin\"\n | Promise<\"viewer\" | \"editor\" | \"admin\">);\n /**\n * When `true`, individual user shares (`principalType: \"user\"`) must target\n * an email that is already a member of the same org as the resource, OR has\n * a pending invitation to that org. Cross-org user shares are rejected.\n *\n * Pair with `allowPublic: false` for resources that need a hard \"this org\n * only\" trust boundary. Extensions set this so a malicious caller can't\n * widen reach by sharing a code-executing extension to an outsider email.\n *\n * Default: `false` (matches the historical behavior — any email can be granted).\n */\n requireOrgMemberForUserShares?: boolean;\n /**\n * Optional per-resource access-context adapter. Most resources should use the\n * request user/org unchanged. Templates with an intentional alternate local\n * identity can normalize here so the generic framework sharing actions and\n * access helpers stay in sync with template-owned actions.\n */\n resolveAccessContext?: (ctx: { userEmail?: string; orgId?: string }) => {\n userEmail?: string;\n orgId?: string;\n };\n /**\n * When true, direct ownership is recognized by owner_email regardless of the\n * caller's active org. Use this only for resource types where the template's\n * own actions already treat owner_email as the cross-org authority and list\n * views add their own org filters.\n *\n * Default: `false`.\n */\n ownerAccessIgnoresOrg?: boolean;\n /**\n * Optional external-agent read handoff. When set, the framework-level\n * `create-agent-resource-link` action can mint a short-lived, read-only\n * `agent_access` URL for this resource. The context endpoint is owned by the\n * template so it can expose the same intentionally shareable shape as the\n * public page, not a generic raw database row.\n */\n agentReadable?:\n | false\n | {\n /** Token scope. Include the app name to avoid cross-app collisions. */\n resourceKind: string;\n /** App-relative JSON endpoint that accepts `id` + `agent_access`. */\n getContextPath: (resource: any) => string | undefined;\n /** Optional override for the page URL. Defaults to getResourcePath. */\n getPagePath?: (resource: any) => string | undefined;\n /** Optional override for the default two-hour token lifetime. */\n ttlSeconds?: number;\n };\n}\n\n// Stash the registry on globalThis so it survives SSR bundle duplication.\n// Vite SSR's `noExternal: /^(?!node:)/` policy means @agent-native/core gets\n// inlined into every server bundle that imports it — and each bundle gets its\n// own module-level state. A plain `new Map()` here would create one Map per\n// bundle, so the template's `registerShareableResource()` (called from the\n// Nitro plugin graph) wouldn't be visible to the framework's auto-mounted\n// share-resource action (loaded via `import(\"../sharing/actions/...js\")` in a\n// different module instance). Using globalThis collapses them back to one Map.\nconst REGISTRY_KEY = \"__agentNativeShareableResources__\";\ntype RegistryStore = Map<string, ShareableResourceRegistration>;\nconst globalRegistry: { [K in typeof REGISTRY_KEY]?: RegistryStore } =\n globalThis as any;\n\nfunction isTestRuntime(): boolean {\n return (\n process.env.NODE_ENV === \"test\" ||\n process.env.VITEST === \"true\" ||\n process.env.VITEST === \"1\"\n );\n}\n\nfunction registrationCameFromTestFile(): boolean {\n const stack = new Error().stack ?? \"\";\n return /[./\\\\](?:[^/\\\\]+[.-])(?:test|spec)\\.[cm]?[jt]sx?(?::\\d+)?(?::\\d+)?/.test(\n stack,\n );\n}\n\nfunction getRegistry(): RegistryStore {\n let r = globalRegistry[REGISTRY_KEY];\n if (!r) {\n r = new Map<string, ShareableResourceRegistration>();\n globalRegistry[REGISTRY_KEY] = r;\n }\n return r;\n}\n\nexport function registerShareableResource(\n entry: ShareableResourceRegistration,\n): void {\n if (!isTestRuntime() && registrationCameFromTestFile()) return;\n getRegistry().set(entry.type, entry);\n}\n\nexport function getShareableResource(\n type: string,\n): ShareableResourceRegistration | undefined {\n return getRegistry().get(type);\n}\n\nexport function requireShareableResource(\n type: string,\n): ShareableResourceRegistration {\n const reg = getRegistry();\n const entry = reg.get(type);\n if (!entry) {\n throw new Error(\n `Unknown shareable resource type: \"${type}\". Did you forget registerShareableResource()?`,\n );\n }\n return entry;\n}\n\nexport function listShareableResources(): ShareableResourceRegistration[] {\n return Array.from(getRegistry().values());\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.126.0",
|
|
4
4
|
"description": "Framework for agent-native application development — where AI agents and UI share SQL state, actions, and context",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -36,6 +36,12 @@ export interface RenderEmailArgs {
|
|
|
36
36
|
paragraphs: string[];
|
|
37
37
|
/** Primary call-to-action rendered as a real button. */
|
|
38
38
|
cta?: EmailCta;
|
|
39
|
+
/**
|
|
40
|
+
* Optional trusted HTML injected above the CTA — e.g. a template-owned video
|
|
41
|
+
* thumbnail with a play badge. Injected verbatim, so only pass markup built
|
|
42
|
+
* by app/template code (never raw user input), and escape any dynamic values.
|
|
43
|
+
*/
|
|
44
|
+
heroHtml?: string;
|
|
39
45
|
/** Small muted text under the CTA (e.g. expiry note). */
|
|
40
46
|
footer?: string;
|
|
41
47
|
/** Optional app name shown beside the framework logo. */
|
|
@@ -108,6 +114,12 @@ export function renderEmail(args: RenderEmailArgs): RenderedEmail {
|
|
|
108
114
|
const ctaFg = brand ? "#ffffff" : "#0a0a0c";
|
|
109
115
|
const linkColor = brand ?? "#a1a1aa";
|
|
110
116
|
|
|
117
|
+
// Trusted markup supplied by the caller (template code, not user input),
|
|
118
|
+
// injected as-is so a template can own app-specific previews (e.g. a video
|
|
119
|
+
// thumbnail with a play badge). Callers are responsible for escaping any
|
|
120
|
+
// dynamic values they interpolate.
|
|
121
|
+
const heroHtml = args.heroHtml ?? "";
|
|
122
|
+
|
|
111
123
|
const paragraphsHtml = args.paragraphs
|
|
112
124
|
.map(
|
|
113
125
|
(p) =>
|
|
@@ -174,6 +186,7 @@ export function renderEmail(args: RenderEmailArgs): RenderedEmail {
|
|
|
174
186
|
${escapeHtml(args.heading)}
|
|
175
187
|
</h1>
|
|
176
188
|
${paragraphsHtml}
|
|
189
|
+
${heroHtml}
|
|
177
190
|
${ctaHtml}
|
|
178
191
|
${footerHtml}
|
|
179
192
|
</td>
|
package/src/server/email.ts
CHANGED
|
@@ -30,6 +30,13 @@ export interface SendEmailArgs {
|
|
|
30
30
|
html: string;
|
|
31
31
|
text?: string;
|
|
32
32
|
from?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Display-name-only override. Keeps the configured (domain-verified) sending
|
|
35
|
+
* address and just changes the name shown to the recipient, e.g.
|
|
36
|
+
* "Alice via Clips". Ignored when `from` is set. Prefer this over `from` for
|
|
37
|
+
* per-user senders: putting a user's own address in `From` breaks SPF/DKIM.
|
|
38
|
+
*/
|
|
39
|
+
fromName?: string;
|
|
33
40
|
cc?: string | string[];
|
|
34
41
|
replyTo?: string;
|
|
35
42
|
inReplyTo?: string;
|
|
@@ -110,9 +117,14 @@ export async function getEmailProvider(): Promise<EmailProvider> {
|
|
|
110
117
|
function getFromAddress(
|
|
111
118
|
config: EmailTransportConfig,
|
|
112
119
|
override?: string,
|
|
120
|
+
fromName?: string,
|
|
113
121
|
): string {
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
if (override) return override;
|
|
123
|
+
const base = config.from ?? defaultFromAddress(config);
|
|
124
|
+
return fromName ? withDisplayName(base, fromName) : base;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function defaultFromAddress(config: EmailTransportConfig): string {
|
|
116
128
|
// Resend lets unverified accounts send from its sandbox domain; SendGrid
|
|
117
129
|
// does not, so falling back there would cause silent 403s at runtime.
|
|
118
130
|
if (config.provider === "sendgrid") {
|
|
@@ -123,6 +135,21 @@ function getFromAddress(
|
|
|
123
135
|
return "Agent Native <onboarding@resend.dev>";
|
|
124
136
|
}
|
|
125
137
|
|
|
138
|
+
/**
|
|
139
|
+
* Swap the display name while keeping the verified address. The name is
|
|
140
|
+
* sanitized and quoted because it lands in a header: CR/LF would allow header
|
|
141
|
+
* injection, and quotes/angle brackets would break address parsing.
|
|
142
|
+
*/
|
|
143
|
+
function withDisplayName(from: string, name: string): string {
|
|
144
|
+
const safe = name
|
|
145
|
+
.replace(/[\r\n"<>\\]/g, " ")
|
|
146
|
+
.replace(/\s+/g, " ")
|
|
147
|
+
.trim();
|
|
148
|
+
if (!safe) return from;
|
|
149
|
+
const address = from.match(/<([^>]+)>/)?.[1]?.trim() ?? from.trim();
|
|
150
|
+
return `"${safe}" <${address}>`;
|
|
151
|
+
}
|
|
152
|
+
|
|
126
153
|
async function sendEmailWithSignal(
|
|
127
154
|
args: SendEmailArgs,
|
|
128
155
|
signal?: AbortSignal,
|
|
@@ -130,7 +157,7 @@ async function sendEmailWithSignal(
|
|
|
130
157
|
const config = await resolveEmailTransport();
|
|
131
158
|
signal?.throwIfAborted();
|
|
132
159
|
const provider = config.provider;
|
|
133
|
-
const from = getFromAddress(config, args.from);
|
|
160
|
+
const from = getFromAddress(config, args.from, args.fromName);
|
|
134
161
|
const attachments = resolveAttachments(args);
|
|
135
162
|
|
|
136
163
|
if (provider === "resend") {
|
|
@@ -270,10 +297,19 @@ export async function sendEmail(args: SendEmailArgs): Promise<void> {
|
|
|
270
297
|
|
|
271
298
|
function parseSendGridFrom(from: string): { email: string; name?: string } {
|
|
272
299
|
const m = from.match(/^\s*(.*?)\s*<(.+)>\s*$/);
|
|
273
|
-
if (m && m[2]) return { name: m[1]
|
|
300
|
+
if (m && m[2]) return { name: unquoteDisplayName(m[1]), email: m[2] };
|
|
274
301
|
return { email: from.trim() };
|
|
275
302
|
}
|
|
276
303
|
|
|
304
|
+
function unquoteDisplayName(name: string): string | undefined {
|
|
305
|
+
const trimmed = name.trim();
|
|
306
|
+
const unquoted =
|
|
307
|
+
trimmed.startsWith('"') && trimmed.endsWith('"')
|
|
308
|
+
? trimmed.slice(1, -1).replace(/\\(.)/g, "$1")
|
|
309
|
+
: trimmed;
|
|
310
|
+
return unquoted || undefined;
|
|
311
|
+
}
|
|
312
|
+
|
|
277
313
|
function stripHtml(html: string): string {
|
|
278
314
|
return html
|
|
279
315
|
.replace(/<br\s*\/?>/gi, "\n")
|