@zeph-to/cli 1.27.1 → 2.1.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.
@@ -1 +1 @@
1
- {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AA4FA,6EAA6E;AAC7E,eAAO,MAAM,WAAW,QAItB,CAAC;AAEH,6EAA6E;AAC7E,eAAO,MAAM,aAAa,QAAyE,CAAC;AAEpG,sDAAsD;AACtD,eAAO,MAAM,WAAW,QAAyE,CAAC;AAElG,oDAAoD;AACpD,eAAO,MAAM,UAAU,QAAyE,CAAC;AAEjG,oFAAoF;AACpF,eAAO,MAAM,YAAY,QAAyE,CAAC;AAEnG,gEAAgE;AAChE,eAAO,MAAM,UAAU,QAAkE,CAAC;AAE1F,4FAA4F;AAC5F,eAAO,MAAM,UAAU,QAAkE,CAAC;AAI1F,eAAO,MAAM,YAAY,QAKd,CAAC;AAEZ,eAAO,MAAM,cAAc,QAOhB,CAAC;AAEZ,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;CAuBxB,CAAC;AAYF,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;CASvB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,OAAO,KAAG,OAUhD,CAAC;AAEF,eAAO,MAAM,aAAa,QASf,CAAC;AASZ,eAAO,MAAM,eAAe,oFAA+E,CAAC;AAC5G,eAAO,MAAM,aAAa,sBAAsB,CAAC;AAQjD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,UAAU,MAAM,EAAE,MAAM,MAAM,KAAG,MAWnE,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,kBAAkB,GAAI,UAAU,MAAM,KAAG,MAOrD,CAAC"}
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAsGA,6EAA6E;AAC7E,eAAO,MAAM,WAAW,QAItB,CAAC;AAEH,6EAA6E;AAC7E,eAAO,MAAM,aAAa,QAAyE,CAAC;AAEpG,sDAAsD;AACtD,eAAO,MAAM,WAAW,QAAyE,CAAC;AAElG,oDAAoD;AACpD,eAAO,MAAM,UAAU,QAAyE,CAAC;AAEjG,oFAAoF;AACpF,eAAO,MAAM,YAAY,QAAyE,CAAC;AAEnG,gEAAgE;AAChE,eAAO,MAAM,UAAU,QAAkE,CAAC;AAE1F,4FAA4F;AAC5F,eAAO,MAAM,UAAU,QAAkE,CAAC;AAI1F,eAAO,MAAM,YAAY,QAKd,CAAC;AAEZ,eAAO,MAAM,cAAc,QAOhB,CAAC;AAEZ,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;CAuBxB,CAAC;AAYF,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;CASvB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,OAAO,KAAG,OAUhD,CAAC;AAEF,eAAO,MAAM,aAAa,QASf,CAAC;AASZ,eAAO,MAAM,eAAe,oFAA+E,CAAC;AAC5G,eAAO,MAAM,aAAa,sBAAsB,CAAC;AAQjD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,UAAU,MAAM,EAAE,MAAM,MAAM,KAAG,MAWnE,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,kBAAkB,GAAI,UAAU,MAAM,KAAG,MAOrD,CAAC"}
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,6 +5,15 @@ 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;
9
18
  /**
10
19
  * A push must not be lost because E2E turned out to be unavailable.
@@ -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;IAY1B;;;;;;;;;;;OAWG;IACG,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,CAAC;YAgB7C,UAAU;YAqCV,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,17 +50,39 @@ 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
  }
66
88
  /**
@@ -99,19 +121,23 @@ class ZephHook {
99
121
  // carry it. Explicit caller values win.
100
122
  if (agentCtx)
101
123
  payload = { ...agentCtx, ...payload };
102
- const canEncrypt = await this.ensureCrypto();
124
+ const recipients = await this.ensureCrypto();
103
125
  const body = payload.body;
104
126
  const bodyBytes = body ? new TextEncoder().encode(body).byteLength : 0;
105
127
  const isLongBody = bodyBytes > BODY_FILE_THRESHOLD;
106
128
  if (isLongBody && body) {
107
- return this.notifyWithFile(payload, body, bodyBytes, canEncrypt);
129
+ return this.notifyWithFile(payload, body, bodyBytes, recipients);
108
130
  }
109
131
  // Encrypt push body if possible
110
132
  let sendPayload = { ...payload };
111
- if (canEncrypt) {
133
+ if (recipients) {
112
134
  try {
113
- const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title: payload.title, body: payload.body, url: payload.url });
114
- 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 };
115
141
  }
116
142
  catch (err) {
117
143
  console.error('[Crypto] Push encryption failed, sending plaintext:', err);
@@ -124,46 +150,51 @@ class ZephHook {
124
150
  }
125
151
  return { pushId };
126
152
  }
127
- async notifyWithFile(payload, body, fileSize, canEncrypt) {
153
+ async notifyWithFile(payload, body, fileSize, recipients) {
128
154
  const fileName = 'response.md';
129
- let fileType = inferMimeType(fileName);
130
- // Encrypt file content if possible
131
- let uploadContent = body;
132
- let uploadSize = fileSize;
133
- let fileIv;
134
- let fileEncryptedKey;
135
- 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) {
136
162
  try {
137
- const encrypted = await (0, crypto_js_1.encryptFileForSelf)(body);
138
- uploadContent = encrypted.ciphertext;
139
- uploadSize = encrypted.ciphertext.length;
140
- fileType = 'application/octet-stream';
141
- fileIv = encrypted.iv;
142
- 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
+ };
143
167
  }
144
168
  catch (err) {
145
- console.error('[Crypto] File encryption failed, sending plaintext:', err);
169
+ console.error('[Crypto] Encryption failed, sending plaintext:', err);
146
170
  }
147
171
  }
148
- const upload = await this.requestUpload({ fileName, fileType, fileSize: uploadSize });
149
- await this.uploadToS3(upload.uploadUrl, uploadContent, fileType);
150
- const preview = body.length > PREVIEW_LENGTH ? body.slice(0, PREVIEW_LENGTH) + '...' : body;
151
- // Encrypt push body
152
- 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 = {
153
178
  ...payload,
154
- body: preview,
179
+ title: encrypted ? undefined : payload.title,
180
+ body: encrypted ? encrypted.push.body : preview,
155
181
  type: payload.type ?? 'file',
156
- 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
+ }),
157
197
  };
158
- if (canEncrypt) {
159
- try {
160
- const enc = await (0, crypto_js_1.encryptPushBodyForSelf)({ title: payload.title, body: preview, url: payload.url });
161
- sendPayload = { ...sendPayload, title: undefined, body: enc.body, isEncrypted: true, encryptedKey: enc.encryptedKey, senderPublicKey: enc.senderPublicKey };
162
- }
163
- catch (err) {
164
- console.error('[Crypto] Push encryption failed, sending plaintext:', err);
165
- }
166
- }
167
198
  const json = await this.request('POST', '/pushes/send', sendPayload);
168
199
  const pushId = json.data?.pushId;
169
200
  if (!pushId) {
@@ -199,8 +230,12 @@ class ZephHook {
199
230
  const pushes = json.data.map((p) => ({
200
231
  pushId: p.pushId,
201
232
  type: p.type,
202
- title: p.title,
203
- 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,
204
239
  createdAt: p.createdAt,
205
240
  }));
206
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.1",
3
+ "version": "2.1.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",