@~lyre/auth 0.0.7 → 0.0.9

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.
@@ -242,6 +242,76 @@ function readCookie(cookieHeader, name) {
242
242
  }
243
243
  return null;
244
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 scopeSatisfied(granted, required) {
312
+ if (!granted || granted.length === 0) return false;
313
+ return granted.includes(required);
314
+ }
245
315
 
246
316
  export {
247
317
  createAccountsClientConfig,
@@ -253,5 +323,10 @@ export {
253
323
  identityPassthroughSync,
254
324
  resolveActiveTenant,
255
325
  readPlatformSessionCookie,
256
- clearPlatformSessionCookie
326
+ clearPlatformSessionCookie,
327
+ serviceKeyFromRequest,
328
+ introspectServiceKey,
329
+ logoutFromAccounts,
330
+ verifyWebhookSignature,
331
+ scopeSatisfied
257
332
  };
package/dist/index.d.ts CHANGED
@@ -142,5 +142,40 @@ type CookieOptions = {
142
142
  secure?: boolean;
143
143
  };
144
144
  declare function clearPlatformSessionCookie(opts?: CookieOptions): string;
145
+ type ServiceKeyApp = {
146
+ id: string;
147
+ slug: string;
148
+ name?: string;
149
+ };
150
+ type ServiceKeyContext = {
151
+ keyId: string;
152
+ tenantId: string | null;
153
+ /** Scopes granted to this key (opaque strings owned by the consuming app). */
154
+ scopes: string[];
155
+ /** Apps this key may act on (auth resolves these from the key's app grant). */
156
+ accessibleApps: ServiceKeyApp[];
157
+ expiresAt: string | null;
158
+ };
159
+ declare function serviceKeyFromRequest(request: Request): string | null;
160
+ declare function introspectServiceKey(input: {
161
+ config: Pick<AccountsClientConfig, 'baseUrl'>;
162
+ key: string;
163
+ fetchImpl?: typeof fetch;
164
+ }): Promise<ServiceKeyContext | null>;
165
+ declare function logoutFromAccounts(input: {
166
+ config: Pick<AccountsClientConfig, 'baseUrl'>;
167
+ accessToken: string;
168
+ all?: boolean;
169
+ fetchImpl?: typeof fetch;
170
+ }): Promise<{
171
+ success: boolean;
172
+ revokedCount?: number;
173
+ }>;
174
+ declare function verifyWebhookSignature(rawBody: string, signature: string | null | undefined, secret: string): boolean;
175
+ type AccountsWebhookEvent = {
176
+ type: string;
177
+ [key: string]: unknown;
178
+ };
179
+ declare function scopeSatisfied(granted: string[] | undefined, required: string): boolean;
145
180
 
146
- export { type AccountsClientConfig, type AccountsIdentity, type AccountsTokenResponse, type AuthenticatedPrincipal, type AuthorizationState, type CookieOptions, type LocalUser, type PermissionSet, type PlatformSession, type SyncAccountsUser, type SyncAccountsUserResult, type TenantAccessContext, type TenantContext, type TenantMembership, beginAccountsLoginRedirect, clearPlatformSessionCookie, createAccountsClientConfig, createPlatformAuth, exchangeAuthorizationCode, handleAccountsCallback, identityPassthroughSync, readPlatformSessionCookie, resolveActiveTenant, syncAccountsUser };
181
+ export { type AccountsClientConfig, type AccountsIdentity, type AccountsTokenResponse, type AccountsWebhookEvent, type AuthenticatedPrincipal, type AuthorizationState, type CookieOptions, type LocalUser, type PermissionSet, type PlatformSession, type ServiceKeyApp, type ServiceKeyContext, type SyncAccountsUser, type SyncAccountsUserResult, type TenantAccessContext, type TenantContext, type TenantMembership, beginAccountsLoginRedirect, clearPlatformSessionCookie, createAccountsClientConfig, createPlatformAuth, exchangeAuthorizationCode, handleAccountsCallback, identityPassthroughSync, introspectServiceKey, logoutFromAccounts, readPlatformSessionCookie, resolveActiveTenant, scopeSatisfied, serviceKeyFromRequest, syncAccountsUser, verifyWebhookSignature };
package/dist/index.js CHANGED
@@ -6,10 +6,15 @@ import {
6
6
  exchangeAuthorizationCode,
7
7
  handleAccountsCallback,
8
8
  identityPassthroughSync,
9
+ introspectServiceKey,
10
+ logoutFromAccounts,
9
11
  readPlatformSessionCookie,
10
12
  resolveActiveTenant,
11
- syncAccountsUser
12
- } from "./chunk-RADVGFYQ.js";
13
+ scopeSatisfied,
14
+ serviceKeyFromRequest,
15
+ syncAccountsUser,
16
+ verifyWebhookSignature
17
+ } from "./chunk-QIAL6IDV.js";
13
18
  export {
14
19
  beginAccountsLoginRedirect,
15
20
  clearPlatformSessionCookie,
@@ -18,7 +23,12 @@ export {
18
23
  exchangeAuthorizationCode,
19
24
  handleAccountsCallback,
20
25
  identityPassthroughSync,
26
+ introspectServiceKey,
27
+ logoutFromAccounts,
21
28
  readPlatformSessionCookie,
22
29
  resolveActiveTenant,
23
- syncAccountsUser
30
+ scopeSatisfied,
31
+ serviceKeyFromRequest,
32
+ syncAccountsUser,
33
+ verifyWebhookSignature
24
34
  };
@@ -1,5 +1,5 @@
1
1
  import { RequestEvent, Handle } from '@sveltejs/kit';
2
- import { PlatformSession, AccountsClientConfig, SyncAccountsUser, CookieOptions } from './index.js';
2
+ import { AccountsWebhookEvent, PlatformSession, AccountsClientConfig, ServiceKeyContext, SyncAccountsUser, CookieOptions } from './index.js';
3
3
  export { clearPlatformSessionCookie } from './index.js';
4
4
 
5
5
  type SvelteKitAuthOptions = {
@@ -35,5 +35,31 @@ type PlatformLocals = {
35
35
  principal: PlatformSession['principal'];
36
36
  };
37
37
  declare function createAuthHandle(opts: SvelteKitAuthOptions): Handle;
38
+ type ServiceKeyHandleOptions = {
39
+ config: AccountsClientConfig;
40
+ fetchImpl?: typeof fetch;
41
+ /** Which requests to attempt key auth on. Default: paths starting with `/api`. */
42
+ match?: (event: RequestEvent) => boolean;
43
+ /** Introspection cache TTL in ms. Default 60_000; set 0 to disable caching. */
44
+ cacheTtlMs?: number;
45
+ /** Map the resolved key grant onto app locals. Called only for a valid key. */
46
+ onResolved?: (event: RequestEvent, ctx: ServiceKeyContext) => void;
47
+ /** Observe a present-but-invalid key (e.g. logging) before the 401 is returned. */
48
+ onInvalid?: (event: RequestEvent) => void;
49
+ };
50
+ type ServiceKeyLocals = {
51
+ serviceKey?: ServiceKeyContext | null;
52
+ };
53
+ declare function createServiceKeyHandle(opts: ServiceKeyHandleOptions): Handle;
54
+ type AccountsWebhookOptions = {
55
+ secret: string;
56
+ /** Route to receive webhooks on. Default '/api/auth/accounts/webhook'. */
57
+ path?: string;
58
+ /** Header carrying the signature. Default 'x-accounts-signature'. */
59
+ signatureHeader?: string;
60
+ /** React to a verified event. Throwing yields a 500 (Accounts may retry). */
61
+ onEvent: (event: AccountsWebhookEvent, raw: string, request: RequestEvent) => void | Promise<void>;
62
+ };
63
+ declare function createAccountsWebhookHandle(opts: AccountsWebhookOptions): Handle;
38
64
 
39
- export { type PlatformLocals, type SvelteKitAuthOptions, createAuthHandle };
65
+ export { type AccountsWebhookOptions, type PlatformLocals, type ServiceKeyHandleOptions, type ServiceKeyLocals, type SvelteKitAuthOptions, createAccountsWebhookHandle, createAuthHandle, createServiceKeyHandle };
package/dist/sveltekit.js CHANGED
@@ -3,10 +3,15 @@ import {
3
3
  clearPlatformSessionCookie,
4
4
  handleAccountsCallback,
5
5
  identityPassthroughSync,
6
- readPlatformSessionCookie
7
- } from "./chunk-RADVGFYQ.js";
6
+ introspectServiceKey,
7
+ logoutFromAccounts,
8
+ readPlatformSessionCookie,
9
+ serviceKeyFromRequest,
10
+ verifyWebhookSignature
11
+ } from "./chunk-QIAL6IDV.js";
8
12
 
9
13
  // src/sveltekit.ts
14
+ import { createHash } from "crypto";
10
15
  var STATE_COOKIE = "accounts_auth_state";
11
16
  function redirectWithCookies(location, cookies) {
12
17
  const headers = new Headers({ location });
@@ -52,6 +57,10 @@ function createAuthHandle(opts) {
52
57
  ]);
53
58
  }
