@tak-ps/node-tak 12.23.1 → 12.25.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 (38) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +1 -0
  4. package/dist/index.js.map +1 -1
  5. package/dist/lib/api/credentials.d.ts +3 -1
  6. package/dist/lib/api/credentials.js +23 -10
  7. package/dist/lib/api/credentials.js.map +1 -1
  8. package/dist/lib/api/mission-layer.d.ts +2 -3
  9. package/dist/lib/api/mission-layer.js +5 -3
  10. package/dist/lib/api/mission-layer.js.map +1 -1
  11. package/dist/lib/api/mission.d.ts +436 -2
  12. package/dist/lib/api/mission.js +38 -11
  13. package/dist/lib/api/mission.js.map +1 -1
  14. package/dist/lib/api.js +5 -1
  15. package/dist/lib/api.js.map +1 -1
  16. package/dist/lib/utils/html-error.d.ts +32 -0
  17. package/dist/lib/utils/html-error.js +172 -0
  18. package/dist/lib/utils/html-error.js.map +1 -0
  19. package/dist/test/html-error.test.d.ts +1 -0
  20. package/dist/test/html-error.test.js +115 -0
  21. package/dist/test/html-error.test.js.map +1 -0
  22. package/dist/test/mission-layer.test.d.ts +1 -0
  23. package/dist/test/mission-layer.test.js +105 -0
  24. package/dist/test/mission-layer.test.js.map +1 -0
  25. package/dist/test/mission.test.d.ts +1 -0
  26. package/dist/test/mission.test.js +137 -0
  27. package/dist/test/mission.test.js.map +1 -0
  28. package/dist/tsconfig.tsbuildinfo +1 -1
  29. package/index.ts +1 -0
  30. package/lib/api/credentials.ts +23 -9
  31. package/lib/api/mission-layer.ts +7 -6
  32. package/lib/api/mission.ts +40 -11
  33. package/lib/api.ts +5 -1
  34. package/lib/utils/html-error.ts +208 -0
  35. package/package.json +3 -3
  36. package/test/html-error.test.ts +139 -0
  37. package/test/mission-layer.test.ts +137 -0
  38. package/test/mission.test.ts +173 -0
