@zeph-to/cli 1.27.0 → 2.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 +59 -29
- package/dist/cli.js +6 -1
- package/dist/crypto.d.ts +74 -53
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +161 -179
- package/dist/gate.d.ts +47 -2
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +50 -4
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +7 -3
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +15 -5
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/zeph-hook.d.ts +22 -0
- package/dist/zeph-hook.d.ts.map +1 -1
- package/dist/zeph-hook.js +110 -47
- package/package.json +1 -1
package/dist/templates.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// and the agents can't drift apart.
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
21
|
exports.removeManagedBlock = exports.upsertManagedBlock = exports.ZEPH_MARK_END = exports.ZEPH_MARK_START = exports.COPILOT_HOOKS = exports.isZephHookGroup = exports.CODEX_HOOKS = exports.GEMINI_HOOKS = exports.WINDSURF_HOOKS = exports.CURSOR_HOOKS = exports.AIDER_RULE = exports.CLINE_RULE = exports.COPILOT_RULE = exports.CODEX_RULE = exports.GEMINI_RULE = exports.WINDSURF_RULE = exports.CURSOR_RULE = void 0;
|
|
22
|
+
const gate_js_1 = require("./gate.js");
|
|
22
23
|
const zeph_core_generated_js_1 = require("./zeph-core.generated.js");
|
|
23
24
|
// Graceful resolution: prefer the installed `zeph` CLI, but fall back to
|
|
24
25
|
// `npx -y @zeph-to/cli` so the hook still fires when the user
|
|
@@ -27,11 +28,20 @@ const zeph_core_generated_js_1 = require("./zeph-core.generated.js");
|
|
|
27
28
|
// pattern in plugin/hooks/zeph-{stop,ask}.sh.
|
|
28
29
|
//
|
|
29
30
|
// `--auto` applies the shared push-gate before sending (see src/gate.ts):
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
31
|
+
// the push still fires (gate defaults assume real work), and the
|
|
32
|
+
// /zeph-quiet | /zeph-loud dial works for every hook-driven agent.
|
|
33
|
+
//
|
|
34
|
+
// `--pushmode-default normal` is what keeps the first half of that true. The
|
|
35
|
+
// built-in default for a project with no dial is quiet, and quiet only lets a
|
|
36
|
+
// `high` Push Signal marker through — a marker these hooks have no way to
|
|
37
|
+
// emit. Without the flag they would install and then never push again. The
|
|
38
|
+
// user's own dial still outranks it, so /zeph-quiet keeps working here.
|
|
39
|
+
//
|
|
40
|
+
// Older installed `zeph` versions parse both flags as unknown booleans and
|
|
41
|
+
// ignore them — graceful backward compatibility, and for `--pushmode-default`
|
|
42
|
+
// specifically the old build's behavior was already "normal when no dial".
|
|
43
|
+
const NOTIFY_CMD = '$(command -v zeph || echo "npx -y @zeph-to/cli") notify --title "Task done" --auto '
|
|
44
|
+
+ `--${gate_js_1.PUSHMODE_DEFAULT_FLAG} normal 2>/dev/null || true`;
|
|
35
45
|
// Prompt-submit hook command — remote-origin detection (ADR-0002). Reads
|
|
36
46
|
// the hook JSON on stdin and prints additionalContext JSON on a marker
|
|
37
47
|
// match (see src/remote-hook.ts). stdout IS the hook response, so only
|
package/dist/types.d.ts
CHANGED
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACzC,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;IAChD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;oEAEgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;CACxD;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;IACzC,QAAQ,CAAC,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;IAChD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;oEAEgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,WAAW,GAAG,MAAM,CAAC;CACxD;AAED,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,QAAQ,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE;QACL,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH"}
|
package/dist/zeph-hook.d.ts
CHANGED
|
@@ -5,8 +5,30 @@ export declare class ZephHook {
|
|
|
5
5
|
private readonly timeoutMs;
|
|
6
6
|
private cryptoInitialized;
|
|
7
7
|
constructor(options: ZephOptions);
|
|
8
|
+
/**
|
|
9
|
+
* Resolve who this push can be encrypted for, or null when it cannot be.
|
|
10
|
+
*
|
|
11
|
+
* The device list is fetched per send rather than cached: a phone that
|
|
12
|
+
* registered its key a minute ago must be able to read the next push, and a
|
|
13
|
+
* long-lived listener would otherwise keep wrapping for a stale set.
|
|
14
|
+
* Failures here are not fatal — plaintext the user can read beats a
|
|
15
|
+
* notification that never arrives.
|
|
16
|
+
*/
|
|
8
17
|
private ensureCrypto;
|
|
18
|
+
/**
|
|
19
|
+
* A push must not be lost because E2E turned out to be unavailable.
|
|
20
|
+
*
|
|
21
|
+
* E2E is Pro-only (ADR-0008) and the server rejects `isEncrypted` from a free
|
|
22
|
+
* account with 403 `PRO_REQUIRED`. This process may have initialized crypto
|
|
23
|
+
* while still Pro — a long-lived listener outlives a downgrade — so the
|
|
24
|
+
* rejection is only visible at send time. Drop the keys and run the send
|
|
25
|
+
* again from the top: the retry re-encodes the payload as plaintext and, on
|
|
26
|
+
* the file path, re-uploads the file unencrypted (a payload patch would leave
|
|
27
|
+
* an undecryptable blob in S3). The first, encrypted object stays orphaned —
|
|
28
|
+
* accepted cost; the upload endpoint has no quota counter to burn.
|
|
29
|
+
*/
|
|
9
30
|
notify(payload: NotifyPayload): Promise<NotifyResult>;
|
|
31
|
+
private notifyOnce;
|
|
10
32
|
private notifyWithFile;
|
|
11
33
|
requestUpload(params: {
|
|
12
34
|
fileName: string;
|
package/dist/zeph-hook.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zeph-hook.d.ts","sourceRoot":"","sources":["../src/zeph-hook.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAY,gBAAgB,EAAE,gBAAgB,EAAoB,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAoCxL,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;
|
|
1
|
+
{"version":3,"file":"zeph-hook.d.ts","sourceRoot":"","sources":["../src/zeph-hook.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAY,gBAAgB,EAAE,gBAAgB,EAAoB,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAoCxL,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;IAShC;;;;;;;;OAQG;YACW,YAAY;IAyB1B;;;;;;;;;;;OAWG;IACG,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;YAgB7C,UAAU;YAyCV,cAAc;IA6DtB,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;IAuB9C,OAAO,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAKlD,UAAU,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAK7C;;;;OAIG;IACG,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;YASxF,OAAO;IAiCrB,OAAO,CAAC,UAAU;CASnB"}
|
package/dist/zeph-hook.js
CHANGED
|
@@ -50,40 +50,94 @@ class ZephHook {
|
|
|
50
50
|
this.baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, '');
|
|
51
51
|
this.timeoutMs = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolve who this push can be encrypted for, or null when it cannot be.
|
|
55
|
+
*
|
|
56
|
+
* The device list is fetched per send rather than cached: a phone that
|
|
57
|
+
* registered its key a minute ago must be able to read the next push, and a
|
|
58
|
+
* long-lived listener would otherwise keep wrapping for a stale set.
|
|
59
|
+
* Failures here are not fatal — plaintext the user can read beats a
|
|
60
|
+
* notification that never arrives.
|
|
61
|
+
*/
|
|
53
62
|
async ensureCrypto() {
|
|
54
|
-
if (this.cryptoInitialized)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
63
|
+
if (!this.cryptoInitialized) {
|
|
64
|
+
try {
|
|
65
|
+
await (0, crypto_js_1.initCrypto)(this.apiKey, this.baseUrl);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// fall through — getKeyPair() stays null and we send plaintext
|
|
69
|
+
}
|
|
58
70
|
this.cryptoInitialized = true;
|
|
59
|
-
return !!(0, crypto_js_1.getKeyPair)();
|
|
60
71
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
72
|
+
if (!(0, crypto_js_1.getKeyPair)())
|
|
73
|
+
return null;
|
|
74
|
+
try {
|
|
75
|
+
const json = await this.request('GET', '/devices');
|
|
76
|
+
const recipients = (0, crypto_js_1.selectRecipients)(json.data ?? []);
|
|
77
|
+
if (recipients.length === 0) {
|
|
78
|
+
console.error('[Crypto] No device has a per-device public key — sending plaintext.');
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return recipients;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
console.error('[Crypto] Could not list devices, sending plaintext:', err);
|
|
85
|
+
return null;
|
|
64
86
|
}
|
|
65
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* A push must not be lost because E2E turned out to be unavailable.
|
|
90
|
+
*
|
|
91
|
+
* E2E is Pro-only (ADR-0008) and the server rejects `isEncrypted` from a free
|
|
92
|
+
* account with 403 `PRO_REQUIRED`. This process may have initialized crypto
|
|
93
|
+
* while still Pro — a long-lived listener outlives a downgrade — so the
|
|
94
|
+
* rejection is only visible at send time. Drop the keys and run the send
|
|
95
|
+
* again from the top: the retry re-encodes the payload as plaintext and, on
|
|
96
|
+
* the file path, re-uploads the file unencrypted (a payload patch would leave
|
|
97
|
+
* an undecryptable blob in S3). The first, encrypted object stays orphaned —
|
|
98
|
+
* accepted cost; the upload endpoint has no quota counter to burn.
|
|
99
|
+
*/
|
|
66
100
|
async notify(payload) {
|
|
101
|
+
// Resolved once for both attempts: the lookup shells out to tmux, and a
|
|
102
|
+
// retry milliseconds later cannot land in a different session.
|
|
103
|
+
const agentCtx = agentSessionContext();
|
|
104
|
+
try {
|
|
105
|
+
return await this.notifyOnce(payload, agentCtx);
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
// Held keys are what proves this send was encrypted, and dropping them is
|
|
109
|
+
// also the recursion guard — the retry has none, so a second 403 propagates.
|
|
110
|
+
if (!(err instanceof errors_js_1.ZephError) || err.code !== 'PRO_REQUIRED' || !(0, crypto_js_1.getKeyPair)())
|
|
111
|
+
throw err;
|
|
112
|
+
(0, crypto_js_1.disableCrypto)();
|
|
113
|
+
console.error('[Crypto] End-to-end encryption requires Zeph Pro — resending as plaintext.');
|
|
114
|
+
return this.notifyOnce(payload, agentCtx);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
async notifyOnce(payload, agentCtx) {
|
|
67
118
|
// Attach the stable session key when in a tmux agent session so the push
|
|
68
119
|
// joins the agent chat. Merged into `payload` here so both the inline and
|
|
69
120
|
// file-upload (notifyWithFile) paths — which each spread `...payload` —
|
|
70
121
|
// carry it. Explicit caller values win.
|
|
71
|
-
const agentCtx = agentSessionContext();
|
|
72
122
|
if (agentCtx)
|
|
73
123
|
payload = { ...agentCtx, ...payload };
|
|
74
|
-
const
|
|
124
|
+
const recipients = await this.ensureCrypto();
|
|
75
125
|
const body = payload.body;
|
|
76
126
|
const bodyBytes = body ? new TextEncoder().encode(body).byteLength : 0;
|
|
77
127
|
const isLongBody = bodyBytes > BODY_FILE_THRESHOLD;
|
|
78
128
|
if (isLongBody && body) {
|
|
79
|
-
return this.notifyWithFile(payload, body, bodyBytes,
|
|
129
|
+
return this.notifyWithFile(payload, body, bodyBytes, recipients);
|
|
80
130
|
}
|
|
81
131
|
// Encrypt push body if possible
|
|
82
132
|
let sendPayload = { ...payload };
|
|
83
|
-
if (
|
|
133
|
+
if (recipients) {
|
|
84
134
|
try {
|
|
85
|
-
const enc = await (0, crypto_js_1.
|
|
86
|
-
|
|
135
|
+
const enc = await (0, crypto_js_1.encryptPushBodyForDevices)({ title: payload.title, body: payload.body, url: payload.url }, recipients);
|
|
136
|
+
// `url: undefined` alongside the title: it is already sealed inside the
|
|
137
|
+
// ciphertext, and on a link push it IS the payload. Spreading `payload`
|
|
138
|
+
// above left the plaintext copy at the top level, handing the server the
|
|
139
|
+
// one thing `isEncrypted` promises it cannot see.
|
|
140
|
+
sendPayload = { ...sendPayload, title: undefined, url: undefined, body: enc.body, isEncrypted: true, deviceKeyMap: enc.deviceKeyMap, senderPublicKey: enc.senderPublicKey };
|
|
87
141
|
}
|
|
88
142
|
catch (err) {
|
|
89
143
|
console.error('[Crypto] Push encryption failed, sending plaintext:', err);
|
|
@@ -96,46 +150,51 @@ class ZephHook {
|
|
|
96
150
|
}
|
|
97
151
|
return { pushId };
|
|
98
152
|
}
|
|
99
|
-
async notifyWithFile(payload, body, fileSize,
|
|
153
|
+
async notifyWithFile(payload, body, fileSize, recipients) {
|
|
100
154
|
const fileName = 'response.md';
|
|
101
|
-
|
|
102
|
-
// Encrypt
|
|
103
|
-
let
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
let
|
|
107
|
-
if (
|
|
155
|
+
const preview = body.length > PREVIEW_LENGTH ? body.slice(0, PREVIEW_LENGTH) + '...' : body;
|
|
156
|
+
// Encrypt the attachment and the push body together, before anything is
|
|
157
|
+
// uploaded. Doing them one at a time around the upload let a failure land
|
|
158
|
+
// in between and ship ciphertext under a push with no `isEncrypted` — an
|
|
159
|
+
// attachment no client would even try to open.
|
|
160
|
+
let encrypted = null;
|
|
161
|
+
if (recipients) {
|
|
108
162
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
fileIv = encrypted.iv;
|
|
114
|
-
fileEncryptedKey = encrypted.encryptedKey;
|
|
163
|
+
encrypted = {
|
|
164
|
+
file: await (0, crypto_js_1.encryptFileForDevices)(body, recipients),
|
|
165
|
+
push: await (0, crypto_js_1.encryptPushBodyForDevices)({ title: payload.title, body: preview, url: payload.url }, recipients),
|
|
166
|
+
};
|
|
115
167
|
}
|
|
116
168
|
catch (err) {
|
|
117
|
-
console.error('[Crypto]
|
|
169
|
+
console.error('[Crypto] Encryption failed, sending plaintext:', err);
|
|
118
170
|
}
|
|
119
171
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
172
|
+
const uploadContent = encrypted?.file.ciphertext ?? body;
|
|
173
|
+
const uploadType = encrypted ? 'application/octet-stream' : inferMimeType(fileName);
|
|
174
|
+
const uploadSize = encrypted ? encrypted.file.ciphertext.length : fileSize;
|
|
175
|
+
const upload = await this.requestUpload({ fileName, fileType: uploadType, fileSize: uploadSize });
|
|
176
|
+
await this.uploadToS3(upload.uploadUrl, uploadContent, uploadType);
|
|
177
|
+
const sendPayload = {
|
|
125
178
|
...payload,
|
|
126
|
-
|
|
179
|
+
title: encrypted ? undefined : payload.title,
|
|
180
|
+
body: encrypted ? encrypted.push.body : preview,
|
|
127
181
|
type: payload.type ?? 'file',
|
|
128
|
-
files: [{
|
|
182
|
+
files: [{
|
|
183
|
+
fileKey: upload.fileKey,
|
|
184
|
+
fileName,
|
|
185
|
+
fileSize,
|
|
186
|
+
fileType: inferMimeType(fileName),
|
|
187
|
+
iv: encrypted?.file.iv,
|
|
188
|
+
deviceKeyMap: encrypted?.file.deviceKeyMap,
|
|
189
|
+
}],
|
|
190
|
+
...(encrypted && {
|
|
191
|
+
// See notifyOnce: the url must not survive in the clear either.
|
|
192
|
+
url: undefined,
|
|
193
|
+
isEncrypted: true,
|
|
194
|
+
deviceKeyMap: encrypted.push.deviceKeyMap,
|
|
195
|
+
senderPublicKey: encrypted.push.senderPublicKey,
|
|
196
|
+
}),
|
|
129
197
|
};
|
|
130
|
-
if (canEncrypt) {
|
|
131
|
-
try {
|
|
132
|
-
const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title: payload.title, body: preview, url: payload.url });
|
|
133
|
-
sendPayload = { ...sendPayload, title: undefined, body: enc.body, isEncrypted: true, encryptedKey: enc.encryptedKey, senderPublicKey: enc.senderPublicKey };
|
|
134
|
-
}
|
|
135
|
-
catch (err) {
|
|
136
|
-
console.error('[Crypto] Push encryption failed, sending plaintext:', err);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
198
|
const json = await this.request('POST', '/pushes/send', sendPayload);
|
|
140
199
|
const pushId = json.data?.pushId;
|
|
141
200
|
if (!pushId) {
|
|
@@ -171,8 +230,12 @@ class ZephHook {
|
|
|
171
230
|
const pushes = json.data.map((p) => ({
|
|
172
231
|
pushId: p.pushId,
|
|
173
232
|
type: p.type,
|
|
174
|
-
title
|
|
175
|
-
body:
|
|
233
|
+
// An encrypted push carries no plaintext title and a JSON envelope in
|
|
234
|
+
// `body`; slicing that renders `{"ciphertext":"AAAB...` as if it were the
|
|
235
|
+
// message. This host cannot decrypt (send-only), so say so instead.
|
|
236
|
+
title: p.isEncrypted ? undefined : p.title,
|
|
237
|
+
body: p.isEncrypted ? '[encrypted]' : p.body?.slice(0, 100),
|
|
238
|
+
isEncrypted: p.isEncrypted,
|
|
176
239
|
createdAt: p.createdAt,
|
|
177
240
|
}));
|
|
178
241
|
return { pushes, count: pushes.length, hasMore: json.pagination?.hasMore ?? false };
|