@aspalagin/openclaw-max 0.7.2

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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +424 -0
  3. package/SKILL.md +108 -0
  4. package/dist/index.js +17 -0
  5. package/dist/setup-entry.js +8 -0
  6. package/dist/src/accounts.js +148 -0
  7. package/dist/src/actions.js +421 -0
  8. package/dist/src/api.js +553 -0
  9. package/dist/src/callbacks.js +111 -0
  10. package/dist/src/channel-agent-prompt.js +38 -0
  11. package/dist/src/channel-config.js +103 -0
  12. package/dist/src/channel-directory.js +84 -0
  13. package/dist/src/channel-lifecycle.js +219 -0
  14. package/dist/src/channel-outbound.js +133 -0
  15. package/dist/src/channel-policy.js +130 -0
  16. package/dist/src/channel.js +76 -0
  17. package/dist/src/config-schema.js +133 -0
  18. package/dist/src/deliver.js +126 -0
  19. package/dist/src/dispatch.js +186 -0
  20. package/dist/src/format.js +31 -0
  21. package/dist/src/inbound-attachments.js +125 -0
  22. package/dist/src/inbound.js +362 -0
  23. package/dist/src/media-temp.js +92 -0
  24. package/dist/src/model-buttons.js +96 -0
  25. package/dist/src/monitor-types.js +27 -0
  26. package/dist/src/monitor.js +40 -0
  27. package/dist/src/onboarding.js +180 -0
  28. package/dist/src/polling.js +98 -0
  29. package/dist/src/presentation.js +374 -0
  30. package/dist/src/runtime.js +36 -0
  31. package/dist/src/russian-trusted-ca.js +84 -0
  32. package/dist/src/send.js +612 -0
  33. package/dist/src/state.js +111 -0
  34. package/dist/src/sticker-cache.js +26 -0
  35. package/dist/src/stream-draft.js +102 -0
  36. package/dist/src/types.js +5 -0
  37. package/dist/src/webhook-queue.js +215 -0
  38. package/dist/src/webhook-runner.js +233 -0
  39. package/dist/src/webhook.js +196 -0
  40. package/openclaw.plugin.json +263 -0
  41. package/package.json +100 -0
  42. package/scripts/check-cycles.mjs +38 -0
  43. package/scripts/test-api.mjs +169 -0
  44. package/scripts/test-send.mjs +162 -0
  45. package/scripts/update-schema.mjs +48 -0
  46. package/src/accounts.ts +193 -0
  47. package/src/actions.ts +501 -0
  48. package/src/api.ts +758 -0
  49. package/src/callbacks.ts +140 -0
  50. package/src/channel-agent-prompt.ts +47 -0
  51. package/src/channel-config.ts +138 -0
  52. package/src/channel-directory.ts +90 -0
  53. package/src/channel-lifecycle.ts +256 -0
  54. package/src/channel-outbound.ts +159 -0
  55. package/src/channel-policy.ts +185 -0
  56. package/src/channel.ts +99 -0
  57. package/src/config-schema.ts +154 -0
  58. package/src/deliver.ts +168 -0
  59. package/src/dispatch.ts +224 -0
  60. package/src/format.ts +33 -0
  61. package/src/inbound-attachments.ts +152 -0
  62. package/src/inbound.ts +412 -0
  63. package/src/media-temp.ts +103 -0
  64. package/src/model-buttons.ts +150 -0
  65. package/src/monitor-types.ts +61 -0
  66. package/src/monitor.ts +49 -0
  67. package/src/onboarding.ts +215 -0
  68. package/src/polling.ts +125 -0
  69. package/src/presentation.ts +452 -0
  70. package/src/runtime.ts +56 -0
  71. package/src/russian-trusted-ca.ts +86 -0
  72. package/src/send.ts +774 -0
  73. package/src/state.ts +149 -0
  74. package/src/sticker-cache.ts +28 -0
  75. package/src/stream-draft.ts +125 -0
  76. package/src/types.ts +263 -0
  77. package/src/webhook-queue.ts +248 -0
  78. package/src/webhook-runner.ts +296 -0
  79. package/src/webhook.ts +268 -0
