enigmatic 0.34.0 → 0.36.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.
@@ -0,0 +1,414 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Enigmatic - Lightweight JavaScript Library</title>
7
+ <style>
8
+ :root {
9
+ --bg: #f8fafc;
10
+ --surface: #ffffff;
11
+ --text: #0f172a;
12
+ --text-muted: #64748b;
13
+ --accent: #6366f1;
14
+ --accent-hover: #4f46e5;
15
+ --border: #e2e8f0;
16
+ --code-bg: #1e293b;
17
+ --code-text: #e2e8f0;
18
+ }
19
+ * { box-sizing: border-box; margin: 0; padding: 0; }
20
+ body {
21
+ font-family: "Inter", system-ui, -apple-system, sans-serif;
22
+ font-size: 16px;
23
+ line-height: 1.6;
24
+ background: var(--bg);
25
+ color: var(--text);
26
+ -webkit-font-smoothing: antialiased;
27
+ }
28
+ .container {
29
+ max-width: 1200px;
30
+ margin: 0 auto;
31
+ padding: 0 24px;
32
+ }
33
+ header {
34
+ background: var(--surface);
35
+ border-bottom: 1px solid var(--border);
36
+ padding: 24px 0;
37
+ margin-bottom: 48px;
38
+ }
39
+ h1 {
40
+ font-size: 2.5rem;
41
+ font-weight: 700;
42
+ letter-spacing: -0.02em;
43
+ margin-bottom: 12px;
44
+ background: linear-gradient(135deg, var(--accent), #8b5cf6);
45
+ -webkit-background-clip: text;
46
+ -webkit-text-fill-color: transparent;
47
+ background-clip: text;
48
+ }
49
+ .tagline {
50
+ font-size: 1.25rem;
51
+ color: var(--text-muted);
52
+ margin-bottom: 32px;
53
+ }
54
+ .badge {
55
+ display: inline-block;
56
+ padding: 4px 12px;
57
+ background: var(--accent);
58
+ color: white;
59
+ border-radius: 6px;
60
+ font-size: 0.875rem;
61
+ font-weight: 500;
62
+ margin-bottom: 24px;
63
+ }
64
+ section {
65
+ background: var(--surface);
66
+ border-radius: 12px;
67
+ padding: 32px;
68
+ margin-bottom: 32px;
69
+ box-shadow: 0 1px 3px rgba(0,0,0,.06);
70
+ border: 1px solid var(--border);
71
+ }
72
+ h2 {
73
+ font-size: 1.5rem;
74
+ font-weight: 600;
75
+ margin-bottom: 16px;
76
+ color: var(--text);
77
+ }
78
+ h3 {
79
+ font-size: 1.125rem;
80
+ font-weight: 600;
81
+ margin-top: 24px;
82
+ margin-bottom: 12px;
83
+ color: var(--text);
84
+ }
85
+ p {
86
+ margin-bottom: 16px;
87
+ color: var(--text-muted);
88
+ }
89
+ ul, ol {
90
+ margin-left: 24px;
91
+ margin-bottom: 16px;
92
+ color: var(--text-muted);
93
+ }
94
+ li {
95
+ margin-bottom: 8px;
96
+ }
97
+ code {
98
+ background: var(--code-bg);
99
+ color: var(--code-text);
100
+ padding: 2px 6px;
101
+ border-radius: 4px;
102
+ font-family: ui-monospace, monospace;
103
+ font-size: 0.9em;
104
+ }
105
+ pre {
106
+ background: var(--code-bg);
107
+ color: var(--code-text);
108
+ padding: 20px;
109
+ border-radius: 8px;
110
+ overflow-x: auto;
111
+ margin: 16px 0;
112
+ font-family: ui-monospace, monospace;
113
+ font-size: 0.875rem;
114
+ line-height: 1.6;
115
+ }
116
+ pre code {
117
+ background: none;
118
+ padding: 0;
119
+ }
120
+ .btn {
121
+ display: inline-block;
122
+ padding: 12px 24px;
123
+ background: var(--accent);
124
+ color: white;
125
+ text-decoration: none;
126
+ border-radius: 8px;
127
+ font-weight: 500;
128
+ transition: background 0.15s;
129
+ margin-right: 12px;
130
+ margin-bottom: 12px;
131
+ }
132
+ .btn:hover {
133
+ background: var(--accent-hover);
134
+ }
135
+ .btn-secondary {
136
+ background: var(--border);
137
+ color: var(--text);
138
+ }
139
+ .btn-secondary:hover {
140
+ background: #cbd5e1;
141
+ }
142
+ .features {
143
+ display: grid;
144
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
145
+ gap: 24px;
146
+ margin-top: 24px;
147
+ }
148
+ .feature {
149
+ padding: 20px;
150
+ background: var(--bg);
151
+ border-radius: 8px;
152
+ border: 1px solid var(--border);
153
+ }
154
+ .feature h4 {
155
+ font-size: 1rem;
156
+ font-weight: 600;
157
+ margin-bottom: 8px;
158
+ color: var(--text);
159
+ }
160
+ .feature p {
161
+ font-size: 0.9375rem;
162
+ margin: 0;
163
+ }
164
+ table {
165
+ width: 100%;
166
+ border-collapse: collapse;
167
+ margin: 16px 0;
168
+ }
169
+ th, td {
170
+ padding: 12px;
171
+ text-align: left;
172
+ border-bottom: 1px solid var(--border);
173
+ }
174
+ th {
175
+ font-weight: 600;
176
+ color: var(--text);
177
+ background: var(--bg);
178
+ }
179
+ td {
180
+ color: var(--text-muted);
181
+ font-size: 0.9375rem;
182
+ }
183
+ .links {
184
+ display: flex;
185
+ gap: 16px;
186
+ flex-wrap: wrap;
187
+ margin-top: 24px;
188
+ }
189
+ footer {
190
+ text-align: center;
191
+ padding: 48px 0;
192
+ color: var(--text-muted);
193
+ font-size: 0.875rem;
194
+ }
195
+ </style>
196
+ <link rel="preconnect" href="https://fonts.googleapis.com">
197
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
198
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
199
+ </head>
200
+ <body>
201
+ <header>
202
+ <div class="container">
203
+ <span class="badge">v0.35.0</span>
204
+ <h1>Enigmatic</h1>
205
+ <p class="tagline">A lightweight client-side JavaScript library for DOM manipulation, reactive state management, and API interactions, with an optional Bun server for backend functionality.</p>
206
+ <div class="links">
207
+ <a href="api.html" class="btn">Try Demo</a>
208
+ <a href="https://www.npmjs.com/package/enigmatic" class="btn btn-secondary" target="_blank">View on npm</a>
209
+ </div>
210
+ </div>
211
+ </header>
212
+
213
+ <div class="container">
214
+ <section>
215
+ <h2>Quick Start</h2>
216
+ <h3>Using client.js via CDN</h3>
217
+ <p>Include <code>client.js</code> in any HTML file using the unpkg CDN:</p>
218
+ <pre><code>&lt;script src="https://unpkg.com/enigmatic"&gt;&lt;/script&gt;
219
+ &lt;script src="https://unpkg.com/enigmatic/client/public/custom.js"&gt;&lt;/script&gt;
220
+ &lt;script&gt;
221
+ window.api_url = 'https://your-server.com';
222
+ window.state.message = 'Hello World';
223
+ &lt;/script&gt;</code></pre>
224
+
225
+ <h3>Using the Bun Server</h3>
226
+ <p>The Bun server provides a complete backend with:</p>
227
+ <ul>
228
+ <li><strong>Key-value storage</strong> – Per-user KV persisted as append-only JSONL</li>
229
+ <li><strong>File storage</strong> – Per-user files via Cloudflare R2 (or S3-compatible API)</li>
230
+ <li><strong>Authentication</strong> – Auth0 OAuth2 login/logout</li>
231
+ <li><strong>Static files</strong> – Served from <code>client/public/</code></li>
232
+ </ul>
233
+
234
+ <h3>Installation</h3>
235
+ <ol>
236
+ <li>Install <a href="https://bun.sh" target="_blank">Bun</a>:
237
+ <pre><code>curl -fsSL https://bun.sh/install | bash</code></pre>
238
+ </li>
239
+ <li>Install dependencies:
240
+ <pre><code>bun install</code></pre>
241
+ </li>
242
+ <li>TLS certificates: place <code>cert.pem</code> and <code>key.pem</code> in <code>server/certs/</code> for HTTPS</li>
243
+ </ol>
244
+
245
+ <h3>Running the Server</h3>
246
+ <pre><code>npm start
247
+ # or
248
+ npx enigmatic
249
+ # or with hot reload
250
+ npm run hot</code></pre>
251
+ <p>Server runs at <strong>https://localhost:3000</strong> (HTTPS is required for Auth0 cookies).</p>
252
+ </section>
253
+
254
+ <section>
255
+ <h2>Features</h2>
256
+ <div class="features">
257
+ <div class="feature">
258
+ <h4>DOM Utilities</h4>
259
+ <p>Simple selectors: <code>window.$</code>, <code>window.$$</code>, <code>window.$c</code></p>
260
+ </div>
261
+ <div class="feature">
262
+ <h4>Reactive State</h4>
263
+ <p>Proxy-based state management that automatically updates DOM elements</p>
264
+ </div>
265
+ <div class="feature">
266
+ <h4>Custom Elements</h4>
267
+ <p>Automatic initialization and reactive updates for custom HTML elements</p>
268
+ </div>
269
+ <div class="feature">
270
+ <h4>KV Storage</h4>
271
+ <p>Simple key-value operations: <code>get</code>, <code>set</code>, <code>delete</code></p>
272
+ </div>
273
+ <div class="feature">
274
+ <h4>File Storage</h4>
275
+ <p>Upload, download, list, and delete files via R2/S3-compatible storage</p>
276
+ </div>
277
+ <div class="feature">
278
+ <h4>Authentication</h4>
279
+ <p>Built-in Auth0 OAuth2 integration with login/logout</p>
280
+ </div>
281
+ </div>
282
+ </section>
283
+
284
+ <section>
285
+ <h2>API Endpoints</h2>
286
+ <table>
287
+ <thead>
288
+ <tr>
289
+ <th>Method</th>
290
+ <th>Path</th>
291
+ <th>Description</th>
292
+ </tr>
293
+ </thead>
294
+ <tbody>
295
+ <tr>
296
+ <td>GET</td>
297
+ <td><code>/</code></td>
298
+ <td>Serves index.html</td>
299
+ </tr>
300
+ <tr>
301
+ <td>GET</td>
302
+ <td><code>/login</code></td>
303
+ <td>Redirects to Auth0 login</td>
304
+ </tr>
305
+ <tr>
306
+ <td>GET</td>
307
+ <td><code>/callback</code></td>
308
+ <td>Auth0 OAuth callback</td>
309
+ </tr>
310
+ <tr>
311
+ <td>GET</td>
312
+ <td><code>/logout</code></td>
313
+ <td>Logs out and clears session</td>
314
+ </tr>
315
+ <tr>
316
+ <td>GET</td>
317
+ <td><code>/me</code></td>
318
+ <td>Current user or 401 (no auth)</td>
319
+ </tr>
320
+ <tr>
321
+ <td>GET</td>
322
+ <td><code>/{key}</code></td>
323
+ <td>KV get (auth required)</td>
324
+ </tr>
325
+ <tr>
326
+ <td>POST</td>
327
+ <td><code>/{key}</code></td>
328
+ <td>KV set (auth required)</td>
329
+ </tr>
330
+ <tr>
331
+ <td>DELETE</td>
332
+ <td><code>/{key}</code></td>
333
+ <td>KV delete (auth required)</td>
334
+ </tr>
335
+ <tr>
336
+ <td>PUT</td>
337
+ <td><code>/{key}</code></td>
338
+ <td>Upload file to R2 (auth required)</td>
339
+ </tr>
340
+ <tr>
341
+ <td>PURGE</td>
342
+ <td><code>/{key}</code></td>
343
+ <td>Delete file from R2 (auth required)</td>
344
+ </tr>
345
+ <tr>
346
+ <td>PROPFIND</td>
347
+ <td><code>/</code></td>
348
+ <td>List R2 files (auth required)</td>
349
+ </tr>
350
+ <tr>
351
+ <td>PATCH</td>
352
+ <td><code>/{key}</code></td>
353
+ <td>Download file from R2 (auth required)</td>
354
+ </tr>
355
+ </tbody>
356
+ </table>
357
+ </section>
358
+
359
+ <section>
360
+ <h2>Example Usage</h2>
361
+ <h3>Reactive State</h3>
362
+ <pre><code>&lt;hello-world data="message"&gt;&lt;/hello-world&gt;
363
+ &lt;script&gt;
364
+ window.custom['hello-world'] = (data) => `Hello ${data}`;
365
+ window.state.message = "World"; // Automatically updates the element
366
+ &lt;/script&gt;</code></pre>
367
+
368
+ <h3>KV Storage</h3>
369
+ <pre><code>// Get value
370
+ const value = await window.get('my-key');
371
+
372
+ // Set value
373
+ await window.set('my-key', 'my-value');
374
+
375
+ // Delete key
376
+ await window.delete('my-key');</code></pre>
377
+
378
+ <h3>File Operations</h3>
379
+ <pre><code>// Upload file
380
+ await window.put('filename.txt', fileBlob);
381
+
382
+ // List files
383
+ const files = await window.list();
384
+
385
+ // Download file
386
+ await window.download('filename.txt');
387
+
388
+ // Delete file
389
+ await window.purge('filename.txt');</code></pre>
390
+ </section>
391
+
392
+ <section>
393
+ <h2>Environment Variables</h2>
394
+ <p>Create a <code>.env</code> file in the project root:</p>
395
+ <pre><code># Auth0
396
+ AUTH0_DOMAIN=your-tenant.auth0.com
397
+ AUTH0_CLIENT_ID=your-client-id
398
+ AUTH0_CLIENT_SECRET=your-client-secret
399
+
400
+ # Cloudflare R2 (optional, for file storage)
401
+ CLOUDFLARE_ACCESS_KEY_ID=your-access-key-id
402
+ CLOUDFLARE_SECRET_ACCESS_KEY=your-secret-access-key
403
+ CLOUDFLARE_BUCKET_NAME=your-bucket-name
404
+ CLOUDFLARE_PUBLIC_URL=https://your-account-id.r2.cloudflarestorage.com</code></pre>
405
+ </section>
406
+ </div>
407
+
408
+ <footer>
409
+ <div class="container">
410
+ <p>MIT License • Built with Bun</p>
411
+ </div>
412
+ </footer>
413
+ </body>
414
+ </html>
Binary file
package/package.json CHANGED
@@ -1,14 +1,12 @@
1
1
  {
2
2
  "name": "enigmatic",
3
- "version": "0.34.0",
4
- "unpkg": "./public/client.js",
5
- "scripts": {
6
- "start": "bun --hot ./bun-server.js",
7
- "test": "jest --config __tests__/jest.config.js"
3
+ "version": "0.36.0",
4
+ "bin": {
5
+ "enigmatic": "./bin/enigmatic.js"
8
6
  },
9
- "devDependencies": {
10
- "jest": "^29.0.0",
11
- "jest-environment-jsdom": "^29.0.0",
12
- "whatwg-fetch": "^3.6.20"
7
+ "unpkg": "./client/public/client.js",
8
+ "scripts": {
9
+ "start": "bun run bin/enigmatic.js",
10
+ "hot": "bun --hot bin/enigmatic.js"
13
11
  }
14
12
  }
@@ -0,0 +1,132 @@
1
+ import { S3Client } from "bun";
2
+ import { join } from "path";
3
+ import { appendFile, mkdir, readFile } from "fs/promises";
4
+
5
+ const dir = import.meta.dir;
6
+ const certsDir = join(dir, "certs");
7
+ const publicDir = join(dir, "..", "client", "public");
8
+ const kvDir = join(dir, "kv");
9
+ const sessions = new Map();
10
+ const userKv = {};
11
+ let site_origin = "";
12
+
13
+ const kvPath = (sub) => join(kvDir, `${String(sub).replace(/[^a-zA-Z0-9_-]/g, "_")}.jsonl`);
14
+ const json = (d, s = 200, h = {}, origin = null) => new Response(JSON.stringify(d), { status: s, headers: { "Access-Control-Allow-Origin": origin || "*", "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PURGE, PROPFIND, PATCH, OPTIONS", "Access-Control-Allow-Headers": "Content-Type, Authorization, Cookie", "Access-Control-Allow-Credentials": "true", "Content-Type": "application/json", ...h } });
15
+ const redir = (url, cookie) => new Response(null, { status: 302, headers: { Location: url, ...(cookie && { "Set-Cookie": cookie }) } });
16
+
17
+ async function getUserMap(sub) {
18
+ if (userKv[sub]) return userKv[sub];
19
+ const m = new Map();
20
+ try {
21
+ let buf = await readFile(kvPath(sub), "utf8");
22
+ if (buf.charCodeAt(0) === 0xfeff) buf = buf.slice(1);
23
+ const lines = buf.trim().split("\n").filter(Boolean);
24
+ for (const line of lines) {
25
+ try {
26
+ const row = JSON.parse(line);
27
+ if (Array.isArray(row) && row.length >= 2) {
28
+ m.set(row[0], row[1]);
29
+ } else if (row?.action === "update" && row.key !== undefined) {
30
+ m.set(row.key, row.value);
31
+ } else if (row?.action === "delete" && row.key !== undefined) {
32
+ m.delete(row.key);
33
+ }
34
+ } catch (_) { /* skip malformed line */ }
35
+ }
36
+ } catch (_) { /* file missing or unreadable */ }
37
+ userKv[sub] = m;
38
+ return m;
39
+ }
40
+
41
+ async function appendKvLog(sub, action, key, value) {
42
+ await mkdir(kvDir, { recursive: true });
43
+ const ts = new Date().toISOString();
44
+ const row = action === "update" ? { action, key, value, timestamp: ts } : { action, key, timestamp: ts };
45
+ await appendFile(kvPath(sub), JSON.stringify(row) + "\n");
46
+ }
47
+
48
+ async function saveUserKv(sub, action, key, value) {
49
+ await appendKvLog(sub, action, key, value);
50
+ }
51
+
52
+ const s3 = new S3Client({
53
+ accessKeyId: Bun.env.CLOUDFLARE_ACCESS_KEY_ID,
54
+ secretAccessKey: Bun.env.CLOUDFLARE_SECRET_ACCESS_KEY,
55
+ bucket: Bun.env.CLOUDFLARE_BUCKET_NAME,
56
+ endpoint: Bun.env.CLOUDFLARE_PUBLIC_URL
57
+ });
58
+
59
+ export default {
60
+ async fetch(req) {
61
+ const url = new URL(req.url), key = url.pathname.slice(1), cb = `${url.origin}/callback`;
62
+ const origin = req.headers.get("Origin") || url.origin;
63
+ const token = req.headers.get("Cookie")?.match(/token=([^;]+)/)?.[1];
64
+ const user = (Bun.env.TEST_MODE === "1" && token === Bun.env.TEST_SESSION_ID) ? { sub: "test-user" } : (token ? sessions.get(token) : null);
65
+
66
+ if (req.method === "OPTIONS") return json(null, 204, {}, origin);
67
+
68
+ if (req.method === "GET") {
69
+ const p = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
70
+ if (p === "index.html" || /\.[a-z0-9]+$/i.test(p)) {
71
+ const f = Bun.file(join(publicDir, p));
72
+ if (await f.exists()) return new Response(f);
73
+ }
74
+ }
75
+
76
+ if (url.pathname === "/login") {
77
+ site_origin = req.headers.get("referer");
78
+ return Response.redirect(`https://${Bun.env.AUTH0_DOMAIN}/authorize?${new URLSearchParams({ response_type: "code", client_id: Bun.env.AUTH0_CLIENT_ID, redirect_uri: cb, scope: "openid email profile" })}`);
79
+ }
80
+
81
+ if (url.pathname === "/callback") {
82
+ const code = url.searchParams.get("code");
83
+ if (!code) return json({ error: "No code" }, 400, {}, origin);
84
+ const tRes = await fetch(`https://${Bun.env.AUTH0_DOMAIN}/oauth/token`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ grant_type: "authorization_code", client_id: Bun.env.AUTH0_CLIENT_ID, client_secret: Bun.env.AUTH0_CLIENT_SECRET, code, redirect_uri: cb }) });
85
+ if (!tRes.ok) return json({ error: "Auth error" }, 401, {}, origin);
86
+ const tokens = await tRes.json();
87
+ const userInfo = await (await fetch(`https://${Bun.env.AUTH0_DOMAIN}/userinfo`, { headers: { Authorization: `Bearer ${tokens.access_token}` } })).json();
88
+ const sid = crypto.randomUUID();
89
+ sessions.set(sid, { ...userInfo, login_time: new Date().toISOString(), access_token_expires_at: tokens.expires_in ? new Date(Date.now() + tokens.expires_in * 1000).toISOString() : null });
90
+ return redir(site_origin || url.origin, `token=${sid}; HttpOnly; Path=/; Secure; SameSite=None; Max-Age=86400`);
91
+ }
92
+
93
+ if (url.pathname === "/me") return user ? json(user, 200, {}, origin) : json({ error: "Unauthorized" }, 401, {}, origin);
94
+ if (!token || !user) return json({ error: "Unauthorized" }, 401, {}, origin);
95
+ if (url.pathname === "/logout") { sessions.delete(token); return redir(url.origin, "token=; Max-Age=0; Path=/; Secure; SameSite=None"); }
96
+
97
+ const m = await getUserMap(user.sub);
98
+ switch (req.method) {
99
+ case "GET": return json(m.get(key) ?? null, 200, {}, origin);
100
+ case "POST":
101
+ const val = await req.text();
102
+ const v = (() => { try { return JSON.parse(val); } catch { return val; } })();
103
+ m.set(key, v);
104
+ await saveUserKv(user.sub, "update", key, v);
105
+ return json({ key, value: v }, 200, {}, origin);
106
+ case "DELETE": m.delete(key); await saveUserKv(user.sub, "delete", key); return json({ status: "Deleted" }, 200, {}, origin);
107
+ case "PUT": await s3.write(`${user.sub}/${key}`, req.body); return json({ status: "Saved to R2" }, 200, {}, origin);
108
+ case "PURGE": await s3.delete(`${user.sub}/${key}`); return json({ status: "Deleted from R2" }, 200, {}, origin);
109
+ case "PROPFIND":
110
+ const list = await s3.list({ prefix: `${user.sub}/` });
111
+ const items = Array.isArray(list) ? list : (list?.contents || []);
112
+ return json(items.map((i) => ({ name: i.key?.split("/").pop() || i.name || i.Key, lastModified: i.lastModified || i.LastModified, size: i.size || i.Size || 0 })), 200, {}, origin);
113
+ case "PATCH":
114
+ try {
115
+ if (!(await s3.exists(`${user.sub}/${key}`))) {
116
+ const headers = { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true" };
117
+ return new Response(JSON.stringify({ error: "File not found" }), { status: 404, headers: { ...headers, "Content-Type": "application/json" } });
118
+ }
119
+ const f = await s3.file(`${user.sub}/${key}`);
120
+ if (f) {
121
+ const headers = { ...f.headers, "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true" };
122
+ return new Response(f.stream(), { headers });
123
+ }
124
+ const headers = { "Access-Control-Allow-Origin": origin, "Access-Control-Allow-Credentials": "true" };
125
+ return new Response(JSON.stringify({ error: "File not found" }), { status: 404, headers: { ...headers, "Content-Type": "application/json" } });
126
+ } catch (e) { return json({ error: "File not found", details: e.message }, 404, {}, origin); }
127
+ default: return json({ error: "Method not allowed" }, 405, {}, origin);
128
+ }
129
+ },
130
+ port: 3000,
131
+ tls: { cert: Bun.file(join(certsDir, "cert.pem")), key: Bun.file(join(certsDir, "key.pem")) }
132
+ };