@jami-studio/core 0.92.26 → 0.92.28

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 (66) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +23 -0
  3. package/corpus/core/package.json +2 -1
  4. package/corpus/core/src/deploy/build.ts +56 -2
  5. package/corpus/core/src/event-bus/bus.ts +138 -140
  6. package/corpus/core/src/event-bus/registry.ts +81 -79
  7. package/corpus/core/src/file-upload/registry.ts +135 -125
  8. package/corpus/core/src/notifications/registry.ts +218 -216
  9. package/corpus/core/src/observability/store.ts +1313 -1321
  10. package/corpus/core/src/private-blob/registry.ts +246 -242
  11. package/corpus/core/src/secrets/register.ts +132 -129
  12. package/corpus/core/src/shared/global-scope.ts +75 -0
  13. package/corpus/core/src/shared/init-memo.ts +94 -0
  14. package/corpus/core/src/sharing/registry.ts +193 -194
  15. package/corpus/core/src/tracking/providers.ts +425 -422
  16. package/corpus/core/src/tracking/registry.ts +102 -102
  17. package/dist/collab/routes.d.ts +1 -1
  18. package/dist/deploy/build.d.ts +23 -0
  19. package/dist/deploy/build.d.ts.map +1 -1
  20. package/dist/deploy/build.js +36 -2
  21. package/dist/deploy/build.js.map +1 -1
  22. package/dist/event-bus/bus.d.ts.map +1 -1
  23. package/dist/event-bus/bus.js +8 -6
  24. package/dist/event-bus/bus.js.map +1 -1
  25. package/dist/event-bus/registry.d.ts.map +1 -1
  26. package/dist/event-bus/registry.js +10 -6
  27. package/dist/event-bus/registry.js.map +1 -1
  28. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  29. package/dist/file-upload/registry.d.ts.map +1 -1
  30. package/dist/file-upload/registry.js +28 -8
  31. package/dist/file-upload/registry.js.map +1 -1
  32. package/dist/notifications/registry.d.ts.map +1 -1
  33. package/dist/notifications/registry.js +6 -5
  34. package/dist/notifications/registry.js.map +1 -1
  35. package/dist/observability/routes.d.ts +5 -5
  36. package/dist/observability/store.d.ts +1 -1
  37. package/dist/observability/store.d.ts.map +1 -1
  38. package/dist/observability/store.js +311 -316
  39. package/dist/observability/store.js.map +1 -1
  40. package/dist/private-blob/registry.d.ts.map +1 -1
  41. package/dist/private-blob/registry.js +18 -13
  42. package/dist/private-blob/registry.js.map +1 -1
  43. package/dist/progress/routes.d.ts +1 -1
  44. package/dist/resources/handlers.d.ts +2 -2
  45. package/dist/secrets/register.d.ts.map +1 -1
  46. package/dist/secrets/register.js +11 -7
  47. package/dist/secrets/register.js.map +1 -1
  48. package/dist/secrets/routes.d.ts +9 -9
  49. package/dist/shared/global-scope.d.ts +55 -0
  50. package/dist/shared/global-scope.d.ts.map +1 -0
  51. package/dist/shared/global-scope.js +70 -0
  52. package/dist/shared/global-scope.js.map +1 -0
  53. package/dist/shared/init-memo.d.ts +34 -0
  54. package/dist/shared/init-memo.d.ts.map +1 -0
  55. package/dist/shared/init-memo.js +80 -0
  56. package/dist/shared/init-memo.js.map +1 -0
  57. package/dist/sharing/registry.d.ts.map +1 -1
  58. package/dist/sharing/registry.js +2 -16
  59. package/dist/sharing/registry.js.map +1 -1
  60. package/dist/tracking/providers.d.ts.map +1 -1
  61. package/dist/tracking/providers.js +11 -9
  62. package/dist/tracking/providers.js.map +1 -1
  63. package/dist/tracking/registry.d.ts.map +1 -1
  64. package/dist/tracking/registry.js +5 -5
  65. package/dist/tracking/registry.js.map +1 -1
  66. package/package.json +2 -1
