@jskit-ai/auth-provider-local-core 0.1.1

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,57 @@
1
+ import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
+
3
+ function base64urlJson(value) {
4
+ return Buffer.from(JSON.stringify(value)).toString("base64url");
5
+ }
6
+
7
+ function parseBase64urlJson(value) {
8
+ return JSON.parse(Buffer.from(String(value || ""), "base64url").toString("utf8"));
9
+ }
10
+
11
+ function randomToken(bytes = 32) {
12
+ return randomBytes(bytes).toString("base64url");
13
+ }
14
+
15
+ function sha256Base64url(value) {
16
+ return createHash("sha256")
17
+ .update(String(value || ""))
18
+ .digest("base64url");
19
+ }
20
+
21
+ function signToken(payload, secret) {
22
+ const header = {
23
+ alg: "HS256",
24
+ typ: "JWT"
25
+ };
26
+ const encodedHeader = base64urlJson(header);
27
+ const encodedPayload = base64urlJson(payload);
28
+ const body = `${encodedHeader}.${encodedPayload}`;
29
+ const signature = createHmac("sha256", String(secret || ""))
30
+ .update(body)
31
+ .digest("base64url");
32
+ return `${body}.${signature}`;
33
+ }
34
+
35
+ function verifySignedToken(token, secret) {
36
+ const parts = String(token || "").split(".");
37
+ if (parts.length !== 3) {
38
+ return null;
39
+ }
40
+ const body = `${parts[0]}.${parts[1]}`;
41
+ const expected = createHmac("sha256", String(secret || ""))
42
+ .update(body)
43
+ .digest("base64url");
44
+ const actual = parts[2];
45
+ const expectedBuffer = Buffer.from(expected);
46
+ const actualBuffer = Buffer.from(actual);
47
+ if (expectedBuffer.length !== actualBuffer.length || !timingSafeEqual(expectedBuffer, actualBuffer)) {
48
+ return null;
49
+ }
50
+ const payload = parseBase64urlJson(parts[1]);
51
+ if (Number(payload.exp || 0) > 0 && Number(payload.exp) <= Math.floor(Date.now() / 1000)) {
52
+ return null;
53
+ }
54
+ return payload;
55
+ }
56
+
57
+ export { randomToken, sha256Base64url, signToken, verifySignedToken };
@@ -0,0 +1,194 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { randomBytes } from "node:crypto";
4
+ import { createLocalAuthService } from "../lib/service.js";
5
+ import { createLocalFileBackend } from "../lib/fileBackend.js";
6
+
7
+ const DEFAULT_STORE_DIR = ".jskit/auth";
8
+
9
+ function parseBoolean(value, fallback = false) {
10
+ const raw = String(value || "").trim().toLowerCase();
11
+ if (!raw) {
12
+ return fallback;
13
+ }
14
+ if (["1", "true", "yes", "on"].includes(raw)) {
15
+ return true;
16
+ }
17
+ if (["0", "false", "no", "off"].includes(raw)) {
18
+ return false;
19
+ }
20
+ return fallback;
21
+ }
22
+
23
+ function resolveRuntimeEnv(scope) {
24
+ const env = scope && typeof scope.has === "function" && scope.has("jskit.env") ? scope.make("jskit.env") : {};
25
+ return {
26
+ ...process.env,
27
+ ...(env && typeof env === "object" ? env : {})
28
+ };
29
+ }
30
+
31
+ function assertSelectedAuthProvider(env) {
32
+ const selectedProvider = String(env?.AUTH_PROVIDER || "").trim().toLowerCase();
33
+ if (selectedProvider && selectedProvider !== "local") {
34
+ throw new Error(
35
+ `AUTH_PROVIDER is "${selectedProvider}", but @jskit-ai/auth-provider-local-core is installed as the selected auth provider.`
36
+ );
37
+ }
38
+ }
39
+
40
+ function resolveStoreDir(env) {
41
+ return path.resolve(String(env.AUTH_LOCAL_STORE_DIR || DEFAULT_STORE_DIR).trim() || DEFAULT_STORE_DIR);
42
+ }
43
+
44
+ function resolveSessionSecret(env, { storeDir, isProduction }) {
45
+ const explicit = String(env.AUTH_LOCAL_SESSION_SECRET || "").trim();
46
+ if (explicit) {
47
+ return explicit;
48
+ }
49
+ if (isProduction) {
50
+ throw new Error("AUTH_LOCAL_SESSION_SECRET is required for local auth in production.");
51
+ }
52
+
53
+ const secretPath = path.join(storeDir, "session.secret");
54
+ try {
55
+ const existing = fs.readFileSync(secretPath, "utf8").trim();
56
+ if (existing) {
57
+ return existing;
58
+ }
59
+ } catch (error) {
60
+ if (!error || error.code !== "ENOENT") {
61
+ throw error;
62
+ }
63
+ }
64
+
65
+ fs.mkdirSync(storeDir, { recursive: true, mode: 0o700 });
66
+ const generated = randomBytes(32).toString("base64url");
67
+ fs.writeFileSync(secretPath, `${generated}\n`, { mode: 0o600 });
68
+ return generated;
69
+ }
70
+
71
+ function resolveSmtpConfig(env) {
72
+ const host = String(env.AUTH_LOCAL_SMTP_HOST || "").trim();
73
+ if (!host) {
74
+ return {
75
+ smtpConfigured: false,
76
+ smtp: null
77
+ };
78
+ }
79
+ const from = String(env.AUTH_LOCAL_SMTP_FROM || "").trim();
80
+ if (!from) {
81
+ throw new Error("AUTH_LOCAL_SMTP_FROM is required when AUTH_LOCAL_SMTP_HOST is set.");
82
+ }
83
+ return {
84
+ smtpConfigured: true,
85
+ smtp: {
86
+ host,
87
+ port: Number(env.AUTH_LOCAL_SMTP_PORT || 587),
88
+ secure: parseBoolean(env.AUTH_LOCAL_SMTP_SECURE, false),
89
+ user: String(env.AUTH_LOCAL_SMTP_USER || "").trim(),
90
+ password: String(env.AUTH_LOCAL_SMTP_PASSWORD || ""),
91
+ from,
92
+ replyTo: String(env.AUTH_LOCAL_SMTP_REPLY_TO || "").trim()
93
+ }
94
+ };
95
+ }
96
+
97
+ function resolveAppPublicUrl(env, { smtpConfigured }) {
98
+ const configured = String(env.APP_PUBLIC_URL || "").trim();
99
+ if (!configured) {
100
+ if (smtpConfigured) {
101
+ throw new Error("APP_PUBLIC_URL is required when local auth SMTP recovery is configured.");
102
+ }
103
+ return "http://localhost:5173";
104
+ }
105
+
106
+ let parsed;
107
+ try {
108
+ parsed = new URL(configured);
109
+ } catch {
110
+ throw new Error("APP_PUBLIC_URL must be a valid URL when local auth SMTP recovery is configured.");
111
+ }
112
+
113
+ if (!["http:", "https:"].includes(parsed.protocol)) {
114
+ throw new Error("APP_PUBLIC_URL must use http or https when local auth SMTP recovery is configured.");
115
+ }
116
+
117
+ return parsed.toString().replace(/\/$/, "");
118
+ }
119
+
120
+ function resolveConfig(scope) {
121
+ const env = resolveRuntimeEnv(scope);
122
+ assertSelectedAuthProvider(env);
123
+ const nodeEnv = String(env.NODE_ENV || "development").trim() || "development";
124
+ const isProduction = nodeEnv === "production";
125
+ const backend = String(env.AUTH_LOCAL_BACKEND || "file").trim().toLowerCase() || "file";
126
+ const storeDir = resolveStoreDir(env);
127
+ if (backend === "file" && isProduction && !parseBoolean(env.AUTH_LOCAL_FILE_PRODUCTION_ACK, false)) {
128
+ throw new Error("AUTH_LOCAL_FILE_PRODUCTION_ACK is required to use the local file auth backend in production.");
129
+ }
130
+ const smtp = resolveSmtpConfig(env);
131
+ return {
132
+ backend,
133
+ storeDir,
134
+ nodeEnv,
135
+ logger: scope.has("jskit.logger") ? scope.make("jskit.logger") : console,
136
+ appPublicUrl: resolveAppPublicUrl(env, smtp),
137
+ sessionSecret: resolveSessionSecret(env, {
138
+ storeDir,
139
+ isProduction
140
+ }),
141
+ recoveryDevOutput: String(env.AUTH_LOCAL_RECOVERY_DEV_OUTPUT || "log").trim().toLowerCase() || "log",
142
+ ...smtp
143
+ };
144
+ }
145
+
146
+ class AuthLocalServiceProvider {
147
+ static id = "auth.provider.local";
148
+
149
+ register(app) {
150
+ if (!app || typeof app.singleton !== "function" || typeof app.has !== "function") {
151
+ throw new Error("AuthLocalServiceProvider requires application singleton()/has().");
152
+ }
153
+
154
+ assertSelectedAuthProvider(resolveRuntimeEnv(app));
155
+
156
+ if (app.has("authService")) {
157
+ throw new Error("AuthLocalServiceProvider cannot register authService because another auth provider already registered it.");
158
+ }
159
+
160
+ if (!app.has("auth.local.backend")) {
161
+ app.singleton("auth.local.backend", (scope) => {
162
+ const config = resolveConfig(scope);
163
+ if (config.backend !== "file") {
164
+ throw new Error(`AUTH_LOCAL_BACKEND="${config.backend}" requires a custom auth.local.backend provider.`);
165
+ }
166
+ return createLocalFileBackend({
167
+ storeDir: config.storeDir
168
+ });
169
+ });
170
+ }
171
+
172
+ app.singleton("authService", (scope) => {
173
+ const config = resolveConfig(scope);
174
+ const backend = scope.make("auth.local.backend");
175
+ const profileProjector = scope.has("auth.profile.projector")
176
+ ? scope.make("auth.profile.projector")
177
+ : null;
178
+ return createLocalAuthService({
179
+ backend,
180
+ config,
181
+ profileProjector
182
+ });
183
+ });
184
+ }
185
+
186
+ boot(app) {
187
+ if (!app || typeof app.make !== "function") {
188
+ throw new Error("AuthLocalServiceProvider requires application make().");
189
+ }
190
+ app.make("authService");
191
+ }
192
+ }
193
+
194
+ export { AuthLocalServiceProvider };
@@ -0,0 +1,9 @@
1
+ class AuthProviderServiceProvider {
2
+ static id = "auth.provider";
3
+
4
+ static dependsOn = ["auth.provider.local"];
5
+
6
+ register() {}
7
+ }
8
+
9
+ export { AuthProviderServiceProvider };