@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.
- package/dist/crypto/push-envelope.d.ts +1 -1
- package/dist/index.cjs +56 -50
- package/dist/index.mjs +56 -50
- package/dist/wire/payloads.d.ts +9 -0
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const PUSH_CATEGORIES: readonly ["
|
|
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;
|
package/dist/wire/payloads.d.ts
CHANGED
|
@@ -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.
|
|
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",
|