poi-plugin-mcp 0.2.22 → 0.2.24

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