@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
|
@@ -35,6 +35,7 @@ declare var __DESIGN_CANVAS_CONTENT_OFFSET_X__: number;
|
|
|
35
35
|
declare var __DESIGN_CANVAS_CONTENT_OFFSET_Y__: number;
|
|
36
36
|
declare var __RUNTIME_LAYER_SNAPSHOT_ENABLED__: boolean;
|
|
37
37
|
declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
38
|
+
declare var __SELECTED_LAYER_DRAG_PRIORITY__: boolean;
|
|
38
39
|
|
|
39
40
|
(function () {
|
|
40
41
|
// Idempotency guard: replace-document-content / srcdoc rebuilds can end up
|
|
@@ -78,6 +79,16 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
|
78
79
|
return false;
|
|
79
80
|
}
|
|
80
81
|
})();
|
|
82
|
+
// Selected-layer drag priority: when on, a pointerdown inside the selection
|
|
83
|
+
// box keeps the selected element as the drag target even when an overlapping
|
|
84
|
+
// non-descendant sibling wins the hit test.
|
|
85
|
+
var selectedLayerDragPriorityEnabled = (function () {
|
|
86
|
+
try {
|
|
87
|
+
return !!__SELECTED_LAYER_DRAG_PRIORITY__;
|
|
88
|
+
} catch (_e) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
81
92
|
var scaleToolEnabled = false;
|
|
82
93
|
|
|
83
94
|
// ── Drag-and-drop debug logging ────────────────────────────────────
|
|
@@ -2248,6 +2259,9 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
|
2248
2259
|
}
|
|
2249
2260
|
|
|
2250
2261
|
var selectedEl: Element | null = null;
|
|
2262
|
+
// When true, selection chrome stays hidden through async reflows so a
|
|
2263
|
+
// keyboard-nudge burst does not flicker; selection itself is unchanged.
|
|
2264
|
+
var selectionChromeHidden = false;
|
|
2251
2265
|
var hoveredEl: Element | null = null;
|
|
2252
2266
|
var highlightOverlayStyle: "default" | "soft" = "default";
|
|
2253
2267
|
type NodeHtmlPreviewSession = {
|
|
@@ -4080,7 +4094,11 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
|
4080
4094
|
hideSelectionOverlay();
|
|
4081
4095
|
}
|
|
4082
4096
|
} else if (selectedEl) {
|
|
4083
|
-
|
|
4097
|
+
if (selectionChromeHidden) {
|
|
4098
|
+
hideSelectionOverlay();
|
|
4099
|
+
} else {
|
|
4100
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
4101
|
+
}
|
|
4084
4102
|
} else {
|
|
4085
4103
|
hideParentAutoLayoutOverlay();
|
|
4086
4104
|
}
|
|
@@ -5103,9 +5121,17 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
|
5103
5121
|
document.addEventListener(events.up, onUp, true);
|
|
5104
5122
|
}
|
|
5105
5123
|
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5124
|
+
// Returns the full z-stack of selectable layers under a point (topmost
|
|
5125
|
+
// first), each element index-aligned with a { key, label, info } descriptor.
|
|
5126
|
+
// Overlays are briefly made pointer-transparent so elementsFromPoint sees
|
|
5127
|
+
// through the editor chrome.
|
|
5128
|
+
function collectLayerHitCandidates(
|
|
5129
|
+
clientX: number,
|
|
5130
|
+
clientY: number,
|
|
5131
|
+
): {
|
|
5132
|
+
elements: Element[];
|
|
5133
|
+
layerCandidates: Array<{ key: string; label: string; info: unknown }>;
|
|
5134
|
+
} {
|
|
5109
5135
|
var shieldPointerEvents = shieldOverlay.style.pointerEvents;
|
|
5110
5136
|
var selectionPointerEvents = selectionOverlay.style.pointerEvents;
|
|
5111
5137
|
var highlightPointerEvents = highlightOverlay.style.pointerEvents;
|
|
@@ -5113,13 +5139,13 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
|
5113
5139
|
selectionOverlay.style.pointerEvents = "none";
|
|
5114
5140
|
highlightOverlay.style.pointerEvents = "none";
|
|
5115
5141
|
var pointTargets = document.elementsFromPoint
|
|
5116
|
-
? document.elementsFromPoint(
|
|
5117
|
-
: [document.elementFromPoint(
|
|
5142
|
+
? document.elementsFromPoint(clientX, clientY)
|
|
5143
|
+
: [document.elementFromPoint(clientX, clientY)];
|
|
5118
5144
|
shieldOverlay.style.pointerEvents = shieldPointerEvents;
|
|
5119
5145
|
selectionOverlay.style.pointerEvents = selectionPointerEvents;
|
|
5120
5146
|
highlightOverlay.style.pointerEvents = highlightPointerEvents;
|
|
5121
5147
|
|
|
5122
|
-
var
|
|
5148
|
+
var elements: Element[] = [];
|
|
5123
5149
|
var layerCandidates: Array<{
|
|
5124
5150
|
key: string;
|
|
5125
5151
|
label: string;
|
|
@@ -5135,11 +5161,11 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
|
5135
5161
|
isOverlayElement(candidate) ||
|
|
5136
5162
|
isLayerInteractionBlocked(candidate) ||
|
|
5137
5163
|
isTemplateCloneElement(candidate) ||
|
|
5138
|
-
|
|
5164
|
+
elements.indexOf(candidate) !== -1
|
|
5139
5165
|
) {
|
|
5140
5166
|
return;
|
|
5141
5167
|
}
|
|
5142
|
-
|
|
5168
|
+
elements.push(candidate);
|
|
5143
5169
|
var candidateInfo = getElementInfo(candidate);
|
|
5144
5170
|
var explicitLabel =
|
|
5145
5171
|
(candidate.getAttribute &&
|
|
@@ -5162,6 +5188,39 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
|
5162
5188
|
info: candidateInfo,
|
|
5163
5189
|
});
|
|
5164
5190
|
});
|
|
5191
|
+
return { elements: elements, layerCandidates: layerCandidates };
|
|
5192
|
+
}
|
|
5193
|
+
|
|
5194
|
+
// Given a point and the current selection, return the next layer BELOW it in
|
|
5195
|
+
// the hit stack (wrapping at the bottom), or null when the selection is not
|
|
5196
|
+
// in the stack or the next layer is blocked. Backs Cmd/Ctrl+click deep-select.
|
|
5197
|
+
function stackCycleTarget(
|
|
5198
|
+
clientX: number,
|
|
5199
|
+
clientY: number,
|
|
5200
|
+
currentEl: Element | null,
|
|
5201
|
+
): Element | null {
|
|
5202
|
+
if (!currentEl) return null;
|
|
5203
|
+
var stack = collectLayerHitCandidates(clientX, clientY);
|
|
5204
|
+
var currentIdx = stack.elements.indexOf(currentEl);
|
|
5205
|
+
if (currentIdx === -1) return null;
|
|
5206
|
+
var keys = stack.layerCandidates.map(function (candidate) {
|
|
5207
|
+
return candidate.key;
|
|
5208
|
+
});
|
|
5209
|
+
var nextKey = nextStackCandidate(
|
|
5210
|
+
keys,
|
|
5211
|
+
stack.layerCandidates[currentIdx].key,
|
|
5212
|
+
);
|
|
5213
|
+
if (nextKey === null) return null;
|
|
5214
|
+
var nextEl = stack.elements[keys.indexOf(nextKey)];
|
|
5215
|
+
return nextEl && !isLayerInteractionBlocked(nextEl) ? nextEl : null;
|
|
5216
|
+
}
|
|
5217
|
+
|
|
5218
|
+
function openContextMenuAtEvent(e) {
|
|
5219
|
+
stopNativeInteraction(e);
|
|
5220
|
+
blurActiveTextEditor();
|
|
5221
|
+
var collected = collectLayerHitCandidates(e.clientX, e.clientY);
|
|
5222
|
+
var candidateElements = collected.elements;
|
|
5223
|
+
var layerCandidates = collected.layerCandidates;
|
|
5165
5224
|
|
|
5166
5225
|
var target = candidateElements[0] || null;
|
|
5167
5226
|
var info = null;
|
|
@@ -8227,10 +8286,9 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
|
8227
8286
|
// Minimal, dependency-free port of the overview canvas's edge/center snap
|
|
8228
8287
|
// routine (shared/canvas-math.ts computeMoveSnap) for in-iframe element
|
|
8229
8288
|
// dragging. The bridge's pointer coordinates and getBoundingClientRect()
|
|
8230
|
-
// values are
|
|
8231
|
-
//
|
|
8232
|
-
//
|
|
8233
|
-
// its own camera zoom — no extra scale correction is needed here.
|
|
8289
|
+
// values are iframe-local content px, so SNAP_THRESHOLD_PX is a screen-space
|
|
8290
|
+
// base converted to content px at snap time via chromeLineScale (1/zoom) to
|
|
8291
|
+
// keep the snap tolerance constant on screen at any zoom.
|
|
8234
8292
|
var SNAP_THRESHOLD_PX = 6;
|
|
8235
8293
|
var SNAP_CANDIDATE_CAP = 200;
|
|
8236
8294
|
|
|
@@ -9321,7 +9379,8 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
|
9321
9379
|
height: dragElStartHeight,
|
|
9322
9380
|
},
|
|
9323
9381
|
snapCandidateRects,
|
|
9324
|
-
|
|
9382
|
+
// Convert the screen-space base to content px (1/zoom).
|
|
9383
|
+
SNAP_THRESHOLD_PX * chromeLineScale(),
|
|
9325
9384
|
)
|
|
9326
9385
|
: { dx: 0, dy: 0, guideV: null, guideH: null };
|
|
9327
9386
|
nextLeft += snapResult.dx;
|
|
@@ -9970,6 +10029,53 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
|
9970
10029
|
pendingShieldDrag = null;
|
|
9971
10030
|
}
|
|
9972
10031
|
|
|
10032
|
+
// Decides the drag target for a pointerdown. Descendant hits keep the
|
|
10033
|
+
// selected element; with preferSelected on, a point inside the selection box
|
|
10034
|
+
// also keeps it over an overlapping non-descendant sibling. Falls through to
|
|
10035
|
+
// hitEl when the selection is detached or zero-area. Pure and self-contained
|
|
10036
|
+
// so the snap test can brace-extract and evaluate it in isolation.
|
|
10037
|
+
function dragTargetForPointerDown(args) {
|
|
10038
|
+
var selectedEl = args.selectedEl;
|
|
10039
|
+
var hitEl = args.hitEl;
|
|
10040
|
+
var hitRaw = args.hitRaw || hitEl;
|
|
10041
|
+
var selectedAlive = !!args.selectedAlive;
|
|
10042
|
+
if (
|
|
10043
|
+
selectedEl &&
|
|
10044
|
+
selectedAlive &&
|
|
10045
|
+
selectedEl.contains &&
|
|
10046
|
+
selectedEl.contains(hitRaw)
|
|
10047
|
+
) {
|
|
10048
|
+
return selectedEl;
|
|
10049
|
+
}
|
|
10050
|
+
if (args.preferSelected && selectedEl && selectedAlive) {
|
|
10051
|
+
var r = args.selectedRect;
|
|
10052
|
+
var p = args.point;
|
|
10053
|
+
if (
|
|
10054
|
+
r &&
|
|
10055
|
+
p &&
|
|
10056
|
+
r.width > 0 &&
|
|
10057
|
+
r.height > 0 &&
|
|
10058
|
+
p.x >= r.left &&
|
|
10059
|
+
p.x <= r.right &&
|
|
10060
|
+
p.y >= r.top &&
|
|
10061
|
+
p.y <= r.bottom
|
|
10062
|
+
) {
|
|
10063
|
+
return selectedEl;
|
|
10064
|
+
}
|
|
10065
|
+
}
|
|
10066
|
+
return hitEl;
|
|
10067
|
+
}
|
|
10068
|
+
|
|
10069
|
+
// Given the hit-stack candidate keys (topmost first) and the current
|
|
10070
|
+
// selection key, returns the next key below it, wrapping to the top. Returns
|
|
10071
|
+
// null when the selection is not in the stack. Pure, for the snap test.
|
|
10072
|
+
function nextStackCandidate(candidateKeys, currentKey) {
|
|
10073
|
+
if (!candidateKeys || candidateKeys.length === 0) return null;
|
|
10074
|
+
var idx = candidateKeys.indexOf(currentKey);
|
|
10075
|
+
if (idx === -1) return null;
|
|
10076
|
+
return candidateKeys[(idx + 1) % candidateKeys.length];
|
|
10077
|
+
}
|
|
10078
|
+
|
|
9973
10079
|
function beginPotentialShieldDrag(e) {
|
|
9974
10080
|
stopNativeInteraction(e);
|
|
9975
10081
|
if (e.button !== 0) return;
|
|
@@ -9988,12 +10094,21 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
|
9988
10094
|
beginMarqueeSelection(e);
|
|
9989
10095
|
return;
|
|
9990
10096
|
}
|
|
9991
|
-
var
|
|
9992
|
-
selectedEl &&
|
|
9993
|
-
|
|
9994
|
-
selectedEl.
|
|
9995
|
-
? selectedEl
|
|
9996
|
-
:
|
|
10097
|
+
var selectedAlive =
|
|
10098
|
+
!!selectedEl && document.documentElement.contains(selectedEl);
|
|
10099
|
+
var selectedRect =
|
|
10100
|
+
selectedAlive && selectedEl.getBoundingClientRect
|
|
10101
|
+
? selectedEl.getBoundingClientRect()
|
|
10102
|
+
: null;
|
|
10103
|
+
var dragTarget = dragTargetForPointerDown({
|
|
10104
|
+
selectedEl: selectedEl,
|
|
10105
|
+
selectedAlive: selectedAlive,
|
|
10106
|
+
selectedRect: selectedRect,
|
|
10107
|
+
hitEl: hitTarget,
|
|
10108
|
+
hitRaw: hit,
|
|
10109
|
+
point: { x: e.clientX, y: e.clientY },
|
|
10110
|
+
preferSelected: selectedLayerDragPriorityEnabled,
|
|
10111
|
+
});
|
|
9997
10112
|
var clickTarget = hitTarget;
|
|
9998
10113
|
if (
|
|
9999
10114
|
!dragTarget ||
|
|
@@ -10073,7 +10188,21 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
|
10073
10188
|
postCrossScreenDrag("cancel");
|
|
10074
10189
|
}
|
|
10075
10190
|
if (ev) stopNativeInteraction(ev);
|
|
10076
|
-
|
|
10191
|
+
// Cmd/Ctrl+click (no Shift) deep-selects the next layer below the current
|
|
10192
|
+
// selection in the z-stack under the pointer, wrapping at the bottom.
|
|
10193
|
+
// Runs here (not in selectElementAtEvent) because a shield click resolves
|
|
10194
|
+
// selection in this onUp and then suppresses the click handler. Selected
|
|
10195
|
+
// plain (no ev) so it replaces the selection rather than adding to it;
|
|
10196
|
+
// Shift-click stays additive via the normal path below.
|
|
10197
|
+
var cycledEl =
|
|
10198
|
+
!readOnly && (e.metaKey || e.ctrlKey) && !e.shiftKey
|
|
10199
|
+
? stackCycleTarget(e.clientX, e.clientY, selectedEl)
|
|
10200
|
+
: null;
|
|
10201
|
+
if (cycledEl) {
|
|
10202
|
+
selectTarget(cycledEl);
|
|
10203
|
+
} else {
|
|
10204
|
+
selectTarget(clickTarget || dragTarget, ev);
|
|
10205
|
+
}
|
|
10077
10206
|
suppressNextShieldClickBriefly();
|
|
10078
10207
|
}
|
|
10079
10208
|
clearPendingShieldDrag();
|
|
@@ -11599,6 +11728,17 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
|
11599
11728
|
);
|
|
11600
11729
|
return;
|
|
11601
11730
|
}
|
|
11731
|
+
if (e.data.type === "set-selection-chrome-hidden") {
|
|
11732
|
+
selectionChromeHidden = !!e.data.hidden;
|
|
11733
|
+
if (selectionChromeHidden) {
|
|
11734
|
+
hideSelectionOverlay();
|
|
11735
|
+
} else if (selectedEl) {
|
|
11736
|
+
positionOverlay(selectionOverlay, selectedEl);
|
|
11737
|
+
updateParentAutoLayoutOverlay(selectedEl);
|
|
11738
|
+
refreshOverlays();
|
|
11739
|
+
}
|
|
11740
|
+
return;
|
|
11741
|
+
}
|
|
11602
11742
|
if (e.data.type === "select-element") {
|
|
11603
11743
|
var candidates: string[] = [];
|
|
11604
11744
|
if (Array.isArray(e.data.selectorCandidates)) {
|
|
@@ -11650,7 +11790,14 @@ declare var __LIVE_REFLOW_ENABLED__: boolean;
|
|
|
11650
11790
|
hoveredSpacingHandleKey = "";
|
|
11651
11791
|
}
|
|
11652
11792
|
selectedEl = target;
|
|
11653
|
-
|
|
11793
|
+
// Respect a nudge-burst chrome-hide: the host re-sends select-element on
|
|
11794
|
+
// every poll tick, and repeated nudges don't resend hidden:true (its ref
|
|
11795
|
+
// stays set), so a replay must not re-show the overlay on its own.
|
|
11796
|
+
if (selectionChromeHidden) {
|
|
11797
|
+
hideSelectionOverlay();
|
|
11798
|
+
} else {
|
|
11799
|
+
positionOverlay(selectionOverlay, target);
|
|
11800
|
+
}
|
|
11654
11801
|
if (hoveredEl === selectedEl) highlightOverlay.style.display = "none";
|
|
11655
11802
|
// A host-driven selection (e.g. picking a layer in the Layers panel)
|
|
11656
11803
|
// only ever moved the overlay above — it never sent the rich
|
package/dist/collab/routes.d.ts
CHANGED
|
@@ -41,9 +41,9 @@ export declare const postCollabUpdate: import("h3").EventHandlerWithFetch<import
|
|
|
41
41
|
* Body: { text: string, fieldName?: string, requestSource?: string }
|
|
42
42
|
*/
|
|
43
43
|
export declare const postCollabText: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
44
|
-
text?: undefined;
|
|
45
44
|
ok?: undefined;
|
|
46
45
|
error: string;
|
|
46
|
+
text?: undefined;
|
|
47
47
|
} | {
|
|
48
48
|
error?: undefined;
|
|
49
49
|
ok: boolean;
|
|
@@ -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;
|
|
@@ -17,12 +17,12 @@ declare const _default: import("../../action.js").ActionDefinition<{
|
|
|
17
17
|
id?: undefined;
|
|
18
18
|
provider?: undefined;
|
|
19
19
|
} | {
|
|
20
|
+
error?: undefined;
|
|
20
21
|
configured?: undefined;
|
|
21
22
|
connectPath?: undefined;
|
|
22
23
|
url: string;
|
|
23
24
|
id: string;
|
|
24
25
|
provider: string;
|
|
25
|
-
error?: undefined;
|
|
26
26
|
}>;
|
|
27
27
|
export default _default;
|
|
28
28
|
//# sourceMappingURL=upload-image.d.ts.map
|
|
@@ -311,9 +311,8 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
311
311
|
notes?: string | undefined;
|
|
312
312
|
scope?: "org" | "user" | undefined;
|
|
313
313
|
}, {
|
|
314
|
-
message?: undefined;
|
|
315
|
-
id?: undefined;
|
|
316
314
|
deleted?: undefined;
|
|
315
|
+
id?: undefined;
|
|
317
316
|
found?: undefined;
|
|
318
317
|
providers: {
|
|
319
318
|
id: string;
|
|
@@ -328,28 +327,28 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
328
327
|
provider?: undefined;
|
|
329
328
|
registered?: undefined;
|
|
330
329
|
label?: undefined;
|
|
331
|
-
} | {
|
|
332
330
|
message?: undefined;
|
|
331
|
+
} | {
|
|
332
|
+
deleted?: undefined;
|
|
333
333
|
count?: undefined;
|
|
334
334
|
id?: undefined;
|
|
335
|
-
deleted?: undefined;
|
|
336
335
|
providers?: undefined;
|
|
337
336
|
found: boolean;
|
|
338
337
|
provider: import("../custom-registry.js").CustomProviderConfig;
|
|
339
338
|
registered?: undefined;
|
|
340
339
|
label?: undefined;
|
|
341
|
-
} | {
|
|
342
340
|
message?: undefined;
|
|
343
|
-
|
|
341
|
+
} | {
|
|
344
342
|
deleted?: undefined;
|
|
343
|
+
count?: undefined;
|
|
345
344
|
providers?: undefined;
|
|
346
345
|
provider?: undefined;
|
|
347
346
|
found: boolean;
|
|
348
347
|
id: string;
|
|
349
348
|
registered?: undefined;
|
|
350
349
|
label?: undefined;
|
|
351
|
-
} | {
|
|
352
350
|
message?: undefined;
|
|
351
|
+
} | {
|
|
353
352
|
count?: undefined;
|
|
354
353
|
found?: undefined;
|
|
355
354
|
providers?: undefined;
|
|
@@ -358,9 +357,10 @@ export declare function createProviderApiActions(runtime: Pick<ProviderApiRuntim
|
|
|
358
357
|
id: string;
|
|
359
358
|
registered?: undefined;
|
|
360
359
|
label?: undefined;
|
|
360
|
+
message?: undefined;
|
|
361
361
|
} | {
|
|
362
|
-
count?: undefined;
|
|
363
362
|
deleted?: undefined;
|
|
363
|
+
count?: undefined;
|
|
364
364
|
found?: undefined;
|
|
365
365
|
providers?: undefined;
|
|
366
366
|
provider?: undefined;
|
package/dist/secrets/routes.d.ts
CHANGED
|
@@ -34,37 +34,37 @@ export declare function createListSecretsHandler(): import("h3").EventHandlerWit
|
|
|
34
34
|
/** POST /_agent-native/secrets/:key — write a secret. */
|
|
35
35
|
export declare function createWriteSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
36
36
|
error: string;
|
|
37
|
-
status?: undefined;
|
|
38
37
|
ok?: undefined;
|
|
38
|
+
status?: undefined;
|
|
39
39
|
} | {
|
|
40
|
+
error?: undefined;
|
|
40
41
|
ok: boolean;
|
|
41
42
|
status: string;
|
|
42
|
-
error?: undefined;
|
|
43
43
|
} | {
|
|
44
|
+
ok?: undefined;
|
|
44
45
|
error: string;
|
|
45
46
|
removed?: undefined;
|
|
46
|
-
ok?: undefined;
|
|
47
47
|
} | {
|
|
48
|
+
error?: undefined;
|
|
48
49
|
ok: boolean;
|
|
49
50
|
removed: boolean;
|
|
50
|
-
error?: undefined;
|
|
51
51
|
}>>;
|
|
52
52
|
/**
|
|
53
53
|
* POST /_agent-native/secrets/:key/test — re-run the validator against the
|
|
54
54
|
* current stored value without changing anything. Useful for the "Test" button.
|
|
55
55
|
*/
|
|
56
56
|
export declare function createTestSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
57
|
+
ok?: undefined;
|
|
57
58
|
error: string;
|
|
58
59
|
note?: undefined;
|
|
59
|
-
ok?: undefined;
|
|
60
60
|
} | {
|
|
61
|
+
error?: undefined;
|
|
61
62
|
ok: boolean;
|
|
62
63
|
note?: undefined;
|
|
63
|
-
error?: undefined;
|
|
64
64
|
} | {
|
|
65
|
+
error?: undefined;
|
|
65
66
|
ok: boolean;
|
|
66
67
|
note: string;
|
|
67
|
-
error?: undefined;
|
|
68
68
|
} | {
|
|
69
69
|
note?: undefined;
|
|
70
70
|
ok: boolean;
|
|
@@ -95,12 +95,12 @@ export interface AdHocSecretPayload {
|
|
|
95
95
|
export declare function createAdHocSecretHandler(): import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<AdHocSecretPayload[] | {
|
|
96
96
|
error: string;
|
|
97
97
|
} | {
|
|
98
|
+
error?: undefined;
|
|
98
99
|
ok: boolean;
|
|
99
100
|
key: string;
|
|
100
|
-
error?: undefined;
|
|
101
101
|
} | {
|
|
102
|
+
error?: undefined;
|
|
102
103
|
ok: boolean;
|
|
103
104
|
removed: boolean;
|
|
104
|
-
error?: undefined;
|
|
105
105
|
}>>;
|
|
106
106
|
//# sourceMappingURL=routes.d.ts.map
|
|
@@ -31,6 +31,12 @@ export interface RenderEmailArgs {
|
|
|
31
31
|
paragraphs: string[];
|
|
32
32
|
/** Primary call-to-action rendered as a real button. */
|
|
33
33
|
cta?: EmailCta;
|
|
34
|
+
/**
|
|
35
|
+
* Optional trusted HTML injected above the CTA — e.g. a template-owned video
|
|
36
|
+
* thumbnail with a play badge. Injected verbatim, so only pass markup built
|
|
37
|
+
* by app/template code (never raw user input), and escape any dynamic values.
|
|
38
|
+
*/
|
|
39
|
+
heroHtml?: string;
|
|
34
40
|
/** Small muted text under the CTA (e.g. expiry note). */
|
|
35
41
|
footer?: string;
|
|
36
42
|
/** Optional app name shown beside the framework logo. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email-template.d.ts","sourceRoot":"","sources":["../../src/server/email-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,eAAO,MAAM,kCAAkC,sBAAsB,CAAC;AAEtE,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,wDAAwD;IACxD,GAAG,CAAC,EAAE,QAAQ,CAAC;IACf,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAsCD,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,aAAa,
|
|
1
|
+
{"version":3,"file":"email-template.d.ts","sourceRoot":"","sources":["../../src/server/email-template.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAIH,eAAO,MAAM,kCAAkC,sBAAsB,CAAC;AAEtE,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,eAAe;IAC9B,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,wDAAwD;IACxD,GAAG,CAAC,EAAE,QAAQ,CAAC;IACf;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAsCD,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,aAAa,CAkHhE;AAeD;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAE5D"}
|
|
@@ -65,6 +65,11 @@ export function renderEmail(args) {
|
|
|
65
65
|
const ctaBg = brand ?? "#fafafa";
|
|
66
66
|
const ctaFg = brand ? "#ffffff" : "#0a0a0c";
|
|
67
67
|
const linkColor = brand ?? "#a1a1aa";
|
|
68
|
+
// Trusted markup supplied by the caller (template code, not user input),
|
|
69
|
+
// injected as-is so a template can own app-specific previews (e.g. a video
|
|
70
|
+
// thumbnail with a play badge). Callers are responsible for escaping any
|
|
71
|
+
// dynamic values they interpolate.
|
|
72
|
+
const heroHtml = args.heroHtml ?? "";
|
|
68
73
|
const paragraphsHtml = args.paragraphs
|
|
69
74
|
.map((p) => `<p style="margin:0 0 16px 0; font-size:16px; line-height:1.6; color:#d4d4d8;">${p}</p>`)
|
|
70
75
|
.join("");
|
|
@@ -124,6 +129,7 @@ export function renderEmail(args) {
|
|
|
124
129
|
${escapeHtml(args.heading)}
|
|
125
130
|
</h1>
|
|
126
131
|
${paragraphsHtml}
|
|
132
|
+
${heroHtml}
|
|
127
133
|
${ctaHtml}
|
|
128
134
|
${footerHtml}
|
|
129
135
|
</td>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email-template.js","sourceRoot":"","sources":["../../src/server/email-template.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reusable dark-themed HTML email template.\n *\n * Email clients have limited CSS support, so everything is inlined and layout\n * uses tables for Outlook compatibility. The design mirrors the app's dark UI:\n * near-black card on neutral background, Inter typography with safe fallbacks.\n *\n * Default is monochrome (white CTA on dark). Pass `brandColor` to tint the\n * CTA button and inline links — Clips, for example, passes its purple.\n *\n * Usage:\n * const { html, text } = renderEmail({\n * preheader: \"…\",\n * heading: \"You're invited to join Acme\",\n * paragraphs: [\"Alice invited you to join…\"],\n * cta: { label: \"Accept invite\", url: \"https://…\" },\n * footer: \"If you weren't expecting this, ignore this email.\",\n * });\n */\n\nimport { getAppName } from \"./app-name.js\";\n\nexport const AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID = \"agent-native-logo\";\n\nexport interface EmailCta {\n label: string;\n url: string;\n}\n\nexport interface RenderEmailArgs {\n /** Short preview text shown by email clients next to the subject. */\n preheader?: string;\n /** Large headline at the top of the card. */\n heading: string;\n /** Body paragraphs rendered after the heading. Plain strings — escaped. */\n paragraphs: string[];\n /** Primary call-to-action rendered as a real button. */\n cta?: EmailCta;\n /** Small muted text under the CTA (e.g. expiry note). */\n footer?: string;\n /** Optional app name shown beside the framework logo. */\n brandName?: string;\n /**\n * Optional absolute `https://` logo URL shown in the brand header. When a\n * valid URL is provided it replaces the default embedded Agent Native logo;\n * anything else (missing, relative, non-https) falls back to that logo.\n */\n brandLogoUrl?: string;\n /**\n * Optional brand hex color for the CTA button and inline links. Defaults to\n * a monochrome near-white button with dark text.\n */\n brandColor?: string;\n}\n\nexport interface RenderedEmail {\n html: string;\n text: string;\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\nfunction escapeAttr(s: string): string {\n return escapeHtml(s);\n}\n\n/**\n * Only accept a strict `#rrggbb` hex color for `brandColor`. Anything else\n * could inject CSS into the inline `style` attribute (`red; background:url(…)`).\n */\nfunction sanitizeHexColor(input: string | undefined): string | undefined {\n if (!input) return undefined;\n return /^#[0-9a-fA-F]{6}$/.test(input) ? input : undefined;\n}\n\n/**\n * Only accept an absolute `https://` URL for the brand logo. Email clients drop\n * relative and mixed-content images, and an unvalidated string in `src` is an\n * injection surface — so anything else falls back to the embedded logo.\n */\nfunction sanitizeLogoUrl(input: string | undefined): string | undefined {\n if (!input) return undefined;\n try {\n return new URL(input).protocol === \"https:\" ? input : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport function renderEmail(args: RenderEmailArgs): RenderedEmail {\n const preheader = args.preheader || \"\";\n const brand = sanitizeHexColor(args.brandColor);\n const brandName = args.brandName?.trim() || getAppName() || \"Agent Native\";\n const logoSrc =\n sanitizeLogoUrl(args.brandLogoUrl) ??\n `cid:${AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID}`;\n\n // Monochrome default: near-white button with dark text. Brand override:\n // colored button with white text.\n const ctaBg = brand ?? \"#fafafa\";\n const ctaFg = brand ? \"#ffffff\" : \"#0a0a0c\";\n const linkColor = brand ?? \"#a1a1aa\";\n\n const paragraphsHtml = args.paragraphs\n .map(\n (p) =>\n `<p style=\"margin:0 0 16px 0; font-size:16px; line-height:1.6; color:#d4d4d8;\">${p}</p>`,\n )\n .join(\"\");\n\n const ctaHtml = args.cta\n ? `\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"margin:24px 0 0 0;\">\n <tr>\n <td style=\"border-radius:10px; background:${ctaBg};\">\n <a href=\"${escapeAttr(args.cta.url)}\"\n style=\"display:inline-block; padding:14px 26px; font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size:15px; font-weight:600; color:${ctaFg}; text-decoration:none; border-radius:10px;\">\n ${escapeHtml(args.cta.label)}\n </a>\n </td>\n </tr>\n </table>\n `\n : \"\";\n\n const footerHtml = args.footer\n ? `<p style=\"margin:28px 0 0 0; font-size:13px; line-height:1.5; color:#71717a;\">${escapeHtml(args.footer)}</p>`\n : \"\";\n\n const brandHeaderHtml = `\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"margin:0 0 28px 0; padding:0 0 24px 0; border-bottom:1px solid #27272a;\">\n <tr>\n <td align=\"center\">\n <img src=\"${escapeAttr(logoSrc)}\" alt=\"${escapeAttr(brandName)}\" width=\"28\" height=\"28\" style=\"display:inline-block; vertical-align:middle; width:28px; height:28px; margin:0 8px 0 0; border:0;\" />\n <span style=\"font-size:18px; line-height:28px; font-weight:600; color:#fafafa; vertical-align:middle;\">${escapeHtml(brandName)}</span>\n </td>\n </tr>\n </table>`;\n\n const html = `<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta name=\"color-scheme\" content=\"dark light\" />\n <meta name=\"supported-color-schemes\" content=\"dark light\" />\n <title>${escapeHtml(args.heading)}</title>\n <style>\n @media (prefers-color-scheme: light) {\n .bg-outer { background-color: #0a0a0c !important; }\n }\n a { color: ${linkColor}; }\n </style>\n </head>\n <body style=\"margin:0; padding:0; background-color:#0a0a0c; font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; -webkit-font-smoothing:antialiased;\">\n <div style=\"display:none; max-height:0; overflow:hidden; opacity:0; color:transparent;\">\n ${escapeHtml(preheader)}\n </div>\n <table role=\"presentation\" class=\"bg-outer\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"background-color:#0a0a0c; padding:40px 16px;\">\n <tr>\n <td align=\"center\">\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"max-width:560px;\">\n <tr>\n <td style=\"background-color:#141417; border:1px solid #27272a; border-radius:16px; padding:36px 36px 32px 36px;\">\n ${brandHeaderHtml}\n <h1 style=\"margin:0 0 20px 0; font-size:24px; line-height:1.3; font-weight:600; color:#fafafa; letter-spacing:-0.02em;\">\n ${escapeHtml(args.heading)}\n </h1>\n ${paragraphsHtml}\n ${ctaHtml}\n ${footerHtml}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n</html>`;\n\n const textLines: string[] = [];\n textLines.push(args.heading);\n textLines.push(\"\");\n for (const p of args.paragraphs) {\n textLines.push(stripTags(p));\n textLines.push(\"\");\n }\n if (args.cta) {\n textLines.push(`${args.cta.label}: ${args.cta.url}`);\n textLines.push(\"\");\n }\n if (args.footer) {\n textLines.push(args.footer);\n }\n\n return { html, text: textLines.join(\"\\n\").trim() };\n}\n\nfunction stripTags(s: string): string {\n return s\n .replace(/<br\\s*\\/?>/gi, \"\\n\")\n .replace(/<[^>]+>/g, \"\")\n .replace(/ /g, \" \")\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .trim();\n}\n\n/**\n * Build an inline `<strong>` tag with consistent styling for use inside\n * paragraph strings passed to `renderEmail`. Escapes the content.\n */\nexport function emailStrong(text: string): string {\n return `<strong style=\"color:#fafafa; font-weight:600;\">${escapeHtml(text)}</strong>`;\n}\n\n/**\n * Build a labelled inline link for paragraph strings passed to `renderEmail`.\n * Use this instead of rendering raw URLs in the visible email body.\n */\nexport function emailLink(label: string, url: string): string {\n return `<a href=\"${escapeAttr(url)}\" style=\"color:#a1a1aa; text-decoration:underline;\">${escapeHtml(label)}</a>`;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"email-template.js","sourceRoot":"","sources":["../../src/server/email-template.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Reusable dark-themed HTML email template.\n *\n * Email clients have limited CSS support, so everything is inlined and layout\n * uses tables for Outlook compatibility. The design mirrors the app's dark UI:\n * near-black card on neutral background, Inter typography with safe fallbacks.\n *\n * Default is monochrome (white CTA on dark). Pass `brandColor` to tint the\n * CTA button and inline links — Clips, for example, passes its purple.\n *\n * Usage:\n * const { html, text } = renderEmail({\n * preheader: \"…\",\n * heading: \"You're invited to join Acme\",\n * paragraphs: [\"Alice invited you to join…\"],\n * cta: { label: \"Accept invite\", url: \"https://…\" },\n * footer: \"If you weren't expecting this, ignore this email.\",\n * });\n */\n\nimport { getAppName } from \"./app-name.js\";\n\nexport const AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID = \"agent-native-logo\";\n\nexport interface EmailCta {\n label: string;\n url: string;\n}\n\nexport interface RenderEmailArgs {\n /** Short preview text shown by email clients next to the subject. */\n preheader?: string;\n /** Large headline at the top of the card. */\n heading: string;\n /** Body paragraphs rendered after the heading. Plain strings — escaped. */\n paragraphs: string[];\n /** Primary call-to-action rendered as a real button. */\n cta?: EmailCta;\n /**\n * Optional trusted HTML injected above the CTA — e.g. a template-owned video\n * thumbnail with a play badge. Injected verbatim, so only pass markup built\n * by app/template code (never raw user input), and escape any dynamic values.\n */\n heroHtml?: string;\n /** Small muted text under the CTA (e.g. expiry note). */\n footer?: string;\n /** Optional app name shown beside the framework logo. */\n brandName?: string;\n /**\n * Optional absolute `https://` logo URL shown in the brand header. When a\n * valid URL is provided it replaces the default embedded Agent Native logo;\n * anything else (missing, relative, non-https) falls back to that logo.\n */\n brandLogoUrl?: string;\n /**\n * Optional brand hex color for the CTA button and inline links. Defaults to\n * a monochrome near-white button with dark text.\n */\n brandColor?: string;\n}\n\nexport interface RenderedEmail {\n html: string;\n text: string;\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/\"/g, \""\")\n .replace(/'/g, \"'\");\n}\n\nfunction escapeAttr(s: string): string {\n return escapeHtml(s);\n}\n\n/**\n * Only accept a strict `#rrggbb` hex color for `brandColor`. Anything else\n * could inject CSS into the inline `style` attribute (`red; background:url(…)`).\n */\nfunction sanitizeHexColor(input: string | undefined): string | undefined {\n if (!input) return undefined;\n return /^#[0-9a-fA-F]{6}$/.test(input) ? input : undefined;\n}\n\n/**\n * Only accept an absolute `https://` URL for the brand logo. Email clients drop\n * relative and mixed-content images, and an unvalidated string in `src` is an\n * injection surface — so anything else falls back to the embedded logo.\n */\nfunction sanitizeLogoUrl(input: string | undefined): string | undefined {\n if (!input) return undefined;\n try {\n return new URL(input).protocol === \"https:\" ? input : undefined;\n } catch {\n return undefined;\n }\n}\n\nexport function renderEmail(args: RenderEmailArgs): RenderedEmail {\n const preheader = args.preheader || \"\";\n const brand = sanitizeHexColor(args.brandColor);\n const brandName = args.brandName?.trim() || getAppName() || \"Agent Native\";\n const logoSrc =\n sanitizeLogoUrl(args.brandLogoUrl) ??\n `cid:${AGENT_NATIVE_EMAIL_LOGO_CONTENT_ID}`;\n\n // Monochrome default: near-white button with dark text. Brand override:\n // colored button with white text.\n const ctaBg = brand ?? \"#fafafa\";\n const ctaFg = brand ? \"#ffffff\" : \"#0a0a0c\";\n const linkColor = brand ?? \"#a1a1aa\";\n\n // Trusted markup supplied by the caller (template code, not user input),\n // injected as-is so a template can own app-specific previews (e.g. a video\n // thumbnail with a play badge). Callers are responsible for escaping any\n // dynamic values they interpolate.\n const heroHtml = args.heroHtml ?? \"\";\n\n const paragraphsHtml = args.paragraphs\n .map(\n (p) =>\n `<p style=\"margin:0 0 16px 0; font-size:16px; line-height:1.6; color:#d4d4d8;\">${p}</p>`,\n )\n .join(\"\");\n\n const ctaHtml = args.cta\n ? `\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" style=\"margin:24px 0 0 0;\">\n <tr>\n <td style=\"border-radius:10px; background:${ctaBg};\">\n <a href=\"${escapeAttr(args.cta.url)}\"\n style=\"display:inline-block; padding:14px 26px; font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size:15px; font-weight:600; color:${ctaFg}; text-decoration:none; border-radius:10px;\">\n ${escapeHtml(args.cta.label)}\n </a>\n </td>\n </tr>\n </table>\n `\n : \"\";\n\n const footerHtml = args.footer\n ? `<p style=\"margin:28px 0 0 0; font-size:13px; line-height:1.5; color:#71717a;\">${escapeHtml(args.footer)}</p>`\n : \"\";\n\n const brandHeaderHtml = `\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"margin:0 0 28px 0; padding:0 0 24px 0; border-bottom:1px solid #27272a;\">\n <tr>\n <td align=\"center\">\n <img src=\"${escapeAttr(logoSrc)}\" alt=\"${escapeAttr(brandName)}\" width=\"28\" height=\"28\" style=\"display:inline-block; vertical-align:middle; width:28px; height:28px; margin:0 8px 0 0; border:0;\" />\n <span style=\"font-size:18px; line-height:28px; font-weight:600; color:#fafafa; vertical-align:middle;\">${escapeHtml(brandName)}</span>\n </td>\n </tr>\n </table>`;\n\n const html = `<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta name=\"color-scheme\" content=\"dark light\" />\n <meta name=\"supported-color-schemes\" content=\"dark light\" />\n <title>${escapeHtml(args.heading)}</title>\n <style>\n @media (prefers-color-scheme: light) {\n .bg-outer { background-color: #0a0a0c !important; }\n }\n a { color: ${linkColor}; }\n </style>\n </head>\n <body style=\"margin:0; padding:0; background-color:#0a0a0c; font-family:'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; -webkit-font-smoothing:antialiased;\">\n <div style=\"display:none; max-height:0; overflow:hidden; opacity:0; color:transparent;\">\n ${escapeHtml(preheader)}\n </div>\n <table role=\"presentation\" class=\"bg-outer\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"background-color:#0a0a0c; padding:40px 16px;\">\n <tr>\n <td align=\"center\">\n <table role=\"presentation\" cellpadding=\"0\" cellspacing=\"0\" border=\"0\" width=\"100%\" style=\"max-width:560px;\">\n <tr>\n <td style=\"background-color:#141417; border:1px solid #27272a; border-radius:16px; padding:36px 36px 32px 36px;\">\n ${brandHeaderHtml}\n <h1 style=\"margin:0 0 20px 0; font-size:24px; line-height:1.3; font-weight:600; color:#fafafa; letter-spacing:-0.02em;\">\n ${escapeHtml(args.heading)}\n </h1>\n ${paragraphsHtml}\n ${heroHtml}\n ${ctaHtml}\n ${footerHtml}\n </td>\n </tr>\n </table>\n </td>\n </tr>\n </table>\n </body>\n</html>`;\n\n const textLines: string[] = [];\n textLines.push(args.heading);\n textLines.push(\"\");\n for (const p of args.paragraphs) {\n textLines.push(stripTags(p));\n textLines.push(\"\");\n }\n if (args.cta) {\n textLines.push(`${args.cta.label}: ${args.cta.url}`);\n textLines.push(\"\");\n }\n if (args.footer) {\n textLines.push(args.footer);\n }\n\n return { html, text: textLines.join(\"\\n\").trim() };\n}\n\nfunction stripTags(s: string): string {\n return s\n .replace(/<br\\s*\\/?>/gi, \"\\n\")\n .replace(/<[^>]+>/g, \"\")\n .replace(/ /g, \" \")\n .replace(/&/g, \"&\")\n .replace(/</g, \"<\")\n .replace(/>/g, \">\")\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .trim();\n}\n\n/**\n * Build an inline `<strong>` tag with consistent styling for use inside\n * paragraph strings passed to `renderEmail`. Escapes the content.\n */\nexport function emailStrong(text: string): string {\n return `<strong style=\"color:#fafafa; font-weight:600;\">${escapeHtml(text)}</strong>`;\n}\n\n/**\n * Build a labelled inline link for paragraph strings passed to `renderEmail`.\n * Use this instead of rendering raw URLs in the visible email body.\n */\nexport function emailLink(label: string, url: string): string {\n return `<a href=\"${escapeAttr(url)}\" style=\"color:#a1a1aa; text-decoration:underline;\">${escapeHtml(label)}</a>`;\n}\n"]}
|
package/dist/server/email.d.ts
CHANGED
|
@@ -23,6 +23,13 @@ export interface SendEmailArgs {
|
|
|
23
23
|
html: string;
|
|
24
24
|
text?: string;
|
|
25
25
|
from?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Display-name-only override. Keeps the configured (domain-verified) sending
|
|
28
|
+
* address and just changes the name shown to the recipient, e.g.
|
|
29
|
+
* "Alice via Clips". Ignored when `from` is set. Prefer this over `from` for
|
|
30
|
+
* per-user senders: putting a user's own address in `From` breaks SPF/DKIM.
|
|
31
|
+
*/
|
|
32
|
+
fromName?: string;
|
|
26
33
|
cc?: string | string[];
|
|
27
34
|
replyTo?: string;
|
|
28
35
|
inReplyTo?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../../src/server/email.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,UAAU,GAAG,KAAK,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,YAAY,GAAG,QAAQ,CAAC;CACvC;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA+DD,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC,CAE1D;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,aAAa,CAAC,CAE/D;
|
|
1
|
+
{"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../../src/server/email.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAMH,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,UAAU,GAAG,KAAK,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,YAAY,GAAG,QAAQ,CAAC;CACvC;AAED,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,eAAe,EAAE,CAAC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA+DD,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,OAAO,CAAC,CAE1D;AAED,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,aAAa,CAAC,CAE/D;AA8JD,wBAAsB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBlE"}
|
package/dist/server/email.js
CHANGED
|
@@ -61,10 +61,13 @@ export async function isEmailConfigured() {
|
|
|
61
61
|
export async function getEmailProvider() {
|
|
62
62
|
return (await resolveEmailTransport()).provider;
|
|
63
63
|
}
|
|
64
|
-
function getFromAddress(config, override) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
function getFromAddress(config, override, fromName) {
|
|
65
|
+
if (override)
|
|
66
|
+
return override;
|
|
67
|
+
const base = config.from ?? defaultFromAddress(config);
|
|
68
|
+
return fromName ? withDisplayName(base, fromName) : base;
|
|
69
|
+
}
|
|
70
|
+
function defaultFromAddress(config) {
|
|
68
71
|
// Resend lets unverified accounts send from its sandbox domain; SendGrid
|
|
69
72
|
// does not, so falling back there would cause silent 403s at runtime.
|
|
70
73
|
if (config.provider === "sendgrid") {
|
|
@@ -72,11 +75,26 @@ function getFromAddress(config, override) {
|
|
|
72
75
|
}
|
|
73
76
|
return "Agent Native <onboarding@resend.dev>";
|
|
74
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Swap the display name while keeping the verified address. The name is
|
|
80
|
+
* sanitized and quoted because it lands in a header: CR/LF would allow header
|
|
81
|
+
* injection, and quotes/angle brackets would break address parsing.
|
|
82
|
+
*/
|
|
83
|
+
function withDisplayName(from, name) {
|
|
84
|
+
const safe = name
|
|
85
|
+
.replace(/[\r\n"<>\\]/g, " ")
|
|
86
|
+
.replace(/\s+/g, " ")
|
|
87
|
+
.trim();
|
|
88
|
+
if (!safe)
|
|
89
|
+
return from;
|
|
90
|
+
const address = from.match(/<([^>]+)>/)?.[1]?.trim() ?? from.trim();
|
|
91
|
+
return `"${safe}" <${address}>`;
|
|
92
|
+
}
|
|
75
93
|
async function sendEmailWithSignal(args, signal) {
|
|
76
94
|
const config = await resolveEmailTransport();
|
|
77
95
|
signal?.throwIfAborted();
|
|
78
96
|
const provider = config.provider;
|
|
79
|
-
const from = getFromAddress(config, args.from);
|
|
97
|
+
const from = getFromAddress(config, args.from, args.fromName);
|
|
80
98
|
const attachments = resolveAttachments(args);
|
|
81
99
|
if (provider === "resend") {
|
|
82
100
|
const payload = {
|
|
@@ -213,9 +231,16 @@ export async function sendEmail(args) {
|
|
|
213
231
|
function parseSendGridFrom(from) {
|
|
214
232
|
const m = from.match(/^\s*(.*?)\s*<(.+)>\s*$/);
|
|
215
233
|
if (m && m[2])
|
|
216
|
-
return { name: m[1]
|
|
234
|
+
return { name: unquoteDisplayName(m[1]), email: m[2] };
|
|
217
235
|
return { email: from.trim() };
|
|
218
236
|
}
|
|
237
|
+
function unquoteDisplayName(name) {
|
|
238
|
+
const trimmed = name.trim();
|
|
239
|
+
const unquoted = trimmed.startsWith('"') && trimmed.endsWith('"')
|
|
240
|
+
? trimmed.slice(1, -1).replace(/\\(.)/g, "$1")
|
|
241
|
+
: trimmed;
|
|
242
|
+
return unquoted || undefined;
|
|
243
|
+
}
|
|
219
244
|
function stripHtml(html) {
|
|
220
245
|
return html
|
|
221
246
|
.replace(/<br\s*\/?>/gi, "\n")
|