@tiddlywiki/mws 0.0.12 → 0.0.14
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/.env-cmdrc +5 -0
- 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 +17 -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/StateObject.ts
DELETED
|
@@ -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
|
-
}
|