@~lyre/auth 0.0.8 → 0.1.1
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 +37 -0
- package/dist/{chunk-2FSPKJAC.js → chunk-TQNQLX3C.js} +38 -0
- package/dist/index.d.ts +25 -1
- package/dist/index.js +9 -3
- package/dist/nest.d.ts +85 -0
- package/dist/nest.js +134 -0
- package/dist/sveltekit.d.ts +15 -2
- package/dist/sveltekit.js +36 -2
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -53,6 +53,43 @@ export const handle = createAuthHandle({
|
|
|
53
53
|
`@sveltejs/kit` is an **optional peer dependency** — only required if you import
|
|
54
54
|
the `/sveltekit` entry.
|
|
55
55
|
|
|
56
|
+
### `@~lyre/auth/nest` — Nest / Fastify adapter
|
|
57
|
+
|
|
58
|
+
For services on Nest + Fastify. Adds **no dependencies** — nothing here imports
|
|
59
|
+
`@nestjs/*` or `fastify`; you supply the guard class and DI wiring, the SDK
|
|
60
|
+
supplies the Accounts logic. Works from a plain Fastify app too: any request
|
|
61
|
+
whose `headers` is a plain object satisfies the input type.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { createServiceKeyResolver, createUserSessionResolver, hasScopes }
|
|
65
|
+
from '@~lyre/auth/nest';
|
|
66
|
+
|
|
67
|
+
const keys = createServiceKeyResolver({ config: { baseUrl: ACCOUNTS_BASE_URL } });
|
|
68
|
+
const sessions = createUserSessionResolver({ config: { baseUrl: ACCOUNTS_BASE_URL } });
|
|
69
|
+
|
|
70
|
+
// In a Nest guard (req is the Fastify request):
|
|
71
|
+
const key = await keys.resolve(req);
|
|
72
|
+
if (key.status === 'invalid') throw new UnauthorizedException(); // present but bad
|
|
73
|
+
if (key.status === 'ok') {
|
|
74
|
+
if (!hasScopes(key.context, ['my-app:things:read'])) throw new ForbiddenException();
|
|
75
|
+
// key.context.accessibleApps tells you which apps this key may act on
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// A user session bearer resolves to who + which app, from the token alone:
|
|
79
|
+
const user = await sessions.resolve(req);
|
|
80
|
+
// → { userId, sessionId, appId, tenantId, email, name } | null
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Semantics match `createServiceKeyHandle`: **absent key → `absent`** (fall through to
|
|
84
|
+
other auth), **present-but-invalid → `invalid`** (reject rather than silently
|
|
85
|
+
continue). Introspection is cached by `sha256` of the credential — negative
|
|
86
|
+
results too, so a bad key can't hammer Accounts. Neither resolver throws: a
|
|
87
|
+
transport failure reads as "not authenticated", so a flaky Accounts cannot 500
|
|
88
|
+
the consuming service.
|
|
89
|
+
|
|
90
|
+
`resolve()` needs `GET /api/auth/me` to return `session.appId` (Accounts ≥ the
|
|
91
|
+
release that added it); older deployments yield `appId: null`.
|
|
92
|
+
|
|
56
93
|
## Publishing
|
|
57
94
|
|
|
58
95
|
See [PUBLISHING.md](PUBLISHING.md). Published to npm via GitHub Actions Trusted
|
|
@@ -278,6 +278,41 @@ async function introspectServiceKey(input) {
|
|
|
278
278
|
return null;
|
|
279
279
|
}
|
|
280
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
|
+
}
|
|
281
316
|
function scopeSatisfied(granted, required) {
|
|
282
317
|
if (!granted || granted.length === 0) return false;
|
|
283
318
|
return granted.includes(required);
|
|
@@ -296,5 +331,8 @@ export {
|
|
|
296
331
|
clearPlatformSessionCookie,
|
|
297
332
|
serviceKeyFromRequest,
|
|
298
333
|
introspectServiceKey,
|
|
334
|
+
logoutFromAccounts,
|
|
335
|
+
verifyWebhookSignature,
|
|
336
|
+
normalizeAccountsWebhook,
|
|
299
337
|
scopeSatisfied
|
|
300
338
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -162,6 +162,30 @@ declare function introspectServiceKey(input: {
|
|
|
162
162
|
key: string;
|
|
163
163
|
fetchImpl?: typeof fetch;
|
|
164
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
|
+
data?: unknown;
|
|
178
|
+
[key: string]: unknown;
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Normalize an Accounts webhook body into a stable `AccountsWebhookEvent`.
|
|
182
|
+
*
|
|
183
|
+
* The Accounts webhook worker sends `{ event, timestamp, data }` (the event NAME under `event`,
|
|
184
|
+
* the payload under `data`). Older/other senders may put the name under `type`. This collapses both
|
|
185
|
+
* so a consumer always reads `event.type` and `event.data`, and keeps the raw fields alongside.
|
|
186
|
+
* Framework-agnostic so both the SvelteKit handle and the Nest helper share one shape.
|
|
187
|
+
*/
|
|
188
|
+
declare function normalizeAccountsWebhook(payload: unknown): AccountsWebhookEvent;
|
|
165
189
|
declare function scopeSatisfied(granted: string[] | undefined, required: string): boolean;
|
|
166
190
|
|
|
167
|
-
export { type AccountsClientConfig, type AccountsIdentity, type AccountsTokenResponse, 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, readPlatformSessionCookie, resolveActiveTenant, scopeSatisfied, serviceKeyFromRequest, syncAccountsUser };
|
|
191
|
+
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, normalizeAccountsWebhook, readPlatformSessionCookie, resolveActiveTenant, scopeSatisfied, serviceKeyFromRequest, syncAccountsUser, verifyWebhookSignature };
|
package/dist/index.js
CHANGED
|
@@ -7,12 +7,15 @@ import {
|
|
|
7
7
|
handleAccountsCallback,
|
|
8
8
|
identityPassthroughSync,
|
|
9
9
|
introspectServiceKey,
|
|
10
|
+
logoutFromAccounts,
|
|
11
|
+
normalizeAccountsWebhook,
|
|
10
12
|
readPlatformSessionCookie,
|
|
11
13
|
resolveActiveTenant,
|
|
12
14
|
scopeSatisfied,
|
|
13
15
|
serviceKeyFromRequest,
|
|
14
|
-
syncAccountsUser
|
|
15
|
-
|
|
16
|
+
syncAccountsUser,
|
|
17
|
+
verifyWebhookSignature
|
|
18
|
+
} from "./chunk-TQNQLX3C.js";
|
|
16
19
|
export {
|
|
17
20
|
beginAccountsLoginRedirect,
|
|
18
21
|
clearPlatformSessionCookie,
|
|
@@ -22,9 +25,12 @@ export {
|
|
|
22
25
|
handleAccountsCallback,
|
|
23
26
|
identityPassthroughSync,
|
|
24
27
|
introspectServiceKey,
|
|
28
|
+
logoutFromAccounts,
|
|
29
|
+
normalizeAccountsWebhook,
|
|
25
30
|
readPlatformSessionCookie,
|
|
26
31
|
resolveActiveTenant,
|
|
27
32
|
scopeSatisfied,
|
|
28
33
|
serviceKeyFromRequest,
|
|
29
|
-
syncAccountsUser
|
|
34
|
+
syncAccountsUser,
|
|
35
|
+
verifyWebhookSignature
|
|
30
36
|
};
|
package/dist/nest.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { AccountsWebhookEvent, ServiceKeyContext, AccountsClientConfig } from './index.js';
|
|
2
|
+
export { normalizeAccountsWebhook } from './index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The minimum a request must expose. Fastify's `request` satisfies this as-is
|
|
6
|
+
* (`headers` is a plain object), as does Node's IncomingMessage.
|
|
7
|
+
*/
|
|
8
|
+
type HeaderCarrier = {
|
|
9
|
+
headers: Record<string, string | string[] | undefined>;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Adapt a header-bearing request to the web `Request` that the core helpers expect.
|
|
13
|
+
* Only headers are needed — `serviceKeyFromRequest` reads `x-api-key` / `authorization`.
|
|
14
|
+
*/
|
|
15
|
+
declare function toWebRequest(carrier: HeaderCarrier): Request;
|
|
16
|
+
/** Extract an `sk_` service key from a Fastify/Node request, or null when absent. */
|
|
17
|
+
declare function serviceKeyFromHeaders(carrier: HeaderCarrier): string | null;
|
|
18
|
+
type ServiceKeyResolverOptions = {
|
|
19
|
+
config: Pick<AccountsClientConfig, 'baseUrl'>;
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
/** Introspection cache TTL in ms. Default 60_000; 0 disables caching. */
|
|
22
|
+
cacheTtlMs?: number;
|
|
23
|
+
};
|
|
24
|
+
type ServiceKeyResolution = {
|
|
25
|
+
status: 'absent';
|
|
26
|
+
} | {
|
|
27
|
+
status: 'invalid';
|
|
28
|
+
} | {
|
|
29
|
+
status: 'ok';
|
|
30
|
+
context: ServiceKeyContext;
|
|
31
|
+
};
|
|
32
|
+
declare function createServiceKeyResolver(opts: ServiceKeyResolverOptions): {
|
|
33
|
+
resolve: (carrier: HeaderCarrier) => Promise<ServiceKeyResolution>;
|
|
34
|
+
invalidate: (key: string) => void;
|
|
35
|
+
};
|
|
36
|
+
type UserSessionContext = {
|
|
37
|
+
userId: string;
|
|
38
|
+
email?: string;
|
|
39
|
+
name?: string;
|
|
40
|
+
sessionId: string;
|
|
41
|
+
/** The app the session was issued for. Null for sessions created outside an app context. */
|
|
42
|
+
appId: string | null;
|
|
43
|
+
tenantId: string | null;
|
|
44
|
+
expiresAt?: string;
|
|
45
|
+
};
|
|
46
|
+
type UserSessionResolverOptions = {
|
|
47
|
+
config: Pick<AccountsClientConfig, 'baseUrl'>;
|
|
48
|
+
fetchImpl?: typeof fetch;
|
|
49
|
+
/** Cache TTL in ms. Default 60_000; 0 disables caching. */
|
|
50
|
+
cacheTtlMs?: number;
|
|
51
|
+
};
|
|
52
|
+
/** A session bearer is any Authorization: Bearer token that is NOT an `sk_` service key. */
|
|
53
|
+
declare function sessionTokenFromHeaders(carrier: HeaderCarrier): string | null;
|
|
54
|
+
declare function createUserSessionResolver(opts: UserSessionResolverOptions): {
|
|
55
|
+
resolve: (carrier: HeaderCarrier) => Promise<UserSessionContext | null>;
|
|
56
|
+
invalidate: (token: string) => void;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Whether a resolved key grants every required scope. Uses the core's `scopeSatisfied`
|
|
60
|
+
* (flat exact match, no wildcards) so behaviour matches Accounts' own enforcement.
|
|
61
|
+
*/
|
|
62
|
+
declare function hasScopes(ctx: ServiceKeyContext, required: readonly string[]): boolean;
|
|
63
|
+
/** Whether a resolved key may act on an app, matched by Accounts app id OR slug. */
|
|
64
|
+
declare function canAccessApp(ctx: ServiceKeyContext, appIdOrSlug: string): boolean;
|
|
65
|
+
type AccountsWebhookResult = {
|
|
66
|
+
ok: true;
|
|
67
|
+
event: AccountsWebhookEvent;
|
|
68
|
+
} | {
|
|
69
|
+
ok: false;
|
|
70
|
+
reason: 'bad_signature' | 'bad_json';
|
|
71
|
+
};
|
|
72
|
+
/** Header the Accounts webhook worker sends (HMAC-SHA256 hex of the raw body). */
|
|
73
|
+
declare const ACCOUNTS_WEBHOOK_SIGNATURE_HEADER = "x-webhook-signature";
|
|
74
|
+
/**
|
|
75
|
+
* Verify an Accounts webhook and parse it into a normalized `{ type, data, ... }` event.
|
|
76
|
+
*
|
|
77
|
+
* Pass the RAW request body (a Nest/Fastify controller must read the raw body, not a re-serialized
|
|
78
|
+
* object — the signature is over the exact bytes), the signature header value
|
|
79
|
+
* (`request.headers['x-webhook-signature']`), and the shared secret. Returns `{ok:false}` on a bad
|
|
80
|
+
* signature or unparseable JSON so the controller can 401/400; `{ok:true, event}` otherwise. The
|
|
81
|
+
* controller then reacts to `event.type` / `event.data` and returns 200 (so Accounts doesn't retry).
|
|
82
|
+
*/
|
|
83
|
+
declare function verifyAndParseAccountsWebhook(rawBody: string, signatureHeaderValue: string | string[] | undefined, secret: string): AccountsWebhookResult;
|
|
84
|
+
|
|
85
|
+
export { ACCOUNTS_WEBHOOK_SIGNATURE_HEADER, AccountsClientConfig, AccountsWebhookEvent, type AccountsWebhookResult, type HeaderCarrier, ServiceKeyContext, type ServiceKeyResolution, type ServiceKeyResolverOptions, type UserSessionContext, type UserSessionResolverOptions, canAccessApp, createServiceKeyResolver, createUserSessionResolver, hasScopes, serviceKeyFromHeaders, sessionTokenFromHeaders, toWebRequest, verifyAndParseAccountsWebhook };
|
package/dist/nest.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
introspectServiceKey,
|
|
3
|
+
normalizeAccountsWebhook,
|
|
4
|
+
scopeSatisfied,
|
|
5
|
+
serviceKeyFromRequest,
|
|
6
|
+
verifyWebhookSignature
|
|
7
|
+
} from "./chunk-TQNQLX3C.js";
|
|
8
|
+
|
|
9
|
+
// src/nest.ts
|
|
10
|
+
import { createHash } from "crypto";
|
|
11
|
+
function headerValue(carrier, name) {
|
|
12
|
+
const v = carrier.headers[name] ?? carrier.headers[name.toLowerCase()];
|
|
13
|
+
return Array.isArray(v) ? v[0] : v;
|
|
14
|
+
}
|
|
15
|
+
function toWebRequest(carrier) {
|
|
16
|
+
const headers = new Headers();
|
|
17
|
+
for (const [k, v] of Object.entries(carrier.headers)) {
|
|
18
|
+
if (v === void 0) continue;
|
|
19
|
+
if (Array.isArray(v)) {
|
|
20
|
+
for (const item of v) headers.append(k, item);
|
|
21
|
+
} else {
|
|
22
|
+
headers.set(k, v);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return new Request("http://localhost/", { headers });
|
|
26
|
+
}
|
|
27
|
+
function serviceKeyFromHeaders(carrier) {
|
|
28
|
+
return serviceKeyFromRequest(toWebRequest(carrier));
|
|
29
|
+
}
|
|
30
|
+
function createServiceKeyResolver(opts) {
|
|
31
|
+
const ttl = opts.cacheTtlMs ?? 6e4;
|
|
32
|
+
const cache = /* @__PURE__ */ new Map();
|
|
33
|
+
async function resolve(carrier) {
|
|
34
|
+
const key = serviceKeyFromHeaders(carrier);
|
|
35
|
+
if (!key) return { status: "absent" };
|
|
36
|
+
const hash = createHash("sha256").update(key).digest("hex");
|
|
37
|
+
const now = Date.now();
|
|
38
|
+
let ctx;
|
|
39
|
+
const cached = ttl > 0 ? cache.get(hash) : void 0;
|
|
40
|
+
if (cached && cached.expires > now) {
|
|
41
|
+
ctx = cached.value;
|
|
42
|
+
} else {
|
|
43
|
+
ctx = await introspectServiceKey({ config: opts.config, key, fetchImpl: opts.fetchImpl });
|
|
44
|
+
if (ttl > 0) cache.set(hash, { value: ctx, expires: now + ttl });
|
|
45
|
+
}
|
|
46
|
+
return ctx ? { status: "ok", context: ctx } : { status: "invalid" };
|
|
47
|
+
}
|
|
48
|
+
function invalidate(key) {
|
|
49
|
+
cache.delete(createHash("sha256").update(key).digest("hex"));
|
|
50
|
+
}
|
|
51
|
+
return { resolve, invalidate };
|
|
52
|
+
}
|
|
53
|
+
function sessionTokenFromHeaders(carrier) {
|
|
54
|
+
const auth = headerValue(carrier, "authorization");
|
|
55
|
+
if (!auth || !auth.startsWith("Bearer ")) return null;
|
|
56
|
+
const token = auth.slice(7).trim();
|
|
57
|
+
if (!token || token.startsWith("sk_")) return null;
|
|
58
|
+
return token;
|
|
59
|
+
}
|
|
60
|
+
function createUserSessionResolver(opts) {
|
|
61
|
+
const ttl = opts.cacheTtlMs ?? 6e4;
|
|
62
|
+
const cache = /* @__PURE__ */ new Map();
|
|
63
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
64
|
+
async function introspect(token) {
|
|
65
|
+
const base = opts.config.baseUrl?.replace(/\/$/, "");
|
|
66
|
+
if (!base || !token) return null;
|
|
67
|
+
try {
|
|
68
|
+
const res = await fetchImpl(new URL("/api/auth/me", base), {
|
|
69
|
+
headers: { authorization: `Bearer ${token}`, accept: "application/json" }
|
|
70
|
+
});
|
|
71
|
+
if (!res.ok) return null;
|
|
72
|
+
const data = await res.json();
|
|
73
|
+
const userId = data.user?.id;
|
|
74
|
+
const sessionId = data.session?.id;
|
|
75
|
+
if (!userId || !sessionId) return null;
|
|
76
|
+
return {
|
|
77
|
+
userId,
|
|
78
|
+
email: data.user?.emailAddress ?? data.user?.email,
|
|
79
|
+
name: data.user?.name,
|
|
80
|
+
sessionId,
|
|
81
|
+
appId: data.session?.appId ?? null,
|
|
82
|
+
tenantId: data.session?.tenantId ?? null,
|
|
83
|
+
expiresAt: data.session?.expiresAt
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function resolve(carrier) {
|
|
90
|
+
const token = sessionTokenFromHeaders(carrier);
|
|
91
|
+
if (!token) return null;
|
|
92
|
+
const hash = createHash("sha256").update(token).digest("hex");
|
|
93
|
+
const now = Date.now();
|
|
94
|
+
const cached = ttl > 0 ? cache.get(hash) : void 0;
|
|
95
|
+
if (cached && cached.expires > now) return cached.value;
|
|
96
|
+
const ctx = await introspect(token);
|
|
97
|
+
if (ttl > 0) cache.set(hash, { value: ctx, expires: now + ttl });
|
|
98
|
+
return ctx;
|
|
99
|
+
}
|
|
100
|
+
function invalidate(token) {
|
|
101
|
+
cache.delete(createHash("sha256").update(token).digest("hex"));
|
|
102
|
+
}
|
|
103
|
+
return { resolve, invalidate };
|
|
104
|
+
}
|
|
105
|
+
function hasScopes(ctx, required) {
|
|
106
|
+
return required.every((scope) => scopeSatisfied(ctx.scopes, scope));
|
|
107
|
+
}
|
|
108
|
+
function canAccessApp(ctx, appIdOrSlug) {
|
|
109
|
+
return ctx.accessibleApps.some((a) => a.id === appIdOrSlug || a.slug === appIdOrSlug);
|
|
110
|
+
}
|
|
111
|
+
var ACCOUNTS_WEBHOOK_SIGNATURE_HEADER = "x-webhook-signature";
|
|
112
|
+
function verifyAndParseAccountsWebhook(rawBody, signatureHeaderValue, secret) {
|
|
113
|
+
const signature = Array.isArray(signatureHeaderValue) ? signatureHeaderValue[0] : signatureHeaderValue;
|
|
114
|
+
if (!verifyWebhookSignature(rawBody, signature ?? null, secret)) {
|
|
115
|
+
return { ok: false, reason: "bad_signature" };
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
return { ok: true, event: normalizeAccountsWebhook(JSON.parse(rawBody)) };
|
|
119
|
+
} catch {
|
|
120
|
+
return { ok: false, reason: "bad_json" };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
export {
|
|
124
|
+
ACCOUNTS_WEBHOOK_SIGNATURE_HEADER,
|
|
125
|
+
canAccessApp,
|
|
126
|
+
createServiceKeyResolver,
|
|
127
|
+
createUserSessionResolver,
|
|
128
|
+
hasScopes,
|
|
129
|
+
normalizeAccountsWebhook,
|
|
130
|
+
serviceKeyFromHeaders,
|
|
131
|
+
sessionTokenFromHeaders,
|
|
132
|
+
toWebRequest,
|
|
133
|
+
verifyAndParseAccountsWebhook
|
|
134
|
+
};
|
package/dist/sveltekit.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RequestEvent, Handle } from '@sveltejs/kit';
|
|
2
|
-
import { PlatformSession, AccountsClientConfig, ServiceKeyContext, 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 = {
|
|
@@ -51,5 +51,18 @@ type ServiceKeyLocals = {
|
|
|
51
51
|
serviceKey?: ServiceKeyContext | null;
|
|
52
52
|
};
|
|
53
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
|
+
/**
|
|
59
|
+
* Header carrying the signature. Default 'x-webhook-signature' — the header the Accounts
|
|
60
|
+
* webhook worker actually sends (HMAC-SHA256 hex of the raw body).
|
|
61
|
+
*/
|
|
62
|
+
signatureHeader?: string;
|
|
63
|
+
/** React to a verified event. Throwing yields a 500 (Accounts may retry). */
|
|
64
|
+
onEvent: (event: AccountsWebhookEvent, raw: string, request: RequestEvent) => void | Promise<void>;
|
|
65
|
+
};
|
|
66
|
+
declare function createAccountsWebhookHandle(opts: AccountsWebhookOptions): Handle;
|
|
54
67
|
|
|
55
|
-
export { type PlatformLocals, type ServiceKeyHandleOptions, type ServiceKeyLocals, type SvelteKitAuthOptions, createAuthHandle, createServiceKeyHandle };
|
|
68
|
+
export { type AccountsWebhookOptions, type PlatformLocals, type ServiceKeyHandleOptions, type ServiceKeyLocals, type SvelteKitAuthOptions, createAccountsWebhookHandle, createAuthHandle, createServiceKeyHandle };
|
package/dist/sveltekit.js
CHANGED
|
@@ -4,9 +4,12 @@ import {
|
|
|
4
4
|
handleAccountsCallback,
|
|
5
5
|
identityPassthroughSync,
|
|
6
6
|
introspectServiceKey,
|
|
7
|
+
logoutFromAccounts,
|
|
8
|
+
normalizeAccountsWebhook,
|
|
7
9
|
readPlatformSessionCookie,
|
|
8
|
-
serviceKeyFromRequest
|
|
9
|
-
|
|
10
|
+
serviceKeyFromRequest,
|
|
11
|
+
verifyWebhookSignature
|
|
12
|
+
} from "./chunk-TQNQLX3C.js";
|
|
10
13
|
|
|
11
14
|
// src/sveltekit.ts
|
|
12
15
|
import { createHash } from "crypto";
|
|
@@ -55,6 +58,10 @@ function createAuthHandle(opts) {
|
|
|
55
58
|
]);
|
|
56
59
|
}
|
|
57
60
|
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 });
|
|
64
|
+
}
|
|
58
65
|
return redirectWithCookies(postLogoutPath, [
|
|
59
66
|
clearPlatformSessionCookie(cookieOptions),
|
|
60
67
|
`${STATE_COOKIE}=; ${cookieOptions?.sameSite === "none" ? "Path=/; HttpOnly; SameSite=None; Secure" : "Path=/; HttpOnly; SameSite=Lax"}; Max-Age=0`
|
|
@@ -102,8 +109,35 @@ function createServiceKeyHandle(opts) {
|
|
|
102
109
|
});
|
|
103
110
|
};
|
|
104
111
|
}
|
|
112
|
+
function jsonResponse(status, body) {
|
|
113
|
+
return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
114
|
+
}
|
|
115
|
+
function createAccountsWebhookHandle(opts) {
|
|
116
|
+
const path = opts.path ?? "/api/auth/accounts/webhook";
|
|
117
|
+
const sigHeader = opts.signatureHeader ?? "x-webhook-signature";
|
|
118
|
+
return async ({ event, resolve }) => {
|
|
119
|
+
if (event.url.pathname !== path || event.request.method !== "POST") return resolve(event);
|
|
120
|
+
const raw = await event.request.text();
|
|
121
|
+
if (!verifyWebhookSignature(raw, event.request.headers.get(sigHeader), opts.secret)) {
|
|
122
|
+
return jsonResponse(401, { ok: false, error: "Invalid webhook signature." });
|
|
123
|
+
}
|
|
124
|
+
let parsed;
|
|
125
|
+
try {
|
|
126
|
+
parsed = normalizeAccountsWebhook(JSON.parse(raw));
|
|
127
|
+
} catch {
|
|
128
|
+
return jsonResponse(400, { ok: false, error: "Invalid JSON body." });
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
await opts.onEvent(parsed, raw, event);
|
|
132
|
+
} catch {
|
|
133
|
+
return jsonResponse(500, { ok: false, error: "Webhook handler failed." });
|
|
134
|
+
}
|
|
135
|
+
return jsonResponse(200, { ok: true });
|
|
136
|
+
};
|
|
137
|
+
}
|
|
105
138
|
export {
|
|
106
139
|
clearPlatformSessionCookie,
|
|
140
|
+
createAccountsWebhookHandle,
|
|
107
141
|
createAuthHandle,
|
|
108
142
|
createServiceKeyHandle
|
|
109
143
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@~lyre/auth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
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,
|
|
@@ -33,7 +33,12 @@
|
|
|
33
33
|
"./sveltekit": {
|
|
34
34
|
"types": "./dist/sveltekit.d.ts",
|
|
35
35
|
"import": "./dist/sveltekit.js"
|
|
36
|
-
}
|
|
36
|
+
},
|
|
37
|
+
"./nest": {
|
|
38
|
+
"types": "./dist/nest.d.ts",
|
|
39
|
+
"import": "./dist/nest.js"
|
|
40
|
+
},
|
|
41
|
+
"./package.json": "./package.json"
|
|
37
42
|
},
|
|
38
43
|
"files": [
|
|
39
44
|
"dist",
|
|
@@ -41,7 +46,7 @@
|
|
|
41
46
|
"LICENSE"
|
|
42
47
|
],
|
|
43
48
|
"scripts": {
|
|
44
|
-
"build": "tsup src/index.ts src/sveltekit.ts --format esm --dts --clean",
|
|
49
|
+
"build": "tsup src/index.ts src/sveltekit.ts src/nest.ts --format esm --dts --clean",
|
|
45
50
|
"check": "tsc --noEmit"
|
|
46
51
|
},
|
|
47
52
|
"peerDependencies": {
|