@yackey-labs/yauth-client 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.
Files changed (55) hide show
  1. package/dist/index.js +196 -0
  2. package/package.json +30 -0
  3. package/src/bindings/AccountLockoutMessageResponse.ts +3 -0
  4. package/src/bindings/ApiKeyResponse.ts +3 -0
  5. package/src/bindings/AuthConfigResponse.ts +14 -0
  6. package/src/bindings/AuthMethod.ts +3 -0
  7. package/src/bindings/AuthUser.ts +4 -0
  8. package/src/bindings/AuthorizeQuery.ts +3 -0
  9. package/src/bindings/AuthorizeResponse.ts +3 -0
  10. package/src/bindings/BackupCodeCountResponse.ts +3 -0
  11. package/src/bindings/BackupCodesResponse.ts +3 -0
  12. package/src/bindings/BanRequest.ts +3 -0
  13. package/src/bindings/CallbackBody.ts +3 -0
  14. package/src/bindings/ChangePasswordRequest.ts +3 -0
  15. package/src/bindings/ConfirmTotpRequest.ts +3 -0
  16. package/src/bindings/CreateApiKeyRequest.ts +3 -0
  17. package/src/bindings/CreateApiKeyResponse.ts +3 -0
  18. package/src/bindings/CreateWebhookRequest.ts +3 -0
  19. package/src/bindings/ForgotPasswordRequest.ts +3 -0
  20. package/src/bindings/ListSessionsQuery.ts +3 -0
  21. package/src/bindings/ListUsersQuery.ts +3 -0
  22. package/src/bindings/LoginRequest.ts +3 -0
  23. package/src/bindings/MagicLinkMessageResponse.ts +3 -0
  24. package/src/bindings/MagicLinkSendRequest.ts +3 -0
  25. package/src/bindings/MagicLinkVerifyRequest.ts +3 -0
  26. package/src/bindings/MessageResponse.ts +3 -0
  27. package/src/bindings/MfaAuthResponse.ts +3 -0
  28. package/src/bindings/MfaMessageResponse.ts +3 -0
  29. package/src/bindings/OAuthAccountResponse.ts +3 -0
  30. package/src/bindings/OAuthAuthResponse.ts +3 -0
  31. package/src/bindings/PasskeyInfo.ts +3 -0
  32. package/src/bindings/PasskeyLoginBeginRequest.ts +3 -0
  33. package/src/bindings/PasskeyLoginFinishRequest.ts +3 -0
  34. package/src/bindings/RefreshRequest.ts +3 -0
  35. package/src/bindings/RegisterFinishRequest.ts +3 -0
  36. package/src/bindings/RegisterRequest.ts +3 -0
  37. package/src/bindings/RequestUnlockRequest.ts +3 -0
  38. package/src/bindings/ResendVerificationRequest.ts +3 -0
  39. package/src/bindings/ResetPasswordRequest.ts +3 -0
  40. package/src/bindings/RevokeRequest.ts +3 -0
  41. package/src/bindings/SetupTotpResponse.ts +3 -0
  42. package/src/bindings/TokenRequest.ts +7 -0
  43. package/src/bindings/TokenResponse.ts +3 -0
  44. package/src/bindings/UnlockAccountRequest.ts +3 -0
  45. package/src/bindings/UpdateProfileRequest.ts +3 -0
  46. package/src/bindings/UpdateUserRequest.ts +3 -0
  47. package/src/bindings/UpdateWebhookRequest.ts +3 -0
  48. package/src/bindings/VerifyEmailRequest.ts +3 -0
  49. package/src/bindings/VerifyMfaRequest.ts +3 -0
  50. package/src/bindings/WebhookDeliveryResponse.ts +3 -0
  51. package/src/bindings/WebhookDetailResponse.ts +5 -0
  52. package/src/bindings/WebhookResponse.ts +3 -0
  53. package/src/generated.test.ts +337 -0
  54. package/src/generated.ts +385 -0
  55. package/src/index.ts +419 -0