@@ -0,0 +1,374 @@
1
+ /**
2
+ * MessagePresentation → MAX message (text in MAX markdown + inline_keyboard).
3
+ *
4
+ * Contract: docs/plugins/message-presentation.md. Core adapts a presentation to
5
+ * `MAX_PRESENTATION_CAPABILITIES` and calls `renderMaxPresentation`; the result
6
+ * is an ordinary payload with `channelData.max.buttons`, sent by sendPayload.
7
+ * Channel-local funnels (agent replies, the message tool) go through the same
8
+ * `renderPresentationForDelivery` so they behave identically.
9
+ *
10
+ * Presentation actions become transport-private callback payloads (≤1024
11
+ * bytes) that `decodeMaxPresentationCallback` maps back on `message_callback`:
12
+ * - command → the command text itself ("/status"): it re-enters as a user
13
+ * message and takes core's native command path;
14
+ * - callback → "mxcb1:<value>": opaque, delivered to the agent as
15
+ * "callback_data: <value>" and never parsed as a slash command;
16
+ * - approval → "mxa1:<e|p|s>:<o|a|d>:<approvalId>": resolved through the
17
+ * canonical approval service (approval-gateway-runtime);
18
+ * - question → "mxq1:<questionId>:<optionValue>": resolved through
19
+ * question-gateway-runtime;
20
+ * - url / URL-backed web-app → a MAX `link` button.
21
+ */
22
+ import { renderMessagePresentationFallbackText, renderPresentationForDelivery, resolveMessagePresentationButtonAction, resolveMessagePresentationOptionAction, } from 'openclaw/plugin-sdk/interactive-runtime';
23
+ /** MAX Bot API limits (schema.yaml: Button.text, CallbackButton.payload, InlineKeyboard). */
24
+ export const MAX_BUTTON_TEXT_LIMIT = 128;
25
+ export const MAX_CALLBACK_PAYLOAD_BYTES = 1024;
26
+ export const MAX_KEYBOARD_ROWS = 30;
27
+ export const MAX_KEYBOARD_BUTTONS = 210;
28
+ export const MAX_LINK_URL_LENGTH = 2048;
29
+ export const MAX_TEXT_LIMIT = 4000;
30
+ /** Buttons per row we render: within MAX's 7 (3 for link/open_app/request_*). */
31
+ const BUTTONS_PER_ROW = 3;
32
+ const SELECT_OPTIONS_PER_ROW = 2;
33
+ export const MAX_PRESENTATION_CAPABILITIES = {
34
+ supported: true,
35
+ buttons: true,
36
+ selects: true,
37
+ context: true,
38
+ divider: true,
39
+ // Rendered as monospace text blocks (MAX has no native charts/tables).
40
+ charts: true,
41
+ tables: true,
42
+ limits: {
43
+ actions: {
44
+ maxActions: MAX_KEYBOARD_BUTTONS,
45
+ maxActionsPerRow: BUTTONS_PER_ROW,
46
+ maxRows: MAX_KEYBOARD_ROWS,
47
+ maxLabelLength: MAX_BUTTON_TEXT_LIMIT,
48
+ // Leave room for the private envelope prefix inside the 1024-byte payload.
49
+ maxValueBytes: 960,
50
+ supportsStyles: false,
51
+ supportsDisabled: false,
52
+ },
53
+ selects: {
54
+ maxOptions: 20,
55
+ maxLabelLength: MAX_BUTTON_TEXT_LIMIT,
56
+ maxValueBytes: 960,
57
+ },
58
+ text: {
59
+ maxLength: MAX_TEXT_LIMIT,
60
+ encoding: 'characters',
61
+ markdownDialect: 'markdown',
62
+ },
63
+ },
64
+ };
65
+ // ── Callback envelopes ──
66
+ const CALLBACK_PREFIX = 'mxcb1:';
67
+ const APPROVAL_PREFIX = 'mxa1:';
68
+ const QUESTION_PREFIX = 'mxq1:';
69
+ const APPROVAL_KIND_CODES = {
70
+ exec: 'e',
71
+ plugin: 'p',
72
+ 'system-agent': 's',
73
+ };
74
+ const APPROVAL_DECISION_CODES = {
75
+ 'allow-once': 'o',
76
+ 'allow-always': 'a',
77
+ deny: 'd',
78
+ };
79
+ function fitsPayload(payload) {
80
+ return payload.length > 0 && Buffer.byteLength(payload, 'utf8') <= MAX_CALLBACK_PAYLOAD_BYTES;
81
+ }
82
+ /** Decode a payload produced by this renderer; null for anything else (legacy buttons). */
83
+ export function decodeMaxPresentationCallback(payload) {
84
+ if (!payload)
85
+ return null;
86
+ if (payload.startsWith(CALLBACK_PREFIX)) {
87
+ const value = payload.slice(CALLBACK_PREFIX.length);
88
+ return value ? { kind: 'callback', value } : null;
89
+ }
90
+ if (payload.startsWith(APPROVAL_PREFIX)) {
91
+ const match = /^mxa1:([eps]):([oad]):(.+)$/su.exec(payload);
92
+ if (!match)
93
+ return null;
94
+ const approvalKind = Object.keys(APPROVAL_KIND_CODES).find((kind) => APPROVAL_KIND_CODES[kind] === match[1]);
95
+ const decision = Object.keys(APPROVAL_DECISION_CODES).find((code) => APPROVAL_DECISION_CODES[code] === match[2]);
96
+ return approvalKind && decision
97
+ ? { kind: 'approval', approvalKind, decision, approvalId: match[3] }
98
+ : null;
99
+ }
100
+ if (payload.startsWith(QUESTION_PREFIX)) {
101
+ const rest = payload.slice(QUESTION_PREFIX.length);
102
+ const separator = rest.indexOf(':');
103
+ if (separator <= 0 || separator === rest.length - 1)
104
+ return null;
105
+ return {
106
+ kind: 'question',
107
+ questionId: rest.slice(0, separator),
108
+ optionValue: rest.slice(separator + 1),
109
+ };
110
+ }
111
+ return null;
112
+ }
113
+ function clipLabel(label) {
114
+ const chars = Array.from(label.trim());
115
+ return chars.length <= MAX_BUTTON_TEXT_LIMIT
116
+ ? chars.join('')
117
+ : `${chars.slice(0, MAX_BUTTON_TEXT_LIMIT - 1).join('')}…`;
118
+ }
119
+ function callbackButton(label, payload) {
120
+ return fitsPayload(payload)
121
+ ? { button: { text: clipLabel(label), type: 'callback', payload } }
122
+ : { dropped: label };
123
+ }
124
+ function linkButton(label, url) {
125
+ const trimmed = url.trim();
126
+ if (!/^https?:\/\//iu.test(trimmed) || trimmed.length > MAX_LINK_URL_LENGTH)
127
+ return { dropped: label };
128
+ return { button: { text: clipLabel(label), type: 'link', url: trimmed } };
129
+ }
130
+ function commandPayload(command) {
131
+ const trimmed = command.trim();
132
+ return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
133
+ }
134
+ function toControl(button) {
135
+ const label = button.label;
136
+ const action = resolveMessagePresentationButtonAction(button);
137
+ if (!action || button.disabled)
138
+ return { dropped: label };
139
+ switch (action.type) {
140
+ case 'url':
141
+ return linkButton(label, action.url);
142
+ case 'web-app':
143
+ // MAX open_app addresses a bot's mini app by name, not by URL.
144
+ return action.url ? linkButton(label, action.url) : { dropped: label };
145
+ case 'command':
146
+ return action.command.trim()
147
+ ? callbackButton(label, commandPayload(action.command))
148
+ : { dropped: label };
149
+ case 'callback':
150
+ return callbackButton(label, `${CALLBACK_PREFIX}${action.value}`);
151
+ case 'approval': {
152
+ const kind = APPROVAL_KIND_CODES[action.approvalKind];
153
+ const decision = APPROVAL_DECISION_CODES[action.decision];
154
+ if (!kind || !decision || !action.approvalId)
155
+ return { dropped: label };
156
+ return callbackButton(label, `${APPROVAL_PREFIX}${kind}:${decision}:${action.approvalId}`);
157
+ }
158
+ case 'question':
159
+ // custom-input needs a free-text composer target MAX cannot address; the
160
+ // producer states the text route in the message, so the control is omitted.
161
+ if ('intent' in action || action.questionId.includes(':'))
162
+ return { dropped: label };
163
+ return callbackButton(label, `${QUESTION_PREFIX}${action.questionId}:${action.optionValue}`);
164
+ default:
165
+ return { dropped: label };
166
+ }
167
+ }
168
+ function toOptionControl(option) {
169
+ const action = resolveMessagePresentationOptionAction(option);
170
+ if (!action)
171
+ return { dropped: option.label };
172
+ return action.type === 'command'
173
+ ? callbackButton(option.label, commandPayload(action.command))
174
+ : callbackButton(option.label, `${CALLBACK_PREFIX}${action.value}`);
175
+ }
176
+ // ── Text ──
177
+ const TONE_PREFIX = {
178
+ info: 'ℹ️',
179
+ success: '✅',
180
+ warning: '⚠️',
181
+ danger: '⛔',
182
+ };
183
+ function collapse(value) {
184
+ return String(value).replace(/\s+/gu, ' ').replace(/`/gu, "'").trim();
185
+ }
186
+ function monospaceTable(headers, rows) {
187
+ const widths = headers.map((header, column) => Math.max(Array.from(header).length, ...rows.map((row) => Array.from(row[column] ?? '').length)));
188
+ const pad = (cells) => cells
189
+ .map((cell, column) => cell + ' '.repeat(Math.max(0, widths[column] - Array.from(cell).length)))
190
+ .join(' | ')
191
+ .trimEnd();
192
+ const lines = [
193
+ pad(headers),
194
+ widths.map((width) => '-'.repeat(Math.max(1, width))).join('-+-'),
195
+ ...rows.map(pad),
196
+ ];
197
+ return ['```', ...lines, '```'].join('\n');
198
+ }
199
+ function renderDataBlock(block) {
200
+ if (block.type === 'table') {
201
+ return [
202
+ `**${collapse(block.caption)}**`,
203
+ monospaceTable(block.headers.map(collapse), block.rows.map((row) => row.map(collapse))),
204
+ ].join('\n');
205
+ }
206
+ if (block.chartType === 'pie') {
207
+ const total = block.segments.reduce((sum, segment) => sum + segment.value, 0);
208
+ const rows = block.segments.map((segment) => [
209
+ collapse(segment.label),
210
+ collapse(segment.value),
211
+ total > 0 ? `${((segment.value / total) * 100).toFixed(1)}%` : '',
212
+ ]);
213
+ return [`**${collapse(block.title)}** (pie)`, monospaceTable(['', '', '%'], rows)].join('\n');
214
+ }
215
+ const headers = [
216
+ collapse(block.xLabel ?? ''),
217
+ ...block.series.map((series) => collapse(series.name)),
218
+ ];
219
+ const rows = block.categories.map((category, index) => [
220
+ collapse(category),
221
+ ...block.series.map((series) => collapse(series.values[index] ?? '')),
222
+ ]);
223
+ const axis = block.yLabel ? `, ${collapse(block.yLabel)}` : '';
224
+ return [
225
+ `**${collapse(block.title)}** (${block.chartType}${axis})`,
226
+ monospaceTable(headers, rows),
227
+ ].join('\n');
228
+ }
229
+ function renderContext(text) {
230
+ const trimmed = text.trim();
231
+ return /[_*\n]/u.test(trimmed) ? trimmed : `_${trimmed}_`;
232
+ }
233
+ function chunk(items, size) {
234
+ const rows = [];
235
+ for (let index = 0; index < items.length; index += size)
236
+ rows.push(items.slice(index, index + size));
237
+ return rows;
238
+ }
239
+ /** Pure mapping of an (already adapted) presentation to MAX text + keyboard rows. */
240
+ export function renderMaxPresentationParts(params) {
241
+ const { presentation } = params;
242
+ const baseText = params.text?.trim() ?? '';
243
+ const parts = [];
244
+ const title = presentation.title?.trim();
245
+ if (title && !baseText.startsWith(title)) {
246
+ const tone = presentation.tone ? TONE_PREFIX[presentation.tone] : undefined;
247
+ parts.push(tone ? `${tone} **${title}**` : `**${title}**`);
248
+ }
249
+ if (baseText)
250
+ parts.push(baseText);
251
+ const rows = [];
252
+ const dropped = [];
253
+ let buttonCount = 0;
254
+ const pushRows = (candidates) => {
255
+ for (const row of candidates) {
256
+ const room = Math.min(row.length, MAX_KEYBOARD_BUTTONS - buttonCount);
257
+ if (rows.length >= MAX_KEYBOARD_ROWS || room <= 0) {
258
+ dropped.push(...row.map((button) => button.text));
259
+ continue;
260
+ }
261
+ rows.push(row.slice(0, room));
262
+ dropped.push(...row.slice(room).map((button) => button.text));
263
+ buttonCount += room;
264
+ }
265
+ };
266
+ for (const block of presentation.blocks) {
267
+ switch (block.type) {
268
+ case 'text':
269
+ if (block.text.trim())
270
+ parts.push(block.text.trim());
271
+ break;
272
+ case 'context':
273
+ if (block.text.trim())
274
+ parts.push(renderContext(block.text));
275
+ break;
276
+ case 'divider':
277
+ parts.push('———');
278
+ break;
279
+ case 'table':
280
+ case 'chart':
281
+ parts.push(renderDataBlock(block));
282
+ break;
283
+ case 'buttons': {
284
+ const buttons = [];
285
+ for (const control of block.buttons.map(toControl)) {
286
+ if ('button' in control)
287
+ buttons.push(control.button);
288
+ else
289
+ dropped.push(control.dropped);
290
+ }
291
+ pushRows(chunk(buttons, BUTTONS_PER_ROW));
292
+ break;
293
+ }
294
+ case 'select': {
295
+ const options = [];
296
+ for (const control of block.options.map(toOptionControl)) {
297
+ if ('button' in control)
298
+ options.push(control.button);
299
+ else
300
+ dropped.push(control.dropped);
301
+ }
302
+ pushRows(chunk(options, SELECT_OPTIONS_PER_ROW));
303
+ break;
304
+ }
305
+ }
306
+ }
307
+ if (dropped.length > 0) {
308
+ // Label-only fallback, as core renders controls a channel cannot carry.
309
+ const fallback = renderMessagePresentationFallbackText({
310
+ presentation: {
311
+ blocks: [
312
+ { type: 'buttons', buttons: dropped.map((label) => ({ label, value: 'unavailable' })) },
313
+ ],
314
+ },
315
+ });
316
+ if (fallback)
317
+ parts.push(fallback);
318
+ }
319
+ return { text: parts.join('\n\n'), buttons: rows };
320
+ }
321
+ function readChannelDataMax(channelData) {
322
+ if (!channelData || typeof channelData !== 'object' || Array.isArray(channelData))
323
+ return {};
324
+ const max = channelData.max;
325
+ return max && typeof max === 'object' && !Array.isArray(max)
326
+ ? max
327
+ : {};
328
+ }
329
+ /** `renderPresentation` for the outbound adapter: presentation → channelData.max.buttons. */
330
+ export function renderMaxPresentation(payload, presentation) {
331
+ const rendered = renderMaxPresentationParts({ presentation, text: payload.text });
332
+ const existingMax = readChannelDataMax(payload.channelData);
333
+ const existingButtons = Array.isArray(existingMax.buttons)
334
+ ? existingMax.buttons
335
+ : [];
336
+ const buttons = [...existingButtons, ...rendered.buttons];
337
+ const rest = { ...payload };
338
+ delete rest.presentation;
339
+ return {
340
+ ...rest,
341
+ text: rendered.text,
342
+ ...(buttons.length > 0 || payload.channelData
343
+ ? {
344
+ channelData: {
345
+ ...(payload.channelData ?? {}),
346
+ max: { ...existingMax, ...(buttons.length > 0 ? { buttons } : {}) },
347
+ },
348
+ }
349
+ : {}),
350
+ };
351
+ }
352
+ /** Same adaptation + render + fallback policy core applies on its outbound path. */
353
+ export async function materializeMaxPresentation(payload) {
354
+ if (!payload.presentation)
355
+ return payload;
356
+ return renderPresentationForDelivery({
357
+ presentationCapabilities: MAX_PRESENTATION_CAPABILITIES,
358
+ renderPresentation: (adapted) => renderMaxPresentation(adapted, adapted.presentation),
359
+ }, payload);
360
+ }
361
+ /** Normalize `delivery.pin` (true | {enabled, notify, required}) or the tool's `pin: true`. */
362
+ export function readMaxDeliveryPin(delivery, pinFlag) {
363
+ const raw = delivery && typeof delivery === 'object' && !Array.isArray(delivery)
364
+ ? delivery.pin
365
+ : undefined;
366
+ if (raw === true)
367
+ return { enabled: true };
368
+ if (raw && typeof raw === 'object' && raw.enabled) {
369
+ return { enabled: true, notify: raw.notify === true, required: raw.required === true };
370
+ }
371
+ if (pinFlag === true)
372
+ return { enabled: true };
373
+ return undefined;
374
+ }
@@ -0,0 +1,36 @@
1
+ let runtime = null;
2
+ export function setMaxRuntime(next) {
3
+ runtime = next;
4
+ }
5
+ export function getMaxRuntime() {
6
+ if (!runtime) {
7
+ throw new Error('MAX runtime not initialized');
8
+ }
9
+ return runtime;
10
+ }
11
+ /** Read the current config snapshot through the plugin runtime, on any supported OpenClaw version. */
12
+ export async function loadMaxConfig() {
13
+ const config = getMaxRuntime().config;
14
+ if (typeof config.current === 'function') {
15
+ return config.current();
16
+ }
17
+ const legacy = config;
18
+ if (typeof legacy.loadConfig === 'function') {
19
+ return await legacy.loadConfig();
20
+ }
21
+ throw new Error('MAX runtime config API unavailable: neither config.current() nor config.loadConfig()');
22
+ }
23
+ /** Persist a full config replacement through the plugin runtime, on any supported OpenClaw version. */
24
+ export async function writeMaxConfig(nextConfig) {
25
+ const config = getMaxRuntime().config;
26
+ if (typeof config.replaceConfigFile === 'function') {
27
+ await config.replaceConfigFile({ nextConfig, afterWrite: { mode: 'auto' } });
28
+ return;
29
+ }
30
+ const legacy = config;
31
+ if (typeof legacy.writeConfigFile === 'function') {
32
+ await legacy.writeConfigFile(nextConfig);
33
+ return;
34
+ }
35
+ throw new Error('MAX runtime config API unavailable: neither config.replaceConfigFile() nor config.writeConfigFile()');
36
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Russian Trusted Root/Sub CA (Минцифры РФ) — required for TLS to platform-api2.max.ru.
3
+ *
4
+ * Source: https://gu-st.ru/content/lending/russian_trusted_root_ca_pem.crt
5
+ * https://gu-st.ru/content/lending/russian_trusted_sub_ca_pem.crt
6
+ * Root CA valid until 2032-02-27, Sub CA until 2027-03-06.
7
+ * These are used ONLY for MAX API connections (per-dispatcher ca), never installed process-wide.
8
+ */
9
+ export const RUSSIAN_TRUSTED_ROOT_CA = `
10
+ -----BEGIN CERTIFICATE-----
11
+ MIIFwjCCA6qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
12
+ PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
13
+ ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
14
+ Q0EwHhcNMjIwMzAxMjEwNDE1WhcNMzIwMjI3MjEwNDE1WjBwMQswCQYDVQQGEwJS
15
+ VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
16
+ YW5kIENvbW11bmljYXRpb25zMSAwHgYDVQQDDBdSdXNzaWFuIFRydXN0ZWQgUm9v
17
+ dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMfFOZ8pUAL3+r2n
18
+ qqE0Zp52selXsKGFYoG0GM5bwz1bSFtCt+AZQMhkWQheI3poZAToYJu69pHLKS6Q
19
+ XBiwBC1cvzYmUYKMYZC7jE5YhEU2bSL0mX7NaMxMDmH2/NwuOVRj8OImVa5s1F4U
20
+ zn4Kv3PFlDBjjSjXKVY9kmjUBsXQrIHeaqmUIsPIlNWUnimXS0I0abExqkbdrXbX
21
+ YwCOXhOO2pDUx3ckmJlCMUGacUTnylyQW2VsJIyIGA8V0xzdaeUXg0VZ6ZmNUr5Y
22
+ Ber/EAOLPb8NYpsAhJe2mXjMB/J9HNsoFMBFJ0lLOT/+dQvjbdRZoOT8eqJpWnVD
23
+ U+QL/qEZnz57N88OWM3rabJkRNdU/Z7x5SFIM9FrqtN8xewsiBWBI0K6XFuOBOTD
24
+ 4V08o4TzJ8+Ccq5XlCUW2L48pZNCYuBDfBh7FxkB7qDgGDiaftEkZZfApRg2E+M9
25
+ G8wkNKTPLDc4wH0FDTijhgxR3Y4PiS1HL2Zhw7bD3CbslmEGgfnnZojNkJtcLeBH
26
+ BLa52/dSwNU4WWLubaYSiAmA9IUMX1/RpfpxOxd4Ykmhz97oFbUaDJFipIggx5sX
27
+ ePAlkTdWnv+RWBxlJwMQ25oEHmRguNYf4Zr/Rxr9cS93Y+mdXIZaBEE0KS2iLRqa
28
+ OiWBki9IMQU4phqPOBAaG7A+eP8PAgMBAAGjZjBkMB0GA1UdDgQWBBTh0YHlzlpf
29
+ BKrS6badZrHF+qwshzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzAS
30
+ BgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF
31
+ AAOCAgEAALIY1wkilt/urfEVM5vKzr6utOeDWCUczmWX/RX4ljpRdgF+5fAIS4vH
32
+ tmXkqpSCOVeWUrJV9QvZn6L227ZwuE15cWi8DCDal3Ue90WgAJJZMfTshN4OI8cq
33
+ W9E4EG9wglbEtMnObHlms8F3CHmrw3k6KmUkWGoa+/ENmcVl68u/cMRl1JbW2bM+
34
+ /3A+SAg2c6iPDlehczKx2oa95QW0SkPPWGuNA/CE8CpyANIhu9XFrj3RQ3EqeRcS
35
+ AQQod1RNuHpfETLU/A2gMmvn/w/sx7TB3W5BPs6rprOA37tutPq9u6FTZOcG1Oqj
36
+ C/B7yTqgI7rbyvox7DEXoX7rIiEqyNNUguTk/u3SZ4VXE2kmxdmSh3TQvybfbnXV
37
+ 4JbCZVaqiZraqc7oZMnRoWrXRG3ztbnbes/9qhRGI7PqXqeKJBztxRTEVj8ONs1d
38
+ WN5szTwaPIvhkhO3CO5ErU2rVdUr89wKpNXbBODFKRtgxUT70YpmJ46VVaqdAhOZ
39
+ D9EUUn4YaeLaS8AjSF/h7UkjOibNc4qVDiPP+rkehFWM66PVnP1Msh93tc+taIfC
40
+ EYVMxjh8zNbFuoc7fzvvrFILLe7ifvEIUqSVIC/AzplM/Jxw7buXFeGP1qVCBEHq
41
+ 391d/9RAfaZ12zkwFsl+IKwE/OZxW8AHa9i1p4GO0YSNuczzEm4=
42
+ -----END CERTIFICATE-----`;
43
+ export const RUSSIAN_TRUSTED_SUB_CA = `
44
+ -----BEGIN CERTIFICATE-----
45
+ MIIHQjCCBSqgAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
46
+ PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
47
+ ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
48
+ Q0EwHhcNMjIwMzAyMTEyNTE5WhcNMjcwMzA2MTEyNTE5WjBvMQswCQYDVQQGEwJS
49
+ VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
50
+ YW5kIENvbW11bmljYXRpb25zMR8wHQYDVQQDDBZSdXNzaWFuIFRydXN0ZWQgU3Vi
51
+ IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9YPqBKOk19NFymrE
52
+ wehzrhBEgT2atLezpduB24mQ7CiOa/HVpFCDRZzdxqlh8drku408/tTmWzlNH/br
53
+ HuQhZ/miWKOf35lpKzjyBd6TPM23uAfJvEOQ2/dnKGGJbsUo1/udKSvxQwVHpVv3
54
+ S80OlluKfhWPDEXQpgyFqIzPoxIQTLZ0deirZwMVHarZ5u8HqHetRuAtmO2ZDGQn
55
+ vVOJYAjls+Hiueq7Lj7Oce7CQsTwVZeP+XQx28PAaEZ3y6sQEt6rL06ddpSdoTMp
56
+ BnCqTbxW+eWMyjkIn6t9GBtUV45yB1EkHNnj2Ex4GwCiN9T84QQjKSr+8f0psGrZ
57
+ vPbCbQAwNFJjisLixnjlGPLKa5vOmNwIh/LAyUW5DjpkCx004LPDuqPpFsKXNKpa
58
+ L2Dm6uc0x4Jo5m+gUTVORB6hOSzWnWDj2GWfomLzzyjG81DRGFBpco/O93zecsIN
59
+ 3SL2Ysjpq1zdoS01CMYxie//9zWvYwzI25/OZigtnpCIrcd2j1Y6dMUFQAzAtHE+
60
+ qsXflSL8HIS+IJEFIQobLlYhHkoE3avgNx5jlu+OLYe0dF0Ykx1PGNjbwqvTX37R
61
+ Cn32NMjlotW2QcGEZhDKj+3urZizp5xdTPZitA+aEjZM/Ni71VOdiOP0igbw6asZ
62
+ 2fxdozZ1TnSSYNYvNATwthNmZysCAwEAAaOCAeUwggHhMBIGA1UdEwEB/wQIMAYB
63
+ Af8CAQAwDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTR4XENCy2BTm6KSo9MI7NM
64
+ XqtpCzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzCBxwYIKwYBBQUH
65
+ AQEEgbowgbcwOwYIKwYBBQUHMAKGL2h0dHA6Ly9yb3N0ZWxlY29tLnJ1L2NkcC9y
66
+ b290Y2Ffc3NsX3JzYTIwMjIuY3J0MDsGCCsGAQUFBzAChi9odHRwOi8vY29tcGFu
67
+ eS5ydC5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNydDA7BggrBgEFBQcwAoYv
68
+ aHR0cDovL3JlZXN0ci1wa2kucnUvY2RwL3Jvb3RjYV9zc2xfcnNhMjAyMi5jcnQw
69
+ gbAGA1UdHwSBqDCBpTA1oDOgMYYvaHR0cDovL3Jvc3RlbGVjb20ucnUvY2RwL3Jv
70
+ b3RjYV9zc2xfcnNhMjAyMi5jcmwwNaAzoDGGL2h0dHA6Ly9jb21wYW55LnJ0LnJ1
71
+ L2NkcC9yb290Y2Ffc3NsX3JzYTIwMjIuY3JsMDWgM6Axhi9odHRwOi8vcmVlc3Ry
72
+ LXBraS5ydS9jZHAvcm9vdGNhX3NzbF9yc2EyMDIyLmNybDANBgkqhkiG9w0BAQsF
73
+ AAOCAgEARBVzZls79AdiSCpar15dA5Hr/rrT4WbrOfzlpI+xrLeRPrUG6eUWIW4v
74
+ Sui1yx3iqGLCjPcKb+HOTwoRMbI6ytP/ndp3TlYua2advYBEhSvjs+4vDZNwXr/D
75
+ anbwIWdurZmViQRBDFebpkvnIvru/RpWud/5r624Wp8voZMRtj/cm6aI9LtvBfT9
76
+ cfzhOaexI/99c14dyiuk1+6QhdwKaCRTc1mdfNQmnfWNRbfWhWBlK3h4GGE9JK33
77
+ Gk8ZS8DMrkdAh0xby4xAQ/mSWAfWrBmfzlOqGyoB1U47WTOeqNbWkkoAP2ys94+s
78
+ Jg4NTkiDVtXRF6nr6fYi0bSOvOFg0IQrMXO2Y8gyg9ARdPJwKtvWX8VPADCYMiWH
79
+ h4n8bZokIrImVKLDQKHY4jCsND2HHdJfnrdL2YJw1qFskNO4cSNmZydw0Wkgjv9k
80
+ F+KxqrDKlB8MZu2Hclph6v/CZ0fQ9YuE8/lsHZ0Qc2HyiSMnvjgK5fDc3TD4fa8F
81
+ E8gMNurM+kV8PT8LNIM+4Zs+LKEV8nqRWBaxkIVJGekkVKO8xDBOG/aN62AZKHOe
82
+ GcyIdu7yNMMRihGVZCYr8rYiJoKiOzDqOkPkLOPdhtVlgnhowzHDxMHND/E2WA5p
83
+ ZHuNM/m0TXt2wTTPL7JH2YC0gPz/BvvSzjksgzU5rLbRyUKQkgU=
84
+ -----END CERTIFICATE-----`;