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.
Files changed (52) hide show
  1. package/README.md +26 -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 +154 -0
  10. package/dist/client.d.ts.map +1 -1
  11. package/dist/client.js +139 -1
  12. package/dist/client.js.map +1 -1
  13. package/dist/crypto.d.ts +18 -4
  14. package/dist/crypto.d.ts.map +1 -1
  15. package/dist/crypto.js +34 -11
  16. package/dist/crypto.js.map +1 -1
  17. package/dist/device/client.d.ts +12 -4
  18. package/dist/device/client.d.ts.map +1 -1
  19. package/dist/device/client.js +44 -1
  20. package/dist/device/client.js.map +1 -1
  21. package/dist/device/host.d.ts +74 -3
  22. package/dist/device/host.d.ts.map +1 -1
  23. package/dist/device/host.js +262 -12
  24. package/dist/device/host.js.map +1 -1
  25. package/dist/device/http.d.ts.map +1 -1
  26. package/dist/device/http.js +3 -2
  27. package/dist/device/http.js.map +1 -1
  28. package/dist/device/protocol.d.ts +97 -1
  29. package/dist/device/protocol.d.ts.map +1 -1
  30. package/dist/device/protocol.js +155 -1
  31. package/dist/device/protocol.js.map +1 -1
  32. package/dist/help.d.ts +54 -0
  33. package/dist/help.d.ts.map +1 -0
  34. package/dist/help.js +62 -0
  35. package/dist/help.js.map +1 -0
  36. package/dist/index.d.ts +3 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +3 -0
  39. package/dist/index.js.map +1 -1
  40. package/dist/keyReset.d.ts +117 -0
  41. package/dist/keyReset.d.ts.map +1 -0
  42. package/dist/keyReset.js +267 -0
  43. package/dist/keyReset.js.map +1 -0
  44. package/dist/webhook.d.ts +80 -2
  45. package/dist/webhook.d.ts.map +1 -1
  46. package/dist/webhook.js +130 -17
  47. package/dist/webhook.js.map +1 -1
  48. package/dist/work.d.ts +2 -0
  49. package/dist/work.d.ts.map +1 -1
  50. package/dist/work.js +4 -0
  51. package/dist/work.js.map +1 -1
  52. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
- import { DeviceProtocolError, type AppsResult, type ClickArgs, type DeviceCountClass, type DeviceErrorCode, type DeviceOp, type DevicePauseReason, type FileResult, type KeyArgs, type ObservationResult, type ObserveArgs, type Progress, type ScrollArgs, type SubmitArgs, type TaskArgs, type TaskResult } from "./protocol.js";
1
+ import { DeviceProtocolError, type AppsResult, type ClickArgs, type DeviceCountClass, type DeviceErrorCode, type DeviceOp, type DevicePauseReason, type FileResult, type KeyArgs, type ObservationResult, type ObserveArgs, type PointedBody, type Progress, type ScrollArgs, type StillBody, type SubmitArgs, type TaskArgs, type TaskResult } from "./protocol.js";
2
2
  import type { LaneReaders } from "./client.js";
3
+ import { type HelpRequest, type HelpViewer } from "../help.js";
3
4
  import { type SaltId } from "../ids.js";
4
5
  /** The seams. The desktop app implements every method; the host calls one only
5
6
  * after it has passed every gate below. A handler may throw -- the host turns
@@ -30,7 +31,24 @@ export interface DeviceHostHandlers {
30
31
  * extra field never reaches the agent. Let a `DeviceBudgetSpentError` from `ctx.spend`
31
32
  * bubble: it ends the task and the session. */
32
33
  task?(args: TaskArgs, ctx: DeviceTaskContext): Promise<TaskResult>;
