@fitzzero/quickdraw-core 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.
@@ -0,0 +1,707 @@
1
+ import express from 'express';
2
+ import { createServer } from 'http';
3
+ import { Server } from 'socket.io';
4
+ import * as jose from 'jose';
5
+
6
+ // src/shared/types.ts
7
+ var consoleLogger = {
8
+ info: (message, meta) => console.log(`[INFO] ${message}`, meta ?? ""),
9
+ warn: (message, meta) => console.warn(`[WARN] ${message}`, meta ?? ""),
10
+ error: (message, meta) => console.error(`[ERROR] ${message}`, meta ?? ""),
11
+ debug: (message, meta) => console.debug(`[DEBUG] ${message}`, meta ?? ""),
12
+ child: () => consoleLogger
13
+ };
14
+
15
+ // src/server/BaseService.ts
16
+ var BaseService = class {
17
+ serviceName;
18
+ hasEntryACL;
19
+ defaultACL;
20
+ logger;
21
+ // Subscription tracking: entryId -> Set of sockets
22
+ subscribers = /* @__PURE__ */ new Map();
23
+ // Prisma delegate for DB operations
24
+ delegate;
25
+ // Collection of public methods for registry discovery
26
+ publicMethods = /* @__PURE__ */ new Map();
27
+ constructor(options) {
28
+ this.serviceName = options.serviceName;
29
+ this.hasEntryACL = options.hasEntryACL ?? false;
30
+ this.defaultACL = options.defaultACL ?? [];
31
+ this.logger = options.logger?.child({ service: this.serviceName }) ?? consoleLogger.child({ service: this.serviceName });
32
+ }
33
+ /**
34
+ * Set the Prisma delegate for this service.
35
+ * Must be called in the constructor of derived classes.
36
+ */
37
+ setDelegate(delegate) {
38
+ this.delegate = delegate;
39
+ }
40
+ /**
41
+ * Get the delegate, throwing if not set.
42
+ */
43
+ getDelegate() {
44
+ if (!this.delegate) {
45
+ throw new Error(
46
+ `Delegate not set for service ${this.serviceName}. Call setDelegate() in constructor.`
47
+ );
48
+ }
49
+ return this.delegate;
50
+ }
51
+ // ===========================================================================
52
+ // Subscription Management
53
+ // ===========================================================================
54
+ /**
55
+ * Subscribe a socket to an entity's updates.
56
+ * Returns the current entity data if access is granted, null otherwise.
57
+ */
58
+ async subscribe(entryId, socket, requiredLevel = "Read") {
59
+ if (!socket.userId) {
60
+ return null;
61
+ }
62
+ const allowed = await this.checkSubscriptionAccess(
63
+ socket.userId,
64
+ entryId,
65
+ requiredLevel,
66
+ socket
67
+ );
68
+ if (!allowed) {
69
+ return null;
70
+ }
71
+ if (!this.subscribers.has(entryId)) {
72
+ this.subscribers.set(entryId, /* @__PURE__ */ new Set());
73
+ }
74
+ this.subscribers.get(entryId).add(socket);
75
+ this.logger.debug(`User ${socket.userId} subscribed to ${entryId}`);
76
+ const entity = await this.findById(entryId);
77
+ return entity;
78
+ }
79
+ /**
80
+ * Unsubscribe a socket from an entity's updates.
81
+ */
82
+ unsubscribe(entryId, socket) {
83
+ const subs = this.subscribers.get(entryId);
84
+ if (subs) {
85
+ subs.delete(socket);
86
+ if (subs.size === 0) {
87
+ this.subscribers.delete(entryId);
88
+ }
89
+ }
90
+ }
91
+ /**
92
+ * Remove a socket from all subscriptions (called on disconnect).
93
+ */
94
+ unsubscribeSocket(socket) {
95
+ for (const [entryId, sockets] of this.subscribers.entries()) {
96
+ if (sockets.has(socket)) {
97
+ sockets.delete(socket);
98
+ if (sockets.size === 0) {
99
+ this.subscribers.delete(entryId);
100
+ }
101
+ }
102
+ }
103
+ }
104
+ /**
105
+ * Emit an update to all subscribers of an entity.
106
+ */
107
+ emitUpdate(entryId, data) {
108
+ const eventName = `${this.serviceName}:update:${entryId}`;
109
+ const subs = this.subscribers.get(entryId);
110
+ if (subs) {
111
+ for (const socket of subs) {
112
+ socket.emit(eventName, data);
113
+ }
114
+ }
115
+ }
116
+ // ===========================================================================
117
+ // Access Control
118
+ // ===========================================================================
119
+ /**
120
+ * Check if a user has access to subscribe to an entity.
121
+ * Override in derived classes for custom logic.
122
+ */
123
+ async checkSubscriptionAccess(userId, entryId, requiredLevel, socket) {
124
+ if (this.hasServiceAccess(socket, requiredLevel)) {
125
+ return true;
126
+ }
127
+ if (this.checkAccess(userId, entryId, requiredLevel, socket)) {
128
+ return true;
129
+ }
130
+ if (this.hasEntryACL) {
131
+ return await this.checkEntryACL(userId, entryId, requiredLevel);
132
+ }
133
+ return false;
134
+ }
135
+ /**
136
+ * Check if a socket has service-level access.
137
+ */
138
+ hasServiceAccess(socket, requiredLevel) {
139
+ const userLevel = socket.serviceAccess?.[this.serviceName];
140
+ if (!userLevel) return false;
141
+ return this.isLevelSufficient(userLevel, requiredLevel);
142
+ }
143
+ /**
144
+ * Check if a user has access to an entity.
145
+ * Override in derived classes for custom logic (e.g., self-access).
146
+ */
147
+ checkAccess(_userId, _entryId, _requiredLevel, _socket) {
148
+ return false;
149
+ }
150
+ /**
151
+ * Check entry-level ACL stored on the entity.
152
+ */
153
+ async checkEntryACL(userId, entryId, requiredLevel) {
154
+ try {
155
+ const entity = await this.getDelegate().findUnique({
156
+ where: { id: entryId },
157
+ select: { acl: true }
158
+ });
159
+ const acl = entity?.acl;
160
+ if (!acl || !Array.isArray(acl)) return false;
161
+ const ace = acl.find((a) => a.userId === userId);
162
+ if (!ace) return false;
163
+ return this.isLevelSufficient(ace.level, requiredLevel);
164
+ } catch {
165
+ return false;
166
+ }
167
+ }
168
+ /**
169
+ * Compare access levels.
170
+ */
171
+ isLevelSufficient(userLevel, requiredLevel) {
172
+ const order = {
173
+ Public: 0,
174
+ Read: 1,
175
+ Moderate: 2,
176
+ Admin: 3
177
+ };
178
+ return (order[userLevel] ?? 0) >= (order[requiredLevel] ?? 0);
179
+ }
180
+ /**
181
+ * Ensure access for a public method.
182
+ * Throws if access is denied.
183
+ */
184
+ async ensureAccessForMethod(requiredLevel, socket, entryId) {
185
+ if (requiredLevel === "Public") {
186
+ return;
187
+ }
188
+ if (!socket.userId) {
189
+ throw new Error("Authentication required");
190
+ }
191
+ if (this.hasServiceAccess(socket, requiredLevel)) {
192
+ return;
193
+ }
194
+ if (entryId) {
195
+ if (this.checkAccess(socket.userId, entryId, requiredLevel, socket)) {
196
+ return;
197
+ }
198
+ if (this.hasEntryACL && await this.checkEntryACL(socket.userId, entryId, requiredLevel)) {
199
+ return;
200
+ }
201
+ throw new Error("Insufficient permissions");
202
+ }
203
+ if (requiredLevel === "Read") {
204
+ return;
205
+ }
206
+ throw new Error("Insufficient permissions");
207
+ }
208
+ // ===========================================================================
209
+ // CRUD Operations
210
+ // ===========================================================================
211
+ /**
212
+ * Find an entity by ID.
213
+ */
214
+ async findById(id) {
215
+ return await this.getDelegate().findUnique({
216
+ where: { id }
217
+ });
218
+ }
219
+ /**
220
+ * Create an entity and emit to subscribers.
221
+ */
222
+ async create(data) {
223
+ const entity = await this.getDelegate().create({ data });
224
+ this.emitUpdate(entity.id, entity);
225
+ this.logger.info(`Created entity ${entity.id}`);
226
+ return entity;
227
+ }
228
+ /**
229
+ * Update an entity and emit to subscribers.
230
+ */
231
+ async update(id, data) {
232
+ try {
233
+ const entity = await this.getDelegate().update({
234
+ where: { id },
235
+ data
236
+ });
237
+ this.emitUpdate(id, entity);
238
+ this.logger.info(`Updated entity ${id}`);
239
+ return entity;
240
+ } catch {
241
+ return null;
242
+ }
243
+ }
244
+ /**
245
+ * Delete an entity and emit deletion event to subscribers.
246
+ */
247
+ async delete(id) {
248
+ try {
249
+ await this.getDelegate().delete({
250
+ where: { id }
251
+ });
252
+ this.emitUpdate(id, { id, deleted: true });
253
+ this.logger.info(`Deleted entity ${id}`);
254
+ return true;
255
+ } catch {
256
+ return false;
257
+ }
258
+ }
259
+ // ===========================================================================
260
+ // Public Method Definition
261
+ // ===========================================================================
262
+ /**
263
+ * Define a public method that will be exposed via Socket.io.
264
+ *
265
+ * @typeParam K - Method name from the service methods map
266
+ */
267
+ defineMethod(name, access, handler, options) {
268
+ const definition = {
269
+ name,
270
+ access,
271
+ handler,
272
+ schema: options?.schema,
273
+ resolveEntryId: options?.resolveEntryId
274
+ };
275
+ this.publicMethods.set(
276
+ name,
277
+ definition
278
+ );
279
+ return definition;
280
+ }
281
+ /**
282
+ * Get all public methods for registry discovery.
283
+ */
284
+ getPublicMethods() {
285
+ return Array.from(this.publicMethods.values());
286
+ }
287
+ // ===========================================================================
288
+ // Admin Methods (Scaffolding)
289
+ // ===========================================================================
290
+ /**
291
+ * Install standard admin CRUD methods.
292
+ * Call this in derived class constructor to expose admin endpoints.
293
+ */
294
+ installAdminMethods(_options) {
295
+ this.logger.debug("Admin methods installation placeholder");
296
+ }
297
+ // ===========================================================================
298
+ // Utility Methods
299
+ // ===========================================================================
300
+ /**
301
+ * Fields that should never be exposed or editable via admin methods.
302
+ */
303
+ getDefaultDeniedFields() {
304
+ return ["id", "createdAt", "updatedAt", "acl", "serviceAccess"];
305
+ }
306
+ };
307
+
308
+ // src/server/ServiceRegistry.ts
309
+ var ServiceRegistry = class {
310
+ io;
311
+ services = /* @__PURE__ */ new Map();
312
+ logger;
313
+ constructor(io, options) {
314
+ this.io = io;
315
+ this.logger = options?.logger?.child({ service: "ServiceRegistry" }) ?? consoleLogger.child({ service: "ServiceRegistry" });
316
+ }
317
+ /**
318
+ * Register a service and auto-discover its public methods.
319
+ */
320
+ registerService(serviceName, service) {
321
+ this.services.set(serviceName, service);
322
+ this.logger.info(`Registered service: ${serviceName}`);
323
+ this.discoverServiceMethods(serviceName, service);
324
+ }
325
+ /**
326
+ * Discover and register all public methods from a service.
327
+ */
328
+ discoverServiceMethods(serviceName, service) {
329
+ const methods = service.getPublicMethods();
330
+ for (const method of methods) {
331
+ const eventName = `${serviceName}:${method.name}`;
332
+ this.logger.info(`Registering socket event: ${eventName}`);
333
+ this.io.on("connection", (socket) => {
334
+ this.registerMethodListener(
335
+ socket,
336
+ eventName,
337
+ method,
338
+ service
339
+ );
340
+ });
341
+ }
342
+ this.io.on("connection", (socket) => {
343
+ this.registerSubscriptionListener(
344
+ socket,
345
+ `${serviceName}:subscribe`,
346
+ service
347
+ );
348
+ this.registerUnsubscriptionListener(
349
+ socket,
350
+ `${serviceName}:unsubscribe`,
351
+ service
352
+ );
353
+ });
354
+ }
355
+ /**
356
+ * Register a method as a socket event listener.
357
+ */
358
+ registerMethodListener(socket, eventName, method, service) {
359
+ socket.on(
360
+ eventName,
361
+ async (payload, callback) => {
362
+ const startedAt = Date.now();
363
+ const [serviceName, methodName] = eventName.split(":");
364
+ const userIdShort = socket.userId?.slice(0, 8) ?? "anon";
365
+ try {
366
+ if (!socket.userId && method.access !== "Public") {
367
+ const errorResponse = {
368
+ success: false,
369
+ error: "Authentication required",
370
+ code: 401
371
+ };
372
+ callback?.(errorResponse);
373
+ return;
374
+ }
375
+ let validatedPayload = payload;
376
+ if (method.schema) {
377
+ const result2 = method.schema.safeParse(payload);
378
+ if (!result2.success) {
379
+ const errorResponse = {
380
+ success: false,
381
+ error: `Validation error: ${result2.error.message}`,
382
+ code: 400
383
+ };
384
+ callback?.(errorResponse);
385
+ return;
386
+ }
387
+ validatedPayload = result2.data;
388
+ }
389
+ let entryId;
390
+ if (method.resolveEntryId) {
391
+ entryId = method.resolveEntryId(validatedPayload) ?? void 0;
392
+ } else if (validatedPayload && typeof validatedPayload === "object" && "id" in validatedPayload) {
393
+ const idValue = validatedPayload.id;
394
+ if (typeof idValue === "string") {
395
+ entryId = idValue;
396
+ }
397
+ }
398
+ await service.ensureAccessForMethod(method.access, socket, entryId);
399
+ this.logger.info(
400
+ `User ${userIdShort} ${serviceName}.${methodName} -> start`,
401
+ {
402
+ category: "request_processing",
403
+ serviceName,
404
+ methodName,
405
+ userId: socket.userId,
406
+ socketId: socket.id
407
+ }
408
+ );
409
+ const result = await method.handler(validatedPayload, {
410
+ userId: socket.userId,
411
+ socketId: socket.id,
412
+ serviceAccess: socket.serviceAccess ?? {}
413
+ });
414
+ const durationMs = Date.now() - startedAt;
415
+ this.logger.info(
416
+ `User ${userIdShort} ${serviceName}.${methodName} -> success`,
417
+ {
418
+ category: "request_processing",
419
+ outcome: "success",
420
+ durationMs,
421
+ serviceName,
422
+ methodName,
423
+ userId: socket.userId
424
+ }
425
+ );
426
+ const successResponse = {
427
+ success: true,
428
+ data: result
429
+ };
430
+ callback?.(successResponse);
431
+ } catch (error) {
432
+ const durationMs = Date.now() - startedAt;
433
+ this.logger.error(
434
+ `User ${userIdShort} ${serviceName}.${methodName} -> fail`,
435
+ {
436
+ category: "request_processing",
437
+ outcome: "fail",
438
+ durationMs,
439
+ error: error instanceof Error ? error.message : "Unknown error"
440
+ }
441
+ );
442
+ const errorResponse = {
443
+ success: false,
444
+ error: error instanceof Error ? error.message : "Internal error",
445
+ code: 500
446
+ };
447
+ callback?.(errorResponse);
448
+ }
449
+ }
450
+ );
451
+ }
452
+ /**
453
+ * Register subscription listener for a service.
454
+ */
455
+ registerSubscriptionListener(socket, eventName, service) {
456
+ socket.on(
457
+ eventName,
458
+ async (payload, callback) => {
459
+ try {
460
+ if (!socket.userId) {
461
+ callback?.({
462
+ success: false,
463
+ error: "Authentication required",
464
+ code: 401
465
+ });
466
+ return;
467
+ }
468
+ const data = await service.subscribe(
469
+ payload.entryId,
470
+ socket,
471
+ payload.requiredLevel ?? "Read"
472
+ );
473
+ if (data === null) {
474
+ callback?.({
475
+ success: false,
476
+ error: "Access denied or entry not found",
477
+ code: 403
478
+ });
479
+ return;
480
+ }
481
+ callback?.({ success: true, data });
482
+ } catch (error) {
483
+ this.logger.error(`Error in subscription ${eventName}:`, {
484
+ error: error instanceof Error ? error.message : "Unknown error"
485
+ });
486
+ callback?.({
487
+ success: false,
488
+ error: error instanceof Error ? error.message : "Subscription failed"
489
+ });
490
+ }
491
+ }
492
+ );
493
+ }
494
+ /**
495
+ * Register unsubscription listener for a service.
496
+ */
497
+ registerUnsubscriptionListener(socket, eventName, service) {
498
+ socket.on(
499
+ eventName,
500
+ (payload, callback) => {
501
+ try {
502
+ service.unsubscribe(payload.entryId, socket);
503
+ callback?.({
504
+ success: true,
505
+ data: { unsubscribed: true, entryId: payload.entryId }
506
+ });
507
+ } catch (error) {
508
+ this.logger.error(`Error in unsubscription ${eventName}:`, {
509
+ error: error instanceof Error ? error.message : "Unknown error"
510
+ });
511
+ callback?.({
512
+ success: false,
513
+ error: error instanceof Error ? error.message : "Unsubscription failed"
514
+ });
515
+ }
516
+ }
517
+ );
518
+ }
519
+ /**
520
+ * Get list of registered service names.
521
+ */
522
+ getServices() {
523
+ return Array.from(this.services.keys());
524
+ }
525
+ /**
526
+ * Get all service instances for lifecycle management.
527
+ */
528
+ getServiceInstances() {
529
+ return Array.from(this.services.values());
530
+ }
531
+ };
532
+ function createQuickdrawServer(options) {
533
+ const logger = options.logger ?? consoleLogger;
534
+ const serverLogger = logger.child({ service: "QuickdrawServer" });
535
+ const app = express();
536
+ app.use(express.json());
537
+ app.get("/health", (_req, res) => {
538
+ res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
539
+ });
540
+ const httpServer = createServer(app);
541
+ const io = new Server(httpServer, {
542
+ cors: {
543
+ origin: options.cors?.origin ?? "*",
544
+ methods: options.cors?.methods ?? ["GET", "POST"],
545
+ credentials: options.cors?.credentials ?? true
546
+ }
547
+ });
548
+ const registry = new ServiceRegistry(io, { logger });
549
+ io.use(async (socket, next) => {
550
+ const quickdrawSocket = socket;
551
+ try {
552
+ if (options.auth?.authenticate) {
553
+ const auth = socket.handshake.auth;
554
+ const userId = await options.auth.authenticate(quickdrawSocket, auth);
555
+ quickdrawSocket.userId = userId;
556
+ quickdrawSocket.serviceAccess = {};
557
+ }
558
+ next();
559
+ } catch (error) {
560
+ serverLogger.error("Authentication failed", {
561
+ error: error instanceof Error ? error.message : "Unknown error",
562
+ socketId: socket.id
563
+ });
564
+ next(new Error("Authentication failed"));
565
+ }
566
+ });
567
+ for (const [serviceName, service] of Object.entries(options.services)) {
568
+ registry.registerService(serviceName, service);
569
+ }
570
+ io.on("connection", (socket) => {
571
+ const quickdrawSocket = socket;
572
+ serverLogger.info("Socket connected", {
573
+ socketId: quickdrawSocket.id,
574
+ userId: quickdrawSocket.userId
575
+ });
576
+ quickdrawSocket.on("disconnect", () => {
577
+ serverLogger.info("Socket disconnected", {
578
+ socketId: quickdrawSocket.id,
579
+ userId: quickdrawSocket.userId
580
+ });
581
+ for (const service of registry.getServiceInstances()) {
582
+ try {
583
+ service.unsubscribeSocket(quickdrawSocket);
584
+ } catch {
585
+ }
586
+ }
587
+ });
588
+ });
589
+ httpServer.listen(options.port, "0.0.0.0", () => {
590
+ serverLogger.info(`Server listening on port ${options.port}`);
591
+ serverLogger.info(
592
+ `Registered services: ${registry.getServices().join(", ")}`
593
+ );
594
+ });
595
+ return { io, httpServer, registry };
596
+ }
597
+ var DEFAULT_EXPIRATION = "7d";
598
+ async function createJWT(payload, secret, expiresIn = DEFAULT_EXPIRATION) {
599
+ const secretKey = new TextEncoder().encode(secret);
600
+ const jwt = await new jose.SignJWT(payload).setProtectedHeader({ alg: "HS256" }).setIssuedAt().setExpirationTime(expiresIn).sign(secretKey);
601
+ return jwt;
602
+ }
603
+ async function verifyJWT(token, secret) {
604
+ try {
605
+ const secretKey = new TextEncoder().encode(secret);
606
+ const { payload } = await jose.jwtVerify(token, secretKey);
607
+ return {
608
+ userId: payload.userId,
609
+ email: payload.email,
610
+ exp: payload.exp,
611
+ iat: payload.iat
612
+ };
613
+ } catch {
614
+ return null;
615
+ }
616
+ }
617
+
618
+ // src/server/auth/oauth.ts
619
+ function createOAuthURL(provider, config, state) {
620
+ const params = new URLSearchParams({
621
+ client_id: config.clientId,
622
+ redirect_uri: config.redirectUri,
623
+ response_type: "code",
624
+ scope: provider.scopes.join(" ")
625
+ });
626
+ if (state) {
627
+ params.set("state", state);
628
+ }
629
+ return `${provider.authorizationUrl}?${params.toString()}`;
630
+ }
631
+ async function exchangeOAuthCode(provider, config, code) {
632
+ const tokenResponse = await fetch(provider.tokenUrl, {
633
+ method: "POST",
634
+ headers: {
635
+ "Content-Type": "application/x-www-form-urlencoded",
636
+ Accept: "application/json"
637
+ },
638
+ body: new URLSearchParams({
639
+ client_id: config.clientId,
640
+ client_secret: config.clientSecret,
641
+ code,
642
+ grant_type: "authorization_code",
643
+ redirect_uri: config.redirectUri
644
+ })
645
+ });
646
+ if (!tokenResponse.ok) {
647
+ const error = await tokenResponse.text();
648
+ throw new Error(`Token exchange failed: ${error}`);
649
+ }
650
+ const tokens = await tokenResponse.json();
651
+ const userResponse = await fetch(provider.userInfoUrl, {
652
+ headers: {
653
+ Authorization: `Bearer ${tokens.access_token}`,
654
+ Accept: "application/json"
655
+ }
656
+ });
657
+ if (!userResponse.ok) {
658
+ const error = await userResponse.text();
659
+ throw new Error(`User info fetch failed: ${error}`);
660
+ }
661
+ const userData = await userResponse.json();
662
+ const user = provider.parseUserInfo(userData);
663
+ return { tokens, user };
664
+ }
665
+
666
+ // src/server/auth/discord.ts
667
+ var discordProvider = {
668
+ name: "discord",
669
+ authorizationUrl: "https://discord.com/api/oauth2/authorize",
670
+ tokenUrl: "https://discord.com/api/oauth2/token",
671
+ userInfoUrl: "https://discord.com/api/users/@me",
672
+ scopes: ["identify", "email"],
673
+ parseUserInfo: (data) => {
674
+ const d = data;
675
+ return {
676
+ id: String(d.id),
677
+ username: String(d.username),
678
+ discriminator: String(d.discriminator ?? "0"),
679
+ email: d.email ? String(d.email) : null,
680
+ avatar: d.avatar ? String(d.avatar) : null,
681
+ verified: Boolean(d.verified)
682
+ };
683
+ }
684
+ };
685
+
686
+ // src/server/auth/google.ts
687
+ var googleProvider = {
688
+ name: "google",
689
+ authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
690
+ tokenUrl: "https://oauth2.googleapis.com/token",
691
+ userInfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
692
+ scopes: ["openid", "email", "profile"],
693
+ parseUserInfo: (data) => {
694
+ const d = data;
695
+ return {
696
+ id: String(d.id),
697
+ email: String(d.email),
698
+ name: String(d.name),
699
+ picture: d.picture ? String(d.picture) : null,
700
+ verified_email: Boolean(d.verified_email)
701
+ };
702
+ }
703
+ };
704
+
705
+ export { BaseService, ServiceRegistry, createJWT, createOAuthURL, createQuickdrawServer, discordProvider, exchangeOAuthCode, googleProvider, verifyJWT };
706
+ //# sourceMappingURL=index.js.map
707
+ //# sourceMappingURL=index.js.map