@critical-path/server 0.16.0 → 0.17.0
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/CHANGELOG.md +34 -0
- package/README.md +5 -5
- package/dist/index.test.js +41 -36
- package/dist/index.test.js.map +1 -1
- package/dist/openapi.js +1 -1
- package/dist/openapi.js.map +1 -1
- package/dist/router.d.ts +12 -8
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +17 -33
- package/dist/router.js.map +1 -1
- package/package.json +2 -2
- package/src/index.test.ts +50 -36
- package/src/openapi.ts +1 -1
- package/src/router.ts +26 -35
package/src/router.ts
CHANGED
|
@@ -2,6 +2,9 @@ import { CriticalPathEngine, type AuthorType, type CriticalPathConfig } from '@c
|
|
|
2
2
|
import * as schemas from '@critical-path/core/schemas';
|
|
3
3
|
import { buildOpenApiDocument } from './openapi.js';
|
|
4
4
|
|
|
5
|
+
/** Actor for requests when no `getContext` is configured or it resolves no user. */
|
|
6
|
+
export const ANONYMOUS_ACTOR = Object.freeze({ userId: 'anonymous', username: 'Anonymous', actorType: 'user' as const });
|
|
7
|
+
|
|
5
8
|
const CORS_ALLOW_METHODS = 'GET, POST, PUT, PATCH, DELETE, OPTIONS';
|
|
6
9
|
const DEFAULT_CORS_ALLOW_HEADERS = ['Content-Type', 'Authorization'];
|
|
7
10
|
|
|
@@ -53,8 +56,8 @@ export interface CriticalPathRouterOptions {
|
|
|
53
56
|
exposeErrors?: boolean;
|
|
54
57
|
/**
|
|
55
58
|
* Resolves the caller's identity from the request (session cookie, bearer token, etc.).
|
|
56
|
-
* When it returns a `userId`, all mutations in the request are attributed to that user
|
|
57
|
-
*
|
|
59
|
+
* When it returns a `userId`, all mutations in the request are attributed to that user;
|
|
60
|
+
* otherwise they are attributed to `ANONYMOUS_ACTOR`. Request bodies never carry identity.
|
|
58
61
|
*/
|
|
59
62
|
getContext?: (request: Request) => RequestContext | null | undefined | Promise<RequestContext | null | undefined>;
|
|
60
63
|
/** Reject requests with `401` unless `getContext` returns a `userId`. Defaults to `false`. */
|
|
@@ -64,7 +67,10 @@ export interface CriticalPathRouterOptions {
|
|
|
64
67
|
* When omitted, everything up to the first `/critical-path` segment is stripped.
|
|
65
68
|
*/
|
|
66
69
|
basePath?: string;
|
|
67
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* CORS policy. Defaults to `false` (no CORS headers, so only same-origin browsers can call the
|
|
72
|
+
* API). Set `{ origins: [...] }` to allow specific sites, or `{ origins: '*' }` for any site.
|
|
73
|
+
*/
|
|
68
74
|
cors?: CorsOptions | false;
|
|
69
75
|
}
|
|
70
76
|
|
|
@@ -92,7 +98,7 @@ export class CriticalPathRouter {
|
|
|
92
98
|
this.getContext = options.getContext;
|
|
93
99
|
this.requireAuth = options.requireAuth ?? false;
|
|
94
100
|
this.basePath = options.basePath ? '/' + options.basePath.replace(/^\/+|\/+$/g, '') : undefined;
|
|
95
|
-
this.cors = options.cors
|
|
101
|
+
this.cors = options.cors ?? false;
|
|
96
102
|
if (this.cors && this.cors.origins === '*' && this.cors.credentials) {
|
|
97
103
|
throw new Error('CORS credentials cannot be combined with origins "*"; list the allowed origins instead.');
|
|
98
104
|
}
|
|
@@ -170,9 +176,12 @@ export class CriticalPathRouter {
|
|
|
170
176
|
if (this.requireAuth && !context?.userId) {
|
|
171
177
|
return this.jsonResponse({ error: 'Authentication required' }, 401);
|
|
172
178
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
179
|
+
// Every request runs as an actor, so authorship never comes from request bodies.
|
|
180
|
+
const engine = this.engine.withActor(
|
|
181
|
+
context?.userId
|
|
182
|
+
? { userId: context.userId, username: context.userName, actorType: context.actorType }
|
|
183
|
+
: ANONYMOUS_ACTOR
|
|
184
|
+
);
|
|
176
185
|
|
|
177
186
|
try {
|
|
178
187
|
// OpenAPI contract (behind the same auth as every other route)
|
|
@@ -304,7 +313,7 @@ export class CriticalPathRouter {
|
|
|
304
313
|
}
|
|
305
314
|
if (method === 'POST') {
|
|
306
315
|
const body = await this.readBody(request, schemas.CreateCommentSchema, { taskId });
|
|
307
|
-
const comment = await engine.addComment(
|
|
316
|
+
const comment = await engine.addComment(body);
|
|
308
317
|
return this.jsonResponse({ comment }, 201);
|
|
309
318
|
}
|
|
310
319
|
} else if (subResource === 'attachments') {
|
|
@@ -314,7 +323,7 @@ export class CriticalPathRouter {
|
|
|
314
323
|
}
|
|
315
324
|
if (method === 'POST') {
|
|
316
325
|
const body = await this.readBody(request, schemas.CreateAttachmentSchema, { taskId });
|
|
317
|
-
const attachment = await engine.createAttachment(
|
|
326
|
+
const attachment = await engine.createAttachment(body);
|
|
318
327
|
return this.jsonResponse({ attachment }, 201);
|
|
319
328
|
}
|
|
320
329
|
} else if (subResource === 'dependencies') {
|
|
@@ -542,24 +551,16 @@ export class CriticalPathRouter {
|
|
|
542
551
|
if (commentId && subResource === 'reactions') {
|
|
543
552
|
if (method === 'POST') {
|
|
544
553
|
const body = await this.readBody(request, schemas.CommentReactionSchema);
|
|
545
|
-
const
|
|
546
|
-
const comment = await engine.addCommentReaction(commentId, { emoji: body.emoji, userId });
|
|
554
|
+
const comment = await engine.addCommentReaction(commentId, body);
|
|
547
555
|
if (!comment) return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
548
556
|
return this.jsonResponse({ comment }, 200);
|
|
549
557
|
}
|
|
550
558
|
if (method === 'DELETE') {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
} catch {
|
|
555
|
-
// Body may be empty on DELETE, fallback to searchParams
|
|
556
|
-
}
|
|
557
|
-
const emoji = body.emoji || url.searchParams.get('emoji');
|
|
558
|
-
const userId = context?.userId ?? (body.userId || url.searchParams.get('userId'));
|
|
559
|
-
if (!emoji || !userId) {
|
|
560
|
-
return this.jsonResponse({ error: 'emoji and userId are required' }, 400);
|
|
559
|
+
const emoji = url.searchParams.get('emoji');
|
|
560
|
+
if (!emoji) {
|
|
561
|
+
return this.jsonResponse({ error: 'emoji query parameter is required' }, 400);
|
|
561
562
|
}
|
|
562
|
-
const comment = await engine.removeCommentReaction(commentId, { emoji
|
|
563
|
+
const comment = await engine.removeCommentReaction(commentId, { emoji });
|
|
563
564
|
if (!comment) return this.jsonResponse({ error: 'Comment not found' }, 404);
|
|
564
565
|
return this.jsonResponse({ comment }, 200);
|
|
565
566
|
}
|
|
@@ -572,7 +573,7 @@ export class CriticalPathRouter {
|
|
|
572
573
|
}
|
|
573
574
|
if (method === 'POST') {
|
|
574
575
|
const body = await this.readBody(request, schemas.CreateCommentSchema);
|
|
575
|
-
const comment = await engine.addComment(
|
|
576
|
+
const comment = await engine.addComment(body);
|
|
576
577
|
return this.jsonResponse({ comment }, 201);
|
|
577
578
|
}
|
|
578
579
|
} else {
|
|
@@ -607,7 +608,7 @@ export class CriticalPathRouter {
|
|
|
607
608
|
} else if (attachmentId === 'upload') {
|
|
608
609
|
if (method === 'POST') {
|
|
609
610
|
const body = await this.readBody(request, schemas.UploadAttachmentSchema);
|
|
610
|
-
const attachment = await engine.uploadAttachmentFile(
|
|
611
|
+
const attachment = await engine.uploadAttachmentFile(body);
|
|
611
612
|
return this.jsonResponse({ attachment }, 201);
|
|
612
613
|
}
|
|
613
614
|
} else if (!attachmentId) {
|
|
@@ -620,7 +621,7 @@ export class CriticalPathRouter {
|
|
|
620
621
|
}
|
|
621
622
|
if (method === 'POST') {
|
|
622
623
|
const body = await this.readBody(request, schemas.CreateAttachmentSchema);
|
|
623
|
-
const attachment = await engine.createAttachment(
|
|
624
|
+
const attachment = await engine.createAttachment(body);
|
|
624
625
|
return this.jsonResponse({ attachment }, 201);
|
|
625
626
|
}
|
|
626
627
|
} else {
|
|
@@ -724,16 +725,6 @@ export class CriticalPathRouter {
|
|
|
724
725
|
return schemas.parsePayload(schema as any, { ...body, ...overrides });
|
|
725
726
|
}
|
|
726
727
|
|
|
727
|
-
/**
|
|
728
|
-
* Returns the identity to record for a write: the resolved caller when there is one, otherwise
|
|
729
|
-
* the value supplied in the body. Throws a 400 when neither is present.
|
|
730
|
-
*/
|
|
731
|
-
private requireIdentity(context: RequestContext | null | undefined, claimed: string | undefined, field: string): string {
|
|
732
|
-
const identity = context?.userId ?? claimed;
|
|
733
|
-
if (!identity) throw new BadRequestError(`${field} is required`);
|
|
734
|
-
return identity;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
728
|
/** The API root as seen by this request, e.g. `https://host/api/critical-path`. */
|
|
738
729
|
private openApiServerUrl(url: URL, subpath: string): string {
|
|
739
730
|
const root = url.pathname.slice(0, url.pathname.length - subpath.length).replace(/\/+$/, '');
|