@draftlab/auth 0.15.1 → 0.16.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/esm/allow.js +26 -0
- package/dist/esm/client.js +254 -0
- package/dist/esm/core.js +597 -0
- package/dist/esm/css.d.js +0 -0
- package/dist/esm/error.js +88 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/keys.js +126 -0
- package/dist/esm/mutex.js +53 -0
- package/dist/esm/pkce.js +87 -0
- package/dist/esm/provider/apple.js +15 -0
- package/dist/esm/provider/code.js +62 -0
- package/dist/esm/provider/discord.js +15 -0
- package/dist/esm/provider/facebook.js +15 -0
- package/dist/esm/provider/github.js +15 -0
- package/dist/esm/provider/gitlab.js +15 -0
- package/dist/esm/provider/google.js +16 -0
- package/dist/esm/provider/linkedin.js +15 -0
- package/dist/esm/provider/magiclink.js +83 -0
- package/dist/esm/provider/microsoft.js +15 -0
- package/dist/esm/provider/oauth2.js +130 -0
- package/dist/esm/provider/password.js +331 -0
- package/dist/esm/provider/provider.js +18 -0
- package/dist/esm/provider/reddit.js +15 -0
- package/dist/esm/provider/slack.js +15 -0
- package/dist/esm/provider/spotify.js +15 -0
- package/dist/esm/provider/twitch.js +15 -0
- package/dist/esm/provider/vercel.js +17 -0
- package/dist/esm/random.js +40 -0
- package/dist/esm/revocation.js +27 -0
- package/dist/esm/storage/memory.js +110 -0
- package/dist/esm/storage/storage.js +56 -0
- package/dist/esm/storage/turso.js +93 -0
- package/dist/esm/storage/unstorage.js +78 -0
- package/dist/esm/subject.js +7 -0
- package/dist/esm/themes/theme.js +115 -0
- package/dist/esm/toolkit/client.js +119 -0
- package/dist/esm/toolkit/index.js +25 -0
- package/dist/esm/toolkit/providers/facebook.js +11 -0
- package/dist/esm/toolkit/providers/github.js +11 -0
- package/dist/esm/toolkit/providers/google.js +11 -0
- package/dist/esm/toolkit/providers/strategy.js +0 -0
- package/dist/esm/toolkit/storage.js +81 -0
- package/dist/esm/toolkit/utils.js +18 -0
- package/dist/esm/types.js +0 -0
- package/dist/esm/ui/base.js +478 -0
- package/dist/esm/ui/code.js +186 -0
- package/dist/esm/ui/form.js +46 -0
- package/dist/esm/ui/icon.js +242 -0
- package/dist/esm/ui/magiclink.js +158 -0
- package/dist/esm/ui/password.js +435 -0
- package/dist/esm/ui/select.js +102 -0
- package/dist/esm/util.js +59 -0
- package/dist/{allow.d.mts → types/allow.d.ts} +9 -11
- package/dist/types/allow.d.ts.map +1 -0
- package/dist/types/client.d.ts +462 -0
- package/dist/types/client.d.ts.map +1 -0
- package/dist/types/core.d.ts +113 -0
- package/dist/types/core.d.ts.map +1 -0
- package/dist/{error.d.mts → types/error.d.ts} +95 -97
- package/dist/types/error.d.ts.map +1 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/{keys.d.mts → types/keys.d.ts} +20 -23
- package/dist/types/keys.d.ts.map +1 -0
- package/dist/types/mutex.d.ts +42 -0
- package/dist/types/mutex.d.ts.map +1 -0
- package/dist/{pkce.d.mts → types/pkce.d.ts} +10 -11
- package/dist/types/pkce.d.ts.map +1 -0
- package/dist/types/provider/apple.d.ts +197 -0
- package/dist/types/provider/apple.d.ts.map +1 -0
- package/dist/types/provider/code.d.ts +288 -0
- package/dist/types/provider/code.d.ts.map +1 -0
- package/dist/types/provider/discord.d.ts +206 -0
- package/dist/types/provider/discord.d.ts.map +1 -0
- package/dist/types/provider/facebook.d.ts +200 -0
- package/dist/types/provider/facebook.d.ts.map +1 -0
- package/dist/types/provider/github.d.ts +220 -0
- package/dist/types/provider/github.d.ts.map +1 -0
- package/dist/types/provider/gitlab.d.ts +180 -0
- package/dist/types/provider/gitlab.d.ts.map +1 -0
- package/dist/types/provider/google.d.ts +158 -0
- package/dist/types/provider/google.d.ts.map +1 -0
- package/dist/types/provider/linkedin.d.ts +190 -0
- package/dist/types/provider/linkedin.d.ts.map +1 -0
- package/dist/types/provider/magiclink.d.ts +141 -0
- package/dist/types/provider/magiclink.d.ts.map +1 -0
- package/dist/types/provider/microsoft.d.ts +247 -0
- package/dist/types/provider/microsoft.d.ts.map +1 -0
- package/dist/types/provider/oauth2.d.ts +229 -0
- package/dist/types/provider/oauth2.d.ts.map +1 -0
- package/dist/types/provider/password.d.ts +408 -0
- package/dist/types/provider/password.d.ts.map +1 -0
- package/dist/types/provider/provider.d.ts +226 -0
- package/dist/types/provider/provider.d.ts.map +1 -0
- package/dist/types/provider/reddit.d.ts +159 -0
- package/dist/types/provider/reddit.d.ts.map +1 -0
- package/dist/types/provider/slack.d.ts +171 -0
- package/dist/types/provider/slack.d.ts.map +1 -0
- package/dist/types/provider/spotify.d.ts +168 -0
- package/dist/types/provider/spotify.d.ts.map +1 -0
- package/dist/types/provider/twitch.d.ts +163 -0
- package/dist/types/provider/twitch.d.ts.map +1 -0
- package/dist/types/provider/vercel.d.ts +294 -0
- package/dist/types/provider/vercel.d.ts.map +1 -0
- package/dist/{random.d.mts → types/random.d.ts} +4 -6
- package/dist/types/random.d.ts.map +1 -0
- package/dist/types/revocation.d.ts +76 -0
- package/dist/types/revocation.d.ts.map +1 -0
- package/dist/{storage/memory.d.mts → types/storage/memory.d.ts} +17 -20
- package/dist/types/storage/memory.d.ts.map +1 -0
- package/dist/types/storage/storage.d.ts +177 -0
- package/dist/types/storage/storage.d.ts.map +1 -0
- package/dist/{storage/turso.d.mts → types/storage/turso.d.ts} +4 -7
- package/dist/types/storage/turso.d.ts.map +1 -0
- package/dist/{storage/unstorage.d.mts → types/storage/unstorage.d.ts} +12 -10
- package/dist/types/storage/unstorage.d.ts.map +1 -0
- package/dist/types/subject.d.ts +115 -0
- package/dist/types/subject.d.ts.map +1 -0
- package/dist/types/themes/theme.d.ts +207 -0
- package/dist/types/themes/theme.d.ts.map +1 -0
- package/dist/types/toolkit/client.d.ts +235 -0
- package/dist/types/toolkit/client.d.ts.map +1 -0
- package/dist/types/toolkit/index.d.ts +45 -0
- package/dist/types/toolkit/index.d.ts.map +1 -0
- package/dist/types/toolkit/providers/facebook.d.ts +8 -0
- package/dist/types/toolkit/providers/facebook.d.ts.map +1 -0
- package/dist/types/toolkit/providers/github.d.ts +8 -0
- package/dist/types/toolkit/providers/github.d.ts.map +1 -0
- package/dist/types/toolkit/providers/google.d.ts +8 -0
- package/dist/types/toolkit/providers/google.d.ts.map +1 -0
- package/dist/types/toolkit/providers/strategy.d.ts +38 -0
- package/dist/types/toolkit/providers/strategy.d.ts.map +1 -0
- package/dist/{toolkit/storage.d.mts → types/toolkit/storage.d.ts} +37 -39
- package/dist/types/toolkit/storage.d.ts.map +1 -0
- package/dist/{toolkit/utils.d.mts → types/toolkit/utils.d.ts} +2 -4
- package/dist/types/toolkit/utils.d.ts.map +1 -0
- package/dist/types/types.d.ts +92 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/ui/base.d.ts +18 -0
- package/dist/types/ui/base.d.ts.map +1 -0
- package/dist/types/ui/code.d.ts +43 -0
- package/dist/types/ui/code.d.ts.map +1 -0
- package/dist/types/ui/form.d.ts +24 -0
- package/dist/types/ui/form.d.ts.map +1 -0
- package/dist/types/ui/icon.d.ts +60 -0
- package/dist/types/ui/icon.d.ts.map +1 -0
- package/dist/types/ui/magiclink.d.ts +41 -0
- package/dist/types/ui/magiclink.d.ts.map +1 -0
- package/dist/types/ui/password.d.ts +43 -0
- package/dist/types/ui/password.d.ts.map +1 -0
- package/dist/types/ui/select.d.ts +33 -0
- package/dist/types/ui/select.d.ts.map +1 -0
- package/dist/{util.d.mts → types/util.d.ts} +11 -12
- package/dist/types/util.d.ts.map +1 -0
- package/package.json +10 -16
- package/dist/adapters/node.d.mts +0 -17
- package/dist/adapters/node.mjs +0 -69
- package/dist/allow.mjs +0 -63
- package/dist/client.d.mts +0 -462
- package/dist/client.mjs +0 -284
- package/dist/core.d.mts +0 -109
- package/dist/core.mjs +0 -595
- package/dist/error.mjs +0 -237
- package/dist/index.d.mts +0 -2
- package/dist/index.mjs +0 -3
- package/dist/keys.mjs +0 -146
- package/dist/mutex.d.mts +0 -44
- package/dist/mutex.mjs +0 -110
- package/dist/pkce.mjs +0 -157
- package/dist/provider/apple.d.mts +0 -110
- package/dist/provider/apple.mjs +0 -164
- package/dist/provider/code.d.mts +0 -218
- package/dist/provider/code.mjs +0 -246
- package/dist/provider/discord.d.mts +0 -145
- package/dist/provider/discord.mjs +0 -156
- package/dist/provider/facebook.d.mts +0 -141
- package/dist/provider/facebook.mjs +0 -150
- package/dist/provider/github.d.mts +0 -139
- package/dist/provider/github.mjs +0 -169
- package/dist/provider/gitlab.d.mts +0 -105
- package/dist/provider/gitlab.mjs +0 -147
- package/dist/provider/google.d.mts +0 -111
- package/dist/provider/google.mjs +0 -109
- package/dist/provider/linkedin.d.mts +0 -131
- package/dist/provider/linkedin.mjs +0 -142
- package/dist/provider/magiclink.d.mts +0 -79
- package/dist/provider/magiclink.mjs +0 -143
- package/dist/provider/microsoft.d.mts +0 -177
- package/dist/provider/microsoft.mjs +0 -177
- package/dist/provider/oauth2.d.mts +0 -175
- package/dist/provider/oauth2.mjs +0 -222
- package/dist/provider/passkey.d.mts +0 -103
- package/dist/provider/passkey.mjs +0 -320
- package/dist/provider/password.d.mts +0 -384
- package/dist/provider/password.mjs +0 -363
- package/dist/provider/provider.d.mts +0 -225
- package/dist/provider/provider.mjs +0 -44
- package/dist/provider/reddit.d.mts +0 -106
- package/dist/provider/reddit.mjs +0 -127
- package/dist/provider/slack.d.mts +0 -113
- package/dist/provider/slack.mjs +0 -138
- package/dist/provider/spotify.d.mts +0 -112
- package/dist/provider/spotify.mjs +0 -135
- package/dist/provider/totp.d.mts +0 -111
- package/dist/provider/totp.mjs +0 -191
- package/dist/provider/twitch.d.mts +0 -107
- package/dist/provider/twitch.mjs +0 -131
- package/dist/provider/vercel.d.mts +0 -176
- package/dist/provider/vercel.mjs +0 -230
- package/dist/random.mjs +0 -86
- package/dist/revocation.d.mts +0 -54
- package/dist/revocation.mjs +0 -63
- package/dist/router/context.d.mts +0 -21
- package/dist/router/context.mjs +0 -193
- package/dist/router/cookies.d.mts +0 -8
- package/dist/router/cookies.mjs +0 -13
- package/dist/router/index.d.mts +0 -21
- package/dist/router/index.mjs +0 -107
- package/dist/router/matcher.d.mts +0 -15
- package/dist/router/matcher.mjs +0 -76
- package/dist/router/middleware/cors.d.mts +0 -15
- package/dist/router/middleware/cors.mjs +0 -114
- package/dist/router/safe-request.d.mts +0 -52
- package/dist/router/safe-request.mjs +0 -160
- package/dist/router/types.d.mts +0 -67
- package/dist/router/types.mjs +0 -1
- package/dist/router/variables.d.mts +0 -12
- package/dist/router/variables.mjs +0 -20
- package/dist/storage/memory.mjs +0 -125
- package/dist/storage/storage.d.mts +0 -179
- package/dist/storage/storage.mjs +0 -104
- package/dist/storage/turso.mjs +0 -117
- package/dist/storage/unstorage.mjs +0 -103
- package/dist/subject.d.mts +0 -61
- package/dist/subject.mjs +0 -36
- package/dist/themes/theme.d.mts +0 -209
- package/dist/themes/theme.mjs +0 -120
- package/dist/toolkit/client.d.mts +0 -168
- package/dist/toolkit/client.mjs +0 -209
- package/dist/toolkit/index.d.mts +0 -9
- package/dist/toolkit/index.mjs +0 -9
- package/dist/toolkit/providers/facebook.d.mts +0 -11
- package/dist/toolkit/providers/facebook.mjs +0 -16
- package/dist/toolkit/providers/github.d.mts +0 -11
- package/dist/toolkit/providers/github.mjs +0 -16
- package/dist/toolkit/providers/google.d.mts +0 -11
- package/dist/toolkit/providers/google.mjs +0 -20
- package/dist/toolkit/providers/strategy.d.mts +0 -40
- package/dist/toolkit/providers/strategy.mjs +0 -1
- package/dist/toolkit/storage.mjs +0 -157
- package/dist/toolkit/utils.mjs +0 -30
- package/dist/types.d.mts +0 -94
- package/dist/types.mjs +0 -1
- package/dist/ui/base.d.mts +0 -29
- package/dist/ui/base.mjs +0 -407
- package/dist/ui/code.d.mts +0 -42
- package/dist/ui/code.mjs +0 -173
- package/dist/ui/form.d.mts +0 -31
- package/dist/ui/form.mjs +0 -49
- package/dist/ui/icon.d.mts +0 -57
- package/dist/ui/icon.mjs +0 -247
- package/dist/ui/magiclink.d.mts +0 -40
- package/dist/ui/magiclink.mjs +0 -152
- package/dist/ui/passkey.d.mts +0 -26
- package/dist/ui/passkey.mjs +0 -323
- package/dist/ui/password.d.mts +0 -41
- package/dist/ui/password.mjs +0 -402
- package/dist/ui/select.d.mts +0 -33
- package/dist/ui/select.mjs +0 -98
- package/dist/ui/totp.d.mts +0 -33
- package/dist/ui/totp.mjs +0 -270
- package/dist/util.mjs +0 -128
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// src/provider/password.ts
|
|
2
|
+
import { UnknownStateError } from "../error";
|
|
3
|
+
import { generateUnbiasedDigits, timingSafeCompare } from "../random";
|
|
4
|
+
import { Storage } from "../storage/storage";
|
|
5
|
+
import { getRelativeUrl } from "../util";
|
|
6
|
+
import { TextEncoder } from "node:util";
|
|
7
|
+
import * as jose from "jose";
|
|
8
|
+
import { randomBytes, scrypt, timingSafeEqual } from "node:crypto";
|
|
9
|
+
var PasswordProvider = (config) => {
|
|
10
|
+
const hasher = config.hasher ?? ScryptHasher();
|
|
11
|
+
const generateCode = () => {
|
|
12
|
+
return generateUnbiasedDigits(config.length ?? 6);
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
type: "password",
|
|
16
|
+
init(routes, ctx) {
|
|
17
|
+
routes.get("/authorize", async (c) => ctx.forward(c, await config.login(c.req.raw)));
|
|
18
|
+
routes.post("/authorize", async (c) => {
|
|
19
|
+
const formData = await c.req.formData();
|
|
20
|
+
const error = async (err) => {
|
|
21
|
+
return ctx.forward(c, await config.login(c.req.raw, formData, err));
|
|
22
|
+
};
|
|
23
|
+
const email = formData.get("email")?.toString()?.toLowerCase();
|
|
24
|
+
if (!email) {
|
|
25
|
+
return error({ type: "invalid_email" });
|
|
26
|
+
}
|
|
27
|
+
const storedHash = await Storage.get(ctx.storage, [
|
|
28
|
+
"email",
|
|
29
|
+
email,
|
|
30
|
+
"password"
|
|
31
|
+
]);
|
|
32
|
+
const password = formData.get("password")?.toString();
|
|
33
|
+
if (!(password && storedHash && await hasher.verify(password, storedHash))) {
|
|
34
|
+
return error({ type: "invalid_password" });
|
|
35
|
+
}
|
|
36
|
+
return ctx.success(c, { email }, {
|
|
37
|
+
invalidate: async (subject) => {
|
|
38
|
+
await Storage.set(ctx.storage, ["email", email, "subject"], subject);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
routes.get("/register", async (c) => {
|
|
43
|
+
const state = { type: "start" };
|
|
44
|
+
await ctx.set(c, "provider", 60 * 60 * 24, state);
|
|
45
|
+
return ctx.forward(c, await config.register(c.req.raw, state));
|
|
46
|
+
});
|
|
47
|
+
routes.post("/register", async (c) => {
|
|
48
|
+
const formData = await c.req.formData();
|
|
49
|
+
const email = formData.get("email")?.toString()?.toLowerCase();
|
|
50
|
+
const action = formData.get("action")?.toString();
|
|
51
|
+
let provider = await ctx.get(c, "provider");
|
|
52
|
+
if (!provider) {
|
|
53
|
+
const state = { type: "start" };
|
|
54
|
+
await ctx.set(c, "provider", 60 * 60 * 24, state);
|
|
55
|
+
if (action === "register") {
|
|
56
|
+
provider = state;
|
|
57
|
+
} else {
|
|
58
|
+
return ctx.forward(c, await config.register(c.req.raw, state));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const transition = async (next, err) => {
|
|
62
|
+
await ctx.set(c, "provider", 60 * 60 * 24, next);
|
|
63
|
+
return ctx.forward(c, await config.register(c.req.raw, next, formData, err));
|
|
64
|
+
};
|
|
65
|
+
if (action === "register" && provider.type === "start") {
|
|
66
|
+
const password = formData.get("password")?.toString();
|
|
67
|
+
const repeat = formData.get("repeat")?.toString();
|
|
68
|
+
if (!email) {
|
|
69
|
+
return transition(provider, { type: "invalid_email" });
|
|
70
|
+
}
|
|
71
|
+
if (!password) {
|
|
72
|
+
return transition(provider, { type: "invalid_password" });
|
|
73
|
+
}
|
|
74
|
+
if (password !== repeat) {
|
|
75
|
+
return transition(provider, { type: "password_mismatch" });
|
|
76
|
+
}
|
|
77
|
+
if (config.validatePassword) {
|
|
78
|
+
let validationError;
|
|
79
|
+
try {
|
|
80
|
+
if (typeof config.validatePassword === "function") {
|
|
81
|
+
validationError = await config.validatePassword(password);
|
|
82
|
+
} else {
|
|
83
|
+
const result = await config.validatePassword["~standard"].validate(password);
|
|
84
|
+
if (result.issues?.length) {
|
|
85
|
+
throw new Error(result.issues.map((issue) => issue.message).join(", "));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch (error) {
|
|
89
|
+
validationError = error instanceof Error ? error.message : undefined;
|
|
90
|
+
}
|
|
91
|
+
if (validationError) {
|
|
92
|
+
return transition(provider, {
|
|
93
|
+
type: "validation_error",
|
|
94
|
+
message: validationError
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const existingUser = await Storage.get(ctx.storage, ["email", email, "password"]);
|
|
99
|
+
if (existingUser) {
|
|
100
|
+
return transition(provider, { type: "email_taken" });
|
|
101
|
+
}
|
|
102
|
+
const code = generateCode();
|
|
103
|
+
await config.sendCode(email, code, "register");
|
|
104
|
+
return transition({
|
|
105
|
+
type: "code",
|
|
106
|
+
code,
|
|
107
|
+
password: await hasher.hash(password),
|
|
108
|
+
email
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (action === "register" && provider.type === "code") {
|
|
112
|
+
const code = generateCode();
|
|
113
|
+
await config.sendCode(provider.email, code, "register:resend");
|
|
114
|
+
return transition({
|
|
115
|
+
type: "code",
|
|
116
|
+
code,
|
|
117
|
+
password: provider.password,
|
|
118
|
+
email: provider.email
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
if (action === "verify" && provider.type === "code") {
|
|
122
|
+
const code = formData.get("code")?.toString();
|
|
123
|
+
if (!(code && timingSafeCompare(code, provider.code))) {
|
|
124
|
+
return transition(provider, { type: "invalid_code" });
|
|
125
|
+
}
|
|
126
|
+
const existingUser = await Storage.get(ctx.storage, [
|
|
127
|
+
"email",
|
|
128
|
+
provider.email,
|
|
129
|
+
"password"
|
|
130
|
+
]);
|
|
131
|
+
if (existingUser) {
|
|
132
|
+
return transition({ type: "start" }, { type: "email_taken" });
|
|
133
|
+
}
|
|
134
|
+
await Storage.set(ctx.storage, ["email", provider.email, "password"], provider.password);
|
|
135
|
+
return ctx.success(c, { email: provider.email });
|
|
136
|
+
}
|
|
137
|
+
return transition({ type: "start" });
|
|
138
|
+
});
|
|
139
|
+
routes.get("/change", async (c) => {
|
|
140
|
+
const redirect = c.req.query("redirect_uri") || getRelativeUrl(c, "./authorize");
|
|
141
|
+
const state = {
|
|
142
|
+
type: "start",
|
|
143
|
+
redirect
|
|
144
|
+
};
|
|
145
|
+
await ctx.set(c, "provider", 60 * 60 * 24, state);
|
|
146
|
+
return ctx.forward(c, await config.change(c.req.raw, state));
|
|
147
|
+
});
|
|
148
|
+
routes.post("/change", async (c) => {
|
|
149
|
+
const formData = await c.req.formData();
|
|
150
|
+
const action = formData.get("action")?.toString();
|
|
151
|
+
const provider = await ctx.get(c, "provider");
|
|
152
|
+
if (!provider) {
|
|
153
|
+
throw new UnknownStateError;
|
|
154
|
+
}
|
|
155
|
+
const transition = async (next, err) => {
|
|
156
|
+
await ctx.set(c, "provider", 60 * 60 * 24, next);
|
|
157
|
+
return ctx.forward(c, await config.change(c.req.raw, next, formData, err));
|
|
158
|
+
};
|
|
159
|
+
if (action === "code") {
|
|
160
|
+
const email = formData.get("email")?.toString()?.toLowerCase();
|
|
161
|
+
if (!email) {
|
|
162
|
+
return transition({ type: "start", redirect: provider.redirect }, { type: "invalid_email" });
|
|
163
|
+
}
|
|
164
|
+
const existingPassword = await Storage.get(ctx.storage, ["email", email, "password"]);
|
|
165
|
+
if (!existingPassword) {
|
|
166
|
+
return transition({ type: "start", redirect: provider.redirect }, { type: "invalid_email" });
|
|
167
|
+
}
|
|
168
|
+
const code = generateCode();
|
|
169
|
+
const context = provider.type === "code" && provider.email === email ? "reset:resend" : "reset";
|
|
170
|
+
await config.sendCode(email, code, context);
|
|
171
|
+
return transition({
|
|
172
|
+
type: "code",
|
|
173
|
+
code,
|
|
174
|
+
email,
|
|
175
|
+
redirect: provider.redirect
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
if (action === "verify" && provider.type === "code") {
|
|
179
|
+
const code = formData.get("code")?.toString();
|
|
180
|
+
if (!(code && timingSafeCompare(code, provider.code))) {
|
|
181
|
+
return transition(provider, { type: "invalid_code" });
|
|
182
|
+
}
|
|
183
|
+
return transition({
|
|
184
|
+
type: "update",
|
|
185
|
+
email: provider.email,
|
|
186
|
+
redirect: provider.redirect
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (action === "update" && provider.type === "update") {
|
|
190
|
+
const existingPassword = await Storage.get(ctx.storage, [
|
|
191
|
+
"email",
|
|
192
|
+
provider.email,
|
|
193
|
+
"password"
|
|
194
|
+
]);
|
|
195
|
+
if (!existingPassword) {
|
|
196
|
+
return c.redirect(provider.redirect, 302);
|
|
197
|
+
}
|
|
198
|
+
const password = formData.get("password")?.toString();
|
|
199
|
+
const repeat = formData.get("repeat")?.toString();
|
|
200
|
+
if (!password) {
|
|
201
|
+
return transition(provider, { type: "invalid_password" });
|
|
202
|
+
}
|
|
203
|
+
if (!repeat) {
|
|
204
|
+
return transition(provider, { type: "invalid_password" });
|
|
205
|
+
}
|
|
206
|
+
if (password !== repeat) {
|
|
207
|
+
return transition(provider, { type: "password_mismatch" });
|
|
208
|
+
}
|
|
209
|
+
if (config.validatePassword) {
|
|
210
|
+
let validationError;
|
|
211
|
+
try {
|
|
212
|
+
if (typeof config.validatePassword === "function") {
|
|
213
|
+
validationError = await config.validatePassword(password);
|
|
214
|
+
} else {
|
|
215
|
+
const result = await config.validatePassword["~standard"].validate(password);
|
|
216
|
+
if (result.issues?.length) {
|
|
217
|
+
throw new Error(result.issues.map((issue) => issue.message).join(", "));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
} catch (error) {
|
|
221
|
+
validationError = error instanceof Error ? error.message : undefined;
|
|
222
|
+
}
|
|
223
|
+
if (validationError) {
|
|
224
|
+
return transition(provider, {
|
|
225
|
+
type: "validation_error",
|
|
226
|
+
message: validationError
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
await Storage.set(ctx.storage, ["email", provider.email, "password"], await hasher.hash(password));
|
|
231
|
+
const subject = await Storage.get(ctx.storage, [
|
|
232
|
+
"email",
|
|
233
|
+
provider.email,
|
|
234
|
+
"subject"
|
|
235
|
+
]);
|
|
236
|
+
if (subject) {
|
|
237
|
+
await ctx.invalidate(subject);
|
|
238
|
+
}
|
|
239
|
+
return c.redirect(provider.redirect, 302);
|
|
240
|
+
}
|
|
241
|
+
return transition({ type: "start", redirect: provider.redirect });
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
var PBKDF2Hasher = (opts) => {
|
|
247
|
+
const iterations = opts?.iterations ?? 600000;
|
|
248
|
+
return {
|
|
249
|
+
async hash(password) {
|
|
250
|
+
const encoder = new TextEncoder;
|
|
251
|
+
const passwordBytes = encoder.encode(password);
|
|
252
|
+
const salt = crypto.getRandomValues(new Uint8Array(16));
|
|
253
|
+
const keyMaterial = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits"]);
|
|
254
|
+
const hashBuffer = await crypto.subtle.deriveBits({
|
|
255
|
+
name: "PBKDF2",
|
|
256
|
+
hash: "SHA-256",
|
|
257
|
+
salt,
|
|
258
|
+
iterations
|
|
259
|
+
}, keyMaterial, 256);
|
|
260
|
+
const hashBase64 = jose.base64url.encode(new Uint8Array(hashBuffer));
|
|
261
|
+
const saltBase64 = jose.base64url.encode(salt);
|
|
262
|
+
return {
|
|
263
|
+
hash: hashBase64,
|
|
264
|
+
salt: saltBase64,
|
|
265
|
+
iterations
|
|
266
|
+
};
|
|
267
|
+
},
|
|
268
|
+
async verify(password, compare) {
|
|
269
|
+
const encoder = new TextEncoder;
|
|
270
|
+
const passwordBytes = encoder.encode(password);
|
|
271
|
+
const salt = jose.base64url.decode(compare.salt);
|
|
272
|
+
const keyMaterial = await crypto.subtle.importKey("raw", passwordBytes, "PBKDF2", false, ["deriveBits"]);
|
|
273
|
+
const hashBuffer = await crypto.subtle.deriveBits({
|
|
274
|
+
name: "PBKDF2",
|
|
275
|
+
hash: "SHA-256",
|
|
276
|
+
salt,
|
|
277
|
+
iterations: compare.iterations
|
|
278
|
+
}, keyMaterial, 256);
|
|
279
|
+
const hashBase64 = jose.base64url.encode(new Uint8Array(hashBuffer));
|
|
280
|
+
return timingSafeCompare(hashBase64, compare.hash);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
var ScryptHasher = (opts) => {
|
|
285
|
+
const N = opts?.N ?? 16384;
|
|
286
|
+
const r = opts?.r ?? 8;
|
|
287
|
+
const p = opts?.p ?? 1;
|
|
288
|
+
return {
|
|
289
|
+
async hash(password) {
|
|
290
|
+
const salt = randomBytes(16);
|
|
291
|
+
const keyLength = 32;
|
|
292
|
+
const derivedKey = await new Promise((resolve, reject) => {
|
|
293
|
+
scrypt(password, salt, keyLength, { N, r, p }, (err, derivedKey2) => {
|
|
294
|
+
if (err) {
|
|
295
|
+
reject(err);
|
|
296
|
+
} else {
|
|
297
|
+
resolve(derivedKey2);
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
});
|
|
301
|
+
const hashBase64 = derivedKey.toString("base64");
|
|
302
|
+
const saltBase64 = salt.toString("base64");
|
|
303
|
+
return {
|
|
304
|
+
hash: hashBase64,
|
|
305
|
+
salt: saltBase64,
|
|
306
|
+
N,
|
|
307
|
+
r,
|
|
308
|
+
p
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
async verify(password, compare) {
|
|
312
|
+
const salt = Buffer.from(compare.salt, "base64");
|
|
313
|
+
const keyLength = 32;
|
|
314
|
+
const derivedKey = await new Promise((resolve, reject) => {
|
|
315
|
+
scrypt(password, salt, keyLength, { N: compare.N, r: compare.r, p: compare.p }, (err, derivedKey2) => {
|
|
316
|
+
if (err) {
|
|
317
|
+
reject(err);
|
|
318
|
+
} else {
|
|
319
|
+
resolve(derivedKey2);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
return timingSafeEqual(derivedKey, Buffer.from(compare.hash, "base64"));
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
export {
|
|
328
|
+
ScryptHasher,
|
|
329
|
+
PasswordProvider,
|
|
330
|
+
PBKDF2Hasher
|
|
331
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/provider/provider.ts
|
|
2
|
+
class ProviderError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "ProviderError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class ProviderUnknownError extends ProviderError {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message || "An unknown provider error occurred");
|
|
12
|
+
this.name = "ProviderUnknownError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
ProviderUnknownError,
|
|
17
|
+
ProviderError
|
|
18
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/provider/reddit.ts
|
|
2
|
+
import { Oauth2Provider } from "./oauth2";
|
|
3
|
+
var RedditProvider = (config) => {
|
|
4
|
+
return Oauth2Provider({
|
|
5
|
+
...config,
|
|
6
|
+
type: "reddit",
|
|
7
|
+
endpoint: {
|
|
8
|
+
authorization: "https://www.reddit.com/api/v1/authorize",
|
|
9
|
+
token: "https://www.reddit.com/api/v1/access_token"
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
RedditProvider
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/provider/slack.ts
|
|
2
|
+
import { Oauth2Provider } from "./oauth2";
|
|
3
|
+
var SlackProvider = (config) => {
|
|
4
|
+
return Oauth2Provider({
|
|
5
|
+
...config,
|
|
6
|
+
type: "slack",
|
|
7
|
+
endpoint: {
|
|
8
|
+
authorization: "https://slack.com/oauth/v2/authorize",
|
|
9
|
+
token: "https://slack.com/api/oauth.v2.access"
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
SlackProvider
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/provider/spotify.ts
|
|
2
|
+
import { Oauth2Provider } from "./oauth2";
|
|
3
|
+
var SpotifyProvider = (config) => {
|
|
4
|
+
return Oauth2Provider({
|
|
5
|
+
...config,
|
|
6
|
+
type: "spotify",
|
|
7
|
+
endpoint: {
|
|
8
|
+
authorization: "https://accounts.spotify.com/authorize",
|
|
9
|
+
token: "https://accounts.spotify.com/api/token"
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
SpotifyProvider
|
|
15
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// src/provider/twitch.ts
|
|
2
|
+
import { Oauth2Provider } from "./oauth2";
|
|
3
|
+
var TwitchProvider = (config) => {
|
|
4
|
+
return Oauth2Provider({
|
|
5
|
+
...config,
|
|
6
|
+
type: "twitch",
|
|
7
|
+
endpoint: {
|
|
8
|
+
authorization: "https://id.twitch.tv/oauth2/authorize",
|
|
9
|
+
token: "https://id.twitch.tv/oauth2/token"
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
export {
|
|
14
|
+
TwitchProvider
|
|
15
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// src/provider/vercel.ts
|
|
2
|
+
import { Oauth2Provider } from "./oauth2";
|
|
3
|
+
var VercelProvider = (config) => {
|
|
4
|
+
return Oauth2Provider({
|
|
5
|
+
...config,
|
|
6
|
+
type: "vercel",
|
|
7
|
+
endpoint: {
|
|
8
|
+
authorization: "https://vercel.com/oauth/authorize",
|
|
9
|
+
token: "https://api.vercel.com/login/oauth/token",
|
|
10
|
+
jwks: "https://vercel.com/.well-known/jwks.json"
|
|
11
|
+
},
|
|
12
|
+
pkce: true
|
|
13
|
+
});
|
|
14
|
+
};
|
|
15
|
+
export {
|
|
16
|
+
VercelProvider
|
|
17
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// src/random.ts
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
3
|
+
var generateSecureToken = (length = 32) => {
|
|
4
|
+
if (length <= 0 || !Number.isInteger(length)) {
|
|
5
|
+
throw new RangeError("Token length must be a positive integer");
|
|
6
|
+
}
|
|
7
|
+
const randomBytes = new Uint8Array(length);
|
|
8
|
+
crypto.getRandomValues(randomBytes);
|
|
9
|
+
const base64 = btoa(String.fromCharCode.apply(null, Array.from(randomBytes)));
|
|
10
|
+
return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
11
|
+
};
|
|
12
|
+
var generateUnbiasedDigits = (length) => {
|
|
13
|
+
if (length <= 0 || !Number.isInteger(length)) {
|
|
14
|
+
throw new RangeError("Length must be a positive integer");
|
|
15
|
+
}
|
|
16
|
+
const result = [];
|
|
17
|
+
while (result.length < length) {
|
|
18
|
+
const buffer = crypto.getRandomValues(new Uint8Array(length * 2));
|
|
19
|
+
for (const byte of buffer) {
|
|
20
|
+
if (byte < 250 && result.length < length) {
|
|
21
|
+
result.push(byte % 10);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return result.join("");
|
|
26
|
+
};
|
|
27
|
+
var timingSafeCompare = (a, b) => {
|
|
28
|
+
if (typeof a !== "string" || typeof b !== "string") {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (a.length !== b.length) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
return timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
35
|
+
};
|
|
36
|
+
export {
|
|
37
|
+
timingSafeCompare,
|
|
38
|
+
generateUnbiasedDigits,
|
|
39
|
+
generateSecureToken
|
|
40
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/revocation.ts
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { Storage } from "./storage/storage";
|
|
4
|
+
var hashToken = (token) => {
|
|
5
|
+
return createHash("sha256").update(token).digest("hex");
|
|
6
|
+
};
|
|
7
|
+
var Revocation = {
|
|
8
|
+
revoke: async (storage, token, expiresAt) => {
|
|
9
|
+
const hash = hashToken(token);
|
|
10
|
+
const key = ["revocation:token", hash];
|
|
11
|
+
const record = {
|
|
12
|
+
revokedAt: Date.now(),
|
|
13
|
+
expiresAt
|
|
14
|
+
};
|
|
15
|
+
const ttlSeconds = Math.ceil((expiresAt - Date.now()) / 1000);
|
|
16
|
+
await Storage.set(storage, key, record, Math.max(1, ttlSeconds));
|
|
17
|
+
},
|
|
18
|
+
isRevoked: async (storage, token) => {
|
|
19
|
+
const hash = hashToken(token);
|
|
20
|
+
const key = ["revocation:token", hash];
|
|
21
|
+
const record = await Storage.get(storage, key);
|
|
22
|
+
return !!record;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
export {
|
|
26
|
+
Revocation
|
|
27
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// src/storage/memory.ts
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { writeFile } from "node:fs/promises";
|
|
4
|
+
import { joinKey, splitKey } from "./storage";
|
|
5
|
+
var MemoryStorage = (options) => {
|
|
6
|
+
const store = [];
|
|
7
|
+
const isValidStoreData = (data) => {
|
|
8
|
+
return Array.isArray(data) && data.every((item) => Array.isArray(item) && item.length === 2 && typeof item[0] === "string" && typeof item[1] === "object" && item[1] !== null && ("value" in item[1]));
|
|
9
|
+
};
|
|
10
|
+
if (options?.persist && existsSync(options.persist)) {
|
|
11
|
+
try {
|
|
12
|
+
const fileContent = readFileSync(options.persist, "utf8");
|
|
13
|
+
const parsed = JSON.parse(fileContent);
|
|
14
|
+
if (isValidStoreData(parsed)) {
|
|
15
|
+
store.push(...parsed);
|
|
16
|
+
}
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
const save = async () => {
|
|
20
|
+
if (!options?.persist) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const serialized = JSON.stringify(store, null, 2);
|
|
25
|
+
await writeFile(options.persist, serialized, "utf8");
|
|
26
|
+
} catch {}
|
|
27
|
+
};
|
|
28
|
+
const search = (key) => {
|
|
29
|
+
let left = 0;
|
|
30
|
+
let right = store.length - 1;
|
|
31
|
+
while (left <= right) {
|
|
32
|
+
const mid = Math.floor((left + right) / 2);
|
|
33
|
+
const midEntry = store[mid];
|
|
34
|
+
if (!midEntry) {
|
|
35
|
+
return { found: false, index: left };
|
|
36
|
+
}
|
|
37
|
+
const comparison = key.localeCompare(midEntry[0]);
|
|
38
|
+
if (comparison === 0) {
|
|
39
|
+
return { found: true, index: mid };
|
|
40
|
+
}
|
|
41
|
+
if (comparison < 0) {
|
|
42
|
+
right = mid - 1;
|
|
43
|
+
} else {
|
|
44
|
+
left = mid + 1;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { found: false, index: left };
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
async get(key) {
|
|
51
|
+
const searchKey = joinKey(key);
|
|
52
|
+
const match = search(searchKey);
|
|
53
|
+
if (!match.found) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const storeEntry = store[match.index];
|
|
57
|
+
if (!storeEntry) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const entry = storeEntry[1];
|
|
61
|
+
if (entry.expiry && Date.now() >= entry.expiry) {
|
|
62
|
+
store.splice(match.index, 1);
|
|
63
|
+
await save();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
return entry.value;
|
|
67
|
+
},
|
|
68
|
+
async set(key, value, expiry) {
|
|
69
|
+
const searchKey = joinKey(key);
|
|
70
|
+
const match = search(searchKey);
|
|
71
|
+
const entry = [
|
|
72
|
+
searchKey,
|
|
73
|
+
{
|
|
74
|
+
value,
|
|
75
|
+
expiry: expiry?.getTime()
|
|
76
|
+
}
|
|
77
|
+
];
|
|
78
|
+
if (match.found) {
|
|
79
|
+
store[match.index] = entry;
|
|
80
|
+
} else {
|
|
81
|
+
store.splice(match.index, 0, entry);
|
|
82
|
+
}
|
|
83
|
+
await save();
|
|
84
|
+
},
|
|
85
|
+
async remove(key) {
|
|
86
|
+
const searchKey = joinKey(key);
|
|
87
|
+
const match = search(searchKey);
|
|
88
|
+
if (match.found) {
|
|
89
|
+
store.splice(match.index, 1);
|
|
90
|
+
await save();
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
async* scan(prefix) {
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
const prefixStr = joinKey(prefix);
|
|
96
|
+
for (const [key, entry] of store) {
|
|
97
|
+
if (!key.startsWith(prefixStr)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (entry.expiry && now >= entry.expiry) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
yield [splitKey(key), entry.value];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
export {
|
|
109
|
+
MemoryStorage
|
|
110
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/storage/storage.ts
|
|
2
|
+
var SEPARATOR = String.fromCharCode(31);
|
|
3
|
+
var ESCAPE = "\\";
|
|
4
|
+
var joinKey = (key) => {
|
|
5
|
+
return key.join(SEPARATOR);
|
|
6
|
+
};
|
|
7
|
+
var splitKey = (key) => {
|
|
8
|
+
return key.split(SEPARATOR);
|
|
9
|
+
};
|
|
10
|
+
var encodeSegment = (segment) => {
|
|
11
|
+
if (!segment || !segment.trim()) {
|
|
12
|
+
throw new Error(`Storage key segment cannot be empty or whitespace-only: "${segment}"`);
|
|
13
|
+
}
|
|
14
|
+
return segment.replaceAll(ESCAPE, ESCAPE + ESCAPE).replaceAll(SEPARATOR, ESCAPE + SEPARATOR);
|
|
15
|
+
};
|
|
16
|
+
var decodeSegment = (segment) => {
|
|
17
|
+
return segment.replaceAll(ESCAPE + SEPARATOR, SEPARATOR).replaceAll(ESCAPE + ESCAPE, ESCAPE);
|
|
18
|
+
};
|
|
19
|
+
var Storage = {
|
|
20
|
+
encode: (key) => {
|
|
21
|
+
return key.map(encodeSegment);
|
|
22
|
+
},
|
|
23
|
+
decode: (key) => {
|
|
24
|
+
return key.map(decodeSegment);
|
|
25
|
+
},
|
|
26
|
+
get: (adapter, key) => {
|
|
27
|
+
return adapter.get(Storage.encode(key));
|
|
28
|
+
},
|
|
29
|
+
set: (adapter, key, value, ttlSeconds) => {
|
|
30
|
+
if (ttlSeconds !== undefined && ttlSeconds !== null) {
|
|
31
|
+
if (!Number.isInteger(ttlSeconds)) {
|
|
32
|
+
throw new RangeError(`Storage TTL must be an integer in seconds, received ${typeof ttlSeconds}`);
|
|
33
|
+
}
|
|
34
|
+
if (ttlSeconds <= 0) {
|
|
35
|
+
throw new RangeError(`Storage TTL must be positive, received ${ttlSeconds}`);
|
|
36
|
+
}
|
|
37
|
+
const maxTtlSeconds = 60 * 60 * 24 * 365 * 10;
|
|
38
|
+
if (ttlSeconds > maxTtlSeconds) {
|
|
39
|
+
throw new RangeError(`Storage TTL exceeds maximum (${maxTtlSeconds}s = 10 years), received ${ttlSeconds}s`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const expiry = ttlSeconds ? new Date(Date.now() + ttlSeconds * 1000) : undefined;
|
|
43
|
+
return adapter.set(Storage.encode(key), value, expiry);
|
|
44
|
+
},
|
|
45
|
+
remove: (adapter, key) => {
|
|
46
|
+
return adapter.remove(Storage.encode(key));
|
|
47
|
+
},
|
|
48
|
+
scan: (adapter, prefix) => {
|
|
49
|
+
return adapter.scan(Storage.encode(prefix));
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
export {
|
|
53
|
+
splitKey,
|
|
54
|
+
joinKey,
|
|
55
|
+
Storage
|
|
56
|
+
};
|