@agent-native/core 0.84.21 → 0.84.23

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.
Files changed (137) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/agent/model-config.ts +1 -1
  5. package/corpus/core/src/cli/skills.ts +17 -3
  6. package/corpus/core/src/client/AssistantChat.tsx +27 -4
  7. package/corpus/core/src/client/guided-questions.tsx +11 -6
  8. package/corpus/core/src/client/tool-display.ts +8 -0
  9. package/corpus/core/src/file-upload/builder.ts +7 -2
  10. package/corpus/core/src/notifications/channels.ts +235 -38
  11. package/corpus/core/src/notifications/index.ts +1 -0
  12. package/corpus/core/src/notifications/registry.ts +13 -1
  13. package/corpus/core/src/server/analytics.ts +32 -6
  14. package/corpus/core/src/server/ssr-handler.ts +31 -12
  15. package/corpus/core/src/templates/default/.agents/skills/frontend-design/SKILL.md +2 -0
  16. package/corpus/core/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +2 -0
  17. package/corpus/templates/analytics/.agents/skills/frontend-design/SKILL.md +2 -0
  18. package/corpus/templates/analytics/AGENTS.md +21 -0
  19. package/corpus/templates/analytics/README.md +35 -0
  20. package/corpus/templates/analytics/actions/delete-analytics-alert-rule.ts +23 -0
  21. package/corpus/templates/analytics/actions/list-analytics-alert-rules.ts +20 -0
  22. package/corpus/templates/analytics/actions/run-analytics-alerts.ts +23 -0
  23. package/corpus/templates/analytics/actions/save-analytics-alert-rule.ts +69 -0
  24. package/corpus/templates/analytics/changelog/2026-07-01-analytics-can-alert-when-first-party-events-spike.md +6 -0
  25. package/corpus/templates/analytics/changelog/2026-07-01-session-replays-play-back-in-production-instead-of-showing.md +6 -0
  26. package/corpus/templates/analytics/netlify.toml +3 -0
  27. package/corpus/templates/analytics/scripts/emit-netlify-dashboard-report-cron.ts +126 -0
  28. package/corpus/templates/analytics/server/db/schema.ts +55 -0
  29. package/corpus/templates/analytics/server/handlers/session-replay.ts +5 -2
  30. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +74 -0
  31. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +803 -0
  32. package/corpus/templates/analytics/server/lib/session-replay.ts +10 -3
  33. package/corpus/templates/analytics/server/plugins/analytics-alert-jobs.ts +54 -0
  34. package/corpus/templates/analytics/server/plugins/db.ts +100 -0
  35. package/corpus/templates/analytics/server/routes/api/analytics-alerts/run.post.ts +60 -0
  36. package/corpus/templates/assets/.agents/skills/frontend-design/SKILL.md +2 -0
  37. package/corpus/templates/assets/README.md +30 -8
  38. package/corpus/templates/brain/.agents/skills/frontend-design/SKILL.md +2 -0
  39. package/corpus/templates/brain/README.md +20 -552
  40. package/corpus/templates/calendar/.agents/skills/frontend-design/SKILL.md +2 -0
  41. package/corpus/templates/calendar/README.md +22 -28
  42. package/corpus/templates/chat/.agents/skills/frontend-design/SKILL.md +2 -0
  43. package/corpus/templates/chat/README.md +32 -0
  44. package/corpus/templates/clips/.agents/skills/frontend-design/SKILL.md +2 -0
  45. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +30 -1
  46. package/corpus/templates/clips/README.md +37 -0
  47. package/corpus/templates/clips/actions/finalize-recording.ts +84 -4
  48. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +256 -0
  49. package/corpus/templates/clips/actions/reprocess-recording.ts +160 -0
  50. package/corpus/templates/clips/app/components/library/library-grid.tsx +18 -7
  51. package/corpus/templates/clips/app/components/library/recording-card.tsx +42 -5
  52. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +41 -1
  53. package/corpus/templates/clips/changelog/2026-07-01-move-to-folder-now-opens-folder-choices-directly-instead-of.md +6 -0
  54. package/corpus/templates/clips/changelog/2026-07-01-shared-clips-now-start-playing-and-scrub-instantly-instead-o.md +6 -0
  55. package/corpus/templates/clips/chrome-extension/public/manifest.json +1 -1
  56. package/corpus/templates/clips/server/lib/video-remux.ts +222 -0
  57. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +37 -0
  58. package/corpus/templates/content/.agents/skills/frontend-design/SKILL.md +2 -0
  59. package/corpus/templates/content/AGENTS.md +7 -3
  60. package/corpus/templates/content/README.md +22 -34
  61. package/corpus/templates/content/actions/list-trashed-content-databases.ts +4 -0
  62. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +58 -6
  63. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +257 -24
  64. package/corpus/templates/content/app/components/sidebar/DocumentTreeItem.tsx +41 -15
  65. package/corpus/templates/content/app/i18n/zh-TW.ts +25 -7
  66. package/corpus/templates/content/app/i18n-data.ts +278 -40
  67. package/corpus/templates/content/changelog/2026-06-30-database-once-again-creates-an-inline-database-in-the-curren.md +6 -0
  68. package/corpus/templates/content/shared/api.ts +1 -0
  69. package/corpus/templates/design/.agents/skills/design-generation/SKILL.md +75 -19
  70. package/corpus/templates/design/.agents/skills/frontend-design/SKILL.md +2 -0
  71. package/corpus/templates/design/AGENTS.md +7 -6
  72. package/corpus/templates/design/README.md +33 -0
  73. package/corpus/templates/design/actions/delete-file.ts +86 -10
  74. package/corpus/templates/design/actions/get-design-snapshot.ts +13 -0
  75. package/corpus/templates/design/actions/present-design-variants.ts +11 -13
  76. package/corpus/templates/design/app/hooks/use-question-flow.ts +2 -2
  77. package/corpus/templates/design/app/pages/DesignEditor.tsx +13 -3
  78. package/corpus/templates/design/changelog/2026-07-01-design-chat-no-longer-shows-an-empty-state-selection-chip-in-the-composer.md +6 -0
  79. package/corpus/templates/dispatch/.agents/skills/frontend-design/SKILL.md +2 -0
  80. package/corpus/templates/dispatch/README.md +34 -0
  81. package/corpus/templates/forms/.agents/skills/frontend-design/SKILL.md +2 -0
  82. package/corpus/templates/forms/README.md +34 -0
  83. package/corpus/templates/macros/.agents/skills/frontend-design/SKILL.md +2 -0
  84. package/corpus/templates/macros/README.md +28 -0
  85. package/corpus/templates/mail/.agents/skills/frontend-design/SKILL.md +2 -0
  86. package/corpus/templates/mail/README.md +35 -0
  87. package/corpus/templates/plan/.agents/skills/frontend-design/SKILL.md +2 -0
  88. package/corpus/templates/plan/README.md +30 -118
  89. package/corpus/templates/slides/.agents/skills/frontend-design/SKILL.md +2 -0
  90. package/corpus/templates/slides/README.md +34 -0
  91. package/corpus/templates/videos/.agents/skills/frontend-design/SKILL.md +2 -0
  92. package/corpus/templates/videos/README.md +22 -201
  93. package/dist/agent/model-config.js +1 -1
  94. package/dist/agent/model-config.js.map +1 -1
  95. package/dist/cli/skills.d.ts.map +1 -1
  96. package/dist/cli/skills.js +17 -3
  97. package/dist/cli/skills.js.map +1 -1
  98. package/dist/client/AssistantChat.d.ts.map +1 -1
  99. package/dist/client/AssistantChat.js +28 -4
  100. package/dist/client/AssistantChat.js.map +1 -1
  101. package/dist/client/guided-questions.d.ts.map +1 -1
  102. package/dist/client/guided-questions.js +10 -6
  103. package/dist/client/guided-questions.js.map +1 -1
  104. package/dist/client/tool-display.d.ts.map +1 -1
  105. package/dist/client/tool-display.js +8 -0
  106. package/dist/client/tool-display.js.map +1 -1
  107. package/dist/collab/routes.d.ts +1 -1
  108. package/dist/file-upload/builder.d.ts.map +1 -1
  109. package/dist/file-upload/builder.js +6 -2
  110. package/dist/file-upload/builder.js.map +1 -1
  111. package/dist/notifications/channels.d.ts +10 -0
  112. package/dist/notifications/channels.d.ts.map +1 -1
  113. package/dist/notifications/channels.js +172 -20
  114. package/dist/notifications/channels.js.map +1 -1
  115. package/dist/notifications/index.d.ts +1 -1
  116. package/dist/notifications/index.d.ts.map +1 -1
  117. package/dist/notifications/index.js +1 -1
  118. package/dist/notifications/index.js.map +1 -1
  119. package/dist/notifications/registry.d.ts +5 -0
  120. package/dist/notifications/registry.d.ts.map +1 -1
  121. package/dist/notifications/registry.js +4 -1
  122. package/dist/notifications/registry.js.map +1 -1
  123. package/dist/progress/routes.d.ts +1 -1
  124. package/dist/resources/handlers.d.ts +1 -1
  125. package/dist/server/analytics.d.ts +16 -0
  126. package/dist/server/analytics.d.ts.map +1 -1
  127. package/dist/server/analytics.js +30 -7
  128. package/dist/server/analytics.js.map +1 -1
  129. package/dist/server/ssr-handler.d.ts.map +1 -1
  130. package/dist/server/ssr-handler.js +28 -12
  131. package/dist/server/ssr-handler.js.map +1 -1
  132. package/dist/server/transcribe-voice.d.ts +1 -1
  133. package/dist/templates/default/.agents/skills/frontend-design/SKILL.md +2 -0
  134. package/dist/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +2 -0
  135. package/package.json +1 -1
  136. package/src/templates/default/.agents/skills/frontend-design/SKILL.md +2 -0
  137. package/src/templates/workspace-core/.agents/skills/frontend-design/SKILL.md +2 -0
@@ -10,9 +10,20 @@
10
10
  * value never enters the agent context.
11
11
  * NOTIFICATIONS_WEBHOOK_AUTH → optional `Authorization` header value (also
12
12
  * supports `${keys.NAME}`).
