@opendatalabs/service-auth 1.0.0-next.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/README.md +140 -0
- package/dist/client/index.d.ts +141 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +192 -0
- package/dist/client/index.js.map +1 -0
- package/dist/jwks/index.d.ts +58 -0
- package/dist/jwks/index.d.ts.map +1 -0
- package/dist/jwks/index.js +84 -0
- package/dist/jwks/index.js.map +1 -0
- package/dist/registration/index.d.ts +7 -0
- package/dist/registration/index.d.ts.map +1 -0
- package/dist/registration/index.js +7 -0
- package/dist/registration/index.js.map +1 -0
- package/dist/registration/manifest.d.ts +70 -0
- package/dist/registration/manifest.d.ts.map +1 -0
- package/dist/registration/manifest.js +105 -0
- package/dist/registration/manifest.js.map +1 -0
- package/dist/registration/provision.d.ts +68 -0
- package/dist/registration/provision.d.ts.map +1 -0
- package/dist/registration/provision.js +132 -0
- package/dist/registration/provision.js.map +1 -0
- package/dist/registration/schema.d.ts +65 -0
- package/dist/registration/schema.d.ts.map +1 -0
- package/dist/registration/schema.js +59 -0
- package/dist/registration/schema.js.map +1 -0
- package/dist/resource-server/index.d.ts +73 -0
- package/dist/resource-server/index.d.ts.map +1 -0
- package/dist/resource-server/index.js +199 -0
- package/dist/resource-server/index.js.map +1 -0
- package/dist/testkit/index.d.ts +91 -0
- package/dist/testkit/index.d.ts.map +1 -0
- package/dist/testkit/index.js +108 -0
- package/dist/testkit/index.js.map +1 -0
- package/package.json +83 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest loading + Hydra-shape translation.
|
|
3
|
+
*
|
|
4
|
+
* `loadManifest(path)` reads a YAML file from disk and runs it
|
|
5
|
+
* through the zod schema; `manifestToHydraClient(manifest)` returns
|
|
6
|
+
* the body shape Hydra v2.x accepts at
|
|
7
|
+
* `POST /admin/clients` / `PUT /admin/clients/{id}`.
|
|
8
|
+
*
|
|
9
|
+
* Hydra v2.x mapping
|
|
10
|
+
* ------------------
|
|
11
|
+
*
|
|
12
|
+
* client_id: <hydra_client_id>
|
|
13
|
+
* client_name: <service_id>
|
|
14
|
+
* token_endpoint_auth_method: private_key_jwt
|
|
15
|
+
* jwks_uri: <jwks_uri>
|
|
16
|
+
* grant_types: <grant_types>
|
|
17
|
+
* audience: <allowed_audiences>
|
|
18
|
+
* scope: <allowed_scopes joined by space>
|
|
19
|
+
* access_token_strategy: jwt
|
|
20
|
+
* metadata: { owner_team, key_rotation_policy }
|
|
21
|
+
*
|
|
22
|
+
* `access_token_strategy` is the per-client knob Hydra v2.x exposes
|
|
23
|
+
* (server-wide default of `opaque` is overridden here). See Hydra docs
|
|
24
|
+
* referenced in package README.
|
|
25
|
+
*/
|
|
26
|
+
import { serviceAuthManifestSchema, type ServiceAuthManifest } from "./schema";
|
|
27
|
+
export type { ServiceAuthManifest };
|
|
28
|
+
export { serviceAuthManifestSchema };
|
|
29
|
+
export declare class ManifestValidationError extends Error {
|
|
30
|
+
readonly path: string;
|
|
31
|
+
readonly issues: Array<{
|
|
32
|
+
path: string;
|
|
33
|
+
message: string;
|
|
34
|
+
}>;
|
|
35
|
+
constructor(path: string, issues: Array<{
|
|
36
|
+
path: string;
|
|
37
|
+
message: string;
|
|
38
|
+
}>);
|
|
39
|
+
}
|
|
40
|
+
export declare function parseManifestText(text: string, sourcePath?: string): ServiceAuthManifest;
|
|
41
|
+
export declare function loadManifest(filePath: string): Promise<ServiceAuthManifest>;
|
|
42
|
+
/**
|
|
43
|
+
* Hydra v2.x OAuth2 client body. We don't import the @ory/client SDK
|
|
44
|
+
* to keep this package zero-runtime-dep on Ory tooling; the shape is
|
|
45
|
+
* a stable HTTP contract.
|
|
46
|
+
*/
|
|
47
|
+
export interface HydraClientBody {
|
|
48
|
+
client_id: string;
|
|
49
|
+
client_name: string;
|
|
50
|
+
token_endpoint_auth_method: "private_key_jwt";
|
|
51
|
+
jwks_uri: string;
|
|
52
|
+
grant_types: string[];
|
|
53
|
+
audience: string[];
|
|
54
|
+
scope: string;
|
|
55
|
+
access_token_strategy: "jwt";
|
|
56
|
+
metadata: Record<string, unknown>;
|
|
57
|
+
}
|
|
58
|
+
export declare function manifestToHydraClient(manifest: ServiceAuthManifest): HydraClientBody;
|
|
59
|
+
/**
|
|
60
|
+
* Diff a desired client body against an existing one. Returns the
|
|
61
|
+
* subset of keys whose values changed; empty object means "no-op".
|
|
62
|
+
*
|
|
63
|
+
* Comparison is JSON-value equality with array order-sensitivity
|
|
64
|
+
* preserved (Hydra returns arrays in registration order).
|
|
65
|
+
*/
|
|
66
|
+
export declare function diffHydraClient(desired: HydraClientBody, existing: Partial<HydraClientBody> | null): Record<string, {
|
|
67
|
+
from: unknown;
|
|
68
|
+
to: unknown;
|
|
69
|
+
}>;
|
|
70
|
+
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/registration/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAIH,OAAO,EACL,yBAAyB,EACzB,KAAK,mBAAmB,EACzB,MAAM,UAAU,CAAC;AAElB,YAAY,EAAE,mBAAmB,EAAE,CAAC;AACpC,OAAO,EAAE,yBAAyB,EAAE,CAAC;AAErC,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;gBAC9C,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAU3E;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,UAAU,SAAa,GACtB,mBAAmB,CAwBrB;AAED,wBAAsB,YAAY,CAChC,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,mBAAmB,CAAC,CAG9B;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,0BAA0B,EAAE,iBAAiB,CAAC;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,qBAAqB,EAAE,KAAK,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,mBAAmB,GAC5B,eAAe,CAgBjB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,GACxC,MAAM,CAAC,MAAM,EAAE;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,EAAE,EAAE,OAAO,CAAA;CAAE,CAAC,CAchD"}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Manifest loading + Hydra-shape translation.
|
|
3
|
+
*
|
|
4
|
+
* `loadManifest(path)` reads a YAML file from disk and runs it
|
|
5
|
+
* through the zod schema; `manifestToHydraClient(manifest)` returns
|
|
6
|
+
* the body shape Hydra v2.x accepts at
|
|
7
|
+
* `POST /admin/clients` / `PUT /admin/clients/{id}`.
|
|
8
|
+
*
|
|
9
|
+
* Hydra v2.x mapping
|
|
10
|
+
* ------------------
|
|
11
|
+
*
|
|
12
|
+
* client_id: <hydra_client_id>
|
|
13
|
+
* client_name: <service_id>
|
|
14
|
+
* token_endpoint_auth_method: private_key_jwt
|
|
15
|
+
* jwks_uri: <jwks_uri>
|
|
16
|
+
* grant_types: <grant_types>
|
|
17
|
+
* audience: <allowed_audiences>
|
|
18
|
+
* scope: <allowed_scopes joined by space>
|
|
19
|
+
* access_token_strategy: jwt
|
|
20
|
+
* metadata: { owner_team, key_rotation_policy }
|
|
21
|
+
*
|
|
22
|
+
* `access_token_strategy` is the per-client knob Hydra v2.x exposes
|
|
23
|
+
* (server-wide default of `opaque` is overridden here). See Hydra docs
|
|
24
|
+
* referenced in package README.
|
|
25
|
+
*/
|
|
26
|
+
import { readFile } from "node:fs/promises";
|
|
27
|
+
import { parse as parseYaml } from "yaml";
|
|
28
|
+
import { serviceAuthManifestSchema, } from "./schema";
|
|
29
|
+
export { serviceAuthManifestSchema };
|
|
30
|
+
export class ManifestValidationError extends Error {
|
|
31
|
+
path;
|
|
32
|
+
issues;
|
|
33
|
+
constructor(path, issues) {
|
|
34
|
+
super(`service-auth: manifest ${path} failed validation: ${issues
|
|
35
|
+
.map((i) => `${i.path}: ${i.message}`)
|
|
36
|
+
.join("; ")}`);
|
|
37
|
+
this.name = "ManifestValidationError";
|
|
38
|
+
this.path = path;
|
|
39
|
+
this.issues = issues;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function parseManifestText(text, sourcePath = "<inline>") {
|
|
43
|
+
let raw;
|
|
44
|
+
try {
|
|
45
|
+
raw = parseYaml(text);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
throw new ManifestValidationError(sourcePath, [
|
|
49
|
+
{
|
|
50
|
+
path: "<root>",
|
|
51
|
+
message: err instanceof Error ? err.message : "yaml parse failed",
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
const parsed = serviceAuthManifestSchema.safeParse(raw);
|
|
56
|
+
if (!parsed.success) {
|
|
57
|
+
throw new ManifestValidationError(sourcePath, parsed.error.issues.map((i) => ({
|
|
58
|
+
path: i.path.join(".") || "<root>",
|
|
59
|
+
message: i.message,
|
|
60
|
+
})));
|
|
61
|
+
}
|
|
62
|
+
return parsed.data;
|
|
63
|
+
}
|
|
64
|
+
export async function loadManifest(filePath) {
|
|
65
|
+
const text = await readFile(filePath, "utf8");
|
|
66
|
+
return parseManifestText(text, filePath);
|
|
67
|
+
}
|
|
68
|
+
export function manifestToHydraClient(manifest) {
|
|
69
|
+
return {
|
|
70
|
+
client_id: manifest.hydra_client_id,
|
|
71
|
+
client_name: manifest.service_id,
|
|
72
|
+
token_endpoint_auth_method: "private_key_jwt",
|
|
73
|
+
jwks_uri: manifest.jwks_uri,
|
|
74
|
+
grant_types: [...manifest.grant_types],
|
|
75
|
+
audience: [...manifest.allowed_audiences],
|
|
76
|
+
scope: manifest.allowed_scopes.join(" "),
|
|
77
|
+
access_token_strategy: "jwt",
|
|
78
|
+
metadata: {
|
|
79
|
+
owner_team: manifest.owner_team,
|
|
80
|
+
key_rotation_policy: manifest.key_rotation_policy,
|
|
81
|
+
service_id: manifest.service_id,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Diff a desired client body against an existing one. Returns the
|
|
87
|
+
* subset of keys whose values changed; empty object means "no-op".
|
|
88
|
+
*
|
|
89
|
+
* Comparison is JSON-value equality with array order-sensitivity
|
|
90
|
+
* preserved (Hydra returns arrays in registration order).
|
|
91
|
+
*/
|
|
92
|
+
export function diffHydraClient(desired, existing) {
|
|
93
|
+
if (!existing) {
|
|
94
|
+
return Object.fromEntries(Object.entries(desired).map(([k, v]) => [k, { from: undefined, to: v }]));
|
|
95
|
+
}
|
|
96
|
+
const out = {};
|
|
97
|
+
for (const [key, want] of Object.entries(desired)) {
|
|
98
|
+
const have = existing[key];
|
|
99
|
+
if (JSON.stringify(have) !== JSON.stringify(want)) {
|
|
100
|
+
out[key] = { from: have, to: want };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/registration/manifest.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hydra-client provisioning. INTERNAL Vana package.
|
|
3
|
+
*
|
|
4
|
+
* Takes a parsed manifest and reconciles it against a Hydra admin API:
|
|
5
|
+
*
|
|
6
|
+
* 1. GET /admin/clients/{client_id}
|
|
7
|
+
* -> 404: create via POST /admin/clients
|
|
8
|
+
* -> 200: PUT /admin/clients/{client_id} with the full desired
|
|
9
|
+
* body (Hydra v2.x semantics: PUT replaces).
|
|
10
|
+
* 2. Logs the planned diff before any write.
|
|
11
|
+
* 3. Returns the resulting client config.
|
|
12
|
+
*
|
|
13
|
+
* `dryRun: true` performs steps 1 and 2 only.
|
|
14
|
+
*
|
|
15
|
+
* Auth
|
|
16
|
+
* ----
|
|
17
|
+
*
|
|
18
|
+
* The caller passes a `fetch` and (for the prod Hydra admin behind
|
|
19
|
+
* Cloud IAP) an `authHeaderFor(audience)` async hook. The hook
|
|
20
|
+
* returns the `Authorization` header value (e.g. `"Bearer <ID
|
|
21
|
+
* token>"`); a local Hydra in dev returns `null` and no header is
|
|
22
|
+
* sent. We do NOT import Account's `hydra-admin.ts` directly here
|
|
23
|
+
* because this package must not depend on apps/*.
|
|
24
|
+
*/
|
|
25
|
+
import { type HydraClientBody, type ServiceAuthManifest } from "./manifest";
|
|
26
|
+
export type ProvisionFetch = (input: string, init?: RequestInit) => Promise<Response>;
|
|
27
|
+
export interface ProvisionOptions {
|
|
28
|
+
/** Hydra admin base URL, e.g. `https://oauth-admin-dev.vana.org`. */
|
|
29
|
+
adminUrl: string;
|
|
30
|
+
/**
|
|
31
|
+
* Audience to mint an auth header for (Cloud IAP id-token aud, or
|
|
32
|
+
* the admin URL itself). Same value Account's hydra-admin.ts uses.
|
|
33
|
+
*/
|
|
34
|
+
adminAudience: string;
|
|
35
|
+
/**
|
|
36
|
+
* Returns the value of the `Authorization` header for an outbound
|
|
37
|
+
* admin request, or null/undefined to send no header (local dev).
|
|
38
|
+
* Typically wraps `fetchGoogleIdTokenForAudience` from Account.
|
|
39
|
+
*/
|
|
40
|
+
authHeaderFor?: (audience: string) => Promise<string | null | undefined> | string | null | undefined;
|
|
41
|
+
/** Test/dev seam. */
|
|
42
|
+
fetch?: ProvisionFetch;
|
|
43
|
+
/**
|
|
44
|
+
* Sink for human-readable plan output. Defaults to console.log; tests
|
|
45
|
+
* pass an array.push.
|
|
46
|
+
*/
|
|
47
|
+
log?: (line: string) => void;
|
|
48
|
+
/** If true, skip writes. */
|
|
49
|
+
dryRun?: boolean;
|
|
50
|
+
}
|
|
51
|
+
export type ProvisionAction = "create" | "update" | "noop";
|
|
52
|
+
export interface ProvisionResult {
|
|
53
|
+
action: ProvisionAction;
|
|
54
|
+
client: HydraClientBody;
|
|
55
|
+
diff: Record<string, {
|
|
56
|
+
from: unknown;
|
|
57
|
+
to: unknown;
|
|
58
|
+
}>;
|
|
59
|
+
dryRun: boolean;
|
|
60
|
+
}
|
|
61
|
+
declare class HydraAdminApiError extends Error {
|
|
62
|
+
readonly status: number;
|
|
63
|
+
readonly body: unknown;
|
|
64
|
+
constructor(status: number, body: unknown, hint: string);
|
|
65
|
+
}
|
|
66
|
+
export declare function provisionServiceClient(manifest: ServiceAuthManifest, options: ProvisionOptions): Promise<ProvisionResult>;
|
|
67
|
+
export { HydraAdminApiError };
|
|
68
|
+
//# sourceMappingURL=provision.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provision.d.ts","sourceRoot":"","sources":["../../src/registration/provision.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAEL,KAAK,eAAe,EAEpB,KAAK,mBAAmB,EACzB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,cAAc,GAAG,CAC3B,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,WAAW,KACf,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEvB,MAAM,WAAW,gBAAgB;IAC/B,qEAAqE;IACrE,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CACd,QAAQ,EAAE,MAAM,KACb,OAAO,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IACpE,qBAAqB;IACrB,KAAK,CAAC,EAAE,cAAc,CAAC;IACvB;;;OAGG;IACH,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,4BAA4B;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE3D,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,eAAe,CAAC;IACxB,MAAM,EAAE,eAAe,CAAC;IACxB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,OAAO,CAAC;QAAC,EAAE,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACrD,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,cAAM,kBAAmB,SAAQ,KAAK;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;gBACX,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM;CAMxD;AA2BD,wBAAsB,sBAAsB,CAC1C,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,EAAE,gBAAgB,GACxB,OAAO,CAAC,eAAe,CAAC,CAwF1B;AAED,OAAO,EAAE,kBAAkB,EAAE,CAAC"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hydra-client provisioning. INTERNAL Vana package.
|
|
3
|
+
*
|
|
4
|
+
* Takes a parsed manifest and reconciles it against a Hydra admin API:
|
|
5
|
+
*
|
|
6
|
+
* 1. GET /admin/clients/{client_id}
|
|
7
|
+
* -> 404: create via POST /admin/clients
|
|
8
|
+
* -> 200: PUT /admin/clients/{client_id} with the full desired
|
|
9
|
+
* body (Hydra v2.x semantics: PUT replaces).
|
|
10
|
+
* 2. Logs the planned diff before any write.
|
|
11
|
+
* 3. Returns the resulting client config.
|
|
12
|
+
*
|
|
13
|
+
* `dryRun: true` performs steps 1 and 2 only.
|
|
14
|
+
*
|
|
15
|
+
* Auth
|
|
16
|
+
* ----
|
|
17
|
+
*
|
|
18
|
+
* The caller passes a `fetch` and (for the prod Hydra admin behind
|
|
19
|
+
* Cloud IAP) an `authHeaderFor(audience)` async hook. The hook
|
|
20
|
+
* returns the `Authorization` header value (e.g. `"Bearer <ID
|
|
21
|
+
* token>"`); a local Hydra in dev returns `null` and no header is
|
|
22
|
+
* sent. We do NOT import Account's `hydra-admin.ts` directly here
|
|
23
|
+
* because this package must not depend on apps/*.
|
|
24
|
+
*/
|
|
25
|
+
import { diffHydraClient, manifestToHydraClient, } from "./manifest";
|
|
26
|
+
class HydraAdminApiError extends Error {
|
|
27
|
+
status;
|
|
28
|
+
body;
|
|
29
|
+
constructor(status, body, hint) {
|
|
30
|
+
super(`service-auth provision: hydra admin ${hint} failed (${status})`);
|
|
31
|
+
this.name = "HydraAdminApiError";
|
|
32
|
+
this.status = status;
|
|
33
|
+
this.body = body;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function trimSlash(url) {
|
|
37
|
+
return url.replace(/\/+$/, "");
|
|
38
|
+
}
|
|
39
|
+
async function buildHeaders(options) {
|
|
40
|
+
const headers = { accept: "application/json" };
|
|
41
|
+
if (options.authHeaderFor) {
|
|
42
|
+
const value = await options.authHeaderFor(options.adminAudience);
|
|
43
|
+
if (value)
|
|
44
|
+
headers.authorization = value;
|
|
45
|
+
}
|
|
46
|
+
return headers;
|
|
47
|
+
}
|
|
48
|
+
async function parseBody(response) {
|
|
49
|
+
const text = await response.text();
|
|
50
|
+
if (!text)
|
|
51
|
+
return null;
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(text);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return { raw: text };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export async function provisionServiceClient(manifest, options) {
|
|
60
|
+
const adminUrl = trimSlash(options.adminUrl);
|
|
61
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
62
|
+
const log = options.log ?? ((line) => console.log(line));
|
|
63
|
+
const dryRun = options.dryRun === true;
|
|
64
|
+
const desired = manifestToHydraClient(manifest);
|
|
65
|
+
log(`-> service: ${manifest.service_id}`);
|
|
66
|
+
log(`-> hydra client_id: ${desired.client_id}`);
|
|
67
|
+
log(`-> admin: ${adminUrl}`);
|
|
68
|
+
// 1. Read existing.
|
|
69
|
+
const getUrl = `${adminUrl}/admin/clients/${encodeURIComponent(desired.client_id)}`;
|
|
70
|
+
const headers = await buildHeaders(options);
|
|
71
|
+
const getResp = await fetchImpl(getUrl, { method: "GET", headers });
|
|
72
|
+
let existing = null;
|
|
73
|
+
if (getResp.status === 200) {
|
|
74
|
+
existing = (await parseBody(getResp));
|
|
75
|
+
}
|
|
76
|
+
else if (getResp.status === 404) {
|
|
77
|
+
existing = null;
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
throw new HydraAdminApiError(getResp.status, await parseBody(getResp), `GET ${getUrl}`);
|
|
81
|
+
}
|
|
82
|
+
const diff = diffHydraClient(desired, existing);
|
|
83
|
+
const action = existing
|
|
84
|
+
? Object.keys(diff).length === 0
|
|
85
|
+
? "noop"
|
|
86
|
+
: "update"
|
|
87
|
+
: "create";
|
|
88
|
+
log(`-> action: ${action}`);
|
|
89
|
+
if (action === "noop") {
|
|
90
|
+
log("-> no changes; client is up to date.");
|
|
91
|
+
return { action, client: desired, diff, dryRun };
|
|
92
|
+
}
|
|
93
|
+
log("-> planned diff:");
|
|
94
|
+
for (const [key, { from, to }] of Object.entries(diff)) {
|
|
95
|
+
log(` ${key}: ${JSON.stringify(from)} -> ${JSON.stringify(to)}`);
|
|
96
|
+
}
|
|
97
|
+
if (dryRun) {
|
|
98
|
+
log("-> dryRun=true; not writing.");
|
|
99
|
+
return { action, client: desired, diff, dryRun };
|
|
100
|
+
}
|
|
101
|
+
// 2. Write.
|
|
102
|
+
if (action === "create") {
|
|
103
|
+
const resp = await fetchImpl(`${adminUrl}/admin/clients`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
...headers,
|
|
107
|
+
"content-type": "application/json",
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify(desired),
|
|
110
|
+
});
|
|
111
|
+
if (!resp.ok) {
|
|
112
|
+
throw new HydraAdminApiError(resp.status, await parseBody(resp), `POST /admin/clients`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
const resp = await fetchImpl(`${adminUrl}/admin/clients/${encodeURIComponent(desired.client_id)}`, {
|
|
117
|
+
method: "PUT",
|
|
118
|
+
headers: {
|
|
119
|
+
...headers,
|
|
120
|
+
"content-type": "application/json",
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify(desired),
|
|
123
|
+
});
|
|
124
|
+
if (!resp.ok) {
|
|
125
|
+
throw new HydraAdminApiError(resp.status, await parseBody(resp), `PUT /admin/clients/${desired.client_id}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
log(`-> ${action} complete.`);
|
|
129
|
+
return { action, client: desired, diff, dryRun };
|
|
130
|
+
}
|
|
131
|
+
export { HydraAdminApiError };
|
|
132
|
+
//# sourceMappingURL=provision.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provision.js","sourceRoot":"","sources":["../../src/registration/provision.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* service-auth.yaml manifest schema. INTERNAL Vana package.
|
|
3
|
+
*
|
|
4
|
+
* Every service that authenticates to Vana lives as a manifest in
|
|
5
|
+
* `service-manifests/<service-id>.yaml`. CI validates this shape; the
|
|
6
|
+
* provisioning script consumes it and emits a Hydra client config.
|
|
7
|
+
*
|
|
8
|
+
* Schema rationale
|
|
9
|
+
* ----------------
|
|
10
|
+
*
|
|
11
|
+
* - `service_id` is the human label; `hydra_client_id` is what
|
|
12
|
+
* actually gets registered. Keeping them separate lets us evolve
|
|
13
|
+
* the user-facing name without touching Hydra.
|
|
14
|
+
* - `allowed_audiences` mirrors Hydra v2.x `audience` on the client
|
|
15
|
+
* and is enforced when a `client_credentials` token is requested.
|
|
16
|
+
* - `access_token_strategy: jwt` is REQUIRED. Opaque tokens would
|
|
17
|
+
* defeat the whole point of the package (local JWKS verification).
|
|
18
|
+
* - `grant_types` is fixed at `["client_credentials"]` for now; we
|
|
19
|
+
* reject any manifest that asks for `authorization_code` etc. so
|
|
20
|
+
* the package's blast radius is exactly the service-to-service
|
|
21
|
+
* case. If we need to broaden later, lift this constraint.
|
|
22
|
+
* - `key_rotation_policy` is captured but not enforced by code; it
|
|
23
|
+
* gives ops a single source of truth for "is this key overdue".
|
|
24
|
+
*/
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
export declare const serviceAuthManifestSchema: z.ZodObject<{
|
|
27
|
+
service_id: z.ZodString;
|
|
28
|
+
hydra_client_id: z.ZodString;
|
|
29
|
+
token_endpoint_auth_method: z.ZodLiteral<"private_key_jwt">;
|
|
30
|
+
jwks_uri: z.ZodString;
|
|
31
|
+
allowed_audiences: z.ZodArray<z.ZodString, "many">;
|
|
32
|
+
allowed_scopes: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
33
|
+
access_token_strategy: z.ZodLiteral<"jwt">;
|
|
34
|
+
grant_types: z.ZodDefault<z.ZodArray<z.ZodLiteral<"client_credentials">, "many">>;
|
|
35
|
+
owner_team: z.ZodString;
|
|
36
|
+
key_rotation_policy: z.ZodDefault<z.ZodString>;
|
|
37
|
+
/** Optional human description; ignored by the provisioner. */
|
|
38
|
+
description: z.ZodOptional<z.ZodString>;
|
|
39
|
+
}, "strict", z.ZodTypeAny, {
|
|
40
|
+
service_id: string;
|
|
41
|
+
hydra_client_id: string;
|
|
42
|
+
token_endpoint_auth_method: "private_key_jwt";
|
|
43
|
+
jwks_uri: string;
|
|
44
|
+
allowed_audiences: string[];
|
|
45
|
+
allowed_scopes: string[];
|
|
46
|
+
access_token_strategy: "jwt";
|
|
47
|
+
grant_types: "client_credentials"[];
|
|
48
|
+
owner_team: string;
|
|
49
|
+
key_rotation_policy: string;
|
|
50
|
+
description?: string | undefined;
|
|
51
|
+
}, {
|
|
52
|
+
service_id: string;
|
|
53
|
+
hydra_client_id: string;
|
|
54
|
+
token_endpoint_auth_method: "private_key_jwt";
|
|
55
|
+
jwks_uri: string;
|
|
56
|
+
allowed_audiences: string[];
|
|
57
|
+
access_token_strategy: "jwt";
|
|
58
|
+
owner_team: string;
|
|
59
|
+
allowed_scopes?: string[] | undefined;
|
|
60
|
+
grant_types?: "client_credentials"[] | undefined;
|
|
61
|
+
key_rotation_policy?: string | undefined;
|
|
62
|
+
description?: string | undefined;
|
|
63
|
+
}>;
|
|
64
|
+
export type ServiceAuthManifest = z.infer<typeof serviceAuthManifestSchema>;
|
|
65
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/registration/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAIxB,eAAO,MAAM,yBAAyB;;;;;;;;;;;IA4BlC,8DAA8D;;;;;;;;;;;;;;;;;;;;;;;;;;EAGvD,CAAC;AAEZ,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* service-auth.yaml manifest schema. INTERNAL Vana package.
|
|
3
|
+
*
|
|
4
|
+
* Every service that authenticates to Vana lives as a manifest in
|
|
5
|
+
* `service-manifests/<service-id>.yaml`. CI validates this shape; the
|
|
6
|
+
* provisioning script consumes it and emits a Hydra client config.
|
|
7
|
+
*
|
|
8
|
+
* Schema rationale
|
|
9
|
+
* ----------------
|
|
10
|
+
*
|
|
11
|
+
* - `service_id` is the human label; `hydra_client_id` is what
|
|
12
|
+
* actually gets registered. Keeping them separate lets us evolve
|
|
13
|
+
* the user-facing name without touching Hydra.
|
|
14
|
+
* - `allowed_audiences` mirrors Hydra v2.x `audience` on the client
|
|
15
|
+
* and is enforced when a `client_credentials` token is requested.
|
|
16
|
+
* - `access_token_strategy: jwt` is REQUIRED. Opaque tokens would
|
|
17
|
+
* defeat the whole point of the package (local JWKS verification).
|
|
18
|
+
* - `grant_types` is fixed at `["client_credentials"]` for now; we
|
|
19
|
+
* reject any manifest that asks for `authorization_code` etc. so
|
|
20
|
+
* the package's blast radius is exactly the service-to-service
|
|
21
|
+
* case. If we need to broaden later, lift this constraint.
|
|
22
|
+
* - `key_rotation_policy` is captured but not enforced by code; it
|
|
23
|
+
* gives ops a single source of truth for "is this key overdue".
|
|
24
|
+
*/
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
const KEY_ROTATION_PATTERN = /^\d+(d|w|m|y)$/;
|
|
27
|
+
export const serviceAuthManifestSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
service_id: z
|
|
30
|
+
.string()
|
|
31
|
+
.min(1)
|
|
32
|
+
.regex(/^[a-z0-9][a-z0-9-]*$/u, {
|
|
33
|
+
message: "service_id must be lowercase kebab-case",
|
|
34
|
+
}),
|
|
35
|
+
hydra_client_id: z
|
|
36
|
+
.string()
|
|
37
|
+
.min(1)
|
|
38
|
+
.regex(/^[a-zA-Z0-9][a-zA-Z0-9-]*$/u),
|
|
39
|
+
token_endpoint_auth_method: z.literal("private_key_jwt"),
|
|
40
|
+
jwks_uri: z.string().url(),
|
|
41
|
+
allowed_audiences: z.array(z.string().min(1)).min(1),
|
|
42
|
+
allowed_scopes: z.array(z.string()).default([]),
|
|
43
|
+
access_token_strategy: z.literal("jwt"),
|
|
44
|
+
grant_types: z
|
|
45
|
+
.array(z.literal("client_credentials"))
|
|
46
|
+
.min(1)
|
|
47
|
+
.default(["client_credentials"]),
|
|
48
|
+
owner_team: z.string().min(1),
|
|
49
|
+
key_rotation_policy: z
|
|
50
|
+
.string()
|
|
51
|
+
.regex(KEY_ROTATION_PATTERN, {
|
|
52
|
+
message: "key_rotation_policy must look like '90d', '12w', '6m', or '1y'",
|
|
53
|
+
})
|
|
54
|
+
.default("90d"),
|
|
55
|
+
/** Optional human description; ignored by the provisioner. */
|
|
56
|
+
description: z.string().optional(),
|
|
57
|
+
})
|
|
58
|
+
.strict();
|
|
59
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/registration/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,oBAAoB,GAAG,gBAAgB,CAAC;AAE9C,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC;KACvC,MAAM,CAAC;IACN,UAAU,EAAE,CAAC;SACV,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,KAAK,CAAC,uBAAuB,EAAE;QAC9B,OAAO,EAAE,yCAAyC;KACnD,CAAC;IACJ,eAAe,EAAE,CAAC;SACf,MAAM,EAAE;SACR,GAAG,CAAC,CAAC,CAAC;SACN,KAAK,CAAC,6BAA6B,CAAC;IACvC,0BAA0B,EAAE,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC;IACxD,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE;IAC1B,iBAAiB,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACpD,cAAc,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IAC/C,qBAAqB,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;IACvC,WAAW,EAAE,CAAC;SACX,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,oBAAoB,CAAC,CAAC;SACtC,GAAG,CAAC,CAAC,CAAC;SACN,OAAO,CAAC,CAAC,oBAAoB,CAAC,CAAC;IAClC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IAC7B,mBAAmB,EAAE,CAAC;SACnB,MAAM,EAAE;SACR,KAAK,CAAC,oBAAoB,EAAE;QAC3B,OAAO,EAAE,gEAAgE;KAC1E,CAAC;SACD,OAAO,CAAC,KAAK,CAAC;IACjB,8DAA8D;IAC9D,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACnC,CAAC;KACD,MAAM,EAAE,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resource-server middleware. INTERNAL Vana package.
|
|
3
|
+
*
|
|
4
|
+
* Validates an inbound `Authorization: Bearer <jwt>` header against
|
|
5
|
+
* Hydra's published JWKS, enforces `iss`/`aud`/`exp`/`nbf`/`alg`, and
|
|
6
|
+
* returns a structured success or failure. Two surfaces:
|
|
7
|
+
*
|
|
8
|
+
* - `validateAccessToken(authHeader, opts?)` -- pure function. Use
|
|
9
|
+
* this when you control the response shape.
|
|
10
|
+
* - `requireJwt({ requiredScopes? })` -- Next.js-style middleware
|
|
11
|
+
* wrapper that returns a `Response` on rejection.
|
|
12
|
+
*
|
|
13
|
+
* Compliance
|
|
14
|
+
* ----------
|
|
15
|
+
*
|
|
16
|
+
* - RFC 9068 access token shape. We do NOT invent Vana-specific
|
|
17
|
+
* claim names; we read `aud`, `iss`, `client_id`, `scope`, `sub`.
|
|
18
|
+
* - Algorithm allowlist defaults to `["RS256", "ES256"]`. Hydra's
|
|
19
|
+
* defaults are `ES256` for access tokens; allowing both lets
|
|
20
|
+
* services bring their own key shape without our code knowing.
|
|
21
|
+
*/
|
|
22
|
+
import { type JSONWebKeySet, type JWTPayload } from "jose";
|
|
23
|
+
export type ServiceAuthAllowedAlgorithm = "RS256" | "RS384" | "RS512" | "ES256" | "ES384" | "ES512";
|
|
24
|
+
export interface ServiceAuthValidatorOptions {
|
|
25
|
+
/** Either jwksUri or jwks must be set. */
|
|
26
|
+
jwksUri?: string;
|
|
27
|
+
/** Inline JWKS (test seam). */
|
|
28
|
+
jwks?: JSONWebKeySet;
|
|
29
|
+
/** Expected `iss` claim, e.g. `"https://oauth-dev.vana.org"`. */
|
|
30
|
+
issuer: string;
|
|
31
|
+
/**
|
|
32
|
+
* Expected resource audience. The token's `aud` (string or string[])
|
|
33
|
+
* must include this value.
|
|
34
|
+
*/
|
|
35
|
+
audience: string;
|
|
36
|
+
/** Defaults to `["RS256", "ES256"]`. */
|
|
37
|
+
allowedAlgorithms?: ServiceAuthAllowedAlgorithm[];
|
|
38
|
+
/** Clock-skew tolerance in seconds; default 30. */
|
|
39
|
+
clockToleranceSec?: number;
|
|
40
|
+
}
|
|
41
|
+
export type AccessTokenValidationResult = {
|
|
42
|
+
valid: true;
|
|
43
|
+
claims: JWTPayload & {
|
|
44
|
+
client_id?: string;
|
|
45
|
+
sub?: string;
|
|
46
|
+
scope?: string;
|
|
47
|
+
};
|
|
48
|
+
clientId: string;
|
|
49
|
+
} | {
|
|
50
|
+
valid: false;
|
|
51
|
+
reason: "not_bearer" | "not_a_jwt" | "expired" | "wrong_issuer" | "wrong_audience" | "wrong_algorithm" | "bad_signature" | "missing_claims" | "insufficient_scope" | "verify_error";
|
|
52
|
+
detail?: string;
|
|
53
|
+
};
|
|
54
|
+
export interface ValidateAccessTokenInput {
|
|
55
|
+
/** Required scopes; ALL must be present in the token's `scope`. */
|
|
56
|
+
requiredScopes?: string[];
|
|
57
|
+
}
|
|
58
|
+
export interface ServiceAuthValidator {
|
|
59
|
+
validateAccessToken(authorizationHeader: string | null | undefined, input?: ValidateAccessTokenInput): Promise<AccessTokenValidationResult>;
|
|
60
|
+
requireJwt(input?: ValidateAccessTokenInput): (request: Request) => Promise<{
|
|
61
|
+
ok: true;
|
|
62
|
+
result: Extract<AccessTokenValidationResult, {
|
|
63
|
+
valid: true;
|
|
64
|
+
}>;
|
|
65
|
+
} | {
|
|
66
|
+
ok: false;
|
|
67
|
+
response: Response;
|
|
68
|
+
}>;
|
|
69
|
+
}
|
|
70
|
+
/** Test-only: drop cached resolvers between tests. */
|
|
71
|
+
export declare function _resetJwksCacheForTests(): void;
|
|
72
|
+
export declare function createServiceAuthValidator(options: ServiceAuthValidatorOptions): ServiceAuthValidator;
|
|
73
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resource-server/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAIL,KAAK,aAAa,EAClB,KAAK,UAAU,EAEhB,MAAM,MAAM,CAAC;AAId,MAAM,MAAM,2BAA2B,GACnC,OAAO,GACP,OAAO,GACP,OAAO,GACP,OAAO,GACP,OAAO,GACP,OAAO,CAAC;AAEZ,MAAM,WAAW,2BAA2B;IAC1C,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+BAA+B;IAC/B,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB,iEAAiE;IACjE,MAAM,EAAE,MAAM,CAAC;IACf;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,iBAAiB,CAAC,EAAE,2BAA2B,EAAE,CAAC;IAClD,mDAAmD;IACnD,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,MAAM,2BAA2B,GACnC;IACE,KAAK,EAAE,IAAI,CAAC;IACZ,MAAM,EAAE,UAAU,GAAG;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IACF,QAAQ,EAAE,MAAM,CAAC;CAClB,GACD;IACE,KAAK,EAAE,KAAK,CAAC;IACb,MAAM,EACF,YAAY,GACZ,WAAW,GACX,SAAS,GACT,cAAc,GACd,gBAAgB,GAChB,iBAAiB,GACjB,eAAe,GACf,gBAAgB,GAChB,oBAAoB,GACpB,cAAc,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEN,MAAM,WAAW,wBAAwB;IACvC,mEAAmE;IACnE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,mBAAmB,CACjB,mBAAmB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAC9C,KAAK,CAAC,EAAE,wBAAwB,GAC/B,OAAO,CAAC,2BAA2B,CAAC,CAAC;IACxC,UAAU,CACR,KAAK,CAAC,EAAE,wBAAwB,GAC/B,CACD,OAAO,EAAE,OAAO,KACb,OAAO,CACV;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,MAAM,EAAE,OAAO,CAAC,2BAA2B,EAAE;YAAE,KAAK,EAAE,IAAI,CAAA;SAAE,CAAC,CAAA;KAAE,GACzE;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,QAAQ,EAAE,QAAQ,CAAA;KAAE,CACpC,CAAC;CACH;AAmBD,sDAAsD;AACtD,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C;AAqCD,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,2BAA2B,GACnC,oBAAoB,CA+EtB"}
|