@opentray/ext-notification 0.0.0 → 0.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @opentray/ext-notification
2
+
3
+ Official OpenTray notification extension: OS-standard title/body notifications for host-side callers (tray menu actions, background CLIs, moments without a visible page). There is no page bridge; v1 has no activation events, action buttons, inline replies, grouping, or custom sounds.
4
+
5
+ ## Role
6
+
7
+ - `attachNotification(tray)` returns a session-scoped capability: `notify(options)`, `getAuthorizationStatus()`, `requestAuthorization()`, and an async `getBackend()` capabilities snapshot.
8
+ - `notify` is resolve-on-acceptance (the sound law): the promise resolves when the system accepts the notification for delivery; whether and when the user sees it is presentation policy, not acceptance.
9
+ - Payload bounds are frozen platform-independent common contract measured in UTF-16 code units (`String.length`), enforced in the facade before any dispatch, and never silently truncated: `title` non-empty ≤ 64, `body` ≤ 256, `subtitle` ≤ 64; violations reject typed `notification_payload_invalid` (`details: { field, lengthUtf16, limit }`). Unknown `NotifyOptions` fields reject the same way (`details: { field }`) — v1 has no platform namespace and no passthrough. Unknown attach option fields throw instead of being silently ignored.
10
+ - darwin authorization is real: `UNUserNotificationCenter`-backed status reads and requests settle as deferred transactions with a 10 s timeout (`notification_failed`, `details: { reason: "authorization-timeout" }`); a denied state makes `notify` reject typed `notification_denied` (`details: { status }`) with zero delivery calls, and acceptance linearizes against the cached last authorization snapshot for the session (never trusted across sessions).
11
+ - win32 is a documented always-granted degradation: authorization commands answer `granted`/`true` immediately. `notify` rides the consumer's own registered tray icon balloon/toast channel (`Shell_NotifyIcon(NIM_MODIFY, NIF_INFO)` on the existing `(HWND, uID)` registration) — no new window or identity atom; a scope without a live registered icon rejects typed `notification_tray_absent`. One balloon slot per icon: a later notification replaces the earlier one. `silent: true` projects `NIIF_NOSOUND`.
12
+ - win32 subtitle degradation: the facade joins the subtitle into the body as `subtitle + "—" + body` (em dash; a subtitle without a body stays alone), and the joined string is jointly validated against the 256-unit limit — an overflow rejects typed, never truncates.
13
+ - All methods on Linux reject typed `notification_platform_unsupported` before any dispatch.
14
+ - Rejections surface as `NotificationError` with a stable `code` from the frozen five-code family; backend error details pass through as-is. The shared transport-close code surfaces unchanged (the notification family defines no transport alias).
15
+
16
+ The shared schema (types, the frozen bounds, and the typed error family) lives in `@opentray/spec` and is re-exported here.
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pnpm add opentray @opentray/ext-notification
22
+ ```
23
+
24
+ ## Typed Errors
25
+
26
+ | Code | Meaning | Details |
27
+ | ---- | ------- | ------- |
28
+ | `notification_platform_unsupported` | No native notification runtime for this platform (Linux). | `{ kind: "platform", platform }` |
29
+ | `notification_denied` | darwin: authorization is denied; zero delivery calls were made. | `{ status }` |
30
+ | `notification_payload_invalid` | Options shape/bounds failure (unknown field, bad type, empty title, over-limit, joined-body overflow on win32). | `{ field, lengthUtf16?, limit? }` |
31
+ | `notification_tray_absent` | win32 bridge: the command scope has no live registered tray icon channel. | bridge scope payload |
32
+ | `notification_failed` | Native failure or the 10 s darwin authorization timeout. | `{ reason }` (e.g. `"authorization-timeout"`) or an OS error code |
33
+
34
+ ## Platform Matrix
35
+
36
+ | Capability | macOS | Windows |
37
+ | ---------- | ----- | ------- |
38
+ | Channel | `UNUserNotificationCenter` | Registered tray icon `Shell_NotifyIcon(NIM_MODIFY, NIF_INFO)` balloon/toast channel |
39
+ | `authorizationModel` | `user` (`granted`/`denied`/`notDetermined`; deferred, 10 s timeout) | `always-granted` (immediate `granted`/`true`, documented degradation) |
40
+ | `title` / `body` / `subtitle` limits | 64 / 256 / 64 UTF-16 units (platform-independent constants) | 64 / 256 / 64 UTF-16 units |
41
+ | `subtitle` | Native field (`supportsSubtitle: true`) | Joined into body as `subtitle—body` (`supportsSubtitle: false`); joined form ≤ 256 jointly validated |
42
+ | `silent` | Default = platform alert sound; `true` omits it | `NIIF_NOSOUND` when `true` |
43
+ | Concurrency | System notification-center policy | One balloon slot per icon: later replaces earlier (latest-state, not a queue) |
44
+ | Rendering | Notification Center (retention is system policy) | Toast on Win10+, classic balloon before; no action buttons, system-controlled duration |
45
+ | Linux | `unsupported by design` (typed rejection before dispatch) | — |
46
+
47
+ ## Packaging
48
+
49
+ The facade ships one embedded native package: `platforms/<target>/` libraries for darwin-arm64/x64 and win32-arm64/x64 plus the `platforms/manifest.json` identity chain — no `optionalDependencies` platform packages. Consumers never install or locate the native library manually.
50
+
51
+ Consumer guides live in the public `skills/opentray` documentation.
package/contract.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "extensionName": "notification",
3
+ "contractFingerprint": "opentray-ext-notification-contract-1"
4
+ }
@@ -0,0 +1,216 @@
1
+ import { NativeExtensionArtifact, TrayExtensionContext, TrayHandle } from "opentray";
2
+
3
+ //#region ../spec/src/index.d.ts
4
+ /** Frozen notification payload bounds (UTF-16 code units; platform-independent common subset). */
5
+ declare const NOTIFICATION_TITLE_LIMIT_UTF16 = 64;
6
+ declare const NOTIFICATION_BODY_LIMIT_UTF16 = 256;
7
+ declare const NOTIFICATION_SUBTITLE_LIMIT_UTF16 = 64;
8
+ type NotificationAuthorizationStatus = "granted" | "denied" | "notDetermined";
9
+ type NotificationAuthorizationModel = "user" | "always-granted";
10
+ type NotificationChannel = "user-notification-center" | "tray-icon-info";
11
+ /** Shared notification backend capabilities DTO (add-ext-notification design reference section 3). */
12
+ interface NotificationBackendCapabilities {
13
+ platform: "darwin" | "win32";
14
+ authorizationModel: NotificationAuthorizationModel;
15
+ channel: NotificationChannel;
16
+ titleLimitUtf16: typeof NOTIFICATION_TITLE_LIMIT_UTF16;
17
+ bodyLimitUtf16: typeof NOTIFICATION_BODY_LIMIT_UTF16;
18
+ subtitleLimitUtf16: typeof NOTIFICATION_SUBTITLE_LIMIT_UTF16;
19
+ supportsSubtitle: boolean;
20
+ }
21
+ /** Returns true when an unknown value is a complete notification backend capabilities DTO. */
22
+ declare const isNotificationBackendCapabilities: (value: unknown) => value is NotificationBackendCapabilities;
23
+ /** Frozen notification typed-error code set (add-ext-notification design reference section 3). */
24
+ declare const NOTIFICATION_ERROR_CODES: {
25
+ readonly platformUnsupported: "notification_platform_unsupported";
26
+ readonly denied: "notification_denied";
27
+ readonly payloadInvalid: "notification_payload_invalid";
28
+ readonly trayAbsent: "notification_tray_absent";
29
+ readonly failed: "notification_failed";
30
+ };
31
+ type NotificationErrorCode = (typeof NOTIFICATION_ERROR_CODES)[keyof typeof NOTIFICATION_ERROR_CODES];
32
+ declare const isNotificationErrorCode: (value: string) => value is NotificationErrorCode;
33
+ //#endregion
34
+ //#region src/shared.d.ts
35
+ type NotificationPlatform = "darwin" | "win32" | "linux";
36
+ type NotificationNativePlatform = "darwin" | "win32";
37
+ interface NotifyOptions {
38
+ /** Required, non-empty, at most 64 UTF-16 code units (platform-independent common contract). */
39
+ title: string;
40
+ /** At most 256 UTF-16 code units. */
41
+ body?: string;
42
+ /**
43
+ * Distinct native field on darwin. win32 documented degradation: the facade
44
+ * joins it into the body as a prefix (combined post-join limit 256 UTF-16
45
+ * code units; never truncated). At most 64 UTF-16 code units.
46
+ */
47
+ subtitle?: string;
48
+ /** Default `false` (platform default alert sound); `true` means no sound. */
49
+ silent?: boolean;
50
+ }
51
+ /** Wire form of a notify payload after facade preflight (`silent` always explicit). */
52
+ interface NotifyWireOptions {
53
+ title: string;
54
+ body?: string;
55
+ subtitle?: string;
56
+ silent: boolean;
57
+ }
58
+ /** Wire command surface of the notification extension (camelCase `ext-command`
59
+ * payloads). `notify` carries its fields FLAT next to `type` — the exact shape
60
+ * the native `NotificationCommand` serde tag and the broker bridge decode
61
+ * (implementation review I3a P0: a nested `{options: {...}}` wrapper never
62
+ * reached a decoder). */
63
+ type NotificationCommand = {
64
+ type: "getBackend";
65
+ } | ({
66
+ type: "notify";
67
+ } & NotifyWireOptions) | {
68
+ type: "getAuthorizationStatus";
69
+ } | {
70
+ type: "requestAuthorization";
71
+ };
72
+ /** Wire event that answers the `getBackend` command on the immediate path. */
73
+ interface NotificationBackendEvent {
74
+ type: "backend";
75
+ backend: NotificationBackendCapabilities;
76
+ }
77
+ /** Authorization read result: the `getAuthorizationStatus` answer. */
78
+ interface NotificationAuthorizationEvent {
79
+ type: "authorization";
80
+ status: NotificationAuthorizationStatus;
81
+ }
82
+ /** Authorization request decision: the `requestAuthorization` answer. */
83
+ interface NotificationAuthorizationDecisionEvent {
84
+ type: "authorizationDecision";
85
+ granted: boolean;
86
+ }
87
+ /**
88
+ * Facade-originated details payloads (broker-originated details pass through
89
+ * unchanged: `notification_denied` carries `{status}`; `notification_failed`
90
+ * carries `{reason}` or an OS error code; the win32 bridge's
91
+ * `notification_tray_absent` carries its own scope payload).
92
+ */
93
+ type NotificationErrorDetails = {
94
+ kind: "platform";
95
+ platform: string;
96
+ } | {
97
+ field: string;
98
+ lengthUtf16?: number;
99
+ limit?: number;
100
+ };
101
+ interface NotificationErrorDescriptor {
102
+ code: NotificationErrorCode;
103
+ message: string;
104
+ details?: NotificationErrorDetails;
105
+ }
106
+ /**
107
+ * Typed rejection of the notification facade: facade preflight failures
108
+ * (platform, payload bounds/shape) and normalized broker/native notification
109
+ * rejections. Consumers match on `code`; the human `message` is not a
110
+ * contract. The shared transport-close code is surfaced unchanged (the frozen
111
+ * notification family has no transport alias; core never branches on
112
+ * extension names).
113
+ */
114
+ declare class NotificationError extends Error {
115
+ readonly code: NotificationErrorCode;
116
+ readonly details?: unknown;
117
+ constructor(code: NotificationErrorCode, message: string, options?: {
118
+ details?: unknown;
119
+ cause?: unknown;
120
+ });
121
+ }
122
+ declare const notificationErrorFromDescriptor: (descriptor: NotificationErrorDescriptor, options?: {
123
+ cause?: unknown;
124
+ }) => NotificationError;
125
+ declare const platformUnsupported: (platform: string) => NotificationErrorDescriptor;
126
+ declare const isNotificationBackendEvent: (value: unknown) => value is NotificationBackendEvent;
127
+ declare const isNotificationAuthorizationEvent: (value: unknown) => value is NotificationAuthorizationEvent;
128
+ declare const isNotificationAuthorizationDecisionEvent: (value: unknown) => value is NotificationAuthorizationDecisionEvent;
129
+ /**
130
+ * win32 documented degradation (design reference section 1): the subtitle
131
+ * prefixes the body through a single em dash; a subtitle without a body —
132
+ * or with an EMPTY-string body — is the subtitle alone (never a dangling
133
+ * separator; implementation review I3a P1 parity with the crate's
134
+ * `win32_joined_body`). The joined string is what the combined 256-unit
135
+ * limit measures.
136
+ */
137
+ declare const joinWin32Body: (subtitle: string, body: string | undefined) => string;
138
+ declare const validateNotifyOptions: (options: unknown, platform: NotificationNativePlatform) => NotificationErrorDescriptor | null;
139
+ /** Apply the frozen wire projection: `silent` explicit; win32 joins the subtitle into `body` (no `subtitle` on the win32 wire). */
140
+ declare const notifyCommandOptions: (options: NotifyOptions, platform: NotificationNativePlatform) => NotifyWireOptions;
141
+ //#endregion
142
+ //#region src/native-artifact.d.ts
143
+ declare const NOTIFICATION_NATIVE_ARTIFACT: {
144
+ kind: "embedded";
145
+ packageJsonUrl: string;
146
+ contractManifestUrl: string;
147
+ targets: {
148
+ "darwin-arm64": {
149
+ libraryPath: string;
150
+ };
151
+ "darwin-x64": {
152
+ libraryPath: string;
153
+ };
154
+ "win32-arm64": {
155
+ libraryPath: string;
156
+ };
157
+ "win32-x64": {
158
+ libraryPath: string;
159
+ };
160
+ };
161
+ };
162
+ //#endregion
163
+ //#region src/index.d.ts
164
+ interface NotificationExtensionOptions {
165
+ mountId?: string;
166
+ artifact?: NativeExtensionArtifact;
167
+ /** Test/seam override for the host platform; Linux rejects typed before any dispatch. */
168
+ platform?: NotificationPlatform;
169
+ }
170
+ /**
171
+ * Public notification capability (design reference section 1): host-side OS
172
+ * notifications plus the authorization surface. `notify` is
173
+ * resolve-on-acceptance (the sound law): whether and when the user actually
174
+ * sees the notification is not part of the acceptance semantics. On darwin
175
+ * the authorization commands carry real UNUserNotificationCenter semantics;
176
+ * on win32 they are documented always-granted degradations answered
177
+ * Immediate — the facade accepts both settle shapes for either platform.
178
+ */
179
+ interface NotificationCapability {
180
+ notify(options: NotifyOptions): Promise<void>;
181
+ getAuthorizationStatus(): Promise<NotificationAuthorizationStatus>;
182
+ /** darwin: real decision. win32: documented degradation, always `true`. */
183
+ requestAuthorization(): Promise<boolean>;
184
+ getBackend(): Promise<NotificationBackendCapabilities>;
185
+ }
186
+ declare const attachNotification: (tray: TrayHandle, options?: NotificationExtensionOptions) => NotificationCapability;
187
+ declare const NotificationExt: {
188
+ name: string;
189
+ artifact: {
190
+ kind: "embedded";
191
+ packageJsonUrl: string;
192
+ contractManifestUrl: string;
193
+ targets: {
194
+ "darwin-arm64": {
195
+ libraryPath: string;
196
+ };
197
+ "darwin-x64": {
198
+ libraryPath: string;
199
+ };
200
+ "win32-arm64": {
201
+ libraryPath: string;
202
+ };
203
+ "win32-x64": {
204
+ libraryPath: string;
205
+ };
206
+ };
207
+ };
208
+ resolveMount(options: NotificationExtensionOptions | undefined): {
209
+ artifact?: NativeExtensionArtifact;
210
+ mountId?: string;
211
+ };
212
+ extend(tray: TrayHandle, context: TrayExtensionContext, options: NotificationExtensionOptions | undefined): NotificationCapability;
213
+ };
214
+ //#endregion
215
+ export { type NOTIFICATION_BODY_LIMIT_UTF16, NOTIFICATION_ERROR_CODES, NOTIFICATION_NATIVE_ARTIFACT, type NOTIFICATION_SUBTITLE_LIMIT_UTF16, type NOTIFICATION_TITLE_LIMIT_UTF16, type NotificationAuthorizationDecisionEvent, type NotificationAuthorizationEvent, type NotificationAuthorizationModel, type NotificationAuthorizationStatus, type NotificationBackendCapabilities, type NotificationBackendEvent, NotificationCapability, type NotificationChannel, type NotificationCommand, NotificationError, type NotificationErrorCode, type NotificationErrorDescriptor, type NotificationErrorDetails, NotificationExt, NotificationExtensionOptions, type NotificationNativePlatform, type NotificationPlatform, type NotifyOptions, type NotifyWireOptions, attachNotification, isNotificationAuthorizationDecisionEvent, isNotificationAuthorizationEvent, type isNotificationBackendCapabilities, isNotificationBackendEvent, isNotificationErrorCode, joinWin32Body, notificationErrorFromDescriptor, type notifyCommandOptions, type platformUnsupported, validateNotifyOptions };
216
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../spec/src/index.ts","../src/shared.ts","../src/native-artifact.ts","../src/index.ts"],"mappings":"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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,276 @@
1
+ import { BrokerServerError, ExtensionOperationError, NativeExtensionEmbeddedArtifactError } from "opentray";
2
+ //#region src/native-artifact.ts
3
+ const NOTIFICATION_NATIVE_ARTIFACT = {
4
+ kind: "embedded",
5
+ packageJsonUrl: new URL("../package.json", import.meta.url).href,
6
+ contractManifestUrl: new URL("../contract.json", import.meta.url).href,
7
+ targets: {
8
+ "darwin-arm64": { libraryPath: "platforms/darwin-arm64/libopentray_ext_notification.dylib" },
9
+ "darwin-x64": { libraryPath: "platforms/darwin-x64/libopentray_ext_notification.dylib" },
10
+ "win32-arm64": { libraryPath: "platforms/win32-arm64/opentray_ext_notification.dll" },
11
+ "win32-x64": { libraryPath: "platforms/win32-x64/opentray_ext_notification.dll" }
12
+ }
13
+ };
14
+ Object.freeze({
15
+ notification: Object.freeze({
16
+ darwin: "Glass",
17
+ win32: "SystemAsterisk"
18
+ }),
19
+ warning: Object.freeze({
20
+ darwin: "Sosumi",
21
+ win32: "SystemExclamation"
22
+ }),
23
+ error: Object.freeze({
24
+ darwin: "Basso",
25
+ win32: "SystemHand"
26
+ })
27
+ });
28
+ new Set(Object.values({
29
+ platformUnsupported: "sound_platform_unsupported",
30
+ notFound: "sound_not_found",
31
+ formatUnsupported: "sound_format_unsupported",
32
+ fileUnreadable: "sound_file_unreadable"
33
+ }));
34
+ /** Returns true when an unknown value is a complete notification backend capabilities DTO. */
35
+ const isNotificationBackendCapabilities = (value) => {
36
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
37
+ const record = value;
38
+ return (record.platform === "darwin" || record.platform === "win32") && (record.authorizationModel === "user" || record.authorizationModel === "always-granted") && (record.channel === "user-notification-center" || record.channel === "tray-icon-info") && record.titleLimitUtf16 === 64 && record.bodyLimitUtf16 === 256 && record.subtitleLimitUtf16 === 64 && typeof record.supportsSubtitle === "boolean";
39
+ };
40
+ /** Frozen notification typed-error code set (add-ext-notification design reference section 3). */
41
+ const NOTIFICATION_ERROR_CODES = {
42
+ platformUnsupported: "notification_platform_unsupported",
43
+ denied: "notification_denied",
44
+ payloadInvalid: "notification_payload_invalid",
45
+ trayAbsent: "notification_tray_absent",
46
+ failed: "notification_failed"
47
+ };
48
+ const isNotificationErrorCode = (value) => Object.values(NOTIFICATION_ERROR_CODES).includes(value);
49
+ //#endregion
50
+ //#region src/shared.ts
51
+ /**
52
+ * Typed rejection of the notification facade: facade preflight failures
53
+ * (platform, payload bounds/shape) and normalized broker/native notification
54
+ * rejections. Consumers match on `code`; the human `message` is not a
55
+ * contract. The shared transport-close code is surfaced unchanged (the frozen
56
+ * notification family has no transport alias; core never branches on
57
+ * extension names).
58
+ */
59
+ var NotificationError = class extends Error {
60
+ code;
61
+ details;
62
+ constructor(code, message, options = {}) {
63
+ super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
64
+ this.name = "NotificationError";
65
+ this.code = code;
66
+ if (options.details !== void 0) this.details = options.details;
67
+ }
68
+ };
69
+ const notificationErrorFromDescriptor = (descriptor, options = {}) => new NotificationError(descriptor.code, descriptor.message, {
70
+ ...descriptor.details === void 0 ? {} : { details: descriptor.details },
71
+ ...options.cause === void 0 ? {} : { cause: options.cause }
72
+ });
73
+ const platformUnsupported = (platform) => ({
74
+ code: NOTIFICATION_ERROR_CODES.platformUnsupported,
75
+ message: `native notifications are unsupported on ${platform} (Linux has no native implementation; add-ext-notification design reference section 0)`,
76
+ details: {
77
+ kind: "platform",
78
+ platform
79
+ }
80
+ });
81
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
82
+ const AUTHORIZATION_STATUSES = [
83
+ "granted",
84
+ "denied",
85
+ "notDetermined"
86
+ ];
87
+ const isNotificationBackendEvent = (value) => isRecord(value) && value.type === "backend" && isNotificationBackendCapabilities(value.backend);
88
+ const isNotificationAuthorizationEvent = (value) => isRecord(value) && value.type === "authorization" && typeof value.status === "string" && AUTHORIZATION_STATUSES.includes(value.status);
89
+ const isNotificationAuthorizationDecisionEvent = (value) => isRecord(value) && value.type === "authorizationDecision" && typeof value.granted === "boolean";
90
+ const NOTIFY_FIELDS = [
91
+ "title",
92
+ "body",
93
+ "subtitle",
94
+ "silent"
95
+ ];
96
+ const payloadInvalid = (reason, field, lengthUtf16, limit) => ({
97
+ code: NOTIFICATION_ERROR_CODES.payloadInvalid,
98
+ message: field === void 0 ? `invalid notify options: ${reason}` : `invalid notify options (${field}): ${reason}`,
99
+ ...field === void 0 ? {} : { details: {
100
+ field,
101
+ ...lengthUtf16 === void 0 ? {} : { lengthUtf16 },
102
+ ...limit === void 0 ? {} : { limit }
103
+ } }
104
+ });
105
+ /**
106
+ * win32 documented degradation (design reference section 1): the subtitle
107
+ * prefixes the body through a single em dash; a subtitle without a body —
108
+ * or with an EMPTY-string body — is the subtitle alone (never a dangling
109
+ * separator; implementation review I3a P1 parity with the crate's
110
+ * `win32_joined_body`). The joined string is what the combined 256-unit
111
+ * limit measures.
112
+ */
113
+ const joinWin32Body = (subtitle, body) => body === void 0 || body === "" ? subtitle : `${subtitle}—${body}`;
114
+ const validateNotifyOptions = (options, platform) => {
115
+ if (!isRecord(options)) return payloadInvalid("options must be an object");
116
+ for (const key of Object.keys(options)) if (!NOTIFY_FIELDS.includes(key)) return payloadInvalid("unknown field", key);
117
+ if (typeof options.title !== "string") return payloadInvalid("must be a string", "title");
118
+ if (options.title.length === 0) return payloadInvalid("must be non-empty", "title", 0, 64);
119
+ if (options.title.length > 64) return payloadInvalid(`exceeds the 64 UTF-16 code unit limit`, "title", options.title.length, 64);
120
+ if (options.body !== void 0) {
121
+ if (typeof options.body !== "string") return payloadInvalid("must be a string when present", "body");
122
+ if (options.body.length > 256) return payloadInvalid(`exceeds the 256 UTF-16 code unit limit`, "body", options.body.length, 256);
123
+ }
124
+ if (options.subtitle !== void 0) {
125
+ if (typeof options.subtitle !== "string") return payloadInvalid("must be a string when present", "subtitle");
126
+ if (options.subtitle.length > 64) return payloadInvalid(`exceeds the 64 UTF-16 code unit limit`, "subtitle", options.subtitle.length, 64);
127
+ }
128
+ if (options.silent !== void 0 && typeof options.silent !== "boolean") return payloadInvalid("must be a boolean when present", "silent");
129
+ if (platform === "win32" && options.subtitle !== void 0) {
130
+ const joined = joinWin32Body(options.subtitle, options.body);
131
+ if (joined.length > 256) return payloadInvalid(`the joined subtitle-prefixed body exceeds the 256 UTF-16 code unit limit (win32 subtitle degradation)`, "body", joined.length, 256);
132
+ }
133
+ return null;
134
+ };
135
+ /** Apply the frozen wire projection: `silent` explicit; win32 joins the subtitle into `body` (no `subtitle` on the win32 wire). */
136
+ const notifyCommandOptions = (options, platform) => {
137
+ if (platform === "win32" && options.subtitle !== void 0) {
138
+ const joined = joinWin32Body(options.subtitle, options.body);
139
+ return {
140
+ title: options.title,
141
+ body: joined,
142
+ silent: options.silent === true
143
+ };
144
+ }
145
+ return {
146
+ title: options.title,
147
+ ...options.body !== void 0 ? { body: options.body } : {},
148
+ ...options.subtitle !== void 0 ? { subtitle: options.subtitle } : {},
149
+ silent: options.silent === true
150
+ };
151
+ };
152
+ //#endregion
153
+ //#region src/index.ts
154
+ const NOTIFICATION_EXTENSION_NAME = "notification";
155
+ const ATTACH_OPTION_KEYS = /* @__PURE__ */ new Set([
156
+ "mountId",
157
+ "artifact",
158
+ "platform"
159
+ ]);
160
+ const attachNotification = (tray, options = {}) => {
161
+ const unknown = Object.keys(options).find((key) => !ATTACH_OPTION_KEYS.has(key));
162
+ if (unknown !== void 0) throw new TypeError(`attachNotification received an unknown option field: ${unknown} (v1 accepts mountId, artifact, platform)`);
163
+ return tray.extend(NotificationExt, options);
164
+ };
165
+ const NotificationExt = {
166
+ name: NOTIFICATION_EXTENSION_NAME,
167
+ artifact: NOTIFICATION_NATIVE_ARTIFACT,
168
+ resolveMount(options) {
169
+ return {
170
+ ...options?.mountId === void 0 ? {} : { mountId: options.mountId },
171
+ ...options?.artifact === void 0 ? {} : { artifact: options.artifact }
172
+ };
173
+ },
174
+ extend(tray, context, options) {
175
+ return createNotificationCapability(context, options?.platform ?? process.platform);
176
+ }
177
+ };
178
+ function createNotificationCapability(context, platform) {
179
+ const nativePlatform = platform === "darwin" || platform === "win32" ? platform : void 0;
180
+ const requirePlatform = () => {
181
+ if (nativePlatform === void 0) throw notificationErrorFromDescriptor(platformUnsupported(platform));
182
+ return nativePlatform;
183
+ };
184
+ const mapTransportError = (error) => {
185
+ if (error instanceof NotificationError) return error;
186
+ if (error instanceof NativeExtensionEmbeddedArtifactError && error.reason === "target-unsupported") return new NotificationError(NOTIFICATION_ERROR_CODES.platformUnsupported, `native notification extension ships no library for this platform: ${error.message}`, {
187
+ details: {
188
+ kind: "platform",
189
+ platform
190
+ },
191
+ cause: error
192
+ });
193
+ if ((error instanceof ExtensionOperationError || error instanceof BrokerServerError) && isNotificationErrorCode(error.code)) return new NotificationError(error.code, error.message, {
194
+ ...error.details === void 0 ? {} : { details: error.details },
195
+ cause: error
196
+ });
197
+ return error;
198
+ };
199
+ const dispatch = async (command) => {
200
+ try {
201
+ return await context.request(command);
202
+ } catch (error) {
203
+ throw mapTransportError(error);
204
+ }
205
+ };
206
+ const dispatchImmediate = async (command) => {
207
+ const result = await dispatch(command);
208
+ if (result.kind !== "immediate") throw new Error(`notification extension command ${command.type} settles on the immediate path (add-ext-notification design reference sections 1-2; only the authorization commands defer); received ${result.kind}`);
209
+ return result;
210
+ };
211
+ /**
212
+ * Transport-agnostic authorization settle (design reference section 4):
213
+ * darwin completes through the DeferredOperation terminal whose result
214
+ * value carries the event shape; win32 answers Immediate with the same
215
+ * event shape in the events array. Either resolution path is accepted for
216
+ * either platform — the facade cannot and must not tell them apart.
217
+ */
218
+ const runAuthorizationCommand = async (command, guard, label) => {
219
+ const result = await dispatch(command);
220
+ if (result.kind === "terminal") {
221
+ if (!guard(result.value)) throw new Error(`${label} deferred terminal payload violated the wire contract`);
222
+ return result.value;
223
+ }
224
+ for (const event of result.events) {
225
+ const data = event?.data;
226
+ if (guard(data)) return data;
227
+ }
228
+ throw new Error(`${label} settled without an authorization payload (wire contract violation; expected a deferred terminal or an immediate authorization event)`);
229
+ };
230
+ const backendContractError = () => /* @__PURE__ */ new Error("notification extension answered getBackend without a backend capabilities payload (wire contract violation)");
231
+ const expectBackendPlatform = (backend, native) => {
232
+ if (backend.platform !== native) throw new Error(`notification backend reported platform ${backend.platform} on a ${native} host (wire contract violation)`);
233
+ return backend;
234
+ };
235
+ /** Frozen immutable snapshot: the record is deeply plain, so a shallow freeze is total. */
236
+ const freezeBackend = (backend) => Object.freeze({ ...backend });
237
+ let backendPromise;
238
+ const ensureBackend = (native) => {
239
+ backendPromise ??= (async () => {
240
+ const result = await dispatchImmediate({ type: "getBackend" });
241
+ for (const event of result.events) {
242
+ const data = event?.data;
243
+ if (isNotificationBackendEvent(data)) return freezeBackend(expectBackendPlatform(data.backend, native));
244
+ }
245
+ throw backendContractError();
246
+ })().catch((error) => {
247
+ backendPromise = void 0;
248
+ throw mapTransportError(error);
249
+ });
250
+ return backendPromise;
251
+ };
252
+ return {
253
+ notify: async (options) => {
254
+ const native = requirePlatform();
255
+ const issue = validateNotifyOptions(options, native);
256
+ if (issue !== null) throw notificationErrorFromDescriptor(issue);
257
+ await dispatchImmediate({
258
+ type: "notify",
259
+ ...notifyCommandOptions(options, native)
260
+ });
261
+ },
262
+ getAuthorizationStatus: async () => {
263
+ requirePlatform();
264
+ return (await runAuthorizationCommand({ type: "getAuthorizationStatus" }, isNotificationAuthorizationEvent, "getAuthorizationStatus")).status;
265
+ },
266
+ requestAuthorization: async () => {
267
+ requirePlatform();
268
+ return (await runAuthorizationCommand({ type: "requestAuthorization" }, isNotificationAuthorizationDecisionEvent, "requestAuthorization")).granted;
269
+ },
270
+ getBackend: async () => ensureBackend(requirePlatform())
271
+ };
272
+ }
273
+ //#endregion
274
+ export { NOTIFICATION_ERROR_CODES, NOTIFICATION_NATIVE_ARTIFACT, NotificationError, NotificationExt, attachNotification, isNotificationAuthorizationDecisionEvent, isNotificationAuthorizationEvent, isNotificationBackendEvent, isNotificationErrorCode, joinWin32Body, notificationErrorFromDescriptor, validateNotifyOptions };
275
+
276
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/native-artifact.ts","../../spec/src/index.ts","../src/shared.ts","../src/index.ts"],"sourcesContent":["// Orthogonal intents (2026-09-18; add-ext-notification batch C):\n// 1. Declare the official notification platform library catalog as\n// platform-neutral data shipped inside this facade package (design\n// reference section 3, the archived dialog/sound embedded packaging law).\n// 2. Keep the embedded identity chain (facade package.json + contract.json)\n// relative to this facade instead of consumer cwd.\n\nimport type { NativeExtensionEmbeddedArtifact } from \"opentray\";\n\nexport const NOTIFICATION_NATIVE_ARTIFACT = {\n kind: \"embedded\",\n packageJsonUrl: new URL(\"../package.json\", import.meta.url).href,\n contractManifestUrl: new URL(\"../contract.json\", import.meta.url).href,\n targets: {\n \"darwin-arm64\": {\n libraryPath: \"platforms/darwin-arm64/libopentray_ext_notification.dylib\",\n },\n \"darwin-x64\": {\n libraryPath: \"platforms/darwin-x64/libopentray_ext_notification.dylib\",\n },\n \"win32-arm64\": {\n libraryPath: \"platforms/win32-arm64/opentray_ext_notification.dll\",\n },\n \"win32-x64\": {\n libraryPath: \"platforms/win32-x64/opentray_ext_notification.dll\",\n },\n },\n} satisfies NativeExtensionEmbeddedArtifact;\n","export type SessionId = string;\nexport type RequestId = string;\nexport type AppId = string;\nexport type TrayId = string;\nexport type MenuItemId = number;\n\n// Multi-webview orchestration, message-channel, and canonical JSON protocol\n// contracts (add-webview-orchestration); re-exported flat to keep one public\n// entry surface for consumers.\nexport * from \"./canonical-json\";\nexport * from \"./channel\";\nexport * from \"./webview\";\n\n/**\n * Protocol 2 (add-ext-dialog section 5.1) adds the DeferredOperation transaction\n * frames `ext-command-accepted` and `ext-operation-terminal`. The bump is one\n * matrix: Node client, broker, socket endpoint, and ready metadata all carry 2,\n * and a version-1 Init is rejected as incompatible (mirrors the Rust\n * `opentray-spec` PROTOCOL_VERSION truth).\n */\nexport const PROTOCOL_VERSION = 2;\nexport const OPENTRAY_PROTOCOL_FAMILY = \"opentray-protocol\";\n\n/**\n * Frozen wire form of a deferred operation id: exactly 16 lowercase hex\n * digits (the u64 handle's hex projection, add-ext-dialog design reference\n * section 5.7 ruling #2). Shared parser truth for `ext-command-accepted` and\n * `ext-operation-terminal`; empty, uppercase, non-hex, or wrong-length ids\n * are structurally invalid.\n */\nexport const OPERATION_ID_PATTERN = /^[0-9a-f]{16}$/u;\n\n/** Returns true when an unknown value matches the frozen 16-hex operation id form. */\nexport const isOperationId = (value: unknown): value is string =>\n typeof value === \"string\" && OPERATION_ID_PATTERN.test(value);\nexport const OPENTRAY_PROTOCOL_LINE_MAJOR = 1;\nexport const OPENTRAY_PROTOCOL_LINE_MINOR = 1;\n\nexport const protocolLineReleaseChannels = [\"stable\", \"alpha\"] as const;\nexport type ProtocolLineReleaseChannel = (typeof protocolLineReleaseChannels)[number];\n\nexport interface OpenTrayProtocolLine {\n family: typeof OPENTRAY_PROTOCOL_FAMILY;\n major: number;\n minor: number;\n}\n\nexport interface ProtocolDistTagOptions {\n channel: ProtocolLineReleaseChannel;\n major?: number;\n minor?: number;\n}\n\nexport interface ProtocolDistTag {\n channel: ProtocolLineReleaseChannel;\n major: number;\n minor: number;\n}\n\nexport const OPENTRAY_PROTOCOL_LINE: OpenTrayProtocolLine = {\n family: OPENTRAY_PROTOCOL_FAMILY,\n major: OPENTRAY_PROTOCOL_LINE_MAJOR,\n minor: OPENTRAY_PROTOCOL_LINE_MINOR,\n};\n\n// Install-time protocol lines can advance by minor version while runtime authority stays numeric.\nexport const compareOpenTrayProtocolLine = (\n left: OpenTrayProtocolLine,\n right: OpenTrayProtocolLine,\n): number => {\n assertOpenTrayProtocolLine(left, \"left\");\n assertOpenTrayProtocolLine(right, \"right\");\n if (left.major !== right.major) {\n return left.major - right.major;\n }\n return left.minor - right.minor;\n};\n\nexport const isOpenTrayProtocolLineCompatible = (\n supported: OpenTrayProtocolLine,\n required: OpenTrayProtocolLine,\n): boolean => {\n assertOpenTrayProtocolLine(supported, \"supported\");\n assertOpenTrayProtocolLine(required, \"required\");\n return supported.major === required.major && supported.minor >= required.minor;\n};\n\nexport const formatOpenTrayProtocolLine = ({\n family,\n major,\n minor,\n}: OpenTrayProtocolLine = OPENTRAY_PROTOCOL_LINE): string => {\n assertProtocolLineVersion(major, \"major\");\n assertProtocolLineVersion(minor, \"minor\");\n return `${family}/${major}.${minor}`;\n};\n\nexport const isProtocolLineReleaseChannel = (value: string): value is ProtocolLineReleaseChannel =>\n protocolLineReleaseChannels.includes(value as ProtocolLineReleaseChannel);\n\nexport const formatProtocolDistTag = ({\n channel,\n major = OPENTRAY_PROTOCOL_LINE_MAJOR,\n minor = OPENTRAY_PROTOCOL_LINE_MINOR,\n}: ProtocolDistTagOptions): string => {\n assertProtocolLineReleaseChannel(channel);\n assertProtocolLineVersion(major, \"major\");\n assertProtocolLineVersion(minor, \"minor\");\n return `${channel}-${major}-${minor}`;\n};\n\nexport const parseProtocolDistTag = (tag: string): ProtocolDistTag => {\n const match = /^(?<channel>[a-z]+)-(?<major>\\d+)-(?<minor>\\d+)$/u.exec(tag);\n const groups = match?.groups;\n if (groups === undefined) {\n throw new Error(`invalid OpenTray protocol dist-tag: ${tag}`);\n }\n const channel = groups.channel;\n if (channel === undefined || !isProtocolLineReleaseChannel(channel)) {\n throw new Error(`unsupported OpenTray protocol dist-tag channel: ${channel ?? \"\"}`);\n }\n const major = Number(groups.major);\n const minor = Number(groups.minor);\n assertProtocolLineVersion(major, \"major\");\n assertProtocolLineVersion(minor, \"minor\");\n return { channel, major, minor };\n};\n\n/**\n * Neutral caller label used when no usable caller identity can be derived.\n * Keeps the broker honest instead of impersonating an unrelated application.\n */\nexport const DEFAULT_CALLER_LABEL = \"opentray\";\n\n/**\n * Maximum length of a sanitized caller label. Keeps socket paths, runtime\n * directory names, and process titles within platform limits.\n */\nexport const CALLER_LABEL_MAX_LENGTH = 48;\n\nconst callerLabelAllowedPattern = /[a-z0-9-]+/g;\n\nexport interface BrokerEndpointIdentity {\n packageVersion: string;\n protocolVersion: number;\n callerLabel: string;\n}\n\nexport interface BrokerEndpointIdentityOptions {\n packageVersion: string;\n protocolVersion?: number;\n callerLabel?: string;\n}\n\nexport const sanitizeCallerLabel = (value: string | undefined): string => {\n if (value !== undefined && typeof value !== \"string\") {\n throw new Error(`callerLabel must be a string: ${String(value)}`);\n }\n const raw = value ?? \"\";\n const lowered = raw.toLowerCase();\n const segments = lowered.match(callerLabelAllowedPattern);\n if (segments === null || segments.length === 0) {\n return DEFAULT_CALLER_LABEL;\n }\n const joined = segments.join(\"-\");\n const trimmed = joined.replace(/^-+|-+$/gu, \"\");\n if (trimmed.length === 0) {\n return DEFAULT_CALLER_LABEL;\n }\n return trimmed.slice(0, CALLER_LABEL_MAX_LENGTH);\n};\n\nexport const createBrokerEndpointIdentity = ({\n packageVersion,\n protocolVersion = PROTOCOL_VERSION,\n callerLabel,\n}: BrokerEndpointIdentityOptions): BrokerEndpointIdentity => {\n assertEndpointComponent(packageVersion, \"packageVersion\");\n if (!Number.isInteger(protocolVersion) || protocolVersion <= 0) {\n throw new Error(`protocolVersion must be a positive integer: ${protocolVersion}`);\n }\n\n return {\n packageVersion,\n protocolVersion,\n callerLabel: sanitizeCallerLabel(callerLabel),\n };\n};\n\nexport const isSupportedProtocolVersion = (protocolVersion: number): boolean =>\n protocolVersion === PROTOCOL_VERSION;\n\nexport const formatBrokerEndpointName = (identity: BrokerEndpointIdentity): string => {\n assertEndpointIdentity(identity);\n return `opentray-${identity.packageVersion}-p${identity.protocolVersion}-${identity.callerLabel}`;\n};\n\nexport const formatBrokerStateRoot = (\n homeDir: string,\n identity: BrokerEndpointIdentity,\n): string => {\n assertEndpointIdentity(identity);\n if (homeDir.length === 0) {\n throw new Error(\"homeDir must not be empty\");\n }\n\n const normalizedHome = homeDir.replace(/[\\\\/]+$/u, \"\");\n return `${normalizedHome}/.opentray/${identity.packageVersion}/${identity.callerLabel}`;\n};\n\nexport const formatUnixSocketPath = (homeDir: string, identity: BrokerEndpointIdentity): string =>\n `${formatBrokerStateRoot(homeDir, identity)}/opentray-p${identity.protocolVersion}.sock`;\n\nexport const formatWindowsPipeName = (identity: BrokerEndpointIdentity): string =>\n `\\\\\\\\.\\\\pipe\\\\${formatBrokerEndpointName(identity)}`;\n\n/**\n * Human-readable process title for a broker pinned to a caller. Used by the SDK\n * spawn path so task managers show the owning application, not a generic name.\n */\nexport const formatBrokerProcessTitle = (identity: BrokerEndpointIdentity): string => {\n assertEndpointIdentity(identity);\n return `opentray · ${identity.callerLabel}`;\n};\n\nconst endpointComponentPattern = /^[0-9A-Za-z._+-]+$/u;\n\nconst assertOpenTrayProtocolLine = (line: OpenTrayProtocolLine, name: string): void => {\n assertProtocolLineVersion(line.major, `${name}.major`);\n assertProtocolLineVersion(line.minor, `${name}.minor`);\n};\n\nconst assertProtocolLineReleaseChannel = (channel: string): void => {\n if (!isProtocolLineReleaseChannel(channel)) {\n throw new Error(`unsupported OpenTray protocol release channel: ${channel}`);\n }\n};\n\nconst assertProtocolLineVersion = (value: number, name: string): void => {\n if (!Number.isInteger(value) || value < 0) {\n throw new Error(`protocol line ${name} must be a non-negative integer: ${value}`);\n }\n};\n\nconst assertEndpointIdentity = (identity: BrokerEndpointIdentity): void => {\n assertEndpointComponent(identity.packageVersion, \"packageVersion\");\n if (!Number.isInteger(identity.protocolVersion) || identity.protocolVersion <= 0) {\n throw new Error(`protocolVersion must be a positive integer: ${identity.protocolVersion}`);\n }\n};\n\nconst assertEndpointComponent = (value: string, name: string): void => {\n if (value.length === 0) {\n throw new Error(`${name} must not be empty`);\n }\n if (!endpointComponentPattern.test(value)) {\n throw new Error(`${name} contains invalid endpoint characters: ${value}`);\n }\n};\n\nexport interface AppOptions {\n id?: AppId;\n name?: string;\n appIcon?: AppIcon;\n default?: boolean;\n}\n\nexport interface AppRef {\n appId: AppId;\n}\n\nexport interface AppIdentity {\n appId: AppId;\n appName: string;\n appIcon?: AppIcon;\n appIconVariant?: string;\n}\n\nexport interface TrayOptions {\n id: string;\n tooltip?: Tooltip;\n icon?: Icon;\n menu?: Menu;\n}\n\nexport interface Tooltip {\n title: string;\n description: string;\n}\n\nexport interface Menu {\n items: MenuItem[];\n}\n\nexport type MenuItem =\n | {\n type: \"item\";\n id: MenuItemId;\n title: string;\n primaryEvent?: boolean;\n enabled?: boolean;\n shortcut?: string;\n }\n | {\n type: \"check\";\n id: MenuItemId;\n title: string;\n enabled?: boolean;\n checked?: boolean;\n }\n | {\n type: \"radio\";\n id: MenuItemId;\n title: string;\n enabled?: boolean;\n checked?: boolean;\n group: number;\n }\n | {\n type: \"separator\";\n }\n | {\n type: \"submenu\";\n title: string;\n enabled?: boolean;\n items: MenuItem[];\n };\n\nexport type IconImage =\n | { type: \"rgba\"; data: Uint8Array | number[]; width: number; height: number }\n | { type: \"encoded\"; data: Uint8Array | number[] }\n | { type: \"file\"; path: string };\n\n/** A source carrying one platform-native application icon file. */\nexport type AppIconSource =\n | { type: \"encoded\"; data: Uint8Array | readonly number[] }\n | { type: \"file\"; path: string };\n\n/** One or more application-defined semantic names for a native App icon asset. */\nexport type AppIconVariant = string | readonly string[];\n\ninterface AppIconVariantField {\n /** Omission is equivalent to the canonical `default` variant. */\n variant?: AppIconVariant;\n}\n\n/** One standards-compliant application icon asset for a specific operating system. */\nexport type AppIconAsset = AppIconVariantField &\n (\n | { platform: \"darwin\"; format: \"icns\"; source: AppIconSource }\n | { platform: \"windows\"; format: \"ico\"; source: AppIconSource }\n | {\n platform: \"linux\";\n format: \"png\";\n size: number;\n source: AppIconSource;\n }\n | { platform: \"linux\"; format: \"svg\"; source: AppIconSource }\n );\n\n/** Cross-platform application identity assets; tray `Icon` is a separate contract. */\nexport type AppIcon = readonly AppIconAsset[];\n\ntype AppIconAssetVariantName<TAsset> = TAsset extends {\n readonly variant: infer TVariant;\n}\n ? TVariant extends readonly string[]\n ? TVariant[number]\n : TVariant extends string\n ? TVariant\n : never\n : never;\n\n/** Literal variant names declared by an `as const` AppIcon catalog. */\nexport type AppIconVariantOf<TCatalog extends AppIcon> =\n | \"default\"\n | AppIconAssetVariantName<TCatalog[number]>;\n\nexport type SimpleIcon = IconImage & { text?: string };\n\nexport type IconText = IconImage & { text: string };\n\nexport type DarwinIcon = IconImage & { isTemplate?: boolean };\n\nexport type Win32Icon = IconImage;\n\nexport type LinuxIcon = IconImage;\n\nexport type DarwinIconText = DarwinIcon & { text: string };\n\nexport interface IconCandidates {\n \"icon-only\"?: IconImage;\n \"text-only\"?: string;\n \"icon-text\"?: IconText;\n \"darwin-icon-only\"?: DarwinIcon;\n \"darwin-icon-text\"?: DarwinIconText;\n \"win32-icon-only\"?: Win32Icon;\n \"win32-icon-text\"?: IconText;\n \"linux-icon-only\"?: LinuxIcon;\n \"linux-icon-text\"?: IconText;\n}\n\nexport type Icon = IconCandidates & Partial<SimpleIcon>;\n\nexport interface Rect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\n\nexport type TrayBoundsKind = \"native\" | \"inferred\" | \"unavailable\";\n\nexport interface TrayBoundsResult {\n kind: TrayBoundsKind;\n source: string;\n rect: Rect | null;\n}\n\nexport type MouseButton = \"left\" | \"right\" | \"middle\";\n\n/** Application-scoped lifecycle intent emitted by a native runtime carrier. */\nexport type AppEvent = { type: \"reopenRequested\"; appId: AppId };\n\nexport type TrayEvent =\n | { type: \"ready\"; appId: AppId }\n | { type: \"menuClick\"; appId: AppId; trayId: TrayId; itemId: MenuItemId }\n | {\n type: \"trayClick\";\n appId: AppId;\n trayId: TrayId;\n button: MouseButton;\n x: number;\n y: number;\n }\n | {\n type: \"trayDoubleClick\";\n appId: AppId;\n trayId: TrayId;\n button: MouseButton;\n x: number;\n y: number;\n };\n\nexport interface ExtensionScope {\n appId: AppId;\n trayId?: TrayId;\n ext: string;\n}\n\n/**\n * Broker-injected command ownership (add-ext-dialog section 5.5): the host derives\n * `{ appId, trayId, sessionId, instanceGeneration }` for every command\n * dispatch and extensions must never self-report it. All busy/operation\n * registries key on this scope; the deferred operation binding is\n * `(sessionId, instanceGeneration, operationId)`.\n */\nexport interface CommandScope {\n appId: AppId;\n trayId: TrayId;\n sessionId: SessionId;\n instanceGeneration: number;\n}\n\nexport interface ExtensionEnvelope<TData = unknown> {\n scope: ExtensionScope;\n /**\n * Present only on the command dispatch path: the broker-injected ownership\n * scope for this invocation. Event envelopes never carry it.\n */\n commandScope?: CommandScope;\n data: TData;\n}\n\n/**\n * Typed extension error envelope (add-ext-dialog section 7.5): `{ code, message,\n * details }` with a discriminated `details` JSON shape shared by the Rust\n * `ExtensionError::Detailed` projection, server error frames, and the Node\n * typed error factory. Consumers must match on `code`; parsing the human\n * `message` is forbidden by contract.\n */\nexport interface TypedExtensionError {\n code: string;\n message: string;\n /** Discriminated JSON payload whose shape each error code freezes. Absent for codes without structured detail. */\n details?: unknown;\n}\n\n/**\n * Terminal payload of a deferred operation (add-ext-dialog section 5.1 frozen): the\n * `result` branch resolves the client promise with `value`; the `error`\n * branch rejects it with a typed extension error. The cancel path is a\n * `result` payload isomorphic to user cancellation — there is no third\n * channel.\n */\nexport type ExtOperationPayload =\n | { kind: \"result\"; value: unknown }\n | { kind: \"error\"; error: TypedExtensionError };\n\n/**\n * Shared ingress bound for one extension event/terminal record (64 KiB). The\n * value is the single source of truth for both the EventPort record bound and\n * the DeferredPort terminal payload bound; the Rust `opentray-spec` crate\n * exports the same number (Rust/TS fixture parity).\n */\nexport const EXTENSION_EVENT_RECORD_MAX_BYTES = 64 * 1024;\n\nexport interface ExtensionArtifactTarget {\n os: string;\n arch: string;\n}\n\nexport interface ExpectedExtensionIdentity {\n extensionName: string;\n artifactSetVersion: string;\n contractFingerprint: string;\n target: ExtensionArtifactTarget;\n /**\n * Optional embedded-artifact identity-chain inputs (add-ext-dialog section 6.4):\n * lowercase hex SHA-256 of the resolved library file, verified by the broker\n * before `dlopen`. Absent means the caller supplied no byte hash\n * (registry-era identity only).\n */\n sha256?: string;\n /**\n * Optional build identity carried by the embedded staging manifest; the\n * broker compares it against the native manifest between `Library::new` and\n * `init` when provided.\n */\n buildIdentity?: string;\n}\n\n/** Operating-system and architecture identity for one broker executable. */\nexport interface BrokerArtifactTarget {\n os: \"darwin\" | \"linux\" | \"win32\";\n arch: \"arm64\" | \"x64\";\n}\n\n/** Content-derived identity used to decide whether a live broker may be reused. */\nexport interface BrokerArtifactIdentity {\n packageVersion: string;\n target: BrokerArtifactTarget;\n executableHash: string;\n buildIdentity: string;\n}\n\n/** Caller-scoped readiness evidence written by a successfully initialized broker. */\nexport interface BrokerReadyMetadata {\n pid: number;\n endpoint: string;\n packageVersion: string;\n protocolVersion: number;\n appId: AppId;\n appName: string;\n callerLabel: string;\n executablePath: string;\n brokerArtifactIdentity: BrokerArtifactIdentity;\n}\n\nexport interface RuntimeHostSessionHealth {\n sessionId: number;\n internalSessionId?: SessionId;\n initialized: boolean;\n}\n\nexport interface RuntimeHostHealth {\n pid: number;\n packageVersion: string;\n protocolVersion: number;\n endpoint: string;\n appId: AppId;\n appName: string;\n callerLabel: string;\n sessionCount: number;\n sessions: RuntimeHostSessionHealth[];\n}\n\nexport type ClientFrame =\n | { type: \"init\"; protocolVersion: number; clientVersion: string }\n | ClientRequestFrame\n | { type: \"exit\" };\n\nexport type ClientRequestFrame =\n | ({ type: \"create-app\"; requestId: RequestId } & AppOptions)\n | { type: \"resolve-default-app\"; requestId: RequestId }\n | { type: \"get-app-identity\"; requestId: RequestId; appId: AppId }\n | {\n type: \"set-app-name\";\n requestId: RequestId;\n appId: AppId;\n name: string;\n }\n | {\n type: \"set-app-icon\";\n requestId: RequestId;\n appId: AppId;\n appIcon: AppIcon | null;\n }\n | {\n type: \"set-app-icon-variant\";\n requestId: RequestId;\n appId: AppId;\n variant: string;\n }\n | {\n type: \"create-tray\";\n requestId: RequestId;\n app: AppRef;\n tray: TrayOptions;\n }\n | { type: \"destroy-tray\"; requestId: RequestId; appId: AppId; trayId: TrayId }\n | {\n type: \"get-tray-bounds\";\n requestId: RequestId;\n appId: AppId;\n trayId: TrayId;\n }\n | {\n type: \"set-tray-menu\";\n requestId: RequestId;\n appId: AppId;\n trayId: TrayId;\n menu: Menu;\n }\n | {\n type: \"set-tray-icon\";\n requestId: RequestId;\n appId: AppId;\n trayId: TrayId;\n icon: Icon;\n }\n | {\n type: \"set-tray-tooltip\";\n requestId: RequestId;\n appId: AppId;\n trayId: TrayId;\n tooltip: Tooltip;\n }\n | {\n type: \"load-ext\";\n requestId: RequestId;\n appId: AppId;\n name: string;\n path: string;\n expectedIdentity: ExpectedExtensionIdentity;\n mountId?: string;\n }\n | {\n type: \"ext-command\";\n requestId: RequestId;\n appId: AppId;\n trayId: TrayId;\n ext: string;\n data: unknown;\n }\n | { type: \"unload-ext\"; requestId: RequestId; appId: AppId; name: string }\n | { type: \"health\"; requestId: RequestId };\n\nexport type ServerFrame =\n | {\n type: \"ready\";\n protocolVersion: number;\n brokerVersion: string;\n brokerArtifactIdentity: BrokerArtifactIdentity;\n sessionId: SessionId;\n }\n | { type: \"app-created\"; requestId: RequestId; app: AppRef }\n | { type: \"default-app\"; requestId: RequestId; app: AppRef }\n | {\n type: \"app-identity\";\n requestId: RequestId;\n identity: AppIdentity;\n }\n | { type: \"tray-created\"; requestId: RequestId; appId: AppId; trayId: TrayId }\n | {\n type: \"tray-bounds\";\n requestId: RequestId;\n appId: AppId;\n trayId: TrayId;\n bounds: TrayBoundsResult;\n }\n | { type: \"ack\"; requestId: RequestId }\n | {\n type: \"ext-command-result\";\n requestId: RequestId;\n events: ExtensionEnvelope[];\n }\n | {\n /**\n * DeferredOperation acceptance (section 5.3): the command did not complete\n * inside the dispatch; the client keeps the request pending until the\n * matching `ext-operation-terminal` (or a transport-close rejection).\n */\n type: \"ext-command-accepted\";\n requestId: RequestId;\n operationId: string;\n }\n | {\n /**\n * The single terminal frame of a deferred operation. Exactly one is\n * delivered per accepted operation; duplicates are dropped by the\n * broker's owner loop before any frame is written.\n */\n type: \"ext-operation-terminal\";\n operationId: string;\n payload: ExtOperationPayload;\n }\n | {\n type: \"runtime-host-health\";\n requestId: RequestId;\n health: RuntimeHostHealth;\n }\n | { type: \"event\"; event: TrayEvent }\n | { type: \"app-event\"; event: AppEvent }\n | {\n type: \"ext-event\";\n appId: AppId;\n trayId: TrayId;\n ext: string;\n data: unknown;\n }\n | {\n /**\n * Synchronous broker error frame. Carries the same typed error envelope\n * as a deferred terminal's `error` branch (add-ext-dialog design\n * reference section 7.5): when the error code freezes a structured\n * payload it rides `details` as a JSON object; the field is absent for\n * codes without structured detail (never `null`).\n */\n type: \"error\";\n requestId?: RequestId;\n code: string;\n message: string;\n details?: unknown;\n };\n\nexport interface ParseResult<T> {\n ok: boolean;\n frame?: T;\n error?: string;\n}\n\nexport const parseServerFrame = (line: string): ParseResult<ServerFrame> => {\n try {\n const value: unknown = JSON.parse(line);\n if (!isServerFrame(value)) {\n return { ok: false, error: \"invalid server frame\" };\n }\n return { ok: true, frame: value };\n } catch (error) {\n return {\n ok: false,\n error: error instanceof Error ? error.message : \"unknown parse error\",\n };\n }\n};\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\n/** Returns true when an unknown value is a complete broker artifact identity. */\nexport const isBrokerArtifactIdentity = (value: unknown): value is BrokerArtifactIdentity =>\n isRecord(value) &&\n typeof value.packageVersion === \"string\" &&\n isRecord(value.target) &&\n (value.target.os === \"darwin\" || value.target.os === \"linux\" || value.target.os === \"win32\") &&\n (value.target.arch === \"arm64\" || value.target.arch === \"x64\") &&\n typeof value.executableHash === \"string\" &&\n /^[a-f0-9]{64}$/u.test(value.executableHash) &&\n typeof value.buildIdentity === \"string\" &&\n value.buildIdentity.length > 0;\n\n/** Compares every compatibility-bearing broker artifact identity field. */\nexport const brokerArtifactIdentityEquals = (\n left: BrokerArtifactIdentity,\n right: BrokerArtifactIdentity,\n): boolean =>\n left.packageVersion === right.packageVersion &&\n left.target.os === right.target.os &&\n left.target.arch === right.target.arch &&\n left.executableHash === right.executableHash &&\n left.buildIdentity === right.buildIdentity;\n\nexport const isServerFrame = (value: unknown): value is ServerFrame => {\n if (!isRecord(value) || typeof value.type !== \"string\") {\n return false;\n }\n\n switch (value.type) {\n case \"ready\":\n return (\n typeof value.protocolVersion === \"number\" &&\n typeof value.brokerVersion === \"string\" &&\n isBrokerArtifactIdentity(value.brokerArtifactIdentity) &&\n typeof value.sessionId === \"string\"\n );\n case \"app-created\":\n return typeof value.requestId === \"string\" && isRecord(value.app);\n case \"default-app\":\n return typeof value.requestId === \"string\" && isRecord(value.app);\n case \"app-identity\":\n return typeof value.requestId === \"string\" && isRecord(value.identity);\n case \"tray-created\":\n return (\n typeof value.requestId === \"string\" &&\n typeof value.appId === \"string\" &&\n typeof value.trayId === \"string\"\n );\n case \"tray-bounds\":\n return (\n typeof value.requestId === \"string\" &&\n typeof value.appId === \"string\" &&\n typeof value.trayId === \"string\" &&\n isTrayBoundsResult(value.bounds)\n );\n case \"ack\":\n return typeof value.requestId === \"string\";\n case \"ext-command-result\":\n return (\n typeof value.requestId === \"string\" &&\n Array.isArray(value.events) &&\n value.events.every(isExtensionEnvelope)\n );\n case \"ext-command-accepted\":\n return (\n typeof value.requestId === \"string\" && isOperationId(value.operationId)\n );\n case \"ext-operation-terminal\":\n return isOperationId(value.operationId) && isExtOperationPayload(value.payload);\n case \"runtime-host-health\":\n return typeof value.requestId === \"string\" && isRuntimeHostHealth(value.health);\n case \"event\":\n return isTrayEvent(value.event);\n case \"app-event\":\n return isAppEvent(value.event);\n case \"ext-event\":\n return (\n typeof value.appId === \"string\" &&\n typeof value.trayId === \"string\" &&\n typeof value.ext === \"string\"\n );\n case \"error\":\n return (\n (value.requestId === undefined || typeof value.requestId === \"string\") &&\n typeof value.code === \"string\" &&\n typeof value.message === \"string\" &&\n // details is a JSON object when present (absent for codes without\n // structured detail); nulls and scalars are structurally invalid.\n (value.details === undefined || isRecord(value.details))\n );\n default:\n return false;\n }\n};\n\nconst isAppEvent = (value: unknown): value is AppEvent =>\n isRecord(value) &&\n value.type === \"reopenRequested\" &&\n typeof value.appId === \"string\";\n\nconst isExtensionEnvelope = (value: unknown): value is ExtensionEnvelope => {\n if (!isRecord(value) || !isRecord(value.scope)) {\n return false;\n }\n const scope = value.scope as Record<string, unknown>;\n return (\n typeof scope.appId === \"string\" &&\n (scope.trayId === undefined || typeof scope.trayId === \"string\") &&\n typeof scope.ext === \"string\" &&\n (value.commandScope === undefined || isCommandScope(value.commandScope)) &&\n \"data\" in value\n );\n};\n\n/** Returns true when an unknown value is a complete broker-injected command scope. */\nexport const isCommandScope = (value: unknown): value is CommandScope =>\n isRecord(value) &&\n typeof value.appId === \"string\" &&\n typeof value.trayId === \"string\" &&\n typeof value.sessionId === \"string\" &&\n typeof value.instanceGeneration === \"number\" &&\n Number.isInteger(value.instanceGeneration) &&\n value.instanceGeneration >= 0;\n\n/** Returns true when an unknown value is a complete typed extension error envelope. */\nexport const isTypedExtensionError = (value: unknown): value is TypedExtensionError =>\n isRecord(value) &&\n typeof value.code === \"string\" &&\n typeof value.message === \"string\" &&\n // details is a JSON object when present (absent for codes without\n // structured detail); nulls, scalars, and arrays are structurally invalid\n // on the synchronous error frame and on deferred terminal errors alike.\n (value.details === undefined || isRecord(value.details));\n\n/** Parser truth for the frozen terminal payload discriminant (no third branch is guessed). */\nexport const isExtOperationPayload = (value: unknown): value is ExtOperationPayload => {\n if (!isRecord(value)) {\n return false;\n }\n switch (value.kind) {\n case \"result\":\n return \"value\" in value;\n case \"error\":\n return isTypedExtensionError(value.error);\n default:\n return false;\n }\n};\n\nconst isRuntimeHostHealth = (value: unknown): value is RuntimeHostHealth => {\n if (!isRecord(value)) {\n return false;\n }\n\n return (\n typeof value.pid === \"number\" &&\n typeof value.packageVersion === \"string\" &&\n typeof value.protocolVersion === \"number\" &&\n typeof value.endpoint === \"string\" &&\n typeof value.appId === \"string\" &&\n typeof value.appName === \"string\" &&\n typeof value.callerLabel === \"string\" &&\n typeof value.sessionCount === \"number\" &&\n Array.isArray(value.sessions) &&\n value.sessions.every(isRuntimeHostSessionHealth)\n );\n};\n\nconst isRuntimeHostSessionHealth = (value: unknown): value is RuntimeHostSessionHealth =>\n isRecord(value) &&\n typeof value.sessionId === \"number\" &&\n (value.internalSessionId === undefined || typeof value.internalSessionId === \"string\") &&\n typeof value.initialized === \"boolean\";\n\nconst isRect = (value: unknown): value is Rect =>\n isRecord(value) &&\n typeof value.x === \"number\" &&\n typeof value.y === \"number\" &&\n typeof value.width === \"number\" &&\n typeof value.height === \"number\";\n\nconst isTrayBoundsResult = (value: unknown): value is TrayBoundsResult =>\n isRecord(value) &&\n (value.kind === \"native\" || value.kind === \"inferred\" || value.kind === \"unavailable\") &&\n typeof value.source === \"string\" &&\n (value.rect === null || isRect(value.rect));\n\nconst isTrayEvent = (value: unknown): value is TrayEvent => {\n if (!isRecord(value) || typeof value.type !== \"string\") {\n return false;\n }\n\n switch (value.type) {\n case \"ready\":\n return typeof value.appId === \"string\";\n case \"menuClick\":\n return (\n typeof value.appId === \"string\" &&\n typeof value.trayId === \"string\" &&\n typeof value.itemId === \"number\"\n );\n case \"trayClick\":\n case \"trayDoubleClick\":\n return (\n typeof value.appId === \"string\" &&\n typeof value.trayId === \"string\" &&\n isMouseButton(value.button) &&\n typeof value.x === \"number\" &&\n typeof value.y === \"number\"\n );\n default:\n return false;\n }\n};\n\nconst isMouseButton = (value: unknown): value is MouseButton =>\n value === \"left\" || value === \"right\" || value === \"middle\";\n\n// ---------------------------------------------------------------------------\n// Sound extension shared schema (add-ext-sound design reference sections 1-2,\n// task 2.1): the schema is promoted to this shared spec so `@opentray/ext-sound`\n// and the `opentray-spec` crate project the same DTO truth. The frozen\n// common-name table and the typed error family live here so facade, native,\n// and broker rejections stay code-identical across the wire.\n// ---------------------------------------------------------------------------\n\n/**\n * Alert-grade beep kinds (`beep` only, add-ext-sound design reference section\n * 1.1). Alert grading and named system sounds are two separate semantic\n * layers: `default`/`info`/`question` never enter the common system sound\n * catalog.\n */\nexport type BeepKind = \"default\" | \"info\" | \"warning\" | \"error\" | \"question\";\n\n/**\n * Common system sound names (add-ext-sound R1 section 4.1, frozen catalog:\n * no additions or removals in v1).\n */\nexport type CommonSystemSoundName = \"notification\" | \"warning\" | \"error\";\n\n/** Frozen common-name catalog in wire order; the facade consults it before any native passthrough. */\nexport const COMMON_SYSTEM_SOUND_NAMES: readonly CommonSystemSoundName[] = [\n \"notification\",\n \"warning\",\n \"error\",\n];\n\n/**\n * Frozen per-platform native projection of each common name (add-ext-sound\n * design reference section 1.2): darwin `NSSound` system catalog names and\n * win32 registry sound-scheme aliases (`SND_ALIAS`). Both records are frozen;\n * a changed projection is a contract change, not a data edit.\n */\nexport const COMMON_SYSTEM_SOUND_PROJECTIONS: Readonly<\n Record<CommonSystemSoundName, { readonly darwin: string; readonly win32: string }>\n> = Object.freeze({\n notification: Object.freeze({ darwin: \"Glass\", win32: \"SystemAsterisk\" }),\n warning: Object.freeze({ darwin: \"Sosumi\", win32: \"SystemExclamation\" }),\n error: Object.freeze({ darwin: \"Basso\", win32: \"SystemHand\" }),\n});\n\n/** Returns true when an unknown value is one of the frozen common system sound names. */\nexport const isCommonSystemSoundName = (\n value: string\n): value is CommonSystemSoundName =>\n (COMMON_SYSTEM_SOUND_NAMES as readonly string[]).includes(value);\n\n/**\n * A common name (typed union with autocomplete) or any platform-native sound\n * name (arbitrary string, resolved at runtime against the platform catalog).\n */\nexport type SystemSoundName = CommonSystemSoundName | (string & {});\n\n/**\n * v1 deliberately carries no options (add-ext-sound design reference section\n * 1.3 ruling: the rejection path already excluded volume control). Reserved\n * empty interface — future fields land in platform namespaces first (dialog\n * law), never as silently ignored values.\n */\nexport interface PlaySoundOptions {\n // Intentionally empty in v1.\n}\n\n/**\n * Shared sound backend capabilities DTO (add-ext-sound design reference\n * section 2). `fileFormats` is the v1 committed set (finite canonical:\n * darwin `[\"wav\",\"aiff\",\"mp3\",\"m4a\"]`, win32 `[\"wav\"]`), not a runtime-open\n * catalog of whatever the OS currently accepts.\n */\nexport interface SoundBackendCapabilities {\n platform: \"darwin\" | \"win32\";\n /** Platform-native sound-name catalog resolvable (`playSystemSound` passthrough). */\n systemSoundCatalog: boolean;\n /** File playback available (`playSound`). */\n playFile: boolean;\n fileFormats: readonly string[];\n}\n\n/** Returns true when an unknown value is a complete sound backend capabilities DTO. */\nexport const isSoundBackendCapabilities = (\n value: unknown\n): value is SoundBackendCapabilities => {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return false;\n }\n const record = value as Record<string, unknown>;\n if (record.platform !== \"darwin\" && record.platform !== \"win32\") {\n return false;\n }\n return (\n typeof record.systemSoundCatalog === \"boolean\" &&\n typeof record.playFile === \"boolean\" &&\n Array.isArray(record.fileFormats) &&\n record.fileFormats.every((format) => typeof format === \"string\")\n );\n};\n\n/** Typed sound error family (add-ext-sound design reference section 3, frozen at four codes). */\nexport const SOUND_ERROR_CODES = {\n platformUnsupported: \"sound_platform_unsupported\",\n notFound: \"sound_not_found\",\n formatUnsupported: \"sound_format_unsupported\",\n fileUnreadable: \"sound_file_unreadable\",\n} as const;\n\nexport type SoundErrorCode = (typeof SOUND_ERROR_CODES)[keyof typeof SOUND_ERROR_CODES];\n\nconst SOUND_ERROR_CODE_VALUES = new Set<string>(Object.values(SOUND_ERROR_CODES));\n\n/** Returns true when an unknown value is one of the frozen typed sound error codes. */\nexport const isSoundErrorCode = (value: string): value is SoundErrorCode =>\n SOUND_ERROR_CODE_VALUES.has(value);\n\n/** How a `sound_not_found` miss was searched, in resolution order (common table first). */\nexport type SystemSoundAttemptedMode = \"common-table\" | \"platform-catalog\";\n\n/**\n * Discriminated details payloads for the typed sound error family\n * (add-ext-sound design reference section 1.2: a miss never stays silent and\n * never falls back — `sound_not_found` details carry at least the requested\n * name, the platform, and the attempted modes/catalog).\n */\nexport type SoundErrorDetails =\n | { kind: \"platform\"; platform: string }\n | {\n kind: \"not-found\";\n requested: string;\n platform: \"darwin\" | \"win32\";\n attempted: readonly SystemSoundAttemptedMode[];\n }\n | { kind: \"format\"; path: string; reason: string }\n | { kind: \"unreadable\"; path: string };\n\n// ---------------------------------------------------------------------------\n// Clipboard extension shared schema (add-ext-clipboard design reference)\n// ---------------------------------------------------------------------------\n\n/** Frozen clipboard write bound: 1 MiB = 1,048,576 UTF-16 code units. */\nexport const CLIPBOARD_MAX_WRITE_UTF16 = 1_048_576;\n\n/** Shared clipboard backend capabilities DTO (add-ext-clipboard design reference). */\nexport interface ClipboardBackendCapabilities {\n platform: \"darwin\" | \"win32\";\n /** v1 is text-only; the format catalog is a v2 extension point. */\n textOnly: true;\n /** Frozen platform-independent write bound (UTF-16 code units). */\n maxWriteUtf16: typeof CLIPBOARD_MAX_WRITE_UTF16;\n /** win32 = true (bounded open-retry discipline); darwin = false (AppKit serializes). */\n boundedOpenRetry: boolean;\n}\n\n/** Returns true when an unknown value is a complete clipboard backend capabilities DTO. */\nexport const isClipboardBackendCapabilities = (\n value: unknown\n): value is ClipboardBackendCapabilities => {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return false;\n }\n const record = value as Record<string, unknown>;\n return (\n (record.platform === \"darwin\" || record.platform === \"win32\") &&\n record.textOnly === true &&\n record.maxWriteUtf16 === CLIPBOARD_MAX_WRITE_UTF16 &&\n typeof record.boundedOpenRetry === \"boolean\"\n );\n};\n\n/** Frozen clipboard typed-error code set (add-ext-clipboard design reference section 3). */\nexport const CLIPBOARD_ERROR_CODES = {\n platformUnsupported: \"clipboard_platform_unsupported\",\n locked: \"clipboard_locked\",\n unavailable: \"clipboard_unavailable\",\n payloadTooLarge: \"clipboard_payload_too_large\",\n payloadInvalid: \"clipboard_payload_invalid\",\n} as const;\n\nexport type ClipboardErrorCode = (typeof CLIPBOARD_ERROR_CODES)[keyof typeof CLIPBOARD_ERROR_CODES];\n\nexport const isClipboardErrorCode = (value: string): value is ClipboardErrorCode =>\n Object.values(CLIPBOARD_ERROR_CODES).includes(value as ClipboardErrorCode);\n\n// ---------------------------------------------------------------------------\n// Opener extension shared schema (add-ext-opener design reference)\n// ---------------------------------------------------------------------------\n\n/** Frozen v1 scheme allowlist (lowercase canonical; matched case-insensitively). */\nexport const OPENER_ALLOWED_SCHEMES = [\"http\", \"https\", \"file\", \"mailto\"] as const;\n\nexport type OpenerAllowedScheme = (typeof OPENER_ALLOWED_SCHEMES)[number];\n\nexport const isOpenerAllowedScheme = (value: string): value is OpenerAllowedScheme =>\n (OPENER_ALLOWED_SCHEMES as readonly string[]).includes(value.toLowerCase()) === true;\n\n/** Shared opener backend capabilities DTO (add-ext-opener design reference). */\nexport interface OpenerBackendCapabilities {\n platform: \"darwin\" | \"win32\";\n allowedSchemes: typeof OPENER_ALLOWED_SCHEMES;\n supportsRevealInFolder: true;\n}\n\n/** Returns true when an unknown value is a complete opener backend capabilities DTO. */\nexport const isOpenerBackendCapabilities = (\n value: unknown\n): value is OpenerBackendCapabilities => {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return false;\n }\n const record = value as Record<string, unknown>;\n if (record.platform !== \"darwin\" && record.platform !== \"win32\") {\n return false;\n }\n if (\n !Array.isArray(record.allowedSchemes) ||\n record.allowedSchemes.length !== OPENER_ALLOWED_SCHEMES.length ||\n !(record.allowedSchemes as unknown[]).every((scheme, index) => scheme === OPENER_ALLOWED_SCHEMES[index])\n ) {\n return false;\n }\n return record.supportsRevealInFolder === true;\n};\n\n/** Frozen opener typed-error code set (add-ext-opener design reference section 3). */\nexport const OPENER_ERROR_CODES = {\n platformUnsupported: \"opener_platform_unsupported\",\n targetInvalid: \"opener_target_invalid\",\n schemeBlocked: \"opener_scheme_blocked\",\n failed: \"opener_failed\",\n} as const;\n\nexport type OpenerErrorCode = (typeof OPENER_ERROR_CODES)[keyof typeof OPENER_ERROR_CODES];\n\nexport const isOpenerErrorCode = (value: string): value is OpenerErrorCode =>\n Object.values(OPENER_ERROR_CODES).includes(value as OpenerErrorCode);\n\n// ---------------------------------------------------------------------------\n// Notification extension shared schema (add-ext-notification design reference)\n// ---------------------------------------------------------------------------\n\n/** Frozen notification payload bounds (UTF-16 code units; platform-independent common subset). */\nexport const NOTIFICATION_TITLE_LIMIT_UTF16 = 64;\nexport const NOTIFICATION_BODY_LIMIT_UTF16 = 256;\nexport const NOTIFICATION_SUBTITLE_LIMIT_UTF16 = 64;\n\nexport type NotificationAuthorizationStatus =\n | \"granted\"\n | \"denied\"\n | \"notDetermined\";\n\nexport type NotificationAuthorizationModel = \"user\" | \"always-granted\";\nexport type NotificationChannel = \"user-notification-center\" | \"tray-icon-info\";\n\n/** Shared notification backend capabilities DTO (add-ext-notification design reference section 3). */\nexport interface NotificationBackendCapabilities {\n platform: \"darwin\" | \"win32\";\n authorizationModel: NotificationAuthorizationModel;\n channel: NotificationChannel;\n titleLimitUtf16: typeof NOTIFICATION_TITLE_LIMIT_UTF16;\n bodyLimitUtf16: typeof NOTIFICATION_BODY_LIMIT_UTF16;\n subtitleLimitUtf16: typeof NOTIFICATION_SUBTITLE_LIMIT_UTF16;\n supportsSubtitle: boolean;\n}\n\n/** Returns true when an unknown value is a complete notification backend capabilities DTO. */\nexport const isNotificationBackendCapabilities = (\n value: unknown\n): value is NotificationBackendCapabilities => {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return false;\n }\n const record = value as Record<string, unknown>;\n return (\n (record.platform === \"darwin\" || record.platform === \"win32\") &&\n (record.authorizationModel === \"user\" || record.authorizationModel === \"always-granted\") &&\n (record.channel === \"user-notification-center\" || record.channel === \"tray-icon-info\") &&\n record.titleLimitUtf16 === NOTIFICATION_TITLE_LIMIT_UTF16 &&\n record.bodyLimitUtf16 === NOTIFICATION_BODY_LIMIT_UTF16 &&\n record.subtitleLimitUtf16 === NOTIFICATION_SUBTITLE_LIMIT_UTF16 &&\n typeof record.supportsSubtitle === \"boolean\"\n );\n};\n\n/** Frozen notification typed-error code set (add-ext-notification design reference section 3). */\nexport const NOTIFICATION_ERROR_CODES = {\n platformUnsupported: \"notification_platform_unsupported\",\n denied: \"notification_denied\",\n payloadInvalid: \"notification_payload_invalid\",\n trayAbsent: \"notification_tray_absent\",\n failed: \"notification_failed\",\n} as const;\n\nexport type NotificationErrorCode =\n (typeof NOTIFICATION_ERROR_CODES)[keyof typeof NOTIFICATION_ERROR_CODES];\n\nexport const isNotificationErrorCode = (value: string): value is NotificationErrorCode =>\n Object.values(NOTIFICATION_ERROR_CODES).includes(value as NotificationErrorCode);\n","// Orthogonal intents (2026-09-18; add-ext-notification batch C):\n// 1. Keep every notification option/wire-event/error type plus the payload\n// preflight matrix and the win32 subtitle join as pure, transport-free\n// data (design reference sections 1-3). The DTO schema and the frozen\n// error-code family are owned by the shared @opentray/spec schema and\n// re-exported here so consumers need one import surface.\n// 2. Never perform I/O here; the facade (index.ts) composes these pure\n// pieces with the tray extension transport.\n\nimport {\n NOTIFICATION_BODY_LIMIT_UTF16,\n NOTIFICATION_ERROR_CODES,\n NOTIFICATION_SUBTITLE_LIMIT_UTF16,\n NOTIFICATION_TITLE_LIMIT_UTF16,\n isNotificationBackendCapabilities,\n type NotificationAuthorizationStatus,\n type NotificationBackendCapabilities,\n type NotificationErrorCode,\n} from \"@opentray/spec\";\n\nexport type NotificationPlatform = \"darwin\" | \"win32\" | \"linux\";\n\nexport type NotificationNativePlatform = \"darwin\" | \"win32\";\n\n// Shared schema (add-ext-notification batch A): re-exported so the facade\n// public surface and the @opentray/spec truth cannot drift.\nexport type {\n NotificationAuthorizationModel,\n NotificationAuthorizationStatus,\n NotificationBackendCapabilities,\n NotificationChannel,\n NotificationErrorCode,\n} from \"@opentray/spec\";\nexport {\n NOTIFICATION_BODY_LIMIT_UTF16,\n NOTIFICATION_ERROR_CODES,\n NOTIFICATION_SUBTITLE_LIMIT_UTF16,\n NOTIFICATION_TITLE_LIMIT_UTF16,\n isNotificationBackendCapabilities,\n isNotificationErrorCode,\n} from \"@opentray/spec\";\n\n// ---------------------------------------------------------------------------\n// Options (design reference section 1; v1 has NO platform namespace)\n// ---------------------------------------------------------------------------\n\nexport interface NotifyOptions {\n /** Required, non-empty, at most 64 UTF-16 code units (platform-independent common contract). */\n title: string;\n /** At most 256 UTF-16 code units. */\n body?: string;\n /**\n * Distinct native field on darwin. win32 documented degradation: the facade\n * joins it into the body as a prefix (combined post-join limit 256 UTF-16\n * code units; never truncated). At most 64 UTF-16 code units.\n */\n subtitle?: string;\n /** Default `false` (platform default alert sound); `true` means no sound. */\n silent?: boolean;\n}\n\n/** Wire form of a notify payload after facade preflight (`silent` always explicit). */\nexport interface NotifyWireOptions {\n title: string;\n body?: string;\n subtitle?: string;\n silent: boolean;\n}\n\n/** Wire command surface of the notification extension (camelCase `ext-command`\n * payloads). `notify` carries its fields FLAT next to `type` — the exact shape\n * the native `NotificationCommand` serde tag and the broker bridge decode\n * (implementation review I3a P0: a nested `{options: {...}}` wrapper never\n * reached a decoder). */\nexport type NotificationCommand =\n | { type: \"getBackend\" }\n | ({ type: \"notify\" } & NotifyWireOptions)\n | { type: \"getAuthorizationStatus\" }\n | { type: \"requestAuthorization\" };\n\n// ---------------------------------------------------------------------------\n// Wire events (design reference section 4): the two authorization commands\n// settle darwin-side through DeferredOperation terminals whose result value\n// carries the same event shape the win32 Immediate path emits as an ext\n// event — the facade stays transport-agnostic across both.\n// ---------------------------------------------------------------------------\n\n/** Wire event that answers the `getBackend` command on the immediate path. */\nexport interface NotificationBackendEvent {\n type: \"backend\";\n backend: NotificationBackendCapabilities;\n}\n\n/** Authorization read result: the `getAuthorizationStatus` answer. */\nexport interface NotificationAuthorizationEvent {\n type: \"authorization\";\n status: NotificationAuthorizationStatus;\n}\n\n/** Authorization request decision: the `requestAuthorization` answer. */\nexport interface NotificationAuthorizationDecisionEvent {\n type: \"authorizationDecision\";\n granted: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// Typed error family (design reference section 3; codes frozen in @opentray/spec)\n// ---------------------------------------------------------------------------\n\n/**\n * Facade-originated details payloads (broker-originated details pass through\n * unchanged: `notification_denied` carries `{status}`; `notification_failed`\n * carries `{reason}` or an OS error code; the win32 bridge's\n * `notification_tray_absent` carries its own scope payload).\n */\nexport type NotificationErrorDetails =\n | { kind: \"platform\"; platform: string }\n | { field: string; lengthUtf16?: number; limit?: number };\n\nexport interface NotificationErrorDescriptor {\n code: NotificationErrorCode;\n message: string;\n details?: NotificationErrorDetails;\n}\n\n/**\n * Typed rejection of the notification facade: facade preflight failures\n * (platform, payload bounds/shape) and normalized broker/native notification\n * rejections. Consumers match on `code`; the human `message` is not a\n * contract. The shared transport-close code is surfaced unchanged (the frozen\n * notification family has no transport alias; core never branches on\n * extension names).\n */\nexport class NotificationError extends Error {\n readonly code: NotificationErrorCode;\n readonly details?: unknown;\n\n constructor(\n code: NotificationErrorCode,\n message: string,\n options: { details?: unknown; cause?: unknown } = {}\n ) {\n super(message, options.cause === undefined ? undefined : { cause: options.cause });\n this.name = \"NotificationError\";\n this.code = code;\n if (options.details !== undefined) {\n this.details = options.details;\n }\n }\n}\n\nexport const notificationErrorFromDescriptor = (\n descriptor: NotificationErrorDescriptor,\n options: { cause?: unknown } = {}\n): NotificationError =>\n new NotificationError(descriptor.code, descriptor.message, {\n ...(descriptor.details === undefined ? {} : { details: descriptor.details }),\n ...(options.cause === undefined ? {} : { cause: options.cause }),\n });\n\nexport const platformUnsupported = (platform: string): NotificationErrorDescriptor => ({\n code: NOTIFICATION_ERROR_CODES.platformUnsupported,\n message: `native notifications are unsupported on ${platform} (Linux has no native implementation; add-ext-notification design reference section 0)`,\n details: { kind: \"platform\", platform },\n});\n\n// ---------------------------------------------------------------------------\n// Runtime shape guards (wire results and DTO)\n// ---------------------------------------------------------------------------\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null && !Array.isArray(value);\n\nconst AUTHORIZATION_STATUSES: readonly string[] = [\"granted\", \"denied\", \"notDetermined\"];\n\nexport const isNotificationBackendEvent = (\n value: unknown\n): value is NotificationBackendEvent =>\n isRecord(value) &&\n value.type === \"backend\" &&\n isNotificationBackendCapabilities(value.backend);\n\nexport const isNotificationAuthorizationEvent = (\n value: unknown\n): value is NotificationAuthorizationEvent =>\n isRecord(value) &&\n value.type === \"authorization\" &&\n typeof value.status === \"string\" &&\n AUTHORIZATION_STATUSES.includes(value.status);\n\nexport const isNotificationAuthorizationDecisionEvent = (\n value: unknown\n): value is NotificationAuthorizationDecisionEvent =>\n isRecord(value) && value.type === \"authorizationDecision\" && typeof value.granted === \"boolean\";\n\n// ---------------------------------------------------------------------------\n// Preflight validation (design reference section 1, frozen bounds; UTF-16\n// code units counted by String.length; deterministic order: object shape ->\n// unknown fields -> title -> body -> subtitle -> silent -> win32 joined\n// body). Everything here fails before any state change or transport use, and\n// payloads are never silently truncated.\n// ---------------------------------------------------------------------------\n\nconst NOTIFY_FIELDS = [\"title\", \"body\", \"subtitle\", \"silent\"] as const;\n\nconst payloadInvalid = (\n reason: string,\n field?: string,\n lengthUtf16?: number,\n limit?: number\n): NotificationErrorDescriptor => ({\n code: NOTIFICATION_ERROR_CODES.payloadInvalid,\n message:\n field === undefined\n ? `invalid notify options: ${reason}`\n : `invalid notify options (${field}): ${reason}`,\n ...(field === undefined\n ? {}\n : {\n details: {\n field,\n ...(lengthUtf16 === undefined ? {} : { lengthUtf16 }),\n ...(limit === undefined ? {} : { limit }),\n },\n }),\n});\n\n/**\n * win32 documented degradation (design reference section 1): the subtitle\n * prefixes the body through a single em dash; a subtitle without a body —\n * or with an EMPTY-string body — is the subtitle alone (never a dangling\n * separator; implementation review I3a P1 parity with the crate's\n * `win32_joined_body`). The joined string is what the combined 256-unit\n * limit measures.\n */\nexport const joinWin32Body = (subtitle: string, body: string | undefined): string =>\n body === undefined || body === \"\" ? subtitle : `${subtitle}—${body}`;\n\nexport const validateNotifyOptions = (\n options: unknown,\n platform: NotificationNativePlatform\n): NotificationErrorDescriptor | null => {\n if (!isRecord(options)) {\n return payloadInvalid(\"options must be an object\");\n }\n for (const key of Object.keys(options)) {\n if (!(NOTIFY_FIELDS as readonly string[]).includes(key)) {\n // v1 has no platform namespace and no passthrough fields (design\n // reference section 1); an unknown field is never silently ignored.\n return payloadInvalid(\"unknown field\", key);\n }\n }\n if (typeof options.title !== \"string\") {\n return payloadInvalid(\"must be a string\", \"title\");\n }\n if (options.title.length === 0) {\n return payloadInvalid(\"must be non-empty\", \"title\", 0, NOTIFICATION_TITLE_LIMIT_UTF16);\n }\n if (options.title.length > NOTIFICATION_TITLE_LIMIT_UTF16) {\n return payloadInvalid(\n `exceeds the ${NOTIFICATION_TITLE_LIMIT_UTF16} UTF-16 code unit limit`,\n \"title\",\n options.title.length,\n NOTIFICATION_TITLE_LIMIT_UTF16\n );\n }\n if (options.body !== undefined) {\n if (typeof options.body !== \"string\") {\n return payloadInvalid(\"must be a string when present\", \"body\");\n }\n if (options.body.length > NOTIFICATION_BODY_LIMIT_UTF16) {\n return payloadInvalid(\n `exceeds the ${NOTIFICATION_BODY_LIMIT_UTF16} UTF-16 code unit limit`,\n \"body\",\n options.body.length,\n NOTIFICATION_BODY_LIMIT_UTF16\n );\n }\n }\n if (options.subtitle !== undefined) {\n if (typeof options.subtitle !== \"string\") {\n return payloadInvalid(\"must be a string when present\", \"subtitle\");\n }\n if (options.subtitle.length > NOTIFICATION_SUBTITLE_LIMIT_UTF16) {\n return payloadInvalid(\n `exceeds the ${NOTIFICATION_SUBTITLE_LIMIT_UTF16} UTF-16 code unit limit`,\n \"subtitle\",\n options.subtitle.length,\n NOTIFICATION_SUBTITLE_LIMIT_UTF16\n );\n }\n }\n if (options.silent !== undefined && typeof options.silent !== \"boolean\") {\n return payloadInvalid(\"must be a boolean when present\", \"silent\");\n }\n if (platform === \"win32\" && options.subtitle !== undefined) {\n // The degradation join is jointly validated against the post-join 256\n // limit — an overflow rejects typed rather than truncating.\n const joined = joinWin32Body(options.subtitle, options.body);\n if (joined.length > NOTIFICATION_BODY_LIMIT_UTF16) {\n return payloadInvalid(\n `the joined subtitle-prefixed body exceeds the ${NOTIFICATION_BODY_LIMIT_UTF16} UTF-16 code unit limit (win32 subtitle degradation)`,\n \"body\",\n joined.length,\n NOTIFICATION_BODY_LIMIT_UTF16\n );\n }\n }\n return null;\n};\n\n/** Apply the frozen wire projection: `silent` explicit; win32 joins the subtitle into `body` (no `subtitle` on the win32 wire). */\nexport const notifyCommandOptions = (\n options: NotifyOptions,\n platform: NotificationNativePlatform\n): NotifyWireOptions => {\n if (platform === \"win32\" && options.subtitle !== undefined) {\n const joined = joinWin32Body(options.subtitle, options.body);\n return {\n title: options.title,\n body: joined,\n silent: options.silent === true,\n };\n }\n return {\n title: options.title,\n ...(options.body !== undefined ? { body: options.body } : {}),\n ...(options.subtitle !== undefined ? { subtitle: options.subtitle } : {}),\n silent: options.silent === true,\n };\n};\n","// Orthogonal intents (2026-09-18; add-ext-notification batch C):\n// 1. Attach the notification capability through the normal tray extension\n// family (tray.extend, session-scoped, isomorphic to\n// attachDialog/attachSound/attachClipboard).\n// 2. Fail every preflight branch before any transport use: typed platform\n// rejection on Linux (zero broker frames), the frozen payload bounds\n// matrix including the win32 subtitle-join combined limit, and unknown\n// attach option fields (TypeError) — never a silent truncation and never\n// a silently ignored setting.\n// 3. Dispatch notify/getBackend on the V2 immediate path (design reference\n// sections 1-2: notify is non-modal resolve-on-acceptance) and settle the\n// two authorization commands transport-agnostically: darwin's\n// DeferredOperation terminal (O2 ruling — the 10s timeout fires\n// broker-side into typed notification_failed) or win32's Immediate event\n// answer, whichever the broker resolves the request with.\n\nimport type {\n ExtensionRequestResult,\n NativeExtensionArtifact,\n TrayExtensionContext,\n TrayHandle,\n} from \"opentray\";\nimport {\n BrokerServerError,\n ExtensionOperationError,\n NativeExtensionEmbeddedArtifactError,\n} from \"opentray\";\n\nimport { NOTIFICATION_NATIVE_ARTIFACT } from \"./native-artifact\";\nimport {\n NOTIFICATION_ERROR_CODES,\n NotificationError,\n isNotificationAuthorizationDecisionEvent,\n isNotificationAuthorizationEvent,\n isNotificationBackendEvent,\n isNotificationErrorCode,\n notificationErrorFromDescriptor,\n notifyCommandOptions,\n platformUnsupported,\n validateNotifyOptions,\n type NotificationAuthorizationStatus,\n type NotificationBackendCapabilities,\n type NotificationCommand,\n type NotificationNativePlatform,\n type NotificationPlatform,\n type NotifyOptions,\n} from \"./shared\";\n\nexport type * from \"./shared\";\nexport {\n NOTIFICATION_ERROR_CODES,\n NotificationError,\n isNotificationAuthorizationDecisionEvent,\n isNotificationAuthorizationEvent,\n isNotificationBackendEvent,\n isNotificationErrorCode,\n joinWin32Body,\n notificationErrorFromDescriptor,\n validateNotifyOptions,\n} from \"./shared\";\nexport { NOTIFICATION_NATIVE_ARTIFACT } from \"./native-artifact\";\n\nconst NOTIFICATION_EXTENSION_NAME = \"notification\";\n\nconst ATTACH_OPTION_KEYS: ReadonlySet<string> = new Set([\"mountId\", \"artifact\", \"platform\"]);\n\nexport interface NotificationExtensionOptions {\n mountId?: string;\n artifact?: NativeExtensionArtifact;\n /** Test/seam override for the host platform; Linux rejects typed before any dispatch. */\n platform?: NotificationPlatform;\n}\n\n/**\n * Public notification capability (design reference section 1): host-side OS\n * notifications plus the authorization surface. `notify` is\n * resolve-on-acceptance (the sound law): whether and when the user actually\n * sees the notification is not part of the acceptance semantics. On darwin\n * the authorization commands carry real UNUserNotificationCenter semantics;\n * on win32 they are documented always-granted degradations answered\n * Immediate — the facade accepts both settle shapes for either platform.\n */\nexport interface NotificationCapability {\n notify(options: NotifyOptions): Promise<void>;\n getAuthorizationStatus(): Promise<NotificationAuthorizationStatus>;\n /** darwin: real decision. win32: documented degradation, always `true`. */\n requestAuthorization(): Promise<boolean>;\n getBackend(): Promise<NotificationBackendCapabilities>;\n}\n\nexport const attachNotification = (\n tray: TrayHandle,\n options: NotificationExtensionOptions = {}\n): NotificationCapability => {\n const unknown = Object.keys(options).find((key) => !ATTACH_OPTION_KEYS.has(key));\n if (unknown !== undefined) {\n // v1 has exactly the family option surface; an unknown field is a\n // programming error, never a silently ignored setting (pre-transport).\n throw new TypeError(\n `attachNotification received an unknown option field: ${unknown} (v1 accepts mountId, artifact, platform)`\n );\n }\n return tray.extend(NotificationExt, options);\n};\n\nexport const NotificationExt = {\n name: NOTIFICATION_EXTENSION_NAME,\n artifact: NOTIFICATION_NATIVE_ARTIFACT,\n resolveMount(options: NotificationExtensionOptions | undefined) {\n return {\n ...(options?.mountId === undefined ? {} : { mountId: options.mountId }),\n ...(options?.artifact === undefined ? {} : { artifact: options.artifact }),\n };\n },\n extend(\n tray: TrayHandle,\n context: TrayExtensionContext,\n options: NotificationExtensionOptions | undefined\n ): NotificationCapability {\n return createNotificationCapability(\n context,\n (options?.platform ?? process.platform) as NotificationPlatform\n );\n },\n} satisfies import(\"opentray\").TrayExtension<NotificationCapability, NotificationExtensionOptions>;\n\nfunction createNotificationCapability(\n context: TrayExtensionContext,\n platform: NotificationPlatform\n): NotificationCapability {\n const nativePlatform: NotificationNativePlatform | undefined =\n platform === \"darwin\" || platform === \"win32\" ? platform : undefined;\n\n const requirePlatform = (): NotificationNativePlatform => {\n if (nativePlatform === undefined) {\n throw notificationErrorFromDescriptor(platformUnsupported(platform));\n }\n return nativePlatform;\n };\n\n const mapTransportError = (error: unknown): unknown => {\n if (error instanceof NotificationError) {\n return error;\n }\n if (\n error instanceof NativeExtensionEmbeddedArtifactError &&\n error.reason === \"target-unsupported\"\n ) {\n return new NotificationError(\n NOTIFICATION_ERROR_CODES.platformUnsupported,\n `native notification extension ships no library for this platform: ${error.message}`,\n { details: { kind: \"platform\", platform }, cause: error }\n );\n }\n if (\n (error instanceof ExtensionOperationError || error instanceof BrokerServerError) &&\n isNotificationErrorCode(error.code)\n ) {\n // Includes the broker-side 10s authorization timeout: an error terminal\n // arrives as notification_failed with details {reason:\n // \"authorization-timeout\"} and normalizes here with details intact.\n return new NotificationError(error.code, error.message, {\n ...(error.details === undefined ? {} : { details: error.details }),\n cause: error,\n });\n }\n // The shared transport-close code and unknown codes surface unchanged:\n // the frozen notification family has no transport alias and core never\n // branches on extension names (add-ext-notification design reference\n // section 3).\n return error;\n };\n\n const dispatch = async <TValue>(\n command: NotificationCommand\n ): Promise<ExtensionRequestResult<TValue>> => {\n try {\n return await context.request<TValue>(command);\n } catch (error: unknown) {\n throw mapTransportError(error);\n }\n };\n\n const dispatchImmediate = async (\n command: NotificationCommand\n ): Promise<Extract<ExtensionRequestResult<unknown>, { kind: \"immediate\" }>> => {\n const result = await dispatch<unknown>(command);\n if (result.kind !== \"immediate\") {\n throw new Error(\n `notification extension command ${command.type} settles on the immediate path (add-ext-notification design reference sections 1-2; only the authorization commands defer); received ${result.kind}`\n );\n }\n return result;\n };\n\n /**\n * Transport-agnostic authorization settle (design reference section 4):\n * darwin completes through the DeferredOperation terminal whose result\n * value carries the event shape; win32 answers Immediate with the same\n * event shape in the events array. Either resolution path is accepted for\n * either platform — the facade cannot and must not tell them apart.\n */\n const runAuthorizationCommand = async <TEvent>(\n command: NotificationCommand,\n guard: (value: unknown) => value is TEvent,\n label: string\n ): Promise<TEvent> => {\n const result = await dispatch<unknown>(command);\n if (result.kind === \"terminal\") {\n if (!guard(result.value)) {\n throw new Error(`${label} deferred terminal payload violated the wire contract`);\n }\n return result.value;\n }\n for (const event of result.events) {\n const data = (event as { data?: unknown } | null)?.data;\n if (guard(data)) {\n return data;\n }\n }\n throw new Error(\n `${label} settled without an authorization payload (wire contract violation; expected a deferred terminal or an immediate authorization event)`\n );\n };\n\n const backendContractError = (): Error =>\n new Error(\n \"notification extension answered getBackend without a backend capabilities payload (wire contract violation)\"\n );\n\n const expectBackendPlatform = (\n backend: NotificationBackendCapabilities,\n native: NotificationNativePlatform\n ): NotificationBackendCapabilities => {\n if (backend.platform !== native) {\n throw new Error(\n `notification backend reported platform ${backend.platform} on a ${native} host (wire contract violation)`\n );\n }\n return backend;\n };\n\n /** Frozen immutable snapshot: the record is deeply plain, so a shallow freeze is total. */\n const freezeBackend = (backend: NotificationBackendCapabilities): NotificationBackendCapabilities =>\n Object.freeze({ ...backend });\n\n let backendPromise: Promise<NotificationBackendCapabilities> | undefined;\n\n const ensureBackend = (\n native: NotificationNativePlatform\n ): Promise<NotificationBackendCapabilities> => {\n backendPromise ??= (async () => {\n const result = await dispatchImmediate({ type: \"getBackend\" });\n for (const event of result.events) {\n const data = (event as { data?: unknown } | null)?.data;\n if (isNotificationBackendEvent(data)) {\n return freezeBackend(expectBackendPlatform(data.backend, native));\n }\n }\n throw backendContractError();\n })().catch((error: unknown) => {\n // Allow a later retry after a failed snapshot (e.g. transport death);\n // a successful snapshot is immutable and shared by every dispatch.\n backendPromise = undefined;\n throw mapTransportError(error);\n });\n return backendPromise;\n };\n\n return {\n notify: async (options: NotifyOptions): Promise<void> => {\n const native = requirePlatform();\n const issue = validateNotifyOptions(options, native);\n if (issue !== null) {\n throw notificationErrorFromDescriptor(issue);\n }\n // Resolve-on-acceptance: the immediate result IS the acceptance; the\n // win32 broker-internal tray-notification bridge answers with the same\n // immediate shape (design reference section 2, O1 ruling B). The\n // fields travel FLAT next to `type` — the shape the native serde tag\n // and the bridge decode (implementation review I3a P0).\n await dispatchImmediate({\n type: \"notify\",\n ...notifyCommandOptions(options, native),\n });\n },\n getAuthorizationStatus: async (): Promise<NotificationAuthorizationStatus> => {\n requirePlatform();\n const event = await runAuthorizationCommand(\n { type: \"getAuthorizationStatus\" },\n isNotificationAuthorizationEvent,\n \"getAuthorizationStatus\"\n );\n return event.status;\n },\n requestAuthorization: async (): Promise<boolean> => {\n requirePlatform();\n const event = await runAuthorizationCommand(\n { type: \"requestAuthorization\" },\n isNotificationAuthorizationDecisionEvent,\n \"requestAuthorization\"\n );\n return event.granted;\n },\n getBackend: async (): Promise<NotificationBackendCapabilities> =>\n ensureBackend(requirePlatform()),\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,9 +1,40 @@
1
1
  {
2
2
  "name": "@opentray/ext-notification",
3
- "version": "0.0.0",
4
- "description": "Generated placeholder package reserved by pnpm-pub.",
5
- "main": "index.js",
3
+ "version": "0.30.1",
4
+ "description": "Official OpenTray native notification extension: OS-standard host-side notifications through UNUserNotificationCenter or the registered tray icon's balloon/toast channel.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/jixoai/opentray"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.mts",
14
+ "import": "./dist/index.mjs"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
6
18
  "files": [
7
- "index.js"
8
- ]
19
+ "contract.json",
20
+ "platforms",
21
+ "dist",
22
+ "README.md"
23
+ ],
24
+ "sideEffects": false,
25
+ "peerDependencies": {
26
+ "opentray": ">=0.0.0"
27
+ },
28
+ "devDependencies": {
29
+ "tsdown": "^0.22.1",
30
+ "typescript": "^6.0.3",
31
+ "vitest": "^4.1.7",
32
+ "@opentray/spec": "0.30.1",
33
+ "opentray": "0.30.1"
34
+ },
35
+ "scripts": {
36
+ "build": "tsdown src/index.ts --format esm --dts",
37
+ "test": "vitest run",
38
+ "typecheck": "tsc --noEmit"
39
+ }
9
40
  }
