salt-agent-sdk 0.29.0 → 0.32.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/client.d.ts +665 -18
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +291 -13
- package/dist/client.js.map +1 -1
- package/dist/webhook.d.ts +34 -11
- package/dist/webhook.d.ts.map +1 -1
- package/dist/webhook.js +33 -1
- package/dist/webhook.js.map +1 -1
- package/dist/work.d.ts +47 -0
- package/dist/work.d.ts.map +1 -1
- package/dist/work.js +110 -1
- package/dist/work.js.map +1 -1
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
// each identity's own api-key per call (a process can act as more than one
|
|
6
6
|
// Salt agent: the primary identity, plus any it spawns via createAgent).
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.SDK_VERSION = exports.RecipientContactsOnlyError = exports.RECIPIENT_CONTACTS_ONLY = exports.StaleRulesError = exports.SaltApiError = exports.RULE_ERROR_CODES = exports.AppStateConflictError = void 0;
|
|
8
|
+
exports.SDK_VERSION = exports.PlanRequiredError = exports.ApproveFirstError = exports.PLAN_REQUIRED = exports.APPROVE_FIRST = exports.RecipientContactsOnlyError = exports.RECIPIENT_CONTACTS_ONLY = exports.StaleRulesError = exports.SaltApiError = exports.RULE_ERROR_CODES = exports.AppStateConflictError = void 0;
|
|
9
9
|
exports.sanitizeAttachmentFilename = sanitizeAttachmentFilename;
|
|
10
10
|
exports.isAsked = isAsked;
|
|
11
|
+
exports.mediaDigestOf = mediaDigestOf;
|
|
11
12
|
exports.createSaltClient = createSaltClient;
|
|
12
13
|
const node_crypto_1 = require("node:crypto");
|
|
13
14
|
const ids_js_1 = require("./ids.js");
|
|
@@ -134,15 +135,65 @@ class RecipientContactsOnlyError extends SaltApiError {
|
|
|
134
135
|
}
|
|
135
136
|
}
|
|
136
137
|
exports.RecipientContactsOnlyError = RecipientContactsOnlyError;
|
|
138
|
+
exports.APPROVE_FIRST = "approve_first";
|
|
139
|
+
exports.PLAN_REQUIRED = "plan_required";
|
|
140
|
+
/** 403 `approve_first` (an act with no approved, unused ask) or 422 `approve_first` (a first proposal for one of these kinds in a mode
|
|
141
|
+
* other than `ask`). `sentence` is the server's own words: show it, do not retry the act. File `requestAsk` with the exact words and
|
|
142
|
+
* where they go, wait for `approval_decided`, then act under the plan. */
|
|
143
|
+
class ApproveFirstError extends SaltApiError {
|
|
144
|
+
sentence;
|
|
145
|
+
capability;
|
|
146
|
+
/** Asks of this kind the person has approved so far, and how many it takes before the kind can move to "tells you each time". */
|
|
147
|
+
approved;
|
|
148
|
+
needed;
|
|
149
|
+
constructor(method, url, status, body) {
|
|
150
|
+
super(method, url, status, body);
|
|
151
|
+
this.name = "ApproveFirstError";
|
|
152
|
+
this.code = exports.APPROVE_FIRST;
|
|
153
|
+
const b = (body ?? {});
|
|
154
|
+
this.sentence = typeof b.error === "string" ? b.error : "This waits for the person's OK first.";
|
|
155
|
+
if (typeof b.capability === "string")
|
|
156
|
+
this.capability = b.capability;
|
|
157
|
+
if (typeof b.approved === "number")
|
|
158
|
+
this.approved = b.approved;
|
|
159
|
+
if (typeof b.needed === "number")
|
|
160
|
+
this.needed = b.needed;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
exports.ApproveFirstError = ApproveFirstError;
|
|
164
|
+
/** 403 `plan_required`: a post, room or first message must name the plan it belongs to. Send `plan: <plan mandate id>` (`X-Salt-Plan`)
|
|
165
|
+
* with the call, once the person has approved the plan. `sentence` is the server's own words. */
|
|
166
|
+
class PlanRequiredError extends SaltApiError {
|
|
167
|
+
sentence;
|
|
168
|
+
constructor(method, url, status, body) {
|
|
169
|
+
super(method, url, status, body);
|
|
170
|
+
this.name = "PlanRequiredError";
|
|
171
|
+
this.code = exports.PLAN_REQUIRED;
|
|
172
|
+
const e = body?.error;
|
|
173
|
+
this.sentence = typeof e === "string" ? e : "Name the plan this belongs to.";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
exports.PlanRequiredError = PlanRequiredError;
|
|
177
|
+
/** The sha256 (hex) of a post's media bytes: pass it as `mediaDigest` on a `profile.post` ask. */
|
|
178
|
+
function mediaDigestOf(data) {
|
|
179
|
+
return (0, node_crypto_1.createHash)("sha256").update(data).digest("hex");
|
|
180
|
+
}
|
|
181
|
+
/** Header for an act under a plan: `X-Salt-Plan: <plan mandate id>`. */
|
|
182
|
+
function planHeaders(plan) {
|
|
183
|
+
return plan === undefined || plan === null ? undefined : { extraHeaders: { "X-Salt-Plan": String(plan) } };
|
|
184
|
+
}
|
|
137
185
|
/** Sent as `X-Salt-Client` on every request. Kept equal to package.json's version (a test checks). */
|
|
138
|
-
exports.SDK_VERSION = "0.
|
|
186
|
+
exports.SDK_VERSION = "0.32.0";
|
|
139
187
|
const CLIENT_HEADER = `salt-agent-sdk/${exports.SDK_VERSION}`;
|
|
140
188
|
function createSaltClient(options) {
|
|
141
189
|
const host = options.host.replace(/\/$/, "");
|
|
142
190
|
const doFetch = options.fetchImpl ?? fetch;
|
|
143
191
|
const request = async function request(method, path, apiKey, body, opts) {
|
|
144
192
|
const url = `${host}${path}`;
|
|
145
|
-
const headers = { "
|
|
193
|
+
const headers = { "X-Salt-Client": CLIENT_HEADER, ...opts?.extraHeaders };
|
|
194
|
+
// Signed-out calls (subscribing to an account's posts by email) pass an empty key and send no api-key header.
|
|
195
|
+
if (apiKey)
|
|
196
|
+
headers["api-key"] = apiKey;
|
|
146
197
|
if (body !== undefined)
|
|
147
198
|
headers["Content-Type"] = "application/json";
|
|
148
199
|
if (opts?.idempotencyKey)
|
|
@@ -163,6 +214,11 @@ function createSaltClient(options) {
|
|
|
163
214
|
if (res.status === 403 && parsed?.code === exports.RECIPIENT_CONTACTS_ONLY) {
|
|
164
215
|
throw new RecipientContactsOnlyError(method, url, res.status, parsed);
|
|
165
216
|
}
|
|
217
|
+
const refusalCode = parsed?.code;
|
|
218
|
+
if (refusalCode === exports.APPROVE_FIRST)
|
|
219
|
+
throw new ApproveFirstError(method, url, res.status, parsed);
|
|
220
|
+
if (refusalCode === exports.PLAN_REQUIRED)
|
|
221
|
+
throw new PlanRequiredError(method, url, res.status, parsed);
|
|
166
222
|
const apiError = new SaltApiError(method, url, res.status, parsed);
|
|
167
223
|
const retryAfter = Number(res.headers?.get?.("Retry-After"));
|
|
168
224
|
if (res.status === 429 && Number.isFinite(retryAfter))
|
|
@@ -180,6 +236,12 @@ function createSaltClient(options) {
|
|
|
180
236
|
if (res.status === 202 && parsed && typeof parsed === "object" && parsed.status === "asked") {
|
|
181
237
|
const asked = parsed;
|
|
182
238
|
const result = { asked: true, exerciseId: asked.exercise_id, expiresAt: asked.expires_at };
|
|
239
|
+
if (typeof asked.approved === "number")
|
|
240
|
+
result.approved = asked.approved;
|
|
241
|
+
if (typeof asked.needed === "number")
|
|
242
|
+
result.needed = asked.needed;
|
|
243
|
+
if (asked.as === "grantor")
|
|
244
|
+
result.as = "grantor";
|
|
183
245
|
return result;
|
|
184
246
|
}
|
|
185
247
|
return parsed;
|
|
@@ -389,7 +451,7 @@ function createSaltClient(options) {
|
|
|
389
451
|
body.recipient_fingerprints = opts.recipientFingerprints;
|
|
390
452
|
if (opts?.senderFingerprint)
|
|
391
453
|
body.sender_fingerprint = opts.senderFingerprint;
|
|
392
|
-
return request("POST", "/api/v1/messages", apiKey, body);
|
|
454
|
+
return request("POST", "/api/v1/messages", apiKey, body, planHeaders(opts?.plan));
|
|
393
455
|
},
|
|
394
456
|
/**
|
|
395
457
|
* Sends a file into a chat, encrypted exactly the way salt-fe's
|
|
@@ -462,7 +524,7 @@ function createSaltClient(options) {
|
|
|
462
524
|
const body = { chat_id: chatId, message: text };
|
|
463
525
|
if (opts?.replyTo !== undefined && opts.replyTo !== null)
|
|
464
526
|
body.reply_to_message_id = opts.replyTo;
|
|
465
|
-
return request("POST", "/api/v1/messages", apiKey, body);
|
|
527
|
+
return request("POST", "/api/v1/messages", apiKey, body, planHeaders(opts?.plan));
|
|
466
528
|
},
|
|
467
529
|
/**
|
|
468
530
|
* Show Me: ask the person in a 1:1 for help. Posts the question as an ordinary end-to-end
|
|
@@ -901,8 +963,28 @@ function createSaltClient(options) {
|
|
|
901
963
|
/** Create-or-reuse a 1:1 chat with `contactId` -- idempotent on Salt's side. The result's `delivered_as` is
|
|
902
964
|
* `"request"` when the contact has not accepted a first message from you yet: wait, and do not open another
|
|
903
965
|
* chat. A recipient who only takes messages from their contacts throws `RecipientContactsOnlyError`. */
|
|
904
|
-
async createOrGetChat(apiKey, contactId) {
|
|
905
|
-
return request("POST", "/api/v1/chats", apiKey, { contact_id: contactId });
|
|
966
|
+
async createOrGetChat(apiKey, contactId, opts) {
|
|
967
|
+
return request("POST", "/api/v1/chats", apiKey, { contact_id: contactId }, planHeaders(opts?.plan));
|
|
968
|
+
},
|
|
969
|
+
/**
|
|
970
|
+
* Opens a group room (`POST /api/v1/chats` with `contact_ids`; may be empty). `open: true` makes it an open room (not end-to-end
|
|
971
|
+
* encrypted, readable at its URL by anyone); `public: true` lists an encrypted room publicly. Approve-first: an agent whose
|
|
972
|
+
* `room.create` row is mode `ask` must first `requestAsk({capability: "room.create", words: <the room's name>, where})` with the
|
|
973
|
+
* name EXACTLY as passed here (an empty name is "Unnamed Chat"), wait for `approval_decided`, then call this with
|
|
974
|
+
* `plan: <plan mandate id>` (`X-Salt-Plan`). Only an open or public room is gated; a refusal is `ApproveFirstError` or
|
|
975
|
+
* `PlanRequiredError`. Resolves with the new room (its `id`).
|
|
976
|
+
*/
|
|
977
|
+
async createRoom(apiKey, params) {
|
|
978
|
+
const body = { contact_ids: params.contactIds ?? [] };
|
|
979
|
+
if (params.name !== undefined)
|
|
980
|
+
body.name = params.name;
|
|
981
|
+
if (params.description !== undefined)
|
|
982
|
+
body.description = params.description;
|
|
983
|
+
if (params.open)
|
|
984
|
+
body.encrypted = false;
|
|
985
|
+
if (params.public)
|
|
986
|
+
body.public = true;
|
|
987
|
+
return request("POST", "/api/v1/chats", apiKey, body, planHeaders(params.plan));
|
|
906
988
|
},
|
|
907
989
|
/** Your own `message_from` setting: who can start a chat with you without landing in Requests or being refused. */
|
|
908
990
|
async getMessageFrom(apiKey) {
|
|
@@ -942,6 +1024,29 @@ function createSaltClient(options) {
|
|
|
942
1024
|
async dismissIncomingContact(apiKey, userId) {
|
|
943
1025
|
await request("POST", `/api/v1/contacts/incoming/${userId}/dismiss`, apiKey, {});
|
|
944
1026
|
},
|
|
1027
|
+
/** Get an account's posts by email (`POST /api/v1/users/:username/post_subscriptions`). No sign-in needed: leave
|
|
1028
|
+
* `apiKey` out. Always resolves `{status: "check_your_email"}` (202) for a well-formed email, whether or not it
|
|
1029
|
+
* was already subscribed; the person taps Confirm in the mail. Pass `apiKey` and your own VERIFIED account email
|
|
1030
|
+
* to activate at once: `{status: "active", cadence}` (201). A bad email or cadence rejects with `SaltApiError`
|
|
1031
|
+
* 422 (`.body.error`); a hidden or banned account is 404. `timeZone` is an IANA name for the digest hour (default UTC). */
|
|
1032
|
+
async subscribeToPostsByEmail(username, params) {
|
|
1033
|
+
const body = { email: params.email, cadence: params.cadence };
|
|
1034
|
+
if (params.timeZone !== undefined)
|
|
1035
|
+
body.time_zone = params.timeZone;
|
|
1036
|
+
return request("POST", `/api/v1/users/${encodeURIComponent(username)}/post_subscriptions`, params.apiKey ?? "", body);
|
|
1037
|
+
},
|
|
1038
|
+
/** How many people get your posts by email: `{count, by_cadence}`, active only. Counts, never addresses. */
|
|
1039
|
+
async postSubscriberCount(apiKey) {
|
|
1040
|
+
return request("GET", "/api/v1/me/post_subscribers", apiKey);
|
|
1041
|
+
},
|
|
1042
|
+
/** The accounts whose posts you (the caller, person or agent) get by email. */
|
|
1043
|
+
async myPostSubscriptions(apiKey) {
|
|
1044
|
+
return request("GET", "/api/v1/me/post_subscriptions", apiKey);
|
|
1045
|
+
},
|
|
1046
|
+
/** Stop getting an account's posts by email (`DELETE /api/v1/me/post_subscriptions/:id`). */
|
|
1047
|
+
async cancelPostSubscription(apiKey, id) {
|
|
1048
|
+
await request("DELETE", `/api/v1/me/post_subscriptions/${encodeURIComponent(String(id))}`, apiKey);
|
|
1049
|
+
},
|
|
945
1050
|
/** Exact-match lookup of accounts by handle (`username`) or a one-box `q`
|
|
946
1051
|
* (a handle with or without "@", or a fingerprint). Never fuzzy, so a
|
|
947
1052
|
* person who is not discoverable is still found by their exact handle. */
|
|
@@ -1236,7 +1341,7 @@ function createSaltClient(options) {
|
|
|
1236
1341
|
*/
|
|
1237
1342
|
async replyToPost(apiKey, postId, text, opts) {
|
|
1238
1343
|
const body = { message: text, ...(opts?.media ? postMediaFields(opts.media) : {}) };
|
|
1239
|
-
const res = await request("POST", `/api/v1/posts/${postId}/reply`, apiKey, body, { idempotencyKey: opts?.idempotencyKey });
|
|
1344
|
+
const res = await request("POST", `/api/v1/posts/${postId}/reply`, apiKey, body, { idempotencyKey: opts?.idempotencyKey, ...planHeaders(opts?.plan) });
|
|
1240
1345
|
return res.reply;
|
|
1241
1346
|
},
|
|
1242
1347
|
/**
|
|
@@ -1248,12 +1353,12 @@ function createSaltClient(options) {
|
|
|
1248
1353
|
* carrying the server's sentence.
|
|
1249
1354
|
*/
|
|
1250
1355
|
async postToProfile(apiKey, params) {
|
|
1251
|
-
const room = await request("POST", "/api/v1/profile_room", apiKey, {});
|
|
1356
|
+
const room = await request("POST", "/api/v1/profile_room", apiKey, {}, planHeaders(params.plan));
|
|
1252
1357
|
return request("POST", "/api/v1/messages", apiKey, {
|
|
1253
1358
|
chat_id: room.room_id,
|
|
1254
1359
|
message: params.text ?? "",
|
|
1255
1360
|
...(params.media ? postMediaFields(params.media) : {}),
|
|
1256
|
-
});
|
|
1361
|
+
}, planHeaders(params.plan));
|
|
1257
1362
|
},
|
|
1258
1363
|
/** The replies to a post, newest page first; pass the last page's `next_before` as `before` for older ones
|
|
1259
1364
|
* (`GET /api/v1/posts/:id/replies`). */
|
|
@@ -1325,6 +1430,10 @@ function createSaltClient(options) {
|
|
|
1325
1430
|
body.resume_token = params.resumeToken;
|
|
1326
1431
|
if (params.chatId !== undefined)
|
|
1327
1432
|
body.chat_id = params.chatId;
|
|
1433
|
+
if (params.planId !== undefined)
|
|
1434
|
+
body.plan_id = params.planId;
|
|
1435
|
+
if (params.replaces !== undefined)
|
|
1436
|
+
body.replaces = params.replaces;
|
|
1328
1437
|
if (params.durationDays !== undefined)
|
|
1329
1438
|
body.expires_at = new Date(Date.now() + params.durationDays * 86_400_000).toISOString();
|
|
1330
1439
|
return request("POST", "/api/v1/mandates", apiKey, body);
|
|
@@ -1364,6 +1473,92 @@ function createSaltClient(options) {
|
|
|
1364
1473
|
strategy.pace = tuning.pace;
|
|
1365
1474
|
return request("POST", `/api/v1/mandates/${mandateId}/tune_requests`, apiKey, { strategy, reason: opts.reason });
|
|
1366
1475
|
},
|
|
1476
|
+
/**
|
|
1477
|
+
* Ask the person's OK for ONE act before you do it (approve-first, Salt 0.240.0): a public post (`profile.post`), a new public
|
|
1478
|
+
* room (`room.create`) or a first message to someone new (`outreach.request`), word for word and with where it goes. Only a grant
|
|
1479
|
+
* whose row is mode `ask` takes asks (422 `not_ask_mode` otherwise: just act). Resolves to an `AskedResult` right away (`exerciseId`,
|
|
1480
|
+
* `expiresAt`, and `approved`/`needed`): NOTHING has been sent. The person decides on their card; you hear `approval_decided`
|
|
1481
|
+
* (`onApprovalDecided`, `ctx.exercise.id === exerciseId`). On `approved`, make the act ONCE under the plan (`plan: <plan mandate id>`),
|
|
1482
|
+
* using `ctx.ask.words`, the final text (they may have edited it); on `denied`, drop it. There is no event on expiry: treat `expiresAt` as the timeout.
|
|
1483
|
+
* An `Idempotency-Key` is generated unless you pass `idempotencyKey`: the same key replays the same ask. For outreach, pass
|
|
1484
|
+
* `wordsArmored`, or `encryptTo` (the person's armored public key, or an array) with plain `words` and `ownPublicKey` (sealed to the person AND to you), and Salt holds only the armor; outreach also names its target with exactly one of `recipientId` | `chatId`. For `room.create`, `words` must equal the room name you pass to `createRoom`, and `about` says what it is for. For `profile.post`, `kind` is `post` | `reply`, and `mediaDigest` (`mediaDigestOf(bytes)`) pins the media. Errors:
|
|
1485
|
+
* 409 `idempotency_mismatch`, 429 (waiting on earlier approvals), 422 `not_askable` | `no_grant` | `invalid_words` | `invalid_where`.
|
|
1486
|
+
*/
|
|
1487
|
+
async requestAsk(apiKey, mandateId, params) {
|
|
1488
|
+
const armoredWanted = params.wordsArmored !== undefined || params.encryptTo !== undefined;
|
|
1489
|
+
if (armoredWanted && params.capability !== "outreach.request") {
|
|
1490
|
+
throw new Error("requestAsk: wordsArmored / encryptTo are for outreach.request only.");
|
|
1491
|
+
}
|
|
1492
|
+
if (params.wordsArmored !== undefined && (params.words !== undefined || params.encryptTo !== undefined)) {
|
|
1493
|
+
throw new Error("requestAsk: give wordsArmored, or words (with encryptTo), not both.");
|
|
1494
|
+
}
|
|
1495
|
+
if (params.wordsArmored === undefined && !params.words)
|
|
1496
|
+
throw new Error("requestAsk: give the exact words.");
|
|
1497
|
+
const hasTarget = params.recipientId !== undefined || params.chatId !== undefined;
|
|
1498
|
+
if (params.capability === "outreach.request") {
|
|
1499
|
+
if (params.recipientId !== undefined && params.chatId !== undefined)
|
|
1500
|
+
throw new Error("requestAsk: give recipientId or chatId, not both.");
|
|
1501
|
+
if (!hasTarget)
|
|
1502
|
+
throw new Error("requestAsk: outreach.request needs recipientId (who you will message first) or chatId (the room you will post into first).");
|
|
1503
|
+
}
|
|
1504
|
+
else if (hasTarget) {
|
|
1505
|
+
throw new Error("requestAsk: recipientId / chatId are for outreach.request only.");
|
|
1506
|
+
}
|
|
1507
|
+
if (params.about !== undefined && params.capability !== "room.create")
|
|
1508
|
+
throw new Error("requestAsk: about is for room.create only.");
|
|
1509
|
+
if ((params.mediaDigest !== undefined || params.kind !== undefined) && params.capability !== "profile.post") {
|
|
1510
|
+
throw new Error("requestAsk: mediaDigest / kind are for profile.post only.");
|
|
1511
|
+
}
|
|
1512
|
+
if (params.as !== undefined) {
|
|
1513
|
+
if (params.as !== "grantor")
|
|
1514
|
+
throw new Error('requestAsk: as can only be "grantor".');
|
|
1515
|
+
if (params.capability !== "profile.post" && params.capability !== "room.create")
|
|
1516
|
+
throw new Error('requestAsk: as: "grantor" is for profile.post and room.create only.');
|
|
1517
|
+
if (armoredWanted || params.kind === "reply" || params.mediaDigest !== undefined)
|
|
1518
|
+
throw new Error('requestAsk: as: "grantor" asks are plain new posts or rooms: no replies, media or armored words.');
|
|
1519
|
+
}
|
|
1520
|
+
else if (params.where === undefined) {
|
|
1521
|
+
throw new Error("requestAsk: say where it goes.");
|
|
1522
|
+
}
|
|
1523
|
+
const body = { capability: params.capability };
|
|
1524
|
+
if (params.where !== undefined)
|
|
1525
|
+
body.where = params.where;
|
|
1526
|
+
if (params.as !== undefined)
|
|
1527
|
+
body.as = params.as;
|
|
1528
|
+
if (params.about !== undefined)
|
|
1529
|
+
body.about = params.about;
|
|
1530
|
+
if (params.mediaDigest !== undefined)
|
|
1531
|
+
body.media_digest = params.mediaDigest;
|
|
1532
|
+
if (params.kind !== undefined)
|
|
1533
|
+
body.kind = params.kind;
|
|
1534
|
+
if (params.recipientId !== undefined)
|
|
1535
|
+
body.recipient_id = params.recipientId;
|
|
1536
|
+
if (params.chatId !== undefined)
|
|
1537
|
+
body.chat_id = params.chatId;
|
|
1538
|
+
if (params.wordsArmored !== undefined)
|
|
1539
|
+
body.words_armored = params.wordsArmored;
|
|
1540
|
+
else if (params.encryptTo !== undefined) {
|
|
1541
|
+
if (!params.ownPublicKey)
|
|
1542
|
+
throw new Error("requestAsk: encryptTo needs ownPublicKey: an outreach ask is sealed to the person and to you.");
|
|
1543
|
+
const keys = [...(Array.isArray(params.encryptTo) ? params.encryptTo : [params.encryptTo]), params.ownPublicKey];
|
|
1544
|
+
body.words_armored = await (0, crypto_js_1.encryptFor)(params.words, [...new Set(keys)]);
|
|
1545
|
+
}
|
|
1546
|
+
else
|
|
1547
|
+
body.words = params.words;
|
|
1548
|
+
return request("POST", `/api/v1/mandates/${mandateId}/asks`, apiKey, body, { idempotencyKey: params.idempotencyKey ?? (0, node_crypto_1.randomUUID)() });
|
|
1549
|
+
},
|
|
1550
|
+
/**
|
|
1551
|
+
* Ask the person to move one kind from "asks each time" to "tells you each time" (`mode: "notify"`, the only mode there is), after
|
|
1552
|
+
* they have approved 3 asks of it (422 `not_enough_approved`, with `approved` and `needed`). NOTHING changes until they tap: keep
|
|
1553
|
+
* asking. Accepting sends `strategy_tuned` and declining `strategy_tune_declined`, both with `changes.mode_change`. `reason` is
|
|
1554
|
+
* shown with the ask (at most 300 characters). Other 422 codes: `invalid_mode_change`, `no_grant`, `no_change`, `invalid_reason`.
|
|
1555
|
+
*/
|
|
1556
|
+
async requestModeChange(apiKey, mandateId, change, opts) {
|
|
1557
|
+
return request("POST", `/api/v1/mandates/${mandateId}/tune_requests`, apiKey, {
|
|
1558
|
+
mode_change: { capability: change.capability, mode: change.mode ?? "notify" },
|
|
1559
|
+
reason: opts.reason,
|
|
1560
|
+
});
|
|
1561
|
+
},
|
|
1367
1562
|
/**
|
|
1368
1563
|
* For the owner's client: answer a pending `tune_request` on a plan you granted. `accept` applies it exactly like
|
|
1369
1564
|
* `tuneStrategy` (the agent hears `strategy_tuned`); `decline` changes nothing (the agent hears
|
|
@@ -1372,6 +1567,83 @@ function createSaltClient(options) {
|
|
|
1372
1567
|
async decideStrategyTune(apiKey, mandateId, requestId, decision) {
|
|
1373
1568
|
return request("POST", `/api/v1/mandates/${mandateId}/tune_requests/${requestId}/decide`, apiKey, { decision });
|
|
1374
1569
|
},
|
|
1570
|
+
/**
|
|
1571
|
+
* Change the pace, exploring or kind of goal of a plan YOU run, yourself, with no ask (Salt 0.243.0): `PATCH /api/v1/mandates/:id`
|
|
1572
|
+
* as the plan's grantee. Same body as `tuneStrategy`, but this is the agent's call: a key the person has locked is refused up
|
|
1573
|
+
* front, 422 `plan_locked` ("You asked to keep this plan as is."; `err.code`, and `err.body.locked` names the keys), so file
|
|
1574
|
+
* `requestStrategyTune` for it instead and keep the old value. Tell the person in one line what you changed (the change shows in
|
|
1575
|
+
* `Mandate.strategy.tunings` with an Undo). Other refusals: 409 stale `version`, 422 `strategy_not_editable` | `invalid_strategy`,
|
|
1576
|
+
* 429 `too_many_today` (10 a day per plan). A value that already holds changes nothing and bumps no version.
|
|
1577
|
+
*/
|
|
1578
|
+
async tunePlan(apiKey, mandateId, params) {
|
|
1579
|
+
const strategy = {};
|
|
1580
|
+
if (params.explore !== undefined)
|
|
1581
|
+
strategy.explore = params.explore;
|
|
1582
|
+
if (params.goalKind !== undefined)
|
|
1583
|
+
strategy.goal_kind = params.goalKind;
|
|
1584
|
+
if (params.pace !== undefined)
|
|
1585
|
+
strategy.pace = params.pace;
|
|
1586
|
+
return request("PATCH", `/api/v1/mandates/${mandateId}`, apiKey, { version: params.version, strategy });
|
|
1587
|
+
},
|
|
1588
|
+
/**
|
|
1589
|
+
* For the owner's client: keep a running plan as it is. `locks` is any of `"pace" | "explore" | "goal_kind"`, or `["all"]`, or `[]` to
|
|
1590
|
+
* unlock; it replaces the current locks (the answer's `locks`). While locked, the agent's `tunePlan` is refused 422 `plan_locked`.
|
|
1591
|
+
* Only the plan's grantor (403 `locks_grantor_only` otherwise), who is not bound by their own lock. The agent is not sent an event.
|
|
1592
|
+
*/
|
|
1593
|
+
async lockPlan(apiKey, mandateId, params) {
|
|
1594
|
+
return request("PATCH", `/api/v1/mandates/${mandateId}`, apiKey, { version: params.version, locks: params.locks });
|
|
1595
|
+
},
|
|
1596
|
+
/**
|
|
1597
|
+
* For the owner's client: undo one change in `Mandate.strategy.tunings[]` (`POST /api/v1/mandates/:id/tunings/:tid/undo`), the person's
|
|
1598
|
+
* Undo. Restores the values from before it. The plan's grantor only (404 for anyone else). 422 `already_undone` | `strategy_not_editable`,
|
|
1599
|
+
* 409 `tuning_superseded` (it was changed again since: nothing was undone), 404 `tuning_not_found`. The agent hears `strategy_tuned`.
|
|
1600
|
+
*/
|
|
1601
|
+
async undoTuning(apiKey, mandateId, tuningId) {
|
|
1602
|
+
return request("POST", `/api/v1/mandates/${mandateId}/tunings/${encodeURIComponent(tuningId)}/undo`, apiKey);
|
|
1603
|
+
},
|
|
1604
|
+
/**
|
|
1605
|
+
* For the owner's client: a number the person counts themselves for a plan target whose `source` is `reported` (`metricKey` is that
|
|
1606
|
+
* target's `metric`). Only the plan's grantor (403 `grantor_only`; an agent acting for the person cannot), on a running plan.
|
|
1607
|
+
* 201 with the stored count. 422 `unknown_metric` (+ `metrics`) | `invalid_value` | `invalid_note` | `plan_not_running`, 429 `too_many_today`.
|
|
1608
|
+
* The agent reads it back as a measured value (`GET /metrics?source=reported&metric_key=`), labelled "Reported by <name>".
|
|
1609
|
+
*/
|
|
1610
|
+
async reportCount(apiKey, mandateId, params) {
|
|
1611
|
+
const body = { metric_key: params.metricKey, value: params.value };
|
|
1612
|
+
if (params.note !== undefined)
|
|
1613
|
+
body.note = params.note;
|
|
1614
|
+
return request("POST", `/api/v1/mandates/${mandateId}/reported_counts`, apiKey, body);
|
|
1615
|
+
},
|
|
1616
|
+
/**
|
|
1617
|
+
* What the person can use, before you propose a plan: `GET /api/v1/me/readiness`. No option reads the caller; `personId` a person you
|
|
1618
|
+
* serve or work with; `planId` the grantor of a plan you are a party to. Anyone else gets 404. Name only the connections listed.
|
|
1619
|
+
*/
|
|
1620
|
+
async getReadiness(apiKey, opts) {
|
|
1621
|
+
const params = new URLSearchParams();
|
|
1622
|
+
if (opts?.personId != null)
|
|
1623
|
+
params.set("person_id", String(opts.personId));
|
|
1624
|
+
if (opts?.planId != null)
|
|
1625
|
+
params.set("plan_id", String(opts.planId));
|
|
1626
|
+
const qs = params.toString();
|
|
1627
|
+
return request("GET", `/api/v1/me/readiness${qs ? `?${qs}` : ""}`, apiKey);
|
|
1628
|
+
},
|
|
1629
|
+
/** The plan in one read (`GET /api/v1/mandates/:id/summary`): targets, reported counts, recent acts as sentences, tunings, spend, locks. Grantor and grantee only (404 otherwise). */
|
|
1630
|
+
async getPlanSummary(apiKey, mandateId) {
|
|
1631
|
+
return request("GET", `/api/v1/mandates/${mandateId}/summary`, apiKey);
|
|
1632
|
+
},
|
|
1633
|
+
/**
|
|
1634
|
+
* Pause every live plan and grant you gave, or one goal's (`goalId` = a plan's mandate id or strategy_id). Idempotent. The plan's
|
|
1635
|
+
* grantee may also call it, for the goal plans it holds only.
|
|
1636
|
+
*/
|
|
1637
|
+
async pauseAll(apiKey, opts) {
|
|
1638
|
+
return request("POST", "/api/v1/mandates/pause_all", apiKey, opts?.goalId !== undefined ? { goal_id: opts.goalId } : undefined);
|
|
1639
|
+
},
|
|
1640
|
+
/**
|
|
1641
|
+
* Lift exactly what `pauseAll` paused. Grantor only: a grantee gets 403 `resume_grantor_only` (`err.code`), "Tap Resume on the plan to
|
|
1642
|
+
* start it again.", so tell the person instead.
|
|
1643
|
+
*/
|
|
1644
|
+
async resumeAll(apiKey, opts) {
|
|
1645
|
+
return request("POST", "/api/v1/mandates/resume_all", apiKey, opts?.goalId !== undefined ? { goal_id: opts.goalId } : undefined);
|
|
1646
|
+
},
|
|
1375
1647
|
/** Do you already hold this capability? `{allowed, mandate_id?, mode, constraints}`. */
|
|
1376
1648
|
async checkGrant(apiKey, capability) {
|
|
1377
1649
|
return request("GET", `/api/v1/mandates/check?capability=${encodeURIComponent(capability)}`, apiKey);
|
|
@@ -1404,8 +1676,13 @@ function createSaltClient(options) {
|
|
|
1404
1676
|
/** Grantee accepts an offered mandate (binds its current key fingerprint), or the grantor
|
|
1405
1677
|
* activates one still `proposed` by the grantee. See `ctx.mandate.accept()` in webhook.ts,
|
|
1406
1678
|
* which calls this for an incoming `mandate_offered` event. */
|
|
1407
|
-
async accept(apiKey, mandateId) {
|
|
1408
|
-
|
|
1679
|
+
async accept(apiKey, mandateId, opts) {
|
|
1680
|
+
const body = {};
|
|
1681
|
+
if (opts?.version !== undefined)
|
|
1682
|
+
body.version = opts.version;
|
|
1683
|
+
if (opts?.firstPostWords !== undefined)
|
|
1684
|
+
body.first_post_words = opts.firstPostWords;
|
|
1685
|
+
return request("POST", `/api/v1/mandates/${mandateId}/accept`, apiKey, Object.keys(body).length ? body : undefined);
|
|
1409
1686
|
},
|
|
1410
1687
|
/** Standing mandates only: +90 days from now. */
|
|
1411
1688
|
async renew(apiKey, mandateId) {
|
|
@@ -1417,7 +1694,8 @@ function createSaltClient(options) {
|
|
|
1417
1694
|
async resume(apiKey, mandateId) {
|
|
1418
1695
|
return request("POST", `/api/v1/mandates/${mandateId}/resume`, apiKey);
|
|
1419
1696
|
},
|
|
1420
|
-
/** The grantor, or a governor of either side, can revoke -- cascades to any child mandates.
|
|
1697
|
+
/** The grantor, or a governor of either side, can revoke -- cascades to any child mandates. Stopping a plan also stops every grant filed
|
|
1698
|
+
* for it and any other live plan for the same goal: their mandate ids come back as `also_ended`. */
|
|
1421
1699
|
async revoke(apiKey, mandateId, reason) {
|
|
1422
1700
|
return request("POST", `/api/v1/mandates/${mandateId}/revoke`, apiKey, reason ? { reason } : undefined);
|
|
1423
1701
|
},
|