13
+ * NOTIFICATIONS_SLACK_WEBHOOK_URL
14
+ * → POST notifications to a Slack incoming webhook.
15
+ * Supports `${keys.NAME}` substitution.
16
+ * NOTIFICATIONS_EMAIL_CHANNEL=1
17
+ * → enable the built-in email channel for
18
+ * per-notification recipients.
19
+ * NOTIFICATIONS_EMAIL_RECIPIENTS
20
+ * → comma-separated fallback recipients for email
21
+ * notifications that do not pass
22
+ * `metadata.emailRecipients`.
13
23
  */
14
24
  import { ssrfSafeFetch } from "../extensions/url-safety.js";
15
25
  import { resolveKeyReferences, validateUrlAllowlist, getKeyAllowlist, } from "../secrets/substitution.js";
26
+ import { sendEmail } from "../server/email.js";
16
27
  import { registerNotificationChannel } from "./registry.js";
17
28
  let _registered = false;
18
29
  export function registerBuiltinNotificationChannels() {
@@ -23,32 +34,21 @@ export function registerBuiltinNotificationChannels() {
23
34
  if (url) {
24
35
  registerNotificationChannel(createWebhookChannel(url));
25
36
  }
37
+ const slackUrl = process.env.NOTIFICATIONS_SLACK_WEBHOOK_URL;
38
+ if (slackUrl) {
39
+ registerNotificationChannel(createSlackWebhookChannel(slackUrl));
40
+ }
41
+ const emailRecipients = process.env.NOTIFICATIONS_EMAIL_RECIPIENTS?.trim();
42
+ if (process.env.NOTIFICATIONS_EMAIL_CHANNEL === "1" || emailRecipients) {
43
+ registerNotificationChannel(createEmailChannel());
44
+ }
26
45
  }
27
46
  function createWebhookChannel(urlTemplate) {
28
47
  const authTemplate = process.env.NOTIFICATIONS_WEBHOOK_AUTH;
29
48
  return {
30
49
  name: "webhook",
31
50
  async deliver(input, meta) {
32
- // Resolve `${keys.NAME}` references against the owner's user-scope
33
- // secrets. Missing keys throw — the error surfaces in logs and the
34
- // channel is marked un-delivered, but other channels still run.
35
- const { resolved: url } = await resolveKeyReferences(urlTemplate, "user", meta.owner);
36
- const headers = {
37
- "Content-Type": "application/json",
38
- };
39
- if (authTemplate) {
40
- const { resolved: auth } = await resolveKeyReferences(authTemplate, "user", meta.owner);
41
- headers.Authorization = auth;
42
- }
43
- // If the user set an allowlist on a referenced key, enforce it here —
44
- // origin-level check, same rule the automations fetch-tool applies.
45
- const keyNames = Array.from(new Set(Array.from(urlTemplate.matchAll(/\$\{keys\.([A-Za-z0-9_-]+)\}/g), (m) => m[1])));
46
- const allowlists = await Promise.all(keyNames.map((name) => getKeyAllowlist(name, "user", meta.owner)));
47
- keyNames.forEach((name, i) => {
48
- if (!validateUrlAllowlist(url, allowlists[i])) {
49
- throw new Error(`[notifications] webhook URL ${new URL(url).origin} is not in the allowlist for key "${name}"`);
50
- }
51
- });
51
+ const { url, headers } = await resolveWebhookRequest(urlTemplate, authTemplate, meta.owner, "webhook");
52
52
  const res = await ssrfSafeFetch(url, {
53
53
  method: "POST",
54
54
  headers,
@@ -67,6 +67,158 @@ function createWebhookChannel(urlTemplate) {
67
67
  },
68
68
  };
69
69
  }
70
+ function createSlackWebhookChannel(urlTemplate) {
71
+ const authTemplate = process.env.NOTIFICATIONS_SLACK_WEBHOOK_AUTH;
72
+ return {
73
+ name: "slack",
74
+ async deliver(input, meta) {
75
+ const { url, headers } = await resolveWebhookRequest(urlTemplate, authTemplate, meta.owner, "Slack webhook");
76
+ const res = await ssrfSafeFetch(url, {
77
+ method: "POST",
78
+ headers,
79
+ body: JSON.stringify({
80
+ text: slackText(input.severity, input.title, input.body),
81
+ blocks: [
82
+ {
83
+ type: "section",
84
+ text: {
85
+ type: "mrkdwn",
86
+ text: `*${escapeSlack(input.title)}*`,
87
+ },
88
+ },
89
+ ...(input.body
90
+ ? [
91
+ {
92
+ type: "section",
93
+ text: {
94
+ type: "mrkdwn",
95
+ text: escapeSlack(input.body),
96
+ },
97
+ },
98
+ ]
99
+ : []),
100
+ {
101
+ type: "context",
102
+ elements: [
103
+ {
104
+ type: "mrkdwn",
105
+ text: `Severity: \`${input.severity}\` Owner: ${escapeSlack(meta.owner)}`,
106
+ },
107
+ ],
108
+ },
109
+ ],
110
+ }),
111
+ }, { maxRedirects: 3 });
112
+ if (!res.ok) {
113
+ throw new Error(`[notifications] Slack webhook ${new URL(url).origin} returned ${res.status}${(await readErrorSnippet(res)) || ""}`);
114
+ }
115
+ },
116
+ };
117
+ }
118
+ function createEmailChannel() {
119
+ return {
120
+ name: "email",
121
+ async deliver(input) {
122
+ const recipients = notificationEmailRecipients(input.metadata);
123
+ if (recipients.length === 0)
124
+ return;
125
+ const subject = typeof input.metadata?.emailSubject === "string" &&
126
+ input.metadata.emailSubject.trim()
127
+ ? input.metadata.emailSubject.trim()
128
+ : `[${input.severity}] ${input.title}`;
129
+ const metadata = scrubEmailMetadata(input.metadata);
130
+ const metadataText = metadata
131
+ ? `\n\nMetadata:\n${JSON.stringify(metadata, null, 2)}`
132
+ : "";
133
+ const text = `${input.title}\n\n${input.body ?? ""}${metadataText}`;
134
+ const html = [
135
+ `<p><strong>${escapeHtml(input.title)}</strong></p>`,
136
+ input.body ? `<p>${escapeHtml(input.body)}</p>` : "",
137
+ metadata
138
+ ? `<pre>${escapeHtml(JSON.stringify(metadata, null, 2))}</pre>`
139
+ : "",
140
+ ].join("");
141
+ await Promise.all(recipients.map((to) => sendEmail({
142
+ to,
143
+ subject,
144
+ text,
145
+ html,
146
+ })));
147
+ },
148
+ };
149
+ }
150
+ async function resolveWebhookRequest(urlTemplate, authTemplate, owner, label) {
151
+ // Resolve `${keys.NAME}` references against the owner's user-scope secrets.
152
+ // Missing keys throw — the error surfaces in logs and the channel is marked
153
+ // un-delivered, but other channels still run.
154
+ const { resolved: url } = await resolveKeyReferences(urlTemplate, "user", owner);
155
+ const headers = {
156
+ "Content-Type": "application/json",
157
+ };
158
+ if (authTemplate) {
159
+ const { resolved: auth } = await resolveKeyReferences(authTemplate, "user", owner);
160
+ headers.Authorization = auth;
161
+ }
162
+ // If the user set an allowlist on a referenced key, enforce it here —
163
+ // origin-level check, same rule the automations fetch-tool applies.
164
+ const keyNames = Array.from(new Set(Array.from(urlTemplate.matchAll(/\$\{keys\.([A-Za-z0-9_-]+)\}/g), (m) => String(m[1]))));
165
+ const allowlists = await Promise.all(keyNames.map((name) => getKeyAllowlist(name, "user", owner)));
166
+ keyNames.forEach((name, i) => {
167
+ if (!validateUrlAllowlist(url, allowlists[i])) {
168
+ throw new Error(`[notifications] ${label} URL ${new URL(url).origin} is not in the allowlist for key "${name}"`);
169
+ }
170
+ });
171
+ return { url, headers };
172
+ }
173
+ function notificationEmailRecipients(metadata) {
174
+ const fromMetadata = stringArray(metadata?.emailRecipients);
175
+ const fromEnv = commaList(process.env.NOTIFICATIONS_EMAIL_RECIPIENTS);
176
+ const seen = new Set();
177
+ const recipients = [];
178
+ for (const raw of [...fromMetadata, ...fromEnv]) {
179
+ const email = raw.trim().toLowerCase();
180
+ if (!email || seen.has(email))
181
+ continue;
182
+ seen.add(email);
183
+ recipients.push(email);
184
+ }
185
+ return recipients;
186
+ }
187
+ function stringArray(value) {
188
+ if (!Array.isArray(value))
189
+ return [];
190
+ return value.filter((item) => typeof item === "string");
191
+ }
192
+ function commaList(value) {
193
+ return value
194
+ ? value
195
+ .split(",")
196
+ .map((item) => item.trim())
197
+ .filter(Boolean)
198
+ : [];
199
+ }
200
+ function scrubEmailMetadata(metadata) {
201
+ if (!metadata)
202
+ return null;
203
+ const entries = Object.entries(metadata).filter(([key]) => key !== "emailRecipients" && key !== "emailSubject");
204
+ return entries.length ? Object.fromEntries(entries) : null;
205
+ }
206
+ function slackText(severity, title, body) {
207
+ return `[${severity}] ${title}${body ? `\n${body}` : ""}`;
208
+ }
209
+ function escapeSlack(value) {
210
+ return value
211
+ .replace(/&/g, "&amp;")
212
+ .replace(/</g, "&lt;")
213
+ .replace(/>/g, "&gt;");
214
+ }
215
+ function escapeHtml(value) {
216
+ return value
217
+ .replace(/&/g, "&amp;")
218
+ .replace(/</g, "&lt;")
219
+ .replace(/>/g, "&gt;")
220
+ .replace(/"/g, "&quot;");
221
+ }
70
222
  /**
71
223
  * Read up to ~1 KB from the body for error context. Streams chunks so a
72
224
  * misbehaving endpoint returning a large error page doesn't pin that whole
@@ -1 +1 @@
1
- {"version":3,"file":"channels.js","sourceRoot":"","sources":["../../src/notifications/channels.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Built-in notification channels.\n *\n * Set environment variables to auto-register the webhook channel at startup.\n * Extra channels can be registered at any time via\n * `registerNotificationChannel()` from a server plugin.\n *\n * NOTIFICATIONS_WEBHOOK_URL → POST notifications as JSON to this URL.\n * Supports `${keys.NAME}` substitution — the raw\n * value never enters the agent context.\n * NOTIFICATIONS_WEBHOOK_AUTH → optional `Authorization` header value (also\n * supports `${keys.NAME}`).\n */\n\nimport { ssrfSafeFetch } from \"../extensions/url-safety.js\";\nimport {\n resolveKeyReferences,\n validateUrlAllowlist,\n getKeyAllowlist,\n} from \"../secrets/substitution.js\";\nimport { registerNotificationChannel } from \"./registry.js\";\nimport type { NotificationChannel } from \"./types.js\";\n\nlet _registered = false;\n\nexport function registerBuiltinNotificationChannels(): void {\n if (_registered) return;\n _registered = true;\n\n const url = process.env.NOTIFICATIONS_WEBHOOK_URL;\n if (url) {\n registerNotificationChannel(createWebhookChannel(url));\n }\n}\n\nfunction createWebhookChannel(urlTemplate: string): NotificationChannel {\n const authTemplate = process.env.NOTIFICATIONS_WEBHOOK_AUTH;\n return {\n name: \"webhook\",\n async deliver(input, meta) {\n // Resolve `${keys.NAME}` references against the owner's user-scope\n // secrets. Missing keys throw — the error surfaces in logs and the\n // channel is marked un-delivered, but other channels still run.\n const { resolved: url } = await resolveKeyReferences(\n urlTemplate,\n \"user\",\n meta.owner,\n );\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n if (authTemplate) {\n const { resolved: auth } = await resolveKeyReferences(\n authTemplate,\n \"user\",\n meta.owner,\n );\n headers.Authorization = auth;\n }\n\n // If the user set an allowlist on a referenced key, enforce it here —\n // origin-level check, same rule the automations fetch-tool applies.\n const keyNames = Array.from(\n new Set(\n Array.from(\n urlTemplate.matchAll(/\\$\\{keys\\.([A-Za-z0-9_-]+)\\}/g),\n (m) => m[1],\n ),\n ),\n );\n const allowlists = await Promise.all(\n keyNames.map((name) => getKeyAllowlist(name, \"user\", meta.owner)),\n );\n keyNames.forEach((name, i) => {\n if (!validateUrlAllowlist(url, allowlists[i])) {\n throw new Error(\n `[notifications] webhook URL ${new URL(url).origin} is not in the allowlist for key \"${name}\"`,\n );\n }\n });\n\n const res = await ssrfSafeFetch(\n url,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n severity: input.severity,\n title: input.title,\n body: input.body,\n metadata: input.metadata,\n owner: meta.owner,\n emittedAt: new Date().toISOString(),\n }),\n },\n { maxRedirects: 3 },\n );\n if (!res.ok) {\n throw new Error(\n `[notifications] webhook ${new URL(url).origin} returned ${res.status}${\n (await readErrorSnippet(res)) || \"\"\n }`,\n );\n }\n },\n };\n}\n\n/**\n * Read up to ~1 KB from the body for error context. Streams chunks so a\n * misbehaving endpoint returning a large error page doesn't pin that whole\n * payload in memory per failed webhook.\n */\nasync function readErrorSnippet(res: Response): Promise<string> {\n const reader = res.body?.getReader();\n if (!reader) return \"\";\n const decoder = new TextDecoder();\n const MAX = 1024;\n let buf = \"\";\n try {\n while (buf.length < MAX) {\n const { value, done } = await reader.read();\n if (done) break;\n buf += decoder.decode(value, { stream: true });\n }\n reader.cancel().catch(() => {});\n } catch {\n return \"\";\n }\n if (!buf) return \"\";\n return `: ${buf.slice(0, 200)}`;\n}\n"]}
1
+ {"version":3,"file":"channels.js","sourceRoot":"","sources":["../../src/notifications/channels.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Built-in notification channels.\n *\n * Set environment variables to auto-register the webhook channel at startup.\n * Extra channels can be registered at any time via\n * `registerNotificationChannel()` from a server plugin.\n *\n * NOTIFICATIONS_WEBHOOK_URL → POST notifications as JSON to this URL.\n * Supports `${keys.NAME}` substitution — the raw\n * value never enters the agent context.\n * NOTIFICATIONS_WEBHOOK_AUTH → optional `Authorization` header value (also\n * supports `${keys.NAME}`).\n * NOTIFICATIONS_SLACK_WEBHOOK_URL\n * → POST notifications to a Slack incoming webhook.\n * Supports `${keys.NAME}` substitution.\n * NOTIFICATIONS_EMAIL_CHANNEL=1\n * → enable the built-in email channel for\n * per-notification recipients.\n * NOTIFICATIONS_EMAIL_RECIPIENTS\n * → comma-separated fallback recipients for email\n * notifications that do not pass\n * `metadata.emailRecipients`.\n */\n\nimport { ssrfSafeFetch } from \"../extensions/url-safety.js\";\nimport {\n resolveKeyReferences,\n validateUrlAllowlist,\n getKeyAllowlist,\n} from \"../secrets/substitution.js\";\nimport { sendEmail } from \"../server/email.js\";\nimport { registerNotificationChannel } from \"./registry.js\";\nimport type { NotificationChannel } from \"./types.js\";\n\nlet _registered = false;\n\nexport function registerBuiltinNotificationChannels(): void {\n if (_registered) return;\n _registered = true;\n\n const url = process.env.NOTIFICATIONS_WEBHOOK_URL;\n if (url) {\n registerNotificationChannel(createWebhookChannel(url));\n }\n\n const slackUrl = process.env.NOTIFICATIONS_SLACK_WEBHOOK_URL;\n if (slackUrl) {\n registerNotificationChannel(createSlackWebhookChannel(slackUrl));\n }\n\n const emailRecipients = process.env.NOTIFICATIONS_EMAIL_RECIPIENTS?.trim();\n if (process.env.NOTIFICATIONS_EMAIL_CHANNEL === \"1\" || emailRecipients) {\n registerNotificationChannel(createEmailChannel());\n }\n}\n\nfunction createWebhookChannel(urlTemplate: string): NotificationChannel {\n const authTemplate = process.env.NOTIFICATIONS_WEBHOOK_AUTH;\n return {\n name: \"webhook\",\n async deliver(input, meta) {\n const { url, headers } = await resolveWebhookRequest(\n urlTemplate,\n authTemplate,\n meta.owner,\n \"webhook\",\n );\n const res = await ssrfSafeFetch(\n url,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n severity: input.severity,\n title: input.title,\n body: input.body,\n metadata: input.metadata,\n owner: meta.owner,\n emittedAt: new Date().toISOString(),\n }),\n },\n { maxRedirects: 3 },\n );\n if (!res.ok) {\n throw new Error(\n `[notifications] webhook ${new URL(url).origin} returned ${res.status}${\n (await readErrorSnippet(res)) || \"\"\n }`,\n );\n }\n },\n };\n}\n\nfunction createSlackWebhookChannel(urlTemplate: string): NotificationChannel {\n const authTemplate = process.env.NOTIFICATIONS_SLACK_WEBHOOK_AUTH;\n return {\n name: \"slack\",\n async deliver(input, meta) {\n const { url, headers } = await resolveWebhookRequest(\n urlTemplate,\n authTemplate,\n meta.owner,\n \"Slack webhook\",\n );\n const res = await ssrfSafeFetch(\n url,\n {\n method: \"POST\",\n headers,\n body: JSON.stringify({\n text: slackText(input.severity, input.title, input.body),\n blocks: [\n {\n type: \"section\",\n text: {\n type: \"mrkdwn\",\n text: `*${escapeSlack(input.title)}*`,\n },\n },\n ...(input.body\n ? [\n {\n type: \"section\",\n text: {\n type: \"mrkdwn\",\n text: escapeSlack(input.body),\n },\n },\n ]\n : []),\n {\n type: \"context\",\n elements: [\n {\n type: \"mrkdwn\",\n text: `Severity: \\`${input.severity}\\` Owner: ${escapeSlack(meta.owner)}`,\n },\n ],\n },\n ],\n }),\n },\n { maxRedirects: 3 },\n );\n if (!res.ok) {\n throw new Error(\n `[notifications] Slack webhook ${new URL(url).origin} returned ${res.status}${\n (await readErrorSnippet(res)) || \"\"\n }`,\n );\n }\n },\n };\n}\n\nfunction createEmailChannel(): NotificationChannel {\n return {\n name: \"email\",\n async deliver(input) {\n const recipients = notificationEmailRecipients(input.metadata);\n if (recipients.length === 0) return;\n const subject =\n typeof input.metadata?.emailSubject === \"string\" &&\n input.metadata.emailSubject.trim()\n ? input.metadata.emailSubject.trim()\n : `[${input.severity}] ${input.title}`;\n const metadata = scrubEmailMetadata(input.metadata);\n const metadataText = metadata\n ? `\\n\\nMetadata:\\n${JSON.stringify(metadata, null, 2)}`\n : \"\";\n const text = `${input.title}\\n\\n${input.body ?? \"\"}${metadataText}`;\n const html = [\n `<p><strong>${escapeHtml(input.title)}</strong></p>`,\n input.body ? `<p>${escapeHtml(input.body)}</p>` : \"\",\n metadata\n ? `<pre>${escapeHtml(JSON.stringify(metadata, null, 2))}</pre>`\n : \"\",\n ].join(\"\");\n\n await Promise.all(\n recipients.map((to) =>\n sendEmail({\n to,\n subject,\n text,\n html,\n }),\n ),\n );\n },\n };\n}\n\nasync function resolveWebhookRequest(\n urlTemplate: string,\n authTemplate: string | undefined,\n owner: string,\n label: string,\n): Promise<{ url: string; headers: Record<string, string> }> {\n // Resolve `${keys.NAME}` references against the owner's user-scope secrets.\n // Missing keys throw — the error surfaces in logs and the channel is marked\n // un-delivered, but other channels still run.\n const { resolved: url } = await resolveKeyReferences(\n urlTemplate,\n \"user\",\n owner,\n );\n const headers: Record<string, string> = {\n \"Content-Type\": \"application/json\",\n };\n if (authTemplate) {\n const { resolved: auth } = await resolveKeyReferences(\n authTemplate,\n \"user\",\n owner,\n );\n headers.Authorization = auth;\n }\n\n // If the user set an allowlist on a referenced key, enforce it here —\n // origin-level check, same rule the automations fetch-tool applies.\n const keyNames = Array.from(\n new Set(\n Array.from(urlTemplate.matchAll(/\\$\\{keys\\.([A-Za-z0-9_-]+)\\}/g), (m) =>\n String(m[1]),\n ),\n ),\n );\n const allowlists = await Promise.all(\n keyNames.map((name) => getKeyAllowlist(name, \"user\", owner)),\n );\n keyNames.forEach((name, i) => {\n if (!validateUrlAllowlist(url, allowlists[i])) {\n throw new Error(\n `[notifications] ${label} URL ${new URL(url).origin} is not in the allowlist for key \"${name}\"`,\n );\n }\n });\n return { url, headers };\n}\n\nfunction notificationEmailRecipients(\n metadata: Record<string, unknown> | undefined,\n): string[] {\n const fromMetadata = stringArray(metadata?.emailRecipients);\n const fromEnv = commaList(process.env.NOTIFICATIONS_EMAIL_RECIPIENTS);\n const seen = new Set<string>();\n const recipients: string[] = [];\n for (const raw of [...fromMetadata, ...fromEnv]) {\n const email = raw.trim().toLowerCase();\n if (!email || seen.has(email)) continue;\n seen.add(email);\n recipients.push(email);\n }\n return recipients;\n}\n\nfunction stringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n return value.filter((item): item is string => typeof item === \"string\");\n}\n\nfunction commaList(value: string | undefined): string[] {\n return value\n ? value\n .split(\",\")\n .map((item) => item.trim())\n .filter(Boolean)\n : [];\n}\n\nfunction scrubEmailMetadata(\n metadata: Record<string, unknown> | undefined,\n): Record<string, unknown> | null {\n if (!metadata) return null;\n const entries = Object.entries(metadata).filter(\n ([key]) => key !== \"emailRecipients\" && key !== \"emailSubject\",\n );\n return entries.length ? Object.fromEntries(entries) : null;\n}\n\nfunction slackText(\n severity: string,\n title: string,\n body: string | undefined,\n): string {\n return `[${severity}] ${title}${body ? `\\n${body}` : \"\"}`;\n}\n\nfunction escapeSlack(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n}\n\nfunction escapeHtml(value: string): string {\n return value\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n\n/**\n * Read up to ~1 KB from the body for error context. Streams chunks so a\n * misbehaving endpoint returning a large error page doesn't pin that whole\n * payload in memory per failed webhook.\n */\nasync function readErrorSnippet(res: Response): Promise<string> {\n const reader = res.body?.getReader();\n if (!reader) return \"\";\n const decoder = new TextDecoder();\n const MAX = 1024;\n let buf = \"\";\n try {\n while (buf.length < MAX) {\n const { value, done } = await reader.read();\n if (done) break;\n buf += decoder.decode(value, { stream: true });\n }\n reader.cancel().catch(() => {});\n } catch {\n return \"\";\n }\n if (!buf) return \"\";\n return `: ${buf.slice(0, 200)}`;\n}\n"]}
@@ -1,4 +1,4 @@
1
1
  export type { Notification, NotificationSeverity, NotificationInput, NotificationMeta, NotificationChannel, } from "./types.js";
2
- export { notify, registerNotificationChannel, unregisterNotificationChannel, listNotificationChannels, listNotifications, countUnread, markNotificationRead, markAllNotificationsRead, deleteNotification, } from "./registry.js";
2
+ export { notify, notifyWithDelivery, registerNotificationChannel, unregisterNotificationChannel, listNotificationChannels, listNotifications, countUnread, markNotificationRead, markAllNotificationsRead, deleteNotification, } from "./registry.js";
3
3
  export { registerBuiltinNotificationChannels } from "./channels.js";
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,MAAM,EACN,2BAA2B,EAC3B,6BAA6B,EAC7B,wBAAwB,EACxB,iBAAiB,EACjB,WAAW,EACX,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,mCAAmC,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,YAAY,EACZ,oBAAoB,EACpB,iBAAiB,EACjB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,MAAM,EACN,kBAAkB,EAClB,2BAA2B,EAC3B,6BAA6B,EAC7B,wBAAwB,EACxB,iBAAiB,EACjB,WAAW,EACX,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,mCAAmC,EAAE,MAAM,eAAe,CAAC"}
@@ -1,3 +1,3 @@
1
- export { notify, registerNotificationChannel, unregisterNotificationChannel, listNotificationChannels, listNotifications, countUnread, markNotificationRead, markAllNotificationsRead, deleteNotification, } from "./registry.js";
1
+ export { notify, notifyWithDelivery, registerNotificationChannel, unregisterNotificationChannel, listNotificationChannels, listNotifications, countUnread, markNotificationRead, markAllNotificationsRead, deleteNotification, } from "./registry.js";
2
2
  export { registerBuiltinNotificationChannels } from "./channels.js";
3
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,MAAM,EACN,2BAA2B,EAC3B,6BAA6B,EAC7B,wBAAwB,EACxB,iBAAiB,EACjB,WAAW,EACX,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,mCAAmC,EAAE,MAAM,eAAe,CAAC","sourcesContent":["export type {\n Notification,\n NotificationSeverity,\n NotificationInput,\n NotificationMeta,\n NotificationChannel,\n} from \"./types.js\";\n\nexport {\n notify,\n registerNotificationChannel,\n unregisterNotificationChannel,\n listNotificationChannels,\n listNotifications,\n countUnread,\n markNotificationRead,\n markAllNotificationsRead,\n deleteNotification,\n} from \"./registry.js\";\n\nexport { registerBuiltinNotificationChannels } from \"./channels.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/notifications/index.ts"],"names":[],"mappings":"AAQA,OAAO,EACL,MAAM,EACN,kBAAkB,EAClB,2BAA2B,EAC3B,6BAA6B,EAC7B,wBAAwB,EACxB,iBAAiB,EACjB,WAAW,EACX,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,GACnB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,mCAAmC,EAAE,MAAM,eAAe,CAAC","sourcesContent":["export type {\n Notification,\n NotificationSeverity,\n NotificationInput,\n NotificationMeta,\n NotificationChannel,\n} from \"./types.js\";\n\nexport {\n notify,\n notifyWithDelivery,\n registerNotificationChannel,\n unregisterNotificationChannel,\n listNotificationChannels,\n listNotifications,\n countUnread,\n markNotificationRead,\n markAllNotificationsRead,\n deleteNotification,\n} from \"./registry.js\";\n\nexport { registerBuiltinNotificationChannels } from \"./channels.js\";\n"]}
@@ -1,8 +1,13 @@
1
1
  import { type NotificationChannel, type NotificationInput, type NotificationMeta, type Notification } from "./types.js";
2
+ export interface NotificationDeliveryResult {
3
+ notification?: Notification;
4
+ deliveredChannels: string[];
5
+ }
2
6
  export declare function registerNotificationChannel(channel: NotificationChannel): void;
3
7
  export declare function unregisterNotificationChannel(name: string): boolean;
4
8
  export declare function listNotificationChannels(): string[];
5
9
  export declare function notify(input: NotificationInput, meta: NotificationMeta): Promise<Notification | undefined>;
10
+ export declare function notifyWithDelivery(input: NotificationInput, meta: NotificationMeta): Promise<NotificationDeliveryResult>;
6
11
  /** Test helper — drops all registered channels. */
7
12
  export declare function __resetNotificationChannels(): void;
8
13
  export { listNotifications, markNotificationRead, markAllNotificationsRead, deleteNotification, countUnread, } from "./store.js";
@@ -1 +1 @@
1
- {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/notifications/registry.ts"],"names":[],"mappings":"AAOA,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;AAiCpB,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,mBAAmB,GAC3B,IAAI,CAUN;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED,wBAAgB,wBAAwB,IAAI,MAAM,EAAE,CAEnD;AAgBD,wBAAsB,MAAM,CAC1B,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAoFnC;AASD,mDAAmD;AACnD,wBAAgB,2BAA2B,IAAI,IAAI,CAElD;AAED,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,GACZ,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/notifications/registry.ts"],"names":[],"mappings":"AAOA,OAAO,EAEL,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,YAAY,EAClB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,0BAA0B;IACzC,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B;AAiCD,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,mBAAmB,GAC3B,IAAI,CAUN;AAED,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnE;AAED,wBAAgB,wBAAwB,IAAI,MAAM,EAAE,CAEnD;AAgBD,wBAAsB,MAAM,CAC1B,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,CAEnC;AAED,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,iBAAiB,EACxB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,0BAA0B,CAAC,CAoFrC;AASD,mDAAmD;AACnD,wBAAgB,2BAA2B,IAAI,IAAI,CAElD;AAED,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,GACZ,MAAM,YAAY,CAAC"}
@@ -58,6 +58,9 @@ export function listNotificationChannels() {
58
58
  const MAX_TITLE_LEN = 100;
59
59
  const MAX_BODY_LEN = 2000;
60
60
  export async function notify(input, meta) {
61
+ return (await notifyWithDelivery(input, meta)).notification;
62
+ }
63
+ export async function notifyWithDelivery(input, meta) {
61
64
  if (!meta?.owner) {
62
65
  throw new Error("notify: meta.owner is required");
63
66
  }
@@ -129,7 +132,7 @@ export async function notify(input, meta) {
129
132
  // best-effort
130
133
  }
131
134
  }
132
- return stored;
135
+ return { notification: stored, deliveredChannels: delivered };
133
136
  }
134
137
  function selectChannels(allowlist) {
135
138
  const registry = getRegistry();
@@ -1 +1 @@
1
- {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/notifications/registry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,IAAI,IAAI,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAEzD,OAAO,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AACzE,OAAO,EACL,uBAAuB,GAKxB,MAAM,YAAY,CAAC;AAEpB,aAAa,CAAC;IACZ,IAAI,EAAE,mBAAmB;IACzB,WAAW,EACT,mIAAmI;IACrI,aAAa,EAAE,CAAC,CAAC,MAAM,CAAC;QACtB,cAAc,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QACrC,QAAQ,EAAE,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC;QACzC,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE;QACjB,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;QAC3B,iBAAiB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;KACvC,CAAgD;IACjD,OAAO,EAAE;QACP,cAAc,EAAE,SAAS;QACzB,QAAQ,EAAE,UAAU;QACpB,KAAK,EAAE,gBAAgB;QACvB,IAAI,EAAE,2BAA2B;QACjC,iBAAiB,EAAE,CAAC,OAAO,EAAE,SAAS,CAAC;KACxC;CACF,CAAC,CAAC;AAEH,MAAM,YAAY,GAAG,MAAM,CAAC,GAAG,CAAC,2CAA2C,CAAC,CAAC;AAK7E,SAAS,WAAW;IAClB,MAAM,CAAC,GAAG,UAA2C,CAAC;IACtD,IAAI,CAAC,CAAC,CAAC,YAAY,CAAC;QAAE,CAAC,CAAC,YAAY,CAAC,GAAG,IAAI,GAAG,EAAE,CAAC;IAClD,OAAO,CAAC,CAAC,YAAY,CAAC,CAAC;AACzB,CAAC;AAED,MAAM,UAAU,2BAA2B,CACzC,OAA4B;IAE5B,IAAI,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,KAAK,CAAC,uDAAuD,CAAC,CAAC;IAC3E,CAAC;IACD,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;QAC1C,MAAM,IAAI,KAAK,CACb,iEAAiE,CAClE,CAAC;IACJ,CAAC;IACD,WAAW,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,6BAA6B,CAAC,IAAY;IACxD,OAAO,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,wBAAwB;IACtC,OAAO,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC;AAC1C,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,aAAa,GAAG,GAAG,CAAC;AAC1B,MAAM,YAAY,GAAG,IAAI,CAAC;AAE1B,MAAM,CAAC,KAAK,UAAU,MAAM,CAC1B,KAAwB,EACxB,IAAsB;IAEtB,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACpD,CAAC;IACD,KAAK,GAAG;QACN,GAAG,KAAK;QACR,KAAK,EAAE,QAAQ,CAAC,KAAK,CAAC,KAAK,EAAE,aAAa,CAAC;QAC3C,IAAI,EAAE,QAAQ,CAAC,KAAK,CAAC,IAAI,EAAE,YAAY,CAAC;KACzC,CAAC;IACF,MAAM,QAAQ,GAAG,cAAc,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAEhD,mEAAmE;IACnE,MAAM,QAAQ,GAAG,CAAC,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,QAAQ,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IACrE,MAAM,SAAS,GAAa,EAAE,CAAC;IAC/B,IAAI,MAAgC,CAAC;IAErC,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,CAAC;YACH,qEAAqE;YACrE,qEAAqE;YACrE,MAAM,GAAG,MAAM,kBAAkB,CAAC;gBAChC,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,iBAAiB,EAAE,CAAC,OAAO,CAAC;aAC7B,CAAC,CAAC;YACH,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,uCAAuC,EAAE,GAAG,CAAC,CAAC;QAC9D,CAAC;IACH,CAAC;IAED,0EAA0E;IAC1E,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CACtC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE;QAC7B,MAAM,OAAO,CAAC,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QACnC,OAAO,OAAO,CAAC,IAAI,CAAC;IACtB,CAAC,CAAC,CACH,CAAC;IACF,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACvB,IAAI,CAAC,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;YAC7B,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAC1B,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,KAAK,CACX,4BAA4B,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,WAAW,EACvD,CAAC,CAAC,MAAM,CACT,CAAC;QACJ,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,MAAM,eAAe,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC;IAC7D,IAAI,MAAM,IAAI,eAAe,EAAE,CAAC;QAC9B,IAAI,CAAC;YACH,MAAM,uBAAuB,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;YACpD,MAAM,GAAG,EAAE,GAAG,MAAM,EAAE,iBAAiB,EAAE,SAAS,EAAE,CAAC;QACvD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,kDAAkD,EAAE,GAAG,CAAC,CAAC;QACzE,CAAC;IACH,CAAC;IAED,sEAAsE;IACtE,uEAAuE;IACvE,0CAA0C;IAC1C,IAAI,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,YAAY,CACV,mBAAmB,EACnB;gBACE,cAAc,EAAE,MAAM,EAAE,EAAE;gBAC1B,QAAQ,EAAE,KAAK,CAAC,QAAQ;gBACxB,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,IAAI,EAAE,KAAK,CAAC,IAAI;gBAChB,iBAAiB,EAAE,SAAS;aAC7B,EACD,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CACtB,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,cAAc;QAChB,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,cAAc,CAAC,SAAoB;IAC1C,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAC;IAC/B,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1C,IAAI,CAAC,SAAS;QAAE,OAAO,GAAG,CAAC;IAC3B,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AACvD,CAAC;AAED,mDAAmD;AACnD,MAAM,UAAU,2BAA2B;IACzC,WAAW,EAAE,CAAC,KAAK,EAAE,CAAC;AACxB,CAAC;AAED,OAAO,EACL,iBAAiB,EACjB,oBAAoB,EACpB,wBAAwB,EACxB,kBAAkB,EAClB,WAAW,GACZ,MAAM,YAAY,CAAC","sourcesContent":["import { z } from \"zod\";\n\nimport { emit as emitBusEvent } from \"../event-bus/bus.js\";\nimport { registerEvent } from \"../event-bus/registry.js\";\nimport type { EventDefinition } from \"../event-bus/types.js\";\nimport { truncate } from \"../shared/truncate.js\";\nimport { insertNotification, updateDeliveredChannels } from \"./store.js\";\nimport {\n NOTIFICATION_SEVERITIES,\n type NotificationChannel,\n type NotificationInput,\n type NotificationMeta,\n type Notification,\n} from \"./types.js\";\n\nregisterEvent({\n name: \"notification.sent\",\n description:\n \"Fires after notify() delivers to at least one channel. Automations can chain off this — e.g. fan critical notifications to Slack.\",\n payloadSchema: z.object({\n notificationId: z.string().optional(),\n severity: z.enum(NOTIFICATION_SEVERITIES),\n title: z.string(),\n body: z.string().optional(),\n deliveredChannels: z.array(z.string()),\n }) as unknown as EventDefinition[\"payloadSchema\"],\n example: {\n notificationId: \"ntf_abc\",\n severity: \"critical\",\n title: \"Payment failed\",\n body: \"Card ending 4242 declined\",\n deliveredChannels: [\"inbox\", \"webhook\"],\n },\n});\n\nconst REGISTRY_KEY = Symbol.for(\"@agent-native/core/notifications.registry\");\ninterface GlobalWithRegistry {\n [REGISTRY_KEY]?: Map<string, NotificationChannel>;\n}\n\nfunction getRegistry(): Map<string, NotificationChannel> {\n const g = globalThis as unknown as GlobalWithRegistry;\n if (!g[REGISTRY_KEY]) g[REGISTRY_KEY] = new Map();\n return g[REGISTRY_KEY];\n}\n\nexport function registerNotificationChannel(\n channel: NotificationChannel,\n): void {\n if (!channel?.name) {\n throw new Error(\"registerNotificationChannel: channel.name is required\");\n }\n if (typeof channel.deliver !== \"function\") {\n throw new Error(\n \"registerNotificationChannel: channel.deliver must be a function\",\n );\n }\n getRegistry().set(channel.name, channel);\n}\n\nexport function unregisterNotificationChannel(name: string): boolean {\n return getRegistry().delete(name);\n}\n\nexport function listNotificationChannels(): string[] {\n return Array.from(getRegistry().keys());\n}\n\n/**\n * Deliver a notification.\n *\n * The `inbox` channel always persists a row that drives the in-app UI\n * (bell + toast). Additional channels (webhook, custom) run in parallel,\n * best-effort. Returns the stored Notification when `inbox` ran, otherwise\n * `undefined`.\n *\n * Also emits `notification.sent` on the event bus so automations can react\n * to notifications (e.g. \"when a critical notification fires, also page me\").\n */\nconst MAX_TITLE_LEN = 100;\nconst MAX_BODY_LEN = 2000;\n\nexport async function notify(\n input: NotificationInput,\n meta: NotificationMeta,\n): Promise<Notification | undefined> {\n if (!meta?.owner) {\n throw new Error(\"notify: meta.owner is required\");\n }\n input = {\n ...input,\n title: truncate(input.title, MAX_TITLE_LEN),\n body: truncate(input.body, MAX_BODY_LEN),\n };\n const channels = selectChannels(input.channels);\n\n // The inbox channel is always included unless explicitly excluded.\n const runInbox = !input.channels || input.channels.includes(\"inbox\");\n const delivered: string[] = [];\n let stored: Notification | undefined;\n\n if (runInbox) {\n try {\n // Stored with just \"inbox\" first; the real delivered list is written\n // after fan-out so a failing webhook doesn't claim it was delivered.\n stored = await insertNotification({\n owner: meta.owner,\n severity: input.severity,\n title: input.title,\n body: input.body,\n metadata: input.metadata,\n deliveredChannels: [\"inbox\"],\n });\n delivered.push(\"inbox\");\n } catch (err) {\n console.error(\"[notifications] inbox persist failed:\", err);\n }\n }\n\n // Await every channel so a 500-ing webhook doesn't end up in `delivered`.\n const results = await Promise.allSettled(\n channels.map(async (channel) => {\n await channel.deliver(input, meta);\n return channel.name;\n }),\n );\n results.forEach((r, i) => {\n if (r.status === \"fulfilled\") {\n delivered.push(r.value);\n } else {\n console.error(\n `[notifications] channel \"${channels[i].name}\" failed:`,\n r.reason,\n );\n }\n });\n\n const hasExtraChannel = delivered.some((c) => c !== \"inbox\");\n if (stored && hasExtraChannel) {\n try {\n await updateDeliveredChannels(stored.id, delivered);\n stored = { ...stored, deliveredChannels: delivered };\n } catch (err) {\n console.error(\"[notifications] delivered-channel update failed:\", err);\n }\n }\n\n // Only emit when at least one channel delivered — an emission with an\n // empty delivery list (and likely a null notificationId) would mislead\n // any automation chaining off this event.\n if (delivered.length > 0) {\n try {\n emitBusEvent(\n \"notification.sent\",\n {\n notificationId: stored?.id,\n severity: input.severity,\n title: input.title,\n body: input.body,\n deliveredChannels: delivered,\n },\n { owner: meta.owner },\n );\n } catch {\n // best-effort\n }\n }\n\n return stored;\n}\n\nfunction selectChannels(allowlist?: string[]): NotificationChannel[] {\n const registry = getRegistry();\n const all = Array.from(registry.values());\n if (!allowlist) return all;\n return all.filter((c) => allowlist.includes(c.name));\n}\n\n/** Test helper — drops all registered channels. */\nexport function __resetNotificationChannels(): void {\n getRegistry().clear();\n}\n\nexport {\n listNotifications,\n markNotificationRead,\n markAllNotificationsRead,\n deleteNotification,\n countUnread,\n} from \"./store.js\";\n"]}
1
+ {"version":3,"file":"registry.js","sourceRoot":"","sources":["../../src/notifications/registry.ts"],"names":[],"mappings":"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","sourcesContent":["import { z } from \"zod\";\n\nimport { emit as emitBusEvent } from \"../event-bus/bus.js\";\nimport { registerEvent } from \"../event-bus/registry.js\";\nimport type { EventDefinition } from \"../event-bus/types.js\";\nimport { truncate } from \"../shared/truncate.js\";\nimport { insertNotification, updateDeliveredChannels } from \"./store.js\";\nimport {\n NOTIFICATION_SEVERITIES,\n type NotificationChannel,\n type NotificationInput,\n type NotificationMeta,\n type Notification,\n} from \"./types.js\";\n\nexport interface NotificationDeliveryResult {\n notification?: Notification;\n deliveredChannels: string[];\n}\n\nregisterEvent({\n name: \"notification.sent\",\n description:\n \"Fires after notify() delivers to at least one channel. Automations can chain off this — e.g. fan critical notifications to Slack.\",\n payloadSchema: z.object({\n notificationId: z.string().optional(),\n severity: z.enum(NOTIFICATION_SEVERITIES),\n title: z.string(),\n body: z.string().optional(),\n deliveredChannels: z.array(z.string()),\n }) as unknown as EventDefinition[\"payloadSchema\"],\n example: {\n notificationId: \"ntf_abc\",\n severity: \"critical\",\n title: \"Payment failed\",\n body: \"Card ending 4242 declined\",\n deliveredChannels: [\"inbox\", \"webhook\"],\n },\n});\n\nconst REGISTRY_KEY = Symbol.for(\"@agent-native/core/notifications.registry\");\ninterface GlobalWithRegistry {\n [REGISTRY_KEY]?: Map<string, NotificationChannel>;\n}\n\nfunction getRegistry(): Map<string, NotificationChannel> {\n const g = globalThis as unknown as GlobalWithRegistry;\n if (!g[REGISTRY_KEY]) g[REGISTRY_KEY] = new Map();\n return g[REGISTRY_KEY];\n}\n\nexport function registerNotificationChannel(\n channel: NotificationChannel,\n): void {\n if (!channel?.name) {\n throw new Error(\"registerNotificationChannel: channel.name is required\");\n }\n if (typeof channel.deliver !== \"function\") {\n throw new Error(\n \"registerNotificationChannel: channel.deliver must be a function\",\n );\n }\n getRegistry().set(channel.name, channel);\n}\n\nexport function unregisterNotificationChannel(name: string): boolean {\n return getRegistry().delete(name);\n}\n\nexport function listNotificationChannels(): string[] {\n return Array.from(getRegistry().keys());\n}\n\n/**\n * Deliver a notification.\n *\n * The `inbox` channel always persists a row that drives the in-app UI\n * (bell + toast). Additional channels (webhook, custom) run in parallel,\n * best-effort. Returns the stored Notification when `inbox` ran, otherwise\n * `undefined`.\n *\n * Also emits `notification.sent` on the event bus so automations can react\n * to notifications (e.g. \"when a critical notification fires, also page me\").\n */\nconst MAX_TITLE_LEN = 100;\nconst MAX_BODY_LEN = 2000;\n\nexport async function notify(\n input: NotificationInput,\n meta: NotificationMeta,\n): Promise<Notification | undefined> {\n return (await notifyWithDelivery(input, meta)).notification;\n}\n\nexport async function notifyWithDelivery(\n input: NotificationInput,\n meta: NotificationMeta,\n): Promise<NotificationDeliveryResult> {\n if (!meta?.owner) {\n throw new Error(\"notify: meta.owner is required\");\n }\n input = {\n ...input,\n title: truncate(input.title, MAX_TITLE_LEN),\n body: truncate(input.body, MAX_BODY_LEN),\n };\n const channels = selectChannels(input.channels);\n\n // The inbox channel is always included unless explicitly excluded.\n const runInbox = !input.channels || input.channels.includes(\"inbox\");\n const delivered: string[] = [];\n let stored: Notification | undefined;\n\n if (runInbox) {\n try {\n // Stored with just \"inbox\" first; the real delivered list is written\n // after fan-out so a failing webhook doesn't claim it was delivered.\n stored = await insertNotification({\n owner: meta.owner,\n severity: input.severity,\n title: input.title,\n body: input.body,\n metadata: input.metadata,\n deliveredChannels: [\"inbox\"],\n });\n delivered.push(\"inbox\");\n } catch (err) {\n console.error(\"[notifications] inbox persist failed:\", err);\n }\n }\n\n // Await every channel so a 500-ing webhook doesn't end up in `delivered`.\n const results = await Promise.allSettled(\n channels.map(async (channel) => {\n await channel.deliver(input, meta);\n return channel.name;\n }),\n );\n results.forEach((r, i) => {\n if (r.status === \"fulfilled\") {\n delivered.push(r.value);\n } else {\n console.error(\n `[notifications] channel \"${channels[i].name}\" failed:`,\n r.reason,\n );\n }\n });\n\n const hasExtraChannel = delivered.some((c) => c !== \"inbox\");\n if (stored && hasExtraChannel) {\n try {\n await updateDeliveredChannels(stored.id, delivered);\n stored = { ...stored, deliveredChannels: delivered };\n } catch (err) {\n console.error(\"[notifications] delivered-channel update failed:\", err);\n }\n }\n\n // Only emit when at least one channel delivered — an emission with an\n // empty delivery list (and likely a null notificationId) would mislead\n // any automation chaining off this event.\n if (delivered.length > 0) {\n try {\n emitBusEvent(\n \"notification.sent\",\n {\n notificationId: stored?.id,\n severity: input.severity,\n title: input.title,\n body: input.body,\n deliveredChannels: delivered,\n },\n { owner: meta.owner },\n );\n } catch {\n // best-effort\n }\n }\n\n return { notification: stored, deliveredChannels: delivered };\n}\n\nfunction selectChannels(allowlist?: string[]): NotificationChannel[] {\n const registry = getRegistry();\n const all = Array.from(registry.values());\n if (!allowlist) return all;\n return all.filter((c) => allowlist.includes(c.name));\n}\n\n/** Test helper — drops all registered channels. */\nexport function __resetNotificationChannels(): void {\n getRegistry().clear();\n}\n\nexport {\n listNotifications,\n markNotificationRead,\n markAllNotificationsRead,\n deleteNotification,\n countUnread,\n} from \"./store.js\";\n"]}
@@ -15,7 +15,7 @@ export declare function createProgressHandler(): import("h3").EventHandlerWithFe
15
15
  error: string;
16
16
  ok?: undefined;
17
17
  } | {
18
- error?: undefined;
19
18
  ok: boolean;
19
+ error?: undefined;
20
20
  }>>;
21
21
  //# sourceMappingURL=routes.d.ts.map
@@ -49,8 +49,8 @@ export declare function handleUpdateResource(event: any): Promise<import("./stor
49
49
  }>;
50
50
  /** DELETE /_agent-native/resources/:id — delete a resource */
51
51
  export declare function handleDeleteResource(event: any): Promise<{
52
- ok?: undefined;
53
52
  error: string;
53
+ ok?: undefined;
54
54
  } | {
55
55
  error?: undefined;
56
56
  ok: boolean;
@@ -20,5 +20,21 @@
20
20
  * return new Response(wrapWithAnalytics(body), { ... });
21
21
  * ```
22
22
  */
23
+ /**
24
+ * Script hosts the injected GA loader pulls executable code from. Google Tag
25
+ * Manager serves `gtag/js`, and GA4 can lazy-load additional collectors from
26
+ * `www.google-analytics.com`. These must be listed in the document `script-src`
27
+ * so the CSP reflects the code the framework itself injects (see
28
+ * `applyDocumentCsp` in `ssr-handler.ts`).
29
+ */
30
+ export declare const GA_CSP_SCRIPT_HOSTS: readonly ["https://www.googletagmanager.com", "https://www.google-analytics.com"];
31
+ /**
32
+ * The exact JS body (no surrounding `<script>` tags) of the inline gtag config
33
+ * block injected next to the gtag.js loader. Returned so the SSR handler can
34
+ * hash it for the `script-src` CSP directive — the hash must be computed from
35
+ * the identical string that `getGaScript()` embeds, so both call this helper.
36
+ * Returns `null` when GA is not configured.
37
+ */
38
+ export declare function getGaInlineConfigScriptBody(): string | null;
23
39
  export declare function wrapWithAnalytics(body: ReadableStream): ReadableStream;
24
40
  //# sourceMappingURL=analytics.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../src/server/analytics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAwCH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc,CA4BtE"}
1
+ {"version":3,"file":"analytics.d.ts","sourceRoot":"","sources":["../../src/server/analytics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAoBH;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,YAC9B,kCAAkC,EAClC,kCAAkC,CAC1B,CAAC;AAEX;;;;;;GAMG;AACH,wBAAgB,2BAA2B,IAAI,MAAM,GAAG,IAAI,CAc3D;AAaD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,cAAc,GAAG,cAAc,CA4BtE"}
@@ -30,23 +30,46 @@ function getGaMeasurementId() {
30
30
  ? __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__
31
31
  : undefined));
32
32
  }
33
- function getGaScript() {
33
+ /**
34
+ * Script hosts the injected GA loader pulls executable code from. Google Tag
35
+ * Manager serves `gtag/js`, and GA4 can lazy-load additional collectors from
36
+ * `www.google-analytics.com`. These must be listed in the document `script-src`
37
+ * so the CSP reflects the code the framework itself injects (see
38
+ * `applyDocumentCsp` in `ssr-handler.ts`).
39
+ */
40
+ export const GA_CSP_SCRIPT_HOSTS = [
41
+ "https://www.googletagmanager.com",
42
+ "https://www.google-analytics.com",
43
+ ];
44
+ /**
45
+ * The exact JS body (no surrounding `<script>` tags) of the inline gtag config
46
+ * block injected next to the gtag.js loader. Returned so the SSR handler can
47
+ * hash it for the `script-src` CSP directive — the hash must be computed from
48
+ * the identical string that `getGaScript()` embeds, so both call this helper.
49
+ * Returns `null` when GA is not configured.
50
+ */
51
+ export function getGaInlineConfigScriptBody() {
34
52
  const id = getGaMeasurementId();
35
53
  if (!id)
36
54
  return null;
37
- const srcId = encodeURIComponent(id);
38
55
  const jsId = JSON.stringify(id);
39
- return (`<script async src="https://www.googletagmanager.com/gtag/js?id=${srcId}"></script>` +
40
- `<script>` +
41
- `window.dataLayer=window.dataLayer||[];` +
56
+ return (`window.dataLayer=window.dataLayer||[];` +
42
57
  `function gtag(){dataLayer.push(arguments);}` +
43
58
  `gtag('js',new Date());` +
44
59
  `gtag('config',${jsId});` +
45
60
  `if(typeof sessionStorage!=='undefined'&&sessionStorage.getItem('__an_signin')){` +
46
61
  `sessionStorage.removeItem('__an_signin');` +
47
62
  `gtag('event','sign_in');` +
48
- `}` +
49
- `</script>`);
63
+ `}`);
64
+ }
65
+ function getGaScript() {
66
+ const id = getGaMeasurementId();
67
+ if (!id)
68
+ return null;
69
+ const srcId = encodeURIComponent(id);
70
+ const inlineBody = getGaInlineConfigScriptBody();
71
+ return (`<script async src="https://www.googletagmanager.com/gtag/js?id=${srcId}"></script>` +
72
+ `<script>${inlineBody}</script>`);
50
73
  }
51
74
  export function wrapWithAnalytics(body) {
52
75
  const scripts = [getGaScript()].filter(Boolean).join("");
@@ -1 +1 @@
1
- {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../src/server/analytics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,SAAS,sBAAsB,CAAC,KAAyB;IACvD,MAAM,OAAO,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC;IAC9B,OAAO,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AAClC,CAAC;AAED,SAAS,kBAAkB;IACzB,OAAO,CACL,sBAAsB,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;QACrD,sBAAsB,CACpB,OAAO,wCAAwC,KAAK,QAAQ;YAC1D,CAAC,CAAC,wCAAwC;YAC1C,CAAC,CAAC,SAAS,CACd,CACF,CAAC;AACJ,CAAC;AAED,SAAS,WAAW;IAClB,MAAM,EAAE,GAAG,kBAAkB,EAAE,CAAC;IAChC,IAAI,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IACrB,MAAM,KAAK,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAC;IACrC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAChC,OAAO,CACL,kEAAkE,KAAK,aAAa;QACpF,UAAU;QACV,wCAAwC;QACxC,6CAA6C;QAC7C,wBAAwB;QACxB,iBAAiB,IAAI,IAAI;QACzB,iFAAiF;QACjF,2CAA2C;QAC3C,0BAA0B;QAC1B,GAAG;QACH,WAAW,CACZ,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAoB;IACpD,MAAM,OAAO,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzD,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,OAAO,IAAI,CAAC,WAAW,CACrB,IAAI,eAAe,CAAC;QAClB,SAAS,CAAC,KAAK,EAAE,UAAU;YACzB,IAAI,QAAQ,EAAE,CAAC;gBACb,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBAC1B,OAAO;YACT,CAAC;YACD,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YACrD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAC7C,IAAI,YAAY,KAAK,CAAC,CAAC,EAAE,CAAC;gBACxB,MAAM,QAAQ,GACZ,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;gBACnE,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAC7C,QAAQ,GAAG,IAAI,CAAC;YAClB,CAAC;iBAAM,CAAC;gBACN,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;KACF,CAAC,CACH,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Opt-in analytics injection for SSR streams.\n * Supported environment variables:\n * - `GA_MEASUREMENT_ID` — Google Analytics 4 measurement ID\n *\n * Netlify configuration-file env vars are build-time only for serverless\n * functions, so the Vite plugin also bakes this public value into SSR bundles\n * as `__AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__`.\n *\n * Amplitude and Sentry are initialized client-side via their npm packages\n * (see `packages/core/src/client/analytics.ts`). Only GA requires script\n * tag injection because the gtag.js loader must be a `<script src>`.\n *\n * When set, the corresponding script tags are injected before `</head>`.\n * When not set, the stream passes through untouched (zero overhead).\n *\n * Usage in entry.server.tsx:\n * ```ts\n * import { wrapWithAnalytics } from \"@agent-native/core/server\";\n * return new Response(wrapWithAnalytics(body), { ... });\n * ```\n */\n\ndeclare const __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__: string | undefined;\n\nfunction normalizeMeasurementId(value: string | undefined): string | null {\n const trimmed = value?.trim();\n return trimmed ? trimmed : null;\n}\n\nfunction getGaMeasurementId(): string | null {\n return (\n normalizeMeasurementId(process.env.GA_MEASUREMENT_ID) ||\n normalizeMeasurementId(\n typeof __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__ === \"string\"\n ? __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__\n : undefined,\n )\n );\n}\n\nfunction getGaScript(): string | null {\n const id = getGaMeasurementId();\n if (!id) return null;\n const srcId = encodeURIComponent(id);\n const jsId = JSON.stringify(id);\n return (\n `<script async src=\"https://www.googletagmanager.com/gtag/js?id=${srcId}\"></script>` +\n `<script>` +\n `window.dataLayer=window.dataLayer||[];` +\n `function gtag(){dataLayer.push(arguments);}` +\n `gtag('js',new Date());` +\n `gtag('config',${jsId});` +\n `if(typeof sessionStorage!=='undefined'&&sessionStorage.getItem('__an_signin')){` +\n `sessionStorage.removeItem('__an_signin');` +\n `gtag('event','sign_in');` +\n `}` +\n `</script>`\n );\n}\n\nexport function wrapWithAnalytics(body: ReadableStream): ReadableStream {\n const scripts = [getGaScript()].filter(Boolean).join(\"\");\n if (!scripts) return body;\n\n const decoder = new TextDecoder();\n const encoder = new TextEncoder();\n let injected = false;\n\n return body.pipeThrough(\n new TransformStream({\n transform(chunk, controller) {\n if (injected) {\n controller.enqueue(chunk);\n return;\n }\n const text = decoder.decode(chunk, { stream: true });\n const headCloseIdx = text.indexOf(\"</head>\");\n if (headCloseIdx !== -1) {\n const modified =\n text.slice(0, headCloseIdx) + scripts + text.slice(headCloseIdx);\n controller.enqueue(encoder.encode(modified));\n injected = true;\n } else {\n controller.enqueue(chunk);\n }\n },\n }),\n );\n}\n"]}
1
+ {"version":3,"file":"analytics.js","sourceRoot":"","sources":["../../src/server/analytics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,SAAS,sBAAsB,CAAC,KAAyB;IACvD,MAAM,OAAO,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC;IAC9B,OAAO,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AAClC,CAAC;AAED,SAAS,kBAAkB;IACzB,OAAO,CACL,sBAAsB,CAAC,OAAO,CAAC,GAAG,CAAC,iBAAiB,CAAC;QACrD,sBAAsB,CACpB,OAAO,wCAAwC,KAAK,QAAQ;YAC1D,CAAC,CAAC,wCAAwC;YAC1C,CAAC,CAAC,SAAS,CACd,CACF,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,kCAAkC;IAClC,kCAAkC;CAC1B,CAAC;AAEX;;;;;;GAMG;AACH,MAAM,UAAU,2BAA2B;IACzC,MAAM,EAAE,GAAG,kBAAkB,EAAE,CAAC;IAChC,IAAI,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IACrB,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IAChC,OAAO,CACL,wCAAwC;QACxC,6CAA6C;QAC7C,wBAAwB;QACxB,iBAAiB,IAAI,IAAI;QACzB,iFAAiF;QACjF,2CAA2C;QAC3C,0BAA0B;QAC1B,GAAG,CACJ,CAAC;AACJ,CAAC;AAED,SAAS,WAAW;IAClB,MAAM,EAAE,GAAG,kBAAkB,EAAE,CAAC;IAChC,IAAI,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IACrB,MAAM,KAAK,GAAG,kBAAkB,CAAC,EAAE,CAAC,CAAC;IACrC,MAAM,UAAU,GAAG,2BAA2B,EAAE,CAAC;IACjD,OAAO,CACL,kEAAkE,KAAK,aAAa;QACpF,WAAW,UAAU,WAAW,CACjC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAoB;IACpD,MAAM,OAAO,GAAG,CAAC,WAAW,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACzD,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,MAAM,OAAO,GAAG,IAAI,WAAW,EAAE,CAAC;IAClC,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,OAAO,IAAI,CAAC,WAAW,CACrB,IAAI,eAAe,CAAC;QAClB,SAAS,CAAC,KAAK,EAAE,UAAU;YACzB,IAAI,QAAQ,EAAE,CAAC;gBACb,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBAC1B,OAAO;YACT,CAAC;YACD,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC;YACrD,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YAC7C,IAAI,YAAY,KAAK,CAAC,CAAC,EAAE,CAAC;gBACxB,MAAM,QAAQ,GACZ,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,YAAY,CAAC,GAAG,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CAAC;gBACnE,UAAU,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;gBAC7C,QAAQ,GAAG,IAAI,CAAC;YAClB,CAAC;iBAAM,CAAC;gBACN,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAC5B,CAAC;QACH,CAAC;KACF,CAAC,CACH,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Opt-in analytics injection for SSR streams.\n * Supported environment variables:\n * - `GA_MEASUREMENT_ID` — Google Analytics 4 measurement ID\n *\n * Netlify configuration-file env vars are build-time only for serverless\n * functions, so the Vite plugin also bakes this public value into SSR bundles\n * as `__AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__`.\n *\n * Amplitude and Sentry are initialized client-side via their npm packages\n * (see `packages/core/src/client/analytics.ts`). Only GA requires script\n * tag injection because the gtag.js loader must be a `<script src>`.\n *\n * When set, the corresponding script tags are injected before `</head>`.\n * When not set, the stream passes through untouched (zero overhead).\n *\n * Usage in entry.server.tsx:\n * ```ts\n * import { wrapWithAnalytics } from \"@agent-native/core/server\";\n * return new Response(wrapWithAnalytics(body), { ... });\n * ```\n */\n\ndeclare const __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__: string | undefined;\n\nfunction normalizeMeasurementId(value: string | undefined): string | null {\n const trimmed = value?.trim();\n return trimmed ? trimmed : null;\n}\n\nfunction getGaMeasurementId(): string | null {\n return (\n normalizeMeasurementId(process.env.GA_MEASUREMENT_ID) ||\n normalizeMeasurementId(\n typeof __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__ === \"string\"\n ? __AGENT_NATIVE_BUILD_GA_MEASUREMENT_ID__\n : undefined,\n )\n );\n}\n\n/**\n * Script hosts the injected GA loader pulls executable code from. Google Tag\n * Manager serves `gtag/js`, and GA4 can lazy-load additional collectors from\n * `www.google-analytics.com`. These must be listed in the document `script-src`\n * so the CSP reflects the code the framework itself injects (see\n * `applyDocumentCsp` in `ssr-handler.ts`).\n */\nexport const GA_CSP_SCRIPT_HOSTS = [\n \"https://www.googletagmanager.com\",\n \"https://www.google-analytics.com\",\n] as const;\n\n/**\n * The exact JS body (no surrounding `<script>` tags) of the inline gtag config\n * block injected next to the gtag.js loader. Returned so the SSR handler can\n * hash it for the `script-src` CSP directive — the hash must be computed from\n * the identical string that `getGaScript()` embeds, so both call this helper.\n * Returns `null` when GA is not configured.\n */\nexport function getGaInlineConfigScriptBody(): string | null {\n const id = getGaMeasurementId();\n if (!id) return null;\n const jsId = JSON.stringify(id);\n return (\n `window.dataLayer=window.dataLayer||[];` +\n `function gtag(){dataLayer.push(arguments);}` +\n `gtag('js',new Date());` +\n `gtag('config',${jsId});` +\n `if(typeof sessionStorage!=='undefined'&&sessionStorage.getItem('__an_signin')){` +\n `sessionStorage.removeItem('__an_signin');` +\n `gtag('event','sign_in');` +\n `}`\n );\n}\n\nfunction getGaScript(): string | null {\n const id = getGaMeasurementId();\n if (!id) return null;\n const srcId = encodeURIComponent(id);\n const inlineBody = getGaInlineConfigScriptBody();\n return (\n `<script async src=\"https://www.googletagmanager.com/gtag/js?id=${srcId}\"></script>` +\n `<script>${inlineBody}</script>`\n );\n}\n\nexport function wrapWithAnalytics(body: ReadableStream): ReadableStream {\n const scripts = [getGaScript()].filter(Boolean).join(\"\");\n if (!scripts) return body;\n\n const decoder = new TextDecoder();\n const encoder = new TextEncoder();\n let injected = false;\n\n return body.pipeThrough(\n new TransformStream({\n transform(chunk, controller) {\n if (injected) {\n controller.enqueue(chunk);\n return;\n }\n const text = decoder.decode(chunk, { stream: true });\n const headCloseIdx = text.indexOf(\"</head>\");\n if (headCloseIdx !== -1) {\n const modified =\n text.slice(0, headCloseIdx) + scripts + text.slice(headCloseIdx);\n controller.enqueue(encoder.encode(modified));\n injected = true;\n } else {\n controller.enqueue(chunk);\n }\n },\n }),\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"ssr-handler.d.ts","sourceRoot":"","sources":["../../src/server/ssr-handler.ts"],"names":[],"mappings":"AAwCA,OAAO,EACL,yBAAyB,EACzB,gCAAgC,EAChC,yBAAyB,GAC1B,MAAM,4BAA4B,CAAC;AAqVpC;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,2FAgE5E"}
1
+ {"version":3,"file":"ssr-handler.d.ts","sourceRoot":"","sources":["../../src/server/ssr-handler.ts"],"names":[],"mappings":"AA4CA,OAAO,EACL,yBAAyB,EACzB,gCAAgC,EAChC,yBAAyB,GAC1B,MAAM,4BAA4B,CAAC;AAoWpC;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,2FAgE5E"}
@@ -19,6 +19,7 @@ import { defineEventHandler } from "h3";
19
19
  import { createRequestHandler } from "react-router";
20
20
  import { DEFAULT_SSR_CACHE_HEADERS, DEFAULT_SPECULATION_RULES_PATH, } from "../shared/cache-control.js";
21
21
  import { AGENT_NATIVE_SOCIAL_IMAGE_ALT, AGENT_NATIVE_SOCIAL_IMAGE_HEIGHT, AGENT_NATIVE_SOCIAL_IMAGE_PATH, AGENT_NATIVE_SOCIAL_IMAGE_TYPE, AGENT_NATIVE_SOCIAL_IMAGE_WIDTH, withAgentNativeSocialImageCacheBuster, } from "../shared/social-meta.js";
22
+ import { GA_CSP_SCRIPT_HOSTS, getGaInlineConfigScriptBody, } from "./analytics.js";
22
23
  import { getAppBasePathFromViteEnv, stripAppBasePath as canonicalStripAppBasePath, } from "./app-base-path.js";
23
24
  import { runWithRequestContext } from "./request-context.js";
24
25
  import { computeInlineScriptHash } from "./security-headers.js";
@@ -218,13 +219,18 @@ function extractScriptBody(scriptTag) {
218
219
  * user-controlled content reaches the HTML).
219
220
  *
220
221
  * A third directive, `script-src`, is emitted via `Content-Security-Policy-
221
- * Report-Only` rather than enforced. The framework injects one deterministic
222
- * inline script per process (the Sentry config block its hash is computed
223
- * once at process startup from the resolved env vars). Templates additionally
224
- * render a theme-init inline script whose exact content varies by template
225
- * (default theme param, custom docs variant, etc.) and which is rendered by
226
- * React Router, not this handler, so its hash is not available here. Shipping
227
- * script-src as Report-Only surfaces violations without breaking template
222
+ * Report-Only` rather than enforced. The framework injects deterministic inline
223
+ * scripts (the Sentry config block, whose hash is computed once at process
224
+ * startup from the resolved env vars, and — when `GA_MEASUREMENT_ID` is set —
225
+ * the gtag config block, whose hash is derived from the same string
226
+ * `wrapWithAnalytics` embeds). It also loads Google Tag Manager / GA4 from
227
+ * `GA_CSP_SCRIPT_HOSTS`. All of those are listed here so the report-only policy
228
+ * reflects the code the framework itself injects instead of reporting a
229
+ * violation on every page load. Templates additionally render a theme-init
230
+ * inline script whose exact content varies by template (default theme param,
231
+ * custom docs variant, etc.) and which is rendered by React Router, not this
232
+ * handler, so its hash is not available here. Shipping script-src as
233
+ * Report-Only surfaces the remaining violations without breaking template
228
234
  * customisations; teams can graduate to enforcement once their hashes are
229
235
  * enumerated.
230
236
  *
@@ -243,13 +249,23 @@ function applyDocumentCsp(headers, sentryScript) {
243
249
  if (!existing) {
244
250
  headers.set("content-security-policy", "object-src 'none'; base-uri 'self'");
245
251
  }
246
- // script-src as Report-Only: list 'self' plus the hash for the Sentry config
247
- // script the SSR handler injects into every HTML response (the hash is
248
- // computed once from the resolved env vars at process startup). Template
249
- // theme-init hashes are NOT included here see function comment above.
252
+ // script-src as Report-Only: list 'self', the framework-injected inline
253
+ // script hashes (Sentry config + gtag config), and the Google Analytics /
254
+ // Tag Manager loader hosts. These are exactly the scripts the framework
255
+ // itself injects, so listing them keeps the report-only policy from flagging
256
+ // GA on every page load (and keeps it safe to graduate to enforcement).
257
+ // Template theme-init hashes are NOT included here — see function comment.
250
258
  const sentryBody = extractScriptBody(sentryScript);
251
259
  const sentryHash = sentryBody ? computeInlineScriptHash(sentryBody) : null;
252
- const scriptSrcTokens = ["'self'", ...(sentryHash ? [sentryHash] : [])];
260
+ const gaInlineBody = getGaInlineConfigScriptBody();
261
+ const gaHash = gaInlineBody ? computeInlineScriptHash(gaInlineBody) : null;
262
+ const gaHosts = gaInlineBody ? [...GA_CSP_SCRIPT_HOSTS] : [];
263
+ const scriptSrcTokens = [
264
+ "'self'",
265
+ ...(sentryHash ? [sentryHash] : []),
266
+ ...(gaHash ? [gaHash] : []),
267
+ ...gaHosts,
268
+ ];
253
269
  const scriptSrc = `script-src ${scriptSrcTokens.join(" ")}`;
254
270
  const existingRo = headers.get("content-security-policy-report-only") ?? "";
255
271
  if (!existingRo) {