Skip to content

Commit 142bd1c

Browse files
committed
more updates etc
- start scaffolding out api v2 - more docs - some extensions - etc
1 parent c55468f commit 142bd1c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2655
-118
lines changed

.vscode/launch.json

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
// Use IntelliSense to learn about possible attributes.
3+
// Hover to view descriptions of existing attributes.
4+
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5+
"version": "0.2.0",
6+
"configurations": [
7+
{
8+
"request": "launch",
9+
"name": "Run STD Example",
10+
"type": "pwa-node",
11+
"program": "${workspaceFolder}/examples/std.ts",
12+
"cwd": "${workspaceFolder}",
13+
"runtimeExecutable": "deno",
14+
"runtimeArgs": [
15+
"run",
16+
"--inspect",
17+
"--allow-all"
18+
],
19+
"attachSimplePort": 9229
20+
}
21+
]
22+
}

.vscode/settings.json

-3
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,5 @@
22
"deno.enable": true,
33
"deno.lint": true,
44
"deno.unstable": false,
5-
"cSpell.words": [
6-
"GraphiQL"
7-
],
85
// "deno.importMap": "./import_map.json"
96
}

API.md

+14-13
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,20 @@ All URLs land under the `/auth` parent route, e.x.:
1313
- `POST /auth/login`
1414
- `GET /auth/handshake/start`
1515

16-
- POST `/login`
17-
- POST `/register`
18-
- POST `/change-pass`
19-
- GET `/sessions`
20-
- DELETE `/session/:id?`
21-
- POST `/logout`
22-
- GET `/refresh`
23-
- GET `/handshake/start`
24-
- POST `/handshake/complete`
25-
- GET `/handshake/:id/(approve|cancel)?`
26-
- GET | POST `/master-key`
27-
- PUT | DELETE `/master-key/:id`
28-
- POST `/master-key/:id/generate-session`
16+
- POST `/login` - Post with a body of `{ username: string; password: string }` and either get a 403 or a session
17+
- POST `/register` - Post with a body of `{ username: string; password: string }` and get a 401 or a 204.
18+
- POST `/change-pass` - Post with a body of `{ username: string; password: string; newpass: string }`
19+
and either get a 403 or a 204.
20+
- GET `/sessions` - Receives a list of active sessions
21+
- DELETE `/session/:id?` - Revoke all or one session via ID)
22+
- POST `/logout` - Logout of the currently authorized session
23+
- GET `/refresh` - Refresh the current session - revokes the old session and returns a new ID
24+
- GET `/handshake/start` - Start a handshake by redirecting to this route
25+
- POST `/handshake/complete` - Finish a handshake by posting data to this route and getting a session back
26+
- GET `/handshake/:id/(approve|cancel)?` - Approve or cancel a handshake - this is for a Tiny node UI to use
27+
- GET | POST `/master-key` - Get all or add a master-key to the Tiny node with an optional `name` query param
28+
- PUT | DELETE `/master-key/:id` - Update (the name) or remove a master-key
29+
- POST `/master-key/:id/generate-session` - Use a master-key to generate a session
2930

3031
## Scoped Features
3132

api/router.ts

+64-20
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
import { RequestStub, RouteHandler } from './types.ts';
1+
import { SlimRequestStub, RequestStub, RouteHandler } from './types.ts';
22

33
/**
44
* A step on the path of the router
55
*/
66
interface RouteStep<Req extends RequestStub = RequestStub> {
7-
/** The route */
7+
/** The route (pathname) */
88
route: string;
99
/** What request method to limit to; empty for USE, as we do not limit for that */
10-
type?: 'GET' | 'POST' | 'PUT' | 'DELETE';
10+
type?: 'HEAD' | 'OPTIONS' | 'GET' | 'POST' | 'PUT' | 'DELETE';
1111
/** The route handler (or router) */
1212
handler: Router<Req> | RouteHandler<Req>;
1313
}
@@ -75,7 +75,7 @@ export class Router<Req extends RequestStub = RequestStub> {
7575
* @param {string} type The route method (GET/POST/PUT/DELETE)
7676
* @param {Array<RouteHandler>} handlers The handlers to add to our #step array (condensed)
7777
*/
78-
#add<R extends Req = Req>(route: string, type: 'GET' | 'POST' | 'PUT' | 'DELETE', handlers: RouteHandler<R>[]): void {
78+
#add<R extends Req = Req>(route: string, type: 'HEAD' | 'OPTIONS' | 'GET' | 'POST' | 'PUT' | 'DELETE', handlers: RouteHandler<R>[]): void {
7979

8080
// enforce `/{route}` with no trailing `/`'s
8181
route = '/' + route.replace(/^\/+|\/+$/g, '');
@@ -87,6 +87,32 @@ export class Router<Req extends RequestStub = RequestStub> {
8787
});
8888
}
8989

