@kangentic/protocol 0.6.0 → 0.7.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,4 +1,4 @@
1
- export declare const PUSH_CATEGORIES: readonly ["permission-needed", "agent-question", "turn-complete", "session-failed"];
1
+ export declare const PUSH_CATEGORIES: readonly ["input-required", "turn-complete", "session-failed", "plan-complete", "spawn-stalled"];
2
2
  export type PushCategory = (typeof PUSH_CATEGORIES)[number];
3
3
  export declare function isPushCategory(value: unknown): value is PushCategory;
4
4
  /** Reject an opened envelope whose sentAt is older than this (stale replay). */
package/dist/index.cjs CHANGED
@@ -5186,6 +5186,56 @@ function base64UrlDecode(encoded) {
5186
5186
  return bytes;
5187
5187
  }
5188
5188
 
5189
+ // src/crypto/push-envelope.ts
5190
+ var PUSH_CATEGORIES = ["input-required", "turn-complete", "session-failed", "plan-complete", "spawn-stalled"];
5191
+ function isPushCategory(value) {
5192
+ return typeof value === "string" && PUSH_CATEGORIES.includes(value);
5193
+ }
5194
+ var PUSH_ENVELOPE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5195
+ var PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS = 5 * 60 * 1e3;
5196
+ function requireEnvelopeString(record, field) {
5197
+ const value = record[field];
5198
+ if (typeof value !== "string") throw new Error(`push envelope plaintext is missing a string "${field}"`);
5199
+ return value;
5200
+ }
5201
+ function parsePushEnvelopePlaintext(value) {
5202
+ if (!isRecord(value)) throw new Error("push envelope plaintext must be an object");
5203
+ if (!isPushCategory(value.category)) throw new Error('push envelope plaintext has an unknown "category"');
5204
+ if (typeof value.sentAt !== "number" || !Number.isFinite(value.sentAt)) {
5205
+ throw new Error('push envelope plaintext has an invalid "sentAt"');
5206
+ }
5207
+ return {
5208
+ category: value.category,
5209
+ projectId: requireEnvelopeString(value, "projectId"),
5210
+ taskId: requireEnvelopeString(value, "taskId"),
5211
+ sessionId: requireEnvelopeString(value, "sessionId"),
5212
+ taskTitle: requireEnvelopeString(value, "taskTitle"),
5213
+ detail: requireEnvelopeString(value, "detail"),
5214
+ sentAt: value.sentAt
5215
+ };
5216
+ }
5217
+ function sealPushEnvelope(pushKey, recipientStaticPublicKey, plaintext) {
5218
+ const nonce = randomBytes4(XCHACHA_NONCE_LENGTH);
5219
+ const encodedPlaintext = new TextEncoder().encode(JSON.stringify(plaintext));
5220
+ const ciphertext = xaeadEncrypt(pushKey, nonce, recipientStaticPublicKey, encodedPlaintext);
5221
+ return base64UrlEncode(concatBytes4(nonce, ciphertext));
5222
+ }
5223
+ function openPushEnvelope(pushKey, recipientStaticPublicKey, blob) {
5224
+ const bytes = base64UrlDecode(blob);
5225
+ if (bytes.length < XCHACHA_NONCE_LENGTH + AEAD_TAG_LENGTH) {
5226
+ throw new Error("push envelope blob is too short");
5227
+ }
5228
+ const nonce = bytes.subarray(0, XCHACHA_NONCE_LENGTH);
5229
+ const ciphertext = bytes.subarray(XCHACHA_NONCE_LENGTH);
5230
+ const decrypted = xaeadDecrypt(pushKey, nonce, recipientStaticPublicKey, ciphertext);
5231
+ const decoded = JSON.parse(new TextDecoder().decode(decrypted));
5232
+ const plaintext = parsePushEnvelopePlaintext(decoded);
5233
+ const now = Date.now();
5234
+ if (plaintext.sentAt < now - PUSH_ENVELOPE_MAX_AGE_MS) throw new Error("push envelope is stale");
5235
+ if (plaintext.sentAt > now + PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS) throw new Error("push envelope sentAt is in the future");
5236
+ return plaintext;
5237
+ }
5238
+
5189
5239
  // src/events/payloads.ts
5190
5240
  var DIFF_FILE_STATUSES = ["A", "M", "D", "R", "C", "U"];
5191
5241
  var TRANSCRIPT_SYSTEM_SUBTYPES = ["compaction", "command", "command_output", "session_boundary"];
@@ -5651,6 +5701,12 @@ function parseRegisterPushRequestPayload(payload) {
5651
5701
  }
5652
5702
  request.platform = payload.platform;
5653
5703
  }
