jazz-tools 0.18.0 → 0.18.2

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 (46) hide show
  1. package/.turbo/turbo-build.log +44 -30
  2. package/CHANGELOG.md +20 -0
  3. package/dist/better-auth/auth/client.d.ts +29 -0
  4. package/dist/better-auth/auth/client.d.ts.map +1 -0
  5. package/dist/better-auth/auth/client.js +127 -0
  6. package/dist/better-auth/auth/client.js.map +1 -0
  7. package/dist/better-auth/auth/react.d.ts +2170 -0
  8. package/dist/better-auth/auth/react.d.ts.map +1 -0
  9. package/dist/better-auth/auth/react.js +40 -0
  10. package/dist/better-auth/auth/react.js.map +1 -0
  11. package/dist/better-auth/auth/server.d.ts +14 -0
  12. package/dist/better-auth/auth/server.d.ts.map +1 -0
  13. package/dist/better-auth/auth/server.js +198 -0
  14. package/dist/better-auth/auth/server.js.map +1 -0
  15. package/dist/better-auth/auth/tests/client.test.d.ts +2 -0
  16. package/dist/better-auth/auth/tests/client.test.d.ts.map +1 -0
  17. package/dist/better-auth/auth/tests/server.test.d.ts +2 -0
  18. package/dist/better-auth/auth/tests/server.test.d.ts.map +1 -0
  19. package/dist/{chunk-HJ3GTGY7.js → chunk-IERUTUXB.js} +18 -1
  20. package/dist/chunk-IERUTUXB.js.map +1 -0
  21. package/dist/index.js +1 -1
  22. package/dist/react-core/index.js +17 -0
  23. package/dist/react-core/index.js.map +1 -1
  24. package/dist/testing.js +1 -1
  25. package/dist/tools/coValues/account.d.ts +1 -0
  26. package/dist/tools/coValues/account.d.ts.map +1 -1
  27. package/dist/tools/coValues/coMap.d.ts +10 -0
  28. package/dist/tools/coValues/coMap.d.ts.map +1 -1
  29. package/dist/tools/implementation/zodSchema/zodCo.d.ts +1 -1
  30. package/dist/tools/testing.d.ts.map +1 -1
  31. package/package.json +23 -4
  32. package/src/better-auth/auth/client.ts +169 -0
  33. package/src/better-auth/auth/react.tsx +105 -0
  34. package/src/better-auth/auth/server.ts +250 -0
  35. package/src/better-auth/auth/tests/client.test.ts +249 -0
  36. package/src/better-auth/auth/tests/server.test.ts +226 -0
  37. package/src/tools/coValues/account.ts +5 -0
  38. package/src/tools/coValues/coMap.ts +14 -0
  39. package/src/tools/implementation/zodSchema/zodCo.ts +1 -1
  40. package/src/tools/tests/ContextManager.test.ts +2 -2
  41. package/src/tools/tests/account.test.ts +51 -0
  42. package/src/tools/tests/coMap.test.ts +99 -0
  43. package/src/tools/tests/patterns/notifications.test.ts +1 -1
  44. package/src/tools/tests/testing.test.ts +2 -2
  45. package/tsup.config.ts +9 -0
  46. package/dist/chunk-HJ3GTGY7.js.map +0 -1
