@flytedesk/app-kit 0.0.1 → 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.
Files changed (53) hide show
  1. package/dist/auth/guards.d.ts +5 -0
  2. package/dist/auth/guards.js +28 -0
  3. package/dist/auth/guards.js.map +1 -0
  4. package/dist/auth/index.d.ts +33 -5
  5. package/dist/auth/index.js +31 -5
  6. package/dist/auth/index.js.map +1 -1
  7. package/dist/auth/oidc-client.d.ts +47 -0
  8. package/dist/auth/oidc-client.js +162 -0
  9. package/dist/auth/oidc-client.js.map +1 -0
  10. package/dist/auth/pkce.d.ts +8 -0
  11. package/dist/auth/pkce.js +12 -0
  12. package/dist/auth/pkce.js.map +1 -0
  13. package/dist/auth/plugin.d.ts +8 -0
  14. package/dist/auth/plugin.js +362 -0
  15. package/dist/auth/plugin.js.map +1 -0
  16. package/dist/auth/tokens.d.ts +24 -0
  17. package/dist/auth/tokens.js +35 -0
  18. package/dist/auth/tokens.js.map +1 -0
  19. package/dist/auth/types.d.ts +156 -0
  20. package/dist/auth/types.js +2 -0
  21. package/dist/auth/types.js.map +1 -0
  22. package/dist/cli/trace-sync.d.ts +2 -0
  23. package/dist/cli/trace-sync.js +362 -0
  24. package/dist/cli/trace-sync.js.map +1 -0
  25. package/dist/rate-limit/index.d.ts +6 -0
  26. package/dist/rate-limit/index.js +7 -0
  27. package/dist/rate-limit/index.js.map +1 -0
  28. package/dist/rate-limit/plugin.d.ts +23 -0
  29. package/dist/rate-limit/plugin.js +75 -0
  30. package/dist/rate-limit/plugin.js.map +1 -0
  31. package/dist/rate-limit/store.d.ts +51 -0
  32. package/dist/rate-limit/store.js +59 -0
  33. package/dist/rate-limit/store.js.map +1 -0
  34. package/dist/trace/client.d.ts +20 -0
  35. package/dist/trace/client.js +21 -0
  36. package/dist/trace/client.js.map +1 -0
  37. package/dist/trace/errors.d.ts +31 -0
  38. package/dist/trace/errors.js +37 -0
  39. package/dist/trace/errors.js.map +1 -0
  40. package/dist/trace/index.d.ts +15 -4
  41. package/dist/trace/index.js +3 -6
  42. package/dist/trace/index.js.map +1 -1
  43. package/dist/trace/queries.d.ts +59 -0
  44. package/dist/trace/queries.js +147 -0
  45. package/dist/trace/queries.js.map +1 -0
  46. package/dist/trace/types.d.ts +72 -0
  47. package/dist/trace/types.js +14 -0
  48. package/dist/trace/types.js.map +1 -0
  49. package/package.json +54 -8
  50. package/prisma/fragments/trace.meta.json +4 -0
  51. package/prisma/fragments/trace.prisma +87 -0
  52. package/prisma/migrations/0001_app_kit_trace_init/migration.sql +91 -0
  53. package/prisma/migrations/manifest.json +8 -0