@@ -1,216 +1,218 @@
1
- import { z } from "zod";
2
-
3
- import { emit as emitBusEvent } from "../event-bus/bus.js";
4
- import { registerEvent } from "../event-bus/registry.js";
5
- import type { EventDefinition } from "../event-bus/types.js";
6
- import { truncate } from "../shared/truncate.js";
7
- import { insertNotification, updateDeliveredChannels } from "./store.js";
8
- import {
9
- NOTIFICATION_SEVERITIES,
10
- type NotificationChannel,
11
- type NotificationInput,
12
- type NotificationMeta,
13
- type Notification,
14
- } from "./types.js";
15
-
16
- export interface NotificationDeliveryResult {
17
- notification?: Notification;
18
- deliveredChannels: string[];
19
- }
20
-
21
- registerEvent({
22
- name: "notification.sent",
23
- description:
24
- "Fires after notify() delivers to at least one channel. Automations can chain off this — e.g. fan critical notifications to Slack.",
25
- payloadSchema: z.object({
26
- notificationId: z.string().optional(),
27
- severity: z.enum(NOTIFICATION_SEVERITIES),
28
- title: z.string(),
29
- body: z.string().optional(),
30
- deliveredChannels: z.array(z.string()),
31
- }) as unknown as EventDefinition["payloadSchema"],
32
- example: {
33
- notificationId: "ntf_abc",
34
- severity: "critical",
35
- title: "Payment failed",
36
- body: "Card ending 4242 declined",
37
- deliveredChannels: ["inbox", "webhook"],
38
- },
39
- });
40
-
41
- const REGISTRY_KEY = Symbol.for("@agent-native/core/notifications.registry");
42
- interface GlobalWithRegistry {
43
- [REGISTRY_KEY]?: Map<string, NotificationChannel>;
44
- }
45
-
46
- function getRegistry(): Map<string, NotificationChannel> {
47
- const g = globalThis as unknown as GlobalWithRegistry;
48
- if (!g[REGISTRY_KEY]) g[REGISTRY_KEY] = new Map();
49
- return g[REGISTRY_KEY];
50
- }
51
-
52
- export function registerNotificationChannel(
53
- channel: NotificationChannel,
54
- ): void {
55
- if (!channel?.name) {
56
- throw new Error("registerNotificationChannel: channel.name is required");
57
- }
58
- if (typeof channel.deliver !== "function") {
59
- throw new Error(
60
- "registerNotificationChannel: channel.deliver must be a function",
61
- );
62
- }
63
- getRegistry().set(channel.name, channel);
64
- }
65
-
66
- export function unregisterNotificationChannel(name: string): boolean {
67
- return getRegistry().delete(name);
68
- }
69
-
70
- export function listNotificationChannels(): string[] {
71
- return Array.from(getRegistry().keys());
72
- }
73
-
74
- /**
75
- * Deliver a notification.
76
- *
77
- * The `inbox` channel always persists a row that drives the in-app UI
78
- * (bell + toast). Additional channels (webhook, custom) run in parallel,
79
- * best-effort. Returns the stored Notification when `inbox` ran, otherwise
80
- * `undefined`.
81
- *
82
- * Also emits `notification.sent` on the event bus so automations can react
83
- * to notifications (e.g. "when a critical notification fires, also page me").
84
- */
85
- const MAX_TITLE_LEN = 100;
86
- const MAX_BODY_LEN = 2000;
87
-
88
- export async function notify(
89
- input: NotificationInput,
90
- meta: NotificationMeta,
91
- ): Promise<Notification | undefined> {
92
- return (await notifyWithDelivery(input, meta)).notification;
93
- }
94
-
95
- export async function notifyWithDelivery(
96
- input: NotificationInput,
97
- meta: NotificationMeta,
98
- ): Promise<NotificationDeliveryResult> {
99
- if (!meta?.owner) {
100
- throw new Error("notify: meta.owner is required");
101
- }
102
- input = {
103
- ...input,
104
- title: truncate(input.title, MAX_TITLE_LEN),
105
- body: truncate(input.body, MAX_BODY_LEN),
106
- };
107
- const channels = selectChannels(input.channels);
108
- const storedMetadata = scrubStoredMetadata(input.metadata);
109
-
110
- // The inbox channel is always included unless explicitly excluded.
111
- const runInbox = !input.channels || input.channels.includes("inbox");
112
- const delivered: string[] = [];
113
- let stored: Notification | undefined;
114
-
115
- if (runInbox) {
116
- try {
117
- // Stored with just "inbox" first; the real delivered list is written
118
- // after fan-out so a failing webhook doesn't claim it was delivered.
119
- stored = await insertNotification({
120
- owner: meta.owner,
121
- severity: input.severity,
122
- title: input.title,
123
- body: input.body,
124
- metadata: storedMetadata,
125
- deliveredChannels: ["inbox"],
126
- });
127
- delivered.push("inbox");
128
- } catch (err) {
129
- console.error("[notifications] inbox persist failed:", err);
130
- }
131
- }
132
-
133
- // Await every channel so a 500-ing webhook doesn't end up in `delivered`.
134
- const results = await Promise.allSettled(
135
- channels.map(async (channel) => {
136
- const delivered = await channel.deliver(input, meta);
137
- // Explicit `false` means the channel skipped (no URL / recipients).
138
- if (delivered === false) return null;
139
- return channel.name;
140
- }),
141
- );
142
- results.forEach((r, i) => {
143
- if (r.status === "fulfilled") {
144
- if (r.value) delivered.push(r.value);
145
- } else {
146
- console.error(
147
- `[notifications] channel "${channels[i].name}" failed:`,
148
- r.reason,
149
- );
150
- }
151
- });
152
-
153
- const hasExtraChannel = delivered.some((c) => c !== "inbox");
154
- if (stored && hasExtraChannel) {
155
- try {
156
- await updateDeliveredChannels(stored.id, delivered);
157
- stored = { ...stored, deliveredChannels: delivered };
158
- } catch (err) {
159
- console.error("[notifications] delivered-channel update failed:", err);
160
- }
161
- }
162
-
163
- // Only emit when at least one channel delivered — an emission with an
164
- // empty delivery list (and likely a null notificationId) would mislead
165
- // any automation chaining off this event.
166
- if (delivered.length > 0) {
167
- try {
168
- emitBusEvent(
169
- "notification.sent",
170
- {
171
- notificationId: stored?.id,
172
- severity: input.severity,
173
- title: input.title,
174
- body: input.body,
175
- deliveredChannels: delivered,
176
- },
177
- { owner: meta.owner },
178
- );
179
- } catch {
180
- // best-effort
181
- }
182
- }
183
-
184
- return { notification: stored, deliveredChannels: delivered };
185
- }
186
-
187
- function scrubStoredMetadata(
188
- metadata: Record<string, unknown> | undefined,
189
- ): Record<string, unknown> | undefined {
190
- if (!metadata) return undefined;
191
- const entries = Object.entries(metadata).filter(
192
- ([key]) =>
193
- key !== "delivery" && key !== "webhookUrl" && key !== "slackWebhookUrl",
194
- );
195
- return entries.length ? Object.fromEntries(entries) : undefined;
196
- }
197
-
198
- function selectChannels(allowlist?: string[]): NotificationChannel[] {
199
- const registry = getRegistry();
200
- const all = Array.from(registry.values());
201
- if (!allowlist) return all;
202
- return all.filter((c) => allowlist.includes(c.name));
203
- }
204
-
205
- /** Test helper — drops all registered channels. */
206
- export function __resetNotificationChannels(): void {
207
- getRegistry().clear();
208
- }
209
-
210
- export {
211
- listNotifications,
212
- markNotificationRead,
213
- markAllNotificationsRead,
214
- deleteNotification,
215
- countUnread,
216
- } from "./store.js";
1
+ import { z } from "zod";
2
+
3
+ import { emit as emitBusEvent } from "../event-bus/bus.js";
4
+ import { registerEvent } from "../event-bus/registry.js";
5
+ import type { EventDefinition } from "../event-bus/types.js";
6
+ import { getScopedGlobal } from "../shared/global-scope.js";
7
+ import { truncate } from "../shared/truncate.js";
8
+ import { insertNotification, updateDeliveredChannels } from "./store.js";
9
+ import {
10
+ NOTIFICATION_SEVERITIES,
11
+ type NotificationChannel,
12
+ type NotificationInput,
13
+ type NotificationMeta,
14
+ type Notification,
15
+ } from "./types.js";
16
+
17
+ export interface NotificationDeliveryResult {
18
+ notification?: Notification;
19
+ deliveredChannels: string[];
20
+ }
21
+
22
+ registerEvent({
23
+ name: "notification.sent",
24
+ description:
25
+ "Fires after notify() delivers to at least one channel. Automations can chain off this — e.g. fan critical notifications to Slack.",
26
+ payloadSchema: z.object({
27
+ notificationId: z.string().optional(),
28
+ severity: z.enum(NOTIFICATION_SEVERITIES),
29
+ title: z.string(),
30
+ body: z.string().optional(),
31
+ deliveredChannels: z.array(z.string()),
32
+ }) as unknown as EventDefinition["payloadSchema"],
33
+ example: {
34
+ notificationId: "ntf_abc",
35
+ severity: "critical",
36
+ title: "Payment failed",
37
+ body: "Card ending 4242 declined",
38
+ deliveredChannels: ["inbox", "webhook"],
39
+ },
40
+ });
41
+
42
+ const REGISTRY_BASE_KEY = "agent-native.notifications.registry";
43
+
44
+ // globalThis-pinned so one app's ESM graphs share one channel registry, but
45
+ // scope-aware + lazily resolved so unified workspace deployments (all apps in
46
+ // one isolate) keep per-app channels. See shared/global-scope.
47
+ function getRegistry(): Map<string, NotificationChannel> {
48
+ return getScopedGlobal(
49
+ REGISTRY_BASE_KEY,
50
+ () => new Map<string, NotificationChannel>(),
51
+ );
52
+ }
53
+
54
+ export function registerNotificationChannel(
55
+ channel: NotificationChannel,
56
+ ): void {
57
+ if (!channel?.name) {
58
+ throw new Error("registerNotificationChannel: channel.name is required");
59
+ }
60
+ if (typeof channel.deliver !== "function") {
61
+ throw new Error(
62
+ "registerNotificationChannel: channel.deliver must be a function",
63
+ );
64
+ }
65
+ getRegistry().set(channel.name, channel);
66
+ }
67
+
68
+ export function unregisterNotificationChannel(name: string): boolean {
69
+ return getRegistry().delete(name);
70
+ }
71
+
72
+ export function listNotificationChannels(): string[] {
73
+ return Array.from(getRegistry().keys());
74
+ }
75
+
76
+ /**
77
+ * Deliver a notification.
78
+ *
79
+ * The `inbox` channel always persists a row that drives the in-app UI
80
+ * (bell + toast). Additional channels (webhook, custom) run in parallel,
81
+ * best-effort. Returns the stored Notification when `inbox` ran, otherwise
82
+ * `undefined`.
83
+ *
84
+ * Also emits `notification.sent` on the event bus so automations can react
85
+ * to notifications (e.g. "when a critical notification fires, also page me").
86
+ */
87
+ const MAX_TITLE_LEN = 100;
88
+ const MAX_BODY_LEN = 2000;
89
+
90
+ export async function notify(
91
+ input: NotificationInput,
92
+ meta: NotificationMeta,
93
+ ): Promise<Notification | undefined> {
94
+ return (await notifyWithDelivery(input, meta)).notification;
95
+ }
96
+
97
+ export async function notifyWithDelivery(
98
+ input: NotificationInput,
99
+ meta: NotificationMeta,
100
+ ): Promise<NotificationDeliveryResult> {
101
+ if (!meta?.owner) {
102
+ throw new Error("notify: meta.owner is required");
103
+ }
104
+ input = {
105
+ ...input,
106
+ title: truncate(input.title, MAX_TITLE_LEN),
107
+ body: truncate(input.body, MAX_BODY_LEN),
108
+ };
109
+ const channels = selectChannels(input.channels);
110
+ const storedMetadata = scrubStoredMetadata(input.metadata);
111
+
112
+ // The inbox channel is always included unless explicitly excluded.
113
+ const runInbox = !input.channels || input.channels.includes("inbox");
114
+ const delivered: string[] = [];
115
+ let stored: Notification | undefined;
116
+
117
+ if (runInbox) {
118
+ try {
119
+ // Stored with just "inbox" first; the real delivered list is written
120
+ // after fan-out so a failing webhook doesn't claim it was delivered.
121
+ stored = await insertNotification({
122
+ owner: meta.owner,
123
+ severity: input.severity,
124
+ title: input.title,
125
+ body: input.body,
126
+ metadata: storedMetadata,
127
+ deliveredChannels: ["inbox"],
128
+ });
129
+ delivered.push("inbox");
130
+ } catch (err) {
131
+ console.error("[notifications] inbox persist failed:", err);
132
+ }
133
+ }
134
+
135
+ // Await every channel so a 500-ing webhook doesn't end up in `delivered`.
136
+ const results = await Promise.allSettled(
137
+ channels.map(async (channel) => {
138
+ const delivered = await channel.deliver(input, meta);
139
+ // Explicit `false` means the channel skipped (no URL / recipients).
140
+ if (delivered === false) return null;
141
+ return channel.name;
142
+ }),
143
+ );
144
+ results.forEach((r, i) => {
145
+ if (r.status === "fulfilled") {
146
+ if (r.value) delivered.push(r.value);
147
+ } else {
148
+ console.error(
149
+ `[notifications] channel "${channels[i].name}" failed:`,
150
+ r.reason,
151
+ );
152
+ }
153
+ });
154
+
155
+ const hasExtraChannel = delivered.some((c) => c !== "inbox");
156
+ if (stored && hasExtraChannel) {
157
+ try {
158
+ await updateDeliveredChannels(stored.id, delivered);
159
+ stored = { ...stored, deliveredChannels: delivered };
160
+ } catch (err) {
161
+ console.error("[notifications] delivered-channel update failed:", err);
162
+ }
163
+ }
164
+
165
+ // Only emit when at least one channel delivered — an emission with an
166
+ // empty delivery list (and likely a null notificationId) would mislead
167
+ // any automation chaining off this event.
168
+ if (delivered.length > 0) {
169
+ try {
170
+ emitBusEvent(
171
+ "notification.sent",
172
+ {
173
+ notificationId: stored?.id,
174
+ severity: input.severity,
175
+ title: input.title,
176
+ body: input.body,
177
+ deliveredChannels: delivered,
178
+ },
179
+ { owner: meta.owner },
180
+ );
181
+ } catch {
182
+ // best-effort
183
+ }
184
+ }
185
+
186
+ return { notification: stored, deliveredChannels: delivered };
187
+ }
188
+
189
+ function scrubStoredMetadata(
190
+ metadata: Record<string, unknown> | undefined,
191
+ ): Record<string, unknown> | undefined {
192
+ if (!metadata) return undefined;
193
+ const entries = Object.entries(metadata).filter(
194
+ ([key]) =>
195
+ key !== "delivery" && key !== "webhookUrl" && key !== "slackWebhookUrl",
196
+ );
197
+ return entries.length ? Object.fromEntries(entries) : undefined;
198
+ }
199
+
200
+ function selectChannels(allowlist?: string[]): NotificationChannel[] {
201
+ const registry = getRegistry();
202
+ const all = Array.from(registry.values());
203
+ if (!allowlist) return all;
204
+ return all.filter((c) => allowlist.includes(c.name));
205
+ }
206
+
207
+ /** Test helper — drops all registered channels. */
208
+ export function __resetNotificationChannels(): void {
209
+ getRegistry().clear();
210
+ }
211
+
212
+ export {
213
+ listNotifications,
214
+ markNotificationRead,
215
+ markAllNotificationsRead,
216
+ deleteNotification,
217
+ countUnread,
218
+ } from "./store.js";