@rimelight/auth 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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,11 @@
1
+ //#region src/plugins/reserved-usernames/index.d.ts
2
+ export declare const normalizeUsername: (input: string) => string;
3
+ export declare const STANDARD_RESTRICTED_GROUPS: {
4
+ STAFF_ROLES: string[];
5
+ LEGAL_FINANCIAL: string[];
6
+ TECHNICAL: string[];
7
+ LEETSPEAK: string[];
8
+ };
9
+ export declare const createRestrictedUsernameSet: (customBlacklist?: string[]) => Set<string>;
10
+ export declare const RESTRICTED_SET: Set<string>;
11
+ //#endregion
@@ -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 };
@@ -0,0 +1,57 @@
1
+ //#region src/types.d.ts
2
+ export type UserType = "user" | "employee" | "guest" | (string & {});
3
+ export 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
+ export 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
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.2",
3
+ "version": "0.0.4",
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,46 +16,71 @@
16
16
  "url": "git+https://github.com/Rimelight-Entertainment/rimelight.git"
17
17
  },
18
18
  "files": [
19
- "src"
19
+ "dist"
20
20
  ],
21
21
  "type": "module",
22
22
  "exports": {
23
- ".": "./src/index.ts",
24
- "./server": "./src/server/index.ts",
25
- "./client": "./src/client/index.ts",
26
- "./permissions": "./src/permissions/index.ts",
27
- "./schema/drizzle": "./src/schema/drizzle/index.ts",
28
- "./plugins/reserved-usernames": "./src/plugins/reserved-usernames/index.ts",
29
- "./plugins/discriminator-tag": "./src/plugins/discriminator-tag/index.ts",
30
- "./plugins/synthetic-user": "./src/plugins/synthetic-user/index.ts",
31
- "./plugins/construction-guest": "./src/plugins/construction-guest/index.ts"
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
- "@better-auth/drizzle-adapter": "1.7.2",
38
- "better-auth": "1.7.2",
39
- "drizzle-orm": "0.45.2"
60
+ "drizzle-orm": "0.45.2",
61
+ "jose": "6.2.12"
40
62
  },
41
63
  "devDependencies": {
42
- "@rimelight/config": "0.0.3",
43
- "@types/node": "26.3.0"
64
+ "@rimelight/config": "0.0.6",
65
+ "@types/node": "26.5.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
  }
54
78
  },
55
79
  "engines": {
56
- "node": ">=26.7.0"
80
+ "node": ">=26.8.2"
57
81
  },
58
82
  "scripts": {
83
+ "build": "vp pack",
59
84
  "check": "vp check --fix"
60
85
  }
61
86
  }
@@ -1,3 +0,0 @@
1
- import { createAuthClient } from "better-auth/client"
2
-
3
- export const createRimelightAuthClient = createAuthClient
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"
@@ -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
- }