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.
@@ -0,0 +1,1181 @@
1
+ const crypto = require('node:crypto')
2
+
3
+ const MAX_DEBUG_SCRIPT_LENGTH = 32 * 1024
4
+ const MAX_RESULT_BYTES = 2 * 1024 * 1024
5
+ const MAX_PATH_SEGMENTS = 64
6
+ const MAX_TEXTURE_RECTANGLE_VALUE = 1_000_000
7
+ const MAX_TEXTURE_URL_LENGTH = 2048
8
+ const SENSITIVE_KEY = /^(?:api_token|authorization|cookies?|credentials?|loginData|password|secret|ticket|accessToken|refreshToken)$/iu
9
+ const WEBVIEW_ROOTS = Object.freeze(['globalThis', 'pixi.last-rendered'])
10
+ const WEBVIEW_FIND_PROJECTIONS = Object.freeze(['pixi.interactive'])
11
+
12
+ function createPoiWebviewRuntime(options = {}) {
13
+ const getStore = options.getStore || defaultGetStore
14
+ const resolveWebContents = options.resolveWebContents || defaultResolveWebContents
15
+ const logger = options.logger || console
16
+ const now = options.now || (() => new Date())
17
+
18
+ function currentWebContents() {
19
+ const layout = getStore('layout.webview')
20
+ if (!layout || !layout.ref) {
21
+ throw new Error('Poi game WebView is not ready')
22
+ }
23
+ if (typeof layout.ref.getWebContents === 'function') {
24
+ const webContents = layout.ref.getWebContents()
25
+ if (webContents) return webContents
26
+ }
27
+ if (typeof layout.ref.getWebContentsId === 'function') {
28
+ const id = layout.ref.getWebContentsId()
29
+ if (Number.isInteger(id) && id > 0) {
30
+ const webContents = resolveWebContents(id)
31
+ if (webContents) return webContents
32
+ }
33
+ }
34
+ throw new Error('Poi game WebView webContents is unavailable')
35
+ }
36
+
37
+ function frames() {
38
+ const webContents = currentWebContents()
39
+ const mainFrame = webContents.mainFrame
40
+ if (!mainFrame) {
41
+ return [{
42
+ id: 'main',
43
+ name: '',
44
+ url: typeof webContents.getURL === 'function' ? webContents.getURL() : '',
45
+ frame: null,
46
+ webContents,
47
+ }]
48
+ }
49
+ const candidates = Array.isArray(mainFrame.framesInSubtree)
50
+ ? mainFrame.framesInSubtree
51
+ : [mainFrame]
52
+ const unique = candidates.includes(mainFrame)
53
+ ? candidates
54
+ : [mainFrame, ...candidates]
55
+ return unique
56
+ .map((frame, index) => ({
57
+ id: frameId(frame, index),
58
+ name: typeof frame.name === 'string' ? frame.name : '',
59
+ url: typeof frame.url === 'string' ? frame.url : '',
60
+ frame,
61
+ webContents,
62
+ }))
63
+ .filter((candidate) => isKanColleGameUrl(candidate.url))
64
+ }
65
+
66
+ function selectFrame(requestedId) {
67
+ const available = frames()
68
+ if (available.length === 0) {
69
+ throw new Error('KanColle game frame is not ready')
70
+ }
71
+ if (requestedId == null || requestedId === '') {
72
+ throw new Error('frameId is required for WebView inspection')
73
+ }
74
+ const selected = available.find((candidate) => candidate.id === requestedId)
75
+ if (!selected) throw new Error(`Unknown WebView frame: ${requestedId}`)
76
+ return selected
77
+ }
78
+
79
+ // Read-only inspection scripts are synchronous IIFEs; a hang can only come
80
+ // from executeJavaScript's IPC never settling (e.g. after frame navigation).
81
+ // Bound it so the bridge fails fast instead of holding the request forever.
82
+ const EXECUTE_INSPECTION_TIMEOUT_MS = 10000
83
+
84
+ async function execute(frameInfo, expression) {
85
+ const run = () => {
86
+ if (frameInfo.frame && typeof frameInfo.frame.executeJavaScript === 'function') {
87
+ return frameInfo.frame.executeJavaScript(expression, false)
88
+ }
89
+ if (typeof frameInfo.webContents.executeJavaScript === 'function') {
90
+ return frameInfo.webContents.executeJavaScript(expression, false)
91
+ }
92
+ return Promise.reject(new Error('Poi game WebView does not support JavaScript inspection'))
93
+ }
94
+ let timer
95
+ try {
96
+ return await Promise.race([
97
+ run(),
98
+ new Promise((_, reject) => {
99
+ timer = setTimeout(
100
+ () => reject(new Error('WebView inspection timed out')),
101
+ EXECUTE_INSPECTION_TIMEOUT_MS,
102
+ )
103
+ }),
104
+ ])
105
+ } finally {
106
+ clearTimeout(timer)
107
+ }
108
+ }
109
+
110
+ async function listFrames() {
111
+ return frames().map(({ id, name, url }) => ({
112
+ id,
113
+ name,
114
+ url: publicFrameUrl(url),
115
+ }))
116
+ }
117
+
118
+ async function readStorage(request = {}) {
119
+ const storage = request.storage === 'session' ? 'sessionStorage' : 'localStorage'
120
+ const frameInfo = selectFrame(request.frameId)
121
+ const key = request.key == null ? null : boundedString(request.key, 256, 'key')
122
+ const expression = `(() => {
123
+ const storage = globalThis[${JSON.stringify(storage)}];
124
+ if (!storage) return { available: false, values: {} };
125
+ const requestedKey = ${JSON.stringify(key)};
126
+ if (requestedKey !== null) {
127
+ return {
128
+ available: true,
129
+ values: Object.prototype.hasOwnProperty.call(storage, requestedKey) ||
130
+ storage.getItem(requestedKey) !== null
131
+ ? { [requestedKey]: storage.getItem(requestedKey) }
132
+ : {},
133
+ };
134
+ }
135
+ const values = {};
136
+ for (let index = 0; index < Math.min(storage.length, 4096); index += 1) {
137
+ const itemKey = storage.key(index);
138
+ if (typeof itemKey === 'string' && itemKey.length <= 256) {
139
+ values[itemKey] = storage.getItem(itemKey);
140
+ }
141
+ }
142
+ return { available: true, values };
143
+ })()`
144
+ return boundedResult(await execute(frameInfo, expression))
145
+ }
146
+
147
+ async function readPath(request = {}) {
148
+ const path = validatePath(request.path)
149
+ const root = validateInspectionRoot(request.root)
150
+ const frameInfo = selectFrame(request.frameId)
151
+ const expression = `(() => {
152
+ const path = ${JSON.stringify(path)};
153
+ const root = ${JSON.stringify(root)};
154
+ let value;
155
+ if (root === 'globalThis') {
156
+ value = globalThis;
157
+ } else {
158
+ const ticker = globalThis.PIXI && globalThis.PIXI.ticker;
159
+ const head = ticker && ticker.shared && ticker.shared._head;
160
+ const interaction = head && head.next && head.next.context;
161
+ const renderer = interaction && interaction.renderer;
162
+ value = renderer && renderer._lastObjectRendered;
163
+ if (!value) return { available: false, reason: 'root_unavailable' };
164
+ }
165
+ for (const segment of path) {
166
+ if ((typeof value !== 'object' && typeof value !== 'function') || value === null) {
167
+ return { available: false, reason: 'non_object_parent' };
168
+ }
169
+ const descriptor = Object.getOwnPropertyDescriptor(value, segment);
170
+ if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
171
+ return { available: false, reason: 'missing_or_accessor_property' };
172
+ }
173
+ value = descriptor.value;
174
+ }
175
+ const seen = new WeakSet();
176
+ const copy = (item, depth) => {
177
+ if (item === null || typeof item === 'boolean' || typeof item === 'number') return item;
178
+ if (typeof item === 'string') return item.slice(0, 65536);
179
+ if (typeof item === 'bigint') return String(item);
180
+ if (typeof item === 'function') return { type: 'function', name: item.name || '' };
181
+ if (typeof item !== 'object') return String(item);
182
+ if (seen.has(item)) return '[Circular]';
183
+ if (depth >= 8) return '[MaxDepth]';
184
+ seen.add(item);
185
+ if (Array.isArray(item)) return item.slice(0, 2048).map((entry) => copy(entry, depth + 1));
186
+ const output = {};
187
+ for (const key of Object.getOwnPropertyNames(item).slice(0, 512)) {
188
+ const descriptor = Object.getOwnPropertyDescriptor(item, key);
189
+ if (descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
190
+ output[key] = copy(descriptor.value, depth + 1);
191
+ }
192
+ }
193
+ return output;
194
+ };
195
+ return { available: true, value: copy(value, 0) };
196
+ })()`
197
+ return boundedResult(await execute(frameInfo, expression))
198
+ }
199
+
200
+ async function findObjects(request = {}) {
201
+ const rootName = validateInspectionRoot(request.root)
202
+ const projection = validateFindProjection(request.projection)
203
+ if (projection !== 'pixi.interactive' && request.anyRequiredKeys != null) {
204
+ throw new Error('anyRequiredKeys is only supported by the pixi.interactive projection')
205
+ }
206
+ const frameInfo = selectFrame(request.frameId)
207
+ const rootPath = validatePath(request.rootPath == null ? [] : request.rootPath)
208
+ const anyRequiredKeys = request.anyRequiredKeys == null
209
+ ? []
210
+ : validateKeyList(request.anyRequiredKeys, 1, 64, 'anyRequiredKeys')
211
+ const requiredKeys = projection === 'pixi.interactive' && request.requiredKeys == null
212
+ ? []
213
+ : validateKeyList(
214
+ request.requiredKeys,
215
+ projection === 'pixi.interactive' ? 0 : 1,
216
+ 16,
217
+ 'requiredKeys',
218
+ )
219
+ const selectKeys = request.selectKeys == null
220
+ ? requiredKeys
221
+ : validateKeyList(
222
+ request.selectKeys,
223
+ projection === 'pixi.interactive' ? 0 : 1,
224
+ 64,
225
+ 'selectKeys',
226
+ )
227
+ const maxDepth = boundedInteger(request.maxDepth, 6, 0, 12, 'maxDepth')
228
+ const maxNodes = boundedInteger(request.maxNodes, 20_000, 1, 50_000, 'maxNodes')
229
+ const maxMatches = boundedInteger(request.maxMatches, 16, 1, 256, 'maxMatches')
230
+ if (projection === 'pixi.interactive') {
231
+ const capturedAt = timestamp(now())
232
+ const expression = pixiInteractiveExpression({
233
+ anyRequiredKeys,
234
+ maxDepth,
235
+ maxMatches,
236
+ maxNodes,
237
+ requiredKeys,
238
+ rootName,
239
+ rootPath,
240
+ selectKeys,
241
+ })
242
+ const projected = boundedResult(await execute(frameInfo, expression))
243
+ return boundedResult(finalizePixiInteractiveProjection(projected, {
244
+ capturedAt,
245
+ frameId: frameInfo.id,
246
+ frameUrl: publicFrameUrl(frameInfo.url),
247
+ maxMatches,
248
+ rootName,
249
+ rootPath,
250
+ }))
251
+ }
252
+ const expression = `(() => {
253
+ const rootName = ${JSON.stringify(rootName)};
254
+ const rootPath = ${JSON.stringify(rootPath)};
255
+ const requiredKeys = ${JSON.stringify(requiredKeys)};
256
+ const selectKeys = ${JSON.stringify(selectKeys)};
257
+ const maxDepth = ${maxDepth};
258
+ const maxNodes = ${maxNodes};
259
+ const maxMatches = ${maxMatches};
260
+ const blockedKeys = new Set([
261
+ 'window', 'self', 'globalThis', 'parent', 'top', 'frames',
262
+ 'document', 'ownerDocument', 'prototype', 'constructor',
263
+ 'caller', 'callee', 'arguments',
264
+ ]);
265
+ let root;
266
+ if (rootName === 'globalThis') {
267
+ root = globalThis;
268
+ } else {
269
+ const ticker = globalThis.PIXI && globalThis.PIXI.ticker;
270
+ const head = ticker && ticker.shared && ticker.shared._head;
271
+ const interaction = head && head.next && head.next.context;
272
+ const renderer = interaction && interaction.renderer;
273
+ root = renderer && renderer._lastObjectRendered;
274
+ if (!root) {
275
+ return { available: false, reason: 'root_unavailable', matches: [] };
276
+ }
277
+ }
278
+ for (const segment of rootPath) {
279
+ if ((typeof root !== 'object' && typeof root !== 'function') || root === null) {
280
+ return { available: false, reason: 'root_path_not_found', matches: [] };
281
+ }
282
+ const descriptor = Object.getOwnPropertyDescriptor(root, segment);
283
+ if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
284
+ return { available: false, reason: 'root_path_not_found', matches: [] };
285
+ }
286
+ root = descriptor.value;
287
+ }
288
+ const isObject = (value) =>
289
+ (typeof value === 'object' || typeof value === 'function') && value !== null;
290
+ if (!isObject(root)) {
291
+ return { available: false, reason: 'root_is_not_object', matches: [] };
292
+ }
293
+ const scalar = (value) => {
294
+ if (value === null || typeof value === 'boolean' || typeof value === 'number') return value;
295
+ if (typeof value === 'string') return value.slice(0, 65536);
296
+ if (typeof value === 'bigint') return String(value);
297
+ if (Array.isArray(value)) {
298
+ return value.slice(0, 20000).map((item) => {
299
+ if (item && typeof item === 'object') {
300
+ const id = Object.getOwnPropertyDescriptor(item, 'api_id');
301
+ return id && Object.prototype.hasOwnProperty.call(id, 'value')
302
+ ? { api_id: id.value }
303
+ : '[Object]';
304
+ }
305
+ return scalar(item);
306
+ });
307
+ }
308
+ if (typeof value === 'function') return { type: 'function', name: value.name || '' };
309
+ if (typeof value === 'object') {
310
+ const output = {};
311
+ for (const key of Object.getOwnPropertyNames(value).slice(0, 256)) {
312
+ if (blockedKeys.has(key)) continue;
313
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
314
+ if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) continue;
315
+ const item = descriptor.value;
316
+ if (item === null || ['boolean', 'number', 'string', 'bigint'].includes(typeof item)) {
317
+ output[key] = scalar(item);
318
+ }
319
+ }
320
+ return output;
321
+ }
322
+ return String(value);
323
+ };
324
+ const queue = [{ value: root, path: rootPath, depth: 0 }];
325
+ const seen = new WeakSet();
326
+ const matches = [];
327
+ let visited = 0;
328
+ while (queue.length > 0 && visited < maxNodes && matches.length < maxMatches) {
329
+ const current = queue.shift();
330
+ if (!isObject(current.value) || seen.has(current.value)) continue;
331
+ seen.add(current.value);
332
+ visited += 1;
333
+ let names;
334
+ try {
335
+ names = Object.getOwnPropertyNames(current.value).slice(0, 2048);
336
+ } catch (_) {
337
+ continue;
338
+ }
339
+ if (requiredKeys.every((key) => names.includes(key))) {
340
+ const selected = {};
341
+ for (const key of selectKeys) {
342
+ const descriptor = Object.getOwnPropertyDescriptor(current.value, key);
343
+ if (descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value')) {
344
+ selected[key] = scalar(descriptor.value);
345
+ }
346
+ }
347
+ matches.push({ path: current.path, selected });
348
+ }
349
+ if (current.depth >= maxDepth) continue;
350
+ for (const key of names) {
351
+ if (blockedKeys.has(key) || key.length > 256) continue;
352
+ let descriptor;
353
+ try {
354
+ descriptor = Object.getOwnPropertyDescriptor(current.value, key);
355
+ } catch (_) {
356
+ continue;
357
+ }
358
+ if (!descriptor || !Object.prototype.hasOwnProperty.call(descriptor, 'value')) continue;
359
+ const child = descriptor.value;
360
+ if (!isObject(child) || seen.has(child)) continue;
361
+ if (child.nodeType || child.ownerDocument) continue;
362
+ queue.push({
363
+ value: child,
364
+ path: [...current.path, key],
365
+ depth: current.depth + 1,
366
+ });
367
+ }
368
+ }
369
+ return {
370
+ available: true,
371
+ visited,
372
+ exhausted: queue.length === 0,
373
+ matches,
374
+ };
375
+ })()`
376
+ return boundedResult(await execute(frameInfo, expression))
377
+ }
378
+
379
+ async function evaluate(request = {}) {
380
+ const script = boundedString(
381
+ request.script,
382
+ MAX_DEBUG_SCRIPT_LENGTH,
383
+ 'script',
384
+ )
385
+ const timeoutMs = boundedInteger(request.timeoutMs, 1000, 50, 5000)
386
+ const frameInfo = selectFrame(request.frameId)
387
+ const scriptHash = crypto.createHash('sha256').update(script).digest('hex')
388
+ const startedAt = timestamp(now())
389
+ const expression = `(() => {
390
+ const script = ${JSON.stringify(script)};
391
+ const timeoutMs = ${timeoutMs};
392
+ const timeout = new Promise((_, reject) => setTimeout(
393
+ () => reject(new Error('Debug evaluation timed out')), timeoutMs));
394
+ return Promise.race([
395
+ Promise.resolve().then(() => (0, eval)(script)),
396
+ timeout,
397
+ ]);
398
+ })()`
399
+ try {
400
+ const value = boundedResult(await execute(frameInfo, expression))
401
+ logger.log(
402
+ `[poi-plugin-mcp] debug eval ${scriptHash} frame=${frameInfo.id} ok`,
403
+ )
404
+ return {
405
+ ok: true,
406
+ frameId: frameInfo.id,
407
+ startedAt,
408
+ scriptHash,
409
+ value,
410
+ }
411
+ } catch (error) {
412
+ logger.error(
413
+ `[poi-plugin-mcp] debug eval ${scriptHash} frame=${frameInfo.id} failed: ${error.message}`,
414
+ )
415
+ const failure = new Error(error.message)
416
+ failure.code = 'DEBUG_EVAL_FAILED'
417
+ failure.details = { frameId: frameInfo.id, startedAt, scriptHash }
418
+ throw failure
419
+ }
420
+ }
421
+
422
+ return Object.freeze({ evaluate, findObjects, listFrames, readPath, readStorage })
423
+ }
424
+
425
+ function pixiInteractiveExpression({
426
+ anyRequiredKeys,
427
+ maxDepth,
428
+ maxMatches,
429
+ maxNodes,
430
+ requiredKeys,
431
+ rootName,
432
+ rootPath,
433
+ selectKeys,
434
+ }) {
435
+ return `(() => {
436
+ const rootName = ${JSON.stringify(rootName)};
437
+ const rootPath = ${JSON.stringify(rootPath)};
438
+ const anyRequiredKeys = ${JSON.stringify(anyRequiredKeys)};
439
+ const requiredKeys = ${JSON.stringify(requiredKeys)};
440
+ const selectKeys = ${JSON.stringify(selectKeys)};
441
+ const maxDepth = ${maxDepth};
442
+ const maxNodes = ${maxNodes};
443
+ const maxMatches = ${maxMatches};
444
+ const isObject = (value) =>
445
+ (typeof value === 'object' || typeof value === 'function') && value !== null;
446
+ const ownDescriptor = (object, key) => {
447
+ if (!isObject(object)) return null;
448
+ try {
449
+ const descriptor = Object.getOwnPropertyDescriptor(object, key);
450
+ return descriptor && Object.prototype.hasOwnProperty.call(descriptor, 'value')
451
+ ? descriptor
452
+ : null;
453
+ } catch (_) {
454
+ return null;
455
+ }
456
+ };
457
+ const ownData = (object, key) => {
458
+ const descriptor = ownDescriptor(object, key);
459
+ return descriptor ? descriptor.value : undefined;
460
+ };
461
+ const finiteNumber = (value) =>
462
+ typeof value === 'number' && Number.isFinite(value)
463
+ ? (Object.is(value, -0) ? 0 : value)
464
+ : null;
465
+ const scalar = (value) => {
466
+ if (value === null || typeof value === 'boolean') return value;
467
+ if (typeof value === 'number') return finiteNumber(value);
468
+ if (typeof value === 'string') return value.slice(0, 65536);
469
+ if (typeof value === 'bigint') return String(value);
470
+ return undefined;
471
+ };
472
+ const scalarOrNull = (value) => {
473
+ const selected = scalar(value);
474
+ return selected === undefined ? null : selected;
475
+ };
476
+ const booleanOrNull = (value) => typeof value === 'boolean' ? value : null;
477
+ const textureImageUrl = (value) => {
478
+ if (typeof value !== 'string' || value.length === 0 || value.length > ${MAX_TEXTURE_URL_LENGTH}) {
479
+ return null;
480
+ }
481
+ try {
482
+ const relative = !/^[a-z][a-z0-9+.-]*:/iu.test(value);
483
+ const url = new URL(value, 'https://poi-texture.invalid/');
484
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
485
+ if (relative && url.origin !== 'https://poi-texture.invalid') return null;
486
+ return relative
487
+ ? (value.startsWith('/') ? url.pathname : url.pathname.slice(1))
488
+ : url.origin + url.pathname;
489
+ } catch (_) {
490
+ return null;
491
+ }
492
+ };
493
+ const rectangle = (value) => {
494
+ if (!isObject(value)) return null;
495
+ const x = finiteNumber(ownData(value, 'x'));
496
+ const y = finiteNumber(ownData(value, 'y'));
497
+ const width = finiteNumber(ownData(value, 'width'));
498
+ const height = finiteNumber(ownData(value, 'height'));
499
+ if (x === null || y === null || width === null || height === null || width < 0 || height < 0) {
500
+ return null;
501
+ }
502
+ return { x, y, width, height };
503
+ };
504
+ const textureRectangle = (value) => {
505
+ const rect = rectangle(value);
506
+ if (
507
+ rect === null ||
508
+ Math.abs(rect.x) > ${MAX_TEXTURE_RECTANGLE_VALUE} ||
509
+ Math.abs(rect.y) > ${MAX_TEXTURE_RECTANGLE_VALUE} ||
510
+ rect.width > ${MAX_TEXTURE_RECTANGLE_VALUE} ||
511
+ rect.height > ${MAX_TEXTURE_RECTANGLE_VALUE}
512
+ ) return null;
513
+ return rect;
514
+ };
515
+ const textureIdentity = (node) => {
516
+ const texture = ownData(node, '_texture') ?? ownData(node, 'texture');
517
+ if (!isObject(texture)) return { imageUrl: null, frame: null, orig: null };
518
+ const baseTexture = ownData(texture, 'baseTexture');
519
+ return {
520
+ imageUrl: textureImageUrl(ownData(baseTexture, 'imageUrl')),
521
+ frame: textureRectangle(ownData(texture, '_frame') ?? ownData(texture, 'frame')),
522
+ orig: textureRectangle(ownData(texture, '_orig') ?? ownData(texture, 'orig')),
523
+ };
524
+ };
525
+ const affineTransform = (value) => {
526
+ if (!isObject(value)) return null;
527
+ const transform = {};
528
+ for (const key of ['a', 'b', 'c', 'd', 'tx', 'ty']) {
529
+ const item = finiteNumber(ownData(value, key));
530
+ if (item === null) return null;
531
+ transform[key] = item;
532
+ }
533
+ return transform;
534
+ };
535
+ const unionBounds = (left, right) => {
536
+ if (!left) return right;
537
+ if (!right) return left;
538
+ const x = Math.min(left.x, right.x);
539
+ const y = Math.min(left.y, right.y);
540
+ const farX = Math.max(left.x + left.width, right.x + right.width);
541
+ const farY = Math.max(left.y + left.height, right.y + right.height);
542
+ return { x, y, width: farX - x, height: farY - y };
543
+ };
544
+ const circleBounds = (value) => {
545
+ if (!isObject(value)) return null;
546
+ const x = finiteNumber(ownData(value, 'x'));
547
+ const y = finiteNumber(ownData(value, 'y'));
548
+ const radius = finiteNumber(ownData(value, 'radius'));
549
+ if (x === null || y === null || radius === null || radius < 0) return null;
550
+ return { x: x - radius, y: y - radius, width: radius * 2, height: radius * 2 };
551
+ };
552
+ const polygonBounds = (value) => {
553
+ if (!isObject(value)) return null;
554
+ const points = ownData(value, 'points');
555
+ if (!Array.isArray(points) || points.length < 4 || points.length > 4096) return null;
556
+ const numbers = [];
557
+ for (let index = 0; index < points.length; index += 1) {
558
+ const point = ownData(points, String(index));
559
+ const number = finiteNumber(point);
560
+ if (number === null) return null;
561
+ numbers.push(number);
562
+ }
563
+ if (numbers.length % 2 !== 0) return null;
564
+ const xs = [];
565
+ const ys = [];
566
+ for (let index = 0; index < numbers.length; index += 2) {
567
+ xs.push(numbers[index]);
568
+ ys.push(numbers[index + 1]);
569
+ }
570
+ const x = Math.min(...xs);
571
+ const y = Math.min(...ys);
572
+ return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
573
+ };
574
+ const shapeBounds = (shape) => {
575
+ if (!isObject(shape)) return null;
576
+ const type = finiteNumber(ownData(shape, 'type'));
577
+ if (type === 2) return circleBounds(shape);
578
+ if (type === 0) return polygonBounds(shape);
579
+ const rect = rectangle(shape);
580
+ if (rect === null) return null;
581
+ if (type === 3) {
582
+ return {
583
+ x: rect.x - rect.width,
584
+ y: rect.y - rect.height,
585
+ width: rect.width * 2,
586
+ height: rect.height * 2,
587
+ };
588
+ }
589
+ return rect;
590
+ };
591
+ const graphicsBounds = (node) => {
592
+ const graphicsData = ownData(node, 'graphicsData');
593
+ if (!Array.isArray(graphicsData)) return null;
594
+ const length = ownData(graphicsData, 'length');
595
+ if (!Number.isInteger(length) || length < 0 || length > 256) return null;
596
+ let bounds = null;
597
+ for (let index = 0; index < length; index += 1) {
598
+ const item = ownData(graphicsData, String(index));
599
+ const shape = ownData(item, 'shape');
600
+ bounds = unionBounds(bounds, shapeBounds(shape));
601
+ }
602
+ return bounds;
603
+ };
604
+ const spriteBounds = (node) => {
605
+ const texture = ownData(node, '_texture') || ownData(node, 'texture');
606
+ if (!isObject(texture)) return null;
607
+ const frame = ownData(texture, '_orig') || ownData(texture, 'orig') ||
608
+ ownData(texture, '_frame') || ownData(texture, 'frame');
609
+ const rect = rectangle(frame);
610
+ if (rect === null || rect.width <= 0 || rect.height <= 0) return null;
611
+ const anchor = ownData(node, '_anchor') || ownData(node, 'anchor');
612
+ const anchorX = finiteNumber(ownData(anchor, '_x')) ?? finiteNumber(ownData(anchor, 'x')) ?? 0;
613
+ const anchorY = finiteNumber(ownData(anchor, '_y')) ?? finiteNumber(ownData(anchor, 'y')) ?? 0;
614
+ return {
615
+ x: -anchorX * rect.width,
616
+ y: -anchorY * rect.height,
617
+ width: rect.width,
618
+ height: rect.height,
619
+ };
620
+ };
621
+ const localBounds = (node) => {
622
+ const hitArea = rectangle(ownData(node, 'hitArea'));
623
+ if (hitArea !== null) return hitArea;
624
+ const graphics = graphicsBounds(node);
625
+ if (graphics !== null) return graphics;
626
+ return spriteBounds(node);
627
+ };
628
+ const worldTransform = (node) => {
629
+ const direct = affineTransform(ownData(node, 'worldTransform'));
630
+ if (direct !== null) return direct;
631
+ return affineTransform(ownData(ownData(node, 'transform'), 'worldTransform'));
632
+ };
633
+ const shown = (node) => {
634
+ if (ownData(node, 'visible') === false || ownData(node, 'renderable') === false) return false;
635
+ const alpha = finiteNumber(ownData(node, 'worldAlpha'));
636
+ return alpha === null || alpha > 0;
637
+ };
638
+ const transformedBounds = (hitArea, transform) => {
639
+ if (!hitArea || !transform) return null;
640
+ const corners = [
641
+ [hitArea.x, hitArea.y],
642
+ [hitArea.x + hitArea.width, hitArea.y],
643
+ [hitArea.x, hitArea.y + hitArea.height],
644
+ [hitArea.x + hitArea.width, hitArea.y + hitArea.height],
645
+ ].map(([x, y]) => ({
646
+ x: transform.a * x + transform.c * y + transform.tx,
647
+ y: transform.b * x + transform.d * y + transform.ty,
648
+ }));
649
+ if (corners.some((corner) => !Number.isFinite(corner.x) || !Number.isFinite(corner.y))) {
650
+ return null;
651
+ }
652
+ const xs = corners.map((corner) => corner.x);
653
+ const ys = corners.map((corner) => corner.y);
654
+ const left = Math.min(...xs);
655
+ const top = Math.min(...ys);
656
+ const right = Math.max(...xs);
657
+ const bottom = Math.max(...ys);
658
+ return {
659
+ x: Object.is(left, -0) ? 0 : left,
660
+ y: Object.is(top, -0) ? 0 : top,
661
+ width: Object.is(right - left, -0) ? 0 : right - left,
662
+ height: Object.is(bottom - top, -0) ? 0 : bottom - top,
663
+ };
664
+ };
665
+ let root;
666
+ if (rootName === 'globalThis') {
667
+ root = globalThis;
668
+ } else {
669
+ const ticker = globalThis.PIXI && globalThis.PIXI.ticker;
670
+ const head = ticker && ticker.shared && ticker.shared._head;
671
+ const interaction = head && head.next && head.next.context;
672
+ const renderer = interaction && interaction.renderer;
673
+ root = renderer && renderer._lastObjectRendered;
674
+ if (!isObject(root)) return { available: false, matches: [] };
675
+ }
676
+ for (const segment of rootPath) {
677
+ root = ownData(root, segment);
678
+ if (!isObject(root)) return { available: false, matches: [] };
679
+ }
680
+ if (!isObject(root)) return { available: false, matches: [] };
681
+
682
+ const queue = [{ node: root, path: rootPath, depth: 0, parent: -1 }];
683
+ const seen = new WeakSet();
684
+ const entries = [];
685
+ let cursor = 0;
686
+ let visited = 0;
687
+ let inspectedChildren = 0;
688
+ let depthLimited = false;
689
+ let childScanLimited = false;
690
+ let truncatedBy = null;
691
+ while (cursor < queue.length) {
692
+ const current = queue[cursor];
693
+ cursor += 1;
694
+ if (!isObject(current.node) || seen.has(current.node)) continue;
695
+ if (visited >= maxNodes) {
696
+ truncatedBy = 'maxNodes';
697
+ break;
698
+ }
699
+ seen.add(current.node);
700
+ visited += 1;
701
+ const entryIndex = entries.length;
702
+ entries.push({
703
+ node: current.node,
704
+ path: current.path,
705
+ depth: current.depth,
706
+ parent: current.parent,
707
+ ownWorldBounds: shown(current.node)
708
+ ? transformedBounds(localBounds(current.node), worldTransform(current.node))
709
+ : null,
710
+ subtreeWorldBounds: null,
711
+ });
712
+
713
+ const children = ownData(current.node, 'children');
714
+ if (!Array.isArray(children)) continue;
715
+ const length = ownData(children, 'length');
716
+ if (!Number.isInteger(length) || length < 0) continue;
717
+ for (let index = 0; index < length; index += 1) {
718
+ if (inspectedChildren >= maxNodes) {
719
+ if (index < length) childScanLimited = true;
720
+ break;
721
+ }
722
+ inspectedChildren += 1;
723
+ const child = ownData(children, String(index));
724
+ if (!isObject(child) || seen.has(child)) continue;
725
+ if (current.depth >= maxDepth) {
726
+ depthLimited = true;
727
+ continue;
728
+ }
729
+ queue.push({
730
+ node: child,
731
+ path: [...current.path, 'children', String(index)],
732
+ depth: current.depth + 1,
733
+ parent: entryIndex,
734
+ });
735
+ }
736
+ }
737
+ if (truncatedBy === null && cursor < queue.length) truncatedBy = 'maxNodes';
738
+ if (truncatedBy === null && depthLimited) truncatedBy = 'maxDepth';
739
+ if (truncatedBy === null && childScanLimited) truncatedBy = 'maxNodes';
740
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
741
+ const entry = entries[index];
742
+ entry.subtreeWorldBounds = unionBounds(entry.ownWorldBounds, entry.subtreeWorldBounds);
743
+ if (entry.parent >= 0 && entry.subtreeWorldBounds !== null) {
744
+ entries[entry.parent].subtreeWorldBounds = unionBounds(
745
+ entries[entry.parent].subtreeWorldBounds,
746
+ entry.subtreeWorldBounds,
747
+ );
748
+ }
749
+ }
750
+ const matches = [];
751
+ for (const entry of entries) {
752
+ if (
753
+ anyRequiredKeys.length > 0 &&
754
+ !anyRequiredKeys.some((key) => ownDescriptor(entry.node, key) !== null)
755
+ ) continue;
756
+ if (!requiredKeys.every((key) => ownDescriptor(entry.node, key) !== null)) continue;
757
+ const name = scalarOrNull(ownData(entry.node, 'name'));
758
+ const id = scalarOrNull(ownData(entry.node, 'id'));
759
+ const hitArea = rectangle(ownData(entry.node, 'hitArea'));
760
+ const attributes = {};
761
+ for (const key of selectKeys) {
762
+ const selected = scalar(ownData(entry.node, key));
763
+ if (selected !== undefined) attributes[key] = selected;
764
+ }
765
+ const interaction = {
766
+ interactive: booleanOrNull(ownData(entry.node, 'interactive')),
767
+ eventMode: scalarOrNull(ownData(entry.node, 'eventMode')),
768
+ enabled: booleanOrNull(ownData(entry.node, 'enabled')),
769
+ visible: booleanOrNull(ownData(entry.node, 'visible')),
770
+ renderable: booleanOrNull(ownData(entry.node, 'renderable')),
771
+ worldAlpha: finiteNumber(ownData(entry.node, 'worldAlpha')),
772
+ };
773
+ const texture = textureIdentity(entry.node);
774
+ const hasInteractionIdentity = interaction.interactive !== null || interaction.eventMode !== null;
775
+ const hasSemanticIdentity = name !== null || id !== null;
776
+ const hasSelectedScalar = Object.keys(attributes).length > 0;
777
+ const hasTextureIdentity = texture.imageUrl !== null || texture.frame !== null || texture.orig !== null;
778
+ if (
779
+ entry.subtreeWorldBounds === null ||
780
+ !(hasInteractionIdentity || hasSemanticIdentity || hasSelectedScalar || hasTextureIdentity)
781
+ ) continue;
782
+ if (matches.length >= maxMatches) {
783
+ truncatedBy = 'maxMatches';
784
+ break;
785
+ }
786
+ matches.push({
787
+ path: entry.path,
788
+ semanticId: { name, id },
789
+ interaction,
790
+ texture,
791
+ hitArea,
792
+ worldBounds: entry.subtreeWorldBounds,
793
+ attributes,
794
+ });
795
+ }
796
+ return {
797
+ available: true,
798
+ visited,
799
+ exhausted: truncatedBy === null,
800
+ truncatedBy,
801
+ matches,
802
+ };
803
+ })()`
804
+ }
805
+
806
+ function finalizePixiInteractiveProjection(value, options) {
807
+ const available = Boolean(value && value.available === true)
808
+ const rawMatches = available && Array.isArray(value.matches)
809
+ ? Array.from(value.matches).slice(0, options.maxMatches)
810
+ : []
811
+ const matches = rawMatches.map((match) => normalizeProjectedMatch(match))
812
+ const semanticCounts = new Map()
813
+ for (const match of matches) {
814
+ const signature = semanticSignature(match)
815
+ if (signature !== null) {
816
+ semanticCounts.set(signature, (semanticCounts.get(signature) || 0) + 1)
817
+ }
818
+ }
819
+ const identifiedMatches = matches.map((match) => {
820
+ const signature = semanticSignature(match)
821
+ const semanticId = {
822
+ name: match.semanticId.name,
823
+ id: match.semanticId.id,
824
+ unique: signature !== null && semanticCounts.get(signature) === 1,
825
+ }
826
+ const normalized = {
827
+ path: match.path,
828
+ semanticId,
829
+ interaction: match.interaction,
830
+ texture: match.texture,
831
+ hitArea: match.hitArea,
832
+ worldBounds: match.worldBounds,
833
+ attributes: match.attributes,
834
+ }
835
+ return {
836
+ path: normalized.path,
837
+ semanticId: normalized.semanticId,
838
+ nodeToken: sha256Token({
839
+ kind: 'poi.webview.pixi.node.v1',
840
+ path: normalized.path,
841
+ semanticId: {
842
+ name: normalized.semanticId.name,
843
+ id: normalized.semanticId.id,
844
+ },
845
+ interaction: normalized.interaction,
846
+ texture: normalized.texture,
847
+ hitArea: normalized.hitArea,
848
+ worldBounds: normalized.worldBounds,
849
+ attributes: normalized.attributes,
850
+ }),
851
+ interaction: normalized.interaction,
852
+ texture: normalized.texture,
853
+ hitArea: normalized.hitArea,
854
+ worldBounds: normalized.worldBounds,
855
+ attributes: normalized.attributes,
856
+ }
857
+ })
858
+ const documentToken = sha256Token({
859
+ kind: 'poi.webview.document.v1',
860
+ frameId: options.frameId,
861
+ frameUrl: options.frameUrl,
862
+ root: options.rootName,
863
+ rootPath: options.rootPath,
864
+ })
865
+ const pageToken = sha256Token({
866
+ kind: 'poi.webview.pixi.page.v1',
867
+ documentToken,
868
+ matches: identifiedMatches.map((match) => ({
869
+ path: match.path,
870
+ semanticId: match.semanticId,
871
+ texture: match.texture,
872
+ attributes: match.attributes,
873
+ })),
874
+ })
875
+ const visited = available && Number.isInteger(value.visited) && value.visited >= 0
876
+ ? value.visited
877
+ : 0
878
+ const truncatedBy = available && ['maxDepth', 'maxNodes', 'maxMatches'].includes(value.truncatedBy)
879
+ ? value.truncatedBy
880
+ : null
881
+ const exhausted = available && value.exhausted === true && truncatedBy === null
882
+ const snapshotDigest = sha256Token({
883
+ kind: 'poi.webview.pixi.snapshot.v1',
884
+ available,
885
+ documentToken,
886
+ pageToken,
887
+ visited,
888
+ exhausted,
889
+ truncatedBy,
890
+ matches: identifiedMatches,
891
+ })
892
+ return {
893
+ available,
894
+ frameId: options.frameId,
895
+ root: options.rootName,
896
+ capturedAt: options.capturedAt,
897
+ documentToken,
898
+ pageToken,
899
+ snapshotDigest,
900
+ visited,
901
+ exhausted,
902
+ truncatedBy,
903
+ matches: identifiedMatches,
904
+ }
905
+ }
906
+
907
+ function normalizeProjectedMatch(value) {
908
+ const match = value && typeof value === 'object' ? value : {}
909
+ const semanticId = match.semanticId && typeof match.semanticId === 'object'
910
+ ? match.semanticId
911
+ : {}
912
+ const interaction = match.interaction && typeof match.interaction === 'object'
913
+ ? match.interaction
914
+ : {}
915
+ const attributes = match.attributes && typeof match.attributes === 'object' && !Array.isArray(match.attributes)
916
+ ? match.attributes
917
+ : {}
918
+ const normalizedAttributes = {}
919
+ for (const [key, item] of Object.entries(attributes)) {
920
+ const scalar = normalizedScalar(item)
921
+ if (scalar !== undefined) normalizedAttributes[key] = scalar
922
+ }
923
+ return {
924
+ path: Array.isArray(match.path)
925
+ ? Array.from(match.path, (segment) => String(segment).slice(0, 256))
926
+ .slice(0, MAX_PATH_SEGMENTS)
927
+ : [],
928
+ semanticId: {
929
+ name: normalizedScalarOrNull(semanticId.name),
930
+ id: normalizedScalarOrNull(semanticId.id),
931
+ },
932
+ interaction: {
933
+ interactive: normalizedBooleanOrNull(interaction.interactive),
934
+ eventMode: normalizedScalarOrNull(interaction.eventMode),
935
+ enabled: normalizedBooleanOrNull(interaction.enabled),
936
+ visible: normalizedBooleanOrNull(interaction.visible),
937
+ renderable: normalizedBooleanOrNull(interaction.renderable),
938
+ worldAlpha: normalizedFiniteOrNull(interaction.worldAlpha),
939
+ },
940
+ texture: normalizedTextureIdentity(match.texture),
941
+ hitArea: normalizedRectangle(match.hitArea),
942
+ worldBounds: normalizedRectangle(match.worldBounds),
943
+ attributes: normalizedAttributes,
944
+ }
945
+ }
946
+
947
+ function normalizedTextureIdentity(value) {
948
+ const texture = value && typeof value === 'object' && !Array.isArray(value) ? value : {}
949
+ return {
950
+ imageUrl: normalizedTextureImageUrl(texture.imageUrl),
951
+ frame: normalizedTextureRectangle(texture.frame),
952
+ orig: normalizedTextureRectangle(texture.orig),
953
+ }
954
+ }
955
+
956
+ function normalizedTextureImageUrl(value) {
957
+ if (typeof value !== 'string' || value.length === 0 || value.length > MAX_TEXTURE_URL_LENGTH) return null
958
+ try {
959
+ const relative = !/^[a-z][a-z0-9+.-]*:/iu.test(value)
960
+ const url = new URL(value, 'https://poi-texture.invalid/')
961
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') return null
962
+ if (relative && url.origin !== 'https://poi-texture.invalid') return null
963
+ return relative
964
+ ? (value.startsWith('/') ? url.pathname : url.pathname.slice(1))
965
+ : `${url.origin}${url.pathname}`
966
+ } catch (_) {
967
+ return null
968
+ }
969
+ }
970
+
971
+ function normalizedTextureRectangle(value) {
972
+ const rectangle = normalizedRectangle(value)
973
+ if (
974
+ rectangle === null ||
975
+ Math.abs(rectangle.x) > MAX_TEXTURE_RECTANGLE_VALUE ||
976
+ Math.abs(rectangle.y) > MAX_TEXTURE_RECTANGLE_VALUE ||
977
+ rectangle.width > MAX_TEXTURE_RECTANGLE_VALUE ||
978
+ rectangle.height > MAX_TEXTURE_RECTANGLE_VALUE
979
+ ) return null
980
+ return rectangle
981
+ }
982
+
983
+ function normalizedScalar(value) {
984
+ if (value === null || typeof value === 'boolean') return value
985
+ if (typeof value === 'number') return Number.isFinite(value) ? normalizeZero(value) : null
986
+ if (typeof value === 'string') return value.slice(0, 65536)
987
+ if (typeof value === 'bigint') return String(value)
988
+ return undefined
989
+ }
990
+
991
+ function normalizedScalarOrNull(value) {
992
+ const scalar = normalizedScalar(value)
993
+ return scalar === undefined ? null : scalar
994
+ }
995
+
996
+ function normalizedBooleanOrNull(value) {
997
+ return typeof value === 'boolean' ? value : null
998
+ }
999
+
1000
+ function normalizedFiniteOrNull(value) {
1001
+ return typeof value === 'number' && Number.isFinite(value) ? normalizeZero(value) : null
1002
+ }
1003
+
1004
+ function normalizedRectangle(value) {
1005
+ if (!value || typeof value !== 'object') return null
1006
+ const x = normalizedFiniteOrNull(value.x)
1007
+ const y = normalizedFiniteOrNull(value.y)
1008
+ const width = normalizedFiniteOrNull(value.width)
1009
+ const height = normalizedFiniteOrNull(value.height)
1010
+ if (x === null || y === null || width === null || height === null || width < 0 || height < 0) {
1011
+ return null
1012
+ }
1013
+ return { x, y, width, height }
1014
+ }
1015
+
1016
+ function semanticSignature(match) {
1017
+ if (
1018
+ match.semanticId.name === null &&
1019
+ match.semanticId.id === null &&
1020
+ Object.keys(match.attributes).length === 0
1021
+ ) return null
1022
+ return canonicalJson({
1023
+ attributes: match.attributes,
1024
+ id: match.semanticId.id,
1025
+ name: match.semanticId.name,
1026
+ })
1027
+ }
1028
+
1029
+ function sha256Token(value) {
1030
+ return `sha256:${crypto.createHash('sha256').update(canonicalJson(value)).digest('hex')}`
1031
+ }
1032
+
1033
+ function canonicalJson(value) {
1034
+ if (value === null || typeof value === 'boolean') return JSON.stringify(value)
1035
+ if (typeof value === 'number') return JSON.stringify(normalizeZero(value))
1036
+ if (typeof value === 'string') return JSON.stringify(value.normalize('NFC'))
1037
+ if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(',')}]`
1038
+ const keys = Object.keys(value).sort()
1039
+ return `{${keys.map((key) =>
1040
+ `${JSON.stringify(key.normalize('NFC'))}:${canonicalJson(value[key])}`).join(',')}}`
1041
+ }
1042
+
1043
+ function normalizeZero(value) {
1044
+ return Object.is(value, -0) ? 0 : value
1045
+ }
1046
+
1047
+ function frameId(frame, fallbackIndex) {
1048
+ const processId = Number(frame.processId)
1049
+ const routingId = Number(frame.routingId)
1050
+ if (Number.isInteger(processId) && Number.isInteger(routingId)) {
1051
+ return `${processId}:${routingId}`
1052
+ }
1053
+ return fallbackIndex === 0 ? 'main' : `frame-${fallbackIndex}`
1054
+ }
1055
+
1056
+ function isKanColleGameUrl(value) {
1057
+ if (typeof value !== 'string' || value.length === 0) return false
1058
+ try {
1059
+ const url = new URL(value)
1060
+ return (
1061
+ url.protocol === 'https:' &&
1062
+ (
1063
+ url.hostname === 'kancolle-server.com' ||
1064
+ url.hostname.endsWith('.kancolle-server.com')
1065
+ )
1066
+ )
1067
+ } catch (_) {
1068
+ return false
1069
+ }
1070
+ }
1071
+
1072
+ function publicFrameUrl(value) {
1073
+ try {
1074
+ const url = new URL(value)
1075
+ return `${url.origin}${url.pathname}`
1076
+ } catch (_) {
1077
+ return ''
1078
+ }
1079
+ }
1080
+
1081
+ function validateInspectionRoot(value) {
1082
+ const root = value == null ? 'globalThis' : boundedString(value, 64, 'root')
1083
+ if (!WEBVIEW_ROOTS.includes(root)) {
1084
+ throw new Error(`Unsupported WebView inspection root: ${root}`)
1085
+ }
1086
+ return root
1087
+ }
1088
+
1089
+ function validateFindProjection(value) {
1090
+ if (value == null) return null
1091
+ const projection = boundedString(value, 64, 'projection')
1092
+ if (!WEBVIEW_FIND_PROJECTIONS.includes(projection)) {
1093
+ throw new Error(`Unsupported WebView find projection: ${projection}`)
1094
+ }
1095
+ return projection
1096
+ }
1097
+
1098
+ function validatePath(value) {
1099
+ if (!Array.isArray(value) || value.length > MAX_PATH_SEGMENTS) {
1100
+ throw new Error(`path must be an array with at most ${MAX_PATH_SEGMENTS} segments`)
1101
+ }
1102
+ return value.map((segment) => {
1103
+ const text = boundedString(segment, 256, 'path segment')
1104
+ if (
1105
+ ['__proto__', 'prototype', 'constructor'].includes(text) ||
1106
+ SENSITIVE_KEY.test(text)
1107
+ ) {
1108
+ throw new Error(`Unsafe path segment: ${text}`)
1109
+ }
1110
+ return text
1111
+ })
1112
+ }
1113
+
1114
+ function validateKeyList(value, minimum, maximum, name) {
1115
+ if (!Array.isArray(value) || value.length < minimum || value.length > maximum) {
1116
+ throw new Error(`${name} must contain ${minimum} to ${maximum} property names`)
1117
+ }
1118
+ const keys = value.map((key) => boundedString(key, 256, `${name} item`))
1119
+ if (
1120
+ new Set(keys).size !== keys.length ||
1121
+ keys.some((key) =>
1122
+ ['__proto__', 'prototype', 'constructor'].includes(key) ||
1123
+ SENSITIVE_KEY.test(key))
1124
+ ) {
1125
+ throw new Error(`${name} contains duplicate or unsafe property names`)
1126
+ }
1127
+ return keys
1128
+ }
1129
+
1130
+ function boundedResult(value) {
1131
+ let encoded
1132
+ try {
1133
+ encoded = JSON.stringify(value)
1134
+ } catch (_) {
1135
+ throw new Error('WebView result is not serializable')
1136
+ }
1137
+ if (Buffer.byteLength(encoded, 'utf8') > MAX_RESULT_BYTES) {
1138
+ throw new Error('WebView result exceeds 2MB')
1139
+ }
1140
+ return value
1141
+ }
1142
+
1143
+ function boundedString(value, maximum, name) {
1144
+ if (typeof value !== 'string' || value.length === 0 || value.length > maximum) {
1145
+ throw new Error(`${name} must be a non-empty string up to ${maximum} characters`)
1146
+ }
1147
+ return value
1148
+ }
1149
+
1150
+ function boundedInteger(value, fallback, minimum, maximum, name = 'timeoutMs') {
1151
+ if (value == null) return fallback
1152
+ const parsed = Number(value)
1153
+ if (!Number.isInteger(parsed) || parsed < minimum || parsed > maximum) {
1154
+ throw new Error(`${name} must be an integer from ${minimum} to ${maximum}`)
1155
+ }
1156
+ return parsed
1157
+ }
1158
+
1159
+ function timestamp(value) {
1160
+ const date = value instanceof Date ? value : new Date(value)
1161
+ if (!Number.isFinite(date.getTime())) throw new Error('now must return a valid date')
1162
+ return date.toISOString()
1163
+ }
1164
+
1165
+ function defaultGetStore(path) {
1166
+ if (typeof window !== 'undefined' && typeof window.getStore === 'function') {
1167
+ return window.getStore(path)
1168
+ }
1169
+ return null
1170
+ }
1171
+
1172
+ function defaultResolveWebContents(id) {
1173
+ return require('@electron/remote').webContents.fromId(id)
1174
+ }
1175
+
1176
+ module.exports = {
1177
+ MAX_DEBUG_SCRIPT_LENGTH,
1178
+ MAX_RESULT_BYTES,
1179
+ createPoiWebviewRuntime,
1180
+ isKanColleGameUrl,
1181
+ }