@lalternative/auth 0.19.1 → 1.0.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,625 @@
1
+ import {
2
+ forwardHeaders
3
+ } from "./chunk-MJI5PR73.js";
4
+
5
+ // src/urbangate/cookies.ts
6
+ function readCookie(headers, name) {
7
+ const raw = headers.get("cookie") ?? "";
8
+ for (const part of raw.split(";")) {
9
+ const [k, ...v] = part.trim().split("=");
10
+ if (k === name) return decodeURIComponent(v.join("="));
11
+ }
12
+ return void 0;
13
+ }
14
+ function serializeCookie(name, value, o) {
15
+ const attrs = [
16
+ `${name}=${encodeURIComponent(value)}`,
17
+ "Path=/",
18
+ "HttpOnly",
19
+ "SameSite=Lax",
20
+ `Max-Age=${value ? o.maxAge : 0}`
21
+ ];
22
+ if (o.secure) attrs.push("Secure");
23
+ return attrs.join("; ");
24
+ }
25
+ function clearCookie(name, secure) {
26
+ return serializeCookie(name, "", { maxAge: 0, secure });
27
+ }
28
+
29
+ // src/urbangate/exchange.ts
30
+ var JWT_BEARER = "urn:ietf:params:oauth:grant-type:jwt-bearer";
31
+ var EXCHANGE_SCOPE = "urbangate:sessions:exchange";
32
+ var REFRESH_MARGIN_MS = 6e4;
33
+ function basic(id, secret) {
34
+ return "Basic " + btoa(`${id}:${secret}`);
35
+ }
36
+ var Exchange = class {
37
+ machine = null;
38
+ config;
39
+ fetchImpl;
40
+ constructor(config, fetchImpl = fetch) {
41
+ this.config = config;
42
+ this.fetchImpl = fetchImpl;
43
+ }
44
+ needsRefresh(token, now = Date.now()) {
45
+ return !token || token.expiresAt - now < REFRESH_MARGIN_MS;
46
+ }
47
+ async exchange(sessionToken) {
48
+ const machine = await this.machineToken();
49
+ if (!machine) return { status: "unavailable", reason: "machine_token" };
50
+ const url = new URL(
51
+ "/api/machine/sessions/exchange",
52
+ this.config.issuerUrl
53
+ );
54
+ let res;
55
+ try {
56
+ res = await this.fetchImpl(url, {
57
+ method: "POST",
58
+ headers: {
59
+ authorization: `Bearer ${machine}`,
60
+ "content-type": "application/json"
61
+ },
62
+ body: JSON.stringify({ session_token: sessionToken })
63
+ });
64
+ } catch {
65
+ return { status: "unavailable", reason: "exchange" };
66
+ }
67
+ if (res.status === 401) {
68
+ const body = await res.json().catch(() => ({}));
69
+ if (body.error === "invalid_token") this.machine = null;
70
+ return body.error === "session" ? { status: "session_gone" } : { status: "unavailable", reason: "exchange_401" };
71
+ }
72
+ if (res.status === 403) return { status: "inactive" };
73
+ if (!res.ok)
74
+ return { status: "unavailable", reason: `exchange_${res.status}` };
75
+ const answer = await res.json();
76
+ return this.hydraToken(answer);
77
+ }
78
+ async hydraToken(answer) {
79
+ const form = new URLSearchParams({
80
+ grant_type: JWT_BEARER,
81
+ assertion: answer.assertion,
82
+ audience: this.config.product
83
+ });
84
+ let res;
85
+ try {
86
+ res = await this.fetchImpl(
87
+ new URL("/oauth2/token", this.config.issuerUrl),
88
+ {
89
+ method: "POST",
90
+ headers: {
91
+ "content-type": "application/x-www-form-urlencoded",
92
+ authorization: basic(
93
+ this.config.admin.clientId,
94
+ this.config.admin.clientSecret
95
+ )
96
+ },
97
+ body: form
98
+ }
99
+ );
100
+ } catch {
101
+ return { status: "unavailable", reason: "hydra" };
102
+ }
103
+ if (!res.ok)
104
+ return { status: "unavailable", reason: `hydra_${res.status}` };
105
+ const body = await res.json();
106
+ return {
107
+ status: "ok",
108
+ token: {
109
+ accessToken: body.access_token,
110
+ expiresAt: Date.now() + body.expires_in * 1e3,
111
+ identityId: answer.identity_id,
112
+ roles: answer.roles
113
+ }
114
+ };
115
+ }
116
+ async machineToken() {
117
+ if (this.machine && this.machine.expiresAt - Date.now() > REFRESH_MARGIN_MS) {
118
+ return this.machine.token;
119
+ }
120
+ let res;
121
+ try {
122
+ res = await this.fetchImpl(
123
+ new URL("/oauth2/token", this.config.issuerUrl),
124
+ {
125
+ method: "POST",
126
+ headers: {
127
+ "content-type": "application/x-www-form-urlencoded",
128
+ authorization: basic(
129
+ this.config.provisioner.clientId,
130
+ this.config.provisioner.clientSecret
131
+ )
132
+ },
133
+ body: new URLSearchParams({
134
+ grant_type: "client_credentials",
135
+ scope: EXCHANGE_SCOPE,
136
+ audience: "urbangate"
137
+ })
138
+ }
139
+ );
140
+ } catch {
141
+ return null;
142
+ }
143
+ if (!res.ok) return null;
144
+ const body = await res.json();
145
+ this.machine = {
146
+ token: body.access_token,
147
+ expiresAt: Date.now() + body.expires_in * 1e3
148
+ };
149
+ return this.machine.token;
150
+ }
151
+ };
152
+ function decodeToken(raw) {
153
+ const parts = raw.split(".");
154
+ if (parts.length !== 3) return null;
155
+ try {
156
+ const b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
157
+ const payload = JSON.parse(decodeURIComponent(escape(atob(b64))));
158
+ if (!payload.sub || !payload.exp) return null;
159
+ return {
160
+ identityId: payload.sub,
161
+ roles: Array.isArray(payload.roles) ? payload.roles.filter((r) => typeof r === "string") : [],
162
+ expiresAt: payload.exp * 1e3
163
+ };
164
+ } catch {
165
+ return null;
166
+ }
167
+ }
168
+
169
+ // src/urbangate/kratos.ts
170
+ var KratosError = class extends Error {
171
+ failure;
172
+ constructor(failure2) {
173
+ super(failure2.status);
174
+ this.failure = failure2;
175
+ }
176
+ };
177
+ var INVALID_CREDENTIALS = 4000006;
178
+ var INVALID_CODE = /* @__PURE__ */ new Set([4010008, 4060006, 4070006]);
179
+ var CODE_SENT = /* @__PURE__ */ new Set([1010014, 1040005, 1060003, 1070003, 1080003]);
180
+ var ALREADY_REGISTERED = 4000007;
181
+ var PASSWORD_POLICY = /* @__PURE__ */ new Set([4000005, 4000031, 4000032, 4000033, 4000034]);
182
+ var FLOW_EXPIRED = /* @__PURE__ */ new Set([4010001, 4040001, 4060005, 4070005]);
183
+ function failureOf(status, body) {
184
+ const b = body ?? {};
185
+ if (status === 410 || FLOW_EXPIRED.has(b.error?.code ?? -1)) {
186
+ return { status: "flow_expired" };
187
+ }
188
+ if (b.error?.id === "session_aal2_required") {
189
+ return { status: "second_factor_required" };
190
+ }
191
+ if (b.error?.id === "self_service_flow_expired")
192
+ return { status: "flow_expired" };
193
+ if (status >= 500 || status === 0) return { status: "unavailable" };
194
+ const messages = [
195
+ ...b.ui?.messages ?? [],
196
+ ...(b.ui?.nodes ?? []).flatMap((n) => n.messages ?? [])
197
+ ].filter((m) => m.type === "error");
198
+ for (const m of messages) {
199
+ if (m.id === INVALID_CREDENTIALS) return { status: "invalid_credentials" };
200
+ if (INVALID_CODE.has(m.id ?? -1)) return { status: "invalid_code" };
201
+ if (m.id === ALREADY_REGISTERED) return { status: "already_registered" };
202
+ if (PASSWORD_POLICY.has(m.id ?? -1)) {
203
+ return { status: "password_refused", message: m.text ?? "" };
204
+ }
205
+ if (FLOW_EXPIRED.has(m.id ?? -1)) return { status: "flow_expired" };
206
+ }
207
+ if (b.error?.id === "account_disabled" || status === 401) {
208
+ return { status: "account_disabled" };
209
+ }
210
+ const first = messages[0]?.text ?? b.error?.message ?? "";
211
+ return { status: "invalid_input", message: first };
212
+ }
213
+ function codeWasSent(flow) {
214
+ return (flow.ui?.messages ?? []).some((m) => CODE_SENT.has(m.id ?? -1));
215
+ }
216
+ var KratosFlows = class {
217
+ baseUrl;
218
+ fetchImpl;
219
+ constructor(baseUrl, fetchImpl = fetch) {
220
+ this.baseUrl = baseUrl;
221
+ this.fetchImpl = fetchImpl;
222
+ }
223
+ async start(kind, sessionToken, params = {}) {
224
+ const url = new URL(`/self-service/${kind}/api`, this.baseUrl);
225
+ for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
226
+ const res = await this.call(url, { method: "GET" }, sessionToken);
227
+ return await this.body(res);
228
+ }
229
+ async submit(kind, flowId, body, sessionToken) {
230
+ const url = new URL(`/self-service/${kind}`, this.baseUrl);
231
+ url.searchParams.set("flow", flowId);
232
+ const res = await this.call(
233
+ url,
234
+ {
235
+ method: "POST",
236
+ headers: { "content-type": "application/json" },
237
+ body: JSON.stringify(body)
238
+ },
239
+ sessionToken
240
+ );
241
+ return await this.body(res);
242
+ }
243
+ async whoami(sessionToken) {
244
+ const url = new URL("/sessions/whoami", this.baseUrl);
245
+ let res;
246
+ try {
247
+ res = await this.fetchImpl(url, {
248
+ headers: { "x-session-token": sessionToken }
249
+ });
250
+ } catch {
251
+ throw new KratosError({ status: "unavailable" });
252
+ }
253
+ if (res.status === 401 || res.status === 403) return null;
254
+ if (!res.ok) throw new KratosError({ status: "unavailable" });
255
+ return await res.json();
256
+ }
257
+ async logout(sessionToken) {
258
+ const url = new URL("/self-service/logout/api", this.baseUrl);
259
+ try {
260
+ await this.fetchImpl(url, {
261
+ method: "DELETE",
262
+ headers: { "content-type": "application/json" },
263
+ body: JSON.stringify({ session_token: sessionToken })
264
+ });
265
+ } catch {
266
+ }
267
+ }
268
+ async call(url, init, sessionToken) {
269
+ const headers = new Headers(init.headers);
270
+ headers.set("accept", "application/json");
271
+ if (sessionToken) headers.set("x-session-token", sessionToken);
272
+ try {
273
+ return await this.fetchImpl(url, { ...init, headers });
274
+ } catch {
275
+ throw new KratosError({ status: "unavailable" });
276
+ }
277
+ }
278
+ async body(res) {
279
+ const body = await res.json().catch(() => null);
280
+ if (res.ok || res.status === 422 && body && "continue_with" in body) {
281
+ return body;
282
+ }
283
+ throw new KratosError(failureOf(res.status, body));
284
+ }
285
+ };
286
+
287
+ // src/urbangate/server.ts
288
+ var SESSION_MAX_AGE = 30 * 24 * 60 * 60;
289
+ var FLOW_MAX_AGE = 600;
290
+ var TOKEN_MAX_AGE = 15 * 60;
291
+ var FLOW_OF_TYPE = {
292
+ "email-verification": "verification",
293
+ "forget-password": "recovery",
294
+ "sign-in": "login"
295
+ };
296
+ var FAILURE_STATUS = {
297
+ invalid_credentials: 401,
298
+ invalid_code: 400,
299
+ email_not_verified: 403,
300
+ second_factor_required: 403,
301
+ account_disabled: 403,
302
+ already_registered: 409,
303
+ password_refused: 422,
304
+ invalid_input: 400,
305
+ flow_expired: 410,
306
+ unavailable: 503
307
+ };
308
+ function json(status, body, cookies = []) {
309
+ const headers = new Headers({ "content-type": "application/json" });
310
+ for (const c of cookies) headers.append("set-cookie", c);
311
+ return new Response(JSON.stringify(body), { status, headers });
312
+ }
313
+ function failure(code, status, message) {
314
+ return json(status, {
315
+ error: { code, status, ...message ? { message } : {} }
316
+ });
317
+ }
318
+ function failed(error) {
319
+ if (error instanceof KratosError) {
320
+ const f = error.failure;
321
+ return failure(
322
+ f.status,
323
+ FAILURE_STATUS[f.status],
324
+ "message" in f ? f.message : void 0
325
+ );
326
+ }
327
+ return failure("unavailable", 503);
328
+ }
329
+ function userOf(session, roles, product) {
330
+ const identity = session.identity;
331
+ if (!identity) return null;
332
+ const email = identity.traits?.email ?? "";
333
+ const address = identity.verifiable_addresses?.find((a) => a.value === email);
334
+ return {
335
+ id: identity.id,
336
+ identityId: identity.id,
337
+ email,
338
+ emailVerified: address?.verified ?? false,
339
+ name: identity.traits?.name ?? "",
340
+ role: roles.includes(`${product}:admin`) ? "admin" : "user"
341
+ };
342
+ }
343
+ function continueWith(list, action) {
344
+ return list?.find(
345
+ (c) => c.action === action
346
+ );
347
+ }
348
+ function createUrbangateAuth(config) {
349
+ const product = config.product;
350
+ const kratos = new KratosFlows(config.kratosUrl, config.fetch);
351
+ const exchange = new Exchange({ ...config.urbangate, product }, config.fetch);
352
+ const names = {
353
+ session: config.cookie?.name ?? `${product}_session`,
354
+ token: `${product}_token`,
355
+ flow: `${product}_flow`
356
+ };
357
+ const secure = config.cookie?.secure ?? config.urbangate.issuerUrl.startsWith("https://");
358
+ const cookie = (name, value, maxAge) => serializeCookie(name, value, { maxAge, secure });
359
+ const signedIn = (result) => {
360
+ const token = result.session_token;
361
+ if (!token) return [];
362
+ return [
363
+ cookie(names.session, token, SESSION_MAX_AGE),
364
+ clearCookie(names.flow, secure)
365
+ ];
366
+ };
367
+ const readToken = (headers) => {
368
+ const raw = readCookie(headers, names.token);
369
+ const decoded = raw ? decodeToken(raw) : null;
370
+ return raw && decoded ? { accessToken: raw, ...decoded } : null;
371
+ };
372
+ async function accessToken(headers) {
373
+ const sessionToken = readCookie(headers, names.session);
374
+ if (!sessionToken) return null;
375
+ const held = readToken(headers);
376
+ if (held && !exchange.needsRefresh(held))
377
+ return { token: held.accessToken };
378
+ const outcome = await exchange.exchange(sessionToken);
379
+ if (outcome.status === "session_gone" || outcome.status === "inactive")
380
+ return null;
381
+ if (outcome.status === "unavailable")
382
+ throw new KratosError({ status: "unavailable" });
383
+ return {
384
+ token: outcome.token.accessToken,
385
+ setCookie: cookie(names.token, outcome.token.accessToken, TOKEN_MAX_AGE)
386
+ };
387
+ }
388
+ async function getSession(headers) {
389
+ const sessionToken = readCookie(headers, names.session);
390
+ if (!sessionToken) return null;
391
+ const session2 = await kratos.whoami(sessionToken);
392
+ if (!session2?.active) return null;
393
+ const held = readToken(headers);
394
+ const user = userOf(session2, held?.roles ?? [], product);
395
+ if (!user) return null;
396
+ return { user, session: { expiresAt: session2.expires_at ?? "" } };
397
+ }
398
+ async function body(request) {
399
+ const raw = await request.json().catch(() => null);
400
+ return raw ?? {};
401
+ }
402
+ const str = (b, key) => typeof b[key] === "string" ? b[key].trim() : "";
403
+ async function signInEmail(request) {
404
+ const b = await body(request);
405
+ const email = str(b, "email");
406
+ const password = typeof b.password === "string" ? b.password : "";
407
+ if (!email || !password)
408
+ return failure("invalid_input", 400, "email and password are required");
409
+ const flow = await kratos.start("login");
410
+ const result = await kratos.submit("login", flow.id, {
411
+ method: "password",
412
+ identifier: email,
413
+ password
414
+ });
415
+ return json(
416
+ 200,
417
+ { user: result.session ? userOf(result.session, [], product) : null },
418
+ signedIn(result)
419
+ );
420
+ }
421
+ async function signUpEmail(request) {
422
+ const b = await body(request);
423
+ const email = str(b, "email");
424
+ const password = typeof b.password === "string" ? b.password : "";
425
+ if (!email || !password)
426
+ return failure("invalid_input", 400, "email and password are required");
427
+ const flow = await kratos.start("registration");
428
+ const result = await kratos.submit(
429
+ "registration",
430
+ flow.id,
431
+ {
432
+ method: "password",
433
+ traits: { email, ...str(b, "name") ? { name: str(b, "name") } : {} },
434
+ password
435
+ }
436
+ );
437
+ const verification = continueWith(
438
+ result.continue_with,
439
+ "show_verification_ui"
440
+ );
441
+ const cookies = signedIn(result);
442
+ if (verification)
443
+ cookies.push(
444
+ cookie(
445
+ names.flow,
446
+ `verification:${verification.flow.id}`,
447
+ FLOW_MAX_AGE
448
+ )
449
+ );
450
+ return json(
451
+ 200,
452
+ {
453
+ user: result.session ? userOf(result.session, [], product) : null,
454
+ ...verification ? { verification: { flowId: verification.flow.id } } : {}
455
+ },
456
+ cookies
457
+ );
458
+ }
459
+ async function sendOtp(request) {
460
+ const b = await body(request);
461
+ const email = str(b, "email");
462
+ const type = str(b, "type");
463
+ const kind = FLOW_OF_TYPE[type];
464
+ if (!email || !kind)
465
+ return failure("invalid_input", 400, "email and type are required");
466
+ const flow = await kratos.start(kind);
467
+ const submitted = await kratos.submit(kind, flow.id, {
468
+ method: "code",
469
+ ...kind === "login" ? { identifier: email } : { email }
470
+ });
471
+ if (!codeWasSent(submitted) && submitted.state !== "sent_email") {
472
+ return failure("invalid_input", 400, "the code could not be sent");
473
+ }
474
+ return json(200, { sent: true }, [
475
+ cookie(names.flow, `${kind}:${flow.id}`, FLOW_MAX_AGE)
476
+ ]);
477
+ }
478
+ function pendingFlow(headers, kind) {
479
+ const raw = readCookie(headers, names.flow) ?? "";
480
+ const [k, id] = raw.split(":");
481
+ return k === kind && id ? id : null;
482
+ }
483
+ async function signInOtp(request) {
484
+ const b = await body(request);
485
+ const email = str(b, "email");
486
+ const code = str(b, "otp");
487
+ const flowId = pendingFlow(request.headers, "login");
488
+ if (!flowId) return failure("flow_expired", 410);
489
+ if (!email || !code)
490
+ return failure("invalid_input", 400, "email and otp are required");
491
+ const result = await kratos.submit("login", flowId, {
492
+ method: "code",
493
+ identifier: email,
494
+ code
495
+ });
496
+ return json(
497
+ 200,
498
+ { user: result.session ? userOf(result.session, [], product) : null },
499
+ signedIn(result)
500
+ );
501
+ }
502
+ async function verifyEmail(request) {
503
+ const b = await body(request);
504
+ const code = str(b, "otp");
505
+ const flowId = pendingFlow(request.headers, "verification");
506
+ if (!flowId) return failure("flow_expired", 410);
507
+ if (!code) return failure("invalid_input", 400, "otp is required");
508
+ const flow = await kratos.submit("verification", flowId, {
509
+ method: "code",
510
+ code
511
+ });
512
+ if (flow.state !== "passed_challenge") return failure("invalid_code", 400);
513
+ return json(200, { verified: true }, [clearCookie(names.flow, secure)]);
514
+ }
515
+ async function resetPassword(request) {
516
+ const b = await body(request);
517
+ const code = str(b, "otp");
518
+ const password = typeof b.password === "string" ? b.password : "";
519
+ const flowId = pendingFlow(request.headers, "recovery");
520
+ if (!flowId) return failure("flow_expired", 410);
521
+ if (!code || !password)
522
+ return failure("invalid_input", 400, "otp and password are required");
523
+ const recovered = await kratos.submit(
524
+ "recovery",
525
+ flowId,
526
+ { method: "code", code }
527
+ );
528
+ const token = continueWith(
529
+ recovered.continue_with,
530
+ "set_ory_session_token"
531
+ )?.ory_session_token;
532
+ const settings = continueWith(recovered.continue_with, "show_settings_ui");
533
+ if (!token || !settings) return failure("invalid_code", 400);
534
+ await kratos.submit(
535
+ "settings",
536
+ settings.flow.id,
537
+ { method: "password", password },
538
+ token
539
+ );
540
+ return json(200, { reset: true }, [
541
+ cookie(names.session, token, SESSION_MAX_AGE),
542
+ clearCookie(names.flow, secure)
543
+ ]);
544
+ }
545
+ async function signOut(request) {
546
+ const token = readCookie(request.headers, names.session);
547
+ if (token) await kratos.logout(token);
548
+ return json(200, { signedOut: true }, [
549
+ clearCookie(names.session, secure),
550
+ clearCookie(names.token, secure),
551
+ clearCookie(names.flow, secure)
552
+ ]);
553
+ }
554
+ async function session(request) {
555
+ const s = await getSession(request.headers);
556
+ return json(200, s);
557
+ }
558
+ const routes = {
559
+ "POST sign-in/email": signInEmail,
560
+ "POST sign-in/email-otp": signInOtp,
561
+ "POST sign-up/email": signUpEmail,
562
+ "POST email-otp/send-verification-otp": sendOtp,
563
+ "POST email-otp/verify-email": verifyEmail,
564
+ "POST email-otp/reset-password": resetPassword,
565
+ "POST sign-out": signOut,
566
+ "GET get-session": session
567
+ };
568
+ async function handler(request) {
569
+ const path = new URL(request.url).pathname.replace(/\/$/, "");
570
+ const suffix = path.slice(path.indexOf("/api/auth/") + "/api/auth/".length);
571
+ if (suffix === "sign-in/social") return failure("not_supported", 501);
572
+ const route = routes[`${request.method} ${suffix}`];
573
+ if (!route) return failure("not_found", 404);
574
+ try {
575
+ return await route(request);
576
+ } catch (error) {
577
+ return failed(error);
578
+ }
579
+ }
580
+ function coreProxy(options) {
581
+ const coreUrl = options.coreUrl.replace(/\/$/, "");
582
+ return async (request) => {
583
+ let s;
584
+ let token;
585
+ try {
586
+ token = await accessToken(request.headers);
587
+ s = token ? await getSession(
588
+ new Headers({ ...cookieHeader(request.headers, names, token) })
589
+ ) : null;
590
+ } catch {
591
+ return failure("identity_provider_unavailable", 503);
592
+ }
593
+ if (!token || !s) return failure("sign_in_required", 401);
594
+ if (options.adminOnly && s.user.role !== "admin")
595
+ return failure("forbidden", 403);
596
+ const url = new URL(request.url);
597
+ const headers = forwardHeaders(request.headers);
598
+ headers.set("authorization", `Bearer ${token.token}`);
599
+ const upstream = await fetch(`${coreUrl}${url.pathname}${url.search}`, {
600
+ method: request.method,
601
+ headers,
602
+ body: request.method === "GET" || request.method === "HEAD" ? void 0 : request.body,
603
+ duplex: "half",
604
+ redirect: "manual"
605
+ });
606
+ const out = forwardHeaders(upstream.headers);
607
+ if (token.setCookie) out.append("set-cookie", token.setCookie);
608
+ return new Response(upstream.body, {
609
+ status: upstream.status,
610
+ headers: out
611
+ });
612
+ };
613
+ }
614
+ return { handler, getSession, accessToken, coreProxy };
615
+ }
616
+ function cookieHeader(headers, names, token) {
617
+ const session = readCookie(headers, names.session) ?? "";
618
+ return {
619
+ cookie: `${names.session}=${encodeURIComponent(session)}; ${names.token}=${encodeURIComponent(token.token)}`
620
+ };
621
+ }
622
+ export {
623
+ createUrbangateAuth
624
+ };
625
+ //# sourceMappingURL=urbangate.js.map