@experia-labs/plugin-dev-mcp 0.1.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/LICENSE +21 -0
- package/README.md +82 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +13 -0
- package/dist/cli.js.map +1 -0
- package/dist/data/api-endpoints.d.ts +15 -0
- package/dist/data/api-endpoints.d.ts.map +1 -0
- package/dist/data/api-endpoints.js +80 -0
- package/dist/data/api-endpoints.js.map +1 -0
- package/dist/data/events.d.ts +13 -0
- package/dist/data/events.d.ts.map +1 -0
- package/dist/data/events.js +110 -0
- package/dist/data/events.js.map +1 -0
- package/dist/data/scopes.d.ts +10 -0
- package/dist/data/scopes.d.ts.map +1 -0
- package/dist/data/scopes.js +70 -0
- package/dist/data/scopes.js.map +1 -0
- package/dist/data/slots.d.ts +9 -0
- package/dist/data/slots.d.ts.map +1 -0
- package/dist/data/slots.js +34 -0
- package/dist/data/slots.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/manifest/starter.d.ts +16 -0
- package/dist/manifest/starter.d.ts.map +1 -0
- package/dist/manifest/starter.js +79 -0
- package/dist/manifest/starter.js.map +1 -0
- package/dist/manifest/validate.d.ts +8 -0
- package/dist/manifest/validate.d.ts.map +1 -0
- package/dist/manifest/validate.js +182 -0
- package/dist/manifest/validate.js.map +1 -0
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +243 -0
- package/dist/server.js.map +1 -0
- package/dist/webhook/verify.d.ts +28 -0
- package/dist/webhook/verify.d.ts.map +1 -0
- package/dist/webhook/verify.js +42 -0
- package/dist/webhook/verify.js.map +1 -0
- package/package.json +62 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export function generateStarterManifest(args) {
|
|
2
|
+
const flavor = args.flavor ?? 'attendee-app';
|
|
3
|
+
const base = args.baseUrl.replace(/\/+$/, '');
|
|
4
|
+
const common = {
|
|
5
|
+
key: args.key,
|
|
6
|
+
version: '0.1.0',
|
|
7
|
+
name: args.name,
|
|
8
|
+
tagline: 'Describe your plugin in one sentence',
|
|
9
|
+
developer: {
|
|
10
|
+
name: args.developerName,
|
|
11
|
+
email: args.developerEmail,
|
|
12
|
+
},
|
|
13
|
+
pricing: { model: 'free' },
|
|
14
|
+
lifecycle: {
|
|
15
|
+
onInstall: `${base}/experia/hooks/install`,
|
|
16
|
+
onUninstall: `${base}/experia/hooks/uninstall`,
|
|
17
|
+
},
|
|
18
|
+
timeWindow: 'always',
|
|
19
|
+
};
|
|
20
|
+
if (flavor === 'minimal') {
|
|
21
|
+
return {
|
|
22
|
+
...common,
|
|
23
|
+
scopes: ['event.read'],
|
|
24
|
+
events: [],
|
|
25
|
+
extensions: {},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (flavor === 'organizer-tool') {
|
|
29
|
+
return {
|
|
30
|
+
...common,
|
|
31
|
+
scopes: ['event.read', 'event.attendees.read'],
|
|
32
|
+
events: ['event.started', 'event.ended'],
|
|
33
|
+
extensions: {
|
|
34
|
+
'organizer.event.tabs': [
|
|
35
|
+
{
|
|
36
|
+
id: 'config',
|
|
37
|
+
label: args.name,
|
|
38
|
+
iframe: `${base}/organizer/config?eventId={eventId}`,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: 'insights',
|
|
42
|
+
label: `${args.name} Insights`,
|
|
43
|
+
iframe: `${base}/organizer/insights?eventId={eventId}`,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
// attendee-app — covers networking-style plugins
|
|
50
|
+
return {
|
|
51
|
+
...common,
|
|
52
|
+
timeWindow: 'event-live',
|
|
53
|
+
scopes: [
|
|
54
|
+
'event.read',
|
|
55
|
+
'event.attendees.read',
|
|
56
|
+
'attendee.profile.read',
|
|
57
|
+
'attendee.profile.write',
|
|
58
|
+
'messaging.send',
|
|
59
|
+
],
|
|
60
|
+
events: ['event.started', 'event.ended', 'attendee.checked_in'],
|
|
61
|
+
extensions: {
|
|
62
|
+
'organizer.event.tabs': [
|
|
63
|
+
{
|
|
64
|
+
id: 'config',
|
|
65
|
+
label: args.name,
|
|
66
|
+
iframe: `${base}/organizer/config?eventId={eventId}`,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
'experia.event.tabs': [
|
|
70
|
+
{
|
|
71
|
+
id: 'home',
|
|
72
|
+
label: args.name,
|
|
73
|
+
iframe: `${base}/experia/home?eventId={eventId}&jwt={pluginJwt}`,
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=starter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"starter.js","sourceRoot":"","sources":["../../src/manifest/starter.ts"],"names":[],"mappings":"AAgBA,MAAM,UAAU,uBAAuB,CAAC,IAAiB;IACvD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,cAAc,CAAC;IAC7C,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAE9C,MAAM,MAAM,GAAG;QACb,GAAG,EAAE,IAAI,CAAC,GAAG;QACb,OAAO,EAAE,OAAO;QAChB,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,OAAO,EAAE,sCAAsC;QAC/C,SAAS,EAAE;YACT,IAAI,EAAE,IAAI,CAAC,aAAa;YACxB,KAAK,EAAE,IAAI,CAAC,cAAc;SAC3B;QACD,OAAO,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE;QAC1B,SAAS,EAAE;YACT,SAAS,EAAE,GAAG,IAAI,wBAAwB;YAC1C,WAAW,EAAE,GAAG,IAAI,0BAA0B;SAC/C;QACD,UAAU,EAAE,QAAQ;KACZ,CAAC;IAEX,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,OAAO;YACL,GAAG,MAAM;YACT,MAAM,EAAE,CAAC,YAAY,CAAC;YACtB,MAAM,EAAE,EAAE;YACV,UAAU,EAAE,EAAE;SACf,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,KAAK,gBAAgB,EAAE,CAAC;QAChC,OAAO;YACL,GAAG,MAAM;YACT,MAAM,EAAE,CAAC,YAAY,EAAE,sBAAsB,CAAC;YAC9C,MAAM,EAAE,CAAC,eAAe,EAAE,aAAa,CAAC;YACxC,UAAU,EAAE;gBACV,sBAAsB,EAAE;oBACtB;wBACE,EAAE,EAAE,QAAQ;wBACZ,KAAK,EAAE,IAAI,CAAC,IAAI;wBAChB,MAAM,EAAE,GAAG,IAAI,qCAAqC;qBACrD;oBACD;wBACE,EAAE,EAAE,UAAU;wBACd,KAAK,EAAE,GAAG,IAAI,CAAC,IAAI,WAAW;wBAC9B,MAAM,EAAE,GAAG,IAAI,uCAAuC;qBACvD;iBACF;aACF;SACF,CAAC;IACJ,CAAC;IAED,iDAAiD;IACjD,OAAO;QACL,GAAG,MAAM;QACT,UAAU,EAAE,YAAY;QACxB,MAAM,EAAE;YACN,YAAY;YACZ,sBAAsB;YACtB,uBAAuB;YACvB,wBAAwB;YACxB,gBAAgB;SACjB;QACD,MAAM,EAAE,CAAC,eAAe,EAAE,aAAa,EAAE,qBAAqB,CAAC;QAC/D,UAAU,EAAE;YACV,sBAAsB,EAAE;gBACtB;oBACE,EAAE,EAAE,QAAQ;oBACZ,KAAK,EAAE,IAAI,CAAC,IAAI;oBAChB,MAAM,EAAE,GAAG,IAAI,qCAAqC;iBACrD;aACF;YACD,oBAAoB,EAAE;gBACpB;oBACE,EAAE,EAAE,MAAM;oBACV,KAAK,EAAE,IAAI,CAAC,IAAI;oBAChB,MAAM,EAAE,GAAG,IAAI,iDAAiD;iBACjE;aACF;SACF;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface ManifestValidationResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
issues: string[];
|
|
4
|
+
/** Suggested fixes / next steps (only set when there are issues). */
|
|
5
|
+
hints: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare function validateManifest(raw: unknown): ManifestValidationResult;
|
|
8
|
+
//# sourceMappingURL=validate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["../../src/manifest/validate.ts"],"names":[],"mappings":"AAyBA,MAAM,WAAW,wBAAwB;IACvC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,qEAAqE;IACrE,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,wBAAwB,CAgNvE"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Stateless port of the platform's PluginManifestValidatorService.
|
|
2
|
+
// Keep these rules in lock-step with
|
|
3
|
+
// creatoros-api/src/features/plugin-platform/manifest/plugin-manifest.validator.service.ts
|
|
4
|
+
// so a manifest that passes locally also passes server-side at publish time.
|
|
5
|
+
import { ALL_SCOPES, isValidScope } from '../data/scopes.js';
|
|
6
|
+
import { VALID_DOMAIN_EVENT_TYPES, isValidDomainEvent, } from '../data/events.js';
|
|
7
|
+
import { ALL_SLOTS } from '../data/slots.js';
|
|
8
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z-.]+)?(?:\+[0-9A-Za-z-.]+)?$/;
|
|
9
|
+
const KEY_RE = /^[a-z0-9](?:[a-z0-9._-]{0,62}[a-z0-9])?$/;
|
|
10
|
+
const HTTPS_RE = /^https:\/\/[^\s]+$/;
|
|
11
|
+
const VALID_TIME_WINDOWS = new Set(['always', 'event-live', 'post-event']);
|
|
12
|
+
const VALID_PRICING_MODELS = new Set([
|
|
13
|
+
'free',
|
|
14
|
+
'flat',
|
|
15
|
+
'per_attendee',
|
|
16
|
+
'revshare',
|
|
17
|
+
]);
|
|
18
|
+
const VALID_EXTENSION_SLOTS = new Set(ALL_SLOTS);
|
|
19
|
+
export function validateManifest(raw) {
|
|
20
|
+
const issues = [];
|
|
21
|
+
const hints = [];
|
|
22
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
issues: ['Manifest must be a non-array object'],
|
|
26
|
+
hints: ['Pass the parsed JSON, not a string. Use get_starter_manifest to scaffold.'],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const m = raw;
|
|
30
|
+
if (typeof m.key !== 'string' || !KEY_RE.test(m.key)) {
|
|
31
|
+
issues.push('key: must be a lowercase string of [a-z0-9._-], 1-64 chars, no leading/trailing punctuation');
|
|
32
|
+
hints.push('Use the pattern "<vendor>.<name>" — e.g. "acme.sponsors-lounge".');
|
|
33
|
+
}
|
|
34
|
+
if (typeof m.version !== 'string' || !SEMVER_RE.test(m.version)) {
|
|
35
|
+
issues.push('version: must be a valid semver string (e.g. "1.0.0")');
|
|
36
|
+
}
|
|
37
|
+
if (typeof m.name !== 'string' ||
|
|
38
|
+
m.name.length === 0 ||
|
|
39
|
+
m.name.length > 100) {
|
|
40
|
+
issues.push('name: must be a non-empty string, max 100 chars');
|
|
41
|
+
}
|
|
42
|
+
for (const f of ['tagline', 'description', 'category', 'iconUrl']) {
|
|
43
|
+
if (m[f] !== undefined && typeof m[f] !== 'string') {
|
|
44
|
+
issues.push(`${f}: must be a string when present`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (m.iconUrl !== undefined &&
|
|
48
|
+
typeof m.iconUrl === 'string' &&
|
|
49
|
+
!HTTPS_RE.test(m.iconUrl)) {
|
|
50
|
+
issues.push('iconUrl: must use https://');
|
|
51
|
+
}
|
|
52
|
+
if (m.screenshots !== undefined) {
|
|
53
|
+
if (!Array.isArray(m.screenshots)) {
|
|
54
|
+
issues.push('screenshots: must be an array of https URLs');
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
for (const s of m.screenshots) {
|
|
58
|
+
if (typeof s !== 'string' || !HTTPS_RE.test(s)) {
|
|
59
|
+
issues.push('screenshots: every entry must be an https URL');
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const dev = m.developer;
|
|
66
|
+
if (!dev || typeof dev !== 'object') {
|
|
67
|
+
issues.push('developer: missing or not an object');
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
if (typeof dev.name !== 'string' || dev.name.length === 0) {
|
|
71
|
+
issues.push('developer.name: required, non-empty string');
|
|
72
|
+
}
|
|
73
|
+
if (typeof dev.email !== 'string' || !dev.email.includes('@')) {
|
|
74
|
+
issues.push('developer.email: required, must be an email');
|
|
75
|
+
}
|
|
76
|
+
if (dev.url !== undefined &&
|
|
77
|
+
(typeof dev.url !== 'string' || !HTTPS_RE.test(dev.url))) {
|
|
78
|
+
issues.push('developer.url: must be an https URL when present');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
const pricing = m.pricing;
|
|
82
|
+
if (!pricing || typeof pricing !== 'object') {
|
|
83
|
+
issues.push('pricing: missing or not an object');
|
|
84
|
+
hints.push('Add pricing: { "model": "free" } if your plugin is free.');
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
if (typeof pricing.model !== 'string' ||
|
|
88
|
+
!VALID_PRICING_MODELS.has(pricing.model)) {
|
|
89
|
+
issues.push(`pricing.model: must be one of ${Array.from(VALID_PRICING_MODELS).join(', ')}`);
|
|
90
|
+
}
|
|
91
|
+
if (pricing.model === 'flat' || pricing.model === 'per_attendee') {
|
|
92
|
+
if (typeof pricing.amount !== 'number' || pricing.amount < 0) {
|
|
93
|
+
issues.push(`pricing.amount: required and >= 0 for model=${pricing.model}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (pricing.model === 'revshare') {
|
|
97
|
+
if (typeof pricing.revshareBps !== 'number' ||
|
|
98
|
+
pricing.revshareBps < 0 ||
|
|
99
|
+
pricing.revshareBps > 10000) {
|
|
100
|
+
issues.push('pricing.revshareBps: required, integer between 0 and 10000');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!Array.isArray(m.scopes)) {
|
|
105
|
+
issues.push('scopes: must be an array of scope strings');
|
|
106
|
+
hints.push(`Pick from: ${ALL_SCOPES.join(', ')}`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
for (const s of m.scopes) {
|
|
110
|
+
if (typeof s !== 'string' || !isValidScope(s)) {
|
|
111
|
+
issues.push(`scopes: "${String(s)}" is not a valid scope. Valid scopes: ${ALL_SCOPES.join(', ')}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const lc = m.lifecycle;
|
|
116
|
+
if (lc !== undefined) {
|
|
117
|
+
if (typeof lc !== 'object' || Array.isArray(lc)) {
|
|
118
|
+
issues.push('lifecycle: must be an object when present');
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
for (const f of ['onInstall', 'onUninstall', 'onConfigure']) {
|
|
122
|
+
const v = lc[f];
|
|
123
|
+
if (v !== undefined && (typeof v !== 'string' || !HTTPS_RE.test(v))) {
|
|
124
|
+
issues.push(`lifecycle.${f}: must be an https URL when present`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (!Array.isArray(m.events)) {
|
|
130
|
+
issues.push('events: must be an array (use [] if no domain event subscriptions)');
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
for (const e of m.events) {
|
|
134
|
+
if (typeof e !== 'string' || !isValidDomainEvent(e)) {
|
|
135
|
+
issues.push(`events: "${String(e)}" is not a valid domain event. Valid events: ${VALID_DOMAIN_EVENT_TYPES.join(', ')}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (m.extensions !== undefined) {
|
|
140
|
+
if (typeof m.extensions !== 'object' || Array.isArray(m.extensions)) {
|
|
141
|
+
issues.push('extensions: must be an object keyed by slot name');
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
for (const [slot, val] of Object.entries(m.extensions)) {
|
|
145
|
+
if (!VALID_EXTENSION_SLOTS.has(slot)) {
|
|
146
|
+
issues.push(`extensions: "${slot}" is not a known extension slot. Valid slots: ${ALL_SLOTS.join(', ')}`);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (!Array.isArray(val)) {
|
|
150
|
+
issues.push(`extensions.${slot}: must be an array`);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
val.forEach((ext, i) => {
|
|
154
|
+
if (!ext || typeof ext !== 'object') {
|
|
155
|
+
issues.push(`extensions.${slot}[${i}]: must be an object`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const e = ext;
|
|
159
|
+
if (typeof e.id !== 'string' || e.id.length === 0) {
|
|
160
|
+
issues.push(`extensions.${slot}[${i}].id: required string`);
|
|
161
|
+
}
|
|
162
|
+
if (typeof e.label !== 'string' || e.label.length === 0) {
|
|
163
|
+
issues.push(`extensions.${slot}[${i}].label: required string`);
|
|
164
|
+
}
|
|
165
|
+
if (typeof e.iframe !== 'string' || !HTTPS_RE.test(e.iframe)) {
|
|
166
|
+
issues.push(`extensions.${slot}[${i}].iframe: required https URL`);
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (m.configSchema !== undefined &&
|
|
173
|
+
(typeof m.configSchema !== 'object' || Array.isArray(m.configSchema))) {
|
|
174
|
+
issues.push('configSchema: must be a JSON Schema object when present');
|
|
175
|
+
}
|
|
176
|
+
if (typeof m.timeWindow !== 'string' ||
|
|
177
|
+
!VALID_TIME_WINDOWS.has(m.timeWindow)) {
|
|
178
|
+
issues.push(`timeWindow: required, one of ${Array.from(VALID_TIME_WINDOWS).join(', ')}`);
|
|
179
|
+
}
|
|
180
|
+
return { ok: issues.length === 0, issues, hints };
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=validate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate.js","sourceRoot":"","sources":["../../src/manifest/validate.ts"],"names":[],"mappings":"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"}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAoBpE;;;GAGG;AACH,wBAAgB,YAAY,IAAI,SAAS,CAkLxC"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { validateManifest } from './manifest/validate.js';
|
|
4
|
+
import { generateStarterManifest } from './manifest/starter.js';
|
|
5
|
+
import { verifyWebhookSignature } from './webhook/verify.js';
|
|
6
|
+
import { SCOPE_REGISTRY } from './data/scopes.js';
|
|
7
|
+
import { LIFECYCLE_EVENTS, DOMAIN_EVENTS } from './data/events.js';
|
|
8
|
+
import { SLOT_REGISTRY } from './data/slots.js';
|
|
9
|
+
import { PLUGIN_API_ENDPOINTS } from './data/api-endpoints.js';
|
|
10
|
+
function asJson(value) {
|
|
11
|
+
return {
|
|
12
|
+
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function asMarkdown(text) {
|
|
16
|
+
return { content: [{ type: 'text', text }] };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Builds the MCP server. Exported so tests can drive it without
|
|
20
|
+
* spawning a stdio process.
|
|
21
|
+
*/
|
|
22
|
+
export function createServer() {
|
|
23
|
+
const server = new McpServer({
|
|
24
|
+
name: 'experia-plugin-dev',
|
|
25
|
+
version: '0.1.0',
|
|
26
|
+
});
|
|
27
|
+
// ---- Discovery tools (no inputs) ----
|
|
28
|
+
server.registerTool('list_scopes', {
|
|
29
|
+
title: 'List plugin scopes',
|
|
30
|
+
description: 'Returns every OAuth-style scope a plugin can request in its manifest, with a description and the /plugin-api/v1 endpoints each unlocks. Use this when picking the minimal set of scopes for a plugin.',
|
|
31
|
+
}, async () => asJson(SCOPE_REGISTRY));
|
|
32
|
+
server.registerTool('list_events', {
|
|
33
|
+
title: 'List webhook events',
|
|
34
|
+
description: 'Returns the lifecycle events (always delivered) and domain events (subscribe via manifest.events) a plugin can receive, with example payloads. Use this when planning your webhook handler.',
|
|
35
|
+
}, async () => asJson({ lifecycle: LIFECYCLE_EVENTS, domain: DOMAIN_EVENTS }));
|
|
36
|
+
server.registerTool('list_extension_slots', {
|
|
37
|
+
title: 'List UI extension slots',
|
|
38
|
+
description: 'Returns every named slot where a plugin\'s iframe can render (organizer dashboard, attendee app, check-in flow). Use this when picking where in the host UI your plugin should appear.',
|
|
39
|
+
}, async () => asJson(SLOT_REGISTRY));
|
|
40
|
+
server.registerTool('list_api_endpoints', {
|
|
41
|
+
title: 'List /plugin-api/v1 endpoints',
|
|
42
|
+
description: 'Returns every endpoint your plugin can call after install (Bearer = installationToken). Each entry lists the required scope. Use this to plan API calls and the matching scope set.',
|
|
43
|
+
}, async () => asJson(PLUGIN_API_ENDPOINTS));
|
|
44
|
+
// ---- Manifest helpers ----
|
|
45
|
+
server.registerTool('validate_manifest', {
|
|
46
|
+
title: 'Validate a plugin manifest',
|
|
47
|
+
description: 'Validates a JSON manifest against the platform\'s rules. Identical to the server-side check at publish time — if this passes, POST /developer-portal/plugins/:id/versions will too. Returns { ok, issues, hints }.',
|
|
48
|
+
inputSchema: {
|
|
49
|
+
manifest: z
|
|
50
|
+
.unknown()
|
|
51
|
+
.describe('The parsed JSON manifest object (NOT a string).'),
|
|
52
|
+
},
|
|
53
|
+
}, async ({ manifest }) => asJson(validateManifest(manifest)));
|
|
54
|
+
server.registerTool('get_starter_manifest', {
|
|
55
|
+
title: 'Generate a starter manifest',
|
|
56
|
+
description: 'Returns a manifest skeleton wired for a common plugin shape. Pick "minimal" for the smallest possible plugin, "organizer-tool" for an organizer-facing config+insights pattern, or "attendee-app" for a Networking-style plugin (default).',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
key: z
|
|
59
|
+
.string()
|
|
60
|
+
.describe('Vendor-prefixed plugin key, e.g. "acme.sponsors-lounge".'),
|
|
61
|
+
name: z.string().describe('Display name shown in the marketplace.'),
|
|
62
|
+
developerName: z.string().describe('Your organization name.'),
|
|
63
|
+
developerEmail: z.string().describe('Contact email for review feedback.'),
|
|
64
|
+
baseUrl: z
|
|
65
|
+
.string()
|
|
66
|
+
.describe('HTTPS base URL where your plugin is hosted (no trailing slash).'),
|
|
67
|
+
flavor: z
|
|
68
|
+
.enum(['minimal', 'attendee-app', 'organizer-tool'])
|
|
69
|
+
.optional()
|
|
70
|
+
.describe('Which template to use. Default: attendee-app.'),
|
|
71
|
+
},
|
|
72
|
+
}, async (args) => asJson(generateStarterManifest(args)));
|
|
73
|
+
// ---- Webhook helpers ----
|
|
74
|
+
server.registerTool('verify_webhook_signature', {
|
|
75
|
+
title: 'Verify an X-Experia-Signature header',
|
|
76
|
+
description: 'Verifies a webhook delivery using the per-installation `webhookSecret` you stored from plugin.installed. Use this when debugging a 401/403 from your handler — it will tell you exactly why a signature failed (malformed-header / timestamp-out-of-tolerance / signature-mismatch).',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
payload: z
|
|
79
|
+
.string()
|
|
80
|
+
.describe('Raw request body, exactly as received (do not JSON.parse).'),
|
|
81
|
+
signatureHeader: z
|
|
82
|
+
.string()
|
|
83
|
+
.describe('Value of the X-Experia-Signature header.'),
|
|
84
|
+
secret: z.string().describe('Per-installation webhookSecret.'),
|
|
85
|
+
toleranceSeconds: z
|
|
86
|
+
.number()
|
|
87
|
+
.optional()
|
|
88
|
+
.describe('Replay-window tolerance in seconds. Default 300.'),
|
|
89
|
+
},
|
|
90
|
+
}, async (args) => asJson(verifyWebhookSignature(args)));
|
|
91
|
+
// ---- Resources (read-only docs) ----
|
|
92
|
+
server.registerResource('plugin-overview', 'experia://docs/overview', {
|
|
93
|
+
title: 'Experia Plugin Platform Overview',
|
|
94
|
+
description: 'High-level explanation of the plugin lifecycle, manifests, webhooks, and the public /plugin-api/v1.',
|
|
95
|
+
mimeType: 'text/markdown',
|
|
96
|
+
}, async (uri) => ({
|
|
97
|
+
contents: [
|
|
98
|
+
{
|
|
99
|
+
uri: uri.href,
|
|
100
|
+
mimeType: 'text/markdown',
|
|
101
|
+
text: OVERVIEW_DOC,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
}));
|
|
105
|
+
server.registerResource('manifest-spec', 'experia://docs/manifest', {
|
|
106
|
+
title: 'Manifest field-by-field reference',
|
|
107
|
+
description: 'Every field of the manifest, what it does, validation rules.',
|
|
108
|
+
mimeType: 'text/markdown',
|
|
109
|
+
}, async (uri) => ({
|
|
110
|
+
contents: [
|
|
111
|
+
{
|
|
112
|
+
uri: uri.href,
|
|
113
|
+
mimeType: 'text/markdown',
|
|
114
|
+
text: MANIFEST_DOC,
|
|
115
|
+
},
|
|
116
|
+
],
|
|
117
|
+
}));
|
|
118
|
+
server.registerResource('webhook-spec', 'experia://docs/webhooks', {
|
|
119
|
+
title: 'Webhook delivery, signing, retries',
|
|
120
|
+
description: 'How HMAC signatures work, the retry schedule, and how to verify deliveries.',
|
|
121
|
+
mimeType: 'text/markdown',
|
|
122
|
+
}, async (uri) => ({
|
|
123
|
+
contents: [
|
|
124
|
+
{
|
|
125
|
+
uri: uri.href,
|
|
126
|
+
mimeType: 'text/markdown',
|
|
127
|
+
text: WEBHOOKS_DOC,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
}));
|
|
131
|
+
// Side-effect: silence unused-import linters for asMarkdown if no doc tool uses it.
|
|
132
|
+
void asMarkdown;
|
|
133
|
+
return server;
|
|
134
|
+
}
|
|
135
|
+
const OVERVIEW_DOC = `# Experia Plugin Platform — Overview
|
|
136
|
+
|
|
137
|
+
Plugins extend Experia for organizers and attendees. They are described by a
|
|
138
|
+
JSON **manifest** and talk to the platform over a public API.
|
|
139
|
+
|
|
140
|
+
## Lifecycle
|
|
141
|
+
|
|
142
|
+
1. You **submit a plugin** via the developer portal (creates a Plugin row in DRAFT/PRIVATE).
|
|
143
|
+
2. You **publish a version** (uploads a manifest snapshot, validated by the platform).
|
|
144
|
+
3. An organizer **installs** your plugin on one of their events. The platform:
|
|
145
|
+
- generates a one-time \`installationToken\` (Bearer for /plugin-api/v1)
|
|
146
|
+
- generates a per-install \`webhookSecret\` (HMAC for verifying webhooks)
|
|
147
|
+
- delivers them in a \`plugin.installed\` webhook to your \`lifecycle.onInstall\` URL
|
|
148
|
+
4. Your plugin receives subsequent **webhook events** and calls
|
|
149
|
+
\`/plugin-api/v1/*\` to read/write data — limited to the **scopes** declared
|
|
150
|
+
in your manifest.
|
|
151
|
+
5. Your plugin's iframes render in **extension slots** chosen in the manifest.
|
|
152
|
+
|
|
153
|
+
## What you build
|
|
154
|
+
|
|
155
|
+
- **A web service** that hosts \`lifecycle.onInstall\` (and optionally
|
|
156
|
+
\`onUninstall\`/\`onConfigure\`) and any iframe pages declared in
|
|
157
|
+
\`manifest.extensions\`.
|
|
158
|
+
- A storage layer (your DB) keyed by \`eventPluginId\` to remember the
|
|
159
|
+
installation token + webhook secret.
|
|
160
|
+
|
|
161
|
+
## What you DON'T do
|
|
162
|
+
|
|
163
|
+
- You don't host the database. Plugin data that's per-installation can use the
|
|
164
|
+
built-in \`storage.read\` / \`storage.write\` scopes.
|
|
165
|
+
- You don't manage auth. Bearer = installation token; iframe = JWT.
|
|
166
|
+
`;
|
|
167
|
+
const MANIFEST_DOC = `# Manifest reference
|
|
168
|
+
|
|
169
|
+
\`\`\`jsonc
|
|
170
|
+
{
|
|
171
|
+
"key": "vendor.name", // [a-z0-9._-], 1-64 chars
|
|
172
|
+
"version": "1.0.0", // semver
|
|
173
|
+
"name": "Display name", // ≤100 chars
|
|
174
|
+
"tagline": "One-sentence pitch.", // optional
|
|
175
|
+
"description": "Long description.", // optional
|
|
176
|
+
"category": "engagement", // optional
|
|
177
|
+
"iconUrl": "https://...", // optional, https
|
|
178
|
+
"screenshots": ["https://..."], // optional, all https
|
|
179
|
+
|
|
180
|
+
"developer": {
|
|
181
|
+
"name": "Acme Corp", // required
|
|
182
|
+
"email": "dev@acme.com", // required, must contain @
|
|
183
|
+
"url": "https://acme.com" // optional, https
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
"pricing": { "model": "free" }, // or flat/per_attendee + amount, or revshare + revshareBps (0-10000)
|
|
187
|
+
|
|
188
|
+
"scopes": ["event.read", "event.attendees.read"], // see list_scopes
|
|
189
|
+
|
|
190
|
+
"lifecycle": { // entire object optional
|
|
191
|
+
"onInstall": "https://api.acme.com/experia/install",
|
|
192
|
+
"onUninstall": "https://api.acme.com/experia/uninstall",
|
|
193
|
+
"onConfigure": "https://api.acme.com/experia/configure"
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
"events": ["event.started", "attendee.checked_in"], // see list_events (domain only)
|
|
197
|
+
|
|
198
|
+
"extensions": { // see list_extension_slots
|
|
199
|
+
"experia.event.tabs": [
|
|
200
|
+
{ "id": "home", "label": "Network", "iframe": "https://acme.com/?eventId={eventId}&jwt={pluginJwt}" }
|
|
201
|
+
]
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
"configSchema": { "type": "object", "properties": { ... } }, // optional JSON Schema
|
|
205
|
+
"timeWindow": "always" // always | event-live | post-event
|
|
206
|
+
}
|
|
207
|
+
\`\`\`
|
|
208
|
+
|
|
209
|
+
Use **validate_manifest** in this MCP server to confirm a manifest passes
|
|
210
|
+
the same rules as the platform's publish endpoint.
|
|
211
|
+
`;
|
|
212
|
+
const WEBHOOKS_DOC = `# Webhooks
|
|
213
|
+
|
|
214
|
+
Outbound webhook deliveries are signed Stripe-style:
|
|
215
|
+
|
|
216
|
+
\`\`\`
|
|
217
|
+
X-Experia-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>
|
|
218
|
+
Body: <raw JSON>
|
|
219
|
+
\`\`\`
|
|
220
|
+
|
|
221
|
+
Where \`v1 = HMAC_SHA256(webhookSecret, "${"$"}{t}.${"$"}{rawBody}")\`.
|
|
222
|
+
|
|
223
|
+
The \`webhookSecret\` is **per-installation** — you receive it once in the
|
|
224
|
+
\`plugin.installed\` payload. Persist it immediately.
|
|
225
|
+
|
|
226
|
+
## Verification rule
|
|
227
|
+
|
|
228
|
+
1. Parse the header into \`t\` and \`v1\`.
|
|
229
|
+
2. Reject if \`abs(now - t) > 300\` (replay window).
|
|
230
|
+
3. Recompute \`v1\` using your saved secret + the raw body.
|
|
231
|
+
4. Constant-time compare. Reject on mismatch.
|
|
232
|
+
|
|
233
|
+
The **verify_webhook_signature** tool in this MCP server runs the exact same
|
|
234
|
+
algorithm — useful for debugging a 401/403 from your handler.
|
|
235
|
+
|
|
236
|
+
## Retries
|
|
237
|
+
|
|
238
|
+
Failed deliveries are retried with exponential backoff (~1m, 5m, 30m, 2h, 12h).
|
|
239
|
+
After ~5 attempts the delivery is marked failed; you can replay it from the
|
|
240
|
+
developer portal once the issue is fixed. All deliveries are logged in
|
|
241
|
+
\`PluginWebhookDelivery\`.
|
|
242
|
+
`;
|
|
243
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface VerifyWebhookArgs {
|
|
2
|
+
/** Raw request body as it was received (string or Buffer). */
|
|
3
|
+
payload: string | Buffer;
|
|
4
|
+
/** Value of the `X-Experia-Signature` header. Format: `t=<unix>,v1=<hex>`. */
|
|
5
|
+
signatureHeader: string;
|
|
6
|
+
/** The webhookSecret you stored from the plugin.installed payload. */
|
|
7
|
+
secret: string;
|
|
8
|
+
/** Tolerance window in seconds. Default 5 minutes. */
|
|
9
|
+
toleranceSeconds?: number;
|
|
10
|
+
/** Override "now" (testing only). */
|
|
11
|
+
nowSeconds?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface VerifyWebhookResult {
|
|
14
|
+
ok: boolean;
|
|
15
|
+
reason?: 'malformed-header' | 'timestamp-out-of-tolerance' | 'signature-mismatch';
|
|
16
|
+
/** Parsed timestamp (when header parses). */
|
|
17
|
+
timestamp?: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Verifies an `X-Experia-Signature` header. Uses constant-time compare
|
|
21
|
+
* and a timestamp-tolerance window to prevent replay.
|
|
22
|
+
*
|
|
23
|
+
* Mirrors the signing rule:
|
|
24
|
+
* sig = HMAC_SHA256(secret, `${timestamp}.${rawPayload}`)
|
|
25
|
+
* header = `t=${timestamp},v1=${sig}`
|
|
26
|
+
*/
|
|
27
|
+
export declare function verifyWebhookSignature(args: VerifyWebhookArgs): VerifyWebhookResult;
|
|
28
|
+
//# sourceMappingURL=verify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.d.ts","sourceRoot":"","sources":["../../src/webhook/verify.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,iBAAiB;IAChC,8DAA8D;IAC9D,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,8EAA8E;IAC9E,eAAe,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAC;IACf,sDAAsD;IACtD,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qCAAqC;IACrC,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EACH,kBAAkB,GAClB,4BAA4B,GAC5B,oBAAoB,CAAC;IACzB,6CAA6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,iBAAiB,GACtB,mBAAmB,CAyCrB"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
2
|
+
/**
|
|
3
|
+
* Verifies an `X-Experia-Signature` header. Uses constant-time compare
|
|
4
|
+
* and a timestamp-tolerance window to prevent replay.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors the signing rule:
|
|
7
|
+
* sig = HMAC_SHA256(secret, `${timestamp}.${rawPayload}`)
|
|
8
|
+
* header = `t=${timestamp},v1=${sig}`
|
|
9
|
+
*/
|
|
10
|
+
export function verifyWebhookSignature(args) {
|
|
11
|
+
const tolerance = args.toleranceSeconds ?? 300;
|
|
12
|
+
const now = args.nowSeconds ?? Math.floor(Date.now() / 1000);
|
|
13
|
+
const parts = args.signatureHeader.split(',').reduce((acc, p) => {
|
|
14
|
+
const [k, v] = p.split('=');
|
|
15
|
+
if (k && v)
|
|
16
|
+
acc[k.trim()] = v.trim();
|
|
17
|
+
return acc;
|
|
18
|
+
}, {});
|
|
19
|
+
const t = parts['t'];
|
|
20
|
+
const v1 = parts['v1'];
|
|
21
|
+
if (!t || !v1)
|
|
22
|
+
return { ok: false, reason: 'malformed-header' };
|
|
23
|
+
const ts = Number(t);
|
|
24
|
+
if (!Number.isFinite(ts))
|
|
25
|
+
return { ok: false, reason: 'malformed-header' };
|
|
26
|
+
if (Math.abs(now - ts) > tolerance) {
|
|
27
|
+
return { ok: false, reason: 'timestamp-out-of-tolerance', timestamp: ts };
|
|
28
|
+
}
|
|
29
|
+
const payloadStr = typeof args.payload === 'string'
|
|
30
|
+
? args.payload
|
|
31
|
+
: args.payload.toString('utf8');
|
|
32
|
+
const expected = createHmac('sha256', args.secret)
|
|
33
|
+
.update(`${t}.${payloadStr}`)
|
|
34
|
+
.digest('hex');
|
|
35
|
+
const a = Buffer.from(expected, 'hex');
|
|
36
|
+
const b = Buffer.from(v1, 'hex');
|
|
37
|
+
if (a.length !== b.length || !timingSafeEqual(a, b)) {
|
|
38
|
+
return { ok: false, reason: 'signature-mismatch', timestamp: ts };
|
|
39
|
+
}
|
|
40
|
+
return { ok: true, timestamp: ts };
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=verify.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verify.js","sourceRoot":"","sources":["../../src/webhook/verify.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAyB1D;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CACpC,IAAuB;IAEvB,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,IAAI,GAAG,CAAC;IAC/C,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAE7D,MAAM,KAAK,GAAG,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAClD,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE;QACT,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC5B,IAAI,CAAC,IAAI,CAAC;YAAE,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACrC,OAAO,GAAG,CAAC;IACb,CAAC,EACD,EAAE,CACH,CAAC;IAEF,MAAM,CAAC,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;IACrB,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC;IACvB,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAEhE,MAAM,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;IACrB,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAE3E,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,SAAS,EAAE,CAAC;QACnC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,4BAA4B,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;IAC5E,CAAC;IAED,MAAM,UAAU,GACd,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ;QAC9B,CAAC,CAAC,IAAI,CAAC,OAAO;QACd,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IAEpC,MAAM,QAAQ,GAAG,UAAU,CAAC,QAAQ,EAAE,IAAI,CAAC,MAAM,CAAC;SAC/C,MAAM,CAAC,GAAG,CAAC,IAAI,UAAU,EAAE,CAAC;SAC5B,MAAM,CAAC,KAAK,CAAC,CAAC;IAEjB,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACvC,MAAM,CAAC,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;IAEjC,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM,IAAI,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC;QACpD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,oBAAoB,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;IACpE,CAAC;IAED,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,EAAE,CAAC;AACrC,CAAC"}
|