salt-agent-sdk 0.20.0 → 0.22.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.
Files changed (48) hide show
  1. package/README.md +28 -3
  2. package/dist/actions.d.ts.map +1 -1
  3. package/dist/actions.js +72 -1
  4. package/dist/actions.js.map +1 -1
  5. package/dist/callBundle.d.ts +90 -0
  6. package/dist/callBundle.d.ts.map +1 -0
  7. package/dist/callBundle.js +199 -0
  8. package/dist/callBundle.js.map +1 -0
  9. package/dist/client.d.ts +88 -3
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +80 -2
  12. package/dist/client.js.map +1 -1
  13. package/dist/device/client.d.ts +12 -4
  14. package/dist/device/client.d.ts.map +1 -1
  15. package/dist/device/client.js +44 -1
  16. package/dist/device/client.js.map +1 -1
  17. package/dist/device/host.d.ts +74 -3
  18. package/dist/device/host.d.ts.map +1 -1
  19. package/dist/device/host.js +262 -12
  20. package/dist/device/host.js.map +1 -1
  21. package/dist/device/http.d.ts.map +1 -1
  22. package/dist/device/http.js +3 -2
  23. package/dist/device/http.js.map +1 -1
  24. package/dist/device/protocol.d.ts +97 -1
  25. package/dist/device/protocol.d.ts.map +1 -1
  26. package/dist/device/protocol.js +155 -1
  27. package/dist/device/protocol.js.map +1 -1
  28. package/dist/help.d.ts +54 -0
  29. package/dist/help.d.ts.map +1 -0
  30. package/dist/help.js +62 -0
  31. package/dist/help.js.map +1 -0
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +2 -0
  35. package/dist/index.js.map +1 -1
  36. package/dist/lattice.d.ts +206 -0
  37. package/dist/lattice.d.ts.map +1 -0
  38. package/dist/lattice.js +366 -0
  39. package/dist/lattice.js.map +1 -0
  40. package/dist/webhook.d.ts +67 -3
  41. package/dist/webhook.d.ts.map +1 -1
  42. package/dist/webhook.js +72 -4
  43. package/dist/webhook.js.map +1 -1
  44. package/dist/work.d.ts +2 -0
  45. package/dist/work.d.ts.map +1 -1
  46. package/dist/work.js +4 -0
  47. package/dist/work.js.map +1 -1
  48. package/package.json +1 -1
