@agent-native/core 0.140.0 → 0.141.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/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
- package/corpus/templates/analytics/changelog/2026-08-05-historical-analytics-rollups-stay-complete-and-atomic.md +6 -0
- package/corpus/templates/analytics/server/lib/dashboard-report.ts +0 -2
- package/corpus/templates/analytics/server/lib/first-party-analytics-rollups.ts +11 -6
- package/corpus/templates/analytics/server/lib/first-party-analytics.ts +8 -18
- package/corpus/templates/analytics/server/plugins/db.ts +174 -0
- package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +38 -38
- package/corpus/templates/calendar/changelog/2026-08-05-out-of-office-blocks-now-open-from-their-scheduled-time-rang.md +6 -0
- package/corpus/templates/calendar/server/lib/booking-emails.ts +70 -154
- package/corpus/templates/calendar/server/lib/event-guest-notifications.ts +28 -64
- package/corpus/templates/clips/actions/get-recording-player-data.ts +1 -0
- package/corpus/templates/clips/actions/invite-member.ts +16 -39
- package/corpus/templates/clips/app/components/player/comments-panel.tsx +2 -3
- package/corpus/templates/clips/app/components/player/reaction-emojis.ts +10 -0
- package/corpus/templates/clips/app/components/player/reactions-tray.tsx +2 -2
- package/corpus/templates/clips/app/components/player/video-player.tsx +20 -6
- package/corpus/templates/clips/app/routes/embed.$shareId.tsx +33 -0
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +5 -1
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +27 -1
- package/corpus/templates/clips/changelog/2026-08-05-comment-and-video-reaction-pickers-now-include-eyes-and-mind.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-05-fresh-recordings-now-open-with-the-complete-video.md +6 -0
- package/corpus/templates/clips/server/lib/transactional-email-templates.ts +0 -24
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
- package/corpus/templates/content/server/lib/comment-notifications.ts +26 -57
- package/corpus/templates/design/.agents/skills/design-systems/SKILL.md +1 -44
- package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +1 -265
- package/corpus/templates/design/actions/create-design-system.ts +10 -9
- package/corpus/templates/design/actions/get-design-system.ts +0 -53
- package/corpus/templates/design/actions/import-design-tokens.ts +71 -5
- package/corpus/templates/design/actions/index-design-tokens.ts +80 -61
- package/corpus/templates/design/actions/update-design-system.ts +10 -7
- package/corpus/templates/design/app/components/design/EditPanel.tsx +2 -45
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +0 -12
- package/corpus/templates/design/app/components/design/edit-panel/document-colors.ts +6 -10
- package/corpus/templates/design/app/components/design/edit-panel/effects-properties.tsx +3 -19
- package/corpus/templates/design/app/components/design/edit-panel/fill-properties.tsx +10 -36
- package/corpus/templates/design/app/components/design/edit-panel/interaction-state-helpers.ts +0 -15
- package/corpus/templates/design/app/components/design/edit-panel/panel-primitives.tsx +1 -19
- package/corpus/templates/design/app/components/design/edit-panel/position-helpers.ts +0 -6
- package/corpus/templates/design/app/components/design/edit-panel/stroke-properties.tsx +23 -53
- package/corpus/templates/design/app/components/design/inspector/DesignColorPicker.tsx +4 -142
- package/corpus/templates/design/app/components/design/types.ts +0 -6
- package/corpus/templates/design/app/i18n-data.ts +0 -20
- package/corpus/templates/design/app/pages/DesignSystemSetup.tsx +1 -1
- package/corpus/templates/design/app/pages/DesignSystems.tsx +1 -78
- package/corpus/templates/design/app/pages/design-editor/html-layer-positioning.ts +1 -6
- package/corpus/templates/design/e2e/helpers.ts +0 -17
- package/corpus/templates/design/shared/resolve-tweaks.ts +18 -10
- package/corpus/templates/forms/server/lib/response-email.ts +0 -2
- package/corpus/templates/plan/actions/request-plan-access.ts +12 -33
- package/corpus/templates/plan/server/lib/comment-notifications.ts +27 -53
- package/corpus/templates/slides/server/lib/comment-notifications.ts +21 -44
- package/dist/a2a/handlers.js +9 -10
- package/dist/brand-kit/index.d.ts +0 -1
- package/dist/brand-kit/index.js +0 -1
- package/dist/brand-kit/types.d.ts +0 -25
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/db/client.js +8 -8
- package/dist/db/migrations.js +39 -19
- package/dist/localization/default-messages.d.ts +0 -45
- package/dist/localization/default-messages.js +0 -45
- package/dist/notifications/routes.d.ts +6 -6
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/handlers.js +1 -8
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +7 -7
- package/dist/provider-api/actions/provider-api.d.ts +7 -7
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-discovery.js +0 -22
- package/dist/server/auth.js +3 -1
- package/dist/server/better-auth-instance.js +2 -17
- package/dist/server/email.d.ts +0 -9
- package/dist/server/email.js +3 -44
- package/dist/server/index.d.ts +0 -1
- package/dist/server/index.js +0 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/templates/workspace-core/.agents/skills/authentication/SKILL.md +3 -2
- package/docs/content/authentication.mdx +25 -19
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/authentication/SKILL.md +3 -2
- package/corpus/templates/analytics/server/lib/emails.ts +0 -51
- package/corpus/templates/analytics/server/plugins/transactional-emails.ts +0 -5
- package/corpus/templates/calendar/server/lib/emails.ts +0 -167
- package/corpus/templates/calendar/server/plugins/transactional-emails.ts +0 -5
- package/corpus/templates/clips/server/lib/emails.ts +0 -256
- package/corpus/templates/clips/server/plugins/transactional-email-catalog.ts +0 -5
- package/corpus/templates/content/server/lib/emails.ts +0 -70
- package/corpus/templates/content/server/plugins/transactional-emails.ts +0 -5
- package/corpus/templates/design/app/components/design/edit-panel/design-system-swatches.ts +0 -215
- package/corpus/templates/design/changelog/2026-08-04-design-system-tokens-show-by-name-in-the-editor.md +0 -6
- package/corpus/templates/design/server/lib/design-system-tokens.ts +0 -35
- package/corpus/templates/design/shared/authored-color-styles.ts +0 -401
- package/corpus/templates/dispatch/app/routes/transactional-email.$appId.$id.tsx +0 -4
- package/corpus/templates/dispatch/app/routes/transactional-email.tsx +0 -4
- package/corpus/templates/forms/server/lib/emails.ts +0 -52
- package/corpus/templates/forms/server/plugins/transactional-emails.ts +0 -5
- package/corpus/templates/plan/server/lib/emails.ts +0 -67
- package/corpus/templates/plan/server/plugins/transactional-emails.ts +0 -5
- package/corpus/templates/slides/server/lib/emails.ts +0 -40
- package/corpus/templates/slides/server/plugins/transactional-emails.ts +0 -5
- package/dist/brand-kit/tokens.d.ts +0 -92
- package/dist/brand-kit/tokens.js +0 -280
- package/dist/email-catalog/actions/list-email-activity.d.ts +0 -6
- package/dist/email-catalog/actions/list-email-activity.js +0 -15
- package/dist/email-catalog/actions/list-email-engagement.d.ts +0 -6
- package/dist/email-catalog/actions/list-email-engagement.js +0 -15
- package/dist/email-catalog/actions/list-email-log.d.ts +0 -8
- package/dist/email-catalog/actions/list-email-log.js +0 -22
- package/dist/email-catalog/actions/list-transactional-emails.d.ts +0 -23
- package/dist/email-catalog/actions/list-transactional-emails.js +0 -61
- package/dist/email-catalog/actions/render-transactional-email-preview.d.ts +0 -10
- package/dist/email-catalog/actions/render-transactional-email-preview.js +0 -24
- package/dist/email-catalog/authorize.d.ts +0 -2
- package/dist/email-catalog/authorize.js +0 -10
- package/dist/email-catalog/log.d.ts +0 -57
- package/dist/email-catalog/log.js +0 -117
- package/dist/email-catalog/provider-metrics.d.ts +0 -34
- package/dist/email-catalog/provider-metrics.js +0 -165
- package/dist/email-catalog/registry.d.ts +0 -79
- package/dist/email-catalog/registry.js +0 -71
- package/dist/email-catalog/schema.d.ts +0 -212
- package/dist/email-catalog/schema.js +0 -49
- package/dist/email-catalog/system-emails.d.ts +0 -14
- package/dist/email-catalog/system-emails.js +0 -68
|
@@ -18,7 +18,6 @@ import { TEMPLATES } from "../cli/templates-meta.js";
|
|
|
18
18
|
import { getDbExec, isPostgres } from "../db/client.js";
|
|
19
19
|
import { getDialect, getDatabaseUrl, getDatabaseAuthToken, closePgliteClients, getPgliteClient, isPgliteUrl, loadPgliteDrizzle, pgPoolOptions, neonPoolMax, attachNeonPoolErrorLogger, sharedDbPool, onSharedDbPoolsClosed, onSharedDbPoolReplaced, } from "../db/client.js";
|
|
20
20
|
import { ensureTableExists } from "../db/ddl-guard.js";
|
|
21
|
-
import { CORE_RESET_PASSWORD_EMAIL_ID, CORE_VERIFY_SIGNUP_EMAIL_ID, } from "../email-catalog/system-emails.js";
|
|
22
21
|
import { saveOAuthTokens } from "../oauth-tokens/store.js";
|
|
23
22
|
import { acceptPendingInvitationsForEmail } from "../org/accept-pending.js";
|
|
24
23
|
import { getAuthEmailForUserId, getRequiredAuthProviderForEmail, } from "../org/auth-policy.js";
|
|
@@ -919,14 +918,7 @@ async function createBetterAuthInstance(config) {
|
|
|
919
918
|
email: user.email,
|
|
920
919
|
resetUrl,
|
|
921
920
|
});
|
|
922
|
-
await sendEmail({
|
|
923
|
-
to: user.email,
|
|
924
|
-
subject,
|
|
925
|
-
html,
|
|
926
|
-
text,
|
|
927
|
-
appSender,
|
|
928
|
-
templateId: CORE_RESET_PASSWORD_EMAIL_ID,
|
|
929
|
-
});
|
|
921
|
+
await sendEmail({ to: user.email, subject, html, text, appSender });
|
|
930
922
|
},
|
|
931
923
|
},
|
|
932
924
|
emailVerification: {
|
|
@@ -950,14 +942,7 @@ async function createBetterAuthInstance(config) {
|
|
|
950
942
|
email: user.email,
|
|
951
943
|
verifyUrl,
|
|
952
944
|
});
|
|
953
|
-
await sendEmail({
|
|
954
|
-
to: user.email,
|
|
955
|
-
subject,
|
|
956
|
-
html,
|
|
957
|
-
text,
|
|
958
|
-
appSender,
|
|
959
|
-
templateId: CORE_VERIFY_SIGNUP_EMAIL_ID,
|
|
960
|
-
});
|
|
945
|
+
await sendEmail({ to: user.email, subject, html, text, appSender });
|
|
961
946
|
},
|
|
962
947
|
},
|
|
963
948
|
socialProviders,
|
package/dist/server/email.d.ts
CHANGED
|
@@ -58,15 +58,6 @@ export interface SendEmailArgs {
|
|
|
58
58
|
references?: string;
|
|
59
59
|
attachments?: EmailAttachment[];
|
|
60
60
|
timeoutMs?: number;
|
|
61
|
-
/**
|
|
62
|
-
* Registered transactional email id (see `defineTransactionalEmail`), e.g.
|
|
63
|
-
* `calendar.booking-confirmed`. Tags the message at the provider so delivery
|
|
64
|
-
* and open metrics attribute to one email instead of to the whole account,
|
|
65
|
-
* and keys the row written to `email_log`. Omit for genuinely one-off sends.
|
|
66
|
-
*/
|
|
67
|
-
templateId?: string;
|
|
68
|
-
/** App slug that owns the send. Defaults to the running app. */
|
|
69
|
-
app?: string;
|
|
70
61
|
}
|
|
71
62
|
export declare function isEmailConfigured(): Promise<boolean>;
|
|
72
63
|
/**
|
package/dist/server/email.js
CHANGED
|
@@ -10,8 +10,6 @@
|
|
|
10
10
|
* so the reset-password flow still works end-to-end for local development.
|
|
11
11
|
*/
|
|
12
12
|
import { FAVICON_PNG_BASE64 } from "../assets/branding/favicon-base64.js";
|
|
13
|
-
import { recordEmailSend } from "../email-catalog/log.js";
|
|
14
|
-
import { getAppSlug } from "./app-name.js";
|
|
15
13
|
import { resolveSecret } from "./credential-provider.js";
|
|
16
14
|
import { AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID } from "./email-template.js";
|
|
17
15
|
let cachedAgentNativeLogo;
|
|
@@ -156,7 +154,7 @@ function resolveAppSender(configuredFrom, appSender) {
|
|
|
156
154
|
replyTo: appSender.replyTo,
|
|
157
155
|
};
|
|
158
156
|
}
|
|
159
|
-
async function
|
|
157
|
+
async function sendEmailWithSignal(args, signal) {
|
|
160
158
|
const config = await resolveEmailTransport();
|
|
161
159
|
signal?.throwIfAborted();
|
|
162
160
|
const provider = config.provider;
|
|
@@ -208,7 +206,7 @@ async function deliverEmail(args, signal) {
|
|
|
208
206
|
const body = await res.text().catch(() => "");
|
|
209
207
|
throw new Error(`Resend error ${res.status}: ${body}`);
|
|
210
208
|
}
|
|
211
|
-
return
|
|
209
|
+
return;
|
|
212
210
|
}
|
|
213
211
|
if (provider === "sendgrid") {
|
|
214
212
|
const personalization = {
|
|
@@ -229,12 +227,6 @@ async function deliverEmail(args, signal) {
|
|
|
229
227
|
};
|
|
230
228
|
if (replyTo)
|
|
231
229
|
sgPayload.reply_to = parseSendGridFrom(replyTo);
|
|
232
|
-
// Categories are how per-email delivery/open stats are attributed. Without
|
|
233
|
-
// them every send lands in one undifferentiated account-wide bucket, which
|
|
234
|
-
// is indistinguishable from an email that never sent.
|
|
235
|
-
const categories = [args.templateId, args.app ?? getAppSlug()].filter((value) => Boolean(value));
|
|
236
|
-
if (categories.length)
|
|
237
|
-
sgPayload.categories = categories;
|
|
238
230
|
const sgHeaders = {};
|
|
239
231
|
if (args.inReplyTo)
|
|
240
232
|
sgHeaders["In-Reply-To"] = args.inReplyTo;
|
|
@@ -266,7 +258,7 @@ async function deliverEmail(args, signal) {
|
|
|
266
258
|
const body = await res.text().catch(() => "");
|
|
267
259
|
throw new Error(`SendGrid error ${res.status}: ${body}`);
|
|
268
260
|
}
|
|
269
|
-
return
|
|
261
|
+
return;
|
|
270
262
|
}
|
|
271
263
|
// Dev fallback — no provider configured. Logging the full body exposes
|
|
272
264
|
// reset tokens, so only do it outside production. In production, refuse
|
|
@@ -278,39 +270,6 @@ async function deliverEmail(args, signal) {
|
|
|
278
270
|
`Save RESEND_API_KEY or SENDGRID_API_KEY in settings to send real emails.\n` +
|
|
279
271
|
`---\nTo: ${args.to}\nFrom: ${from}\nSubject: ${args.subject}\n\n` +
|
|
280
272
|
`${args.text || stripHtml(args.html)}\n---\n`);
|
|
281
|
-
return { provider, from };
|
|
282
|
-
}
|
|
283
|
-
/**
|
|
284
|
-
* Deliver, then record the attempt. Recording lives here rather than in each
|
|
285
|
-
* provider branch so a new transport cannot be added without being logged.
|
|
286
|
-
*/
|
|
287
|
-
async function sendEmailWithSignal(args, signal) {
|
|
288
|
-
let outcome;
|
|
289
|
-
try {
|
|
290
|
-
outcome = await deliverEmail(args, signal);
|
|
291
|
-
}
|
|
292
|
-
catch (error) {
|
|
293
|
-
await recordEmailSend({
|
|
294
|
-
templateId: args.templateId,
|
|
295
|
-
app: args.app ?? getAppSlug() ?? "unknown",
|
|
296
|
-
recipient: args.to,
|
|
297
|
-
sender: outcome?.from ?? args.from ?? "unknown",
|
|
298
|
-
subject: args.subject,
|
|
299
|
-
status: "failed",
|
|
300
|
-
error: error instanceof Error ? error.message : String(error),
|
|
301
|
-
provider: outcome?.provider ?? "unknown",
|
|
302
|
-
});
|
|
303
|
-
throw error;
|
|
304
|
-
}
|
|
305
|
-
await recordEmailSend({
|
|
306
|
-
templateId: args.templateId,
|
|
307
|
-
app: args.app ?? getAppSlug() ?? "unknown",
|
|
308
|
-
recipient: args.to,
|
|
309
|
-
sender: outcome.from,
|
|
310
|
-
subject: args.subject,
|
|
311
|
-
status: "sent",
|
|
312
|
-
provider: outcome.provider,
|
|
313
|
-
});
|
|
314
273
|
}
|
|
315
274
|
export async function sendEmail(args) {
|
|
316
275
|
const requestedTimeoutMs = Number(args.timeoutMs);
|
package/dist/server/index.d.ts
CHANGED
|
@@ -66,7 +66,6 @@ export { builderDesignSystemUrl, builderProjectBranchUrl, buildBuilderDesignSyst
|
|
|
66
66
|
export { getBuilderBranchProjectId, isBuilderBranchingEnabled, requestBuilderBrowserConnection, resolveBuilderBranchProjectId, resolveIsBuilderBranchingEnabled, runBuilderAgent, type RunBuilderAgentResult, } from "./builder-browser.js";
|
|
67
67
|
export { ensureFusionContainer, sendFusionBranchMessage, pushFusionBranch, reserveFusionHostingSlug, deployFusionProject, getFusionDeploys, getFusionBranchEditorUrl, getFusionHostingUrl, type FusionBranchRef, type EnsureFusionContainerResult, type SendFusionMessageResult, } from "./fusion-app.js";
|
|
68
68
|
export { sendEmail, isEmailConfigured, getEmailProvider, type EmailAttachment, type EmailProvider, type SendEmailArgs, } from "./email.js";
|
|
69
|
-
export { defineTransactionalEmail, listTransactionalEmails, getTransactionalEmail, renderTransactionalEmailPreview, type TransactionalEmailDefinition, type RegisteredTransactionalEmail, } from "../email-catalog/registry.js";
|
|
70
69
|
export { notifyActivity, runActivityNotification, resolveActivityRecipients, type ActivityDeliveryFailure, type ActivityNotificationResult, type ActivityNotificationStatus, type NotifyActivityInput, type ResolveActivityRecipientsInput, } from "./activity-notifications.js";
|
|
71
70
|
export { renderEmail, emailStrong, emailLink, type RenderEmailArgs, type RenderedEmail, type EmailCta, } from "./email-template.js";
|
|
72
71
|
export { getAppProductionUrl, getFirstPartyProdUrl } from "./app-url.js";
|
package/dist/server/index.js
CHANGED
|
@@ -67,7 +67,6 @@ export { builderDesignSystemUrl, builderProjectBranchUrl, buildBuilderDesignSyst
|
|
|
67
67
|
export { getBuilderBranchProjectId, isBuilderBranchingEnabled, requestBuilderBrowserConnection, resolveBuilderBranchProjectId, resolveIsBuilderBranchingEnabled, runBuilderAgent, } from "./builder-browser.js";
|
|
68
68
|
export { ensureFusionContainer, sendFusionBranchMessage, pushFusionBranch, reserveFusionHostingSlug, deployFusionProject, getFusionDeploys, getFusionBranchEditorUrl, getFusionHostingUrl, } from "./fusion-app.js";
|
|
69
69
|
export { sendEmail, isEmailConfigured, getEmailProvider, } from "./email.js";
|
|
70
|
-
export { defineTransactionalEmail, listTransactionalEmails, getTransactionalEmail, renderTransactionalEmailPreview, } from "../email-catalog/registry.js";
|
|
71
70
|
export { notifyActivity, runActivityNotification, resolveActivityRecipients, } from "./activity-notifications.js";
|
|
72
71
|
export { renderEmail, emailStrong, emailLink, } from "./email-template.js";
|
|
73
72
|
export { getAppProductionUrl, getFirstPartyProdUrl } from "./app-url.js";
|
|
@@ -20,9 +20,10 @@ Auth is powered by **Better Auth** with account-first design. Every new user cre
|
|
|
20
20
|
| Mode | Behavior |
|
|
21
21
|
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
22
22
|
| **Development (default)** | Real Better Auth — same flow as production. There is **no auth bypass**. On first run the framework auto-creates a throwaway dev account and signs you in without printing its credentials (disable with `AGENT_NATIVE_DISABLE_AUTO_DEV_ACCOUNT=1`), so you are not stuck at a login wall. `getSession()` returns the signed-in user or `null` — it never falls back to a sentinel identity. |
|
|
23
|
-
| **Production (default)** | Better Auth with email/password
|
|
23
|
+
| **Production (default)** | Magic-link-first Better Auth when outbound email is ready, with email/password fallback and social providers (Google, GitHub). Organizations built in. |
|
|
24
24
|
| **`AUTH_MODE=local`** | **Not** a browser auth bypass, and never returns `local@localhost`. It only affects CLI/agent identity: it lets `pnpm action` / the local agent loop auto-bind to the single real signed-in dev user from the `sessions` table (see `scripts/dev-session.ts`). Browser login is unchanged. |
|
|
25
|
-
| **`AUTH_SKIP_EMAIL_VERIFICATION=1`** | QA/preview escape hatch for
|
|
25
|
+
| **`AUTH_SKIP_EMAIL_VERIFICATION=1`** | QA/preview escape hatch for password-fallback accounts. Signup skips email verification and does not send the signup verification email. Local dev/test skips verification by default; set `AUTH_SKIP_EMAIL_VERIFICATION=0` only when testing verification itself. It does not change magic-link delivery. Use `+qa` emails for test accounts. |
|
|
26
|
+
| **`AUTH_MAGIC_LINK=0`** | Force the email/password fallback even when outbound email is ready. |
|
|
26
27
|
| **`AUTH_DISABLED=true`** | Skip login/signup entirely — every request runs as `dev@local.test`. For local dev, cloud previews, and internal demos only; not for production with real users. |
|
|
27
28
|
| **`ACCESS_TOKEN` / `ACCESS_TOKENS`** | Static bearer fallback for MCP/connect clients that cannot use OAuth. Not browser auth and never a token login page. |
|
|
28
29
|
| **Custom** | Pass your own `getSession` to `autoMountAuth(app, { getSession })`. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Authentication"
|
|
3
|
-
description: "Better Auth integration with
|
|
3
|
+
description: "Magic-link-first Better Auth integration with password fallback, social providers, organizations, and MCP bearer credentials."
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Authentication
|
|
@@ -11,7 +11,7 @@ Agent-native apps use [Better Auth](https://better-auth.com) for authentication
|
|
|
11
11
|
|
|
12
12
|
Auth is configured automatically via `autoMountAuth(app)` in the auth server plugin. There are three modes:
|
|
13
13
|
|
|
14
|
-
- **Default:** Better Auth with email/password
|
|
14
|
+
- **Default:** Magic-link-first Better Auth when outbound email is ready, with email/password fallback and social providers. Onboarding page shown on first visit.
|
|
15
15
|
- **Remote MCP OAuth:** Standard OAuth 2.1 for MCP hosts such as Claude Code and ChatGPT connectors.
|
|
16
16
|
- **Custom:** Bring your own auth via `getSession` callback.
|
|
17
17
|
|
|
@@ -24,7 +24,7 @@ Auth is configured automatically via `autoMountAuth(app)` in the auth server plu
|
|
|
24
24
|
<span class="diagram-pill accent">Default</span
|
|
25
25
|
><strong>Better Auth</strong
|
|
26
26
|
><small class="diagram-muted"
|
|
27
|
-
>
|
|
27
|
+
>magic link / password · Google · GitHub</small
|
|
28
28
|
>
|
|
29
29
|
</div>
|
|
30
30
|
<div class="diagram-card">
|
|
@@ -84,11 +84,11 @@ Auth is configured automatically via `autoMountAuth(app)` in the auth server plu
|
|
|
84
84
|
|
|
85
85
|
The browser flow is the same Better Auth flow everywhere — there is **no dev auth bypass**, and `getSession()` never falls back to a `local@localhost` sentinel. What changes between environments is signup friction, not the login wall:
|
|
86
86
|
|
|
87
|
-
| Environment | First-load behavior
|
|
88
|
-
| ---------------- |
|
|
89
|
-
| **Local dev** | Auto-creates a throwaway dev account and signs you in (no login wall)
|
|
90
|
-
| **QA / preview** | Normal signup, but verification can be skipped so testers don't wait on email
|
|
91
|
-
| **Production** |
|
|
87
|
+
| Environment | First-load behavior | Email verification |
|
|
88
|
+
| ---------------- | --------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
|
|
89
|
+
| **Local dev** | Auto-creates a throwaway dev account and signs you in (no login wall) | Skipped by default (and when no email provider) |
|
|
90
|
+
| **QA / preview** | Normal signup, but verification can be skipped so testers don't wait on email | Skip with `AUTH_SKIP_EMAIL_VERIFICATION=1` |
|
|
91
|
+
| **Production** | Magic-link signup/login when outbound email is ready; email/password fallback otherwise | Required for magic links (when an email provider is configured) |
|
|
92
92
|
|
|
93
93
|
A few flags tune this; full details are in the [Environment Variables](#environment-variables) table:
|
|
94
94
|
|
|
@@ -104,9 +104,12 @@ A few flags tune this; full details are in the [Environment Variables](#environm
|
|
|
104
104
|
|
|
105
105
|
## Better Auth (Default) {#better-auth}
|
|
106
106
|
|
|
107
|
-
By default, Better Auth powers authentication.
|
|
107
|
+
By default, Better Auth powers authentication. When outbound email is ready,
|
|
108
|
+
magic-link signup/login is the primary browser flow. Email/password remains
|
|
109
|
+
the fallback when email is unavailable, or when `AUTH_MAGIC_LINK=0` is set. It
|
|
110
|
+
provides:
|
|
108
111
|
|
|
109
|
-
-
|
|
112
|
+
- Magic-link registration and login, with email/password fallback
|
|
110
113
|
- Social providers (Google, GitHub, and 35+ others)
|
|
111
114
|
- Organizations with roles and invitations
|
|
112
115
|
- JWT tokens for API and A2A access
|
|
@@ -115,7 +118,8 @@ By default, Better Auth powers authentication. It provides:
|
|
|
115
118
|
Better Auth routes are mounted at `/_agent-native/auth/ba/*`. The framework also provides backward-compatible endpoints:
|
|
116
119
|
|
|
117
120
|
- `GET /_agent-native/auth/session` — get current session
|
|
118
|
-
- `POST /_agent-native/auth/
|
|
121
|
+
- `POST /_agent-native/auth/magic-link` — request a magic-link email
|
|
122
|
+
- `POST /_agent-native/auth/login` — email/password fallback login
|
|
119
123
|
- `POST /_agent-native/auth/register` — create account
|
|
120
124
|
- `POST /_agent-native/auth/logout` — sign out
|
|
121
125
|
|
|
@@ -142,9 +146,10 @@ subdomains, set `AGENT_NATIVE_SHARE_COOKIE_DOMAIN=1` alongside
|
|
|
142
146
|
|
|
143
147
|
## QA Accounts {#qa-accounts}
|
|
144
148
|
|
|
145
|
-
Local development and tests skip signup email verification by default,
|
|
146
|
-
|
|
147
|
-
verification locally while testing that
|
|
149
|
+
Local development and tests skip password-signup email verification by default,
|
|
150
|
+
so the email/password fallback can be tested without waiting on an inbox. To
|
|
151
|
+
force verification locally while testing that fallback, set
|
|
152
|
+
`AUTH_SKIP_EMAIL_VERIFICATION=0`.
|
|
148
153
|
|
|
149
154
|
For hosted QA environments where testers need real accounts but should not wait
|
|
150
155
|
on email delivery, set:
|
|
@@ -153,9 +158,9 @@ on email delivery, set:
|
|
|
153
158
|
AUTH_SKIP_EMAIL_VERIFICATION=1
|
|
154
159
|
```
|
|
155
160
|
|
|
156
|
-
When this flag is set,
|
|
157
|
-
verification and the signup verification email is not sent.
|
|
158
|
-
or preview environments, and name test accounts with a `+qa` address
|
|
161
|
+
When this flag is set, password fallback signup does not require email
|
|
162
|
+
verification and the signup verification email is not sent. It does not change
|
|
163
|
+
magic-link delivery. Use it only for QA or preview environments, and name test accounts with a `+qa` address
|
|
159
164
|
(`name+qa@example.com`) so they are easy to identify.
|
|
160
165
|
|
|
161
166
|
## Social Providers {#social-providers}
|
|
@@ -382,7 +387,7 @@ Templates with **public pages** (share links, embeds, marketing pages) often nee
|
|
|
382
387
|
/_agent-native/sign-in?return=<same-origin-path>
|
|
383
388
|
```
|
|
384
389
|
|
|
385
|
-
When an anonymous viewer hits this URL, the framework's login page is served. After a successful sign-in (
|
|
390
|
+
When an anonymous viewer hits this URL, the framework's login page is served. After a successful sign-in (magic link, email/password, token, or Google OAuth), the browser returns to `return`.
|
|
386
391
|
|
|
387
392
|
The `return` parameter is validated as a **same-origin path**. Network-path references (`//evil.com/...`), absolute URLs, `data:` / `javascript:` schemes, and embedded control characters all fall back to `/`. The validated path is reconstructed from the URL parser, not echoed back from the input.
|
|
388
393
|
|
|
@@ -429,7 +434,8 @@ The default `/_agent-native/google/auth-url` route does this automatically — o
|
|
|
429
434
|
| Variable | Purpose |
|
|
430
435
|
| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
431
436
|
| `BETTER_AUTH_SECRET` | Signing key for Better Auth (auto-generated if not set) |
|
|
432
|
-
| `AUTH_SKIP_EMAIL_VERIFICATION` | Set to `1` in QA/preview environments to let
|
|
437
|
+
| `AUTH_SKIP_EMAIL_VERIFICATION` | Set to `1` in QA/preview environments to let password-fallback signups proceed without verification; local dev/test skips by default |
|
|
438
|
+
| `AUTH_MAGIC_LINK` | Set to `0`, `false`, or `off` to force the email/password fallback even when outbound email is ready |
|
|
433
439
|
| `AUTH_DISABLED` | Set to `true` or `1` to skip login/signup; all requests run as one shared user (local dev/preview only — not for production with real users) |
|
|
434
440
|
| `AGENT_NATIVE_DISABLE_AUTO_DEV_ACCOUNT` | Set to `1` to disable localhost auto-sign-in on a fresh dev database |
|
|
435
441
|
| `AUTH_MODE` | `local` resolves CLI/agent identity only (which dev user `pnpm action` runs as); never a browser login bypass |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.141.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": {
|
|
@@ -20,9 +20,10 @@ Auth is powered by **Better Auth** with account-first design. Every new user cre
|
|
|
20
20
|
| Mode | Behavior |
|
|
21
21
|
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
|
22
22
|
| **Development (default)** | Real Better Auth — same flow as production. There is **no auth bypass**. On first run the framework auto-creates a throwaway dev account and signs you in without printing its credentials (disable with `AGENT_NATIVE_DISABLE_AUTO_DEV_ACCOUNT=1`), so you are not stuck at a login wall. `getSession()` returns the signed-in user or `null` — it never falls back to a sentinel identity. |
|
|
23
|
-
| **Production (default)** | Better Auth with email/password
|
|
23
|
+
| **Production (default)** | Magic-link-first Better Auth when outbound email is ready, with email/password fallback and social providers (Google, GitHub). Organizations built in. |
|
|
24
24
|
| **`AUTH_MODE=local`** | **Not** a browser auth bypass, and never returns `local@localhost`. It only affects CLI/agent identity: it lets `pnpm action` / the local agent loop auto-bind to the single real signed-in dev user from the `sessions` table (see `scripts/dev-session.ts`). Browser login is unchanged. |
|
|
25
|
-
| **`AUTH_SKIP_EMAIL_VERIFICATION=1`** | QA/preview escape hatch for
|
|
25
|
+
| **`AUTH_SKIP_EMAIL_VERIFICATION=1`** | QA/preview escape hatch for password-fallback accounts. Signup skips email verification and does not send the signup verification email. Local dev/test skips verification by default; set `AUTH_SKIP_EMAIL_VERIFICATION=0` only when testing verification itself. It does not change magic-link delivery. Use `+qa` emails for test accounts. |
|
|
26
|
+
| **`AUTH_MAGIC_LINK=0`** | Force the email/password fallback even when outbound email is ready. |
|
|
26
27
|
| **`AUTH_DISABLED=true`** | Skip login/signup entirely — every request runs as `dev@local.test`. For local dev, cloud previews, and internal demos only; not for production with real users. |
|
|
27
28
|
| **`ACCESS_TOKEN` / `ACCESS_TOKENS`** | Static bearer fallback for MCP/connect clients that cannot use OAuth. Not browser auth and never a token login page. |
|
|
28
29
|
| **Custom** | Pass your own `getSession` to `autoMountAuth(app, { getSession })`. |
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Catalog entries for the transactional emails Analytics sends.
|
|
3
|
-
*
|
|
4
|
-
* Registered from `server/plugins/transactional-emails.ts` so Dispatch can list
|
|
5
|
-
* and preview them without the app having sent anything yet.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { defineTransactionalEmail } from "@agent-native/core/email-catalog";
|
|
9
|
-
import { emailStrong, renderEmail } from "@agent-native/core/server";
|
|
10
|
-
|
|
11
|
-
export const ANALYTICS_DASHBOARD_REPORT_EMAIL_ID = "analytics.dashboard-report";
|
|
12
|
-
|
|
13
|
-
let registered = false;
|
|
14
|
-
|
|
15
|
-
export function registerAnalyticsEmails(): void {
|
|
16
|
-
if (registered) return;
|
|
17
|
-
registered = true;
|
|
18
|
-
|
|
19
|
-
defineTransactionalEmail({
|
|
20
|
-
id: ANALYTICS_DASHBOARD_REPORT_EMAIL_ID,
|
|
21
|
-
name: "Scheduled dashboard report",
|
|
22
|
-
trigger:
|
|
23
|
-
"A dashboard report subscription comes due and at least one panel produced usable data. A run where every queried panel failed throws instead of mailing, and a degraded run is skipped when the subscription asks for complete reports only.",
|
|
24
|
-
recipientLabel: "Subscription recipients",
|
|
25
|
-
recipient:
|
|
26
|
-
"The normalized recipient list stored on the subscription. One email per address, all carrying the same rendered snapshot.",
|
|
27
|
-
senderLabel: "Default sender",
|
|
28
|
-
sender:
|
|
29
|
-
"The configured default sender. This call site sets no `from`, `fromName`, `replyTo`, or `appSender`.",
|
|
30
|
-
// The real renderer runs every panel query and rasterizes charts, so it
|
|
31
|
-
// cannot back a preview that must stay offline. This shows the frame a
|
|
32
|
-
// recipient sees; the panel body is whatever that run produced.
|
|
33
|
-
preview: () => ({
|
|
34
|
-
subject: "Daily dashboard: Growth overview — 3/4/2025",
|
|
35
|
-
...renderEmail({
|
|
36
|
-
preheader: "Your daily Growth overview report is ready.",
|
|
37
|
-
heading: "Daily dashboard: Growth overview",
|
|
38
|
-
paragraphs: [
|
|
39
|
-
`This report renders each panel of ${emailStrong("Growth overview")} as of the run time, with charts drawn server-side and attached inline.`,
|
|
40
|
-
"Panels that could not be queried are called out in place rather than dropped, so a partial report never reads as a complete one.",
|
|
41
|
-
],
|
|
42
|
-
cta: {
|
|
43
|
-
label: "Open dashboard",
|
|
44
|
-
url: "https://example.com/dashboards/dash_sample",
|
|
45
|
-
},
|
|
46
|
-
footer:
|
|
47
|
-
"You received this because you are on the recipient list for this dashboard report.",
|
|
48
|
-
}),
|
|
49
|
-
}),
|
|
50
|
-
});
|
|
51
|
-
}
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Catalog entries for the transactional emails Calendar sends.
|
|
3
|
-
*
|
|
4
|
-
* Registered from `server/plugins/transactional-emails.ts` so Dispatch can list
|
|
5
|
-
* and preview them without the app having sent anything yet.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { defineTransactionalEmail } from "@agent-native/core/email-catalog";
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
renderBookingCancelledEmail,
|
|
12
|
-
renderBookingCancelledHostEmail,
|
|
13
|
-
renderBookingConfirmedEmail,
|
|
14
|
-
renderBookingReceivedEmail,
|
|
15
|
-
} from "./booking-emails.js";
|
|
16
|
-
import { renderEventGuestNote } from "./event-guest-notifications.js";
|
|
17
|
-
|
|
18
|
-
/** Obviously-fake sample data — these render in a preview pane, never send. */
|
|
19
|
-
const SAMPLE_TITLE = "Intro call";
|
|
20
|
-
const SAMPLE_WHEN = "Tuesday, March 4, 2025, 10:00 AM - 10:30 AM PST";
|
|
21
|
-
const SAMPLE_HOST = "dana.hill@example.com";
|
|
22
|
-
const SAMPLE_GUEST = "sam.rivera@example.com";
|
|
23
|
-
const SAMPLE_GUEST_NAME = "Sam Rivera";
|
|
24
|
-
const SAMPLE_MANAGE_URL = "https://example.com/book/dana/manage/sample-token";
|
|
25
|
-
const SAMPLE_BOOK_AGAIN_URL = "https://example.com/book/dana/intro-call";
|
|
26
|
-
const SAMPLE_MEETING_LINK = "https://meet.example.com/sample-intro-call";
|
|
27
|
-
|
|
28
|
-
export const CALENDAR_BOOKING_CONFIRMED_EMAIL_ID = "calendar.booking-confirmed";
|
|
29
|
-
export const CALENDAR_BOOKING_RECEIVED_EMAIL_ID = "calendar.booking-received";
|
|
30
|
-
export const CALENDAR_BOOKING_CANCELLED_EMAIL_ID = "calendar.booking-cancelled";
|
|
31
|
-
export const CALENDAR_BOOKING_CANCELLED_HOST_EMAIL_ID =
|
|
32
|
-
"calendar.booking-cancelled-host";
|
|
33
|
-
export const CALENDAR_EVENT_UPDATE_NOTE_EMAIL_ID = "calendar.event-update-note";
|
|
34
|
-
export const CALENDAR_EVENT_CANCELLATION_NOTE_EMAIL_ID =
|
|
35
|
-
"calendar.event-cancellation-note";
|
|
36
|
-
|
|
37
|
-
let registered = false;
|
|
38
|
-
|
|
39
|
-
export function registerCalendarEmails(): void {
|
|
40
|
-
if (registered) return;
|
|
41
|
-
registered = true;
|
|
42
|
-
|
|
43
|
-
defineTransactionalEmail({
|
|
44
|
-
id: CALENDAR_BOOKING_CONFIRMED_EMAIL_ID,
|
|
45
|
-
name: "Booking confirmed (guest)",
|
|
46
|
-
trigger:
|
|
47
|
-
"A guest completes a booking on a public `/book/{username}/{slug}` page and the booking row is created.",
|
|
48
|
-
recipientLabel: "Booking guest",
|
|
49
|
-
recipient:
|
|
50
|
-
"The email address the guest typed into the booking form (`booking.email`).",
|
|
51
|
-
senderLabel: "Default, reply-to host",
|
|
52
|
-
sender:
|
|
53
|
-
"The configured EMAIL_FROM, with reply-to set to the booking link owner's address.",
|
|
54
|
-
preview: () =>
|
|
55
|
-
renderBookingConfirmedEmail({
|
|
56
|
-
title: SAMPLE_TITLE,
|
|
57
|
-
when: SAMPLE_WHEN,
|
|
58
|
-
host: SAMPLE_HOST,
|
|
59
|
-
manageUrl: SAMPLE_MANAGE_URL,
|
|
60
|
-
meetingLink: SAMPLE_MEETING_LINK,
|
|
61
|
-
}),
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
defineTransactionalEmail({
|
|
65
|
-
id: CALENDAR_BOOKING_RECEIVED_EMAIL_ID,
|
|
66
|
-
name: "New booking (host)",
|
|
67
|
-
trigger:
|
|
68
|
-
"Sent alongside the guest confirmation, immediately after a public booking is created.",
|
|
69
|
-
recipientLabel: "Booking link owner",
|
|
70
|
-
recipient:
|
|
71
|
-
"The owner of the booking link, looked up from the link's slug at send time.",
|
|
72
|
-
senderLabel: "Default, reply-to guest",
|
|
73
|
-
sender:
|
|
74
|
-
"The configured EMAIL_FROM, with reply-to set to the guest so the host can reply directly.",
|
|
75
|
-
preview: () =>
|
|
76
|
-
renderBookingReceivedEmail({
|
|
77
|
-
title: SAMPLE_TITLE,
|
|
78
|
-
when: SAMPLE_WHEN,
|
|
79
|
-
attendeeName: SAMPLE_GUEST_NAME,
|
|
80
|
-
attendee: SAMPLE_GUEST,
|
|
81
|
-
manageUrl: SAMPLE_MANAGE_URL,
|
|
82
|
-
meetingLink: SAMPLE_MEETING_LINK,
|
|
83
|
-
}),
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
defineTransactionalEmail({
|
|
87
|
-
id: CALENDAR_BOOKING_CANCELLED_EMAIL_ID,
|
|
88
|
-
name: "Booking cancelled (guest)",
|
|
89
|
-
trigger:
|
|
90
|
-
"A booking that was not already cancelled is cancelled, either by the guest through the manage/cancel token link or by someone with access to the booking link.",
|
|
91
|
-
recipientLabel: "Booking guest",
|
|
92
|
-
recipient: "The guest address stored on the booking row.",
|
|
93
|
-
senderLabel: "Default, reply-to host",
|
|
94
|
-
sender:
|
|
95
|
-
"The configured EMAIL_FROM, with reply-to set to the host when a host address could be resolved from the link slug.",
|
|
96
|
-
preview: () =>
|
|
97
|
-
renderBookingCancelledEmail({
|
|
98
|
-
title: SAMPLE_TITLE,
|
|
99
|
-
when: SAMPLE_WHEN,
|
|
100
|
-
host: SAMPLE_HOST,
|
|
101
|
-
bookAgainUrl: SAMPLE_BOOK_AGAIN_URL,
|
|
102
|
-
}),
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
defineTransactionalEmail({
|
|
106
|
-
id: CALENDAR_BOOKING_CANCELLED_HOST_EMAIL_ID,
|
|
107
|
-
name: "Booking cancelled (host)",
|
|
108
|
-
trigger:
|
|
109
|
-
"Sent after the guest cancellation notice, and only when a host address could be resolved from the booking link slug.",
|
|
110
|
-
recipientLabel: "Booking link owner",
|
|
111
|
-
recipient: "The booking link owner's address.",
|
|
112
|
-
senderLabel: "Default, reply-to guest",
|
|
113
|
-
sender:
|
|
114
|
-
"The configured EMAIL_FROM, with reply-to set to the guest who was booked.",
|
|
115
|
-
preview: () =>
|
|
116
|
-
renderBookingCancelledHostEmail({
|
|
117
|
-
title: SAMPLE_TITLE,
|
|
118
|
-
when: SAMPLE_WHEN,
|
|
119
|
-
attendeeName: SAMPLE_GUEST_NAME,
|
|
120
|
-
attendee: SAMPLE_GUEST,
|
|
121
|
-
}),
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
defineTransactionalEmail({
|
|
125
|
-
id: CALENDAR_EVENT_UPDATE_NOTE_EMAIL_ID,
|
|
126
|
-
name: "Event update note",
|
|
127
|
-
trigger:
|
|
128
|
-
"`update-event` runs with a non-empty guest notification message. Google Calendar still sends its own update invite; this carries only the organizer's note.",
|
|
129
|
-
recipientLabel: "Event attendees",
|
|
130
|
-
recipient:
|
|
131
|
-
"Every attendee on the event with a syntactically valid address, excluding the organizer's own `self` attendee row. One email per address.",
|
|
132
|
-
senderLabel: "Default, reply-to organizer",
|
|
133
|
-
sender:
|
|
134
|
-
"The configured EMAIL_FROM, with reply-to set to the organizer running the update.",
|
|
135
|
-
preview: () =>
|
|
136
|
-
renderEventGuestNote({
|
|
137
|
-
title: "Design review",
|
|
138
|
-
organizer: "Dana Hill",
|
|
139
|
-
message: "Moving this an hour later so the whole team can join.",
|
|
140
|
-
when: SAMPLE_WHEN,
|
|
141
|
-
kind: "update",
|
|
142
|
-
htmlLink: "https://calendar.example.com/event/sample",
|
|
143
|
-
}),
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
defineTransactionalEmail({
|
|
147
|
-
id: CALENDAR_EVENT_CANCELLATION_NOTE_EMAIL_ID,
|
|
148
|
-
name: "Event cancellation note",
|
|
149
|
-
trigger:
|
|
150
|
-
"`delete-event` runs with guest notification requested and a non-empty message. Google Calendar sends the cancellation itself; this carries only the organizer's note.",
|
|
151
|
-
recipientLabel: "Event attendees",
|
|
152
|
-
recipient:
|
|
153
|
-
"Every attendee on the deleted event with a syntactically valid address, excluding the organizer's own `self` attendee row. One email per address.",
|
|
154
|
-
senderLabel: "Default, reply-to organizer",
|
|
155
|
-
sender:
|
|
156
|
-
"The configured EMAIL_FROM, with reply-to set to the organizer running the deletion.",
|
|
157
|
-
preview: () =>
|
|
158
|
-
renderEventGuestNote({
|
|
159
|
-
title: "Design review",
|
|
160
|
-
organizer: "Dana Hill",
|
|
161
|
-
message: "Cancelling this week — we will pick it up after the launch.",
|
|
162
|
-
when: SAMPLE_WHEN,
|
|
163
|
-
kind: "cancellation",
|
|
164
|
-
appliesTo: "all events in the series",
|
|
165
|
-
}),
|
|
166
|
-
});
|
|
167
|
-
}
|