@tak-ps/node-tak 12.23.0 → 12.24.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 +13 -0
- package/dist/lib/api/credentials.d.ts +3 -1
- package/dist/lib/api/credentials.js +23 -10
- package/dist/lib/api/credentials.js.map +1 -1
- package/dist/lib/api/mission-layer.d.ts +5 -6
- package/dist/lib/api/mission-layer.js +10 -4
- package/dist/lib/api/mission-layer.js.map +1 -1
- package/dist/lib/api/mission.d.ts +436 -2
- package/dist/lib/api/mission.js +38 -11
- package/dist/lib/api/mission.js.map +1 -1
- package/dist/test/mission-layer.test.d.ts +1 -0
- package/dist/test/mission-layer.test.js +105 -0
- package/dist/test/mission-layer.test.js.map +1 -0
- package/dist/test/mission.test.d.ts +1 -0
- package/dist/test/mission.test.js +137 -0
- package/dist/test/mission.test.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/lib/api/credentials.ts +23 -9
- package/lib/api/mission-layer.ts +12 -7
- package/lib/api/mission.ts +40 -11
- package/package.json +3 -3
- package/test/mission-layer.test.ts +137 -0
- package/test/mission.test.ts +173 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tak-ps/node-tak",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "12.
|
|
4
|
+
"version": "12.24.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",
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
"@tak-ps/xml-js": "^2.0.3"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@tak-ps/node-cot": "^
|
|
40
|
+
"@tak-ps/node-cot": "^14.52.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@eslint/js": "^10.0.1",
|
|
44
44
|
"@inquirer/prompts": "^8.0.1",
|
|
45
45
|
"@types/node": "^26.0.0",
|
|
46
46
|
"@types/pem": "^1.14.4",
|
|
47
|
-
"c8": "^
|
|
47
|
+
"c8": "^12.0.0",
|
|
48
48
|
"eslint": "^10.0.0",
|
|
49
49
|
"tsx": "^4.19.4",
|
|
50
50
|
"typedoc": "^0.28.1",
|
|
@@ -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 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
|
+
test('MissionLayer.attachUids files UIDs under a layer via the contents paths body', async () => {
|
|
21
|
+
const originalRequest = Client.prototype.request;
|
|
22
|
+
|
|
23
|
+
let captured: RequestArgs | undefined;
|
|
24
|
+
|
|
25
|
+
Client.prototype.request = async function(opts: RequestArgs): Promise<Dispatcher.ResponseData> {
|
|
26
|
+
captured = opts;
|
|
27
|
+
return mockResponse(200, 'text/plain', '');
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
|
|
32
|
+
|
|
33
|
+
await api.MissionLayer.attachUids('mission-1', 'layer-1', {
|
|
34
|
+
uids: ['uid-1', 'uid-2'],
|
|
35
|
+
creatorUid: 'alice'
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
assert.ok(captured);
|
|
39
|
+
assert.equal(captured.method, 'PUT');
|
|
40
|
+
assert.equal(captured.path, '/Marti/api/missions/mission-1/contents?creatorUid=alice');
|
|
41
|
+
assert.deepEqual(JSON.parse(String(captured.body)), {
|
|
42
|
+
paths: {
|
|
43
|
+
'layer-1': [{ uids: ['uid-1', 'uid-2'] }]
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
} finally {
|
|
47
|
+
Client.prototype.request = originalRequest;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('MissionLayer.attachUids uses the guid contents endpoint for GUID missions', async () => {
|
|
52
|
+
const originalRequest = Client.prototype.request;
|
|
53
|
+
|
|
54
|
+
let captured: RequestArgs | undefined;
|
|
55
|
+
|
|
56
|
+
Client.prototype.request = async function(opts: RequestArgs): Promise<Dispatcher.ResponseData> {
|
|
57
|
+
captured = opts;
|
|
58
|
+
return mockResponse(200, 'text/plain', '');
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
|
|
63
|
+
|
|
64
|
+
await api.MissionLayer.attachUids('11111111-2222-3333-4444-555555555555', 'layer-1', {
|
|
65
|
+
uids: ['uid-1'],
|
|
66
|
+
creatorUid: 'alice'
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
assert.ok(captured);
|
|
70
|
+
assert.equal(captured.method, 'PUT');
|
|
71
|
+
assert.equal(
|
|
72
|
+
captured.path,
|
|
73
|
+
'/Marti/api/missions/guid/11111111-2222-3333-4444-555555555555/contents?creatorUid=alice'
|
|
74
|
+
);
|
|
75
|
+
} finally {
|
|
76
|
+
Client.prototype.request = originalRequest;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('MissionLayer.setParent re-parents layers via query params', async () => {
|
|
81
|
+
const originalRequest = Client.prototype.request;
|
|
82
|
+
|
|
83
|
+
let captured: RequestArgs | undefined;
|
|
84
|
+
|
|
85
|
+
Client.prototype.request = async function(opts: RequestArgs): Promise<Dispatcher.ResponseData> {
|
|
86
|
+
captured = opts;
|
|
87
|
+
return mockResponse(200, 'text/plain', '');
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
|
|
92
|
+
|
|
93
|
+
await api.MissionLayer.setParent('mission-1', {
|
|
94
|
+
layerUids: ['uid-1', 'uid-2'],
|
|
95
|
+
parentUid: 'layer-2',
|
|
96
|
+
creatorUid: 'alice'
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
assert.ok(captured);
|
|
100
|
+
assert.equal(captured.method, 'PUT');
|
|
101
|
+
assert.equal(
|
|
102
|
+
captured.path,
|
|
103
|
+
'/Marti/api/missions/mission-1/layers/parent?layerUid=uid-1&layerUid=uid-2&parentUid=layer-2&creatorUid=alice'
|
|
104
|
+
);
|
|
105
|
+
} finally {
|
|
106
|
+
Client.prototype.request = originalRequest;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('MissionLayer.setParent omits parentUid to move a layer to the mission root', async () => {
|
|
111
|
+
const originalRequest = Client.prototype.request;
|
|
112
|
+
|
|
113
|
+
let captured: RequestArgs | undefined;
|
|
114
|
+
|
|
115
|
+
Client.prototype.request = async function(opts: RequestArgs): Promise<Dispatcher.ResponseData> {
|
|
116
|
+
captured = opts;
|
|
117
|
+
return mockResponse(200, 'text/plain', '');
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
|
|
122
|
+
|
|
123
|
+
await api.MissionLayer.setParent('11111111-2222-3333-4444-555555555555', {
|
|
124
|
+
layerUids: ['uid-1'],
|
|
125
|
+
creatorUid: 'alice'
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
assert.ok(captured);
|
|
129
|
+
assert.equal(captured.method, 'PUT');
|
|
130
|
+
assert.equal(
|
|
131
|
+
captured.path,
|
|
132
|
+
'/Marti/api/missions/guid/11111111-2222-3333-4444-555555555555/layers/parent?layerUid=uid-1&creatorUid=alice'
|
|
133
|
+
);
|
|
134
|
+
} finally {
|
|
135
|
+
Client.prototype.request = originalRequest;
|
|
136
|
+
}
|
|
137
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
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
|
+
import { MissionSubscriberRole } from '../lib/api/mission.js';
|
|
9
|
+
|
|
10
|
+
type RequestArgs = Parameters<Client['request']>[0];
|
|
11
|
+
|
|
12
|
+
function mockResponse(statusCode: number, contentType: string, body: string): Dispatcher.ResponseData {
|
|
13
|
+
return {
|
|
14
|
+
statusCode,
|
|
15
|
+
headers: { 'content-type': contentType },
|
|
16
|
+
body: Readable.from([Buffer.from(body)]),
|
|
17
|
+
trailers: {}
|
|
18
|
+
} as unknown as Dispatcher.ResponseData;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
test('Mission.setRole sends role as a bare query param and returns void', async () => {
|
|
22
|
+
const originalRequest = Client.prototype.request;
|
|
23
|
+
|
|
24
|
+
let captured: RequestArgs | undefined;
|
|
25
|
+
|
|
26
|
+
Client.prototype.request = async function(opts: RequestArgs): Promise<Dispatcher.ResponseData> {
|
|
27
|
+
captured = opts;
|
|
28
|
+
return mockResponse(200, 'text/plain', '');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
|
|
33
|
+
|
|
34
|
+
const res = await api.Mission.setRole('mission-1', {
|
|
35
|
+
clientUid: 'client-1',
|
|
36
|
+
username: 'alice',
|
|
37
|
+
role: MissionSubscriberRole.MISSION_OWNER
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
assert.equal(res, undefined);
|
|
41
|
+
assert.ok(captured);
|
|
42
|
+
assert.equal(captured.method, 'PUT');
|
|
43
|
+
assert.equal(
|
|
44
|
+
captured.path,
|
|
45
|
+
'/Marti/api/missions/mission-1/role?clientUid=client-1&username=alice&role=MISSION_OWNER'
|
|
46
|
+
);
|
|
47
|
+
} finally {
|
|
48
|
+
Client.prototype.request = originalRequest;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('Mission.subscriptionRoles parses a list of subscribers with roles', async () => {
|
|
53
|
+
const originalRequest = Client.prototype.request;
|
|
54
|
+
|
|
55
|
+
Client.prototype.request = async function(): Promise<Dispatcher.ResponseData> {
|
|
56
|
+
return mockResponse(200, 'application/json', JSON.stringify({
|
|
57
|
+
data: [{
|
|
58
|
+
clientUid: 'client-1',
|
|
59
|
+
username: 'alice',
|
|
60
|
+
createTime: '2026-01-01T00:00:00.000Z',
|
|
61
|
+
role: {
|
|
62
|
+
permissions: ['MISSION_READ', 'MISSION_WRITE'],
|
|
63
|
+
type: 'MISSION_SUBSCRIBER'
|
|
64
|
+
}
|
|
65
|
+
}]
|
|
66
|
+
}));
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
|
|
71
|
+
|
|
72
|
+
const res = await api.Mission.subscriptionRoles('mission-1');
|
|
73
|
+
|
|
74
|
+
assert.equal(res.data.length, 1);
|
|
75
|
+
assert.equal(res.data[0].username, 'alice');
|
|
76
|
+
assert.equal(res.data[0].role.type, 'MISSION_SUBSCRIBER');
|
|
77
|
+
} finally {
|
|
78
|
+
Client.prototype.request = originalRequest;
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('Mission.unsubscribe issues a DELETE with uid query param', async () => {
|
|
83
|
+
const originalRequest = Client.prototype.request;
|
|
84
|
+
|
|
85
|
+
let captured: RequestArgs | undefined;
|
|
86
|
+
|
|
87
|
+
Client.prototype.request = async function(opts: RequestArgs): Promise<Dispatcher.ResponseData> {
|
|
88
|
+
captured = opts;
|
|
89
|
+
return mockResponse(200, 'text/plain', '');
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
|
|
94
|
+
|
|
95
|
+
await api.Mission.unsubscribe('mission-1', { uid: 'client-1' });
|
|
96
|
+
|
|
97
|
+
assert.ok(captured);
|
|
98
|
+
assert.equal(captured.method, 'DELETE');
|
|
99
|
+
assert.equal(captured.path, '/Marti/api/missions/mission-1/subscription?uid=client-1');
|
|
100
|
+
} finally {
|
|
101
|
+
Client.prototype.request = originalRequest;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('Mission.latestFeats parses each CoT individually and isolates invalid CoTs', async () => {
|
|
106
|
+
const originalRequest = Client.prototype.request;
|
|
107
|
+
|
|
108
|
+
let captured: RequestArgs | undefined;
|
|
109
|
+
|
|
110
|
+
Client.prototype.request = async function(opts: RequestArgs): Promise<Dispatcher.ResponseData> {
|
|
111
|
+
captured = opts;
|
|
112
|
+
return mockResponse(200, 'application/xml', `
|
|
113
|
+
<events>
|
|
114
|
+
<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">
|
|
115
|
+
<point lat="1.1" lon="2.2" hae="0.0" ce="9999999.0" le="9999999.0"/>
|
|
116
|
+
<detail>
|
|
117
|
+
<contact callsign="ALPHA"/>
|
|
118
|
+
</detail>
|
|
119
|
+
</event>
|
|
120
|
+
<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">
|
|
121
|
+
<detail>
|
|
122
|
+
<contact callsign="BRAVO"/>
|
|
123
|
+
</detail>
|
|
124
|
+
</event>
|
|
125
|
+
</events>
|
|
126
|
+
`);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
|
|
131
|
+
|
|
132
|
+
const res = await api.Mission.latestFeats('mission-1');
|
|
133
|
+
|
|
134
|
+
assert.ok(captured);
|
|
135
|
+
assert.equal(captured.method, 'GET');
|
|
136
|
+
assert.equal(captured.path, '/Marti/api/missions/mission-1/cot');
|
|
137
|
+
|
|
138
|
+
assert.equal(res.features.length, 1);
|
|
139
|
+
assert.equal(res.features[0].id, 'uid-valid');
|
|
140
|
+
assert.equal(res.features[0].properties.callsign, 'ALPHA');
|
|
141
|
+
|
|
142
|
+
assert.equal(res.invalid.length, 1);
|
|
143
|
+
assert.ok(res.invalid[0].error.length > 0);
|
|
144
|
+
|
|
145
|
+
const event = res.invalid[0].feature as {
|
|
146
|
+
_attributes?: Record<string, unknown>;
|
|
147
|
+
detail?: { contact?: { _attributes?: Record<string, unknown> } };
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
assert.equal(event._attributes?.uid, 'uid-poisoned');
|
|
151
|
+
assert.equal(event.detail?.contact?._attributes?.callsign, 'BRAVO');
|
|
152
|
+
} finally {
|
|
153
|
+
Client.prototype.request = originalRequest;
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('Mission.latestFeats returns empty results for an empty document', async () => {
|
|
158
|
+
const originalRequest = Client.prototype.request;
|
|
159
|
+
|
|
160
|
+
Client.prototype.request = async function(): Promise<Dispatcher.ResponseData> {
|
|
161
|
+
return mockResponse(200, 'application/xml', '<events></events>');
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const api = new TAKAPI(new URL('https://tak.example.com'), new APIAuthCertificate('cert', 'key'));
|
|
166
|
+
|
|
167
|
+
const res = await api.Mission.latestFeats('mission-1');
|
|
168
|
+
|
|
169
|
+
assert.deepEqual(res, { features: [], invalid: [] });
|
|
170
|
+
} finally {
|
|
171
|
+
Client.prototype.request = originalRequest;
|
|
172
|
+
}
|
|
173
|
+
});
|