@jxsuite/auth 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/auth",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "Better Auth sessions, sign-in flows, and table permissions for Jx sites over /_jx/auth",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,13 +38,14 @@
38
38
  "upgrade": "bun update --latest && bun install"
39
39
  },
40
40
  "dependencies": {
41
- "@jxsuite/connector": "^0.6.2",
42
- "@jxsuite/schema": "^3.0.0",
43
- "better-auth": "^1.7.7",
44
- "kysely": "^0.29.6"
41
+ "@jxsuite/connector": "^0.7.0",
42
+ "@jxsuite/schema": "^4.0.0",
43
+ "better-auth": "^1.7.11",
44
+ "kysely": "^0.29.7"
45
45
  },
46
46
  "devDependencies": {
47
- "@happy-dom/global-registrator": "^20.14.5",
47
+ "@happy-dom/global-registrator": "^21.0.3",
48
+ "@jxsuite/test-support": "^0.0.0",
48
49
  "ajv": "^8.20.0"
49
50
  },
50
51
  "jx": "./jx-extension.json"
package/src/actions.ts CHANGED
@@ -9,9 +9,12 @@
9
9
  * `"onsubmit": { "$ref": "auth.signInEmail" }` on a form with email/password fields.
10
10
  */
11
11
 
12
+ import { isFunction, isString } from "@jxsuite/schema/guards";
12
13
  import { getAuthClient } from "./client.ts";
13
14
  import { clearSession, fetchSession } from "./session.ts";
14
- import type { AuthClientResult } from "./client.ts";
15
+ import type { ActionScope } from "@jxsuite/connector/table-actions";
16
+ import type { AuthClientResult, SocialSignInInput } from "./client.ts";
17
+ import type { AuthSection } from "./config.ts";
15
18
 
16
19
  /** Redirect paths applied after auth state changes. */
