@astralarium/repo-auth 0.0.0-stage → 0.2.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.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -2
  3. package/dist/client.d.ts +2173 -0
  4. package/dist/client.js +203 -0
  5. package/dist/dev-sign-in/client.d.ts +21 -0
  6. package/dist/dev-sign-in/client.js +17 -0
  7. package/dist/dev-sign-in/endpoint.d.ts +2 -0
  8. package/dist/dev-sign-in/endpoint.js +2 -0
  9. package/dist/dev-sign-in/index.d.ts +4 -0
  10. package/dist/dev-sign-in/index.js +4 -0
  11. package/dist/dev-sign-in/provider-user.d.ts +30 -0
  12. package/dist/dev-sign-in/provider-user.js +55 -0
  13. package/dist/dev-sign-in/sign-in.d.ts +62 -0
  14. package/dist/dev-sign-in/sign-in.js +116 -0
  15. package/dist/index.server.d.ts +645 -0
  16. package/dist/index.server.js +307 -0
  17. package/dist/providers/google.d.ts +8 -0
  18. package/dist/providers/google.js +11 -0
  19. package/dist/providers/index.d.ts +3 -0
  20. package/dist/providers/index.js +3 -0
  21. package/dist/providers/provider.d.ts +135 -0
  22. package/dist/providers/provider.js +86 -0
  23. package/dist/request.d.ts +42 -0
  24. package/dist/request.js +75 -0
  25. package/dist/schema.d.ts +6 -0
  26. package/dist/schema.js +23 -0
  27. package/dist/start/index.d.ts +6 -0
  28. package/dist/start/index.js +6 -0
  29. package/dist/start/index.server.d.ts +28 -0
  30. package/dist/start/index.server.js +29 -0
  31. package/dist/start/middleware.d.ts +33 -0
  32. package/dist/start/middleware.js +70 -0
  33. package/dist/start/return-path.d.ts +16 -0
  34. package/dist/start/return-path.js +32 -0
  35. package/dist/start/viewer.d.ts +8 -0
  36. package/dist/start/viewer.js +15 -0
  37. package/dist/testkit/index.d.ts +131 -0
  38. package/dist/testkit/index.js +238 -0
  39. package/dist/testkit/json-post.d.ts +8 -0
  40. package/dist/testkit/json-post.js +12 -0
  41. package/dist/testkit/oidc.d.ts +35 -0
  42. package/dist/testkit/oidc.js +154 -0
  43. package/dist/testkit/suites.d.ts +126 -0
  44. package/dist/testkit/suites.js +255 -0
  45. package/dist/types.d.ts +7 -0
  46. package/dist/types.js +1 -0
  47. package/package.json +86 -3
  48. package/sql/jobs/schema.sql +19 -0
  49. package/sql/schema.sql +97 -0
  50. package/sql/scrub.sql +6 -0