34
+ /** Show Me: what is under the spot a person pointed at? Optional: a host without it answers a
35
+ * point `refused: "not_allowed"`. Called only after the host accepted the point (a person over
36
+ * the device, a help request or watch still open, a fresh point id). `x` and `y` are fractions
37
+ * of the picture the person tapped (`obs` names the still; absent = the live frame). Return the
38
+ * `screen` spot, the frontmost `app`, the `element` under it and, when the answer must be withheld,
39
+ * `refused`. The host drops the element of a refused answer and of any never-allowed app, so a
40
+ * handler that forgets cannot leak a name. A throw is answered `not_allowed`. */
41
+ resolvePoint?(args: ResolvePointArgs): Promise<ResolvedPoint>;
33
42
  }
43
+ export interface ResolvePointArgs {
44
+ help_id: string;
45
+ obs?: string;
46
+ x: number;
47
+ y: number;
48
+ words: string;
49
+ }
50
+ /** What `resolvePoint` hands back; the host adds `help_id`, `x`, `y` and `words` itself. */
51
+ export type ResolvedPoint = Pick<PointedBody, "screen" | "app" | "element" | "refused">;
34
52
  /** What `ctx.spend` counts a task step as. `act` is counted as a `click` (the closest of the
35
53
  * existing classes: a press of a control) and `apps` as `app_focus`. */
36
54
  export type TaskSpendClass = "command" | "look" | "act" | "observe" | "apps";
@@ -126,7 +144,9 @@ export interface DeviceHostTransport {
126
144
  * endpoint's shape lives is httpDeviceHostTransport (http.ts). */
127
145
  pause?(sessionId: SaltId, reason: DevicePauseReason): Promise<void>;
128
146
  /** POST /device_sessions/:sid/resume (device key). */
129
- resume?(sessionId: SaltId): Promise<void>;
147
+ resume?(sessionId: SaltId, opts?: {
148
+ local?: boolean;
149
+ }): Promise<void>;
130
150
  }
131
151
  /** The optional body of a beat: when the device last executed a command
132
152
  * (ISO8601) and that command's seq. Sent on every beat once one has run. */
@@ -169,6 +189,14 @@ export interface DeviceHostCallbacks {
169
189
  /** Every gate decision, for the app's append-only on-disk audit log. Content
170
190
  * is the caller's to redact; the host passes op + decision + reason only. */
171
191
  onAudit?(entry: DeviceAuditEntry): void;
192
+ /** Show Me: a help request opened on this session. The app posts a still now and then stills on
193
+ * change (via `host.postStill`) for ten minutes. */
194
+ onHelpOpened?(req: HelpRequest): void;
195
+ /** Show Me: the person is looking from their phone. The app posts a still now, restarts its
196
+ * ten-minute window and shows "<name> is looking from their phone". */
197
+ onHelpViewing?(req: HelpRequest, viewer: HelpViewer | null): void;
198
+ /** Show Me: the help request closed (answered, withdrawn or ended). The app stops and clears its lines. */
199
+ onHelpClosed?(req: HelpRequest): void;
172
200
  }
