poi-plugin-mcp 0.2.16 → 0.2.21
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/README.md +290 -201
- package/index.js +3 -0
- package/lib/bridge-controller.js +75 -1
- package/lib/fleet-metrics.js +249 -0
- package/lib/poi-action-events.js +405 -292
- package/lib/poi-api-responses.js +165 -0
- package/lib/poi-auto-interaction-recorder.js +399 -0
- package/lib/poi-data-query.js +254 -0
- package/lib/poi-http-bridge.js +1673 -1320
- package/lib/poi-input.js +366 -302
- package/lib/poi-interaction-recorder.js +1693 -0
- package/lib/poi-telemetry.js +515 -439
- package/lib/poi-webview-runtime.js +1181 -0
- package/lib/settings-view.js +112 -0
- package/lib/settings.js +8 -0
- package/mcp-server.js +542 -456
- package/package.json +3 -3
package/lib/poi-action-events.js
CHANGED
|
@@ -1,292 +1,405 @@
|
|
|
1
|
-
const crypto = require('node:crypto')
|
|
2
|
-
|
|
3
|
-
const DEFAULT_CAPACITY = 256
|
|
4
|
-
const DEFAULT_LIMIT = 64
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
'/kcsapi/
|
|
17
|
-
'/kcsapi/
|
|
18
|
-
'/kcsapi/api_get_member/
|
|
19
|
-
'/kcsapi/api_get_member/
|
|
20
|
-
'/kcsapi/api_get_member/
|
|
21
|
-
'/kcsapi/api_get_member/
|
|
22
|
-
'/kcsapi/api_get_member/
|
|
23
|
-
'/kcsapi/api_get_member/
|
|
24
|
-
'/kcsapi/api_get_member/
|
|
25
|
-
'/kcsapi/api_get_member/
|
|
26
|
-
'/kcsapi/api_get_member/
|
|
27
|
-
'/kcsapi/api_get_member/
|
|
28
|
-
'/kcsapi/
|
|
29
|
-
'/kcsapi/
|
|
30
|
-
'/kcsapi/
|
|
31
|
-
'/kcsapi/
|
|
32
|
-
'/kcsapi/
|
|
33
|
-
'/kcsapi/
|
|
34
|
-
'/kcsapi/
|
|
35
|
-
'/kcsapi/
|
|
36
|
-
'/kcsapi/
|
|
37
|
-
'/kcsapi/
|
|
38
|
-
'/kcsapi/
|
|
39
|
-
'/kcsapi/
|
|
40
|
-
'/kcsapi/
|
|
41
|
-
'/kcsapi/
|
|
42
|
-
'/kcsapi/api_req_kaisou/
|
|
43
|
-
'/kcsapi/
|
|
44
|
-
'/kcsapi/
|
|
45
|
-
'/kcsapi/
|
|
46
|
-
'/kcsapi/
|
|
47
|
-
'/kcsapi/
|
|
48
|
-
'/kcsapi/api_req_kousyou/
|
|
49
|
-
'/kcsapi/api_req_kousyou/
|
|
50
|
-
'/kcsapi/
|
|
51
|
-
'/kcsapi/
|
|
52
|
-
'/kcsapi/
|
|
53
|
-
'/kcsapi/
|
|
54
|
-
'/kcsapi/
|
|
55
|
-
'/kcsapi/
|
|
56
|
-
'/kcsapi/
|
|
57
|
-
'/kcsapi/
|
|
58
|
-
'/kcsapi/
|
|
59
|
-
'/kcsapi/
|
|
60
|
-
'/kcsapi/
|
|
61
|
-
'/kcsapi/
|
|
62
|
-
'/kcsapi/
|
|
63
|
-
'/kcsapi/
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
'/kcsapi/
|
|
68
|
-
'/kcsapi/
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
'/kcsapi/
|
|
75
|
-
'/kcsapi/
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
'/kcsapi/
|
|
80
|
-
'/kcsapi/
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
)
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
)
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
)
|
|
278
|
-
}
|
|
279
|
-
return
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
function
|
|
283
|
-
return
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
1
|
+
const crypto = require('node:crypto')
|
|
2
|
+
|
|
3
|
+
const DEFAULT_CAPACITY = 256
|
|
4
|
+
const DEFAULT_LIMIT = 64
|
|
5
|
+
const DEFAULT_WAIT_TIMEOUT_MS = 30_000
|
|
6
|
+
const MAX_CAPACITY = 256
|
|
7
|
+
const MAX_WAIT_TIMEOUT_MS = 60_000
|
|
8
|
+
const DEFAULT_MAX_WAITERS = 128
|
|
9
|
+
const MAX_WAITERS = 256
|
|
10
|
+
const MAX_OBJECT_KEYS = 32
|
|
11
|
+
const MAX_ARRAY_ITEMS = 16
|
|
12
|
+
const MAX_STRING_LENGTH = 256
|
|
13
|
+
const MAX_RESPONSE_DEPTH = 4
|
|
14
|
+
|
|
15
|
+
const EXACT_PATHS = new Set([
|
|
16
|
+
'/kcsapi/api_start2/getData',
|
|
17
|
+
'/kcsapi/api_port/port',
|
|
18
|
+
'/kcsapi/api_get_member/base_air_corps',
|
|
19
|
+
'/kcsapi/api_get_member/deck',
|
|
20
|
+
'/kcsapi/api_get_member/kdock',
|
|
21
|
+
'/kcsapi/api_get_member/mapinfo',
|
|
22
|
+
'/kcsapi/api_get_member/material',
|
|
23
|
+
'/kcsapi/api_get_member/ndock',
|
|
24
|
+
'/kcsapi/api_get_member/practice',
|
|
25
|
+
'/kcsapi/api_get_member/questlist',
|
|
26
|
+
'/kcsapi/api_get_member/require_info',
|
|
27
|
+
'/kcsapi/api_get_member/ship2',
|
|
28
|
+
'/kcsapi/api_get_member/ship3',
|
|
29
|
+
'/kcsapi/api_get_member/slot_item',
|
|
30
|
+
'/kcsapi/api_get_member/unsetslot',
|
|
31
|
+
'/kcsapi/api_get_member/useitem',
|
|
32
|
+
'/kcsapi/api_req_air_corps/set_action',
|
|
33
|
+
'/kcsapi/api_req_air_corps/change_deployment_base',
|
|
34
|
+
'/kcsapi/api_req_air_corps/set_plane',
|
|
35
|
+
'/kcsapi/api_req_air_corps/supply',
|
|
36
|
+
'/kcsapi/api_req_battle_midnight/battle',
|
|
37
|
+
'/kcsapi/api_req_battle_midnight/sp_midnight',
|
|
38
|
+
'/kcsapi/api_req_hensei/change',
|
|
39
|
+
'/kcsapi/api_req_hensei/combined',
|
|
40
|
+
'/kcsapi/api_req_hensei/preset_select',
|
|
41
|
+
'/kcsapi/api_req_hokyu/charge',
|
|
42
|
+
'/kcsapi/api_req_kaisou/powerup',
|
|
43
|
+
'/kcsapi/api_req_kaisou/slot_select',
|
|
44
|
+
'/kcsapi/api_req_kaisou/slot_deprive',
|
|
45
|
+
'/kcsapi/api_req_kaisou/slotset',
|
|
46
|
+
'/kcsapi/api_req_kaisou/slotset_ex',
|
|
47
|
+
'/kcsapi/api_req_kaisou/unsetslot_all',
|
|
48
|
+
'/kcsapi/api_req_kousyou/createitem',
|
|
49
|
+
'/kcsapi/api_req_kousyou/createship',
|
|
50
|
+
'/kcsapi/api_req_kousyou/createship_speedchange',
|
|
51
|
+
'/kcsapi/api_req_kousyou/destroyitem2',
|
|
52
|
+
'/kcsapi/api_req_kousyou/destroyship',
|
|
53
|
+
'/kcsapi/api_req_kousyou/getship',
|
|
54
|
+
'/kcsapi/api_req_kousyou/remodel_slot',
|
|
55
|
+
'/kcsapi/api_req_map/next',
|
|
56
|
+
'/kcsapi/api_req_map/select_eventmap_rank',
|
|
57
|
+
'/kcsapi/api_req_map/start',
|
|
58
|
+
'/kcsapi/api_req_map/start_air_base',
|
|
59
|
+
'/kcsapi/api_req_member/get_practice_enemyinfo',
|
|
60
|
+
'/kcsapi/api_req_mission/result',
|
|
61
|
+
'/kcsapi/api_req_mission/start',
|
|
62
|
+
'/kcsapi/api_req_nyukyo/speedchange',
|
|
63
|
+
'/kcsapi/api_req_nyukyo/start',
|
|
64
|
+
'/kcsapi/api_req_practice/battle',
|
|
65
|
+
'/kcsapi/api_req_practice/battle_result',
|
|
66
|
+
'/kcsapi/api_req_practice/midnight_battle',
|
|
67
|
+
'/kcsapi/api_req_quest/clearitemget',
|
|
68
|
+
'/kcsapi/api_req_quest/start',
|
|
69
|
+
'/kcsapi/api_req_quest/stop',
|
|
70
|
+
'/kcsapi/api_req_quest/clearitemget',
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
const ALLOWED_PREFIXES = [
|
|
74
|
+
'/kcsapi/api_req_sortie/',
|
|
75
|
+
'/kcsapi/api_req_combined_battle/',
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
const SHARED_TELEMETRY_TIMESTAMP_PATHS = new Set([
|
|
79
|
+
'/kcsapi/api_get_member/questlist',
|
|
80
|
+
'/kcsapi/api_get_member/unsetslot',
|
|
81
|
+
'/kcsapi/api_req_quest/start',
|
|
82
|
+
'/kcsapi/api_req_quest/stop',
|
|
83
|
+
'/kcsapi/api_req_quest/clearitemget',
|
|
84
|
+
'/kcsapi/api_req_kaisou/slotset',
|
|
85
|
+
'/kcsapi/api_req_kaisou/slotset_ex',
|
|
86
|
+
'/kcsapi/api_req_kaisou/slot_deprive',
|
|
87
|
+
'/kcsapi/api_req_kaisou/unsetslot_all',
|
|
88
|
+
'/kcsapi/api_req_hensei/change',
|
|
89
|
+
])
|
|
90
|
+
|
|
91
|
+
function createPoiActionEvents(options = {}) {
|
|
92
|
+
const now = options.now || (() => new Date())
|
|
93
|
+
const inferredNow = options.inferredNow || (() => new Date())
|
|
94
|
+
const sessionId = boundedId(options.sessionId || crypto.randomUUID(), 'sessionId')
|
|
95
|
+
const capacity = boundedInteger(
|
|
96
|
+
options.capacity == null ? DEFAULT_CAPACITY : options.capacity,
|
|
97
|
+
1,
|
|
98
|
+
MAX_CAPACITY,
|
|
99
|
+
'capacity',
|
|
100
|
+
)
|
|
101
|
+
const maxWaiters = boundedInteger(
|
|
102
|
+
options.maxWaiters == null ? DEFAULT_MAX_WAITERS : options.maxWaiters,
|
|
103
|
+
1,
|
|
104
|
+
MAX_WAITERS,
|
|
105
|
+
'maxWaiters',
|
|
106
|
+
)
|
|
107
|
+
const events = []
|
|
108
|
+
const waiters = new Set()
|
|
109
|
+
let latestGeneration = 0
|
|
110
|
+
|
|
111
|
+
function capture(detail) {
|
|
112
|
+
if (!isCapturableDetail(detail)) return null
|
|
113
|
+
const explicitApiResult = readApiResult(detail)
|
|
114
|
+
const apiResult = explicitApiResult == null && hasUnwrappedResponseBody(detail)
|
|
115
|
+
? 1
|
|
116
|
+
: explicitApiResult
|
|
117
|
+
if (apiResult !== 1) return null
|
|
118
|
+
|
|
119
|
+
latestGeneration += 1
|
|
120
|
+
const eventNow = explicitApiResult != null ||
|
|
121
|
+
SHARED_TELEMETRY_TIMESTAMP_PATHS.has(detail.path)
|
|
122
|
+
? now
|
|
123
|
+
: inferredNow
|
|
124
|
+
const event = Object.freeze({
|
|
125
|
+
generation: latestGeneration,
|
|
126
|
+
capturedAt: timestamp(eventNow()),
|
|
127
|
+
path: detail.path,
|
|
128
|
+
apiResult,
|
|
129
|
+
postBody: sanitizePostBody(detail.postBody),
|
|
130
|
+
responseSummary: sanitizeResponse(detail.body),
|
|
131
|
+
})
|
|
132
|
+
events.push(event)
|
|
133
|
+
if (events.length > capacity) events.splice(0, events.length - capacity)
|
|
134
|
+
notifyWaiters()
|
|
135
|
+
return event
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function read(options = {}) {
|
|
139
|
+
const after = boundedInteger(
|
|
140
|
+
options.after == null ? 0 : options.after,
|
|
141
|
+
0,
|
|
142
|
+
Number.MAX_SAFE_INTEGER,
|
|
143
|
+
'after',
|
|
144
|
+
)
|
|
145
|
+
const limit = boundedInteger(
|
|
146
|
+
options.limit == null ? DEFAULT_LIMIT : options.limit,
|
|
147
|
+
1,
|
|
148
|
+
MAX_CAPACITY,
|
|
149
|
+
'limit',
|
|
150
|
+
)
|
|
151
|
+
return {
|
|
152
|
+
available: true,
|
|
153
|
+
sessionId,
|
|
154
|
+
latestGeneration,
|
|
155
|
+
events: events
|
|
156
|
+
.filter((event) => event.generation > after)
|
|
157
|
+
.slice(0, limit),
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function wait(options = {}) {
|
|
162
|
+
const after = boundedInteger(
|
|
163
|
+
options.after == null ? 0 : options.after,
|
|
164
|
+
0,
|
|
165
|
+
Number.MAX_SAFE_INTEGER,
|
|
166
|
+
'after',
|
|
167
|
+
)
|
|
168
|
+
const limit = boundedInteger(
|
|
169
|
+
options.limit == null ? DEFAULT_LIMIT : options.limit,
|
|
170
|
+
1,
|
|
171
|
+
MAX_CAPACITY,
|
|
172
|
+
'limit',
|
|
173
|
+
)
|
|
174
|
+
const timeoutMs = boundedInteger(
|
|
175
|
+
options.timeoutMs == null ? DEFAULT_WAIT_TIMEOUT_MS : options.timeoutMs,
|
|
176
|
+
1,
|
|
177
|
+
MAX_WAIT_TIMEOUT_MS,
|
|
178
|
+
'timeoutMs',
|
|
179
|
+
)
|
|
180
|
+
const signal = options.signal
|
|
181
|
+
if (
|
|
182
|
+
signal != null &&
|
|
183
|
+
(
|
|
184
|
+
typeof signal !== 'object' ||
|
|
185
|
+
typeof signal.addEventListener !== 'function' ||
|
|
186
|
+
typeof signal.removeEventListener !== 'function'
|
|
187
|
+
)
|
|
188
|
+
) {
|
|
189
|
+
throw new Error('signal must be an AbortSignal')
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const immediate = read({ after, limit })
|
|
193
|
+
if (immediate.events.length > 0) {
|
|
194
|
+
return Promise.resolve({ ...immediate, timedOut: false })
|
|
195
|
+
}
|
|
196
|
+
if (signal && signal.aborted) return Promise.reject(createAbortError())
|
|
197
|
+
if (waiters.size >= maxWaiters) {
|
|
198
|
+
const error = new Error('Too many action event waiters.')
|
|
199
|
+
error.code = 'ACTION_EVENT_WAITERS_LIMIT'
|
|
200
|
+
return Promise.reject(error)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
const waiter = {
|
|
205
|
+
after,
|
|
206
|
+
limit,
|
|
207
|
+
resolve,
|
|
208
|
+
reject,
|
|
209
|
+
signal,
|
|
210
|
+
onAbort: null,
|
|
211
|
+
timer: null,
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const settle = (settlement) => {
|
|
215
|
+
if (!waiters.delete(waiter)) return
|
|
216
|
+
if (waiter.timer != null) clearTimeout(waiter.timer)
|
|
217
|
+
if (waiter.signal && waiter.onAbort) {
|
|
218
|
+
waiter.signal.removeEventListener('abort', waiter.onAbort)
|
|
219
|
+
}
|
|
220
|
+
settlement()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
waiter.resolveWait = (result) => settle(() => resolve(result))
|
|
224
|
+
waiter.rejectWait = (error) => settle(() => reject(error))
|
|
225
|
+
waiter.timer = setTimeout(() => {
|
|
226
|
+
waiter.resolveWait({ ...read({ after, limit }), timedOut: true })
|
|
227
|
+
}, timeoutMs)
|
|
228
|
+
if (signal) {
|
|
229
|
+
waiter.onAbort = () => waiter.rejectWait(createAbortError())
|
|
230
|
+
signal.addEventListener('abort', waiter.onAbort, { once: true })
|
|
231
|
+
}
|
|
232
|
+
waiters.add(waiter)
|
|
233
|
+
})
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function notifyWaiters() {
|
|
237
|
+
if (waiters.size === 0) return
|
|
238
|
+
for (const waiter of Array.from(waiters)) {
|
|
239
|
+
if (latestGeneration <= waiter.after) continue
|
|
240
|
+
waiter.resolveWait({
|
|
241
|
+
...read({ after: waiter.after, limit: waiter.limit }),
|
|
242
|
+
timedOut: false,
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function close(reason = createAbortError('Action event provider closed.')) {
|
|
248
|
+
for (const waiter of Array.from(waiters)) waiter.rejectWait(reason)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return Object.freeze({ capture, read, wait, close })
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function isCapturableDetail(detail) {
|
|
255
|
+
return Boolean(
|
|
256
|
+
detail &&
|
|
257
|
+
typeof detail === 'object' &&
|
|
258
|
+
!Array.isArray(detail) &&
|
|
259
|
+
typeof detail.path === 'string' &&
|
|
260
|
+
(
|
|
261
|
+
EXACT_PATHS.has(detail.path) ||
|
|
262
|
+
ALLOWED_PREFIXES.some((prefix) => detail.path.startsWith(prefix))
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function readApiResult(detail) {
|
|
268
|
+
const candidates = [
|
|
269
|
+
detail.apiResult,
|
|
270
|
+
detail.api_result,
|
|
271
|
+
detail.result,
|
|
272
|
+
detail.body && detail.body.api_result,
|
|
273
|
+
]
|
|
274
|
+
for (const candidate of candidates) {
|
|
275
|
+
if (candidate == null || candidate === '') continue
|
|
276
|
+
const parsed = Number(candidate)
|
|
277
|
+
return Number.isInteger(parsed) ? parsed : null
|
|
278
|
+
}
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function hasUnwrappedResponseBody(detail) {
|
|
283
|
+
if (!Object.prototype.hasOwnProperty.call(detail, 'body')) return false
|
|
284
|
+
const body = detail.body
|
|
285
|
+
if (!isPlainObject(body) && !Array.isArray(body)) return false
|
|
286
|
+
return !(
|
|
287
|
+
isPlainObject(body) &&
|
|
288
|
+
(
|
|
289
|
+
Object.prototype.hasOwnProperty.call(body, 'api_result') ||
|
|
290
|
+
Object.prototype.hasOwnProperty.call(body, 'api_data')
|
|
291
|
+
)
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function sanitizePostBody(value) {
|
|
296
|
+
if (!isPlainObject(value)) return {}
|
|
297
|
+
const output = {}
|
|
298
|
+
for (const [key, item] of Object.entries(value).slice(0, MAX_OBJECT_KEYS)) {
|
|
299
|
+
if (!safeKey(key)) continue
|
|
300
|
+
const scalar = sanitizeScalar(item)
|
|
301
|
+
if (scalar !== undefined) {
|
|
302
|
+
output[key] = scalar
|
|
303
|
+
continue
|
|
304
|
+
}
|
|
305
|
+
if (
|
|
306
|
+
Array.isArray(item) &&
|
|
307
|
+
item.length <= MAX_ARRAY_ITEMS
|
|
308
|
+
) {
|
|
309
|
+
const array = item.map(sanitizeScalar)
|
|
310
|
+
if (array.every((entry) => entry !== undefined)) output[key] = array
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return output
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function sanitizeResponse(value) {
|
|
317
|
+
const sanitized = sanitizeNested(value, 0)
|
|
318
|
+
return isPlainObject(sanitized) ? sanitized : {}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function sanitizeNested(value, depth) {
|
|
322
|
+
const scalar = sanitizeScalar(value)
|
|
323
|
+
if (scalar !== undefined) return scalar
|
|
324
|
+
if (depth >= MAX_RESPONSE_DEPTH) return undefined
|
|
325
|
+
if (Array.isArray(value)) {
|
|
326
|
+
return value
|
|
327
|
+
.slice(0, MAX_ARRAY_ITEMS)
|
|
328
|
+
.map((item) => sanitizeNested(item, depth + 1))
|
|
329
|
+
.filter((item) => item !== undefined)
|
|
330
|
+
}
|
|
331
|
+
if (!isPlainObject(value)) return undefined
|
|
332
|
+
const output = {}
|
|
333
|
+
for (const [key, item] of Object.entries(value).slice(0, MAX_OBJECT_KEYS)) {
|
|
334
|
+
if (!safeKey(key)) continue
|
|
335
|
+
const sanitized = sanitizeNested(item, depth + 1)
|
|
336
|
+
if (sanitized !== undefined) output[key] = sanitized
|
|
337
|
+
}
|
|
338
|
+
return output
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function sanitizeScalar(value) {
|
|
342
|
+
if (
|
|
343
|
+
value === null ||
|
|
344
|
+
typeof value === 'boolean' ||
|
|
345
|
+
(typeof value === 'number' && Number.isFinite(value))
|
|
346
|
+
) {
|
|
347
|
+
return value
|
|
348
|
+
}
|
|
349
|
+
if (typeof value === 'string') return value.slice(0, MAX_STRING_LENGTH)
|
|
350
|
+
return undefined
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function safeKey(key) {
|
|
354
|
+
return (
|
|
355
|
+
typeof key === 'string' &&
|
|
356
|
+
key.length > 0 &&
|
|
357
|
+
key.length <= 64 &&
|
|
358
|
+
!/(authorization|cookie|header|password|secret|token)/i.test(key)
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function timestamp(value) {
|
|
363
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
364
|
+
if (!Number.isFinite(date.getTime())) throw new Error('now must return a valid date')
|
|
365
|
+
return date.toISOString()
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function boundedId(value, name) {
|
|
369
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 256) {
|
|
370
|
+
throw new Error(`${name} must be a non-empty bounded string`)
|
|
371
|
+
}
|
|
372
|
+
return value
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function boundedInteger(value, minimum, maximum, name) {
|
|
376
|
+
if (
|
|
377
|
+
!Number.isSafeInteger(value) ||
|
|
378
|
+
value < minimum ||
|
|
379
|
+
value > maximum
|
|
380
|
+
) {
|
|
381
|
+
throw new Error(
|
|
382
|
+
`${name} must be an integer from ${minimum} through ${maximum}`,
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
return value
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function createAbortError(message = 'Action event wait aborted.') {
|
|
389
|
+
const error = new Error(message)
|
|
390
|
+
error.name = 'AbortError'
|
|
391
|
+
error.code = 'ABORT_ERR'
|
|
392
|
+
return error
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function isPlainObject(value) {
|
|
396
|
+
return Boolean(
|
|
397
|
+
value &&
|
|
398
|
+
typeof value === 'object' &&
|
|
399
|
+
!Array.isArray(value),
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
module.exports = {
|
|
404
|
+
createPoiActionEvents,
|
|
405
|
+
}
|