@@ -0,0 +1,199 @@
1
+ "use strict";
2
+ // The Calls v1 crypto binding (salt-api CALL_PROTOCOL_SPEC.md section 4), ported from salt-fe's
3
+ // src/utilities/callCrypto.js and the bundle builders in src/utilities/webrtc.js. Byte-compatible:
4
+ // a bundle this module signs and encrypts is read by salt-fe's decryptAndVerify, and the other way
5
+ // round (tests/call-bundle.test.js proves both against a fixture generated from salt-fe itself).
6
+ //
7
+ // The signed-then-encrypted offer/answer bundle is
8
+ // { v, call_id, kind, sdp, dtls_fingerprints, ts, nonce }
9
+ // (a renegotiation adds `sdp_type`; a trickle-ICE batch is { v, call_id, candidates, ts, nonce }).
10
+ // The sender SIGNS it with their own PGP private key and ENCRYPTS it to the peer's public key in one
11
+ // openpgp call. That closes the hole WebRTC leaves open: a signaling server substituting its own SDP
12
+ // to sit in the media path. Salt relays the opaque armored `payload` and never reads it.
13
+ //
14
+ // Pure crypto and string handling: no network, no WebRTC. Used by the device (it answers a watch call
15
+ // with a signed bundle to the caller) and by any agent that places or receives calls.
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.LEGACY_KEY_PASSPHRASE = exports.TS_SKEW_MS = exports.CALL_BUNDLE_VERSION = void 0;
51
+ exports.randomNonce = randomNonce;
52
+ exports.buildCallBundle = buildCallBundle;
53
+ exports.buildSignalBundle = buildSignalBundle;
54
+ exports.signAndEncrypt = signAndEncrypt;
55
+ exports.decryptAndVerify = decryptAndVerify;
56
+ exports.isBundleFresh = isBundleFresh;
57
+ exports.parseSdpFingerprints = parseSdpFingerprints;
58
+ exports.fingerprintsMatch = fingerprintsMatch;
59
+ exports.senderKeyMatchesContact = senderKeyMatchesContact;
60
+ const node_crypto_1 = require("node:crypto");
61
+ const openpgp = __importStar(require("openpgp"));
62
+ exports.CALL_BUNDLE_VERSION = 1;
63
+ /** Reject a bundle whose `ts` has drifted more than this from wall-clock. */
64
+ exports.TS_SKEW_MS = 5 * 60 * 1000;
65
+ /** A legacy server-custody private key is still armored under this constant passphrase. */
66
+ exports.LEGACY_KEY_PASSPHRASE = "salt";
67
+ // Accepts a vault-issued key (already decrypted: no passphrase) or a legacy key armored under the
68
+ // passphrase (salt-fe's constant "salt" unless one is given).
69
+ async function unlockPrivateKey(armoredPrivateKey, passphrase = exports.LEGACY_KEY_PASSPHRASE) {
70
+ const key = await openpgp.readPrivateKey({ armoredKey: armoredPrivateKey });
71
+ if (key.isDecrypted())
72
+ return key;
73
+ return openpgp.decryptKey({ privateKey: key, passphrase });
74
+ }
75
+ /** A 128-bit random nonce, hex (section 4's bundle shape). */
76
+ function randomNonce() {
77
+ return (0, node_crypto_1.randomBytes)(16).toString("hex");
78
+ }
79
+ /** Build the plaintext offer/answer bundle. `dtls_fingerprints` is always derived from `sdp` itself,
80
+ * never supplied: pinning only means something if the fingerprints are the ones in the SDP being sent.
81
+ * `ts` is unix seconds. Throws when the SDP carries no `a=fingerprint:` line. */
82
+ function buildCallBundle(p) {
83
+ const dtls_fingerprints = parseSdpFingerprints(p.sdp);
84
+ if (dtls_fingerprints.length === 0)
85
+ throw new Error("sdp carries no a=fingerprint lines to bind");
86
+ const bundle = {
87
+ v: exports.CALL_BUNDLE_VERSION,
88
+ call_id: p.callId,
89
+ kind: p.kind,
90
+ ...(p.sdpType !== undefined ? { sdp_type: p.sdpType } : {}),
91
+ sdp: p.sdp,
92
+ dtls_fingerprints,
93
+ ts: p.ts ?? Math.floor(Date.now() / 1000),
94
+ nonce: p.nonce ?? randomNonce(),
95
+ };
96
+ return bundle;
97
+ }
98
+ /** Build the plaintext trickle-ICE bundle. */
99
+ function buildSignalBundle(p) {
100
+ return {
101
+ v: exports.CALL_BUNDLE_VERSION,
102
+ call_id: p.callId,
103
+ candidates: p.candidates,
104
+ ts: p.ts ?? Math.floor(Date.now() / 1000),
105
+ nonce: p.nonce ?? randomNonce(),
106
+ };
107
+ }
108
+ /** Sign `bundle` with the sender's private key and encrypt the result to the receiver's public key.
109
+ * Returns armored ciphertext, ready for the opaque `payload` field of POST /api/v1/calls (or /answer,
110
+ * /signal). `passphrase` only matters for a legacy key that is still locked (default "salt"). */
111
+ async function signAndEncrypt(p) {
112
+ const privateKey = await unlockPrivateKey(p.signingPrivateKey, p.passphrase);
113
+ const publicKey = await openpgp.readKey({ armoredKey: p.receiverPublicKey });
114
+ const message = await openpgp.createMessage({ text: JSON.stringify(p.bundle) });
115
+ return (await openpgp.encrypt({ message, encryptionKeys: publicKey, signingKeys: privateKey }));
116
+ }
117
+ /** Decrypt an armored bundle and check its signature against the SENDER's public key (never a key
118
+ * embedded in the message). Never throws for an ordinary crypto failure (corrupted payload, a signature
119
+ * from the wrong key): those come back as `{ bundle: null, signatureValid: false }` so callers fail
120
+ * closed. A bundle that decrypts but is not JSON is the same. */
121
+ async function decryptAndVerify(p) {
122
+ try {
123
+ const privateKey = await unlockPrivateKey(p.decryptionPrivateKey, p.passphrase);
124
+ const publicKey = await openpgp.readKey({ armoredKey: p.senderPublicKey });
125
+ const message = await openpgp.readMessage({ armoredMessage: p.armored });
126
+ const { data, signatures } = await openpgp.decrypt({
127
+ message,
128
+ decryptionKeys: privateKey,
129
+ verificationKeys: publicKey,
130
+ expectSigned: false, // the signatures are inspected below
131
+ });
132
+ let signatureValid = false;
133
+ if (signatures && signatures.length > 0) {
134
+ try {
135
+ await signatures[0].verified;
136
+ signatureValid = true;
137
+ }
138
+ catch {
139
+ signatureValid = false;
140
+ }
141
+ }
142
+ let bundle = null;
143
+ try {
144
+ bundle = JSON.parse(data);
145
+ }
146
+ catch {
147
+ return { bundle: null, signatureValid: false };
148
+ }
149
+ return { bundle, signatureValid };
150
+ }
151
+ catch {
152
+ return { bundle: null, signatureValid: false };
153
+ }
154
+ }
155
+ /** Section 4's replay half: `call_id` and `ts` bind the bundle to THIS call. No crypto here. `ts` is
156
+ * unix SECONDS and must be within 5 minutes of `now` (milliseconds, default the wall clock); when
157
+ * `expectedCallId` is given the bundle's `call_id` must equal it. */
158
+ function isBundleFresh(p) {
159
+ const { bundle, expectedCallId } = p;
160
+ const now = p.now ?? Date.now();
161
+ if (!bundle)
162
+ return false;
163
+ if (expectedCallId && bundle.call_id !== expectedCallId)
164
+ return false;
165
+ if (typeof bundle.ts !== "number")
166
+ return false;
167
+ return Math.abs(now - bundle.ts * 1000) <= exports.TS_SKEW_MS;
168
+ }
169
+ function normalizeFingerprint(fp) {
170
+ return String(fp ?? "").trim().toUpperCase().replace(/\s+/g, " ");
171
+ }
172
+ /** Every `a=fingerprint:<algo> <hex>` line of an SDP, normalised (trimmed, upper-cased, whitespace
173
+ * collapsed): `"SHA-256 AB:CD:..."`. Accepts CRLF or LF line ends. */
174
+ function parseSdpFingerprints(sdp) {
175
+ return Array.from(String(sdp || "").matchAll(/^a=fingerprint:(.+)$/gim)).map((m) => normalizeFingerprint(m[1]));
176
+ }
177
+ /** Section 4's pin: every fingerprint in the SDP being applied must be one the signed bundle carries
178
+ * (case and whitespace insensitive). False when either side has none. A mismatch means: hang up with
179
+ * reason `error`, do not apply the SDP. */
180
+ function fingerprintsMatch(signed, actualSdp) {
181
+ const signedSet = new Set((signed || []).map(normalizeFingerprint));
182
+ const actual = parseSdpFingerprints(actualSdp);
183
+ if (actual.length === 0 || signedSet.size === 0)
184
+ return false;
185
+ return actual.every((fp) => signedSet.has(fp));
186
+ }
187
+ /** The verified badge: only when the signing key's own fingerprint matches the stored public key. */
188
+ async function senderKeyMatchesContact(p) {
189
+ if (!p.senderPublicKey || !p.contactPublicKey)
190
+ return false;
191
+ try {
192
+ const [a, b] = await Promise.all([openpgp.readKey({ armoredKey: p.senderPublicKey }), openpgp.readKey({ armoredKey: p.contactPublicKey })]);
193
+ return a.getFingerprint().toUpperCase() === b.getFingerprint().toUpperCase();
194
+ }
195
+ catch {
196
+ return false;
197
+ }
198
+ }
199
+ //# sourceMappingURL=callBundle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"callBundle.js","sourceRoot":"","sources":["../src/callBundle.ts"],"names":[],"mappings":";AAAA,gGAAgG;AAChG,mGAAmG;AACnG,mGAAmG;AACnG,iGAAiG;AACjG,EAAE;AACF,mDAAmD;AACnD,4DAA4D;AAC5D,mGAAmG;AACnG,qGAAqG;AACrG,qGAAqG;AACrG,yFAAyF;AACzF,EAAE;AACF,sGAAsG;AACtG,sFAAsF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CtF,kCAEC;AAKD,0CAqBC;AAGD,8CAQC;AAKD,wCAUC;AAMD,4CAqCC;AAKD,sCAOC;AAQD,oDAEC;AAKD,8CAKC;AAGD,0DAQC;AAxLD,6CAA0C;AAC1C,iDAAmC;AAEtB,QAAA,mBAAmB,GAAG,CAAC,CAAC;AAErC,6EAA6E;AAChE,QAAA,UAAU,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAExC,2FAA2F;AAC9E,QAAA,qBAAqB,GAAG,MAAM,CAAC;AA0B5C,kGAAkG;AAClG,8DAA8D;AAC9D,KAAK,UAAU,gBAAgB,CAAC,iBAAyB,EAAE,aAAqB,6BAAqB;IACnG,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,EAAE,UAAU,EAAE,iBAAiB,EAAE,CAAC,CAAC;IAC5E,IAAI,GAAG,CAAC,WAAW,EAAE;QAAE,OAAO,GAAG,CAAC;IAClC,OAAO,OAAO,CAAC,UAAU,CAAC,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU,EAAE,CAAC,CAAC;AAC7D,CAAC;AAED,8DAA8D;AAC9D,SAAgB,WAAW;IACzB,OAAO,IAAA,yBAAW,EAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACzC,CAAC;AAED;;kFAEkF;AAClF,SAAgB,eAAe,CAAC,CAO/B;IACC,MAAM,iBAAiB,GAAG,oBAAoB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACtD,IAAI,iBAAiB,CAAC,MAAM,KAAK,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,4CAA4C,CAAC,CAAC;IAClG,MAAM,MAAM,GAAe;QACzB,CAAC,EAAE,2BAAmB;QACtB,OAAO,EAAE,CAAC,CAAC,MAAM;QACjB,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,GAAG,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,GAAG,EAAE,CAAC,CAAC,GAAG;QACV,iBAAiB;QACjB,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;QACzC,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,WAAW,EAAE;KAChC,CAAC;IACF,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,8CAA8C;AAC9C,SAAgB,iBAAiB,CAAC,CAAyE;IACzG,OAAO;QACL,CAAC,EAAE,2BAAmB;QACtB,OAAO,EAAE,CAAC,CAAC,MAAM;QACjB,UAAU,EAAE,CAAC,CAAC,UAAU;QACxB,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;QACzC,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,WAAW,EAAE;KAChC,CAAC;AACJ,CAAC;AAED;;kGAEkG;AAC3F,KAAK,UAAU,cAAc,CAAC,CAKpC;IACC,MAAM,UAAU,GAAG,MAAM,gBAAgB,CAAC,CAAC,CAAC,iBAAiB,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;IAC7E,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,iBAAiB,EAAE,CAAC,CAAC;IAC7E,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,aAAa,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAChF,OAAO,CAAC,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,cAAc,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC,CAAW,CAAC;AAC5G,CAAC;AAED;;;kEAGkE;AAC3D,KAAK,UAAU,gBAAgB,CAAC,CAKtC;IACC,IAAI,CAAC;QACH,MAAM,UAAU,GAAG,MAAM,gBAAgB,CAAC,CAAC,CAAC,oBAAoB,EAAE,CAAC,CAAC,UAAU,CAAC,CAAC;QAChF,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,eAAe,EAAE,CAAC,CAAC;QAC3E,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,WAAW,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QACzE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC;YACjD,OAAO;YACP,cAAc,EAAE,UAAU;YAC1B,gBAAgB,EAAE,SAAS;YAC3B,YAAY,EAAE,KAAK,EAAE,qCAAqC;SAC3D,CAAC,CAAC;QAEH,IAAI,cAAc,GAAG,KAAK,CAAC;QAC3B,IAAI,UAAU,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxC,IAAI,CAAC;gBACH,MAAM,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;gBAC7B,cAAc,GAAG,IAAI,CAAC;YACxB,CAAC;YAAC,MAAM,CAAC;gBACP,cAAc,GAAG,KAAK,CAAC;YACzB,CAAC;QACH,CAAC;QAED,IAAI,MAAM,GAA+B,IAAI,CAAC;QAC9C,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAc,CAAC,CAAC;QACtC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC;QACjD,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,CAAC;IACpC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC;IACjD,CAAC;AACH,CAAC;AAED;;sEAEsE;AACtE,SAAgB,aAAa,CAAC,CAA4F;IACxH,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,GAAG,CAAC,CAAC;IACrC,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC;IAChC,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC1B,IAAI,cAAc,IAAI,MAAM,CAAC,OAAO,KAAK,cAAc;QAAE,OAAO,KAAK,CAAC;IACtE,IAAI,OAAO,MAAM,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAChD,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,MAAM,CAAC,EAAE,GAAG,IAAI,CAAC,IAAI,kBAAU,CAAC;AACxD,CAAC;AAED,SAAS,oBAAoB,CAAC,EAAW;IACvC,OAAO,MAAM,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AACpE,CAAC;AAED;uEACuE;AACvE,SAAgB,oBAAoB,CAAC,GAAW;IAC9C,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,yBAAyB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAClH,CAAC;AAED;;4CAE4C;AAC5C,SAAgB,iBAAiB,CAAC,MAAmC,EAAE,SAAiB;IACtF,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC,CAAC;IACpE,MAAM,MAAM,GAAG,oBAAoB,CAAC,SAAS,CAAC,CAAC;IAC/C,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9D,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AACjD,CAAC;AAED,qGAAqG;AAC9F,KAAK,UAAU,uBAAuB,CAAC,CAAwD;IACpG,IAAI,CAAC,CAAC,CAAC,eAAe,IAAI,CAAC,CAAC,CAAC,gBAAgB;QAAE,OAAO,KAAK,CAAC;IAC5D,IAAI,CAAC;QACH,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,eAAe,EAAE,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,gBAAgB,EAAE,CAAC,CAAC,CAAC,CAAC;QAC5I,OAAO,CAAC,CAAC,cAAc,EAAE,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,cAAc,EAAE,CAAC,WAAW,EAAE,CAAC;IAC/E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
package/dist/client.d.ts CHANGED
@@ -1,6 +1,26 @@
1
1
  import { type SaltId } from "./ids.js";
