@tak-ps/node-tak 12.31.1 → 12.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/CHANGELOG.md CHANGED
@@ -12,6 +12,11 @@
12
12
 
13
13
  ### Pending
14
14
 
15
+ ### v12.32.0 - 2026-10-26
16
+
17
+ - :tada: Add `Query.sa` & `Query.saFeats` for the TAK Server `/Marti/api/cot/sa` Situational Awareness query - every CoT visible to the caller within a time window given as `start`/`end` or `secago` (max 24 hours, validated client-side), optionally restricted to a bounding box
18
+ - :rocket: `Query.historyFeats` tolerates an empty / missing `<events>` body instead of throwing
19
+
15
20
  ### v12.31.1 - 2026-10-05
16
21
 
17
22
  - :arrow_up: Update core deps
@@ -6,11 +6,41 @@ export declare const HistoryOptions: import("@sinclair/typebox").TObject<{
6
6
  end: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
7
7
  secago: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
8
8
  }>;
9
+ /** TAK Server refuses Situational Awareness windows longer than this */
10
+ export declare const SA_MAX_WINDOW_SECONDS: number;
11
+ /**
12
+ * Options for the Situational Awareness query - every CoT the caller's groups
13
+ * can see within a time window of at most 24 hours
14
+ */
15
+ export declare const SituationalAwarenessOptions: import("@sinclair/typebox").TObject<{
16
+ start: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
17
+ end: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
18
+ secago: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnion<[import("@sinclair/typebox").TInteger, import("@sinclair/typebox").TString]>>;
19
+ bbox: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TTuple<[import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNumber, import("@sinclair/typebox").TNumber]>>;
20
+ filtered: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TBoolean>;
21
+ }>;
22
+ /** The explicit window TAK Server is queried with once the options are resolved */
23
+ export declare const SituationalAwarenessWindow: import("@sinclair/typebox").TObject<{
24
+ start: import("@sinclair/typebox").TString;
25
+ end: import("@sinclair/typebox").TString;
26
+ }>;
9
27
  export default class QueryCommands extends Commands {
10
28
  schema: {};
11
29
  cli(): Promise<object | string>;
12
30
  singleFeat(uid: string): Promise<Static<typeof Feature.Feature>>;
13
31
  single(uid: string): Promise<string>;
14
32
  historyFeats(uid: string, opts?: Static<typeof HistoryOptions>): Promise<Array<Static<typeof Feature.Feature>>>;
33
+ /**
34
+ * Parse an `<events>` document into GeoJSON Features - an empty body
35
+ * (TAK Server answers 404 with no body when nothing matched) yields []
36
+ */
37
+ private parseEvents;
38
+ saFeats(opts?: Static<typeof SituationalAwarenessOptions>): Promise<Array<Static<typeof Feature.Feature>>>;
39
+ /**
40
+ * Raw XML of every CoT visible to the caller within the time window.
41
+ * TAK Server requires an explicit start & end and refuses inverted or
42
+ * >24 hour windows, so those are resolved and rejected here
43
+ */
44
+ sa(opts?: Static<typeof SituationalAwarenessOptions>): Promise<string>;
15
45
  history(uid: string, opts?: Static<typeof HistoryOptions>): Promise<string>;
16
46
  }
@@ -8,6 +8,30 @@ export const HistoryOptions = Type.Object({
8
8
  end: Type.Optional(Type.String()),
9
9
  secago: Type.Optional(Type.String()),
10
10
  });
