@frontmcp/auth 0.8.1 → 0.10.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/README.md +80 -6
- package/esm/index.mjs +61 -6
- package/esm/package.json +2 -2
- package/index.js +61 -6
- package/jwks/jwks.utils.d.ts.map +1 -1
- package/package.json +2 -2
- package/utils/www-authenticate.utils.d.ts +2 -0
- package/utils/www-authenticate.utils.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,85 @@
|
|
|
1
|
-
# auth
|
|
1
|
+
# @frontmcp/auth
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Authentication, session management, and credential vault for FrontMCP servers.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@frontmcp/auth)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## Install
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
```bash
|
|
10
|
+
npm install @frontmcp/auth
|
|
11
|
+
```
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
> Typically consumed via `@frontmcp/sdk` — direct installation is only needed for advanced use cases.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Remote OAuth** — delegate authentication to an external IdP with optional DCR ([docs][docs-remote])
|
|
18
|
+
- **Local OAuth** — built-in token issuance with configurable sign keys ([docs][docs-local])
|
|
19
|
+
- **JWKS validation** — JSON Web Key Set discovery and token verification ([docs][docs-jwks])
|
|
20
|
+
- **OAuth stores** — session, token, and authorization code persistence (memory, Redis, Vercel KV) ([docs][docs-stores])
|
|
21
|
+
- **Credential vault** — encrypted storage for secrets and API keys ([docs][docs-vault])
|
|
22
|
+
- **PKCE** — Proof Key for Code Exchange (RFC 7636) built on `@frontmcp/utils` crypto ([docs][docs-pkce])
|
|
23
|
+
- **CIMD** — Client Instance Machine Detection for session continuity ([docs][docs-cimd])
|
|
24
|
+
- **Auth UI templates** — consent, login, and error pages ([docs][docs-ui])
|
|
25
|
+
- **Audience validation** — per-app audience and scope enforcement ([docs][docs-audience])
|
|
26
|
+
- **Token vault** — secure token exchange and refresh management ([docs][docs-token-vault])
|
|
27
|
+
|
|
28
|
+
## Quick Example
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { FrontMcp, App } from '@frontmcp/sdk';
|
|
32
|
+
|
|
33
|
+
@FrontMcp({
|
|
34
|
+
info: { name: 'Secure Server', version: '1.0.0' },
|
|
35
|
+
apps: [MyApp],
|
|
36
|
+
auth: {
|
|
37
|
+
type: 'remote',
|
|
38
|
+
name: 'my-idp',
|
|
39
|
+
baseUrl: 'https://idp.example.com',
|
|
40
|
+
},
|
|
41
|
+
})
|
|
42
|
+
export default class Server {}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> Full guide: [Authentication Overview][docs-overview]
|
|
46
|
+
|
|
47
|
+
## Docs
|
|
48
|
+
|
|
49
|
+
| Topic | Link |
|
|
50
|
+
| ----------------- | ---------------------------------------------- |
|
|
51
|
+
| Overview | [Authentication Overview][docs-overview] |
|
|
52
|
+
| Remote OAuth | [Remote OAuth][docs-remote] |
|
|
53
|
+
| Local OAuth | [Local OAuth][docs-local] |
|
|
54
|
+
| JWKS | [JWKS Validation][docs-jwks] |
|
|
55
|
+
| Session stores | [Session Stores][docs-stores] |
|
|
56
|
+
| Credential vault | [Credential Vault][docs-vault] |
|
|
57
|
+
| PKCE | [PKCE][docs-pkce] |
|
|
58
|
+
| CIMD | [Client Instance Machine Detection][docs-cimd] |
|
|
59
|
+
| Auth UI | [Auth UI Templates][docs-ui] |
|
|
60
|
+
| Audience & scopes | [Audience Validation][docs-audience] |
|
|
61
|
+
| Token vault | [Token Vault][docs-token-vault] |
|
|
62
|
+
|
|
63
|
+
## Related Packages
|
|
64
|
+
|
|
65
|
+
- [`@frontmcp/sdk`](../sdk) — core framework (imports auth internally)
|
|
66
|
+
- [`@frontmcp/utils`](../utils) — crypto primitives used by PKCE and vault
|
|
67
|
+
- [`@frontmcp/ui`](../ui) — consent and login page components
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
Apache-2.0 — see [LICENSE](../../LICENSE).
|
|
72
|
+
|
|
73
|
+
<!-- links -->
|
|
74
|
+
|
|
75
|
+
[docs-overview]: https://docs.agentfront.dev/frontmcp/authentication/overview
|
|
76
|
+
[docs-remote]: https://docs.agentfront.dev/frontmcp/authentication/remote
|
|
77
|
+
[docs-local]: https://docs.agentfront.dev/frontmcp/authentication/local
|
|
78
|
+
[docs-jwks]: https://docs.agentfront.dev/frontmcp/authentication/jwks
|
|
79
|
+
[docs-stores]: https://docs.agentfront.dev/frontmcp/authentication/session-stores
|
|
80
|
+
[docs-vault]: https://docs.agentfront.dev/frontmcp/authentication/credential-vault
|
|
81
|
+
[docs-pkce]: https://docs.agentfront.dev/frontmcp/authentication/pkce
|
|
82
|
+
[docs-cimd]: https://docs.agentfront.dev/frontmcp/authentication/cimd
|
|
83
|
+
[docs-ui]: https://docs.agentfront.dev/frontmcp/authentication/auth-ui
|
|
84
|
+
[docs-audience]: https://docs.agentfront.dev/frontmcp/authentication/audience
|
|
85
|
+
[docs-token-vault]: https://docs.agentfront.dev/frontmcp/authentication/token-vault
|
package/esm/index.mjs
CHANGED
|
@@ -219,7 +219,12 @@ import { bytesToHex, randomBytes, rsaVerify, createKeyPersistence } from "@front
|
|
|
219
219
|
|
|
220
220
|
// libs/auth/src/jwks/jwks.utils.ts
|
|
221
221
|
function trimSlash(s) {
|
|
222
|
-
|
|
222
|
+
const str = s ?? "";
|
|
223
|
+
let end = str.length;
|
|
224
|
+
while (end > 0 && str[end - 1] === "/") {
|
|
225
|
+
end--;
|
|
226
|
+
}
|
|
227
|
+
return str.slice(0, end);
|
|
223
228
|
}
|
|
224
229
|
function normalizeIssuer(u) {
|
|
225
230
|
return trimSlash(String(u ?? ""));
|
|
@@ -2627,10 +2632,8 @@ function parseWwwAuthenticate(header) {
|
|
|
2627
2632
|
return result;
|
|
2628
2633
|
}
|
|
2629
2634
|
const paramString = header.substring(6).trim();
|
|
2630
|
-
const
|
|
2631
|
-
|
|
2632
|
-
while ((match = paramRegex.exec(paramString)) !== null) {
|
|
2633
|
-
const [, key, value] = match;
|
|
2635
|
+
const params = parseQuotedParams(paramString);
|
|
2636
|
+
for (const [key, value] of params) {
|
|
2634
2637
|
const unescapedValue = unescapeQuotedString(value);
|
|
2635
2638
|
switch (key.toLowerCase()) {
|
|
2636
2639
|
case "resource_metadata":
|
|
@@ -2655,6 +2658,54 @@ function parseWwwAuthenticate(header) {
|
|
|
2655
2658
|
}
|
|
2656
2659
|
return result;
|
|
2657
2660
|
}
|
|
2661
|
+
function parseQuotedParams(input) {
|
|
2662
|
+
const result = [];
|
|
2663
|
+
let i = 0;
|
|
2664
|
+
const len = input.length;
|
|
2665
|
+
while (i < len) {
|
|
2666
|
+
while (i < len && (input[i] === " " || input[i] === "," || input[i] === " ")) {
|
|
2667
|
+
i++;
|
|
2668
|
+
}
|
|
2669
|
+
if (i >= len) break;
|
|
2670
|
+
const keyStart = i;
|
|
2671
|
+
while (i < len && /\w/.test(input[i])) {
|
|
2672
|
+
i++;
|
|
2673
|
+
}
|
|
2674
|
+
const key = input.slice(keyStart, i);
|
|
2675
|
+
if (!key) break;
|
|
2676
|
+
while (i < len && (input[i] === " " || input[i] === " ")) {
|
|
2677
|
+
i++;
|
|
2678
|
+
}
|
|
2679
|
+
if (i >= len || input[i] !== "=") {
|
|
2680
|
+
while (i < len && input[i] !== ",") i++;
|
|
2681
|
+
continue;
|
|
2682
|
+
}
|
|
2683
|
+
i++;
|
|
2684
|
+
while (i < len && (input[i] === " " || input[i] === " ")) {
|
|
2685
|
+
i++;
|
|
2686
|
+
}
|
|
2687
|
+
if (i >= len || input[i] !== '"') {
|
|
2688
|
+
while (i < len && input[i] !== ",") i++;
|
|
2689
|
+
continue;
|
|
2690
|
+
}
|
|
2691
|
+
i++;
|
|
2692
|
+
let value = "";
|
|
2693
|
+
while (i < len && input[i] !== '"') {
|
|
2694
|
+
if (input[i] === "\\" && i + 1 < len) {
|
|
2695
|
+
value += input[i + 1];
|
|
2696
|
+
i += 2;
|
|
2697
|
+
} else {
|
|
2698
|
+
value += input[i];
|
|
2699
|
+
i++;
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
if (i < len && input[i] === '"') {
|
|
2703
|
+
i++;
|
|
2704
|
+
}
|
|
2705
|
+
result.push([key, value]);
|
|
2706
|
+
}
|
|
2707
|
+
return result;
|
|
2708
|
+
}
|
|
2658
2709
|
function escapeQuotedString(value) {
|
|
2659
2710
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2660
2711
|
}
|
|
@@ -2664,7 +2715,11 @@ function unescapeQuotedString(value) {
|
|
|
2664
2715
|
function normalizePathSegment(path2) {
|
|
2665
2716
|
if (!path2 || path2 === "/") return "";
|
|
2666
2717
|
const normalized = path2.startsWith("/") ? path2 : `/${path2}`;
|
|
2667
|
-
|
|
2718
|
+
let end = normalized.length;
|
|
2719
|
+
while (end > 0 && normalized[end - 1] === "/") {
|
|
2720
|
+
end--;
|
|
2721
|
+
}
|
|
2722
|
+
return normalized.slice(0, end);
|
|
2668
2723
|
}
|
|
2669
2724
|
|
|
2670
2725
|
// libs/auth/src/utils/audience.validator.ts
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "FrontMCP Auth - Authentication, session management, and credential vault",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"homepage": "https://docs.agentfront.dev",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"zod": "^4.0.0"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@frontmcp/utils": "0.
|
|
53
|
+
"@frontmcp/utils": "0.10.0",
|
|
54
54
|
"jose": "^6.0.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
package/index.js
CHANGED
|
@@ -361,7 +361,12 @@ var import_utils = require("@frontmcp/utils");
|
|
|
361
361
|
|
|
362
362
|
// libs/auth/src/jwks/jwks.utils.ts
|
|
363
363
|
function trimSlash(s) {
|
|
364
|
-
|
|
364
|
+
const str = s ?? "";
|
|
365
|
+
let end = str.length;
|
|
366
|
+
while (end > 0 && str[end - 1] === "/") {
|
|
367
|
+
end--;
|
|
368
|
+
}
|
|
369
|
+
return str.slice(0, end);
|
|
365
370
|
}
|
|
366
371
|
function normalizeIssuer(u) {
|
|
367
372
|
return trimSlash(String(u ?? ""));
|
|
@@ -2756,10 +2761,8 @@ function parseWwwAuthenticate(header) {
|
|
|
2756
2761
|
return result;
|
|
2757
2762
|
}
|
|
2758
2763
|
const paramString = header.substring(6).trim();
|
|
2759
|
-
const
|
|
2760
|
-
|
|
2761
|
-
while ((match = paramRegex.exec(paramString)) !== null) {
|
|
2762
|
-
const [, key, value] = match;
|
|
2764
|
+
const params = parseQuotedParams(paramString);
|
|
2765
|
+
for (const [key, value] of params) {
|
|
2763
2766
|
const unescapedValue = unescapeQuotedString(value);
|
|
2764
2767
|
switch (key.toLowerCase()) {
|
|
2765
2768
|
case "resource_metadata":
|
|
@@ -2784,6 +2787,54 @@ function parseWwwAuthenticate(header) {
|
|
|
2784
2787
|
}
|
|
2785
2788
|
return result;
|
|
2786
2789
|
}
|
|
2790
|
+
function parseQuotedParams(input) {
|
|
2791
|
+
const result = [];
|
|
2792
|
+
let i = 0;
|
|
2793
|
+
const len = input.length;
|
|
2794
|
+
while (i < len) {
|
|
2795
|
+
while (i < len && (input[i] === " " || input[i] === "," || input[i] === " ")) {
|
|
2796
|
+
i++;
|
|
2797
|
+
}
|
|
2798
|
+
if (i >= len) break;
|
|
2799
|
+
const keyStart = i;
|
|
2800
|
+
while (i < len && /\w/.test(input[i])) {
|
|
2801
|
+
i++;
|
|
2802
|
+
}
|
|
2803
|
+
const key = input.slice(keyStart, i);
|
|
2804
|
+
if (!key) break;
|
|
2805
|
+
while (i < len && (input[i] === " " || input[i] === " ")) {
|
|
2806
|
+
i++;
|
|
2807
|
+
}
|
|
2808
|
+
if (i >= len || input[i] !== "=") {
|
|
2809
|
+
while (i < len && input[i] !== ",") i++;
|
|
2810
|
+
continue;
|
|
2811
|
+
}
|
|
2812
|
+
i++;
|
|
2813
|
+
while (i < len && (input[i] === " " || input[i] === " ")) {
|
|
2814
|
+
i++;
|
|
2815
|
+
}
|
|
2816
|
+
if (i >= len || input[i] !== '"') {
|
|
2817
|
+
while (i < len && input[i] !== ",") i++;
|
|
2818
|
+
continue;
|
|
2819
|
+
}
|
|
2820
|
+
i++;
|
|
2821
|
+
let value = "";
|
|
2822
|
+
while (i < len && input[i] !== '"') {
|
|
2823
|
+
if (input[i] === "\\" && i + 1 < len) {
|
|
2824
|
+
value += input[i + 1];
|
|
2825
|
+
i += 2;
|
|
2826
|
+
} else {
|
|
2827
|
+
value += input[i];
|
|
2828
|
+
i++;
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
if (i < len && input[i] === '"') {
|
|
2832
|
+
i++;
|
|
2833
|
+
}
|
|
2834
|
+
result.push([key, value]);
|
|
2835
|
+
}
|
|
2836
|
+
return result;
|
|
2837
|
+
}
|
|
2787
2838
|
function escapeQuotedString(value) {
|
|
2788
2839
|
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2789
2840
|
}
|
|
@@ -2793,7 +2844,11 @@ function unescapeQuotedString(value) {
|
|
|
2793
2844
|
function normalizePathSegment(path2) {
|
|
2794
2845
|
if (!path2 || path2 === "/") return "";
|
|
2795
2846
|
const normalized = path2.startsWith("/") ? path2 : `/${path2}`;
|
|
2796
|
-
|
|
2847
|
+
let end = normalized.length;
|
|
2848
|
+
while (end > 0 && normalized[end - 1] === "/") {
|
|
2849
|
+
end--;
|
|
2850
|
+
}
|
|
2851
|
+
return normalized.slice(0, end);
|
|
2797
2852
|
}
|
|
2798
2853
|
|
|
2799
2854
|
// libs/auth/src/utils/audience.validator.ts
|
package/jwks/jwks.utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"jwks.utils.d.ts","sourceRoot":"","sources":["../../src/jwks/jwks.utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"jwks.utils.d.ts","sourceRoot":"","sources":["../../src/jwks/jwks.utils.ts"],"names":[],"mappings":"AAAA,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,UAQlC;AACD,wBAAgB,eAAe,CAAC,CAAC,CAAC,EAAE,MAAM,UAEzC;AAED,uEAAuE;AACvE,wBAAgB,oBAAoB,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAgBxF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "FrontMCP Auth - Authentication, session management, and credential vault",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"homepage": "https://docs.agentfront.dev",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"zod": "^4.0.0"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@frontmcp/utils": "0.
|
|
53
|
+
"@frontmcp/utils": "0.10.0",
|
|
54
54
|
"jose": "^6.0.0"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
@@ -91,6 +91,8 @@ export declare function buildInvalidRequestHeader(prmUrl: string, description?:
|
|
|
91
91
|
/**
|
|
92
92
|
* Parse a WWW-Authenticate header value
|
|
93
93
|
*
|
|
94
|
+
* Uses character-by-character parsing to avoid ReDoS vulnerabilities.
|
|
95
|
+
*
|
|
94
96
|
* @param header - The WWW-Authenticate header value
|
|
95
97
|
* @returns Parsed header options
|
|
96
98
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"www-authenticate.utils.d.ts","sourceRoot":"","sources":["../../src/utils/www-authenticate.utils.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,iBAAiB,GAAG,eAAe,GAAG,oBAAoB,CAAC;AAEzF;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAE7B;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAE1B;;OAEG;IACH,KAAK,CAAC,EAAE,eAAe,CAAC;IAExB;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,sBAA2B,GAAG,MAAM,CAwCjF;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAIzF;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAI9D;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAMpF;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAOnH;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAMtF;AAED
|
|
1
|
+
{"version":3,"file":"www-authenticate.utils.d.ts","sourceRoot":"","sources":["../../src/utils/www-authenticate.utils.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,MAAM,MAAM,eAAe,GAAG,iBAAiB,GAAG,eAAe,GAAG,oBAAoB,CAAC;AAEzF;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAE7B;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAE1B;;OAEG;IACH,KAAK,CAAC,EAAE,eAAe,CAAC;IAExB;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,GAAE,sBAA2B,GAAG,MAAM,CAwCjF;AAED;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAIzF;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAI9D;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAMpF;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAOnH;AAED;;GAEG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAMtF;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG,sBAAsB,CAsC3E"}
|