@jxsuite/auth 0.1.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/src/worker.ts ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Worker — the /_jx/auth mount (specs/extensions.md §11, order 10).
3
+ *
4
+ * `Auth.mount(options, ctx)` publishes the auth hooks on the shared server context — `ctx.auth = {
5
+ * getSession, authorize }` — and returns a fetch-style handler delegating to Better Auth's own
6
+ * request handler for everything under the base path (sign-up, sign-in, sign-out, get-session,
7
+ * social callbacks). The data mount (order 20) consumes ctx.auth; without this mount it fails
8
+ * closed. Better Auth instances are memoized per env identity: hosts pass one stable env object per
9
+ * isolate (the generated worker's c.env, the dev server's merged .dev.vars env), so the instance —
10
+ * and its database connection — is created once. Runs on Workers, Bun, and node.
11
+ */
12
+
13
+ import { evaluatePermission } from "./authorize.ts";
14
+ import { createJxAuth, getAuthMigrations, getSessionContext } from "./server.ts";
15
+ import type {
16
+ AuthorizeDecision,
17
+ AuthorizeInput,
18
+ JxServerContext,
19
+ SessionInfo,
20
+ } from "@jxsuite/connector/types";
21
+ import type { AuthConnectorProvider, JxAuthInstance } from "./server.ts";
22
+ import type { AuthProjectConfig, AuthSection } from "./config.ts";
23
+
24
+ type Env = Record<string, unknown>;
25
+
26
+ export interface AuthMountOptions {
27
+ /** Route subtree this mount owns. Defaults to "/_jx/auth". */
28
+ basePath?: string;
29
+ /** Extension-contributed project sections (needs `auth` and `connections`). */
30
+ sections: {
31
+ auth?: AuthSection;
32
+ connections?: Record<string, { provider: string; [key: string]: unknown }>;
33
+ [key: string]: unknown;
34
+ };
35
+ /** Provider implementations keyed by `connector.provider` id (dev stand-ins applied by hosts). */
36
+ connectors?: Record<string, AuthConnectorProvider>;
37
+ /** Run Better Auth's additive migrations on first touch (dev server / local sqlite). */
38
+ autoSync?: boolean;
39
+ }
40
+
41
+ /** Per-isolate mount state: Better Auth instances memoized per env identity. */
42
+ export interface AuthMountState {
43
+ auths: WeakMap<object, Promise<JxAuthInstance>>;
44
+ }
45
+
46
+ /** Create fresh mount state (exported for hosts that dispatch without `Auth.mount`). */
47
+ export function createAuthMountState(): AuthMountState {
48
+ return { auths: new WeakMap() };
49
+ }
50
+
51
+ /** The project-config slice the auth modules need, rebuilt from the mount's section manifest. */
52
+ function configFromSections(options: AuthMountOptions): {
53
+ section: AuthSection;
54
+ projectConfig: AuthProjectConfig;
55
+ } {
56
+ const section = options.sections.auth ?? {};
57
+ return {
58
+ projectConfig: { auth: section, connections: options.sections.connections ?? {} },
59
+ section,
60
+ };
61
+ }
62
+
63
+ /** Open (or reuse) the Better Auth instance for an env identity. */
64
+ export function getAuthForEnv(
65
+ state: AuthMountState,
66
+ options: AuthMountOptions,
67
+ env: Env,
68
+ ): Promise<JxAuthInstance> {
69
+ const memo = state.auths.get(env);
70
+ if (memo) {
71
+ return memo;
72
+ }
73
+ const { section, projectConfig } = configFromSections(options);
74
+ const open = createJxAuth(section, projectConfig, env, {
75
+ autoSync: options.autoSync ?? false,
76
+ connectors: options.connectors,
77
+ });
78
+ state.auths.set(env, open);
79
+ open.catch(() => state.auths.delete(env));
80
+ return open;
81
+ }
82
+
83
+ /**
84
+ * Handle one /_jx/auth request by delegating to Better Auth's fetch handler.
85
+ *
86
+ * @param {Request} request
87
+ * @param {Env} env - Worker env (bindings + vars; the signing secret lives here)
88
+ * @param {AuthMountOptions} options
89
+ * @param {AuthMountState} [state] - Memoized instances; omit for a throwaway state
90
+ * @returns {Promise<Response>}
91
+ */
92
+ export async function handleAuthRequest(
93
+ request: Request,
94
+ env: Env,
95
+ options: AuthMountOptions,
96
+ state: AuthMountState = createAuthMountState(),
97
+ ): Promise<Response> {
98
+ try {
99
+ const auth = await getAuthForEnv(state, options, env);
100
+ return await auth.handler(request);
101
+ } catch (error) {
102
+ const message = error instanceof Error ? error.message : String(error);
103
+ return Response.json({ error: message }, { status: 500 });
104
+ }
105
+ }
106
+
107
+ /** The mount + section-owner class for the `auth` section. */
108
+ export const Auth = {
109
+ /**
110
+ * Static `mount` capability (specs/extensions.md §11): publishes ctx.auth for later mounts and
111
+ * returns the Better Auth route handler.
112
+ */
113
+ mount(options: AuthMountOptions, ctx: JxServerContext) {
114
+ const state = createAuthMountState();
115
+ ctx.auth = {
116
+ authorize: (input: AuthorizeInput): Promise<AuthorizeDecision> =>
117
+ Promise.resolve(evaluatePermission(input)),
118
+ getSession: async (request: Request, env: Env): Promise<SessionInfo | null> => {
119
+ try {
120
+ const auth = await getAuthForEnv(state, options, env);
121
+ return await getSessionContext(auth, request);
122
+ } catch (error) {
123
+ // Fail-closed: an unconfigured/unreachable auth backend means "signed out", never a
124
+ // Silent grant. The cause is surfaced once here rather than as a 500 per data request.
125
+ const message = error instanceof Error ? error.message : String(error);
126
+ console.warn(`jx auth: session resolution failed: ${message}`);
127
+ return null;
128
+ }
129
+ },
130
+ };
131
+ return (request: Request, env: Env): Promise<Response> =>
132
+ handleAuthRequest(request, env, options, state);
133
+ },
134
+
135
+ /** Static `projectData` capability: expose the auth section as `_project.auth`. */
136
+ projectData(sectionValue: unknown): AuthSection {
137
+ const section = (sectionValue ?? {}) as AuthSection;
138
+ return { ...section };
139
+ },
140
+
141
+ /**
142
+ * Static `deploySchema` capability (section-owner variant): compile Better Auth's additive
143
+ * system-table migration into push steps the host composes after the connector plan.
144
+ *
145
+ * @param {unknown} sectionValue - The project.json `auth` section value
146
+ * @param {unknown} projectConfig - The full project config (connection lookup)
147
+ * @param {object} options - { env, dryRun?, connection?, connectors? }
148
+ * @returns {Promise<object>} { steps, applied, warnings, connection }
149
+ */
150
+ async deploySchema(
151
+ sectionValue: unknown,
152
+ projectConfig: unknown,
153
+ options: {
154
+ env: Env;
155
+ dryRun?: boolean;
156
+ connection?: string;
157
+ connectors?: Record<string, AuthConnectorProvider>;
158
+ },
159
+ ) {
160
+ const section = (sectionValue ?? {}) as AuthSection;
161
+ const config = (projectConfig ?? {}) as AuthProjectConfig;
162
+ const dryRun = options.dryRun === true;
163
+ const migrations = await getAuthMigrations(section, config, options.env, {
164
+ connectors: options.connectors,
165
+ });
166
+ if (options.connection !== undefined && options.connection !== migrations.connection) {
167
+ // The push targets a different connection — nothing to contribute.
168
+ return { applied: false, connection: migrations.connection, steps: [], warnings: [] };
169
+ }
170
+ if (!dryRun) {
171
+ await migrations.apply();
172
+ }
173
+ return {
174
+ applied: !dryRun && migrations.steps.length > 0,
175
+ connection: migrations.connection,
176
+ steps: migrations.steps,
177
+ warnings: [],
178
+ };
179
+ },
180
+ };