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 +21 -0
- package/README.md +149 -0
- package/dist/index.d.mts +114 -0
- package/dist/index.mjs +438 -0
- package/package.json +17 -12
- package/dist/autopush.d.ts +0 -29
- package/dist/autopush.d.ts.map +0 -1
- package/dist/client.d.ts +0 -20
- package/dist/client.d.ts.map +0 -1
- package/dist/decrypt.d.ts +0 -13
- package/dist/decrypt.d.ts.map +0 -1
- package/dist/index.d.ts +0 -6
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -447
- package/dist/twitter.d.ts +0 -9
- package/dist/twitter.d.ts.map +0 -1
- package/dist/types.d.ts +0 -49
- package/dist/types.d.ts.map +0 -1
- package/dist/utils.d.ts +0 -4
- package/dist/utils.d.ts.map +0 -1
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
|
+
[](https://www.npmjs.com/package/xnotif)
|
|
4
|
+
[](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
|
package/dist/index.d.mts
ADDED
|
@@ -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 };
|