@agent-native/core 0.153.0 → 0.153.2
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/templates/analytics/.agents/skills/adhoc-analysis/SKILL.md +21 -19
- package/corpus/templates/calendar/app/components/calendar/DayView.tsx +3 -2
- package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +3 -2
- package/corpus/templates/calendar/app/lib/event-layout.ts +16 -35
- package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +27 -55
- package/corpus/templates/clips/app/components/player/share-dialog.tsx +104 -66
- package/corpus/templates/clips/app/components/sharing/share-ui.tsx +45 -43
- package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +0 -1
- package/corpus/templates/clips/desktop/src/app.tsx +818 -442
- package/corpus/templates/clips/desktop/src/styles.css +244 -2
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +2 -2
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +584 -1
- package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +25 -0
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +1 -1
- package/corpus/templates/factory/app/routes/factory.tsx +0 -3
- package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/ShareDialog.tsx +28 -25
- package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +50 -0
- package/corpus/templates/slides/app/components/editor/SlideEditor.tsx +12 -60
- package/dist/agent/production-agent.d.ts +8 -0
- package/dist/agent/production-agent.js +25 -2
- package/dist/agent/run-manager.js +13 -3
- package/dist/client/AgentPanel.js +6 -4
- package/dist/client/org/WorkspaceNotice.js +7 -4
- package/dist/client/sharing/ShareButton.d.ts +2 -2
- package/dist/client/sharing/ShareButton.js +18 -13
- package/dist/client/sharing/ShareDialog.d.ts +1 -1
- package/dist/client/sharing/ShareDialog.js +16 -11
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/server/auth.js +18 -4
- package/dist/server/better-auth-instance.js +8 -1
- package/dist/server/magic-link-attribution.d.ts +21 -0
- package/dist/server/magic-link-attribution.js +115 -0
- package/dist/server/realtime-token.d.ts +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@agent-native/toolkit/ui";
|
|
2
3
|
import * as Select from "@radix-ui/react-select";
|
|
3
4
|
import { IconLock, IconWorld, IconTrash, IconCheck, IconChevronDown, IconCopy, IconLoader2, IconSearch, IconSearchOff, IconShare3, IconUsersGroup, } from "@tabler/icons-react";
|
|
4
5
|
import { useEffect, useId, useRef, useState, } from "react";
|
|
@@ -114,25 +115,29 @@ function SharePanel(props) {
|
|
|
114
115
|
(Boolean(props.shareUrl) ||
|
|
115
116
|
Boolean(props.shareUrlPlaceholder) ||
|
|
116
117
|
Boolean(props.secondaryShareUrl));
|
|
117
|
-
const shareUrlPlacement = props.shareUrlPlacement ?? "
|
|
118
|
+
const shareUrlPlacement = props.shareUrlPlacement ?? "top";
|
|
119
|
+
const [advancedOpen, setAdvancedOpen] = useState(!showShareLinks);
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
setAdvancedOpen(!showShareLinks);
|
|
122
|
+
}, [props.resourceId, showShareLinks]);
|
|
118
123
|
const extraTabs = props.shareTabs?.tabs ?? [];
|
|
119
124
|
const hasTabs = extraTabs.length > 0;
|
|
120
125
|
const shareTabLabel = props.shareTabs?.shareLabel ?? "Share link";
|
|
121
126
|
const titleText = resourceTitle
|
|
122
127
|
? `Share "${resourceTitle}"`
|
|
123
128
|
: `Share ${props.resourceType}`;
|
|
124
|
-
const sharePanel = isLoading ? (_jsxs("div", { children: [!hasTabs ? (_jsx("div", { className: "mb-3 truncate text-base font-semibold", title: titleText, children: titleText })) : null, _jsx("div", { className: "mb-4 h-9 rounded-md bg-muted animate-pulse" }), _jsx("div", { className: "mb-2 text-sm font-semibold", children: peopleAccessLabel }), _jsx("div", { className: "mb-4 h-7 rounded-md bg-muted animate-pulse" }), _jsx("div", { className: "mb-2 text-sm font-semibold", children: generalAccessLabel }), _jsx("div", { className: "mb-4 h-9 rounded-md bg-muted animate-pulse" })] })) : (_jsxs("div", { children: [!hasTabs ? (_jsx("div", { className: "mb-3 truncate text-base font-semibold", title: titleText, children: titleText })) : null, showShareLinks && shareUrlPlacement === "top" ? shareLinks : null, canManage ? (_jsxs("div", { className: "
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
const sharePanel = isLoading ? (_jsxs("div", { children: [!hasTabs ? (_jsx("div", { className: "mb-3 truncate text-base font-semibold", title: titleText, children: titleText })) : null, _jsx("div", { className: "mb-4 h-9 rounded-md bg-muted animate-pulse" }), _jsx("div", { className: "mb-2 text-sm font-semibold", children: peopleAccessLabel }), _jsx("div", { className: "mb-4 h-7 rounded-md bg-muted animate-pulse" }), _jsx("div", { className: "mb-2 text-sm font-semibold", children: generalAccessLabel }), _jsx("div", { className: "mb-4 h-9 rounded-md bg-muted animate-pulse" })] })) : (_jsxs("div", { children: [!hasTabs ? (_jsx("div", { className: "mb-3 truncate text-base font-semibold", title: titleText, children: titleText })) : null, showShareLinks && shareUrlPlacement === "top" ? shareLinks : null, _jsxs(Collapsible, { open: advancedOpen, onOpenChange: setAdvancedOpen, className: "mb-4 overflow-hidden rounded-md border border-border", children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "flex min-h-10 w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm font-medium transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-2 truncate", children: [_jsx(IconUsersGroup, { "aria-hidden": true, size: 15, strokeWidth: 1.8, className: "shrink-0 text-muted-foreground" }), canManage ? "Manage access" : peopleAccessLabel] }), _jsx(IconChevronDown, { "aria-hidden": true, size: 16, strokeWidth: 1.8, className: cn("shrink-0 text-muted-foreground transition-transform", advancedOpen && "rotate-180") })] }) }), _jsx(CollapsibleContent, { className: "border-t border-border px-3 py-3", children: _jsxs("div", { className: "space-y-4", children: [canManage ? (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-stretch gap-2", children: [_jsx(MemberAutocomplete, { value: inviteEmail, open: suggestionsOpen, onOpenChange: setSuggestionsOpen, onValueChange: (next) => {
|
|
130
|
+
onInviteEmailChange(next);
|
|
131
|
+
if (shareError)
|
|
132
|
+
setShareError(null);
|
|
133
|
+
}, onSelectMember: (member) => {
|
|
134
|
+
onInviteEmailChange(member.email);
|
|
135
|
+
setSuggestionsOpen(false);
|
|
136
|
+
if (shareError)
|
|
137
|
+
setShareError(null);
|
|
138
|
+
}, onSubmit: handleAdd, placeholder: policy.requireOrgMemberForUserShares
|
|
139
|
+
? "Add people from your organization"
|
|
140
|
+
: "Add people by email", suggestions: memberSuggestions, search: memberSearch }), _jsx(RoleSelect, { value: role, onChange: setRole, roleCopy: props.roleCopy, allowedRoles: props.allowedRoles }), _jsx("button", { type: "button", onClick: handleAdd, disabled: !hasInviteEmail, className: BUTTON_PRIMARY_SM, children: "Add" })] }), shareError ? (_jsx("div", { role: "alert", className: "rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive", children: shareError })) : null, hasInviteEmail ? (_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 text-xs text-muted-foreground", children: [_jsxs("label", { className: "inline-flex items-center gap-2", children: [_jsx("input", { type: "checkbox", checked: notifyPeople, onChange: (e) => setNotifyPeople(e.target.checked), className: "h-4 w-4 rounded border-input accent-primary" }), "Notify people"] }), notifyPeople ? (_jsx("button", { type: "button", "aria-expanded": messageOpen, onClick: () => setMessageOpen(!messageOpen), className: "rounded-sm px-1 py-0.5 font-medium text-foreground underline decoration-border underline-offset-2 transition-colors hover:bg-accent hover:text-accent-foreground", children: messageOpen ? "Hide message" : "Add a message" })) : null] })) : null, hasInviteEmail && notifyPeople && messageOpen ? (_jsx("div", { className: "rounded-md border border-border/70 bg-muted/20 p-2.5", children: _jsx("textarea", { "aria-label": "Message", placeholder: "Add a short note (optional)", value: shareMessage, onChange: (event) => setShareMessage(event.target.value), maxLength: 500, rows: 3, className: "w-full resize-y rounded-md border border-input bg-card px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background" }) })) : null] })) : null, _jsxs("div", { children: [_jsx("div", { className: "mb-2 text-sm font-semibold", children: peopleAccessLabel }), _jsxs("ul", { className: "flex flex-col gap-1 list-none p-0 m-0", children: [data?.ownerEmail ? (_jsxs("li", { className: "flex items-center gap-3 px-1 py-1.5 text-sm", children: [_jsx(Avatar, { label: displayName(data.ownerEmail, knownMembers) }), _jsx("span", { className: "flex-1 min-w-0 truncate", children: displayName(data.ownerEmail, knownMembers) }), _jsx("span", { className: "text-xs text-muted-foreground", children: "Owner" })] })) : null, shares.map((s) => (_jsxs("li", { className: cn("flex items-center gap-3 px-1 py-1.5 text-sm", inFlight.has(keyOf(s)) && "opacity-60"), children: [_jsx(Avatar, { label: principalLabel(s, knownMembers), org: s.principalType === "org" }), _jsx("span", { className: "flex-1 min-w-0 truncate", children: principalLabel(s, knownMembers) }), canManage ? (_jsx(RoleSelect, { value: s.role, onChange: (r) => handleChangeRole(s, r), disabled: inFlight.has(keyOf(s)), plain: true, roleCopy: props.roleCopy, allowedRoles: props.allowedRoles })) : (_jsx("span", { className: "text-xs text-muted-foreground", children: roleMeta(s.role, props.roleCopy).label })), canManage ? (_jsx("button", { type: "button", "aria-label": "Remove", onClick: () => handleRemove(s), disabled: inFlight.has(keyOf(s)), className: BUTTON_GHOST_ICON, children: _jsx(IconTrash, { size: 14 }) })) : null] }, keyOf(s)))), !shares.length && !data?.ownerEmail ? (_jsx("li", { className: "px-1 py-1.5 text-sm text-muted-foreground", children: "No one has access yet." })) : null] })] })] }) })] }), _jsx("div", { className: "mb-2 text-sm font-semibold", children: generalAccessLabel }), _jsxs("div", { className: "mb-4 flex items-center gap-3", children: [_jsx("span", { "aria-hidden": true, className: "inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground", children: _jsx(meta.Icon, { size: 16, strokeWidth: 1.75 }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(VisibilitySelect, { value: visibility, onChange: handleVisibility, disabled: !canManage, visibilityCopy: props.visibilityCopy, allowPublic: policy.allowPublic }), _jsxs("div", { className: "mt-0.5 flex flex-wrap items-center gap-x-2 gap-y-1 text-xs text-muted-foreground", children: [_jsx("span", { className: "sr-only", children: meta.description }), visibility === "org" && props.hideInSearchControl ? (_jsx(AdvancedAccessPopover, { control: props.hideInSearchControl, canManage: canManage, onToggle: handleHideInSearch })) : null] })] })] }), shareError && !canManage ? (_jsx("div", { role: "alert", className: "mb-4 rounded-md border border-destructive/40 bg-destructive/10 px-3 py-2 text-xs text-destructive", children: shareError })) : null, props.accessNote ? (_jsx("div", { className: "mb-4 rounded-md border border-border bg-muted/35 p-3 text-xs text-muted-foreground", children: props.accessNote })) : null, showShareLinks && shareUrlPlacement === "bottom" ? shareLinks : null, props.shareFooterContent] }));
|
|
136
141
|
if (!hasTabs)
|
|
137
142
|
return sharePanel;
|
|
138
143
|
const tabs = [
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { ActionButton, Avatar as DesignSystemAvatar, Dialog as DesignSystemDialog, IconButton, Picker, Status, TextArea, TextField, } from "@agent-native/toolkit/design-system";
|
|
3
|
-
import {
|
|
3
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@agent-native/toolkit/ui";
|
|
4
|
+
import { IconCheck, IconCode, IconCopy, IconChevronDown, IconLink, IconLock, IconMail, IconTrash, IconUsersGroup, IconWorld, } from "@tabler/icons-react";
|
|
5
|
+
import { useState } from "react";
|
|
4
6
|
import { cn } from "../utils.js";
|
|
5
7
|
import { useShareDialogController, } from "./useShareDialogController.js";
|
|
6
8
|
const BUTTON_BASE = "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors active:!scale-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0";
|
|
@@ -35,7 +37,9 @@ export function ShareDialog({ open, onClose, resourceType, resourceId, resourceT
|
|
|
35
37
|
});
|
|
36
38
|
if (!open)
|
|
37
39
|
return null;
|
|
38
|
-
return (_jsxs(DesignSystemDialog, { open: controller.open, onOpenChange: controller.onOpenChange, title: _jsx("span", { className:
|
|
40
|
+
return (_jsxs(DesignSystemDialog, { open: controller.open, onOpenChange: controller.onOpenChange, title: _jsx("span", { className: cn(controller.tabsEnabled
|
|
41
|
+
? "sr-only"
|
|
42
|
+
: "truncate !text-base !leading-normal !tracking-normal !text-inherit"), title: controller.title, children: controller.title }), closeLabel: controller.labels.close, size: "large", className: "!top-4 !z-[2010] !block !max-h-none !w-[calc(100vw-2rem)] !max-w-lg !translate-y-0 !gap-0 !overflow-visible !rounded-xl !border-border !bg-popover !p-0 !text-popover-foreground !shadow-2xl sm:!top-1/2 sm:!-translate-y-1/2", "aria-label": controller.title, children: [!controller.tabsEnabled && controller.ownerLabel ? (_jsx("div", { className: "px-5 pt-0 pb-3", children: _jsx("div", { className: "truncate text-xs text-muted-foreground", children: controller.ownerLabel }) })) : null, controller.tabsEnabled ? (_jsx("div", { role: "tablist", "aria-label": controller.labels.shareOptions, className: "mx-5 mt-1 flex gap-1 border-b border-border", children: controller.tabs.map((tab) => {
|
|
39
43
|
const Icon = TAB_ICONS[tab.value];
|
|
40
44
|
return (_jsx(TabTrigger, { active: controller.activeTab === tab.value, onClick: () => controller.setActiveTab(tab.value), icon: _jsx(Icon, { size: 14, strokeWidth: 1.75 }), label: tab.label }, tab.value));
|
|
41
45
|
}) })) : null, _jsxs("div", { className: "px-5 py-4", children: [controller.tabsEnabled && controller.activeTab === "link" ? (_jsx(LinkTab, { controller: controller, extras: linkTabExtras })) : null, !controller.tabsEnabled || controller.activeTab === "invite" ? (_jsx(InviteTab, { controller: controller, showVisibility: !controller.tabsEnabled })) : null, controller.tabsEnabled && controller.activeTab === "embed"
|
|
@@ -49,18 +53,19 @@ function TabTrigger({ active, onClick, icon, label, }) {
|
|
|
49
53
|
}
|
|
50
54
|
function LinkTab({ controller, extras, }) {
|
|
51
55
|
const Icon = VIS_ICONS[controller.visibility.value];
|
|
52
|
-
return (_jsxs("div", { className: "space-y-
|
|
56
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsx(CopyField, { field: "share-link", label: controller.labels.shareLink, value: controller.shareUrl, controller: controller }), _jsxs("div", { children: [_jsx("div", { className: "mb-2 text-xs font-medium text-muted-foreground", children: controller.labels.generalAccess }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { "aria-hidden": true, className: "inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground", children: _jsx(Icon, { size: 16, strokeWidth: 1.75 }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(VisibilitySelect, { controller: controller }), _jsx("div", { className: "sr-only text-xs text-muted-foreground", children: controller.visibility.description })] })] })] }), extras] }));
|
|
53
57
|
}
|
|
54
58
|
function InviteTab({ controller, showVisibility, }) {
|
|
55
59
|
const Icon = VIS_ICONS[controller.visibility.value];
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
const [advancedOpen, setAdvancedOpen] = useState(false);
|
|
61
|
+
return (_jsxs("div", { className: "space-y-3", children: [_jsxs(Collapsible, { open: advancedOpen, onOpenChange: setAdvancedOpen, className: "overflow-hidden rounded-md border border-border", children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsxs("button", { type: "button", className: "flex min-h-10 w-full items-center justify-between gap-3 px-3 py-2 text-left text-sm font-medium transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset", children: [_jsx("span", { className: "truncate", children: controller.labels.peopleWithAccess }), _jsx(IconChevronDown, { "aria-hidden": true, size: 16, strokeWidth: 1.8, className: cn("shrink-0 text-muted-foreground transition-transform", advancedOpen && "rotate-180") })] }) }), _jsx(CollapsibleContent, { className: "border-t border-border px-3 py-3", children: _jsxs("div", { className: "space-y-4", children: [controller.canManage ? (_jsxs("div", { className: "space-y-2", children: [_jsxs("div", { className: "flex items-stretch gap-2", children: [_jsx("input", { type: "email", placeholder: controller.labels.addPeopleByEmail, value: controller.invite.email, onChange: (event) => controller.invite.setEmail(event.currentTarget.value), onKeyDown: (event) => {
|
|
62
|
+
if (event.key === "Enter")
|
|
63
|
+
controller.invite.submit();
|
|
64
|
+
}, autoComplete: "off", className: "h-9 min-w-0 flex-1 rounded-md border border-input bg-background px-3 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:ring-offset-background" }), _jsx(RoleSelect, { controller: controller })] }), controller.invite.showNotifyPeople ? (_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 text-xs text-muted-foreground", children: [_jsxs("label", { className: "inline-flex items-center gap-2", children: [_jsx("input", { type: "checkbox", checked: controller.invite.notifyPeople, onChange: (event) => controller.invite.setNotifyPeople(event.currentTarget.checked), className: "h-4 w-4 rounded border-input accent-primary" }), controller.labels.notifyPeople] }), controller.invite.notifyPeople ? (_jsx(ActionButton, { type: "button", emphasis: "ghost", "aria-expanded": controller.invite.messageOpen, onPress: () => controller.invite.setMessageOpen(!controller.invite.messageOpen), className: "!h-auto !rounded-sm !px-1 !py-0.5 text-xs font-medium !text-foreground underline decoration-border underline-offset-2 hover:!bg-accent hover:!text-accent-foreground active:!scale-100", children: controller.invite.messageOpen
|
|
65
|
+
? controller.labels.hideMessage
|
|
66
|
+
: controller.labels.addMessage })) : null] })) : null, controller.invite.showNotifyPeople &&
|
|
67
|
+
controller.invite.notifyPeople &&
|
|
68
|
+
controller.invite.messageOpen ? (_jsx(TextArea, { "aria-label": controller.labels.addMessage, placeholder: controller.labels.messagePlaceholder, value: controller.invite.message, onChange: (value) => controller.invite.setMessage(value), maxLength: 500, rows: 3, className: "min-h-20 resize-y text-sm" })) : null] })) : null, _jsxs("div", { children: [_jsx("div", { className: "mb-2 text-sm font-semibold", children: controller.labels.peopleWithAccess }), _jsxs("ul", { className: "m-0 flex list-none flex-col gap-1 p-0", children: [controller.people.map((person) => (_jsx(PersonRow, { person: person, canManage: controller.canManage, removeLabel: controller.labels.remove, onRemove: controller.removeShare }, person.key))), !controller.people.length ? (_jsx("li", { className: "px-1 py-1.5 text-sm text-muted-foreground", children: controller.labels.noAccess })) : null] })] })] }) })] }), showVisibility ? (_jsxs("div", { children: [_jsx("div", { className: "mb-2 text-sm font-semibold", children: controller.labels.generalAccess }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { "aria-hidden": true, className: "inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground", children: _jsx(Icon, { size: 16, strokeWidth: 1.75 }) }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsx(VisibilitySelect, { controller: controller }), _jsx("div", { className: "sr-only text-xs text-muted-foreground", children: controller.visibility.description })] })] })] })) : null] }));
|
|
64
69
|
}
|
|
65
70
|
function PersonRow({ person, canManage, removeLabel, onRemove, }) {
|
|
66
71
|
return (_jsxs("li", { className: "flex items-center gap-3 px-1 py-1.5 text-sm", children: [_jsx(DesignSystemAvatar, { name: person.label, fallback: person.principalType === "org" ? (_jsx(IconUsersGroup, { size: 14, strokeWidth: 1.75 })) : (person.avatarText), size: "compact", className: "inline-flex h-7 w-7 shrink-0 text-[11px] font-semibold" }), _jsx("span", { className: "flex-1 min-w-0 truncate", children: person.label }), _jsx(Status, { tone: "neutral", size: "compact", className: "border-0 bg-transparent px-0 text-xs text-muted-foreground", children: person.roleLabel }), canManage && person.share ? (_jsx(IconButton, { intent: "danger", emphasis: "ghost", size: "compact", icon: _jsx(IconTrash, { size: 14 }), label: removeLabel, "aria-label": removeLabel, onPress: () => onRemove(person.share), className: cn(BUTTON_GHOST_ICON, "[&_svg]:!size-auto") })) : null] }));
|
|
@@ -62,11 +62,11 @@ export declare const postAwareness: import("h3").EventHandlerWithFetch<import("h
|
|
|
62
62
|
error: string;
|
|
63
63
|
states?: undefined;
|
|
64
64
|
} | {
|
|
65
|
+
error?: undefined;
|
|
65
66
|
states: {
|
|
66
67
|
clientId: number;
|
|
67
68
|
state: string;
|
|
68
69
|
}[];
|
|
69
|
-
error?: undefined;
|
|
70
70
|
}>>;
|
|
71
71
|
/**
|
|
72
72
|
* GET /_agent-native/collab/:docId/users
|
|
@@ -77,9 +77,9 @@ export declare const getActiveUsers: import("h3").EventHandlerWithFetch<import("
|
|
|
77
77
|
error: string;
|
|
78
78
|
users?: undefined;
|
|
79
79
|
} | {
|
|
80
|
+
error?: undefined;
|
|
80
81
|
users: {
|
|
81
82
|
clientId: number;
|
|
82
83
|
lastSeen: number;
|
|
83
84
|
}[];
|
|
84
|
-
error?: undefined;
|
|
85
85
|
}>>;
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* Body: { json: any, fieldName?: string, type?: "map"|"array", requestSource?: string }
|
|
14
14
|
*/
|
|
15
15
|
export declare const postCollabJson: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
16
|
-
error: string;
|
|
17
16
|
ok?: undefined;
|
|
17
|
+
error: string;
|
|
18
18
|
} | {
|
|
19
19
|
error?: undefined;
|
|
20
20
|
ok: boolean;
|
|
@@ -48,8 +48,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
|
|
|
48
48
|
}>;
|
|
49
49
|
/** DELETE /_agent-native/resources/:id — delete a resource */
|
|
50
50
|
export declare function handleDeleteResource(event: any): Promise<{
|
|
51
|
-
ok?: undefined;
|
|
52
51
|
error: string;
|
|
52
|
+
ok?: undefined;
|
|
53
53
|
} | {
|
|
54
54
|
error?: undefined;
|
|
55
55
|
ok: boolean;
|
package/dist/server/auth.js
CHANGED
|
@@ -56,9 +56,9 @@ import { isValidWorkspaceAppIdFormat } from "../shared/workspace-app-id.js";
|
|
|
56
56
|
import { injectAnalyticsIntoHtml } from "./analytics.js";
|
|
57
57
|
import { getConfiguredAppBasePath } from "./app-base-path.js";
|
|
58
58
|
import { getAppProductionUrl } from "./app-url.js";
|
|
59
|
-
import { readAnalyticsAnonymousId, signupAttributionFromCookieHeader, } from "./attribution.js";
|
|
59
|
+
import { readAnalyticsAnonymousId, readFirstTouchAttribution, signupAttributionFromCookieHeader, } from "./attribution.js";
|
|
60
60
|
import { getAuthLoginMode } from "./auth-login-mode.js";
|
|
61
|
-
import { createBetterAuthSessionForEmail, ensureGoogleAuthIdentity, getBetterAuth, getBetterAuthSync, isDeployPreview, } from "./better-auth-instance.js";
|
|
61
|
+
import { createBetterAuthSessionForEmail, ensureGoogleAuthIdentity, getAuthSecret, getBetterAuth, getBetterAuthSync, isDeployPreview, } from "./better-auth-instance.js";
|
|
62
62
|
import { BUILDER_CONNECT_OWNER_COOKIE, BUILDER_CONNECT_PARAM, BUILDER_RELAY_PATH, BUILDER_RELAY_STATE_PARAM, BUILDER_STATE_PARAM, verifyBuilderCallbackStateAndGetOwner, verifyBuilderConnectTokenAndGetOwner, verifyBuilderPreviewRelayStateForCallback, } from "./builder-browser.js";
|
|
63
63
|
import { resolveAuthCookieNamespace } from "./cookie-namespace.js";
|
|
64
64
|
import { getAllowedCorsOrigin, readCorsAllowedOrigins, } from "./cors-origins.js";
|
|
@@ -67,6 +67,7 @@ import { resolveGoogleSignInCredentials } from "./google-oauth-credentials.js";
|
|
|
67
67
|
import { isElectron as isElectronRequest, getAppBasePath, getAppUrl, getOrigin, encodeOAuthState, decodeOAuthState, createOAuthSession, oauthCallbackResponse, oauthDesktopExchangePage, oauthErrorPage, resolveOAuthRedirectUri, isAllowedOAuthRedirectUri, } from "./google-oauth.js";
|
|
68
68
|
import { isCanonicalAgentNativeAppRequest, isDesktopSsoCanaryUserAgent, isIdentitySsoEnabled, } from "./identity-sso-store.js";
|
|
69
69
|
import { ensureCanonicalUserForLegacySession } from "./legacy-auth-migration.js";
|
|
70
|
+
import { encodeMagicLinkSignupAttribution, MAGIC_LINK_ATTRIBUTION_PARAM, } from "./magic-link-attribution.js";
|
|
70
71
|
import { safeOAuthReturnUrl } from "./oauth-return-url.js";
|
|
71
72
|
import { getOnboardingHtml, getResetPasswordHtml, } from "./onboarding-html.js";
|
|
72
73
|
import { captureAuthError } from "./sentry.js";
|
|
@@ -3333,12 +3334,25 @@ async function mountBetterAuthRoutes(app, options) {
|
|
|
3333
3334
|
callbackPath = withDesktopMagicLinkFlow(callbackPath, desktopFlow);
|
|
3334
3335
|
}
|
|
3335
3336
|
const callbackURL = betterAuthCallbackURL(callbackPath, true, event);
|
|
3336
|
-
const
|
|
3337
|
+
const cookieHeader = getHeader(event, "cookie") ?? null;
|
|
3338
|
+
const signupAttribution = signupAttributionFromCookieHeader(cookieHeader);
|
|
3339
|
+
const signupAnonymousId = readAnalyticsAnonymousId(cookieHeader);
|
|
3340
|
+
const hasSignupAttribution = !!readFirstTouchAttribution(cookieHeader) || !!signupAnonymousId;
|
|
3341
|
+
const attributionToken = hasSignupAttribution
|
|
3342
|
+
? encodeMagicLinkSignupAttribution({
|
|
3343
|
+
attribution: signupAttribution,
|
|
3344
|
+
anonymousId: signupAnonymousId,
|
|
3345
|
+
}, getAuthSecret())
|
|
3346
|
+
: undefined;
|
|
3347
|
+
const newUserCallbackUrl = new URL(`${getAppBasePath()}/_agent-native/auth/magic-link/new-user?return=${encodeURIComponent(callbackPath)}`, getOrigin(event));
|
|
3348
|
+
if (attributionToken) {
|
|
3349
|
+
newUserCallbackUrl.searchParams.set(MAGIC_LINK_ATTRIBUTION_PARAM, attributionToken);
|
|
3350
|
+
}
|
|
3337
3351
|
await auth.api.signInMagicLink({
|
|
3338
3352
|
body: {
|
|
3339
3353
|
email,
|
|
3340
3354
|
callbackURL,
|
|
3341
|
-
newUserCallbackURL: betterAuthCallbackURL(
|
|
3355
|
+
newUserCallbackURL: betterAuthCallbackURL(`${newUserCallbackUrl.pathname}${newUserCallbackUrl.search}`, true, event),
|
|
3342
3356
|
},
|
|
3343
3357
|
headers: event.headers,
|
|
3344
3358
|
});
|
|
@@ -32,6 +32,7 @@ import { getWorkspaceA2ADerivedSecret } from "./derived-secret.js";
|
|
|
32
32
|
import { renderMagicLinkEmail, renderResetPasswordEmail, renderVerifySignupEmail, } from "./email-templates.js";
|
|
33
33
|
import { getEmailReadiness, sendEmail } from "./email.js";
|
|
34
34
|
import { resolveGoogleSignInCredentials } from "./google-oauth-credentials.js";
|
|
35
|
+
import { readMagicLinkSignupAttribution } from "./magic-link-attribution.js";
|
|
35
36
|
export { getAuthLoginMode, resolveAuthLoginMode, resolveAuthLoginModeFromReadiness, } from "./auth-login-mode.js";
|
|
36
37
|
async function flushSignupTracking() {
|
|
37
38
|
try {
|
|
@@ -982,8 +983,14 @@ async function createBetterAuthInstance(config) {
|
|
|
982
983
|
const cookieHeader = context?.headers?.get("cookie") ??
|
|
983
984
|
context?.request?.headers?.get("cookie") ??
|
|
984
985
|
null;
|
|
986
|
+
const magicLinkAttribution = context?.request?.url?.includes("newUserCallbackURL")
|
|
987
|
+
? readMagicLinkSignupAttribution(context.request.url, getAuthSecret())
|
|
988
|
+
: undefined;
|
|
985
989
|
attribution = signupAttributionFromCookieHeader(cookieHeader);
|
|
986
|
-
|
|
990
|
+
attribution = magicLinkAttribution?.attribution ?? attribution;
|
|
991
|
+
anonymousId =
|
|
992
|
+
magicLinkAttribution?.anonymousId ??
|
|
993
|
+
readAnalyticsAnonymousId(cookieHeader);
|
|
987
994
|
}
|
|
988
995
|
catch (err) {
|
|
989
996
|
console.error("[auth] failed to derive signup attribution", err);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Query parameter carried by Better Auth's new-user callback URL. */
|
|
2
|
+
export declare const MAGIC_LINK_ATTRIBUTION_PARAM = "signup_attribution";
|
|
3
|
+
export interface MagicLinkSignupAttribution {
|
|
4
|
+
attribution?: Record<string, string>;
|
|
5
|
+
anonymousId?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Sign the attribution captured when a magic link is requested. The token is
|
|
9
|
+
* analytics context only; it never authorizes the user or changes the return
|
|
10
|
+
* URL. The Better Auth secret keeps the callback from accepting forged data.
|
|
11
|
+
*/
|
|
12
|
+
export declare function encodeMagicLinkSignupAttribution(value: MagicLinkSignupAttribution, secret: string, now?: number): string | undefined;
|
|
13
|
+
/** Verify and decode a token previously produced by
|
|
14
|
+
* {@link encodeMagicLinkSignupAttribution}. */
|
|
15
|
+
export declare function decodeMagicLinkSignupAttribution(token: string | null | undefined, secret: string, now?: number): MagicLinkSignupAttribution | null | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Read the signed handoff from Better Auth's magic-link verification request.
|
|
18
|
+
* Better Auth copies `newUserCallbackURL` into the verification URL, so this
|
|
19
|
+
* remains available even when the link is opened on another device.
|
|
20
|
+
*/
|
|
21
|
+
export declare function readMagicLinkSignupAttribution(requestUrl: string | undefined, secret: string, now?: number): MagicLinkSignupAttribution | null | undefined;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { normalizeAnalyticsAnonymousId } from "../shared/analytics-anonymous-id.js";
|
|
3
|
+
/** Query parameter carried by Better Auth's new-user callback URL. */
|
|
4
|
+
export const MAGIC_LINK_ATTRIBUTION_PARAM = "signup_attribution";
|
|
5
|
+
/** Better Auth's magic-link token is valid for five minutes; keep this handoff
|
|
6
|
+
* slightly longer so clock skew does not discard a still-valid link. */
|
|
7
|
+
const MAGIC_LINK_ATTRIBUTION_TTL_SECONDS = 10 * 60;
|
|
8
|
+
const MAX_ATTRIBUTION_FIELDS = 32;
|
|
9
|
+
const MAX_ATTRIBUTION_VALUE_LENGTH = 200;
|
|
10
|
+
function sanitizeAttribution(value) {
|
|
11
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
const out = {};
|
|
15
|
+
for (const [key, raw] of Object.entries(value).slice(0, MAX_ATTRIBUTION_FIELDS)) {
|
|
16
|
+
if (!/^[a-z][a-z0-9_]{0,63}$/.test(key) ||
|
|
17
|
+
typeof raw !== "string" ||
|
|
18
|
+
raw.length === 0) {
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
out[key] = raw.slice(0, MAX_ATTRIBUTION_VALUE_LENGTH);
|
|
22
|
+
}
|
|
23
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
24
|
+
}
|
|
25
|
+
function encodePayload(payload) {
|
|
26
|
+
return Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Sign the attribution captured when a magic link is requested. The token is
|
|
30
|
+
* analytics context only; it never authorizes the user or changes the return
|
|
31
|
+
* URL. The Better Auth secret keeps the callback from accepting forged data.
|
|
32
|
+
*/
|
|
33
|
+
export function encodeMagicLinkSignupAttribution(value, secret, now = Date.now()) {
|
|
34
|
+
if (!secret)
|
|
35
|
+
return undefined;
|
|
36
|
+
const attribution = sanitizeAttribution(value.attribution);
|
|
37
|
+
const anonymousId = normalizeAnalyticsAnonymousId(value.anonymousId);
|
|
38
|
+
if (!attribution && !anonymousId)
|
|
39
|
+
return undefined;
|
|
40
|
+
const payload = {
|
|
41
|
+
exp: Math.floor(now / 1000) + MAGIC_LINK_ATTRIBUTION_TTL_SECONDS,
|
|
42
|
+
...(attribution ? { attribution } : {}),
|
|
43
|
+
...(anonymousId ? { anonymousId } : {}),
|
|
44
|
+
};
|
|
45
|
+
const data = encodePayload(payload);
|
|
46
|
+
const signature = crypto
|
|
47
|
+
.createHmac("sha256", secret)
|
|
48
|
+
.update(data)
|
|
49
|
+
.digest("base64url");
|
|
50
|
+
return `${data}.${signature}`;
|
|
51
|
+
}
|
|
52
|
+
/** Verify and decode a token previously produced by
|
|
53
|
+
* {@link encodeMagicLinkSignupAttribution}. */
|
|
54
|
+
export function decodeMagicLinkSignupAttribution(token, secret, now = Date.now()) {
|
|
55
|
+
if (token === null || token === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
if (!token || !secret)
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
const dotIndex = token.lastIndexOf(".");
|
|
61
|
+
if (dotIndex <= 0 || dotIndex === token.length - 1)
|
|
62
|
+
return null;
|
|
63
|
+
const data = token.slice(0, dotIndex);
|
|
64
|
+
const signature = token.slice(dotIndex + 1);
|
|
65
|
+
const expected = crypto
|
|
66
|
+
.createHmac("sha256", secret)
|
|
67
|
+
.update(data)
|
|
68
|
+
.digest("base64url");
|
|
69
|
+
if (signature.length !== expected.length ||
|
|
70
|
+
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const payload = JSON.parse(Buffer.from(data, "base64url").toString("utf8"));
|
|
74
|
+
if (typeof payload.exp !== "number" ||
|
|
75
|
+
payload.exp < Math.floor(now / 1000)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const attribution = sanitizeAttribution(payload.attribution);
|
|
79
|
+
const anonymousId = normalizeAnalyticsAnonymousId(payload.anonymousId);
|
|
80
|
+
if (!attribution && !anonymousId)
|
|
81
|
+
return undefined;
|
|
82
|
+
return {
|
|
83
|
+
...(attribution ? { attribution } : {}),
|
|
84
|
+
...(anonymousId ? { anonymousId } : {}),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// coercion-ok: malformed signed input is distinct from absent context.
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Read the signed handoff from Better Auth's magic-link verification request.
|
|
94
|
+
* Better Auth copies `newUserCallbackURL` into the verification URL, so this
|
|
95
|
+
* remains available even when the link is opened on another device.
|
|
96
|
+
*/
|
|
97
|
+
export function readMagicLinkSignupAttribution(requestUrl, secret, now = Date.now()) {
|
|
98
|
+
if (!requestUrl)
|
|
99
|
+
return undefined;
|
|
100
|
+
try {
|
|
101
|
+
const verificationUrl = new URL(requestUrl, "http://localhost");
|
|
102
|
+
if (!verificationUrl.pathname.endsWith("/magic-link/verify")) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const callback = verificationUrl.searchParams.get("newUserCallbackURL");
|
|
106
|
+
if (!callback)
|
|
107
|
+
return undefined;
|
|
108
|
+
const callbackUrl = new URL(callback, verificationUrl.origin);
|
|
109
|
+
return decodeMagicLinkSignupAttribution(callbackUrl.searchParams.get(MAGIC_LINK_ATTRIBUTION_PARAM), secret, now);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// coercion-ok: malformed request input is distinct from absent context.
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -26,8 +26,8 @@ export declare function createRealtimeTokenHandler(): import("h3").EventHandlerW
|
|
|
26
26
|
expiresAt?: undefined;
|
|
27
27
|
ttlSeconds?: undefined;
|
|
28
28
|
} | {
|
|
29
|
+
error?: undefined;
|
|
29
30
|
token: string;
|
|
30
31
|
expiresAt: string;
|
|
31
32
|
ttlSeconds: number;
|
|
32
|
-
error?: undefined;
|
|
33
33
|
}>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/core",
|
|
3
|
-
"version": "0.153.
|
|
3
|
+
"version": "0.153.2",
|
|
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": {
|