90+
/**
91+
* Add handlers to a HEAD {route} call
92+
* @param {string} route The route
93+
* @param {Router | RouteHandler} handler A router or route handler
94+
* @param {Array<Router | RouteHandler>} handlers Any other route handlers to chain
95+
* @returns {this} this
96+
*/
97+
head<R extends Req = Req>(route: string, handler: RouteHandler<R>, ...handlers: RouteHandler<R>[]): this {
98+
handlers.unshift(handler);
99+
this.#add(route, 'HEAD', handlers);
100+
return this;
101+
}
102+
103+
/**
104+
* Add handlers to a OPTIONS {route} call
105+
* @param {string} route The route
106+
* @param {Router | RouteHandler} handler A router or route handler
107+
* @param {Array<Router | RouteHandler>} handlers Any other route handlers to chain
108+
* @returns {this} this
109+
*/
110+
options<R extends Req = Req>(route: string, handler: RouteHandler<R>, ...handlers: RouteHandler<R>[]): this {
111+
handlers.unshift(handler);
112+
this.#add(route, 'OPTIONS', handlers);
113+
return this;
114+
}
115+
90116
/**
91117
* Add handlers to a GET {route} call
92118
* @param {string} route The route
@@ -146,11 +172,11 @@ export class Router<Req extends RequestStub = RequestStub> {
146172
* @param {string?} type The route type -- if undefined (for USE), it also matches any sub-routes
147173
* @returns {boolean} Whether or not it matches
148174
*/
149-
#matchRoute(url: string, route: string, type?: string): boolean {
175+
#matchRoute(url: string, route: string, matchExtra?: boolean): boolean {
150176
let pattern = new URLPattern({ pathname: route });
151177
let test = pattern.test(url);
152178

153-
if(type || test)
179+
if(!matchExtra || test)
154180
return test;
155181

