@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.
Files changed (65) hide show
  1. package/README.md +3 -1
  2. package/dist/server.d.ts +923 -0
  3. package/dist/server.js +1763 -1415
  4. package/dist/server.js.map +1 -1
  5. package/editions/mws-docs/tiddlers/$__StoryList.tid +3 -3
  6. package/editions/mws-docs/tiddlers/MWS and SQLite.md +11 -0
  7. package/editions/mws-docs/tiddlers/Request Server.md +20 -0
  8. package/editions/mws-docs/tiddlers/Request Server.md.meta +5 -0
  9. package/package.json +16 -10
  10. package/prisma/schema.prisma +1 -0
  11. package/react-user-mgmt/package-lock.json +76 -321
  12. package/react-user-mgmt/package.json +2 -5
  13. package/react-user-mgmt/public/chunk-YALWN5OT.js +2 -0
  14. package/react-user-mgmt/public/chunk-YALWN5OT.js.map +7 -0
  15. package/react-user-mgmt/public/esm-QKDFEP3B.js +3 -0
  16. package/react-user-mgmt/public/esm-QKDFEP3B.js.map +7 -0
  17. package/react-user-mgmt/public/index.html +1 -26
  18. package/react-user-mgmt/public/main.js +68 -109
  19. package/react-user-mgmt/public/main.js.map +4 -4
  20. package/react-user-mgmt/public/stats.css +597 -0
  21. package/react-user-mgmt/public/stats.html +59 -0
  22. package/react-user-mgmt/public/stats.js +2013 -0
  23. package/react-user-mgmt/public/stats.json +1 -0
  24. package/react-user-mgmt/public/stats.svg +4 -0
  25. package/react-user-mgmt/tsconfig.json +3 -0
  26. package/dist/server.cjs +0 -16677
  27. package/dist/server.cjs.map +0 -1
  28. package/src/StateObject.ts +0 -316
  29. package/src/commander.ts +0 -417
  30. package/src/commands/divider.ts +0 -20
  31. package/src/commands/index.ts +0 -24
  32. package/src/commands/mws-init-store.ts +0 -77
  33. package/src/commands/mws-listen.ts +0 -136
  34. package/src/commands/mws-load-archive.ts +0 -281
  35. package/src/commands/mws-load-plugin-bags.ts +0 -151
  36. package/src/commands/mws-load-tiddlers.ts +0 -37
  37. package/src/commands/mws-load-wiki-folder.ts +0 -124
  38. package/src/commands/mws-render-tiddlywiki5.ts +0 -55
  39. package/src/commands/mws-save-archive.ts +0 -111
  40. package/src/commands/mws-save-tiddler-text.ts +0 -33
  41. package/src/commands/tests-complete.ts +0 -21
  42. package/src/global.ts +0 -247
  43. package/src/routes/TiddlerStore.ts +0 -722
  44. package/src/routes/bag-file-server.ts +0 -340
  45. package/src/routes/cache.ts +0 -25
  46. package/src/routes/index.ts +0 -138
  47. package/src/routes/managers/index.ts +0 -17
  48. package/src/routes/managers/manager-recipes.ts +0 -438
  49. package/src/routes/managers/manager-users.ts +0 -201
  50. package/src/routes/managers/router-tiddlers.ts +0 -313
  51. package/src/routes/router.ts +0 -429
  52. package/src/routes/services/PasswordService.ts +0 -178
  53. package/src/routes/services/attachments.ts +0 -259
  54. package/src/routes/services/sessions.ts +0 -166
  55. package/src/routes/tw-test.ts +0 -65
  56. package/src/server.ts +0 -151
  57. package/src/setupDevServer.ts +0 -71
  58. package/src/streamer.ts +0 -430
  59. package/src/tiddlywiki.ts +0 -94
  60. package/src/utils/data-checks.ts +0 -172
  61. package/src/utils/index.ts +0 -4
  62. package/src/utils/rootRoute.ts +0 -237
  63. package/src/utils/utils.ts +0 -403
  64. package/src/utils/zodAssert.ts +0 -269
  65. /package/prisma/{migrations/notes.md → notes.md} +0 -0
