@shware/security 7.1.1 → 8.0.1
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/README.md +113 -32
- package/dist/auth/attributes.cjs +4 -3
- package/dist/auth/attributes.cjs.map +1 -1
- package/dist/auth/attributes.d.cts +3 -3
- package/dist/auth/attributes.d.cts.map +1 -1
- package/dist/auth/attributes.d.mts +3 -3
- package/dist/auth/attributes.d.mts.map +1 -1
- package/dist/auth/attributes.mjs +4 -3
- package/dist/auth/attributes.mjs.map +1 -1
- package/dist/auth/desktop-codes.cjs +57 -0
- package/dist/auth/desktop-codes.cjs.map +1 -0
- package/dist/auth/desktop-codes.d.cts +24 -0
- package/dist/auth/desktop-codes.d.cts.map +1 -0
- package/dist/auth/desktop-codes.d.mts +24 -0
- package/dist/auth/desktop-codes.d.mts.map +1 -0
- package/dist/auth/desktop-codes.mjs +55 -0
- package/dist/auth/desktop-codes.mjs.map +1 -0
- package/dist/auth/email-codes.cjs +77 -0
- package/dist/auth/email-codes.cjs.map +1 -0
- package/dist/auth/email-codes.d.cts +39 -0
- package/dist/auth/email-codes.d.cts.map +1 -0
- package/dist/auth/email-codes.d.mts +39 -0
- package/dist/auth/email-codes.d.mts.map +1 -0
- package/dist/auth/email-codes.mjs +75 -0
- package/dist/auth/email-codes.mjs.map +1 -0
- package/dist/auth/error.cjs +47 -0
- package/dist/auth/error.cjs.map +1 -0
- package/dist/auth/error.d.cts +88 -0
- package/dist/auth/error.d.cts.map +1 -0
- package/dist/auth/error.d.mts +88 -0
- package/dist/auth/error.d.mts.map +1 -0
- package/dist/auth/error.mjs +45 -0
- package/dist/auth/error.mjs.map +1 -0
- package/dist/auth/index.cjs +249 -384
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.d.cts +66 -55
- package/dist/auth/index.d.cts.map +1 -1
- package/dist/auth/index.d.mts +66 -55
- package/dist/auth/index.d.mts.map +1 -1
- package/dist/auth/index.mjs +252 -387
- package/dist/auth/index.mjs.map +1 -1
- package/dist/auth/respond.cjs +99 -0
- package/dist/auth/respond.cjs.map +1 -0
- package/dist/auth/respond.d.cts +39 -0
- package/dist/auth/respond.d.cts.map +1 -0
- package/dist/auth/respond.d.mts +39 -0
- package/dist/auth/respond.d.mts.map +1 -0
- package/dist/auth/respond.mjs +92 -0
- package/dist/auth/respond.mjs.map +1 -0
- package/dist/auth/types.d.cts +27 -11
- package/dist/auth/types.d.cts.map +1 -1
- package/dist/auth/types.d.mts +27 -11
- package/dist/auth/types.d.mts.map +1 -1
- package/dist/index.cjs +5 -0
- package/dist/index.d.cts +4 -1
- package/dist/index.d.mts +4 -1
- package/dist/index.mjs +3 -1
- package/dist/oauth2/error.cjs +0 -7
- package/dist/oauth2/error.cjs.map +1 -1
- package/dist/oauth2/error.d.cts +0 -3
- package/dist/oauth2/error.d.cts.map +1 -1
- package/dist/oauth2/error.d.mts +0 -3
- package/dist/oauth2/error.d.mts.map +1 -1
- package/dist/oauth2/error.mjs +1 -7
- package/dist/oauth2/error.mjs.map +1 -1
- package/dist/oauth2/provider/common.cjs +17 -3
- package/dist/oauth2/provider/common.cjs.map +1 -1
- package/dist/oauth2/provider/common.d.cts.map +1 -1
- package/dist/oauth2/provider/common.d.mts.map +1 -1
- package/dist/oauth2/provider/common.mjs +17 -3
- package/dist/oauth2/provider/common.mjs.map +1 -1
- package/dist/oauth2/provider/facebook.cjs +2 -4
- package/dist/oauth2/provider/facebook.cjs.map +1 -1
- package/dist/oauth2/provider/facebook.d.cts +0 -1
- package/dist/oauth2/provider/facebook.d.cts.map +1 -1
- package/dist/oauth2/provider/facebook.d.mts +0 -1
- package/dist/oauth2/provider/facebook.d.mts.map +1 -1
- package/dist/oauth2/provider/facebook.mjs +2 -4
- package/dist/oauth2/provider/facebook.mjs.map +1 -1
- package/dist/oauth2/provider/github.cjs +1 -1
- package/dist/oauth2/provider/github.cjs.map +1 -1
- package/dist/oauth2/provider/github.d.cts +1 -1
- package/dist/oauth2/provider/github.d.cts.map +1 -1
- package/dist/oauth2/provider/github.d.mts +1 -1
- package/dist/oauth2/provider/github.d.mts.map +1 -1
- package/dist/oauth2/provider/github.mjs +1 -1
- package/dist/oauth2/provider/github.mjs.map +1 -1
- package/dist/oauth2/provider/x.cjs +6 -4
- package/dist/oauth2/provider/x.cjs.map +1 -1
- package/dist/oauth2/provider/x.d.cts +3 -2
- package/dist/oauth2/provider/x.d.cts.map +1 -1
- package/dist/oauth2/provider/x.d.mts +3 -2
- package/dist/oauth2/provider/x.d.mts.map +1 -1
- package/dist/oauth2/provider/x.mjs +6 -4
- package/dist/oauth2/provider/x.mjs.map +1 -1
- package/dist/oauth2/types.cjs.map +1 -1
- package/dist/oauth2/types.d.cts +5 -0
- package/dist/oauth2/types.d.cts.map +1 -1
- package/dist/oauth2/types.d.mts +5 -0
- package/dist/oauth2/types.d.mts.map +1 -1
- package/dist/oauth2/types.mjs.map +1 -1
- package/dist/session/common.cjs +10 -13
- package/dist/session/common.cjs.map +1 -1
- package/dist/session/common.d.cts +7 -3
- package/dist/session/common.d.cts.map +1 -1
- package/dist/session/common.d.mts +7 -3
- package/dist/session/common.d.mts.map +1 -1
- package/dist/session/common.mjs +10 -12
- package/dist/session/common.mjs.map +1 -1
- package/dist/session/db.cjs +2 -2
- package/dist/session/db.cjs.map +1 -1
- package/dist/session/db.mjs +3 -3
- package/dist/session/db.mjs.map +1 -1
- package/dist/session/redis.cjs +6 -8
- package/dist/session/redis.cjs.map +1 -1
- package/dist/session/redis.d.cts.map +1 -1
- package/dist/session/redis.d.mts.map +1 -1
- package/dist/session/redis.mjs +7 -9
- package/dist/session/redis.mjs.map +1 -1
- package/dist/session/types.d.cts +0 -12
- package/dist/session/types.d.cts.map +1 -1
- package/dist/session/types.d.mts +0 -12
- package/dist/session/types.d.mts.map +1 -1
- package/dist/utils/valid.cjs +34 -52
- package/dist/utils/valid.cjs.map +1 -1
- package/dist/utils/valid.d.cts +8 -23
- package/dist/utils/valid.d.cts.map +1 -1
- package/dist/utils/valid.d.mts +8 -23
- package/dist/utils/valid.d.mts.map +1 -1
- package/dist/utils/valid.mjs +32 -50
- package/dist/utils/valid.mjs.map +1 -1
- package/package.json +3 -3
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
const require_auth_error = require("./error.cjs");
|
|
3
|
+
const require_auth_respond = require("./respond.cjs");
|
|
4
|
+
let node_crypto = require("node:crypto");
|
|
5
|
+
// src/auth/email-codes.ts
|
|
6
|
+
/** Seconds an email verification code, and its attempt counter, stay valid. */
|
|
7
|
+
const EMAIL_VERIFICATION_CODE_TTL = 600;
|
|
8
|
+
/**
|
|
9
|
+
* The six-digit codes mailed to prove an address, in the KV store: one pending code per email,
|
|
10
|
+
* its guess counter, and the resend cooldown. `send` and `verify` are the whole protocol, so the
|
|
11
|
+
* login route and a host flow that only needs proof of the address (verifying or changing a
|
|
12
|
+
* signed-in user's email) share it.
|
|
13
|
+
*/
|
|
14
|
+
var EmailVerificationCodes = class {
|
|
15
|
+
kv;
|
|
16
|
+
maxAttempts;
|
|
17
|
+
resendCooldown;
|
|
18
|
+
constructor(kv, options) {
|
|
19
|
+
this.kv = kv;
|
|
20
|
+
this.maxAttempts = options.maxAttempts;
|
|
21
|
+
this.resendCooldown = options.resendCooldown;
|
|
22
|
+
}
|
|
23
|
+
codeKey(email) {
|
|
24
|
+
return `email:verification:${email}`;
|
|
25
|
+
}
|
|
26
|
+
attemptsKey(email) {
|
|
27
|
+
return `email:verification-attempts:${email}`;
|
|
28
|
+
}
|
|
29
|
+
cooldownKey(email) {
|
|
30
|
+
return `email:verification-cooldown:${email}`;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Mails a fresh code, at most once per `resendCooldown`. The cooldown is claimed before sending,
|
|
34
|
+
* so of concurrent requests exactly one sends. A failed delivery releases it again: no code was
|
|
35
|
+
* stored, so an immediate retry gains an attacker nothing, while keeping it would lock the user
|
|
36
|
+
* out of signing in for the whole cooldown.
|
|
37
|
+
*/
|
|
38
|
+
async send(email, sender) {
|
|
39
|
+
const cooldownKey = this.cooldownKey(email);
|
|
40
|
+
if (await this.kv.increment(cooldownKey, this.resendCooldown) > 1) throw require_auth_respond.resourceExhausted("VERIFICATION_CODE_RESEND_TOO_SOON", "A code was sent recently");
|
|
41
|
+
const verificationCode = (0, node_crypto.randomInt)(1e6).toString().padStart(6, "0");
|
|
42
|
+
try {
|
|
43
|
+
await sender({
|
|
44
|
+
email,
|
|
45
|
+
verificationCode
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error("Failed to send email verification code", error);
|
|
49
|
+
await this.kv.removeItem(cooldownKey);
|
|
50
|
+
throw require_auth_respond.unavailable("EMAIL_DELIVERY_FAILED", "The verification email could not be sent");
|
|
51
|
+
}
|
|
52
|
+
await this.kv.removeItem(this.attemptsKey(email));
|
|
53
|
+
await this.kv.setItem(this.codeKey(email), verificationCode, 600);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Spends the pending code if `verificationCode` is it; otherwise throws `AuthenticationError`:
|
|
57
|
+
* `INVALID_VERIFICATION_CODE` (wrong, or none pending), or `VERIFICATION_CODE_ATTEMPTS_EXCEEDED`
|
|
58
|
+
* once more than `maxAttempts` guesses were made, which discards the code.
|
|
59
|
+
*/
|
|
60
|
+
async verify(email, verificationCode) {
|
|
61
|
+
const codeKey = this.codeKey(email);
|
|
62
|
+
const attemptsKey = this.attemptsKey(email);
|
|
63
|
+
const stored = await this.kv.getItem(codeKey);
|
|
64
|
+
if (typeof stored !== "string") throw new require_auth_error.AuthenticationError("INVALID_VERIFICATION_CODE");
|
|
65
|
+
if (await this.kv.increment(attemptsKey, 600) > this.maxAttempts) {
|
|
66
|
+
await this.kv.removeItem(codeKey);
|
|
67
|
+
throw new require_auth_error.AuthenticationError("VERIFICATION_CODE_ATTEMPTS_EXCEEDED");
|
|
68
|
+
}
|
|
69
|
+
if (stored.length !== verificationCode.length || !(0, node_crypto.timingSafeEqual)(Buffer.from(stored), Buffer.from(verificationCode))) throw new require_auth_error.AuthenticationError("INVALID_VERIFICATION_CODE");
|
|
70
|
+
await this.kv.removeItem(codeKey);
|
|
71
|
+
await this.kv.removeItem(attemptsKey);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
exports.EMAIL_VERIFICATION_CODE_TTL = EMAIL_VERIFICATION_CODE_TTL;
|
|
75
|
+
exports.EmailVerificationCodes = EmailVerificationCodes;
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=email-codes.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-codes.cjs","names":["resourceExhausted","randomInt","unavailable","AuthenticationError","timingSafeEqual"],"sources":["../../src/auth/email-codes.ts"],"sourcesContent":["import { randomInt, timingSafeEqual } from 'node:crypto';\nimport type { KVRepository } from '../session/types';\nimport { AuthenticationError } from './error';\nimport { resourceExhausted, unavailable } from './respond';\n\n/** Seconds an email verification code, and its attempt counter, stay valid. */\nexport const EMAIL_VERIFICATION_CODE_TTL = 10 * 60;\n\nexport type EmailSender = (data: { email: string; verificationCode: string }) => Promise<void>;\n\n/**\n * The six-digit codes mailed to prove an address, in the KV store: one pending code per email,\n * its guess counter, and the resend cooldown. `send` and `verify` are the whole protocol, so the\n * login route and a host flow that only needs proof of the address (verifying or changing a\n * signed-in user's email) share it.\n */\nexport class EmailVerificationCodes {\n private readonly kv: KVRepository;\n private readonly maxAttempts: number;\n private readonly resendCooldown: number;\n\n constructor(kv: KVRepository, options: { maxAttempts: number; resendCooldown: number }) {\n this.kv = kv;\n this.maxAttempts = options.maxAttempts;\n this.resendCooldown = options.resendCooldown;\n }\n\n private codeKey(email: string) {\n return `email:verification:${email}`;\n }\n\n private attemptsKey(email: string) {\n return `email:verification-attempts:${email}`;\n }\n\n private cooldownKey(email: string) {\n return `email:verification-cooldown:${email}`;\n }\n\n /**\n * Mails a fresh code, at most once per `resendCooldown`. The cooldown is claimed before sending,\n * so of concurrent requests exactly one sends. A failed delivery releases it again: no code was\n * stored, so an immediate retry gains an attacker nothing, while keeping it would lock the user\n * out of signing in for the whole cooldown.\n */\n async send(email: string, sender: EmailSender): Promise<void> {\n const cooldownKey = this.cooldownKey(email);\n if ((await this.kv.increment(cooldownKey, this.resendCooldown)) > 1) {\n throw resourceExhausted('VERIFICATION_CODE_RESEND_TOO_SOON', 'A code was sent recently');\n }\n const verificationCode = randomInt(1_000_000).toString().padStart(6, '0');\n try {\n // Sent before it is stored, so a failed delivery leaves the previous code usable.\n await sender({ email, verificationCode });\n } catch (error) {\n console.error('Failed to send email verification code', error);\n await this.kv.removeItem(cooldownKey);\n throw unavailable('EMAIL_DELIVERY_FAILED', 'The verification email could not be sent');\n }\n // The new code gets the full attempt budget; the cooldown bounds how often that happens.\n // Reset before storing, so every comparison against the new code is counted.\n await this.kv.removeItem(this.attemptsKey(email));\n await this.kv.setItem(this.codeKey(email), verificationCode, EMAIL_VERIFICATION_CODE_TTL);\n }\n\n /**\n * Spends the pending code if `verificationCode` is it; otherwise throws `AuthenticationError`:\n * `INVALID_VERIFICATION_CODE` (wrong, or none pending), or `VERIFICATION_CODE_ATTEMPTS_EXCEEDED`\n * once more than `maxAttempts` guesses were made, which discards the code.\n */\n async verify(email: string, verificationCode: string): Promise<void> {\n const codeKey = this.codeKey(email);\n const attemptsKey = this.attemptsKey(email);\n const stored = await this.kv.getItem(codeKey);\n if (typeof stored !== 'string') throw new AuthenticationError('INVALID_VERIFICATION_CODE');\n\n // Count the attempt before comparing: the atomic increment hands every request, parallel ones\n // included, a distinct count, so at most maxAttempts comparisons are ever made against a code.\n const attempts = await this.kv.increment(attemptsKey, EMAIL_VERIFICATION_CODE_TTL);\n if (attempts > this.maxAttempts) {\n // Only the code is discarded. The counter stays until it expires (or a new code is sent):\n // removing it would hand requests that already read the code a fresh budget.\n await this.kv.removeItem(codeKey);\n throw new AuthenticationError('VERIFICATION_CODE_ATTEMPTS_EXCEEDED');\n }\n if (\n stored.length !== verificationCode.length ||\n !timingSafeEqual(Buffer.from(stored), Buffer.from(verificationCode))\n ) {\n throw new AuthenticationError('INVALID_VERIFICATION_CODE');\n }\n await this.kv.removeItem(codeKey);\n await this.kv.removeItem(attemptsKey);\n }\n}\n"],"mappings":";;;;;;AAMA,MAAa,8BAA8B;;;;;;;AAU3C,IAAa,yBAAb,MAAoC;CAClC;CACA;CACA;CAEA,YAAY,IAAkB,SAA0D;EACtF,KAAK,KAAK;EACV,KAAK,cAAc,QAAQ;EAC3B,KAAK,iBAAiB,QAAQ;CAChC;CAEA,QAAgB,OAAe;EAC7B,OAAO,sBAAsB;CAC/B;CAEA,YAAoB,OAAe;EACjC,OAAO,+BAA+B;CACxC;CAEA,YAAoB,OAAe;EACjC,OAAO,+BAA+B;CACxC;;;;;;;CAQA,MAAM,KAAK,OAAe,QAAoC;EAC5D,MAAM,cAAc,KAAK,YAAY,KAAK;EAC1C,IAAK,MAAM,KAAK,GAAG,UAAU,aAAa,KAAK,cAAc,IAAK,GAChE,MAAMA,qBAAAA,kBAAkB,qCAAqC,0BAA0B;EAEzF,MAAM,oBAAA,GAAmBC,YAAAA,UAAAA,CAAU,GAAS,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,GAAG,GAAG;EACxE,IAAI;GAEF,MAAM,OAAO;IAAE;IAAO;GAAiB,CAAC;EAC1C,SAAS,OAAO;GACd,QAAQ,MAAM,0CAA0C,KAAK;GAC7D,MAAM,KAAK,GAAG,WAAW,WAAW;GACpC,MAAMC,qBAAAA,YAAY,yBAAyB,0CAA0C;EACvF;EAGA,MAAM,KAAK,GAAG,WAAW,KAAK,YAAY,KAAK,CAAC;EAChD,MAAM,KAAK,GAAG,QAAQ,KAAK,QAAQ,KAAK,GAAG,kBAAA,GAA6C;CAC1F;;;;;;CAOA,MAAM,OAAO,OAAe,kBAAyC;EACnE,MAAM,UAAU,KAAK,QAAQ,KAAK;EAClC,MAAM,cAAc,KAAK,YAAY,KAAK;EAC1C,MAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,OAAO;EAC5C,IAAI,OAAO,WAAW,UAAU,MAAM,IAAIC,mBAAAA,oBAAoB,2BAA2B;EAKzF,IAAI,MADmB,KAAK,GAAG,UAAU,aAAA,GAAwC,IAClE,KAAK,aAAa;GAG/B,MAAM,KAAK,GAAG,WAAW,OAAO;GAChC,MAAM,IAAIA,mBAAAA,oBAAoB,qCAAqC;EACrE;EACA,IACE,OAAO,WAAW,iBAAiB,UACnC,EAAA,GAACC,YAAAA,gBAAAA,CAAgB,OAAO,KAAK,MAAM,GAAG,OAAO,KAAK,gBAAgB,CAAC,GAEnE,MAAM,IAAID,mBAAAA,oBAAoB,2BAA2B;EAE3D,MAAM,KAAK,GAAG,WAAW,OAAO;EAChC,MAAM,KAAK,GAAG,WAAW,WAAW;CACtC;AACF"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { KVRepository } from "../session/types.cjs";
|
|
2
|
+
/** Seconds an email verification code, and its attempt counter, stay valid. */
|
|
3
|
+
export declare const EMAIL_VERIFICATION_CODE_TTL: number;
|
|
4
|
+
export type EmailSender = (data: {
|
|
5
|
+
email: string;
|
|
6
|
+
verificationCode: string;
|
|
7
|
+
}) => Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* The six-digit codes mailed to prove an address, in the KV store: one pending code per email,
|
|
10
|
+
* its guess counter, and the resend cooldown. `send` and `verify` are the whole protocol, so the
|
|
11
|
+
* login route and a host flow that only needs proof of the address (verifying or changing a
|
|
12
|
+
* signed-in user's email) share it.
|
|
13
|
+
*/
|
|
14
|
+
export declare class EmailVerificationCodes {
|
|
15
|
+
private readonly kv;
|
|
16
|
+
private readonly maxAttempts;
|
|
17
|
+
private readonly resendCooldown;
|
|
18
|
+
constructor(kv: KVRepository, options: {
|
|
19
|
+
maxAttempts: number;
|
|
20
|
+
resendCooldown: number;
|
|
21
|
+
});
|
|
22
|
+
private codeKey;
|
|
23
|
+
private attemptsKey;
|
|
24
|
+
private cooldownKey;
|
|
25
|
+
/**
|
|
26
|
+
* Mails a fresh code, at most once per `resendCooldown`. The cooldown is claimed before sending,
|
|
27
|
+
* so of concurrent requests exactly one sends. A failed delivery releases it again: no code was
|
|
28
|
+
* stored, so an immediate retry gains an attacker nothing, while keeping it would lock the user
|
|
29
|
+
* out of signing in for the whole cooldown.
|
|
30
|
+
*/
|
|
31
|
+
send(email: string, sender: EmailSender): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Spends the pending code if `verificationCode` is it; otherwise throws `AuthenticationError`:
|
|
34
|
+
* `INVALID_VERIFICATION_CODE` (wrong, or none pending), or `VERIFICATION_CODE_ATTEMPTS_EXCEEDED`
|
|
35
|
+
* once more than `maxAttempts` guesses were made, which discards the code.
|
|
36
|
+
*/
|
|
37
|
+
verify(email: string, verificationCode: string): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=email-codes.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-codes.d.cts","names":[],"sources":["../../src/auth/email-codes.ts"],"mappings":";;qBAMa;YAED,eAAe;EAAQ;EAAe;MAA+B;;;;;;;qBAQpE;mBACM;mBACA;mBACA;EAEjB,YAAY,IAAI,cAAc;IAAW;IAAqB;;UAMtD;UAIA;UAIA;;;;;;;EAUF,KAAK,eAAe,QAAQ,cAAc;;;;;;EAyB1C,OAAO,eAAe,2BAA2B"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { KVRepository } from "../session/types.mjs";
|
|
2
|
+
/** Seconds an email verification code, and its attempt counter, stay valid. */
|
|
3
|
+
export declare const EMAIL_VERIFICATION_CODE_TTL: number;
|
|
4
|
+
export type EmailSender = (data: {
|
|
5
|
+
email: string;
|
|
6
|
+
verificationCode: string;
|
|
7
|
+
}) => Promise<void>;
|
|
8
|
+
/**
|
|
9
|
+
* The six-digit codes mailed to prove an address, in the KV store: one pending code per email,
|
|
10
|
+
* its guess counter, and the resend cooldown. `send` and `verify` are the whole protocol, so the
|
|
11
|
+
* login route and a host flow that only needs proof of the address (verifying or changing a
|
|
12
|
+
* signed-in user's email) share it.
|
|
13
|
+
*/
|
|
14
|
+
export declare class EmailVerificationCodes {
|
|
15
|
+
private readonly kv;
|
|
16
|
+
private readonly maxAttempts;
|
|
17
|
+
private readonly resendCooldown;
|
|
18
|
+
constructor(kv: KVRepository, options: {
|
|
19
|
+
maxAttempts: number;
|
|
20
|
+
resendCooldown: number;
|
|
21
|
+
});
|
|
22
|
+
private codeKey;
|
|
23
|
+
private attemptsKey;
|
|
24
|
+
private cooldownKey;
|
|
25
|
+
/**
|
|
26
|
+
* Mails a fresh code, at most once per `resendCooldown`. The cooldown is claimed before sending,
|
|
27
|
+
* so of concurrent requests exactly one sends. A failed delivery releases it again: no code was
|
|
28
|
+
* stored, so an immediate retry gains an attacker nothing, while keeping it would lock the user
|
|
29
|
+
* out of signing in for the whole cooldown.
|
|
30
|
+
*/
|
|
31
|
+
send(email: string, sender: EmailSender): Promise<void>;
|
|
32
|
+
/**
|
|
33
|
+
* Spends the pending code if `verificationCode` is it; otherwise throws `AuthenticationError`:
|
|
34
|
+
* `INVALID_VERIFICATION_CODE` (wrong, or none pending), or `VERIFICATION_CODE_ATTEMPTS_EXCEEDED`
|
|
35
|
+
* once more than `maxAttempts` guesses were made, which discards the code.
|
|
36
|
+
*/
|
|
37
|
+
verify(email: string, verificationCode: string): Promise<void>;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=email-codes.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-codes.d.mts","names":[],"sources":["../../src/auth/email-codes.ts"],"mappings":";;qBAMa;YAED,eAAe;EAAQ;EAAe;MAA+B;;;;;;;qBAQpE;mBACM;mBACA;mBACA;EAEjB,YAAY,IAAI,cAAc;IAAW;IAAqB;;UAMtD;UAIA;UAIA;;;;;;;EAUF,KAAK,eAAe,QAAQ,cAAc;;;;;;EAyB1C,OAAO,eAAe,2BAA2B"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { AuthenticationError } from "./error.mjs";
|
|
2
|
+
import { resourceExhausted, unavailable } from "./respond.mjs";
|
|
3
|
+
import { randomInt, timingSafeEqual } from "node:crypto";
|
|
4
|
+
// src/auth/email-codes.ts
|
|
5
|
+
/** Seconds an email verification code, and its attempt counter, stay valid. */
|
|
6
|
+
const EMAIL_VERIFICATION_CODE_TTL = 600;
|
|
7
|
+
/**
|
|
8
|
+
* The six-digit codes mailed to prove an address, in the KV store: one pending code per email,
|
|
9
|
+
* its guess counter, and the resend cooldown. `send` and `verify` are the whole protocol, so the
|
|
10
|
+
* login route and a host flow that only needs proof of the address (verifying or changing a
|
|
11
|
+
* signed-in user's email) share it.
|
|
12
|
+
*/
|
|
13
|
+
var EmailVerificationCodes = class {
|
|
14
|
+
kv;
|
|
15
|
+
maxAttempts;
|
|
16
|
+
resendCooldown;
|
|
17
|
+
constructor(kv, options) {
|
|
18
|
+
this.kv = kv;
|
|
19
|
+
this.maxAttempts = options.maxAttempts;
|
|
20
|
+
this.resendCooldown = options.resendCooldown;
|
|
21
|
+
}
|
|
22
|
+
codeKey(email) {
|
|
23
|
+
return `email:verification:${email}`;
|
|
24
|
+
}
|
|
25
|
+
attemptsKey(email) {
|
|
26
|
+
return `email:verification-attempts:${email}`;
|
|
27
|
+
}
|
|
28
|
+
cooldownKey(email) {
|
|
29
|
+
return `email:verification-cooldown:${email}`;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Mails a fresh code, at most once per `resendCooldown`. The cooldown is claimed before sending,
|
|
33
|
+
* so of concurrent requests exactly one sends. A failed delivery releases it again: no code was
|
|
34
|
+
* stored, so an immediate retry gains an attacker nothing, while keeping it would lock the user
|
|
35
|
+
* out of signing in for the whole cooldown.
|
|
36
|
+
*/
|
|
37
|
+
async send(email, sender) {
|
|
38
|
+
const cooldownKey = this.cooldownKey(email);
|
|
39
|
+
if (await this.kv.increment(cooldownKey, this.resendCooldown) > 1) throw resourceExhausted("VERIFICATION_CODE_RESEND_TOO_SOON", "A code was sent recently");
|
|
40
|
+
const verificationCode = randomInt(1e6).toString().padStart(6, "0");
|
|
41
|
+
try {
|
|
42
|
+
await sender({
|
|
43
|
+
email,
|
|
44
|
+
verificationCode
|
|
45
|
+
});
|
|
46
|
+
} catch (error) {
|
|
47
|
+
console.error("Failed to send email verification code", error);
|
|
48
|
+
await this.kv.removeItem(cooldownKey);
|
|
49
|
+
throw unavailable("EMAIL_DELIVERY_FAILED", "The verification email could not be sent");
|
|
50
|
+
}
|
|
51
|
+
await this.kv.removeItem(this.attemptsKey(email));
|
|
52
|
+
await this.kv.setItem(this.codeKey(email), verificationCode, 600);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Spends the pending code if `verificationCode` is it; otherwise throws `AuthenticationError`:
|
|
56
|
+
* `INVALID_VERIFICATION_CODE` (wrong, or none pending), or `VERIFICATION_CODE_ATTEMPTS_EXCEEDED`
|
|
57
|
+
* once more than `maxAttempts` guesses were made, which discards the code.
|
|
58
|
+
*/
|
|
59
|
+
async verify(email, verificationCode) {
|
|
60
|
+
const codeKey = this.codeKey(email);
|
|
61
|
+
const attemptsKey = this.attemptsKey(email);
|
|
62
|
+
const stored = await this.kv.getItem(codeKey);
|
|
63
|
+
if (typeof stored !== "string") throw new AuthenticationError("INVALID_VERIFICATION_CODE");
|
|
64
|
+
if (await this.kv.increment(attemptsKey, 600) > this.maxAttempts) {
|
|
65
|
+
await this.kv.removeItem(codeKey);
|
|
66
|
+
throw new AuthenticationError("VERIFICATION_CODE_ATTEMPTS_EXCEEDED");
|
|
67
|
+
}
|
|
68
|
+
if (stored.length !== verificationCode.length || !timingSafeEqual(Buffer.from(stored), Buffer.from(verificationCode))) throw new AuthenticationError("INVALID_VERIFICATION_CODE");
|
|
69
|
+
await this.kv.removeItem(codeKey);
|
|
70
|
+
await this.kv.removeItem(attemptsKey);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
export { EMAIL_VERIFICATION_CODE_TTL, EmailVerificationCodes };
|
|
74
|
+
|
|
75
|
+
//# sourceMappingURL=email-codes.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email-codes.mjs","names":[],"sources":["../../src/auth/email-codes.ts"],"sourcesContent":["import { randomInt, timingSafeEqual } from 'node:crypto';\nimport type { KVRepository } from '../session/types';\nimport { AuthenticationError } from './error';\nimport { resourceExhausted, unavailable } from './respond';\n\n/** Seconds an email verification code, and its attempt counter, stay valid. */\nexport const EMAIL_VERIFICATION_CODE_TTL = 10 * 60;\n\nexport type EmailSender = (data: { email: string; verificationCode: string }) => Promise<void>;\n\n/**\n * The six-digit codes mailed to prove an address, in the KV store: one pending code per email,\n * its guess counter, and the resend cooldown. `send` and `verify` are the whole protocol, so the\n * login route and a host flow that only needs proof of the address (verifying or changing a\n * signed-in user's email) share it.\n */\nexport class EmailVerificationCodes {\n private readonly kv: KVRepository;\n private readonly maxAttempts: number;\n private readonly resendCooldown: number;\n\n constructor(kv: KVRepository, options: { maxAttempts: number; resendCooldown: number }) {\n this.kv = kv;\n this.maxAttempts = options.maxAttempts;\n this.resendCooldown = options.resendCooldown;\n }\n\n private codeKey(email: string) {\n return `email:verification:${email}`;\n }\n\n private attemptsKey(email: string) {\n return `email:verification-attempts:${email}`;\n }\n\n private cooldownKey(email: string) {\n return `email:verification-cooldown:${email}`;\n }\n\n /**\n * Mails a fresh code, at most once per `resendCooldown`. The cooldown is claimed before sending,\n * so of concurrent requests exactly one sends. A failed delivery releases it again: no code was\n * stored, so an immediate retry gains an attacker nothing, while keeping it would lock the user\n * out of signing in for the whole cooldown.\n */\n async send(email: string, sender: EmailSender): Promise<void> {\n const cooldownKey = this.cooldownKey(email);\n if ((await this.kv.increment(cooldownKey, this.resendCooldown)) > 1) {\n throw resourceExhausted('VERIFICATION_CODE_RESEND_TOO_SOON', 'A code was sent recently');\n }\n const verificationCode = randomInt(1_000_000).toString().padStart(6, '0');\n try {\n // Sent before it is stored, so a failed delivery leaves the previous code usable.\n await sender({ email, verificationCode });\n } catch (error) {\n console.error('Failed to send email verification code', error);\n await this.kv.removeItem(cooldownKey);\n throw unavailable('EMAIL_DELIVERY_FAILED', 'The verification email could not be sent');\n }\n // The new code gets the full attempt budget; the cooldown bounds how often that happens.\n // Reset before storing, so every comparison against the new code is counted.\n await this.kv.removeItem(this.attemptsKey(email));\n await this.kv.setItem(this.codeKey(email), verificationCode, EMAIL_VERIFICATION_CODE_TTL);\n }\n\n /**\n * Spends the pending code if `verificationCode` is it; otherwise throws `AuthenticationError`:\n * `INVALID_VERIFICATION_CODE` (wrong, or none pending), or `VERIFICATION_CODE_ATTEMPTS_EXCEEDED`\n * once more than `maxAttempts` guesses were made, which discards the code.\n */\n async verify(email: string, verificationCode: string): Promise<void> {\n const codeKey = this.codeKey(email);\n const attemptsKey = this.attemptsKey(email);\n const stored = await this.kv.getItem(codeKey);\n if (typeof stored !== 'string') throw new AuthenticationError('INVALID_VERIFICATION_CODE');\n\n // Count the attempt before comparing: the atomic increment hands every request, parallel ones\n // included, a distinct count, so at most maxAttempts comparisons are ever made against a code.\n const attempts = await this.kv.increment(attemptsKey, EMAIL_VERIFICATION_CODE_TTL);\n if (attempts > this.maxAttempts) {\n // Only the code is discarded. The counter stays until it expires (or a new code is sent):\n // removing it would hand requests that already read the code a fresh budget.\n await this.kv.removeItem(codeKey);\n throw new AuthenticationError('VERIFICATION_CODE_ATTEMPTS_EXCEEDED');\n }\n if (\n stored.length !== verificationCode.length ||\n !timingSafeEqual(Buffer.from(stored), Buffer.from(verificationCode))\n ) {\n throw new AuthenticationError('INVALID_VERIFICATION_CODE');\n }\n await this.kv.removeItem(codeKey);\n await this.kv.removeItem(attemptsKey);\n }\n}\n"],"mappings":";;;;;AAMA,MAAa,8BAA8B;;;;;;;AAU3C,IAAa,yBAAb,MAAoC;CAClC;CACA;CACA;CAEA,YAAY,IAAkB,SAA0D;EACtF,KAAK,KAAK;EACV,KAAK,cAAc,QAAQ;EAC3B,KAAK,iBAAiB,QAAQ;CAChC;CAEA,QAAgB,OAAe;EAC7B,OAAO,sBAAsB;CAC/B;CAEA,YAAoB,OAAe;EACjC,OAAO,+BAA+B;CACxC;CAEA,YAAoB,OAAe;EACjC,OAAO,+BAA+B;CACxC;;;;;;;CAQA,MAAM,KAAK,OAAe,QAAoC;EAC5D,MAAM,cAAc,KAAK,YAAY,KAAK;EAC1C,IAAK,MAAM,KAAK,GAAG,UAAU,aAAa,KAAK,cAAc,IAAK,GAChE,MAAM,kBAAkB,qCAAqC,0BAA0B;EAEzF,MAAM,mBAAmB,UAAU,GAAS,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,GAAG,GAAG;EACxE,IAAI;GAEF,MAAM,OAAO;IAAE;IAAO;GAAiB,CAAC;EAC1C,SAAS,OAAO;GACd,QAAQ,MAAM,0CAA0C,KAAK;GAC7D,MAAM,KAAK,GAAG,WAAW,WAAW;GACpC,MAAM,YAAY,yBAAyB,0CAA0C;EACvF;EAGA,MAAM,KAAK,GAAG,WAAW,KAAK,YAAY,KAAK,CAAC;EAChD,MAAM,KAAK,GAAG,QAAQ,KAAK,QAAQ,KAAK,GAAG,kBAAA,GAA6C;CAC1F;;;;;;CAOA,MAAM,OAAO,OAAe,kBAAyC;EACnE,MAAM,UAAU,KAAK,QAAQ,KAAK;EAClC,MAAM,cAAc,KAAK,YAAY,KAAK;EAC1C,MAAM,SAAS,MAAM,KAAK,GAAG,QAAQ,OAAO;EAC5C,IAAI,OAAO,WAAW,UAAU,MAAM,IAAI,oBAAoB,2BAA2B;EAKzF,IAAI,MADmB,KAAK,GAAG,UAAU,aAAA,GAAwC,IAClE,KAAK,aAAa;GAG/B,MAAM,KAAK,GAAG,WAAW,OAAO;GAChC,MAAM,IAAI,oBAAoB,qCAAqC;EACrE;EACA,IACE,OAAO,WAAW,iBAAiB,UACnC,CAAC,gBAAgB,OAAO,KAAK,MAAM,GAAG,OAAO,KAAK,gBAAgB,CAAC,GAEnE,MAAM,IAAI,oBAAoB,2BAA2B;EAE3D,MAAM,KAAK,GAAG,WAAW,OAAO;EAChC,MAAM,KAAK,GAAG,WAAW,WAAW;CACtC;AACF"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
// src/auth/error.ts
|
|
3
|
+
/** The built-in reasons, for a client's message table or a check that every reason is handled. */
|
|
4
|
+
const AUTHENTICATION_REASONS = [
|
|
5
|
+
"INVALID_VERIFICATION_CODE",
|
|
6
|
+
"VERIFICATION_CODE_ATTEMPTS_EXCEEDED",
|
|
7
|
+
"NONCE_MISSING",
|
|
8
|
+
"NONCE_MISMATCH",
|
|
9
|
+
"INVALID_DESKTOP_CODE",
|
|
10
|
+
"ACCOUNT_DISABLED",
|
|
11
|
+
"ACCOUNT_LOCKED",
|
|
12
|
+
"EMAIL_ALREADY_USED",
|
|
13
|
+
"EMAIL_NOT_VERIFIED",
|
|
14
|
+
"SIGNUP_DISABLED"
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* Thrown by the host's `onAuthorized` to refuse a sign-in it could complete but will not: an
|
|
18
|
+
* address another account holds, a banned user, a closed beta. `reason` is the code the client
|
|
19
|
+
* branches on, `metadata` what it needs to act (the provider to sign in with instead); both
|
|
20
|
+
* travel as they are, so keep them short and free of secrets. `Auth` answers a refusal itself,
|
|
21
|
+
* in the shape of the route it came through:
|
|
22
|
+
*
|
|
23
|
+
* - redirect flow (`loginOAuth2Code`): `errorUri?error=access_denied&reason=<reason>&<metadata>`;
|
|
24
|
+
* - JSON routes (native, One Tap, email, desktop): `403` with a google.rpc error body whose
|
|
25
|
+
* `ErrorInfo` holds `reason` and `metadata`.
|
|
26
|
+
*
|
|
27
|
+
* The SDK throws it too, for a credential it checked itself (a wrong emailed code, a replayed
|
|
28
|
+
* nonce), so a client handles both the same way. Anything else `onAuthorized` throws is a server
|
|
29
|
+
* fault: `server_error` on the redirect, rethrown to the host's error handler on the JSON routes.
|
|
30
|
+
*/
|
|
31
|
+
var AuthenticationError = class AuthenticationError extends Error {
|
|
32
|
+
reason;
|
|
33
|
+
metadata;
|
|
34
|
+
constructor(reason, ...[options]) {
|
|
35
|
+
const { message, cause, metadata = {} } = options ?? {};
|
|
36
|
+
super(message ?? `Sign-in refused: ${reason}`, { cause });
|
|
37
|
+
this.name = "AuthenticationError";
|
|
38
|
+
this.reason = reason;
|
|
39
|
+
this.metadata = {};
|
|
40
|
+
for (const [key, value] of Object.entries(metadata)) if (typeof value === "string") this.metadata[key] = value;
|
|
41
|
+
Object.setPrototypeOf(this, AuthenticationError.prototype);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
exports.AUTHENTICATION_REASONS = AUTHENTICATION_REASONS;
|
|
45
|
+
exports.AuthenticationError = AuthenticationError;
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=error.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.cjs","names":["given"],"sources":["../../src/auth/error.ts"],"sourcesContent":["type Metadata = Record<string, string>;\n/** A reason that carries no metadata. */\ntype NoMetadata = { [key: string]: never };\n\n/**\n * The reasons `onAuthorized` may refuse a sign-in with, each with the metadata it carries (string\n * fields only: metadata travels as URL parameters and JSON). The built-in ones cover the account\n * states every host meets; add your own by augmenting the interface, and `AuthenticationError`\n * accepts exactly these reasons, requiring the metadata when one of its fields is required:\n *\n * ```ts\n * declare module '@shware/security' {\n * interface AuthenticationReasons {\n * CLOSED_BETA: { invite?: string };\n * }\n * }\n * ```\n */\nexport interface AuthenticationReasons {\n // Thrown by the SDK itself.\n /** The emailed code is wrong, or none is pending for the address. */\n INVALID_VERIFICATION_CODE: NoMetadata;\n /** More than `verificationCode.maxAttempts` guesses at the code; it is discarded. */\n VERIFICATION_CODE_ATTEMPTS_EXCEEDED: NoMetadata;\n /** The id token carries no nonce and one is required (`oauth2.onetap.requireNonce`). */\n NONCE_MISSING: NoMetadata;\n /** The id token's nonce was not issued for this provider, or was used already. */\n NONCE_MISMATCH: NoMetadata;\n /** The desktop sign-in code is unknown, expired, used, or its verifier does not match. */\n INVALID_DESKTOP_CODE: NoMetadata;\n\n // For the host's `onAuthorized`.\n /** The account is disabled: banned, or closed by its owner. */\n ACCOUNT_DISABLED: NoMetadata;\n /** The account is locked for now; `until` is when it opens again, when known. */\n ACCOUNT_LOCKED: { until?: string };\n /** The address belongs to another account; `provider` is the one to sign in with instead. */\n EMAIL_ALREADY_USED: { provider?: string };\n /** The host accepts only verified addresses and this provider did not verify it. */\n EMAIL_NOT_VERIFIED: { provider?: string };\n /** No new accounts: registration is closed or by invitation. */\n SIGNUP_DISABLED: NoMetadata;\n}\n\n/** The built-in reasons, for a client's message table or a check that every reason is handled. */\nexport const AUTHENTICATION_REASONS = [\n 'INVALID_VERIFICATION_CODE',\n 'VERIFICATION_CODE_ATTEMPTS_EXCEEDED',\n 'NONCE_MISSING',\n 'NONCE_MISMATCH',\n 'INVALID_DESKTOP_CODE',\n 'ACCOUNT_DISABLED',\n 'ACCOUNT_LOCKED',\n 'EMAIL_ALREADY_USED',\n 'EMAIL_NOT_VERIFIED',\n 'SIGNUP_DISABLED',\n] as const satisfies readonly (keyof AuthenticationReasons)[];\n\n/**\n * Each reason's metadata as declared; a field that is not a string makes the reason unusable\n * (`never`).\n */\ntype Reasons = {\n [K in keyof AuthenticationReasons]: AuthenticationReasons[K] extends Partial<Metadata>\n ? AuthenticationReasons[K]\n : never;\n};\n\n/** A reason `onAuthorized` may refuse with: the keys of `AuthenticationReasons`. */\nexport type AuthenticationReason = keyof Reasons;\n\n/** `true` when the reason's metadata has no required field, so it may be left out. */\ntype Optional<R extends AuthenticationReason> = Reasons[R] extends NoMetadata\n ? true\n : Partial<Reasons[R]> extends Reasons[R]\n ? true\n : false;\ntype Options<R extends AuthenticationReason> = {\n message?: string;\n cause?: unknown;\n} & (Optional<R> extends true ? { metadata?: Reasons[R] } : { metadata: Reasons[R] });\n\n/**\n * Thrown by the host's `onAuthorized` to refuse a sign-in it could complete but will not: an\n * address another account holds, a banned user, a closed beta. `reason` is the code the client\n * branches on, `metadata` what it needs to act (the provider to sign in with instead); both\n * travel as they are, so keep them short and free of secrets. `Auth` answers a refusal itself,\n * in the shape of the route it came through:\n *\n * - redirect flow (`loginOAuth2Code`): `errorUri?error=access_denied&reason=<reason>&<metadata>`;\n * - JSON routes (native, One Tap, email, desktop): `403` with a google.rpc error body whose\n * `ErrorInfo` holds `reason` and `metadata`.\n *\n * The SDK throws it too, for a credential it checked itself (a wrong emailed code, a replayed\n * nonce), so a client handles both the same way. Anything else `onAuthorized` throws is a server\n * fault: `server_error` on the redirect, rethrown to the host's error handler on the JSON routes.\n */\nexport class AuthenticationError<\n R extends AuthenticationReason = AuthenticationReason,\n> extends Error {\n readonly reason: R;\n readonly metadata: Metadata;\n\n constructor(reason: R, ...[options]: Optional<R> extends true ? [Options<R>?] : [Options<R>]) {\n const given = options;\n const { message, cause, metadata = {} } = given ?? {};\n super(message ?? `Sign-in refused: ${reason}`, { cause });\n this.name = 'AuthenticationError';\n this.reason = reason;\n this.metadata = {};\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === 'string') this.metadata[key] = value;\n }\n Object.setPrototypeOf(this, AuthenticationError.prototype);\n }\n}\n"],"mappings":";;;AA6CA,MAAa,yBAAyB;CACpC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;;;;AAyCA,IAAa,sBAAb,MAAa,4BAEH,MAAM;CACd;CACA;CAEA,YAAY,QAAW,GAAG,CAAC,UAAmE;EAE5F,MAAM,EAAE,SAAS,OAAO,WAAW,CAAC,MAAMA,WAAS,CAAC;EACpD,MAAM,WAAW,oBAAoB,UAAU,EAAE,MAAM,CAAC;EACxD,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,WAAW,CAAC;EACjB,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,GAChD,IAAI,OAAO,UAAU,UAAU,KAAK,SAAS,OAAO;EAEtD,OAAO,eAAe,MAAM,oBAAoB,SAAS;CAC3D;AACF"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
type Metadata = Record<string, string>;
|
|
2
|
+
/** A reason that carries no metadata. */
|
|
3
|
+
type NoMetadata = {
|
|
4
|
+
[key: string]: never;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* The reasons `onAuthorized` may refuse a sign-in with, each with the metadata it carries (string
|
|
8
|
+
* fields only: metadata travels as URL parameters and JSON). The built-in ones cover the account
|
|
9
|
+
* states every host meets; add your own by augmenting the interface, and `AuthenticationError`
|
|
10
|
+
* accepts exactly these reasons, requiring the metadata when one of its fields is required:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* declare module '@shware/security' {
|
|
14
|
+
* interface AuthenticationReasons {
|
|
15
|
+
* CLOSED_BETA: { invite?: string };
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export interface AuthenticationReasons {
|
|
21
|
+
/** The emailed code is wrong, or none is pending for the address. */
|
|
22
|
+
INVALID_VERIFICATION_CODE: NoMetadata;
|
|
23
|
+
/** More than `verificationCode.maxAttempts` guesses at the code; it is discarded. */
|
|
24
|
+
VERIFICATION_CODE_ATTEMPTS_EXCEEDED: NoMetadata;
|
|
25
|
+
/** The id token carries no nonce and one is required (`oauth2.onetap.requireNonce`). */
|
|
26
|
+
NONCE_MISSING: NoMetadata;
|
|
27
|
+
/** The id token's nonce was not issued for this provider, or was used already. */
|
|
28
|
+
NONCE_MISMATCH: NoMetadata;
|
|
29
|
+
/** The desktop sign-in code is unknown, expired, used, or its verifier does not match. */
|
|
30
|
+
INVALID_DESKTOP_CODE: NoMetadata;
|
|
31
|
+
/** The account is disabled: banned, or closed by its owner. */
|
|
32
|
+
ACCOUNT_DISABLED: NoMetadata;
|
|
33
|
+
/** The account is locked for now; `until` is when it opens again, when known. */
|
|
34
|
+
ACCOUNT_LOCKED: {
|
|
35
|
+
until?: string;
|
|
36
|
+
};
|
|
37
|
+
/** The address belongs to another account; `provider` is the one to sign in with instead. */
|
|
38
|
+
EMAIL_ALREADY_USED: {
|
|
39
|
+
provider?: string;
|
|
40
|
+
};
|
|
41
|
+
/** The host accepts only verified addresses and this provider did not verify it. */
|
|
42
|
+
EMAIL_NOT_VERIFIED: {
|
|
43
|
+
provider?: string;
|
|
44
|
+
};
|
|
45
|
+
/** No new accounts: registration is closed or by invitation. */
|
|
46
|
+
SIGNUP_DISABLED: NoMetadata;
|
|
47
|
+
}
|
|
48
|
+
/** The built-in reasons, for a client's message table or a check that every reason is handled. */
|
|
49
|
+
export declare const AUTHENTICATION_REASONS: readonly ["INVALID_VERIFICATION_CODE", "VERIFICATION_CODE_ATTEMPTS_EXCEEDED", "NONCE_MISSING", "NONCE_MISMATCH", "INVALID_DESKTOP_CODE", "ACCOUNT_DISABLED", "ACCOUNT_LOCKED", "EMAIL_ALREADY_USED", "EMAIL_NOT_VERIFIED", "SIGNUP_DISABLED"];
|
|
50
|
+
/**
|
|
51
|
+
* Each reason's metadata as declared; a field that is not a string makes the reason unusable
|
|
52
|
+
* (`never`).
|
|
53
|
+
*/
|
|
54
|
+
type Reasons = { [K in keyof AuthenticationReasons]: AuthenticationReasons[K] extends Partial<Metadata> ? AuthenticationReasons[K] : never; };
|
|
55
|
+
/** A reason `onAuthorized` may refuse with: the keys of `AuthenticationReasons`. */
|
|
56
|
+
export type AuthenticationReason = keyof Reasons;
|
|
57
|
+
/** `true` when the reason's metadata has no required field, so it may be left out. */
|
|
58
|
+
type Optional<R extends AuthenticationReason> = Reasons[R] extends NoMetadata ? true : Partial<Reasons[R]> extends Reasons[R] ? true : false;
|
|
59
|
+
type Options<R extends AuthenticationReason> = {
|
|
60
|
+
message?: string;
|
|
61
|
+
cause?: unknown;
|
|
62
|
+
} & (Optional<R> extends true ? {
|
|
63
|
+
metadata?: Reasons[R];
|
|
64
|
+
} : {
|
|
65
|
+
metadata: Reasons[R];
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Thrown by the host's `onAuthorized` to refuse a sign-in it could complete but will not: an
|
|
69
|
+
* address another account holds, a banned user, a closed beta. `reason` is the code the client
|
|
70
|
+
* branches on, `metadata` what it needs to act (the provider to sign in with instead); both
|
|
71
|
+
* travel as they are, so keep them short and free of secrets. `Auth` answers a refusal itself,
|
|
72
|
+
* in the shape of the route it came through:
|
|
73
|
+
*
|
|
74
|
+
* - redirect flow (`loginOAuth2Code`): `errorUri?error=access_denied&reason=<reason>&<metadata>`;
|
|
75
|
+
* - JSON routes (native, One Tap, email, desktop): `403` with a google.rpc error body whose
|
|
76
|
+
* `ErrorInfo` holds `reason` and `metadata`.
|
|
77
|
+
*
|
|
78
|
+
* The SDK throws it too, for a credential it checked itself (a wrong emailed code, a replayed
|
|
79
|
+
* nonce), so a client handles both the same way. Anything else `onAuthorized` throws is a server
|
|
80
|
+
* fault: `server_error` on the redirect, rethrown to the host's error handler on the JSON routes.
|
|
81
|
+
*/
|
|
82
|
+
export declare class AuthenticationError<R extends AuthenticationReason = AuthenticationReason> extends Error {
|
|
83
|
+
readonly reason: R;
|
|
84
|
+
readonly metadata: Metadata;
|
|
85
|
+
constructor(reason: R, ...[options]: Optional<R> extends true ? [Options<R>?] : [Options<R>]);
|
|
86
|
+
}
|
|
87
|
+
export {};
|
|
88
|
+
//# sourceMappingURL=error.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.d.cts","names":[],"sources":["../../src/auth/error.ts"],"mappings":"A,KAAK,WAAW;;KAEX;GAAgB;;;;;;;;;;;;;;;;iBAgBJ;;EAGf,2BAA2B;;EAE3B,qCAAqC;;EAErC,eAAe;;EAEf,gBAAgB;;EAEhB,sBAAsB;;EAItB,kBAAkB;;EAElB;IAAkB;;;EAElB;IAAsB;;;EAEtB;IAAsB;;;EAEtB,iBAAiB;;;qBAIN;;;;;KAiBR,aACF,WAAW,wBAAwB,sBAAsB,WAAW,QAAQ,YACzE,sBAAsB;;YAKhB,6BAA6B;;KAGpC,SAAS,UAAU,wBAAwB,QAAQ,WAAW,oBAE/D,QAAQ,QAAQ,YAAY,QAAQ;KAGnC,QAAQ,UAAU;EACrB;EACA;KACG,SAAS;EAAoB,WAAW,QAAQ;;EAAS,UAAU,QAAQ;;;;;;;;;;;;;;;;;qBAiBnE,oBACX,UAAU,uBAAuB,8BACzB;WACC,QAAQ;WACR,UAAU;EAEnB,YAAY,QAAQ,OAAO,UAAU,SAAS,mBAAmB,QAAQ,QAAQ,QAAQ"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
type Metadata = Record<string, string>;
|
|
2
|
+
/** A reason that carries no metadata. */
|
|
3
|
+
type NoMetadata = {
|
|
4
|
+
[key: string]: never;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* The reasons `onAuthorized` may refuse a sign-in with, each with the metadata it carries (string
|
|
8
|
+
* fields only: metadata travels as URL parameters and JSON). The built-in ones cover the account
|
|
9
|
+
* states every host meets; add your own by augmenting the interface, and `AuthenticationError`
|
|
10
|
+
* accepts exactly these reasons, requiring the metadata when one of its fields is required:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* declare module '@shware/security' {
|
|
14
|
+
* interface AuthenticationReasons {
|
|
15
|
+
* CLOSED_BETA: { invite?: string };
|
|
16
|
+
* }
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export interface AuthenticationReasons {
|
|
21
|
+
/** The emailed code is wrong, or none is pending for the address. */
|
|
22
|
+
INVALID_VERIFICATION_CODE: NoMetadata;
|
|
23
|
+
/** More than `verificationCode.maxAttempts` guesses at the code; it is discarded. */
|
|
24
|
+
VERIFICATION_CODE_ATTEMPTS_EXCEEDED: NoMetadata;
|
|
25
|
+
/** The id token carries no nonce and one is required (`oauth2.onetap.requireNonce`). */
|
|
26
|
+
NONCE_MISSING: NoMetadata;
|
|
27
|
+
/** The id token's nonce was not issued for this provider, or was used already. */
|
|
28
|
+
NONCE_MISMATCH: NoMetadata;
|
|
29
|
+
/** The desktop sign-in code is unknown, expired, used, or its verifier does not match. */
|
|
30
|
+
INVALID_DESKTOP_CODE: NoMetadata;
|
|
31
|
+
/** The account is disabled: banned, or closed by its owner. */
|
|
32
|
+
ACCOUNT_DISABLED: NoMetadata;
|
|
33
|
+
/** The account is locked for now; `until` is when it opens again, when known. */
|
|
34
|
+
ACCOUNT_LOCKED: {
|
|
35
|
+
until?: string;
|
|
36
|
+
};
|
|
37
|
+
/** The address belongs to another account; `provider` is the one to sign in with instead. */
|
|
38
|
+
EMAIL_ALREADY_USED: {
|
|
39
|
+
provider?: string;
|
|
40
|
+
};
|
|
41
|
+
/** The host accepts only verified addresses and this provider did not verify it. */
|
|
42
|
+
EMAIL_NOT_VERIFIED: {
|
|
43
|
+
provider?: string;
|
|
44
|
+
};
|
|
45
|
+
/** No new accounts: registration is closed or by invitation. */
|
|
46
|
+
SIGNUP_DISABLED: NoMetadata;
|
|
47
|
+
}
|
|
48
|
+
/** The built-in reasons, for a client's message table or a check that every reason is handled. */
|
|
49
|
+
export declare const AUTHENTICATION_REASONS: readonly ["INVALID_VERIFICATION_CODE", "VERIFICATION_CODE_ATTEMPTS_EXCEEDED", "NONCE_MISSING", "NONCE_MISMATCH", "INVALID_DESKTOP_CODE", "ACCOUNT_DISABLED", "ACCOUNT_LOCKED", "EMAIL_ALREADY_USED", "EMAIL_NOT_VERIFIED", "SIGNUP_DISABLED"];
|
|
50
|
+
/**
|
|
51
|
+
* Each reason's metadata as declared; a field that is not a string makes the reason unusable
|
|
52
|
+
* (`never`).
|
|
53
|
+
*/
|
|
54
|
+
type Reasons = { [K in keyof AuthenticationReasons]: AuthenticationReasons[K] extends Partial<Metadata> ? AuthenticationReasons[K] : never; };
|
|
55
|
+
/** A reason `onAuthorized` may refuse with: the keys of `AuthenticationReasons`. */
|
|
56
|
+
export type AuthenticationReason = keyof Reasons;
|
|
57
|
+
/** `true` when the reason's metadata has no required field, so it may be left out. */
|
|
58
|
+
type Optional<R extends AuthenticationReason> = Reasons[R] extends NoMetadata ? true : Partial<Reasons[R]> extends Reasons[R] ? true : false;
|
|
59
|
+
type Options<R extends AuthenticationReason> = {
|
|
60
|
+
message?: string;
|
|
61
|
+
cause?: unknown;
|
|
62
|
+
} & (Optional<R> extends true ? {
|
|
63
|
+
metadata?: Reasons[R];
|
|
64
|
+
} : {
|
|
65
|
+
metadata: Reasons[R];
|
|
66
|
+
});
|
|
67
|
+
/**
|
|
68
|
+
* Thrown by the host's `onAuthorized` to refuse a sign-in it could complete but will not: an
|
|
69
|
+
* address another account holds, a banned user, a closed beta. `reason` is the code the client
|
|
70
|
+
* branches on, `metadata` what it needs to act (the provider to sign in with instead); both
|
|
71
|
+
* travel as they are, so keep them short and free of secrets. `Auth` answers a refusal itself,
|
|
72
|
+
* in the shape of the route it came through:
|
|
73
|
+
*
|
|
74
|
+
* - redirect flow (`loginOAuth2Code`): `errorUri?error=access_denied&reason=<reason>&<metadata>`;
|
|
75
|
+
* - JSON routes (native, One Tap, email, desktop): `403` with a google.rpc error body whose
|
|
76
|
+
* `ErrorInfo` holds `reason` and `metadata`.
|
|
77
|
+
*
|
|
78
|
+
* The SDK throws it too, for a credential it checked itself (a wrong emailed code, a replayed
|
|
79
|
+
* nonce), so a client handles both the same way. Anything else `onAuthorized` throws is a server
|
|
80
|
+
* fault: `server_error` on the redirect, rethrown to the host's error handler on the JSON routes.
|
|
81
|
+
*/
|
|
82
|
+
export declare class AuthenticationError<R extends AuthenticationReason = AuthenticationReason> extends Error {
|
|
83
|
+
readonly reason: R;
|
|
84
|
+
readonly metadata: Metadata;
|
|
85
|
+
constructor(reason: R, ...[options]: Optional<R> extends true ? [Options<R>?] : [Options<R>]);
|
|
86
|
+
}
|
|
87
|
+
export {};
|
|
88
|
+
//# sourceMappingURL=error.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.d.mts","names":[],"sources":["../../src/auth/error.ts"],"mappings":"A,KAAK,WAAW;;KAEX;GAAgB;;;;;;;;;;;;;;;;iBAgBJ;;EAGf,2BAA2B;;EAE3B,qCAAqC;;EAErC,eAAe;;EAEf,gBAAgB;;EAEhB,sBAAsB;;EAItB,kBAAkB;;EAElB;IAAkB;;;EAElB;IAAsB;;;EAEtB;IAAsB;;;EAEtB,iBAAiB;;;qBAIN;;;;;KAiBR,aACF,WAAW,wBAAwB,sBAAsB,WAAW,QAAQ,YACzE,sBAAsB;;YAKhB,6BAA6B;;KAGpC,SAAS,UAAU,wBAAwB,QAAQ,WAAW,oBAE/D,QAAQ,QAAQ,YAAY,QAAQ;KAGnC,QAAQ,UAAU;EACrB;EACA;KACG,SAAS;EAAoB,WAAW,QAAQ;;EAAS,UAAU,QAAQ;;;;;;;;;;;;;;;;;qBAiBnE,oBACX,UAAU,uBAAuB,8BACzB;WACC,QAAQ;WACR,UAAU;EAEnB,YAAY,QAAQ,OAAO,UAAU,SAAS,mBAAmB,QAAQ,QAAQ,QAAQ"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/auth/error.ts
|
|
2
|
+
/** The built-in reasons, for a client's message table or a check that every reason is handled. */
|
|
3
|
+
const AUTHENTICATION_REASONS = [
|
|
4
|
+
"INVALID_VERIFICATION_CODE",
|
|
5
|
+
"VERIFICATION_CODE_ATTEMPTS_EXCEEDED",
|
|
6
|
+
"NONCE_MISSING",
|
|
7
|
+
"NONCE_MISMATCH",
|
|
8
|
+
"INVALID_DESKTOP_CODE",
|
|
9
|
+
"ACCOUNT_DISABLED",
|
|
10
|
+
"ACCOUNT_LOCKED",
|
|
11
|
+
"EMAIL_ALREADY_USED",
|
|
12
|
+
"EMAIL_NOT_VERIFIED",
|
|
13
|
+
"SIGNUP_DISABLED"
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Thrown by the host's `onAuthorized` to refuse a sign-in it could complete but will not: an
|
|
17
|
+
* address another account holds, a banned user, a closed beta. `reason` is the code the client
|
|
18
|
+
* branches on, `metadata` what it needs to act (the provider to sign in with instead); both
|
|
19
|
+
* travel as they are, so keep them short and free of secrets. `Auth` answers a refusal itself,
|
|
20
|
+
* in the shape of the route it came through:
|
|
21
|
+
*
|
|
22
|
+
* - redirect flow (`loginOAuth2Code`): `errorUri?error=access_denied&reason=<reason>&<metadata>`;
|
|
23
|
+
* - JSON routes (native, One Tap, email, desktop): `403` with a google.rpc error body whose
|
|
24
|
+
* `ErrorInfo` holds `reason` and `metadata`.
|
|
25
|
+
*
|
|
26
|
+
* The SDK throws it too, for a credential it checked itself (a wrong emailed code, a replayed
|
|
27
|
+
* nonce), so a client handles both the same way. Anything else `onAuthorized` throws is a server
|
|
28
|
+
* fault: `server_error` on the redirect, rethrown to the host's error handler on the JSON routes.
|
|
29
|
+
*/
|
|
30
|
+
var AuthenticationError = class AuthenticationError extends Error {
|
|
31
|
+
reason;
|
|
32
|
+
metadata;
|
|
33
|
+
constructor(reason, ...[options]) {
|
|
34
|
+
const { message, cause, metadata = {} } = options ?? {};
|
|
35
|
+
super(message ?? `Sign-in refused: ${reason}`, { cause });
|
|
36
|
+
this.name = "AuthenticationError";
|
|
37
|
+
this.reason = reason;
|
|
38
|
+
this.metadata = {};
|
|
39
|
+
for (const [key, value] of Object.entries(metadata)) if (typeof value === "string") this.metadata[key] = value;
|
|
40
|
+
Object.setPrototypeOf(this, AuthenticationError.prototype);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
export { AUTHENTICATION_REASONS, AuthenticationError };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=error.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"error.mjs","names":["given"],"sources":["../../src/auth/error.ts"],"sourcesContent":["type Metadata = Record<string, string>;\n/** A reason that carries no metadata. */\ntype NoMetadata = { [key: string]: never };\n\n/**\n * The reasons `onAuthorized` may refuse a sign-in with, each with the metadata it carries (string\n * fields only: metadata travels as URL parameters and JSON). The built-in ones cover the account\n * states every host meets; add your own by augmenting the interface, and `AuthenticationError`\n * accepts exactly these reasons, requiring the metadata when one of its fields is required:\n *\n * ```ts\n * declare module '@shware/security' {\n * interface AuthenticationReasons {\n * CLOSED_BETA: { invite?: string };\n * }\n * }\n * ```\n */\nexport interface AuthenticationReasons {\n // Thrown by the SDK itself.\n /** The emailed code is wrong, or none is pending for the address. */\n INVALID_VERIFICATION_CODE: NoMetadata;\n /** More than `verificationCode.maxAttempts` guesses at the code; it is discarded. */\n VERIFICATION_CODE_ATTEMPTS_EXCEEDED: NoMetadata;\n /** The id token carries no nonce and one is required (`oauth2.onetap.requireNonce`). */\n NONCE_MISSING: NoMetadata;\n /** The id token's nonce was not issued for this provider, or was used already. */\n NONCE_MISMATCH: NoMetadata;\n /** The desktop sign-in code is unknown, expired, used, or its verifier does not match. */\n INVALID_DESKTOP_CODE: NoMetadata;\n\n // For the host's `onAuthorized`.\n /** The account is disabled: banned, or closed by its owner. */\n ACCOUNT_DISABLED: NoMetadata;\n /** The account is locked for now; `until` is when it opens again, when known. */\n ACCOUNT_LOCKED: { until?: string };\n /** The address belongs to another account; `provider` is the one to sign in with instead. */\n EMAIL_ALREADY_USED: { provider?: string };\n /** The host accepts only verified addresses and this provider did not verify it. */\n EMAIL_NOT_VERIFIED: { provider?: string };\n /** No new accounts: registration is closed or by invitation. */\n SIGNUP_DISABLED: NoMetadata;\n}\n\n/** The built-in reasons, for a client's message table or a check that every reason is handled. */\nexport const AUTHENTICATION_REASONS = [\n 'INVALID_VERIFICATION_CODE',\n 'VERIFICATION_CODE_ATTEMPTS_EXCEEDED',\n 'NONCE_MISSING',\n 'NONCE_MISMATCH',\n 'INVALID_DESKTOP_CODE',\n 'ACCOUNT_DISABLED',\n 'ACCOUNT_LOCKED',\n 'EMAIL_ALREADY_USED',\n 'EMAIL_NOT_VERIFIED',\n 'SIGNUP_DISABLED',\n] as const satisfies readonly (keyof AuthenticationReasons)[];\n\n/**\n * Each reason's metadata as declared; a field that is not a string makes the reason unusable\n * (`never`).\n */\ntype Reasons = {\n [K in keyof AuthenticationReasons]: AuthenticationReasons[K] extends Partial<Metadata>\n ? AuthenticationReasons[K]\n : never;\n};\n\n/** A reason `onAuthorized` may refuse with: the keys of `AuthenticationReasons`. */\nexport type AuthenticationReason = keyof Reasons;\n\n/** `true` when the reason's metadata has no required field, so it may be left out. */\ntype Optional<R extends AuthenticationReason> = Reasons[R] extends NoMetadata\n ? true\n : Partial<Reasons[R]> extends Reasons[R]\n ? true\n : false;\ntype Options<R extends AuthenticationReason> = {\n message?: string;\n cause?: unknown;\n} & (Optional<R> extends true ? { metadata?: Reasons[R] } : { metadata: Reasons[R] });\n\n/**\n * Thrown by the host's `onAuthorized` to refuse a sign-in it could complete but will not: an\n * address another account holds, a banned user, a closed beta. `reason` is the code the client\n * branches on, `metadata` what it needs to act (the provider to sign in with instead); both\n * travel as they are, so keep them short and free of secrets. `Auth` answers a refusal itself,\n * in the shape of the route it came through:\n *\n * - redirect flow (`loginOAuth2Code`): `errorUri?error=access_denied&reason=<reason>&<metadata>`;\n * - JSON routes (native, One Tap, email, desktop): `403` with a google.rpc error body whose\n * `ErrorInfo` holds `reason` and `metadata`.\n *\n * The SDK throws it too, for a credential it checked itself (a wrong emailed code, a replayed\n * nonce), so a client handles both the same way. Anything else `onAuthorized` throws is a server\n * fault: `server_error` on the redirect, rethrown to the host's error handler on the JSON routes.\n */\nexport class AuthenticationError<\n R extends AuthenticationReason = AuthenticationReason,\n> extends Error {\n readonly reason: R;\n readonly metadata: Metadata;\n\n constructor(reason: R, ...[options]: Optional<R> extends true ? [Options<R>?] : [Options<R>]) {\n const given = options;\n const { message, cause, metadata = {} } = given ?? {};\n super(message ?? `Sign-in refused: ${reason}`, { cause });\n this.name = 'AuthenticationError';\n this.reason = reason;\n this.metadata = {};\n for (const [key, value] of Object.entries(metadata)) {\n if (typeof value === 'string') this.metadata[key] = value;\n }\n Object.setPrototypeOf(this, AuthenticationError.prototype);\n }\n}\n"],"mappings":";;AA6CA,MAAa,yBAAyB;CACpC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;;;;;;;;;;;;AAyCA,IAAa,sBAAb,MAAa,4BAEH,MAAM;CACd;CACA;CAEA,YAAY,QAAW,GAAG,CAAC,UAAmE;EAE5F,MAAM,EAAE,SAAS,OAAO,WAAW,CAAC,MAAMA,WAAS,CAAC;EACpD,MAAM,WAAW,oBAAoB,UAAU,EAAE,MAAM,CAAC;EACxD,KAAK,OAAO;EACZ,KAAK,SAAS;EACd,KAAK,WAAW,CAAC;EACjB,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,GAChD,IAAI,OAAO,UAAU,UAAU,KAAK,SAAS,OAAO;EAEtD,OAAO,eAAe,MAAM,oBAAoB,SAAS;CAC3D;AACF"}
|