173
201
  export interface DeviceAuditEntry {
174
202
  at: number;
@@ -202,9 +230,38 @@ export interface DeviceHost {
202
230
  * interrupt a command already executing. */
203
231
  pause(reason: DevicePauseReason): Promise<void>;
204
232
  /** Clear the pause and tell Salt. No-op when not paused. */
205
- resume(): Promise<void>;
233
+ resume(opts?: {
234
+ local?: boolean;
235
+ }): Promise<void>;
206
236
  /** The current pause reason, or null. */
207
237
  pausedReason(): DevicePauseReason | null;
238
+ /** Show Me: feed a lane message in together with WHO sent it (the decrypted text and the sender's
239
+ * account id). Returns true when it was a `[[SALT-POINT]]` line, consumed here whether or not it
240
+ * was acted on; false for anything else (pass that to `handleCommand`). A point is accepted only
241
+ * from a person `options.isPersonOverDevice` names, only while its help request is open (or
242
+ * `options.isWatchLive()`), and once per point id; the answer is a `pointed` op with `re` = the
243
+ * point id. Every refusal is one audit line (op `point`) and no reply. */
244
+ handleLaneMessage(plaintext: string, senderId: SaltId): Promise<boolean>;
245
+ /** Also takes `device_session_paused {reason:"phone"}` / `device_session_resumed` for this session (a person
246
+ * pausing from their phone): the host pauses or resumes the PHONE pause itself, never a local guard. */
247
+ /** Show Me: feed a `help_request_opened|viewing|closed` delivery in (the `onDeviceDelivery` type
248
+ * and body). Returns true when it was one of those for THIS session and the matching callback
249
+ * ran. */
250
+ handleDelivery(type: string, body: unknown): boolean;
251
+ /** Show Me: post a still into the lane (op `still`, no `re`). Resolves `{posted: true}`, or
252
+ * `{posted: false, reason}` when held back: `too_soon` (under 2 s since the last one),
253
+ * `budget` (the grant's `max_actions` is spent; a still the agent receives costs one `observe`),
254
+ * `too_many` (30 in the last minute), `no_help` (no help request open for that id and no live
255
+ * watch), `secure_field` (the session is paused for a secure field), `ended`. Throws
256
+ * `bad_args` for a malformed body and `too_large` over the per-message cap. */
257
+ postStill(body: StillBody): Promise<{
258
+ posted: true;
259
+ } | {
260
+ posted: false;
261
+ reason: "too_soon" | "too_many" | "no_help" | "budget" | "paused" | "not_viewed" | "not_granted" | "ended";
262
+ }>;
263
+ /** The help request ids currently open on this session. */
264
+ openHelpIds(): string[];
208
265
  }
209
266
  export declare function createDeviceHost(snapshot: DeviceCapsSnapshot, handlers: DeviceHostHandlers, transport: DeviceHostTransport, callbacks?: DeviceHostCallbacks, options?: {
210
267
  beatIntervalMs?: number;
@@ -218,5 +275,19 @@ export declare function createDeviceHost(snapshot: DeviceCapsSnapshot, handlers:
218
275
  progressMinIntervalMs?: number;
219
276
  /** How long past `timeout_s` a task handler may run before it is cut off (default 15 s). */
220
277
  taskGraceMs?: number;
278
+ /** Show Me: is this account one of the device's people (owner, chain root or the session's
279
+ * grantor), per the host's cached `GET /devices/:id/grants`? A point from anyone else is ignored.
280
+ * Absent, every point is ignored. */
281
+ isPersonOverDevice?: (accountId: SaltId) => boolean;
282
+ /** Show Me: is a live watch call on this device right now? While it is, a point is accepted
283
+ * without an open help request. Default: never. */
284
+ isWatchLive?: () => boolean;
285
+ /** Show Me: armored public keys of the people over the device (owner, root, this session's grantor).
286
+ * Used when the grant lacks `device.observe` in auto or notify mode: a still is then encrypted for
287
+ * these keys ONLY, never the agent's. Absent or empty in that case, stills are refused `not_granted`. */
288
+ peopleReaderKeys?: () => string[];
289
+ /** Least gap between two stills (default STILL_MIN_INTERVAL_MS, 2 s). For tests: the 30-a-minute
290
+ * cap can only be reached below the default gap. */
291
+ stillMinIntervalMs?: number;
221
292
  }): DeviceHost;
222
293
  //# sourceMappingURL=host.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/device/host.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../../src/device/host.ts"],"names":[],"mappings":"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"}
@@ -40,6 +40,7 @@ exports.ackBody = ackBody;
40
40
  exports.createDeviceHost = createDeviceHost;
41
41
  const protocol_js_1 = require("./protocol.js");
42
42
  const node_path_1 = __importDefault(require("node:path"));
43
+ const help_js_1 = require("../help.js");
43
44
  const ids_js_1 = require("../ids.js");
44
45
  const SPEND_COUNT = {
45
46
  command: "command",
@@ -150,7 +151,17 @@ function createDeviceHost(snapshot, handlers, transport, callbacks = {}, options
150
151
  // Newest command this host actually executed (for lease reports).
151
152
  let lastRanAt = null;
152
153
  let lastRanSeq = null;
154
+ // The effective pause reason is a LOCAL guard pause (person_active, secure_field, person, ...) if one
155
+ // holds, else "phone" while the person has paused from their phone. The two are tracked apart: a
156
+ // phone resume never clears a local guard, and the machine's own idle resume never clears the phone.
157
+ let localPause = null;
158
+ let phonePause = false;
153
159
  let pausedReason = null;
160
+ // Salt still holds a phone pause that the person at this machine released; told on the next sync.
161
+ let serverPhoneToClear = false;
162
+ const recomputePause = () => {
163
+ pausedReason = localPause ?? (phonePause ? "phone" : null);
164
+ };
154
165
  // One command at a time per session: every command runs behind this tail.
155
166
  let tail = Promise.resolve();
156
167
  // Ids received and not yet started / ids a `cancel` asked us to drop.
@@ -816,6 +827,8 @@ function createDeviceHost(snapshot, handlers, transport, callbacks = {}, options
816
827
  beatTimer = durationTimer = null;
817
828
  for (const ac of runningTasks.values())
818
829
  ac.abort("session_ended");
830
+ openHelp.clear();
831
+ viewedHelp.clear();
819
832
  audit({ commandId: "-", op: "stop", decision: "ended", reason });
820
833
  callbacks.onEnded?.(reason);
821
834
  try {
@@ -827,15 +840,27 @@ function createDeviceHost(snapshot, handlers, transport, callbacks = {}, options
827
840
  }
828
841
  /** Make Salt's picture of our pause state match ours. Salt only accepts a
829
842
  * pause from `active`, so a reason CHANGE is resume-then-pause. A failed
830
- * report is retried on the next beat (the local pause stands meanwhile). */
843
+ * report is retried on the next beat (the local pause stands meanwhile).
844
+ * Only LOCAL reasons are reported; a phone pause is Salt's own, and while it holds Salt keeps
845
+ * its reason whatever we report. */
831
846
  function syncPause() {
832
847
  pauseSync = pauseSync.then(async () => {
833
848
  if (ended)
834
849
  return;
835
- const want = pausedReason;
836
- if (reportedPause === want)
837
- return;
838
850
  try {
851
+ if (serverPhoneToClear) {
852
+ // The person pressed Resume at this machine: the one resume Salt accepts for a phone pause.
853
+ await transport.resume?.(snapshot.sessionId, { local: true });
854
+ serverPhoneToClear = false;
855
+ reportedPause = null;
856
+ }
857
+ const want = localPause;
858
+ if (phonePause) {
859
+ reportedPause = want;
860
+ return;
861
+ }
862
+ if (reportedPause === want)
863
+ return;
839
864
  if (reportedPause !== null) {
840
865
  await transport.resume?.(snapshot.sessionId);
841
866
  reportedPause = null;
@@ -854,23 +879,248 @@ function createDeviceHost(snapshot, handlers, transport, callbacks = {}, options
854
879
  async function pause(reason) {
855
880
  if (ended)
856
881
  return;
857
- const changed = pausedReason !== reason;
858
- pausedReason = reason;
882
+ if (reason === "phone") {
883
+ // Not a local guard: only the phone delivery path sets it.
884
+ return;
885
+ }
886
+ const changed = localPause !== reason;
887
+ localPause = reason;
888
+ recomputePause();
859
889
  audit({ commandId: "-", op: "pause", decision: "refused", reason });
860
890
  if (!changed)
861
891
  return;
862
892
  await syncPause();
863
893
  }
864
- async function resume() {
865
- if (ended || pausedReason === null)
894
+ /** Clear the local pause. A phone pause is cleared only by `{local: true}`: the person pressed
895
+ * Resume at the machine. An automatic resume (the idle timer, the end of a secure field) leaves it. */
896
+ async function resume(opts = {}) {
897
+ if (ended)
898
+ return;
899
+ const clearPhone = opts.local === true && phonePause;
900
+ if (localPause === null && !clearPhone)
866
901
  return;
867
- pausedReason = null;
902
+ localPause = null;
903
+ if (clearPhone) {
904
+ phonePause = false;
905
+ serverPhoneToClear = true;
906
+ }
907
+ recomputePause();
868
908
  audit({ commandId: "-", op: "resume", decision: "ran" });
869
- for (const release of [...resumeWaiters])
870
- release();
909
+ if (pausedReason === null)
910
+ for (const release of [...resumeWaiters])
911
+ release();
871
912
  await syncPause();
872
913
  }
873
- return { handleCommand, start, stop, actionsSpent: () => spent, pause, resume, pausedReason: () => pausedReason };
914
+ // ---- Show Me: help requests, stills and points (CONTRACT.md sections 3 and 4) ----------------
915
+ // Help requests open on THIS session, by id. A point is acted on only while its help id is here
916
+ // (or a watch is live).
917
+ const openHelp = new Map();
918
+ // Help requests the person has tapped (help_request_viewing). Stills start only then.
919
+ const viewedHelp = new Set();
920
+ // Point ids already answered, oldest dropped: a replayed or re-delivered point is answered once.
921
+ const seenPoints = new Set();
922
+ const SEEN_POINTS_CAP = 256;
923
+ // Times of recently posted stills (the 2 s gap and the 30-a-minute window).
924
+ const stillTimes = [];
925
+ function helpFromBody(body) {
926
+ const hr = body?.help_request;
927
+ if (!hr || typeof hr !== "object")
928
+ return null;
929
+ const r = hr;
930
+ if (typeof r.id !== "string" || r.id === "")
931
+ return null;
932
+ // A delivery about another session is not ours.
933
+ if (r.device_session_id == null || !(0, ids_js_1.sameId)(r.device_session_id, snapshot.sessionId))
934
+ return null;
935
+ return r;
936
+ }
937
+ // A person pausing or resuming from their phone reaches the device as a server delivery for THIS
938
+ // session: `device_session_paused {session_id, reason:"phone"}` and `device_session_resumed {session_id}`.
939
+ // Only the phone's own reason is taken from the wire. A phone pause never replaces a local guard
940
+ // (it waits underneath it), and a resume clears only the phone pause, never a local guard.
941
+ function handleSessionPauseDelivery(type, body) {
942
+ const b = (body ?? {});
943
+ const sid = b.session_id ?? b.session?.id ?? b.id;
944
+ if (sid == null || !(0, ids_js_1.sameId)(sid, snapshot.sessionId))
945
+ return false;
946
+ if (type === "device_session_paused") {
947
+ if ((b.reason ?? b.pause_reason) !== "phone")
948
+ return false;
949
+ phonePause = true;
950
+ serverPhoneToClear = false;
951
+ recomputePause();
952
+ audit({ commandId: "-", op: "pause", decision: "refused", reason: "phone" });
953
+ return true;
954
+ }
955
+ if (!phonePause)
956
+ return true;
957
+ phonePause = false;
958
+ serverPhoneToClear = false;
959
+ reportedPause = null; // Salt is active again; re-report any local pause that still holds
960
+ recomputePause();
961
+ if (pausedReason === null)
962
+ for (const release of [...resumeWaiters])
963
+ release();
964
+ void syncPause();
965
+ return true;
966
+ }
967
+ function handleDelivery(type, body) {
968
+ if (ended)
969
+ return false;
970
+ if (type === "device_session_paused" || type === "device_session_resumed")
971
+ return handleSessionPauseDelivery(type, body);
972
+ if (!help_js_1.HELP_DELIVERY_TYPES.includes(type))
973
+ return false;
974
+ if (ended)
975
+ return false;
976
+ const req = helpFromBody(body);
977
+ if (!req)
978
+ return false;
979
+ if (type === "help_request_opened") {
980
+ openHelp.set(req.id, req);
981
+ callbacks.onHelpOpened?.(req);
982
+ }
983
+ else if (type === "help_request_viewing") {
984
+ openHelp.set(req.id, req);
985
+ viewedHelp.add(req.id);
986
+ const v = body.viewer;
987
+ callbacks.onHelpViewing?.(req, v && typeof v === "object" && typeof v.id === "string" ? v : null);
988
+ }
989
+ else {
990
+ openHelp.delete(req.id);
991
+ viewedHelp.delete(req.id);
992
+ callbacks.onHelpClosed?.(req);
993
+ }
994
+ return true;
995
+ }
996
+ function helpAllowsStills(helpId) {
997
+ return openHelp.has(helpId) || (options.isWatchLive?.() ?? false);
998
+ }
999
+ async function postStill(raw) {
1000
+ if (ended)
1001
+ return { posted: false, reason: "ended" };
1002
+ const body = (0, protocol_js_1.normalizeStill)(raw);
1003
+ if (!body)
1004
+ throw new protocol_js_1.DeviceProtocolError("bad_args", "malformed still");
1005
+ if (!helpAllowsStills(body.help_id))
1006
+ return { posted: false, reason: "no_help" };
1007
+ // Never while paused, for any reason (the person has the computer, a secure field, the phone).
1008
+ if (pausedReason !== null)
1009
+ return { posted: false, reason: "paused" };
1010
+ // Only after the person tapped (help_request_viewing), never on `opened`. A live watch is its own consent.
1011
+ if (!viewedHelp.has(body.help_id) && !(options.isWatchLive?.() ?? false))
1012
+ return { posted: false, reason: "not_viewed" };
1013
+ // A picture goes to the agent only when its grant allows observing. Otherwise it goes to the people
1014
+ // over the device alone (the lane readers minus the agent), which the transport can do because every
1015
+ // post names its readers.
1016
+ const observeMode = snapshot.caps["device.observe"]?.mode;
1017
+ const agentMayObserve = observeMode === "auto" || observeMode === "notify";
1018
+ let readers = snapshot.readers;
1019
+ if (!agentMayObserve) {
1020
+ const keys = options.peopleReaderKeys?.() ?? [];
1021
+ if (keys.length === 0)
1022
+ return { posted: false, reason: "not_granted" };
1023
+ readers = { ...snapshot.readers, publicKeys: keys };
1024
+ }
1025
+ const now = clock();
1026
+ while (stillTimes.length && now - stillTimes[0] >= 60_000)
1027
+ stillTimes.shift();
1028
+ if (stillTimes.length && now - stillTimes[stillTimes.length - 1] < (options.stillMinIntervalMs ?? protocol_js_1.STILL_MIN_INTERVAL_MS))
1029
+ return { posted: false, reason: "too_soon" };
1030
+ if (stillTimes.length >= protocol_js_1.STILL_MAX_PER_MINUTE)
1031
+ return { posted: false, reason: "too_many" };
1032
+ // A still the agent receives is an observation: it counts against the session's action budget and
1033
+ // the per-second observe cap. A people-only still (agent not granted) is the agent's no business.
1034
+ const maxActions = snapshot.budget?.maxActions;
1035
+ if (agentMayObserve) {
1036
+ if (maxActions !== undefined && spent >= maxActions)
1037
+ return { posted: false, reason: "budget" };
1038
+ while (obsTimes.length && now - obsTimes[0] >= 1000)
1039
+ obsTimes.shift();
1040
+ if (obsTimes.length >= protocol_js_1.MAX_OBSERVE_PER_SECOND)
1041
+ return { posted: false, reason: "too_soon" };
1042
+ }
1043
+ const plaintext = (0, protocol_js_1.encodeDeviceMessage)({ v: protocol_js_1.DEVICE_PROTOCOL_VERSION, id: (0, protocol_js_1.deviceMessageId)(), seq: nextOutSeq(), op: "still", session: snapshot.sessionId }, body);
1044
+ if (Buffer.byteLength(plaintext, "utf8") > protocol_js_1.MAX_DEVICE_PLAINTEXT_BYTES)
1045
+ throw new protocol_js_1.DeviceProtocolError("too_large", "still exceeds the per-message limit");
1046
+ stillTimes.push(now);
1047
+ await transport.postResult(plaintext, readers);
1048
+ if (agentMayObserve) {
1049
+ spent += 1;
1050
+ obsTimes.push(now);
1051
+ bump("observe");
1052
+ // Spending the last action does not end the session here: the next still is refused "budget" and
1053
+ // the next command hits the budget gate, which ends it (budget_exhausted).
1054
+ }
1055
+ return { posted: true };
1056
+ }
1057
+ function pointRefused(pointId, why) {
1058
+ audit({ commandId: pointId, op: "point", decision: "refused", code: "forbidden", reason: why });
1059
+ }
1060
+ async function handleLaneMessage(plaintext, senderId) {
1061
+ if (!(0, protocol_js_1.isPointMessage)(plaintext))
1062
+ return false;
1063
+ if (ended)
1064
+ return true;
1065
+ let point;
1066
+ try {
1067
+ point = (0, protocol_js_1.decodePoint)(plaintext);
1068
+ }
1069
+ catch (e) {
1070
+ pointRefused("-", `malformed point: ${e.message}`);
1071
+ return true;
1072
+ }
1073
+ if (!point)
1074
+ return true;
1075
+ if (!options.isPersonOverDevice?.(senderId)) {
1076
+ pointRefused(point.id, "sender is not a person over this device");
1077
+ return true;
1078
+ }
1079
+ if (!helpAllowsStills(point.help)) {
1080
+ pointRefused(point.id, "no open help request for that id");
1081
+ return true;
1082
+ }
1083
+ if (seenPoints.has(point.id)) {
1084
+ pointRefused(point.id, "point id already answered");
1085
+ return true;
1086
+ }
1087
+ seenPoints.add(point.id);
1088
+ while (seenPoints.size > SEEN_POINTS_CAP)
1089
+ seenPoints.delete(seenPoints.values().next().value);
1090
+ let resolved = null;
1091
+ let refused = null;
1092
+ if (!handlers.resolvePoint) {
1093
+ refused = "not_allowed";
1094
+ }
1095
+ else {
1096
+ try {
1097
+ resolved = await handlers.resolvePoint({ help_id: point.help, obs: point.obs, x: point.x, y: point.y, words: point.words });
1098
+ }
1099
+ catch {
1100
+ refused = "not_allowed";
1101
+ }
1102
+ }
1103
+ // The floor, again: never name anything in a never-allowed app, whatever the handler said.
1104
+ if (resolved && refused === null) {
1105
+ refused = resolved.refused ?? null;
1106
+ if (refused === null && resolved.app && isNeverAllowed(resolved.app))
1107
+ refused = "forbidden_app";
1108
+ }
1109
+ const answer = (0, protocol_js_1.normalizePointed)({
1110
+ help_id: point.help,
1111
+ x: point.x,
1112
+ y: point.y,
1113
+ screen: resolved?.screen ?? { x: 0, y: 0 },
1114
+ app: refused === "forbidden_app" ? null : (resolved?.app ?? null),
1115
+ element: refused === null ? (resolved?.element ?? null) : null,
1116
+ refused,
1117
+ words: point.words,
1118
+ }) ?? { help_id: point.help, x: point.x, y: point.y, screen: { x: 0, y: 0 }, app: null, element: null, refused: "not_allowed", words: "" };
1119
+ audit({ commandId: point.id, op: "point", decision: "ran", reason: answer.refused ?? undefined });
1120
+ await reply({ id: point.id }, "pointed", answer);
1121
+ return true;
1122
+ }
1123
+ return { handleCommand, start, stop, actionsSpent: () => spent, pause, resume, pausedReason: () => pausedReason, handleLaneMessage, handleDelivery, postStill, openHelpIds: () => [...openHelp.keys()] };
874
1124
  }
875
1125
  const AGENT_OP_SET = new Set([
876
1126
  "observe",