@@ -1,316 +0,0 @@
1
- import { Readable } from 'stream';
2
- import { filterAsync, mapAsync, readMultipartData, sendResponse } from './utils';
3
- import { STREAM_ENDED, Streamer, StreamerState } from './streamer';
4
- import { PassThrough } from 'node:stream';
5
- import { AllowedMethod, BodyFormat, RouteMatch, Router, SiteConfig } from './routes/router';
6
- import * as z from 'zod';
7
- import { AuthUser } from './routes/services/sessions';
8
- import { Prisma, PrismaClient } from '@prisma/client';
9
- import { Types } from '@prisma/client/runtime/library';
10
- import { DataChecks } from './utils';
11
- import { setupDevServer } from "./setupDevServer";
12
- import { Commander } from './commander';
13
- import { PasswordService } from './routes/services/PasswordService';
14
-
15
- export interface AuthStateRouteACL {
16
-
17
- }
18
-
19
-
20
-
21
- // This class abstracts the request/response cycle into a single object.
22
- // It hides most of the details from the routes, allowing us to easily change the underlying server implementation.
23
- export class StateObject<
24
- B extends BodyFormat = BodyFormat,
25
- M extends AllowedMethod = AllowedMethod,
26
- RoutePathParams extends string[][] = string[][],
27
- D = unknown
28
- > extends StreamerState {
29
-
30
- z: typeof z = z;
31
-
32
- STREAM_ENDED: typeof STREAM_ENDED = STREAM_ENDED;
33
-
34
- get method(): M { return super.method as M; }
35
- readMultipartData
36
- sendResponse
37
- /** This is set in makeRouter */
38
- sendDevServer!: () => ReturnType<Awaited<ReturnType<typeof setupDevServer>>>;
39
-
40
- data!:
41
- B extends "string" ? string :
42
- B extends "buffer" ? Buffer :
43
- B extends "www-form-urlencoded-urlsearchparams" ? URLSearchParams :
44
- B extends "stream" ? Readable :
45
- B extends "ignore" ? undefined :
46
- D;
47
- /**
48
- * an *object from entries* of all the pathParams in the tree mapped to the path regex matches from that route.
49
- *
50
- * Object.fromEntries takes the last value if there are duplicates, so conflicting names will have the last value in the path.
51
- *
52
- * Conflicting names would be defined on the route definitions, so just change the name there if there is a conflict.
53
- */
54
- pathParams: Record<RoutePathParams extends (infer X extends string)[][] ? X : never, string | undefined>;
55
- /**
56
- * The query params. Because these aren't checked anywhere,
57
- * the value includes undefined since it will be that if
58
- * the key is not specified at all.
59
- *
60
- * This will always satisfy the zod schema: `z.record(z.array(z.string()))`
61
- */
62
- queryParams: Record<string, string[] | undefined>;
63
-
64
-
65
- private engine: Commander["engine"];
66
- public config: SiteConfig;
67
- public PasswordService: PasswordService;
68
-
69
- constructor(
70
- streamer: Streamer,
71
- /** The array of Route tree nodes the request matched. */
72
- private routePath: RouteMatch[],
73
- /** The bodyformat that ended up taking precedence. This should be correctly typed. */
74
- public bodyFormat: B,
75
- public user: AuthUser,
76
- public commander: Commander,
77
- public tiddlerCache: Router["tiddlerCache"],
78
- ) {
79
- super(streamer);
80
-
81
- this.engine = commander.engine;
82
- this.config = commander.siteConfig;
83
- this.PasswordService = commander.PasswordService;
84
-
85
- this.readMultipartData = readMultipartData.bind(this);
86
- this.sendResponse = sendResponse.bind(undefined, this.config, this);
87
-
88
- this.pathParams = Object.fromEntries<string | undefined>(routePath.map(r =>
89
- r.route.pathParams
90
- ?.map((e, i) => [e, r.params[i]] as const)
91
- .filter(<T>(e: T): e is T & {} => !!e)
92
- ?? []
93
- ).flat()) as any;
94
-
95
- const pathParamsZodCheck = z.record(z.string().transform(zodURIComponent).optional()).safeParse(this.pathParams);
96
- if (!pathParamsZodCheck.success) console.log("BUG: Path params zod error", pathParamsZodCheck.error, this.pathParams);
97
- else this.pathParams = pathParamsZodCheck.data;
98
-
99
- this.queryParams = Object.fromEntries([...this.urlInfo.searchParams.keys()]
100
- .map(key => [key, this.urlInfo.searchParams.getAll(key)] as const));
101
-
102
- const queryParamsZodCheck = z.record(z.array(z.string())).safeParse(this.queryParams);
103
- if (!queryParamsZodCheck.success) console.log("BUG: Query params zod error", queryParamsZodCheck.error, this.queryParams);
104
- else this.queryParams = queryParamsZodCheck.data;
105
-
106
-
107
- }
108
-
109
- okUser() {
110
- if (!this.user.isLoggedIn) throw "User not authenticated";
111
- }
112
- okAdmin() {
113
- if (!this.user.isLoggedIn) throw "User not authenticated";
114
- if (!this.user.isAdmin) throw "User is not an admin";
115
- }
116
-
117
- // createStore(engine: PrismaTxnClient) {
118
- // const sql = createStrictAwaitProxy(new SqlTiddlerDatabase(engine));
119
- // return createStrictAwaitProxy(new SqlTiddlerStore(sql, this.attachmentStore));
120
- // }
121
-
122
- $transaction = async <T>(fn: (prisma: PrismaTxnClient) => Promise<T>): Promise<T> => {
123
- return await this.engine.$transaction(prisma => fn(prisma as PrismaTxnClient));
124
- }
125
-
126
- $transactionTuple<P extends Prisma.PrismaPromise<any>[]>(arg: (prisma: Commander["engine"]) => [...P], options?: { isolationLevel?: Prisma.TransactionIsolationLevel }): Promise<Types.Utils.UnwrapTuple<P>> {
127
- return this.engine.$transaction(arg(this.engine), options);
128
- }
129
-
130
- makeTiddlerEtag(options: { bag_name: string; tiddler_id: string | number; }) {
131
- // why do we need tiddler_id AND bag_name? tiddler_id is unique across all tiddlers
132
- if (options.bag_name && options.tiddler_id) {
133
- return `"tiddler:${options.bag_name}/${options.tiddler_id}"`;
134
- } else {
135
- throw "Missing bag_name or tiddler_id";
136
- }
137
- }
138
-
139
- /** type-narrowing helper function. This affects anywhere T is used. */
140
- isBodyFormat<T extends B, S extends { [K in B]: StateObject<K, M, RoutePathParams, D> }[T]>(format: T): this is S {
141
- return this.bodyFormat as BodyFormat === format;
142
- }
143
-
144
-
145
- /**
146
- *
147
- * Sends a **302** status code and **Location** header to the client.
148
- *
149
- *
150
- * - **301 Moved Permanently:** The resource has been permanently moved to a new URL.
151
- * - **302 Found:** The resource is temporarily located at a different URL.
152
- * - **303 See Other:** Fetch the resource from another URI using a GET request.
153
- * - **307 Temporary Redirect:** The resource is temporarily located at a different URL; the same HTTP method should be used.
154
- * - **308 Permanent Redirect:** The resource has permanently moved; the client should use the new URL in future requests.
155
- */
156
- redirect(location: string, pushLocation?: boolean): typeof STREAM_ENDED {
157
- return this.sendEmpty(302, { 'Location': location });
158
- }
159
-
160
- sendSSE(retryMilliseconds?: number) {
161
- if (retryMilliseconds !== undefined)
162
- if (typeof retryMilliseconds !== "number" || retryMilliseconds < 0)
163
- throw new Error("Invalid retryMilliseconds: must be a non-negative number");
164
-
165
- const stream = new PassThrough();
166
-
167
- this.sendStream(200, {
168
- "content-type": "text/event-stream",
169
- "cache-control": "no-cache",
170
- "connection": "keep-alive",
171
- "x-accel-buffering": "no",
172
- }, stream);
173
-
174
- stream.write(": This page is a server-sent event stream. It will continue loading until you close it.\n");
175
- stream.write(": https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events\n");
176
- stream.write("\n");
177
-
178
- /**
179
- *
180
- * @param {string} eventName The event name. If zero-length, the field is omitted
181
- * @param eventData The data to send. Must be stringify-able to JSON.
182
- * @param {string} eventId The event id. If zero-length, the field is omitted.
183
- */
184
- const emitEvent = (eventName: string, eventData: any, eventId: string) => {
185
- if (typeof eventName !== "string")
186
- throw new Error("Event name must be a string (a zero-length string disables the field)");
187
- if (eventName.includes("\n"))
188
- throw new Error("Event name cannot contain newlines");
189
- if (typeof eventId !== "string")
190
- throw new Error("Event ID must be a string");
191
- if (eventId.includes("\n"))
192
- throw new Error("Event ID cannot contain newlines");
193
-
194
- stream.write([
195
- eventName && `event: ${eventName}`,
196
- `data: ${JSON.stringify(eventData)}`,
197
- eventId && `id: ${eventId}`,
198
- retryMilliseconds && `retry: ${retryMilliseconds}`,
199
- ].filter(e => e).join("\n") + "\n\n");
200
- }
201
-
202
- return {
203
- /** Emit an SSE event */
204
- emitEvent,
205
- emitComment: (comment: string) => stream.write(`: ${comment}\n\n`),
206
- close: () => stream.end(),
207
- onClose: (callback: () => void) => stream.on("close", callback),
208
- };
209
-
210
- }
211
- // several changes were made to this function
212
- // - it shouldn't assume it knows where the entityName is
213
- // - it throws STREAM_ENDED if it ends the request
214
- // - it does not check whether headers have been sent before throwing.
215
- // - headers should not be sent before it is called.
216
- // - it should not send a response more than once since it should throw every time it does.
217
- // and now it is replaced by the getACL prisma query.
218
- async checkACL(entityType: EntityType, entityName: string, permissionName: ACLPermissionName): Promise<void> {
219
- console.error("checkACL is not implemented");
220
- throw this.sendEmpty(500);
221
- }
222
-
223
-
224
- async getRecipeACL(
225
- recipe_name: PrismaField<"Recipes", "recipe_name">,
226
- needWrite: boolean
227
- ) {
228
- const { user_id, isAdmin, role_ids } = this.user;
229
-
230
- const prisma = this.engine;
231
- const read = new DataChecks(this.config).getBagWhereACL({ permission: "READ", user_id, role_ids });
232
- const write = new DataChecks(this.config).getBagWhereACL({ permission: "WRITE", user_id, role_ids });
233
-
234
- const [recipe, canRead, canWrite] = await prisma.$transaction([
235
- prisma.recipes.findUnique({
236
- select: { recipe_id: true },
237
- where: { recipe_name }
238
- }),
239
- isAdmin ? prisma.$queryRaw`SELECT 1` : prisma.recipes.findUnique({
240
- select: { recipe_id: true },
241
- where: { recipe_name, recipe_bags: { every: { bag: { OR: read } } } }
242
- }),
243
- isAdmin ? prisma.$queryRaw`SELECT 1` : needWrite ? prisma.recipes.findUnique({
244
- select: { recipe_id: true },
245
- where: { recipe_name, recipe_bags: { some: { position: 0, bag: { OR: write } } } }
246
- }) : prisma.$queryRaw`SELECT 1`,
247
- ]);
248
-
249
- return { recipe, canRead, canWrite };
250
-
251
- }
252
- async assertRecipeACL(
253
- recipe_name: PrismaField<"Recipes", "recipe_name">,
254
- needWrite: boolean
255
- ) {
256
-
257
- const { recipe, canRead, canWrite } = await this.getRecipeACL(recipe_name, needWrite);
258
-
259
- if (!recipe) throw this.sendEmpty(404, { "x-reason": "recipe not found" });
260
- if (!canRead) throw this.sendEmpty(403, { "x-reason": "no read permission" });
261
- if (!canWrite) throw this.sendEmpty(403, { "x-reason": "no write permission" });
262
-
263
- }
264
-
265
- async getBagACL(
266
- bag_name: PrismaField<"Bags", "bag_name">,
267
- needWrite: boolean
268
- ) {
269
- const { user_id, isAdmin, role_ids } = this.user;
270
- const prisma = this.engine;
271
- const read = new DataChecks(this.config).getBagWhereACL({ permission: "READ", user_id, role_ids });
272
- const write = new DataChecks(this.config).getBagWhereACL({ permission: "WRITE", user_id, role_ids });
273
- const [bag, canRead, canWrite] = await prisma.$transaction([
274
- prisma.bags.findUnique({
275
- select: { bag_id: true, is_plugin: true, owner_id: true },
276
- where: { bag_name }
277
- }),
278
- isAdmin ? prisma.$queryRaw`SELECT 1` : prisma.bags.findUnique({
279
- select: { bag_id: true },
280
- where: { bag_name, OR: read }
281
- }),
282
- isAdmin ? prisma.$queryRaw`SELECT 1` : needWrite ? prisma.bags.findUnique({
283
- select: { bag_id: true },
284
- where: { bag_name, OR: write }
285
- }) : prisma.$queryRaw`SELECT 1`,
286
- ]);
287
- return { bag, canRead, canWrite };
288
- }
289
-
290
- async assertBagACL(
291
- bag_name: PrismaField<"Bags", "bag_name">,
292
- needWrite: boolean
293
- ) {
294
- const { bag, canRead, canWrite } = await this.getBagACL(bag_name, needWrite);
295
-
296
- if (!bag) throw this.sendEmpty(404, { "x-reason": "recipe not found" });
297
- if (!canRead) throw this.sendEmpty(403, { "x-reason": "no read permission" });
298
- if (!canWrite) throw this.sendEmpty(403, { "x-reason": "no write permission" });
299
-
300
- return bag;
301
-
302
- }
303
-
304
- }
305
-
306
- const zodURIComponent = (val: string, ctx: z.RefinementCtx) => {
307
- try {
308
- return decodeURIComponent(val);
309
- } catch (e) {
310
- ctx.addIssue({
311
- code: z.ZodIssueCode.custom,
312
- message: "Invalid URI component",
313
- });
314
- return z.NEVER;
315
- }
316
- }