@~lyre/auth 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -6
- package/dist/chunk-F2GDXTY4.js +971 -0
- package/dist/index.d.ts +323 -1
- package/dist/index.js +37 -1
- package/dist/nest.d.ts +22 -4
- package/dist/nest.js +53 -7
- package/dist/sveltekit.d.ts +19 -1
- package/dist/sveltekit.js +141 -36
- package/package.json +6 -4
- package/dist/chunk-TQNQLX3C.js +0 -338
package/dist/sveltekit.js
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
import {
|
|
2
2
|
beginAccountsLoginRedirect,
|
|
3
|
+
clearLoginStateCookie,
|
|
3
4
|
clearPlatformSessionCookie,
|
|
5
|
+
createLoginStateCookie,
|
|
6
|
+
createOidcClientFromConfig,
|
|
7
|
+
createPlatformSessionCookie,
|
|
4
8
|
handleAccountsCallback,
|
|
9
|
+
identityFromClaims,
|
|
5
10
|
identityPassthroughSync,
|
|
6
11
|
introspectServiceKey,
|
|
7
|
-
|
|
12
|
+
isOidcError,
|
|
8
13
|
normalizeAccountsWebhook,
|
|
14
|
+
readLoginStateCookie,
|
|
9
15
|
readPlatformSessionCookie,
|
|
16
|
+
safeNextPath,
|
|
10
17
|
serviceKeyFromRequest,
|
|
11
18
|
verifyWebhookSignature
|
|
12
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-F2GDXTY4.js";
|
|
13
20
|
|
|
14
21
|
// src/sveltekit.ts
|
|
15
22
|
import { createHash } from "crypto";
|
|
16
23
|
var STATE_COOKIE = "accounts_auth_state";
|
|
24
|
+
var REFRESH_SKEW_MS = 6e4;
|
|
17
25
|
function redirectWithCookies(location, cookies) {
|
|
18
26
|
const headers = new Headers({ location });
|
|
19
27
|
for (const c of cookies) if (c) headers.append("set-cookie", c);
|
|
@@ -27,58 +35,155 @@ function createAuthHandle(opts) {
|
|
|
27
35
|
const postLogoutPath = opts.postLogoutPath ?? "/";
|
|
28
36
|
const cookieOptions = opts.cookieOptions;
|
|
29
37
|
const sessionSecret = opts.sessionSecret;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
38
|
+
const mock = Boolean(opts.config.useMock) && !opts.client;
|
|
39
|
+
const refreshTokens = opts.refreshTokens ?? true;
|
|
40
|
+
let client = opts.client ?? null;
|
|
41
|
+
const getClient = () => client ??= createOidcClientFromConfig(opts.config, { fetchImpl: opts.fetchImpl, ...opts.oidc });
|
|
42
|
+
const refreshes = /* @__PURE__ */ new Map();
|
|
43
|
+
function refreshOnce(refreshToken) {
|
|
44
|
+
const key = createHash("sha256").update(refreshToken).digest("hex");
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
for (const [k, v] of refreshes) if (v.expires <= now) refreshes.delete(k);
|
|
47
|
+
const hit = refreshes.get(key);
|
|
48
|
+
if (hit) return hit.promise;
|
|
49
|
+
const promise = getClient().refresh(refreshToken);
|
|
50
|
+
refreshes.set(key, { promise, expires: now + 3e4 });
|
|
51
|
+
promise.catch(() => refreshes.delete(key));
|
|
52
|
+
return promise;
|
|
53
|
+
}
|
|
54
|
+
const authError = async (error, event) => {
|
|
55
|
+
const response = opts.onAuthError ? await opts.onAuthError(error, event) : new Response(`Sign-in failed (${isOidcError(error) ? error.code : "error"}).`, {
|
|
56
|
+
status: 400,
|
|
57
|
+
headers: { "content-type": "text/plain; charset=utf-8" }
|
|
33
58
|
});
|
|
34
|
-
|
|
35
|
-
|
|
59
|
+
try {
|
|
60
|
+
response.headers.append("set-cookie", clearLoginStateCookie(cookieOptions));
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
return response;
|
|
64
|
+
};
|
|
65
|
+
return async ({ event, resolve }) => {
|
|
66
|
+
const cookieHeader = event.request.headers.get("cookie");
|
|
67
|
+
let session = readPlatformSessionCookie(cookieHeader, { secret: sessionSecret });
|
|
68
|
+
const setLocals = () => {
|
|
69
|
+
event.locals.session = session;
|
|
70
|
+
event.locals.principal = session.principal;
|
|
71
|
+
};
|
|
72
|
+
setLocals();
|
|
36
73
|
const path = event.url.pathname;
|
|
37
74
|
if (path === loginPath) {
|
|
38
|
-
const next = event.url.searchParams.get("next")
|
|
39
|
-
|
|
40
|
-
|
|
75
|
+
const next = safeNextPath(event.url.searchParams.get("next"));
|
|
76
|
+
if (mock) {
|
|
77
|
+
const flow = beginAccountsLoginRedirect(opts.config, { nextPath: next, cookieOptions });
|
|
78
|
+
return redirectWithCookies(flow.redirectUrl, [flow.stateCookie]);
|
|
79
|
+
}
|
|
80
|
+
const login = await getClient().beginLogin();
|
|
81
|
+
return redirectWithCookies(login.url, [
|
|
82
|
+
createLoginStateCookie(login, { nextPath: next, secret: sessionSecret, cookieOptions })
|
|
83
|
+
]);
|
|
41
84
|
}
|
|
42
85
|
if (path === callbackPath) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
86
|
+
if (mock) {
|
|
87
|
+
const result = await handleAccountsCallback({
|
|
88
|
+
config: opts.config,
|
|
89
|
+
code: event.url.searchParams.get("code") ?? "",
|
|
90
|
+
state: event.url.searchParams.get("state") ?? "",
|
|
91
|
+
storedState: event.cookies.get(STATE_COOKIE),
|
|
92
|
+
syncAccountsUser: sync,
|
|
93
|
+
cookieOptions,
|
|
94
|
+
secret: sessionSecret
|
|
95
|
+
});
|
|
96
|
+
return redirectWithCookies(result.nextPath || "/", [result.sessionCookie, result.clearStateCookie]);
|
|
97
|
+
}
|
|
98
|
+
const stored = readLoginStateCookie(cookieHeader, { secret: sessionSecret });
|
|
99
|
+
try {
|
|
100
|
+
const { claims, tokens } = await getClient().handleCallback({ url: event.url, expected: stored });
|
|
101
|
+
const identity = identityFromClaims(claims);
|
|
102
|
+
const local = await sync(identity);
|
|
103
|
+
const next = {
|
|
104
|
+
principal: {
|
|
105
|
+
identity,
|
|
106
|
+
user: local.user,
|
|
107
|
+
tenantAccess: { activeTenantId: local.activeTenantId, memberships: local.memberships }
|
|
108
|
+
},
|
|
109
|
+
tenant: null,
|
|
110
|
+
accessToken: tokens.accessToken,
|
|
111
|
+
refreshToken: tokens.refreshToken,
|
|
112
|
+
expiresAt: new Date(tokens.expiresAt).toISOString()
|
|
113
|
+
};
|
|
114
|
+
return redirectWithCookies(stored?.nextPath ?? "/", [
|
|
115
|
+
createPlatformSessionCookie(next, cookieOptions, sessionSecret),
|
|
116
|
+
clearLoginStateCookie(cookieOptions)
|
|
117
|
+
]);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
return authError(error, event);
|
|
120
|
+
}
|
|
59
121
|
}
|
|
60
122
|
if (path === logoutPath) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
123
|
+
let location = postLogoutPath;
|
|
124
|
+
if (!mock) {
|
|
125
|
+
if (session.refreshToken) {
|
|
126
|
+
await getClient().revoke(session.refreshToken, "refresh_token").catch(() => void 0);
|
|
127
|
+
}
|
|
128
|
+
if (opts.endAccountsSession) {
|
|
129
|
+
location = await getClient().logoutUrl().catch(() => postLogoutPath);
|
|
130
|
+
}
|
|
64
131
|
}
|
|
65
|
-
return redirectWithCookies(
|
|
132
|
+
return redirectWithCookies(location, [
|
|
66
133
|
clearPlatformSessionCookie(cookieOptions),
|
|
134
|
+
clearLoginStateCookie(cookieOptions),
|
|
67
135
|
`${STATE_COOKIE}=; ${cookieOptions?.sameSite === "none" ? "Path=/; HttpOnly; SameSite=None; Secure" : "Path=/; HttpOnly; SameSite=Lax"}; Max-Age=0`
|
|
68
136
|
]);
|
|
69
137
|
}
|
|
138
|
+
let sessionCookieUpdate = null;
|
|
139
|
+
if (!mock && refreshTokens && session.principal && session.refreshToken && (!session.expiresAt || Date.parse(session.expiresAt) - Date.now() < REFRESH_SKEW_MS)) {
|
|
140
|
+
try {
|
|
141
|
+
const tokens = await refreshOnce(session.refreshToken);
|
|
142
|
+
session = {
|
|
143
|
+
...session,
|
|
144
|
+
accessToken: tokens.accessToken,
|
|
145
|
+
// Rotation: always keep the NEW refresh token; the old one is dead.
|
|
146
|
+
refreshToken: tokens.refreshToken,
|
|
147
|
+
expiresAt: new Date(tokens.expiresAt).toISOString()
|
|
148
|
+
};
|
|
149
|
+
sessionCookieUpdate = createPlatformSessionCookie(session, cookieOptions, sessionSecret);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (isOidcError(error, "token_request_failed") && error.status && error.status < 500) {
|
|
152
|
+
session = { principal: null, tenant: null };
|
|
153
|
+
sessionCookieUpdate = clearPlatformSessionCookie(cookieOptions);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
setLocals();
|
|
157
|
+
}
|
|
70
158
|
if (opts.protect && opts.protect(event) && !session.principal) {
|
|
71
159
|
if (path.startsWith("/api")) {
|
|
72
|
-
return
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
160
|
+
return withCookie(
|
|
161
|
+
new Response(JSON.stringify({ message: "Authentication required." }), {
|
|
162
|
+
status: 401,
|
|
163
|
+
headers: { "content-type": "application/json" }
|
|
164
|
+
}),
|
|
165
|
+
sessionCookieUpdate
|
|
166
|
+
);
|
|
76
167
|
}
|
|
77
|
-
return redirectWithCookies(
|
|
168
|
+
return redirectWithCookies(
|
|
169
|
+
`${loginPath}?next=${encodeURIComponent(path + event.url.search)}`,
|
|
170
|
+
sessionCookieUpdate ? [sessionCookieUpdate] : []
|
|
171
|
+
);
|
|
78
172
|
}
|
|
79
|
-
return resolve(event);
|
|
173
|
+
return withCookie(await resolve(event), sessionCookieUpdate);
|
|
80
174
|
};
|
|
81
175
|
}
|
|
176
|
+
function withCookie(response, cookie) {
|
|
177
|
+
if (!cookie) return response;
|
|
178
|
+
try {
|
|
179
|
+
response.headers.append("set-cookie", cookie);
|
|
180
|
+
return response;
|
|
181
|
+
} catch {
|
|
182
|
+
const copy = new Response(response.body, response);
|
|
183
|
+
copy.headers.append("set-cookie", cookie);
|
|
184
|
+
return copy;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
82
187
|
function createServiceKeyHandle(opts) {
|
|
83
188
|
const match = opts.match ?? ((event) => event.url.pathname.startsWith("/api"));
|
|
84
189
|
const ttl = opts.cacheTtlMs ?? 6e4;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@~lyre/auth",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Shared Axis Accounts auth SDK
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"description": "Shared Axis Accounts auth SDK — framework-agnostic session/identity core (HMAC-signed cookies, accounts login/callback/logout) plus an optional turnkey SvelteKit adapter.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
7
7
|
"license": "MIT",
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
],
|
|
52
52
|
"scripts": {
|
|
53
53
|
"build": "tsup src/index.ts src/sveltekit.ts src/nest.ts src/admin.ts --format esm --dts --clean",
|
|
54
|
-
"check": "tsc --noEmit"
|
|
54
|
+
"check": "tsc --noEmit",
|
|
55
|
+
"test": "vitest run"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
58
|
"@sveltejs/kit": "^2.0.0"
|
|
@@ -65,7 +66,8 @@
|
|
|
65
66
|
"@sveltejs/kit": "^2.0.0",
|
|
66
67
|
"@types/node": "^22",
|
|
67
68
|
"tsup": "^8.3.5",
|
|
68
|
-
"typescript": "^6.0.2"
|
|
69
|
+
"typescript": "^6.0.2",
|
|
70
|
+
"vitest": "^4.1.12"
|
|
69
71
|
},
|
|
70
72
|
"publishConfig": {
|
|
71
73
|
"access": "public"
|
package/dist/chunk-TQNQLX3C.js
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
// src/index.ts
|
|
2
|
-
import { createHmac, timingSafeEqual } from "crypto";
|
|
3
|
-
var SESSION_COOKIE_NAME = "platform_session";
|
|
4
|
-
var STATE_COOKIE_NAME = "accounts_auth_state";
|
|
5
|
-
function normalizeOptionalString(value) {
|
|
6
|
-
const trimmed = value?.trim();
|
|
7
|
-
return trimmed ? trimmed : void 0;
|
|
8
|
-
}
|
|
9
|
-
function createAccountsClientConfig(input = {}) {
|
|
10
|
-
return {
|
|
11
|
-
baseUrl: normalizeOptionalString(input.baseUrl ?? process.env.ACCOUNTS_BASE_URL),
|
|
12
|
-
clientId: input.clientId ?? process.env.ACCOUNTS_CLIENT_ID ?? "accounts-app",
|
|
13
|
-
clientSecret: normalizeOptionalString(input.clientSecret ?? process.env.ACCOUNTS_CLIENT_SECRET),
|
|
14
|
-
redirectUri: input.redirectUri ?? process.env.ACCOUNTS_REDIRECT_URI ?? "http://localhost:5173/auth/callback",
|
|
15
|
-
logoutRedirectUri: input.logoutRedirectUri ?? process.env.ACCOUNTS_LOGOUT_REDIRECT_URI ?? "http://localhost:5173/",
|
|
16
|
-
useMock: input.useMock ?? (process.env.ACCOUNTS_USE_MOCK === "true" || !process.env.ACCOUNTS_BASE_URL || !process.env.ACCOUNTS_CLIENT_ID)
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
function createPlatformAuth() {
|
|
20
|
-
return {
|
|
21
|
-
readSession(cookieHeader) {
|
|
22
|
-
return readPlatformSessionCookie(cookieHeader);
|
|
23
|
-
},
|
|
24
|
-
requirePrincipal(session) {
|
|
25
|
-
if (!session.principal) {
|
|
26
|
-
throw new Error("Authentication required.");
|
|
27
|
-
}
|
|
28
|
-
return session.principal;
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
function beginAccountsLoginRedirect(config, options) {
|
|
33
|
-
const nonce = globalThis.crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2);
|
|
34
|
-
const state = {
|
|
35
|
-
nonce,
|
|
36
|
-
nextPath: options.nextPath && options.nextPath.startsWith("/") ? options.nextPath : "/",
|
|
37
|
-
tenantSlug: options.tenantSlug
|
|
38
|
-
};
|
|
39
|
-
if (config.useMock) {
|
|
40
|
-
const redirectUrl = new URL(config.redirectUri);
|
|
41
|
-
redirectUrl.searchParams.set("code", `demo_${nonce}`);
|
|
42
|
-
redirectUrl.searchParams.set("state", serializeAuthorizationState(state));
|
|
43
|
-
return {
|
|
44
|
-
redirectUrl: redirectUrl.toString(),
|
|
45
|
-
stateValue: serializeAuthorizationState(state),
|
|
46
|
-
stateCookie: createStateCookie(serializeAuthorizationState(state), options.cookieOptions)
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
const authorizeUrl = new URL("/auth/authorize", config.baseUrl);
|
|
50
|
-
authorizeUrl.searchParams.set("app_id", config.clientId);
|
|
51
|
-
authorizeUrl.searchParams.set("redirect_uri", config.redirectUri);
|
|
52
|
-
authorizeUrl.searchParams.set("state", serializeAuthorizationState(state));
|
|
53
|
-
return {
|
|
54
|
-
redirectUrl: authorizeUrl.toString(),
|
|
55
|
-
stateValue: serializeAuthorizationState(state),
|
|
56
|
-
stateCookie: createStateCookie(serializeAuthorizationState(state))
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
async function handleAccountsCallback(input) {
|
|
60
|
-
if (!input.code) {
|
|
61
|
-
throw new Error("Missing authorization code.");
|
|
62
|
-
}
|
|
63
|
-
if (input.storedState && input.state !== input.storedState) {
|
|
64
|
-
throw new Error("Invalid authorization state.");
|
|
65
|
-
}
|
|
66
|
-
const parsedState = parseAuthorizationState(input.state);
|
|
67
|
-
const tokenResponse = await exchangeAuthorizationCode({
|
|
68
|
-
config: input.config,
|
|
69
|
-
code: input.code,
|
|
70
|
-
fetchImpl: input.fetchImpl
|
|
71
|
-
});
|
|
72
|
-
const identity = normalizeIdentity(tokenResponse.user);
|
|
73
|
-
const local = await input.syncAccountsUser(identity);
|
|
74
|
-
const session = {
|
|
75
|
-
principal: {
|
|
76
|
-
identity,
|
|
77
|
-
user: local.user,
|
|
78
|
-
tenantAccess: {
|
|
79
|
-
activeTenantId: local.activeTenantId,
|
|
80
|
-
memberships: local.memberships
|
|
81
|
-
}
|
|
82
|
-
},
|
|
83
|
-
tenant: null,
|
|
84
|
-
accessToken: tokenResponse.access_token,
|
|
85
|
-
refreshToken: tokenResponse.refresh_token,
|
|
86
|
-
expiresAt: new Date(Date.now() + tokenResponse.expires_in * 1e3).toISOString()
|
|
87
|
-
};
|
|
88
|
-
return {
|
|
89
|
-
session,
|
|
90
|
-
nextPath: parsedState.nextPath,
|
|
91
|
-
tenantSlug: parsedState.tenantSlug,
|
|
92
|
-
sessionCookie: createSessionCookie(session, input.cookieOptions, input.secret),
|
|
93
|
-
clearStateCookie: clearStateCookie(input.cookieOptions)
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
async function exchangeAuthorizationCode(input) {
|
|
97
|
-
if (input.config.useMock || input.code.startsWith("demo_")) {
|
|
98
|
-
return createMockTokenResponse(input.code);
|
|
99
|
-
}
|
|
100
|
-
const fetchImpl = input.fetchImpl ?? fetch;
|
|
101
|
-
const clientSecret = normalizeOptionalString(input.config.clientSecret);
|
|
102
|
-
const tokenBody = {
|
|
103
|
-
grant_type: "authorization_code",
|
|
104
|
-
code: input.code,
|
|
105
|
-
redirect_uri: input.config.redirectUri,
|
|
106
|
-
client_id: input.config.clientId
|
|
107
|
-
};
|
|
108
|
-
if (clientSecret) {
|
|
109
|
-
tokenBody.client_secret = clientSecret;
|
|
110
|
-
}
|
|
111
|
-
const response = await fetchImpl(new URL("/api/auth/token", input.config.baseUrl), {
|
|
112
|
-
method: "POST",
|
|
113
|
-
headers: {
|
|
114
|
-
"content-type": "application/json"
|
|
115
|
-
},
|
|
116
|
-
body: JSON.stringify(tokenBody)
|
|
117
|
-
});
|
|
118
|
-
if (!response.ok) {
|
|
119
|
-
throw new Error(`Accounts token exchange failed with status ${response.status}.`);
|
|
120
|
-
}
|
|
121
|
-
return await response.json();
|
|
122
|
-
}
|
|
123
|
-
function syncAccountsUser(identity, syncer) {
|
|
124
|
-
return syncer(identity);
|
|
125
|
-
}
|
|
126
|
-
var identityPassthroughSync = (identity) => ({
|
|
127
|
-
user: {
|
|
128
|
-
id: identity.id,
|
|
129
|
-
externalIdentityId: identity.id,
|
|
130
|
-
email: identity.email,
|
|
131
|
-
name: identity.name,
|
|
132
|
-
firstName: identity.firstName,
|
|
133
|
-
lastName: identity.lastName
|
|
134
|
-
},
|
|
135
|
-
memberships: []
|
|
136
|
-
});
|
|
137
|
-
function resolveActiveTenant(memberships, preferredTenantId) {
|
|
138
|
-
if (preferredTenantId && memberships.some((membership) => membership.tenantId === preferredTenantId)) {
|
|
139
|
-
return preferredTenantId;
|
|
140
|
-
}
|
|
141
|
-
return memberships.find((membership) => membership.isDefault)?.tenantId ?? memberships[0]?.tenantId;
|
|
142
|
-
}
|
|
143
|
-
function readPlatformSessionCookie(cookieHeader, opts) {
|
|
144
|
-
const cookieValue = readCookie(cookieHeader, SESSION_COOKIE_NAME);
|
|
145
|
-
if (!cookieValue) {
|
|
146
|
-
return { principal: null, tenant: null };
|
|
147
|
-
}
|
|
148
|
-
let raw = cookieValue;
|
|
149
|
-
if (opts?.secret) {
|
|
150
|
-
const verified = unsignValue(cookieValue, opts.secret);
|
|
151
|
-
if (verified == null) return { principal: null, tenant: null };
|
|
152
|
-
raw = verified;
|
|
153
|
-
}
|
|
154
|
-
try {
|
|
155
|
-
return JSON.parse(decodeURIComponent(raw));
|
|
156
|
-
} catch {
|
|
157
|
-
return { principal: null, tenant: null };
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
function cookieAttrs(opts) {
|
|
161
|
-
const sameSite = opts?.sameSite ?? "lax";
|
|
162
|
-
const secure = opts?.secure || sameSite === "none";
|
|
163
|
-
const cap = sameSite.charAt(0).toUpperCase() + sameSite.slice(1);
|
|
164
|
-
return `Path=/; HttpOnly; SameSite=${cap}${secure ? "; Secure" : ""}`;
|
|
165
|
-
}
|
|
166
|
-
function signValue(value, secret) {
|
|
167
|
-
const sig = createHmac("sha256", secret).update(value).digest("hex");
|
|
168
|
-
return `${value}.${sig}`;
|
|
169
|
-
}
|
|
170
|
-
function unsignValue(signed, secret) {
|
|
171
|
-
const dot = signed.lastIndexOf(".");
|
|
172
|
-
if (dot < 0) return null;
|
|
173
|
-
const value = signed.slice(0, dot);
|
|
174
|
-
const sig = signed.slice(dot + 1);
|
|
175
|
-
if (!/^[0-9a-f]{64}$/.test(sig)) return null;
|
|
176
|
-
const expected = createHmac("sha256", secret).update(value).digest("hex");
|
|
177
|
-
const a = Buffer.from(sig, "hex");
|
|
178
|
-
const b = Buffer.from(expected, "hex");
|
|
179
|
-
if (a.length !== b.length) return null;
|
|
180
|
-
return timingSafeEqual(a, b) ? value : null;
|
|
181
|
-
}
|
|
182
|
-
function clearPlatformSessionCookie(opts) {
|
|
183
|
-
return `${SESSION_COOKIE_NAME}=; ${cookieAttrs(opts)}; Max-Age=0`;
|
|
184
|
-
}
|
|
185
|
-
function createSessionCookie(session, opts, secret) {
|
|
186
|
-
const raw = encodeURIComponent(JSON.stringify(session));
|
|
187
|
-
const value = secret ? signValue(raw, secret) : raw;
|
|
188
|
-
return `${SESSION_COOKIE_NAME}=${value}; ${cookieAttrs(opts)}; Max-Age=${60 * 60 * 24 * 7}`;
|
|
189
|
-
}
|
|
190
|
-
function createStateCookie(value, opts) {
|
|
191
|
-
return `${STATE_COOKIE_NAME}=${encodeURIComponent(value)}; ${cookieAttrs(opts)}; Max-Age=${60 * 10}`;
|
|
192
|
-
}
|
|
193
|
-
function clearStateCookie(opts) {
|
|
194
|
-
return `${STATE_COOKIE_NAME}=; ${cookieAttrs(opts)}; Max-Age=0`;
|
|
195
|
-
}
|
|
196
|
-
function serializeAuthorizationState(state) {
|
|
197
|
-
return encodeURIComponent(JSON.stringify(state));
|
|
198
|
-
}
|
|
199
|
-
function parseAuthorizationState(value) {
|
|
200
|
-
try {
|
|
201
|
-
return JSON.parse(decodeURIComponent(value));
|
|
202
|
-
} catch {
|
|
203
|
-
throw new Error("Invalid authorization state payload.");
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
function normalizeIdentity(user) {
|
|
207
|
-
const firstName = user.firstName ?? user.first_name;
|
|
208
|
-
const lastName = user.lastName ?? user.last_name;
|
|
209
|
-
return {
|
|
210
|
-
id: user.id,
|
|
211
|
-
// Prefer the real address under either key; only synthesize when truly absent.
|
|
212
|
-
email: user.emailAddress ?? user.email ?? `${user.id}@accounts.local`,
|
|
213
|
-
name: user.name ?? ([firstName, lastName].filter(Boolean).join(" ") || "Accounts User"),
|
|
214
|
-
firstName,
|
|
215
|
-
lastName,
|
|
216
|
-
avatarUrl: user.avatarUrl ?? user.avatar_url
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
function createMockTokenResponse(code) {
|
|
220
|
-
const suffix = code.replace("demo_", "").slice(0, 8) || "demo";
|
|
221
|
-
return {
|
|
222
|
-
access_token: `access_${suffix}`,
|
|
223
|
-
refresh_token: `refresh_${suffix}`,
|
|
224
|
-
token_type: "Bearer",
|
|
225
|
-
expires_in: 60 * 60 * 24 * 7,
|
|
226
|
-
user: {
|
|
227
|
-
id: "accounts-user-demo-admin",
|
|
228
|
-
email: "hello@babyplanet.example",
|
|
229
|
-
name: "Baby Planet Admin",
|
|
230
|
-
first_name: "Baby",
|
|
231
|
-
last_name: "Planet"
|
|
232
|
-
}
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
function readCookie(cookieHeader, name) {
|
|
236
|
-
if (!cookieHeader) return null;
|
|
237
|
-
for (const part of cookieHeader.split(";")) {
|
|
238
|
-
const [key, ...rest] = part.trim().split("=");
|
|
239
|
-
if (key === name) {
|
|
240
|
-
return rest.join("=");
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
return null;
|
|
244
|
-
}
|
|
245
|
-
function serviceKeyFromRequest(request) {
|
|
246
|
-
const explicit = request.headers.get("x-api-key");
|
|
247
|
-
if (explicit && explicit.trim()) return explicit.trim();
|
|
248
|
-
const auth = request.headers.get("authorization");
|
|
249
|
-
if (auth && auth.startsWith("Bearer ")) {
|
|
250
|
-
const token = auth.slice(7).trim();
|
|
251
|
-
if (token.startsWith("sk_")) return token;
|
|
252
|
-
}
|
|
253
|
-
return null;
|
|
254
|
-
}
|
|
255
|
-
async function introspectServiceKey(input) {
|
|
256
|
-
const base = normalizeOptionalString(input.config.baseUrl);
|
|
257
|
-
if (!input.key || !base) return null;
|
|
258
|
-
const fetchImpl = input.fetchImpl ?? fetch;
|
|
259
|
-
try {
|
|
260
|
-
const res = await fetchImpl(new URL("/api/auth/service-keys/introspect", base), {
|
|
261
|
-
method: "POST",
|
|
262
|
-
headers: { "x-api-key": input.key, accept: "application/json" }
|
|
263
|
-
});
|
|
264
|
-
if (!res.ok) return null;
|
|
265
|
-
const data = await res.json();
|
|
266
|
-
if (!data.valid || !data.keyId) return null;
|
|
267
|
-
const accessibleApps = (data.accessibleApps ?? []).filter(
|
|
268
|
-
(a) => Boolean(a) && typeof a.id === "string" && typeof a.slug === "string"
|
|
269
|
-
).map((a) => ({ id: a.id, slug: a.slug, name: a.name }));
|
|
270
|
-
return {
|
|
271
|
-
keyId: data.keyId,
|
|
272
|
-
tenantId: data.tenantId ?? null,
|
|
273
|
-
scopes: Array.isArray(data.scopes) ? data.scopes : [],
|
|
274
|
-
accessibleApps,
|
|
275
|
-
expiresAt: data.expiresAt ?? null
|
|
276
|
-
};
|
|
277
|
-
} catch {
|
|
278
|
-
return null;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
async function logoutFromAccounts(input) {
|
|
282
|
-
const base = normalizeOptionalString(input.config.baseUrl);
|
|
283
|
-
if (!base || !input.accessToken) return { success: false };
|
|
284
|
-
const fetchImpl = input.fetchImpl ?? fetch;
|
|
285
|
-
try {
|
|
286
|
-
const res = await fetchImpl(new URL("/api/auth/logout", base), {
|
|
287
|
-
method: "POST",
|
|
288
|
-
headers: {
|
|
289
|
-
"content-type": "application/json",
|
|
290
|
-
authorization: `Bearer ${input.accessToken}`,
|
|
291
|
-
accept: "application/json"
|
|
292
|
-
},
|
|
293
|
-
body: JSON.stringify({ all: input.all ?? false })
|
|
294
|
-
});
|
|
295
|
-
if (!res.ok) return { success: false };
|
|
296
|
-
return await res.json();
|
|
297
|
-
} catch {
|
|
298
|
-
return { success: false };
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
function verifyWebhookSignature(rawBody, signature, secret) {
|
|
302
|
-
if (!signature || !secret) return false;
|
|
303
|
-
const provided = (signature.startsWith("sha256=") ? signature.slice(7) : signature).toLowerCase();
|
|
304
|
-
if (!/^[0-9a-f]{64}$/.test(provided)) return false;
|
|
305
|
-
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
|
|
306
|
-
const a = Buffer.from(provided, "hex");
|
|
307
|
-
const b = Buffer.from(expected, "hex");
|
|
308
|
-
if (a.length !== b.length) return false;
|
|
309
|
-
return timingSafeEqual(a, b);
|
|
310
|
-
}
|
|
311
|
-
function normalizeAccountsWebhook(payload) {
|
|
312
|
-
const body = payload && typeof payload === "object" ? payload : {};
|
|
313
|
-
const type = String(body.event ?? body.type ?? "unknown");
|
|
314
|
-
return { ...body, type, data: body.data };
|
|
315
|
-
}
|
|
316
|
-
function scopeSatisfied(granted, required) {
|
|
317
|
-
if (!granted || granted.length === 0) return false;
|
|
318
|
-
return granted.includes(required);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
export {
|
|
322
|
-
createAccountsClientConfig,
|
|
323
|
-
createPlatformAuth,
|
|
324
|
-
beginAccountsLoginRedirect,
|
|
325
|
-
handleAccountsCallback,
|
|
326
|
-
exchangeAuthorizationCode,
|
|
327
|
-
syncAccountsUser,
|
|
328
|
-
identityPassthroughSync,
|
|
329
|
-
resolveActiveTenant,
|
|
330
|
-
readPlatformSessionCookie,
|
|
331
|
-
clearPlatformSessionCookie,
|
|
332
|
-
serviceKeyFromRequest,
|
|
333
|
-
introspectServiceKey,
|
|
334
|
-
logoutFromAccounts,
|
|
335
|
-
verifyWebhookSignature,
|
|
336
|
-
normalizeAccountsWebhook,
|
|
337
|
-
scopeSatisfied
|
|
338
|
-
};
|