jazz-tools 0.18.0 → 0.18.1
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/.turbo/turbo-build.log +54 -40
- package/CHANGELOG.md +10 -0
- package/dist/better-auth/auth/client.d.ts +29 -0
- package/dist/better-auth/auth/client.d.ts.map +1 -0
- package/dist/better-auth/auth/client.js +127 -0
- package/dist/better-auth/auth/client.js.map +1 -0
- package/dist/better-auth/auth/react.d.ts +2170 -0
- package/dist/better-auth/auth/react.d.ts.map +1 -0
- package/dist/better-auth/auth/react.js +40 -0
- package/dist/better-auth/auth/react.js.map +1 -0
- package/dist/better-auth/auth/server.d.ts +14 -0
- package/dist/better-auth/auth/server.d.ts.map +1 -0
- package/dist/better-auth/auth/server.js +198 -0
- package/dist/better-auth/auth/server.js.map +1 -0
- package/dist/better-auth/auth/tests/client.test.d.ts +2 -0
- package/dist/better-auth/auth/tests/client.test.d.ts.map +1 -0
- package/dist/better-auth/auth/tests/server.test.d.ts +2 -0
- package/dist/better-auth/auth/tests/server.test.d.ts.map +1 -0
- package/dist/{chunk-HJ3GTGY7.js → chunk-IERUTUXB.js} +18 -1
- package/dist/chunk-IERUTUXB.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/react-core/index.js +17 -0
- package/dist/react-core/index.js.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/tools/coValues/account.d.ts +1 -0
- package/dist/tools/coValues/account.d.ts.map +1 -1
- package/dist/tools/coValues/coMap.d.ts +10 -0
- package/dist/tools/coValues/coMap.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/zodCo.d.ts +1 -1
- package/dist/tools/testing.d.ts.map +1 -1
- package/package.json +23 -4
- package/src/better-auth/auth/client.ts +169 -0
- package/src/better-auth/auth/react.tsx +105 -0
- package/src/better-auth/auth/server.ts +250 -0
- package/src/better-auth/auth/tests/client.test.ts +249 -0
- package/src/better-auth/auth/tests/server.test.ts +226 -0
- package/src/tools/coValues/account.ts +5 -0
- package/src/tools/coValues/coMap.ts +14 -0
- package/src/tools/implementation/zodSchema/zodCo.ts +1 -1
- package/src/tools/tests/ContextManager.test.ts +2 -2
- package/src/tools/tests/account.test.ts +51 -0
- package/src/tools/tests/coMap.test.ts +99 -0
- package/src/tools/tests/patterns/notifications.test.ts +1 -1
- package/src/tools/tests/testing.test.ts +2 -2
- package/tsup.config.ts +9 -0
- 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
|
+
});
|