@japikey/cloudflare 0.1.0 → 0.2.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.
- package/LICENSE +21 -0
- package/dist/d1.d.ts +22 -0
- package/dist/d1.d.ts.map +1 -0
- package/dist/d1.js +143 -0
- package/{src/index.ts → dist/index.d.ts} +2 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/router.d.ts +19 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +195 -0
- package/package.json +7 -2
- package/eslint.config.js +0 -3
- package/src/d1.ts +0 -185
- package/src/router.ts +0 -280
- package/test/index.test.ts +0 -14
- package/test/router.test.ts +0 -1120
- package/tsconfig.json +0 -9
package/src/router.ts
DELETED
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import { NotFoundError, JapikeyError } from '@japikey/shared';
|
|
2
|
-
import type { DatabaseDriver, JSONWebKeySet } from '@japikey/japikey';
|
|
3
|
-
import { createApiKey } from '@japikey/japikey';
|
|
4
|
-
import { validate as validateUuid } from 'uuid';
|
|
5
|
-
import type { ExportedHandler } from '@cloudflare/workers-types';
|
|
6
|
-
import { pathToRegexp } from 'path-to-regexp';
|
|
7
|
-
|
|
8
|
-
export type CreateApiKeyData = {
|
|
9
|
-
expiresAt: Date;
|
|
10
|
-
claims: Record<string, unknown>;
|
|
11
|
-
databaseMetadata: Record<string, unknown>;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export type CreateApiKeyRouterOptions<Env> = {
|
|
15
|
-
getUserId: (request: Request, env: Env) => Promise<string>;
|
|
16
|
-
parseCreateApiKeyRequest: (
|
|
17
|
-
request: Request,
|
|
18
|
-
env: Env
|
|
19
|
-
) => Promise<CreateApiKeyData>;
|
|
20
|
-
issuer: URL;
|
|
21
|
-
aud: string;
|
|
22
|
-
db: DatabaseDriver;
|
|
23
|
-
routePrefix: string;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
function wrapError<Args extends unknown[]>(
|
|
27
|
-
fn: (...args: Args) => Promise<Response>
|
|
28
|
-
): (...args: Args) => Promise<Response> {
|
|
29
|
-
return async (...args: Args) => {
|
|
30
|
-
try {
|
|
31
|
-
return await fn(...args);
|
|
32
|
-
} catch (err) {
|
|
33
|
-
if (err instanceof JapikeyError) {
|
|
34
|
-
return new Response(
|
|
35
|
-
JSON.stringify({
|
|
36
|
-
error: {
|
|
37
|
-
type: err.errorType,
|
|
38
|
-
message: err.message,
|
|
39
|
-
},
|
|
40
|
-
}),
|
|
41
|
-
{
|
|
42
|
-
status: err.code,
|
|
43
|
-
headers: {
|
|
44
|
-
'Content-Type': 'application/json',
|
|
45
|
-
},
|
|
46
|
-
}
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
throw err;
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function handleJWKSRequest(
|
|
55
|
-
kid: string,
|
|
56
|
-
db: DatabaseDriver
|
|
57
|
-
): Promise<Response> {
|
|
58
|
-
const row = await db.getApiKey(kid);
|
|
59
|
-
if (!row || row.revoked) {
|
|
60
|
-
throw new NotFoundError('API key not found');
|
|
61
|
-
}
|
|
62
|
-
const jwks: JSONWebKeySet = {
|
|
63
|
-
keys: [row.jwk],
|
|
64
|
-
};
|
|
65
|
-
return new Response(JSON.stringify(jwks), {
|
|
66
|
-
headers: {
|
|
67
|
-
'Content-Type': 'application/json',
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function getWellKnownKid(request: Request, baseIssuer: URL): string {
|
|
73
|
-
if (!request.url.startsWith(baseIssuer.toString())) {
|
|
74
|
-
throw new NotFoundError('Invalid JWKS request');
|
|
75
|
-
}
|
|
76
|
-
let partialPath = request.url.slice(baseIssuer.toString().length);
|
|
77
|
-
if (!partialPath.startsWith('/')) {
|
|
78
|
-
partialPath = `/${partialPath}`;
|
|
79
|
-
}
|
|
80
|
-
const parts = partialPath.split('/');
|
|
81
|
-
if (
|
|
82
|
-
parts.length !== 4 ||
|
|
83
|
-
parts[0] !== '' ||
|
|
84
|
-
parts[2] !== '.well-known' ||
|
|
85
|
-
parts[3] !== 'jwks.json'
|
|
86
|
-
) {
|
|
87
|
-
throw new NotFoundError('Invalid JWKS request');
|
|
88
|
-
}
|
|
89
|
-
const kid = parts[1];
|
|
90
|
-
if (!kid || !validateUuid(kid)) {
|
|
91
|
-
throw new NotFoundError('Invalid JWKS request');
|
|
92
|
-
}
|
|
93
|
-
return kid;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function createJWKSRouter<Env>(
|
|
97
|
-
baseIssuer: URL,
|
|
98
|
-
db: DatabaseDriver
|
|
99
|
-
): ExportedHandler<Env> {
|
|
100
|
-
return {
|
|
101
|
-
fetch: wrapError(async function fetch(
|
|
102
|
-
request: Request,
|
|
103
|
-
env: Env
|
|
104
|
-
): Promise<Response> {
|
|
105
|
-
const kid = getWellKnownKid(request, baseIssuer);
|
|
106
|
-
return handleJWKSRequest(kid, db);
|
|
107
|
-
}),
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function isJWKSPath(request: Request, baseIssuer: URL): boolean {
|
|
112
|
-
try {
|
|
113
|
-
getWellKnownKid(request, baseIssuer);
|
|
114
|
-
return true;
|
|
115
|
-
} catch (err) {
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function matchPath(
|
|
121
|
-
request: Request,
|
|
122
|
-
method: string,
|
|
123
|
-
routePrefix: string,
|
|
124
|
-
pattern: string
|
|
125
|
-
): { valid: boolean; params: Record<string, unknown> } {
|
|
126
|
-
if (request.method !== method) {
|
|
127
|
-
return { valid: false, params: {} };
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const url = new URL(request.url);
|
|
131
|
-
const pathname = url.pathname;
|
|
132
|
-
|
|
133
|
-
const normalizedPrefix =
|
|
134
|
-
routePrefix === ''
|
|
135
|
-
? ''
|
|
136
|
-
: routePrefix.startsWith('/')
|
|
137
|
-
? routePrefix
|
|
138
|
-
: `/${routePrefix}`;
|
|
139
|
-
const fullPattern = `${normalizedPrefix}${pattern}`;
|
|
140
|
-
const regexp = pathToRegexp(fullPattern);
|
|
141
|
-
const match = regexp.regexp.exec(pathname);
|
|
142
|
-
|
|
143
|
-
if (!match) {
|
|
144
|
-
return { valid: false, params: {} };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
const params: Record<string, unknown> = {};
|
|
148
|
-
regexp.keys.forEach((key, index) => {
|
|
149
|
-
params[key.name] = match[index + 1];
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
return { valid: true, params };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function ensureKid(params: Record<string, unknown>): string {
|
|
156
|
-
const kid = params.kid ?? params.id;
|
|
157
|
-
if (!kid || typeof kid !== 'string' || !validateUuid(kid)) {
|
|
158
|
-
throw new NotFoundError('Invalid API key ID');
|
|
159
|
-
}
|
|
160
|
-
return kid;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function handleCreateApiKeyRequest<Env>(
|
|
164
|
-
request: Request,
|
|
165
|
-
env: Env,
|
|
166
|
-
options: CreateApiKeyRouterOptions<Env>
|
|
167
|
-
): Promise<Response> {
|
|
168
|
-
const userId = await options.getUserId(request, env);
|
|
169
|
-
const { expiresAt, claims, databaseMetadata } =
|
|
170
|
-
await options.parseCreateApiKeyRequest(request, env);
|
|
171
|
-
const { jwt, jwk } = await createApiKey(claims, {
|
|
172
|
-
sub: userId,
|
|
173
|
-
iss: options.issuer,
|
|
174
|
-
aud: options.aud,
|
|
175
|
-
expiresAt,
|
|
176
|
-
});
|
|
177
|
-
const kid = jwk.kid as string;
|
|
178
|
-
await options.db.insertApiKey({
|
|
179
|
-
kid,
|
|
180
|
-
user_id: userId,
|
|
181
|
-
revoked: false,
|
|
182
|
-
jwk,
|
|
183
|
-
metadata: databaseMetadata,
|
|
184
|
-
});
|
|
185
|
-
return new Response(JSON.stringify({ api_key: jwt, kid }), {
|
|
186
|
-
headers: {
|
|
187
|
-
'Content-Type': 'application/json',
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async function handleListApiKeysRequest<Env>(
|
|
193
|
-
request: Request,
|
|
194
|
-
env: Env,
|
|
195
|
-
options: CreateApiKeyRouterOptions<Env>
|
|
196
|
-
): Promise<Response> {
|
|
197
|
-
const userId = await options.getUserId(request, env);
|
|
198
|
-
const apiKeys = await options.db.findApiKeys(userId);
|
|
199
|
-
return new Response(JSON.stringify(apiKeys), {
|
|
200
|
-
headers: {
|
|
201
|
-
'Content-Type': 'application/json',
|
|
202
|
-
},
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async function handleGetApiKeyRequest<Env>(
|
|
207
|
-
request: Request,
|
|
208
|
-
env: Env,
|
|
209
|
-
options: CreateApiKeyRouterOptions<Env>,
|
|
210
|
-
keyId: string
|
|
211
|
-
): Promise<Response> {
|
|
212
|
-
const userId = await options.getUserId(request, env);
|
|
213
|
-
const apiKey = await options.db.getApiKey(keyId);
|
|
214
|
-
if (!apiKey || apiKey.user_id !== userId) {
|
|
215
|
-
throw new NotFoundError('API key not found');
|
|
216
|
-
}
|
|
217
|
-
return new Response(JSON.stringify(apiKey), {
|
|
218
|
-
headers: {
|
|
219
|
-
'Content-Type': 'application/json',
|
|
220
|
-
},
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
async function handleRevokeApiKeyRequest<Env>(
|
|
225
|
-
request: Request,
|
|
226
|
-
env: Env,
|
|
227
|
-
options: CreateApiKeyRouterOptions<Env>,
|
|
228
|
-
keyId: string
|
|
229
|
-
): Promise<Response> {
|
|
230
|
-
const userId = await options.getUserId(request, env);
|
|
231
|
-
const apiKey = await options.db.getApiKey(keyId);
|
|
232
|
-
if (!apiKey || apiKey.user_id !== userId) {
|
|
233
|
-
throw new NotFoundError('API key not found');
|
|
234
|
-
}
|
|
235
|
-
await options.db.revokeApiKey({ user_id: userId, kid: keyId });
|
|
236
|
-
return new Response(JSON.stringify({}), {
|
|
237
|
-
headers: {
|
|
238
|
-
'Content-Type': 'application/json',
|
|
239
|
-
},
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export function createApiKeyRouter<Env>(
|
|
244
|
-
options: CreateApiKeyRouterOptions<Env>
|
|
245
|
-
): ExportedHandler<Env> {
|
|
246
|
-
return {
|
|
247
|
-
fetch: wrapError(async function fetch(
|
|
248
|
-
request: Request,
|
|
249
|
-
env: Env
|
|
250
|
-
): Promise<Response> {
|
|
251
|
-
// POST /api-keys - Create API key
|
|
252
|
-
let matchResult = matchPath(request, 'POST', options.routePrefix, '');
|
|
253
|
-
if (matchResult.valid) {
|
|
254
|
-
return handleCreateApiKeyRequest(request, env, options);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// GET /api-keys/my - List user's API keys
|
|
258
|
-
matchResult = matchPath(request, 'GET', options.routePrefix, '/my');
|
|
259
|
-
if (matchResult.valid) {
|
|
260
|
-
return handleListApiKeysRequest(request, env, options);
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// GET /api-keys/:id - Get specific API key
|
|
264
|
-
matchResult = matchPath(request, 'GET', options.routePrefix, '/:id');
|
|
265
|
-
if (matchResult.valid) {
|
|
266
|
-
const keyId = ensureKid(matchResult.params);
|
|
267
|
-
return handleGetApiKeyRequest(request, env, options, keyId);
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// DELETE /api-keys/:id - Revoke API key
|
|
271
|
-
matchResult = matchPath(request, 'DELETE', options.routePrefix, '/:id');
|
|
272
|
-
if (matchResult.valid) {
|
|
273
|
-
const keyId = ensureKid(matchResult.params);
|
|
274
|
-
return handleRevokeApiKeyRequest(request, env, options, keyId);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
throw new NotFoundError('Route not found');
|
|
278
|
-
}),
|
|
279
|
-
};
|
|
280
|
-
}
|
package/test/index.test.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
createApiKeyRouter,
|
|
4
|
-
createJWKSRouter,
|
|
5
|
-
D1Driver,
|
|
6
|
-
} from '../src/index.ts';
|
|
7
|
-
|
|
8
|
-
describe('index', () => {
|
|
9
|
-
test('should export the correct things', () => {
|
|
10
|
-
expect(createApiKeyRouter).toBeDefined();
|
|
11
|
-
expect(createJWKSRouter).toBeDefined();
|
|
12
|
-
expect(D1Driver).toBeDefined();
|
|
13
|
-
});
|
|
14
|
-
});
|