@tiddlywiki/mws 0.0.12 → 0.0.13
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 +3 -1
- package/dist/server.d.ts +923 -0
- package/dist/server.js +1763 -1415
- package/dist/server.js.map +1 -1
- package/editions/mws-docs/tiddlers/$__StoryList.tid +3 -3
- package/editions/mws-docs/tiddlers/MWS and SQLite.md +11 -0
- package/editions/mws-docs/tiddlers/Request Server.md +20 -0
- package/editions/mws-docs/tiddlers/Request Server.md.meta +5 -0
- package/package.json +16 -10
- package/prisma/schema.prisma +1 -0
- package/react-user-mgmt/package-lock.json +76 -321
- package/react-user-mgmt/package.json +2 -5
- package/react-user-mgmt/public/chunk-YALWN5OT.js +2 -0
- package/react-user-mgmt/public/chunk-YALWN5OT.js.map +7 -0
- package/react-user-mgmt/public/esm-QKDFEP3B.js +3 -0
- package/react-user-mgmt/public/esm-QKDFEP3B.js.map +7 -0
- package/react-user-mgmt/public/index.html +1 -26
- package/react-user-mgmt/public/main.js +68 -109
- package/react-user-mgmt/public/main.js.map +4 -4
- package/react-user-mgmt/public/stats.css +597 -0
- package/react-user-mgmt/public/stats.html +59 -0
- package/react-user-mgmt/public/stats.js +2013 -0
- package/react-user-mgmt/public/stats.json +1 -0
- package/react-user-mgmt/public/stats.svg +4 -0
- package/react-user-mgmt/tsconfig.json +3 -0
- package/dist/server.cjs +0 -16677
- package/dist/server.cjs.map +0 -1
- package/src/StateObject.ts +0 -316
- package/src/commander.ts +0 -417
- package/src/commands/divider.ts +0 -20
- package/src/commands/index.ts +0 -24
- package/src/commands/mws-init-store.ts +0 -77
- package/src/commands/mws-listen.ts +0 -136
- package/src/commands/mws-load-archive.ts +0 -281
- package/src/commands/mws-load-plugin-bags.ts +0 -151
- package/src/commands/mws-load-tiddlers.ts +0 -37
- package/src/commands/mws-load-wiki-folder.ts +0 -124
- package/src/commands/mws-render-tiddlywiki5.ts +0 -55
- package/src/commands/mws-save-archive.ts +0 -111
- package/src/commands/mws-save-tiddler-text.ts +0 -33
- package/src/commands/tests-complete.ts +0 -21
- package/src/global.ts +0 -247
- package/src/routes/TiddlerStore.ts +0 -722
- package/src/routes/bag-file-server.ts +0 -340
- package/src/routes/cache.ts +0 -25
- package/src/routes/index.ts +0 -138
- package/src/routes/managers/index.ts +0 -17
- package/src/routes/managers/manager-recipes.ts +0 -438
- package/src/routes/managers/manager-users.ts +0 -201
- package/src/routes/managers/router-tiddlers.ts +0 -313
- package/src/routes/router.ts +0 -429
- package/src/routes/services/PasswordService.ts +0 -178
- package/src/routes/services/attachments.ts +0 -259
- package/src/routes/services/sessions.ts +0 -166
- package/src/routes/tw-test.ts +0 -65
- package/src/server.ts +0 -151
- package/src/setupDevServer.ts +0 -71
- package/src/streamer.ts +0 -430
- package/src/tiddlywiki.ts +0 -94
- package/src/utils/data-checks.ts +0 -172
- package/src/utils/index.ts +0 -4
- package/src/utils/rootRoute.ts +0 -237
- package/src/utils/utils.ts +0 -403
- package/src/utils/zodAssert.ts +0 -269
- /package/prisma/{migrations/notes.md → notes.md} +0 -0
package/src/routes/router.ts
DELETED
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
import { STREAM_ENDED, Streamer } from "../streamer";
|
|
2
|
-
import { StateObject } from "../StateObject";
|
|
3
|
-
import RootRoute from ".";
|
|
4
|
-
import * as z from "zod";
|
|
5
|
-
import { createStrictAwaitProxy, JsonValue, truthy, Z2 } from "../utils";
|
|
6
|
-
import { Route, rootRoute, RouteOptAny, RouteMatch, } from "../utils";
|
|
7
|
-
import { MWSConfigConfig } from "../server";
|
|
8
|
-
import { setupDevServer } from "../setupDevServer";
|
|
9
|
-
import { Commander } from "../commander";
|
|
10
|
-
import { CacheState, startupCache } from "./cache";
|
|
11
|
-
|
|
12
|
-
export { RouteMatch, Route, rootRoute };
|
|
13
|
-
|
|
14
|
-
export const AllowedMethods = [...["GET", "HEAD", "OPTIONS", "POST", "PUT", "DELETE"] as const];
|
|
15
|
-
export type AllowedMethod = typeof AllowedMethods[number];
|
|
16
|
-
|
|
17
|
-
export const BodyFormats = ["stream", "string", "json", "buffer", "www-form-urlencoded", "www-form-urlencoded-urlsearchparams", "ignore"] as const;
|
|
18
|
-
export type BodyFormat = typeof BodyFormats[number];
|
|
19
|
-
|
|
20
|
-
export const PermissionName = []
|
|
21
|
-
|
|
22
|
-
const zodTransformJSON = (arg: string, ctx: z.RefinementCtx) => {
|
|
23
|
-
try {
|
|
24
|
-
if (arg === "") return undefined;
|
|
25
|
-
return JSON.parse(arg, (key, value) => {
|
|
26
|
-
//https://github.com/fastify/secure-json-parse
|
|
27
|
-
if (key === '__proto__')
|
|
28
|
-
throw new Error('Invalid key: __proto__');
|
|
29
|
-
if (key === 'constructor' && Object.prototype.hasOwnProperty.call(value, 'prototype'))
|
|
30
|
-
throw new Error('Invalid key: constructor.prototype');
|
|
31
|
-
return value;
|
|
32
|
-
});
|
|
33
|
-
} catch (e) {
|
|
34
|
-
ctx.addIssue({
|
|
35
|
-
code: z.ZodIssueCode.custom,
|
|
36
|
-
message: e instanceof Error ? e.message : `${e}`,
|
|
37
|
-
fatal: true,
|
|
38
|
-
});
|
|
39
|
-
return z.NEVER;
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export interface SiteConfig extends MWSConfigConfig {
|
|
44
|
-
wikiPath: string;
|
|
45
|
-
attachmentSizeLimit: number;
|
|
46
|
-
attachmentsEnabled: boolean;
|
|
47
|
-
contentTypeInfo: Record<string, any>;
|
|
48
|
-
saveLargeTextToFileSystem: never;
|
|
49
|
-
storePath: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export class Router {
|
|
53
|
-
|
|
54
|
-
static async makeRouter(
|
|
55
|
-
commander: Commander,
|
|
56
|
-
enableDevServer: string | undefined
|
|
57
|
-
) {
|
|
58
|
-
|
|
59
|
-
const sendDevServer = await setupDevServer(enableDevServer);
|
|
60
|
-
|
|
61
|
-
const rootRoute = defineRoute(ROOT_ROUTE, {
|
|
62
|
-
method: AllowedMethods,
|
|
63
|
-
path: /^/,
|
|
64
|
-
denyFinal: true,
|
|
65
|
-
}, async (state: StateObject) => {
|
|
66
|
-
state.sendDevServer = sendDevServer.bind(undefined, state);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
await commander.SessionManager.defineRoutes(rootRoute);
|
|
70
|
-
|
|
71
|
-
await RootRoute(rootRoute, commander.siteConfig);
|
|
72
|
-
|
|
73
|
-
const cache = await startupCache(commander);
|
|
74
|
-
|
|
75
|
-
return new Router(rootRoute, commander, cache);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
pathPrefix: string = "";
|
|
80
|
-
enableBrowserCache: boolean = true;
|
|
81
|
-
enableGzip: boolean = false;
|
|
82
|
-
csrfDisable: boolean = false;
|
|
83
|
-
servername: string = "";
|
|
84
|
-
variables = new Map();
|
|
85
|
-
get(name: string): string {
|
|
86
|
-
return this.variables.get(name) || "";
|
|
87
|
-
}
|
|
88
|
-
public engine: Commander["engine"];
|
|
89
|
-
private SessionManager: Commander["SessionManager"];
|
|
90
|
-
|
|
91
|
-
constructor(
|
|
92
|
-
private rootRoute: rootRoute,
|
|
93
|
-
private commander: Commander,
|
|
94
|
-
private tiddlerCache: CacheState,
|
|
95
|
-
) {
|
|
96
|
-
this.engine = commander.engine;
|
|
97
|
-
this.SessionManager = commander.SessionManager;
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
async handle(streamer: Streamer) {
|
|
102
|
-
|
|
103
|
-
if (!this.csrfDisable
|
|
104
|
-
&& ["POST", "PUT", "DELETE"].includes(streamer.method)
|
|
105
|
-
&& streamer.headers["x-requested-with"] !== "TiddlyWiki"
|
|
106
|
-
)
|
|
107
|
-
throw streamer.sendString(403, { "x-reason": "x-requested-with missing" },
|
|
108
|
-
`'X-Requested-With' header required to login to '${this.servername}'`, "utf8");
|
|
109
|
-
|
|
110
|
-
const authUser = await this.SessionManager.parseIncomingRequest(streamer, this);
|
|
111
|
-
|
|
112
|
-
/** This should always have a length of at least 1 because of the root route. */
|
|
113
|
-
const routePath = this.findRoute(streamer);
|
|
114
|
-
if (!routePath.length || routePath[routePath.length - 1]?.route.denyFinal)
|
|
115
|
-
return streamer.sendEmpty(404, { "x-reason": "no-route" });
|
|
116
|
-
|
|
117
|
-
// Optionally output debug info
|
|
118
|
-
console.log(streamer.method, streamer.url);
|
|
119
|
-
// if no bodyFormat is specified, we default to "buffer" since we do still need to recieve the body
|
|
120
|
-
const bodyFormat = routePath.find(e => e.route.bodyFormat)?.route.bodyFormat || "buffer";
|
|
121
|
-
|
|
122
|
-
type statetype = { [K in BodyFormat]: StateObject<K> }[BodyFormat]
|
|
123
|
-
|
|
124
|
-
const state = createStrictAwaitProxy(
|
|
125
|
-
new StateObject(
|
|
126
|
-
streamer,
|
|
127
|
-
routePath,
|
|
128
|
-
bodyFormat,
|
|
129
|
-
authUser,
|
|
130
|
-
this.commander,
|
|
131
|
-
this.tiddlerCache,
|
|
132
|
-
) as statetype
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const method = streamer.method;
|
|
137
|
-
|
|
138
|
-
// anything that sends a response before this should have thrown, but just in case
|
|
139
|
-
if (streamer.headersSent) return;
|
|
140
|
-
|
|
141
|
-
if (["GET", "HEAD"].includes(method)) state.bodyFormat = "ignore";
|
|
142
|
-
|
|
143
|
-
if (state.bodyFormat === "stream" || state.bodyFormat === "ignore") {
|
|
144
|
-
// this starts dumping bytes early, rather than letting node do it once the res finishes.
|
|
145
|
-
// the only advantage is that it eases congestion on the socket.
|
|
146
|
-
if (state.bodyFormat === "ignore") streamer.reader.resume();
|
|
147
|
-
|
|
148
|
-
return await this.handleRoute(state, routePath);
|
|
149
|
-
}
|
|
150
|
-
if (state.bodyFormat === "string" || state.bodyFormat === "json") {
|
|
151
|
-
state.data = (await state.readBody()).toString("utf8");
|
|
152
|
-
if (state.bodyFormat === "json") {
|
|
153
|
-
// make sure this parses as valid data
|
|
154
|
-
const { success, data } = z.string().transform(zodTransformJSON).safeParse(state.data);
|
|
155
|
-
if (!success) return state.sendEmpty(400, {});
|
|
156
|
-
state.data = data;
|
|
157
|
-
}
|
|
158
|
-
} else if (state.bodyFormat === "www-form-urlencoded-urlsearchparams"
|
|
159
|
-
|| state.bodyFormat === "www-form-urlencoded") {
|
|
160
|
-
const data = state.data = new URLSearchParams((await state.readBody()).toString("utf8"));
|
|
161
|
-
if (state.bodyFormat === "www-form-urlencoded") {
|
|
162
|
-
state.data = Object.fromEntries(data);
|
|
163
|
-
}
|
|
164
|
-
} else if (state.bodyFormat === "buffer") {
|
|
165
|
-
state.data = await state.readBody();
|
|
166
|
-
} else {
|
|
167
|
-
// because it's a union, state becomes never at this point if we matched every route correctly
|
|
168
|
-
// make sure state is never by assigning it to a never const. This will error if something is missed.
|
|
169
|
-
const t: never = state;
|
|
170
|
-
const state2: StateObject = state as any;
|
|
171
|
-
return state2.sendString(500, {}, "Invalid bodyFormat: " + state2.bodyFormat, "utf8");
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return await this.handleRoute(state, routePath);
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
async handleRoute(state: StateObject<BodyFormat>, route: RouteMatch[]) {
|
|
181
|
-
|
|
182
|
-
let result: any = state;
|
|
183
|
-
for (const match of route) {
|
|
184
|
-
await match.route.handler(result);
|
|
185
|
-
if (state.headersSent) return;
|
|
186
|
-
}
|
|
187
|
-
if (!state.headersSent) {
|
|
188
|
-
state.sendEmpty(404, {});
|
|
189
|
-
console.log("No handler sent headers before the promise resolved.");
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
findRouteRecursive(
|
|
195
|
-
routes: Route[],
|
|
196
|
-
testPath: string,
|
|
197
|
-
method: AllowedMethod
|
|
198
|
-
): RouteMatch[] {
|
|
199
|
-
for (const potentialRoute of routes) {
|
|
200
|
-
// Skip if the method doesn't match.
|
|
201
|
-
if (!potentialRoute.method.includes(method)) continue;
|
|
202
|
-
|
|
203
|
-
// Try to match the path.
|
|
204
|
-
const match = potentialRoute.path.exec(testPath);
|
|
205
|
-
|
|
206
|
-
if (match) {
|
|
207
|
-
// The matched portion of the path.
|
|
208
|
-
const matchedPortion = match[0];
|
|
209
|
-
// Remove the matched portion from the testPath.
|
|
210
|
-
const remainingPath = testPath.slice(matchedPortion.length) || "/";
|
|
211
|
-
|
|
212
|
-
const result = {
|
|
213
|
-
route: potentialRoute,
|
|
214
|
-
params: match.slice(1),
|
|
215
|
-
remainingPath,
|
|
216
|
-
};
|
|
217
|
-
const { childRoutes = [] } = potentialRoute as any; // see this.defineRoute
|
|
218
|
-
// If there are inner routes, try to match them recursively.
|
|
219
|
-
if (childRoutes.length > 0) {
|
|
220
|
-
const innerMatch = this.findRouteRecursive(
|
|
221
|
-
childRoutes,
|
|
222
|
-
remainingPath,
|
|
223
|
-
method
|
|
224
|
-
);
|
|
225
|
-
return [result, ...innerMatch];
|
|
226
|
-
} else {
|
|
227
|
-
return [result];
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
return [];
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
*
|
|
236
|
-
* Top-level function that starts matching from the root routes.
|
|
237
|
-
* Notice that the pathPrefix is assumed to have been handled beforehand.
|
|
238
|
-
*
|
|
239
|
-
* @param streamer
|
|
240
|
-
* @returns The tree path matched
|
|
241
|
-
*/
|
|
242
|
-
findRoute(streamer: Streamer): RouteMatch[] {
|
|
243
|
-
const { method, urlInfo } = streamer;
|
|
244
|
-
let testPath = urlInfo.pathname || "/";
|
|
245
|
-
if (this.pathPrefix && testPath.startsWith(this.pathPrefix))
|
|
246
|
-
testPath = testPath.slice(this.pathPrefix.length) || "/";
|
|
247
|
-
return this.findRouteRecursive([this.rootRoute as any], testPath, method);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const ROOT_ROUTE: unique symbol = Symbol("ROOT_ROUTE");
|
|
253
|
-
function defineRoute(
|
|
254
|
-
parent: { $o?: any, method: any } | typeof ROOT_ROUTE,
|
|
255
|
-
route: RouteOptAny,
|
|
256
|
-
handler: (state: any) => any,
|
|
257
|
-
) {
|
|
258
|
-
|
|
259
|
-
if (route.bodyFormat && !BodyFormats.includes(route.bodyFormat))
|
|
260
|
-
throw new Error("Invalid bodyFormat: " + route.bodyFormat);
|
|
261
|
-
if (!route.method.every(e => (parent === ROOT_ROUTE ? AllowedMethods : parent.method).includes(e)))
|
|
262
|
-
throw new Error("Invalid method: " + route.method);
|
|
263
|
-
if (route.path.source[0] !== "^")
|
|
264
|
-
throw new Error("Path regex must start with ^");
|
|
265
|
-
|
|
266
|
-
if (parent !== ROOT_ROUTE) {
|
|
267
|
-
// the typing is too complicated if we add childRoutes
|
|
268
|
-
if (!(parent as any).childRoutes) (parent as any).childRoutes = [];
|
|
269
|
-
(parent as any).childRoutes.push(route);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
(route as any).defineRoute = (...args: [any, any]) => defineRoute(route, ...args);
|
|
273
|
-
|
|
274
|
-
(route as Route).handler = handler;
|
|
275
|
-
|
|
276
|
-
return route as any; // this is usually ignored except for the root route.
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
export interface ZodAction<T extends z.ZodTypeAny, R extends JsonValue> {
|
|
281
|
-
// (state: StateObject): Promise<typeof STREAM_ENDED>;
|
|
282
|
-
inner: (route: z.output<T>) => Promise<R>
|
|
283
|
-
zodRequest: (z: Z2<"JSON">) => T;
|
|
284
|
-
zodResponse?: (z: Z2<"JSON">) => z.ZodType<R>;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
export interface ZodRoute<
|
|
288
|
-
M extends AllowedMethod,
|
|
289
|
-
B extends BodyFormat,
|
|
290
|
-
P extends Record<string, z.ZodTypeAny>,
|
|
291
|
-
T extends z.ZodTypeAny,
|
|
292
|
-
R extends JsonValue
|
|
293
|
-
> extends ZodAction<T, R> {
|
|
294
|
-
zodPathParams: (z: Z2<"STRING">) => P;
|
|
295
|
-
method: M[];
|
|
296
|
-
path: string;
|
|
297
|
-
bodyFormat: B;
|
|
298
|
-
inner: (state: ZodState<M, B, P, T>) => Promise<R>,
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
export class ZodState<
|
|
302
|
-
M extends AllowedMethod,
|
|
303
|
-
B extends BodyFormat,
|
|
304
|
-
P extends Record<string, z.ZodTypeAny>,
|
|
305
|
-
T extends z.ZodTypeAny
|
|
306
|
-
> extends StateObject<B, M, string[][], z.output<T>> {
|
|
307
|
-
declare pathParams: z.output<z.ZodObject<P>>;
|
|
308
|
-
// declare data: z.output<T>;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
export type RouterRouteMap<T> = {
|
|
313
|
-
[K in keyof T as T[K] extends ZodAction<any, any> ? K : never]:
|
|
314
|
-
T[K] extends {
|
|
315
|
-
zodRequest: (z: any) => infer REQ extends z.ZodTypeAny,
|
|
316
|
-
zodResponse?: (z: any) => infer RES extends z.ZodType<JsonValue>
|
|
317
|
-
} ? ((data: z.input<REQ>) => Promise<jsonify<z.output<RES>>>) : never;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
export type jsonify<T> =
|
|
321
|
-
T extends void ? null :
|
|
322
|
-
T extends Promise<any> ? unknown :
|
|
323
|
-
T extends Date ? string :
|
|
324
|
-
// T extends Map<infer K, infer V> ? [jsonify<K>, jsonify<V>][] :
|
|
325
|
-
// T extends Set<infer U> ? jsonify<U>[] :
|
|
326
|
-
T extends string | number | boolean | null | undefined ? T :
|
|
327
|
-
T extends [...any[]] ? number extends T["length"] ? jsonify<T[number]>[] : [...jsonifyTuple<T>] :
|
|
328
|
-
T extends Array<infer U> ? jsonify<U>[] :
|
|
329
|
-
T extends object ? { [K in keyof T]: jsonify<T[K]> } :
|
|
330
|
-
unknown;
|
|
331
|
-
|
|
332
|
-
export type jsonifyTuple<T> = T extends [infer A, ...infer B] ? [jsonify<A>, ...jsonifyTuple<B>] : T extends [infer A] ? [jsonify<A>] : [];
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
export type RouterKeyMap<T, V> = {
|
|
337
|
-
[K in keyof T as T[K] extends ZodAction<any, any> ? K : never]: V;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
// this is definitely the better version of defineRoute, but that was the starting point
|
|
342
|
-
export function zodRoute<M extends AllowedMethod, B extends "GET" | "HEAD" extends M ? "ignore" : BodyFormat, P extends Record<string, z.ZodTypeAny>, T extends z.ZodTypeAny, R extends JsonValue>(
|
|
343
|
-
method: M[],
|
|
344
|
-
/** `"path/to/route/:var/route/:var2"` */
|
|
345
|
-
path: string,
|
|
346
|
-
zodPathParams: (z: Z2<"STRING">) => P,
|
|
347
|
-
bodyFormat: B,
|
|
348
|
-
zodRequest: (z: Z2<"JSON">) => T,
|
|
349
|
-
inner: (state: ZodState<M, B, P, T>) => Promise<R>,
|
|
350
|
-
): ZodRoute<M, B, P, T, R> {
|
|
351
|
-
return {
|
|
352
|
-
method,
|
|
353
|
-
path,
|
|
354
|
-
bodyFormat,
|
|
355
|
-
inner,
|
|
356
|
-
zodRequest,
|
|
357
|
-
zodPathParams,
|
|
358
|
-
} as ZodRoute<M, B, P, T, R>;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
export const registerZodRoutes = (root: rootRoute, router: any, keys: string[]) => {
|
|
362
|
-
// const router = new TiddlerRouter();
|
|
363
|
-
keys.forEach((key) => {
|
|
364
|
-
const route = router[key as keyof typeof router] as ZodRoute<any, any, any, any, any>;
|
|
365
|
-
const { method, path, bodyFormat, zodPathParams, zodRequest, inner } = route;
|
|
366
|
-
const pathParams = path.split("/").filter(e => e.startsWith(":")).map(e => e.substring(1));
|
|
367
|
-
///^\/recipes\/([^\/]+)\/tiddlers\/(.+)$/,
|
|
368
|
-
if (!path.startsWith("/")) throw new Error(`Path ${path} must start with a forward slash`);
|
|
369
|
-
if (key.startsWith(":")) throw new Error(`Key ${key} must not start with a colon`)
|
|
370
|
-
const pathregex = "^" + path.split("/").map(e =>
|
|
371
|
-
e === "$key" ? key : e.startsWith(":") ? "([^/]+)" : e
|
|
372
|
-
).join("\\/") + "$";
|
|
373
|
-
|
|
374
|
-
root.defineRoute({
|
|
375
|
-
method: [...method, "OPTIONS"],
|
|
376
|
-
path: new RegExp(pathregex),
|
|
377
|
-
pathParams,
|
|
378
|
-
bodyFormat,
|
|
379
|
-
}, async state => {
|
|
380
|
-
if (state.method === "OPTIONS") {
|
|
381
|
-
return state.sendEmpty(200, {
|
|
382
|
-
// "Access-Control-Allow-Origin": "*",
|
|
383
|
-
"Access-Control-Allow-Methods": method.join(","),
|
|
384
|
-
"Access-Control-Allow-Headers": "Accept, Content-Type, X-Requested-With",
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
// return await (router[key] as ZodRoute<any, any, any, any, any>)(state);
|
|
388
|
-
const pathCheck = Z2.object(zodPathParams(Z2)).safeParse(state.pathParams);
|
|
389
|
-
if (!pathCheck.success) {
|
|
390
|
-
console.log(pathCheck.error);
|
|
391
|
-
throw state.sendEmpty(400, { "x-reason": "zod-path" });
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const inputCheck = zodRequest(Z2).safeParse(state.data);
|
|
395
|
-
if (!inputCheck.success) {
|
|
396
|
-
console.log(inputCheck.error);
|
|
397
|
-
throw state.sendEmpty(400, { "x-reason": "zod-request" });
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const [good, error, res] = await inner(state)
|
|
401
|
-
.then(e => [true, undefined, e] as const, e => [false, e, undefined] as const);
|
|
402
|
-
|
|
403
|
-
if (!good) {
|
|
404
|
-
if (error === STREAM_ENDED) {
|
|
405
|
-
return error;
|
|
406
|
-
} else if (typeof error === "string") {
|
|
407
|
-
return state.sendString(400, { "x-reason": "zod-handler" }, error, "utf8");
|
|
408
|
-
} else if (error instanceof Error && error.name === "UserError") {
|
|
409
|
-
return state.sendString(400, { "x-reason": "user-error" }, error.message, "utf8");
|
|
410
|
-
} else {
|
|
411
|
-
throw error;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
return state.sendJSON(200, res);
|
|
416
|
-
});
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
export function zodManage<T extends z.ZodTypeAny, R extends JsonValue>(
|
|
423
|
-
zodRequest: (z: Z2<"JSON">) => T,
|
|
424
|
-
inner: (state: ZodState<"POST", "json", Record<string, z.ZodTypeAny>, T>, prisma: PrismaTxnClient) => Promise<R>
|
|
425
|
-
) {
|
|
426
|
-
return zodRoute(["POST"], "/manager/$key", z => ({}), "json", zodRequest, async state => {
|
|
427
|
-
return state.$transaction(async (prisma) => await inner(state, prisma));
|
|
428
|
-
});
|
|
429
|
-
}
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import * as opaque from "@serenity-kit/opaque";
|
|
3
|
-
import { TypedGenerator } from "../../utils";
|
|
4
|
-
import { randomBytes } from "node:crypto";
|
|
5
|
-
import { ok } from "node:assert";
|
|
6
|
-
|
|
7
|
-
export type PasswordService = Awaited<ReturnType<typeof createPasswordService>>;
|
|
8
|
-
|
|
9
|
-
export async function createPasswordService(serverSetup: string) {
|
|
10
|
-
await opaque.ready;
|
|
11
|
-
|
|
12
|
-
const serverState = new Map<string, TypedGenerator<LoginGeneratorStates>>();
|
|
13
|
-
|
|
14
|
-
const LoginGenerator = TypedGenerator.wrapper<LoginGeneratorStates>()(async function* ({
|
|
15
|
-
user_id,
|
|
16
|
-
startLoginRequest,
|
|
17
|
-
registrationRecord,
|
|
18
|
-
}: {
|
|
19
|
-
user_id: PrismaField<"Users", "user_id">;
|
|
20
|
-
startLoginRequest: string;
|
|
21
|
-
registrationRecord: string;
|
|
22
|
-
}): any {
|
|
23
|
-
|
|
24
|
-
const { asV, asY } = TypedGenerator.checker<LoginGeneratorStates>();
|
|
25
|
-
|
|
26
|
-
const { serverLoginState, loginResponse } = opaque.server.startLogin({
|
|
27
|
-
serverSetup,
|
|
28
|
-
userIdentifier: user_id.toString(),
|
|
29
|
-
registrationRecord,
|
|
30
|
-
startLoginRequest,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const timestamp = Date.now();
|
|
34
|
-
|
|
35
|
-
const finishLoginRequest = asV(1, yield asY(0, loginResponse));
|
|
36
|
-
|
|
37
|
-
// this is supposed to be programmatic
|
|
38
|
-
// it should never last longer than five minutes
|
|
39
|
-
if (timestamp + 1000 * 60 * 5 < Date.now()) return;
|
|
40
|
-
|
|
41
|
-
// per the spec, the sessionKey will only be returned
|
|
42
|
-
// if the client's session key has been verified.
|
|
43
|
-
return asY(1, {
|
|
44
|
-
session: opaque.server.finishLogin({
|
|
45
|
-
finishLoginRequest,
|
|
46
|
-
serverLoginState,
|
|
47
|
-
}),
|
|
48
|
-
user_id,
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* This code creates a password registration. It can be used for temporary passwords, if needed.
|
|
55
|
-
*/
|
|
56
|
-
async function PasswordCreation(userID: string, password: string) {
|
|
57
|
-
// client
|
|
58
|
-
|
|
59
|
-
const { clientRegistrationState, registrationRequest } = opaque.client.startRegistration({ password });
|
|
60
|
-
|
|
61
|
-
// server
|
|
62
|
-
const userIdentifier = userID; // userId/email/username
|
|
63
|
-
|
|
64
|
-
const { registrationResponse } = opaque.server.createRegistrationResponse({
|
|
65
|
-
serverSetup,
|
|
66
|
-
userIdentifier,
|
|
67
|
-
registrationRequest,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// client
|
|
71
|
-
const { registrationRecord } = opaque.client.finishRegistration({
|
|
72
|
-
clientRegistrationState,
|
|
73
|
-
registrationResponse,
|
|
74
|
-
password,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
return registrationRecord as PrismaField<"Users", "password">;
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
async function createRegistrationResponse({ userID, registrationRequest }: {
|
|
84
|
-
userID: PrismaField<"Users", "user_id">;
|
|
85
|
-
registrationRequest: string;
|
|
86
|
-
}) {
|
|
87
|
-
|
|
88
|
-
const { registrationResponse } = opaque.server.createRegistrationResponse({
|
|
89
|
-
serverSetup,
|
|
90
|
-
userIdentifier: userID.toString(),
|
|
91
|
-
registrationRequest,
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
return registrationResponse;
|
|
95
|
-
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function startLoginSession(stater: TypedGenerator<LoginGeneratorStates>) {
|
|
99
|
-
let loginSession;
|
|
100
|
-
do { loginSession = randomBytes(16).toString("base64url"); }
|
|
101
|
-
while (serverState.has(loginSession));
|
|
102
|
-
serverState.set(loginSession, stater);
|
|
103
|
-
return loginSession;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function finishLoginSession(loginSession: string) {
|
|
107
|
-
const stater = serverState.get(loginSession);
|
|
108
|
-
if (stater) serverState.delete(loginSession);
|
|
109
|
-
return stater;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
return {
|
|
113
|
-
serverState,
|
|
114
|
-
LoginGenerator,
|
|
115
|
-
PasswordCreation,
|
|
116
|
-
createRegistrationResponse,
|
|
117
|
-
startLoginSession,
|
|
118
|
-
finishLoginSession
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
type LoginGeneratorStates = [
|
|
125
|
-
[void, loginResponse: string],
|
|
126
|
-
[finishLoginRequest: string, {
|
|
127
|
-
user_id: PrismaField<"Users", "user_id">,
|
|
128
|
-
session: { sessionKey: string; } | undefined
|
|
129
|
-
}]
|
|
130
|
-
]
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* This code verifies plain text passwords.
|
|
137
|
-
* It shouldn't be used in production,
|
|
138
|
-
* but this is how it would be done.
|
|
139
|
-
*/
|
|
140
|
-
function PasswordVerification(userID: string, registrationRecord: string, password: string, serverSetup: string) {
|
|
141
|
-
|
|
142
|
-
const { clientLoginState, startLoginRequest } = opaque.client.startLogin({ password, });
|
|
143
|
-
|
|
144
|
-
// server
|
|
145
|
-
const userIdentifier = userID;
|
|
146
|
-
|
|
147
|
-
const { serverLoginState, loginResponse } = opaque.server.startLogin({
|
|
148
|
-
serverSetup,
|
|
149
|
-
userIdentifier,
|
|
150
|
-
registrationRecord,
|
|
151
|
-
startLoginRequest,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
// client
|
|
155
|
-
const loginResult = opaque.client.finishLogin({
|
|
156
|
-
clientLoginState,
|
|
157
|
-
loginResponse,
|
|
158
|
-
password,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
if (!loginResult) { throw new Error("Login failed"); }
|
|
162
|
-
|
|
163
|
-
const { finishLoginRequest, sessionKey } = loginResult;
|
|
164
|
-
|
|
165
|
-
// server
|
|
166
|
-
// the server session key is only returned after verifying the client's response,
|
|
167
|
-
// which validates that the client actually has the session key.
|
|
168
|
-
const { sessionKey: serversessionkey } = opaque.server.finishLogin({
|
|
169
|
-
finishLoginRequest,
|
|
170
|
-
serverLoginState,
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// This is never done in production, as the key must not be sent across the wire.
|
|
174
|
-
ok(sessionKey === serversessionkey);
|
|
175
|
-
|
|
176
|
-
return { sessionKey };
|
|
177
|
-
|
|
178
|
-
}
|