@striae-org/striae 7.0.0 → 7.1.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/.env.example +8 -14
- package/app/components/canvas/canvas.module.css +12 -0
- package/app/components/canvas/canvas.tsx +26 -5
- package/functions/api/_shared/lists-client.ts +39 -0
- package/functions/api/_shared/registration-allowlist.ts +5 -4
- package/functions/api/auth/can-register.ts +7 -2
- package/functions/api/pdf/[[path]].ts +4 -1
- package/functions/api/user/[[path]].ts +11 -5
- package/package.json +10 -8
- package/scripts/deploy-all.sh +3 -3
- package/scripts/deploy-config/modules/prompt.sh +43 -7
- package/scripts/deploy-config/modules/scaffolding.sh +19 -0
- package/scripts/deploy-config/modules/validation.sh +3 -0
- package/scripts/deploy-config.sh +0 -33
- package/scripts/deploy-pages-secrets.sh +1 -10
- package/scripts/deploy-worker-secrets.sh +19 -1
- package/scripts/install-workers.sh +4 -3
- package/scripts/update-markdown-versions.cjs +1 -0
- package/workers/audit-worker/package.json +2 -2
- package/workers/audit-worker/wrangler.jsonc.example +1 -1
- package/workers/data-worker/package.json +2 -2
- package/workers/data-worker/wrangler.jsonc.example +1 -1
- package/workers/image-worker/package.json +2 -2
- package/workers/image-worker/src/handlers/delete-image.ts +5 -5
- package/workers/image-worker/src/handlers/mint-signed-url.ts +5 -5
- package/workers/image-worker/src/handlers/serve-image.ts +7 -7
- package/workers/image-worker/src/handlers/upload-image.ts +4 -4
- package/workers/image-worker/src/image-worker.ts +4 -4
- package/workers/image-worker/src/router.ts +11 -11
- package/workers/image-worker/src/types.ts +1 -1
- package/workers/image-worker/wrangler.jsonc.example +1 -1
- package/workers/lists-worker/package.json +13 -0
- package/workers/lists-worker/src/lists-worker.ts +97 -0
- package/workers/lists-worker/src/types.ts +4 -0
- package/workers/lists-worker/wrangler.jsonc.example +23 -0
- package/workers/pdf-worker/package.json +2 -2
- package/workers/pdf-worker/wrangler.jsonc.example +1 -1
- package/workers/user-worker/package.json +2 -2
- package/workers/user-worker/src/handlers/user-routes.ts +26 -34
- package/workers/user-worker/src/types.ts +13 -0
- package/workers/user-worker/src/user-worker.ts +18 -24
- package/workers/user-worker/wrangler.jsonc.example +1 -1
- package/wrangler.toml.example +6 -2
- package/app/config-example/members.emails +0 -11
- package/app/config-example/primershear.emails +0 -6
- package/scripts/deploy-members-emails.sh +0 -102
- package/scripts/deploy-primershear-emails.sh +0 -101
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "data-worker",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"deploy": "wrangler deploy",
|
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
11
|
"@cloudflare/vitest-pool-workers": "^0.14.9",
|
|
12
|
-
"wrangler": "^4.
|
|
12
|
+
"wrangler": "^4.85.0"
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "image-worker",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"deploy": "wrangler deploy",
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
"start": "wrangler dev"
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
|
-
"wrangler": "^4.
|
|
11
|
+
"wrangler": "^4.85.0"
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CreateResponse, Env } from '../types';
|
|
2
2
|
import { parseFileId } from '../utils/path-utils';
|
|
3
3
|
|
|
4
4
|
export async function handleImageDelete(
|
|
5
5
|
request: Request,
|
|
6
6
|
env: Env,
|
|
7
|
-
|
|
7
|
+
respond: CreateResponse
|
|
8
8
|
): Promise<Response> {
|
|
9
9
|
const fileId = parseFileId(new URL(request.url).pathname);
|
|
10
10
|
if (!fileId) {
|
|
11
|
-
return
|
|
11
|
+
return respond({ error: 'Image ID is required' }, 400);
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const existing = await env.STRIAE_FILES.head(fileId);
|
|
15
15
|
if (!existing) {
|
|
16
|
-
return
|
|
16
|
+
return respond({ error: 'File not found' }, 404);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
await env.STRIAE_FILES.delete(fileId);
|
|
20
|
-
return
|
|
20
|
+
return respond({ success: true });
|
|
21
21
|
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
signSignedAccessPayload
|
|
6
6
|
} from '../security/signed-url';
|
|
7
7
|
import type {
|
|
8
|
-
|
|
8
|
+
CreateResponse,
|
|
9
9
|
Env,
|
|
10
10
|
SignedAccessPayload
|
|
11
11
|
} from '../types';
|
|
@@ -18,13 +18,13 @@ export async function handleSignedUrlMinting(
|
|
|
18
18
|
request: Request,
|
|
19
19
|
env: Env,
|
|
20
20
|
fileId: string,
|
|
21
|
-
|
|
21
|
+
respond: CreateResponse
|
|
22
22
|
): Promise<Response> {
|
|
23
23
|
requireSignedUrlConfig(env);
|
|
24
24
|
|
|
25
25
|
const existing = await env.STRIAE_FILES.head(fileId);
|
|
26
26
|
if (!existing) {
|
|
27
|
-
return
|
|
27
|
+
return respond({ error: 'File not found' }, 404);
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
let requestedExpiresInSeconds: number | undefined;
|
|
@@ -58,7 +58,7 @@ export async function handleSignedUrlMinting(
|
|
|
58
58
|
console.error('Invalid IMAGE_SIGNED_URL_BASE_URL configuration', {
|
|
59
59
|
reason: error instanceof Error ? error.message : String(error)
|
|
60
60
|
});
|
|
61
|
-
return
|
|
61
|
+
return respond({ error: 'Signed URL base URL is misconfigured' }, 500);
|
|
62
62
|
}
|
|
63
63
|
} else {
|
|
64
64
|
baseUrl = new URL(request.url).origin;
|
|
@@ -66,7 +66,7 @@ export async function handleSignedUrlMinting(
|
|
|
66
66
|
|
|
67
67
|
const signedUrl = `${baseUrl}/${encodeURIComponent(fileId)}?st=${encodeURIComponent(signedToken)}`;
|
|
68
68
|
|
|
69
|
-
return
|
|
69
|
+
return respond({
|
|
70
70
|
success: true,
|
|
71
71
|
result: {
|
|
72
72
|
fileId,
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
requireEncryptionRetrievalConfig
|
|
4
4
|
} from '../security/key-registry';
|
|
5
5
|
import { requireSignedUrlConfig, verifySignedAccessToken } from '../security/signed-url';
|
|
6
|
-
import type {
|
|
6
|
+
import type { CreateResponse, Env } from '../types';
|
|
7
7
|
import { buildSafeContentDisposition } from '../utils/content-disposition';
|
|
8
8
|
import { extractEnvelope } from '../utils/storage-metadata';
|
|
9
9
|
|
|
@@ -11,7 +11,7 @@ export async function handleImageServing(
|
|
|
11
11
|
request: Request,
|
|
12
12
|
env: Env,
|
|
13
13
|
fileId: string,
|
|
14
|
-
|
|
14
|
+
respond: CreateResponse
|
|
15
15
|
): Promise<Response> {
|
|
16
16
|
const requestUrl = new URL(request.url);
|
|
17
17
|
const hasSignedToken = requestUrl.searchParams.has('st');
|
|
@@ -21,27 +21,27 @@ export async function handleImageServing(
|
|
|
21
21
|
requireSignedUrlConfig(env);
|
|
22
22
|
|
|
23
23
|
if (!signedToken || signedToken.trim().length === 0) {
|
|
24
|
-
return
|
|
24
|
+
return respond({ error: 'Invalid or expired signed URL token' }, 403);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const tokenValid = await verifySignedAccessToken(signedToken, fileId, env);
|
|
28
28
|
if (!tokenValid) {
|
|
29
|
-
return
|
|
29
|
+
return respond({ error: 'Invalid or expired signed URL token' }, 403);
|
|
30
30
|
}
|
|
31
31
|
} else {
|
|
32
|
-
return
|
|
32
|
+
return respond({ error: 'Unauthorized' }, 403);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
requireEncryptionRetrievalConfig(env);
|
|
36
36
|
|
|
37
37
|
const file = await env.STRIAE_FILES.get(fileId);
|
|
38
38
|
if (!file) {
|
|
39
|
-
return
|
|
39
|
+
return respond({ error: 'File not found' }, 404);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const envelope = extractEnvelope(file);
|
|
43
43
|
if (!envelope) {
|
|
44
|
-
return
|
|
44
|
+
return respond({ error: 'Missing data-at-rest envelope metadata' }, 500);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
const encryptedData = await file.arrayBuffer();
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
import { encryptBinaryForStorage } from '../encryption-utils';
|
|
2
2
|
import { requireEncryptionUploadConfig } from '../security/key-registry';
|
|
3
|
-
import type {
|
|
3
|
+
import type { CreateResponse, Env } from '../types';
|
|
4
4
|
import { deriveFileKind } from '../utils/content-disposition';
|
|
5
5
|
|
|
6
6
|
export async function handleImageUpload(
|
|
7
7
|
request: Request,
|
|
8
8
|
env: Env,
|
|
9
|
-
|
|
9
|
+
respond: CreateResponse
|
|
10
10
|
): Promise<Response> {
|
|
11
11
|
requireEncryptionUploadConfig(env);
|
|
12
12
|
|
|
13
13
|
const formData = await request.formData();
|
|
14
14
|
const fileValue = formData.get('file');
|
|
15
15
|
if (!(fileValue instanceof Blob)) {
|
|
16
|
-
return
|
|
16
|
+
return respond({ error: 'Missing file upload payload' }, 400);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const fileBlob = fileValue;
|
|
@@ -44,7 +44,7 @@ export async function handleImageUpload(
|
|
|
44
44
|
}
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
return
|
|
47
|
+
return respond({
|
|
48
48
|
success: true,
|
|
49
49
|
errors: [],
|
|
50
50
|
messages: [],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { routeImageWorkerRequest } from './router';
|
|
2
|
-
import type { APIResponse, Env } from './types';
|
|
2
|
+
import type { APIResponse, CreateResponse, Env } from './types';
|
|
3
3
|
|
|
4
|
-
const
|
|
4
|
+
const createWorkerResponse: CreateResponse = (data: APIResponse, status: number = 200): Response => new Response(
|
|
5
5
|
JSON.stringify(data),
|
|
6
6
|
{
|
|
7
7
|
status,
|
|
@@ -12,10 +12,10 @@ const createJsonResponse = (data: APIResponse, status: number = 200): Response =
|
|
|
12
12
|
export default {
|
|
13
13
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
14
14
|
try {
|
|
15
|
-
return await routeImageWorkerRequest(request, env,
|
|
15
|
+
return await routeImageWorkerRequest(request, env, createWorkerResponse);
|
|
16
16
|
} catch (error) {
|
|
17
17
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
18
|
-
return
|
|
18
|
+
return createWorkerResponse({ error: errorMessage }, 500);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
};
|
|
@@ -2,51 +2,51 @@ import { handleImageDelete } from './handlers/delete-image';
|
|
|
2
2
|
import { handleSignedUrlMinting } from './handlers/mint-signed-url';
|
|
3
3
|
import { handleImageServing } from './handlers/serve-image';
|
|
4
4
|
import { handleImageUpload } from './handlers/upload-image';
|
|
5
|
-
import type {
|
|
5
|
+
import type { CreateResponse, Env } from './types';
|
|
6
6
|
import { parsePathSegments } from './utils/path-utils';
|
|
7
7
|
|
|
8
8
|
export async function routeImageWorkerRequest(
|
|
9
9
|
request: Request,
|
|
10
10
|
env: Env,
|
|
11
|
-
|
|
11
|
+
respond: CreateResponse
|
|
12
12
|
): Promise<Response> {
|
|
13
13
|
const requestUrl = new URL(request.url);
|
|
14
14
|
const pathSegments = parsePathSegments(requestUrl.pathname);
|
|
15
15
|
if (!pathSegments) {
|
|
16
|
-
return
|
|
16
|
+
return respond({ error: 'Invalid image path encoding' }, 400);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
switch (request.method) {
|
|
20
20
|
case 'POST': {
|
|
21
21
|
if (pathSegments.length === 0) {
|
|
22
|
-
return handleImageUpload(request, env,
|
|
22
|
+
return handleImageUpload(request, env, respond);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
if (pathSegments.length === 2 && pathSegments[1] === 'signed-url') {
|
|
26
|
-
return handleSignedUrlMinting(request, env, pathSegments[0],
|
|
26
|
+
return handleSignedUrlMinting(request, env, pathSegments[0], respond);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
return
|
|
29
|
+
return respond({ error: 'Not found' }, 404);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
case 'GET': {
|
|
33
33
|
const fileId = pathSegments.length === 1 ? pathSegments[0] : null;
|
|
34
34
|
if (!fileId) {
|
|
35
|
-
return
|
|
35
|
+
return respond({ error: 'Image ID is required' }, 400);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
return handleImageServing(request, env, fileId,
|
|
38
|
+
return handleImageServing(request, env, fileId, respond);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
case 'DELETE': {
|
|
42
42
|
if (pathSegments.length !== 1) {
|
|
43
|
-
return
|
|
43
|
+
return respond({ error: 'Not found' }, 404);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
return handleImageDelete(request, env,
|
|
46
|
+
return handleImageDelete(request, env, respond);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
default:
|
|
50
|
-
return
|
|
50
|
+
return respond({ error: 'Method not allowed' }, 405);
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { Env } from './types';
|
|
2
|
+
|
|
3
|
+
const JSON_HEADERS: HeadersInit = {
|
|
4
|
+
'Content-Type': 'application/json',
|
|
5
|
+
'Cache-Control': 'no-store',
|
|
6
|
+
'Pragma': 'no-cache',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** Routes map URL path segment to the KV key used in STRIAE_LISTS. */
|
|
10
|
+
const ROUTE_TO_KV_KEY: Record<string, string> = {
|
|
11
|
+
members: 'allow',
|
|
12
|
+
primershear: 'primershear',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function jsonResponse(data: Record<string, unknown>, status = 200): Response {
|
|
16
|
+
return new Response(JSON.stringify(data), { status, headers: JSON_HEADERS });
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Constant-time string comparison to mitigate timing side-channels on auth checks.
|
|
21
|
+
* Both strings are encoded to bytes and compared with a full XOR pass.
|
|
22
|
+
*/
|
|
23
|
+
function timingSafeEqual(a: string, b: string): boolean {
|
|
24
|
+
const encoder = new TextEncoder();
|
|
25
|
+
const aBytes = encoder.encode(a);
|
|
26
|
+
const bBytes = encoder.encode(b);
|
|
27
|
+
if (aBytes.length !== bBytes.length) return false;
|
|
28
|
+
let diff = 0;
|
|
29
|
+
for (let i = 0; i < aBytes.length; i++) {
|
|
30
|
+
diff |= aBytes[i] ^ bBytes[i];
|
|
31
|
+
}
|
|
32
|
+
return diff === 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isAuthorized(request: Request, secret: string): boolean {
|
|
36
|
+
if (!secret) return false;
|
|
37
|
+
const auth = request.headers.get('Authorization');
|
|
38
|
+
if (!auth || !auth.startsWith('Bearer ')) return false;
|
|
39
|
+
return timingSafeEqual(auth.slice(7), secret);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default {
|
|
43
|
+
async fetch(request: Request, env: Env): Promise<Response> {
|
|
44
|
+
const url = new URL(request.url);
|
|
45
|
+
const segment = url.pathname.replace(/^\/+|\/+$/g, '');
|
|
46
|
+
const kvKey = ROUTE_TO_KV_KEY[segment];
|
|
47
|
+
|
|
48
|
+
if (!kvKey) {
|
|
49
|
+
return jsonResponse({ error: 'Not found' }, 404);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (request.method === 'GET') {
|
|
53
|
+
if (!isAuthorized(request, env.LISTS_ADMIN_SECRET)) {
|
|
54
|
+
return jsonResponse({ error: 'Unauthorized' }, 401);
|
|
55
|
+
}
|
|
56
|
+
const raw = (await env.STRIAE_LISTS.get(kvKey)) ?? '';
|
|
57
|
+
const list = raw ? raw.split(',').map(e => e.trim().toLowerCase()).filter(Boolean).join(',') : '';
|
|
58
|
+
return jsonResponse({ list });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (request.method === 'POST' || request.method === 'DELETE') {
|
|
62
|
+
if (!isAuthorized(request, env.LISTS_ADMIN_SECRET)) {
|
|
63
|
+
return jsonResponse({ error: 'Unauthorized' }, 401);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
let body: { entry?: unknown };
|
|
67
|
+
try {
|
|
68
|
+
body = await request.json() as { entry?: unknown };
|
|
69
|
+
} catch {
|
|
70
|
+
return jsonResponse({ error: 'Invalid JSON body' }, 400);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const entry = typeof body.entry === 'string' ? body.entry.trim().toLowerCase() : '';
|
|
74
|
+
if (!entry) {
|
|
75
|
+
return jsonResponse({ error: 'Missing or empty entry' }, 400);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const current = (await env.STRIAE_LISTS.get(kvKey)) ?? '';
|
|
79
|
+
const entries = current ? current.split(',').map(e => e.trim().toLowerCase()).filter(Boolean) : [];
|
|
80
|
+
|
|
81
|
+
if (request.method === 'POST') {
|
|
82
|
+
if (!entries.includes(entry)) {
|
|
83
|
+
entries.push(entry);
|
|
84
|
+
}
|
|
85
|
+
await env.STRIAE_LISTS.put(kvKey, entries.join(','));
|
|
86
|
+
return jsonResponse({ ok: true });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// DELETE
|
|
90
|
+
const filtered = entries.filter(e => e !== entry);
|
|
91
|
+
await env.STRIAE_LISTS.put(kvKey, filtered.join(','));
|
|
92
|
+
return jsonResponse({ ok: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return jsonResponse({ error: 'Method not allowed' }, 405);
|
|
96
|
+
},
|
|
97
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "LISTS_WORKER_NAME",
|
|
3
|
+
"account_id": "ACCOUNT_ID",
|
|
4
|
+
"main": "src/lists-worker.ts",
|
|
5
|
+
"workers_dev": false,
|
|
6
|
+
"compatibility_date": "2026-04-25",
|
|
7
|
+
"compatibility_flags": [
|
|
8
|
+
"nodejs_compat"
|
|
9
|
+
],
|
|
10
|
+
|
|
11
|
+
"observability": {
|
|
12
|
+
"enabled": true
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
"kv_namespaces": [
|
|
16
|
+
{
|
|
17
|
+
"binding": "STRIAE_LISTS",
|
|
18
|
+
"id": "STRIAE_LISTS_KV_ID"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
|
|
22
|
+
"placement": { "mode": "smart" }
|
|
23
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pdf-worker",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"generate:assets": "node scripts/generate-assets.js",
|
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
"start": "wrangler dev"
|
|
10
10
|
},
|
|
11
11
|
"devDependencies": {
|
|
12
|
-
"wrangler": "^4.
|
|
12
|
+
"wrangler": "^4.85.0"
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "user-worker",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"deploy": "wrangler deploy",
|
|
@@ -8,6 +8,6 @@
|
|
|
8
8
|
"start": "wrangler dev"
|
|
9
9
|
},
|
|
10
10
|
"devDependencies": {
|
|
11
|
-
"wrangler": "^4.
|
|
11
|
+
"wrangler": "^4.85.0"
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -3,49 +3,38 @@ import { readUserRecord, writeUserRecord } from '../storage/user-records';
|
|
|
3
3
|
import type {
|
|
4
4
|
AddCasesRequest,
|
|
5
5
|
AccountDeletionProgressEvent,
|
|
6
|
+
CreateResponse,
|
|
6
7
|
DeleteCasesRequest,
|
|
7
8
|
Env,
|
|
8
9
|
UserData,
|
|
9
10
|
UserRequestData
|
|
10
11
|
} from '../types';
|
|
11
12
|
|
|
12
|
-
function createJsonResponse(data: unknown, status: number = 200): Response {
|
|
13
|
-
return new Response(JSON.stringify(data), {
|
|
14
|
-
status,
|
|
15
|
-
headers: { 'Content-Type': 'application/json; charset=utf-8' }
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function createTextResponse(message: string, status: number): Response {
|
|
20
|
-
return new Response(message, {
|
|
21
|
-
status,
|
|
22
|
-
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
|
|
26
13
|
export async function handleGetUser(
|
|
27
14
|
env: Env,
|
|
28
|
-
userUid: string
|
|
15
|
+
userUid: string,
|
|
16
|
+
respond: CreateResponse
|
|
29
17
|
): Promise<Response> {
|
|
30
18
|
try {
|
|
31
19
|
const userData = await readUserRecord(env, userUid);
|
|
32
20
|
if (userData === null) {
|
|
33
|
-
return
|
|
21
|
+
return respond({ error: 'User not found' }, 404);
|
|
34
22
|
}
|
|
35
23
|
|
|
36
|
-
return
|
|
24
|
+
return respond(userData);
|
|
37
25
|
} catch (error) {
|
|
38
26
|
const errorMessage = error instanceof Error ? error.message : 'Unknown user data read error';
|
|
39
27
|
console.error('Failed to get user data:', { uid: userUid, reason: errorMessage });
|
|
40
28
|
|
|
41
|
-
return
|
|
29
|
+
return respond({ error: 'Failed to get user data' }, 500);
|
|
42
30
|
}
|
|
43
31
|
}
|
|
44
32
|
|
|
45
33
|
export async function handleAddUser(
|
|
46
34
|
request: Request,
|
|
47
35
|
env: Env,
|
|
48
|
-
userUid: string
|
|
36
|
+
userUid: string,
|
|
37
|
+
respond: CreateResponse
|
|
49
38
|
): Promise<Response> {
|
|
50
39
|
try {
|
|
51
40
|
const requestData: UserRequestData = await request.json();
|
|
@@ -87,20 +76,21 @@ export async function handleAddUser(
|
|
|
87
76
|
|
|
88
77
|
await writeUserRecord(env, userUid, userData);
|
|
89
78
|
|
|
90
|
-
return
|
|
79
|
+
return respond(userData, existingUser !== null ? 200 : 201);
|
|
91
80
|
} catch {
|
|
92
|
-
return
|
|
81
|
+
return respond({ error: 'Failed to save user data' }, 500);
|
|
93
82
|
}
|
|
94
83
|
}
|
|
95
84
|
|
|
96
85
|
export async function handleDeleteUser(
|
|
97
86
|
env: Env,
|
|
98
|
-
userUid: string
|
|
87
|
+
userUid: string,
|
|
88
|
+
respond: CreateResponse
|
|
99
89
|
): Promise<Response> {
|
|
100
90
|
try {
|
|
101
91
|
const result = await executeUserDeletion(env, userUid);
|
|
102
92
|
|
|
103
|
-
return
|
|
93
|
+
return respond({
|
|
104
94
|
success: result.success,
|
|
105
95
|
message: result.message
|
|
106
96
|
});
|
|
@@ -109,10 +99,10 @@ export async function handleDeleteUser(
|
|
|
109
99
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
110
100
|
|
|
111
101
|
if (errorMessage === 'User not found') {
|
|
112
|
-
return
|
|
102
|
+
return respond({ error: 'User not found' }, 404);
|
|
113
103
|
}
|
|
114
104
|
|
|
115
|
-
return
|
|
105
|
+
return respond({
|
|
116
106
|
success: false,
|
|
117
107
|
message: 'Failed to delete user account'
|
|
118
108
|
}, 500);
|
|
@@ -170,13 +160,14 @@ export function handleDeleteUserWithProgress(
|
|
|
170
160
|
export async function handleAddCases(
|
|
171
161
|
request: Request,
|
|
172
162
|
env: Env,
|
|
173
|
-
userUid: string
|
|
163
|
+
userUid: string,
|
|
164
|
+
respond: CreateResponse
|
|
174
165
|
): Promise<Response> {
|
|
175
166
|
try {
|
|
176
167
|
const { cases = [] }: AddCasesRequest = await request.json();
|
|
177
168
|
const userData = await readUserRecord(env, userUid);
|
|
178
169
|
if (!userData) {
|
|
179
|
-
return
|
|
170
|
+
return respond({ error: 'User not found' }, 404);
|
|
180
171
|
}
|
|
181
172
|
|
|
182
173
|
const existingCases = userData.cases || [];
|
|
@@ -188,30 +179,31 @@ export async function handleAddCases(
|
|
|
188
179
|
userData.updatedAt = new Date().toISOString();
|
|
189
180
|
await writeUserRecord(env, userUid, userData);
|
|
190
181
|
|
|
191
|
-
return
|
|
182
|
+
return respond(userData);
|
|
192
183
|
} catch {
|
|
193
|
-
return
|
|
184
|
+
return respond({ error: 'Failed to add cases' }, 500);
|
|
194
185
|
}
|
|
195
186
|
}
|
|
196
187
|
|
|
197
188
|
export async function handleDeleteCases(
|
|
198
189
|
request: Request,
|
|
199
190
|
env: Env,
|
|
200
|
-
userUid: string
|
|
191
|
+
userUid: string,
|
|
192
|
+
respond: CreateResponse
|
|
201
193
|
): Promise<Response> {
|
|
202
194
|
try {
|
|
203
195
|
const { casesToDelete }: DeleteCasesRequest = await request.json();
|
|
204
196
|
const userData = await readUserRecord(env, userUid);
|
|
205
197
|
if (!userData) {
|
|
206
|
-
return
|
|
198
|
+
return respond({ error: 'User not found' }, 404);
|
|
207
199
|
}
|
|
208
200
|
|
|
209
|
-
userData.cases = userData.cases.filter((caseItem) => !casesToDelete.includes(caseItem.caseNumber));
|
|
201
|
+
userData.cases = (userData.cases || []).filter((caseItem) => !casesToDelete.includes(caseItem.caseNumber));
|
|
210
202
|
userData.updatedAt = new Date().toISOString();
|
|
211
203
|
await writeUserRecord(env, userUid, userData);
|
|
212
204
|
|
|
213
|
-
return
|
|
205
|
+
return respond(userData);
|
|
214
206
|
} catch {
|
|
215
|
-
return
|
|
207
|
+
return respond({ error: 'Failed to delete cases' }, 500);
|
|
216
208
|
}
|
|
217
209
|
}
|
|
@@ -28,6 +28,19 @@ export interface PrivateKeyRegistry {
|
|
|
28
28
|
|
|
29
29
|
export type DecryptionTelemetryOutcome = 'primary-hit' | 'fallback-hit' | 'all-failed';
|
|
30
30
|
|
|
31
|
+
export interface SuccessResponse {
|
|
32
|
+
success: boolean;
|
|
33
|
+
message?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ErrorResponse {
|
|
37
|
+
error: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export type APIResponse = SuccessResponse | ErrorResponse | UserData;
|
|
41
|
+
|
|
42
|
+
export type CreateResponse = (data: APIResponse, status?: number) => Response;
|
|
43
|
+
|
|
31
44
|
export interface CaseItem {
|
|
32
45
|
caseNumber: string;
|
|
33
46
|
caseName?: string;
|