@adakrpos/auth 0.0.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/README.md ADDED
@@ -0,0 +1,221 @@
1
+ # @adakrpos/auth
2
+
3
+ Authentication SDK for Apple Developer Academy @ POSTECH services.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @adakrpos/auth
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ Get an API key from the [ADA Developer Portal](https://ada-kr-pos.com/developer), then:
14
+
15
+ ```typescript
16
+ import { adakrposAuth, getAuth } from '@adakrpos/auth/hono';
17
+
18
+ app.use('*', adakrposAuth({ apiKey: env.ADAKRPOS_API_KEY }));
19
+
20
+ app.get('/protected', async (c) => {
21
+ const auth = await getAuth(c);
22
+ if (!auth.isAuthenticated) return c.json({ error: 'Unauthorized' }, 401);
23
+ return c.json({ user: auth.user });
24
+ });
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Hono
30
+
31
+ ```typescript
32
+ import { Hono } from 'hono';
33
+ import { adakrposAuth, getAuth, requireAuth } from '@adakrpos/auth/hono';
34
+
35
+ const app = new Hono();
36
+
37
+ // Optional auth — check manually
38
+ app.use('*', adakrposAuth({ apiKey: env.ADAKRPOS_API_KEY }));
39
+
40
+ app.get('/profile', async (c) => {
41
+ const auth = await getAuth(c);
42
+ if (!auth.isAuthenticated) return c.json({ error: 'Login required' }, 401);
43
+ return c.json({ user: auth.user });
44
+ });
45
+
46
+ // Required auth — auto 401 if not authenticated
47
+ app.get('/dashboard', requireAuth({ apiKey: env.ADAKRPOS_API_KEY }), (c) => {
48
+ return c.json({ message: 'Welcome!' });
49
+ });
50
+ ```
51
+
52
+ ### Express
53
+
54
+ ```typescript
55
+ import express from 'express';
56
+ import { adakrposAuthExpress, requireAuthExpress } from '@adakrpos/auth/express';
57
+
58
+ const app = express();
59
+
60
+ // Optional auth
61
+ app.use(adakrposAuthExpress({ apiKey: process.env.ADAKRPOS_API_KEY! }));
62
+
63
+ app.get('/profile', async (req, res) => {
64
+ const auth = await req.auth?.();
65
+ if (!auth?.isAuthenticated) return res.status(401).json({ error: 'Unauthorized' });
66
+ res.json({ user: auth.user });
67
+ });
68
+
69
+ // Required auth
70
+ app.get('/dashboard', requireAuthExpress({ apiKey: process.env.ADAKRPOS_API_KEY! }), (req, res) => {
71
+ res.json({ message: 'Welcome!' });
72
+ });
73
+ ```
74
+
75
+ ### Generic (Web Standard Request)
76
+
77
+ Works with Cloudflare Workers, Deno, Bun, and any Web standard environment:
78
+
79
+ ```typescript
80
+ import { verifyRequest } from '@adakrpos/auth/generic';
81
+
82
+ export default {
83
+ async fetch(request: Request, env: Env) {
84
+ const auth = await verifyRequest(request, { apiKey: env.ADAKRPOS_API_KEY });
85
+ if (!auth.isAuthenticated) {
86
+ return new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });
87
+ }
88
+ return new Response(JSON.stringify({ user: auth.user }));
89
+ }
90
+ };
91
+ ```
92
+
93
+ ## API Reference
94
+
95
+ ### `adakrposAuth(config)` — Hono middleware
96
+
97
+ Attaches a lazy `auth` function to the Hono context. Call `getAuth(c)` in your handler to resolve it.
98
+
99
+ | Parameter | Type | Default | Description |
100
+ |-----------|------|---------|-------------|
101
+ | `apiKey` | `string` | required | Your API key from the developer portal |
102
+ | `authUrl` | `string` | `https://ada-kr-pos.com` | Auth server URL (for self-hosting) |
103
+
104
+ ### `getAuth(c)` — Hono helper
105
+
106
+ ```typescript
107
+ const auth = await getAuth(c); // AuthContext
108
+ ```
109
+
110
+ Returns the `AuthContext` for the current request. Must be called after `adakrposAuth` middleware.
111
+
112
+ ### `requireAuth(config)` — Hono middleware
113
+
114
+ Drop-in middleware that returns `401` automatically if the request isn't authenticated. No need to call `getAuth` manually.
115
+
116
+ ### `adakrposAuthExpress(config)` — Express middleware
117
+
118
+ Attaches `req.auth()` as a lazy async function. Call it in your handler to get the `AuthContext`.
119
+
120
+ ### `requireAuthExpress(config)` — Express middleware
121
+
122
+ Same as `adakrposAuthExpress`, but automatically returns `401` if not authenticated.
123
+
124
+ ### `verifyRequest(request, config)` — Generic helper
125
+
126
+ ```typescript
127
+ const auth = await verifyRequest(request, { apiKey: env.ADAKRPOS_API_KEY });
128
+ ```
129
+
130
+ Takes a Web standard `Request` and returns `AuthContext`. No middleware needed.
131
+
132
+ ### `createAdakrposAuth(config)`
133
+
134
+ Creates a raw auth client for advanced use cases.
135
+
136
+ ```typescript
137
+ import { createAdakrposAuth } from '@adakrpos/auth';
138
+
139
+ const client = createAdakrposAuth({ apiKey: env.ADAKRPOS_API_KEY });
140
+ ```
141
+
142
+ Returns an `AdakrposAuthClient` with these methods:
143
+
144
+ | Method | Returns | Description |
145
+ |--------|---------|-------------|
146
+ | `verifySession(sessionId)` | `Promise<{user, session} \| null>` | Verify a session ID directly |
147
+ | `getUser(userId)` | `Promise<AdakrposUser \| null>` | Fetch a user by ID |
148
+ | `getCurrentUser(sessionId)` | `Promise<AdakrposUser \| null>` | Get the user from a session |
149
+
150
+ ### Types
151
+
152
+ ```typescript
153
+ interface AdakrposUser {
154
+ id: string;
155
+ email: string | null; // Apple email
156
+ verifiedEmail: string | null; // @pos.idserve.net email
157
+ nickname: string | null;
158
+ name: string | null;
159
+ profilePhotoUrl: string | null;
160
+ bio: string | null;
161
+ contact: string | null;
162
+ snsLinks: Record<string, string>;
163
+ isVerified: boolean; // true if @pos.idserve.net verified
164
+ createdAt: number; // Unix ms
165
+ updatedAt: number; // Unix ms
166
+ }
167
+
168
+ interface AdakrposSession {
169
+ id: string;
170
+ userId: string;
171
+ expiresAt: number; // Unix ms
172
+ createdAt: number; // Unix ms
173
+ }
174
+
175
+ type AuthContext = AdakrposAuthContext | AdakrposUnauthContext;
176
+
177
+ interface AdakrposAuthContext {
178
+ user: AdakrposUser;
179
+ session: AdakrposSession;
180
+ isAuthenticated: true;
181
+ }
182
+
183
+ interface AdakrposUnauthContext {
184
+ user: null;
185
+ session: null;
186
+ isAuthenticated: false;
187
+ }
188
+ ```
189
+
190
+ ## Caching
191
+
192
+ API key validation results are cached in-memory for 30 seconds to reduce latency. This means:
193
+
194
+ - First request: validates with auth server (~50ms)
195
+ - Subsequent requests within 30s: instant (cached)
196
+ - After 30s: re-validates with auth server
197
+
198
+ You can clear the cache manually if needed:
199
+
200
+ ```typescript
201
+ import { clearApiKeyCache } from '@adakrpos/auth';
202
+
203
+ clearApiKeyCache();
204
+ ```
205
+
206
+ ## FAQ
207
+
208
+ **Q: Where do I get an API key?**
209
+ Log in at [ada-kr-pos.com](https://ada-kr-pos.com) with your @pos.idserve.net email, then visit the [Developer Portal](https://ada-kr-pos.com/developer).
210
+
211
+ **Q: Can I use this on the client side?**
212
+ No. API keys must stay server-side only. Never expose your API key in browser code.
213
+
214
+ **Q: What happens when a session expires?**
215
+ `auth.isAuthenticated` will be `false`. Redirect the user to `https://ada-kr-pos.com/login`.
216
+
217
+ **Q: Does this work with Cloudflare Workers?**
218
+ Yes. Use `@adakrpos/auth/generic` with `verifyRequest`, or `@adakrpos/auth/hono` if you're using Hono.
219
+
220
+ **Q: What's the difference between `adakrposAuth` and `requireAuth`?**
221
+ `adakrposAuth` is optional auth: it attaches the auth context but lets unauthenticated requests through. `requireAuth` blocks unauthenticated requests with a `401` automatically.
@@ -0,0 +1,7 @@
1
+ import { describe, it, expect } from "vitest";
2
+
3
+ describe("Auth SDK", () => {
4
+ it("SDK test infrastructure is set up", () => {
5
+ expect(true).toBe(true);
6
+ });
7
+ });
@@ -0,0 +1,206 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { createAdakrposAuth } from "../src/client";
4
+ import {
5
+ clearApiKeyCache,
6
+ getCachedApiKeyValidity,
7
+ setCachedApiKeyValidity,
8
+ } from "../src/cache";
9
+
10
+ describe("createAdakrposAuth", () => {
11
+ beforeEach(() => {
12
+ clearApiKeyCache();
13
+ });
14
+
15
+ afterEach(() => {
16
+ clearApiKeyCache();
17
+ vi.restoreAllMocks();
18
+ });
19
+
20
+ it("returns a client with verifySession, getUser, and getCurrentUser", () => {
21
+ const client = createAdakrposAuth({ apiKey: "ak_test" });
22
+
23
+ expect(client.verifySession).toBeTypeOf("function");
24
+ expect(client.getUser).toBeTypeOf("function");
25
+ expect(client.getCurrentUser).toBeTypeOf("function");
26
+ });
27
+
28
+ it("sends verifySession requests to the SDK verify endpoint", async () => {
29
+ const fetchSpy = vi.spyOn(global, "fetch").mockResolvedValue(
30
+ new Response(JSON.stringify({ user: null, session: null }), { status: 404 }),
31
+ );
32
+ const client = createAdakrposAuth({
33
+ apiKey: "ak_test",
34
+ authUrl: "https://example.com",
35
+ });
36
+
37
+ await client.verifySession("session_123");
38
+
39
+ expect(fetchSpy).toHaveBeenCalledWith(
40
+ "https://example.com/api/sdk/verify-session",
41
+ expect.objectContaining({
42
+ method: "POST",
43
+ body: JSON.stringify({ sessionId: "session_123" }),
44
+ headers: expect.objectContaining({
45
+ Authorization: "Bearer ak_test",
46
+ "Content-Type": "application/json",
47
+ }),
48
+ }),
49
+ );
50
+ });
51
+
52
+ it("returns null when verifySession receives a 401 response", async () => {
53
+ vi.spyOn(global, "fetch").mockResolvedValue(new Response(null, { status: 401 }));
54
+ const client = createAdakrposAuth({ apiKey: "ak_test" });
55
+
56
+ await expect(client.verifySession("session_123")).resolves.toBeNull();
57
+ expect(getCachedApiKeyValidity("ak_test")).toBe(false);
58
+ });
59
+
60
+ it("returns user and session data when verifySession succeeds", async () => {
61
+ const payload = {
62
+ user: {
63
+ id: "user_123",
64
+ email: "user@example.com",
65
+ verifiedEmail: "verified@example.com",
66
+ nickname: "ada",
67
+ name: "Ada",
68
+ profilePhotoUrl: null,
69
+ bio: null,
70
+ contact: null,
71
+ snsLinks: {},
72
+ isVerified: true,
73
+ createdAt: 1,
74
+ updatedAt: 2,
75
+ },
76
+ session: {
77
+ id: "session_123",
78
+ userId: "user_123",
79
+ expiresAt: 10,
80
+ createdAt: 5,
81
+ },
82
+ };
83
+ vi.spyOn(global, "fetch").mockResolvedValue(
84
+ new Response(JSON.stringify(payload), { status: 200 }),
85
+ );
86
+ const client = createAdakrposAuth({ apiKey: "ak_test" });
87
+
88
+ await expect(client.verifySession("session_123")).resolves.toEqual(payload);
89
+ expect(getCachedApiKeyValidity("ak_test")).toBe(true);
90
+ });
91
+
92
+ it("sends getUser requests to the SDK user endpoint", async () => {
93
+ const fetchSpy = vi.spyOn(global, "fetch").mockResolvedValue(
94
+ new Response(null, { status: 404 }),
95
+ );
96
+ const client = createAdakrposAuth({
97
+ apiKey: "ak_test",
98
+ authUrl: "https://example.com/base",
99
+ });
100
+
101
+ await client.getUser("user_123");
102
+
103
+ expect(fetchSpy).toHaveBeenCalledWith(
104
+ "https://example.com/api/sdk/users/user_123",
105
+ expect.objectContaining({
106
+ method: "GET",
107
+ headers: expect.objectContaining({
108
+ Authorization: "Bearer ak_test",
109
+ }),
110
+ }),
111
+ );
112
+ });
113
+
114
+ it("returns null when getUser receives a 404 response", async () => {
115
+ vi.spyOn(global, "fetch").mockResolvedValue(new Response(null, { status: 404 }));
116
+ const client = createAdakrposAuth({ apiKey: "ak_test" });
117
+
118
+ await expect(client.getUser("missing_user")).resolves.toBeNull();
119
+ expect(getCachedApiKeyValidity("ak_test")).toBe(true);
120
+ });
121
+
122
+ it("returns the authenticated user from getCurrentUser", async () => {
123
+ vi.spyOn(global, "fetch").mockResolvedValue(
124
+ new Response(
125
+ JSON.stringify({
126
+ user: {
127
+ id: "user_123",
128
+ email: null,
129
+ verifiedEmail: null,
130
+ nickname: null,
131
+ name: "Ada",
132
+ profilePhotoUrl: null,
133
+ bio: null,
134
+ contact: null,
135
+ snsLinks: {},
136
+ isVerified: false,
137
+ createdAt: 1,
138
+ updatedAt: 2,
139
+ },
140
+ session: {
141
+ id: "session_123",
142
+ userId: "user_123",
143
+ expiresAt: 10,
144
+ createdAt: 5,
145
+ },
146
+ }),
147
+ { status: 200 },
148
+ ),
149
+ );
150
+ const client = createAdakrposAuth({ apiKey: "ak_test" });
151
+
152
+ await expect(client.getCurrentUser("session_123")).resolves.toEqual(
153
+ expect.objectContaining({ id: "user_123", name: "Ada" }),
154
+ );
155
+ });
156
+
157
+ it("skips duplicate requests after caching an invalid API key", async () => {
158
+ const fetchSpy = vi.spyOn(global, "fetch").mockResolvedValue(
159
+ new Response(null, { status: 401 }),
160
+ );
161
+ const client = createAdakrposAuth({ apiKey: "ak_test" });
162
+
163
+ await expect(client.verifySession("session_123")).resolves.toBeNull();
164
+ await expect(client.verifySession("session_123")).resolves.toBeNull();
165
+
166
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
167
+ });
168
+ });
169
+
170
+ describe("API key cache", () => {
171
+ beforeEach(() => {
172
+ clearApiKeyCache();
173
+ });
174
+
175
+ afterEach(() => {
176
+ clearApiKeyCache();
177
+ vi.restoreAllMocks();
178
+ });
179
+
180
+ it("returns null for unknown API keys", () => {
181
+ expect(getCachedApiKeyValidity("unknown")).toBeNull();
182
+ });
183
+
184
+ it("stores and retrieves cached API key validity", () => {
185
+ setCachedApiKeyValidity("ak_test", true);
186
+
187
+ expect(getCachedApiKeyValidity("ak_test")).toBe(true);
188
+ });
189
+
190
+ it("returns null when a cache entry expires", () => {
191
+ vi.useFakeTimers();
192
+ setCachedApiKeyValidity("ak_test", true, 100);
193
+
194
+ vi.advanceTimersByTime(101);
195
+
196
+ expect(getCachedApiKeyValidity("ak_test")).toBeNull();
197
+ vi.useRealTimers();
198
+ });
199
+
200
+ it("clears all cached API key entries", () => {
201
+ setCachedApiKeyValidity("ak_test", true);
202
+ clearApiKeyCache();
203
+
204
+ expect(getCachedApiKeyValidity("ak_test")).toBeNull();
205
+ });
206
+ });
@@ -0,0 +1,241 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import { clearApiKeyCache } from "../src/cache";
4
+ import { adakrposAuthExpress, requireAuthExpress } from "../src/express";
5
+ import { verifyRequest } from "../src/generic";
6
+
7
+ const config = {
8
+ apiKey: "ak_test",
9
+ authUrl: "https://example.com",
10
+ };
11
+
12
+ const validSession = {
13
+ user: {
14
+ id: "user_123",
15
+ email: "user@example.com",
16
+ verifiedEmail: "verified@example.com",
17
+ nickname: "ada",
18
+ name: "Ada Lovelace",
19
+ profilePhotoUrl: null,
20
+ bio: null,
21
+ contact: null,
22
+ snsLinks: {},
23
+ isVerified: true,
24
+ createdAt: 1,
25
+ updatedAt: 2,
26
+ },
27
+ session: {
28
+ id: "session_123",
29
+ userId: "user_123",
30
+ expiresAt: 10,
31
+ createdAt: 5,
32
+ },
33
+ };
34
+
35
+ describe("Express middleware", () => {
36
+ beforeEach(() => {
37
+ clearApiKeyCache();
38
+ });
39
+
40
+ afterEach(() => {
41
+ clearApiKeyCache();
42
+ vi.restoreAllMocks();
43
+ });
44
+
45
+ it("attaches auth function to req", async () => {
46
+ const middleware = adakrposAuthExpress(config);
47
+ const req = { headers: {} };
48
+ const res = {};
49
+ let nextCalled = false;
50
+
51
+ await middleware(req, res, () => {
52
+ nextCalled = true;
53
+ });
54
+
55
+ expect(nextCalled).toBe(true);
56
+ expect(typeof req.auth).toBe("function");
57
+ });
58
+
59
+ it("returns an unauthenticated context when no session cookie is present", async () => {
60
+ const fetchSpy = vi.spyOn(global, "fetch");
61
+ const middleware = adakrposAuthExpress(config);
62
+ const req = { headers: {} };
63
+ const res = {};
64
+
65
+ await middleware(req, res, () => {});
66
+
67
+ const authContext = await req.auth();
68
+
69
+ expect(authContext).toEqual({
70
+ user: null,
71
+ session: null,
72
+ isAuthenticated: false,
73
+ });
74
+ expect(fetchSpy).not.toHaveBeenCalled();
75
+ });
76
+
77
+ it("returns an authenticated context when the session is valid", async () => {
78
+ const fetchSpy = vi.spyOn(global, "fetch").mockResolvedValue(
79
+ new Response(JSON.stringify(validSession), { status: 200 }),
80
+ );
81
+ const middleware = adakrposAuthExpress(config);
82
+ const req = { headers: { cookie: "adakrpos_session=session_123" } };
83
+ const res = {};
84
+
85
+ await middleware(req, res, () => {});
86
+
87
+ const authContext = await req.auth();
88
+
89
+ expect(authContext).toEqual({
90
+ ...validSession,
91
+ isAuthenticated: true,
92
+ });
93
+ expect(fetchSpy).toHaveBeenCalledWith(
94
+ "https://example.com/api/sdk/verify-session",
95
+ expect.objectContaining({
96
+ method: "POST",
97
+ body: JSON.stringify({ sessionId: "session_123" }),
98
+ }),
99
+ );
100
+ });
101
+
102
+ it("returns 401 when auth is required and no session exists", async () => {
103
+ const middleware = requireAuthExpress(config);
104
+ const req = { headers: {} };
105
+ const res = {
106
+ status: vi.fn().mockReturnThis(),
107
+ json: vi.fn().mockReturnValue({}),
108
+ };
109
+
110
+ await middleware(req, res, () => {});
111
+
112
+ expect(res.status).toHaveBeenCalledWith(401);
113
+ expect(res.json).toHaveBeenCalledWith({ error: "Unauthorized" });
114
+ });
115
+
116
+ it("allows authenticated requests through requireAuthExpress", async () => {
117
+ const fetchSpy = vi.spyOn(global, "fetch").mockResolvedValue(
118
+ new Response(JSON.stringify(validSession), { status: 200 }),
119
+ );
120
+ const middleware = requireAuthExpress(config);
121
+ const req = { headers: { cookie: "adakrpos_session=session_123" } };
122
+ const res = {
123
+ status: vi.fn().mockReturnThis(),
124
+ json: vi.fn(),
125
+ };
126
+ let nextCalled = false;
127
+
128
+ await middleware(req, res, () => {
129
+ nextCalled = true;
130
+ });
131
+
132
+ expect(nextCalled).toBe(true);
133
+ expect(res.status).not.toHaveBeenCalled();
134
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
135
+ });
136
+
137
+ it("does not call the auth server until auth is invoked", async () => {
138
+ const fetchSpy = vi.spyOn(global, "fetch").mockResolvedValue(
139
+ new Response(JSON.stringify(validSession), { status: 200 }),
140
+ );
141
+ const middleware = adakrposAuthExpress(config);
142
+ const req = { headers: { cookie: "adakrpos_session=session_123" } };
143
+ const res = {};
144
+
145
+ await middleware(req, res, () => {});
146
+
147
+ expect(fetchSpy).not.toHaveBeenCalled();
148
+
149
+ await req.auth();
150
+
151
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
152
+ });
153
+
154
+ it("caches auth result on subsequent calls", async () => {
155
+ const fetchSpy = vi.spyOn(global, "fetch").mockResolvedValue(
156
+ new Response(JSON.stringify(validSession), { status: 200 }),
157
+ );
158
+ const middleware = adakrposAuthExpress(config);
159
+ const req = { headers: { cookie: "adakrpos_session=session_123" } };
160
+ const res = {};
161
+
162
+ await middleware(req, res, () => {});
163
+
164
+ await req.auth();
165
+ await req.auth();
166
+ await req.auth();
167
+
168
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
169
+ });
170
+ });
171
+
172
+ describe("Generic verifyRequest helper", () => {
173
+ beforeEach(() => {
174
+ clearApiKeyCache();
175
+ });
176
+
177
+ afterEach(() => {
178
+ clearApiKeyCache();
179
+ vi.restoreAllMocks();
180
+ });
181
+
182
+ it("returns an unauthenticated context when no session cookie is present", async () => {
183
+ const fetchSpy = vi.spyOn(global, "fetch");
184
+ const request = new Request("http://localhost/", {
185
+ headers: {},
186
+ });
187
+
188
+ const authContext = await verifyRequest(request, config);
189
+
190
+ expect(authContext).toEqual({
191
+ user: null,
192
+ session: null,
193
+ isAuthenticated: false,
194
+ });
195
+ expect(fetchSpy).not.toHaveBeenCalled();
196
+ });
197
+
198
+ it("returns an authenticated context when the session is valid", async () => {
199
+ const fetchSpy = vi.spyOn(global, "fetch").mockResolvedValue(
200
+ new Response(JSON.stringify(validSession), { status: 200 }),
201
+ );
202
+ const request = new Request("http://localhost/", {
203
+ headers: { Cookie: "adakrpos_session=session_123" },
204
+ });
205
+
206
+ const authContext = await verifyRequest(request, config);
207
+
208
+ expect(authContext).toEqual({
209
+ ...validSession,
210
+ isAuthenticated: true,
211
+ });
212
+ expect(fetchSpy).toHaveBeenCalledWith(
213
+ "https://example.com/api/sdk/verify-session",
214
+ expect.objectContaining({
215
+ method: "POST",
216
+ body: JSON.stringify({ sessionId: "session_123" }),
217
+ }),
218
+ );
219
+ });
220
+
221
+ it("handles URL-encoded session IDs", async () => {
222
+ const fetchSpy = vi.spyOn(global, "fetch").mockResolvedValue(
223
+ new Response(JSON.stringify(validSession), { status: 200 }),
224
+ );
225
+ const encodedSessionId = encodeURIComponent("session_with_special_chars=123");
226
+ const request = new Request("http://localhost/", {
227
+ headers: { Cookie: `adakrpos_session=${encodedSessionId}` },
228
+ });
229
+
230
+ const authContext = await verifyRequest(request, config);
231
+
232
+ expect(authContext.isAuthenticated).toBe(true);
233
+ expect(fetchSpy).toHaveBeenCalledWith(
234
+ "https://example.com/api/sdk/verify-session",
235
+ expect.objectContaining({
236
+ method: "POST",
237
+ body: JSON.stringify({ sessionId: "session_with_special_chars=123" }),
238
+ }),
239
+ );
240
+ });
241
+ });