17
20
  export interface AuthRedirects {
@@ -26,34 +29,48 @@ export interface AuthActionsConfig {
26
29
  provider?: string;
27
30
  baseUrl?: string;
28
31
  /** Injected by hosts (compiler/dev server): the project sections, including `auth`. */
29
- _project?: { auth?: { redirects?: AuthRedirects }; [key: string]: unknown };
30
- [key: string]: unknown;
32
+ _project?: { auth?: AuthSection };
31
33
  }
32
34
 
33
- type Scope = Record<string, unknown>;
34
-
35
35
  /** A `(scope, event)`-callable auth handler resolving true when the call succeeded. */
36
- export type AuthActionHandler = (scope: Scope, event?: Event) => Promise<boolean>;
36
+ export type AuthActionHandler = (scope: ActionScope, event?: Event) => Promise<boolean>;
37
+
38
+ /** The handler map `AuthActions.resolve()` returns, one entry per `$ref`-able action. */
39
+ export interface AuthActionHandlers {
40
+ signInEmail: AuthActionHandler;
41
+ signInSocial: AuthActionHandler;
42
+ signOut: AuthActionHandler;
43
+ signUpEmail: AuthActionHandler;
44
+ }
45
+
46
+ /** String field values of a submitted form, keyed by field name. */
47
+ type FormFields = Record<string, string>;
37
48
 
38
49
  /** The submitting form: the event target itself, or the target control's owner form. */
39
50
  function formOf(event?: Event): HTMLFormElement | null {
40
- const target = event?.target as (HTMLElement & { form?: HTMLFormElement | null }) | null;
51
+ const target = event?.target;
41
52
  if (!target) {
42
53
  return null;
43
54
  }
44
- return target.tagName === "FORM" ? (target as HTMLFormElement) : (target.form ?? null);
55
+ if (target instanceof HTMLFormElement) {
56
+ return target;
57
+ }
58
+ // Form-associated controls (input, button, select, …) expose their owner form as `.form`.
59
+ if (target instanceof Element && "form" in target && target.form instanceof HTMLFormElement) {
60
+ return target.form;
61
+ }
62
+ return null;
45
63
  }
46
64
 
47
65
  /** String field values of the submitting form (files are ignored). */
48
- function formValues(event?: Event): Record<string, string> {
66
+ function formValues(event?: Event) {
67
+ const out: FormFields = {};
49
68
  const form = formOf(event);
50
- if (!form) {
51
- return {};
52
- }
53
- const out: Record<string, string> = {};
54
- for (const [key, value] of new FormData(form)) {
55
- if (typeof value === "string") {
56
- out[key] = value;
69
+ if (form) {
70
+ for (const [key, value] of new FormData(form)) {
71
+ if (isString(value)) {
72
+ out[key] = value;
73
+ }
57
74
  }
58
75
  }
59
76
  return out;
@@ -67,8 +84,8 @@ function navigate(path: string | undefined): void {
67
84
  }
68
85
 
69
86
  /** Bump the read-after-write version so owner-scoped queries re-run under the new session. */
70
- function bumpVersion(scope: Scope): void {
71
- scope._v = ((scope._v as number) || 0) + 1;
87
+ function bumpVersion(scope: ActionScope): void {
88
+ scope._v = (scope._v || 0) + 1;
72
89
  }
73
90
 
74
91
  export class AuthActions {
@@ -84,7 +101,7 @@ export class AuthActions {
84
101
  }
85
102
 
86
103
  /** The handler map, wired via $ref (e.g. `onsubmit: { "$ref": "auth.signInEmail" }`). */
87
- resolve(): Record<string, AuthActionHandler> {
104
+ resolve(): AuthActionHandlers {
88
105
  return {
89
106
  signInEmail: this.#authCall((values, client) =>
90
107
  client.signIn.email({ email: values.email ?? "", password: values.password ?? "" }),
@@ -93,16 +110,19 @@ export class AuthActions {
93
110
  (values, client) => {
94
111
  const provider = values.provider ?? this.config.provider ?? "";
95
112
  const callbackURL = this.redirects().afterSignIn;
96
- return client.signIn.social({
97
- provider,
98
- ...(callbackURL === undefined ? {} : { callbackURL }),
99
- });
113
+ const input: SocialSignInInput = { provider };
114
+ if (callbackURL !== undefined) {
115
+ input.callbackURL = callbackURL;
116
+ }
117
+ return client.signIn.social(input);
100
118
  // Social sign-in redirects the whole page itself — no local navigation.
101
119
  },
102
120
  { redirect: false },
103
121
  ),
104
- signOut: async (scope: Scope, event?: Event): Promise<boolean> => {
105
- event?.preventDefault?.();
122
+ signOut: async (scope: ActionScope, event?: Event): Promise<boolean> => {
123
+ if (event && isFunction(event.preventDefault)) {
124
+ event.preventDefault();
125
+ }
106
126
  const { error } = await getAuthClient(this.config.baseUrl).signOut();
107
127
  if (error) {
108
128
  return false;
@@ -126,13 +146,15 @@ export class AuthActions {
126
146
  /** Build a form-reading handler around one auth client call. */
127
147
  #authCall(
128
148
  call: (
129
- values: Record<string, string>,
149
+ values: FormFields,
130
150
  client: ReturnType<typeof getAuthClient>,
131
151
  ) => Promise<AuthClientResult>,
132
152
  options: { redirect?: boolean } = {},
133
153
  ): AuthActionHandler {
134
- return async (scope: Scope, event?: Event): Promise<boolean> => {
135
- event?.preventDefault?.();
154
+ return async (scope: ActionScope, event?: Event): Promise<boolean> => {
155
+ if (event && isFunction(event.preventDefault)) {
156
+ event.preventDefault();
157
+ }
136
158
  const client = getAuthClient(this.config.baseUrl);
137
159
  const { error } = await call(formValues(event), client);
138
160
  if (error) {
package/src/client.ts CHANGED
@@ -10,11 +10,22 @@
10
10
 
11
11
  import { createAuthClient } from "better-auth/client";
12
12
  import { AUTH_BASE_PATH } from "./config.ts";
13
+ import type { AuthSessionPayload } from "./config.ts";
13
14
 
14
- /** The `{ data, error }` envelope every client call resolves to. */
15
+ /** The `{ data, error }` envelope every client call resolves to (only `error` is consumed). */
15
16
  export interface AuthClientResult {
16
- data?: unknown;
17
- error?: { message?: string; status?: number } | null;
17
+ error?: { message?: string | undefined; status?: number | undefined } | null;
18
+ }
19
+
20
+ /** The `getSession()` envelope: `data` is the parsed `{ user, session }` payload, or null. */
21
+ export interface AuthSessionResult extends AuthClientResult {
22
+ data?: AuthSessionPayload | null;
23
+ }
24
+
25
+ /** The `signIn.social` input: the provider id, plus where to land after the OAuth round trip. */
26
+ export interface SocialSignInInput {
27
+ provider: string;
28
+ callbackURL?: string;
18
29
  }
19
30
 
20
31
  /** The Better Auth client slice consumed by the session store and the auth actions. */
@@ -25,13 +36,13 @@ export interface JxAuthClient {
25
36
  password: string;
26
37
  callbackURL?: string;
27
38
  }) => Promise<AuthClientResult>;
28
- social: (input: { provider: string; callbackURL?: string }) => Promise<AuthClientResult>;
39
+ social: (input: SocialSignInInput) => Promise<AuthClientResult>;
29
40
  };
30
41
  signUp: {
31
42
  email: (input: { email: string; password: string; name: string }) => Promise<AuthClientResult>;
32
43
  };
33
44
  signOut: () => Promise<AuthClientResult>;
34
- getSession: () => Promise<AuthClientResult>;
45
+ getSession: () => Promise<AuthSessionResult>;
35
46
  }
36
47
 
37
48
  let client: JxAuthClient | null = null;
@@ -63,7 +74,7 @@ export function getAuthClient(baseUrl?: string): JxAuthClient {
63
74
  }
64
75
  const base = resolveAuthBaseUrl(baseUrl);
65
76
  if (!client || clientBase !== base) {
66
- client = createAuthClient({ baseURL: base }) as unknown as JxAuthClient;
77
+ client = createAuthClient({ baseURL: base });
67
78
  clientBase = base;
68
79
  }
69
80
  return client;
package/src/config.ts CHANGED
@@ -4,11 +4,15 @@
4
4
  * The section carries identifiers and env-var NAMES only (specs/extensions.md §13): the signing
5
5
  * secret comes from `env[secretEnv]` (default BETTER_AUTH_SECRET), social-provider credentials from
6
6
  * their `clientIdEnv`/`clientSecretEnv` vars (defaults <ID>_CLIENT_ID / <ID>_CLIENT_SECRET). This
7
- * module is pure and dependency-free so browser bundles (session store, actions) can share the
8
- * section types and the SessionInfo mapping without pulling the Better Auth server in.
7
+ * module is pure (its only runtime import is the schema guards) so browser bundles (session store,
8
+ * actions) can share the section types and the SessionInfo mapping without pulling the Better Auth
9
+ * server in.
9
10
  */
10
11
 
11
- import type { SessionInfo } from "@jxsuite/connector/types";
12
+ import { isString } from "@jxsuite/schema/guards";
13
+ import type { Dialect } from "kysely";
14
+ import type { HostEnv } from "@jxsuite/connector/provider-utils";
15
+ import type { AuthRecord, ConnectionDef, SessionInfo } from "@jxsuite/connector/types";
12
16
 
13
17
  /** The Better Auth route subtree (specs/extensions.md §11: the auth mount's basePath). */
14
18
  export const AUTH_BASE_PATH = "/_jx/auth";
@@ -46,12 +50,12 @@ const COOKIE_IDS = ["session_token", "session_data", "dont_remember", "account_d
46
50
  * genuinely serves auth over plain HTTP is the local dev server, and it pins `BETTER_AUTH_URL` to
47
51
  * its own origin so this reads `http:` there rather than guessing.
48
52
  *
49
- * @param {Env} env
53
+ * @param {HostEnv} env
50
54
  * @returns {boolean}
51
55
  */
52
- export function cookiesAreSecure(env: Env): boolean {
56
+ export function cookiesAreSecure(env: HostEnv): boolean {
53
57
  const baseURL = env["BETTER_AUTH_URL"];
54
- if (typeof baseURL === "string" && baseURL !== "") {
58
+ if (isString(baseURL) && baseURL !== "") {
55
59
  return !baseURL.startsWith("http://");
56
60
  }
57
61
  return true;
@@ -78,14 +82,14 @@ export function cookiesAreSecure(env: Env): boolean {
78
82
  * whenever the embedding page changed.
79
83
  *
80
84
  * @param {boolean} secure
81
- * @returns {Record<string, unknown>}
85
+ * @returns {AuthAdvancedOptions}
82
86
  */
83
- function advancedCookieOptions(secure: boolean): Record<string, unknown> {
87
+ function advancedCookieOptions(secure: boolean): AuthAdvancedOptions {
84
88
  if (!secure) {
85
89
  // Plain-HTTP dev origin: a prefixed cookie without `Secure` is rejected outright.
86
90
  return { cookiePrefix: COOKIE_PREFIX, useSecureCookies: false };
87
91
  }
88
- const cookies: Record<string, { name: string }> = {};
92
+ const cookies: AuthCookieNames = {};
89
93
  for (const id of COOKIE_IDS) {
90
94
  cookies[id] = { name: `__Host-${COOKIE_PREFIX}.${id}` };
91
95
  }
@@ -97,6 +101,17 @@ function advancedCookieOptions(secure: boolean): Record<string, unknown> {
97
101
  };
98
102
  }
99
103
 
104
+ /** Cookie names keyed by Better Auth cookie id (`session_token`, …). */
105
+ export type AuthCookieNames = Record<string, { name: string }>;
106
+
107
+ /** The `advanced` block of BetterAuthOptions this module sets (cookie names and attributes). */
108
+ export interface AuthAdvancedOptions {
109
+ cookiePrefix: string;
110
+ cookies?: AuthCookieNames;
111
+ defaultCookieAttributes?: { secure: boolean };
112
+ useSecureCookies: boolean;
113
+ }
114
+
100
115
  /** One social provider entry: env-var NAMES for the OAuth credentials, never values. */
101
116
  export interface AuthProviderDef {
102
117
  clientIdEnv?: string;
@@ -113,18 +128,14 @@ export interface AuthSection {
113
128
  redirects?: { afterSignIn?: string; afterSignOut?: string };
114
129
  roles?: string[];
115
130
  trustedOrigins?: string[];
116
- [key: string]: unknown;
117
131
  }
118
132
 
119
133
  /** The `connections`-bearing slice of a project config (the connector owns the full type). */
120
134
  export interface AuthProjectConfig {
121
- connections?: Record<string, { provider: string; [key: string]: unknown }>;
135
+ connections?: Record<string, ConnectionDef>;
122
136
  auth?: AuthSection;
123
- [key: string]: unknown;
124
137
  }
125
138
 
126
- type Env = Record<string, unknown>;
127
-
128
139
  /** The connection name the auth tables live on: declared, else the first-declared connection. */
129
140
  export function resolveAuthConnectionName(
130
141
  section: AuthSection,
@@ -141,10 +152,10 @@ export function resolveAuthConnectionName(
141
152
  }
142
153
 
143
154
  /** The signing secret from `env[secretEnv]`, failing closed with the env-var name when unset. */
144
- export function resolveAuthSecret(section: AuthSection, env: Env): string {
155
+ export function resolveAuthSecret(section: AuthSection, env: HostEnv): string {
145
156
  const name = section.secretEnv ?? DEFAULT_SECRET_ENV;
146
157
  const value = env[name];
147
- if (typeof value !== "string" || value === "") {
158
+ if (!isString(value) || value === "") {
148
159
  throw new Error(
149
160
  `Auth is not configured: set the ${name} secret (in .dev.vars locally, ` +
150
161
  `or \`wrangler secret put ${name}\` in production)`,
@@ -158,48 +169,77 @@ function defaultProviderEnv(providerId: string, suffix: "CLIENT_ID" | "CLIENT_SE
158
169
  return `${providerId.toUpperCase().replaceAll(/[^A-Z0-9]+/g, "_")}_${suffix}`;
159
170
  }
160
171
 
172
+ /** Resolved OAuth credentials per social provider id (Better Auth's `socialProviders` shape). */
173
+ export type AuthSocialProviders = Record<string, { clientId: string; clientSecret: string }>;
174
+
161
175
  /**
162
176
  * Social providers with resolvable credentials, in Better Auth's `socialProviders` shape. Providers
163
177
  * whose env vars are unset are omitted (fail-closed: no half-configured OAuth).
164
178
  */
165
- export function resolveSocialProviders(
166
- section: AuthSection,
167
- env: Env,
168
- ): Record<string, { clientId: string; clientSecret: string }> {
169
- const out: Record<string, { clientId: string; clientSecret: string }> = {};
179
+ export function resolveSocialProviders(section: AuthSection, env: HostEnv) {
180
+ const out: AuthSocialProviders = {};
170
181
  for (const [id, def] of Object.entries(section.providers ?? {})) {
171
182
  const clientId = env[def.clientIdEnv ?? defaultProviderEnv(id, "CLIENT_ID")];
172
183
  const clientSecret = env[def.clientSecretEnv ?? defaultProviderEnv(id, "CLIENT_SECRET")];
173
- if (typeof clientId === "string" && clientId !== "" && typeof clientSecret === "string") {
184
+ if (isString(clientId) && clientId !== "" && isString(clientSecret)) {
174
185
  out[id] = { clientId, clientSecret };
175
186
  }
176
187
  }
177
188
  return out;
178
189
  }
179
190
 
191
+ /** Better Auth's Kysely database config: the connection's dialect and its SQL family. */
192
+ export interface AuthDatabaseConfig {
193
+ dialect: Dialect;
194
+ type: "sqlite" | "postgres";
195
+ }
196
+
180
197
  /**
181
- * Map the auth section + env onto BetterAuthOptions (typed structurally — the exact
182
- * BetterAuthOptions type lives in better-auth, which this pure module never imports).
198
+ * The BetterAuthOptions this module produces (typed structurally — the exact BetterAuthOptions type
199
+ * lives in better-auth, which this pure module never imports).
200
+ */
201
+ export interface JxAuthOptions {
202
+ baseURL?: string;
203
+ advanced: AuthAdvancedOptions;
204
+ basePath: string;
205
+ emailAndPassword: { enabled: boolean };
206
+ rateLimit: { enabled: boolean; max: number; window: number };
207
+ session: { expiresIn: number; updateAge: number };
208
+ socialProviders?: AuthSocialProviders;
209
+ user?: {
210
+ additionalFields: { role: { input: boolean; required: boolean; type: "string" } };
211
+ };
212
+ trustedOrigins?: string[];
213
+ database?: AuthDatabaseConfig;
214
+ secret?: string;
215
+ }
216
+
217
+ /**
218
+ * Map the auth section + env onto BetterAuthOptions. Optional keys are present only when set.
183
219
  *
184
220
  * @param {AuthSection} section
185
- * @param {Env} env
221
+ * @param {HostEnv} env
186
222
  * @param {object} [extras]
187
- * @param {unknown} [extras.database] - Better Auth database config ({ dialect, type })
223
+ * @param {AuthDatabaseConfig} [extras.database] - Better Auth database config ({ dialect, type })
188
224
  * @param {string} [extras.secret] - Signing secret (omit for secret-free uses like migrations)
189
- * @returns {Record<string, unknown>} Options for `betterAuth()` / `getMigrations()`
225
+ * @returns {JxAuthOptions} Options for `betterAuth()` / `getMigrations()`
190
226
  */
191
227
  export function buildAuthOptions(
192
228
  section: AuthSection,
193
- env: Env,
194
- extras: { database?: unknown; secret?: string } = {},
195
- ): Record<string, unknown> {
229
+ env: HostEnv,
230
+ extras: { database?: AuthDatabaseConfig; secret?: string } = {},
231
+ ): JxAuthOptions {
196
232
  const socialProviders = resolveSocialProviders(section, env);
197
233
  const roles = section.roles ?? [];
198
234
  const baseURL = env["BETTER_AUTH_URL"];
199
- return {
200
- // Without BETTER_AUTH_URL Better Auth derives the origin from each incoming request, which
201
- // Is correct for same-origin /_jx/auth mounts; setting it pins callbacks for OAuth flows.
202
- ...(typeof baseURL === "string" && baseURL !== "" ? { baseURL } : {}),
235
+ const pinned: Pick<JxAuthOptions, "baseURL"> = {};
236
+ // Without BETTER_AUTH_URL Better Auth derives the origin from each incoming request, which
237
+ // Is correct for same-origin /_jx/auth mounts; setting it pins callbacks for OAuth flows.
238
+ if (isString(baseURL) && baseURL !== "") {
239
+ pinned.baseURL = baseURL;
240
+ }
241
+ const options: JxAuthOptions = {
242
+ ...pinned,
203
243
  advanced: advancedCookieOptions(cookiesAreSecure(env)),
204
244
  basePath: AUTH_BASE_PATH,
205
245
  emailAndPassword: { enabled: section.methods?.emailPassword ?? true },
@@ -213,43 +253,61 @@ export function buildAuthOptions(
213
253
  */
214
254
  rateLimit: { enabled: true, max: RATE_LIMIT_MAX, window: RATE_LIMIT_WINDOW_SECONDS },
215
255
  session: { expiresIn: SESSION_EXPIRES_SECONDS, updateAge: SESSION_UPDATE_SECONDS },
216
- ...(Object.keys(socialProviders).length > 0 ? { socialProviders } : {}),
217
- ...(roles.length > 0
218
- ? {
219
- user: {
220
- additionalFields: {
221
- // Roles are assigned via the data grid, never by sign-up input.
222
- role: { input: false, required: false, type: "string" },
223
- },
224
- },
225
- }
226
- : {}),
227
- ...(section.trustedOrigins && section.trustedOrigins.length > 0
228
- ? { trustedOrigins: section.trustedOrigins }
229
- : {}),
230
- ...(extras.database === undefined ? {} : { database: extras.database }),
231
- ...(extras.secret === undefined ? {} : { secret: extras.secret }),
232
256
  };
257
+ if (Object.keys(socialProviders).length > 0) {
258
+ options.socialProviders = socialProviders;
259
+ }
260
+ if (roles.length > 0) {
261
+ options.user = {
262
+ additionalFields: {
263
+ // Roles are assigned via the data grid, never by sign-up input.
264
+ role: { input: false, required: false, type: "string" },
265
+ },
266
+ };
267
+ }
268
+ if (section.trustedOrigins && section.trustedOrigins.length > 0) {
269
+ options.trustedOrigins = section.trustedOrigins;
270
+ }
271
+ if (extras.database !== undefined) {
272
+ options.database = extras.database;
273
+ }
274
+ if (extras.secret !== undefined) {
275
+ options.secret = extras.secret;
276
+ }
277
+ return options;
278
+ }
279
+
280
+ /**
281
+ * A Better Auth session payload: `auth.api.getSession`'s result on the server (records carry `Date`
282
+ * fields), or the client's parsed `getSession().data` (dates revived from ISO strings).
283
+ */
284
+ export interface AuthSessionPayload {
285
+ user?: AuthRecord | null;
286
+ session?: AuthRecord | null;
233
287
  }
234
288
 
235
289
  /**
236
290
  * Map a Better Auth session payload ({ user, session }) onto the connector's SessionInfo shape.
237
291
  *
238
- * @param {unknown} data - `auth.api.getSession` result or the client's `getSession().data`
292
+ * @param {AuthSessionPayload | null | undefined} data - `auth.api.getSession` result or the
293
+ * client's `getSession().data`
239
294
  * @returns {SessionInfo | null}
240
295
  */
241
- export function toSessionInfo(data: unknown): SessionInfo | null {
242
- if (!data || typeof data !== "object") {
296
+ export function toSessionInfo(data: AuthSessionPayload | null | undefined): SessionInfo | null {
297
+ if (!data) {
243
298
  return null;
244
299
  }
245
- const { user, session } = data as { user?: Record<string, unknown>; session?: unknown };
246
- if (!user || typeof user.id !== "string") {
300
+ const { user, session } = data;
301
+ if (!user || !isString(user.id)) {
247
302
  return null;
248
303
  }
249
- return {
250
- userId: user.id,
251
- ...(typeof user.role === "string" && user.role !== "" ? { role: user.role } : {}),
252
- user,
253
- ...(session === undefined ? {} : { session }),
254
- };
304
+ const info: SessionInfo = { userId: user.id };
305
+ if (isString(user.role) && user.role !== "") {
306
+ info.role = user.role;
307
+ }
308
+ info.user = user;
309
+ if (session !== undefined) {
310
+ info.session = session;
311
+ }
312
+ return info;
255
313
  }
package/src/server.ts CHANGED
@@ -20,21 +20,25 @@ import {
20
20
  toSessionInfo,
21
21
  } from "./config.ts";
22
22
  import type { Dialect } from "kysely";
23
+ import type { HostEnv } from "@jxsuite/connector/provider-utils";
23
24
  import type { ConnectionDef, SessionInfo } from "@jxsuite/connector/types";
24
- import type { AuthProjectConfig, AuthSection } from "./config.ts";
25
-
26
- type Env = Record<string, unknown>;
25
+ import type {
26
+ AuthDatabaseConfig,
27
+ AuthProjectConfig,
28
+ AuthSection,
29
+ AuthSessionPayload,
30
+ } from "./config.ts";
27
31
 
28
32
  /** The provider surface the auth extension needs (a slice of ConnectorProvider). */
29
33
  export interface AuthConnectorProvider {
30
34
  kind: "sqlite" | "postgres";
31
- dialect: (connection: ConnectionDef, env: Env) => Dialect | Promise<Dialect>;
35
+ dialect: (connection: ConnectionDef, env: HostEnv) => Dialect | Promise<Dialect>;
32
36
  }
33
37
 
34
38
  /** The Better Auth surface consumed here (structural — betterAuth() returns a far larger type). */
35
39
  export interface JxAuthInstance {
36
40
  handler: (request: Request) => Promise<Response>;
37
- api: { getSession: (input: { headers: Headers }) => Promise<unknown> };
41
+ api: { getSession: (input: { headers: Headers }) => Promise<AuthSessionPayload | null> };
38
42
  }
39
43
 
40
44
  export interface CreateJxAuthOptions {
@@ -48,9 +52,9 @@ export interface CreateJxAuthOptions {
48
52
  async function resolveAuthDatabase(
49
53
  section: AuthSection,
50
54
  projectConfig: AuthProjectConfig,
51
- env: Env,
55
+ env: HostEnv,
52
56
  connectors?: Record<string, AuthConnectorProvider>,
53
- ): Promise<{ dialect: Dialect; type: "sqlite" | "postgres"; connection: string }> {
57
+ ): Promise<AuthDatabaseConfig & { connection: string }> {
54
58
  const connection = resolveAuthConnectionName(section, projectConfig);
55
59
  const def = projectConfig.connections?.[connection];
56
60
  if (!def) {
@@ -58,7 +62,7 @@ async function resolveAuthDatabase(
58
62
  }
59
63
  const provider = connectors?.[def.provider];
60
64
  if (provider) {
61
- const dialect = await provider.dialect({ ...def, $name: connection } as ConnectionDef, env);
65
+ const dialect = await provider.dialect({ ...def, $name: connection }, env);
62
66
  return { connection, dialect, type: provider.kind };
63
67
  }
64
68
  const resolved = await resolveDialect(connection, projectConfig, env);
@@ -70,14 +74,14 @@ async function resolveAuthDatabase(
70
74
  *
71
75
  * @param {AuthSection} section - The project.json `auth` section
72
76
  * @param {AuthProjectConfig} projectConfig - Project config (connections lookup)
73
- * @param {Env} env - Host environment (bindings + env vars; the secret lives here)
77
+ * @param {HostEnv} env - Host environment (bindings + env vars; the secret lives here)
74
78
  * @param {CreateJxAuthOptions} [options]
75
79
  * @returns {Promise<JxAuthInstance>}
76
80
  */
77
81
  export async function createJxAuth(
78
82
  section: AuthSection,
79
83
  projectConfig: AuthProjectConfig,
80
- env: Env,
84
+ env: HostEnv,
81
85
  options: CreateJxAuthOptions = {},
82
86
  ): Promise<JxAuthInstance> {
83
87
  const secret = resolveAuthSecret(section, env);
@@ -92,10 +96,10 @@ export async function createJxAuth(
92
96
  secret,
93
97
  });
94
98
  if (options.autoSync) {
95
- const migrations = await getMigrations(authOptions as Parameters<typeof getMigrations>[0]);
99
+ const migrations = await getMigrations(authOptions);
96
100
  await migrations.runMigrations();
97
101
  }
98
- return betterAuth(authOptions as Parameters<typeof betterAuth>[0]) as JxAuthInstance;
102
+ return betterAuth(authOptions);
99
103
  }
100
104
 
101
105
  /**
@@ -182,7 +186,7 @@ export function classifyAuthStatement(statement: string, connection: string): Au
182
186
  *
183
187
  * @param {AuthSection} section
184
188
  * @param {AuthProjectConfig} projectConfig
185
- * @param {Env} env
189
+ * @param {HostEnv} env
186
190
  * @param {object} [options]
187
191
  * @param {Record<string, AuthConnectorProvider>} [options.connectors] - Provider stand-ins
188
192
  * @returns {Promise<AuthMigrationsResult>}
@@ -190,7 +194,7 @@ export function classifyAuthStatement(statement: string, connection: string): Au
190
194
  export async function getAuthMigrations(
191
195
  section: AuthSection,
192
196
  projectConfig: AuthProjectConfig,
193
- env: Env,
197
+ env: HostEnv,
194
198
  options: { connectors?: Record<string, AuthConnectorProvider> | undefined } = {},
195
199
  ): Promise<AuthMigrationsResult> {
196
200
  const { dialect, type, connection } = await resolveAuthDatabase(
@@ -200,7 +204,7 @@ export async function getAuthMigrations(
200
204
  options.connectors,
201
205
  );
202
206
  const authOptions = buildAuthOptions(section, env, { database: { dialect, type } });
203
- const migrations = await getMigrations(authOptions as Parameters<typeof getMigrations>[0]);
207
+ const migrations = await getMigrations(authOptions);
204
208
  const sqlBlob = await migrations.compileMigrations();
205
209
  const steps = splitSqlStatements(sqlBlob).map((statement) =>
206
210
  classifyAuthStatement(statement, connection),
package/src/session.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  * returns null, keeping statically generated pages on the signed-out state.
9
9
  */
10
10
 
11
+ import { isFunction } from "@jxsuite/schema/guards";
11
12
  import { getAuthClient } from "./client.ts";
12
13
  import { toSessionInfo } from "./config.ts";
13
14
  import type { SessionInfo } from "@jxsuite/connector/types";
@@ -20,7 +21,7 @@ const listeners = new Set<SessionListener>();
20
21
 
21
22
  /** True when running in a browser (session fetching is a client-only concern). */
22
23
  export function inBrowser(): boolean {
23
- return typeof document !== "undefined" && typeof fetch === "function";
24
+ return typeof document !== "undefined" && isFunction(globalThis.fetch);
24
25
  }
25
26
 
26
27
  function notify(): void {
@@ -89,7 +90,6 @@ export function resetSessionStore(): void {
89
90
  /** Config of the Session state class. */
90
91
  export interface SessionConfig {
91
92
  baseUrl?: string;
92
- [key: string]: unknown;
93
93
  }
94
94
 
95
95
  export class Session {
package/src/worker.ts CHANGED
@@ -12,25 +12,24 @@
12
12
 
13
13
  import { evaluatePermission } from "./authorize.ts";
14
14
  import { createJxAuth, getAuthMigrations, getSessionContext } from "./server.ts";
15
+ import type { HostEnv } from "@jxsuite/connector/provider-utils";
15
16
  import type {
16
17
  AuthorizeDecision,
17
18
  AuthorizeInput,
19
+ ConnectionDef,
18
20
  JxServerContext,
19
21
  SessionInfo,
20
22
  } from "@jxsuite/connector/types";
21
23
  import type { AuthConnectorProvider, JxAuthInstance } from "./server.ts";
22
24
  import type { AuthProjectConfig, AuthSection } from "./config.ts";
23
25
 
24
- type Env = Record<string, unknown>;
25
-
26
26
  export interface AuthMountOptions {
27
27
  /** Route subtree this mount owns. Defaults to "/_jx/auth". */
28
28
  basePath?: string;
29
29
  /** Extension-contributed project sections (needs `auth` and `connections`). */
30
30
  sections: {
31
31
  auth?: AuthSection;
32
- connections?: Record<string, { provider: string; [key: string]: unknown }>;
33
- [key: string]: unknown;
32
+ connections?: Record<string, ConnectionDef>;
34
33
  };
35
34
  /** Provider implementations keyed by `connector.provider` id (dev stand-ins applied by hosts). */
36
35
  connectors?: Record<string, AuthConnectorProvider>;
@@ -40,7 +39,7 @@ export interface AuthMountOptions {
40
39
 
41
40
  /** Per-isolate mount state: Better Auth instances memoized per env identity. */
42
41
  export interface AuthMountState {
43
- auths: WeakMap<object, Promise<JxAuthInstance>>;
42
+ auths: WeakMap<HostEnv, Promise<JxAuthInstance>>;
44
43
  }
45
44
 
46
45
  /** Create fresh mount state (exported for hosts that dispatch without `Auth.mount`). */
@@ -48,11 +47,14 @@ export function createAuthMountState(): AuthMountState {
48
47
  return { auths: new WeakMap() };
49
48
  }
50
49
 
51
- /** The project-config slice the auth modules need, rebuilt from the mount's section manifest. */
52
- function configFromSections(options: AuthMountOptions): {
50
+ /** The auth section plus the project-config slice the auth modules resolve it against. */
51
+ interface AuthMountConfig {
53
52
  section: AuthSection;
54
53
  projectConfig: AuthProjectConfig;
55
- } {
54
+ }
55
+
56
+ /** The project-config slice the auth modules need, rebuilt from the mount's section manifest. */
57
+ function configFromSections(options: AuthMountOptions): AuthMountConfig {
56
58
  const section = options.sections.auth ?? {};
57
59
  return {
58
60
  projectConfig: { auth: section, connections: options.sections.connections ?? {} },
@@ -64,7 +66,7 @@ function configFromSections(options: AuthMountOptions): {
64
66
  export function getAuthForEnv(
65
67
  state: AuthMountState,
66
68
  options: AuthMountOptions,
67
- env: Env,
69
+ env: HostEnv,
68
70
  ): Promise<JxAuthInstance> {
69
71
  const memo = state.auths.get(env);
70
72
  if (memo) {
@@ -84,14 +86,14 @@ export function getAuthForEnv(
84
86
  * Handle one /_jx/auth request by delegating to Better Auth's fetch handler.
85
87
  *
86
88
  * @param {Request} request
87
- * @param {Env} env - Worker env (bindings + vars; the signing secret lives here)
89
+ * @param {HostEnv} env - Worker env (bindings + vars; the signing secret lives here)
88
90
  * @param {AuthMountOptions} options
89
91
  * @param {AuthMountState} [state] - Memoized instances; omit for a throwaway state
90
92
  * @returns {Promise<Response>}
91
93
  */
92
94
  export async function handleAuthRequest(
93
95
  request: Request,
94
- env: Env,
96
+ env: HostEnv,
95
97
  options: AuthMountOptions,
96
98
  state: AuthMountState = createAuthMountState(),
97
99
  ): Promise<Response> {
@@ -115,7 +117,7 @@ export const Auth = {
115
117
  ctx.auth = {
116
118
  authorize: (input: AuthorizeInput): Promise<AuthorizeDecision> =>
117
119
  Promise.resolve(evaluatePermission(input)),
118
- getSession: async (request: Request, env: Env): Promise<SessionInfo | null> => {
120
+ getSession: async (request: Request, env: HostEnv): Promise<SessionInfo | null> => {
119
121
  try {
120
122
  const auth = await getAuthForEnv(state, options, env);
121
123
  return await getSessionContext(auth, request);
@@ -128,37 +130,37 @@ export const Auth = {
128
130
  }
129
131
  },
130
132
  };
131
- return (request: Request, env: Env): Promise<Response> =>
133
+ return (request: Request, env: HostEnv): Promise<Response> =>
132
134
  handleAuthRequest(request, env, options, state);
133
135
  },
134
136
 
135
137
  /** Static `projectData` capability: expose the auth section as `_project.auth`. */
136
- projectData(sectionValue: unknown): AuthSection {
137
- const section = (sectionValue ?? {}) as AuthSection;
138
- return { ...section };
138
+ projectData(sectionValue: AuthSection | null | undefined): AuthSection {
139
+ return { ...sectionValue };
139
140
  },
140
141
 
141
142
  /**
142
143
  * Static `deploySchema` capability (section-owner variant): compile Better Auth's additive
143
144
  * system-table migration into push steps the host composes after the connector plan.
144
145
  *
145
- * @param {unknown} sectionValue - The project.json `auth` section value
146
- * @param {unknown} projectConfig - The full project config (connection lookup)
146
+ * @param {AuthSection | null | undefined} sectionValue - The project.json `auth` section value
147
+ * @param {AuthProjectConfig | null | undefined} projectConfig - The full project config
148
+ * (connection lookup)
147
149
  * @param {object} options - { env, dryRun?, connection?, connectors? }
148
150
  * @returns {Promise<object>} { steps, applied, warnings, connection }
149
151
  */
150
152
  async deploySchema(
151
- sectionValue: unknown,
152
- projectConfig: unknown,
153
+ sectionValue: AuthSection | null | undefined,
154
+ projectConfig: AuthProjectConfig | null | undefined,
153
155
  options: {
154
- env: Env;
156
+ env: HostEnv;
155
157
  dryRun?: boolean;
156
158
  connection?: string;
157
159
  connectors?: Record<string, AuthConnectorProvider>;
158
160
  },
159
161
  ) {
160
- const section = (sectionValue ?? {}) as AuthSection;
161
- const config = (projectConfig ?? {}) as AuthProjectConfig;
162
+ const section = sectionValue ?? {};
163
+ const config = projectConfig ?? {};
162
164
  const dryRun = options.dryRun === true;
163
165
  const migrations = await getAuthMigrations(section, config, options.env, {
164
166
  connectors: options.connectors,