2
2
  import { type IdentityClaims, type IdentitySections, type SignedCard } from "./identity.js";
3
3
  import { type HeldRecoveryShare, type RecoveryGuardianInvitation, type RecoveryRequestForGuardian } from "./recovery.js";
4
+ import { type HelpReason, type HelpRequest } from "./help.js";
5
+ /** What `client.askForHelp` / `ctx.askForHelp` take. The question rides end-to-end encrypted in the 1:1; Salt learns only the reason. */
6
+ export interface AskForHelpParams {
7
+ /** The 1:1 with the person. Exactly two non-observer members, else the call is refused before anything is sent. */
8
+ chatId: SaltId;
9
+ /** In plain words, at most 500 characters. */
10
+ question: string;
11
+ reason: HelpReason;
12
+ /** The live device session this question is about; the person's phone then shows the computer's screen. */
13
+ deviceSessionId?: SaltId;
14
+ /** This agent's own account id, when the caller already knows it (saves a `whoAmI` call). */
15
+ selfId?: SaltId;
16
+ }
17
+ /** `GET /api/v1/help_requests` filters. */
18
+ export interface ListHelpRequestsParams {
19
+ state?: "open" | "all";
20
+ chatId?: SaltId;
21
+ /** Return rows older than this help request id. */
22
+ before?: SaltId;
23
+ }
4
24
  export interface IdentityDisclosure {
5
25
  id: string;
6
26
  section_keys: string[];
@@ -314,6 +334,8 @@ export interface SendAttachmentParams {
314
334
  * chatbox.jsx sendOneAttachment uses, so a captionless attachment reads
315
335
  * identically whether it came from a person or an agent. */
316
336
  caption?: string;
337
+ /** A message in this chat to link as the one being answered (`reply_to_message_id`). Not defaulted here. */
338
+ replyTo?: SaltId;
317
339
  }
318
340
  /**
319
341
  * Basename only, control characters and path separators stripped, capped at
@@ -991,6 +1013,7 @@ export declare function createSaltClient(options: SaltClientOptions): {
991
1013
  username: string;
992
1014
  }>, mentions?: SaltId[], opts?: {
993
1015
  quiet?: boolean;
1016
+ replyTo?: SaltId;
994
1017
  }): Promise<PostMessageResult>;
995
1018
  /**
996
1019
  * Sends a file into a chat, encrypted exactly the way salt-fe's
@@ -1024,7 +1047,20 @@ export declare function createSaltClient(options: SaltClientOptions): {
1024
1047
  * which hands the body straight through as ctx.text (ctx.encrypted
1025
1048
  * false) instead of attempting PGP decrypt.
1026
1049
  */
1027
- postPlainMessage(apiKey: string, chatId: SaltId, text: string): Promise<unknown>;
1050
+ postPlainMessage(apiKey: string, chatId: SaltId, text: string, opts?: {
1051
+ replyTo?: SaltId;
1052
+ }): Promise<unknown>;
1053
+ /**
1054
+ * Show Me: ask the person in a 1:1 for help. Posts the question as an ordinary end-to-end
1055
+ * encrypted message (`[[SALT-HELP reason=...]]` + the question, help.ts) and then opens the
1056
+ * help request on Salt (`POST /api/v1/help_requests`), which rings the person's phone and,
1057
+ * with `deviceSessionId`, tells the computer to start sending stills. Returns the help request.
1058
+ * Throws before sending anything when the chat is not an encrypted 1:1, the question is empty
1059
+ * or over 500 characters, or no one in it has a usable key. A 422 from Salt (a rate limit of 6
1060
+ * an hour per chat, or a device session that is not yours) surfaces as a SaltApiError with
1061
+ * the server's sentence, after the question was already posted: the message stays in the chat.
1062
+ */
1063
+ askForHelp(apiKey: string, params: AskForHelpParams): Promise<HelpRequest>;
1028
1064
  /**
1029
1065
  * Open rooms (0.6x): declares what this identity wants delivered from
1030
1066
  * a chat it isn't necessarily addressed in every message of --
@@ -1704,6 +1740,13 @@ export declare function createSaltClient(options: SaltClientOptions): {
1704
1740
  counted: boolean;
1705
1741
  }>;
1706
1742
  };
1743
+ helpRequests: {
1744
+ /** Newest first, at most 50; `state` defaults to `open` on the server. */
1745
+ list(apiKey: string, params?: ListHelpRequestsParams): Promise<HelpRequest[]>;
1746
+ get(apiKey: string, id: SaltId): Promise<HelpRequest>;
1747
+ /** Close a request you asked: `answered` (you have what you need) or `withdrawn`. Stops the computer's stills. */
1748
+ close(apiKey: string, id: SaltId, status: "answered" | "withdrawn"): Promise<HelpRequest>;
1749
+ };
1707
1750
  apps: {
1708
1751
  /**
1709
1752
  * Creates an app + version 1. `forId` builds it for someone else (that
@@ -1803,6 +1846,7 @@ export declare function createSaltClient(options: SaltClientOptions): {
1803
1846
  username: string;
1804
1847
  }>, mentions?: SaltId[], opts?: {
1805
1848
  quiet?: boolean;
1849
+ replyTo?: SaltId;
1806
1850
  }): Promise<PostMessageResult>;
1807
1851
  /**
1808
1852
  * Sends a file into a chat, encrypted exactly the way salt-fe's
@@ -1836,7 +1880,20 @@ export declare function createSaltClient(options: SaltClientOptions): {
1836
1880
  * which hands the body straight through as ctx.text (ctx.encrypted
1837
1881
  * false) instead of attempting PGP decrypt.
1838
1882
  */
1839
- postPlainMessage(apiKey: string, chatId: SaltId, text: string): Promise<unknown>;
1883
+ postPlainMessage(apiKey: string, chatId: SaltId, text: string, opts?: {
1884
+ replyTo?: SaltId;
1885
+ }): Promise<unknown>;
1886
+ /**
1887
+ * Show Me: ask the person in a 1:1 for help. Posts the question as an ordinary end-to-end
1888
+ * encrypted message (`[[SALT-HELP reason=...]]` + the question, help.ts) and then opens the
1889
+ * help request on Salt (`POST /api/v1/help_requests`), which rings the person's phone and,
1890
+ * with `deviceSessionId`, tells the computer to start sending stills. Returns the help request.
1891
+ * Throws before sending anything when the chat is not an encrypted 1:1, the question is empty
1892
+ * or over 500 characters, or no one in it has a usable key. A 422 from Salt (a rate limit of 6
1893
+ * an hour per chat, or a device session that is not yours) surfaces as a SaltApiError with
1894
+ * the server's sentence, after the question was already posted: the message stays in the chat.
1895
+ */
1896
+ askForHelp(apiKey: string, params: AskForHelpParams): Promise<HelpRequest>;
1840
1897
  /**
1841
1898
  * Open rooms (0.6x): declares what this identity wants delivered from
1842
1899
  * a chat it isn't necessarily addressed in every message of --
@@ -2516,6 +2573,13 @@ export declare function createSaltClient(options: SaltClientOptions): {
2516
2573
  counted: boolean;
2517
2574
  }>;
2518
2575
  };
2576
+ helpRequests: {
2577
+ /** Newest first, at most 50; `state` defaults to `open` on the server. */
2578
+ list(apiKey: string, params?: ListHelpRequestsParams): Promise<HelpRequest[]>;
2579
+ get(apiKey: string, id: SaltId): Promise<HelpRequest>;
2580
+ /** Close a request you asked: `answered` (you have what you need) or `withdrawn`. Stops the computer's stills. */
2581
+ close(apiKey: string, id: SaltId, status: "answered" | "withdrawn"): Promise<HelpRequest>;
2582
+ };
2519
2583
  apps: {
2520
2584
  /**
2521
2585
  * Creates an app + version 1. `forId` builds it for someone else (that
@@ -2615,6 +2679,7 @@ export declare function createSaltClient(options: SaltClientOptions): {
2615
2679
  username: string;
2616
2680
  }>, mentions?: SaltId[], opts?: {
2617
2681
  quiet?: boolean;
2682
+ replyTo?: SaltId;
2618
2683
  }): Promise<PostMessageResult>;
2619
2684
  /**
2620
2685
  * Sends a file into a chat, encrypted exactly the way salt-fe's
@@ -2648,7 +2713,20 @@ export declare function createSaltClient(options: SaltClientOptions): {
2648
2713
  * which hands the body straight through as ctx.text (ctx.encrypted
2649
2714
  * false) instead of attempting PGP decrypt.
2650
2715
  */
2651
- postPlainMessage(apiKey: string, chatId: SaltId, text: string): Promise<unknown>;
2716
+ postPlainMessage(apiKey: string, chatId: SaltId, text: string, opts?: {
2717
+ replyTo?: SaltId;
2718
+ }): Promise<unknown>;
2719
+ /**
2720
+ * Show Me: ask the person in a 1:1 for help. Posts the question as an ordinary end-to-end
2721
+ * encrypted message (`[[SALT-HELP reason=...]]` + the question, help.ts) and then opens the
2722
+ * help request on Salt (`POST /api/v1/help_requests`), which rings the person's phone and,
2723
+ * with `deviceSessionId`, tells the computer to start sending stills. Returns the help request.
2724
+ * Throws before sending anything when the chat is not an encrypted 1:1, the question is empty
2725
+ * or over 500 characters, or no one in it has a usable key. A 422 from Salt (a rate limit of 6
2726
+ * an hour per chat, or a device session that is not yours) surfaces as a SaltApiError with
2727
+ * the server's sentence, after the question was already posted: the message stays in the chat.
2728
+ */
2729
+ askForHelp(apiKey: string, params: AskForHelpParams): Promise<HelpRequest>;
2652
2730
  /**
2653
2731
  * Open rooms (0.6x): declares what this identity wants delivered from
2654
2732
  * a chat it isn't necessarily addressed in every message of --
@@ -3328,6 +3406,13 @@ export declare function createSaltClient(options: SaltClientOptions): {
3328
3406
  counted: boolean;
3329
3407
  }>;
3330
3408
  };
3409
+ helpRequests: {
3410
+ /** Newest first, at most 50; `state` defaults to `open` on the server. */
3411
+ list(apiKey: string, params?: ListHelpRequestsParams): Promise<HelpRequest[]>;
3412
+ get(apiKey: string, id: SaltId): Promise<HelpRequest>;
3413
+ /** Close a request you asked: `answered` (you have what you need) or `withdrawn`. Stops the computer's stills. */
3414
+ close(apiKey: string, id: SaltId, status: "answered" | "withdrawn"): Promise<HelpRequest>;
3415
+ };
3331
3416
  apps: {
3332
3417
  /**
3333
3418
  * Creates an app + version 1. `forId` builds it for someone else (that