@@ -0,0 +1,337 @@
1
+ import { describe, expect, mock, test } from "bun:test";
2
+ import { createYAuthClient, YAuthError } from "./generated";
3
+
4
+ type MockFetch = ReturnType<typeof mock<(...args: unknown[]) => unknown>>;
5
+
6
+ function mockFetch(status: number, body: unknown = {}) {
7
+ return mock(async () => ({
8
+ ok: status >= 200 && status < 300,
9
+ status,
10
+ text: async () => JSON.stringify(body),
11
+ })) as unknown as typeof fetch;
12
+ }
13
+
14
+ function firstCall(fn: typeof fetch): [string, RequestInit] {
15
+ const c = (fn as unknown as MockFetch).mock.calls[0];
16
+ if (!c) throw new Error("Expected at least one call");
17
+ return c as unknown as [string, RequestInit];
18
+ }
19
+
20
+ function createClient(fetchFn: typeof fetch) {
21
+ return createYAuthClient({
22
+ baseUrl: "http://localhost:3000/auth",
23
+ fetch: fetchFn,
24
+ credentials: "include",
25
+ });
26
+ }
27
+
28
+ describe("createYAuthClient", () => {
29
+ test("getSession sends GET to /session with auth", async () => {
30
+ const user = {
31
+ id: "1",
32
+ email: "test@example.com",
33
+ display_name: "Test",
34
+ email_verified: true,
35
+ role: "user",
36
+ banned: false,
37
+ auth_method: "Session" as const,
38
+ };
39
+ const fetchFn = mockFetch(200, user);
40
+ const client = createClient(fetchFn);
41
+
42
+ const result = await client.getSession();
43
+ expect(result).toEqual(user);
44
+ expect(fetchFn).toHaveBeenCalledTimes(1);
45
+
46
+ const [url, opts] = firstCall(fetchFn);
47
+ expect(url).toBe("http://localhost:3000/auth/session");
48
+ expect(opts.method).toBe("GET");
49
+ expect(opts.credentials).toBe("include");
50
+ });
51
+
52
+ test("logout sends POST to /logout", async () => {
53
+ const fetchFn = mockFetch(200);
54
+ const client = createClient(fetchFn);
55
+
56
+ await client.logout();
57
+ const [url, opts] = firstCall(fetchFn);
58
+ expect(url).toBe("http://localhost:3000/auth/logout");
59
+ expect(opts.method).toBe("POST");
60
+ });
61
+
62
+ test("emailPassword.register sends correct body", async () => {
63
+ const fetchFn = mockFetch(200, { message: "Check your email" });
64
+ const client = createClient(fetchFn);
65
+
66
+ await client.emailPassword.register({
67
+ email: "new@example.com",
68
+ password: "secureP@ss1",
69
+ display_name: null,
70
+ });
71
+
72
+ const [url, opts] = firstCall(fetchFn);
73
+ expect(url).toBe("http://localhost:3000/auth/register");
74
+ expect(opts.method).toBe("POST");
75
+ expect(JSON.parse(opts.body as string)).toEqual({
76
+ email: "new@example.com",
77
+ password: "secureP@ss1",
78
+ display_name: null,
79
+ });
80
+ });
81
+
82
+ test("emailPassword.login sends correct body", async () => {
83
+ const fetchFn = mockFetch(200, {});
84
+ const client = createClient(fetchFn);
85
+
86
+ await client.emailPassword.login({
87
+ email: "user@example.com",
88
+ password: "pass123",
89
+ remember_me: null,
90
+ });
91
+
92
+ const [, opts] = firstCall(fetchFn);
93
+ expect(opts.method).toBe("POST");
94
+ expect(JSON.parse(opts.body as string)).toEqual({
95
+ email: "user@example.com",
96
+ password: "pass123",
97
+ remember_me: null,
98
+ });
99
+ });
100
+
101
+ test("throws YAuthError on non-ok response", async () => {
102
+ const fetchFn = mockFetch(401, { error: "Unauthorized" });
103
+ const client = createClient(fetchFn);
104
+
105
+ try {
106
+ await client.getSession();
107
+ expect(true).toBe(false); // should not reach here
108
+ } catch (e) {
109
+ expect(e).toBeInstanceOf(YAuthError);
110
+ expect((e as YAuthError).status).toBe(401);
111
+ expect((e as YAuthError).message).toBe("Unauthorized");
112
+ }
113
+ });
114
+
115
+ test("YAuthError includes body on JSON error response", async () => {
116
+ const fetchFn = mockFetch(403, {
117
+ error: "Banned",
118
+ details: "Account suspended",
119
+ });
120
+ const client = createClient(fetchFn);
121
+
122
+ try {
123
+ await client.getSession();
124
+ } catch (e) {
125
+ expect((e as YAuthError).body).toEqual({
126
+ error: "Banned",
127
+ details: "Account suspended",
128
+ });
129
+ }
130
+ });
131
+
132
+ test("onError callback is called on error", async () => {
133
+ const fetchFn = mockFetch(500, { error: "Internal" });
134
+ const onError = mock(() => {});
135
+ const client = createYAuthClient({
136
+ baseUrl: "http://localhost:3000/auth",
137
+ fetch: fetchFn,
138
+ onError,
139
+ });
140
+
141
+ try {
142
+ await client.getSession();
143
+ } catch {
144
+ // expected
145
+ }
146
+ expect(onError).toHaveBeenCalledTimes(1);
147
+ });
148
+
149
+ test("bearer token is attached when getToken provided", async () => {
150
+ const fetchFn = mockFetch(200, {});
151
+ const client = createYAuthClient({
152
+ baseUrl: "http://localhost:3000/auth",
153
+ fetch: fetchFn,
154
+ getToken: async () => "my-jwt-token",
155
+ });
156
+
157
+ await client.getSession();
158
+ const [, opts] = firstCall(fetchFn);
159
+ expect((opts.headers as Record<string, string>).Authorization).toBe(
160
+ "Bearer my-jwt-token",
161
+ );
162
+ });
163
+
164
+ test("updateProfile sends PATCH to /me", async () => {
165
+ const fetchFn = mockFetch(200, {});
166
+ const client = createClient(fetchFn);
167
+
168
+ await client.updateProfile({ display_name: "New Name" });
169
+ const [url, opts] = firstCall(fetchFn);
170
+ expect(url).toBe("http://localhost:3000/auth/me");
171
+ expect(opts.method).toBe("PATCH");
172
+ expect(JSON.parse(opts.body as string)).toEqual({
173
+ display_name: "New Name",
174
+ });
175
+ });
176
+ });
177
+
178
+ describe("client API groups", () => {
179
+ test("mfa.setup sends POST to /mfa/totp/setup", async () => {
180
+ const fetchFn = mockFetch(200, {
181
+ otpauth_url: "otpauth://...",
182
+ secret: "BASE32SECRET",
183
+ });
184
+ const client = createClient(fetchFn);
185
+
186
+ await client.mfa.setup();
187
+ const [url, opts] = firstCall(fetchFn);
188
+ expect(url).toBe("http://localhost:3000/auth/mfa/totp/setup");
189
+ expect(opts.method).toBe("POST");
190
+ });
191
+
192
+ test("passkey.list sends GET to /passkeys", async () => {
193
+ const fetchFn = mockFetch(200, []);
194
+ const client = createClient(fetchFn);
195
+
196
+ await client.passkey.list();
197
+ const [url, opts] = firstCall(fetchFn);
198
+ expect(url).toBe("http://localhost:3000/auth/passkeys");
199
+ expect(opts.method).toBe("GET");
200
+ });
201
+
202
+ test("apiKeys.create sends POST with body", async () => {
203
+ const fetchFn = mockFetch(200, { id: "key-1", key: "yauth_..." });
204
+ const client = createClient(fetchFn);
205
+
206
+ await client.apiKeys.create({
207
+ name: "My Key",
208
+ scopes: null,
209
+ expires_in_days: null,
210
+ });
211
+ const [url, opts] = firstCall(fetchFn);
212
+ expect(url).toBe("http://localhost:3000/auth/api-keys");
213
+ expect(opts.method).toBe("POST");
214
+ expect(JSON.parse(opts.body as string)).toEqual({
215
+ name: "My Key",
216
+ scopes: null,
217
+ expires_in_days: null,
218
+ });
219
+ });
220
+
221
+ test("admin.deleteUser sends DELETE", async () => {
222
+ const fetchFn = mockFetch(200);
223
+ const client = createClient(fetchFn);
224
+
225
+ await client.admin.deleteUser("user-123");
226
+ const [url, opts] = firstCall(fetchFn);
227
+ expect(url).toBe("http://localhost:3000/auth/admin/users/user-123");
228
+ expect(opts.method).toBe("DELETE");
229
+ });
230
+
231
+ test("webhooks.create sends POST with body", async () => {
232
+ const fetchFn = mockFetch(200, { id: "wh-1" });
233
+ const client = createClient(fetchFn);
234
+
235
+ await client.webhooks.create({
236
+ url: "https://example.com/hook",
237
+ events: ["user.registered"],
238
+ secret: null,
239
+ });
240
+ const [url, opts] = firstCall(fetchFn);
241
+ expect(url).toBe("http://localhost:3000/auth/webhooks");
242
+ expect(opts.method).toBe("POST");
243
+ });
244
+
245
+ test("oidc.openidConfiguration sends GET", async () => {
246
+ const fetchFn = mockFetch(200, { issuer: "https://auth.example.com" });
247
+ const client = createClient(fetchFn);
248
+
249
+ await client.oidc.openidConfiguration();
250
+ const [url] = firstCall(fetchFn);
251
+ expect(url).toBe(
252
+ "http://localhost:3000/auth/.well-known/openid-configuration",
253
+ );
254
+ });
255
+
256
+ test("bearer.getToken sends POST to /token", async () => {
257
+ const fetchFn = mockFetch(200, {
258
+ access_token: "jwt",
259
+ token_type: "Bearer",
260
+ });
261
+ const client = createClient(fetchFn);
262
+
263
+ await client.bearer.getToken({
264
+ email: "a@b.com",
265
+ password: "pass",
266
+ scope: null,
267
+ });
268
+ const [url, opts] = firstCall(fetchFn);
269
+ expect(url).toBe("http://localhost:3000/auth/token");
270
+ expect(opts.method).toBe("POST");
271
+ });
272
+
273
+ test("magicLink.send sends POST with email", async () => {
274
+ const fetchFn = mockFetch(200, { message: "sent" });
275
+ const client = createClient(fetchFn);
276
+
277
+ await client.magicLink.send({ email: "magic@example.com" });
278
+ const [url, opts] = firstCall(fetchFn);
279
+ expect(url).toBe("http://localhost:3000/auth/magic-link/send");
280
+ expect(JSON.parse(opts.body as string)).toEqual({
281
+ email: "magic@example.com",
282
+ });
283
+ });
284
+
285
+ test("oauth.authorize returns URL string", () => {
286
+ const client = createYAuthClient({
287
+ baseUrl: "http://localhost:3000/auth",
288
+ });
289
+
290
+ const url = client.oauth.authorize("github");
291
+ expect(url).toBe("http://localhost:3000/auth/oauth/github/authorize");
292
+ });
293
+
294
+ test("oauth.authorize appends query params", () => {
295
+ const client = createYAuthClient({
296
+ baseUrl: "http://localhost:3000/auth",
297
+ });
298
+
299
+ const url = client.oauth.authorize("github", {
300
+ redirect_url: "http://localhost:5173/callback",
301
+ });
302
+ expect(url).toContain("redirect_url=");
303
+ });
304
+ });
305
+
306
+ describe("YAuthError", () => {
307
+ test("is an instance of Error", () => {
308
+ const err = new YAuthError("test", 400);
309
+ expect(err).toBeInstanceOf(Error);
310
+ expect(err.name).toBe("YAuthError");
311
+ });
312
+
313
+ test("has status and message", () => {
314
+ const err = new YAuthError("Not Found", 404);
315
+ expect(err.message).toBe("Not Found");
316
+ expect(err.status).toBe(404);
317
+ });
318
+
319
+ test("has optional body", () => {
320
+ const err = new YAuthError("Bad", 400, { details: "missing field" });
321
+ expect(err.body).toEqual({ details: "missing field" });
322
+ });
323
+ });
324
+
325
+ describe("empty response handling", () => {
326
+ test("handles empty response body", async () => {
327
+ const fetchFn = mock(async () => ({
328
+ ok: true,
329
+ status: 204,
330
+ text: async () => "",
331
+ })) as unknown as typeof fetch;
332
+ const client = createClient(fetchFn);
333
+
334
+ const result = await client.logout();
335
+ expect(result).toBeUndefined();
336
+ });
337
+ });