@aplons/auth 0.1.0 → 0.2.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/dist/next.js CHANGED
@@ -1,262 +1,362 @@
1
1
  /**
2
- * @aplons/auth/next — der Teil, den man sonst jedes Mal neu schreibt.
2
+ * @aplons/auth/next — the part you would otherwise write from scratch every
3
+ * time.
3
4
  *
4
- * Der Kern kennt keine Cookies; er weiß nichts davon, wo `verifier` und
5
- * `state` zwischen zwei Aufrufen liegen. Genau dort steckt aber die Arbeit,
6
- * und genau dort werden die Fehler gemacht: der Verifier im localStorage
7
- * (jedes Skript auf der Seite liest ihn), der State ohne `httpOnly`, ein
8
- * Cookie ohne `secure`, das über einen offenen Hotspot geht.
5
+ * The core knows nothing about cookies; it has no idea where `verifier` and
6
+ * `state` live between two requests. But that is exactly where the work is,
7
+ * and exactly where the mistakes get made: the verifier in localStorage
8
+ * (every script on the page reads it), the state without `httpOnly`, a
9
+ * cookie without `secure` travelling over an open hotspot.
9
10
  *
10
- * Hier passiert das einmal richtig:
11
+ * Here that happens once, correctly:
11
12
  *
12
13
  * app/api/auth/[...aplons]/route.ts
13
14
  * ------------------------------------------------------------------
14
- * import { handhabe } from "@aplons/auth/next";
15
+ * import { createHandler } from "@aplons/auth/next";
15
16
  *
16
- * export const { GET, POST } = handhabe({
17
+ * export const { GET, POST } = createHandler({
17
18
  * issuer: process.env.APLONS_ISSUER!,
18
19
  * clientId: process.env.APLONS_CLIENT_ID!,
19
20
  * clientSecret: process.env.APLONS_CLIENT_SECRET,
20
21
  * redirectUri: process.env.APLONS_REDIRECT_URI!,
21
22
  * });
22
23
  *
23
- * Das ergibt vier Adressen: /api/auth/login, /callback, /logout, /me.
24
+ * That yields four routes: /api/auth/login, /callback, /logout, /me.
24
25
  *
25
- * Next.js ist eine Peer-Abhängigkeit und wird hier absichtlich nicht
26
- * importiert dieses Modul kommt mit `Request` und `Response` aus, und die
27
- * gibt es überall. So lässt es sich auch woanders verwenden, und wer nur den
28
- * Kern will, zieht sich Next nicht mit ein.
26
+ * Next.js is a peer dependency and is deliberately not imported here — this
27
+ * module gets by with `Request` and `Response`, which exist everywhere. That
28
+ * makes it usable elsewhere too, and whoever only wants the core does not
29
+ * pull Next in with it.
29
30
  */
30
31
  import { AplonsAuth } from "./client.js";
31
32
  import { AplonsError } from "./errors.js";
32
- const VORGANG_COOKIE = "aplons_vorgang";
33
+ const TRANSACTION_COOKIE = "aplons_tx";
33
34
  /**
34
- * Die vier Adressen.
35
+ * The four routes.
35
36
  *
36
- * Zurückgegeben als `{ GET, POST }`, weil der App Router genau das erwartet.
37
+ * Returned as `{ GET, POST }`, because that is what the App Router expects.
37
38
  */