@@ -0,0 +1,362 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import fp from "fastify-plugin";
3
+ import fastifyCookie from "@fastify/cookie";
4
+ import { createOidcClient } from "./oidc-client.js";
5
+ import { generatePkce, randomState } from "./pkce.js";
6
+ import { generateRefreshToken, hashRefreshToken, signAccessToken, verifyAccessToken } from "./tokens.js";
7
+ const DEFAULT_ROUTE_PREFIX = "/auth";
8
+ const DEFAULT_ACCESS_TOKEN_TTL_SECONDS = 900;
9
+ const DEFAULT_REFRESH_TOKEN_TTL_SECONDS = 2_592_000;
10
+ const DEFAULT_LOGIN_ATTEMPT_TTL_SECONDS = 600;
11
+ const REQUIRED_STRING_OPTIONS = [
12
+ "clientId",
13
+ "clientSecret",
14
+ "issuer",
15
+ "redirectUri",
16
+ "appId",
17
+ "postLoginRedirect",
18
+ "accessTokenSecret",
19
+ ];
20
+ function assertRequiredOptions(options) {
21
+ for (const key of REQUIRED_STRING_OPTIONS) {
22
+ const value = options[key];
23
+ if (typeof value !== "string" || value.length === 0) {
24
+ throw new Error(`@flytedesk/app-kit/auth: missing required option "${key}"`);
25
+ }
26
+ }
27
+ if (options.cookieSecret == null ||
28
+ (Array.isArray(options.cookieSecret) ? options.cookieSecret.length === 0 : options.cookieSecret.length === 0)) {
29
+ throw new Error('@flytedesk/app-kit/auth: missing required option "cookieSecret"');
30
+ }
31
+ if (typeof options.secureCookies !== "boolean") {
32
+ throw new Error('@flytedesk/app-kit/auth: missing required option "secureCookies"');
33
+ }
34
+ if (!options.store) {
35
+ throw new Error('@flytedesk/app-kit/auth: missing required option "store"');
36
+ }
37
+ }
38
+ const flytedeskAuthPlugin = async (fastify, options) => {
39
+ assertRequiredOptions(options);
40
+ const routePrefix = options.routePrefix ?? DEFAULT_ROUTE_PREFIX;
41
+ const accessTokenTtlSeconds = options.accessTokenTtlSeconds ?? DEFAULT_ACCESS_TOKEN_TTL_SECONDS;
42
+ const refreshTokenTtlSeconds = options.refreshTokenTtlSeconds ?? DEFAULT_REFRESH_TOKEN_TTL_SECONDS;
43
+ const loginAttemptTtlSeconds = options.loginAttemptTtlSeconds ?? DEFAULT_LOGIN_ATTEMPT_TTL_SECONDS;
44
+ const store = options.store;
45
+ if (options.authorizationCacheTtlMs) {
46
+ fastify.log.warn({ authorizationCacheTtlMs: options.authorizationCacheTtlMs }, "@flytedesk/app-kit/auth: authorizationCacheTtlMs is set — flytedesk-id authorization " +
47
+ "checks will be cached instead of live. A role/permission revoked in flytedesk-id can " +
48
+ "keep working here for up to that long. Confirm this tradeoff is intentional.");
49
+ }
50
+ const oidc = createOidcClient({
51
+ issuer: options.issuer,
52
+ clientId: options.clientId,
53
+ clientSecret: options.clientSecret,
54
+ redirectUri: options.redirectUri,
55
+ });
56
+ await fastify.register(fastifyCookie, { secret: options.cookieSecret });
57
+ fastify.decorateRequest("authUser", undefined);
58
+ // __Host- requires Secure, no Domain attribute, and Path=/ — so the prefix (and the
59
+ // Path=/ it forces) is only used when options.secureCookies says this is actually an
60
+ // HTTPS deployment. Plain, routePrefix-scoped cookies are used for local HTTP dev,
61
+ // where __Host- would just make the browser silently refuse to set the cookie.
62
+ const useHostPrefix = options.secureCookies;
63
+ const refreshCookieName = useHostPrefix ? "__Host-fd_refresh" : "fd_refresh";
64
+ const loginCookieName = useHostPrefix ? "__Host-fd_login_attempt" : "fd_login_attempt";
65
+ const cookiePath = useHostPrefix ? "/" : routePrefix;
66
+ function refreshCookieOptions(maxAgeSeconds) {
67
+ return {
68
+ httpOnly: true,
69
+ sameSite: "lax",
70
+ secure: options.secureCookies,
71
+ path: cookiePath,
72
+ maxAge: maxAgeSeconds,
73
+ };
74
+ }
75
+ const authorizationCache = new Map();
76
+ /**
77
+ * Live per-request by default: resolves `request.authUser` from the access token's
78
+ * `sid` by looking up the IdpSession, refreshing the IdP token pair if it's near
79
+ * expiry, then calling flytedesk-id's authorization endpoint fresh — see
80
+ * FlytedeskAuthOptions.authorizationCacheTtlMs for the one opt-in exception. Never
81
+ * throws: any failure just means no AuthedUser gets attached.
82
+ */
83
+ async function loadAuthedUser(claims) {
84
+ const session = await store.findIdpSession(claims.sid);
85
+ if (!session || session.subject !== claims.sub)
86
+ return undefined;
87
+ let idpAccessToken = session.idpAccessToken;
88
+ const EXPIRY_SKEW_MS = 5_000;
89
+ if (session.idpAccessTokenExpiresAt.getTime() - EXPIRY_SKEW_MS < Date.now()) {
90
+ if (!session.idpRefreshToken)
91
+ return undefined; // nothing left to refresh with — force re-login
92
+ try {
93
+ const refreshed = await oidc.refreshIdpTokens(session.idpRefreshToken);
94
+ idpAccessToken = refreshed.access_token;
95
+ await store.updateIdpSession(session.id, {
96
+ idpAccessToken: refreshed.access_token,
97
+ idpAccessTokenExpiresAt: new Date(Date.now() + refreshed.expires_in * 1000),
98
+ // flytedesk-id rotates refresh tokens on every use — must persist the new one.
99
+ idpRefreshToken: refreshed.refresh_token ?? session.idpRefreshToken,
100
+ });
101
+ }
102
+ catch {
103
+ return undefined; // flytedesk-id refresh token revoked/expired — force re-login
104
+ }
105
+ }
106
+ let authorization;
107
+ if (options.authorizationCacheTtlMs) {
108
+ const cached = authorizationCache.get(claims.sid);
109
+ if (cached && cached.expiresAt > Date.now())
110
+ authorization = cached.value;
111
+ }
112
+ if (!authorization) {
113
+ try {
114
+ authorization = await oidc.fetchAuthorization(idpAccessToken, options.appId);
115
+ }
116
+ catch {
117
+ return undefined;
118
+ }
119
+ if (options.authorizationCacheTtlMs) {
120
+ authorizationCache.set(claims.sid, {
121
+ value: authorization,
122
+ expiresAt: Date.now() + options.authorizationCacheTtlMs,
123
+ });
124
+ }
125
+ }
126
+ // flytedesk-id's permission keys are app-prefixed ("<appId>:plan.view") so they
127
+ // never collide across apps in its catalog — strip the prefix so the consuming
128
+ // app's own permission checks don't have to know about it.
129
+ const prefix = `${options.appId}:`;
130
+ return {
131
+ id: session.subject,
132
+ email: session.email,
133
+ name: session.name,
134
+ sessionId: session.id,
135
+ roles: authorization.roles,
136
+ permissions: authorization.permissions.map((p) => (p.startsWith(prefix) ? p.slice(prefix.length) : p)),
137
+ };
138
+ }
139
+ // onRequest, not preHandler: this needs to run before any route-level preHandler
140
+ // (requireAuth/requirePermission included) so request.authUser is already resolved
141
+ // by the time those run.
142
+ fastify.addHook("onRequest", async (request) => {
143
+ const header = request.headers.authorization;
144
+ if (!header?.startsWith("Bearer "))
145
+ return;
146
+ try {
147
+ const claims = await verifyAccessToken(options.accessTokenSecret, header.slice(7));
148
+ const user = await loadAuthedUser(claims);
149
+ if (user)
150
+ request.authUser = user;
151
+ }
152
+ catch {
153
+ // Invalid/expired access token: leave request.authUser unset; requireAuth 401s.
154
+ }
155
+ });
156
+ async function issueSession(request, reply, userId, idpSessionId, familyId) {
157
+ const { token: refreshToken, hash } = generateRefreshToken();
158
+ const expiresAt = new Date(Date.now() + refreshTokenTtlSeconds * 1000);
159
+ await store.createRefreshToken({
160
+ id: randomUUID(),
161
+ familyId,
162
+ userId,
163
+ tokenHash: hash,
164
+ expiresAt,
165
+ idpSessionId,
166
+ userAgent: request.headers["user-agent"] ?? null,
167
+ ip: request.ip,
168
+ });
169
+ const { token: accessToken, expiresAt: accessExpiresAt } = await signAccessToken(options.accessTokenSecret, accessTokenTtlSeconds, userId, idpSessionId);
170
+ reply.setCookie(refreshCookieName, refreshToken, refreshCookieOptions(refreshTokenTtlSeconds));
171
+ return { accessToken, accessTokenExpiresAt: accessExpiresAt.toISOString() };
172
+ }
173
+ /**
174
+ * GET {routePrefix}/login — starts the flytedesk-id sign-in. A plain redirect meant
175
+ * to be hit via full-page navigation, not fetch/XHR. Generates a PKCE verifier/
176
+ * challenge (S256 — flytedesk-id makes PKCE mandatory) and a CSRF `state`, stashes
177
+ * both in a short-lived signed cookie tied to this browser, then redirects to
178
+ * flytedesk-id's own /auth endpoint.
179
+ */
180
+ fastify.get(`${routePrefix}/login`, async (request, reply) => {
181
+ const { verifier, challenge } = generatePkce();
182
+ const state = randomState();
183
+ reply.setCookie(loginCookieName, JSON.stringify({ verifier, state }), {
184
+ httpOnly: true,
185
+ sameSite: "lax",
186
+ secure: options.secureCookies,
187
+ path: cookiePath,
188
+ maxAge: loginAttemptTtlSeconds,
189
+ signed: true,
190
+ });
191
+ return reply.redirect(oidc.buildAuthorizeUrl({ state, challenge }));
192
+ });
193
+ /**
194
+ * GET {routePrefix}/callback — flytedesk-id's redirect_uri for this client. Validates
195
+ * `state`, exchanges `code` + the stashed PKCE verifier at flytedesk-id's /token
196
+ * (Basic auth — the one place client_secret is used), verifies the returned
197
+ * id_token's signature via flytedesk-id's JWKS, fetches profile claims from
198
+ * userinfo, then mints this service's own session.
199
+ */
200
+ fastify.get(`${routePrefix}/callback`, async (request, reply) => {
201
+ const { code, state, error } = request.query;
202
+ const raw = request.cookies[loginCookieName];
203
+ reply.clearCookie(loginCookieName, { path: cookiePath });
204
+ if (error) {
205
+ return reply
206
+ .code(400)
207
+ .send({ error: `flytedesk-id sign-in failed: ${error}`, code: "idp_error" });
208
+ }
209
+ if (!code || !state || !raw) {
210
+ return reply.code(400).send({
211
+ error: "Missing code/state, or the login attempt expired",
212
+ code: "invalid_callback",
213
+ });
214
+ }
215
+ const unsigned = request.unsignCookie(raw);
216
+ if (!unsigned.valid || !unsigned.value) {
217
+ return reply.code(400).send({
218
+ error: "Login attempt cookie failed verification",
219
+ code: "invalid_login_cookie",
220
+ });
221
+ }
222
+ let stash;
223
+ try {
224
+ stash = JSON.parse(unsigned.value);
225
+ }
226
+ catch {
227
+ return reply.code(400).send({ error: "Corrupt login attempt", code: "invalid_login_cookie" });
228
+ }
229
+ if (stash.state !== state) {
230
+ return reply.code(400).send({ error: "State mismatch", code: "invalid_state" });
231
+ }
232
+ try {
233
+ const tokens = await oidc.exchangeCodeForTokens(code, stash.verifier);
234
+ // Confirms WHO signed in (sub) via a local signature check — aud/iss are
235
+ // verified inside verifyIdToken against options.clientId/options.issuer.
236
+ const idToken = await oidc.verifyIdToken(tokens.id_token);
237
+ const profile = await oidc.fetchUserinfo(tokens.access_token);
238
+ if (profile.sub !== idToken.sub) {
239
+ return reply.code(401).send({
240
+ error: "flytedesk-id id_token/userinfo subject mismatch",
241
+ code: "subject_mismatch",
242
+ });
243
+ }
244
+ if (!profile.emailVerified) {
245
+ return reply.code(401).send({
246
+ error: "flytedesk-id account email is not verified",
247
+ code: "email_not_verified",
248
+ });
249
+ }
250
+ if (options.onSignIn) {
251
+ await options.onSignIn(profile, request);
252
+ }
253
+ const name = profile.name ?? profile.email.split("@")[0];
254
+ const idpSession = await store.createIdpSession({
255
+ subject: profile.sub,
256
+ name,
257
+ email: profile.email,
258
+ idpAccessToken: tokens.access_token,
259
+ idpAccessTokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1000),
260
+ idpRefreshToken: tokens.refresh_token ?? null,
261
+ });
262
+ // A fresh family for every login — reuse detection (see POST /refresh below)
263
+ // only ever revokes the chain descended from one login, never sibling sessions.
264
+ const familyId = randomUUID();
265
+ await issueSession(request, reply, profile.sub, idpSession.id, familyId);
266
+ return reply.redirect(options.postLoginRedirect);
267
+ }
268
+ catch (err) {
269
+ request.log.error({ err }, "flytedesk-id callback failed");
270
+ return reply
271
+ .code(502)
272
+ .send({ error: "Sign-in with flytedesk-id failed", code: "idp_exchange_failed" });
273
+ }
274
+ });
275
+ /**
276
+ * POST {routePrefix}/refresh — no body, reads the refresh cookie. Rotates it on every
277
+ * successful call. A hash match on a row that's already revoked means this exact
278
+ * token was replayed (its legitimate rotation already consumed it) — that's treated
279
+ * as a security event: the whole family is revoked, not just this row, since every
280
+ * token downstream of the replayed one must be assumed compromised too.
281
+ *
282
+ * The claim (store.claimRefreshTokenForRotation) happens BEFORE the expiry check and
283
+ * is what actually detects reuse — not a `stored.revokedAt` read, which would leave a
284
+ * window between two concurrent requests both observing "not yet revoked" before
285
+ * either write lands. See that method's doc comment in ./types.ts.
286
+ */
287
+ fastify.post(`${routePrefix}/refresh`, async (request, reply) => {
288
+ const token = request.cookies[refreshCookieName];
289
+ if (!token) {
290
+ return reply.code(401).send({ error: "No refresh token", code: "unauthenticated" });
291
+ }
292
+ const hash = hashRefreshToken(token);
293
+ const stored = await store.findRefreshTokenByHash(hash);
294
+ if (!stored) {
295
+ reply.clearCookie(refreshCookieName, { path: cookiePath });
296
+ return reply.code(401).send({ error: "Refresh token not recognized", code: "unauthenticated" });
297
+ }
298
+ const claimed = await store.claimRefreshTokenForRotation(stored.id);
299
+ if (!claimed) {
300
+ // Already revoked by the time this call's UPDATE ran — either a genuine prior
301
+ // revocation or a concurrent /refresh call that won the race. Both are reuse.
302
+ request.log.warn({ familyId: stored.familyId, userId: stored.userId }, "@flytedesk/app-kit/auth: refresh token reuse detected — revoking token family");
303
+ await store.revokeFamily(stored.familyId);
304
+ reply.clearCookie(refreshCookieName, { path: cookiePath });
305
+ return reply.code(401).send({ error: "Refresh token reuse detected", code: "token_reuse_detected" });
306
+ }
307
+ if (stored.expiresAt < new Date()) {
308
+ // Claimed (now revoked, so it can never be presented again) but past its TTL —
309
+ // an expired token, not a reuse event.
310
+ reply.clearCookie(refreshCookieName, { path: cookiePath });
311
+ return reply.code(401).send({ error: "Refresh token expired", code: "unauthenticated" });
312
+ }
313
+ // Rotate: the claim above already revoked the used token; issue a fresh one in the
314
+ // same family — limits replay if a cookie leaks, and idpSessionId carries forward
315
+ // unchanged so this session's link to its flytedesk-id token pair survives the
316
+ // rotation.
317
+ const next = generateRefreshToken();
318
+ const expiresAt = new Date(Date.now() + refreshTokenTtlSeconds * 1000);
319
+ await store.createRefreshToken({
320
+ id: randomUUID(),
321
+ familyId: stored.familyId,
322
+ userId: stored.userId,
323
+ tokenHash: next.hash,
324
+ expiresAt,
325
+ idpSessionId: stored.idpSessionId,
326
+ userAgent: request.headers["user-agent"] ?? null,
327
+ ip: request.ip,
328
+ });
329
+ const { token: accessToken, expiresAt: accessExpiresAt } = await signAccessToken(options.accessTokenSecret, accessTokenTtlSeconds, stored.userId, stored.idpSessionId);
330
+ reply.setCookie(refreshCookieName, next.token, refreshCookieOptions(refreshTokenTtlSeconds));
331
+ return reply.send({ accessToken, accessTokenExpiresAt: accessExpiresAt.toISOString() });
332
+ });
333
+ /**
334
+ * POST {routePrefix}/logout — revokes the whole refresh-token family (not just the
335
+ * presented cookie) and the IdpSession row, then best-effort revokes the IdP-side
336
+ * refresh token if flytedesk-id's discovery document advertises a revocation
337
+ * endpoint. flytedesk-id currently documents NOT having one, so that step is
338
+ * expected to no-op — it must never throw or block local logout either way, since
339
+ * the local session is what actually keeps a signed-out browser locked out.
340
+ */
341
+ fastify.post(`${routePrefix}/logout`, async (request, reply) => {
342
+ const token = request.cookies[refreshCookieName];
343
+ if (token) {
344
+ const hash = hashRefreshToken(token);
345
+ const stored = await store.findRefreshTokenByHash(hash);
346
+ if (stored) {
347
+ await store.revokeFamily(stored.familyId);
348
+ const idpSession = await store.findIdpSession(stored.idpSessionId);
349
+ if (idpSession?.idpRefreshToken) {
350
+ await oidc.revokeIdpToken(idpSession.idpRefreshToken).catch(() => undefined);
351
+ }
352
+ await store.deleteIdpSession(stored.idpSessionId).catch(() => undefined);
353
+ }
354
+ }
355
+ reply.clearCookie(refreshCookieName, { path: cookiePath });
356
+ return reply.send({ loggedOut: true });
357
+ });
358
+ };
359
+ export const flytedeskAuth = fp(flytedeskAuthPlugin, {
360
+ name: "@flytedesk/app-kit/auth",
361
+ });
362
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../../src/auth/plugin.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ export interface AccessTokenClaims {
2
+ /** flytedesk-id's `sub` — the token carries only the user id, never permissions. */
3
+ sub: string;
4
+ /** The IdpSession row id this session is backed by — loadAuthedUser (./plugin.ts)
5
+ * needs it to call flytedesk-id's live authorization endpoint on every request. */
6
+ sid: string;
7
+ }
8
+ /**
9
+ * Bearer JWT this service issues to itself. Carries only `sub`/`sid` — never the
10
+ * permission set, or a role revoked in flytedesk-id would keep working here until the
11
+ * token expired instead of on the next authorization check.
12
+ */
13
+ export declare function signAccessToken(secret: string, ttlSeconds: number, userId: string, sessionId: string): Promise<{
14
+ token: string;
15
+ expiresAt: Date;
16
+ }>;
17
+ export declare function verifyAccessToken(secret: string, token: string): Promise<AccessTokenClaims>;
18
+ /** Opaque refresh token: random bytes handed to the browser as a cookie, only the hash
19
+ * ever persisted server-side (via AuthStore#createRefreshToken). */
20
+ export declare function generateRefreshToken(): {
21
+ token: string;
22
+ hash: string;
23
+ };
24
+ export declare function hashRefreshToken(token: string): string;
@@ -0,0 +1,35 @@
1
+ import { randomBytes, createHash } from "node:crypto";
2
+ import { SignJWT, jwtVerify } from "jose";
3
+ /**
4
+ * Bearer JWT this service issues to itself. Carries only `sub`/`sid` — never the
5
+ * permission set, or a role revoked in flytedesk-id would keep working here until the
6
+ * token expired instead of on the next authorization check.
7
+ */
8
+ export async function signAccessToken(secret, ttlSeconds, userId, sessionId) {
9
+ const key = new TextEncoder().encode(secret);
10
+ const expiresAt = new Date(Date.now() + ttlSeconds * 1000);
11
+ const token = await new SignJWT({ sub: userId, sid: sessionId })
12
+ .setProtectedHeader({ alg: "HS256" })
13
+ .setIssuedAt()
14
+ .setExpirationTime(Math.floor(expiresAt.getTime() / 1000))
15
+ .sign(key);
16
+ return { token, expiresAt };
17
+ }
18
+ export async function verifyAccessToken(secret, token) {
19
+ const key = new TextEncoder().encode(secret);
20
+ const { payload } = await jwtVerify(token, key, { algorithms: ["HS256"] });
21
+ if (typeof payload.sub !== "string" || typeof payload.sid !== "string") {
22
+ throw new Error("access token is missing the required sub/sid claims");
23
+ }
24
+ return { sub: payload.sub, sid: payload.sid };
25
+ }
26
+ /** Opaque refresh token: random bytes handed to the browser as a cookie, only the hash
27
+ * ever persisted server-side (via AuthStore#createRefreshToken). */
28
+ export function generateRefreshToken() {
29
+ const token = randomBytes(48).toString("base64url");
30
+ return { token, hash: hashRefreshToken(token) };
31
+ }
32
+ export function hashRefreshToken(token) {
33
+ return createHash("sha256").update(token).digest("hex");
34
+ }
35
+ //# sourceMappingURL=tokens.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../../src/auth/tokens.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAU1C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAAc,EACd,UAAkB,EAClB,MAAc,EACd,SAAiB;IAEjB,MAAM,GAAG,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,UAAU,GAAG,IAAI,CAAC,CAAC;IAC3D,MAAM,KAAK,GAAG,MAAM,IAAI,OAAO,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC;SAC7D,kBAAkB,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;SACpC,WAAW,EAAE;SACb,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,CAAC;SACzD,IAAI,CAAC,GAAG,CAAC,CAAC;IACb,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC;AAC9B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAAc,EAAE,KAAa;IACnE,MAAM,GAAG,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC7C,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC3E,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;QACvE,MAAM,IAAI,KAAK,CAAC,qDAAqD,CAAC,CAAC;IACzE,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;AAChD,CAAC;AAED;qEACqE;AACrE,MAAM,UAAU,oBAAoB;IAClC,MAAM,KAAK,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;IACpD,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1D,CAAC"}
@@ -0,0 +1,156 @@
1
+ import type { FastifyRequest } from "fastify";
2
+ /**
3
+ * Profile claims returned by flytedesk-id's userinfo endpoint (`GET {issuer}/me`).
4
+ * Deliberately does NOT include roles/permissions — those come from flytedesk-id's
5
+ * per-app authorization endpoint (see loadAuthedUser in ./plugin.ts), fetched live on
6
+ * every request rather than baked into a token, so a revoked role takes effect
7
+ * immediately instead of surviving until some token/cache expires.
8
+ */
9
+ export interface UserinfoClaims {
10
+ /** flytedesk-id's subject id — the only user id any consuming app should key on. */
11
+ sub: string;
12
+ email: string;
13
+ emailVerified: boolean;
14
+ name?: string;
15
+ }
16
+ export interface IdpRole {
17
+ id: string;
18
+ key: string;
19
+ name: string;
20
+ }
21
+ /** What ends up on `request.authUser` for every authenticated request. */
22
+ export interface AuthedUser {
23
+ id: string;
24
+ email: string;
25
+ name: string;
26
+ roles: IdpRole[];
27
+ /** Already stripped of the `${appId}:` prefix flytedesk-id's catalog uses to keep
28
+ * permission keys collision-free across apps — see loadAuthedUser in ./plugin.ts. */
29
+ permissions: string[];
30
+ /** The IdpSession row id backing this session — the same value AuthStore rows key
31
+ * refresh tokens to via `idpSessionId`. */
32
+ sessionId: string;
33
+ }
34
+ export interface RefreshTokenRow {
35
+ id: string;
36
+ /** Shared by every refresh token descended from the same login, via rotation.
37
+ * Revoking a family (see revokeFamily below) is how reuse detection invalidates an
38
+ * entire compromised chain in one call, not just the one token that got replayed. */
39
+ familyId: string;
40
+ userId: string;
41
+ idpSessionId: string;
42
+ revokedAt: Date | null;
43
+ expiresAt: Date;
44
+ }
45
+ export interface CreateRefreshTokenInput {
46
+ id: string;
47
+ familyId: string;
48
+ userId: string;
49
+ /** sha256 hex digest of the opaque refresh token — the raw token itself is never
50
+ * persisted, only ever handed to the browser as a cookie. */
51
+ tokenHash: string;
52
+ expiresAt: Date;
53
+ idpSessionId: string;
54
+ userAgent: string | null;
55
+ ip: string;
56
+ }
57
+ export interface IdpSessionRow {
58
+ id: string;
59
+ subject: string;
60
+ name: string;
61
+ email: string;
62
+ idpAccessToken: string;
63
+ idpAccessTokenExpiresAt: Date;
64
+ idpRefreshToken: string | null;
65
+ }
66
+ export interface CreateIdpSessionInput {
67
+ subject: string;
68
+ name: string;
69
+ email: string;
70
+ idpAccessToken: string;
71
+ idpAccessTokenExpiresAt: Date;
72
+ idpRefreshToken: string | null;
73
+ }
74
+ export interface UpdateIdpSessionInput {
75
+ idpAccessToken?: string;
76
+ idpAccessTokenExpiresAt?: Date;
77
+ idpRefreshToken?: string | null;
78
+ }
79
+ /**
80
+ * Storage seam this package needs, and nothing more — kept ORM-agnostic (no Prisma
81
+ * import here) so a consumer can back it with whatever their own DB layer already is.
82
+ * The two "families" of rows below mirror media-planner's RefreshToken/IdpSession
83
+ * tables, which this module generalizes from (see DEC-38).
84
+ */
85
+ export interface AuthStore {
86
+ createRefreshToken(row: CreateRefreshTokenInput): Promise<void>;
87
+ findRefreshTokenByHash(hash: string): Promise<RefreshTokenRow | null>;
88
+ /**
89
+ * MUST be implemented as a single atomic conditional update — e.g.
90
+ * `UPDATE refresh_tokens SET revoked_at = now() WHERE id = $1 AND revoked_at IS NULL`
91
+ * — returning whether THIS call was the one that actually revoked the row (true), as
92
+ * opposed to finding it already revoked (false, by a prior request or a concurrent
93
+ * one that won the race).
94
+ *
95
+ * This is the fix for a real race in the plugin's original find-then-revoke logic:
96
+ * two /refresh calls presenting the same still-valid token at the same instant could
97
+ * both read `revokedAt: null` before either write landed, both rotate successfully,
98
+ * and the replay would go undetected. A plain `find -> check -> revoke` sequence in
99
+ * the CALLER can't close this gap no matter how carefully it's written — the
100
+ * atomicity has to live in this one storage operation. Never implement this as a
101
+ * separate read followed by an unconditional write.
102
+ */
103
+ claimRefreshTokenForRotation(id: string): Promise<boolean>;
104
+ /** Reuse-detection path: revoke every row sharing this familyId. */
105
+ revokeFamily(familyId: string): Promise<void>;
106
+ createIdpSession(row: CreateIdpSessionInput): Promise<{
107
+ id: string;
108
+ }>;
109
+ findIdpSession(id: string): Promise<IdpSessionRow | null>;
110
+ updateIdpSession(id: string, patch: UpdateIdpSessionInput): Promise<void>;
111
+ deleteIdpSession(id: string): Promise<void>;
112
+ }
113
+ export interface FlytedeskAuthOptions {
114
+ /** This app's OAuth client_id, as registered with flytedesk-id. */
115
+ clientId: string;
116
+ /** Never sent to the browser — used once per callback, at the /token exchange. */
117
+ clientSecret: string;
118
+ /** flytedesk-id's base URL, e.g. "https://id.flytedesk.com". */
119
+ issuer: string;
120
+ redirectUri: string;
121
+ /** flytedesk-id's id for THIS app's authorization catalog — distinct from clientId:
122
+ * it's what `GET {issuer}/apps/{appId}/authorization` and the permission-key prefix
123
+ * (`${appId}:...`) key on. */
124
+ appId: string;
125
+ /** Where GET /callback redirects to once a session has been issued. */
126
+ postLoginRedirect: string;
127
+ /** Base path the four routes are mounted under. Default "/auth". */
128
+ routePrefix?: string;
129
+ /** Passed straight through to @fastify/cookie, which this plugin registers itself. */
130
+ cookieSecret: string | string[];
131
+ /** Gates the __Host- cookie prefix (requires Secure — see ./plugin.ts). Set false only
132
+ * for local HTTP dev. */
133
+ secureCookies: boolean;
134
+ /** HS256 signing secret for this service's own short-lived access token. Never shared
135
+ * with flytedesk-id or any other app. */
136
+ accessTokenSecret: string;
137
+ /** Default 900 (15 minutes). */
138
+ accessTokenTtlSeconds?: number;
139
+ /** Default 2_592_000 (30 days). */
140
+ refreshTokenTtlSeconds?: number;
141
+ /** Default 600 (10 minutes). */
142
+ loginAttemptTtlSeconds?: number;
143
+ /**
144
+ * Escape hatch: cache the flytedesk-id authorization lookup for this many ms instead
145
+ * of calling it live on every request. Default undefined/0 — live, uncached. Setting
146
+ * this weakens the "a revoked role takes effect immediately" guarantee by up to this
147
+ * many ms, so the plugin logs a startup warning naming the value rather than letting
148
+ * it take effect silently.
149
+ */
150
+ authorizationCacheTtlMs?: number;
151
+ store: AuthStore;
152
+ /** Called once per successful /callback, after profile verification and before the
153
+ * session is issued — the hook for a consumer's own app-specific bookkeeping (e.g.
154
+ * upserting a local profile row, checking a local suspension flag). */
155
+ onSignIn?: (profile: UserinfoClaims, request: FastifyRequest) => Promise<void> | void;
156
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/auth/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(argv: string[]): Promise<void>;