@@ -0,0 +1,250 @@
1
+ import { AuthContext, MiddlewareContext, MiddlewareOptions } from "better-auth";
2
+ import { APIError } from "better-auth/api";
3
+ import { symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
4
+ import { BetterAuthPlugin, createAuthMiddleware } from "better-auth/plugins";
5
+ import type { Account, AuthCredentials, ID } from "jazz-tools";
6
+
7
+ /**
8
+ * @returns The BetterAuth server plugin.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const auth = betterAuth({
13
+ * plugins: [jazzPlugin()],
14
+ * // ... other BetterAuth options
15
+ * });
16
+ * ```
17
+ */
18
+ export const jazzPlugin = (): BetterAuthPlugin => {
19
+ return {
20
+ id: "jazz-plugin",
21
+ schema: {
22
+ user: {
23
+ fields: {
24
+ accountID: {
25
+ type: "string",
26
+ required: false,
27
+ input: false,
28
+ },
29
+ encryptedCredentials: {
30
+ type: "string",
31
+ required: false,
32
+ input: false,
33
+ returned: false,
34
+ },
35
+ },
36
+ },
37
+ },
38
+
39
+ init() {
40
+ return {
41
+ options: {
42
+ databaseHooks: {
43
+ user: {
44
+ create: {
45
+ before: async (user, context) => {
46
+ // If the user is created without a jazzAuth, it will throw an error.
47
+ if (!contextContainsJazzAuth(context)) {
48
+ throw new APIError(422, {
49
+ message: "JazzAuth is required",
50
+ });
51
+ }
52
+ // Decorate the user with the jazz's credentials.
53
+ return {
54
+ data: {
55
+ accountID: context?.jazzAuth?.accountID,
56
+ encryptedCredentials:
57
+ context?.jazzAuth?.encryptedCredentials,
58
+ },
59
+ };
60
+ },
61
+ },
62
+ },
63
+ verification: {
64
+ create: {
65
+ before: async (verification, context) => {
66
+ // If a jazzAuth is provided, save it for later usage.
67
+ if (contextContainsJazzAuth(context)) {
68
+ const parsed = JSON.parse(verification.value);
69
+ const newValue = JSON.stringify({
70
+ ...parsed,
71
+ jazzAuth: context.jazzAuth,
72
+ });
73
+
74
+ return {
75
+ data: {
76
+ value: newValue,
77
+ },
78
+ };
79
+ }
80
+ },
81
+ },
82
+ },
83
+ },
84
+ },
85
+ };
86
+ },
87
+
88
+ hooks: {
89
+ before: [
90
+ /**
91
+ * If the client sends a x-jazz-auth header,
92
+ * we encrypt the credentials and inject them into the context.
93
+ */
94
+ {
95
+ matcher: (context) => {
96
+ return !!context.headers?.get("x-jazz-auth");
97
+ },
98
+ handler: createAuthMiddleware(async (ctx) => {
99
+ const jazzAuth = JSON.parse(ctx.headers?.get("x-jazz-auth")!);
100
+
101
+ const credentials: AuthCredentials = {
102
+ accountID: jazzAuth.accountID as ID<Account>,
103
+ secretSeed: jazzAuth.secretSeed,
104
+ accountSecret: jazzAuth.accountSecret as any,
105
+ // If the provider remains 'anonymous', Jazz will not consider us authenticated later.
106
+ provider: "better-auth",
107
+ };
108
+
109
+ const encryptedCredentials = await symmetricEncrypt({
110
+ key: ctx.context.secret,
111
+ data: JSON.stringify(credentials),
112
+ });
113
+
114
+ return {
115
+ context: {
116
+ ...ctx,
117
+ jazzAuth: {
118
+ accountID: jazzAuth.accountID,
119
+ encryptedCredentials: encryptedCredentials,
120
+ },
121
+ },
122
+ };
123
+ }),
124
+ },
125
+
126
+ /**
127
+ * /callback is the endpoint that BetterAuth uses to authenticate the user coming from a social provider.
128
+ * 1. Catch the state
129
+ * 2. Find the verification value
130
+ * 3. If the verification value contains a jazzAuth, inject into the context to have it in case of registration.
131
+ */
132
+ {
133
+ matcher: (context) => {
134
+ return context.path.startsWith("/callback");
135
+ },
136
+ handler: createAuthMiddleware(async (ctx) => {
137
+ const state = ctx.query?.state || ctx.body?.state;
138
+
139
+ const data = await ctx.context.adapter.findOne<{ value: string }>({
140
+ model: ctx.context.tables.verification!.modelName,
141
+ where: [
142
+ {
143
+ field: "identifier",
144
+ operator: "eq",
145
+ value: state,
146
+ },
147
+ ],
148
+ select: ["value"],
149
+ });
150
+
151
+ // if not found, the social plugin will throw later anyway
152
+ if (!data) {
153
+ throw new APIError(404, {
154
+ message: "Verification not found",
155
+ });
156
+ }
157
+
158
+ const parsed = JSON.parse(data.value);
159
+
160
+ if (parsed && "jazzAuth" in parsed) {
161
+ ctx.context.jazzAuth = parsed.jazzAuth;
162
+ } else {
163
+ throw new APIError(404, {
164
+ message: "JazzAuth not found in verification value",
165
+ });
166
+ }
167
+ }),
168
+ },
169
+ ],
170
+ after: [
171
+ /**
172
+ * This middleware is used to extract the jazzAuth from the user and return it in the response.
173
+ * It is used in the following endpoints that return the user:
174
+ * - /sign-up/email
175
+ * - /sign-in/email
176
+ * - /get-session
177
+ */
178
+ {
179
+ matcher: (context) => {
180
+ return (
181
+ context.path.startsWith("/sign-up") ||
182
+ context.path.startsWith("/sign-in") ||
183
+ context.path.startsWith("/get-session")
184
+ );
185
+ },
186
+ handler: createAuthMiddleware({}, async (ctx) => {
187
+ const returned = ctx.context.returned as any;
188
+ if (!returned?.user?.id) {
189
+ return;
190
+ }
191
+ const jazzAuth = await extractJazzAuth(returned.user.id, ctx);
192
+
193
+ return ctx.json({
194
+ ...returned,
195
+ jazzAuth: jazzAuth,
196
+ });
197
+ }),
198
+ },
199
+ ],
200
+ },
201
+ };
202
+ };
203
+
204
+ function contextContainsJazzAuth(ctx: unknown): ctx is {
205
+ jazzAuth: {
206
+ accountID: string;
207
+ encryptedCredentials: string;
208
+ };
209
+ } {
210
+ return !!ctx && typeof ctx === "object" && "jazzAuth" in ctx;
211
+ }
212
+
213
+ async function extractJazzAuth(
214
+ userId: string,
215
+ ctx: MiddlewareContext<
216
+ MiddlewareOptions,
217
+ AuthContext & {
218
+ returned?: unknown;
219
+ responseHeaders?: Headers;
220
+ }
221
+ >,
222
+ ) {
223
+ const user = await ctx.context.adapter.findOne<{
224
+ accountID: string;
225
+ encryptedCredentials: string;
226
+ }>({
227
+ model: ctx.context.tables.user!.modelName,
228
+ where: [
229
+ {
230
+ field: "id",
231
+ operator: "eq",
232
+ value: userId,
233
+ },
234
+ ],
235
+ select: ["accountID", "encryptedCredentials"],
236
+ });
237
+
238
+ if (!user) {
239
+ return;
240
+ }
241
+
242
+ const jazzAuth = JSON.parse(
243
+ await symmetricDecrypt({
244
+ key: ctx.context.secret,
245
+ data: user.encryptedCredentials,
246
+ }),
247
+ );
248
+
249
+ return jazzAuth;
250
+ }
@@ -0,0 +1,249 @@
1
+ import { createAuthClient } from "better-auth/client";
2
+ import type { Account, AuthSecretStorage } from "jazz-tools";
3
+ import {
4
+ TestJazzContextManager,
5
+ setActiveAccount,
6
+ setupJazzTestSync,
7
+ } from "jazz-tools/testing";
8
+ import { assert, beforeEach, describe, expect, it, vi } from "vitest";
9
+ import { jazzPluginClient } from "../client.js";
10
+
11
+ describe("auth client", () => {
12
+ let account: Account;
13
+ let jazzContextManager: TestJazzContextManager<Account>;
14
+ let authSecretStorage: AuthSecretStorage;
15
+ let authClient: ReturnType<
16
+ typeof createAuthClient<{
17
+ plugins: ReturnType<typeof jazzPluginClient>[];
18
+ }>
19
+ >;
20
+ let customFetchImpl = vi.fn();
21
+
22
+ beforeEach(async () => {
23
+ account = await setupJazzTestSync();
24
+ setActiveAccount(account);
25
+
26
+ jazzContextManager = TestJazzContextManager.fromAccountOrGuest(account);
27
+ authSecretStorage = jazzContextManager.getAuthSecretStorage();
28
+
29
+ // start a new context
30
+ await jazzContextManager.createContext({});
31
+
32
+ authClient = createAuthClient({
33
+ baseURL: "http://localhost:3000",
34
+ plugins: [jazzPluginClient()],
35
+ fetchOptions: {
36
+ customFetchImpl,
37
+ },
38
+ });
39
+
40
+ const context = jazzContextManager.getCurrentValue();
41
+ assert(context, "Jazz context is not available");
42
+ authClient.jazz.setJazzContext(context);
43
+ authClient.jazz.setAuthSecretStorage(authSecretStorage);
44
+
45
+ customFetchImpl.mockReset();
46
+ });
47
+
48
+ it("should send Jazz credentials over signup", async () => {
49
+ const credentials = await authSecretStorage.get();
50
+ expect(authSecretStorage.isAuthenticated).toBe(false);
51
+ assert(credentials, "Jazz credentials are not available");
52
+
53
+ customFetchImpl.mockResolvedValue(
54
+ new Response(
55
+ JSON.stringify({
56
+ token: "6diDScDDcLJLl3sxAEestZz63mrw9Azy",
57
+ user: {
58
+ id: "S6SDKApdnh746gUnP3zujzsEY53tjuTm",
59
+ email: "test@jazz.dev",
60
+ name: "Matteo",
61
+ image: null,
62
+ emailVerified: false,
63
+ createdAt: new Date(),
64
+ updatedAt: new Date(),
65
+ },
66
+ jazzAuth: {
67
+ accountID: credentials.accountID,
68
+ secretSeed: credentials.secretSeed,
69
+ accountSecret: credentials.accountSecret,
70
+ },
71
+ }),
72
+ ),
73
+ );
74
+
75
+ // Sign up
76
+ await authClient.signUp.email({
77
+ email: "test@jazz.dev",
78
+ password: "12345678",
79
+ name: "Matteo",
80
+ });
81
+
82
+ expect(customFetchImpl).toHaveBeenCalledTimes(1);
83
+ expect(customFetchImpl.mock.calls[0]![0].toString()).toBe(
84
+ "http://localhost:3000/api/auth/sign-up/email",
85
+ );
86
+
87
+ // Verify the credentials have been injected in the request body
88
+ expect(
89
+ customFetchImpl.mock.calls[0]![1].headers.get("x-jazz-auth")!,
90
+ ).toEqual(
91
+ JSON.stringify({
92
+ accountID: credentials!.accountID,
93
+ secretSeed: credentials!.secretSeed,
94
+ accountSecret: credentials!.accountSecret,
95
+ }),
96
+ );
97
+
98
+ expect(authSecretStorage.isAuthenticated).toBe(true);
99
+
100
+ // Verify the profile name has been updated
101
+ const context = jazzContextManager.getCurrentValue();
102
+ assert(context && "me" in context);
103
+ expect(context.me.$jazz.id).toBe(credentials!.accountID);
104
+ });
105
+
106
+ it("should become logged in Jazz credentials after sign-in", async () => {
107
+ const credentials = await jazzContextManager.getAuthSecretStorage().get();
108
+
109
+ // Log out from initial context
110
+ await jazzContextManager.logOut();
111
+ expect(authSecretStorage.isAuthenticated).toBe(false);
112
+
113
+ customFetchImpl.mockResolvedValue(
114
+ new Response(
115
+ JSON.stringify({
116
+ user: {
117
+ id: "123",
118
+ email: "test@jazz.dev",
119
+ name: "Matteo",
120
+ },
121
+ jazzAuth: {
122
+ accountID: credentials!.accountID,
123
+ secretSeed: credentials!.secretSeed,
124
+ accountSecret: credentials!.accountSecret,
125
+ provider: "better-auth",
126
+ },
127
+ }),
128
+ ),
129
+ );
130
+
131
+ // Retrieve the BetterAuth session and trigger the authentication
132
+ await authClient.signIn.email({
133
+ email: "test@jazz.dev",
134
+ password: "12345678",
135
+ });
136
+
137
+ expect(customFetchImpl).toHaveBeenCalledTimes(1);
138
+ expect(customFetchImpl.mock.calls[0]![0].toString()).toBe(
139
+ "http://localhost:3000/api/auth/sign-in/email",
140
+ );
141
+
142
+ expect(authSecretStorage.isAuthenticated).toBe(true);
143
+
144
+ const newContext = jazzContextManager.getCurrentValue()!;
145
+ expect("me" in newContext).toBe(true);
146
+ expect(await authSecretStorage.get()).toMatchObject({
147
+ accountID: credentials!.accountID,
148
+ provider: "better-auth",
149
+ });
150
+ });
151
+
152
+ it("should logout from Jazz after BetterAuth sign-out", async () => {
153
+ const credentials = await authSecretStorage.get();
154
+ expect(authSecretStorage.isAuthenticated).toBe(false);
155
+ customFetchImpl.mockResolvedValueOnce(
156
+ new Response(
157
+ JSON.stringify({
158
+ token: "6diDScDDcLJLl3sxAEestZz63mrw9Azy",
159
+ user: {
160
+ id: "S6SDKApdnh746gUnP3zujzsEY53tjuTm",
161
+ email: "test@jazz.dev",
162
+ name: "Matteo",
163
+ image: null,
164
+ emailVerified: false,
165
+ createdAt: new Date(),
166
+ updatedAt: new Date(),
167
+ },
168
+ jazzAuth: {
169
+ accountID: credentials!.accountID,
170
+ secretSeed: credentials!.secretSeed,
171
+ accountSecret: credentials!.accountSecret,
172
+ provider: "better-auth",
173
+ },
174
+ }),
175
+ ),
176
+ );
177
+
178
+ // 1. Sign up
179
+ await authClient.signUp.email({
180
+ email: "test@jazz.dev",
181
+ password: "12345678",
182
+ name: "Matteo",
183
+ });
184
+
185
+ expect(authSecretStorage.isAuthenticated).toBe(true);
186
+
187
+ // 2. Sign out
188
+ customFetchImpl.mockResolvedValueOnce(
189
+ new Response(JSON.stringify({ success: true })),
190
+ );
191
+
192
+ await authClient.signOut();
193
+
194
+ expect(authSecretStorage.isAuthenticated).toBe(false);
195
+
196
+ const anonymousCredentials = await authSecretStorage.get();
197
+ expect(anonymousCredentials).not.toMatchObject(credentials!);
198
+ });
199
+
200
+ it("should logout from Jazz after BetterAuth user deletion", async () => {
201
+ const credentials = await authSecretStorage.get();
202
+ expect(authSecretStorage.isAuthenticated).toBe(false);
203
+ customFetchImpl.mockResolvedValueOnce(
204
+ new Response(
205
+ JSON.stringify({
206
+ token: "6diDScDDcLJLl3sxAEestZz63mrw9Azy",
207
+ user: {
208
+ id: "S6SDKApdnh746gUnP3zujzsEY53tjuTm",
209
+ email: "test@jazz.dev",
210
+ name: "Matteo",
211
+ image: null,
212
+ emailVerified: false,
213
+ createdAt: new Date(),
214
+ updatedAt: new Date(),
215
+ },
216
+ jazzAuth: {
217
+ accountID: credentials!.accountID,
218
+ secretSeed: credentials!.secretSeed,
219
+ accountSecret: credentials!.accountSecret,
220
+ provider: "better-auth",
221
+ },
222
+ }),
223
+ ),
224
+ );
225
+
226
+ // 1. Sign up
227
+ await authClient.signUp.email({
228
+ email: "test@jazz.dev",
229
+ password: "12345678",
230
+ name: "Matteo",
231
+ });
232
+
233
+ expect(authSecretStorage.isAuthenticated).toBe(true);
234
+
235
+ // 2. Delete user
236
+ customFetchImpl.mockResolvedValueOnce(
237
+ new Response(JSON.stringify({ success: true })),
238
+ );
239
+
240
+ await authClient.deleteUser();
241
+
242
+ expect(authSecretStorage.isAuthenticated).toBe(false);
243
+
244
+ const anonymousCredentials = await authSecretStorage.get();
245
+ expect(anonymousCredentials).not.toMatchObject(credentials!);
246
+ });
247
+
248
+ it.todo("should logout from Better Auth after Jazz's log-out");
249
+ });