38
- export function handhabe(options) {
39
+ export function createHandler(options) {
39
40
  const auth = new AplonsAuth(options);
40
- const basePath = options.basePath ?? ableitenBasePath(options.redirectUri);
41
+ const basePath = options.basePath ?? deriveBasePath(options.redirectUri);
41
42
  const cookieName = options.cookieName ?? "aplons_session";
42
43
  async function GET(request) {
43
44
  const url = new URL(request.url);
44
- const aktion = url.pathname.slice(basePath.length).replace(/^\/+/, "");
45
+ const action = url.pathname.slice(basePath.length).replace(/^\/+/, "");
45
46
  /*
46
- Kein Fehler verlässt diese Funktion.
47
+ No error leaves this function.
47
48
 
48
- Ein Wurf hier landet sonst als unbehandelte Ablehnung im Prozess der
49
- Kundenanwendungbeim Ausprobieren hat das den Testserver schlicht
50
- beendet, ohne dass irgendwo stand, warum. Ein Anmeldeknopf, der die
51
- Anwendung abschießt, ist das Gegenteil von dem, was ein Paket abnehmen
52
- soll. Die Meldung von AplonsError sagt bereits, was zu tun ist; sie
53
- kommt hier heraus statt in einem Stapelabzug.
49
+ A throw here would otherwise land as an unhandled rejection in the
50
+ customer's process while testing, that simply killed the server with
51
+ nothing anywhere saying why. A login button that takes down the
52
+ application is the opposite of what a package is supposed to save you.
53
+ The message on AplonsError already says what to do; it comes out here
54
+ instead of in a stack trace.
54
55
  */
55
56
  try {
56
- switch (aktion) {
57
+ switch (action) {
57
58
  case "login":
58
- return await anmelden(request, url);
59
+ return await login(request, url);
59
60
  case "callback":
60
- return await rueckkehr(request, url);
61
+ return await callback(request, url);
61
62
  case "logout":
62
- return await abmelden(request);
63
+ return await logout(request);
63
64
  case "me":
64
- return await wer(request);
65
+ return await me(request);
66
+ case "branding":
67
+ return await branding(request);
65
68
  default:
66
69
  return json({ error: "not_found" }, 404);
67
70
  }
68
71
  }
69
- catch (fehler) {
70
- if (fehler instanceof AplonsError) {
71
- return json({ error: fehler.code, message: fehler.message }, 500);
72
+ catch (error) {
73
+ if (error instanceof AplonsError) {
74
+ return json({ error: error.code, message: error.message }, 500);
72
75
  }
73
- // Etwas Unerwartetes: die Meldung geht ins Log des Servers, nicht in
74
- // die Antwortdort könnte sie interne Pfade preisgeben.
75
- console.error("[@aplons/auth]", fehler);
76
+ // Something unexpected: the message goes to the server log, not into
77
+ // the responsethere it could give away internal paths.
78
+ console.error("[@aplons/auth]", error);
76
79
  return json({
77
- error: "unerwartet",
78
- message: "Bei der Anmeldung ist etwas schiefgegangen. Siehe Serverlog.",
80
+ error: "unexpected",
81
+ message: "Something went wrong during login. See the server log.",
79
82
  }, 500);
80
83
  }
81
84
  }
82
- async function anmelden(request, url) {
83
- const vorgang = await auth.start({
85
+ async function login(request, url) {
86
+ const authorization = await auth.startLogin({
84
87
  tenant: url.searchParams.get("tenant") ?? undefined,
85
- erneutAnmelden: url.searchParams.get("prompt") === "login",
88
+ forceLogin: url.searchParams.get("prompt") === "login",
86
89
  });
87
- const antwort = weiter(vorgang.url);
90
+ const response = redirect(authorization.url);
88
91
  /*
89
- Verifier, State und Nonce zusammen in ein kurzlebiges Cookie.
92
+ Verifier, state and nonce together in one short-lived cookie.
90
93
 
91
- httpOnly, damit kein Skript sie liest; sameSite=lax, weil die Rückkehr
92
- von Aplons eine Navigation von außen ist und ein `strict`-Cookie dabei
93
- nicht mitgeschickt würdeder Vorgang bräche dann ausgerechnet im
94
- letzten Schritt ab. Zehn Minuten: so lange braucht niemand für eine
95
- Anmeldung, und was länger offen liegt, ist eher vergessen als in Arbeit.
94
+ httpOnly so no script reads them; sameSite=lax because the return from
95
+ Aplons is a cross-site navigation and a `strict` cookie would not be
96
+ sent along with it the flow would then break at the very last step.
97
+ Ten minutes: nobody needs longer for a login, and whatever sits open
98
+ longer is forgotten rather than in progress.
96
99
  */
97
- setzeCookie(antwort, VORGANG_COOKIE, JSON.stringify({
98
- v: vorgang.verifier,
99
- s: vorgang.state,
100
- n: vorgang.nonce,
101
- z: url.searchParams.get("weiter") ?? options.nachAnmeldung ?? "/",
102
- }), { maxAge: 600, sicher: istHttps(url) });
103
- return antwort;
100
+ setCookie(response, TRANSACTION_COOKIE, JSON.stringify({
101
+ v: authorization.verifier,
102
+ s: authorization.state,
103
+ n: authorization.nonce,
104
+ next: url.searchParams.get("next") ?? options.afterLogin ?? "/",
105
+ }), { maxAge: 600, secure: isHttps(url) });
106
+ return response;
104
107
  }
105
- async function rueckkehr(request, url) {
106
- const roh = liesCookie(request, VORGANG_COOKIE);
107
- if (!roh) {
108
- return fehlerSeite("Der Anmeldevorgang ist nicht mehr da. Das passiert, wenn die " +
109
- "Anmeldung länger als zehn Minuten offen lag oder in einem anderen " +
110
- "Browser begonnen wurde. Fang noch einmal an.");
108
+ async function callback(request, url) {
109
+ const raw = readCookie(request, TRANSACTION_COOKIE);
110
+ if (!raw) {
111
+ return errorResponse("The login is gone. That happens when it sat open for more than ten " +
112
+ "minutes, or was started in a different browser. Start again.");
111
113
  }
112
- let vorgang;
114
+ let transaction;
113
115
  try {
114
- vorgang = JSON.parse(roh);
116
+ transaction = JSON.parse(raw);
115
117
  }
116
118
  catch {
117
- return fehlerSeite("Der Anmeldevorgang ist unlesbar. Fang noch einmal an.");
119
+ return errorResponse("The login is unreadable. Start again.");
118
120
  }
119
- let sitzung;
121
+ let session;
120
122
  try {
121
- sitzung = await auth.rueckkehr({
123
+ session = await auth.completeLogin({
122
124
  url,
123
- verifier: vorgang.v,
124
- state: vorgang.s,
125
- nonce: vorgang.n,
125
+ verifier: transaction.v,
126
+ state: transaction.s,
127
+ nonce: transaction.n,
126
128
  });
127
129
  }
128
- catch (fehler) {
129
- return fehlerSeite(fehler instanceof AplonsError ? fehler.message : "Die Anmeldung ist fehlgeschlagen.");
130
+ catch (error) {
131
+ return errorResponse(error instanceof AplonsError ? error.message : "The login failed.");
130
132
  }
131
- // Nur relative Ziele: ein „weiter"-Parameter, der auf eine fremde Adresse
132
- // zeigt, macht aus der eigenen Anmeldung eine Weiterleitung für andere.
133
- const ziel = vorgang.z.startsWith("/") && !vorgang.z.startsWith("//") ? vorgang.z : "/";
134
- const antwort = weiter(new URL(ziel, url).toString());
135
- // Der Vorgang ist erledigt; sein Cookie hat nichts mehr zu suchen.
136
- loescheCookie(antwort, VORGANG_COOKIE, istHttps(url));
137
- await legeSitzungAb(antwort, sitzung, istHttps(url));
138
- return antwort;
133
+ // Relative targets only: a `next` parameter pointing at a foreign address
134
+ // would turn your own login into an open redirect for someone else.
135
+ const target = transaction.next.startsWith("/") && !transaction.next.startsWith("//")
136
+ ? transaction.next
137
+ : "/";
138
+ const response = redirect(new URL(target, url).toString());
139
+ // The login is done; its cookie has no business sticking around.
140
+ clearCookie(response, TRANSACTION_COOKIE, isHttps(url));
141
+ await write(response, session, null, isHttps(url));
142
+ return response;
139
143
  }
140
- async function abmelden(request) {
141
- const sitzung = await holeSitzung(request);
144
+ async function logout(request) {
145
+ const stored = await readEntry(request);
146
+ const session = stored?.session ?? null;
142
147
  const url = new URL(request.url);
143
- if (sitzung?.refreshToken) {
144
- // Das Refresh-Token gilt dreißig Tage weiter, wenn es niemand
145
- // entwertetein gelöschtes Cookie beeindruckt es nicht.
146
- await auth.widerrufen(sitzung.refreshToken).catch(() => undefined);
148
+ if (session?.refreshToken) {
149
+ // The refresh token stays valid for thirty days unless somebody
150
+ // revokes it a deleted cookie does not impress it.
151
+ await auth.revoke(session.refreshToken).catch(() => undefined);
147
152
  }
148
- const abmeldeUrl = await auth.abmeldeUrl({
149
- danach: options.nachAbmeldung
150
- ? new URL(options.nachAbmeldung, url).toString()
153
+ /*
154
+ Und aus der eigenen Ablage heraus.
155
+
156
+ Das Cookie zu löschen nahm dem Browser nur den Schlüssel; der Eintrag
157
+ selbst — mitsamt Refresh-Token — blieb in der Datenbank liegen, für
158
+ jede Anmeldung eines jeden Kontos einer. `store.delete` stand in der
159
+ Beschreibung des Typs und wurde nirgends aufgerufen: wer die eigene
160
+ Ablage einbaute, sammelte abgemeldete Sitzungen, bis jemand nachsah.
161
+ */
162
+ if (options.store && stored?.id) {
163
+ await options.store.delete(stored.id).catch(() => undefined);
164
+ }
165
+ const target = await auth.logoutUrl({
166
+ returnTo: options.afterLogout
167
+ ? new URL(options.afterLogout, url).toString()
151
168
  : undefined,
152
- idToken: sitzung?.idToken,
169
+ idToken: session?.idToken,
153
170
  });
154
- const antwort = weiter(abmeldeUrl);
155
- loescheCookie(antwort, cookieName, istHttps(url));
156
- if (options.speicher)
157
- loescheCookie(antwort, cookieName + "_id", istHttps(url));
158
- return antwort;
171
+ const response = redirect(target);
172
+ clearCookie(response, cookieName, isHttps(url));
173
+ if (options.store)
174
+ clearCookie(response, cookieName + "_id", isHttps(url));
175
+ return response;
159
176
  }
160
- async function wer(request) {
161
- const sitzung = await holeSitzung(request);
162
- if (!sitzung)
163
- return json({ angemeldet: false }, 401);
164
- // Abgelaufen? Dann still erneuern, statt den Aufrufer abzuweisen.
165
- if (sitzung.accessTokenExpiresAt.getTime() < Date.now() && sitzung.refreshToken) {
166
- try {
167
- const frisch = await auth.erneuern(sitzung.refreshToken);
168
- const antwort = json({ angemeldet: true, konto: frisch.claims ?? null });
169
- await legeSitzungAb(antwort, frisch, istHttps(new URL(request.url)));
170
- return antwort;
171
- }
172
- catch {
173
- return json({ angemeldet: false }, 401);
174
- }
177
+ async function me(request) {
178
+ const state = await current(request);
179
+ if (!state)
180
+ return json({ authenticated: false }, 401);
181
+ return respond(json({ authenticated: true, account: state.session.claims ?? null }), state, request);
182
+ }
183
+ /**
184
+ * Das Erscheinungsbild des Mandanten, für die eigene Oberfläche.
185
+ *
186
+ * Braucht den Bereich `branding` in `scope` und einen Mandanten, der das
187
+ * Weitergeben in seinen Richtlinien erlaubt hat — sonst kommt die Absage von
188
+ * Aplons hier unverändert wieder heraus, damit die Ursache erkennbar bleibt
189
+ * statt als „irgendwas mit 500" zu enden.
190
+ */
191
+ async function branding(request) {
192
+ const state = await current(request);
193
+ if (!state)
194
+ return json({ error: "not_authenticated" }, 401);
195
+ let payload;
196
+ try {
197
+ payload = json(await auth.branding(state.session.accessToken));
198
+ }
199
+ catch (error) {
200
+ if (!(error instanceof AplonsError))
201
+ throw error;
202
+ payload = json({ error: error.code, message: error.message }, error.status ?? 500);
175
203
  }
176
- return json({ angemeldet: true, konto: sitzung.claims ?? null });
204
+ return respond(payload, state, request);
177
205
  }
178
- async function holeSitzung(request) {
179
- if (options.speicher) {
180
- const id = liesCookie(request, cookieName + "_id");
181
- return id ? options.speicher.lies(id) : null;
206
+ /**
207
+ * Die Sitzung, wie sie *jetzt* gilt.
208
+ *
209
+ * Ist das Zugriffstoken abgelaufen, wird still erneuert. Ob das passiert
210
+ * ist, steht im Ergebnis: die erneuerte Sitzung muss in die Antwort dieses
211
+ * einen Aufrufs geschrieben werden, und nur der Aufrufer hält sie in der
212
+ * Hand.
213
+ */
214
+ async function current(request) {
215
+ const stored = await readEntry(request);
216
+ if (!stored)
217
+ return null;
218
+ const { session, id } = stored;
219
+ if (session.accessTokenExpiresAt.getTime() >= Date.now() ||
220
+ !session.refreshToken) {
221
+ return { session, id, refreshed: false };
182
222
  }
183
- const roh = liesCookie(request, cookieName);
184
- if (!roh)
223
+ try {
224
+ return { session: await auth.refresh(session.refreshToken), id, refreshed: true };
225
+ }
226
+ catch {
227
+ return null;
228
+ }
229
+ }
230
+ /** Eine Antwort, die eine unterwegs erneuerte Sitzung mitnimmt. */
231
+ async function respond(response, state, request) {
232
+ if (state.refreshed) {
233
+ await write(response, state.session, state.id, isHttps(new URL(request.url)));
234
+ }
235
+ return response;
236
+ }
237
+ async function readEntry(request) {
238
+ if (options.store) {
239
+ const id = readCookie(request, cookieName + "_id");
240
+ if (!id)
241
+ return null;
242
+ const session = await options.store.read(id);
243
+ return session ? { id, session: withDate(session) } : null;
244
+ }
245
+ const raw = readCookie(request, cookieName);
246
+ if (!raw)
185
247
  return null;
186
248
  try {
187
- const daten = JSON.parse(roh);
188
- return { ...daten, accessTokenExpiresAt: new Date(daten.accessTokenExpiresAt) };
249
+ return { id: null, session: withDate(JSON.parse(raw)) };
189
250
  }
190
251
  catch {
191
252
  return null;
192
253
  }
193
254
  }
194
- async function legeSitzungAb(antwort, sitzung, sicher) {
195
- if (options.speicher) {
196
- const id = crypto.randomUUID();
197
- await options.speicher.schreib(id, sitzung);
198
- setzeCookie(antwort, cookieName + "_id", id, { maxAge: 2592000, sicher });
255
+ async function readSession(request) {
256
+ return (await readEntry(request))?.session ?? null;
257
+ }
258
+ /**
259
+ * Die Sitzung in die Antwort schreiben und in die eigene Ablage.
260
+ *
261
+ * `id` ist die Kennung, unter der sie dort schon liegt, oder `null` für eine
262
+ * neue Anmeldung. Sie wiederzuverwenden ist der Unterschied zwischen einer
263
+ * Zeile pro Sitzung und einer pro Erneuerung: erneuert wird alle zehn
264
+ * Minuten, und der alte Eintrag verschwand dabei nicht, sondern wurde nur
265
+ * unerreichbar.
266
+ */
267
+ async function write(response, session, id, secure) {
268
+ if (options.store) {
269
+ const key = id ?? crypto.randomUUID();
270
+ await options.store.write(key, session);
271
+ setCookie(response, cookieName + "_id", key, { maxAge: 2592000, secure });
199
272
  return;
200
273
  }
201
- setzeCookie(antwort, cookieName, JSON.stringify(sitzung), {
274
+ setCookie(response, cookieName, JSON.stringify(session), {
202
275
  maxAge: 2592000,
203
- sicher,
276
+ secure,
204
277
  });
205
278
  }
206
- /** Die Sitzung aus einer eigenen Route heraus lesen. */
207
- async function sitzungAus(request) {
208
- return holeSitzung(request);
279
+ /**
280
+ * Read the session from one of your own routes.
281
+ *
282
+ * Erneuert nicht: eine eigene Route kann kein Cookie mitschicken, das sie
283
+ * nicht selbst schreibt. Wer sicher ein gültiges Token braucht, ruft `/me`
284
+ * auf oder erneuert selbst über `auth.refresh`.
285
+ */
286
+ async function getSession(request) {
287
+ return readSession(request);
288
+ }
289
+ /** Das Erscheinungsbild, aus einer eigenen Route heraus. */
290
+ async function getBranding(request) {
291
+ const session = await readSession(request);
292
+ return session ? auth.branding(session.accessToken) : null;
209
293
  }
210
- return { GET, POST: GET, auth, sitzungAus };
294
+ return { GET, POST: GET, auth, getSession, getBranding };
211
295
  }
212
296
  // ---------------------------------------------------------------------------
213
- function ableitenBasePath(redirectUri) {
214
- const pfad = new URL(redirectUri).pathname;
297
+ /**
298
+ * Aus dem Abgelegten wieder eine Sitzung machen.
299
+ *
300
+ * `accessTokenExpiresAt` ist ein Datum. Durch JSON — im Cookie ebenso wie in
301
+ * einer fremden Ablage — kommt es als Zeichenkette zurück, und `.getTime()`
302
+ * gibt es darauf nicht: die Erneuerung lief dann in einen TypeError statt zu
303
+ * erneuern. Eine eigene Ablage, die Objekte behält, liefert dagegen ein
304
+ * richtiges Datum; beides muss hier durchkommen.
305
+ */
306
+ function withDate(session) {
307
+ const expires = session.accessTokenExpiresAt;
308
+ return {
309
+ ...session,
310
+ accessTokenExpiresAt: expires instanceof Date ? expires : new Date(expires),
311
+ };
312
+ }
313
+ function deriveBasePath(redirectUri) {
314
+ const path = new URL(redirectUri).pathname;
215
315
  // .../api/auth/callback → .../api/auth
216
- return pfad.replace(/\/callback\/?$/, "");
316
+ return path.replace(/\/callback\/?$/, "");
217
317
  }
218
- function istHttps(url) {
318
+ function isHttps(url) {
219
319
  return url.protocol === "https:";
220
320
  }
221
- function weiter(ziel) {
222
- return new Response(null, { status: 302, headers: { location: ziel } });
321
+ function redirect(target) {
322
+ return new Response(null, { status: 302, headers: { location: target } });
223
323
  }
224
- function json(daten, status = 200) {
225
- return new Response(JSON.stringify(daten), {
324
+ function json(data, status = 200) {
325
+ return new Response(JSON.stringify(data), {
226
326
  status,
227
327
  headers: { "content-type": "application/json" },
228
328
  });
229
329
  }
230
- function fehlerSeite(text) {
231
- return json({ error: "anmeldung_fehlgeschlagen", message: text }, 400);
330
+ function errorResponse(message) {
331
+ return json({ error: "login_failed", message }, 400);
232
332
  }
233
- function setzeCookie(antwort, name, wert, options) {
234
- const teile = [
235
- `${name}=${encodeURIComponent(wert)}`,
333
+ function setCookie(response, name, value, options) {
334
+ const parts = [
335
+ `${name}=${encodeURIComponent(value)}`,
236
336
  "Path=/",
237
337
  "HttpOnly",
238
338
  "SameSite=Lax",
239
339
  `Max-Age=${options.maxAge}`,
240
340
  ];
241
- // Ohne HTTPS kein Securesonst käme das Cookie in der Entwicklung unter
242
- // http://localhost gar nicht erst an, und niemand fände den Grund.
243
- if (options.sicher)
244
- teile.push("Secure");
245
- antwort.headers.append("set-cookie", teile.join("; "));
341
+ // No Secure without HTTPSotherwise the cookie would never arrive during
342
+ // development on http://localhost, and nobody would find the reason.
343
+ if (options.secure)
344
+ parts.push("Secure");
345
+ response.headers.append("set-cookie", parts.join("; "));
246
346
  }
247
- function loescheCookie(antwort, name, sicher) {
248
- setzeCookie(antwort, name, "", { maxAge: 0, sicher });
347
+ function clearCookie(response, name, secure) {
348
+ setCookie(response, name, "", { maxAge: 0, secure });
249
349
  }
250
- function liesCookie(request, name) {
251
- const kopf = request.headers.get("cookie");
252
- if (!kopf)
350
+ function readCookie(request, name) {
351
+ const header = request.headers.get("cookie");
352
+ if (!header)
253
353
  return null;
254
- for (const teil of kopf.split(";")) {
255
- const trenn = teil.indexOf("=");
256
- if (trenn < 0)
354
+ for (const part of header.split(";")) {
355
+ const separator = part.indexOf("=");
356
+ if (separator < 0)
257
357
  continue;
258
- if (teil.slice(0, trenn).trim() === name) {
259
- return decodeURIComponent(teil.slice(trenn + 1));
358
+ if (part.slice(0, separator).trim() === name) {
359
+ return decodeURIComponent(part.slice(separator + 1));
260
360
  }
261
361
  }
262
362
  return null;
package/dist/pkce.d.ts CHANGED
@@ -1,48 +1,46 @@
1
1
  /**
2
- * PKCE — der Nachweis, dass der, der den Code einlöst, auch der ist, der ihn
3
- * angefordert hat.
2
+ * PKCE — proof that whoever redeems the code is the one who asked for it.
4
3
  *
5
- * Der Code kommt über einen Umleitungs-URL zurück und steht damit im Verlauf
6
- * des Browsers, im Log jedes Proxys dazwischen und im Referer der nächsten
7
- * Seite. Wer ihn dort aufliest, kann ihn ohne PKCE einlösen. Mit PKCE braucht
8
- * er zusätzlich den Verifier, und der hat den Browser nie verlassen.
4
+ * The code comes back through a redirect URL, which puts it in the browser
5
+ * history, in the log of every proxy in between, and in the referer of the
6
+ * next page. Without PKCE, anyone who reads it there can redeem it. With
7
+ * PKCE they also need the verifier, and that never left the browser.
9
8
  *
10
- * Alles hier läuft über die eingebaute Web-Crypto — kein Paket, keine
11
- * Abhängigkeit. Verfügbar in Node ab 18, in jedem Browser und am Rand.
9
+ * Everything here runs on built-in Web Crypto — no package, no dependency.
10
+ * Available in Node 18+, in every browser, and at the edge.
12
11
  */
13
12
  /**
14
- * Base64 ohne die drei Zeichen, die in einem URL etwas anderes bedeuten.
13
+ * Base64 without the three characters that mean something else in a URL.
15
14
  *
16
- * `+` wird in einer Formularkodierung zum Leerzeichen, `/` trennt Pfade, und
17
- * `=` trennt Parameter von Werten. Ein Verifier mit diesen Zeichen kommt am
18
- * anderen Ende verändert an und passt dann nicht mehr zu seinem Challenge
19
- * ein Fehler, der sich als „invalid_grant" zeigt und nach allem aussieht
20
- * außer nach seiner Ursache.
15
+ * `+` becomes a space in form encoding, `/` separates path segments, and `=`
16
+ * separates a parameter from its value. A verifier containing them arrives
17
+ * altered at the other end and no longer matches its challenge a failure
18
+ * that surfaces as `invalid_grant` and looks like anything but its cause.
21
19
  */
22
- export declare function base64url(daten: ArrayBuffer | Uint8Array): string;
20
+ export declare function base64url(data: ArrayBuffer | Uint8Array): string;
23
21
  /**
24
- * Ein Verifier: 32 Byte Zufall, base64url — 43 Zeichen.
22
+ * A verifier: 32 bytes of randomness, base64url — 43 characters.
25
23
  *
26
- * Das ist die Untergrenze aus RFC 7636 und zugleich genug: 256 Bit Zufall
27
- * lassen sich nicht raten. Die Obergrenze von 128 Zeichen brächte nichts
28
- * dazu.
24
+ * That is the lower bound from RFC 7636 and it is also enough: 256 bits of
25
+ * randomness cannot be guessed. Going up to the 128-character limit would
26
+ * add nothing.
29
27
  */
30
28
  export declare function createVerifier(): string;
31
- /** Was davon in den Anmelde-URL geht: der Hash, nie der Verifier selbst. */
29
+ /** What goes into the authorization URL: the hash, never the verifier. */
32
30
  export declare function createChallenge(verifier: string): Promise<string>;
33
31
  /**
34
- * Der Wert gegen fremde Anfragen (CSRF).
32
+ * The value that guards against cross-site request forgery.
35
33
  *
36
- * Ohne ihn könnte jemand einen Anmeldevorgang mit *seinem* Konto beginnen und
37
- * dem Opfer den Rückkehr-URL unterschieben; das Opfer wäre danach im fremden
38
- * Konto angemeldet und legte dort Daten ab.
34
+ * Without it someone could start a login with *their* account and hand the
35
+ * victim the finished redirect URL; the victim would end up signed into a
36
+ * stranger's account and file their data there.
39
37
  */
40
38
  export declare function createState(): string;
41
39
  /**
42
- * Zwei Zeichenketten vergleichen, ohne über die Dauer zu verraten, ab welcher
43
- * Stelle sie sich unterscheiden.
40
+ * Compare two strings without revealing, through timing, where they start
41
+ * to differ.
44
42
  *
45
- * Für den State ist das strenggenommen mehr, als nötig wäre aber die
46
- * Funktion steht auch für den, der sie auf etwas Empfindlicheres anwendet.
43
+ * For the state this is strictly more than necessarybut the function is
44
+ * also here for whoever applies it to something more sensitive.
47
45
  */
48
- export declare function gleich(a: string, b: string): boolean;
46
+ export declare function timingSafeEqual(a: string, b: string): boolean;