@kaminari-ad/mcp 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -1
- package/README.md +9 -6
- package/dist/bin.js +3 -3
- package/dist/{chunk-5UVZYMC3.js → chunk-T6GEYHLP.js} +67 -19
- package/dist/chunk-T6GEYHLP.js.map +1 -0
- package/dist/{chunk-KFVVS55N.js → chunk-TMUGAKI2.js} +3 -3
- package/dist/{chunk-KFVVS55N.js.map → chunk-TMUGAKI2.js.map} +1 -1
- package/dist/{http-bootstrap-3MGAQHND.js → http-bootstrap-Y4YKT7KV.js} +4 -4
- package/dist/{http-bootstrap-3MGAQHND.js.map → http-bootstrap-Y4YKT7KV.js.map} +1 -1
- package/dist/{stdio-bootstrap-7EXISZ66.js → stdio-bootstrap-JW6I4H3U.js} +4 -4
- package/dist/{stdio-bootstrap-7EXISZ66.js.map → stdio-bootstrap-JW6I4H3U.js.map} +1 -1
- package/package.json +6 -3
- package/dist/chunk-5UVZYMC3.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,97 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.12.0] - 2026-08-07
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
|
|
14
|
+
- **Bump `undici` 8.5.0 to 8.10.0** to clear the npm-audit gate after the
|
|
15
|
+
upstream disclosure of one high-severity advisory
|
|
16
|
+
(GHSA-4cwx-7wf7-3272 — a malformed `private` Cache-Control directive
|
|
17
|
+
makes a shared cache store a per-user response and serve it to other
|
|
18
|
+
users) and four medium ones (GHSA-jr45-8vmc-qm54,
|
|
19
|
+
GHSA-m8rv-5g2x-5cg5, GHSA-8xcm-r25x-g524, GHSA-v3r7-h72x-cjcm). All
|
|
20
|
+
five are fixed in 8.9.0. `undici` is a direct dependency.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Requires api >= v1.33.0 for the webhook tools.** The regenerated zod
|
|
25
|
+
schemas make `failing_since` and `paused_until` required on
|
|
26
|
+
`EndpointHealthResponse`, and the webhook parser picks the whole nested
|
|
27
|
+
`health` object, so against an older API every webhook tool
|
|
28
|
+
(`list_webhooks`, `get_webhook`, `create_webhook`, `update_webhook`,
|
|
29
|
+
`rotate_webhook_secret`) fails with
|
|
30
|
+
`upstream: malformed webhook: health.failing_since: Invalid input`.
|
|
31
|
+
- **README tool list matches the registry again.** It documented 83 tools
|
|
32
|
+
and had drifted 15 behind: the custom-taxonomy domain, the three
|
|
33
|
+
screenshot fetchers, `get_invoice_pdf`, `list_scan_children`,
|
|
34
|
+
`create_custom_role`, and the two account-label tools were all
|
|
35
|
+
registered but undocumented — and the screenshot fetchers and invoice
|
|
36
|
+
PDF were still listed as deliberately not exposed.
|
|
37
|
+
- **`get_alert_stats` counts all time.** The API used to count only the last
|
|
38
|
+
30 days when no dates were given, while `list_alerts` listed all time, so
|
|
39
|
+
the four buckets never summed to the list's `total`. The API now defaults
|
|
40
|
+
both to all time; the tool description says so, so a model comparing the
|
|
41
|
+
two no longer has to guess which window it got. Requires the API deploy
|
|
42
|
+
that ships the new default — against an older API the counts still cover
|
|
43
|
+
30 days.
|
|
44
|
+
- **Regenerated `/api/v1` types and zod schemas.** Picks up the new alert
|
|
45
|
+
filters (`policy_set_id`, `tag`, `country_code`, `date_from`, `date_to`,
|
|
46
|
+
`timezone` on list + stats, and the matching `filter_*` fields on
|
|
47
|
+
bulk-status), `policy_set_name` on `AlertResponse`, plus drift that had
|
|
48
|
+
accumulated since the last regen: `last_run_at` on campaign groups,
|
|
49
|
+
campaign/group date filters, and `failing_since` / `paused_until` on
|
|
50
|
+
webhook endpoint health.
|
|
51
|
+
|
|
52
|
+
## [0.11.0] - 2026-07-28
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- **Combo-rule match scope.** `create_custom_rule`, `update_custom_rule`, and
|
|
57
|
+
`test_custom_rule` now document the rule-level `config.match_scope` key used
|
|
58
|
+
by `rule_type='combo'`: `"scan"` (the default, and the behaviour when the key
|
|
59
|
+
is absent) counts conditions across the union of all tags on the scan, while
|
|
60
|
+
`"url"` requires every condition to be satisfied by tags on the same link and
|
|
61
|
+
assigns the output tag to that link. The description warns about the traps a
|
|
62
|
+
per-link rule falls into: a combo built only from link-less tags (AI verdicts,
|
|
63
|
+
crawler behaviour) has no link to attach to and never matches, AI verdicts and
|
|
64
|
+
per-link detections are not evaluated together today, and a per-link rule needs
|
|
65
|
+
at least one positive condition. Any other `match_scope` value is rejected
|
|
66
|
+
before the request is sent, so a misspelled scope surfaces as a tool-input
|
|
67
|
+
error instead of a round trip; the API validates `match_scope` on create,
|
|
68
|
+
update, and the rule-test preview as well, so this is early feedback rather
|
|
69
|
+
than the durable guarantee. The rest of a combo config is still validated only
|
|
70
|
+
for admin-authored system rules, which is why the per-link traps are documented
|
|
71
|
+
rather than enforced. `get_custom_rule` and `list_custom_rules` now flag that
|
|
72
|
+
the returned `config` may carry `match_scope` and that it must be resent
|
|
73
|
+
verbatim, since `update_custom_rule` replaces `config` wholesale.
|
|
74
|
+
- **`referrer` on scan and campaign creation.** `create_scan`,
|
|
75
|
+
`create_bulk_scans`, `create_campaign`, and `update_campaign` accept an
|
|
76
|
+
optional http(s) page URL the check is performed from. With `ad_tag` or
|
|
77
|
+
`vast_tag` it is the publisher page the tag is embedded in: the browser
|
|
78
|
+
commits the page document on that URL without fetching the publisher, so the
|
|
79
|
+
creative renders as if embedded there. With `url` or `ad_discovery` it is
|
|
80
|
+
where the visitor came from, sent as the `Referer` of the page request.
|
|
81
|
+
`update_campaign` also accepts `referrer: null` to clear a stored one.
|
|
82
|
+
Scan and campaign responses surface `referrer` too, so an agent can confirm
|
|
83
|
+
what a scan actually ran with.
|
|
84
|
+
|
|
85
|
+
**Release ordering — this depends on an unreleased API.** The snapshots were
|
|
86
|
+
generated from api `feat/scan-referrer`
|
|
87
|
+
([api!341](https://gitlab.sdev.pw/adverif/api/-/merge_requests/341)). API
|
|
88
|
+
request DTOs are `extra="ignore"`, so an API without the field drops
|
|
89
|
+
`referrer` silently: no 422, the scan runs with no referrer, and the response
|
|
90
|
+
carries no `referrer` key — the agent gets a green result for a check it
|
|
91
|
+
believes ran from a publisher page. **Do not cut the npm tag or the
|
|
92
|
+
`kaminariad-mcp` image until api!341 is merged and deployed to
|
|
93
|
+
`kaminariadprod1`.** Nothing automated enforces this.
|
|
94
|
+
|
|
95
|
+
### Changed
|
|
96
|
+
|
|
97
|
+
- Regenerated the OpenAPI type + zod-schema snapshots so the `/api/v1` surface
|
|
98
|
+
they describe carries `referrer`. That field is their only delta versus the
|
|
99
|
+
0.10.0 snapshots.
|
|
100
|
+
|
|
10
101
|
## [0.10.0] - 2026-07-28
|
|
11
102
|
|
|
12
103
|
### Added
|
|
@@ -934,7 +1025,16 @@ Initial public release. The first version that ships to npm under
|
|
|
934
1025
|
need them.
|
|
935
1026
|
- Invoice PDF fetcher — same reason.
|
|
936
1027
|
|
|
937
|
-
[Unreleased]: https://github.com/kaminari-ad/mcp/compare/v0.
|
|
1028
|
+
[Unreleased]: https://github.com/kaminari-ad/mcp/compare/v0.12.0...HEAD
|
|
1029
|
+
[0.12.0]: https://github.com/kaminari-ad/mcp/compare/v0.11.0...v0.12.0
|
|
1030
|
+
[0.11.0]: https://github.com/kaminari-ad/mcp/compare/v0.10.0...v0.11.0
|
|
1031
|
+
[0.10.0]: https://github.com/kaminari-ad/mcp/compare/v0.8.0...v0.10.0
|
|
1032
|
+
[0.8.0]: https://github.com/kaminari-ad/mcp/compare/v0.6.0...v0.8.0
|
|
1033
|
+
[0.6.0]: https://github.com/kaminari-ad/mcp/compare/v0.5.2...v0.6.0
|
|
1034
|
+
[0.5.2]: https://github.com/kaminari-ad/mcp/compare/v0.5.0...v0.5.2
|
|
1035
|
+
[0.5.0]: https://github.com/kaminari-ad/mcp/compare/v0.3.0...v0.5.0
|
|
1036
|
+
[0.3.0]: https://github.com/kaminari-ad/mcp/compare/v0.2.2...v0.3.0
|
|
1037
|
+
[0.2.2]: https://github.com/kaminari-ad/mcp/compare/v0.2.1...v0.2.2
|
|
938
1038
|
[0.2.1]: https://github.com/kaminari-ad/mcp/compare/v0.2.0...v0.2.1
|
|
939
1039
|
[0.2.0]: https://github.com/kaminari-ad/mcp/compare/v0.1.5...v0.2.0
|
|
940
1040
|
[0.1.5]: https://github.com/kaminari-ad/mcp/compare/v0.1.0...v0.1.5
|
package/README.md
CHANGED
|
@@ -63,7 +63,7 @@ Add to your MCP client config (Cursor: `~/.cursor/mcp.json`; Claude Desktop: `~/
|
|
|
63
63
|
}
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
Restart your client. You should see `kaminari-ad` in the MCP servers list with
|
|
66
|
+
Restart your client. You should see `kaminari-ad` in the MCP servers list with 98 tools exposed.
|
|
67
67
|
|
|
68
68
|
### 2b. Hosted HTTP transport (no install)
|
|
69
69
|
|
|
@@ -113,24 +113,27 @@ which decides which credential type minted it.
|
|
|
113
113
|
|
|
114
114
|
## Tools
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
98 tools mirroring most of the public `/api/v1` surface of Kaminari Ad. Every tool carries MCP behaviour annotations (`title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) so MCP clients can warn before destructive actions. The complete list, by domain:
|
|
117
117
|
|
|
118
|
-
- **Account** (
|
|
119
|
-
- **Scans** (
|
|
118
|
+
- **Account** (14) — `get_account`, `update_org`, `list_org_users`, `invite_user`, `update_user_role`, `remove_user`, `transfer_ownership`, `list_org_roles`, `create_custom_role`, `list_account_labels`, `update_account_labels`, `list_api_keys`, `create_api_key`, `revoke_api_key`
|
|
119
|
+
- **Scans** (10) — `list_scans`, `get_scan`, `list_scan_children`, `create_scan`, `create_bulk_scans`, `recheck_scans`, `cancel_scan`, `get_scan_screenshot`, `get_scan_creative_screenshot`, `get_scan_landing_screenshot`
|
|
120
120
|
- **Campaigns** (10) — `list_campaigns`, `list_campaigns_picker`, `get_campaign`, `create_campaign`, `update_campaign`, `archive_campaign`, `unarchive_campaign`, `cancel_campaign`, `run_campaign`, `list_campaign_runs`
|
|
121
121
|
- **Campaign groups** (10) — list/get/create/update/run/cancel/archive/unarchive + `pause_campaign_group_schedule`, `resume_campaign_group_schedule`
|
|
122
122
|
- **Runs** (3) — `get_run`, `list_run_scans`, `cancel_run` (use `list_campaign_runs` to enumerate runs of a campaign — the API has no standalone `/runs` index)
|
|
123
123
|
- **Tags** (5) — `list_tags`, `get_tag_definition`, `update_tag_definition`, `delete_tag_definition`, `list_scan_tags`
|
|
124
124
|
- **Custom rules** (6) — `list_custom_rules`, `get_custom_rule`, `create_custom_rule`, `update_custom_rule`, `delete_custom_rule`, `test_custom_rule`
|
|
125
|
+
- **Custom taxonomies** (7) — `list_custom_taxonomies`, `get_custom_taxonomy`, `create_custom_taxonomy`, `update_custom_taxonomy`, `delete_custom_taxonomy`, `restore_custom_taxonomy`, `parse_custom_taxonomy_text`
|
|
125
126
|
- **Policy sets** (6) — `list_policy_sets`, `get_policy_set`, `create_policy_set`, `update_policy_set`, `delete_policy_set`, `request_policy_set_approval`
|
|
126
127
|
- **Alerts** (3) — `list_alerts`, `update_alert_status`, `get_alert_stats`
|
|
127
128
|
- **Webhooks** (11) — `list_webhooks`, `get_webhook`, `create_webhook`, `update_webhook`, `delete_webhook`, `list_webhook_event_types`, `list_webhook_deliveries`, `test_webhook`, `rotate_webhook_secret`, `replay_webhook_delivery`, `bulk_replay_webhook`
|
|
128
129
|
- **Billing** (4) — `get_billing_summary`, `list_usage`, `get_usage_summary`, `list_balance_history`
|
|
129
|
-
- **Invoicing** (
|
|
130
|
+
- **Invoicing** (2) — `list_invoices`, `get_invoice_pdf`
|
|
130
131
|
- **Alert notifications** (5) — `list_alert_destinations`, `delete_alert_destination`, `set_alert_destination_version`, `get_campaign_alert_overrides`, `set_campaign_alert_overrides`
|
|
131
132
|
- **Reference data** (2) — `list_geos`, `list_emulators`
|
|
132
133
|
|
|
133
|
-
|
|
134
|
+
Screenshots (`get_scan_screenshot`, `get_scan_creative_screenshot`, `get_scan_landing_screenshot`) come back as inline MCP `image` blocks and `get_invoice_pdf` as an inline resource block — no second fetch, no presigned URL.
|
|
135
|
+
|
|
136
|
+
Not exposed (intentionally): the raw creative artifacts (`creative-html`, `creative-video`, `vast-xml`) and the public marketing forms (`/contact`, `/demo-inquiries`). Open an issue if you need one of those.
|
|
134
137
|
|
|
135
138
|
## Example agent prompts
|
|
136
139
|
|
package/dist/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { NAME, VERSION, err, ok } from './chunk-
|
|
2
|
+
import { NAME, VERSION, err, ok } from './chunk-TMUGAKI2.js';
|
|
3
3
|
import process from 'process';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
|
|
@@ -160,10 +160,10 @@ async function main() {
|
|
|
160
160
|
}
|
|
161
161
|
const config = configResult.value;
|
|
162
162
|
if (config.transport === "stdio") {
|
|
163
|
-
const { bootstrapStdio } = await import('./stdio-bootstrap-
|
|
163
|
+
const { bootstrapStdio } = await import('./stdio-bootstrap-JW6I4H3U.js');
|
|
164
164
|
return bootstrapStdio(config);
|
|
165
165
|
}
|
|
166
|
-
const { bootstrapHttp } = await import('./http-bootstrap-
|
|
166
|
+
const { bootstrapHttp } = await import('./http-bootstrap-Y4YKT7KV.js');
|
|
167
167
|
return bootstrapHttp(config);
|
|
168
168
|
}
|
|
169
169
|
main().then(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { err, ok } from './chunk-
|
|
2
|
+
import { err, ok } from './chunk-TMUGAKI2.js';
|
|
3
3
|
import { createHash, randomUUID } from 'crypto';
|
|
4
4
|
import createClient from 'openapi-fetch';
|
|
5
5
|
import { fetch } from 'undici';
|
|
@@ -245,6 +245,7 @@ z.object({
|
|
|
245
245
|
url: z.union([z.string(), z.null()]).optional(),
|
|
246
246
|
ad_tag: z.union([z.string(), z.null()]).optional(),
|
|
247
247
|
vast_tag: z.union([z.string(), z.null()]).optional(),
|
|
248
|
+
referrer: z.union([z.string(), z.null()]).optional(),
|
|
248
249
|
country_code: z.string().min(2).max(2),
|
|
249
250
|
emulator_id: z.string().min(1).max(100),
|
|
250
251
|
proxy: ProxyTargetRequest.optional(),
|
|
@@ -362,6 +363,7 @@ var ScanResponse = z.object({
|
|
|
362
363
|
public_report_url: z.string().optional().default(""),
|
|
363
364
|
ad_tag: z.union([z.string(), z.null()]).optional(),
|
|
364
365
|
vast_tag: z.union([z.string(), z.null()]).optional(),
|
|
366
|
+
referrer: z.union([z.string(), z.null()]).optional(),
|
|
365
367
|
creative_kind: z.enum(["banner", "video"]).optional().default("banner"),
|
|
366
368
|
creative_screenshot_url: z.string().optional().default(""),
|
|
367
369
|
creative_video_url: z.string().optional().default(""),
|
|
@@ -433,6 +435,7 @@ z.object({
|
|
|
433
435
|
url: z.union([z.string(), z.null()]).optional(),
|
|
434
436
|
ad_tag: z.union([z.string(), z.null()]).optional(),
|
|
435
437
|
vast_tag: z.union([z.string(), z.null()]).optional(),
|
|
438
|
+
referrer: z.union([z.string(), z.null()]).optional(),
|
|
436
439
|
country_codes: z.array(z.string()).min(1),
|
|
437
440
|
emulator_id: z.string().min(1).max(100),
|
|
438
441
|
proxy: ProxyTargetRequest.optional(),
|
|
@@ -468,7 +471,8 @@ var CampaignGroupResponse = z.object({
|
|
|
468
471
|
is_archived: z.boolean(),
|
|
469
472
|
schedule_paused: z.boolean(),
|
|
470
473
|
created_at: z.string().datetime({ offset: true }),
|
|
471
|
-
campaign_count: z.union([z.number(), z.null()]).optional()
|
|
474
|
+
campaign_count: z.union([z.number(), z.null()]).optional(),
|
|
475
|
+
last_run_at: z.union([z.string(), z.null()]).optional()
|
|
472
476
|
}).passthrough();
|
|
473
477
|
z.object({ name: z.union([z.string(), z.null()]) }).partial().passthrough();
|
|
474
478
|
var BulkCampaignFailure = z.object({
|
|
@@ -490,6 +494,7 @@ z.object({
|
|
|
490
494
|
url: z.union([z.string(), z.null()]).optional(),
|
|
491
495
|
ad_tag: z.union([z.string(), z.null()]).optional(),
|
|
492
496
|
vast_tag: z.union([z.string(), z.null()]).optional(),
|
|
497
|
+
referrer: z.union([z.string(), z.null()]).optional(),
|
|
493
498
|
country_codes: z.array(z.string()).min(1),
|
|
494
499
|
group_id: z.union([z.string(), z.null()]).optional(),
|
|
495
500
|
emulator_categories: z.array(z.string()).optional(),
|
|
@@ -522,6 +527,7 @@ var CampaignResponse = z.object({
|
|
|
522
527
|
url: z.string(),
|
|
523
528
|
ad_tag: z.union([z.string(), z.null()]).optional(),
|
|
524
529
|
vast_tag: z.union([z.string(), z.null()]).optional(),
|
|
530
|
+
referrer: z.union([z.string(), z.null()]).optional(),
|
|
525
531
|
country_codes: z.array(z.string()),
|
|
526
532
|
group_id: z.string().uuid(),
|
|
527
533
|
emulator_selection: EmulatorSelectionResponse,
|
|
@@ -562,6 +568,7 @@ z.object({
|
|
|
562
568
|
url: z.union([z.string(), z.null()]),
|
|
563
569
|
ad_tag: z.union([z.string(), z.null()]),
|
|
564
570
|
vast_tag: z.union([z.string(), z.null()]),
|
|
571
|
+
referrer: z.union([z.string(), z.null()]),
|
|
565
572
|
country_codes: z.union([z.array(z.string()), z.null()]),
|
|
566
573
|
group_id: z.union([z.string(), z.null()]),
|
|
567
574
|
emulator_categories: z.union([z.array(z.string()), z.null()]),
|
|
@@ -861,6 +868,7 @@ var AlertResponse = z.object({
|
|
|
861
868
|
scan_url: z.string(),
|
|
862
869
|
offer_url: z.string(),
|
|
863
870
|
tag_display_name: z.string(),
|
|
871
|
+
policy_set_name: z.union([z.string(), z.null()]).optional(),
|
|
864
872
|
rule_type: z.string().optional().default("tag"),
|
|
865
873
|
matched_value: z.union([z.string(), z.null()]).optional()
|
|
866
874
|
}).passthrough();
|
|
@@ -877,7 +885,13 @@ z.object({
|
|
|
877
885
|
ids: z.union([z.array(z.string().uuid()), z.null()]).optional(),
|
|
878
886
|
all_matching: z.boolean().optional().default(false),
|
|
879
887
|
filter_status: z.union([AlertStatus, z.null()]).optional(),
|
|
880
|
-
filter_campaign_id: z.union([z.string(), z.null()]).optional()
|
|
888
|
+
filter_campaign_id: z.union([z.string(), z.null()]).optional(),
|
|
889
|
+
filter_policy_set_ids: z.union([z.array(z.string().uuid()), z.null()]).optional(),
|
|
890
|
+
filter_tag_slugs: z.union([z.array(z.string()), z.null()]).optional(),
|
|
891
|
+
filter_country_codes: z.union([z.array(z.string()), z.null()]).optional(),
|
|
892
|
+
filter_date_from: z.union([z.string(), z.null()]).optional(),
|
|
893
|
+
filter_date_to: z.union([z.string(), z.null()]).optional(),
|
|
894
|
+
filter_timezone: z.union([z.string(), z.null()]).optional()
|
|
881
895
|
}).passthrough();
|
|
882
896
|
z.object({ updated: z.number().int(), skipped: z.number().int() }).passthrough();
|
|
883
897
|
var AlertStatsResponse = z.object({
|
|
@@ -968,7 +982,9 @@ var EndpointHealthResponse = z.object({
|
|
|
968
982
|
consecutive_failures: z.number().int(),
|
|
969
983
|
last_delivery_at: z.union([z.string(), z.null()]),
|
|
970
984
|
last_delivery_status: z.union([z.number(), z.null()]),
|
|
971
|
-
success_rate_7d: z.number()
|
|
985
|
+
success_rate_7d: z.number(),
|
|
986
|
+
failing_since: z.union([z.string(), z.null()]),
|
|
987
|
+
paused_until: z.union([z.string(), z.null()])
|
|
972
988
|
}).passthrough();
|
|
973
989
|
var WebhookResponse = z.object({
|
|
974
990
|
id: z.string().uuid(),
|
|
@@ -1339,6 +1355,7 @@ var CampaignSchema = schemas.CampaignResponse.pick({
|
|
|
1339
1355
|
url: true,
|
|
1340
1356
|
ad_tag: true,
|
|
1341
1357
|
vast_tag: true,
|
|
1358
|
+
referrer: true,
|
|
1342
1359
|
country_codes: true,
|
|
1343
1360
|
group_id: true,
|
|
1344
1361
|
emulator_selection: true,
|
|
@@ -1772,6 +1789,7 @@ var ScanSchema = schemas.ScanResponse.pick({
|
|
|
1772
1789
|
public_report_url: true,
|
|
1773
1790
|
ad_tag: true,
|
|
1774
1791
|
vast_tag: true,
|
|
1792
|
+
referrer: true,
|
|
1775
1793
|
creative_kind: true,
|
|
1776
1794
|
video: true,
|
|
1777
1795
|
creative_screenshot_url: true,
|
|
@@ -3179,7 +3197,7 @@ var setCampaignAlertOverridesTool = {
|
|
|
3179
3197
|
var GetAlertStatsInputShape = {};
|
|
3180
3198
|
var getAlertStatsTool = {
|
|
3181
3199
|
name: "get_alert_stats",
|
|
3182
|
-
description: "Get alert counts grouped by status: open, escalated, resolved, dismissed
|
|
3200
|
+
description: "Get alert counts grouped by status: open, escalated, resolved, dismissed. Counts cover the caller's organization over all time and are not filtered, so the four sum to the `total` an unfiltered `list_alerts` reports. Status names match the canonical AlertStatus enum used by `list_alerts` and `update_alert_status`.",
|
|
3183
3201
|
annotations: {
|
|
3184
3202
|
title: "Get Alert Stats",
|
|
3185
3203
|
readOnlyHint: true,
|
|
@@ -3672,6 +3690,10 @@ function pickCampaignConfigBody(input) {
|
|
|
3672
3690
|
if (input.schedule_timezone !== void 0) body.schedule_timezone = input.schedule_timezone;
|
|
3673
3691
|
return body;
|
|
3674
3692
|
}
|
|
3693
|
+
var campaignReferrerUrl = z.string().url().regex(/^https?:\/\//i, "Must be an http(s) URL.").max(2048);
|
|
3694
|
+
var CAMPAIGN_REFERRER_DESCRIPTION = "Optional http(s) page URL every scan of this campaign is checked from. For `ad_tag` and `vast` campaigns this is the publisher page the tag is embedded in \u2014 NOT the creative and NOT its landing page; the browser commits the page document on this URL without fetching the publisher, so the creative renders as if embedded there, and its cross-origin subrequests receive the origin only (https://publisher.example/, no path). For `url` and `ad_discovery` campaigns this is where the visitor came from, sent in full as the Referer of the page request.";
|
|
3695
|
+
var campaignReferrerField = campaignReferrerUrl.optional().describe(CAMPAIGN_REFERRER_DESCRIPTION);
|
|
3696
|
+
var campaignReferrerUpdateField = campaignReferrerUrl.nullable().optional().describe(`${CAMPAIGN_REFERRER_DESCRIPTION} Pass null to clear it.`);
|
|
3675
3697
|
|
|
3676
3698
|
// src/application/tools/campaigns/create-campaign.tool.ts
|
|
3677
3699
|
var CreateCampaignInputShape = {
|
|
@@ -3686,6 +3708,7 @@ var CreateCampaignInputShape = {
|
|
|
3686
3708
|
vast_tag: z.string().optional().describe(
|
|
3687
3709
|
"VAST video ad tag: an http(s) URL of a VAST endpoint OR raw VAST XML (required if campaign_type=vast)."
|
|
3688
3710
|
),
|
|
3711
|
+
referrer: campaignReferrerField,
|
|
3689
3712
|
country_codes: z.array(z.string().length(2)).min(1).describe("ISO 3166-1 alpha-2 codes \u2014 one scan per country per run."),
|
|
3690
3713
|
group_id: z.string().uuid().optional().describe("Parent group UUID; defaults to the org's default group."),
|
|
3691
3714
|
...campaignConfigFields,
|
|
@@ -3712,6 +3735,7 @@ var createCampaignTool = {
|
|
|
3712
3735
|
...input.url !== void 0 ? { url: input.url } : {},
|
|
3713
3736
|
...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
|
|
3714
3737
|
...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
|
|
3738
|
+
...input.referrer !== void 0 ? { referrer: input.referrer } : {},
|
|
3715
3739
|
...input.group_id !== void 0 ? { group_id: input.group_id } : {},
|
|
3716
3740
|
...input.labels !== void 0 ? { labels: input.labels } : {},
|
|
3717
3741
|
...input.policy_set_id !== void 0 ? { policy_set_id: input.policy_set_id } : {},
|
|
@@ -3882,6 +3906,7 @@ var UpdateCampaignInputShape = {
|
|
|
3882
3906
|
vast_tag: z.string().optional().describe(
|
|
3883
3907
|
"New VAST video ad tag: an http(s) URL of a VAST endpoint OR raw VAST XML (vast-type campaigns)."
|
|
3884
3908
|
),
|
|
3909
|
+
referrer: campaignReferrerUpdateField,
|
|
3885
3910
|
country_codes: z.array(z.string().length(2)).optional().describe("Replace the country list."),
|
|
3886
3911
|
group_id: z.string().uuid().optional().describe("Move the campaign to another group."),
|
|
3887
3912
|
...campaignConfigFields,
|
|
@@ -3891,7 +3916,7 @@ var UpdateCampaignInputShape = {
|
|
|
3891
3916
|
};
|
|
3892
3917
|
var updateCampaignTool = {
|
|
3893
3918
|
name: "update_campaign",
|
|
3894
|
-
description: "Update one or more fields of a campaign. Fields not supplied are left unchanged. `policy_set_id` accepts null to clear the binding.",
|
|
3919
|
+
description: "Update one or more fields of a campaign. Fields not supplied are left unchanged. `policy_set_id` accepts null to clear the binding, and `referrer` accepts null to clear the publisher page scans are checked from.",
|
|
3895
3920
|
annotations: {
|
|
3896
3921
|
title: "Update Campaign",
|
|
3897
3922
|
readOnlyHint: false,
|
|
@@ -3906,6 +3931,7 @@ var updateCampaignTool = {
|
|
|
3906
3931
|
...input.url !== void 0 ? { url: input.url } : {},
|
|
3907
3932
|
...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
|
|
3908
3933
|
...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
|
|
3934
|
+
...input.referrer !== void 0 ? { referrer: input.referrer } : {},
|
|
3909
3935
|
...input.country_codes !== void 0 ? { country_codes: input.country_codes } : {},
|
|
3910
3936
|
...input.group_id !== void 0 ? { group_id: input.group_id } : {},
|
|
3911
3937
|
...input.labels !== void 0 ? { labels: input.labels } : {},
|
|
@@ -3918,6 +3944,21 @@ var updateCampaignTool = {
|
|
|
3918
3944
|
return ok(result.value);
|
|
3919
3945
|
}
|
|
3920
3946
|
};
|
|
3947
|
+
var COMBO_MATCH_SCOPE_DOC = 'For `rule_type=\'combo\'` the optional rule-level key `match_scope` decides where conditions are counted. `"scan"` (the default, and the behaviour when the key is absent) checks them against the union of all tags on the scan. With `"url"`, every condition must be satisfied by tags on the same link, and the tag is assigned to that link \u2014 so a rule built only from link-less tags (AI verdicts, crawler behaviour) has no link to attach to and will never match; keep those on the whole scan. Mixing the two kinds does not help either: AI verdicts and per-link detections reach the engine in different scan parts and are not evaluated together today, so such a combo fires under neither scope. Give a `"url"` rule at least one positive condition (`all_of` / `any_of` / `tag_category` + `count_gte`) \u2014 a config carrying only `none_of` matches every link that merely lacks those tags, and the API does not reject that on this path. Thresholds stay "N or more" (`count_gte` / `any_of_min`) in both scopes; no other `match_scope` value is accepted.';
|
|
3948
|
+
var COMBO_MATCH_SCOPE_READ_DOC = 'For `rule_type=\'combo\'` the returned `config` may carry the rule-level key `match_scope` ("scan" = count conditions across the whole scan, "url" = every condition must be satisfied by tags on the same link). Resend it verbatim when updating: `update_custom_rule` replaces `config` wholesale, so dropping the key reverts the rule to whole-scan matching.';
|
|
3949
|
+
var ruleConfigField = z.record(z.unknown()).superRefine((config, ctx) => {
|
|
3950
|
+
const scope = config["match_scope"];
|
|
3951
|
+
if (scope === void 0 || scope === "scan" || scope === "url") {
|
|
3952
|
+
return;
|
|
3953
|
+
}
|
|
3954
|
+
ctx.addIssue({
|
|
3955
|
+
code: z.ZodIssueCode.custom,
|
|
3956
|
+
path: ["match_scope"],
|
|
3957
|
+
message: 'config.match_scope must be "scan" or "url". Omit the key for scan-wide matching (the default).'
|
|
3958
|
+
});
|
|
3959
|
+
});
|
|
3960
|
+
|
|
3961
|
+
// src/application/tools/custom-rules/create-custom-rule.tool.ts
|
|
3921
3962
|
var CreateCustomRuleInputShape = {
|
|
3922
3963
|
name: z.string().min(1).max(200).describe(
|
|
3923
3964
|
"Display name. For non-LLM rules this also becomes the `display_name` of the auto-registered tag definition (see `tag_slug`)."
|
|
@@ -3928,8 +3969,8 @@ var CreateCustomRuleInputShape = {
|
|
|
3928
3969
|
rule_type: z.string().max(50).describe(
|
|
3929
3970
|
"Rule engine. One of: `stopword_content`, `stopword_url`, `regexp_content`, `regexp_url`, `blacklist_domain`, `combo`, `llm`. The API validates."
|
|
3930
3971
|
),
|
|
3931
|
-
config:
|
|
3932
|
-
"Rule-type-specific configuration object. Shape depends on `rule_type`. For `rule_type='llm'` the shape is `{ prompt: string, tags: { <tag_slug>: <description>, ... } }`; each key in `config.tags` is auto-registered as a custom tag definition AND must not collide with a system slug (same 422 contract as `tag_slug`)."
|
|
3972
|
+
config: ruleConfigField.describe(
|
|
3973
|
+
"Rule-type-specific configuration object. Shape depends on `rule_type`. For `rule_type='llm'` the shape is `{ prompt: string, tags: { <tag_slug>: <description>, ... } }`; each key in `config.tags` is auto-registered as a custom tag definition AND must not collide with a system slug (same 422 contract as `tag_slug`). " + COMBO_MATCH_SCOPE_DOC
|
|
3933
3974
|
),
|
|
3934
3975
|
target: z.string().max(30).optional().describe(
|
|
3935
3976
|
"Where to apply the rule (e.g. 'page' for landing HTML). Default: page. See API docs for the full set of valid values."
|
|
@@ -3937,7 +3978,7 @@ var CreateCustomRuleInputShape = {
|
|
|
3937
3978
|
};
|
|
3938
3979
|
var createCustomRuleTool = {
|
|
3939
3980
|
name: "create_custom_rule",
|
|
3940
|
-
description: "Define a custom tag-detection rule. The API auto-registers a tag definition for each slug the rule emits (`tag_slug` for non-LLM rules; `config.tags` keys for `rule_type='llm'`); slugs that collide with a built-in system tag are rejected with HTTP 422 / code `checking.system_slug_reserved`. Matches tag every future scan; existing scans are untouched until you call `recheck_scans`.",
|
|
3981
|
+
description: "Define a custom tag-detection rule. The API auto-registers a tag definition for each slug the rule emits (`tag_slug` for non-LLM rules; `config.tags` keys for `rule_type='llm'`); slugs that collide with a built-in system tag are rejected with HTTP 422 / code `checking.system_slug_reserved`. For `rule_type='combo'` set `config.match_scope='url'` when the thresholds must be met inside one link instead of anywhere on the scan (default `'scan'`). Matches tag every future scan; existing scans are untouched until you call `recheck_scans`.",
|
|
3941
3982
|
annotations: {
|
|
3942
3983
|
title: "Create Custom Rule",
|
|
3943
3984
|
readOnlyHint: false,
|
|
@@ -3982,7 +4023,7 @@ var deleteCustomRuleTool = {
|
|
|
3982
4023
|
var GetCustomRuleInputShape = { rule_id: z.string().uuid().describe("Rule UUID.") };
|
|
3983
4024
|
var getCustomRuleTool = {
|
|
3984
4025
|
name: "get_custom_rule",
|
|
3985
|
-
description: "Get one custom rule by UUID with name, tag-slug, type, config object, target, active flag.",
|
|
4026
|
+
description: "Get one custom rule by UUID with name, tag-slug, type, config object, target, active flag. " + COMBO_MATCH_SCOPE_READ_DOC,
|
|
3986
4027
|
annotations: {
|
|
3987
4028
|
title: "Get Custom Rule",
|
|
3988
4029
|
readOnlyHint: true,
|
|
@@ -4003,7 +4044,7 @@ var ListCustomRulesInputShape = {
|
|
|
4003
4044
|
};
|
|
4004
4045
|
var listCustomRulesTool = {
|
|
4005
4046
|
name: "list_custom_rules",
|
|
4006
|
-
description: "Paginated list of the organization's custom tag-detection rules (regex / heuristics) with their config, target, and active flag. Returns `{items, total, page, limit}`. To iterate: there is a next page iff `page * limit < total` (or equivalently `items.length === limit` until the last page). `page` is 1-indexed.",
|
|
4047
|
+
description: "Paginated list of the organization's custom tag-detection rules (regex / heuristics) with their config, target, and active flag. Returns `{items, total, page, limit}`. To iterate: there is a next page iff `page * limit < total` (or equivalently `items.length === limit` until the last page). `page` is 1-indexed. " + COMBO_MATCH_SCOPE_READ_DOC,
|
|
4007
4048
|
annotations: {
|
|
4008
4049
|
title: "List Custom Rules",
|
|
4009
4050
|
readOnlyHint: true,
|
|
@@ -4022,8 +4063,8 @@ var TestCustomRuleInputShape = {
|
|
|
4022
4063
|
rule_type: z.string().max(50).describe(
|
|
4023
4064
|
"Rule engine type. One of: `stopword_content`, `stopword_url`, `regexp_content`, `regexp_url`, `blacklist_domain`, `combo`, `llm`."
|
|
4024
4065
|
),
|
|
4025
|
-
config:
|
|
4026
|
-
"Rule-type-specific config to test. Same shape as `create_custom_rule`'s `config`. NOTE: `test_custom_rule` evaluates the rule against a scan WITHOUT persisting it, so slug-collision validation does NOT run here \u2014 verify slugs against `list_tags` (`scope=system`) before promoting to `create_custom_rule`."
|
|
4066
|
+
config: ruleConfigField.describe(
|
|
4067
|
+
"Rule-type-specific config to test. Same shape as `create_custom_rule`'s `config`. NOTE: `test_custom_rule` evaluates the rule against a scan WITHOUT persisting it, so slug-collision validation does NOT run here \u2014 verify slugs against `list_tags` (`scope=system`) before promoting to `create_custom_rule`. " + COMBO_MATCH_SCOPE_DOC
|
|
4027
4068
|
),
|
|
4028
4069
|
target: z.string().max(30).describe(
|
|
4029
4070
|
"Where to apply the rule (e.g. 'page' for landing HTML). See API docs for the full set of valid values."
|
|
@@ -4032,7 +4073,7 @@ var TestCustomRuleInputShape = {
|
|
|
4032
4073
|
};
|
|
4033
4074
|
var testCustomRuleTool = {
|
|
4034
4075
|
name: "test_custom_rule",
|
|
4035
|
-
description: "Preview-test a rule definition against an existing scan WITHOUT persisting the rule. Returns `matched: bool`, evaluation time, and per-tag-slug details. Use to validate config before `create_custom_rule`. NOTE: slug-collision validation does NOT run in preview mode \u2014 verify any `tag_slug` (or LLM `config.tags` keys) against `list_tags` (`scope=system`) before promoting to `create_custom_rule`, which would otherwise reject the same payload with HTTP 422 / code `checking.system_slug_reserved`.",
|
|
4076
|
+
description: "Preview-test a rule definition against an existing scan WITHOUT persisting the rule. Returns `matched: bool`, evaluation time, and per-tag-slug details. Use to validate config before `create_custom_rule`. Combo rules are evaluated against the scan's existing tags, so `config.match_scope` is honoured \u2014 but per-URL hits are collapsed to one row per tag slug, so the result says whether the rule fires, not which URLs it would tag. NOTE: slug-collision validation does NOT run in preview mode \u2014 verify any `tag_slug` (or LLM `config.tags` keys) against `list_tags` (`scope=system`) before promoting to `create_custom_rule`, which would otherwise reject the same payload with HTTP 422 / code `checking.system_slug_reserved`.",
|
|
4036
4077
|
annotations: {
|
|
4037
4078
|
title: "Test Custom Rule",
|
|
4038
4079
|
readOnlyHint: true,
|
|
@@ -4060,8 +4101,8 @@ var UpdateCustomRuleInputShape = {
|
|
|
4060
4101
|
tag_slug: z.string().max(100).optional().describe(
|
|
4061
4102
|
"New tag slug to assign on match. **MUST NOT collide with a built-in system tag slug** (see `list_tags` where `scope=system`); colliding requests return 422 with code `checking.system_slug_reserved`. Re-registering an existing custom slug refreshes its tag definition's `display_name` / `description`."
|
|
4062
4103
|
),
|
|
4063
|
-
config:
|
|
4064
|
-
"New rule-type-specific config object. For `rule_type='llm'` the keys of `config.tags` are auto-registered as tag definitions; any key that collides with a system slug returns the same 422 contract."
|
|
4104
|
+
config: ruleConfigField.optional().describe(
|
|
4105
|
+
"New rule-type-specific config object. Replaces the stored config wholesale \u2014 resend every key you want to keep, including a combo rule's `match_scope`. For `rule_type='llm'` the keys of `config.tags` are auto-registered as tag definitions; any key that collides with a system slug returns the same 422 contract. " + COMBO_MATCH_SCOPE_DOC
|
|
4065
4106
|
),
|
|
4066
4107
|
target: z.string().max(30).optional().describe(
|
|
4067
4108
|
"Where to apply the rule (e.g. 'page' for landing HTML). See API docs for the full set of valid values."
|
|
@@ -4070,7 +4111,7 @@ var UpdateCustomRuleInputShape = {
|
|
|
4070
4111
|
};
|
|
4071
4112
|
var updateCustomRuleTool = {
|
|
4072
4113
|
name: "update_custom_rule",
|
|
4073
|
-
description: "Update a custom tag-detection rule. Only supplied fields are sent. Changing `tag_slug` (or `config.tags` keys for `rule_type='llm'`) onto a built-in system slug returns HTTP 422 / code `checking.system_slug_reserved`. To toggle activation, pass `is_active`. Existing tagged scans are NOT re-evaluated \u2014 call `recheck_scans` for that. (Rule engine `rule_type` cannot be changed after creation; create a new rule instead.)",
|
|
4114
|
+
description: "Update a custom tag-detection rule. Only supplied fields are sent, but a supplied `config` replaces the stored one wholesale (omitting a combo rule's `match_scope` reverts it to the scan-wide default). Changing `tag_slug` (or `config.tags` keys for `rule_type='llm'`) onto a built-in system slug returns HTTP 422 / code `checking.system_slug_reserved`. To toggle activation, pass `is_active`. Existing tagged scans are NOT re-evaluated \u2014 call `recheck_scans` for that. (Rule engine `rule_type` cannot be changed after creation; create a new rule instead.)",
|
|
4074
4115
|
annotations: {
|
|
4075
4116
|
title: "Update Custom Rule",
|
|
4076
4117
|
readOnlyHint: false,
|
|
@@ -4733,6 +4774,9 @@ var scanProxyField = z.object({
|
|
|
4733
4774
|
city: z.string().optional().describe("Proxy city targeting."),
|
|
4734
4775
|
isp: z.string().optional().describe("Proxy ISP targeting.")
|
|
4735
4776
|
}).optional().describe("Optional proxy geo targeting for the crawl. Omit to use the org default.");
|
|
4777
|
+
var scanReferrerField = z.string().url().regex(/^https?:\/\//i, "Must be an http(s) URL.").max(2048).optional().describe(
|
|
4778
|
+
"Optional http(s) page URL the check is performed from. With `ad_tag` or `vast_tag` this is the publisher page the tag is embedded in \u2014 NOT the creative and NOT its landing page; the browser commits the page document on this URL without fetching the publisher, so the creative renders as if embedded there, and its cross-origin subrequests receive the origin only (https://publisher.example/, no path). With a `url` target this is where the visitor came from, sent in full as the Referer of the page request."
|
|
4779
|
+
);
|
|
4736
4780
|
|
|
4737
4781
|
// src/application/tools/scans/create-bulk-scans.tool.ts
|
|
4738
4782
|
var CreateBulkScansInputShape = {
|
|
@@ -4743,6 +4787,7 @@ var CreateBulkScansInputShape = {
|
|
|
4743
4787
|
vast_tag: z.string().optional().describe(
|
|
4744
4788
|
"VAST video ad tag: an http(s) URL of a VAST endpoint OR raw VAST XML. Provide exactly one of `url`, `ad_tag`, or `vast_tag`."
|
|
4745
4789
|
),
|
|
4790
|
+
referrer: scanReferrerField,
|
|
4746
4791
|
country_codes: z.array(z.string().length(2)).min(1).max(50).describe("List of ISO 3166-1 alpha-2 country codes; one scan per country is created."),
|
|
4747
4792
|
emulator_id: z.string().min(1).max(100).describe("Device/OS profile slug; same for every country in the batch."),
|
|
4748
4793
|
proxy: scanProxyField,
|
|
@@ -4767,6 +4812,7 @@ var createBulkScansTool = {
|
|
|
4767
4812
|
...input.url !== void 0 ? { url: input.url } : {},
|
|
4768
4813
|
...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
|
|
4769
4814
|
...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
|
|
4815
|
+
...input.referrer !== void 0 ? { referrer: input.referrer } : {},
|
|
4770
4816
|
...input.proxy !== void 0 ? { proxy: input.proxy } : {},
|
|
4771
4817
|
...input.labels !== void 0 ? { labels: input.labels } : {},
|
|
4772
4818
|
...pickRepeatRetryBody(input)
|
|
@@ -4786,6 +4832,7 @@ var CreateScanInputShape = {
|
|
|
4786
4832
|
vast_tag: z.string().optional().describe(
|
|
4787
4833
|
"VAST video ad tag: an http(s) URL of a VAST endpoint OR raw VAST XML (a document containing a <VAST> element). Fetched and played in a real browser. Provide exactly one of `url`, `ad_tag`, or `vast_tag`."
|
|
4788
4834
|
),
|
|
4835
|
+
referrer: scanReferrerField,
|
|
4789
4836
|
country_code: z.string().length(2).describe("ISO 3166-1 alpha-2 country code, e.g. US, DE, JP. Determines proxy geo."),
|
|
4790
4837
|
emulator_id: z.string().min(1).max(100).describe("Device/OS profile slug; use `list_emulators` to discover valid values."),
|
|
4791
4838
|
proxy: scanProxyField,
|
|
@@ -4815,6 +4862,7 @@ var createScanTool = {
|
|
|
4815
4862
|
...input.url !== void 0 ? { url: input.url } : {},
|
|
4816
4863
|
...input.ad_tag !== void 0 ? { ad_tag: input.ad_tag } : {},
|
|
4817
4864
|
...input.vast_tag !== void 0 ? { vast_tag: input.vast_tag } : {},
|
|
4865
|
+
...input.referrer !== void 0 ? { referrer: input.referrer } : {},
|
|
4818
4866
|
...input.proxy !== void 0 ? { proxy: input.proxy } : {},
|
|
4819
4867
|
...input.labels !== void 0 ? { labels: input.labels } : {},
|
|
4820
4868
|
...input.campaign_id !== void 0 ? { campaign_id: input.campaign_id } : {},
|
|
@@ -5611,5 +5659,5 @@ function formatToolError(error) {
|
|
|
5611
5659
|
}
|
|
5612
5660
|
|
|
5613
5661
|
export { BearerToken, SERVER_INSTRUCTIONS, createHttpApiGateway, createPinoLogger, declareEmptyResourcesAndPrompts, newRequestId, wireToolsIntoMcpServer };
|
|
5614
|
-
//# sourceMappingURL=chunk-
|
|
5615
|
-
//# sourceMappingURL=chunk-
|
|
5662
|
+
//# sourceMappingURL=chunk-T6GEYHLP.js.map
|
|
5663
|
+
//# sourceMappingURL=chunk-T6GEYHLP.js.map
|