@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 +221 -0
- package/__tests__/basic.test.ts +7 -0
- package/__tests__/client.test.ts +206 -0
- package/__tests__/express.test.ts +241 -0
- package/__tests__/hono.test.ts +173 -0
- package/dist/client-Dd5DjxzG.d.mts +64 -0
- package/dist/client-Dd5DjxzG.d.ts +64 -0
- package/dist/express.d.mts +13 -0
- package/dist/express.d.ts +13 -0
- package/dist/express.js +173 -0
- package/dist/express.mjs +145 -0
- package/dist/generic.d.mts +5 -0
- package/dist/generic.d.ts +5 -0
- package/dist/generic.js +144 -0
- package/dist/generic.mjs +117 -0
- package/dist/hono.d.mts +17 -0
- package/dist/hono.d.ts +17 -0
- package/dist/hono.js +182 -0
- package/dist/hono.mjs +155 -0
- package/dist/index.d.mts +7 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +118 -0
- package/dist/index.mjs +88 -0
- package/package.json +48 -0
- package/src/cache.ts +38 -0
- package/src/client.ts +93 -0
- package/src/express.ts +94 -0
- package/src/generic.ts +50 -0
- package/src/hono.ts +100 -0
- package/src/index.ts +8 -0
- package/src/types.ts +54 -0
- package/tsconfig.json +13 -0
- package/tsup.config.ts +15 -0
- package/vitest.config.ts +7 -0
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,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
|
+
});
|