xnotif 0.1.0 → 0.2.0-beta.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Yuta Kobayashi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,149 @@
1
+ # xnotif
2
+
3
+ [![npm](https://img.shields.io/npm/v/xnotif)](https://www.npmjs.com/package/xnotif)
4
+ [![CI](https://github.com/yutakobayashidev/xnotif/actions/workflows/ci.yml/badge.svg)](https://github.com/yutakobayashidev/xnotif/actions/workflows/ci.yml)
5
+
6
+ Receive Twitter/X notifications in real-time. No API key, no scraping — just Web Push.
7
+
8
+ ```typescript
9
+ import { createClient } from "xnotif";
10
+
11
+ const client = createClient({
12
+ cookies: { auth_token: "...", ct0: "..." },
13
+ });
14
+
15
+ client.on("notification", (n) => {
16
+ console.log(`${n.title}: ${n.body}`);
17
+ });
18
+
19
+ await client.start();
20
+ ```
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install xnotif
26
+ ```
27
+
28
+ > Requires Node.js >= 22.0.0
29
+
30
+ ## Notification Payload
31
+
32
+ Each `notification` event delivers a `TwitterNotification` object:
33
+
34
+ ```jsonc
35
+ {
36
+ "title": "@jack",
37
+ "body": "just setting up my twttr",
38
+ "icon": "https://pbs.twimg.com/profile_images/...",
39
+ "timestamp": 1142974214000,
40
+ "tag": "mention_12345",
41
+ "data": {
42
+ "type": "mention",
43
+ "uri": "https://x.com/i/web/status/20",
44
+ "title": "@jack",
45
+ "body": "just setting up my twttr",
46
+ "tag": "mention_12345",
47
+ "lang": "en",
48
+ "scribe_target": "mention",
49
+ "impression_id": "abc123",
50
+ },
51
+ }
52
+ ```
53
+
54
+ Top-level fields:
55
+
56
+ | Field | Type | Description |
57
+ | ----------- | --------- | ------------------------------------------------- |
58
+ | `title` | `string` | Who triggered the notification |
59
+ | `body` | `string` | Human-readable description |
60
+ | `icon` | `string?` | Profile image URL |
61
+ | `timestamp` | `number?` | Unix epoch in milliseconds |
62
+ | `tag` | `string?` | Deduplication tag |
63
+ | `data` | `object?` | Structured metadata (see `data.type` for routing) |
64
+
65
+ ## Getting Cookies
66
+
67
+ 1. Log in to [x.com](https://x.com)
68
+ 2. DevTools → Application → Cookies
69
+ 3. Copy `auth_token` and `ct0`
70
+
71
+ ## State Persistence
72
+
73
+ Save the `ClientState` from the `connected` event to skip key generation on restart:
74
+
75
+ ```typescript
76
+ import { createClient, type ClientState } from "xnotif";
77
+
78
+ let state: ClientState | undefined = loadFromDisk(); // your persistence
79
+
80
+ const client = createClient({ cookies: { auth_token: "...", ct0: "..." }, state });
81
+
82
+ client.on("connected", (s) => saveToDisk(s));
83
+
84
+ await client.start();
85
+ ```
86
+
87
+ ## API
88
+
89
+ ### `createClient(options)`
90
+
91
+ | Option | Type | Required | Description |
92
+ | --------- | ------------------------------------- | -------- | ---------------------- |
93
+ | `cookies` | `{ auth_token: string; ct0: string }` | Yes | Session cookies |
94
+ | `state` | `ClientState` | No | Restore previous state |
95
+
96
+ ### Events
97
+
98
+ | Event | Payload | Description |
99
+ | -------------- | --------------------- | ------------------------------ |
100
+ | `notification` | `TwitterNotification` | Decrypted notification |
101
+ | `connected` | `ClientState` | Connected — persist this state |
102
+ | `error` | `Error` | Error (connection continues) |
103
+ | `disconnected` | — | WebSocket closed |
104
+ | `reconnecting` | `number` | Reconnecting in N ms |
105
+
106
+ ### Methods
107
+
108
+ - **`client.start()`** — Connect and begin receiving notifications
109
+ - **`client.stop()`** — Disconnect
110
+
111
+ ### Low-level Exports
112
+
113
+ - `Decryptor` — AESGCM Web Push decryption (ECDH + HKDF + AES-128-GCM)
114
+ - `AutopushClient` — Mozilla Autopush WebSocket client
115
+
116
+ ## How It Works
117
+
118
+ ```mermaid
119
+ sequenceDiagram
120
+ participant App as xnotif
121
+ participant Autopush as Mozilla Autopush<br/>wss://push.services.mozilla.com
122
+ participant Twitter as Twitter/X
123
+
124
+ App->>App: Generate ECDH P-256 key pair + 16-byte auth secret
125
+ App->>Autopush: WebSocket connect (subprotocol: push-notification)
126
+ Autopush-->>App: hello ACK (uaid assigned)
127
+ App->>Autopush: Register channel with VAPID key
128
+ Autopush-->>App: Push Endpoint URL
129
+
130
+ App->>Twitter: POST /1.1/notifications/settings/login.json<br/>{ token: endpoint, encryption_key1: p256dh, encryption_key2: auth }
131
+ Twitter-->>App: 200 OK
132
+
133
+ loop Real-time notifications
134
+ Twitter->>Autopush: Web Push (AESGCM encrypted payload)
135
+ Autopush->>App: WebSocket message
136
+ App->>App: ECDH shared secret (256-bit)<br/>→ HKDF-SHA256 (IKM, CEK, nonce)<br/>→ AES-128-GCM decrypt<br/>→ Strip 2-byte padding
137
+ App-->>App: Emit "notification" event
138
+ end
139
+ ```
140
+
141
+ 1. **Key generation** — Generate an ECDH P-256 key pair and a 16-byte auth secret via `crypto.subtle` (skipped when restoring from saved `state`)
142
+ 2. **Autopush connection** — Open a WebSocket to `wss://push.services.mozilla.com` with the `push-notification` subprotocol, send a `hello` handshake, then register a channel to obtain a Push Endpoint URL
143
+ 3. **Twitter registration** — POST the Push Endpoint, base64url-encoded public key, and auth secret to Twitter's `/1.1/notifications/settings/login.json`, authenticated with your session cookies (`auth_token` / `ct0`)
144
+ 4. **Receive & decrypt** — When Twitter pushes an AESGCM-encrypted payload through Autopush, derive a shared secret via ECDH, expand it with HKDF-SHA256 into a 16-byte CEK and 12-byte nonce, then decrypt with AES-128-GCM
145
+ 5. **Emit** — Parse the decrypted JSON into a `TwitterNotification` and fire it as a `notification` event
146
+
147
+ ## License
148
+
149
+ MIT
@@ -0,0 +1,114 @@
1
+ import { EventEmitter } from "events";
2
+
3
+ //#region src/types.d.ts
4
+ interface AutopushNotification {
5
+ messageType: "notification";
6
+ channelID: string;
7
+ version: string;
8
+ data: string;
9
+ headers: {
10
+ crypto_key: string;
11
+ encryption: string;
12
+ };
13
+ }
14
+ interface TwitterNotification {
15
+ title: string;
16
+ body: string;
17
+ icon?: string;
18
+ timestamp?: number;
19
+ tag?: string;
20
+ data?: {
21
+ type?: string;
22
+ uri?: string;
23
+ title?: string;
24
+ body?: string;
25
+ tag?: string;
26
+ lang?: string;
27
+ bundle_text?: string;
28
+ scribe_target?: string;
29
+ impression_id?: string;
30
+ [key: string]: unknown;
31
+ };
32
+ [key: string]: unknown;
33
+ }
34
+ interface ClientState {
35
+ uaid: string;
36
+ channelId: string;
37
+ endpoint: string;
38
+ remoteBroadcasts: Record<string, string>;
39
+ decryptor: {
40
+ jwk: JsonWebKey;
41
+ auth: string;
42
+ };
43
+ }
44
+ interface NotificationClientOptions {
45
+ cookies: {
46
+ auth_token: string;
47
+ ct0: string;
48
+ [key: string]: string;
49
+ };
50
+ state?: ClientState;
51
+ }
52
+ //#endregion
53
+ //#region src/client.d.ts
54
+ interface NotificationClientEvents {
55
+ notification: [notification: TwitterNotification];
56
+ connected: [state: ClientState];
57
+ error: [error: Error];
58
+ disconnected: [];
59
+ reconnecting: [delay: number];
60
+ }
61
+ declare class NotificationClient extends EventEmitter<NotificationClientEvents> {
62
+ private autopush;
63
+ private running;
64
+ private options;
65
+ constructor(options: NotificationClientOptions);
66
+ start(): Promise<void>;
67
+ stop(): void;
68
+ }
69
+ declare function createClient(options: NotificationClientOptions): NotificationClient;
70
+ //#endregion
71
+ //#region src/decrypt.d.ts
72
+ declare class Decryptor {
73
+ private keyPair;
74
+ private publicKeyRaw;
75
+ private authSecret;
76
+ private jwk;
77
+ private constructor();
78
+ static create(jwk?: JsonWebKey, authBase64url?: string): Promise<Decryptor>;
79
+ getJwk(): JsonWebKey;
80
+ getAuthBase64url(): string;
81
+ getPublicKeyBase64url(): string;
82
+ decrypt(cryptoKeyHeader: string, encryptionHeader: string, payload: ArrayBuffer): Promise<string>;
83
+ }
84
+ //#endregion
85
+ //#region src/autopush.d.ts
86
+ interface AutopushOptions {
87
+ uaid?: string;
88
+ channelId: string;
89
+ vapidKey: string;
90
+ remoteBroadcasts?: Record<string, string>;
91
+ onNotification: (notification: AutopushNotification) => void;
92
+ onError?: (error: unknown) => void;
93
+ onDisconnected?: () => void;
94
+ onReconnecting?: (delay: number) => void;
95
+ }
96
+ declare class AutopushClient {
97
+ private options;
98
+ private ws;
99
+ private uaid;
100
+ private endpoint;
101
+ private reconnectDelay;
102
+ private closed;
103
+ private remoteBroadcasts;
104
+ constructor(options: AutopushOptions);
105
+ getUaid(): string;
106
+ getEndpoint(): string;
107
+ getRemoteBroadcasts(): Record<string, string>;
108
+ connect(): Promise<string>;
109
+ close(): void;
110
+ private send;
111
+ private reconnect;
112
+ }
113
+ //#endregion
114
+ export { AutopushClient, type AutopushNotification, type AutopushOptions, type ClientState, Decryptor, NotificationClient, type NotificationClientOptions, type TwitterNotification, createClient };