@huloglobal/vendure-plugin-geo-block 0.1.0 → 0.2.1
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 +34 -0
- package/dist/geo-block-event.entity.d.ts +22 -0
- package/dist/geo-block-event.entity.d.ts.map +1 -0
- package/dist/geo-block-event.entity.js +68 -0
- package/dist/geo-block-event.entity.js.map +1 -0
- package/dist/geo-block.controller.d.ts +45 -25
- package/dist/geo-block.controller.d.ts.map +1 -1
- package/dist/geo-block.controller.js +390 -97
- package/dist/geo-block.controller.js.map +1 -1
- package/dist/geo-regions.d.ts +38 -13
- package/dist/geo-regions.d.ts.map +1 -1
- package/dist/geo-regions.js +212 -15
- package/dist/geo-regions.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/dist/plugin.d.ts +27 -23
- package/dist/plugin.d.ts.map +1 -1
- package/dist/plugin.js +67 -38
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
- package/ui/components/geo-block.component.ts +519 -253
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,40 @@ All notable changes to `@huloglobal/vendure-plugin-geo-block` are documented
|
|
|
4
4
|
here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
|
5
5
|
and this project adheres to [semantic versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.2.0]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- **37 region presets** (up from 8) — EU, EEA, EFTA, Schengen, Nordic,
|
|
11
|
+
Baltic, Benelux, DACH, Iberia, Balkans, GCC, MENA, ASEAN, APAC, East
|
|
12
|
+
Asia, South Asia, LATAM, Central America, Caribbean, Africa, G7, G20,
|
|
13
|
+
BRICS, OECD, NATO, Five Eyes, Commonwealth, English-speaking, and more.
|
|
14
|
+
- **Soft-block mode** — per-channel `mode` field (`block` or `soft`).
|
|
15
|
+
Soft mode renders the storefront with a "we don't ship here" banner
|
|
16
|
+
instead of hiding it.
|
|
17
|
+
- **IP allowlist with IPv4 CIDR** — per-channel list of IPs / ranges
|
|
18
|
+
that bypass every rule. For offices, oncall, payment processors.
|
|
19
|
+
- **Audit log** — new `GeoBlockEvent` entity records every block
|
|
20
|
+
decision (country, region, IP, UA, reason).
|
|
21
|
+
- **Stats endpoint** — `GET /geo-block/admin/stats` returns block totals,
|
|
22
|
+
top blocked countries, daily series and reason breakdown.
|
|
23
|
+
- **Simulator endpoint** — `POST /geo-block/admin/simulate` dry-runs a
|
|
24
|
+
hypothetical visitor against current rules without persisting anything.
|
|
25
|
+
- **Custom block page** — per-channel `blockMessage`, `blockRedirectUrl`,
|
|
26
|
+
`blockLogoUrl` fields.
|
|
27
|
+
- **Scheduled maintenance window** — plugin option for a one-shot
|
|
28
|
+
date-range lockdown (every visitor blocked except the IP allowlist).
|
|
29
|
+
- **Per-request `/geo-block/check` endpoint** — visitors can be checked
|
|
30
|
+
on the fly with logging to the audit table.
|
|
31
|
+
- **Presets catalogue endpoint** — `GET /geo-block/presets` lists every
|
|
32
|
+
preset with metadata (kind, description, country count).
|
|
33
|
+
- Redesigned admin UI: five tabs (Rules / Block page / IP allowlist /
|
|
34
|
+
Simulate / Stats) with filterable preset picker, soft/hard mode toggle
|
|
35
|
+
and a live simulator.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- Admin UI now calls `/geo-block/admin/*` directly (no `/ees/` prefix).
|
|
39
|
+
- `isAllowed()` and `ipMatchesAny()` exported for downstream use.
|
|
40
|
+
|
|
7
41
|
## [0.1.0] — Unreleased
|
|
8
42
|
|
|
9
43
|
### Added
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { DeepPartial, VendureEntity } from '@vendure/core';
|
|
2
|
+
/**
|
|
3
|
+
* One row per block decision — written every time the
|
|
4
|
+
* `/geo-block/check` (or the storefront's call to `/site-config` followed
|
|
5
|
+
* by a server-side enforcement) decides to refuse a visitor.
|
|
6
|
+
*
|
|
7
|
+
* Cheap to keep around because the admin's stats endpoint aggregates on
|
|
8
|
+
* top of it; older rows can be pruned with `GeoBlockController.gc()`.
|
|
9
|
+
*/
|
|
10
|
+
export declare class GeoBlockEvent extends VendureEntity {
|
|
11
|
+
constructor(input?: DeepPartial<GeoBlockEvent>);
|
|
12
|
+
channelId: number;
|
|
13
|
+
country: string;
|
|
14
|
+
region: string;
|
|
15
|
+
ip: string;
|
|
16
|
+
userAgent: string;
|
|
17
|
+
url: string;
|
|
18
|
+
decision: 'block' | 'soft-block' | 'allow';
|
|
19
|
+
/** Which rule rejected (or allowed) the request. */
|
|
20
|
+
reason: 'denylist' | 'country-not-allowed' | 'uk-region-not-allowed' | 'ip-allowlist' | 'maintenance' | 'ok' | string;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=geo-block-event.entity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geo-block-event.entity.d.ts","sourceRoot":"","sources":["../src/geo-block-event.entity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAG3D;;;;;;;GAOG;AACH,qBACa,aAAc,SAAQ,aAAa;gBAChC,KAAK,CAAC,EAAE,WAAW,CAAC,aAAa,CAAC;IAM9C,SAAS,EAAG,MAAM,CAAC;IAInB,OAAO,EAAG,MAAM,CAAC;IAGjB,MAAM,EAAG,MAAM,CAAC;IAGhB,EAAE,EAAG,MAAM,CAAC;IAGZ,SAAS,EAAG,MAAM,CAAC;IAGnB,GAAG,EAAG,MAAM,CAAC;IAIb,QAAQ,EAAG,OAAO,GAAG,YAAY,GAAG,OAAO,CAAC;IAE5C,oDAAoD;IAEpD,MAAM,EAAG,UAAU,GAAG,qBAAqB,GAAG,uBAAuB,GAAG,cAAc,GAAG,aAAa,GAAG,IAAI,GAAG,MAAM,CAAC;CAC1H"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.GeoBlockEvent = void 0;
|
|
13
|
+
const core_1 = require("@vendure/core");
|
|
14
|
+
const typeorm_1 = require("typeorm");
|
|
15
|
+
/**
|
|
16
|
+
* One row per block decision — written every time the
|
|
17
|
+
* `/geo-block/check` (or the storefront's call to `/site-config` followed
|
|
18
|
+
* by a server-side enforcement) decides to refuse a visitor.
|
|
19
|
+
*
|
|
20
|
+
* Cheap to keep around because the admin's stats endpoint aggregates on
|
|
21
|
+
* top of it; older rows can be pruned with `GeoBlockController.gc()`.
|
|
22
|
+
*/
|
|
23
|
+
let GeoBlockEvent = class GeoBlockEvent extends core_1.VendureEntity {
|
|
24
|
+
constructor(input) {
|
|
25
|
+
super(input);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
exports.GeoBlockEvent = GeoBlockEvent;
|
|
29
|
+
__decorate([
|
|
30
|
+
(0, typeorm_1.Index)(),
|
|
31
|
+
(0, typeorm_1.Column)({ type: 'int', default: 1 }),
|
|
32
|
+
__metadata("design:type", Number)
|
|
33
|
+
], GeoBlockEvent.prototype, "channelId", void 0);
|
|
34
|
+
__decorate([
|
|
35
|
+
(0, typeorm_1.Index)(),
|
|
36
|
+
(0, typeorm_1.Column)({ type: 'varchar', length: 8, nullable: true }),
|
|
37
|
+
__metadata("design:type", String)
|
|
38
|
+
], GeoBlockEvent.prototype, "country", void 0);
|
|
39
|
+
__decorate([
|
|
40
|
+
(0, typeorm_1.Column)({ type: 'varchar', length: 8, nullable: true }),
|
|
41
|
+
__metadata("design:type", String)
|
|
42
|
+
], GeoBlockEvent.prototype, "region", void 0);
|
|
43
|
+
__decorate([
|
|
44
|
+
(0, typeorm_1.Column)({ type: 'varchar', length: 64, nullable: true }),
|
|
45
|
+
__metadata("design:type", String)
|
|
46
|
+
], GeoBlockEvent.prototype, "ip", void 0);
|
|
47
|
+
__decorate([
|
|
48
|
+
(0, typeorm_1.Column)({ type: 'text', nullable: true }),
|
|
49
|
+
__metadata("design:type", String)
|
|
50
|
+
], GeoBlockEvent.prototype, "userAgent", void 0);
|
|
51
|
+
__decorate([
|
|
52
|
+
(0, typeorm_1.Column)({ type: 'varchar', length: 2048, nullable: true }),
|
|
53
|
+
__metadata("design:type", String)
|
|
54
|
+
], GeoBlockEvent.prototype, "url", void 0);
|
|
55
|
+
__decorate([
|
|
56
|
+
(0, typeorm_1.Index)(),
|
|
57
|
+
(0, typeorm_1.Column)({ type: 'varchar', length: 32 }),
|
|
58
|
+
__metadata("design:type", String)
|
|
59
|
+
], GeoBlockEvent.prototype, "decision", void 0);
|
|
60
|
+
__decorate([
|
|
61
|
+
(0, typeorm_1.Column)({ type: 'varchar', length: 64 }),
|
|
62
|
+
__metadata("design:type", String)
|
|
63
|
+
], GeoBlockEvent.prototype, "reason", void 0);
|
|
64
|
+
exports.GeoBlockEvent = GeoBlockEvent = __decorate([
|
|
65
|
+
(0, typeorm_1.Entity)(),
|
|
66
|
+
__metadata("design:paramtypes", [Object])
|
|
67
|
+
], GeoBlockEvent);
|
|
68
|
+
//# sourceMappingURL=geo-block-event.entity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geo-block-event.entity.js","sourceRoot":"","sources":["../src/geo-block-event.entity.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,wCAA2D;AAC3D,qCAAgD;AAEhD;;;;;;;GAOG;AAEI,IAAM,aAAa,GAAnB,MAAM,aAAc,SAAQ,oBAAa;IAC5C,YAAY,KAAkC;QAC1C,KAAK,CAAC,KAAK,CAAC,CAAC;IACjB,CAAC;CA6BJ,CAAA;AAhCY,sCAAa;AAOtB;IAFC,IAAA,eAAK,GAAE;IACP,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;;gDACjB;AAInB;IAFC,IAAA,eAAK,GAAE;IACP,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;8CACtC;AAGjB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;6CACvC;AAGhB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;yCAC5C;AAGZ;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;gDACtB;AAGnB;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;0CAC7C;AAIb;IAFC,IAAA,eAAK,GAAE;IACP,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;;+CACI;AAI5C;IADC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;;6CAC+E;wBA/B9G,aAAa;IADzB,IAAA,gBAAM,GAAE;;GACI,aAAa,CAgCzB"}
|
|
@@ -4,22 +4,14 @@ interface SiteConfig {
|
|
|
4
4
|
channelToken: string;
|
|
5
5
|
geoBlock: {
|
|
6
6
|
enabled: boolean;
|
|
7
|
-
|
|
8
|
-
* the storefront should let through, or `null` meaning "no
|
|
9
|
-
* country restriction" (WORLDWIDE preset). */
|
|
7
|
+
mode: 'block' | 'soft';
|
|
10
8
|
allowedCountries: string[] | null;
|
|
11
|
-
/** Countries the admin always blocks regardless of region. The
|
|
12
|
-
* resolved `allowedCountries` already has these subtracted —
|
|
13
|
-
* this field is exposed for diagnostics + so the storefront can
|
|
14
|
-
* enforce it even when allowedCountries is `null`. */
|
|
15
9
|
blockedCountries: string[];
|
|
16
|
-
/** UK subdivisions that must additionally match when the visitor
|
|
17
|
-
* resolves to GB. Empty = "any UK region allowed". */
|
|
18
10
|
allowedGbRegions: string[];
|
|
19
|
-
/** The raw region presets the admin selected — exposed for
|
|
20
|
-
* diagnostics (and so the frontend can display the admin's
|
|
21
|
-
* intent in any debug UI). */
|
|
22
11
|
allowedRegions: string[];
|
|
12
|
+
blockMessage: string;
|
|
13
|
+
blockRedirectUrl: string | null;
|
|
14
|
+
blockLogoUrl: string | null;
|
|
23
15
|
};
|
|
24
16
|
companyData: {
|
|
25
17
|
showCompanyNumber: boolean;
|
|
@@ -27,30 +19,58 @@ interface SiteConfig {
|
|
|
27
19
|
};
|
|
28
20
|
}
|
|
29
21
|
/**
|
|
30
|
-
* Public
|
|
31
|
-
* channel. The storefront calls GET /ees/site-config and identifies the
|
|
32
|
-
* channel via the standard `vendure-token` header (the same one shop-api
|
|
33
|
-
* uses), or via ?token=<channelToken> as a fallback for static fetchers.
|
|
22
|
+
* Public + admin endpoints for the storefront geo-block.
|
|
34
23
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
24
|
+
* Public:
|
|
25
|
+
* GET /geo-block/site-config — channel-aware rules the storefront caches
|
|
26
|
+
* GET /geo-block/check — per-request decision (with logging)
|
|
27
|
+
* GET /geo-block/presets — preset catalogue for the storefront banner
|
|
28
|
+
*
|
|
29
|
+
* Admin:
|
|
30
|
+
* GET /geo-block/admin/channels — all channels + their rules
|
|
31
|
+
* POST /geo-block/admin/save — save one channel's rules
|
|
32
|
+
* GET /geo-block/admin/stats — block totals + top blocked countries
|
|
33
|
+
* POST /geo-block/admin/simulate — "what would happen if a visitor from X visited?"
|
|
34
|
+
* POST /geo-block/admin/gc — prune old GeoBlockEvent rows
|
|
37
35
|
*/
|
|
38
36
|
export declare class GeoBlockController {
|
|
39
37
|
private connection;
|
|
40
38
|
constructor(connection: TransactionalConnection);
|
|
41
39
|
getConfig(req: Request): Promise<SiteConfig>;
|
|
42
40
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
41
|
+
* Per-request decision endpoint. The storefront calls this on entry
|
|
42
|
+
* (or middleware can call it) with optional `?country=` overrides.
|
|
43
|
+
* Logs the decision so the admin stats panel shows top blocked
|
|
44
|
+
* countries.
|
|
45
|
+
*
|
|
46
|
+
* Returns 200 with `{ allowed: boolean, reason, mode }` regardless —
|
|
47
|
+
* the storefront decides whether to redirect or render a banner.
|
|
47
48
|
*/
|
|
49
|
+
check(req: Request, res: Response): Promise<Response<any, Record<string, any>>>;
|
|
50
|
+
/** Catalogue of available region presets — fed to the admin picker
|
|
51
|
+
* and any storefront that wants to show "we serve <X>". */
|
|
52
|
+
presets(res: Response): Response<any, Record<string, any>>;
|
|
53
|
+
/** Admin: plugin health + update availability. Read by the admin UI
|
|
54
|
+
* banner so the operator sees when a new version is on npm. */
|
|
55
|
+
status(ctx: RequestContext, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
48
56
|
listChannels(ctx: RequestContext, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
57
|
+
saveChannel(ctx: RequestContext, body: any, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
49
58
|
/**
|
|
50
|
-
* Admin:
|
|
51
|
-
*
|
|
59
|
+
* Admin: top blocked countries + daily series + totals over the last
|
|
60
|
+
* N days (default 30). Fed straight into the admin Stats panel.
|
|
52
61
|
*/
|
|
53
|
-
|
|
62
|
+
stats(ctx: RequestContext, req: Request, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
63
|
+
/**
|
|
64
|
+
* Admin: dry-run check — "what would happen if a visitor from US
|
|
65
|
+
* with no UK region came in?". Lets the admin sanity-check their
|
|
66
|
+
* rules without standing up a proxy.
|
|
67
|
+
*/
|
|
68
|
+
simulate(ctx: RequestContext, body: any, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
69
|
+
gc(ctx: RequestContext, body: any, res: Response): Promise<Response<any, Record<string, any>> | undefined>;
|
|
70
|
+
private loadChannelRow;
|
|
71
|
+
private buildConfig;
|
|
72
|
+
private logEvent;
|
|
73
|
+
private defaultMessage;
|
|
54
74
|
}
|
|
55
75
|
export {};
|
|
56
76
|
//# sourceMappingURL=geo-block.controller.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"geo-block.controller.d.ts","sourceRoot":"","sources":["../src/geo-block.controller.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"geo-block.controller.d.ts","sourceRoot":"","sources":["../src/geo-block.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAA2B,cAAc,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AACjG,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AA0B5C,UAAU,UAAU;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE;QACN,OAAO,EAAE,OAAO,CAAC;QACjB,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;QACvB,gBAAgB,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAClC,gBAAgB,EAAE,MAAM,EAAE,CAAC;QAC3B,gBAAgB,EAAE,MAAM,EAAE,CAAC;QAC3B,cAAc,EAAE,MAAM,EAAE,CAAC;QACzB,YAAY,EAAE,MAAM,CAAC;QACrB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;QAChC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;KAC/B,CAAC;IACF,WAAW,EAAE;QACT,iBAAiB,EAAE,OAAO,CAAC;QAC3B,aAAa,EAAE,MAAM,CAAC;KACzB,CAAC;CACL;AA4BD;;;;;;;;;;;;;;GAcG;AACH,qBACa,kBAAkB;IACf,OAAO,CAAC,UAAU;gBAAV,UAAU,EAAE,uBAAuB;IAGjD,SAAS,CAAQ,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC;IAazD;;;;;;;;OAQG;IAEG,KAAK,CAAQ,GAAG,EAAE,OAAO,EAAS,GAAG,EAAE,QAAQ;IAyErD;+DAC2D;IAE3D,OAAO,CAAQ,GAAG,EAAE,QAAQ;IAI5B;mEAC+D;IAEzD,MAAM,CAAQ,GAAG,EAAE,cAAc,EAAS,GAAG,EAAE,QAAQ;IAavD,YAAY,CAAQ,GAAG,EAAE,cAAc,EAAS,GAAG,EAAE,QAAQ;IA+C7D,WAAW,CAAQ,GAAG,EAAE,cAAc,EAAU,IAAI,EAAE,GAAG,EAAS,GAAG,EAAE,QAAQ;IA+CrF;;;OAGG;IAEG,KAAK,CAAQ,GAAG,EAAE,cAAc,EAAS,GAAG,EAAE,OAAO,EAAS,GAAG,EAAE,QAAQ;IA+CjF;;;;OAIG;IAEG,QAAQ,CAAQ,GAAG,EAAE,cAAc,EAAU,IAAI,EAAE,GAAG,EAAS,GAAG,EAAE,QAAQ;IAyB5E,EAAE,CAAQ,GAAG,EAAE,cAAc,EAAU,IAAI,EAAE,GAAG,EAAS,GAAG,EAAE,QAAQ;YAY9D,cAAc;IAqB5B,OAAO,CAAC,WAAW;YA6BL,QAAQ;IA4BtB,OAAO,CAAC,cAAc;CAczB"}
|