package/dist/client.js ADDED
@@ -0,0 +1,203 @@
1
+ /** Browser half of shell-auth: the session store, auth calls and OAuth popups. */
2
+ import { log } from "@astralarium/repo-telemetry";
3
+ import { getGlobalBroadcastChannel } from "better-auth/client";
4
+ import { InferServerPlugin } from "better-auth/client/plugins";
5
+ import { createAuthClient } from "better-auth/react";
6
+ /**
7
+ * Posts Better Auth's session broadcast: every other tab refetches its
8
+ * session. Better Auth posts it itself on sign-out and user updates. The
9
+ * posting tab refetches on its own.
10
+ */
11
+ function broadcastSession() {
12
+ getGlobalBroadcastChannel().post({
13
+ event: "session",
14
+ data: {
15
+ trigger: "getSession"
16
+ },
17
+ clientId: crypto.randomUUID()
18
+ });
19
+ }
20
+ /** Opens `url`, a page starting a provider's OAuth redirect, in a popup
21
+ * centered over this window. */
22
+ export function openAuthPopup(url) {
23
+ const width = Math.min(400, screen.availWidth * 0.8, window.innerWidth);
24
+ const height = Math.min(600, screen.availHeight * 0.8, window.innerHeight);
25
+ const left = window.screenLeft + (window.innerWidth - width) / 2;
26
+ const top = window.screenTop + (window.innerHeight - height) / 2;
27
+ window.open(url, "_blank", `popup,width=${width},height=${height},left=${left},top=${top}`);
28
+ }
29
+ /** Ends an OAuth popup on its landing page: the other tabs, opener
30
+ * included, refresh their session; the popup closes. */
31
+ export function finishAuthPopup() {
32
+ broadcastSession();
33
+ window.close();
34
+ }
35
+ /** The error redirect's params in `search`; non-string values drop. */
36
+ export function parseAuthErrorSearch(search) {
37
+ const {
38
+ error,
39
+ error_description
40
+ } = search;
41
+ return {
42
+ ...(typeof error === "string" ? {
43
+ error
44
+ } : {}),
45
+ ...(typeof error_description === "string" ? {
46
+ error_description
47
+ } : {})
48
+ };
49
+ }
50
+ /** A response that started a session: Better Auth answers sign-in and
51
+ * sign-up with the new session's `token` and its `user`. */
52
+ function startsSession(data) {
53
+ return typeof data === "object" && data !== null && "token" in data && typeof data.token === "string" && "user" in data && typeof data.user === "object" && data.user !== null;
54
+ }
55
+ /** Broadcasts after a request starts a session. Better Auth's own
56
+ * listener refetches this tab; this one tells the others. */
57
+ const sessionBroadcast = {
58
+ id: "session-broadcast",
59
+ fetchPlugins: [{
60
+ id: "session-broadcast",
61
+ name: "session-broadcast",
62
+ hooks: {
63
+ onSuccess({
64
+ data
65
+ }) {
66
+ if (startsSession(data)) broadcastSession();
67
+ }
68
+ }
69
+ }]
70
+ };
71
+ /** Shared live session store and auth operations for `Auth`. */
72
+ export function createShellAuthClient() {
73
+ const authClient = createAuthClient({
74
+ plugins: [InferServerPlugin(), sessionBroadcast],
75
+ sessionOptions: {
76
+ refetchOnWindowFocus: true,
77
+ refetchWhenOffline: false
78
+ }
79
+ });
80
+ /** The client's live session. */
81
+ const sessionStore = authClient.$store.atoms.session;
82
+ /** Seeds the store with the page's dehydrated session. Better Auth types
83
+ * the seed with the base user merged in; the projection omits those
84
+ * fields. */
85
+ function hydrateSession(data) {
86
+ authClient.hydrateSession(data);
87
+ }
88
+ /**
89
+ * Reads the session fresh after a change made outside the auth client
90
+ * (a server function updating the user): past the cookie cache in this
91
+ * tab, then broadcast to the others.
92
+ */
93
+ async function refreshSession() {
94
+ await sessionStore.get().refetch({
95
+ query: {
96
+ disableCookieCache: true
97
+ }
98
+ });
99
+ broadcastSession();
100
+ }
101
+ /** The signed-in user's id, `null` signed out; `undefined` while the
102
+ * store awaits its first read. */
103
+ function readUserId() {
104
+ const {
105
+ data,
106
+ isPending
107
+ } = sessionStore.get();
108
+ return isPending ? undefined : data?.user.id ?? null;
109
+ }
110
+ /**
111
+ * Seeds the store with the page's `session` (`null` signed out), then
112
+ * calls `onChange` whenever the signed-in user changes: sign-in,
113
+ * sign-out, account switch, from this tab or another. An `undefined`
114
+ * session leaves the store unseeded; its first read sets the user. The
115
+ * seed and that first read fire nothing. Returns the unsubscribe.
116
+ */
117
+ function watchSession(session, onChange) {
118
+ if (session !== undefined) hydrateSession(session);
119
+ let userId = session === undefined ? readUserId() : session?.user.id ?? null;
120
+ return sessionStore.listen(() => {
121
+ const next = readUserId();
122
+ if (next === undefined || next === userId) return;
123
+ const known = userId !== undefined;
124
+ userId = next;
125
+ if (known) onChange();
126
+ });
127
+ }
128
+ /**
129
+ * Watches the signed-in user from the first loaded page under `routeId`,
130
+ * the layout route carrying `viewerRouteContext`; other pages read no
131
+ * session. A server-rendered page's session seeds the watch; one reached
132
+ * by client navigation carries none, and the store's first read sets the
133
+ * user. Each user change after clears `queryClient`'s queries and
134
+ * invalidates `router`, both holding the previous user's data; the
135
+ * route loaders refetch the queries they read, then the active queries
136
+ * still unread refetch: one fetch per query. Returns the unsubscribe.
137
+ */
138
+ function watchViewerRouter(router, queryClient, routeId) {
139
+ let stop = router.subscribe("onLoad", () => {
140
+ const match = router.state.matches.find(route => route.routeId === routeId);
141
+ if (!match) return;
142
+ stop();
143
+ const {
144
+ session
145
+ } = match.context;
146
+ stop = watchSession(session, () => {
147
+ for (const query of queryClient.getQueryCache().getAll()) query.reset();
148
+ const failed = exception => log.warn("auth.viewer_reload_failed", "Reloading the page for a new viewer failed", {
149
+ exception
150
+ });
151
+ void router.invalidate().catch(failed).then(() => queryClient.refetchQueries({
152
+ type: "active",
153
+ predicate: ({
154
+ state
155
+ }) => state.status === "pending" && state.fetchStatus === "idle"
156
+ })).catch(failed);
157
+ });
158
+ });
159
+ return () => stop();
160
+ }
161
+ /** POSTs `body` to the auth endpoint at `path`, a form with `form`;
162
+ * resolves to its error. Endpoint methods (`signIn.social`) type per
163
+ * app, unresolved here; plugin endpoints (`devProviderSignIn`) call
164
+ * through it. */
165
+ async function post(path, body, {
166
+ form = false
167
+ } = {}) {
168
+ const {
169
+ error
170
+ } = await authClient.$fetch(path, {
171
+ method: "POST",
172
+ body,
173
+ ...(form && {
174
+ headers: {
175
+ "content-type": "application/x-www-form-urlencoded"
176
+ }
177
+ })
178
+ });
179
+ return error ?? undefined;
180
+ }
181
+ /** Starts `provider`'s OAuth redirect, landing on `callbackURL`.
182
+ * Resolves to the failure detail when the redirect fails to start. */
183
+ async function signInWithProvider(provider, callbackURL) {
184
+ try {
185
+ const error = await post("/sign-in/social", {
186
+ provider,
187
+ callbackURL
188
+ });
189
+ return error ? error.message ?? error.statusText : undefined;
190
+ } catch (cause) {
191
+ return cause instanceof Error ? cause.message : String(cause);
192
+ }
193
+ }
194
+ return {
195
+ authClient,
196
+ sessionStore,
197
+ refreshSession,
198
+ watchSession,
199
+ watchViewerRouter,
200
+ post,
201
+ signInWithProvider
202
+ };
203
+ }
@@ -0,0 +1,21 @@
1
+ /** Dev builds' browser sign-in as provider accounts. */
2
+ import type { createShellAuthClient } from "../client.ts";
3
+ /** What dev sign-in calls of a shell auth client
4
+ * (`createShellAuthClient`). */
5
+ export interface DevSignInClient {
6
+ post: ReturnType<typeof createShellAuthClient>["post"];
7
+ sessionStore: {
8
+ get(): {
9
+ refetch(): Promise<unknown>;
10
+ };
11
+ };
12
+ }
13
+ /** Signs in through `client`'s dev sign-in (`devProviderSignIn`) as the
14
+ * provider account `accountId`, then reads the new session in this tab:
15
+ * `post` skips Better Auth's session signal; `sessionBroadcast` tells the
16
+ * others. Resolves to the error when sign-in fails. */
17
+ export declare function signInWithDevAccount({ post, sessionStore }: DevSignInClient, accountId: string): Promise<{
18
+ message?: string | undefined;
19
+ status: number;
20
+ statusText: string;
21
+ } | undefined>;
@@ -0,0 +1,17 @@
1
+ import { DEV_SIGN_IN_ENDPOINT } from "./endpoint.js";
2
+ /** Signs in through `client`'s dev sign-in (`devProviderSignIn`) as the
3
+ * provider account `accountId`, then reads the new session in this tab:
4
+ * `post` skips Better Auth's session signal; `sessionBroadcast` tells the
5
+ * others. Resolves to the error when sign-in fails. */
6
+ export async function signInWithDevAccount({
7
+ post,
8
+ sessionStore
9
+ }, accountId) {
10
+ const error = await post(DEV_SIGN_IN_ENDPOINT, {
11
+ accountId
12
+ }, {
13
+ form: true
14
+ });
15
+ if (!error) await sessionStore.get().refetch();
16
+ return error;
17
+ }
@@ -0,0 +1,2 @@
1
+ /** Dev sign-in's endpoint under `AUTH_BASE_PATH`. */
2
+ export declare const DEV_SIGN_IN_ENDPOINT = "/dev/sign-in";
@@ -0,0 +1,2 @@
1
+ /** Dev sign-in's endpoint under `AUTH_BASE_PATH`. */
2
+ export const DEV_SIGN_IN_ENDPOINT = "/dev/sign-in";
@@ -0,0 +1,4 @@
1
+ /** Dev builds' sign-in as provider accounts, and provider accounts
2
+ * resolved to users for seeds. */
3
+ export { type ContextAuth, ensureProviderUser, type ProviderUser, } from "./provider-user.ts";
4
+ export { type DevProviderSignIn, devProviderSignIn, type DevProviderSignInOptions, isDevSignIn, } from "./sign-in.ts";
@@ -0,0 +1,4 @@
1
+ /** Dev builds' sign-in as provider accounts, and provider accounts
2
+ * resolved to users for seeds. */
3
+ export { ensureProviderUser } from "./provider-user.js";
4
+ export { devProviderSignIn, isDevSignIn } from "./sign-in.js";
@@ -0,0 +1,30 @@
1
+ /** Provider accounts resolved to users through better-auth's adapter. */
2
+ import type { AuthContext } from "better-auth";
3
+ /** A provider account and the user it creates when new. */
4
+ export interface ProviderUser {
5
+ /** Provider id (`google`, `github`). */
6
+ providerId: string;
7
+ /** The provider's account id. */
8
+ accountId: string;
9
+ /** Name of a new user. */
10
+ name: string;
11
+ /** Email of a new user; a user holding it gains the account. */
12
+ email: string;
13
+ }
14
+ /** What a dev sign-in function reads of an auth instance: its context's
15
+ * `Keys`; `ensureProviderUser` reads `internalAdapter`. */
16
+ export interface ContextAuth<Keys extends keyof AuthContext = "internalAdapter"> {
17
+ $context: Promise<Pick<AuthContext, Keys>>;
18
+ }
19
+ /**
20
+ * The id of the user owning `providerId` account `accountId`, else of the
21
+ * user holding `email`, else of a new user (`name`, verified `email`); the
22
+ * latter two gain a token-less account. `email` must be trusted (the
23
+ * account's own claim): its holder gains the account. Rows go through
24
+ * better-auth's adapter, so the app's `databaseHooks` run. Concurrent
25
+ * calls for one new account resolve to one user. Queries run inside
26
+ * `withShellAuth`. Seeds call it outside any request: better-auth's
27
+ * `handleOAuthUserInfo`, which dev sign-in uses, takes an endpoint context
28
+ * and creates a session.
29
+ */
30
+ export declare function ensureProviderUser(auth: ContextAuth, { providerId, accountId, name, email }: ProviderUser): Promise<string>;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * The id of the user owning `providerId` account `accountId`, else of the
3
+ * user holding `email`, else of a new user (`name`, verified `email`); the
4
+ * latter two gain a token-less account. `email` must be trusted (the
5
+ * account's own claim): its holder gains the account. Rows go through
6
+ * better-auth's adapter, so the app's `databaseHooks` run. Concurrent
7
+ * calls for one new account resolve to one user. Queries run inside
8
+ * `withShellAuth`. Seeds call it outside any request: better-auth's
9
+ * `handleOAuthUserInfo`, which dev sign-in uses, takes an endpoint context
10
+ * and creates a session.
11
+ */
12
+ export async function ensureProviderUser(auth, {
13
+ providerId,
14
+ accountId,
15
+ name,
16
+ email
17
+ }) {
18
+ const {
19
+ internalAdapter
20
+ } = await auth.$context;
21
+ const owner = () => internalAdapter.findAccountOwnerByKey({
22
+ providerId,
23
+ accountId
24
+ }).then(found => found?.kind === "owned" ? found.user.id : undefined);
25
+ const existing = await owner();
26
+ if (existing) return existing;
27
+ try {
28
+ const holder = await internalAdapter.findUserByEmail(email);
29
+ if (holder) {
30
+ await internalAdapter.linkAccount({
31
+ userId: holder.user.id,
32
+ providerId,
33
+ accountId
34
+ });
35
+ return holder.user.id;
36
+ }
37
+ const {
38
+ user
39
+ } = await internalAdapter.createOAuthUser({
40
+ name,
41
+ email,
42
+ emailVerified: true
43
+ }, {
44
+ providerId,
45
+ accountId
46
+ });
47
+ return user.id;
48
+ } catch (error) {
49
+ // A concurrent call created the account first: the unique account or
50
+ // email index rejected this one. A failed re-read leaves `error`.
51
+ const winner = await owner().catch(() => undefined);
52
+ if (winner) return winner;
53
+ throw error;
54
+ }
55
+ }
@@ -0,0 +1,62 @@
1
+ import { z } from "zod";
2
+ import type { ContextAuth } from "./provider-user.ts";
3
+ /** Dev sign-in's provider and its accounts: `devProviderSignIn`'s options. */
4
+ export interface DevProviderSignInOptions {
5
+ /** Provider id the signed-in accounts belong to. */
6
+ provider: string;
7
+ /** Valid account ids; others answer 400. Default: any non-empty id. */
8
+ accountId?: z.ZodType<string>;
9
+ /** Name and email of a new account's user. */
10
+ newUser: (accountId: string) => {
11
+ name: string;
12
+ email: string;
13
+ };
14
+ }
15
+ /**
16
+ * better-auth plugin for dev builds, `ShellAuthApp.devSignIn`: `POST
17
+ * /dev/sign-in` with form field `accountId` signs in as that `provider`
18
+ * account through better-auth's OAuth sign-in (`handleOAuthUserInfo`): its
19
+ * owner, else the user holding `newUser`'s email under the app's account
20
+ * linking, else a new user (`newUser`, verified email); new accounts hold
21
+ * no tokens, stored tokens stay. Requests from a better-auth trusted origin
22
+ * only (`isTrustedOrigin`: the base URL, allowed hosts, `trustedOrigins`);
23
+ * others and a missing `Origin` answer 403; a refused sign-in answers 401.
24
+ * better-auth sets the session cookies, plus a marker cookie holding the
25
+ * session id signed like the session token (`isDevSignIn`), expiring at
26
+ * the session's sign-in expiry. Answers the session's `token` and `user`
27
+ * id.
28
+ */
29
+ export declare function devProviderSignIn({ provider, accountId, newUser, }: DevProviderSignInOptions): {
30
+ id: "dev-provider-sign-in";
31
+ endpoints: {
32
+ devProviderSignIn: import("better-auth").StrictEndpoint<"/dev/sign-in", {
33
+ method: "POST";
34
+ body: z.ZodObject<{
35
+ accountId: z.ZodType<string, unknown, z.core.$ZodTypeInternals<string, unknown>>;
36
+ }, z.core.$strip>;
37
+ requireRequest: true;
38
+ metadata: {
39
+ allowedMediaTypes: string[];
40
+ };
41
+ }, {
42
+ token: string;
43
+ user: {
44
+ id: string;
45
+ };
46
+ }>;
47
+ };
48
+ };
49
+ /** The dev sign-in plugin `devProviderSignIn` builds. */
50
+ export type DevProviderSignIn = ReturnType<typeof devProviderSignIn>;
51
+ /**
52
+ * Whether `headers` carry `auth`'s marker cookie holding `session`'s id
53
+ * signed with `auth`'s secret: the auth server signed that id, as dev
54
+ * sign-in does for each session it creates. The marker's name follows
55
+ * `auth`'s cookie naming (prefix, `__Secure-` on HTTPS). False past the
56
+ * marker's sign-in expiry, while an extended session lives on.
57
+ */
58
+ export declare function isDevSignIn(auth: ContextAuth<"createAuthCookie" | "secret">, headers: Headers, session: {
59
+ session: {
60
+ id: string;
61
+ };
62
+ } | null): Promise<boolean>;
@@ -0,0 +1,116 @@
1
+ import { APIError, createAuthEndpoint } from "better-auth/api";
2
+ import { parseCookies, setSessionCookie } from "better-auth/cookies";
3
+ import { constantTimeEqual, makeSignature } from "better-auth/crypto";
4
+ import { handleOAuthUserInfo } from "better-auth/oauth2";
5
+ import { z } from "zod";
6
+ import { DEV_SIGN_IN_ENDPOINT } from "./endpoint.js";
7
+ /** better-auth cookie holding the id of the session dev sign-in created,
8
+ * signed with the auth secret. Its lifetime is fixed at sign-in to the
9
+ * session's `expiresIn`; a session extended on refresh (`updateAge`)
10
+ * outlives it. */
11
+ const MARKER = "dev_sign_in";
12
+ /**
13
+ * better-auth plugin for dev builds, `ShellAuthApp.devSignIn`: `POST
14
+ * /dev/sign-in` with form field `accountId` signs in as that `provider`
15
+ * account through better-auth's OAuth sign-in (`handleOAuthUserInfo`): its
16
+ * owner, else the user holding `newUser`'s email under the app's account
17
+ * linking, else a new user (`newUser`, verified email); new accounts hold
18
+ * no tokens, stored tokens stay. Requests from a better-auth trusted origin
19
+ * only (`isTrustedOrigin`: the base URL, allowed hosts, `trustedOrigins`);
20
+ * others and a missing `Origin` answer 403; a refused sign-in answers 401.
21
+ * better-auth sets the session cookies, plus a marker cookie holding the
22
+ * session id signed like the session token (`isDevSignIn`), expiring at
23
+ * the session's sign-in expiry. Answers the session's `token` and `user`
24
+ * id.
25
+ */
26
+ export function devProviderSignIn({
27
+ provider,
28
+ accountId = z.string().min(1),
29
+ newUser
30
+ }) {
31
+ return {
32
+ id: "dev-provider-sign-in",
33
+ endpoints: {
34
+ devProviderSignIn: createAuthEndpoint(DEV_SIGN_IN_ENDPOINT, {
35
+ method: "POST",
36
+ body: z.object({
37
+ accountId
38
+ }),
39
+ requireRequest: true,
40
+ metadata: {
41
+ allowedMediaTypes: ["application/x-www-form-urlencoded"]
42
+ }
43
+ }, async ctx => {
44
+ const {
45
+ request,
46
+ context
47
+ } = ctx;
48
+ const origin = request.headers.get("origin");
49
+ if (!origin || !context.isTrustedOrigin(origin)) throw APIError.fromStatus("FORBIDDEN", {
50
+ message: "Untrusted origin sign-in forbidden."
51
+ });
52
+ const id = ctx.body.accountId;
53
+ const signIn = () => handleOAuthUserInfo(ctx, {
54
+ userInfo: {
55
+ id,
56
+ ...newUser(id),
57
+ emailVerified: true
58
+ },
59
+ account: {
60
+ providerId: provider,
61
+ accountId: id
62
+ }
63
+ });
64
+ const first = await signIn();
65
+ // A concurrent sign-in as the same new account fails this one's
66
+ // create; the retry signs in as the winner's user.
67
+ const {
68
+ data,
69
+ error
70
+ } = first.data ? first : await signIn();
71
+ if (!data) throw APIError.fromStatus("UNAUTHORIZED", {
72
+ message: error
73
+ });
74
+ const {
75
+ session,
76
+ user
77
+ } = data;
78
+ await setSessionCookie(ctx, {
79
+ session,
80
+ user
81
+ });
82
+ const marker = context.createAuthCookie(MARKER);
83
+ await ctx.setSignedCookie(marker.name, session.id, context.secret, {
84
+ ...marker.attributes,
85
+ maxAge: context.sessionConfig.expiresIn
86
+ });
87
+ return ctx.json({
88
+ token: session.token,
89
+ user: {
90
+ id: user.id
91
+ }
92
+ });
93
+ })
94
+ }
95
+ };
96
+ }
97
+ /**
98
+ * Whether `headers` carry `auth`'s marker cookie holding `session`'s id
99
+ * signed with `auth`'s secret: the auth server signed that id, as dev
100
+ * sign-in does for each session it creates. The marker's name follows
101
+ * `auth`'s cookie naming (prefix, `__Secure-` on HTTPS). False past the
102
+ * marker's sign-in expiry, while an extended session lives on.
103
+ */
104
+ export async function isDevSignIn(auth, headers, session) {
105
+ if (!session) return false;
106
+ const {
107
+ createAuthCookie,
108
+ secret
109
+ } = await auth.$context;
110
+ const value = parseCookies(headers.get("cookie") ?? "").get(createAuthCookie(MARKER).name);
111
+ if (value === undefined) return false;
112
+ const {
113
+ id
114
+ } = session.session;
115
+ return constantTimeEqual(value, `${id}.${await makeSignature(id, secret)}`);
116
+ }