11
+ /** TAK Server refuses Situational Awareness windows longer than this */
12
+ export const SA_MAX_WINDOW_SECONDS = 24 * 60 * 60;
13
+ /**
14
+ * Options for the Situational Awareness query - every CoT the caller's groups
15
+ * can see within a time window of at most 24 hours
16
+ */
17
+ export const SituationalAwarenessOptions = Type.Object({
18
+ start: Type.Optional(Type.String({ description: 'ISO 8601 start of the window (inclusive). Defaults to `end` minus `secago`' })),
19
+ end: Type.Optional(Type.String({ description: 'ISO 8601 end of the window (inclusive). Defaults to now' })),
20
+ secago: Type.Optional(Type.Union([Type.Integer(), Type.String()], {
21
+ description: 'Window length in seconds counted back from `end`. Default is 3600 when `start` is omitted'
22
+ })),
23
+ bbox: Type.Optional(Type.Tuple([Type.Number(), Type.Number(), Type.Number(), Type.Number()], {
24
+ description: 'Restrict to events within [minLon, minLat, maxLon, maxLat]'
25
+ })),
26
+ filtered: Type.Optional(Type.Boolean({
27
+ description: 'TAK Server default (true) only returns a-f, a-n, a-u, a-h & b-m-r events plus chat; false returns every stored event'
28
+ })),
29
+ });
30
+ /** The explicit window TAK Server is queried with once the options are resolved */
31
+ export const SituationalAwarenessWindow = Type.Object({
32
+ start: Type.String({ description: 'ISO 8601 start of the window (inclusive)' }),
33
+ end: Type.String({ description: 'ISO 8601 end of the window (inclusive)' }),
34
+ });
11
35
  export default class QueryCommands extends Commands {
12
36
  schema = {};
13
37
  async cli() {
@@ -34,9 +58,18 @@ export default class QueryCommands extends Commands {
34
58
  return body;
35
59
  }
36
60
  async historyFeats(uid, opts) {
61
+ return this.parseEvents(await this.history(uid, opts));
62
+ }
63
+ /**
64
+ * Parse an `<events>` document into GeoJSON Features - an empty body
65
+ * (TAK Server answers 404 with no body when nothing matched) yields []
66
+ */
67
+ async parseEvents(xml) {
37
68
  const feats = [];
38
- const res = xml2js(await this.history(uid, opts), { compact: true });
39
- if (!Object.keys(res).length || !Object.keys(res.events).length)
69
+ if (!xml || !xml.trim().length)
70
+ return feats;
71
+ const res = xml2js(xml, { compact: true });
72
+ if (!Object.keys(res).length || !res.events || !Object.keys(res.events).length)
40
73
  return feats;
41
74
  if (!res.events.event || (Array.isArray(res.events.event) && !res.events.event.length))
42
75
  return feats;
@@ -45,6 +78,55 @@ export default class QueryCommands extends Commands {
45
78
  }
46
79
  return feats;
47
80
  }
81
+ async saFeats(opts = {}) {
82
+ return this.parseEvents(await this.sa(opts));
83
+ }
84
+ /**
85
+ * Raw XML of every CoT visible to the caller within the time window.
86
+ * TAK Server requires an explicit start & end and refuses inverted or
87
+ * >24 hour windows, so those are resolved and rejected here
88
+ */
89
+ async sa(opts = {}) {
90
+ const url = new URL('/Marti/api/cot/sa', this.api.url);
91
+ const end = opts.end ? new Date(opts.end) : new Date();
92
+ if (Number.isNaN(end.getTime()))
93
+ throw new Err(400, null, 'end must be an ISO 8601 date');
94
+ let start;
95
+ if (opts.start) {
96
+ start = new Date(opts.start);
97
+ if (Number.isNaN(start.getTime()))
98
+ throw new Err(400, null, 'start must be an ISO 8601 date');
99
+ }
100
+ else {
101
+ const secago = opts.secago === undefined ? 3600 : Number(opts.secago);
102
+ if (!Number.isInteger(secago) || secago < 1)
103
+ throw new Err(400, null, 'secago must be a positive integer');
104
+ start = new Date(end.getTime() - secago * 1000);
105
+ }
106
+ if (start.getTime() > end.getTime())
107
+ throw new Err(400, null, 'start must be before end');
108
+ if (end.getTime() - start.getTime() > SA_MAX_WINDOW_SECONDS * 1000)
109
+ throw new Err(400, null, 'Window must be 24 hours or less');
110
+ const window = {
111
+ start: start.toISOString(),
112
+ end: end.toISOString(),
113
+ };
114
+ url.searchParams.append('start', window.start);
115
+ url.searchParams.append('end', window.end);
116
+ if (opts.bbox) {
117
+ url.searchParams.append('left', String(opts.bbox[0]));
118
+ url.searchParams.append('bottom', String(opts.bbox[1]));
119
+ url.searchParams.append('right', String(opts.bbox[2]));
120
+ url.searchParams.append('top', String(opts.bbox[3]));
121
+ }
122
+ if (opts.filtered !== undefined) {
123
+ url.searchParams.append('isFiltered', String(opts.filtered));
124
+ }
125
+ const res = await this.api.fetch(url, {
126
+ method: 'GET'
127
+ }, true);
128
+ return await res.text();
129
+ }
48
130
  async history(uid, opts) {
49
131
  const url = new URL(`/Marti/api/cot/xml/${encodeURIComponent(uid)}/all`, this.api.url);
50
132
  if (opts) {
@@ -1 +1 @@
1
- {"version":3,"file":"query.js","sourceRoot":"","sources":["../../../lib/api/query.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../../../lib/api/query.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,126 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { Readable } from 'node:stream';
4
+ import { Client } from 'undici';
5
+ import TAKAPI from '../lib/api.js';
6
+ import { APIAuthCertificate } from '../lib/auth.js';
7
+ function mockResponse(statusCode, contentType, body) {
8
+ return {
9
+ statusCode,
10
+ headers: { 'content-type': contentType },
11
+ body: Readable.from([Buffer.from(body)]),
12
+ trailers: {}
13
+ };
14
+ }
15
+ const EVENTS = `<?xml version="1.0" encoding="UTF-8"?>
16
+ <events>
17
+ <event version="2.0" uid="sensor-1" type="a-f-G" time="2026-10-26T10:00:00.000Z" start="2026-10-26T10:00:00.000Z" stale="2026-10-26T10:05:00.000Z" how="m-g">
18
+ <point lat="39.7" lon="-104.9" hae="1600.0" ce="10.0" le="10.0"/>
19
+ <detail><contact callsign="SEE_1"/></detail>
20
+ </event>
21
+ <event version="2.0" uid="sensor-2" type="a-f-G" time="2026-10-26T10:01:00.000Z" start="2026-10-26T10:01:00.000Z" stale="2026-10-26T10:06:00.000Z" how="m-g">
22
+ <point lat="39.8" lon="-104.8" hae="1600.0" ce="10.0" le="10.0"/>
23
+ <detail><contact callsign="SEE_2"/></detail>
24
+ </event>
25
+ </events>`;
26
+ test('Query.saFeats builds the cot/sa request and parses every event', async () => {
27
+ const originalRequest = Client.prototype.request;
28
+ let captured;
29
+ Client.prototype.request = async function (opts) {
30
+ captured = opts;
31
+ return mockResponse(200, 'application/xml', EVENTS);
32
+ };
33
+ try {
34
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
35
+ const feats = await api.Query.saFeats({
36
+ start: '2026-10-26T09:00:00.000Z',
37
+ end: '2026-10-26T11:00:00.000Z',
38
+ bbox: [-105, 39, -104, 40],
39
+ filtered: false
40
+ });
41
+ assert.ok(captured);
42
+ assert.equal(captured.method, 'GET');
43
+ assert.equal(captured.path, '/Marti/api/cot/sa?start=2026-10-26T09%3A00%3A00.000Z&end=2026-10-26T11%3A00%3A00.000Z&left=-105&bottom=39&right=-104&top=40&isFiltered=false');
44
+ assert.equal(feats.length, 2);
45
+ assert.equal(feats[0].id, 'sensor-1');
46
+ assert.equal(feats[0].properties.callsign, 'SEE_1');
47
+ assert.equal(feats[0].properties.time, '2026-10-26T10:00:00.000Z');
48
+ assert.deepEqual(feats[0].geometry, { type: 'Point', coordinates: [-104.9, 39.7, 1600] });
49
+ assert.equal(feats[1].id, 'sensor-2');
50
+ }
51
+ finally {
52
+ Client.prototype.request = originalRequest;
53
+ }
54
+ });
55
+ test('Query.sa resolves secago to an explicit window and rejects what TAK Server would refuse', async () => {
56
+ const originalRequest = Client.prototype.request;
57
+ let captured;
58
+ Client.prototype.request = async function (opts) {
59
+ captured = opts;
60
+ return mockResponse(404, 'application/xml', '');
61
+ };
62
+ try {
63
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
64
+ await api.Query.sa({ end: '2026-10-26T12:00:00.000Z', secago: '7200' });
65
+ assert.equal(captured?.path, '/Marti/api/cot/sa?start=2026-10-26T10%3A00%3A00.000Z&end=2026-10-26T12%3A00%3A00.000Z');
66
+ const before = Date.now();
67
+ await api.Query.sa();
68
+ const params = new URL(String(captured?.path), 'https://tak.example.com').searchParams;
69
+ const start = Date.parse(String(params.get('start')));
70
+ const end = Date.parse(String(params.get('end')));
71
+ assert.equal(end - start, 3600 * 1000);
72
+ assert.ok(end >= before && end <= Date.now());
73
+ await assert.rejects(api.Query.sa({ start: '2026-10-26T13:00:00.000Z', end: '2026-10-26T12:00:00.000Z' }), /start must be before end/);
74
+ await assert.rejects(api.Query.sa({ start: '2026-10-25T11:59:59.000Z', end: '2026-10-26T12:00:00.000Z' }), /24 hours or less/);
75
+ await assert.rejects(api.Query.sa({ start: 'yesterday' }), /ISO 8601/);
76
+ await assert.rejects(api.Query.sa({ secago: 0 }), /positive integer/);
77
+ }
78
+ finally {
79
+ Client.prototype.request = originalRequest;
80
+ }
81
+ });
82
+ test('Query.saFeats omits optional params and returns [] for an empty 404 body', async () => {
83
+ const originalRequest = Client.prototype.request;
84
+ let captured;
85
+ Client.prototype.request = async function (opts) {
86
+ captured = opts;
87
+ return mockResponse(404, 'application/xml', '');
88
+ };
89
+ try {
90
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
91
+ const feats = await api.Query.saFeats({
92
+ start: '2026-10-26T09:00:00.000Z',
93
+ end: '2026-10-26T11:00:00.000Z',
94
+ });
95
+ assert.ok(captured);
96
+ assert.equal(captured.path, '/Marti/api/cot/sa?start=2026-10-26T09%3A00%3A00.000Z&end=2026-10-26T11%3A00%3A00.000Z');
97
+ assert.deepEqual(feats, []);
98
+ }
99
+ finally {
100
+ Client.prototype.request = originalRequest;
101
+ }
102
+ });
103
+ test('Query.historyFeats returns [] for an empty body and parses a single event', async () => {
104
+ const originalRequest = Client.prototype.request;
105
+ let body = '';
106
+ Client.prototype.request = async function () {
107
+ return mockResponse(200, 'application/xml', body);
108
+ };
109
+ try {
110
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
111
+ assert.deepEqual(await api.Query.historyFeats('sensor-1', { secago: '3600' }), []);
112
+ body = `<events>
113
+ <event version="2.0" uid="sensor-1" type="a-f-G" time="2026-10-26T10:00:00.000Z" start="2026-10-26T10:00:00.000Z" stale="2026-10-26T10:05:00.000Z" how="m-g">
114
+ <point lat="39.7" lon="-104.9" hae="1600.0" ce="10.0" le="10.0"/>
115
+ <detail><contact callsign="SEE_1"/></detail>
116
+ </event>
117
+ </events>`;
118
+ const feats = await api.Query.historyFeats('sensor-1', { secago: '3600' });
119
+ assert.equal(feats.length, 1);
120
+ assert.equal(feats[0].id, 'sensor-1');
121
+ }
122
+ finally {
123
+ Client.prototype.request = originalRequest;
124
+ }
125
+ });
126
+ //# sourceMappingURL=query.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.test.js","sourceRoot":"","sources":["../../test/query.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"root":["../cli.ts","../index.ts","../test/certificate.test.ts","../test/default.test.ts","../test/findCoT.test.ts","../test/html-error.test.ts","../test/mission-layer.test.ts","../test/mission-property.test.ts","../test/mission.test.ts","../test/pipeline.test.ts","../test/queue.test.ts","../test/uploadPackage.test.ts","../test/user-management.test.ts","../lib/api.ts","../lib/auth.ts","../lib/commands.ts","../lib/fetch.ts","../lib/multipart.ts","../lib/stream.ts","../lib/api/certificate.ts","../lib/api/client.ts","../lib/api/contacts.ts","../lib/api/credentials.ts","../lib/api/export.ts","../lib/api/files.ts","../lib/api/groups.ts","../lib/api/iconsets.ts","../lib/api/injectors.ts","../lib/api/locate.ts","../lib/api/mission-invite.ts","../lib/api/mission-layer.ts","../lib/api/mission-log.ts","../lib/api/mission-property.ts","../lib/api/mission.ts","../lib/api/oauth.ts","../lib/api/package.ts","../lib/api/profile.ts","../lib/api/query.ts","../lib/api/repeater.ts","../lib/api/security.ts","../lib/api/subscriptions.ts","../lib/api/types.ts","../lib/api/user-management.ts","../lib/api/video.ts","../lib/utils/html-error.ts","../lib/utils/queue.ts"],"version":"6.0.3"}
1
+ {"root":["../cli.ts","../index.ts","../test/certificate.test.ts","../test/default.test.ts","../test/findCoT.test.ts","../test/html-error.test.ts","../test/mission-layer.test.ts","../test/mission-property.test.ts","../test/mission.test.ts","../test/pipeline.test.ts","../test/query.test.ts","../test/queue.test.ts","../test/uploadPackage.test.ts","../test/user-management.test.ts","../lib/api.ts","../lib/auth.ts","../lib/commands.ts","../lib/fetch.ts","../lib/multipart.ts","../lib/stream.ts","../lib/api/certificate.ts","../lib/api/client.ts","../lib/api/contacts.ts","../lib/api/credentials.ts","../lib/api/export.ts","../lib/api/files.ts","../lib/api/groups.ts","../lib/api/iconsets.ts","../lib/api/injectors.ts","../lib/api/locate.ts","../lib/api/mission-invite.ts","../lib/api/mission-layer.ts","../lib/api/mission-log.ts","../lib/api/mission-property.ts","../lib/api/mission.ts","../lib/api/oauth.ts","../lib/api/package.ts","../lib/api/profile.ts","../lib/api/query.ts","../lib/api/repeater.ts","../lib/api/security.ts","../lib/api/subscriptions.ts","../lib/api/types.ts","../lib/api/user-management.ts","../lib/api/video.ts","../lib/utils/html-error.ts","../lib/utils/queue.ts"],"version":"6.0.3"}
package/lib/api/query.ts CHANGED
@@ -11,6 +11,33 @@ export const HistoryOptions = Type.Object({
11
11
  secago: Type.Optional(Type.String()),
12
12
  })
13
13
 
14
+ /** TAK Server refuses Situational Awareness windows longer than this */
15
+ export const SA_MAX_WINDOW_SECONDS = 24 * 60 * 60;
16
+
17
+ /**
18
+ * Options for the Situational Awareness query - every CoT the caller's groups
19
+ * can see within a time window of at most 24 hours
20
+ */
21
+ export const SituationalAwarenessOptions = Type.Object({
22
+ start: Type.Optional(Type.String({ description: 'ISO 8601 start of the window (inclusive). Defaults to `end` minus `secago`' })),
23
+ end: Type.Optional(Type.String({ description: 'ISO 8601 end of the window (inclusive). Defaults to now' })),
24
+ secago: Type.Optional(Type.Union([Type.Integer(), Type.String()], {
25
+ description: 'Window length in seconds counted back from `end`. Default is 3600 when `start` is omitted'
26
+ })),
27
+ bbox: Type.Optional(Type.Tuple([Type.Number(), Type.Number(), Type.Number(), Type.Number()], {
28
+ description: 'Restrict to events within [minLon, minLat, maxLon, maxLat]'
29
+ })),
30
+ filtered: Type.Optional(Type.Boolean({
31
+ description: 'TAK Server default (true) only returns a-f, a-n, a-u, a-h & b-m-r events plus chat; false returns every stored event'
32
+ })),
33
+ })
34
+
35
+ /** The explicit window TAK Server is queried with once the options are resolved */
36
+ export const SituationalAwarenessWindow = Type.Object({
37
+ start: Type.String({ description: 'ISO 8601 start of the window (inclusive)' }),
38
+ end: Type.String({ description: 'ISO 8601 end of the window (inclusive)' }),
39
+ })
40
+
14
41
  export default class QueryCommands extends Commands {
15
42
  schema = {}
16
43
 
@@ -45,11 +72,21 @@ export default class QueryCommands extends Commands {
45
72
  }
46
73
 
47
74
  async historyFeats(uid: string, opts?: Static<typeof HistoryOptions>): Promise<Array<Static<typeof Feature.Feature>>> {
75
+ return this.parseEvents(await this.history(uid, opts));
76
+ }
77
+
78
+ /**
79
+ * Parse an `<events>` document into GeoJSON Features - an empty body
80
+ * (TAK Server answers 404 with no body when nothing matched) yields []
81
+ */
82
+ private async parseEvents(xml: string): Promise<Array<Static<typeof Feature.Feature>>> {
48
83
  const feats: Static<typeof Feature.Feature>[] = [];
49
84
 
50
- const res: any = xml2js(await this.history(uid, opts), { compact: true });
85
+ if (!xml || !xml.trim().length) return feats;
51
86
 
52
- if (!Object.keys(res).length || !Object.keys(res.events).length) return feats;
87
+ const res: any = xml2js(xml, { compact: true });
88
+
89
+ if (!Object.keys(res).length || !res.events || !Object.keys(res.events).length) return feats;
53
90
  if (!res.events.event || (Array.isArray(res.events.event) && !res.events.event.length)) return feats;
54
91
 
55
92
  for (const event of Array.isArray(res.events.event) ? res.events.event : [res.events.event] ) {
@@ -59,6 +96,60 @@ export default class QueryCommands extends Commands {
59
96
  return feats;
60
97
  }
61
98
 
99
+ async saFeats(opts: Static<typeof SituationalAwarenessOptions> = {}): Promise<Array<Static<typeof Feature.Feature>>> {
100
+ return this.parseEvents(await this.sa(opts));
101
+ }
102
+
103
+ /**
104
+ * Raw XML of every CoT visible to the caller within the time window.
105
+ * TAK Server requires an explicit start & end and refuses inverted or
106
+ * >24 hour windows, so those are resolved and rejected here
107
+ */
108
+ async sa(opts: Static<typeof SituationalAwarenessOptions> = {}): Promise<string> {
109
+ const url = new URL('/Marti/api/cot/sa', this.api.url);
110
+
111
+ const end = opts.end ? new Date(opts.end) : new Date();
112
+ if (Number.isNaN(end.getTime())) throw new Err(400, null, 'end must be an ISO 8601 date');
113
+
114
+ let start: Date;
115
+ if (opts.start) {
116
+ start = new Date(opts.start);
117
+ if (Number.isNaN(start.getTime())) throw new Err(400, null, 'start must be an ISO 8601 date');
118
+ } else {
119
+ const secago = opts.secago === undefined ? 3600 : Number(opts.secago);
120
+ if (!Number.isInteger(secago) || secago < 1) throw new Err(400, null, 'secago must be a positive integer');
121
+ start = new Date(end.getTime() - secago * 1000);
122
+ }
123
+
124
+ if (start.getTime() > end.getTime()) throw new Err(400, null, 'start must be before end');
125
+ if (end.getTime() - start.getTime() > SA_MAX_WINDOW_SECONDS * 1000) throw new Err(400, null, 'Window must be 24 hours or less');
126
+
127
+ const window: Static<typeof SituationalAwarenessWindow> = {
128
+ start: start.toISOString(),
129
+ end: end.toISOString(),
130
+ };
131
+
132
+ url.searchParams.append('start', window.start);
133
+ url.searchParams.append('end', window.end);
134
+
135
+ if (opts.bbox) {
136
+ url.searchParams.append('left', String(opts.bbox[0]));
137
+ url.searchParams.append('bottom', String(opts.bbox[1]));
138
+ url.searchParams.append('right', String(opts.bbox[2]));
139
+ url.searchParams.append('top', String(opts.bbox[3]));
140
+ }
141
+
142
+ if (opts.filtered !== undefined) {
143
+ url.searchParams.append('isFiltered', String(opts.filtered));
144
+ }
145
+
146
+ const res = await this.api.fetch(url, {
147
+ method: 'GET'
148
+ }, true);
149
+
150
+ return await res.text();
151
+ }
152
+
62
153
  async history(uid: string, opts?: Static<typeof HistoryOptions>): Promise<string> {
63
154
  const url = new URL(`/Marti/api/cot/xml/${encodeURIComponent(uid)}/all`, this.api.url);
64
155
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tak-ps/node-tak",
3
3
  "type": "module",
4
- "version": "12.31.1",
4
+ "version": "12.32.0",
5
5
  "description": "Lightweight JavaScript library for communicating with TAK Server",
6
6
  "author": "Nick Ingalls <nick@ingalls.ca>",
7
7
  "main": "dist/index.js",
@@ -0,0 +1,156 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { Readable } from 'node:stream';
4
+ import { Client } from 'undici';
5
+ import type { Dispatcher } from 'undici';
6
+ import TAKAPI from '../lib/api.js';
7
+ import { APIAuthCertificate } from '../lib/auth.js';
8
+
9
+ type RequestArgs = Parameters<Client['request']>[0];
10
+
11
+ function mockResponse(statusCode: number, contentType: string, body: string): Dispatcher.ResponseData {
12
+ return {
13
+ statusCode,
14
+ headers: { 'content-type': contentType },
15
+ body: Readable.from([Buffer.from(body)]),
16
+ trailers: {}
17
+ } as unknown as Dispatcher.ResponseData;
18
+ }
19
+
20
+ const EVENTS = `<?xml version="1.0" encoding="UTF-8"?>
21
+ <events>
22
+ <event version="2.0" uid="sensor-1" type="a-f-G" time="2026-10-26T10:00:00.000Z" start="2026-10-26T10:00:00.000Z" stale="2026-10-26T10:05:00.000Z" how="m-g">
23
+ <point lat="39.7" lon="-104.9" hae="1600.0" ce="10.0" le="10.0"/>
24
+ <detail><contact callsign="SEE_1"/></detail>
25
+ </event>
26
+ <event version="2.0" uid="sensor-2" type="a-f-G" time="2026-10-26T10:01:00.000Z" start="2026-10-26T10:01:00.000Z" stale="2026-10-26T10:06:00.000Z" how="m-g">
27
+ <point lat="39.8" lon="-104.8" hae="1600.0" ce="10.0" le="10.0"/>
28
+ <detail><contact callsign="SEE_2"/></detail>
29
+ </event>
30
+ </events>`;
31
+
32
+ test('Query.saFeats builds the cot/sa request and parses every event', async () => {
33
+ const originalRequest = Client.prototype.request;
34
+
35
+ let captured: RequestArgs | undefined;
36
+
37
+ Client.prototype.request = async function(opts: RequestArgs): Promise<Dispatcher.ResponseData> {
38
+ captured = opts;
39
+ return mockResponse(200, 'application/xml', EVENTS);
40
+ };
41
+
42
+ try {
43
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
44
+
45
+ const feats = await api.Query.saFeats({
46
+ start: '2026-10-26T09:00:00.000Z',
47
+ end: '2026-10-26T11:00:00.000Z',
48
+ bbox: [-105, 39, -104, 40],
49
+ filtered: false
50
+ });
51
+
52
+ assert.ok(captured);
53
+ assert.equal(captured.method, 'GET');
54
+ assert.equal(
55
+ captured.path,
56
+ '/Marti/api/cot/sa?start=2026-10-26T09%3A00%3A00.000Z&end=2026-10-26T11%3A00%3A00.000Z&left=-105&bottom=39&right=-104&top=40&isFiltered=false'
57
+ );
58
+
59
+ assert.equal(feats.length, 2);
60
+ assert.equal(feats[0].id, 'sensor-1');
61
+ assert.equal(feats[0].properties.callsign, 'SEE_1');
62
+ assert.equal(feats[0].properties.time, '2026-10-26T10:00:00.000Z');
63
+ assert.deepEqual(feats[0].geometry, { type: 'Point', coordinates: [-104.9, 39.7, 1600] });
64
+ assert.equal(feats[1].id, 'sensor-2');
65
+ } finally {
66
+ Client.prototype.request = originalRequest;
67
+ }
68
+ });
69
+
70
+ test('Query.sa resolves secago to an explicit window and rejects what TAK Server would refuse', async () => {
71
+ const originalRequest = Client.prototype.request;
72
+
73
+ let captured: RequestArgs | undefined;
74
+
75
+ Client.prototype.request = async function(opts: RequestArgs): Promise<Dispatcher.ResponseData> {
76
+ captured = opts;
77
+ return mockResponse(404, 'application/xml', '');
78
+ };
79
+
80
+ try {
81
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
82
+
83
+ await api.Query.sa({ end: '2026-10-26T12:00:00.000Z', secago: '7200' });
84
+ assert.equal(captured?.path, '/Marti/api/cot/sa?start=2026-10-26T10%3A00%3A00.000Z&end=2026-10-26T12%3A00%3A00.000Z');
85
+
86
+ const before = Date.now();
87
+ await api.Query.sa();
88
+ const params = new URL(String(captured?.path), 'https://tak.example.com').searchParams;
89
+ const start = Date.parse(String(params.get('start')));
90
+ const end = Date.parse(String(params.get('end')));
91
+ assert.equal(end - start, 3600 * 1000);
92
+ assert.ok(end >= before && end <= Date.now());
93
+
94
+ await assert.rejects(api.Query.sa({ start: '2026-10-26T13:00:00.000Z', end: '2026-10-26T12:00:00.000Z' }), /start must be before end/);
95
+ await assert.rejects(api.Query.sa({ start: '2026-10-25T11:59:59.000Z', end: '2026-10-26T12:00:00.000Z' }), /24 hours or less/);
96
+ await assert.rejects(api.Query.sa({ start: 'yesterday' }), /ISO 8601/);
97
+ await assert.rejects(api.Query.sa({ secago: 0 }), /positive integer/);
98
+ } finally {
99
+ Client.prototype.request = originalRequest;
100
+ }
101
+ });
102
+
103
+ test('Query.saFeats omits optional params and returns [] for an empty 404 body', async () => {
104
+ const originalRequest = Client.prototype.request;
105
+
106
+ let captured: RequestArgs | undefined;
107
+
108
+ Client.prototype.request = async function(opts: RequestArgs): Promise<Dispatcher.ResponseData> {
109
+ captured = opts;
110
+ return mockResponse(404, 'application/xml', '');
111
+ };
112
+
113
+ try {
114
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
115
+
116
+ const feats = await api.Query.saFeats({
117
+ start: '2026-10-26T09:00:00.000Z',
118
+ end: '2026-10-26T11:00:00.000Z',
119
+ });
120
+
121
+ assert.ok(captured);
122
+ assert.equal(captured.path, '/Marti/api/cot/sa?start=2026-10-26T09%3A00%3A00.000Z&end=2026-10-26T11%3A00%3A00.000Z');
123
+ assert.deepEqual(feats, []);
124
+ } finally {
125
+ Client.prototype.request = originalRequest;
126
+ }
127
+ });
128
+
129
+ test('Query.historyFeats returns [] for an empty body and parses a single event', async () => {
130
+ const originalRequest = Client.prototype.request;
131
+
132
+ let body = '';
133
+
134
+ Client.prototype.request = async function(): Promise<Dispatcher.ResponseData> {
135
+ return mockResponse(200, 'application/xml', body);
136
+ };
137
+
138
+ try {
139
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
140
+
141
+ assert.deepEqual(await api.Query.historyFeats('sensor-1', { secago: '3600' }), []);
142
+
143
+ body = `<events>
144
+ <event version="2.0" uid="sensor-1" type="a-f-G" time="2026-10-26T10:00:00.000Z" start="2026-10-26T10:00:00.000Z" stale="2026-10-26T10:05:00.000Z" how="m-g">
145
+ <point lat="39.7" lon="-104.9" hae="1600.0" ce="10.0" le="10.0"/>
146
+ <detail><contact callsign="SEE_1"/></detail>
147
+ </event>
148
+ </events>`;
149
+
150
+ const feats = await api.Query.historyFeats('sensor-1', { secago: '3600' });
151
+ assert.equal(feats.length, 1);
152
+ assert.equal(feats[0].id, 'sensor-1');
153
+ } finally {
154
+ Client.prototype.request = originalRequest;
155
+ }
156
+ });