@rimelight/auth 0.0.1 → 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.
@@ -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 };
@@ -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.1",
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,40 +16,62 @@
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
- "scripts": {
37
- "check": "pnpm audit --audit-level=moderate && vp check --fix"
38
- },
39
59
  "dependencies": {
40
- "@better-auth/drizzle-adapter": "1.6.30",
41
- "better-auth": "1.6.30",
42
- "drizzle-orm": "0.45.2"
60
+ "drizzle-orm": "0.45.2",
61
+ "jose": "6.2.11"
43
62
  },
44
63
  "devDependencies": {
45
- "@rimelight/config": "workspace:*"
64
+ "@rimelight/config": "0.0.4",
65
+ "@types/node": "26.4.1"
46
66
  },
47
67
  "peerDependencies": {
48
- "better-auth": ">=1.0.0",
49
68
  "drizzle-orm": ">=0.30.0",
50
69
  "hono": ">=4.12.34"
51
70
  },
52
71
  "peerDependenciesMeta": {
72
+ "drizzle-orm": {
73
+ "optional": true
74
+ },
53
75
  "hono": {
54
76
  "optional": true
55
77
  }
@@ -57,5 +79,8 @@
57
79
  "engines": {
58
80
  "node": ">=26.7.0"
59
81
  },
60
- "packageManager": "pnpm@11.22.0"
61
- }
82
+ "scripts": {
83
+ "build": "vp pack",
84
+ "check": "vp check --fix"
85
+ }
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,107 +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
- email: env?.CONSTRUCTION_GUEST_EMAIL ?? process.env.CONSTRUCTION_GUEST_EMAIL,
14
- password: env?.CONSTRUCTION_GUEST_PASSWORD ?? process.env.CONSTRUCTION_GUEST_PASSWORD,
15
- secret:
16
- env?.CONSTRUCTION_GUEST_SECRET ??
17
- process.env.CONSTRUCTION_GUEST_SECRET ??
18
- env?.BETTER_AUTH_SECRET ??
19
- process.env.BETTER_AUTH_SECRET
20
- })
21
-
22
- const encode = (value: Uint8Array) =>
23
- btoa(String.fromCharCode(...value))
24
- .replace(/\+/g, "-")
25
- .replace(/\//g, "_")
26
- .replace(/=+$/, "")
27
-
28
- const decode = (value: string) => {
29
- const padded = value
30
- .replace(/-/g, "+")
31
- .replace(/_/g, "/")
32
- .padEnd(Math.ceil(value.length / 4) * 4, "=")
33
- return Uint8Array.from(atob(padded), (char) => char.charCodeAt(0))
34
- }
35
-
36
- const sign = async (payload: string, secret: string) => {
37
- const key = await crypto.subtle.importKey(
38
- "raw",
39
- new TextEncoder().encode(secret),
40
- { name: "HMAC", hash: "SHA-256" },
41
- false,
42
- ["sign", "verify"]
43
- )
44
- return {
45
- key,
46
- signature: await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(payload))
47
- }
48
- }
49
-
50
- export const isConstructionGuest = async (c: any) => {
51
- const token = getCookie(c, CONSTRUCTION_GUEST_COOKIE)
52
- const secret = getConfig(c.env).secret
53
- if (!token || !secret) return false
54
-
55
- try {
56
- const [encodedPayload, encodedSignature] = token.split(".")
57
- if (!encodedPayload || !encodedSignature) return false
58
- const payload = new TextDecoder().decode(decode(encodedPayload))
59
- const { key } = await sign(payload, secret)
60
- const valid = await crypto.subtle.verify(
61
- "HMAC",
62
- key,
63
- decode(encodedSignature),
64
- new TextEncoder().encode(payload)
65
- )
66
- return valid && JSON.parse(payload).expiresAt > Date.now()
67
- } catch {
68
- return false
69
- }
70
- }
71
-
72
- export const signInConstructionGuest = async (
73
- c: any,
74
- email: string,
75
- password: string,
76
- rememberMe = true
77
- ) => {
78
- const config = getConfig(c.env)
79
- if (
80
- !config.email ||
81
- !config.password ||
82
- !config.secret ||
83
- email !== config.email ||
84
- password !== config.password
85
- ) {
86
- return false
87
- }
88
-
89
- const payload = JSON.stringify({
90
- email: config.email,
91
- expiresAt: Date.now() + 1000 * 60 * 60 * 24 * 7
92
- })
93
- const { signature } = await sign(payload, config.secret)
94
- setCookie(
95
- c,
96
- CONSTRUCTION_GUEST_COOKIE,
97
- encode(new TextEncoder().encode(payload)) + "." + encode(new Uint8Array(signature)),
98
- {
99
- httpOnly: true,
100
- secure: true,
101
- sameSite: "Lax",
102
- path: "/",
103
- ...(rememberMe ? { maxAge: 60 * 60 * 24 * 7 } : {})
104
- }
105
- )
106
- return true
107
- }
@@ -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
- }