salt-agent-sdk 0.21.0 → 0.23.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/README.md +26 -3
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +72 -1
- package/dist/actions.js.map +1 -1
- package/dist/callBundle.d.ts +90 -0
- package/dist/callBundle.d.ts.map +1 -0
- package/dist/callBundle.js +199 -0
- package/dist/callBundle.js.map +1 -0
- package/dist/client.d.ts +154 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +139 -1
- package/dist/client.js.map +1 -1
- package/dist/crypto.d.ts +18 -4
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +34 -11
- package/dist/crypto.js.map +1 -1
- package/dist/device/client.d.ts +12 -4
- package/dist/device/client.d.ts.map +1 -1
- package/dist/device/client.js +44 -1
- package/dist/device/client.js.map +1 -1
- package/dist/device/host.d.ts +74 -3
- package/dist/device/host.d.ts.map +1 -1
- package/dist/device/host.js +262 -12
- package/dist/device/host.js.map +1 -1
- package/dist/device/http.d.ts.map +1 -1
- package/dist/device/http.js +3 -2
- package/dist/device/http.js.map +1 -1
- package/dist/device/protocol.d.ts +97 -1
- package/dist/device/protocol.d.ts.map +1 -1
- package/dist/device/protocol.js +155 -1
- package/dist/device/protocol.js.map +1 -1
- package/dist/help.d.ts +54 -0
- package/dist/help.d.ts.map +1 -0
- package/dist/help.js +62 -0
- package/dist/help.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/keyReset.d.ts +117 -0
- package/dist/keyReset.d.ts.map +1 -0
- package/dist/keyReset.js +267 -0
- package/dist/keyReset.js.map +1 -0
- package/dist/webhook.d.ts +80 -2
- package/dist/webhook.d.ts.map +1 -1
- package/dist/webhook.js +130 -17
- package/dist/webhook.js.map +1 -1
- package/dist/work.d.ts +2 -0
- package/dist/work.d.ts.map +1 -1
- package/dist/work.js +4 -0
- package/dist/work.js.map +1 -1
- 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":"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"}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
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 LinkedDeviceRow } from "./keyReset.js";
|
|
5
|
+
import { type HelpReason, type HelpRequest } from "./help.js";
|
|
6
|
+
/** What `client.askForHelp` / `ctx.askForHelp` take. The question rides end-to-end encrypted in the 1:1; Salt learns only the reason. */
|
|
7
|
+
export interface AskForHelpParams {
|
|
8
|
+
/** The 1:1 with the person. Exactly two non-observer members, else the call is refused before anything is sent. */
|
|
9
|
+
chatId: SaltId;
|
|
10
|
+
/** In plain words, at most 500 characters. */
|
|
11
|
+
question: string;
|
|
12
|
+
reason: HelpReason;
|
|
13
|
+
/** The live device session this question is about; the person's phone then shows the computer's screen. */
|
|
14
|
+
deviceSessionId?: SaltId;
|
|
15
|
+
/** This agent's own account id, when the caller already knows it (saves a `whoAmI` call). */
|
|
16
|
+
selfId?: SaltId;
|
|
17
|
+
}
|
|
18
|
+
/** `GET /api/v1/help_requests` filters. */
|
|
19
|
+
export interface ListHelpRequestsParams {
|
|
20
|
+
state?: "open" | "all";
|
|
21
|
+
chatId?: SaltId;
|
|
22
|
+
/** Return rows older than this help request id. */
|
|
23
|
+
before?: SaltId;
|
|
24
|
+
}
|
|
4
25
|
export interface IdentityDisclosure {
|
|
5
26
|
id: string;
|
|
6
27
|
section_keys: string[];
|
|
@@ -913,11 +934,60 @@ export interface PostRepliesPage {
|
|
|
913
934
|
/** The `code` on a refused rule write or rule card (`SaltApiError.code`). Read it instead of parsing the sentence. */
|
|
914
935
|
export type RuleErrorCode = "rules_cap" | "rule_needs_service" | "rule_expired" | "rule_removes_unnamed" | "rule_invalid" | "stale_rules" | "owner_only" | "payer_only";
|
|
915
936
|
export declare const RULE_ERROR_CODES: readonly RuleErrorCode[];
|
|
937
|
+
/** What a dry run of `resetChatKey` answers: what the reset would touch. */
|
|
938
|
+
export interface ChatKeyResetPlan {
|
|
939
|
+
devices: Array<LinkedDeviceRow & Record<string, unknown>>;
|
|
940
|
+
linked_set?: SaltId[];
|
|
941
|
+
chats_affected?: number;
|
|
942
|
+
members_notified?: number;
|
|
943
|
+
current_generation: number;
|
|
944
|
+
vault_version: number;
|
|
945
|
+
agents_to_rewrap?: Array<{
|
|
946
|
+
id: SaltId;
|
|
947
|
+
handle?: string;
|
|
948
|
+
public_fingerprint: string;
|
|
949
|
+
}>;
|
|
950
|
+
retired_generations?: unknown;
|
|
951
|
+
live_location_sessions?: number;
|
|
952
|
+
[key: string]: unknown;
|
|
953
|
+
}
|
|
954
|
+
export interface ResetChatKeyParams {
|
|
955
|
+
/** The owner's login password. The reset needs it (the Devise gate and the vault lock): an explicit owner grant, never a bare token. */
|
|
956
|
+
password: string;
|
|
957
|
+
/** Only when the vault uses a separate lock password. */
|
|
958
|
+
vaultPassword?: string;
|
|
959
|
+
/** Which linked devices keep working (re-sealed): their ids, or "all" (the default). Every other linked device is unlinked. */
|
|
960
|
+
devices?: SaltId[] | "all";
|
|
961
|
+
/** Ask Salt what would change and touch nothing. */
|
|
962
|
+
dryRun?: boolean;
|
|
963
|
+
/** A human session (`Bearer <jwt>`), sent as `Authorization` beside the api-key. The reset is refused for a bare `sld_` token or an agent account. */
|
|
964
|
+
token?: string;
|
|
965
|
+
/** The owner's user id, when `GET /api/v1/account` does not carry it. */
|
|
966
|
+
userId?: SaltId;
|
|
967
|
+
idempotencyKey?: string;
|
|
968
|
+
}
|
|
969
|
+
export interface ResetChatKeyResult {
|
|
970
|
+
dryRun: boolean;
|
|
971
|
+
plan: ChatKeyResetPlan;
|
|
972
|
+
/** Salt's answer to the reset (new key row, `resealed`/`unlinked` ids, a fresh JWT). Absent on a dry run. */
|
|
973
|
+
response?: Record<string, unknown>;
|
|
974
|
+
/** The NEW recovery phrase: show it to the owner once. Salt never receives it. */
|
|
975
|
+
recoveryPhrase?: string;
|
|
976
|
+
newGeneration?: number;
|
|
977
|
+
/** Older generations that could not be opened with the current DEK (history stays unreadable). */
|
|
978
|
+
skippedGenerations?: number[];
|
|
979
|
+
/** Kept devices that held a generation we could not open: not re-sealed (they read "link this computer again"). */
|
|
980
|
+
notResealed?: SaltId[];
|
|
981
|
+
/** Kept devices left out (so unlinked) because their key certification is missing or did not verify. There is no override: link each again by scan. */
|
|
982
|
+
uncertified?: SaltId[];
|
|
983
|
+
}
|
|
916
984
|
export declare class SaltApiError extends Error {
|
|
917
985
|
status: number;
|
|
918
986
|
body: unknown;
|
|
919
987
|
/** Salt's machine-readable refusal code (`{error, code}`), when the body carries one. */
|
|
920
988
|
code?: string;
|
|
989
|
+
/** Seconds to wait, from `Retry-After`, on a 429 (e.g. `too_many_attempts` from the password-gated reset). */
|
|
990
|
+
retryAfterSeconds?: number;
|
|
921
991
|
constructor(method: string, url: string, status: number, body: unknown);
|
|
922
992
|
}
|
|
923
993
|
/** 409 `stale_rules` from `setTeamRules` when you passed a `baseVersion` and the rules changed since. Carries the
|
|
@@ -951,6 +1021,14 @@ export declare function createSaltClient(options: SaltClientOptions): {
|
|
|
951
1021
|
actFor: (principalId: SaltId, opts?: ActForOptions) => { [K in keyof ReturnType<(request: RequestFn) => {
|
|
952
1022
|
/** A chat's members (with public keys), for encrypting a reply to everyone who should read it. */
|
|
953
1023
|
getChatMembers(apiKey: string, chatId: SaltId): Promise<SaltUser[]>;
|
|
1024
|
+
/**
|
|
1025
|
+
* Reset the owner's chat key (`POST /api/v1/userkeys/reset`, KEY_RESET_DESIGN.md sections 4 and 10): a fresh keypair
|
|
1026
|
+
* and messages DEK, old history still readable by the owner, the owner's agent keys rewrapped, and every device in
|
|
1027
|
+
* `devices` re-sealed (v2 payload, dual signature). Devices not named are unlinked. `dryRun` returns what would
|
|
1028
|
+
* change and touches nothing. Needs the owner's login password: see ResetChatKeyParams. The new recovery phrase is
|
|
1029
|
+
* returned once and never sent to Salt.
|
|
1030
|
+
*/
|
|
1031
|
+
resetChatKey(apiKey: string, params: ResetChatKeyParams): Promise<ResetChatKeyResult>;
|
|
954
1032
|
/** Recent messages in a chat this identity is a member of (even as a silent observer). */
|
|
955
1033
|
getChatMessages(apiKey: string, chatId: SaltId): Promise<unknown[]>;
|
|
956
1034
|
/**
|
|
@@ -994,6 +1072,8 @@ export declare function createSaltClient(options: SaltClientOptions): {
|
|
|
994
1072
|
}>, mentions?: SaltId[], opts?: {
|
|
995
1073
|
quiet?: boolean;
|
|
996
1074
|
replyTo?: SaltId;
|
|
1075
|
+
recipientFingerprints?: Record<string, string>;
|
|
1076
|
+
senderFingerprint?: string;
|
|
997
1077
|
}): Promise<PostMessageResult>;
|
|
998
1078
|
/**
|
|
999
1079
|
* Sends a file into a chat, encrypted exactly the way salt-fe's
|
|
@@ -1030,6 +1110,17 @@ export declare function createSaltClient(options: SaltClientOptions): {
|
|
|
1030
1110
|
postPlainMessage(apiKey: string, chatId: SaltId, text: string, opts?: {
|
|
1031
1111
|
replyTo?: SaltId;
|
|
1032
1112
|
}): Promise<unknown>;
|
|
1113
|
+
/**
|
|
1114
|
+
* Show Me: ask the person in a 1:1 for help. Posts the question as an ordinary end-to-end
|
|
1115
|
+
* encrypted message (`[[SALT-HELP reason=...]]` + the question, help.ts) and then opens the
|
|
1116
|
+
* help request on Salt (`POST /api/v1/help_requests`), which rings the person's phone and,
|
|
1117
|
+
* with `deviceSessionId`, tells the computer to start sending stills. Returns the help request.
|
|
1118
|
+
* Throws before sending anything when the chat is not an encrypted 1:1, the question is empty
|
|
1119
|
+
* or over 500 characters, or no one in it has a usable key. A 422 from Salt (a rate limit of 6
|
|
1120
|
+
* an hour per chat, or a device session that is not yours) surfaces as a SaltApiError with
|
|
1121
|
+
* the server's sentence, after the question was already posted: the message stays in the chat.
|
|
1122
|
+
*/
|
|
1123
|
+
askForHelp(apiKey: string, params: AskForHelpParams): Promise<HelpRequest>;
|
|
1033
1124
|
/**
|
|
1034
1125
|
* Open rooms (0.6x): declares what this identity wants delivered from
|
|
1035
1126
|
* a chat it isn't necessarily addressed in every message of --
|
|
@@ -1709,6 +1800,13 @@ export declare function createSaltClient(options: SaltClientOptions): {
|
|
|
1709
1800
|
counted: boolean;
|
|
1710
1801
|
}>;
|
|
1711
1802
|
};
|
|
1803
|
+
helpRequests: {
|
|
1804
|
+
/** Newest first, at most 50; `state` defaults to `open` on the server. */
|
|
1805
|
+
list(apiKey: string, params?: ListHelpRequestsParams): Promise<HelpRequest[]>;
|
|
1806
|
+
get(apiKey: string, id: SaltId): Promise<HelpRequest>;
|
|
1807
|
+
/** Close a request you asked: `answered` (you have what you need) or `withdrawn`. Stops the computer's stills. */
|
|
1808
|
+
close(apiKey: string, id: SaltId, status: "answered" | "withdrawn"): Promise<HelpRequest>;
|
|
1809
|
+
};
|
|
1712
1810
|
apps: {
|
|
1713
1811
|
/**
|
|
1714
1812
|
* Creates an app + version 1. `forId` builds it for someone else (that
|
|
@@ -1766,6 +1864,14 @@ export declare function createSaltClient(options: SaltClientOptions): {
|
|
|
1766
1864
|
}>]: Acted<ReturnType<(request: RequestFn) => {
|
|
1767
1865
|
/** A chat's members (with public keys), for encrypting a reply to everyone who should read it. */
|
|
1768
1866
|
getChatMembers(apiKey: string, chatId: SaltId): Promise<SaltUser[]>;
|
|
1867
|
+
/**
|
|
1868
|
+
* Reset the owner's chat key (`POST /api/v1/userkeys/reset`, KEY_RESET_DESIGN.md sections 4 and 10): a fresh keypair
|
|
1869
|
+
* and messages DEK, old history still readable by the owner, the owner's agent keys rewrapped, and every device in
|
|
1870
|
+
* `devices` re-sealed (v2 payload, dual signature). Devices not named are unlinked. `dryRun` returns what would
|
|
1871
|
+
* change and touches nothing. Needs the owner's login password: see ResetChatKeyParams. The new recovery phrase is
|
|
1872
|
+
* returned once and never sent to Salt.
|
|
1873
|
+
*/
|
|
1874
|
+
resetChatKey(apiKey: string, params: ResetChatKeyParams): Promise<ResetChatKeyResult>;
|
|
1769
1875
|
/** Recent messages in a chat this identity is a member of (even as a silent observer). */
|
|
1770
1876
|
getChatMessages(apiKey: string, chatId: SaltId): Promise<unknown[]>;
|
|
1771
1877
|
/**
|
|
@@ -1809,6 +1915,8 @@ export declare function createSaltClient(options: SaltClientOptions): {
|
|
|
1809
1915
|
}>, mentions?: SaltId[], opts?: {
|
|
1810
1916
|
quiet?: boolean;
|
|
1811
1917
|
replyTo?: SaltId;
|
|
1918
|
+
recipientFingerprints?: Record<string, string>;
|
|
1919
|
+
senderFingerprint?: string;
|
|
1812
1920
|
}): Promise<PostMessageResult>;
|
|
1813
1921
|
/**
|
|
1814
1922
|
* Sends a file into a chat, encrypted exactly the way salt-fe's
|
|
@@ -1845,6 +1953,17 @@ export declare function createSaltClient(options: SaltClientOptions): {
|
|
|
1845
1953
|
postPlainMessage(apiKey: string, chatId: SaltId, text: string, opts?: {
|
|
1846
1954
|
replyTo?: SaltId;
|
|
1847
1955
|
}): Promise<unknown>;
|
|
1956
|
+
/**
|
|
1957
|
+
* Show Me: ask the person in a 1:1 for help. Posts the question as an ordinary end-to-end
|
|
1958
|
+
* encrypted message (`[[SALT-HELP reason=...]]` + the question, help.ts) and then opens the
|
|
1959
|
+
* help request on Salt (`POST /api/v1/help_requests`), which rings the person's phone and,
|
|
1960
|
+
* with `deviceSessionId`, tells the computer to start sending stills. Returns the help request.
|
|
1961
|
+
* Throws before sending anything when the chat is not an encrypted 1:1, the question is empty
|
|
1962
|
+
* or over 500 characters, or no one in it has a usable key. A 422 from Salt (a rate limit of 6
|
|
1963
|
+
* an hour per chat, or a device session that is not yours) surfaces as a SaltApiError with
|
|
1964
|
+
* the server's sentence, after the question was already posted: the message stays in the chat.
|
|
1965
|
+
*/
|
|
1966
|
+
askForHelp(apiKey: string, params: AskForHelpParams): Promise<HelpRequest>;
|
|
1848
1967
|
/**
|
|
1849
1968
|
* Open rooms (0.6x): declares what this identity wants delivered from
|
|
1850
1969
|
* a chat it isn't necessarily addressed in every message of --
|
|
@@ -2524,6 +2643,13 @@ export declare function createSaltClient(options: SaltClientOptions): {
|
|
|
2524
2643
|
counted: boolean;
|
|
2525
2644
|
}>;
|
|
2526
2645
|
};
|
|
2646
|
+
helpRequests: {
|
|
2647
|
+
/** Newest first, at most 50; `state` defaults to `open` on the server. */
|
|
2648
|
+
list(apiKey: string, params?: ListHelpRequestsParams): Promise<HelpRequest[]>;
|
|
2649
|
+
get(apiKey: string, id: SaltId): Promise<HelpRequest>;
|
|
2650
|
+
/** Close a request you asked: `answered` (you have what you need) or `withdrawn`. Stops the computer's stills. */
|
|
2651
|
+
close(apiKey: string, id: SaltId, status: "answered" | "withdrawn"): Promise<HelpRequest>;
|
|
2652
|
+
};
|
|
2527
2653
|
apps: {
|
|
2528
2654
|
/**
|
|
2529
2655
|
* Creates an app + version 1. `forId` builds it for someone else (that
|
|
@@ -2581,6 +2707,14 @@ export declare function createSaltClient(options: SaltClientOptions): {
|
|
|
2581
2707
|
}>[K]>; };
|
|
2582
2708
|
/** A chat's members (with public keys), for encrypting a reply to everyone who should read it. */
|
|
2583
2709
|
getChatMembers(apiKey: string, chatId: SaltId): Promise<SaltUser[]>;
|
|
2710
|
+
/**
|
|
2711
|
+
* Reset the owner's chat key (`POST /api/v1/userkeys/reset`, KEY_RESET_DESIGN.md sections 4 and 10): a fresh keypair
|
|
2712
|
+
* and messages DEK, old history still readable by the owner, the owner's agent keys rewrapped, and every device in
|
|
2713
|
+
* `devices` re-sealed (v2 payload, dual signature). Devices not named are unlinked. `dryRun` returns what would
|
|
2714
|
+
* change and touches nothing. Needs the owner's login password: see ResetChatKeyParams. The new recovery phrase is
|
|
2715
|
+
* returned once and never sent to Salt.
|
|
2716
|
+
*/
|
|
2717
|
+
resetChatKey(apiKey: string, params: ResetChatKeyParams): Promise<ResetChatKeyResult>;
|
|
2584
2718
|
/** Recent messages in a chat this identity is a member of (even as a silent observer). */
|
|
2585
2719
|
getChatMessages(apiKey: string, chatId: SaltId): Promise<unknown[]>;
|
|
2586
2720
|
/**
|
|
@@ -2624,6 +2758,8 @@ export declare function createSaltClient(options: SaltClientOptions): {
|
|
|
2624
2758
|
}>, mentions?: SaltId[], opts?: {
|
|
2625
2759
|
quiet?: boolean;
|
|
2626
2760
|
replyTo?: SaltId;
|
|
2761
|
+
recipientFingerprints?: Record<string, string>;
|
|
2762
|
+
senderFingerprint?: string;
|
|
2627
2763
|
}): Promise<PostMessageResult>;
|
|
2628
2764
|
/**
|
|
2629
2765
|
* Sends a file into a chat, encrypted exactly the way salt-fe's
|
|
@@ -2660,6 +2796,17 @@ export declare function createSaltClient(options: SaltClientOptions): {
|
|
|
2660
2796
|
postPlainMessage(apiKey: string, chatId: SaltId, text: string, opts?: {
|
|
2661
2797
|
replyTo?: SaltId;
|
|
2662
2798
|
}): Promise<unknown>;
|
|
2799
|
+
/**
|
|
2800
|
+
* Show Me: ask the person in a 1:1 for help. Posts the question as an ordinary end-to-end
|
|
2801
|
+
* encrypted message (`[[SALT-HELP reason=...]]` + the question, help.ts) and then opens the
|
|
2802
|
+
* help request on Salt (`POST /api/v1/help_requests`), which rings the person's phone and,
|
|
2803
|
+
* with `deviceSessionId`, tells the computer to start sending stills. Returns the help request.
|
|
2804
|
+
* Throws before sending anything when the chat is not an encrypted 1:1, the question is empty
|
|
2805
|
+
* or over 500 characters, or no one in it has a usable key. A 422 from Salt (a rate limit of 6
|
|
2806
|
+
* an hour per chat, or a device session that is not yours) surfaces as a SaltApiError with
|
|
2807
|
+
* the server's sentence, after the question was already posted: the message stays in the chat.
|
|
2808
|
+
*/
|
|
2809
|
+
askForHelp(apiKey: string, params: AskForHelpParams): Promise<HelpRequest>;
|
|
2663
2810
|
/**
|
|
2664
2811
|
* Open rooms (0.6x): declares what this identity wants delivered from
|
|
2665
2812
|
* a chat it isn't necessarily addressed in every message of --
|
|
@@ -3339,6 +3486,13 @@ export declare function createSaltClient(options: SaltClientOptions): {
|
|
|
3339
3486
|
counted: boolean;
|
|
3340
3487
|
}>;
|
|
3341
3488
|
};
|
|
3489
|
+
helpRequests: {
|
|
3490
|
+
/** Newest first, at most 50; `state` defaults to `open` on the server. */
|
|
3491
|
+
list(apiKey: string, params?: ListHelpRequestsParams): Promise<HelpRequest[]>;
|
|
3492
|
+
get(apiKey: string, id: SaltId): Promise<HelpRequest>;
|
|
3493
|
+
/** Close a request you asked: `answered` (you have what you need) or `withdrawn`. Stops the computer's stills. */
|
|
3494
|
+
close(apiKey: string, id: SaltId, status: "answered" | "withdrawn"): Promise<HelpRequest>;
|
|
3495
|
+
};
|
|
3342
3496
|
apps: {
|
|
3343
3497
|
/**
|
|
3344
3498
|
* Creates an app + version 1. `forId` builds it for someone else (that
|