@@ -0,0 +1,26 @@
1
+ {
2
+ "facadeVersion": "0.30.1",
3
+ "contractFingerprint": "opentray-ext-notification-contract-1",
4
+ "targets": {
5
+ "darwin-arm64": {
6
+ "path": "platforms/darwin-arm64/libopentray_ext_notification.dylib",
7
+ "sha256": "7a58cf540a8bd96494f906e16dfecfbffeea7378944c1c42ea460ec91df0b2cc",
8
+ "buildIdentity": "github:d4b56bbade6e16cb98eaea98f1dc6387b599ac51"
9
+ },
10
+ "darwin-x64": {
11
+ "path": "platforms/darwin-x64/libopentray_ext_notification.dylib",
12
+ "sha256": "a845622b7816768dd5d78e45fc7ee4be46e36e8cc7a735bedb731aac5e226161",
13
+ "buildIdentity": "github:d4b56bbade6e16cb98eaea98f1dc6387b599ac51"
14
+ },
15
+ "win32-arm64": {
16
+ "path": "platforms/win32-arm64/opentray_ext_notification.dll",
17
+ "sha256": "d272f2dc58d78b5f0f6bc984336b6b17ee52234a03793dff98705a8bac409db2",
18
+ "buildIdentity": "github:d4b56bbade6e16cb98eaea98f1dc6387b599ac51"
19
+ },
20
+ "win32-x64": {
21
+ "path": "platforms/win32-x64/opentray_ext_notification.dll",
22
+ "sha256": "0b85daa9e7fb47b326a447d08d79602b5aeaa0363415a6cc396153288ebb2bf1",
23
+ "buildIdentity": "github:d4b56bbade6e16cb98eaea98f1dc6387b599ac51"
24
+ }
25
+ }
26
+ }
package/index.js DELETED
@@ -1 +0,0 @@
1
- module.exports = {};