@@ -0,0 +1,105 @@
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
+ test('MissionLayer.attachUids files UIDs under a layer via the contents paths body', async () => {
16
+ const originalRequest = Client.prototype.request;
17
+ let captured;
18
+ Client.prototype.request = async function (opts) {
19
+ captured = opts;
20
+ return mockResponse(200, 'text/plain', '');
21
+ };
22
+ try {
23
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
24
+ await api.MissionLayer.attachUids('mission-1', 'layer-1', {
25
+ uids: ['uid-1', 'uid-2'],
26
+ creatorUid: 'alice'
27
+ });
28
+ assert.ok(captured);
29
+ assert.equal(captured.method, 'PUT');
30
+ assert.equal(captured.path, '/Marti/api/missions/mission-1/contents?creatorUid=alice');
31
+ assert.deepEqual(JSON.parse(String(captured.body)), {
32
+ paths: {
33
+ 'layer-1': [{ uids: ['uid-1', 'uid-2'] }]
34
+ }
35
+ });
36
+ }
37
+ finally {
38
+ Client.prototype.request = originalRequest;
39
+ }
40
+ });
41
+ test('MissionLayer.attachUids uses the guid contents endpoint for GUID missions', async () => {
42
+ const originalRequest = Client.prototype.request;
43
+ let captured;
44
+ Client.prototype.request = async function (opts) {
45
+ captured = opts;
46
+ return mockResponse(200, 'text/plain', '');
47
+ };
48
+ try {
49
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
50
+ await api.MissionLayer.attachUids('11111111-2222-3333-4444-555555555555', 'layer-1', {
51
+ uids: ['uid-1'],
52
+ creatorUid: 'alice'
53
+ });
54
+ assert.ok(captured);
55
+ assert.equal(captured.method, 'PUT');
56
+ assert.equal(captured.path, '/Marti/api/missions/guid/11111111-2222-3333-4444-555555555555/contents?creatorUid=alice');
57
+ }
58
+ finally {
59
+ Client.prototype.request = originalRequest;
60
+ }
61
+ });
62
+ test('MissionLayer.setParent re-parents layers via query params', async () => {
63
+ const originalRequest = Client.prototype.request;
64
+ let captured;
65
+ Client.prototype.request = async function (opts) {
66
+ captured = opts;
67
+ return mockResponse(200, 'text/plain', '');
68
+ };
69
+ try {
70
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
71
+ await api.MissionLayer.setParent('mission-1', {
72
+ layerUids: ['uid-1', 'uid-2'],
73
+ parentUid: 'layer-2',
74
+ creatorUid: 'alice'
75
+ });
76
+ assert.ok(captured);
77
+ assert.equal(captured.method, 'PUT');
78
+ assert.equal(captured.path, '/Marti/api/missions/mission-1/layers/parent?layerUid=uid-1&layerUid=uid-2&parentUid=layer-2&creatorUid=alice');
79
+ }
80
+ finally {
81
+ Client.prototype.request = originalRequest;
82
+ }
83
+ });
84
+ test('MissionLayer.setParent omits parentUid to move a layer to the mission root', async () => {
85
+ const originalRequest = Client.prototype.request;
86
+ let captured;
87
+ Client.prototype.request = async function (opts) {
88
+ captured = opts;
89
+ return mockResponse(200, 'text/plain', '');
90
+ };
91
+ try {
92
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
93
+ await api.MissionLayer.setParent('11111111-2222-3333-4444-555555555555', {
94
+ layerUids: ['uid-1'],
95
+ creatorUid: 'alice'
96
+ });
97
+ assert.ok(captured);
98
+ assert.equal(captured.method, 'PUT');
99
+ assert.equal(captured.path, '/Marti/api/missions/guid/11111111-2222-3333-4444-555555555555/layers/parent?layerUid=uid-1&creatorUid=alice');
100
+ }
101
+ finally {
102
+ Client.prototype.request = originalRequest;
103
+ }
104
+ });
105
+ //# sourceMappingURL=mission-layer.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mission-layer.test.js","sourceRoot":"","sources":["../../test/mission-layer.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,137 @@
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
+ import { MissionSubscriberRole } from '../lib/api/mission.js';
8
+ function mockResponse(statusCode, contentType, body) {
9
+ return {
10
+ statusCode,
11
+ headers: { 'content-type': contentType },
12
+ body: Readable.from([Buffer.from(body)]),
13
+ trailers: {}
14
+ };
15
+ }
16
+ test('Mission.setRole sends role as a bare query param and returns void', async () => {
17
+ const originalRequest = Client.prototype.request;
18
+ let captured;
19
+ Client.prototype.request = async function (opts) {
20
+ captured = opts;
21
+ return mockResponse(200, 'text/plain', '');
22
+ };
23
+ try {
24
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
25
+ const res = await api.Mission.setRole('mission-1', {
26
+ clientUid: 'client-1',
27
+ username: 'alice',
28
+ role: MissionSubscriberRole.MISSION_OWNER
29
+ });
30
+ assert.equal(res, undefined);
31
+ assert.ok(captured);
32
+ assert.equal(captured.method, 'PUT');
33
+ assert.equal(captured.path, '/Marti/api/missions/mission-1/role?clientUid=client-1&username=alice&role=MISSION_OWNER');
34
+ }
35
+ finally {
36
+ Client.prototype.request = originalRequest;
37
+ }
38
+ });
39
+ test('Mission.subscriptionRoles parses a list of subscribers with roles', async () => {
40
+ const originalRequest = Client.prototype.request;
41
+ Client.prototype.request = async function () {
42
+ return mockResponse(200, 'application/json', JSON.stringify({
43
+ data: [{
44
+ clientUid: 'client-1',
45
+ username: 'alice',
46
+ createTime: '2026-01-01T00:00:00.000Z',
47
+ role: {
48
+ permissions: ['MISSION_READ', 'MISSION_WRITE'],
49
+ type: 'MISSION_SUBSCRIBER'
50
+ }
51
+ }]
52
+ }));
53
+ };
54
+ try {
55
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
56
+ const res = await api.Mission.subscriptionRoles('mission-1');
57
+ assert.equal(res.data.length, 1);
58
+ assert.equal(res.data[0].username, 'alice');
59
+ assert.equal(res.data[0].role.type, 'MISSION_SUBSCRIBER');
60
+ }
61
+ finally {
62
+ Client.prototype.request = originalRequest;
63
+ }
64
+ });
65
+ test('Mission.unsubscribe issues a DELETE with uid query param', async () => {
66
+ const originalRequest = Client.prototype.request;
67
+ let captured;
68
+ Client.prototype.request = async function (opts) {
69
+ captured = opts;
70
+ return mockResponse(200, 'text/plain', '');
71
+ };
72
+ try {
73
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
74
+ await api.Mission.unsubscribe('mission-1', { uid: 'client-1' });
75
+ assert.ok(captured);
76
+ assert.equal(captured.method, 'DELETE');
77
+ assert.equal(captured.path, '/Marti/api/missions/mission-1/subscription?uid=client-1');
78
+ }
79
+ finally {
80
+ Client.prototype.request = originalRequest;
81
+ }
82
+ });
83
+ test('Mission.latestFeats parses each CoT individually and isolates invalid CoTs', async () => {
84
+ const originalRequest = Client.prototype.request;
85
+ let captured;
86
+ Client.prototype.request = async function (opts) {
87
+ captured = opts;
88
+ return mockResponse(200, 'application/xml', `
89
+ <events>
90
+ <event version="2.0" uid="uid-valid" type="a-f-G" time="2026-08-11T00:00:00.000Z" start="2026-08-11T00:00:00.000Z" stale="2026-08-11T00:05:00.000Z" how="h-g-i-g-o">
91
+ <point lat="1.1" lon="2.2" hae="0.0" ce="9999999.0" le="9999999.0"/>
92
+ <detail>
93
+ <contact callsign="ALPHA"/>
94
+ </detail>
95
+ </event>
96
+ <event version="2.0" uid="uid-poisoned" type="a-f-G" time="2026-08-11T00:00:00.000Z" start="2026-08-11T00:00:00.000Z" stale="2026-08-11T00:05:00.000Z" how="h-g-i-g-o">
97
+ <detail>
98
+ <contact callsign="BRAVO"/>
99
+ </detail>
100
+ </event>
101
+ </events>
102
+ `);
103
+ };
104
+ try {
105
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
106
+ const res = await api.Mission.latestFeats('mission-1');
107
+ assert.ok(captured);
108
+ assert.equal(captured.method, 'GET');
109
+ assert.equal(captured.path, '/Marti/api/missions/mission-1/cot');
110
+ assert.equal(res.features.length, 1);
111
+ assert.equal(res.features[0].id, 'uid-valid');
112
+ assert.equal(res.features[0].properties.callsign, 'ALPHA');
113
+ assert.equal(res.invalid.length, 1);
114
+ assert.ok(res.invalid[0].error.length > 0);
115
+ const event = res.invalid[0].feature;
116
+ assert.equal(event._attributes?.uid, 'uid-poisoned');
117
+ assert.equal(event.detail?.contact?._attributes?.callsign, 'BRAVO');
118
+ }
119
+ finally {
120
+ Client.prototype.request = originalRequest;
121
+ }
122
+ });
123
+ test('Mission.latestFeats returns empty results for an empty document', async () => {
124
+ const originalRequest = Client.prototype.request;
125
+ Client.prototype.request = async function () {
126
+ return mockResponse(200, 'application/xml', '<events></events>');
127
+ };
128
+ try {
129
+ const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
130
+ const res = await api.Mission.latestFeats('mission-1');
131
+ assert.deepEqual(res, { features: [], invalid: [] });
132
+ }
133
+ finally {
134
+ Client.prototype.request = originalRequest;
135
+ }
136
+ });
137
+ //# sourceMappingURL=mission.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mission.test.js","sourceRoot":"","sources":["../../test/mission.test.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"root":["../cli.ts","../index.ts","../test/default.test.ts","../test/findCoT.test.ts","../test/pipeline.test.ts","../test/queue.test.ts","../test/uploadPackage.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.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/video.ts","../lib/utils/queue.ts"],"version":"6.0.3"}
1
+ {"root":["../cli.ts","../index.ts","../test/default.test.ts","../test/findCoT.test.ts","../test/html-error.test.ts","../test/mission-layer.test.ts","../test/mission.test.ts","../test/pipeline.test.ts","../test/queue.test.ts","../test/uploadPackage.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.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/video.ts","../lib/utils/html-error.ts","../lib/utils/queue.ts"],"version":"6.0.3"}
package/index.ts CHANGED
@@ -9,6 +9,7 @@ import TAKAPI from './lib/api.js';
9
9
  import { TAKAuth } from './lib/auth.js';
