@zeph-to/hook-sdk 1.4.0 → 1.5.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.
@@ -0,0 +1,59 @@
1
+ /**
2
+ * E2E encryption for Hook SDK — self-contained ECDH P-256 + AES-256-GCM
3
+ * Mirrors @zeph/crypto API but bundled inline (no external dependency).
4
+ * Uses Web Crypto API (globalThis.crypto.subtle) — Node.js 18+.
5
+ */
6
+ /**
7
+ * Initialize crypto: sync keys with server, then fallback to local/generate.
8
+ * Server is source of truth for per-user key pair.
9
+ * Safe to call concurrently — deduplicates to single init.
10
+ * Returns the exported public key (Base64 SPKI).
11
+ */
12
+ export declare const initCrypto: (apiKey?: string, baseUrl?: string) => Promise<string>;
13
+ export declare const getKeyPair: () => CryptoKeyPair | null;
14
+ export declare const getPublicKey: () => string | null;
15
+ /**
16
+ * Encrypt push body for a recipient.
17
+ * Returns fields ready to merge into the sendPush payload.
18
+ */
19
+ export declare const encryptPushBody: (input: {
20
+ title?: string;
21
+ body?: string;
22
+ url?: string;
23
+ }, recipientPublicKeyRaw: string) => Promise<{
24
+ body: string;
25
+ encryptedKey: string;
26
+ senderPublicKey: string;
27
+ isEncrypted: true;
28
+ }>;
29
+ /**
30
+ * Encrypt push body for self (all own devices).
31
+ */
32
+ export declare const encryptPushBodyForSelf: (input: {
33
+ title?: string;
34
+ body?: string;
35
+ url?: string;
36
+ }) => Promise<{
37
+ body: string;
38
+ encryptedKey: string;
39
+ senderPublicKey: string;
40
+ isEncrypted: true;
41
+ }>;
42
+ /**
43
+ * Encrypt file content for a recipient.
44
+ * Returns encrypted buffer + key material for file attachment metadata.
45
+ */
46
+ export declare const encryptFileForRecipient: (content: string, recipientPublicKeyRaw: string) => Promise<{
47
+ ciphertext: Buffer;
48
+ iv: string;
49
+ encryptedKey: string;
50
+ }>;
51
+ /**
52
+ * Encrypt file content for self (all own devices).
53
+ */
54
+ export declare const encryptFileForSelf: (content: string) => Promise<{
55
+ ciphertext: Buffer;
56
+ iv: string;
57
+ encryptedKey: string;
58
+ }>;
59
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA0JH;;;;;GAKG;AACH,eAAO,MAAM,UAAU,GAAI,SAAS,MAAM,EAAE,UAAU,MAAM,KAAG,OAAO,CAAC,MAAM,CA6D5E,CAAC;AA4BF,eAAO,MAAM,UAAU,QAAO,aAAa,GAAG,IAAqB,CAAC;AACpE,eAAO,MAAM,YAAY,QAAO,MAAM,GAAG,IAA+B,CAAC;AAEzE;;;GAGG;AACH,eAAO,MAAM,eAAe,GAC1B,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACtD,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAeA,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,KACrD,OAAO,CAAC;IACT,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,IAAI,CAAC;CACnB,CAaA,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAClC,SAAS,MAAM,EACf,uBAAuB,MAAM,KAC5B,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CASlE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,kBAAkB,GAC7B,SAAS,MAAM,KACd,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAQlE,CAAC"}
package/dist/crypto.js ADDED
@@ -0,0 +1,257 @@
1
+ "use strict";
2
+ /**
3
+ * E2E encryption for Hook SDK — self-contained ECDH P-256 + AES-256-GCM
4
+ * Mirrors @zeph/crypto API but bundled inline (no external dependency).
5
+ * Uses Web Crypto API (globalThis.crypto.subtle) — Node.js 18+.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.encryptFileForSelf = exports.encryptFileForRecipient = exports.encryptPushBodyForSelf = exports.encryptPushBody = exports.getPublicKey = exports.getKeyPair = exports.initCrypto = void 0;
9
+ /// <reference lib="dom" />
10
+ const fs_1 = require("fs");
11
+ const path_1 = require("path");
12
+ // ─── Base64 helpers ───
13
+ const toBase64 = (buffer) => {
14
+ const bytes = new Uint8Array(buffer);
15
+ let binary = '';
16
+ for (let i = 0; i < bytes.length; i++) {
17
+ binary += String.fromCharCode(bytes[i]);
18
+ }
19
+ return btoa(binary);
20
+ };
21
+ const fromBase64 = (base64) => {
22
+ const binary = atob(base64);
23
+ const bytes = new Uint8Array(binary.length);
24
+ for (let i = 0; i < binary.length; i++) {
25
+ bytes[i] = binary.charCodeAt(i);
26
+ }
27
+ return bytes.buffer;
28
+ };
29
+ // ─── ECDH key management ───
30
+ const ECDH_PARAMS = { name: 'ECDH', namedCurve: 'P-256' };
31
+ const generateKeyPair = async () => crypto.subtle.generateKey(ECDH_PARAMS, true, ['deriveKey', 'deriveBits']);
32
+ const exportKeyPair = async (keyPair) => {
33
+ const [publicRaw, privateRaw] = await Promise.all([
34
+ crypto.subtle.exportKey('spki', keyPair.publicKey),
35
+ crypto.subtle.exportKey('pkcs8', keyPair.privateKey),
36
+ ]);
37
+ return { publicKey: toBase64(publicRaw), privateKey: toBase64(privateRaw) };
38
+ };
39
+ const importPublicKey = async (base64) => crypto.subtle.importKey('spki', fromBase64(base64), ECDH_PARAMS, true, []);
40
+ const importPrivateKey = async (base64) => crypto.subtle.importKey('pkcs8', fromBase64(base64), ECDH_PARAMS, true, ['deriveKey', 'deriveBits']);
41
+ const importKeyPair = async (exported) => {
42
+ const [publicKey, privateKey] = await Promise.all([
43
+ importPublicKey(exported.publicKey),
44
+ importPrivateKey(exported.privateKey),
45
+ ]);
46
+ return { publicKey, privateKey };
47
+ };
48
+ const deriveAesKey = async (privateKey, publicKey) => crypto.subtle.deriveKey({ name: 'ECDH', public: publicKey }, privateKey, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']);
49
+ const encrypt = async (plaintext, senderPrivateKey, recipientPublicKey) => {
50
+ const messageKey = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
51
+ const iv = crypto.getRandomValues(new Uint8Array(12));
52
+ const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, messageKey, new TextEncoder().encode(plaintext));
53
+ const sharedKey = await deriveAesKey(senderPrivateKey, recipientPublicKey);
54
+ const rawMessageKey = await crypto.subtle.exportKey('raw', messageKey);
55
+ const keyIv = crypto.getRandomValues(new Uint8Array(12));
56
+ const encryptedKey = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: keyIv }, sharedKey, rawMessageKey);
57
+ return {
58
+ ciphertext: toBase64(ciphertext),
59
+ iv: toBase64(iv.buffer),
60
+ encryptedKey: toBase64(encryptedKey),
61
+ keyIv: toBase64(keyIv.buffer),
62
+ };
63
+ };
64
+ // ─── File encryption ───
65
+ const encryptFileContent = async (content, senderPrivateKey, recipientPublicKey) => {
66
+ const buffer = new TextEncoder().encode(content).buffer;
67
+ const fileKey = await crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
68
+ const iv = crypto.getRandomValues(new Uint8Array(12));
69
+ const ciphertext = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, fileKey, buffer);
70
+ const sharedKey = await deriveAesKey(senderPrivateKey, recipientPublicKey);
71
+ const rawFileKey = await crypto.subtle.exportKey('raw', fileKey);
72
+ const keyIv = crypto.getRandomValues(new Uint8Array(12));
73
+ const encryptedKey = await crypto.subtle.encrypt({ name: 'AES-GCM', iv: keyIv }, sharedKey, rawFileKey);
74
+ return {
75
+ ciphertext: Buffer.from(ciphertext),
76
+ iv: toBase64(iv.buffer),
77
+ encryptedKey: toBase64(encryptedKey),
78
+ keyIv: toBase64(keyIv.buffer),
79
+ };
80
+ };
81
+ // ─── Key persistence (~/.config/zeph/keys.json) ───
82
+ const KEYS_DIR = (0, path_1.join)(process.env.HOME ?? '~', '.config', 'zeph');
83
+ const KEYS_PATH = (0, path_1.join)(KEYS_DIR, 'keys.json');
84
+ const loadStoredKeys = () => {
85
+ try {
86
+ return JSON.parse((0, fs_1.readFileSync)(KEYS_PATH, 'utf-8'));
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ };
92
+ const storeKeys = (exported) => {
93
+ (0, fs_1.mkdirSync)(KEYS_DIR, { recursive: true, mode: 0o700 });
94
+ (0, fs_1.writeFileSync)(KEYS_PATH, JSON.stringify(exported, null, 2), { mode: 0o600 });
95
+ };
96
+ // ─── Cached state ───
97
+ let cachedKeyPair = null;
98
+ let cachedExportedPublicKey = null;
99
+ let cachedOwnPublicKey = null;
100
+ let initPromise = null;
101
+ /**
102
+ * Initialize crypto: sync keys with server, then fallback to local/generate.
103
+ * Server is source of truth for per-user key pair.
104
+ * Safe to call concurrently — deduplicates to single init.
105
+ * Returns the exported public key (Base64 SPKI).
106
+ */
107
+ const initCrypto = (apiKey, baseUrl) => {
108
+ if (initPromise)
109
+ return initPromise;
110
+ initPromise = (async () => {
111
+ // Try local cache first
112
+ const stored = loadStoredKeys();
113
+ // Try server sync if API key available
114
+ if (apiKey) {
115
+ const serverKeys = await fetchServerKeys(apiKey, baseUrl);
116
+ if (serverKeys) {
117
+ // Server has keys — adopt them (server is source of truth)
118
+ if (!stored || stored.publicKey !== serverKeys.publicKey) {
119
+ storeKeys(serverKeys);
120
+ }
121
+ cachedKeyPair = await importKeyPair(serverKeys);
122
+ cachedExportedPublicKey = serverKeys.publicKey;
123
+ cachedOwnPublicKey = cachedKeyPair.publicKey;
124
+ return serverKeys.publicKey;
125
+ }
126
+ // Server has no keys
127
+ if (stored) {
128
+ // Upload local keys to server
129
+ await uploadServerKeys(stored, apiKey, baseUrl);
130
+ cachedKeyPair = await importKeyPair(stored);
131
+ cachedExportedPublicKey = stored.publicKey;
132
+ cachedOwnPublicKey = cachedKeyPair.publicKey;
133
+ return stored.publicKey;
134
+ }
135
+ // No keys anywhere — generate + upload
136
+ const keyPair = await generateKeyPair();
137
+ const exported = await exportKeyPair(keyPair);
138
+ storeKeys(exported);
139
+ await uploadServerKeys(exported, apiKey, baseUrl);
140
+ cachedKeyPair = keyPair;
141
+ cachedExportedPublicKey = exported.publicKey;
142
+ cachedOwnPublicKey = keyPair.publicKey;
143
+ return exported.publicKey;
144
+ }
145
+ // No API key — local-only mode
146
+ if (stored) {
147
+ cachedKeyPair = await importKeyPair(stored);
148
+ cachedExportedPublicKey = stored.publicKey;
149
+ cachedOwnPublicKey = cachedKeyPair.publicKey;
150
+ return stored.publicKey;
151
+ }
152
+ const keyPair = await generateKeyPair();
153
+ const exported = await exportKeyPair(keyPair);
154
+ storeKeys(exported);
155
+ cachedKeyPair = keyPair;
156
+ cachedExportedPublicKey = exported.publicKey;
157
+ cachedOwnPublicKey = keyPair.publicKey;
158
+ return exported.publicKey;
159
+ })().catch((err) => {
160
+ initPromise = null;
161
+ throw err;
162
+ });
163
+ return initPromise;
164
+ };
165
+ exports.initCrypto = initCrypto;
166
+ // ─── Server key sync helpers ───
167
+ const fetchServerKeys = async (apiKey, baseUrl) => {
168
+ try {
169
+ const url = `${(baseUrl ?? 'https://api.zeph.to/v1').replace(/\/$/, '')}/users/me/keys`;
170
+ const res = await fetch(url, { headers: { 'X-API-Key': apiKey } });
171
+ if (!res.ok)
172
+ return null;
173
+ const json = await res.json();
174
+ const keys = json.data?.encryptionKeys;
175
+ return keys?.publicKey && keys?.privateKey ? keys : null;
176
+ }
177
+ catch {
178
+ return null;
179
+ }
180
+ };
181
+ const uploadServerKeys = async (keys, apiKey, baseUrl) => {
182
+ try {
183
+ const url = `${(baseUrl ?? 'https://api.zeph.to/v1').replace(/\/$/, '')}/users/me/keys`;
184
+ await fetch(url, {
185
+ method: 'PUT',
186
+ headers: { 'X-API-Key': apiKey, 'Content-Type': 'application/json' },
187
+ body: JSON.stringify(keys),
188
+ });
189
+ }
190
+ catch { /* non-critical */ }
191
+ };
192
+ const getKeyPair = () => cachedKeyPair;
193
+ exports.getKeyPair = getKeyPair;
194
+ const getPublicKey = () => cachedExportedPublicKey;
195
+ exports.getPublicKey = getPublicKey;
196
+ /**
197
+ * Encrypt push body for a recipient.
198
+ * Returns fields ready to merge into the sendPush payload.
199
+ */
200
+ const encryptPushBody = async (input, recipientPublicKeyRaw) => {
201
+ if (!cachedKeyPair || !cachedExportedPublicKey)
202
+ throw new Error('Crypto not initialized');
203
+ const recipientKey = await importPublicKey(recipientPublicKeyRaw);
204
+ const payload = await encrypt(JSON.stringify({ title: input.title, body: input.body, url: input.url }), cachedKeyPair.privateKey, recipientKey);
205
+ return {
206
+ body: JSON.stringify({ ciphertext: payload.ciphertext, iv: payload.iv }),
207
+ encryptedKey: JSON.stringify({ encryptedKey: payload.encryptedKey, keyIv: payload.keyIv }),
208
+ senderPublicKey: cachedExportedPublicKey,
209
+ isEncrypted: true,
210
+ };
211
+ };
212
+ exports.encryptPushBody = encryptPushBody;
213
+ /**
214
+ * Encrypt push body for self (all own devices).
215
+ */
216
+ const encryptPushBodyForSelf = async (input) => {
217
+ if (!cachedKeyPair || !cachedExportedPublicKey || !cachedOwnPublicKey)
218
+ throw new Error('Crypto not initialized');
219
+ const payload = await encrypt(JSON.stringify({ title: input.title, body: input.body, url: input.url }), cachedKeyPair.privateKey, cachedOwnPublicKey);
220
+ return {
221
+ body: JSON.stringify({ ciphertext: payload.ciphertext, iv: payload.iv }),
222
+ encryptedKey: JSON.stringify({ encryptedKey: payload.encryptedKey, keyIv: payload.keyIv }),
223
+ senderPublicKey: cachedExportedPublicKey,
224
+ isEncrypted: true,
225
+ };
226
+ };
227
+ exports.encryptPushBodyForSelf = encryptPushBodyForSelf;
228
+ /**
229
+ * Encrypt file content for a recipient.
230
+ * Returns encrypted buffer + key material for file attachment metadata.
231
+ */
232
+ const encryptFileForRecipient = async (content, recipientPublicKeyRaw) => {
233
+ if (!cachedKeyPair)
234
+ throw new Error('Crypto not initialized');
235
+ const recipientKey = await importPublicKey(recipientPublicKeyRaw);
236
+ const result = await encryptFileContent(content, cachedKeyPair.privateKey, recipientKey);
237
+ return {
238
+ ciphertext: result.ciphertext,
239
+ iv: result.iv,
240
+ encryptedKey: JSON.stringify({ encryptedKey: result.encryptedKey, keyIv: result.keyIv }),
241
+ };
242
+ };
243
+ exports.encryptFileForRecipient = encryptFileForRecipient;
244
+ /**
245
+ * Encrypt file content for self (all own devices).
246
+ */
247
+ const encryptFileForSelf = async (content) => {
248
+ if (!cachedKeyPair || !cachedOwnPublicKey)
249
+ throw new Error('Crypto not initialized');
250
+ const result = await encryptFileContent(content, cachedKeyPair.privateKey, cachedOwnPublicKey);
251
+ return {
252
+ ciphertext: result.ciphertext,
253
+ iv: result.iv,
254
+ encryptedKey: JSON.stringify({ encryptedKey: result.encryptedKey, keyIv: result.keyIv }),
255
+ };
256
+ };
257
+ exports.encryptFileForSelf = encryptFileForSelf;
@@ -3,7 +3,9 @@ export declare class ZephHook {
3
3
  private readonly apiKey;
4
4
  private readonly baseUrl;
5
5
  private readonly timeoutMs;
6
+ private cryptoInitialized;
6
7
  constructor(options: ZephOptions);
8
+ private ensureCrypto;
7
9
  notify(payload: NotifyPayload): Promise<NotifyResult>;
8
10
  private notifyWithFile;
9
11
  requestUpload(params: {
@@ -11,7 +13,7 @@ export declare class ZephHook {
11
13
  fileType: string;
12
14
  fileSize: number;
13
15
  }): Promise<UploadRequestResult>;
14
- uploadToS3(url: string, content: string, contentType: string): Promise<void>;
16
+ uploadToS3(url: string, content: string | Buffer, contentType: string): Promise<void>;
15
17
  list(params?: ListParams): Promise<ListResult>;
16
18
  dismiss(pushId: string): Promise<DismissOneResult>;
17
19
  dismissAll(): Promise<DismissAllResult>;
@@ -1 +1 @@
1
- {"version":3,"file":"zeph-hook.d.ts","sourceRoot":"","sources":["../src/zeph-hook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAY,gBAAgB,EAAE,gBAAgB,EAAoB,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAcxL,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;gBAEvB,OAAO,EAAE,WAAW;IAS1B,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;YAiB7C,cAAc;IAsBtB,aAAa,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAK7G,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAY5E,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAmB9C,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAKlD,UAAU,IAAI,OAAO,CAAC,gBAAgB,CAAC;YAK/B,OAAO;IAiCrB,OAAO,CAAC,UAAU;CASnB"}
1
+ {"version":3,"file":"zeph-hook.d.ts","sourceRoot":"","sources":["../src/zeph-hook.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAY,gBAAgB,EAAE,gBAAgB,EAAoB,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAexL,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAS;IAEnC,OAAO,CAAC,iBAAiB,CAAS;gBAEtB,OAAO,EAAE,WAAW;YASlB,YAAY;IAYpB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;YA6B7C,cAAc;IAsDtB,aAAa,CAAC,MAAM,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAK7G,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAcrF,IAAI,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;IAmB9C,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAKlD,UAAU,IAAI,OAAO,CAAC,gBAAgB,CAAC;YAK/B,OAAO;IAiCrB,OAAO,CAAC,UAAU;CASnB"}
package/dist/zeph-hook.js CHANGED
@@ -2,9 +2,10 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ZephHook = void 0;
4
4
  const errors_js_1 = require("./errors.js");
5
+ const crypto_js_1 = require("./crypto.js");
5
6
  const DEFAULT_BASE_URL = 'https://api.zeph.to/v1';
6
7
  const DEFAULT_TIMEOUT_MS = 30_000;
7
- const BODY_FILE_THRESHOLD = 0;
8
+ const BODY_FILE_THRESHOLD = 512;
8
9
  const PREVIEW_LENGTH = 200;
9
10
  const inferMimeType = (fileName) => {
10
11
  const ext = fileName.split('.').pop()?.toLowerCase();
@@ -15,6 +16,7 @@ class ZephHook {
15
16
  apiKey;
16
17
  baseUrl;
17
18
  timeoutMs;
19
+ cryptoInitialized = false;
18
20
  constructor(options) {
19
21
  if (!options.apiKey) {
20
22
  throw new errors_js_1.ZephError('apiKey is required', 'INVALID_OPTIONS', 400);
@@ -23,32 +25,86 @@ class ZephHook {
23
25
  this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, '');
24
26
  this.timeoutMs = options.timeout ?? DEFAULT_TIMEOUT_MS;
25
27
  }
28
+ async ensureCrypto() {
29
+ if (this.cryptoInitialized)
30
+ return !!(0, crypto_js_1.getKeyPair)();
31
+ try {
32
+ await (0, crypto_js_1.initCrypto)(this.apiKey, this.baseUrl);
33
+ this.cryptoInitialized = true;
34
+ return !!(0, crypto_js_1.getKeyPair)();
35
+ }
36
+ catch {
37
+ this.cryptoInitialized = true;
38
+ return false;
39
+ }
40
+ }
26
41
  async notify(payload) {
42
+ const canEncrypt = await this.ensureCrypto();
27
43
  const body = payload.body;
28
44
  const bodyBytes = body ? new TextEncoder().encode(body).byteLength : 0;
29
45
  const isLongBody = bodyBytes > BODY_FILE_THRESHOLD;
30
46
  if (isLongBody && body) {
31
- return this.notifyWithFile(payload, body, bodyBytes);
47
+ return this.notifyWithFile(payload, body, bodyBytes, canEncrypt);
48
+ }
49
+ // Encrypt push body if possible
50
+ let sendPayload = { ...payload };
51
+ if (canEncrypt) {
52
+ try {
53
+ const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title: payload.title, body: payload.body, url: payload.url });
54
+ sendPayload = { ...sendPayload, title: undefined, body: enc.body, isEncrypted: true, encryptedKey: enc.encryptedKey, senderPublicKey: enc.senderPublicKey };
55
+ }
56
+ catch (err) {
57
+ console.error('[Crypto] Push encryption failed, sending plaintext:', err);
58
+ }
32
59
  }
33
- const json = await this.request('POST', '/pushes/send', payload);
60
+ const json = await this.request('POST', '/pushes/send', sendPayload);
34
61
  const pushId = json.data?.pushId;
35
62
  if (!pushId) {
36
63
  throw new errors_js_1.ZephError('Server returned no pushId', 'INVALID_RESPONSE', 500);
37
64
  }
38
65
  return { pushId };
39
66
  }
40
- async notifyWithFile(payload, body, fileSize) {
67
+ async notifyWithFile(payload, body, fileSize, canEncrypt) {
41
68
  const fileName = 'response.md';
42
- const fileType = inferMimeType(fileName);
43
- const upload = await this.requestUpload({ fileName, fileType, fileSize });
44
- await this.uploadToS3(upload.uploadUrl, body, fileType);
69
+ let fileType = inferMimeType(fileName);
70
+ // Encrypt file content if possible
71
+ let uploadContent = body;
72
+ let uploadSize = fileSize;
73
+ let fileIv;
74
+ let fileEncryptedKey;
75
+ if (canEncrypt) {
76
+ try {
77
+ const encrypted = await (0, crypto_js_1.encryptFileForSelf)(body);
78
+ uploadContent = encrypted.ciphertext;
79
+ uploadSize = encrypted.ciphertext.length;
80
+ fileType = 'application/octet-stream';
81
+ fileIv = encrypted.iv;
82
+ fileEncryptedKey = encrypted.encryptedKey;
83
+ }
84
+ catch (err) {
85
+ console.error('[Crypto] File encryption failed, sending plaintext:', err);
86
+ }
87
+ }
88
+ const upload = await this.requestUpload({ fileName, fileType, fileSize: uploadSize });
89
+ await this.uploadToS3(upload.uploadUrl, uploadContent, fileType);
45
90
  const preview = body.length > PREVIEW_LENGTH ? body.slice(0, PREVIEW_LENGTH) + '...' : body;
46
- const json = await this.request('POST', '/pushes/send', {
91
+ // Encrypt push body
92
+ let sendPayload = {
47
93
  ...payload,
48
94
  body: preview,
49
95
  type: payload.type ?? 'file',
50
- files: [{ fileKey: upload.fileKey, fileName, fileSize, fileType }],
51
- });
96
+ files: [{ fileKey: upload.fileKey, fileName, fileSize, fileType: inferMimeType(fileName), iv: fileIv, encryptedKey: fileEncryptedKey }],
97
+ };
98
+ if (canEncrypt) {
99
+ try {
100
+ const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title: payload.title, body: preview, url: payload.url });
101
+ sendPayload = { ...sendPayload, title: undefined, body: enc.body, isEncrypted: true, encryptedKey: enc.encryptedKey, senderPublicKey: enc.senderPublicKey };
102
+ }
103
+ catch (err) {
104
+ console.error('[Crypto] Push encryption failed, sending plaintext:', err);
105
+ }
106
+ }
107
+ const json = await this.request('POST', '/pushes/send', sendPayload);
52
108
  const pushId = json.data?.pushId;
53
109
  if (!pushId) {
54
110
  throw new errors_js_1.ZephError('Server returned no pushId', 'INVALID_RESPONSE', 500);
@@ -60,10 +116,12 @@ class ZephHook {
60
116
  return json.data;
61
117
  }
62
118
  async uploadToS3(url, content, contentType) {
119
+ const isText = typeof content === 'string';
120
+ const body = isText ? content : new Uint8Array(content);
63
121
  const response = await fetch(url, {
64
122
  method: 'PUT',
65
- headers: { 'Content-Type': `${contentType}; charset=utf-8` },
66
- body: content,
123
+ headers: { 'Content-Type': isText ? `${contentType}; charset=utf-8` : contentType },
124
+ body,
67
125
  signal: AbortSignal.timeout(this.timeoutMs),
68
126
  });
69
127
  if (!response.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/hook-sdk",
3
- "version": "1.4.0",
3
+ "version": "1.5.1",
4
4
  "description": "Zeph push notification SDK + CLI — zero dependencies",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",