54
59
  if (path === logoutPath) {
60
+ if (session.accessToken) {
61
+ const all = event.url.searchParams.get("all") === "1" || event.url.searchParams.get("all") === "true";
62
+ await logoutFromAccounts({ config: opts.config, accessToken: session.accessToken, all });
63
+ }
55
64
  return redirectWithCookies(postLogoutPath, [
56
65
  clearPlatformSessionCookie(cookieOptions),
57
66
  `${STATE_COOKIE}=; ${cookieOptions?.sameSite === "none" ? "Path=/; HttpOnly; SameSite=None; Secure" : "Path=/; HttpOnly; SameSite=Lax"}; Max-Age=0`
@@ -69,7 +78,65 @@ function createAuthHandle(opts) {
69
78
  return resolve(event);
70
79
  };
71
80
  }
81
+ function createServiceKeyHandle(opts) {
82
+ const match = opts.match ?? ((event) => event.url.pathname.startsWith("/api"));
83
+ const ttl = opts.cacheTtlMs ?? 6e4;
84
+ const cache = /* @__PURE__ */ new Map();
85
+ return async ({ event, resolve }) => {
86
+ if (!match(event)) return resolve(event);
87
+ const key = serviceKeyFromRequest(event.request);
88
+ if (!key) return resolve(event);
89
+ const hash = createHash("sha256").update(key).digest("hex");
90
+ const now = Date.now();
91
+ let ctx;
92
+ const cached = ttl > 0 ? cache.get(hash) : void 0;
93
+ if (cached && cached.expires > now) {
94
+ ctx = cached.value;
95
+ } else {
96
+ ctx = await introspectServiceKey({ config: opts.config, key, fetchImpl: opts.fetchImpl });
97
+ if (ttl > 0) cache.set(hash, { value: ctx, expires: now + ttl });
98
+ }
99
+ if (ctx) {
100
+ event.locals.serviceKey = ctx;
101
+ opts.onResolved?.(event, ctx);
102
+ return resolve(event);
103
+ }
104
+ opts.onInvalid?.(event);
105
+ return new Response(JSON.stringify({ message: "Invalid or expired API key." }), {
106
+ status: 401,
107
+ headers: { "content-type": "application/json" }
108
+ });
109
+ };
110
+ }
111
+ function jsonResponse(status, body) {
112
+ return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
113
+ }
114
+ function createAccountsWebhookHandle(opts) {
115
+ const path = opts.path ?? "/api/auth/accounts/webhook";
116
+ const sigHeader = opts.signatureHeader ?? "x-accounts-signature";
117
+ return async ({ event, resolve }) => {
118
+ if (event.url.pathname !== path || event.request.method !== "POST") return resolve(event);
119
+ const raw = await event.request.text();
120
+ if (!verifyWebhookSignature(raw, event.request.headers.get(sigHeader), opts.secret)) {
121
+ return jsonResponse(401, { ok: false, error: "Invalid webhook signature." });
122
+ }
123
+ let payload;
124
+ try {
125
+ payload = JSON.parse(raw);
126
+ } catch {
127
+ return jsonResponse(400, { ok: false, error: "Invalid JSON body." });
128
+ }
129
+ try {
130
+ await opts.onEvent({ ...payload, type: String(payload.type ?? "unknown") }, raw, event);
131
+ } catch {
132
+ return jsonResponse(500, { ok: false, error: "Webhook handler failed." });
133
+ }
134
+ return jsonResponse(200, { ok: true });
135
+ };
136
+ }
72
137
  export {
73
138
  clearPlatformSessionCookie,
74
- createAuthHandle
139
+ createAccountsWebhookHandle,
140
+ createAuthHandle,
141
+ createServiceKeyHandle
75
142
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@~lyre/auth",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
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,