5704
+ if (payload.categories !== void 0) {
5705
+ if (!Array.isArray(payload.categories) || !payload.categories.every((entry) => typeof entry === "string")) {
5706
+ throw new Error('register-push payload has a malformed "categories"');
5707
+ }
5708
+ request.categories = payload.categories.filter(isPushCategory);
5709
+ }
5654
5710
  if (request.action === "register") {
5655
5711
  if (request.expoPushToken === void 0) throw new Error('register-push register payload missing "expoPushToken"');
5656
5712
  if (request.pushKeyBase64 === void 0) throw new Error('register-push register payload missing "pushKeyBase64"');
@@ -5685,56 +5741,6 @@ function parseCapabilityRequestPayload(verb, payload) {
5685
5741
  }
5686
5742
  }
5687
5743
 
5688
- // src/crypto/push-envelope.ts
5689
- var PUSH_CATEGORIES = ["permission-needed", "agent-question", "turn-complete", "session-failed"];
5690
- function isPushCategory(value) {
5691
- return typeof value === "string" && PUSH_CATEGORIES.includes(value);
5692
- }
5693
- var PUSH_ENVELOPE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5694
- var PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS = 5 * 60 * 1e3;
5695
- function requireEnvelopeString(record, field) {
5696
- const value = record[field];
5697
- if (typeof value !== "string") throw new Error(`push envelope plaintext is missing a string "${field}"`);
5698
- return value;
5699
- }
5700
- function parsePushEnvelopePlaintext(value) {
5701
- if (!isRecord(value)) throw new Error("push envelope plaintext must be an object");
5702
- if (!isPushCategory(value.category)) throw new Error('push envelope plaintext has an unknown "category"');
5703
- if (typeof value.sentAt !== "number" || !Number.isFinite(value.sentAt)) {
5704
- throw new Error('push envelope plaintext has an invalid "sentAt"');
5705
- }
5706
- return {
5707
- category: value.category,
5708
- projectId: requireEnvelopeString(value, "projectId"),
5709
- taskId: requireEnvelopeString(value, "taskId"),
5710
- sessionId: requireEnvelopeString(value, "sessionId"),
5711
- taskTitle: requireEnvelopeString(value, "taskTitle"),
5712
- detail: requireEnvelopeString(value, "detail"),
5713
- sentAt: value.sentAt
5714
- };
5715
- }
5716
- function sealPushEnvelope(pushKey, recipientStaticPublicKey, plaintext) {
5717
- const nonce = randomBytes4(XCHACHA_NONCE_LENGTH);
5718
- const encodedPlaintext = new TextEncoder().encode(JSON.stringify(plaintext));
5719
- const ciphertext = xaeadEncrypt(pushKey, nonce, recipientStaticPublicKey, encodedPlaintext);
5720
- return base64UrlEncode(concatBytes4(nonce, ciphertext));
5721
- }
5722
- function openPushEnvelope(pushKey, recipientStaticPublicKey, blob) {
5723
- const bytes = base64UrlDecode(blob);
5724
- if (bytes.length < XCHACHA_NONCE_LENGTH + AEAD_TAG_LENGTH) {
5725
- throw new Error("push envelope blob is too short");
5726
- }
5727
- const nonce = bytes.subarray(0, XCHACHA_NONCE_LENGTH);
5728
- const ciphertext = bytes.subarray(XCHACHA_NONCE_LENGTH);
5729
- const decrypted = xaeadDecrypt(pushKey, nonce, recipientStaticPublicKey, ciphertext);
5730
- const decoded = JSON.parse(new TextDecoder().decode(decrypted));
5731
- const plaintext = parsePushEnvelopePlaintext(decoded);
5732
- const now = Date.now();
5733
- if (plaintext.sentAt < now - PUSH_ENVELOPE_MAX_AGE_MS) throw new Error("push envelope is stale");
5734
- if (plaintext.sentAt > now + PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS) throw new Error("push envelope sentAt is in the future");
5735
- return plaintext;
5736
- }
5737
-
5738
5744
  // src/pairing/qr.ts
5739
5745
  var PAIRING_URI_SCHEME = "kangentic-pair";
5740
5746
  var PAYLOAD_VERSION = 1;
package/dist/index.mjs CHANGED
@@ -5075,6 +5075,56 @@ function base64UrlDecode(encoded) {
5075
5075
  return bytes;
5076
5076
  }
5077
5077
 
