@holo-js/auth-workos 0.1.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,135 @@
1
+ import { AuthUserLike, AuthEstablishedSession, authRuntimeInternals } from '@holo-js/auth';
2
+ import { AuthWorkosProviderConfig } from '@holo-js/config';
3
+
4
+ interface WorkosIdentityProfile {
5
+ readonly id: string;
6
+ readonly email?: string;
7
+ readonly emailVerified?: boolean;
8
+ readonly firstName?: string;
9
+ readonly lastName?: string;
10
+ readonly name?: string;
11
+ readonly avatar?: string;
12
+ readonly organizationId?: string;
13
+ readonly raw?: unknown;
14
+ }
15
+ interface WorkosVerifiedSession {
16
+ readonly sessionId: string;
17
+ readonly identity: WorkosIdentityProfile;
18
+ readonly accessToken?: string;
19
+ readonly expiresAt?: Date;
20
+ readonly raw?: unknown;
21
+ }
22
+ interface WorkosVerifyRequestContext {
23
+ readonly provider: string;
24
+ readonly request: Request;
25
+ readonly config: AuthWorkosProviderConfig;
26
+ }
27
+ interface WorkosVerifySessionContext {
28
+ readonly provider: string;
29
+ readonly token: string;
30
+ readonly config: AuthWorkosProviderConfig;
31
+ }
32
+ interface WorkosProviderRuntime {
33
+ verifyRequest?(context: WorkosVerifyRequestContext): Promise<WorkosVerifiedSession | null>;
34
+ verifySession?(context: WorkosVerifySessionContext): Promise<WorkosVerifiedSession | null>;
35
+ }
36
+ type RuntimeAuthProviderAdapter = ReturnType<typeof authRuntimeInternals.getRuntimeBindings>['providers'][string];
37
+ interface HostedIdentityRecord {
38
+ readonly provider: string;
39
+ readonly providerUserId: string;
40
+ readonly guard: string;
41
+ readonly authProvider: string;
42
+ readonly userId: string | number;
43
+ readonly email?: string;
44
+ readonly emailVerified: boolean;
45
+ readonly profile: Readonly<Record<string, unknown>>;
46
+ readonly linkedAt: Date;
47
+ readonly updatedAt: Date;
48
+ }
49
+ interface HostedIdentityStore {
50
+ findByProviderUserId(provider: string, providerUserId: string): Promise<HostedIdentityRecord | null>;
51
+ findByUserId(provider: string, authProvider: string, userId: string | number): Promise<HostedIdentityRecord | null>;
52
+ save(record: HostedIdentityRecord): Promise<void>;
53
+ }
54
+ type WorkosSyncStatus = 'created' | 'updated' | 'linked' | 'relinked';
55
+ interface WorkosAuthenticationResult {
56
+ readonly provider: string;
57
+ readonly guard: string;
58
+ readonly authProvider: string;
59
+ readonly status: WorkosSyncStatus;
60
+ readonly user: AuthUserLike;
61
+ readonly identity: HostedIdentityRecord;
62
+ readonly session: WorkosVerifiedSession;
63
+ readonly authSession?: AuthEstablishedSession;
64
+ }
65
+ interface WorkosAuthBindings {
66
+ readonly providers: Readonly<Record<string, WorkosProviderRuntime>>;
67
+ readonly identityStore: HostedIdentityStore;
68
+ }
69
+ interface ConfigureWorkosAuthRuntimeOptions {
70
+ readonly providers?: Readonly<Record<string, WorkosProviderRuntime>>;
71
+ readonly identityStore?: HostedIdentityStore;
72
+ }
73
+ interface WorkosAuthFacade {
74
+ verifyRequest(request: Request, provider?: string): Promise<WorkosVerifiedSession | null>;
75
+ verifySession(token: string, provider?: string): Promise<WorkosVerifiedSession | null>;
76
+ syncIdentity(session: WorkosVerifiedSession, provider?: string): Promise<WorkosAuthenticationResult>;
77
+ authenticate(request: Request, provider?: string): Promise<WorkosAuthenticationResult | null>;
78
+ }
79
+ declare class WorkosAuthConflictError extends Error {
80
+ readonly code = "workos_identity_conflict";
81
+ readonly provider: string;
82
+ readonly workosUserId: string;
83
+ readonly email?: string;
84
+ constructor(options: {
85
+ readonly provider: string;
86
+ readonly workosUserId: string;
87
+ readonly email?: string;
88
+ readonly message: string;
89
+ });
90
+ }
91
+ declare function getBindings(): WorkosAuthBindings;
92
+ declare function resolveConfiguredProviderName(provider?: string): string;
93
+ declare function getConfiguredProviderConfig(provider?: string): {
94
+ readonly name: string;
95
+ readonly clientId?: string;
96
+ readonly apiKey?: string;
97
+ readonly cookiePassword?: string;
98
+ readonly redirectUri?: string;
99
+ readonly sessionCookie: string;
100
+ readonly guard?: string;
101
+ readonly mapToProvider?: string;
102
+ };
103
+ declare function resolveGuardAndProvider(provider?: string): {
104
+ readonly guard: string;
105
+ readonly authProvider: string;
106
+ readonly adapter: RuntimeAuthProviderAdapter;
107
+ };
108
+ declare function resolveDisplayName(profile: WorkosIdentityProfile): string | undefined;
109
+ declare function resolveEmailForCreation(profile: WorkosIdentityProfile): string;
110
+ declare function normalizeHostedProfile(profile: WorkosIdentityProfile): Readonly<Record<string, unknown>>;
111
+ declare function getSessionTokenFromRequest(request: Request, sessionCookie: string): string | null;
112
+ declare function verifySession(token: string, provider?: string): Promise<WorkosVerifiedSession | null>;
113
+ declare function verifyRequest(request: Request, provider?: string): Promise<WorkosVerifiedSession | null>;
114
+ declare function syncIdentity(session: WorkosVerifiedSession, provider?: string): Promise<WorkosAuthenticationResult>;
115
+ declare function authenticate(request: Request, provider?: string): Promise<WorkosAuthenticationResult | null>;
116
+ declare function configureWorkosAuthRuntime(bindings?: ConfigureWorkosAuthRuntimeOptions): void;
117
+ declare function resetWorkosAuthRuntime(): void;
118
+ declare const workosAuth: Readonly<{
119
+ authenticate: typeof authenticate;
120
+ syncIdentity: typeof syncIdentity;
121
+ verifyRequest: typeof verifyRequest;
122
+ verifySession: typeof verifySession;
123
+ }>;
124
+ declare const workosAuthInternals: {
125
+ getBindings: typeof getBindings;
126
+ getConfiguredProviderConfig: typeof getConfiguredProviderConfig;
127
+ getSessionTokenFromRequest: typeof getSessionTokenFromRequest;
128
+ normalizeHostedProfile: typeof normalizeHostedProfile;
129
+ resolveConfiguredProviderName: typeof resolveConfiguredProviderName;
130
+ resolveDisplayName: typeof resolveDisplayName;
131
+ resolveEmailForCreation: typeof resolveEmailForCreation;
132
+ resolveGuardAndProvider: typeof resolveGuardAndProvider;
133
+ };
134
+
135
+ export { type ConfigureWorkosAuthRuntimeOptions, type HostedIdentityRecord, type HostedIdentityStore, type WorkosAuthBindings, WorkosAuthConflictError, type WorkosAuthFacade, type WorkosAuthenticationResult, type WorkosIdentityProfile, type WorkosProviderRuntime, type WorkosSyncStatus, type WorkosVerifiedSession, type WorkosVerifyRequestContext, type WorkosVerifySessionContext, authenticate, configureWorkosAuthRuntime, resetWorkosAuthRuntime, syncIdentity, verifyRequest, verifySession, workosAuth, workosAuthInternals };
package/dist/index.mjs ADDED
@@ -0,0 +1,677 @@
1
+ // src/index.ts
2
+ import { createPublicKey, verify as verifySignature } from "crypto";
3
+ import { authRuntimeInternals } from "@holo-js/auth";
4
+ import { parseCookieHeader } from "@holo-js/session";
5
+ var WorkosAuthConflictError = class extends Error {
6
+ code = "workos_identity_conflict";
7
+ provider;
8
+ workosUserId;
9
+ email;
10
+ constructor(options) {
11
+ super(options.message);
12
+ this.name = "WorkosAuthConflictError";
13
+ this.provider = options.provider;
14
+ this.workosUserId = options.workosUserId;
15
+ this.email = options.email;
16
+ }
17
+ };
18
+ var workosBindings;
19
+ var WORKOS_API_BASE_URL = "https://api.workos.com";
20
+ var workosDefaultProviderRuntimeCache = /* @__PURE__ */ new Map();
21
+ var workosJwksCache = /* @__PURE__ */ new Map();
22
+ var AUTH_PROVIDER_MARKER = /* @__PURE__ */ Symbol.for("holo-js.auth.provider");
23
+ function throwUnconfigured() {
24
+ throw new Error("[@holo-js/auth-workos] WorkOS auth runtime is not configured yet.");
25
+ }
26
+ function getBindings() {
27
+ if (!workosBindings?.identityStore) {
28
+ throwUnconfigured();
29
+ }
30
+ return {
31
+ providers: workosBindings.providers ?? {},
32
+ identityStore: workosBindings.identityStore
33
+ };
34
+ }
35
+ function decodeJwtSegment(value, label) {
36
+ try {
37
+ return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
38
+ } catch {
39
+ throw new Error(`[@holo-js/auth-workos] WorkOS token ${label} was not valid JSON.`);
40
+ }
41
+ }
42
+ function parseJwt(token) {
43
+ const segments = token.split(".");
44
+ if (segments.length !== 3 || !segments[0] || !segments[1] || !segments[2]) {
45
+ throw new Error("[@holo-js/auth-workos] WorkOS token was not a valid JWT.");
46
+ }
47
+ return {
48
+ header: decodeJwtSegment(segments[0], "header"),
49
+ payload: decodeJwtSegment(segments[1], "payload"),
50
+ signature: Buffer.from(segments[2], "base64url"),
51
+ signingInput: Buffer.from(`${segments[0]}.${segments[1]}`, "utf8")
52
+ };
53
+ }
54
+ function verifyJwtSignatureWithJwk(token, jwk) {
55
+ const algorithm = typeof token.header.alg === "string" ? token.header.alg : "";
56
+ const key = createPublicKey({ key: jwk, format: "jwk" });
57
+ switch (algorithm) {
58
+ case "RS256":
59
+ return verifySignature("RSA-SHA256", token.signingInput, key, token.signature);
60
+ case "RS384":
61
+ return verifySignature("RSA-SHA384", token.signingInput, key, token.signature);
62
+ case "RS512":
63
+ return verifySignature("RSA-SHA512", token.signingInput, key, token.signature);
64
+ default:
65
+ throw new Error(`[@holo-js/auth-workos] Unsupported WorkOS JWT algorithm "${algorithm || "unknown"}".`);
66
+ }
67
+ }
68
+ async function fetchWorkosJwks(clientId, options = {}) {
69
+ const normalizedClientId = clientId.trim();
70
+ if (options.refresh) {
71
+ workosJwksCache.delete(normalizedClientId);
72
+ }
73
+ const existing = workosJwksCache.get(normalizedClientId);
74
+ if (existing) {
75
+ return existing;
76
+ }
77
+ const pending = (async () => {
78
+ const response = await fetch(`${WORKOS_API_BASE_URL}/sso/jwks/${encodeURIComponent(normalizedClientId)}`, {
79
+ headers: {
80
+ accept: "application/json"
81
+ }
82
+ });
83
+ if (!response.ok) {
84
+ throw new Error(`[@holo-js/auth-workos] Failed to load WorkOS JWKS for "${normalizedClientId}".`);
85
+ }
86
+ const payload = await response.json();
87
+ return payload.keys ?? [];
88
+ })();
89
+ workosJwksCache.set(normalizedClientId, pending);
90
+ try {
91
+ return await pending;
92
+ } catch (error) {
93
+ workosJwksCache.delete(normalizedClientId);
94
+ throw error;
95
+ }
96
+ }
97
+ async function verifyWorkosSessionToken(token, config) {
98
+ const clientId = config.clientId?.trim();
99
+ if (!clientId) {
100
+ throw new Error("[@holo-js/auth-workos] WorkOS verification requires clientId to be configured.");
101
+ }
102
+ const parsed = parseJwt(token);
103
+ const headerKid = typeof parsed.header.kid === "string" ? parsed.header.kid : void 0;
104
+ const resolveKey = async (refresh = false) => {
105
+ const keys = await fetchWorkosJwks(clientId, { refresh });
106
+ return headerKid ? keys.find((candidate) => candidate.kid === headerKid) : keys[0];
107
+ };
108
+ let key = await resolveKey();
109
+ if (!key || !verifyJwtSignatureWithJwk(parsed, key)) {
110
+ key = await resolveKey(true);
111
+ }
112
+ if (!key || !verifyJwtSignatureWithJwk(parsed, key)) {
113
+ throw new Error("[@holo-js/auth-workos] WorkOS token signature verification failed.");
114
+ }
115
+ const exp = typeof parsed.payload.exp === "number" ? parsed.payload.exp : void 0;
116
+ if (typeof exp === "number" && exp * 1e3 <= Date.now()) {
117
+ throw new Error("[@holo-js/auth-workos] WorkOS token has expired.");
118
+ }
119
+ const nbf = typeof parsed.payload.nbf === "number" ? parsed.payload.nbf : void 0;
120
+ if (typeof nbf === "number" && nbf * 1e3 > Date.now()) {
121
+ throw new Error("[@holo-js/auth-workos] WorkOS token is not valid yet.");
122
+ }
123
+ return parsed.payload;
124
+ }
125
+ function normalizeWorkosUserProfile(user) {
126
+ const firstName = typeof user.firstName === "string" ? user.firstName : typeof user.first_name === "string" ? user.first_name : void 0;
127
+ const lastName = typeof user.lastName === "string" ? user.lastName : typeof user.last_name === "string" ? user.last_name : void 0;
128
+ const name = typeof user.name === "string" ? user.name : [firstName, lastName].filter(Boolean).join(" ").trim() || void 0;
129
+ return Object.freeze({
130
+ id: String(user.id ?? ""),
131
+ email: typeof user.email === "string" ? user.email : void 0,
132
+ emailVerified: user.emailVerified === true || user.email_verified === true,
133
+ firstName,
134
+ lastName,
135
+ name,
136
+ avatar: typeof user.profilePictureUrl === "string" ? user.profilePictureUrl : typeof user.profile_picture_url === "string" ? user.profile_picture_url : void 0,
137
+ organizationId: typeof user.organizationId === "string" ? user.organizationId : typeof user.organization_id === "string" ? user.organization_id : void 0,
138
+ raw: user
139
+ });
140
+ }
141
+ async function fetchWorkosUserProfile(userId, config) {
142
+ const apiKey = config.apiKey?.trim();
143
+ if (!apiKey) {
144
+ throw new Error("[@holo-js/auth-workos] WorkOS verification requires apiKey to be configured.");
145
+ }
146
+ const response = await fetch(`${WORKOS_API_BASE_URL}/user_management/users/${encodeURIComponent(userId)}`, {
147
+ headers: {
148
+ accept: "application/json",
149
+ authorization: `Bearer ${apiKey}`
150
+ }
151
+ });
152
+ if (!response.ok) {
153
+ throw new Error(`[@holo-js/auth-workos] Failed to load WorkOS user "${userId}".`);
154
+ }
155
+ return normalizeWorkosUserProfile(await response.json());
156
+ }
157
+ function createDefaultProviderRuntime(providerName, config) {
158
+ const cacheKey = JSON.stringify([
159
+ providerName,
160
+ config.clientId ?? "",
161
+ config.apiKey ?? ""
162
+ ]);
163
+ const existing = workosDefaultProviderRuntimeCache.get(cacheKey);
164
+ if (existing) {
165
+ return existing;
166
+ }
167
+ const runtime = Object.freeze({
168
+ async verifySession({ token }) {
169
+ const claims = await verifyWorkosSessionToken(token, config);
170
+ const userId = typeof claims.sub === "string" ? claims.sub : "";
171
+ if (!userId) {
172
+ throw new Error("[@holo-js/auth-workos] WorkOS token did not include a subject.");
173
+ }
174
+ const profile = await fetchWorkosUserProfile(userId, config);
175
+ const sessionId = typeof claims.sid === "string" ? claims.sid : typeof claims.session_id === "string" ? claims.session_id : token;
176
+ return Object.freeze({
177
+ sessionId,
178
+ identity: profile,
179
+ accessToken: token,
180
+ expiresAt: typeof claims.exp === "number" ? new Date(claims.exp * 1e3) : void 0,
181
+ raw: Object.freeze({
182
+ claims,
183
+ user: profile.raw
184
+ })
185
+ });
186
+ }
187
+ });
188
+ workosDefaultProviderRuntimeCache.set(cacheKey, runtime);
189
+ return runtime;
190
+ }
191
+ function resolveConfiguredProviderName(provider) {
192
+ const bindings = authRuntimeInternals.getRuntimeBindings();
193
+ if (provider?.trim()) {
194
+ return provider.trim();
195
+ }
196
+ const configuredProviders = Object.keys(bindings.config.workos);
197
+ if (configuredProviders.length === 0) {
198
+ return "default";
199
+ }
200
+ if (configuredProviders.length === 1) {
201
+ return configuredProviders[0];
202
+ }
203
+ if (configuredProviders.includes("default")) {
204
+ return "default";
205
+ }
206
+ throw new Error("[@holo-js/auth-workos] WorkOS provider name is required when multiple auth.workos entries exist.");
207
+ }
208
+ function getConfiguredProviderConfig(provider) {
209
+ const providerName = resolveConfiguredProviderName(provider);
210
+ const authBindings = authRuntimeInternals.getRuntimeBindings();
211
+ const configured = authBindings.config.workos[providerName];
212
+ if (!configured) {
213
+ throw new Error(`[@holo-js/auth-workos] WorkOS provider "${providerName}" is not configured in auth.workos.`);
214
+ }
215
+ return {
216
+ name: providerName,
217
+ clientId: configured.clientId,
218
+ apiKey: configured.apiKey,
219
+ cookiePassword: configured.cookiePassword,
220
+ redirectUri: configured.redirectUri,
221
+ sessionCookie: configured.sessionCookie,
222
+ guard: configured.guard,
223
+ mapToProvider: configured.mapToProvider
224
+ };
225
+ }
226
+ function getProviderRuntime(provider) {
227
+ const providerName = resolveConfiguredProviderName(provider);
228
+ return getBindings().providers[providerName] ?? createDefaultProviderRuntime(providerName, getConfiguredProviderConfig(providerName));
229
+ }
230
+ function resolveGuardAndProvider(provider) {
231
+ const authBindings = authRuntimeInternals.getRuntimeBindings();
232
+ const providerConfig = getConfiguredProviderConfig(provider);
233
+ const guardName = providerConfig.guard ?? authBindings.config.defaults.guard;
234
+ const guard = authBindings.config.guards[guardName];
235
+ if (!guard) {
236
+ throw new Error(`[@holo-js/auth-workos] Guard "${guardName}" is not configured for WorkOS provider "${providerConfig.name}".`);
237
+ }
238
+ if (guard.driver !== "session") {
239
+ throw new Error(`[@holo-js/auth-workos] WorkOS sign-in requires auth guard "${guardName}" to use the session driver.`);
240
+ }
241
+ const authProvider = providerConfig.mapToProvider ?? guard.provider;
242
+ const adapter = authBindings.providers[authProvider];
243
+ if (!adapter) {
244
+ throw new Error(`[@holo-js/auth-workos] Auth provider runtime "${authProvider}" is not configured.`);
245
+ }
246
+ return {
247
+ guard: guardName,
248
+ authProvider,
249
+ adapter
250
+ };
251
+ }
252
+ function requireUserId(adapter, user, message) {
253
+ if (!user || typeof user !== "object") {
254
+ throw new Error(message);
255
+ }
256
+ const userId = adapter.getId(user);
257
+ if (typeof userId !== "string" && typeof userId !== "number") {
258
+ throw new Error(message);
259
+ }
260
+ return userId;
261
+ }
262
+ function requireUserRecord(user, message) {
263
+ if (!user || typeof user !== "object") {
264
+ throw new Error(message);
265
+ }
266
+ return user;
267
+ }
268
+ function serializeLocalUser(adapter, user, providerName) {
269
+ const id = adapter.getId(user);
270
+ const serialized = adapter.serialize ? adapter.serialize(user) : { ...user };
271
+ const result = {
272
+ ...serialized,
273
+ id
274
+ };
275
+ Object.defineProperty(result, AUTH_PROVIDER_MARKER, {
276
+ value: providerName,
277
+ enumerable: false,
278
+ configurable: true
279
+ });
280
+ return Object.freeze(result);
281
+ }
282
+ function resolveDisplayName(profile) {
283
+ if (profile.name?.trim()) {
284
+ return profile.name.trim();
285
+ }
286
+ const fullName = [profile.firstName?.trim(), profile.lastName?.trim()].filter(Boolean).join(" ").trim();
287
+ return fullName || void 0;
288
+ }
289
+ function resolveEmailForCreation(profile) {
290
+ const normalized = profile.email?.trim();
291
+ if (normalized) {
292
+ return normalized;
293
+ }
294
+ return `${profile.id}@workos.hosted.local`;
295
+ }
296
+ function normalizeHostedProfile(profile) {
297
+ return Object.freeze({
298
+ id: profile.id,
299
+ email: profile.email,
300
+ emailVerified: profile.emailVerified === true,
301
+ firstName: profile.firstName,
302
+ lastName: profile.lastName,
303
+ name: resolveDisplayName(profile),
304
+ avatar: profile.avatar,
305
+ organizationId: profile.organizationId,
306
+ raw: profile.raw
307
+ });
308
+ }
309
+ async function findUserByEmail(adapter, email) {
310
+ if (!email?.trim()) {
311
+ return null;
312
+ }
313
+ const user = await adapter.findByCredentials({ email: email.trim() });
314
+ return user ? requireUserRecord(user, "[@holo-js/auth-workos] Auth provider lookups must return object users.") : null;
315
+ }
316
+ async function updateLocalUser(adapter, user, input) {
317
+ const nextInput = {
318
+ name: input.name,
319
+ email: input.email,
320
+ avatar: typeof input.avatar === "undefined" ? void 0 : input.avatar,
321
+ email_verified_at: input.emailVerified === true ? /* @__PURE__ */ new Date() : void 0
322
+ };
323
+ const current = user;
324
+ const changed = typeof nextInput.name !== "undefined" && nextInput.name !== current.name || typeof nextInput.email !== "undefined" && nextInput.email !== current.email || typeof nextInput.avatar !== "undefined" && nextInput.avatar !== (current.avatar ?? void 0) || input.emailVerified === true && !current.email_verified_at;
325
+ if (!changed) {
326
+ return { user, changed: false };
327
+ }
328
+ if (adapter.update) {
329
+ return {
330
+ user: requireUserRecord(
331
+ await adapter.update(user, nextInput),
332
+ "[@holo-js/auth-workos] Auth provider updates must return object users."
333
+ ),
334
+ changed: true
335
+ };
336
+ }
337
+ throw new Error(
338
+ "[@holo-js/auth-workos] Auth provider adapters must implement update() to persist profile changes."
339
+ );
340
+ }
341
+ async function ensureNoUnexpectedEmailCollision(adapter, authProvider, profile, currentUserId) {
342
+ const email = profile.email?.trim();
343
+ if (!email) {
344
+ return;
345
+ }
346
+ const matched = await findUserByEmail(adapter, email);
347
+ if (!matched) {
348
+ return;
349
+ }
350
+ if (requireUserId(
351
+ adapter,
352
+ matched,
353
+ "[@holo-js/auth-workos] Matched local users must expose a serializable id."
354
+ ) !== currentUserId) {
355
+ throw new WorkosAuthConflictError({
356
+ provider: "workos",
357
+ workosUserId: profile.id,
358
+ email,
359
+ message: `[@holo-js/auth-workos] WorkOS email "${email}" collides with a different local user.`
360
+ });
361
+ }
362
+ }
363
+ async function assertUserLinkAvailable(providerName, authProvider, adapter, user, workosUserId) {
364
+ const existing = await getBindings().identityStore.findByUserId(
365
+ providerName,
366
+ authProvider,
367
+ requireUserId(adapter, user, "[@holo-js/auth-workos] Linked users must expose a serializable id.")
368
+ );
369
+ if (existing && existing.providerUserId !== workosUserId) {
370
+ throw new WorkosAuthConflictError({
371
+ provider: providerName,
372
+ workosUserId,
373
+ email: existing.email,
374
+ message: `[@holo-js/auth-workos] Local user is already linked to WorkOS identity "${existing.providerUserId}".`
375
+ });
376
+ }
377
+ }
378
+ function isEmailVerificationRequired() {
379
+ return authRuntimeInternals.getRuntimeBindings().config.emailVerification.required === true;
380
+ }
381
+ function createIdentityRecord(input) {
382
+ const now = /* @__PURE__ */ new Date();
383
+ return Object.freeze({
384
+ provider: input.provider,
385
+ providerUserId: input.profile.id,
386
+ guard: input.guard,
387
+ authProvider: input.authProvider,
388
+ userId: input.userId,
389
+ email: input.profile.email,
390
+ emailVerified: input.profile.emailVerified === true,
391
+ profile: normalizeHostedProfile(input.profile),
392
+ linkedAt: input.previous?.linkedAt ?? now,
393
+ updatedAt: now
394
+ });
395
+ }
396
+ function getSessionTokenFromRequest(request, sessionCookie) {
397
+ const authorization = request.headers.get("authorization")?.trim();
398
+ if (authorization) {
399
+ const [scheme, token] = authorization.split(/\s+/, 2);
400
+ if (scheme?.toLowerCase() === "bearer" && token?.trim()) {
401
+ return token.trim();
402
+ }
403
+ }
404
+ const cookies = parseCookieHeader(request.headers.get("cookie"));
405
+ return cookies[sessionCookie] ?? null;
406
+ }
407
+ function getHoloSessionIdFromRequest(request) {
408
+ const cookieHeader = authRuntimeInternals.getRuntimeBindings().session.sessionCookie("");
409
+ const separator = cookieHeader.indexOf("=");
410
+ const cookieName = separator > 0 ? decodeURIComponent(cookieHeader.slice(0, separator)) : "";
411
+ if (!cookieName) {
412
+ return null;
413
+ }
414
+ const cookies = parseCookieHeader(request.headers.get("cookie"));
415
+ return cookies[cookieName] ?? null;
416
+ }
417
+ async function reuseExistingHoloSession(request, authenticated) {
418
+ const bindings = authRuntimeInternals.getRuntimeBindings();
419
+ const sessionId = getHoloSessionIdFromRequest(request);
420
+ if (!sessionId) {
421
+ return null;
422
+ }
423
+ bindings.context.setSessionId(authenticated.guard, sessionId);
424
+ const record = await bindings.session.read(sessionId);
425
+ const payload = authRuntimeInternals.readSessionPayload(record, authenticated.guard);
426
+ if (!payload || payload.guard !== authenticated.guard || payload.provider !== authenticated.authProvider || String(payload.userId) !== String(authenticated.user.id)) {
427
+ return null;
428
+ }
429
+ bindings.context.setCachedUser(authenticated.guard, authenticated.user);
430
+ return Object.freeze({
431
+ guard: authenticated.guard,
432
+ user: authenticated.user,
433
+ sessionId,
434
+ cookies: Object.freeze([])
435
+ });
436
+ }
437
+ async function verifySession(token, provider) {
438
+ const providerConfig = getConfiguredProviderConfig(provider);
439
+ const runtime = getProviderRuntime(providerConfig.name);
440
+ if (!runtime.verifySession) {
441
+ throw new Error(`[@holo-js/auth-workos] WorkOS provider runtime "${providerConfig.name}" does not implement verifySession().`);
442
+ }
443
+ return runtime.verifySession({
444
+ provider: providerConfig.name,
445
+ token,
446
+ config: providerConfig
447
+ });
448
+ }
449
+ async function verifyRequest(request, provider) {
450
+ const providerConfig = getConfiguredProviderConfig(provider);
451
+ const runtime = getProviderRuntime(providerConfig.name);
452
+ if (runtime.verifyRequest) {
453
+ return runtime.verifyRequest({
454
+ provider: providerConfig.name,
455
+ request,
456
+ config: providerConfig
457
+ });
458
+ }
459
+ const token = getSessionTokenFromRequest(request, providerConfig.sessionCookie);
460
+ if (!token) {
461
+ return null;
462
+ }
463
+ return verifySession(token, providerConfig.name);
464
+ }
465
+ async function syncIdentity(session, provider) {
466
+ const providerConfig = getConfiguredProviderConfig(provider);
467
+ const providerName = providerConfig.name;
468
+ const profile = session.identity;
469
+ const { guard, authProvider, adapter } = resolveGuardAndProvider(providerName);
470
+ const verificationRequired = isEmailVerificationRequired();
471
+ const verifiedEmail = profile.emailVerified === true && profile.email?.trim() ? profile.email.trim() : void 0;
472
+ if (verificationRequired && !verifiedEmail) {
473
+ throw new Error(`[@holo-js/auth-workos] WorkOS identity "${profile.id}" must provide a verified email address.`);
474
+ }
475
+ const identityStore = getBindings().identityStore;
476
+ const existingIdentity = await identityStore.findByProviderUserId(providerName, profile.id);
477
+ if (existingIdentity) {
478
+ const existingLinkedUser = await adapter.findById(existingIdentity.userId);
479
+ let linkedUser = existingLinkedUser ? requireUserRecord(existingLinkedUser, "[@holo-js/auth-workos] Auth provider lookups must return object users.") : null;
480
+ if (!linkedUser) {
481
+ linkedUser = verifiedEmail ? await findUserByEmail(adapter, verifiedEmail) : null;
482
+ if (linkedUser) {
483
+ await assertUserLinkAvailable(providerName, authProvider, adapter, linkedUser, profile.id);
484
+ }
485
+ if (!linkedUser) {
486
+ linkedUser = requireUserRecord(await adapter.create({
487
+ name: resolveDisplayName(profile),
488
+ email: resolveEmailForCreation(profile),
489
+ password: null,
490
+ avatar: profile.avatar ?? null,
491
+ email_verified_at: profile.emailVerified === true ? /* @__PURE__ */ new Date() : null
492
+ }), "[@holo-js/auth-workos] Auth provider create() must return an object user.");
493
+ }
494
+ const relinked = await updateLocalUser(adapter, linkedUser, {
495
+ name: resolveDisplayName(profile),
496
+ email: profile.email?.trim(),
497
+ avatar: profile.avatar,
498
+ emailVerified: profile.emailVerified === true
499
+ });
500
+ const relinkedUser = relinked.user;
501
+ const identity3 = createIdentityRecord({
502
+ provider: providerName,
503
+ guard,
504
+ authProvider,
505
+ userId: requireUserId(
506
+ adapter,
507
+ relinkedUser,
508
+ "[@holo-js/auth-workos] Relinked local users must expose a serializable id."
509
+ ),
510
+ profile,
511
+ previous: existingIdentity
512
+ });
513
+ await identityStore.save(identity3);
514
+ return Object.freeze({
515
+ provider: providerName,
516
+ guard,
517
+ authProvider,
518
+ status: "relinked",
519
+ user: serializeLocalUser(adapter, relinkedUser, authProvider),
520
+ identity: identity3,
521
+ session
522
+ });
523
+ }
524
+ await ensureNoUnexpectedEmailCollision(
525
+ adapter,
526
+ authProvider,
527
+ profile,
528
+ requireUserId(
529
+ adapter,
530
+ linkedUser,
531
+ "[@holo-js/auth-workos] Linked local users must expose a serializable id."
532
+ )
533
+ );
534
+ const updated = await updateLocalUser(adapter, linkedUser, {
535
+ name: resolveDisplayName(profile),
536
+ email: profile.email?.trim(),
537
+ avatar: profile.avatar,
538
+ emailVerified: profile.emailVerified === true
539
+ });
540
+ const identity2 = createIdentityRecord({
541
+ provider: providerName,
542
+ guard,
543
+ authProvider,
544
+ userId: requireUserId(
545
+ adapter,
546
+ updated.user,
547
+ "[@holo-js/auth-workos] Updated local users must expose a serializable id."
548
+ ),
549
+ profile,
550
+ previous: existingIdentity
551
+ });
552
+ await identityStore.save(identity2);
553
+ return Object.freeze({
554
+ provider: providerName,
555
+ guard,
556
+ authProvider,
557
+ status: updated.changed ? "updated" : "linked",
558
+ user: serializeLocalUser(adapter, updated.user, authProvider),
559
+ identity: identity2,
560
+ session
561
+ });
562
+ }
563
+ let localUser = verifiedEmail ? await findUserByEmail(adapter, verifiedEmail) : null;
564
+ if (localUser) {
565
+ await assertUserLinkAvailable(providerName, authProvider, adapter, localUser, profile.id);
566
+ const linked = await updateLocalUser(adapter, localUser, {
567
+ name: resolveDisplayName(profile),
568
+ email: profile.email?.trim(),
569
+ avatar: profile.avatar,
570
+ emailVerified: profile.emailVerified === true
571
+ });
572
+ const identity2 = createIdentityRecord({
573
+ provider: providerName,
574
+ guard,
575
+ authProvider,
576
+ userId: requireUserId(
577
+ adapter,
578
+ linked.user,
579
+ "[@holo-js/auth-workos] Linked local users must expose a serializable id."
580
+ ),
581
+ profile
582
+ });
583
+ await identityStore.save(identity2);
584
+ return Object.freeze({
585
+ provider: providerName,
586
+ guard,
587
+ authProvider,
588
+ status: "linked",
589
+ user: serializeLocalUser(adapter, linked.user, authProvider),
590
+ identity: identity2,
591
+ session
592
+ });
593
+ }
594
+ localUser = requireUserRecord(await adapter.create({
595
+ name: resolveDisplayName(profile),
596
+ email: resolveEmailForCreation(profile),
597
+ password: null,
598
+ avatar: profile.avatar ?? null,
599
+ email_verified_at: profile.emailVerified === true ? /* @__PURE__ */ new Date() : null
600
+ }), "[@holo-js/auth-workos] Auth provider create() must return an object user.");
601
+ const identity = createIdentityRecord({
602
+ provider: providerName,
603
+ guard,
604
+ authProvider,
605
+ userId: requireUserId(
606
+ adapter,
607
+ localUser,
608
+ "[@holo-js/auth-workos] Created local users must expose a serializable id."
609
+ ),
610
+ profile
611
+ });
612
+ await identityStore.save(identity);
613
+ return Object.freeze({
614
+ provider: providerName,
615
+ guard,
616
+ authProvider,
617
+ status: "created",
618
+ user: serializeLocalUser(adapter, localUser, authProvider),
619
+ identity,
620
+ session
621
+ });
622
+ }
623
+ async function authenticate(request, provider) {
624
+ const session = await verifyRequest(request, provider);
625
+ if (!session) {
626
+ return null;
627
+ }
628
+ const authenticated = await syncIdentity(session, provider);
629
+ const authSession = await reuseExistingHoloSession(request, authenticated) ?? await authRuntimeInternals.establishSessionForUser(authenticated.user, {
630
+ guard: authenticated.guard,
631
+ provider: authenticated.authProvider
632
+ });
633
+ return Object.freeze({
634
+ ...authenticated,
635
+ authSession
636
+ });
637
+ }
638
+ function configureWorkosAuthRuntime(bindings) {
639
+ if (!bindings) {
640
+ workosBindings = void 0;
641
+ return;
642
+ }
643
+ workosBindings = Object.freeze({
644
+ providers: bindings.providers ?? workosBindings?.providers,
645
+ identityStore: bindings.identityStore ?? workosBindings?.identityStore
646
+ });
647
+ }
648
+ function resetWorkosAuthRuntime() {
649
+ workosBindings = void 0;
650
+ }
651
+ var workosAuth = Object.freeze({
652
+ authenticate,
653
+ syncIdentity,
654
+ verifyRequest,
655
+ verifySession
656
+ });
657
+ var workosAuthInternals = {
658
+ getBindings,
659
+ getConfiguredProviderConfig,
660
+ getSessionTokenFromRequest,
661
+ normalizeHostedProfile,
662
+ resolveConfiguredProviderName,
663
+ resolveDisplayName,
664
+ resolveEmailForCreation,
665
+ resolveGuardAndProvider
666
+ };
667
+ export {
668
+ WorkosAuthConflictError,
669
+ authenticate,
670
+ configureWorkosAuthRuntime,
671
+ resetWorkosAuthRuntime,
672
+ syncIdentity,
673
+ verifyRequest,
674
+ verifySession,
675
+ workosAuth,
676
+ workosAuthInternals
677
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@holo-js/auth-workos",
3
+ "version": "0.1.3",
4
+ "description": "Holo-JS Framework - WorkOS auth integration contracts",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.mjs",
11
+ "default": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "main": "./dist/index.mjs",
15
+ "types": "./dist/index.d.ts",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "stub": "tsup",
22
+ "typecheck": "tsc -p tsconfig.json --noEmit",
23
+ "test": "vitest --run"
24
+ },
25
+ "dependencies": {
26
+ "@holo-js/auth": "workspace:*",
27
+ "@holo-js/config": "workspace:*",
28
+ "@holo-js/session": "workspace:*"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.10.2",
32
+ "tsup": "^8.3.5",
33
+ "typescript": "^5.7.2",
34
+ "vitest": "^2.1.8"
35
+ }
36
+ }