Skip to content

Commit 0fea5a2

Browse files
committed
Teach codegen to consume adapter packages: serverPackage entries, setupFn, and manifest routes
1 parent 7f0a0d5 commit 0fea5a2

15 files changed

Lines changed: 257 additions & 21 deletions

File tree

waspc/data/Generator/libs/auth-contract/README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
The contract between Wasp and pluggable auth providers.
44

5-
An auth provider implements this contract to make an external auth solution
6-
(Better Auth, Clerk, WorkOS, ...) usable as a Wasp auth provider: `AuthProvider`,
7-
`VerifiedSession`, and the optional `SessionIssuingAuthProvider` capability.
5+
An auth adapter package implements this contract to make an external auth
6+
solution (Better Auth, Clerk, WorkOS, ...) usable as a Wasp auth provider:
7+
`AuthProvider`, `VerifiedSession`, `WaspServerRuntime`, and the
8+
`createServerAdapter` factory shape adapter packages must export.
89

910
The package is copied into generated Wasp apps as a tarball (like the other libs
1011
in this directory) and installed via a `file:` dependency, so both generated code

waspc/data/Generator/libs/auth-contract/src/index.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,15 @@
55
* pages, `auth: true` operations, `context.user`, `useAuth()` -- so implementing it
66
* is all it takes to make any auth solution (Better Auth, Clerk, WorkOS, ...) a
77
* Wasp auth provider.
8+
*
9+
* An adapter package implements this contract in its server entry and exposes it
10+
* as a named `createServerAdapter` export (see `ServerAdapterFactory`). Its
11+
* client side, if it has one, ships as ordinary React exports on the package's
12+
* own `/client` entry -- Wasp generates no client glue.
813
*/
914

15+
import type { IncomingMessage, ServerResponse } from "node:http";
16+
1017
export type JsonValue =
1118
| string
1219
| number
@@ -199,3 +206,92 @@ export function canManageSessions(
199206
typeof p.revokeAllSessions === "function"
200207
);
201208
}
209+
210+
/**
211+
* Everything Wasp hands a server-side adapter about the app it runs in.
212+
*
213+
* This is the adapter's *only* window into the app: adapters must not import
214+
* generated code (`wasp/...`) and must not read `process.env` themselves. Keeping
215+
* the boundary here is what lets an adapter package typecheck and version
216+
* independently of any particular Wasp app.
217+
*/
218+
export type WaspServerRuntime = {
219+
/**
220+
* The app's PrismaClient instance. Typed as `unknown` because the client's type
221+
* is generated per app; adapters that need it narrow it themselves.
222+
*/
223+
db: unknown;
224+
225+
/**
226+
* The Prisma datasource provider of the app's database: `"sqlite"`,
227+
* `"postgresql"`, ... Adapters that bring their own storage layer (Better
228+
* Auth's prisma adapter, for one) need to know the dialect they are talking to.
229+
*/
230+
dbProvider: string;
231+
232+
/**
233+
* The server-side environment, already validated against the env vars the
234+
* adapter's manifest declared.
235+
*/
236+
env: Record<string, string | undefined>;
237+
238+
/** The URL the Wasp server is reachable at. */
239+
serverUrl: string;
240+
241+
/** The URL the Wasp client is served from. Useful for trusted-origin checks. */
242+
clientUrl: string;
243+
};
244+
245+
/**
246+
* What an adapter's server entry produces: the provider itself, plus, for
247+
* providers that own HTTP endpoints of their own (Better Auth's `/sign-in` and
248+
* friends), the handler Wasp should mount at the manifest's `basePath`.
249+
*
250+
* One factory returns both so they are guaranteed to share one configured
251+
* instance -- a provider verifying against one configuration while its routes run
252+
* another is a bug class this shape makes unrepresentable.
253+
*/
254+
export type ServerAdapter = {
255+
provider: AuthProvider;
256+
257+
/**
258+
* Node-style request handler for the provider's own routes. Wasp mounts it at
259+
* the `basePath` the adapter's manifest declared, with the app's usual
260+
* middleware around it (minus the JSON body parser when the manifest asked for
261+
* raw bodies).
262+
*/
263+
routeHandler?: (
264+
req: IncomingMessage,
265+
res: ServerResponse,
266+
) => void | Promise<void>;
267+
};
268+
269+
/**
270+
* User-code extensions Wasp delivers alongside the serializable options.
271+
*
272+
* `setupFn` follows the same convention as Wasp's `prismaSetupFn`: a user
273+
* function the adapter calls with its integration configuration, whose return
274+
* value becomes the configuration to use. It is the escape hatch for
275+
* everything a manifest cannot carry -- functions, class instances, live
276+
* values -- so the user can reach the underlying library's full surface
277+
* (Better Auth's hooks, plugins and email callbacks, say) without giving up
278+
* the packaged adapter. Adapters should re-assert the invariants their
279+
* integration depends on (route base paths, required plugins, table name
280+
* overrides) after applying it.
281+
*/
282+
export type ServerAdapterExtensions = {
283+
setupFn?: (config: never) => unknown;
284+
};
285+
286+
/**
287+
* The required shape of an adapter package's server entry: a named
288+
* `createServerAdapter` export of this type. `options` is the serializable
289+
* configuration the adapter's spec helper captured in `main.wasp.ts`, delivered
290+
* verbatim; `extensions` carries the user-code escape hatches referenced by the
291+
* manifest.
292+
*/
293+
export type ServerAdapterFactory<Options = unknown> = (
294+
runtime: WaspServerRuntime,
295+
options: Options,
296+
extensions?: ServerAdapterExtensions,
297+
) => ServerAdapter | Promise<ServerAdapter>;

