@degenerate/endpoints 0.2.0 → 0.4.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 +18 -0
- package/dist/examples/express-server/app.d.ts.map +1 -1
- package/dist/examples/express-server/app.js +8 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/openapi/build-openapi-document.d.ts +27 -0
- package/dist/openapi/build-openapi-document.d.ts.map +1 -0
- package/dist/openapi/build-openapi-document.js +223 -0
- package/dist/openapi/domain-schemas.d.ts +16 -0
- package/dist/openapi/domain-schemas.d.ts.map +1 -0
- package/dist/openapi/domain-schemas.js +39 -0
- package/dist/openapi/index.d.ts +6 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +21 -0
- package/dist/openapi/operations.d.ts +8 -0
- package/dist/openapi/operations.d.ts.map +1 -0
- package/dist/openapi/operations.js +22 -0
- package/dist/openapi/response-schemas.d.ts +32 -0
- package/dist/openapi/response-schemas.d.ts.map +1 -0
- package/dist/openapi/response-schemas.js +30 -0
- package/dist/openapi/types.d.ts +29 -0
- package/dist/openapi/types.d.ts.map +1 -0
- package/dist/openapi/types.js +2 -0
- package/dist/routes/authorities/v1.openapi.d.ts +4 -0
- package/dist/routes/authorities/v1.openapi.d.ts.map +1 -0
- package/dist/routes/authorities/v1.openapi.js +68 -0
- package/dist/routes/events/v1.openapi.d.ts +4 -0
- package/dist/routes/events/v1.openapi.d.ts.map +1 -0
- package/dist/routes/events/v1.openapi.js +54 -0
- package/dist/routes/events.routes.d.ts.map +1 -1
- package/dist/routes/events.routes.js +10 -60
- package/dist/routes/functions/v1.openapi.d.ts +4 -0
- package/dist/routes/functions/v1.openapi.d.ts.map +1 -0
- package/dist/routes/functions/v1.openapi.js +68 -0
- package/dist/routes/functions.routes.d.ts.map +1 -1
- package/dist/routes/functions.routes.js +10 -60
- package/dist/routes/rules/v1/rule-set.openapi.d.ts +4 -0
- package/dist/routes/rules/v1/rule-set.openapi.d.ts.map +1 -0
- package/dist/routes/rules/v1/rule-set.openapi.js +72 -0
- package/dist/routes/sharedata/v1.openapi.d.ts +8 -0
- package/dist/routes/sharedata/v1.openapi.d.ts.map +1 -0
- package/dist/routes/sharedata/v1.openapi.js +60 -0
- package/dist/routes/sharedata/v1.routes.d.ts.map +1 -1
- package/dist/routes/sharedata/v1.routes.js +13 -0
- package/dist/routes/validation/rules/validate-rules.openapi.d.ts +8 -0
- package/dist/routes/validation/rules/validate-rules.openapi.d.ts.map +1 -0
- package/dist/routes/validation/rules/validate-rules.openapi.js +76 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -29,3 +29,21 @@ npm install @degenerate/endpoints
|
|
|
29
29
|
ts-node ./packages/endpoints/src/examples/express-server/server.ts
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
+
## OpenAPI
|
|
33
|
+
|
|
34
|
+
The example server exposes a machine-readable description of its HTTP surface at:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
GET /openapi.json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
This route is public (no JWT) so codegen, Swagger UI, and contract-testing tools can fetch it freely. The document is an OpenAPI 3.0 spec generated by `buildOpenApiDocument()` from the same Zod schemas the routes validate against — request and response shapes never drift from what the handlers actually accept. To build the document outside the server (e.g. to write it to a file):
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import { buildOpenApiDocument } from '@degenerate/endpoints';
|
|
44
|
+
|
|
45
|
+
const spec = buildOpenApiDocument();
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The operation surface is described by the `OperationDescriptor` arrays under `src/routes/**/**.openapi.ts`; a coverage test asserts the descriptors and the mounted Express routes stay in lockstep.
|
|
49
|
+
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../src/examples/express-server/app.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,EAAE,EAAE,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../../src/examples/express-server/app.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,EAAE,EAAE,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAYzD,MAAM,WAAW,eAAe;IAC5B,GAAG,EAAE,OAAO,CAAC;IACb,qBAAqB,EAAE,qBAAqB,CAAC;IAC7C,gBAAgB,EAAE,gBAAgB,CAAC;CACtC;AAED;;;;;;;;GAQG;AACH,wBAAgB,SAAS,IAAI,eAAe,CA6B3C;;AAKD,wBAAmC"}
|
|
@@ -15,6 +15,7 @@ const rules_routes_1 = require("../../routes/rules.routes");
|
|
|
15
15
|
const share_data_routes_1 = require("../../routes/share-data.routes");
|
|
16
16
|
const validation_routes_1 = require("../../routes/validation.routes");
|
|
17
17
|
const cors_1 = require("../../cors");
|
|
18
|
+
const openapi_1 = require("../../openapi");
|
|
18
19
|
/**
|
|
19
20
|
* Builds a fresh Express app with all SAFE routes wired in. Returns the app
|
|
20
21
|
* along with the shared `ShareDataManager` instance — handed back so callers
|
|
@@ -31,6 +32,13 @@ function createApp() {
|
|
|
31
32
|
const shareDataManager = new sharedata_1.ShareDataManager();
|
|
32
33
|
app.set('authorizedAuthorities', authorizedAuthorities);
|
|
33
34
|
app.set('shareDataManager', shareDataManager);
|
|
35
|
+
// Public, unauthenticated machine-readable API description. Generated once
|
|
36
|
+
// from the same Zod schemas the routes validate against; no auth so tooling
|
|
37
|
+
// (codegen, Swagger UI, contract tests) can fetch it freely.
|
|
38
|
+
const openApiDocument = (0, openapi_1.buildOpenApiDocument)();
|
|
39
|
+
app.get('/openapi.json', (_req, res) => {
|
|
40
|
+
res.status(200).json(openApiDocument);
|
|
41
|
+
});
|
|
34
42
|
app.use('/authorities', authorities_routes_1.authorities);
|
|
35
43
|
app.use('/events', events_routes_1.events);
|
|
36
44
|
app.use('/functions', functions_routes_1.functions);
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,QAAQ,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,QAAQ,CAAC;AACvB,cAAc,WAAW,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -16,3 +16,4 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
// This is the main file for the package
|
|
18
18
|
__exportStar(require("./cors"), exports);
|
|
19
|
+
__exportStar(require("./openapi"), exports);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { OperationDescriptor } from './types';
|
|
2
|
+
export interface OpenApiInfo {
|
|
3
|
+
title: string;
|
|
4
|
+
version: string;
|
|
5
|
+
description?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Minimal structural shape of the emitted document. Component and path values
|
|
9
|
+
* are JSON Schema / OpenAPI fragments produced by Zod, so they are typed
|
|
10
|
+
* loosely rather than re-modeling the whole OpenAPI 3.0 type tree.
|
|
11
|
+
*/
|
|
12
|
+
export interface OpenApiDocument {
|
|
13
|
+
openapi: string;
|
|
14
|
+
info: OpenApiInfo;
|
|
15
|
+
components: {
|
|
16
|
+
securitySchemes: Record<string, unknown>;
|
|
17
|
+
schemas: Record<string, unknown>;
|
|
18
|
+
};
|
|
19
|
+
paths: Record<string, Record<string, unknown>>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Build an OpenAPI 3.0 document from the operation descriptors. Pure function:
|
|
23
|
+
* same input, same output, no side effects — so it can be computed once at
|
|
24
|
+
* server start and served as static JSON.
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildOpenApiDocument(operations?: OperationDescriptor[], info?: OpenApiInfo): OpenApiDocument;
|
|
27
|
+
//# sourceMappingURL=build-openapi-document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-openapi-document.d.ts","sourceRoot":"","sources":["../../src/openapi/build-openapi-document.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAOnD,MAAM,WAAW,WAAW;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACxB;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE;QACR,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACzC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpC,CAAC;IACF,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAClD;AAqOD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAChC,UAAU,GAAE,mBAAmB,EAAkB,EACjD,IAAI,GAAE,WAA0B,GACjC,eAAe,CAuBjB"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildOpenApiDocument = buildOpenApiDocument;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const operations_1 = require("./operations");
|
|
6
|
+
const domain_schemas_1 = require("./domain-schemas");
|
|
7
|
+
/** Stable component names for the raw `@degenerate/generic` domain schemas. */
|
|
8
|
+
const domainNames = new Map(domain_schemas_1.domainComponentNames);
|
|
9
|
+
const DEFAULT_INFO = {
|
|
10
|
+
title: 'SAFE Endpoints API',
|
|
11
|
+
version: '1.0.0',
|
|
12
|
+
description: 'HTTP surface for the SAFE platform. Request and response schemas are ' +
|
|
13
|
+
'generated from the same Zod schemas the routes validate against.',
|
|
14
|
+
};
|
|
15
|
+
const STATUS_DESCRIPTIONS = {
|
|
16
|
+
200: 'Success',
|
|
17
|
+
400: 'Validation error',
|
|
18
|
+
403: 'Unauthorized',
|
|
19
|
+
404: 'Not found',
|
|
20
|
+
500: 'Internal server error',
|
|
21
|
+
501: 'Not implemented',
|
|
22
|
+
};
|
|
23
|
+
const statusDescription = (code) => STATUS_DESCRIPTIONS[code] ?? `Status ${code}`;
|
|
24
|
+
/** Names of `{param}` placeholders in an OpenAPI path template, in order. */
|
|
25
|
+
const pathParamNames = (path) => {
|
|
26
|
+
const names = [];
|
|
27
|
+
const matcher = /\{([^}]+)\}/g;
|
|
28
|
+
let match;
|
|
29
|
+
while ((match = matcher.exec(path)) !== null) {
|
|
30
|
+
names.push(match[1]);
|
|
31
|
+
}
|
|
32
|
+
return names;
|
|
33
|
+
};
|
|
34
|
+
const componentRef = (name) => ({
|
|
35
|
+
$ref: `#/components/schemas/${name}`,
|
|
36
|
+
});
|
|
37
|
+
/**
|
|
38
|
+
* Prefix of the `$ref`s Zod emits into its internal `__shared` defs bucket when
|
|
39
|
+
* a non-registered sub-schema is reused across two or more registered roots.
|
|
40
|
+
*/
|
|
41
|
+
const SHARED_REF_PREFIX = '#/components/schemas/__shared#/definitions/';
|
|
42
|
+
/**
|
|
43
|
+
* Inline any `__shared` `$ref` with the actual sub-schema it points at, leaving
|
|
44
|
+
* every real `#/components/schemas/<Name>` `$ref` untouched. Returns a fresh
|
|
45
|
+
* deep copy — never mutates the input.
|
|
46
|
+
*
|
|
47
|
+
* Why this is needed: `IShareRuleAppliedSchema` is `IShareRuleSchema.extend(...)`,
|
|
48
|
+
* so both share the very same `z.array(IConditionSchema)` instance for
|
|
49
|
+
* `currentDataConditions` / `proposedDataConditions`. When several rule schemas
|
|
50
|
+
* are registered together, Zod hoists that shared array into a `__shared`
|
|
51
|
+
* defs entry and points to it with a `__shared#/definitions/schemaN` `$ref`,
|
|
52
|
+
* which is not a resolvable OpenAPI component. Inlining the (small, acyclic)
|
|
53
|
+
* shared sub-schema yields a clean spec; the genuinely recursive schemas
|
|
54
|
+
* (`ShareRule` ↔ `Condition`) stay as proper component `$ref`s because they are
|
|
55
|
+
* registered.
|
|
56
|
+
*/
|
|
57
|
+
const inlineSharedRefs = (value, defs, active) => {
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
return value.map((item) => inlineSharedRefs(item, defs, active));
|
|
60
|
+
}
|
|
61
|
+
if (value && typeof value === 'object') {
|
|
62
|
+
const obj = value;
|
|
63
|
+
const ref = obj.$ref;
|
|
64
|
+
if (typeof ref === 'string' && ref.startsWith(SHARED_REF_PREFIX)) {
|
|
65
|
+
const defName = ref.slice(SHARED_REF_PREFIX.length);
|
|
66
|
+
if (active.has(defName)) {
|
|
67
|
+
throw new Error(`Cyclic __shared definition while inlining: ${defName}`);
|
|
68
|
+
}
|
|
69
|
+
if (!(defName in defs)) {
|
|
70
|
+
throw new Error(`Unresolved __shared definition: ${defName}`);
|
|
71
|
+
}
|
|
72
|
+
active.add(defName);
|
|
73
|
+
const resolved = inlineSharedRefs(defs[defName], defs, active);
|
|
74
|
+
active.delete(defName);
|
|
75
|
+
return resolved;
|
|
76
|
+
}
|
|
77
|
+
const out = {};
|
|
78
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
79
|
+
out[key] = inlineSharedRefs(val, defs, active);
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
return value;
|
|
84
|
+
};
|
|
85
|
+
/** Collect distinct schema instances (request + every response) in encounter order. */
|
|
86
|
+
const collectSchemas = (operations) => {
|
|
87
|
+
const seen = new Set();
|
|
88
|
+
const distinct = [];
|
|
89
|
+
const add = (schema) => {
|
|
90
|
+
if (schema && !seen.has(schema)) {
|
|
91
|
+
seen.add(schema);
|
|
92
|
+
distinct.push(schema);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
for (const op of operations) {
|
|
96
|
+
add(op.request);
|
|
97
|
+
for (const schema of Object.values(op.response)) {
|
|
98
|
+
add(schema);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return distinct;
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Register each distinct schema under a stable component name, convert the
|
|
105
|
+
* whole registry to OpenAPI-3.0 JSON Schema in one pass, and return both the
|
|
106
|
+
* `components.schemas` map and the schema→name lookup used to build `$ref`s.
|
|
107
|
+
*
|
|
108
|
+
* One pass with `io: 'output'` is correct for both request and response
|
|
109
|
+
* positions here because no SAFE schema uses a transform / default / coercion —
|
|
110
|
+
* so a schema's input and output JSON Schema are identical. Conversion via the
|
|
111
|
+
* registry overload makes a schema reused across operations (or nested inside a
|
|
112
|
+
* response envelope) emit as a single `$ref`'d component automatically.
|
|
113
|
+
*/
|
|
114
|
+
const buildComponents = (distinct) => {
|
|
115
|
+
const registry = zod_1.z.registry();
|
|
116
|
+
const nameBySchema = new Map();
|
|
117
|
+
const usedNames = new Set();
|
|
118
|
+
let autoCounter = 0;
|
|
119
|
+
for (const schema of distinct) {
|
|
120
|
+
const metaId = schema.meta()?.id ?? domainNames.get(schema);
|
|
121
|
+
let name = metaId ?? `Component${++autoCounter}`;
|
|
122
|
+
// Defensive: distinct schemas must map to distinct component names.
|
|
123
|
+
while (usedNames.has(name)) {
|
|
124
|
+
name = `${name}_${++autoCounter}`;
|
|
125
|
+
}
|
|
126
|
+
usedNames.add(name);
|
|
127
|
+
nameBySchema.set(schema, name);
|
|
128
|
+
registry.add(schema, { id: name });
|
|
129
|
+
}
|
|
130
|
+
const { schemas } = zod_1.z.toJSONSchema(registry, {
|
|
131
|
+
target: 'openapi-3.0',
|
|
132
|
+
io: 'output',
|
|
133
|
+
unrepresentable: 'any',
|
|
134
|
+
uri: (id) => `#/components/schemas/${id}`,
|
|
135
|
+
});
|
|
136
|
+
// Zod parks sub-schemas reused across registered roots in a `__shared`
|
|
137
|
+
// bucket; inline those back into the components and drop the bucket so every
|
|
138
|
+
// remaining `$ref` resolves to a real component. See `inlineSharedRefs`.
|
|
139
|
+
const sharedDefs = schemas.__shared?.definitions ?? {};
|
|
140
|
+
// Strip the non-OpenAPI `$id` Zod stamps on each component; `$ref`s between
|
|
141
|
+
// components are unaffected.
|
|
142
|
+
const cleaned = {};
|
|
143
|
+
for (const [name, schema] of Object.entries(schemas)) {
|
|
144
|
+
if (name === '__shared') {
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
const inlined = inlineSharedRefs(schema, sharedDefs, new Set());
|
|
148
|
+
const { $id, ...rest } = inlined;
|
|
149
|
+
void $id;
|
|
150
|
+
cleaned[name] = rest;
|
|
151
|
+
}
|
|
152
|
+
return { schemas: cleaned, nameBySchema };
|
|
153
|
+
};
|
|
154
|
+
const buildPaths = (operations, nameBySchema) => {
|
|
155
|
+
const paths = {};
|
|
156
|
+
for (const op of operations) {
|
|
157
|
+
const pathItem = paths[op.path] ?? (paths[op.path] = {});
|
|
158
|
+
const responses = {};
|
|
159
|
+
for (const [status, schema] of Object.entries(op.response)) {
|
|
160
|
+
responses[status] = {
|
|
161
|
+
description: statusDescription(Number(status)),
|
|
162
|
+
content: { 'application/json': { schema: componentRef(nameBySchema.get(schema)) } },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const operationObject = {
|
|
166
|
+
summary: op.summary,
|
|
167
|
+
description: op.security === 'public'
|
|
168
|
+
? 'Public — no authentication required.'
|
|
169
|
+
: `Requires a valid JWT for the ${op.security} audience.`,
|
|
170
|
+
security: op.security === 'public' ? [] : [{ bearerAuth: [] }],
|
|
171
|
+
responses,
|
|
172
|
+
};
|
|
173
|
+
if (op.deprecated) {
|
|
174
|
+
operationObject.deprecated = true;
|
|
175
|
+
}
|
|
176
|
+
const params = pathParamNames(op.path);
|
|
177
|
+
if (params.length > 0) {
|
|
178
|
+
operationObject.parameters = params.map((name) => ({
|
|
179
|
+
name,
|
|
180
|
+
in: 'path',
|
|
181
|
+
required: true,
|
|
182
|
+
schema: { type: 'string' },
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
if (op.request) {
|
|
186
|
+
operationObject.requestBody = {
|
|
187
|
+
required: true,
|
|
188
|
+
content: {
|
|
189
|
+
'application/json': { schema: componentRef(nameBySchema.get(op.request)) },
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
pathItem[op.method] = operationObject;
|
|
194
|
+
}
|
|
195
|
+
return paths;
|
|
196
|
+
};
|
|
197
|
+
/**
|
|
198
|
+
* Build an OpenAPI 3.0 document from the operation descriptors. Pure function:
|
|
199
|
+
* same input, same output, no side effects — so it can be computed once at
|
|
200
|
+
* server start and served as static JSON.
|
|
201
|
+
*/
|
|
202
|
+
function buildOpenApiDocument(operations = operations_1.operations, info = DEFAULT_INFO) {
|
|
203
|
+
const { schemas, nameBySchema } = buildComponents(collectSchemas(operations));
|
|
204
|
+
const paths = buildPaths(operations, nameBySchema);
|
|
205
|
+
return {
|
|
206
|
+
openapi: '3.0.3',
|
|
207
|
+
info,
|
|
208
|
+
components: {
|
|
209
|
+
securitySchemes: {
|
|
210
|
+
bearerAuth: {
|
|
211
|
+
type: 'http',
|
|
212
|
+
scheme: 'bearer',
|
|
213
|
+
bearerFormat: 'JWT',
|
|
214
|
+
description: 'JWT bearer token. The token audience (aud) selects which route group it ' +
|
|
215
|
+
'authorizes: AUTHORITIES, EVENTS, FUNCTIONS, RULES, or SHAREDATA. Operations ' +
|
|
216
|
+
'on /share-data additionally enforce a per-resource (BOLA) check.',
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
schemas,
|
|
220
|
+
},
|
|
221
|
+
paths,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Stable OpenAPI component names for the centralized request/domain schemas in
|
|
4
|
+
* `@degenerate/generic`, keyed by the **raw** schema instance.
|
|
5
|
+
*
|
|
6
|
+
* Keyed by the raw instance (not a `.meta()` clone) on purpose: several rule
|
|
7
|
+
* schemas are recursive (`IShareRuleSchema` ↔ `IConditionSchema`, and
|
|
8
|
+
* `IShareRuleGroupSchema` via `z.lazy`), and those `z.lazy` thunks resolve to
|
|
9
|
+
* the raw generic instances. The generator must register the very instances
|
|
10
|
+
* the schema graph references, otherwise the recursive sub-schemas have no
|
|
11
|
+
* component id and Zod hoists them into a malformed shared-defs bucket. Every
|
|
12
|
+
* recursive schema below is also mounted as a `/validation` request body, so
|
|
13
|
+
* registering these names makes the whole graph resolve to clean `$ref`s.
|
|
14
|
+
*/
|
|
15
|
+
export declare const domainComponentNames: ReadonlyArray<readonly [z.ZodType, string]>;
|
|
16
|
+
//# sourceMappingURL=domain-schemas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"domain-schemas.d.ts","sourceRoot":"","sources":["../../src/openapi/domain-schemas.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAwB7B;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,EAAE,aAAa,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,CAqB5E,CAAC"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.domainComponentNames = void 0;
|
|
4
|
+
const generic_1 = require("@degenerate/generic");
|
|
5
|
+
/**
|
|
6
|
+
* Stable OpenAPI component names for the centralized request/domain schemas in
|
|
7
|
+
* `@degenerate/generic`, keyed by the **raw** schema instance.
|
|
8
|
+
*
|
|
9
|
+
* Keyed by the raw instance (not a `.meta()` clone) on purpose: several rule
|
|
10
|
+
* schemas are recursive (`IShareRuleSchema` ↔ `IConditionSchema`, and
|
|
11
|
+
* `IShareRuleGroupSchema` via `z.lazy`), and those `z.lazy` thunks resolve to
|
|
12
|
+
* the raw generic instances. The generator must register the very instances
|
|
13
|
+
* the schema graph references, otherwise the recursive sub-schemas have no
|
|
14
|
+
* component id and Zod hoists them into a malformed shared-defs bucket. Every
|
|
15
|
+
* recursive schema below is also mounted as a `/validation` request body, so
|
|
16
|
+
* registering these names makes the whole graph resolve to clean `$ref`s.
|
|
17
|
+
*/
|
|
18
|
+
exports.domainComponentNames = [
|
|
19
|
+
[generic_1.ISAFETransactionAuthorityAddSchema, 'SAFETransactionAuthorityAdd'],
|
|
20
|
+
[generic_1.ISAFETransactionAuthorityUpdateSchema, 'SAFETransactionAuthorityUpdate'],
|
|
21
|
+
[generic_1.ISAFETransactionAuthorityDeleteSchema, 'SAFETransactionAuthorityDelete'],
|
|
22
|
+
[generic_1.ISAFETransactionEventAddSchema, 'SAFETransactionEventAdd'],
|
|
23
|
+
[generic_1.ISAFETransactionEventUpdateSchema, 'SAFETransactionEventUpdate'],
|
|
24
|
+
[generic_1.ISAFETransactionEventDeleteSchema, 'SAFETransactionEventDelete'],
|
|
25
|
+
[generic_1.ISAFETransactionFunctionAddSchema, 'SAFETransactionFunctionAdd'],
|
|
26
|
+
[generic_1.ISAFETransactionFunctionUpdateSchema, 'SAFETransactionFunctionUpdate'],
|
|
27
|
+
[generic_1.ISAFETransactionFunctionDeleteSchema, 'SAFETransactionFunctionDelete'],
|
|
28
|
+
[generic_1.ISAFETransactionRuleSetAddSchema, 'SAFETransactionRuleSetAdd'],
|
|
29
|
+
[generic_1.ISAFETransactionRuleSetUpdateSchema, 'SAFETransactionRuleSetUpdate'],
|
|
30
|
+
[generic_1.ISAFETransactionRuleSetDeleteSchema, 'SAFETransactionRuleSetDelete'],
|
|
31
|
+
[generic_1.ISAFETransactionShareDataAddSchema, 'SAFETransactionShareDataAdd'],
|
|
32
|
+
[generic_1.ISAFETransactionShareDataUpdateSchema, 'SAFETransactionShareDataUpdate'],
|
|
33
|
+
[generic_1.ISAFETransactionShareDataDeleteSchema, 'SAFETransactionShareDataDelete'],
|
|
34
|
+
[generic_1.IShareRuleSetSchema, 'ShareRuleSet'],
|
|
35
|
+
[generic_1.IShareRuleGroupSchema, 'ShareRuleGroup'],
|
|
36
|
+
[generic_1.IShareRuleAppliedSchema, 'ShareRuleApplied'],
|
|
37
|
+
[generic_1.IShareRuleSchema, 'ShareRule'],
|
|
38
|
+
[generic_1.IConditionSchema, 'Condition'],
|
|
39
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/openapi/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,0BAA0B,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./response-schemas"), exports);
|
|
19
|
+
__exportStar(require("./domain-schemas"), exports);
|
|
20
|
+
__exportStar(require("./operations"), exports);
|
|
21
|
+
__exportStar(require("./build-openapi-document"), exports);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OperationDescriptor } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Every mounted SAFE HTTP operation, one descriptor each. This is the single
|
|
4
|
+
* feed for `buildOpenApiDocument` and the subject the coverage test reconciles
|
|
5
|
+
* against the live Express route table.
|
|
6
|
+
*/
|
|
7
|
+
export declare const operations: OperationDescriptor[];
|
|
8
|
+
//# sourceMappingURL=operations.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"operations.d.ts","sourceRoot":"","sources":["../../src/openapi/operations.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAQnD;;;;GAIG;AACH,eAAO,MAAM,UAAU,EAAE,mBAAmB,EAO3C,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.operations = void 0;
|
|
4
|
+
const v1_openapi_1 = require("../routes/authorities/v1.openapi");
|
|
5
|
+
const v1_openapi_2 = require("../routes/events/v1.openapi");
|
|
6
|
+
const v1_openapi_3 = require("../routes/functions/v1.openapi");
|
|
7
|
+
const rule_set_openapi_1 = require("../routes/rules/v1/rule-set.openapi");
|
|
8
|
+
const v1_openapi_4 = require("../routes/sharedata/v1.openapi");
|
|
9
|
+
const validate_rules_openapi_1 = require("../routes/validation/rules/validate-rules.openapi");
|
|
10
|
+
/**
|
|
11
|
+
* Every mounted SAFE HTTP operation, one descriptor each. This is the single
|
|
12
|
+
* feed for `buildOpenApiDocument` and the subject the coverage test reconciles
|
|
13
|
+
* against the live Express route table.
|
|
14
|
+
*/
|
|
15
|
+
exports.operations = [
|
|
16
|
+
...v1_openapi_1.authorityOperations,
|
|
17
|
+
...v1_openapi_2.eventOperations,
|
|
18
|
+
...v1_openapi_3.functionOperations,
|
|
19
|
+
...rule_set_openapi_1.ruleSetOperations,
|
|
20
|
+
...v1_openapi_4.shareDataOperations,
|
|
21
|
+
...validate_rules_openapi_1.validateRulesOperations,
|
|
22
|
+
];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* HTTP response envelopes for the SAFE endpoints surface. These describe the
|
|
4
|
+
* transport shape (`{ successful, … }`) the route handlers actually return —
|
|
5
|
+
* an HTTP concern specific to this API, deliberately kept out of
|
|
6
|
+
* `@degenerate/generic` (domain models). Payloads inside an envelope reuse
|
|
7
|
+
* generic domain schemas where one exists; otherwise the envelope is documented
|
|
8
|
+
* and the payload left loosely typed rather than fabricated.
|
|
9
|
+
*
|
|
10
|
+
* Define each schema once and reuse the instance so the OpenAPI generator
|
|
11
|
+
* dedupes it to a single `components.schemas` entry.
|
|
12
|
+
*/
|
|
13
|
+
/** Uniform error body emitted by the three error handlers at 400 / 403 / 404 / 500 / 501. */
|
|
14
|
+
export declare const ErrorResponseSchema: z.ZodObject<{
|
|
15
|
+
successful: z.ZodLiteral<false>;
|
|
16
|
+
error: z.ZodString;
|
|
17
|
+
}, z.core.$strip>;
|
|
18
|
+
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
|
|
19
|
+
/** Bare success body: `{ successful: true }` with no additional payload. */
|
|
20
|
+
export declare const SuccessResponseSchema: z.ZodObject<{
|
|
21
|
+
successful: z.ZodLiteral<true>;
|
|
22
|
+
}, z.core.$strip>;
|
|
23
|
+
export type SuccessResponse = z.infer<typeof SuccessResponseSchema>;
|
|
24
|
+
/**
|
|
25
|
+
* Build a success envelope that carries extra fields alongside
|
|
26
|
+
* `successful: true`. Define the result as a module-level const in the group's
|
|
27
|
+
* descriptor file so the generator sees one stable instance per shape.
|
|
28
|
+
*/
|
|
29
|
+
export declare const successEnvelope: <Shape extends z.ZodRawShape>(payload: Shape) => z.ZodObject<{
|
|
30
|
+
successful: z.ZodLiteral<true>;
|
|
31
|
+
} & Shape extends infer T ? { -readonly [P in keyof T]: T[P]; } : never, z.core.$strip>;
|
|
32
|
+
//# sourceMappingURL=response-schemas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"response-schemas.d.ts","sourceRoot":"","sources":["../../src/openapi/response-schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;GAUG;AAEH,6FAA6F;AAC7F,eAAO,MAAM,mBAAmB;;;iBAEE,CAAC;AACnC,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,4EAA4E;AAC5E,eAAO,MAAM,qBAAqB;;iBAEE,CAAC;AACrC,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,SAAS,CAAC,CAAC,WAAW,EAAE,SAAS,KAAK;;uFAClB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.successEnvelope = exports.SuccessResponseSchema = exports.ErrorResponseSchema = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
/**
|
|
6
|
+
* HTTP response envelopes for the SAFE endpoints surface. These describe the
|
|
7
|
+
* transport shape (`{ successful, … }`) the route handlers actually return —
|
|
8
|
+
* an HTTP concern specific to this API, deliberately kept out of
|
|
9
|
+
* `@degenerate/generic` (domain models). Payloads inside an envelope reuse
|
|
10
|
+
* generic domain schemas where one exists; otherwise the envelope is documented
|
|
11
|
+
* and the payload left loosely typed rather than fabricated.
|
|
12
|
+
*
|
|
13
|
+
* Define each schema once and reuse the instance so the OpenAPI generator
|
|
14
|
+
* dedupes it to a single `components.schemas` entry.
|
|
15
|
+
*/
|
|
16
|
+
/** Uniform error body emitted by the three error handlers at 400 / 403 / 404 / 500 / 501. */
|
|
17
|
+
exports.ErrorResponseSchema = zod_1.z
|
|
18
|
+
.object({ successful: zod_1.z.literal(false), error: zod_1.z.string() })
|
|
19
|
+
.meta({ id: 'ErrorResponse' });
|
|
20
|
+
/** Bare success body: `{ successful: true }` with no additional payload. */
|
|
21
|
+
exports.SuccessResponseSchema = zod_1.z
|
|
22
|
+
.object({ successful: zod_1.z.literal(true) })
|
|
23
|
+
.meta({ id: 'SuccessResponse' });
|
|
24
|
+
/**
|
|
25
|
+
* Build a success envelope that carries extra fields alongside
|
|
26
|
+
* `successful: true`. Define the result as a module-level const in the group's
|
|
27
|
+
* descriptor file so the generator sees one stable instance per shape.
|
|
28
|
+
*/
|
|
29
|
+
const successEnvelope = (payload) => zod_1.z.object({ successful: zod_1.z.literal(true), ...payload });
|
|
30
|
+
exports.successEnvelope = successEnvelope;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { z } from 'zod';
|
|
2
|
+
import type { AuthorizationAudience } from '@degenerate/generic';
|
|
3
|
+
/**
|
|
4
|
+
* One descriptor per mounted HTTP operation. This is the single place that
|
|
5
|
+
* records everything OpenAPI needs about an operation that Express does not
|
|
6
|
+
* expose introspectively: human summary, the JWT audience that gates it, and
|
|
7
|
+
* the Zod schemas bound to its request body and each response status.
|
|
8
|
+
*
|
|
9
|
+
* Descriptors live beside the route files they describe (one array per group,
|
|
10
|
+
* e.g. `routes/sharedata/v1.openapi.ts`) and are aggregated into a single
|
|
11
|
+
* `operations[]`. A coverage test reconciles `operations[]` against the live
|
|
12
|
+
* Express route table so the two cannot silently drift.
|
|
13
|
+
*/
|
|
14
|
+
export interface OperationDescriptor {
|
|
15
|
+
method: 'get' | 'post' | 'patch' | 'delete';
|
|
16
|
+
/** OpenAPI path template, e.g. `/share-data/v1/{sharedataid}`. */
|
|
17
|
+
path: string;
|
|
18
|
+
/** JWT audience that gates the operation, or `'public'` for no auth. */
|
|
19
|
+
security: AuthorizationAudience | 'public';
|
|
20
|
+
summary: string;
|
|
21
|
+
/** Request body schema; omit for operations that take no body. */
|
|
22
|
+
request?: z.ZodType;
|
|
23
|
+
/** Response schema per HTTP status — at minimum the success status. */
|
|
24
|
+
response: {
|
|
25
|
+
[status: number]: z.ZodType;
|
|
26
|
+
};
|
|
27
|
+
deprecated?: boolean;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/openapi/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAC;AAEjE;;;;;;;;;;GAUG;AACH,MAAM,WAAW,mBAAmB;IAChC,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IAC5C,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,QAAQ,EAAE,qBAAqB,GAAG,QAAQ,CAAC;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,kEAAkE;IAClE,OAAO,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC;IACpB,uEAAuE;IACvE,QAAQ,EAAE;QAAE,CAAC,MAAM,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,CAAA;KAAE,CAAC;IAC1C,UAAU,CAAC,EAAE,OAAO,CAAC;CACxB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v1.openapi.d.ts","sourceRoot":"","sources":["../../../src/routes/authorities/v1.openapi.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAW/D,mFAAmF;AACnF,eAAO,MAAM,mBAAmB,EAAE,mBAAmB,EAyDpD,CAAC"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.authorityOperations = void 0;
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const generic_1 = require("@degenerate/generic");
|
|
6
|
+
const response_schemas_1 = require("../../openapi/response-schemas");
|
|
7
|
+
const AuthorityIdResponseSchema = (0, response_schemas_1.successEnvelope)({ authorityId: zod_1.z.string() }).meta({
|
|
8
|
+
id: 'AuthorityIdResponse',
|
|
9
|
+
});
|
|
10
|
+
/** Operations under `/authorities` — all gated by the AUTHORITIES JWT audience. */
|
|
11
|
+
exports.authorityOperations = [
|
|
12
|
+
{
|
|
13
|
+
method: 'post',
|
|
14
|
+
path: '/authorities/v1',
|
|
15
|
+
security: generic_1.AuthorizationAudience.AUTHORITIES,
|
|
16
|
+
summary: 'Create an authority.',
|
|
17
|
+
request: generic_1.ISAFETransactionAuthorityAddSchema,
|
|
18
|
+
response: {
|
|
19
|
+
200: response_schemas_1.SuccessResponseSchema,
|
|
20
|
+
400: response_schemas_1.ErrorResponseSchema,
|
|
21
|
+
403: response_schemas_1.ErrorResponseSchema,
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
method: 'get',
|
|
26
|
+
path: '/authorities/v1',
|
|
27
|
+
security: generic_1.AuthorizationAudience.AUTHORITIES,
|
|
28
|
+
summary: 'List authorities.',
|
|
29
|
+
response: {
|
|
30
|
+
200: response_schemas_1.SuccessResponseSchema,
|
|
31
|
+
403: response_schemas_1.ErrorResponseSchema,
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
method: 'get',
|
|
36
|
+
path: '/authorities/v1/{authorityid}',
|
|
37
|
+
security: generic_1.AuthorizationAudience.AUTHORITIES,
|
|
38
|
+
summary: 'Get an authority by id.',
|
|
39
|
+
response: {
|
|
40
|
+
200: AuthorityIdResponseSchema,
|
|
41
|
+
403: response_schemas_1.ErrorResponseSchema,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
method: 'patch',
|
|
46
|
+
path: '/authorities/v1/{authorityid}',
|
|
47
|
+
security: generic_1.AuthorizationAudience.AUTHORITIES,
|
|
48
|
+
summary: 'Update an authority by id.',
|
|
49
|
+
request: generic_1.ISAFETransactionAuthorityUpdateSchema,
|
|
50
|
+
response: {
|
|
51
|
+
200: AuthorityIdResponseSchema,
|
|
52
|
+
400: response_schemas_1.ErrorResponseSchema,
|
|
53
|
+
403: response_schemas_1.ErrorResponseSchema,
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
method: 'delete',
|
|
58
|
+
path: '/authorities/v1/{authorityid}',
|
|
59
|
+
security: generic_1.AuthorizationAudience.AUTHORITIES,
|
|
60
|
+
summary: 'Delete an authority by id.',
|
|
61
|
+
request: generic_1.ISAFETransactionAuthorityDeleteSchema,
|
|
62
|
+
response: {
|
|
63
|
+
200: AuthorityIdResponseSchema,
|
|
64
|
+
400: response_schemas_1.ErrorResponseSchema,
|
|
65
|
+
403: response_schemas_1.ErrorResponseSchema,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"v1.openapi.d.ts","sourceRoot":"","sources":["../../../src/routes/events/v1.openapi.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAG/D,yEAAyE;AACzE,eAAO,MAAM,eAAe,EAAE,mBAAmB,EA+ChD,CAAC"}
|