@rimelight/auth 0.0.2 → 0.0.3
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/adapters/auth0.d.mts +115 -0
- package/dist/adapters/auth0.mjs +256 -0
- package/dist/adapters/cf-access.d.mts +13 -0
- package/dist/adapters/cf-access.mjs +66 -0
- package/dist/adapters/mock.d.mts +9 -0
- package/dist/adapters/mock.mjs +31 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +8 -0
- package/dist/permissions/index.d.mts +47 -0
- package/dist/permissions/index.mjs +83 -0
- package/dist/plugins/construction-guest/index.d.mts +9 -0
- package/dist/plugins/construction-guest/index.mjs +51 -0
- package/dist/plugins/reserved-usernames/index.d.mts +12 -0
- package/dist/plugins/reserved-usernames/index.mjs +192 -0
- package/dist/types.d.mts +58 -0
- package/dist/types.mjs +1 -0
- package/package.json +43 -18
- package/src/client/index.ts +0 -3
- package/src/index.ts +0 -6
- package/src/permissions/index.ts +0 -27
- package/src/plugins/construction-guest/index.ts +0 -117
- package/src/plugins/discriminator-tag/index.ts +0 -55
- package/src/plugins/reserved-usernames/index.ts +0 -198
- package/src/plugins/synthetic-user/index.ts +0 -50
- package/src/schema/drizzle/index.ts +0 -363
- package/src/server/index.ts +0 -149
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getCookie, setCookie } from "hono/cookie";
|
|
2
|
+
//#region src/plugins/construction-guest/index.ts
|
|
3
|
+
const CONSTRUCTION_GUEST_COOKIE = "rimelight-construction-guest";
|
|
4
|
+
const getConfig = (env) => {
|
|
5
|
+
return { passphrase: env?.CONSTRUCTION_PASSPHRASE ?? process.env.CONSTRUCTION_PASSPHRASE };
|
|
6
|
+
};
|
|
7
|
+
const encode = (value) => btoa(String.fromCharCode(...value)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
8
|
+
const decode = (value) => {
|
|
9
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(value.length / 4) * 4, "=");
|
|
10
|
+
return Uint8Array.from(atob(padded), (char) => char.charCodeAt(0));
|
|
11
|
+
};
|
|
12
|
+
const sign = async (payload, secret) => {
|
|
13
|
+
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret), {
|
|
14
|
+
name: "HMAC",
|
|
15
|
+
hash: "SHA-256"
|
|
16
|
+
}, false, ["sign", "verify"]);
|
|
17
|
+
return {
|
|
18
|
+
key,
|
|
19
|
+
signature: await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload))
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
const isConstructionGuest = async (c) => {
|
|
23
|
+
const token = getCookie(c, CONSTRUCTION_GUEST_COOKIE);
|
|
24
|
+
const passphrase = getConfig(c.env).passphrase;
|
|
25
|
+
if (!token || !passphrase) return false;
|
|
26
|
+
try {
|
|
27
|
+
const [encodedPayload, encodedSignature] = token.split(".");
|
|
28
|
+
if (!encodedPayload || !encodedSignature) return false;
|
|
29
|
+
const payload = new TextDecoder().decode(decode(encodedPayload));
|
|
30
|
+
const { key } = await sign(payload, passphrase);
|
|
31
|
+
return await crypto.subtle.verify("HMAC", key, decode(encodedSignature), new TextEncoder().encode(payload)) && JSON.parse(payload).expiresAt > Date.now();
|
|
32
|
+
} catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
const signInConstructionGuest = async (c, passphrase, rememberMe = true) => {
|
|
37
|
+
const config = getConfig(c.env);
|
|
38
|
+
if (!config.passphrase || !passphrase || passphrase !== config.passphrase) return false;
|
|
39
|
+
const payload = JSON.stringify({ expiresAt: Date.now() + 6048e5 });
|
|
40
|
+
const { signature } = await sign(payload, config.passphrase);
|
|
41
|
+
setCookie(c, CONSTRUCTION_GUEST_COOKIE, encode(new TextEncoder().encode(payload)) + "." + encode(new Uint8Array(signature)), {
|
|
42
|
+
httpOnly: true,
|
|
43
|
+
secure: true,
|
|
44
|
+
sameSite: "Lax",
|
|
45
|
+
path: "/",
|
|
46
|
+
...rememberMe ? { maxAge: 604800 } : {}
|
|
47
|
+
});
|
|
48
|
+
return true;
|
|
49
|
+
};
|
|
50
|
+
//#endregion
|
|
51
|
+
export { CONSTRUCTION_GUEST_COOKIE, isConstructionGuest, signInConstructionGuest };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/plugins/reserved-usernames/index.d.ts
|
|
2
|
+
declare const normalizeUsername: (input: string) => string;
|
|
3
|
+
declare const STANDARD_RESTRICTED_GROUPS: {
|
|
4
|
+
STAFF_ROLES: string[];
|
|
5
|
+
LEGAL_FINANCIAL: string[];
|
|
6
|
+
TECHNICAL: string[];
|
|
7
|
+
LEETSPEAK: string[];
|
|
8
|
+
};
|
|
9
|
+
declare const createRestrictedUsernameSet: (customBlacklist?: string[]) => Set<string>;
|
|
10
|
+
declare const RESTRICTED_SET: Set<string>;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { RESTRICTED_SET, STANDARD_RESTRICTED_GROUPS, createRestrictedUsernameSet, normalizeUsername };
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
//#region src/plugins/reserved-usernames/index.ts
|
|
2
|
+
const HOMOGLYPH_MAP = {
|
|
3
|
+
"0": "o",
|
|
4
|
+
"1": "i",
|
|
5
|
+
"3": "e",
|
|
6
|
+
"4": "a",
|
|
7
|
+
"5": "s",
|
|
8
|
+
"7": "t",
|
|
9
|
+
"8": "b",
|
|
10
|
+
"9": "g",
|
|
11
|
+
"@": "a",
|
|
12
|
+
"!": "i",
|
|
13
|
+
"$": "s",
|
|
14
|
+
"|": "i"
|
|
15
|
+
};
|
|
16
|
+
const normalizeUsername = (input) => {
|
|
17
|
+
return input.toLowerCase().split("").map((char) => HOMOGLYPH_MAP[char] || char).join("").replace(/[^a-z0-9]/g, "");
|
|
18
|
+
};
|
|
19
|
+
const STANDARD_RESTRICTED_GROUPS = {
|
|
20
|
+
STAFF_ROLES: [
|
|
21
|
+
"admin",
|
|
22
|
+
"administrator",
|
|
23
|
+
"moderator",
|
|
24
|
+
"mod",
|
|
25
|
+
"staff",
|
|
26
|
+
"support",
|
|
27
|
+
"help",
|
|
28
|
+
"official",
|
|
29
|
+
"verified",
|
|
30
|
+
"system",
|
|
31
|
+
"root",
|
|
32
|
+
"bot",
|
|
33
|
+
"security",
|
|
34
|
+
"community",
|
|
35
|
+
"manager",
|
|
36
|
+
"dev",
|
|
37
|
+
"developer",
|
|
38
|
+
"designer",
|
|
39
|
+
"gamemaster",
|
|
40
|
+
"gm",
|
|
41
|
+
"assistant",
|
|
42
|
+
"coordinator",
|
|
43
|
+
"representative",
|
|
44
|
+
"agent",
|
|
45
|
+
"supervisor",
|
|
46
|
+
"executive",
|
|
47
|
+
"ambassador",
|
|
48
|
+
"expert",
|
|
49
|
+
"specialist",
|
|
50
|
+
"advocate",
|
|
51
|
+
"internal",
|
|
52
|
+
"employee",
|
|
53
|
+
"associate",
|
|
54
|
+
"webmaster",
|
|
55
|
+
"sysop",
|
|
56
|
+
"operator",
|
|
57
|
+
"host",
|
|
58
|
+
"referee",
|
|
59
|
+
"council",
|
|
60
|
+
"ceo",
|
|
61
|
+
"founder",
|
|
62
|
+
"owner"
|
|
63
|
+
],
|
|
64
|
+
LEGAL_FINANCIAL: [
|
|
65
|
+
"checkout",
|
|
66
|
+
"subscribe",
|
|
67
|
+
"subscription",
|
|
68
|
+
"premium",
|
|
69
|
+
"vip",
|
|
70
|
+
"store",
|
|
71
|
+
"shop",
|
|
72
|
+
"marketplace",
|
|
73
|
+
"wallet",
|
|
74
|
+
"refund",
|
|
75
|
+
"invoice",
|
|
76
|
+
"payout",
|
|
77
|
+
"rewards",
|
|
78
|
+
"prize",
|
|
79
|
+
"giveaway",
|
|
80
|
+
"claims",
|
|
81
|
+
"verification",
|
|
82
|
+
"billing",
|
|
83
|
+
"payment",
|
|
84
|
+
"sales",
|
|
85
|
+
"marketing",
|
|
86
|
+
"legal",
|
|
87
|
+
"compliance",
|
|
88
|
+
"privacy",
|
|
89
|
+
"tos",
|
|
90
|
+
"terms",
|
|
91
|
+
"copyright",
|
|
92
|
+
"trademark",
|
|
93
|
+
"dmca",
|
|
94
|
+
"abuse",
|
|
95
|
+
"report"
|
|
96
|
+
],
|
|
97
|
+
TECHNICAL: [
|
|
98
|
+
"null",
|
|
99
|
+
"undefined",
|
|
100
|
+
"nan",
|
|
101
|
+
"none",
|
|
102
|
+
"everyone",
|
|
103
|
+
"all",
|
|
104
|
+
"guest",
|
|
105
|
+
"user",
|
|
106
|
+
"test",
|
|
107
|
+
"tester",
|
|
108
|
+
"account",
|
|
109
|
+
"api",
|
|
110
|
+
"webhook",
|
|
111
|
+
"index",
|
|
112
|
+
"config",
|
|
113
|
+
"settings",
|
|
114
|
+
"profile",
|
|
115
|
+
"auth",
|
|
116
|
+
"login",
|
|
117
|
+
"signup",
|
|
118
|
+
"signin",
|
|
119
|
+
"logout",
|
|
120
|
+
"signout",
|
|
121
|
+
"localhost",
|
|
122
|
+
"ftp",
|
|
123
|
+
"smtp",
|
|
124
|
+
"pop3",
|
|
125
|
+
"imap",
|
|
126
|
+
"dns",
|
|
127
|
+
"proxy",
|
|
128
|
+
"cdn",
|
|
129
|
+
"static",
|
|
130
|
+
"assets",
|
|
131
|
+
"media",
|
|
132
|
+
"upload",
|
|
133
|
+
"download",
|
|
134
|
+
"docs",
|
|
135
|
+
"manual",
|
|
136
|
+
"guide",
|
|
137
|
+
"tutorial",
|
|
138
|
+
"error",
|
|
139
|
+
"404",
|
|
140
|
+
"500",
|
|
141
|
+
"maintenance",
|
|
142
|
+
"update",
|
|
143
|
+
"patch",
|
|
144
|
+
"changelog",
|
|
145
|
+
"status",
|
|
146
|
+
"db",
|
|
147
|
+
"database",
|
|
148
|
+
"sql",
|
|
149
|
+
"query",
|
|
150
|
+
"health",
|
|
151
|
+
"ping",
|
|
152
|
+
"metrics",
|
|
153
|
+
"logs",
|
|
154
|
+
"no-reply",
|
|
155
|
+
"noreply",
|
|
156
|
+
"donotreply",
|
|
157
|
+
"security-alert",
|
|
158
|
+
"mail",
|
|
159
|
+
"email",
|
|
160
|
+
"postmaster"
|
|
161
|
+
],
|
|
162
|
+
LEETSPEAK: [
|
|
163
|
+
"4dmin",
|
|
164
|
+
"@dmin",
|
|
165
|
+
"st4ff",
|
|
166
|
+
"m0d",
|
|
167
|
+
"m0derator",
|
|
168
|
+
"4dm1n",
|
|
169
|
+
"4dmin1strator",
|
|
170
|
+
"m0d3rator",
|
|
171
|
+
"st4f",
|
|
172
|
+
"5upport",
|
|
173
|
+
"0fficial",
|
|
174
|
+
"v3rified",
|
|
175
|
+
"5ystem",
|
|
176
|
+
"r00t",
|
|
177
|
+
"b0t",
|
|
178
|
+
"s3curity",
|
|
179
|
+
"d3v",
|
|
180
|
+
"d3veloper",
|
|
181
|
+
"@dministrator",
|
|
182
|
+
"adm1n",
|
|
183
|
+
"adm1nistrator"
|
|
184
|
+
]
|
|
185
|
+
};
|
|
186
|
+
const createRestrictedUsernameSet = (customBlacklist = []) => {
|
|
187
|
+
const base = Object.values(STANDARD_RESTRICTED_GROUPS).flat().concat(customBlacklist);
|
|
188
|
+
return new Set(base.map((name) => normalizeUsername(name)));
|
|
189
|
+
};
|
|
190
|
+
const RESTRICTED_SET = createRestrictedUsernameSet();
|
|
191
|
+
//#endregion
|
|
192
|
+
export { RESTRICTED_SET, STANDARD_RESTRICTED_GROUPS, createRestrictedUsernameSet, normalizeUsername };
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type UserType = "user" | "employee" | "guest" | (string & {});
|
|
3
|
+
interface UserSessionContext {
|
|
4
|
+
/**
|
|
5
|
+
* Unique user identifier (e.g. Auth0 sub claim, user UUID, or email)
|
|
6
|
+
*/
|
|
7
|
+
userId: string;
|
|
8
|
+
/**
|
|
9
|
+
* Roles assigned to the authenticated user (e.g. ["admin"], ["editor"], ["user"])
|
|
10
|
+
*/
|
|
11
|
+
roles: string[];
|
|
12
|
+
/**
|
|
13
|
+
* Specific permissions assigned (e.g. ["page:create", "draft:edit", "*"])
|
|
14
|
+
*/
|
|
15
|
+
permissions: string[];
|
|
16
|
+
/**
|
|
17
|
+
* User's primary email address
|
|
18
|
+
*/
|
|
19
|
+
email?: string | undefined;
|
|
20
|
+
/**
|
|
21
|
+
* User's display or full name
|
|
22
|
+
*/
|
|
23
|
+
name?: string | undefined;
|
|
24
|
+
/**
|
|
25
|
+
* User's avatar URL or profile picture
|
|
26
|
+
*/
|
|
27
|
+
avatar?: string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Discriminator or classification: "user" (customer/public) vs "employee" (staff/enterprise) vs
|
|
30
|
+
* "guest"
|
|
31
|
+
*/
|
|
32
|
+
userType?: UserType | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Custom identity metadata or raw token claims
|
|
35
|
+
*/
|
|
36
|
+
metadata?: Record<string, any> | undefined;
|
|
37
|
+
}
|
|
38
|
+
interface AuthAdapter {
|
|
39
|
+
/**
|
|
40
|
+
* Unique name of the auth adapter
|
|
41
|
+
*/
|
|
42
|
+
name: string;
|
|
43
|
+
/**
|
|
44
|
+
* Optional configuration options used to initialize the adapter
|
|
45
|
+
*/
|
|
46
|
+
options?: any;
|
|
47
|
+
/**
|
|
48
|
+
* Resolves the authenticated session from an incoming Request. Returns `null` if unauthenticated
|
|
49
|
+
* or invalid.
|
|
50
|
+
*/
|
|
51
|
+
getSession(request: Request): Promise<UserSessionContext | null>;
|
|
52
|
+
/**
|
|
53
|
+
* Optional hook for handling unauthorized page loads / redirects
|
|
54
|
+
*/
|
|
55
|
+
handleUnauthorized?: ((request: Request) => Response | Promise<Response>) | undefined;
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { AuthAdapter, UserSessionContext, UserType };
|
package/dist/types.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimelight/auth",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "Rimelight Entertainment's Authentication Package",
|
|
5
|
+
"description": "Rimelight Entertainment's Universal Authentication & Identity Package",
|
|
6
6
|
"homepage": "https://rimelight.com/docs",
|
|
7
7
|
"bugs": {
|
|
8
8
|
"url": "https://github.com/Rimelight-Entertainment/rimelight/issues"
|
|
@@ -16,38 +16,62 @@
|
|
|
16
16
|
"url": "git+https://github.com/Rimelight-Entertainment/rimelight.git"
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
|
-
"
|
|
19
|
+
"dist"
|
|
20
20
|
],
|
|
21
21
|
"type": "module",
|
|
22
22
|
"exports": {
|
|
23
|
-
".":
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"./
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"./
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.mts",
|
|
25
|
+
"import": "./dist/index.mjs"
|
|
26
|
+
},
|
|
27
|
+
"./types": {
|
|
28
|
+
"types": "./dist/types.d.mts",
|
|
29
|
+
"import": "./dist/types.mjs"
|
|
30
|
+
},
|
|
31
|
+
"./cf-access": {
|
|
32
|
+
"types": "./dist/adapters/cf-access.d.mts",
|
|
33
|
+
"import": "./dist/adapters/cf-access.mjs"
|
|
34
|
+
},
|
|
35
|
+
"./auth0": {
|
|
36
|
+
"types": "./dist/adapters/auth0.d.mts",
|
|
37
|
+
"import": "./dist/adapters/auth0.mjs"
|
|
38
|
+
},
|
|
39
|
+
"./mock": {
|
|
40
|
+
"types": "./dist/adapters/mock.d.mts",
|
|
41
|
+
"import": "./dist/adapters/mock.mjs"
|
|
42
|
+
},
|
|
43
|
+
"./permissions": {
|
|
44
|
+
"types": "./dist/permissions/index.d.mts",
|
|
45
|
+
"import": "./dist/permissions/index.mjs"
|
|
46
|
+
},
|
|
47
|
+
"./plugins/reserved-usernames": {
|
|
48
|
+
"types": "./dist/plugins/reserved-usernames/index.d.mts",
|
|
49
|
+
"import": "./dist/plugins/reserved-usernames/index.mjs"
|
|
50
|
+
},
|
|
51
|
+
"./plugins/construction-guest": {
|
|
52
|
+
"types": "./dist/plugins/construction-guest/index.d.mts",
|
|
53
|
+
"import": "./dist/plugins/construction-guest/index.mjs"
|
|
54
|
+
}
|
|
32
55
|
},
|
|
33
56
|
"publishConfig": {
|
|
34
57
|
"access": "public"
|
|
35
58
|
},
|
|
36
59
|
"dependencies": {
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"drizzle-orm": "0.45.2"
|
|
60
|
+
"drizzle-orm": "0.45.2",
|
|
61
|
+
"jose": "6.2.11"
|
|
40
62
|
},
|
|
41
63
|
"devDependencies": {
|
|
42
|
-
"@rimelight/config": "0.0.
|
|
43
|
-
"@types/node": "26.
|
|
64
|
+
"@rimelight/config": "0.0.4",
|
|
65
|
+
"@types/node": "26.4.1"
|
|
44
66
|
},
|
|
45
67
|
"peerDependencies": {
|
|
46
|
-
"better-auth": ">=1.0.0",
|
|
47
68
|
"drizzle-orm": ">=0.30.0",
|
|
48
69
|
"hono": ">=4.12.34"
|
|
49
70
|
},
|
|
50
71
|
"peerDependenciesMeta": {
|
|
72
|
+
"drizzle-orm": {
|
|
73
|
+
"optional": true
|
|
74
|
+
},
|
|
51
75
|
"hono": {
|
|
52
76
|
"optional": true
|
|
53
77
|
}
|
|
@@ -56,6 +80,7 @@
|
|
|
56
80
|
"node": ">=26.7.0"
|
|
57
81
|
},
|
|
58
82
|
"scripts": {
|
|
83
|
+
"build": "vp pack",
|
|
59
84
|
"check": "vp check --fix"
|
|
60
85
|
}
|
|
61
86
|
}
|
package/src/client/index.ts
DELETED
package/src/index.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export * from "./server/index"
|
|
2
|
-
export * from "./permissions/index"
|
|
3
|
-
export * from "./plugins/reserved-usernames/index"
|
|
4
|
-
export * from "./plugins/synthetic-user/index"
|
|
5
|
-
export * from "./plugins/construction-guest/index"
|
|
6
|
-
export * from "./plugins/discriminator-tag/index"
|
package/src/permissions/index.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { createAccessControl } from "better-auth/plugins/access"
|
|
2
|
-
import {
|
|
3
|
-
defaultStatements,
|
|
4
|
-
ownerAc,
|
|
5
|
-
adminAc,
|
|
6
|
-
memberAc
|
|
7
|
-
} from "better-auth/plugins/organization/access"
|
|
8
|
-
|
|
9
|
-
export { createAccessControl, defaultStatements, ownerAc, adminAc, memberAc }
|
|
10
|
-
|
|
11
|
-
export interface CreatePermissionsOptions<TStatements extends Record<string, readonly string[]>> {
|
|
12
|
-
statements: TStatements
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function createPermissions<const TStatements extends Record<string, readonly string[]>>(
|
|
16
|
-
statements: TStatements
|
|
17
|
-
) {
|
|
18
|
-
const mergedStatements = {
|
|
19
|
-
...defaultStatements,
|
|
20
|
-
...statements
|
|
21
|
-
}
|
|
22
|
-
const ac = createAccessControl(mergedStatements)
|
|
23
|
-
return {
|
|
24
|
-
ac,
|
|
25
|
-
statements: mergedStatements
|
|
26
|
-
}
|
|
27
|
-
}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { getCookie, setCookie } from "hono/cookie"
|
|
2
|
-
|
|
3
|
-
export const CONSTRUCTION_GUEST_COOKIE = "rimelight-construction-guest"
|
|
4
|
-
|
|
5
|
-
export type GuestEnv = {
|
|
6
|
-
CONSTRUCTION_GUEST_EMAIL?: string
|
|
7
|
-
CONSTRUCTION_GUEST_PASSWORD?: string
|
|
8
|
-
CONSTRUCTION_GUEST_SECRET?: string
|
|
9
|
-
BETTER_AUTH_SECRET?: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const getConfig = (env?: GuestEnv) => {
|
|
13
|
-
const metaEnv = (import.meta as any).env || {}
|
|
14
|
-
return {
|
|
15
|
-
email:
|
|
16
|
-
env?.CONSTRUCTION_GUEST_EMAIL ??
|
|
17
|
-
metaEnv.VITE_CONSTRUCTION_GUEST_EMAIL ??
|
|
18
|
-
process.env.VITE_CONSTRUCTION_GUEST_EMAIL,
|
|
19
|
-
password:
|
|
20
|
-
env?.CONSTRUCTION_GUEST_PASSWORD ??
|
|
21
|
-
metaEnv.VITE_CONSTRUCTION_GUEST_PASSWORD ??
|
|
22
|
-
process.env.VITE_CONSTRUCTION_GUEST_PASSWORD,
|
|
23
|
-
secret:
|
|
24
|
-
env?.CONSTRUCTION_GUEST_SECRET ??
|
|
25
|
-
metaEnv.VITE_CONSTRUCTION_GUEST_SECRET ??
|
|
26
|
-
metaEnv.VITE_BETTER_AUTH_SECRET ??
|
|
27
|
-
process.env.VITE_CONSTRUCTION_GUEST_SECRET ??
|
|
28
|
-
process.env.VITE_BETTER_AUTH_SECRET
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const encode = (value: Uint8Array) =>
|
|
33
|
-
btoa(String.fromCharCode(...value))
|
|
34
|
-
.replace(/\+/g, "-")
|
|
35
|
-
.replace(/\//g, "_")
|
|
36
|
-
.replace(/=+$/, "")
|
|
37
|
-
|
|
38
|
-
const decode = (value: string) => {
|
|
39
|
-
const padded = value
|
|
40
|
-
.replace(/-/g, "+")
|
|
41
|
-
.replace(/_/g, "/")
|
|
42
|
-
.padEnd(Math.ceil(value.length / 4) * 4, "=")
|
|
43
|
-
return Uint8Array.from(atob(padded), (char) => char.charCodeAt(0))
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const sign = async (payload: string, secret: string) => {
|
|
47
|
-
const key = await crypto.subtle.importKey(
|
|
48
|
-
"raw",
|
|
49
|
-
new TextEncoder().encode(secret),
|
|
50
|
-
{ name: "HMAC", hash: "SHA-256" },
|
|
51
|
-
false,
|
|
52
|
-
["sign", "verify"]
|
|
53
|
-
)
|
|
54
|
-
return {
|
|
55
|
-
key,
|
|
56
|
-
signature: await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload))
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export const isConstructionGuest = async (c: any) => {
|
|
61
|
-
const token = getCookie(c, CONSTRUCTION_GUEST_COOKIE)
|
|
62
|
-
const secret = getConfig(c.env).secret
|
|
63
|
-
if (!token || !secret) return false
|
|
64
|
-
|
|
65
|
-
try {
|
|
66
|
-
const [encodedPayload, encodedSignature] = token.split(".")
|
|
67
|
-
if (!encodedPayload || !encodedSignature) return false
|
|
68
|
-
const payload = new TextDecoder().decode(decode(encodedPayload))
|
|
69
|
-
const { key } = await sign(payload, secret)
|
|
70
|
-
const valid = await crypto.subtle.verify(
|
|
71
|
-
"HMAC",
|
|
72
|
-
key,
|
|
73
|
-
decode(encodedSignature),
|
|
74
|
-
new TextEncoder().encode(payload)
|
|
75
|
-
)
|
|
76
|
-
return valid && JSON.parse(payload).expiresAt > Date.now()
|
|
77
|
-
} catch {
|
|
78
|
-
return false
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export const signInConstructionGuest = async (
|
|
83
|
-
c: any,
|
|
84
|
-
email: string,
|
|
85
|
-
password: string,
|
|
86
|
-
rememberMe = true
|
|
87
|
-
) => {
|
|
88
|
-
const config = getConfig(c.env)
|
|
89
|
-
if (
|
|
90
|
-
!config.email ||
|
|
91
|
-
!config.password ||
|
|
92
|
-
!config.secret ||
|
|
93
|
-
email !== config.email ||
|
|
94
|
-
password !== config.password
|
|
95
|
-
) {
|
|
96
|
-
return false
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const payload = JSON.stringify({
|
|
100
|
-
email: config.email,
|
|
101
|
-
expiresAt: Date.now() + 1000 * 60 * 60 * 24 * 7
|
|
102
|
-
})
|
|
103
|
-
const { signature } = await sign(payload, config.secret)
|
|
104
|
-
setCookie(
|
|
105
|
-
c,
|
|
106
|
-
CONSTRUCTION_GUEST_COOKIE,
|
|
107
|
-
encode(new TextEncoder().encode(payload)) + "." + encode(new Uint8Array(signature)),
|
|
108
|
-
{
|
|
109
|
-
httpOnly: true,
|
|
110
|
-
secure: true,
|
|
111
|
-
sameSite: "Lax",
|
|
112
|
-
path: "/",
|
|
113
|
-
...(rememberMe ? { maxAge: 60 * 60 * 24 * 7 } : {})
|
|
114
|
-
}
|
|
115
|
-
)
|
|
116
|
-
return true
|
|
117
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { APIError } from "better-auth/api"
|
|
2
|
-
import { normalizeUsername, RESTRICTED_SET } from "../reserved-usernames"
|
|
3
|
-
import { eq, and, count } from "drizzle-orm"
|
|
4
|
-
|
|
5
|
-
export interface TagHookOptions {
|
|
6
|
-
userTable?: any
|
|
7
|
-
restrictedSet?: Set<string>
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function createTagAndUsernameHook(db: any, userTable: any, options?: TagHookOptions) {
|
|
11
|
-
const restricted = options?.restrictedSet ?? RESTRICTED_SET
|
|
12
|
-
|
|
13
|
-
return async (userData: any, _ctx: any) => {
|
|
14
|
-
// 1. Normalize the username and check against the restricted set
|
|
15
|
-
const normalizedInput = normalizeUsername(userData.name)
|
|
16
|
-
if (restricted.has(normalizedInput)) {
|
|
17
|
-
throw new APIError("BAD_REQUEST", {
|
|
18
|
-
message: "This username is reserved for official use."
|
|
19
|
-
})
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// 2. Generate a unique 4-digit discriminator tag
|
|
23
|
-
let uniqueTag = "0000"
|
|
24
|
-
const tryFindTag = async (attemptsLeft: number): Promise<string> => {
|
|
25
|
-
if (attemptsLeft <= 0) return "0000"
|
|
26
|
-
const newTag = Math.floor(Math.random() * 10000)
|
|
27
|
-
.toString()
|
|
28
|
-
.padStart(4, "0")
|
|
29
|
-
|
|
30
|
-
const result = await db
|
|
31
|
-
.select({ count: count() })
|
|
32
|
-
.from(userTable)
|
|
33
|
-
.where(and(eq(userTable.name, userData.name), eq(userTable.tag, newTag)))
|
|
34
|
-
|
|
35
|
-
const tagCount = result[0]?.count ?? 0
|
|
36
|
-
if (tagCount === 0) return newTag
|
|
37
|
-
return tryFindTag(attemptsLeft - 1)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
uniqueTag = await tryFindTag(50)
|
|
41
|
-
|
|
42
|
-
if (uniqueTag === "0000") {
|
|
43
|
-
throw new APIError("INTERNAL_SERVER_ERROR", {
|
|
44
|
-
message: "Failed to generate a unique tag for this username. Please try again."
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
data: {
|
|
50
|
-
...userData,
|
|
51
|
-
tag: uniqueTag
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|