156182
pattern = new URLPattern({ pathname: route + '(.*)' });
@@ -165,35 +191,37 @@ export class Router<Req extends RequestStub = RequestStub> {
165191
* @param {string?} base The base url to append to the routes we are matching; used for sub-routers
166192
* @returns {Array<RouteHandler>} The path through the step list -- should be iterated through to process
167193
*/
168-
#pathfind<R extends Req = Req>(req: R, base = ''): RouteHandler<R>[] {
194+
#pathfind<R extends Req = Req>(req: Partial<R> & SlimRequestStub, options?: { base?: string; matchType?: boolean }): { type: RouteStep['type'], handler: RouteHandler<R> }[] {
195+
let base = options?.base ?? '';
196+
const matchType = options?.matchType !== false;
169197

170198
// enforce `/{route}` with no trailing `/`'s
171199
if(base)
172200
base = '/' + base.replace(/^\/+|\/+$/g, '');
173201

174-
const path: RouteHandler<R>[] = [];
202+
const path: { type: RouteStep['type'], handler: RouteHandler<R> }[] = [];
175203

176204
for(const step of this.#steps) {
177205
// append the base to the route and remove any trailing `/`'s for the proper route to match against
178206
const route = (base + step.route).replace(/\/+$/g, '');
179207

180-
if( (step.type && req.method !== step.type) ||
181-
!this.#matchRoute(req.url, route, step.type) )
208+
if( (matchType && step.type && req.method !== step.type) ||
209+
!this.#matchRoute(req.url, route, !step.type) )
182210
continue;
183211

184212
// move the handler into a variable in case it changes while it is being processed
185213
const handler = step.handler;
186214

187215
if(handler instanceof Router)
188-
path.push(...(handler as Router<R>).#pathfind(req, route));
216+
path.push(...(handler as Router<R>).#pathfind(req, { base: route }));
189217
else {
190-
path.push((req, next) => {
218+
path.push({ type: step.type, handler: (req, next) => {
191219
// match the route params for utility reasons
192-
req.params = (new URLPattern({ pathname: route + '/:else(.*)?' })).exec(req.url)?.pathname?.groups;
193-
delete req.params!.else;
220+
req.params = (new URLPattern({ pathname: route + '/:else(.*)?' })).exec(req.url)?.pathname?.groups ?? { };
221+
delete req.params.else;
194222

195223
return handler(req, next);
196-
});
224+
} });
197225
}
198226
}
199227

@@ -211,9 +239,9 @@ export class Router<Req extends RequestStub = RequestStub> {
211239
* @returns {Promise<Response | undefined>} The response generated from the given routes,
212240
* or undefined if nothing was returned
213241
*/
214-
async process<R extends Req = Req>(req: R, base = ''): Promise<Response | undefined> {
242+
async process<R extends Req = Req>(req: Partial<R> & SlimRequestStub, base = ''): Promise<Response | undefined> {
215243

216-
const path = this.#pathfind(req, base);
244+
const path = this.#pathfind(req, { base });
217245

218246
if(!path.length)
219247
return undefined;
@@ -222,27 +250,43 @@ export class Router<Req extends RequestStub = RequestStub> {
222250

223251
const query = req.query;
224252
const params = req.params;
253+
const context = req.context;
225254

226255
// generate the query object for utility reasons
227-
req.query = req.url.includes('?')
256+
req.query = (req.url.includes('?')
228257
? Object.fromEntries((new URLSearchParams(req.url.slice(req.url.indexOf('?')))).entries())
229-
: undefined;
258+
: undefined) ?? { };
259+
260+
req.context = { };
230261

231262
// used to check if the chain never finished and we got "ghosted"
232263
const nextResponse = new Response();
233264

234-
const res = await this.#condense(path)(req, () => nextResponse);
265+
const res = await this.#condense(path.map(p => p.handler))(req as R, () => nextResponse);
235266

236267
// cleanup
237268

238269
req.query = query;
239270
req.params = params;
271+
req.context = context;
240272

241273
if(!res || res === nextResponse)
242274
return undefined;
243275

244276
return res;
245277
}
278+
279+
parseOptions<R extends Req = Req>(req: Partial<R> & SlimRequestStub, base = ''): { methods: string } {
280+
const path = this.#pathfind(req, { base, matchType: false });
281+
282+
const methods = new Set<string>();
283+
284+
for(const p of path)
285+
if(p.type)
286+
methods.add(p.type);
287+
288+
return { methods: Array.from(methods.values()).join(', ') };
289+
}
246290
}
247291

248292
export default Router;

api/types.ts

+18-3
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,38 @@
22
* A request stub which this library uses. You can either set the two required fields yourself or
33
* just pass something that is compatible (like the standard `Request` object).
44
*/
5-
export interface RequestStub {
5+
export interface SlimRequestStub {
66

77
// CORE (std)
88

99
/** The (full) url */
1010
readonly url: string;
1111
/** The method (GET | PUT | POST | DELETE) */
1212
readonly method: string;
13+
}
14+
15+
/**
16+
* A request stub which this library uses. You can either set the two required fields yourself or
17+
* just pass something that is compatible (like the standard `Request` object).
18+
*/
19+
export interface RequestStub<Context = Record<string, unknown>> extends SlimRequestStub {
1320

1421
// GENERATED
1522

1623
/** Generated via URLPattern */
17-
params?: Record<string, string | undefined>;
24+
params: Record<string, string | undefined>;
1825
/** Generated via URLSearchParams */
19-
query?: Record<string, string | undefined>;
26+
query: Record<string, string | undefined>;
27+
28+
// UTILITY
29+
30+
/** Used for utility */
31+
context: Context;
2032
}
2133

2234
/** A route handler or middleware. Can `await` or just return `next` to continue along the chain, but should otherwise return a Response. */
2335
export type RouteHandler<Req extends RequestStub = RequestStub> = (req: Req, next: () => Promise<Response> | Response) => Promise<Response> | Response;
2436

37+
/** A route handler or middleware. Can `await` or just return `next` to continue along the chain, but should otherwise return a Response. */
38+
export type SlimRouteHandler<Req extends { readonly url: string; readonly method: string; }> = (req: Req, next: () => Promise<Response> | Response) => Promise<Response> | Response;
39+

api/util.ts

+24-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Create a text response
2+
* Create a text response.
33
* @param {string} body The body of the response
44
* @param {ResponseInit} init Response options
55
* @returns {Response} The response
@@ -22,7 +22,7 @@ export function text(body = '', init: ResponseInit = { }): Response {
2222
}
2323

2424
/**
25-
* Create a JSON response
25+
* Create a JSON response.
2626
* @param {any} body The body of the response
2727
* @param {ResponseInit} init Response options
2828
* @returns {Response} The response
@@ -54,11 +54,31 @@ export function noContent(init: ResponseInit = { }): Response {
5454
}
5555

5656
/**
57-
* Create a redirect response.
57+
* Create a 307 redirect response.
5858
* @param {string} url The redirect url
5959
* @param {ResponseInit} init Response options
6060
* @returns {Response} The response
6161
*/
6262
export function redirect(url: string, init: ResponseInit = { }): Response {
63-
return new Response(undefined, { status: 302, ...init, headers: { ...init.headers, location: url } });
63+
return new Response(undefined, { status: 307, ...init, headers: { ...init.headers, location: url } });
64+
}
65+
66+
/**
67+
* Create a 412 precondition failed response.
68+
* @param {BodyInit | undefined | null} body The body of the response
69+
* @param {ResponseInit} init Response options
70+
* @returns {Response} The response
71+
*/
72+
export function preconditionFailed(body?: BodyInit | undefined | null, init?: ResponseInit): Response {
73+
return new Response(body, { status: 412, statusText: 'Precondition Failed', ...init })
74+
}
75+
76+
/**
77+
* Create a 409 conflict response.
78+
* @param {BodyInit | undefined | null} body The body of the response
79+
* @param {ResponseInit} init Response options
80+
* @returns {Response} The response
81+
*/
82+
export function conflict(body?: BodyInit | undefined | null, init?: ResponseInit): Response {
83+
return new Response(body, { status: 409, statusText: 'Conflict', ...init });
6484
}

auth/auth-api.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ export class AuthApi extends Api {
283283
router.get('/sessions', async req => json(await this.sessions(req.user)));
284284

285285
router.delete('/sessions/:id', async req => {
286-
await this.deleteSession(req.params!.id!, req.user);
286+
await this.deleteSession(req.params.id!, req.user);
287287

288288
return noContent();
289289
});
@@ -336,7 +336,7 @@ export class AuthApi extends Api {
336336

337337
console.log(req.params, req.query);
338338

339-
req.handshake = await this.testHandshake(req.params!.id!, req.session!);
339+
req.handshake = await this.testHandshake(req.params.id!, req.session!);
340340
return next();
341341
});
342342

@@ -368,10 +368,10 @@ export class AuthApi extends Api {
368368
masterKeyRouter.use(handleError('auth-master-key'));
369369

370370
masterKeyRouter.post('/:id/generate-session', async req => {
371-
if(!req.query?.scopes)
371+
if(!req.query.scopes)
372372
throw new MalformedError('?scopes=[..] required!');
373373

374-
return json(await this.generateSessionFromMasterKey(req.params!.id!, this.#validateScopes(req.query.scopes)));
374+
return json(await this.generateSessionFromMasterKey(req.params.id!, this.#validateScopes(req.query.scopes)));
375375
});
376376

377377
masterKeyRouter.use(requireUserSession, (req, next) => {
@@ -382,12 +382,12 @@ export class AuthApi extends Api {
382382
});
383383

384384
masterKeyRouter.get('/', async req => json(await this.getMasterKeys(req.user!.id!)));
385-
masterKeyRouter.post('/', async req => text(await this.addMasterKey(req.user!.id!, req.query?.name)));
385+
masterKeyRouter.post('/', async req => text(await this.addMasterKey(req.user!.id!, req.query.name)));
386386

387387
masterKeyRouter.use('/:id', async (req, next) => {
388-
const key = await this.getMasterKey(req.params!.id!, req.user!.id!);
388+
const key = await this.getMasterKey(req.params.id!, req.user!.id!);
389389
if(!key)
390-
throw new NotFoundError('Key not found with id "' + req.params!.id! + '"!');
390+
throw new NotFoundError('Key not found with id "' + req.params.id! + '"!');
391391

392392
req.masterKey = key;
393393

auth/auth-db.ts

+7
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ export abstract class AuthDb {
4545
abstract delUser(id: string): Promise<void>;
4646
abstract getUserFromUsername(username: string): Promise<AuthUser | null>;
4747

48+
// user preferences
49+
50+
/* abstract putUserPref(id: string, key: string, value: string);
51+
abstract getUserPref(id: string, key: string);
52+
abstract findUserPref(id: string, value: string);
53+
abstract findUserFromPref(key: string, value: string); */
54+
4855
// handshakes
4956

5057
abstract addHandshake(hs: Handshake): Promise<string>;

0 commit comments

Comments
 (0)