@kangentic/protocol 0.5.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/events/event.d.ts +10 -1
- package/dist/index.cjs +77 -52
- package/dist/index.mjs +77 -52
- package/dist/wire/payloads.d.ts +24 -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/events/event.d.ts
CHANGED
|
@@ -45,10 +45,19 @@ export type ActivityEventPayload = {
|
|
|
45
45
|
} | {
|
|
46
46
|
type: 'event';
|
|
47
47
|
event: SessionEventWire;
|
|
48
|
-
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* `options`, when present on a pending push, carries the prompt dialog's
|
|
51
|
+
* numbered option labels in keystroke order (options[0] is answered with
|
|
52
|
+
* "1\r"). Absent from pre-0.6.0 desktops and whenever no numbered dialog
|
|
53
|
+
* could be parsed from the PTY frame; the phone then falls back to its
|
|
54
|
+
* blind approve/deny keystrokes.
|
|
55
|
+
*/
|
|
56
|
+
| {
|
|
49
57
|
type: 'permission';
|
|
50
58
|
promptId: string;
|
|
51
59
|
pending: boolean;
|
|
60
|
+
options?: string[];
|
|
52
61
|
}
|
|
53
62
|
/**
|
|
54
63
|
* The streamed session's PTY exited. Pushed once by the desktop's
|
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"];
|
|
@@ -5437,6 +5487,15 @@ function parseReadStreamResponsePayload(payload) {
|
|
|
5437
5487
|
usage: payload.usage === null || payload.usage === void 0 ? null : parseSessionUsageWire(payload.usage),
|
|
5438
5488
|
awaitedPromptId: payload.awaitedPromptId ?? null
|
|
5439
5489
|
};
|
|
5490
|
+
if (payload.awaitedPromptOptions !== void 0) {
|
|
5491
|
+
if (payload.awaitedPromptOptions === null) {
|
|
5492
|
+
response.awaitedPromptOptions = null;
|
|
5493
|
+
} else if (Array.isArray(payload.awaitedPromptOptions) && payload.awaitedPromptOptions.every((option) => typeof option === "string")) {
|
|
5494
|
+
response.awaitedPromptOptions = payload.awaitedPromptOptions;
|
|
5495
|
+
} else {
|
|
5496
|
+
throw new Error('read-stream response has an invalid "awaitedPromptOptions"');
|
|
5497
|
+
}
|
|
5498
|
+
}
|
|
5440
5499
|
if (payload.ptyDimensions !== void 0) {
|
|
5441
5500
|
response.ptyDimensions = parseTerminalDimensionsWire(payload.ptyDimensions);
|
|
5442
5501
|
}
|
|
@@ -5514,12 +5573,16 @@ function parseReadBoardResponsePayload(payload) {
|
|
|
5514
5573
|
if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
|
|
5515
5574
|
if (!Array.isArray(payload.tasks)) throw new Error('read-board response is missing "tasks"');
|
|
5516
5575
|
if (!Array.isArray(payload.backlog)) throw new Error('read-board response is missing "backlog"');
|
|
5576
|
+
if (payload.showTicketNumbers !== void 0 && typeof payload.showTicketNumbers !== "boolean") {
|
|
5577
|
+
throw new Error('read-board response has a non-boolean "showTicketNumbers"');
|
|
5578
|
+
}
|
|
5517
5579
|
return {
|
|
5518
5580
|
projectId: payload.projectId,
|
|
5519
5581
|
columns: payload.columns.map((column) => parseBoardColumnWire(column)),
|
|
5520
5582
|
tasks: payload.tasks.map((task) => parseBoardTaskWire(task)),
|
|
5521
5583
|
backlog: payload.backlog.map((item) => parseBacklogItemWire(item)),
|
|
5522
|
-
...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {}
|
|
5584
|
+
...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {},
|
|
5585
|
+
...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {}
|
|
5523
5586
|
};
|
|
5524
5587
|
}
|
|
5525
5588
|
function parseReadBoardRequestPayload(payload) {
|
|
@@ -5638,6 +5701,12 @@ function parseRegisterPushRequestPayload(payload) {
|
|
|
5638
5701
|
}
|
|
5639
5702
|
request.platform = payload.platform;
|
|
5640
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
|
+
}
|
|
5641
5710
|
if (request.action === "register") {
|
|
5642
5711
|
if (request.expoPushToken === void 0) throw new Error('register-push register payload missing "expoPushToken"');
|
|
5643
5712
|
if (request.pushKeyBase64 === void 0) throw new Error('register-push register payload missing "pushKeyBase64"');
|
|
@@ -5672,56 +5741,6 @@ function parseCapabilityRequestPayload(verb, payload) {
|
|
|
5672
5741
|
}
|
|
5673
5742
|
}
|
|
5674
5743
|
|
|
5675
|
-
// src/crypto/push-envelope.ts
|
|
5676
|
-
var PUSH_CATEGORIES = ["permission-needed", "agent-question", "turn-complete", "session-failed"];
|
|
5677
|
-
function isPushCategory(value) {
|
|
5678
|
-
return typeof value === "string" && PUSH_CATEGORIES.includes(value);
|
|
5679
|
-
}
|
|
5680
|
-
var PUSH_ENVELOPE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
5681
|
-
var PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS = 5 * 60 * 1e3;
|
|
5682
|
-
function requireEnvelopeString(record, field) {
|
|
5683
|
-
const value = record[field];
|
|
5684
|
-
if (typeof value !== "string") throw new Error(`push envelope plaintext is missing a string "${field}"`);
|
|
5685
|
-
return value;
|
|
5686
|
-
}
|
|
5687
|
-
function parsePushEnvelopePlaintext(value) {
|
|
5688
|
-
if (!isRecord(value)) throw new Error("push envelope plaintext must be an object");
|
|
5689
|
-
if (!isPushCategory(value.category)) throw new Error('push envelope plaintext has an unknown "category"');
|
|
5690
|
-
if (typeof value.sentAt !== "number" || !Number.isFinite(value.sentAt)) {
|
|
5691
|
-
throw new Error('push envelope plaintext has an invalid "sentAt"');
|
|
5692
|
-
}
|
|
5693
|
-
return {
|
|
5694
|
-
category: value.category,
|
|
5695
|
-
projectId: requireEnvelopeString(value, "projectId"),
|
|
5696
|
-
taskId: requireEnvelopeString(value, "taskId"),
|
|
5697
|
-
sessionId: requireEnvelopeString(value, "sessionId"),
|
|
5698
|
-
taskTitle: requireEnvelopeString(value, "taskTitle"),
|
|
5699
|
-
detail: requireEnvelopeString(value, "detail"),
|
|
5700
|
-
sentAt: value.sentAt
|
|
5701
|
-
};
|
|
5702
|
-
}
|
|
5703
|
-
function sealPushEnvelope(pushKey, recipientStaticPublicKey, plaintext) {
|
|
5704
|
-
const nonce = randomBytes4(XCHACHA_NONCE_LENGTH);
|
|
5705
|
-
const encodedPlaintext = new TextEncoder().encode(JSON.stringify(plaintext));
|
|
5706
|
-
const ciphertext = xaeadEncrypt(pushKey, nonce, recipientStaticPublicKey, encodedPlaintext);
|
|
5707
|
-
return base64UrlEncode(concatBytes4(nonce, ciphertext));
|
|
5708
|
-
}
|
|
5709
|
-
function openPushEnvelope(pushKey, recipientStaticPublicKey, blob) {
|
|
5710
|
-
const bytes = base64UrlDecode(blob);
|
|
5711
|
-
if (bytes.length < XCHACHA_NONCE_LENGTH + AEAD_TAG_LENGTH) {
|
|
5712
|
-
throw new Error("push envelope blob is too short");
|
|
5713
|
-
}
|
|
5714
|
-
const nonce = bytes.subarray(0, XCHACHA_NONCE_LENGTH);
|
|
5715
|
-
const ciphertext = bytes.subarray(XCHACHA_NONCE_LENGTH);
|
|
5716
|
-
const decrypted = xaeadDecrypt(pushKey, nonce, recipientStaticPublicKey, ciphertext);
|
|
5717
|
-
const decoded = JSON.parse(new TextDecoder().decode(decrypted));
|
|
5718
|
-
const plaintext = parsePushEnvelopePlaintext(decoded);
|
|
5719
|
-
const now = Date.now();
|
|
5720
|
-
if (plaintext.sentAt < now - PUSH_ENVELOPE_MAX_AGE_MS) throw new Error("push envelope is stale");
|
|
5721
|
-
if (plaintext.sentAt > now + PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS) throw new Error("push envelope sentAt is in the future");
|
|
5722
|
-
return plaintext;
|
|
5723
|
-
}
|
|
5724
|
-
|
|
5725
5744
|
// src/pairing/qr.ts
|
|
5726
5745
|
var PAIRING_URI_SCHEME = "kangentic-pair";
|
|
5727
5746
|
var PAYLOAD_VERSION = 1;
|
|
@@ -5895,7 +5914,13 @@ function parseActivityEventPayload(payload) {
|
|
|
5895
5914
|
case "permission": {
|
|
5896
5915
|
if (typeof payload.promptId !== "string") throw new Error('permission payload is missing "promptId"');
|
|
5897
5916
|
if (typeof payload.pending !== "boolean") throw new Error('permission payload is missing "pending"');
|
|
5898
|
-
|
|
5917
|
+
if (payload.options === void 0) {
|
|
5918
|
+
return { type: "permission", promptId: payload.promptId, pending: payload.pending };
|
|
5919
|
+
}
|
|
5920
|
+
if (!Array.isArray(payload.options) || !payload.options.every((option) => typeof option === "string")) {
|
|
5921
|
+
throw new Error('permission payload has an invalid "options"');
|
|
5922
|
+
}
|
|
5923
|
+
return { type: "permission", promptId: payload.promptId, pending: payload.pending, options: payload.options };
|
|
5899
5924
|
}
|
|
5900
5925
|
case "session-ended": {
|
|
5901
5926
|
if (typeof payload.intentional !== "boolean") throw new Error('session-ended payload is missing "intentional"');
|
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"];
|
|
@@ -5326,6 +5376,15 @@ function parseReadStreamResponsePayload(payload) {
|
|
|
5326
5376
|
usage: payload.usage === null || payload.usage === void 0 ? null : parseSessionUsageWire(payload.usage),
|
|
5327
5377
|
awaitedPromptId: payload.awaitedPromptId ?? null
|
|
5328
5378
|
};
|
|
5379
|
+
if (payload.awaitedPromptOptions !== void 0) {
|
|
5380
|
+
if (payload.awaitedPromptOptions === null) {
|
|
5381
|
+
response.awaitedPromptOptions = null;
|
|
5382
|
+
} else if (Array.isArray(payload.awaitedPromptOptions) && payload.awaitedPromptOptions.every((option) => typeof option === "string")) {
|
|
5383
|
+
response.awaitedPromptOptions = payload.awaitedPromptOptions;
|
|
5384
|
+
} else {
|
|
5385
|
+
throw new Error('read-stream response has an invalid "awaitedPromptOptions"');
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5329
5388
|
if (payload.ptyDimensions !== void 0) {
|
|
5330
5389
|
response.ptyDimensions = parseTerminalDimensionsWire(payload.ptyDimensions);
|
|
5331
5390
|
}
|
|
@@ -5403,12 +5462,16 @@ function parseReadBoardResponsePayload(payload) {
|
|
|
5403
5462
|
if (!Array.isArray(payload.columns)) throw new Error('read-board response is missing "columns"');
|
|
5404
5463
|
if (!Array.isArray(payload.tasks)) throw new Error('read-board response is missing "tasks"');
|
|
5405
5464
|
if (!Array.isArray(payload.backlog)) throw new Error('read-board response is missing "backlog"');
|
|
5465
|
+
if (payload.showTicketNumbers !== void 0 && typeof payload.showTicketNumbers !== "boolean") {
|
|
5466
|
+
throw new Error('read-board response has a non-boolean "showTicketNumbers"');
|
|
5467
|
+
}
|
|
5406
5468
|
return {
|
|
5407
5469
|
projectId: payload.projectId,
|
|
5408
5470
|
columns: payload.columns.map((column) => parseBoardColumnWire(column)),
|
|
5409
5471
|
tasks: payload.tasks.map((task) => parseBoardTaskWire(task)),
|
|
5410
5472
|
backlog: payload.backlog.map((item) => parseBacklogItemWire(item)),
|
|
5411
|
-
...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {}
|
|
5473
|
+
...payload.projectColor !== void 0 ? { projectColor: parseAccentColor(payload.projectColor, "read-board snapshot") } : {},
|
|
5474
|
+
...payload.showTicketNumbers !== void 0 ? { showTicketNumbers: payload.showTicketNumbers } : {}
|
|
5412
5475
|
};
|
|
5413
5476
|
}
|
|
5414
5477
|
function parseReadBoardRequestPayload(payload) {
|
|
@@ -5527,6 +5590,12 @@ function parseRegisterPushRequestPayload(payload) {
|
|
|
5527
5590
|
}
|
|
5528
5591
|
request.platform = payload.platform;
|
|
5529
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
|
+
}
|
|
5530
5599
|
if (request.action === "register") {
|
|
5531
5600
|
if (request.expoPushToken === void 0) throw new Error('register-push register payload missing "expoPushToken"');
|
|
5532
5601
|
if (request.pushKeyBase64 === void 0) throw new Error('register-push register payload missing "pushKeyBase64"');
|
|
@@ -5561,56 +5630,6 @@ function parseCapabilityRequestPayload(verb, payload) {
|
|
|
5561
5630
|
}
|
|
5562
5631
|
}
|
|
5563
5632
|
|
|
5564
|
-
// src/crypto/push-envelope.ts
|
|
5565
|
-
var PUSH_CATEGORIES = ["permission-needed", "agent-question", "turn-complete", "session-failed"];
|
|
5566
|
-
function isPushCategory(value) {
|
|
5567
|
-
return typeof value === "string" && PUSH_CATEGORIES.includes(value);
|
|
5568
|
-
}
|
|
5569
|
-
var PUSH_ENVELOPE_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
5570
|
-
var PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS = 5 * 60 * 1e3;
|
|
5571
|
-
function requireEnvelopeString(record, field) {
|
|
5572
|
-
const value = record[field];
|
|
5573
|
-
if (typeof value !== "string") throw new Error(`push envelope plaintext is missing a string "${field}"`);
|
|
5574
|
-
return value;
|
|
5575
|
-
}
|
|
5576
|
-
function parsePushEnvelopePlaintext(value) {
|
|
5577
|
-
if (!isRecord(value)) throw new Error("push envelope plaintext must be an object");
|
|
5578
|
-
if (!isPushCategory(value.category)) throw new Error('push envelope plaintext has an unknown "category"');
|
|
5579
|
-
if (typeof value.sentAt !== "number" || !Number.isFinite(value.sentAt)) {
|
|
5580
|
-
throw new Error('push envelope plaintext has an invalid "sentAt"');
|
|
5581
|
-
}
|
|
5582
|
-
return {
|
|
5583
|
-
category: value.category,
|
|
5584
|
-
projectId: requireEnvelopeString(value, "projectId"),
|
|
5585
|
-
taskId: requireEnvelopeString(value, "taskId"),
|
|
5586
|
-
sessionId: requireEnvelopeString(value, "sessionId"),
|
|
5587
|
-
taskTitle: requireEnvelopeString(value, "taskTitle"),
|
|
5588
|
-
detail: requireEnvelopeString(value, "detail"),
|
|
5589
|
-
sentAt: value.sentAt
|
|
5590
|
-
};
|
|
5591
|
-
}
|
|
5592
|
-
function sealPushEnvelope(pushKey, recipientStaticPublicKey, plaintext) {
|
|
5593
|
-
const nonce = randomBytes4(XCHACHA_NONCE_LENGTH);
|
|
5594
|
-
const encodedPlaintext = new TextEncoder().encode(JSON.stringify(plaintext));
|
|
5595
|
-
const ciphertext = xaeadEncrypt(pushKey, nonce, recipientStaticPublicKey, encodedPlaintext);
|
|
5596
|
-
return base64UrlEncode(concatBytes4(nonce, ciphertext));
|
|
5597
|
-
}
|
|
5598
|
-
function openPushEnvelope(pushKey, recipientStaticPublicKey, blob) {
|
|
5599
|
-
const bytes = base64UrlDecode(blob);
|
|
5600
|
-
if (bytes.length < XCHACHA_NONCE_LENGTH + AEAD_TAG_LENGTH) {
|
|
5601
|
-
throw new Error("push envelope blob is too short");
|
|
5602
|
-
}
|
|
5603
|
-
const nonce = bytes.subarray(0, XCHACHA_NONCE_LENGTH);
|
|
5604
|
-
const ciphertext = bytes.subarray(XCHACHA_NONCE_LENGTH);
|
|
5605
|
-
const decrypted = xaeadDecrypt(pushKey, nonce, recipientStaticPublicKey, ciphertext);
|
|
5606
|
-
const decoded = JSON.parse(new TextDecoder().decode(decrypted));
|
|
5607
|
-
const plaintext = parsePushEnvelopePlaintext(decoded);
|
|
5608
|
-
const now = Date.now();
|
|
5609
|
-
if (plaintext.sentAt < now - PUSH_ENVELOPE_MAX_AGE_MS) throw new Error("push envelope is stale");
|
|
5610
|
-
if (plaintext.sentAt > now + PUSH_ENVELOPE_MAX_FUTURE_SKEW_MS) throw new Error("push envelope sentAt is in the future");
|
|
5611
|
-
return plaintext;
|
|
5612
|
-
}
|
|
5613
|
-
|
|
5614
5633
|
// src/pairing/qr.ts
|
|
5615
5634
|
var PAIRING_URI_SCHEME = "kangentic-pair";
|
|
5616
5635
|
var PAYLOAD_VERSION = 1;
|
|
@@ -5784,7 +5803,13 @@ function parseActivityEventPayload(payload) {
|
|
|
5784
5803
|
case "permission": {
|
|
5785
5804
|
if (typeof payload.promptId !== "string") throw new Error('permission payload is missing "promptId"');
|
|
5786
5805
|
if (typeof payload.pending !== "boolean") throw new Error('permission payload is missing "pending"');
|
|
5787
|
-
|
|
5806
|
+
if (payload.options === void 0) {
|
|
5807
|
+
return { type: "permission", promptId: payload.promptId, pending: payload.pending };
|
|
5808
|
+
}
|
|
5809
|
+
if (!Array.isArray(payload.options) || !payload.options.every((option) => typeof option === "string")) {
|
|
5810
|
+
throw new Error('permission payload has an invalid "options"');
|
|
5811
|
+
}
|
|
5812
|
+
return { type: "permission", promptId: payload.promptId, pending: payload.pending, options: payload.options };
|
|
5788
5813
|
}
|
|
5789
5814
|
case "session-ended": {
|
|
5790
5815
|
if (typeof payload.intentional !== "boolean") throw new Error('session-ended payload is missing "intentional"');
|
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;
|
|
@@ -48,6 +49,15 @@ export interface ReadStreamResponsePayload {
|
|
|
48
49
|
usage: SessionUsageWire | null;
|
|
49
50
|
/** The live outstanding permission-prompt id (see answer-permission-prompt), or null when none is pending. */
|
|
50
51
|
awaitedPromptId: string | null;
|
|
52
|
+
/**
|
|
53
|
+
* The awaited prompt's numbered option labels, parsed by the desktop from
|
|
54
|
+
* the pending dialog's PTY frame, in keystroke order: options[0] is the
|
|
55
|
+
* row answered with "1\r", options[1] with "2\r", and so on. Absent from
|
|
56
|
+
* pre-0.6.0 desktops; absent or null means unknown (no numbered dialog
|
|
57
|
+
* could be parsed), and the phone falls back to its blind
|
|
58
|
+
* approve/deny keystrokes. Only meaningful while awaitedPromptId is set.
|
|
59
|
+
*/
|
|
60
|
+
awaitedPromptOptions?: string[] | null;
|
|
51
61
|
/**
|
|
52
62
|
* The PTY grid the scrollback bytes are laid out for. Absent from
|
|
53
63
|
* pre-0.4.0 desktops; the phone then falls back to inferring a width
|
|
@@ -103,6 +113,12 @@ export interface ReadBoardSnapshotResponsePayload {
|
|
|
103
113
|
backlog: BacklogItemWire[];
|
|
104
114
|
/** The project's accent color ("#rrggbb"); same semantics as ReadBoardProjectSummary.color. Absent from pre-0.5.0 desktops. */
|
|
105
115
|
projectColor?: string;
|
|
116
|
+
/**
|
|
117
|
+
* The desktop's Layout "Ticket Numbers" setting: whether task cards
|
|
118
|
+
* display their #N number. Absent from pre-0.6.0 desktops (treat as
|
|
119
|
+
* true, the desktop default).
|
|
120
|
+
*/
|
|
121
|
+
showTicketNumbers?: boolean;
|
|
106
122
|
}
|
|
107
123
|
export type ReadBoardResponsePayload = ReadBoardProjectListResponsePayload | ReadBoardSnapshotResponsePayload;
|
|
108
124
|
/** Phone-side narrowing of a read-board response (project list or board snapshot). Throws on a malformed required field. */
|
|
@@ -192,12 +208,20 @@ export interface BoardToolResponsePayload {
|
|
|
192
208
|
* notification envelope with (see crypto/push-envelope.ts); 'unregister'
|
|
193
209
|
* carries neither. The desktop keys the registration by the requesting
|
|
194
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.
|
|
195
218
|
*/
|
|
196
219
|
export interface RegisterPushRequestPayload {
|
|
197
220
|
action: 'register' | 'unregister';
|
|
198
221
|
expoPushToken?: string;
|
|
199
222
|
pushKeyBase64?: string;
|
|
200
223
|
platform?: 'android' | 'ios';
|
|
224
|
+
categories?: PushCategory[];
|
|
201
225
|
}
|
|
202
226
|
export interface RegisterPushResponsePayload {
|
|
203
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",
|