@oneunit/auth 2.0.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.
Files changed (75) hide show
  1. package/ARCHITECTURE.md +465 -0
  2. package/CHANGELOG.md +214 -0
  3. package/LICENSE +21 -0
  4. package/README.md +647 -0
  5. package/dist/adapters.d.ts +51 -0
  6. package/dist/adapters.d.ts.map +1 -0
  7. package/dist/adapters.js +301 -0
  8. package/dist/adapters.js.map +1 -0
  9. package/dist/auth.d.ts +59 -0
  10. package/dist/auth.d.ts.map +1 -0
  11. package/dist/auth.js +560 -0
  12. package/dist/auth.js.map +1 -0
  13. package/dist/errors.d.ts +39 -0
  14. package/dist/errors.d.ts.map +1 -0
  15. package/dist/errors.js +65 -0
  16. package/dist/errors.js.map +1 -0
  17. package/dist/index.d.ts +11 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +10 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/jwt.d.ts +11 -0
  22. package/dist/jwt.d.ts.map +1 -0
  23. package/dist/jwt.js +125 -0
  24. package/dist/jwt.js.map +1 -0
  25. package/dist/oauth.d.ts +31 -0
  26. package/dist/oauth.d.ts.map +1 -0
  27. package/dist/oauth.js +178 -0
  28. package/dist/oauth.js.map +1 -0
  29. package/dist/password.d.ts +5 -0
  30. package/dist/password.d.ts.map +1 -0
  31. package/dist/password.js +90 -0
  32. package/dist/password.js.map +1 -0
  33. package/dist/providers.d.ts +37 -0
  34. package/dist/providers.d.ts.map +1 -0
  35. package/dist/providers.js +471 -0
  36. package/dist/providers.js.map +1 -0
  37. package/dist/rbac.d.ts +40 -0
  38. package/dist/rbac.d.ts.map +1 -0
  39. package/dist/rbac.js +240 -0
  40. package/dist/rbac.js.map +1 -0
  41. package/dist/roles.d.ts +2 -0
  42. package/dist/roles.d.ts.map +1 -0
  43. package/dist/roles.js +2 -0
  44. package/dist/roles.js.map +1 -0
  45. package/dist/token.d.ts +2 -0
  46. package/dist/token.d.ts.map +1 -0
  47. package/dist/token.js +2 -0
  48. package/dist/token.js.map +1 -0
  49. package/dist/types.d.ts +383 -0
  50. package/dist/types.d.ts.map +1 -0
  51. package/dist/types.js +2 -0
  52. package/dist/types.js.map +1 -0
  53. package/dist/utils.d.ts +35 -0
  54. package/dist/utils.d.ts.map +1 -0
  55. package/dist/utils.js +192 -0
  56. package/dist/utils.js.map +1 -0
  57. package/examples/express.ts +111 -0
  58. package/examples/fastify.ts +59 -0
  59. package/examples/oauth-social.ts +83 -0
  60. package/examples/standalone.ts +67 -0
  61. package/examples/uwebsockets.ts +143 -0
  62. package/package.json +86 -0
  63. package/src/adapters.ts +333 -0
  64. package/src/auth.ts +684 -0
  65. package/src/errors.ts +76 -0
  66. package/src/index.ts +124 -0
  67. package/src/jwt.ts +159 -0
  68. package/src/oauth.ts +226 -0
  69. package/src/password.ts +111 -0
  70. package/src/providers.ts +551 -0
  71. package/src/rbac.ts +285 -0
  72. package/src/roles.ts +1 -0
  73. package/src/token.ts +1 -0
  74. package/src/types.ts +432 -0
  75. package/src/utils.ts +231 -0