5078
+ // src/crypto/push-envelope.ts
5079
+ var PUSH_CATEGORIES = ["input-required", "turn-complete", "session-failed", "plan-complete", "spawn-stalled"];
5080
+ function isPushCategory(value) {
5081
+ return typeof value === "string" && PUSH_CATEGORIES.includes(value);
5082
+ }
5083
+ var PUSH_ENVELOPE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5084
+ var PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS = 5 * 60 * 1e3;
5085
+ function requireEnvelopeString(record, field) {
5086
+ const value = record[field];
5087
+ if (typeof value !== "string") throw new Error(`push envelope plaintext is missing a string "${field}"`);
5088
+ return value;
5089
+ }
5090
+ function parsePushEnvelopePlaintext(value) {
5091
+ if (!isRecord(value)) throw new Error("push envelope plaintext must be an object");
5092
+ if (!isPushCategory(value.category)) throw new Error('push envelope plaintext has an unknown "category"');
5093
+ if (typeof value.sentAt !== "number" || !Number.isFinite(value.sentAt)) {
5094
+ throw new Error('push envelope plaintext has an invalid "sentAt"');
5095
+ }
5096
+ return {
5097
+ category: value.category,
5098
+ projectId: requireEnvelopeString(value, "projectId"),
5099
+ taskId: requireEnvelopeString(value, "taskId"),
5100
+ sessionId: requireEnvelopeString(value, "sessionId"),
5101
+ taskTitle: requireEnvelopeString(value, "taskTitle"),
5102
+ detail: requireEnvelopeString(value, "detail"),
5103
+ sentAt: value.sentAt
5104
+ };
5105
+ }
5106
+ function sealPushEnvelope(pushKey, recipientStaticPublicKey, plaintext) {
5107
+ const nonce = randomBytes4(XCHACHA_NONCE_LENGTH);
5108
+ const encodedPlaintext = new TextEncoder().encode(JSON.stringify(plaintext));
5109
+ const ciphertext = xaeadEncrypt(pushKey, nonce, recipientStaticPublicKey, encodedPlaintext);
5110
+ return base64UrlEncode(concatBytes4(nonce, ciphertext));
5111
+ }
5112
+ function openPushEnvelope(pushKey, recipientStaticPublicKey, blob) {
5113
+ const bytes = base64UrlDecode(blob);
5114
+ if (bytes.length < XCHACHA_NONCE_LENGTH + AEAD_TAG_LENGTH) {
5115
+ throw new Error("push envelope blob is too short");
5116
+ }
5117
+ const nonce = bytes.subarray(0, XCHACHA_NONCE_LENGTH);
5118
+ const ciphertext = bytes.subarray(XCHACHA_NONCE_LENGTH);
5119
+ const decrypted = xaeadDecrypt(pushKey, nonce, recipientStaticPublicKey, ciphertext);
5120
+ const decoded = JSON.parse(new TextDecoder().decode(decrypted));
5121
+ const plaintext = parsePushEnvelopePlaintext(decoded);
5122
+ const now = Date.now();
5123
+ if (plaintext.sentAt < now - PUSH_ENVELOPE_MAX_AGE_MS) throw new Error("push envelope is stale");
5124
+ if (plaintext.sentAt > now + PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS) throw new Error("push envelope sentAt is in the future");
5125
+ return plaintext;
5126
+ }
5127
+
5078
5128
  // src/events/payloads.ts
5079
5129
  var DIFF_FILE_STATUSES = ["A", "M", "D", "R", "C", "U"];
5080
5130
  var TRANSCRIPT_SYSTEM_SUBTYPES = ["compaction", "command", "command_output", "session_boundary"];
@@ -5540,6 +5590,12 @@ function parseRegisterPushRequestPayload(payload) {
5540
5590
  }
5541
5591
  request.platform = payload.platform;
5542
5592
  }
5593
+ if (payload.categories !== void 0) {
5594
+ if (!Array.isArray(payload.categories) || !payload.categories.every((entry) => typeof entry === "string")) {
5595
+ throw new Error('register-push payload has a malformed "categories"');
5596
+ }
5597
+ request.categories = payload.categories.filter(isPushCategory);
5598
+ }
5543
5599
  if (request.action === "register") {
5544
5600
  if (request.expoPushToken === void 0) throw new Error('register-push register payload missing "expoPushToken"');
5545
5601
  if (request.pushKeyBase64 === void 0) throw new Error('register-push register payload missing "pushKeyBase64"');
@@ -5574,56 +5630,6 @@ function parseCapabilityRequestPayload(verb, payload) {
5574
5630
  }
5575
5631
  }
5576
5632
 
