@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/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
- // in normal mode the push still fires (gate defaults assume real work), but
31
- // the /zeph-quiet | /zeph-loud dial now works for every hook-driven agent.
32
- // Older installed `zeph` versions parse `--auto` as an unknown boolean flag
33
- // and ignore it graceful backward compatibility.
34
- const NOTIFY_CMD = '$(command -v zeph || echo "npx -y @zeph-to/cli") notify --title "Task done" --auto 2>/dev/null || true';
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
@@ -37,6 +37,8 @@ export interface PushItem {
37
37
  title?: string;
38
38
  body?: string;
39
39
  createdAt: string;
40
+ /** E2E: when true, `title`/`body` are absent and the ciphertext is in `body`. */
41
+ isEncrypted?: boolean;
40
42
  }
41
43
  export interface ListResult {
42
44
  pushes: PushItem[];
@@ -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;CACnB;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"}
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"}
@@ -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;
@@ -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;YASlB,YAAY;IAYpB,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;YAmC7C,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;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"}
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
- return !!(0, crypto_js_1.getKeyPair)();
56
- try {
57
- await (0, crypto_js_1.initCrypto)(this.apiKey, this.baseUrl);
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
- catch {
62
- this.cryptoInitialized = true;
63
- return false;
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 canEncrypt = await this.ensureCrypto();
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, canEncrypt);
129
+ return this.notifyWithFile(payload, body, bodyBytes, recipients);
80
130
  }
81
131
  // Encrypt push body if possible
82
132
  let sendPayload = { ...payload };
83
- if (canEncrypt) {
133
+ if (recipients) {
84
134
  try {
85
- const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title: payload.title, body: payload.body, url: payload.url });
86
- sendPayload = { ...sendPayload, title: undefined, body: enc.body, isEncrypted: true, encryptedKey: enc.encryptedKey, senderPublicKey: enc.senderPublicKey };
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, canEncrypt) {
153
+ async notifyWithFile(payload, body, fileSize, recipients) {
100
154
  const fileName = 'response.md';
101
- let fileType = inferMimeType(fileName);
102
- // Encrypt file content if possible
103
- let uploadContent = body;
104
- let uploadSize = fileSize;
105
- let fileIv;
106
- let fileEncryptedKey;
107
- if (canEncrypt) {
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
- const encrypted = await (0, crypto_js_1.encryptFileForSelf)(body);
110
- uploadContent = encrypted.ciphertext;
111
- uploadSize = encrypted.ciphertext.length;
112
- fileType = 'application/octet-stream';
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] File encryption failed, sending plaintext:', err);
169
+ console.error('[Crypto] Encryption failed, sending plaintext:', err);
118
170
  }
119
171
  }
120
- const upload = await this.requestUpload({ fileName, fileType, fileSize: uploadSize });
121
- await this.uploadToS3(upload.uploadUrl, uploadContent, fileType);
122
- const preview = body.length > PREVIEW_LENGTH ? body.slice(0, PREVIEW_LENGTH) + '...' : body;
123
- // Encrypt push body
124
- let sendPayload = {
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
- body: preview,
179
+ title: encrypted ? undefined : payload.title,
180
+ body: encrypted ? encrypted.push.body : preview,
127
181
  type: payload.type ?? 'file',
128
- files: [{ fileKey: upload.fileKey, fileName, fileSize, fileType: inferMimeType(fileName), iv: fileIv, encryptedKey: fileEncryptedKey }],
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: p.title,
175
- body: p.body?.slice(0, 100),
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zeph-to/cli",
3
- "version": "1.27.0",
3
+ "version": "2.0.0",
4
4
  "description": "Zeph CLI + push notification SDK for AI agents",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",