package/src/utils.ts ADDED
@@ -0,0 +1,231 @@
1
+ import { randomBytes, createHash, timingSafeEqual } from "node:crypto";
2
+ import { ValidationError } from "./errors.js";
3
+
4
+ const UNIT_SECONDS = Object.freeze({
5
+ s: 1,
6
+ m: 60,
7
+ h: 60 * 60,
8
+ d: 60 * 60 * 24,
9
+ w: 60 * 60 * 24 * 7,
10
+ });
11
+
12
+ type TimeUnit = keyof typeof UNIT_SECONDS;
13
+
14
+ const EXPIRES_IN_PATTERN = /^(\d+)\s*([smhdw])$/i;
15
+
16
+ /**
17
+ * True when `value` is a TTL this library can interpret unambiguously. Values
18
+ * jsonwebtoken accepts but this library cannot (`"1y"`, `"2 hours"`, `"-5m"`)
19
+ * are rejected so the signed token and any locally computed expiry never diverge.
20
+ */
21
+ export function isValidExpiresIn(value: string | number | undefined | null): boolean {
22
+ if (typeof value === "number") {
23
+ return Number.isFinite(value);
24
+ }
25
+ if (typeof value !== "string") {
26
+ return false;
27
+ }
28
+ return EXPIRES_IN_PATTERN.test(value.trim());
29
+ }
30
+
31
+ export function parseExpiresIn(value: string | number | undefined | null, fallbackSeconds = 60 * 60 * 24): number {
32
+ if (typeof value === "number" && Number.isFinite(value)) {
33
+ return Math.max(0, Math.trunc(value));
34
+ }
35
+
36
+ if (typeof value !== "string") {
37
+ return fallbackSeconds;
38
+ }
39
+
40
+ const trimmed = value.trim();
41
+ if (!trimmed) {
42
+ return fallbackSeconds;
43
+ }
44
+
45
+ const match = trimmed.match(EXPIRES_IN_PATTERN);
46
+ if (!match) {
47
+ return fallbackSeconds;
48
+ }
49
+
50
+ const amount = Number.parseInt(match[1], 10);
51
+ const unit = match[2].toLowerCase() as TimeUnit;
52
+ return amount * UNIT_SECONDS[unit];
53
+ }
54
+
55
+ export function randomToken(bytes = 32): string {
56
+ return randomBytes(bytes).toString("hex");
57
+ }
58
+
59
+ export function randomState(bytes = 16): string {
60
+ return randomBytes(bytes).toString("base64url");
61
+ }
62
+
63
+ export function sha256(value: unknown): string {
64
+ return createHash("sha256").update(String(value)).digest("hex");
65
+ }
66
+
67
+ export function timingSafeEqualString(a: unknown, b: unknown): boolean {
68
+ const left = Buffer.from(String(a ?? ""), "utf8");
69
+ const right = Buffer.from(String(b ?? ""), "utf8");
70
+ if (left.length !== right.length) {
71
+ return false;
72
+ }
73
+ return timingSafeEqual(left, right);
74
+ }
75
+
76
+ export function assertNonEmptyString(value: unknown, name: string): asserts value is string {
77
+ if (typeof value !== "string" || value.length === 0) {
78
+ throw new ValidationError(`${name} must be a non-empty string`);
79
+ }
80
+ }
81
+
82
+ export function isPlainObject(value: unknown): value is Record<string, unknown> {
83
+ return value !== null && typeof value === "object" && !Array.isArray(value);
84
+ }
85
+
86
+ export function unique<T>(values: T[]): T[] {
87
+ return [...new Set(values.filter((item) => item !== undefined && item !== null && item !== ""))];
88
+ }
89
+
90
+ export function toArray(value: unknown): unknown[] {
91
+ if (value === undefined || value === null) {
92
+ return [];
93
+ }
94
+ return Array.isArray(value) ? value.flat(Infinity) : [value];
95
+ }
96
+
97
+ export function pick<T extends Record<string, unknown>>(object: T, keys: Array<keyof T>): Partial<T> {
98
+ const result: Partial<T> = {};
99
+ for (const key of keys) {
100
+ if (object[key] !== undefined) {
101
+ result[key] = object[key];
102
+ }
103
+ }
104
+ return result;
105
+ }
106
+
107
+ export function buildUrl(base: string, path: string, query: Record<string, unknown> = {}): string {
108
+ const url = new URL(path, base);
109
+ for (const [key, value] of Object.entries(query)) {
110
+ if (value !== undefined && value !== null && value !== "") {
111
+ url.searchParams.set(key, String(value));
112
+ }
113
+ }
114
+ return url.toString();
115
+ }
116
+
117
+ export function extractBearerToken(header: string | undefined | null): string | null {
118
+ if (typeof header !== "string") {
119
+ return null;
120
+ }
121
+ const match = header.match(/^Bearer\s+(.+)$/i);
122
+ return match ? match[1].trim() : null;
123
+ }
124
+
125
+ export function parseCookieHeader(header: string | undefined | null): Record<string, string> {
126
+ if (typeof header !== "string" || header.length === 0) {
127
+ return {};
128
+ }
129
+ const cookies: Record<string, string> = {};
130
+ for (const part of header.split(";")) {
131
+ const index = part.indexOf("=");
132
+ if (index === -1) {
133
+ continue;
134
+ }
135
+ const key = part.slice(0, index).trim();
136
+ const value = part.slice(index + 1).trim();
137
+ try {
138
+ cookies[key] = decodeURIComponent(value);
139
+ } catch {
140
+ cookies[key] = value;
141
+ }
142
+ }
143
+ return cookies;
144
+ }
145
+
146
+ export function parseQueryString(value: unknown): Record<string, string> {
147
+ if (!value) {
148
+ return {};
149
+ }
150
+ if (typeof value === "object" && !Array.isArray(value)) {
151
+ const result: Record<string, string> = {};
152
+ for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
153
+ if (item !== undefined && item !== null) {
154
+ result[key] = String(Array.isArray(item) ? item[0] : item);
155
+ }
156
+ }
157
+ return result;
158
+ }
159
+ return Object.fromEntries(new URLSearchParams(String(value)));
160
+ }
161
+
162
+ export function getHeader(
163
+ headers: Record<string, unknown> | { get?(name: string): string | null } | undefined | null,
164
+ name: string,
165
+ ): string | undefined {
166
+ if (!headers) {
167
+ return undefined;
168
+ }
169
+ if (typeof (headers as { get?: unknown }).get === "function") {
170
+ const getter = headers as { get(name: string): string | null };
171
+ return getter.get(name) ?? getter.get(name.toLowerCase()) ?? undefined;
172
+ }
173
+ const map = headers as Record<string, unknown>;
174
+ const value = map[name] ?? map[name.toLowerCase()] ?? map[name.toUpperCase()];
175
+ if (Array.isArray(value)) {
176
+ return value[0] === undefined ? undefined : String(value[0]);
177
+ }
178
+ return value === undefined || value === null ? undefined : String(value);
179
+ }
180
+
181
+ export interface JsonRequestOptions extends RequestInit {
182
+ timeout?: number;
183
+ }
184
+
185
+ export interface JsonResponse {
186
+ ok: boolean;
187
+ status: number;
188
+ body: unknown;
189
+ headers: Headers;
190
+ }
191
+
192
+ export async function requestJson(url: string, options: JsonRequestOptions = {}): Promise<JsonResponse> {
193
+ const { timeout = 15000, ...init } = options;
194
+ const controller = new AbortController();
195
+ const timer = setTimeout(() => controller.abort(), timeout);
196
+
197
+ try {
198
+ const response = await fetch(url, {
199
+ ...init,
200
+ signal: controller.signal,
201
+ headers: {
202
+ accept: "application/json",
203
+ ...(init.headers ?? {}),
204
+ },
205
+ });
206
+
207
+ const text = await response.text();
208
+ let body: unknown = null;
209
+ if (text) {
210
+ try {
211
+ body = JSON.parse(text);
212
+ } catch {
213
+ body = text;
214
+ }
215
+ }
216
+
217
+ return { ok: response.ok, status: response.status, body, headers: response.headers };
218
+ } finally {
219
+ clearTimeout(timer);
220
+ }
221
+ }
222
+
223
+ export function formEncode(data: Record<string, unknown>): URLSearchParams {
224
+ const params = new URLSearchParams();
225
+ for (const [key, value] of Object.entries(data)) {
226
+ if (value !== undefined && value !== null) {
227
+ params.set(key, String(value));
228
+ }
229
+ }
230
+ return params;
231
+ }