@~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/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
- logoutFromAccounts,
12
+ isOidcError,
8
13
  normalizeAccountsWebhook,
14
+ readLoginStateCookie,
9
15
  readPlatformSessionCookie,
16
+ safeNextPath,
10
17
  serviceKeyFromRequest,
11
18
  verifyWebhookSignature
12
- } from "./chunk-TQNQLX3C.js";
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
- return async ({ event, resolve }) => {
31
- const session = readPlatformSessionCookie(event.request.headers.get("cookie"), {
32
- secret: sessionSecret
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
- event.locals.session = session;
35
- event.locals.principal = session.principal;
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
- const flow = beginAccountsLoginRedirect(opts.config, { nextPath: next, cookieOptions });
40
- return redirectWithCookies(flow.redirectUrl, [flow.stateCookie]);
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
- const code = event.url.searchParams.get("code") ?? "";
44
- const state = event.url.searchParams.get("state") ?? "";
45
- const storedState = event.cookies.get(STATE_COOKIE);
46
- const result = await handleAccountsCallback({
47
- config: opts.config,
48
- code,
49
- state,
50
- storedState,
51
- syncAccountsUser: sync,
52
- cookieOptions,
53
- secret: sessionSecret
54
- });
55
- return redirectWithCookies(result.nextPath || "/", [
56
- result.sessionCookie,
57
- result.clearStateCookie
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
- if (session.accessToken) {
62
- const all = event.url.searchParams.get("all") === "1" || event.url.searchParams.get("all") === "true";
63
- await logoutFromAccounts({ config: opts.config, accessToken: session.accessToken, all });
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(postLogoutPath, [
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 new Response(JSON.stringify({ message: "Authentication required." }), {
73
- status: 401,
74
- headers: { "content-type": "application/json" }
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(`${loginPath}?next=${encodeURIComponent(path + event.url.search)}`, []);
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.11.0",
4
- "description": "Shared Axis Accounts auth SDK \u2014 framework-agnostic session/identity core (HMAC-signed cookies, accounts login/callback/logout) plus an optional turnkey SvelteKit adapter.",
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"
@@ -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
- };