10
10
  import { Queue } from './lib/utils/queue.js';
11
11
  export * from './lib/auth.js';
12
+ export * from './lib/utils/html-error.js';
12
13
 
13
14
  /* eslint-disable no-control-regex */
14
15
  export const REGEX_CONTROL = /[\u000B-\u001F\u007F-\u009F]/g;
@@ -1,4 +1,4 @@
1
- import { APIAuthPassword } from '../auth.js';
1
+ import { APIAuthPassword, APIAuthToken } from '../auth.js';
2
2
  import { Static, Type } from '@sinclair/typebox';
3
3
  import Commands, { CommandOutputFormat, type ParsedArgs } from '../commands.js';
4
4
  import pem from 'pem';
@@ -35,8 +35,25 @@ export default class CredentialCommands extends Commands {
35
35
  });
36
36
  }
37
37
 
38
- async generate(): Promise<Static<typeof CertificateResponse>> {
39
- if (!(this.api.auth instanceof APIAuthPassword)) throw new Error('Must use Password Auth');
38
+ async generate(opts: {
39
+ username?: string
40
+ } = {}): Promise<Static<typeof CertificateResponse>> {
41
+ let username: string;
42
+ const headers: Record<string, string> = {
43
+ Accept: 'application/json'
44
+ };
45
+
46
+ if (this.api.auth instanceof APIAuthPassword) {
47
+ username = opts.username || this.api.auth.username;
48
+ headers.Authorization = 'Basic ' + btoa(this.api.auth.username + ":" + this.api.auth.password);
49
+ } else if (this.api.auth instanceof APIAuthToken) {
50
+ // TAK Server derives the enrollment username from the token claims and requires
51
+ // the CSR CN to match it - the caller must supply that username explicitly
52
+ if (!opts.username) throw new Error('Token Auth requires a username for the Certificate CN');
53
+ username = opts.username;
54
+ } else {
55
+ throw new Error('Must use Password or Token Auth');
56
+ }
40
57
 
41
58
  const config: any = xml2js(await this.config(), { compact: true });
42
59
 
@@ -58,20 +75,17 @@ export default class CredentialCommands extends Commands {
58
75
  } = await createCSR({
59
76
  organization,
60
77
  organizationUnit,
61
- commonName: this.api.auth.username
78
+ commonName: username
62
79
  });
63
80
 
64
81
  const url = new URL(`/Marti/api/tls/signClient/v2`, this.api.url);
65
- url.searchParams.append('clientUid', this.api.auth.username + ' (ETL)');
82
+ url.searchParams.append('clientUid', username + ' (ETL)');
66
83
  url.searchParams.append('version', '3');
67
84
 
68
85
  const res = await this.api.fetch(url, {
69
86
  method: 'POST',
70
87
  nocookies: true,
71
- headers: {
72
- Accept: 'application/json',
73
- Authorization: 'Basic ' + btoa(this.api.auth.username + ":" + this.api.auth.password)
74
- },
88
+ headers,
75
89
  body: keys.csr
76
90
  });
77
91
 
@@ -1,10 +1,9 @@
1
1
  import { TAKList, TAKItem } from './types.js';
2
2
  import { Type, Static } from '@sinclair/typebox';
3
- import type { MissionOptions } from './mission.js';
3
+ import type { MissionOptions, MissionLatestFeats } from './mission.js';
4
4
  import { GUIDMatch } from './mission.js';
5
5
  import Err from '@openaddresses/batch-error';
6
6
  import Commands, { CommandOutputFormat } from '../commands.js';
7
- import type { Feature } from '@tak-ps/node-cot';
8
7
 
9
8
  export enum MissionLayerType {
10
9
  GROUP = 'GROUP',
@@ -180,19 +179,21 @@ export default class MissionLayerCommands extends Commands {
180
179
  name: string,
181
180
  layerUid: string, // Layer UID
182
181
  opts?: Static<typeof MissionOptions>
183
- ): Promise<Static<typeof Feature.Feature>[]> {
182
+ ): Promise<Static<typeof MissionLatestFeats>> {
184
183
  const layer = (await this.get(name, layerUid, opts)).data;
185
- const feats = await this.api.Mission.latestFeats(name, opts);
184
+ const res = await this.api.Mission.latestFeats(name, opts);
186
185
 
187
186
  const layerUids = new Set((layer.uids || []).map((u) => {
188
187
  return u.data
189
188
  }));
190
189
 
191
- const filtered = feats.filter((f) => {
190
+ const features = res.features.filter((f) => {
192
191
  return layerUids.has(f.id)
193
192
  });
194
193
 
195
- return filtered;
194
+ // Invalid CoTs cannot be filtered by layer as their parse failed
195
+ // so all Mission-wide invalid CoTs are returned
196
+ return { features, invalid: res.invalid };
196
197
  }
197
198
 
198
199
  async get(
@@ -1,11 +1,10 @@
1
- import { xml2js } from '@tak-ps/xml-js';
2
- import CoT, { CoTParser } from '@tak-ps/node-cot';
1
+ import { CoTParser } from '@tak-ps/node-cot';
3
2
  import { Type, Static } from '@sinclair/typebox';
4
3
  import Err from '@openaddresses/batch-error';
5
4
  import { Readable } from 'node:stream'
6
5
  import { TAKItem, TAKList } from './types.js';
7
6
  import { MissionLog } from './mission-log.js';
8
- import type { Feature } from '@tak-ps/node-cot';
7
+ import { Feature } from '@tak-ps/node-cot';
9
8
  import Commands, { CommandOutputFormat, type ParsedArgs } from '../commands.js';
10
9
 
11
10
  export enum MissionSubscriberRole {
@@ -108,6 +107,21 @@ export const MissionOptions = Type.Object({
108
107
  token: Type.Optional(Type.String())
109
108
  });
110
109
 
110
+ /**
111
+ * A Mission CoT that could not be parsed or failed validation,
112
+ * with the raw xml-js event included so the caller can extract
113
+ * whatever context (UID, Callsign, etc.) it needs
114
+ */
115
+ export const MissionInvalidFeature = Type.Object({
116
+ error: Type.String(),
117
+ feature: Type.Unknown()
118
+ });
119
+
120
+ export const MissionLatestFeats = Type.Object({
121
+ features: Type.Array(Feature.Feature),
122
+ invalid: Type.Array(MissionInvalidFeature)
123
+ });
124
+
111
125
  export const AttachContentsInput = Type.Object({
112
126
  hashes: Type.Optional(Type.Array(Type.String())),
113
127
  uids: Type.Optional(Type.Array(Type.String())),
@@ -324,23 +338,38 @@ export default class MissionCommands extends Commands {
324
338
 
325
339
  /**
326
340
  * Return all current features in the Data Sync as CoT GeoJSON Features
341
+ *
342
+ * Each CoT in the TAK Server response is parsed & validated individually -
343
+ * those that fail are returned in the `invalid` array so a single poisoned
344
+ * CoT doesn't prevent the valid features from being returned
327
345
  */
328
346
  async latestFeats(
329
347
  name: string,
330
348
  opts?: Static<typeof MissionOptions>
331
- ): Promise<Static<typeof Feature.Feature>[]> {
332
- const feats: Static<typeof Feature.Feature>[] = [];
349
+ ): Promise<Static<typeof MissionLatestFeats>> {
350
+ const features: Static<typeof Feature.Feature>[] = [];
333
351
 
334
- const res: any = xml2js(await this.latestCots(name, opts), { compact: true });
352
+ const res = CoTParser.from_xml_document(await this.latestCots(name, opts));
335
353
 
336
- if (!Object.keys(res.events).length) return feats;
337
- if (!res.events.event || (Array.isArray(res.events.event) && !res.events.event.length)) return feats;
354
+ const invalid: Static<typeof MissionInvalidFeature>[] = res.invalid.map((entry) => {
355
+ return {
356
+ error: entry.error,
357
+ feature: entry.event
358
+ };
359
+ });
338
360
 
339
- for (const event of Array.isArray(res.events.event) ? res.events.event : [res.events.event] ) {
340
- feats.push(await CoTParser.to_geojson(new CoT({ event })));
361
+ for (const cot of res.cots) {
362
+ try {
363
+ features.push(await CoTParser.to_geojson(cot));
364
+ } catch (err) {
365
+ invalid.push({
366
+ error: err instanceof Error ? err.message : String(err),
367
+ feature: cot.raw.event
368
+ });
369
+ }
341
370
  }
342
371
 
343
- return feats;
372
+ return { features, invalid };
344
373
  }
345
374
 
346
375
  /**
package/lib/api.ts CHANGED
@@ -21,6 +21,7 @@ import Subscription from './api/subscriptions.js';
21
21
  import Video from './api/video.js';
22
22
  import Export from './api/export.js';
23
23
  import Err from '@openaddresses/batch-error';
24
+ import { TAKServerError, isHTML } from './utils/html-error.js';
24
25
  import * as auth from './auth.js';
25
26
 
26
27
  type ErrorWithCause = Error & {
@@ -210,6 +211,9 @@ export default class TAKAPI {
210
211
  bdy = null;
211
212
  }
212
213
 
214
+ // TAK Server returns HTML error pages
215
+ if (isHTML(bdy)) throw new TAKServerError(res.status, bdy);
216
+
213
217
  throw new Err(res.status, null, bdy || `Status Code: ${res.status}`);
214
218
  }
215
219
 
@@ -219,7 +223,7 @@ export default class TAKAPI {
219
223
  return await res.text();
220
224
  }
221
225
  } catch (err) {
222
- if (err instanceof Error && err.name === 'PublicError') throw err;
226
+ if (err instanceof Err) throw err;
223
227
  throw new Err(400, null, formatFetchError(err));
224
228
  }
225
229
  }