5577
- // src/crypto/push-envelope.ts
5578
- var PUSH_CATEGORIES = ["permission-needed", "agent-question", "turn-complete", "session-failed"];
5579
- function isPushCategory(value) {
5580
- return typeof value === "string" && PUSH_CATEGORIES.includes(value);
5581
- }
5582
- var PUSH_ENVELOPE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
5583
- var PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS = 5 * 60 * 1e3;
5584
- function requireEnvelopeString(record, field) {
5585
- const value = record[field];
5586
- if (typeof value !== "string") throw new Error(`push envelope plaintext is missing a string "${field}"`);
5587
- return value;
5588
- }
5589
- function parsePushEnvelopePlaintext(value) {
5590
- if (!isRecord(value)) throw new Error("push envelope plaintext must be an object");
5591
- if (!isPushCategory(value.category)) throw new Error('push envelope plaintext has an unknown "category"');
5592
- if (typeof value.sentAt !== "number" || !Number.isFinite(value.sentAt)) {
5593
- throw new Error('push envelope plaintext has an invalid "sentAt"');
5594
- }
5595
- return {
5596
- category: value.category,
5597
- projectId: requireEnvelopeString(value, "projectId"),
5598
- taskId: requireEnvelopeString(value, "taskId"),
5599
- sessionId: requireEnvelopeString(value, "sessionId"),
5600
- taskTitle: requireEnvelopeString(value, "taskTitle"),
5601
- detail: requireEnvelopeString(value, "detail"),
5602
- sentAt: value.sentAt
5603
- };
5604
- }
5605
- function sealPushEnvelope(pushKey, recipientStaticPublicKey, plaintext) {
5606
- const nonce = randomBytes4(XCHACHA_NONCE_LENGTH);
5607
- const encodedPlaintext = new TextEncoder().encode(JSON.stringify(plaintext));
5608
- const ciphertext = xaeadEncrypt(pushKey, nonce, recipientStaticPublicKey, encodedPlaintext);
5609
- return base64UrlEncode(concatBytes4(nonce, ciphertext));
5610
- }
5611
- function openPushEnvelope(pushKey, recipientStaticPublicKey, blob) {
5612
- const bytes = base64UrlDecode(blob);
5613
- if (bytes.length < XCHACHA_NONCE_LENGTH + AEAD_TAG_LENGTH) {
5614
- throw new Error("push envelope blob is too short");
5615
- }
5616
- const nonce = bytes.subarray(0, XCHACHA_NONCE_LENGTH);
5617
- const ciphertext = bytes.subarray(XCHACHA_NONCE_LENGTH);
5618
- const decrypted = xaeadDecrypt(pushKey, nonce, recipientStaticPublicKey, ciphertext);
5619
- const decoded = JSON.parse(new TextDecoder().decode(decrypted));
5620
- const plaintext = parsePushEnvelopePlaintext(decoded);
5621
- const now = Date.now();
5622
- if (plaintext.sentAt < now - PUSH_ENVELOPE_MAX_AGE_MS) throw new Error("push envelope is stale");
5623
- if (plaintext.sentAt > now + PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS) throw new Error("push envelope sentAt is in the future");
5624
- return plaintext;
5625
- }
5626
-
5627
5633
  // src/pairing/qr.ts
5628
5634
  var PAIRING_URI_SCHEME = "kangentic-pair";
5629
5635
  var PAYLOAD_VERSION = 1;
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import type { CapabilityVerb } from '../capabilities/verbs';
17
17
  import type { JsonValue } from './messages';
18
+ import { type PushCategory } from '../crypto/push-envelope';
18
19
  import { type ActivityReasonWire, type ActivityStateWire, type BacklogItemWire, type BoardColumnWire, type BoardTaskWire, type DiffFileContentWire, type DiffFileListWire, type SessionUsageWire, type TerminalDimensionsWire, type TranscriptEntryWire } from '../events/payloads';
19
20
  export interface ReadStreamRequestPayload {
20
21
  sessionId: string;
@@ -207,12 +208,20 @@ export interface BoardToolResponsePayload {
207
208
  * notification envelope with (see crypto/push-envelope.ts); 'unregister'
208
209
  * carries neither. The desktop keys the registration by the requesting
209
210
  * device's roster identity, never by anything in this payload.
211
+ *
212
+ * `categories`, when present, is the device's push preferences: the desktop
213
+ * filters outgoing notifications to this set before sending, so preference
214
+ * enforcement never depends on the receiving device (a future iOS
215
+ * Notification Service Extension stays preference-free). Absent means every
216
+ * category (the default, and what an older device that predates this field
217
+ * implicitly requests); an explicit empty array means none.
210
218
  */
211
219
  export interface RegisterPushRequestPayload {
212
220
  action: 'register' | 'unregister';
213
221
  expoPushToken?: string;
214
222
  pushKeyBase64?: string;
215
223
  platform?: 'android' | 'ios';
224
+ categories?: PushCategory[];
216
225
  }
217
226
  export interface RegisterPushResponsePayload {
218
227
  registered: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kangentic/protocol",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Kangentic mobile bridge wire protocol: framing, Noise-based pairing and session handshakes, signed device roster, and capability verbs shared between desktop and mobile.",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",