@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,92 @@
|
|
|
1
|
+
import { normalizeLowerText, normalizeText } from "@jskit-ai/kernel/shared/actions/textNormalization";
|
|
2
|
+
|
|
3
|
+
const USER_PROFILE_EMAIL_CONFLICT_CODE = "USER_PROFILE_EMAIL_CONFLICT";
|
|
4
|
+
|
|
5
|
+
function buildIdentityKey({ authProvider, authProviderUserId } = {}) {
|
|
6
|
+
return `${normalizeLowerText(authProvider)}:${normalizeText(authProviderUserId)}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalizeIdentity(identityLike) {
|
|
10
|
+
const source = identityLike && typeof identityLike === "object" ? identityLike : {};
|
|
11
|
+
const authProvider = normalizeLowerText(source.authProvider || source.provider);
|
|
12
|
+
const authProviderUserId = normalizeText(source.authProviderUserId || source.providerUserId);
|
|
13
|
+
if (!authProvider || !authProviderUserId) {
|
|
14
|
+
throw new TypeError("Standalone profile sync requires authProvider and authProviderUserId.");
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
authProvider,
|
|
18
|
+
authProviderUserId
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeProfile(profileLike) {
|
|
23
|
+
const source = profileLike && typeof profileLike === "object" ? profileLike : {};
|
|
24
|
+
const identity = normalizeIdentity(source);
|
|
25
|
+
const email = normalizeLowerText(source.email);
|
|
26
|
+
const displayName = normalizeText(source.displayName);
|
|
27
|
+
if (!email || !displayName) {
|
|
28
|
+
throw new TypeError("Standalone profile sync requires email and displayName.");
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
authProvider: identity.authProvider,
|
|
32
|
+
authProviderUserId: identity.authProviderUserId,
|
|
33
|
+
email,
|
|
34
|
+
displayName
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function cloneProfile(profile) {
|
|
39
|
+
return profile ? { ...profile } : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function createEmailConflictError() {
|
|
43
|
+
const error = new Error("Email is already linked to a different profile.");
|
|
44
|
+
error.code = USER_PROFILE_EMAIL_CONFLICT_CODE;
|
|
45
|
+
return error;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createStandaloneProfileSyncService() {
|
|
49
|
+
const profilesByIdentityKey = new Map();
|
|
50
|
+
const identityKeyByEmail = new Map();
|
|
51
|
+
let nextId = 1;
|
|
52
|
+
|
|
53
|
+
async function findByIdentity(identityLike) {
|
|
54
|
+
const identity = normalizeIdentity(identityLike);
|
|
55
|
+
return cloneProfile(profilesByIdentityKey.get(buildIdentityKey(identity)) || null);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function syncIdentityProfile(profileLike) {
|
|
59
|
+
const normalizedProfile = normalizeProfile(profileLike);
|
|
60
|
+
const identityKey = buildIdentityKey(normalizedProfile);
|
|
61
|
+
const existing = profilesByIdentityKey.get(identityKey) || null;
|
|
62
|
+
|
|
63
|
+
const existingOwnerIdentityKey = identityKeyByEmail.get(normalizedProfile.email);
|
|
64
|
+
if (existingOwnerIdentityKey && existingOwnerIdentityKey !== identityKey) {
|
|
65
|
+
throw createEmailConflictError();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const next = {
|
|
69
|
+
id: Number(existing?.id || nextId++),
|
|
70
|
+
authProvider: normalizedProfile.authProvider,
|
|
71
|
+
authProviderUserId: normalizedProfile.authProviderUserId,
|
|
72
|
+
email: normalizedProfile.email,
|
|
73
|
+
displayName: normalizedProfile.displayName
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (existing?.email && existing.email !== next.email) {
|
|
77
|
+
identityKeyByEmail.delete(existing.email);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
profilesByIdentityKey.set(identityKey, next);
|
|
81
|
+
identityKeyByEmail.set(next.email, identityKey);
|
|
82
|
+
return cloneProfile(next);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return Object.freeze({
|
|
86
|
+
findByIdentity,
|
|
87
|
+
syncIdentityProfile
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { createStandaloneProfileSyncService };
|
|
92
|
+
export { USER_PROFILE_EMAIL_CONFLICT_CODE };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export { createAccountFlows } from "./accountFlows.js";
|
|
2
|
+
export { createOauthFlows } from "./oauthFlows.js";
|
|
3
|
+
export { createPasswordSecurityFlows } from "./passwordSecurityFlows.js";
|
|
4
|
+
|
|
5
|
+
export {
|
|
6
|
+
mapAuthError,
|
|
7
|
+
validationError,
|
|
8
|
+
isUserNotFoundLikeAuthError,
|
|
9
|
+
mapRecoveryError,
|
|
10
|
+
mapPasswordUpdateError,
|
|
11
|
+
mapOtpVerifyError,
|
|
12
|
+
mapProfileUpdateError,
|
|
13
|
+
mapCurrentPasswordError,
|
|
14
|
+
isTransientAuthMessage,
|
|
15
|
+
isTransientSupabaseError
|
|
16
|
+
} from "./authErrorMappers.js";
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
buildOtpLoginRedirectUrl,
|
|
20
|
+
normalizeOAuthIntent,
|
|
21
|
+
normalizeReturnToPath,
|
|
22
|
+
buildOAuthRedirectUrl,
|
|
23
|
+
buildOAuthLoginRedirectUrl,
|
|
24
|
+
buildOAuthLinkRedirectUrl
|
|
25
|
+
} from "./authRedirectUrls.js";
|
|
26
|
+
|
|
27
|
+
export {
|
|
28
|
+
normalizeOAuthProviderInput,
|
|
29
|
+
parseOAuthCompletePayload,
|
|
30
|
+
parseOtpLoginVerifyPayload,
|
|
31
|
+
mapOAuthCallbackError,
|
|
32
|
+
validatePasswordRecoveryPayload
|
|
33
|
+
} from "./authInputParsers.js";
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
resolveSupabaseOAuthProviderCatalog,
|
|
37
|
+
resolveOAuthProviderQueryParams,
|
|
38
|
+
buildOAuthProviderCatalogResponse
|
|
39
|
+
} from "./oauthProviderCatalog.js";
|
|
40
|
+
|
|
41
|
+
export {
|
|
42
|
+
buildAuthMethodsStatusFromProviderIds,
|
|
43
|
+
collectProviderIdsFromSupabaseUser,
|
|
44
|
+
findAuthMethodById,
|
|
45
|
+
findLinkedIdentityByProvider,
|
|
46
|
+
buildSecurityStatusFromAuthMethodsStatus
|
|
47
|
+
} from "./authMethodStatus.js";
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
2
|
+
import { resolveAllowedOriginsFromSurfaceDefinitions } from "@jskit-ai/kernel/shared/support/returnToPath";
|
|
3
|
+
import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
|
|
4
|
+
import { createService } from "../lib/service.js";
|
|
5
|
+
import { createStandaloneProfileSyncService } from "../lib/standaloneProfileSyncService.js";
|
|
6
|
+
import { createAuthSessionEventsService } from "../lib/authSessionEventsService.js";
|
|
7
|
+
import { authActions } from "../lib/actions/auth.contributor.js";
|
|
8
|
+
|
|
9
|
+
const AUTH_SESSION_EVENTS_SERVICE_TOKEN = "auth.session.events.service";
|
|
10
|
+
const AUTH_SESSION_CHANGED_EVENT = "auth.session.changed";
|
|
11
|
+
const USERS_BOOTSTRAP_CHANGED_EVENT = "users.bootstrap.changed";
|
|
12
|
+
const AUTH_PROFILE_MODE_STANDALONE = "standalone";
|
|
13
|
+
const AUTH_PROFILE_MODE_USERS = "users";
|
|
14
|
+
const SUPPORTED_AUTH_PROFILE_MODES = Object.freeze([AUTH_PROFILE_MODE_STANDALONE, AUTH_PROFILE_MODE_USERS]);
|
|
15
|
+
|
|
16
|
+
function splitCsv(value) {
|
|
17
|
+
return String(value || "")
|
|
18
|
+
.split(",")
|
|
19
|
+
.map((entry) => entry.trim())
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolveAllowedReturnToOrigins({ appConfig = {}, appPublicUrl = "" } = {}) {
|
|
24
|
+
const surfaceDefinitions =
|
|
25
|
+
appConfig && typeof appConfig === "object" && appConfig.surfaceDefinitions && typeof appConfig.surfaceDefinitions === "object"
|
|
26
|
+
? appConfig.surfaceDefinitions
|
|
27
|
+
: {};
|
|
28
|
+
|
|
29
|
+
return resolveAllowedOriginsFromSurfaceDefinitions(surfaceDefinitions, {
|
|
30
|
+
seedOrigins: [appPublicUrl]
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolveAuthProviderConfig(env) {
|
|
35
|
+
const source = env && typeof env === "object" ? env : {};
|
|
36
|
+
return {
|
|
37
|
+
id: "supabase",
|
|
38
|
+
supabaseUrl: String(source.AUTH_SUPABASE_URL || source.SUPABASE_URL || "").trim(),
|
|
39
|
+
supabasePublishableKey: String(
|
|
40
|
+
source.AUTH_SUPABASE_PUBLISHABLE_KEY || source.SUPABASE_PUBLISHABLE_KEY || ""
|
|
41
|
+
).trim(),
|
|
42
|
+
jwtAudience: String(source.AUTH_JWT_AUDIENCE || "authenticated").trim(),
|
|
43
|
+
oauthProviders: splitCsv(source.AUTH_OAUTH_PROVIDERS),
|
|
44
|
+
oauthDefaultProvider: String(source.AUTH_OAUTH_DEFAULT_PROVIDER || "").trim()
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveAuthProfileMode(env) {
|
|
49
|
+
const source = env && typeof env === "object" ? env : {};
|
|
50
|
+
const mode = String(source.AUTH_PROFILE_MODE || AUTH_PROFILE_MODE_STANDALONE)
|
|
51
|
+
.trim()
|
|
52
|
+
.toLowerCase();
|
|
53
|
+
if (SUPPORTED_AUTH_PROFILE_MODES.includes(mode)) {
|
|
54
|
+
return mode;
|
|
55
|
+
}
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Unsupported AUTH_PROFILE_MODE "${mode}". Supported values: ${SUPPORTED_AUTH_PROFILE_MODES.join(", ")}.`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function createInMemoryUserSettingsRepository() {
|
|
62
|
+
const settingsByUserId = new Map();
|
|
63
|
+
|
|
64
|
+
function ensure(userId) {
|
|
65
|
+
const numericUserId = Number(userId);
|
|
66
|
+
if (!settingsByUserId.has(numericUserId)) {
|
|
67
|
+
settingsByUserId.set(numericUserId, {
|
|
68
|
+
userId: numericUserId,
|
|
69
|
+
passwordSignInEnabled: true,
|
|
70
|
+
passwordSetupRequired: false
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return settingsByUserId.get(numericUserId);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return Object.freeze({
|
|
77
|
+
async ensureForUserId(userId) {
|
|
78
|
+
return { ...ensure(userId) };
|
|
79
|
+
},
|
|
80
|
+
async updatePasswordSignInEnabled(userId, enabled) {
|
|
81
|
+
const settings = ensure(userId);
|
|
82
|
+
settings.passwordSignInEnabled = enabled !== false;
|
|
83
|
+
return { ...settings };
|
|
84
|
+
},
|
|
85
|
+
async updatePasswordSetupRequired(userId, required) {
|
|
86
|
+
const settings = ensure(userId);
|
|
87
|
+
settings.passwordSetupRequired = required === true;
|
|
88
|
+
return { ...settings };
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const fallbackUserSettingsRepository = createInMemoryUserSettingsRepository();
|
|
94
|
+
const fallbackStandaloneProfileSyncService = createStandaloneProfileSyncService();
|
|
95
|
+
|
|
96
|
+
function resolveCommonDependencies(scope) {
|
|
97
|
+
const dependencies = {};
|
|
98
|
+
if (scope.has(KERNEL_TOKENS.Env)) {
|
|
99
|
+
dependencies.env = scope.make(KERNEL_TOKENS.Env);
|
|
100
|
+
}
|
|
101
|
+
if (scope.has(KERNEL_TOKENS.Logger)) {
|
|
102
|
+
dependencies.logger = scope.make(KERNEL_TOKENS.Logger);
|
|
103
|
+
}
|
|
104
|
+
return dependencies;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function resolveOptionalRepositories(scope) {
|
|
108
|
+
const repositories = {};
|
|
109
|
+
if (scope.has("userSettingsRepository")) {
|
|
110
|
+
repositories.userSettingsRepository = scope.make("userSettingsRepository");
|
|
111
|
+
}
|
|
112
|
+
return repositories;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
class AuthSupabaseServiceProvider {
|
|
116
|
+
static id = "auth.provider.supabase";
|
|
117
|
+
|
|
118
|
+
static dependsOn = ["runtime.actions"];
|
|
119
|
+
|
|
120
|
+
register(app) {
|
|
121
|
+
if (
|
|
122
|
+
!app ||
|
|
123
|
+
typeof app.singleton !== "function" ||
|
|
124
|
+
typeof app.has !== "function" ||
|
|
125
|
+
typeof app.actions !== "function" ||
|
|
126
|
+
typeof app.service !== "function"
|
|
127
|
+
) {
|
|
128
|
+
throw new Error("AuthSupabaseServiceProvider requires application singleton()/has()/actions()/service().");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!app.has("authService")) {
|
|
132
|
+
app.singleton("authService", (scope) => {
|
|
133
|
+
const dependencies = resolveCommonDependencies(scope);
|
|
134
|
+
const envFromDependencies =
|
|
135
|
+
dependencies?.env && typeof dependencies.env === "object" ? dependencies.env : {};
|
|
136
|
+
const env = {
|
|
137
|
+
...process.env,
|
|
138
|
+
...envFromDependencies
|
|
139
|
+
};
|
|
140
|
+
const appConfig = scope.has("appConfig") ? scope.make("appConfig") : {};
|
|
141
|
+
const authProvider = resolveAuthProviderConfig(env);
|
|
142
|
+
const repositories = resolveOptionalRepositories(scope);
|
|
143
|
+
const userSettingsRepository = repositories.userSettingsRepository || fallbackUserSettingsRepository;
|
|
144
|
+
if (!authProvider.supabaseUrl || !authProvider.supabasePublishableKey) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
const authProfileMode = resolveAuthProfileMode(env);
|
|
148
|
+
let userProfileSyncService = fallbackStandaloneProfileSyncService;
|
|
149
|
+
if (authProfileMode === AUTH_PROFILE_MODE_USERS) {
|
|
150
|
+
if (!scope.has("users.profile.sync.service")) {
|
|
151
|
+
throw new Error(
|
|
152
|
+
"AuthSupabaseServiceProvider requires users.profile.sync.service when AUTH_PROFILE_MODE=users."
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
userProfileSyncService = scope.make("users.profile.sync.service");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return createService({
|
|
159
|
+
authProvider,
|
|
160
|
+
appPublicUrl: String(env.APP_PUBLIC_URL || "").trim(),
|
|
161
|
+
authAllowedReturnToOrigins: resolveAllowedReturnToOrigins({
|
|
162
|
+
appConfig,
|
|
163
|
+
appPublicUrl: String(env.APP_PUBLIC_URL || "").trim()
|
|
164
|
+
}),
|
|
165
|
+
nodeEnv: String(env.NODE_ENV || "development").trim() || "development",
|
|
166
|
+
userSettingsRepository,
|
|
167
|
+
userProfileSyncService
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
app.service(
|
|
173
|
+
AUTH_SESSION_EVENTS_SERVICE_TOKEN,
|
|
174
|
+
() => createAuthSessionEventsService(),
|
|
175
|
+
{
|
|
176
|
+
events: {
|
|
177
|
+
notifySessionChanged: [
|
|
178
|
+
{
|
|
179
|
+
type: "entity.changed",
|
|
180
|
+
source: "auth",
|
|
181
|
+
entity: "session",
|
|
182
|
+
operation: "updated",
|
|
183
|
+
entityId: ({ result }) => result?.id,
|
|
184
|
+
realtime: {
|
|
185
|
+
event: AUTH_SESSION_CHANGED_EVENT,
|
|
186
|
+
audience: "actor_user"
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
type: "entity.changed",
|
|
191
|
+
source: "users",
|
|
192
|
+
entity: "bootstrap",
|
|
193
|
+
operation: "updated",
|
|
194
|
+
entityId: ({ result }) => result?.id,
|
|
195
|
+
realtime: {
|
|
196
|
+
event: USERS_BOOTSTRAP_CHANGED_EVENT,
|
|
197
|
+
audience: "actor_user"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
app.actions(
|
|
206
|
+
withActionDefaults(authActions, {
|
|
207
|
+
domain: "auth",
|
|
208
|
+
dependencies: {
|
|
209
|
+
authService: "authService",
|
|
210
|
+
authSessionEventsService: AUTH_SESSION_EVENTS_SERVICE_TOKEN
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export { AuthSupabaseServiceProvider };
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import * as authProviderSupabase from "../src/server/lib/index.js";
|
|
4
|
+
|
|
5
|
+
test("auth.provider.supabase exports required symbols", () => {
|
|
6
|
+
assert.equal(typeof authProviderSupabase.createService, "function");
|
|
7
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
buildOAuthLoginRedirectUrl,
|
|
5
|
+
buildOtpLoginRedirectUrl
|
|
6
|
+
} from "../src/server/lib/authRedirectUrls.js";
|
|
7
|
+
|
|
8
|
+
test("buildOAuthLoginRedirectUrl targets /auth/login callback route", () => {
|
|
9
|
+
const redirectTo = buildOAuthLoginRedirectUrl({
|
|
10
|
+
appPublicUrl: "http://localhost:5173",
|
|
11
|
+
provider: "google",
|
|
12
|
+
providerIds: ["google"],
|
|
13
|
+
returnTo: "/app"
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const url = new URL(redirectTo);
|
|
17
|
+
assert.equal(url.origin, "http://localhost:5173");
|
|
18
|
+
assert.equal(url.pathname, "/auth/login");
|
|
19
|
+
assert.equal(url.searchParams.get("oauthProvider"), "google");
|
|
20
|
+
assert.equal(url.searchParams.get("oauthIntent"), "login");
|
|
21
|
+
assert.equal(url.searchParams.get("oauthReturnTo"), "/app");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("buildOAuthLoginRedirectUrl preserves app base path", () => {
|
|
25
|
+
const redirectTo = buildOAuthLoginRedirectUrl({
|
|
26
|
+
appPublicUrl: "https://example.com/tenant",
|
|
27
|
+
provider: "google",
|
|
28
|
+
providerIds: ["google"],
|
|
29
|
+
returnTo: "/app"
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const url = new URL(redirectTo);
|
|
33
|
+
assert.equal(url.origin, "https://example.com");
|
|
34
|
+
assert.equal(url.pathname, "/tenant/auth/login");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("buildOtpLoginRedirectUrl targets /auth/login", () => {
|
|
38
|
+
const redirectTo = buildOtpLoginRedirectUrl({
|
|
39
|
+
appPublicUrl: "http://localhost:5173",
|
|
40
|
+
returnTo: "/app"
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const url = new URL(redirectTo);
|
|
44
|
+
assert.equal(url.origin, "http://localhost:5173");
|
|
45
|
+
assert.equal(url.pathname, "/auth/login");
|
|
46
|
+
assert.equal(url.searchParams.get("returnTo"), "/app");
|
|
47
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
|
|
5
|
+
const EXPECTED_EXPORTS = Object.freeze({
|
|
6
|
+
"./server/providers/AuthSupabaseServiceProvider": "./src/server/providers/AuthSupabaseServiceProvider.js",
|
|
7
|
+
"./server/providers/AuthProviderServiceProvider": "./src/server/providers/AuthProviderServiceProvider.js",
|
|
8
|
+
"./server/lib/index": "./src/server/lib/index.js",
|
|
9
|
+
"./client": "./src/client/index.js"
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("auth-provider-supabase-core exports only curated entrypoints", async () => {
|
|
13
|
+
const packageJson = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
14
|
+
const exportsMap = packageJson && typeof packageJson === "object" ? packageJson.exports : {};
|
|
15
|
+
|
|
16
|
+
assert.deepEqual(exportsMap, EXPECTED_EXPORTS);
|
|
17
|
+
assert.equal(exportsMap["./server/lib/service"], undefined);
|
|
18
|
+
assert.equal(exportsMap["./server/lib/test-utils"], undefined);
|
|
19
|
+
assert.equal(exportsMap["./server/lib/oauthFlows"], undefined);
|
|
20
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createService } from "../src/server/lib/index.js";
|
|
4
|
+
|
|
5
|
+
test("auth provider supabase core validates required provider options", () => {
|
|
6
|
+
assert.throws(() => createService({}), /authProvider is required/);
|
|
7
|
+
});
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createApplication } from "@jskit-ai/kernel/_testable";
|
|
4
|
+
import { KERNEL_TOKENS } from "@jskit-ai/kernel/shared/support/tokens";
|
|
5
|
+
import { ActionRuntimeServiceProvider } from "@jskit-ai/kernel/server/actions";
|
|
6
|
+
import { AuthSupabaseServiceProvider } from "../src/server/providers/AuthSupabaseServiceProvider.js";
|
|
7
|
+
|
|
8
|
+
function createAppConfigFixture() {
|
|
9
|
+
return {
|
|
10
|
+
surfaceModeAll: "all",
|
|
11
|
+
surfaceDefaultId: "home",
|
|
12
|
+
surfaceDefinitions: {
|
|
13
|
+
home: { id: "home", pagesRoot: "", enabled: true, requiresAuth: false, requiresWorkspace: false },
|
|
14
|
+
console: {
|
|
15
|
+
id: "console",
|
|
16
|
+
pagesRoot: "console",
|
|
17
|
+
enabled: true,
|
|
18
|
+
requiresAuth: true,
|
|
19
|
+
requiresWorkspace: false
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("auth supabase provider registers authService and contributes auth actions in users mode", async () => {
|
|
26
|
+
const app = createApplication();
|
|
27
|
+
app.instance("appConfig", createAppConfigFixture());
|
|
28
|
+
app.instance(KERNEL_TOKENS.Env, {
|
|
29
|
+
AUTH_SUPABASE_URL: "https://example.supabase.co",
|
|
30
|
+
AUTH_SUPABASE_PUBLISHABLE_KEY: "sb_publishable_test_key",
|
|
31
|
+
AUTH_PROFILE_MODE: "users",
|
|
32
|
+
APP_PUBLIC_URL: "http://localhost:5173",
|
|
33
|
+
NODE_ENV: "test"
|
|
34
|
+
});
|
|
35
|
+
app.instance(KERNEL_TOKENS.Logger, {
|
|
36
|
+
info() {},
|
|
37
|
+
warn() {},
|
|
38
|
+
error() {},
|
|
39
|
+
debug() {}
|
|
40
|
+
});
|
|
41
|
+
app.instance("domainEvents", {
|
|
42
|
+
async publish() {}
|
|
43
|
+
});
|
|
44
|
+
app.instance("users.profile.sync.service", {
|
|
45
|
+
async findByIdentity() {
|
|
46
|
+
return null;
|
|
47
|
+
},
|
|
48
|
+
async syncIdentityProfile(profile) {
|
|
49
|
+
return {
|
|
50
|
+
id: 1,
|
|
51
|
+
authProvider: String(profile?.authProvider || "supabase"),
|
|
52
|
+
authProviderUserId: String(profile?.authProviderUserId || "user-1"),
|
|
53
|
+
email: String(profile?.email || "test@example.com"),
|
|
54
|
+
displayName: String(profile?.displayName || "Test User")
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
await app.start({
|
|
60
|
+
providers: [ActionRuntimeServiceProvider, AuthSupabaseServiceProvider]
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const authService = app.make("authService");
|
|
64
|
+
assert.equal(typeof authService?.login, "function");
|
|
65
|
+
|
|
66
|
+
const actionExecutor = app.make("actionExecutor");
|
|
67
|
+
assert.equal(typeof actionExecutor?.execute, "function");
|
|
68
|
+
|
|
69
|
+
const definitions = actionExecutor.listDefinitions();
|
|
70
|
+
assert.equal(Array.isArray(definitions), true);
|
|
71
|
+
assert.equal(definitions.some((definition) => definition.id === "auth.login.password"), true);
|
|
72
|
+
const sessionRead = definitions.find((definition) => definition.id === "auth.session.read");
|
|
73
|
+
assert.deepEqual(sessionRead?.surfaces, ["home", "console"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("auth supabase provider registers authService in standalone mode without users.profile.sync.service", async () => {
|
|
77
|
+
const app = createApplication();
|
|
78
|
+
app.instance("appConfig", createAppConfigFixture());
|
|
79
|
+
app.instance(KERNEL_TOKENS.Env, {
|
|
80
|
+
AUTH_SUPABASE_URL: "https://example.supabase.co",
|
|
81
|
+
AUTH_SUPABASE_PUBLISHABLE_KEY: "sb_publishable_test_key",
|
|
82
|
+
AUTH_PROFILE_MODE: "standalone",
|
|
83
|
+
APP_PUBLIC_URL: "http://localhost:5173",
|
|
84
|
+
NODE_ENV: "test"
|
|
85
|
+
});
|
|
86
|
+
app.instance(KERNEL_TOKENS.Logger, {
|
|
87
|
+
info() {},
|
|
88
|
+
warn() {},
|
|
89
|
+
error() {},
|
|
90
|
+
debug() {}
|
|
91
|
+
});
|
|
92
|
+
app.instance("domainEvents", {
|
|
93
|
+
async publish() {}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await app.start({
|
|
97
|
+
providers: [ActionRuntimeServiceProvider, AuthSupabaseServiceProvider]
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const authService = app.make("authService");
|
|
101
|
+
assert.equal(typeof authService?.login, "function");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("auth supabase provider requires users.profile.sync.service when AUTH_PROFILE_MODE=users", async () => {
|
|
105
|
+
const app = createApplication();
|
|
106
|
+
app.instance("appConfig", createAppConfigFixture());
|
|
107
|
+
app.instance(KERNEL_TOKENS.Env, {
|
|
108
|
+
AUTH_SUPABASE_URL: "https://example.supabase.co",
|
|
109
|
+
AUTH_SUPABASE_PUBLISHABLE_KEY: "sb_publishable_test_key",
|
|
110
|
+
AUTH_PROFILE_MODE: "users",
|
|
111
|
+
APP_PUBLIC_URL: "http://localhost:5173",
|
|
112
|
+
NODE_ENV: "test"
|
|
113
|
+
});
|
|
114
|
+
app.instance(KERNEL_TOKENS.Logger, {
|
|
115
|
+
info() {},
|
|
116
|
+
warn() {},
|
|
117
|
+
error() {},
|
|
118
|
+
debug() {}
|
|
119
|
+
});
|
|
120
|
+
app.instance("domainEvents", {
|
|
121
|
+
async publish() {}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await app.start({
|
|
125
|
+
providers: [ActionRuntimeServiceProvider, AuthSupabaseServiceProvider]
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
assert.throws(() => app.make("authService"), /AUTH_PROFILE_MODE=users/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("auth supabase provider rejects unsupported AUTH_PROFILE_MODE values", async () => {
|
|
132
|
+
const app = createApplication();
|
|
133
|
+
app.instance("appConfig", createAppConfigFixture());
|
|
134
|
+
app.instance(KERNEL_TOKENS.Env, {
|
|
135
|
+
AUTH_SUPABASE_URL: "https://example.supabase.co",
|
|
136
|
+
AUTH_SUPABASE_PUBLISHABLE_KEY: "sb_publishable_test_key",
|
|
137
|
+
AUTH_PROFILE_MODE: "invalid",
|
|
138
|
+
APP_PUBLIC_URL: "http://localhost:5173",
|
|
139
|
+
NODE_ENV: "test"
|
|
140
|
+
});
|
|
141
|
+
app.instance(KERNEL_TOKENS.Logger, {
|
|
142
|
+
info() {},
|
|
143
|
+
warn() {},
|
|
144
|
+
error() {},
|
|
145
|
+
debug() {}
|
|
146
|
+
});
|
|
147
|
+
app.instance("domainEvents", {
|
|
148
|
+
async publish() {}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await app.start({
|
|
152
|
+
providers: [ActionRuntimeServiceProvider, AuthSupabaseServiceProvider]
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
assert.throws(() => app.make("authService"), /Unsupported AUTH_PROFILE_MODE/);
|
|
156
|
+
});
|