@jskit-ai/auth-provider-supabase-core 0.1.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.
- package/package.descriptor.mjs +138 -0
- package/package.json +20 -0
- package/src/client/index.js +1 -0
- package/src/server/lib/accountFlows.js +276 -0
- package/src/server/lib/actions/auth.contributor.js +225 -0
- package/src/server/lib/authCookies.js +24 -0
- package/src/server/lib/authErrorMappers.js +194 -0
- package/src/server/lib/authInputParsers.js +217 -0
- package/src/server/lib/authJwt.js +49 -0
- package/src/server/lib/authMethodStatus.js +233 -0
- package/src/server/lib/authProfileNames.js +24 -0
- package/src/server/lib/authRedirectUrls.js +135 -0
- package/src/server/lib/authSecrets.js +9 -0
- package/src/server/lib/authSessionEventsService.js +20 -0
- package/src/server/lib/index.js +2 -0
- package/src/server/lib/oauthFlows.js +201 -0
- package/src/server/lib/oauthProviderCatalog.js +272 -0
- package/src/server/lib/passwordSecurityFlows.js +306 -0
- package/src/server/lib/service.js +868 -0
- package/src/server/lib/standaloneProfileSyncService.js +92 -0
- package/src/server/lib/test-utils.js +47 -0
- package/src/server/providers/AuthProviderServiceProvider.js +9 -0
- package/src/server/providers/AuthSupabaseServiceProvider.js +217 -0
- package/test/auth.provider.supabase.test.js +7 -0
- package/test/authRedirectUrls.test.js +47 -0
- package/test/entrypoints.boundary.test.js +20 -0
- package/test/index.test.js +7 -0
- package/test/providerRuntime.test.js +156 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
export default Object.freeze({
|
|
2
|
+
"packageVersion": 1,
|
|
3
|
+
"packageId": "@jskit-ai/auth-provider-supabase-core",
|
|
4
|
+
"version": "0.1.4",
|
|
5
|
+
"options": {
|
|
6
|
+
"auth-supabase-url": {
|
|
7
|
+
"required": true,
|
|
8
|
+
"values": [],
|
|
9
|
+
"promptLabel": "Supabase URL",
|
|
10
|
+
"promptHint": "https://YOUR-PROJECT.supabase.co"
|
|
11
|
+
},
|
|
12
|
+
"auth-supabase-publishable-key": {
|
|
13
|
+
"required": true,
|
|
14
|
+
"values": [],
|
|
15
|
+
"promptLabel": "Supabase publishable key",
|
|
16
|
+
"promptHint": "sb_publishable_..."
|
|
17
|
+
},
|
|
18
|
+
"app-public-url": {
|
|
19
|
+
"required": true,
|
|
20
|
+
"values": [],
|
|
21
|
+
"defaultValue": "http://localhost:5173",
|
|
22
|
+
"promptLabel": "App public URL",
|
|
23
|
+
"promptHint": "Browser URL used for auth redirects"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"dependsOn": [
|
|
27
|
+
"@jskit-ai/auth-core",
|
|
28
|
+
"@jskit-ai/value-app-config-shared"
|
|
29
|
+
],
|
|
30
|
+
"capabilities": {
|
|
31
|
+
"provides": [
|
|
32
|
+
"auth.provider.supabase",
|
|
33
|
+
"auth.provider"
|
|
34
|
+
],
|
|
35
|
+
"requires": [
|
|
36
|
+
"auth.access"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
"runtime": {
|
|
40
|
+
"server": {
|
|
41
|
+
"providerEntrypoint": "src/server/providers/AuthSupabaseServiceProvider.js",
|
|
42
|
+
"providers": [
|
|
43
|
+
{
|
|
44
|
+
"entrypoint": "src/server/providers/AuthSupabaseServiceProvider.js",
|
|
45
|
+
"export": "AuthSupabaseServiceProvider"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"entrypoint": "src/server/providers/AuthProviderServiceProvider.js",
|
|
49
|
+
"export": "AuthProviderServiceProvider"
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"metadata": {
|
|
55
|
+
"apiSummary": {
|
|
56
|
+
"surfaces": [
|
|
57
|
+
{
|
|
58
|
+
"subpath": "./server/providers/AuthSupabaseServiceProvider",
|
|
59
|
+
"summary": "Exports the Supabase auth provider service provider."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"subpath": "./server/providers/AuthProviderServiceProvider",
|
|
63
|
+
"summary": "Exports the generic auth provider registration service provider."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"subpath": "./server/lib/index",
|
|
67
|
+
"summary": "Exports curated server-side Supabase auth service helpers."
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"subpath": "./client",
|
|
71
|
+
"summary": "Exports no runtime API today (reserved client entrypoint)."
|
|
72
|
+
}
|
|
73
|
+
],
|
|
74
|
+
"containerTokens": {
|
|
75
|
+
"server": [
|
|
76
|
+
"authService",
|
|
77
|
+
"auth.provider.supabase.actionContributor"
|
|
78
|
+
],
|
|
79
|
+
"client": []
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"mutations": {
|
|
84
|
+
"dependencies": {
|
|
85
|
+
"runtime": {
|
|
86
|
+
"@jskit-ai/auth-core": "0.1.4",
|
|
87
|
+
"@jskit-ai/kernel": "0.1.4",
|
|
88
|
+
"dotenv": "^16.4.5",
|
|
89
|
+
"@supabase/supabase-js": "^2.57.4",
|
|
90
|
+
"jose": "^6.1.0"
|
|
91
|
+
},
|
|
92
|
+
"dev": {}
|
|
93
|
+
},
|
|
94
|
+
"packageJson": {
|
|
95
|
+
"scripts": {}
|
|
96
|
+
},
|
|
97
|
+
"procfile": {},
|
|
98
|
+
"files": [],
|
|
99
|
+
"text": [
|
|
100
|
+
{
|
|
101
|
+
"file": ".env",
|
|
102
|
+
"op": "upsert-env",
|
|
103
|
+
"key": "AUTH_PROVIDER",
|
|
104
|
+
"value": "supabase",
|
|
105
|
+
"reason": "Select Supabase as the auth provider.",
|
|
106
|
+
"category": "runtime-config",
|
|
107
|
+
"id": "auth-provider"
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
"file": ".env",
|
|
111
|
+
"op": "upsert-env",
|
|
112
|
+
"key": "AUTH_SUPABASE_URL",
|
|
113
|
+
"value": "${option:auth-supabase-url}",
|
|
114
|
+
"reason": "Configure Supabase project URL for auth.",
|
|
115
|
+
"category": "runtime-config",
|
|
116
|
+
"id": "auth-supabase-url"
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"file": ".env",
|
|
120
|
+
"op": "upsert-env",
|
|
121
|
+
"key": "AUTH_SUPABASE_PUBLISHABLE_KEY",
|
|
122
|
+
"value": "${option:auth-supabase-publishable-key}",
|
|
123
|
+
"reason": "Configure Supabase publishable key for auth.",
|
|
124
|
+
"category": "runtime-config",
|
|
125
|
+
"id": "auth-supabase-publishable-key"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"file": ".env",
|
|
129
|
+
"op": "upsert-env",
|
|
130
|
+
"key": "APP_PUBLIC_URL",
|
|
131
|
+
"value": "${option:app-public-url}",
|
|
132
|
+
"reason": "Configure application public URL for auth redirect flows.",
|
|
133
|
+
"category": "runtime-config",
|
|
134
|
+
"id": "auth-app-public-url"
|
|
135
|
+
}
|
|
136
|
+
]
|
|
137
|
+
}
|
|
138
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jskit-ai/auth-provider-supabase-core",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "node --test"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
"./server/providers/AuthSupabaseServiceProvider": "./src/server/providers/AuthSupabaseServiceProvider.js",
|
|
10
|
+
"./server/providers/AuthProviderServiceProvider": "./src/server/providers/AuthProviderServiceProvider.js",
|
|
11
|
+
"./server/lib/index": "./src/server/lib/index.js",
|
|
12
|
+
"./client": "./src/client/index.js"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@jskit-ai/auth-core": "0.1.4",
|
|
16
|
+
"@jskit-ai/kernel": "0.1.4",
|
|
17
|
+
"jose": "^6.1.0",
|
|
18
|
+
"@supabase/supabase-js": "^2.57.4"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
2
|
+
|
|
3
|
+
function normalizeLocalReturnToPath(value, { fallback = "" } = {}) {
|
|
4
|
+
const normalized = String(value || "").trim();
|
|
5
|
+
if (!normalized) {
|
|
6
|
+
return fallback;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (!normalized.startsWith("/") || normalized.startsWith("//")) {
|
|
10
|
+
return fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return normalized;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function createAccountFlows(deps) {
|
|
17
|
+
const {
|
|
18
|
+
ensureConfigured,
|
|
19
|
+
validators,
|
|
20
|
+
validationError,
|
|
21
|
+
getSupabaseClient,
|
|
22
|
+
displayNameFromEmail,
|
|
23
|
+
mapAuthError,
|
|
24
|
+
syncProfileFromSupabaseUser,
|
|
25
|
+
resolvePasswordSignInPolicyForUserId,
|
|
26
|
+
otpLoginRedirectUrl,
|
|
27
|
+
buildOtpLoginRedirectUrl,
|
|
28
|
+
appPublicUrl,
|
|
29
|
+
isTransientSupabaseError,
|
|
30
|
+
isUserNotFoundLikeAuthError,
|
|
31
|
+
parseOtpLoginVerifyPayload,
|
|
32
|
+
mapOtpVerifyError,
|
|
33
|
+
setSessionFromRequestCookies,
|
|
34
|
+
mapProfileUpdateError,
|
|
35
|
+
normalizeReturnToPath = normalizeLocalReturnToPath
|
|
36
|
+
} = deps;
|
|
37
|
+
|
|
38
|
+
function resolveOtpEmailRedirectTo(returnToValue) {
|
|
39
|
+
const returnTo = normalizeReturnToPath(returnToValue, { fallback: "" });
|
|
40
|
+
if (!returnTo) {
|
|
41
|
+
return otpLoginRedirectUrl;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof buildOtpLoginRedirectUrl === "function") {
|
|
45
|
+
try {
|
|
46
|
+
return buildOtpLoginRedirectUrl({
|
|
47
|
+
appPublicUrl,
|
|
48
|
+
returnTo
|
|
49
|
+
});
|
|
50
|
+
} catch {
|
|
51
|
+
// Fall through to the pre-built URL fallback below.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const redirectUrl = new URL(String(otpLoginRedirectUrl || ""));
|
|
57
|
+
redirectUrl.searchParams.set("returnTo", returnTo);
|
|
58
|
+
return redirectUrl.toString();
|
|
59
|
+
} catch {
|
|
60
|
+
return otpLoginRedirectUrl;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function register(payload) {
|
|
65
|
+
ensureConfigured();
|
|
66
|
+
|
|
67
|
+
const parsed = validators.registerInput(payload);
|
|
68
|
+
if (Object.keys(parsed.fieldErrors).length > 0) {
|
|
69
|
+
throw validationError(parsed.fieldErrors);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const supabase = getSupabaseClient();
|
|
73
|
+
const response = await supabase.auth.signUp({
|
|
74
|
+
email: parsed.email,
|
|
75
|
+
password: parsed.password,
|
|
76
|
+
options: {
|
|
77
|
+
data: {
|
|
78
|
+
display_name: displayNameFromEmail(parsed.email)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (response.error) {
|
|
84
|
+
throw mapAuthError(response.error, 400);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!response.data?.user) {
|
|
88
|
+
throw new AppError(500, "Supabase sign-up did not return a user.");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const profile = await syncProfileFromSupabaseUser(response.data.user, parsed.email);
|
|
92
|
+
|
|
93
|
+
if (!response.data.session) {
|
|
94
|
+
return {
|
|
95
|
+
requiresEmailConfirmation: true,
|
|
96
|
+
email: parsed.email,
|
|
97
|
+
profile,
|
|
98
|
+
session: null
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
requiresEmailConfirmation: false,
|
|
104
|
+
profile,
|
|
105
|
+
session: response.data.session
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function login(payload) {
|
|
110
|
+
ensureConfigured();
|
|
111
|
+
|
|
112
|
+
const parsed = validators.loginInput(payload);
|
|
113
|
+
if (Object.keys(parsed.fieldErrors).length > 0) {
|
|
114
|
+
throw validationError(parsed.fieldErrors);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const supabase = getSupabaseClient();
|
|
118
|
+
const response = await supabase.auth.signInWithPassword({
|
|
119
|
+
email: parsed.email,
|
|
120
|
+
password: parsed.password
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
if (response.error || !response.data?.user || !response.data?.session) {
|
|
124
|
+
throw mapAuthError(response.error, 401);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const profile = await syncProfileFromSupabaseUser(response.data.user, parsed.email);
|
|
128
|
+
const passwordSignInPolicy = await resolvePasswordSignInPolicyForUserId(profile.id);
|
|
129
|
+
if (!passwordSignInPolicy.passwordSignInEnabled) {
|
|
130
|
+
throw new AppError(401, "Invalid email or password.");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
profile,
|
|
135
|
+
session: response.data.session
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function requestOtpLogin(payload) {
|
|
140
|
+
ensureConfigured();
|
|
141
|
+
|
|
142
|
+
const parsed = validators.forgotPasswordInput(payload);
|
|
143
|
+
if (Object.keys(parsed.fieldErrors).length > 0) {
|
|
144
|
+
throw validationError(parsed.fieldErrors);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const emailRedirectTo = resolveOtpEmailRedirectTo(payload?.returnTo);
|
|
148
|
+
const supabase = getSupabaseClient();
|
|
149
|
+
let response;
|
|
150
|
+
try {
|
|
151
|
+
response = await supabase.auth.signInWithOtp({
|
|
152
|
+
email: parsed.email,
|
|
153
|
+
options: {
|
|
154
|
+
shouldCreateUser: false,
|
|
155
|
+
emailRedirectTo
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
} catch (error) {
|
|
159
|
+
if (isTransientSupabaseError(error)) {
|
|
160
|
+
throw mapAuthError(error, 503);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
ok: true,
|
|
165
|
+
message: "If an account exists for that email, a one-time code has been sent."
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (response.error) {
|
|
170
|
+
if (isTransientSupabaseError(response.error)) {
|
|
171
|
+
throw mapAuthError(response.error, 503);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (isUserNotFoundLikeAuthError(response.error)) {
|
|
175
|
+
return {
|
|
176
|
+
ok: true,
|
|
177
|
+
message: "If an account exists for that email, a one-time code has been sent."
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
throw mapAuthError(response.error, Number(response.error?.status || 400));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
message: "If an account exists for that email, a one-time code has been sent."
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function verifyOtpLogin(payload) {
|
|
191
|
+
ensureConfigured();
|
|
192
|
+
|
|
193
|
+
const parsed = parseOtpLoginVerifyPayload(payload);
|
|
194
|
+
if (Object.keys(parsed.fieldErrors).length > 0) {
|
|
195
|
+
throw validationError(parsed.fieldErrors);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const supabase = getSupabaseClient();
|
|
199
|
+
let response;
|
|
200
|
+
try {
|
|
201
|
+
if (parsed.tokenHash) {
|
|
202
|
+
response = await supabase.auth.verifyOtp({
|
|
203
|
+
token_hash: parsed.tokenHash,
|
|
204
|
+
type: parsed.type
|
|
205
|
+
});
|
|
206
|
+
} else {
|
|
207
|
+
response = await supabase.auth.verifyOtp({
|
|
208
|
+
email: parsed.email,
|
|
209
|
+
token: parsed.token,
|
|
210
|
+
type: parsed.type
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
} catch (error) {
|
|
214
|
+
throw mapOtpVerifyError(error);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (response.error || !response.data?.session || !response.data?.user) {
|
|
218
|
+
throw mapOtpVerifyError(response.error);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const profile = await syncProfileFromSupabaseUser(response.data.user, response.data.user.email || parsed.email);
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
profile,
|
|
225
|
+
session: response.data.session
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function updateDisplayName(request, displayName) {
|
|
230
|
+
ensureConfigured();
|
|
231
|
+
|
|
232
|
+
const normalizedDisplayName = String(displayName || "").trim();
|
|
233
|
+
if (!normalizedDisplayName) {
|
|
234
|
+
throw validationError({
|
|
235
|
+
displayName: "Display name is required."
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const supabase = getSupabaseClient();
|
|
240
|
+
const sessionResponse = await setSessionFromRequestCookies(request, {
|
|
241
|
+
supabaseClient: supabase
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
let updateResponse;
|
|
245
|
+
try {
|
|
246
|
+
updateResponse = await supabase.auth.updateUser({
|
|
247
|
+
data: {
|
|
248
|
+
display_name: normalizedDisplayName
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
} catch (error) {
|
|
252
|
+
throw mapProfileUpdateError(error);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (updateResponse.error || !updateResponse.data?.user) {
|
|
256
|
+
throw mapProfileUpdateError(updateResponse.error);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const profile = await syncProfileFromSupabaseUser(updateResponse.data.user, updateResponse.data.user.email);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
profile,
|
|
263
|
+
session: sessionResponse.data.session
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
register,
|
|
269
|
+
login,
|
|
270
|
+
requestOtpLogin,
|
|
271
|
+
verifyOtpLogin,
|
|
272
|
+
updateDisplayName
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export { createAccountFlows };
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EMPTY_INPUT_VALIDATOR
|
|
3
|
+
} from "@jskit-ai/kernel/shared/actions/actionContributorHelpers";
|
|
4
|
+
import { authRegisterCommand } from "@jskit-ai/auth-core/shared/commands/authRegisterCommand";
|
|
5
|
+
import { authLoginPasswordCommand } from "@jskit-ai/auth-core/shared/commands/authLoginPasswordCommand";
|
|
6
|
+
import { authLoginOtpRequestCommand } from "@jskit-ai/auth-core/shared/commands/authLoginOtpRequestCommand";
|
|
7
|
+
import { authLoginOtpVerifyCommand } from "@jskit-ai/auth-core/shared/commands/authLoginOtpVerifyCommand";
|
|
8
|
+
import { authLoginOAuthStartCommand } from "@jskit-ai/auth-core/shared/commands/authLoginOAuthStartCommand";
|
|
9
|
+
import { authLoginOAuthCompleteCommand } from "@jskit-ai/auth-core/shared/commands/authLoginOAuthCompleteCommand";
|
|
10
|
+
import { authPasswordResetRequestCommand } from "@jskit-ai/auth-core/shared/commands/authPasswordResetRequestCommand";
|
|
11
|
+
import { authPasswordRecoveryCompleteCommand } from "@jskit-ai/auth-core/shared/commands/authPasswordRecoveryCompleteCommand";
|
|
12
|
+
import { authPasswordResetCommand } from "@jskit-ai/auth-core/shared/commands/authPasswordResetCommand";
|
|
13
|
+
|
|
14
|
+
function requireRequestContext(context, actionId) {
|
|
15
|
+
const request = context?.requestMeta?.request || null;
|
|
16
|
+
if (request) {
|
|
17
|
+
return request;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
throw new Error(`${actionId} requires request context.`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const authActions = Object.freeze([
|
|
24
|
+
{
|
|
25
|
+
id: "auth.register",
|
|
26
|
+
version: 1,
|
|
27
|
+
kind: "command",
|
|
28
|
+
channels: ["api", "internal"],
|
|
29
|
+
surfacesFrom: "enabled",
|
|
30
|
+
inputValidator: authRegisterCommand.operation.bodyValidator,
|
|
31
|
+
idempotency: "none",
|
|
32
|
+
audit: {
|
|
33
|
+
actionName: "auth.register"
|
|
34
|
+
},
|
|
35
|
+
observability: {},
|
|
36
|
+
async execute(input, _context, deps) {
|
|
37
|
+
return deps.authService.register(input);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "auth.login.password",
|
|
42
|
+
version: 1,
|
|
43
|
+
kind: "command",
|
|
44
|
+
channels: ["api", "internal"],
|
|
45
|
+
surfacesFrom: "enabled",
|
|
46
|
+
inputValidator: authLoginPasswordCommand.operation.bodyValidator,
|
|
47
|
+
idempotency: "none",
|
|
48
|
+
audit: {
|
|
49
|
+
actionName: "auth.login.password"
|
|
50
|
+
},
|
|
51
|
+
observability: {},
|
|
52
|
+
async execute(input, _context, deps) {
|
|
53
|
+
return deps.authService.login(input);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
id: "auth.login.otp.request",
|
|
58
|
+
version: 1,
|
|
59
|
+
kind: "command",
|
|
60
|
+
channels: ["api", "internal"],
|
|
61
|
+
surfacesFrom: "enabled",
|
|
62
|
+
inputValidator: authLoginOtpRequestCommand.operation.bodyValidator,
|
|
63
|
+
idempotency: "none",
|
|
64
|
+
audit: {
|
|
65
|
+
actionName: "auth.login.otp.request"
|
|
66
|
+
},
|
|
67
|
+
observability: {},
|
|
68
|
+
async execute(input, _context, deps) {
|
|
69
|
+
return deps.authService.requestOtpLogin(input);
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
id: "auth.login.otp.verify",
|
|
74
|
+
version: 1,
|
|
75
|
+
kind: "command",
|
|
76
|
+
channels: ["api", "internal"],
|
|
77
|
+
surfacesFrom: "enabled",
|
|
78
|
+
inputValidator: authLoginOtpVerifyCommand.operation.bodyValidator,
|
|
79
|
+
idempotency: "none",
|
|
80
|
+
audit: {
|
|
81
|
+
actionName: "auth.login.otp.verify"
|
|
82
|
+
},
|
|
83
|
+
observability: {},
|
|
84
|
+
async execute(input, _context, deps) {
|
|
85
|
+
return deps.authService.verifyOtpLogin(input);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "auth.login.oauth.start",
|
|
90
|
+
version: 1,
|
|
91
|
+
kind: "command",
|
|
92
|
+
channels: ["api", "internal"],
|
|
93
|
+
surfacesFrom: "enabled",
|
|
94
|
+
inputValidator: [authLoginOAuthStartCommand.operation.paramsValidator, authLoginOAuthStartCommand.operation.queryValidator],
|
|
95
|
+
idempotency: "none",
|
|
96
|
+
audit: {
|
|
97
|
+
actionName: "auth.login.oauth.start"
|
|
98
|
+
},
|
|
99
|
+
observability: {},
|
|
100
|
+
async execute(input, _context, deps) {
|
|
101
|
+
return deps.authService.oauthStart(input);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "auth.login.oauth.complete",
|
|
106
|
+
version: 1,
|
|
107
|
+
kind: "command",
|
|
108
|
+
channels: ["api", "internal"],
|
|
109
|
+
surfacesFrom: "enabled",
|
|
110
|
+
inputValidator: authLoginOAuthCompleteCommand.operation.bodyValidator,
|
|
111
|
+
idempotency: "none",
|
|
112
|
+
audit: {
|
|
113
|
+
actionName: "auth.login.oauth.complete"
|
|
114
|
+
},
|
|
115
|
+
observability: {},
|
|
116
|
+
async execute(input, _context, deps) {
|
|
117
|
+
return deps.authService.oauthComplete(input);
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "auth.password.reset.request",
|
|
122
|
+
version: 1,
|
|
123
|
+
kind: "command",
|
|
124
|
+
channels: ["api", "internal"],
|
|
125
|
+
surfacesFrom: "enabled",
|
|
126
|
+
inputValidator: authPasswordResetRequestCommand.operation.bodyValidator,
|
|
127
|
+
idempotency: "none",
|
|
128
|
+
audit: {
|
|
129
|
+
actionName: "auth.password.reset.request"
|
|
130
|
+
},
|
|
131
|
+
observability: {},
|
|
132
|
+
async execute(input, _context, deps) {
|
|
133
|
+
return deps.authService.requestPasswordReset(input);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: "auth.password.recovery.complete",
|
|
138
|
+
version: 1,
|
|
139
|
+
kind: "command",
|
|
140
|
+
channels: ["api", "internal"],
|
|
141
|
+
surfacesFrom: "enabled",
|
|
142
|
+
inputValidator: authPasswordRecoveryCompleteCommand.operation.bodyValidator,
|
|
143
|
+
idempotency: "none",
|
|
144
|
+
audit: {
|
|
145
|
+
actionName: "auth.password.recovery.complete"
|
|
146
|
+
},
|
|
147
|
+
observability: {},
|
|
148
|
+
async execute(input, _context, deps) {
|
|
149
|
+
return deps.authService.completePasswordRecovery(input);
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: "auth.password.reset",
|
|
154
|
+
version: 1,
|
|
155
|
+
kind: "command",
|
|
156
|
+
channels: ["api", "internal"],
|
|
157
|
+
surfacesFrom: "enabled",
|
|
158
|
+
inputValidator: authPasswordResetCommand.operation.bodyValidator,
|
|
159
|
+
idempotency: "none",
|
|
160
|
+
audit: {
|
|
161
|
+
actionName: "auth.password.reset"
|
|
162
|
+
},
|
|
163
|
+
observability: {},
|
|
164
|
+
async execute(input, context, deps) {
|
|
165
|
+
return deps.authService.resetPassword(requireRequestContext(context, "auth.password.reset"), input);
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: "auth.logout",
|
|
170
|
+
version: 1,
|
|
171
|
+
kind: "command",
|
|
172
|
+
channels: ["api", "automation", "internal"],
|
|
173
|
+
surfacesFrom: "enabled",
|
|
174
|
+
inputValidator: EMPTY_INPUT_VALIDATOR,
|
|
175
|
+
outputValidator: {
|
|
176
|
+
schema: {
|
|
177
|
+
type: "object",
|
|
178
|
+
properties: {
|
|
179
|
+
ok: {
|
|
180
|
+
type: "boolean"
|
|
181
|
+
},
|
|
182
|
+
clearSession: {
|
|
183
|
+
type: "boolean"
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
required: ["ok", "clearSession"],
|
|
187
|
+
additionalProperties: false
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
idempotency: "none",
|
|
191
|
+
audit: {
|
|
192
|
+
actionName: "auth.logout"
|
|
193
|
+
},
|
|
194
|
+
observability: {},
|
|
195
|
+
async execute(_input, context, deps) {
|
|
196
|
+
if (deps.authSessionEventsService && typeof deps.authSessionEventsService.notifySessionChanged === "function") {
|
|
197
|
+
await deps.authSessionEventsService.notifySessionChanged({
|
|
198
|
+
context
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
ok: true,
|
|
203
|
+
clearSession: true
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: "auth.session.read",
|
|
209
|
+
version: 1,
|
|
210
|
+
kind: "query",
|
|
211
|
+
channels: ["api", "internal"],
|
|
212
|
+
surfacesFrom: "enabled",
|
|
213
|
+
inputValidator: EMPTY_INPUT_VALIDATOR,
|
|
214
|
+
idempotency: "none",
|
|
215
|
+
audit: {
|
|
216
|
+
actionName: "auth.session.read"
|
|
217
|
+
},
|
|
218
|
+
observability: {},
|
|
219
|
+
async execute(_input, context, deps) {
|
|
220
|
+
return deps.authService.authenticateRequest(requireRequestContext(context, "auth.session.read"));
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
]);
|
|
224
|
+
|
|
225
|
+
export { authActions };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function safeRequestCookies(request) {
|
|
2
|
+
if (request?.cookies && typeof request.cookies === "object") {
|
|
3
|
+
return request.cookies;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
return {};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function cookieOptions(isProduction, maxAge) {
|
|
10
|
+
const options = {
|
|
11
|
+
httpOnly: true,
|
|
12
|
+
sameSite: "lax",
|
|
13
|
+
secure: isProduction,
|
|
14
|
+
path: "/"
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
if (Number.isFinite(maxAge)) {
|
|
18
|
+
options.maxAge = Math.max(0, Math.floor(maxAge));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return options;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { safeRequestCookies, cookieOptions };
|