electron-wns 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # electron-wns
2
+
3
+ A TypeScript library for receiving [Windows Push Notification Service (WNS)](https://learn.microsoft.com/en-us/windows/apps/develop/notifications/push-notifications/wns-overview) push messages in the **main process** of an [Electron](https://www.electronjs.org/) application.
4
+
5
+ ---
6
+
7
+ ## How WNS works
8
+
9
+ ```
10
+ ┌──────────┐ 1. getChannel() ┌──────────────────────┐
11
+ │ Electron │ ──────────────────▶│ WNS (Microsoft) │
12
+ │ App │ ◀────────────────── │ │
13
+ └──────────┘ channel URI └──────────────────────┘
14
+
15
+ │ 2. Send URI to backend
16
+
17
+ ┌──────────┐ 3. POST /notify ┌──────────────────────┐
18
+ │ Your │ ──────────────────▶│ WNS (Microsoft) │
19
+ │ Backend │ │ │
20
+ └──────────┘ └──────────┬───────────┘
21
+ │ 4. push delivered
22
+
23
+ ┌──────────┐
24
+ │ Electron │ ◀── 'notification' event
25
+ │ App │
26
+ └──────────┘
27
+ ```
28
+
29
+ 1. Your Electron app calls `client.getChannel()` to obtain a **channel URI** from WNS.
30
+ 2. Your app sends the channel URI to your backend server.
31
+ 3. Whenever your backend needs to push a message, it makes an authenticated HTTP POST to the channel URI via WNS.
32
+ 4. WNS delivers the push to the device; `electron-wns` emits a `notification` event in your Electron main process.
33
+
34
+ ---
35
+
36
+ ## Requirements
37
+
38
+ | Requirement | Details |
39
+ |---|---|
40
+ | **Operating system** | Windows 10 (build 1803) or later |
41
+ | **PowerShell** | `powershell.exe` (Windows PowerShell 5.1+) or `pwsh` (PowerShell 7+) |
42
+ | **Package identity** | The Electron app must be packaged as **MSIX** (or have a sparse-package identity) so that `PushNotificationChannelManager` can register the app with WNS. |
43
+ | **Node.js / Electron** | Node.js ≥ 18, Electron ≥ 22 |
44
+
45
+ > **Note – unpackaged Electron apps:** The underlying WinRT API (`PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync`) requires a package identity.
46
+ > For unpackaged Win32 apps, consider using the [Windows App SDK push notification API](https://learn.microsoft.com/en-us/windows/apps/develop/notifications/push-notifications/push-quickstart) or packaging your app with MSIX.
47
+
48
+ ---
49
+
50
+ ## Installation
51
+
52
+ ```bash
53
+ npm install electron-wns
54
+ ```
55
+
56
+ ---
57
+
58
+ ## Usage
59
+
60
+ ### Obtain a channel URI
61
+
62
+ ```ts
63
+ import { WNSClient } from 'electron-wns';
64
+
65
+ const client = new WNSClient();
66
+
67
+ // Get the WNS channel URI for this app instance.
68
+ // Share this URI with your backend – it uses it to push notifications.
69
+ const channel = await client.getChannel();
70
+ console.log('Channel URI:', channel.uri);
71
+ console.log('Expires at:', channel.expiresAt);
72
+ ```
73
+
74
+ ### Listen for incoming push notifications
75
+
76
+ ```ts
77
+ import { WNSClient, WNSNotification } from 'electron-wns';
78
+
79
+ const client = new WNSClient();
80
+
81
+ client.on('channelUpdated', (channel) => {
82
+ console.log('New channel URI:', channel.uri);
83
+ // Re-register the new URI with your backend
84
+ });
85
+
86
+ client.on('notification', (n: WNSNotification) => {
87
+ console.log('Push received!');
88
+ console.log(' type :', n.notificationType); // 'toast' | 'tile' | 'badge' | 'raw'
89
+ console.log(' payload:', n.payload);
90
+ console.log(' at :', n.timestamp);
91
+ });
92
+
93
+ client.on('error', (err) => {
94
+ console.error('WNS error:', err);
95
+ });
96
+
97
+ // Start the background WNS listener
98
+ client.startListening();
99
+
100
+ // Later, when the app is about to quit:
101
+ client.stopListening();
102
+ ```
103
+
104
+ ### Refresh an expired channel
105
+
106
+ ```ts
107
+ // Channel URIs have a limited lifetime (check `channel.expiresAt` for the exact expiry).
108
+ // Call refreshChannel() on startup or when the channel is nearing its expiry.
109
+ const newChannel = await client.refreshChannel();
110
+ ```
111
+
112
+ ### Custom PowerShell path
113
+
114
+ ```ts
115
+ const client = new WNSClient({ powershellPath: 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe' });
116
+ ```
117
+
118
+ ---
119
+
120
+ ## API
121
+
122
+ ### `new WNSClient(options?)`
123
+
124
+ | Option | Type | Default | Description |
125
+ |---|---|---|---|
126
+ | `powershellPath` | `string` | `powershell.exe` (Win) / `pwsh` | Path to the PowerShell executable. |
127
+
128
+ ### Methods
129
+
130
+ | Method | Returns | Description |
131
+ |---|---|---|
132
+ | `getChannel()` | `Promise<WNSChannel>` | Obtain (and cache) the WNS channel for this app. |
133
+ | `refreshChannel()` | `Promise<WNSChannel>` | Force a fresh channel request and emit `channelUpdated`. |
134
+ | `startListening()` | `void` | Spawn the background WNS listener. |
135
+ | `stopListening()` | `void` | Kill the background listener. |
136
+ | `isListening()` | `boolean` | Whether the listener is currently running. |
137
+
138
+ ### Events
139
+
140
+ | Event | Payload | Description |
141
+ |---|---|---|
142
+ | `channelUpdated` | `WNSChannel` | Emitted when a new channel URI is obtained. |
143
+ | `notification` | `WNSNotification` | Emitted for each incoming push notification. |
144
+ | `error` | `Error` | Non-fatal listener error. |
145
+
146
+ ### Types
147
+
148
+ ```ts
149
+ interface WNSChannel {
150
+ uri: string; // The channel URI to give to your backend
151
+ expiresAt: string; // ISO 8601 expiry timestamp – check this field for the exact expiry
152
+ }
153
+
154
+ interface WNSNotification {
155
+ notificationType: 'toast' | 'tile' | 'badge' | 'raw';
156
+ payload: string; // XML for toast/tile/badge; arbitrary string for raw
157
+ timestamp: string; // ISO 8601
158
+ }
159
+ ```
160
+
161
+ ---
162
+
163
+ ## How it works internally
164
+
165
+ `electron-wns` ships two PowerShell helper scripts (`scripts/`):
166
+
167
+ | Script | Purpose |
168
+ |---|---|
169
+ | `get-channel.ps1` | Calls `PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync()` (WinRT) and writes the channel URI as JSON to stdout. |
170
+ | `wns-listener.ps1` | Obtains the channel, subscribes to `PushNotificationChannel.PushNotificationReceived`, and continuously writes incoming notification events as JSON to stdout. |
171
+
172
+ The TypeScript library spawns these scripts as child processes and communicates via newline-delimited JSON on stdout, keeping the library dependency-free at runtime.
173
+
174
+ ---
175
+
176
+ ## License
177
+
178
+ MIT
179
+
@@ -0,0 +1,11 @@
1
+ /**
2
+ * WNS channel management – obtaining and caching the push notification channel URI.
3
+ */
4
+ import { WNSChannel } from './types';
5
+ /**
6
+ * Request a push notification channel URI from WNS by running the
7
+ * `get-channel.ps1` PowerShell helper.
8
+ *
9
+ * @throws {Error} If PowerShell fails or the channel URI cannot be obtained.
10
+ */
11
+ export declare function requestChannel(powershellPath: string): Promise<WNSChannel>;
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ /**
3
+ * WNS channel management – obtaining and caching the push notification channel URI.
4
+ */
5
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
6
+ if (k2 === undefined) k2 = k;
7
+ var desc = Object.getOwnPropertyDescriptor(m, k);
8
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
9
+ desc = { enumerable: true, get: function() { return m[k]; } };
10
+ }
11
+ Object.defineProperty(o, k2, desc);
12
+ }) : (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ o[k2] = m[k];
15
+ }));
16
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
18
+ }) : function(o, v) {
19
+ o["default"] = v;
20
+ });
21
+ var __importStar = (this && this.__importStar) || (function () {
22
+ var ownKeys = function(o) {
23
+ ownKeys = Object.getOwnPropertyNames || function (o) {
24
+ var ar = [];
25
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
26
+ return ar;
27
+ };
28
+ return ownKeys(o);
29
+ };
30
+ return function (mod) {
31
+ if (mod && mod.__esModule) return mod;
32
+ var result = {};
33
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
34
+ __setModuleDefault(result, mod);
35
+ return result;
36
+ };
37
+ })();
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.requestChannel = requestChannel;
40
+ const path = __importStar(require("path"));
41
+ const powershell_1 = require("./powershell");
42
+ /** Resolve the path to a bundled PowerShell script. */
43
+ function scriptPath(name) {
44
+ return path.join(__dirname, '..', 'scripts', name);
45
+ }
46
+ /**
47
+ * Request a push notification channel URI from WNS by running the
48
+ * `get-channel.ps1` PowerShell helper.
49
+ *
50
+ * @throws {Error} If PowerShell fails or the channel URI cannot be obtained.
51
+ */
52
+ async function requestChannel(powershellPath) {
53
+ var _a;
54
+ const messages = await (0, powershell_1.runPowershell)(powershellPath, scriptPath('get-channel.ps1'));
55
+ for (const msg of messages) {
56
+ if (msg.type === 'channel') {
57
+ const uri = msg.uri;
58
+ const expiresAt = msg.expiresAt;
59
+ if (typeof uri !== 'string' || typeof expiresAt !== 'string') {
60
+ throw new Error('Malformed channel message from PowerShell helper');
61
+ }
62
+ return { uri, expiresAt };
63
+ }
64
+ if (msg.type === 'error') {
65
+ throw new Error(String((_a = msg.message) !== null && _a !== void 0 ? _a : 'Unknown error from PowerShell helper'));
66
+ }
67
+ }
68
+ throw new Error('No channel message received from PowerShell helper');
69
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * electron-wns
3
+ *
4
+ * A TypeScript library for receiving Windows Push Notification Service (WNS)
5
+ * push messages in the main process of an Electron application.
6
+ *
7
+ * @example
8
+ * ```ts
9
+ * import { WNSClient } from 'electron-wns';
10
+ *
11
+ * const client = new WNSClient();
12
+ *
13
+ * // Obtain the channel URI and share it with your backend.
14
+ * const channel = await client.getChannel();
15
+ * sendToMyServer(channel.uri);
16
+ *
17
+ * // Start receiving push notifications.
18
+ * client.on('notification', (n) => {
19
+ * console.log('Received push:', n.notificationType, n.payload);
20
+ * });
21
+ * client.startListening();
22
+ * ```
23
+ */
24
+ export { WNSClient } from './wns-client';
25
+ export type { WNSChannel, WNSClientOptions, WNSClientEvents, WNSNotification, WNSNotificationType, } from './types';
package/dist/index.js ADDED
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ /**
3
+ * electron-wns
4
+ *
5
+ * A TypeScript library for receiving Windows Push Notification Service (WNS)
6
+ * push messages in the main process of an Electron application.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * import { WNSClient } from 'electron-wns';
11
+ *
12
+ * const client = new WNSClient();
13
+ *
14
+ * // Obtain the channel URI and share it with your backend.
15
+ * const channel = await client.getChannel();
16
+ * sendToMyServer(channel.uri);
17
+ *
18
+ * // Start receiving push notifications.
19
+ * client.on('notification', (n) => {
20
+ * console.log('Received push:', n.notificationType, n.payload);
21
+ * });
22
+ * client.startListening();
23
+ * ```
24
+ */
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.WNSClient = void 0;
27
+ var wns_client_1 = require("./wns-client");
28
+ Object.defineProperty(exports, "WNSClient", { enumerable: true, get: function () { return wns_client_1.WNSClient; } });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Utilities for spawning PowerShell and parsing its newline-delimited JSON output.
3
+ */
4
+ import { ChildProcess } from 'child_process';
5
+ /** A raw message envelope written by the PowerShell helper. */
6
+ export interface PSMessage {
7
+ type: 'channel' | 'notification' | 'error';
8
+ [key: string]: unknown;
9
+ }
10
+ /** Options for {@link spawnPowershell}. */
11
+ export interface SpawnPowershellOptions {
12
+ /** Path to powershell executable. */
13
+ powershellPath: string;
14
+ /** Absolute path to the `.ps1` script to run. */
15
+ scriptPath: string;
16
+ /** Called for each complete JSON line written to stdout. */
17
+ onMessage: (msg: PSMessage) => void;
18
+ /** Called when the process writes to stderr. */
19
+ onError: (err: Error) => void;
20
+ /** Called when the process exits. */
21
+ onExit: (code: number | null) => void;
22
+ }
23
+ /**
24
+ * Spawn a PowerShell process running `scriptPath` and parse its newline-delimited
25
+ * JSON stdout into {@link PSMessage} objects.
26
+ *
27
+ * Returns the {@link ChildProcess} so the caller can stop it.
28
+ */
29
+ export declare function spawnPowershell(opts: SpawnPowershellOptions): ChildProcess;
30
+ /**
31
+ * Run a PowerShell script, collect all stdout, and resolve with the parsed
32
+ * array of {@link PSMessage} objects. Rejects if the process exits non-zero.
33
+ */
34
+ export declare function runPowershell(powershellPath: string, scriptPath: string): Promise<PSMessage[]>;
35
+ /** Return the default powershell executable name for the current platform. */
36
+ export declare function defaultPowershellPath(): string;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ /**
3
+ * Utilities for spawning PowerShell and parsing its newline-delimited JSON output.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.spawnPowershell = spawnPowershell;
7
+ exports.runPowershell = runPowershell;
8
+ exports.defaultPowershellPath = defaultPowershellPath;
9
+ const child_process_1 = require("child_process");
10
+ /**
11
+ * Spawn a PowerShell process running `scriptPath` and parse its newline-delimited
12
+ * JSON stdout into {@link PSMessage} objects.
13
+ *
14
+ * Returns the {@link ChildProcess} so the caller can stop it.
15
+ */
16
+ function spawnPowershell(opts) {
17
+ const ps = (0, child_process_1.spawn)(opts.powershellPath, [
18
+ '-NonInteractive',
19
+ '-NoProfile',
20
+ '-ExecutionPolicy', 'Bypass',
21
+ '-File', opts.scriptPath,
22
+ ], { stdio: ['ignore', 'pipe', 'pipe'] });
23
+ let buffer = '';
24
+ ps.stdout.setEncoding('utf8');
25
+ ps.stdout.on('data', (chunk) => {
26
+ var _a;
27
+ buffer += chunk;
28
+ const lines = buffer.split(/\r?\n/);
29
+ buffer = (_a = lines.pop()) !== null && _a !== void 0 ? _a : '';
30
+ for (const line of lines) {
31
+ const trimmed = line.trim();
32
+ if (!trimmed)
33
+ continue;
34
+ try {
35
+ const msg = JSON.parse(trimmed);
36
+ opts.onMessage(msg);
37
+ }
38
+ catch {
39
+ // Ignore non-JSON lines (e.g. debug output from PowerShell)
40
+ }
41
+ }
42
+ });
43
+ ps.stderr.setEncoding('utf8');
44
+ ps.stderr.on('data', (chunk) => {
45
+ opts.onError(new Error(chunk.trim()));
46
+ });
47
+ ps.on('close', (code) => {
48
+ opts.onExit(code);
49
+ });
50
+ return ps;
51
+ }
52
+ /**
53
+ * Run a PowerShell script, collect all stdout, and resolve with the parsed
54
+ * array of {@link PSMessage} objects. Rejects if the process exits non-zero.
55
+ */
56
+ function runPowershell(powershellPath, scriptPath) {
57
+ return new Promise((resolve, reject) => {
58
+ const messages = [];
59
+ const proc = spawnPowershell({
60
+ powershellPath,
61
+ scriptPath,
62
+ onMessage: (msg) => messages.push(msg),
63
+ onError: (err) => {
64
+ // Collect stderr but don't reject yet – wait for exit code
65
+ messages.push({ type: 'error', message: err.message });
66
+ },
67
+ onExit: (code) => {
68
+ if (code !== 0) {
69
+ const errMsg = messages
70
+ .filter((m) => m.type === 'error')
71
+ .map((m) => String(m.message))
72
+ .join(' ');
73
+ reject(new Error(`PowerShell exited with code ${code}: ${errMsg}`));
74
+ }
75
+ else {
76
+ resolve(messages);
77
+ }
78
+ },
79
+ });
80
+ // Keep a reference so the GC doesn't collect the process
81
+ void proc;
82
+ });
83
+ }
84
+ /** Return the default powershell executable name for the current platform. */
85
+ function defaultPowershellPath() {
86
+ return process.platform === 'win32' ? 'powershell.exe' : 'pwsh';
87
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Type definitions for electron-wns
3
+ */
4
+ /** The type of a WNS notification payload. */
5
+ export type WNSNotificationType = 'toast' | 'tile' | 'badge' | 'raw';
6
+ /** A notification received from WNS. */
7
+ export interface WNSNotification {
8
+ /** The notification type as reported by WNS. */
9
+ notificationType: WNSNotificationType;
10
+ /** The raw string payload of the notification (XML for toast/tile/badge, any string for raw). */
11
+ payload: string;
12
+ /** ISO 8601 timestamp of when the notification was received. */
13
+ timestamp: string;
14
+ }
15
+ /** A WNS push notification channel. */
16
+ export interface WNSChannel {
17
+ /** The channel URI to send to your backend server. */
18
+ uri: string;
19
+ /** ISO 8601 expiry time – request a new channel URI after this point. */
20
+ expiresAt: string;
21
+ }
22
+ /** Options for creating a {@link WNSClient}. */
23
+ export interface WNSClientOptions {
24
+ /**
25
+ * Override the path to `powershell.exe` (or `pwsh`).
26
+ * Defaults to `powershell.exe` on Windows, `pwsh` elsewhere.
27
+ */
28
+ powershellPath?: string;
29
+ }
30
+ /** Events emitted by {@link WNSClient}. */
31
+ export interface WNSClientEvents {
32
+ /** Fired when a new channel URI is obtained from WNS. */
33
+ channelUpdated: [channel: WNSChannel];
34
+ /** Fired each time a push notification arrives. */
35
+ notification: [notification: WNSNotification];
36
+ /** Fired when a recoverable error occurs in the background listener. */
37
+ error: [error: Error];
38
+ }
package/dist/types.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ /**
3
+ * Type definitions for electron-wns
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,63 @@
1
+ /**
2
+ * WNSClient – the main entry point for receiving WNS push notifications in an
3
+ * Electron main process.
4
+ *
5
+ * Usage:
6
+ * ```ts
7
+ * import { WNSClient } from 'electron-wns';
8
+ *
9
+ * const client = new WNSClient();
10
+ *
11
+ * const channel = await client.getChannel();
12
+ * console.log('Send this URI to your backend:', channel.uri);
13
+ *
14
+ * client.on('notification', (n) => console.log('Push received:', n.payload));
15
+ * client.startListening();
16
+ * ```
17
+ */
18
+ import { EventEmitter } from 'events';
19
+ import { WNSChannel, WNSClientOptions } from './types';
20
+ export declare class WNSClient extends EventEmitter {
21
+ private readonly powershellPath;
22
+ private cachedChannel;
23
+ private listenerProcess;
24
+ constructor(options?: WNSClientOptions);
25
+ /**
26
+ * Obtain a WNS push notification channel for this application.
27
+ *
28
+ * The result is cached in memory; call `refreshChannel()` to force a
29
+ * fresh request (e.g. after the channel expiry date has passed).
30
+ *
31
+ * @throws {Error} On Windows if the app does not have a package identity
32
+ * (MSIX packaging is required for `PushNotificationChannelManager`).
33
+ */
34
+ getChannel(): Promise<WNSChannel>;
35
+ /**
36
+ * Force a new channel request and update the internal cache.
37
+ * Emits `channelUpdated` with the new channel.
38
+ */
39
+ refreshChannel(): Promise<WNSChannel>;
40
+ /**
41
+ * Start listening for incoming push notifications.
42
+ *
43
+ * Spawns a PowerShell background process (`wns-listener.ps1`) that
44
+ * registers for `PushNotificationReceived` events on the WNS channel and
45
+ * pipes them back as newline-delimited JSON.
46
+ *
47
+ * Emits:
48
+ * - `channelUpdated` once the channel has been registered
49
+ * - `notification` for each incoming push
50
+ * - `error` for non-fatal errors (the listener continues running)
51
+ *
52
+ * Calling `startListening()` while already listening is a no-op.
53
+ */
54
+ startListening(): void;
55
+ /**
56
+ * Stop the background listener. Safe to call even if not currently
57
+ * listening.
58
+ */
59
+ stopListening(): void;
60
+ /** Returns `true` if the background listener is currently running. */
61
+ isListening(): boolean;
62
+ private handleListenerMessage;
63
+ }
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ /**
3
+ * WNSClient – the main entry point for receiving WNS push notifications in an
4
+ * Electron main process.
5
+ *
6
+ * Usage:
7
+ * ```ts
8
+ * import { WNSClient } from 'electron-wns';
9
+ *
10
+ * const client = new WNSClient();
11
+ *
12
+ * const channel = await client.getChannel();
13
+ * console.log('Send this URI to your backend:', channel.uri);
14
+ *
15
+ * client.on('notification', (n) => console.log('Push received:', n.payload));
16
+ * client.startListening();
17
+ * ```
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.WNSClient = void 0;
54
+ const events_1 = require("events");
55
+ const path = __importStar(require("path"));
56
+ const powershell_1 = require("./powershell");
57
+ const channel_1 = require("./channel");
58
+ class WNSClient extends events_1.EventEmitter {
59
+ constructor(options = {}) {
60
+ var _a;
61
+ super();
62
+ this.cachedChannel = null;
63
+ this.listenerProcess = null;
64
+ this.powershellPath = (_a = options.powershellPath) !== null && _a !== void 0 ? _a : (0, powershell_1.defaultPowershellPath)();
65
+ }
66
+ // ── Channel ────────────────────────────────────────────────────────────────
67
+ /**
68
+ * Obtain a WNS push notification channel for this application.
69
+ *
70
+ * The result is cached in memory; call `refreshChannel()` to force a
71
+ * fresh request (e.g. after the channel expiry date has passed).
72
+ *
73
+ * @throws {Error} On Windows if the app does not have a package identity
74
+ * (MSIX packaging is required for `PushNotificationChannelManager`).
75
+ */
76
+ async getChannel() {
77
+ if (!this.cachedChannel) {
78
+ this.cachedChannel = await (0, channel_1.requestChannel)(this.powershellPath);
79
+ }
80
+ return this.cachedChannel;
81
+ }
82
+ /**
83
+ * Force a new channel request and update the internal cache.
84
+ * Emits `channelUpdated` with the new channel.
85
+ */
86
+ async refreshChannel() {
87
+ this.cachedChannel = await (0, channel_1.requestChannel)(this.powershellPath);
88
+ this.emit('channelUpdated', this.cachedChannel);
89
+ return this.cachedChannel;
90
+ }
91
+ // ── Listening ──────────────────────────────────────────────────────────────
92
+ /**
93
+ * Start listening for incoming push notifications.
94
+ *
95
+ * Spawns a PowerShell background process (`wns-listener.ps1`) that
96
+ * registers for `PushNotificationReceived` events on the WNS channel and
97
+ * pipes them back as newline-delimited JSON.
98
+ *
99
+ * Emits:
100
+ * - `channelUpdated` once the channel has been registered
101
+ * - `notification` for each incoming push
102
+ * - `error` for non-fatal errors (the listener continues running)
103
+ *
104
+ * Calling `startListening()` while already listening is a no-op.
105
+ */
106
+ startListening() {
107
+ if (this.listenerProcess)
108
+ return;
109
+ const scriptFile = path.join(__dirname, '..', 'scripts', 'wns-listener.ps1');
110
+ this.listenerProcess = (0, powershell_1.spawnPowershell)({
111
+ powershellPath: this.powershellPath,
112
+ scriptPath: scriptFile,
113
+ onMessage: (msg) => this.handleListenerMessage(msg),
114
+ onError: (err) => this.emit('error', err),
115
+ onExit: (code) => {
116
+ this.listenerProcess = null;
117
+ if (code !== 0 && code !== null) {
118
+ this.emit('error', new Error(`WNS listener exited with code ${code}`));
119
+ }
120
+ },
121
+ });
122
+ }
123
+ /**
124
+ * Stop the background listener. Safe to call even if not currently
125
+ * listening.
126
+ */
127
+ stopListening() {
128
+ if (this.listenerProcess) {
129
+ this.listenerProcess.kill();
130
+ this.listenerProcess = null;
131
+ }
132
+ }
133
+ /** Returns `true` if the background listener is currently running. */
134
+ isListening() {
135
+ return this.listenerProcess !== null;
136
+ }
137
+ // ── Internal ───────────────────────────────────────────────────────────────
138
+ handleListenerMessage(msg) {
139
+ var _a, _b, _c, _d, _e, _f;
140
+ switch (msg.type) {
141
+ case 'channel': {
142
+ const uri = String((_a = msg.uri) !== null && _a !== void 0 ? _a : '');
143
+ const expiresAt = String((_b = msg.expiresAt) !== null && _b !== void 0 ? _b : '');
144
+ if (uri) {
145
+ const channel = { uri, expiresAt };
146
+ this.cachedChannel = channel;
147
+ this.emit('channelUpdated', channel);
148
+ }
149
+ break;
150
+ }
151
+ case 'notification': {
152
+ const notification = {
153
+ notificationType: (_c = msg.notificationType) !== null && _c !== void 0 ? _c : 'raw',
154
+ payload: String((_d = msg.payload) !== null && _d !== void 0 ? _d : ''),
155
+ timestamp: String((_e = msg.timestamp) !== null && _e !== void 0 ? _e : new Date().toISOString()),
156
+ };
157
+ this.emit('notification', notification);
158
+ break;
159
+ }
160
+ case 'error': {
161
+ this.emit('error', new Error(String((_f = msg.message) !== null && _f !== void 0 ? _f : 'Unknown listener error')));
162
+ break;
163
+ }
164
+ }
165
+ }
166
+ }
167
+ exports.WNSClient = WNSClient;
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "electron-wns",
3
+ "version": "0.0.0",
4
+ "description": "Library for receiving Windows Push Notification Service (WNS) push messages in Electron apps",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist",
9
+ "scripts"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc",
13
+ "test": "jest",
14
+ "prepare": "npm run build"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/iotum/electron-wns.git"
19
+ },
20
+ "keywords": [
21
+ "electron",
22
+ "wns",
23
+ "push",
24
+ "notifications",
25
+ "windows"
26
+ ],
27
+ "author": "",
28
+ "license": "MIT",
29
+ "bugs": {
30
+ "url": "https://github.com/iotum/electron-wns/issues"
31
+ },
32
+ "homepage": "https://github.com/iotum/electron-wns#readme",
33
+ "devDependencies": {
34
+ "@types/jest": "^29.5.14",
35
+ "@types/node": "^20.19.37",
36
+ "jest": "^29.7.0",
37
+ "ts-jest": "^29.4.6",
38
+ "typescript": "^5.9.3"
39
+ }
40
+ }
@@ -0,0 +1,73 @@
1
+ # get-channel.ps1
2
+ #
3
+ # Obtains a WNS push notification channel URI for the current application and
4
+ # writes a single JSON object to stdout:
5
+ #
6
+ # { "type": "channel", "uri": "<channel-uri>", "expiresAt": "<ISO-8601>" }
7
+ #
8
+ # On failure writes:
9
+ # { "type": "error", "message": "<description>" }
10
+ # and exits with code 1.
11
+ #
12
+ # Requirements:
13
+ # - Windows 10 / Windows Server 2019 or later
14
+ # - The calling application must have a package identity (MSIX-packaged).
15
+
16
+ [CmdletBinding()]
17
+ param()
18
+
19
+ Set-StrictMode -Version Latest
20
+ $ErrorActionPreference = 'Stop'
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Helper: convert a WinRT IAsyncOperation<T> to a .NET Task<T> and wait on it
24
+ # ---------------------------------------------------------------------------
25
+ function Await-WinRTAsync {
26
+ param(
27
+ [Parameter(Mandatory)] $AsyncOperation,
28
+ [Parameter(Mandatory)] [Type] $ResultType
29
+ )
30
+
31
+ Add-Type -AssemblyName System.Runtime.WindowsRuntime | Out-Null
32
+
33
+ $extensionMethod = [System.WindowsRuntimeSystemExtensions].GetMethods() |
34
+ Where-Object {
35
+ $_.Name -eq 'AsTask' -and
36
+ $_.GetParameters().Count -eq 1 -and
37
+ $_.IsGenericMethod
38
+ } |
39
+ Select-Object -First 1
40
+
41
+ $task = $extensionMethod.MakeGenericMethod($ResultType).Invoke($null, @($AsyncOperation))
42
+ $task.Wait() | Out-Null
43
+ return $task.Result
44
+ }
45
+
46
+ try {
47
+ Add-Type -AssemblyName System.Runtime.WindowsRuntime | Out-Null
48
+
49
+ # Load WinRT push-notification types
50
+ $channelManagerType = [Windows.Networking.PushNotifications.PushNotificationChannelManager,
51
+ Windows.Networking.PushNotifications, ContentType=WindowsRuntime]
52
+ $channelType = [Windows.Networking.PushNotifications.PushNotificationChannel,
53
+ Windows.Networking.PushNotifications, ContentType=WindowsRuntime]
54
+
55
+ # Request a channel (async → sync)
56
+ $asyncOp = $channelManagerType::CreatePushNotificationChannelForApplicationAsync()
57
+ $channel = Await-WinRTAsync -AsyncOperation $asyncOp -ResultType $channelType
58
+
59
+ $result = [PSCustomObject]@{
60
+ type = 'channel'
61
+ uri = $channel.Uri
62
+ expiresAt = $channel.ExpirationTime.ToUniversalTime().ToString('o')
63
+ }
64
+ Write-Output (ConvertTo-Json $result -Compress)
65
+ }
66
+ catch {
67
+ $err = [PSCustomObject]@{
68
+ type = 'error'
69
+ message = $_.Exception.Message
70
+ }
71
+ Write-Output (ConvertTo-Json $err -Compress)
72
+ exit 1
73
+ }
@@ -0,0 +1,136 @@
1
+ # wns-listener.ps1
2
+ #
3
+ # Registers a WNS push notification channel for the current application,
4
+ # emits the channel details, and then waits for push notifications.
5
+ #
6
+ # Every event is written to stdout as a compact JSON object followed by a
7
+ # newline so that the Node.js parent process can parse it incrementally.
8
+ #
9
+ # Message types:
10
+ # { "type": "channel", "uri": "...", "expiresAt": "..." }
11
+ # { "type": "notification", "notificationType": "...", "payload": "...", "timestamp": "..." }
12
+ # { "type": "error", "message": "..." }
13
+ #
14
+ # The script runs until the parent process terminates it (SIGTERM / kill).
15
+ #
16
+ # Requirements:
17
+ # - Windows 10 / Windows Server 2019 or later
18
+ # - The calling application must have a package identity (MSIX-packaged).
19
+
20
+ [CmdletBinding()]
21
+ param()
22
+
23
+ Set-StrictMode -Version Latest
24
+ $ErrorActionPreference = 'Stop'
25
+
26
+ # ---------------------------------------------------------------------------
27
+ # Helper: convert a WinRT IAsyncOperation<T> to a .NET Task<T> and wait
28
+ # ---------------------------------------------------------------------------
29
+ function Await-WinRTAsync {
30
+ param(
31
+ [Parameter(Mandatory)] $AsyncOperation,
32
+ [Parameter(Mandatory)] [Type] $ResultType
33
+ )
34
+ Add-Type -AssemblyName System.Runtime.WindowsRuntime | Out-Null
35
+ $extensionMethod = [System.WindowsRuntimeSystemExtensions].GetMethods() |
36
+ Where-Object {
37
+ $_.Name -eq 'AsTask' -and
38
+ $_.GetParameters().Count -eq 1 -and
39
+ $_.IsGenericMethod
40
+ } |
41
+ Select-Object -First 1
42
+ $task = $extensionMethod.MakeGenericMethod($ResultType).Invoke($null, @($AsyncOperation))
43
+ $task.Wait() | Out-Null
44
+ return $task.Result
45
+ }
46
+
47
+ # ---------------------------------------------------------------------------
48
+ # Write a JSON message to stdout (single line)
49
+ # ---------------------------------------------------------------------------
50
+ function Write-JsonMessage {
51
+ param([hashtable] $Data)
52
+ [Console]::Out.WriteLine((ConvertTo-Json ([PSCustomObject]$Data) -Compress -Depth 5))
53
+ [Console]::Out.Flush()
54
+ }
55
+
56
+ try {
57
+ Add-Type -AssemblyName System.Runtime.WindowsRuntime | Out-Null
58
+
59
+ $channelManagerType = [Windows.Networking.PushNotifications.PushNotificationChannelManager,
60
+ Windows.Networking.PushNotifications, ContentType=WindowsRuntime]
61
+ $channelType = [Windows.Networking.PushNotifications.PushNotificationChannel,
62
+ Windows.Networking.PushNotifications, ContentType=WindowsRuntime]
63
+
64
+ # Obtain the channel
65
+ $asyncOp = $channelManagerType::CreatePushNotificationChannelForApplicationAsync()
66
+ $channel = Await-WinRTAsync -AsyncOperation $asyncOp -ResultType $channelType
67
+
68
+ # Report the channel URI to the Node.js process
69
+ Write-JsonMessage @{
70
+ type = 'channel'
71
+ uri = $channel.Uri
72
+ expiresAt = $channel.ExpirationTime.ToUniversalTime().ToString('o')
73
+ }
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # Subscribe to incoming push notifications
77
+ # ---------------------------------------------------------------------------
78
+ # A thread-safe queue shared between the WinRT event callback and the
79
+ # polling loop below.
80
+ $queue = [System.Collections.Concurrent.ConcurrentQueue[hashtable]]::new()
81
+
82
+ $handlerBlock = {
83
+ param($sender, $eventArgs)
84
+ $n = $eventArgs.Notification
85
+ $notifType = $n.NotificationType.ToString().ToLower()
86
+ $payload = ''
87
+ if ($notifType -eq 'raw') {
88
+ $payload = $n.RawNotification.Content
89
+ }
90
+ elseif ($notifType -eq 'toast') {
91
+ $payload = $n.ToastNotification.Content.GetXml()
92
+ }
93
+ elseif ($notifType -eq 'tile') {
94
+ $payload = $n.TileNotification.Content.GetXml()
95
+ }
96
+ elseif ($notifType -eq 'badge') {
97
+ $payload = $n.BadgeNotification.Content.GetXml()
98
+ }
99
+
100
+ # Mark the notification as handled so Windows does not show a system UI
101
+ $eventArgs.Cancel = $true
102
+
103
+ $queue.Enqueue(@{
104
+ type = 'notification'
105
+ notificationType = $notifType
106
+ payload = $payload
107
+ timestamp = [DateTime]::UtcNow.ToString('o')
108
+ })
109
+ }
110
+
111
+ $receivedType = [Windows.Foundation.TypedEventHandler``2].MakeGenericType(
112
+ $channelType,
113
+ [Windows.Networking.PushNotifications.PushNotificationReceivedEventArgs,
114
+ Windows.Networking.PushNotifications, ContentType=WindowsRuntime]
115
+ )
116
+ $delegate = [Delegate]::CreateDelegate($receivedType, $handlerBlock.GetNewClosure(), 'Invoke')
117
+ $channel.add_PushNotificationReceived($delegate)
118
+
119
+ # ---------------------------------------------------------------------------
120
+ # Poll the queue until the process is terminated
121
+ # ---------------------------------------------------------------------------
122
+ while ($true) {
123
+ $item = $null
124
+ while ($queue.TryDequeue([ref] $item)) {
125
+ Write-JsonMessage $item
126
+ }
127
+ Start-Sleep -Milliseconds 200
128
+ }
129
+ }
130
+ catch {
131
+ Write-JsonMessage @{
132
+ type = 'error'
133
+ message = $_.Exception.Message
134
+ }
135
+ exit 1
136
+ }