waspc/data/Generator/templates/sdk/wasp/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@
131131
{=! Server: the contract a custom auth provider implements. Adapters live in
132132
user code, so they need to import this as a normal module. =}
133133
"./server/auth/provider/types": "./dist/server/auth/provider/types.js",
134+
{=# isCustomAuthProviderUsed =}
135+
{=! Server: the selected provider and its route handler, for mounting the
136+
provider's own routes. =}
137+
"./server/auth/provider": "./dist/server/auth/provider/index.js",
138+
{=/ isCustomAuthProviderUsed =}
134139
"./server/auth/session": "./dist/server/auth/session.js",
135140
"./server/auth/utils": "./dist/server/auth/utils.js",
136141
"./server/core/auth": "./dist/server/core/auth.js",

waspc/data/Generator/templates/sdk/wasp/server/auth/provider/index.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
{{={= =}=}}
22
import { {=# isCustomAuthProviderUsed =}canManageSessions as canProviderManagesSessions, canRevokeSessions as canProviderRevokeSessions, {=/ isCustomAuthProviderUsed =}type AuthProvider } from './types.js'
33
{=# isCustomAuthProviderUsed =}
4+
{=# isPackageAuthProvider =}
5+
import { createServerAdapter } from '{= serverPackage =}'
6+
import { config, prisma } from '../../index.js'
7+
{=# externalSetupFn.isDefined =}
8+
{=& externalSetupFn.importStatement =}
9+
{=/ externalSetupFn.isDefined =}
10+
{=/ isPackageAuthProvider =}
11+
{=^ isPackageAuthProvider =}
412
{=& authProvider.importStatement =}
13+
{=/ isPackageAuthProvider =}
514
{=/ isCustomAuthProviderUsed =}
615
{=^ isCustomAuthProviderUsed =}
716
import { waspAuthProvider } from './wasp.js'
@@ -17,6 +26,32 @@ export {
1726
canRevokeSessions,
1827
} from './types.js'
1928

29+
{=# isPackageAuthProvider =}
30+
/**
31+
* The adapter package's server factory, called with everything it may know
32+
* about the app. This runtime object is the adapter's *only* window into the
33+
* app: adapters never import generated code and never read `process.env`
34+
* themselves, which is what lets them version independently of any app.
35+
*/
36+
const serverAdapter = await Promise.resolve(
37+
createServerAdapter(
38+
{
39+
db: prisma,
40+
dbProvider: '{= dbProvider =}',
41+
env: process.env,
42+
serverUrl: config.serverUrl,
43+
clientUrl: config.frontendUrl,
44+
},
45+
{=& optionsJson =},
46+
{
47+
// The user's setup function for the adapter's underlying library; the
48+
// adapter calls it with its integration config and uses the result.
49+
setupFn: {=# externalSetupFn.isDefined =}{= externalSetupFn.importIdentifier =}{=/ externalSetupFn.isDefined =}{=^ externalSetupFn.isDefined =}undefined{=/ externalSetupFn.isDefined =},
50+
},
51+
),
52+
)
53+
54+
{=/ isPackageAuthProvider =}
2055
// PRIVATE API
2156
/**
2257
* The auth provider this app runs on.
@@ -26,7 +61,16 @@ export {
2661
* needed to authenticate against something other than Wasp's own auth.
2762
*/
2863
export const authProvider: AuthProvider =
29-
{=# isCustomAuthProviderUsed =}{= authProvider.importIdentifier =}{=/ isCustomAuthProviderUsed =}{=^ isCustomAuthProviderUsed =}waspAuthProvider{=/ isCustomAuthProviderUsed =}
64+
{=# isCustomAuthProviderUsed =}{=# isPackageAuthProvider =}serverAdapter.provider{=/ isPackageAuthProvider =}{=^ isPackageAuthProvider =}{= authProvider.importIdentifier =}{=/ isPackageAuthProvider =}{=/ isCustomAuthProviderUsed =}{=^ isCustomAuthProviderUsed =}waspAuthProvider{=/ isCustomAuthProviderUsed =}
65+
{=# isPackageAuthProvider =}
66+
67+
// PRIVATE API
68+
/**
69+
* Node handler for the provider's own routes, if it brought any. The server
70+
* mounts it at the basePath the manifest declared.
71+
*/
72+
export const authProviderRouteHandler = serverAdapter.routeHandler
73+
{=/ isPackageAuthProvider =}
3074
{=# isCustomAuthProviderUsed =}
3175

3276
/**

waspc/data/Generator/templates/sdk/wasp/server/auth/provider/types.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,16 @@ export type RegisteredAuthProviderUserSignupFields = FromRegister<
4343
'authProviderUserSignupFields',
4444
UserSignupFields
4545
>
46+
47+
// PRIVATE API
48+
/**
49+
* The `setupFn` the developer registered on the external provider's manifest,
50+
* if any: the setup function (the `prismaSetupFn` convention) that lets an app
51+
* reach the underlying auth library's full configuration surface (hooks,
52+
* plugins, email callbacks) while keeping the packaged adapter. The adapter
53+
* types its parameter precisely; here it only has to be *a* function.
54+
*/
55+
export type RegisteredAuthProviderSetupFn = FromRegister<
56+
'authProviderSetupFn',
57+
(config: never) => unknown
58+
>

waspc/data/Generator/templates/server/src/routes/index.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import { globalMiddlewareConfigForExpress } from '../middleware/index.js'
55
{=# isAuthEnabled =}
66
import auth from './auth/index.js'
77
{=/ isAuthEnabled =}
8+
{=# externalAuthProviderRoutes =}
9+
import { authProviderRouteHandler } from 'wasp/server/auth/provider'
10+
{=/ externalAuthProviderRoutes =}
811
{=# areThereAnyCustomApiRoutes =}
912
import apis from './apis/index.js'
1013
{=/ areThereAnyCustomApiRoutes =}
@@ -41,6 +44,25 @@ router.get('/', middleware,
4144
{=# isAuthEnabled =}
4245
router.use('/auth', middleware, auth)
4346
{=/ isAuthEnabled =}
47+
{=# externalAuthProviderRoutes =}
48+
// The external auth provider's own routes, mounted where its manifest asked.
49+
// The usual middleware stack applies{=# rawBody =}, minus the body parsers: the
50+
// provider's handler reads the raw request stream itself, and a body that was
51+
// already consumed would make every request to it hang{=/ rawBody =}.
52+
const authProviderMiddleware = globalMiddlewareConfigForExpress((middlewareConfig) => {
53+
{=# rawBody =}
54+
middlewareConfig.delete('express.json')
55+
middlewareConfig.delete('express.urlencoded')
56+
{=/ rawBody =}
57+
return middlewareConfig
58+
})
59+
router.use('{= basePath =}', authProviderMiddleware, (req, res, next) => {
60+
if (authProviderRouteHandler === undefined) {
61+
return next(new Error('The auth provider manifest declares routes, but its server adapter returned no routeHandler.'))
62+
}
63+
return Promise.resolve(authProviderRouteHandler(req, res)).catch(next)
64+
})
65+
{=/ externalAuthProviderRoutes =}
4466
router.use('/{= operationsRouteInRootRouter =}', middleware, operations)
4567
{=# areThereAnyCrudRoutes =}
4668
router.use('/{= crudRouteInRootRouter =}', middleware, rootCrudRouter)

waspc/e2e-tests/test-outputs/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/.waspchecksums

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

waspc/e2e-tests/test-outputs/snapshots/kitchen-sink-golden/wasp-app/.wasp/out/sdk/wasp/server/auth/provider/types.ts

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

waspc/e2e-tests/test-outputs/snapshots/wasp-build-golden/wasp-app/.wasp/out/.waspchecksums

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

waspc/e2e-tests/test-outputs/snapshots/wasp-compile-golden/wasp-app/.wasp/out/.waspchecksums

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)