@striae-org/striae 6.1.7 → 7.0.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 +0 -26
- package/README.md +1 -2
- package/app/components/actions/image-manage.ts +17 -67
- package/functions/api/audit/[[path]].ts +9 -24
- package/functions/api/data/[[path]].ts +9 -24
- package/functions/api/image/[[path]].ts +14 -30
- package/functions/api/pdf/[[path]].ts +9 -24
- package/functions/api/user/[[path]].ts +20 -36
- package/package.json +143 -137
- package/scripts/deploy-all.sh +29 -10
- package/scripts/deploy-config/modules/env-utils.sh +0 -68
- package/scripts/deploy-config/modules/prompt.sh +4 -110
- package/scripts/deploy-config/modules/scaffolding.sh +5 -68
- package/scripts/deploy-config/modules/validation.sh +1 -30
- package/scripts/deploy-pages-secrets.sh +0 -9
- package/scripts/deploy-worker-secrets.sh +2 -8
- package/tsconfig.json +1 -1
- package/workers/audit-worker/package.json +2 -2
- package/workers/audit-worker/src/{audit-worker.example.ts → audit-worker.ts} +1 -17
- package/workers/audit-worker/src/config.ts +1 -6
- package/workers/audit-worker/src/types.ts +0 -1
- package/workers/audit-worker/wrangler.jsonc.example +2 -6
- package/workers/data-worker/package.json +3 -2
- package/workers/data-worker/src/config.ts +1 -6
- package/workers/data-worker/src/{data-worker.example.ts → data-worker.ts} +2 -18
- package/workers/data-worker/src/types.ts +0 -1
- package/workers/data-worker/wrangler.jsonc.example +2 -4
- package/workers/image-worker/package.json +2 -2
- package/workers/image-worker/src/handlers/delete-image.ts +0 -5
- package/workers/image-worker/src/handlers/mint-signed-url.ts +0 -5
- package/workers/image-worker/src/handlers/serve-image.ts +2 -5
- package/workers/image-worker/src/handlers/upload-image.ts +0 -5
- package/workers/image-worker/src/{image-worker.example.ts → image-worker.ts} +2 -15
- package/workers/image-worker/src/router.ts +2 -3
- package/workers/image-worker/src/security/signed-url.ts +2 -2
- package/workers/image-worker/src/types.ts +0 -1
- package/workers/image-worker/wrangler.jsonc.example +2 -1
- package/workers/pdf-worker/package.json +2 -2
- package/workers/pdf-worker/src/{pdf-worker.example.ts → pdf-worker.ts} +1 -23
- package/workers/pdf-worker/wrangler.jsonc.example +2 -1
- package/workers/user-worker/package.json +2 -2
- package/workers/user-worker/src/auth.ts +0 -7
- package/workers/user-worker/src/handlers/user-routes.ts +25 -39
- package/workers/user-worker/src/types.ts +0 -2
- package/workers/user-worker/src/{user-worker.example.ts → user-worker.ts} +15 -30
- package/workers/user-worker/wrangler.jsonc.example +2 -1
- package/wrangler.toml.example +22 -2
- package/worker-configuration.d.ts +0 -7509
- package/workers/image-worker/src/auth.ts +0 -7
|
@@ -2,7 +2,6 @@ import type { PDFGenerationData, PDFGenerationRequest, ReportModule, ReportPdfOp
|
|
|
2
2
|
import { getAuditTrailPdfOptions, isAuditTrailReportMode, renderAuditTrailReport } from './audit-trail-report';
|
|
3
3
|
|
|
4
4
|
interface Env {
|
|
5
|
-
PDF_WORKER_AUTH: string;
|
|
6
5
|
ACCOUNT_ID?: string;
|
|
7
6
|
BROWSER_API_TOKEN?: string;
|
|
8
7
|
}
|
|
@@ -40,15 +39,6 @@ const reportModuleLoaders: Record<string, () => Promise<ReportModule>> = {
|
|
|
40
39
|
|
|
41
40
|
};
|
|
42
41
|
|
|
43
|
-
const corsHeaders: Record<string, string> = {
|
|
44
|
-
'Access-Control-Allow-Origin': 'PAGES_CUSTOM_DOMAIN',
|
|
45
|
-
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
46
|
-
'Access-Control-Allow-Headers': 'Content-Type, X-Custom-Auth-Key',
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const hasValidHeader = (request: Request, env: Env): boolean =>
|
|
50
|
-
request.headers.get('X-Custom-Auth-Key') === env.PDF_WORKER_AUTH;
|
|
51
|
-
|
|
52
42
|
function isTimeoutError(error: unknown): boolean {
|
|
53
43
|
return error instanceof Error && (
|
|
54
44
|
error.name === 'AbortError' ||
|
|
@@ -60,7 +50,7 @@ function isTimeoutError(error: unknown): boolean {
|
|
|
60
50
|
function jsonResponse(body: unknown, status: number): Response {
|
|
61
51
|
return new Response(JSON.stringify(body), {
|
|
62
52
|
status,
|
|
63
|
-
headers: {
|
|
53
|
+
headers: { 'content-type': 'application/json' },
|
|
64
54
|
});
|
|
65
55
|
}
|
|
66
56
|
|
|
@@ -190,10 +180,6 @@ async function renderPdfViaRestEndpoint(env: Env, html: string, pdfOptions: Repo
|
|
|
190
180
|
responseHeaders.set('cache-control', 'no-store');
|
|
191
181
|
}
|
|
192
182
|
|
|
193
|
-
for (const [headerName, headerValue] of Object.entries(corsHeaders)) {
|
|
194
|
-
responseHeaders.set(headerName, headerValue);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
183
|
return new Response(endpointResponse.body, {
|
|
198
184
|
status: endpointResponse.status,
|
|
199
185
|
statusText: endpointResponse.statusText,
|
|
@@ -203,14 +189,6 @@ async function renderPdfViaRestEndpoint(env: Env, html: string, pdfOptions: Repo
|
|
|
203
189
|
|
|
204
190
|
export default {
|
|
205
191
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
206
|
-
if (request.method === 'OPTIONS') {
|
|
207
|
-
return new Response(null, { headers: corsHeaders });
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (!hasValidHeader(request, env)) {
|
|
211
|
-
return jsonResponse({ error: 'Forbidden' }, 403);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
192
|
if (request.method === 'POST') {
|
|
215
193
|
try {
|
|
216
194
|
const payload = await request.json() as unknown;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "user-worker",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.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.84.
|
|
11
|
+
"wrangler": "^4.84.1"
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -1,12 +1,5 @@
|
|
|
1
1
|
import type { Env } from './types';
|
|
2
2
|
|
|
3
|
-
export async function authenticate(request: Request, env: Env): Promise<void> {
|
|
4
|
-
const authKey = request.headers.get('X-Custom-Auth-Key');
|
|
5
|
-
if (authKey !== env.USER_DB_AUTH) {
|
|
6
|
-
throw new Error('Unauthorized');
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
|
|
10
3
|
export function requireUserKvReadConfig(env: Env): void {
|
|
11
4
|
const hasLegacyPrivateKey = typeof env.USER_KV_ENCRYPTION_PRIVATE_KEY === 'string' && env.USER_KV_ENCRYPTION_PRIVATE_KEY.trim().length > 0;
|
|
12
5
|
const hasRegistryPrivateKeys = typeof env.USER_KV_ENCRYPTION_KEYS_JSON === 'string' && env.USER_KV_ENCRYPTION_KEYS_JSON.trim().length > 0;
|
|
@@ -5,56 +5,47 @@ import type {
|
|
|
5
5
|
AccountDeletionProgressEvent,
|
|
6
6
|
DeleteCasesRequest,
|
|
7
7
|
Env,
|
|
8
|
-
ResponseHeaders,
|
|
9
8
|
UserData,
|
|
10
9
|
UserRequestData
|
|
11
10
|
} from '../types';
|
|
12
11
|
|
|
13
|
-
function createJsonResponse(data: unknown,
|
|
12
|
+
function createJsonResponse(data: unknown, status: number = 200): Response {
|
|
14
13
|
return new Response(JSON.stringify(data), {
|
|
15
14
|
status,
|
|
16
|
-
headers: {
|
|
17
|
-
...headers,
|
|
18
|
-
'Content-Type': 'application/json; charset=utf-8'
|
|
19
|
-
}
|
|
15
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' }
|
|
20
16
|
});
|
|
21
17
|
}
|
|
22
18
|
|
|
23
|
-
function createTextResponse(message: string,
|
|
19
|
+
function createTextResponse(message: string, status: number): Response {
|
|
24
20
|
return new Response(message, {
|
|
25
21
|
status,
|
|
26
|
-
headers: {
|
|
27
|
-
...headers,
|
|
28
|
-
'Content-Type': 'text/plain; charset=utf-8'
|
|
29
|
-
}
|
|
22
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
|
|
30
23
|
});
|
|
31
24
|
}
|
|
32
25
|
|
|
33
26
|
export async function handleGetUser(
|
|
34
27
|
env: Env,
|
|
35
|
-
userUid: string
|
|
36
|
-
corsHeaders: ResponseHeaders
|
|
28
|
+
userUid: string
|
|
37
29
|
): Promise<Response> {
|
|
38
30
|
try {
|
|
39
31
|
const userData = await readUserRecord(env, userUid);
|
|
40
32
|
if (userData === null) {
|
|
41
|
-
return createTextResponse('User not found',
|
|
33
|
+
return createTextResponse('User not found', 404);
|
|
42
34
|
}
|
|
43
35
|
|
|
44
|
-
return createJsonResponse(userData
|
|
36
|
+
return createJsonResponse(userData);
|
|
45
37
|
} catch (error) {
|
|
46
38
|
const errorMessage = error instanceof Error ? error.message : 'Unknown user data read error';
|
|
47
39
|
console.error('Failed to get user data:', { uid: userUid, reason: errorMessage });
|
|
48
40
|
|
|
49
|
-
return createTextResponse('Failed to get user data',
|
|
41
|
+
return createTextResponse('Failed to get user data', 500);
|
|
50
42
|
}
|
|
51
43
|
}
|
|
52
44
|
|
|
53
45
|
export async function handleAddUser(
|
|
54
46
|
request: Request,
|
|
55
47
|
env: Env,
|
|
56
|
-
userUid: string
|
|
57
|
-
corsHeaders: ResponseHeaders
|
|
48
|
+
userUid: string
|
|
58
49
|
): Promise<Response> {
|
|
59
50
|
try {
|
|
60
51
|
const requestData: UserRequestData = await request.json();
|
|
@@ -96,16 +87,15 @@ export async function handleAddUser(
|
|
|
96
87
|
|
|
97
88
|
await writeUserRecord(env, userUid, userData);
|
|
98
89
|
|
|
99
|
-
return createJsonResponse(userData,
|
|
90
|
+
return createJsonResponse(userData, existingUser !== null ? 200 : 201);
|
|
100
91
|
} catch {
|
|
101
|
-
return createTextResponse('Failed to save user data',
|
|
92
|
+
return createTextResponse('Failed to save user data', 500);
|
|
102
93
|
}
|
|
103
94
|
}
|
|
104
95
|
|
|
105
96
|
export async function handleDeleteUser(
|
|
106
97
|
env: Env,
|
|
107
|
-
userUid: string
|
|
108
|
-
corsHeaders: ResponseHeaders
|
|
98
|
+
userUid: string
|
|
109
99
|
): Promise<Response> {
|
|
110
100
|
try {
|
|
111
101
|
const result = await executeUserDeletion(env, userUid);
|
|
@@ -113,29 +103,27 @@ export async function handleDeleteUser(
|
|
|
113
103
|
return createJsonResponse({
|
|
114
104
|
success: result.success,
|
|
115
105
|
message: result.message
|
|
116
|
-
}
|
|
106
|
+
});
|
|
117
107
|
} catch (error) {
|
|
118
108
|
console.error('Delete user error:', error);
|
|
119
109
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
120
110
|
|
|
121
111
|
if (errorMessage === 'User not found') {
|
|
122
|
-
return createTextResponse('User not found',
|
|
112
|
+
return createTextResponse('User not found', 404);
|
|
123
113
|
}
|
|
124
114
|
|
|
125
115
|
return createJsonResponse({
|
|
126
116
|
success: false,
|
|
127
117
|
message: 'Failed to delete user account'
|
|
128
|
-
},
|
|
118
|
+
}, 500);
|
|
129
119
|
}
|
|
130
120
|
}
|
|
131
121
|
|
|
132
122
|
export function handleDeleteUserWithProgress(
|
|
133
123
|
env: Env,
|
|
134
|
-
userUid: string
|
|
135
|
-
corsHeaders: ResponseHeaders
|
|
124
|
+
userUid: string
|
|
136
125
|
): Response {
|
|
137
|
-
const sseHeaders
|
|
138
|
-
...corsHeaders,
|
|
126
|
+
const sseHeaders = {
|
|
139
127
|
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
140
128
|
'Cache-Control': 'no-cache, no-transform',
|
|
141
129
|
Connection: 'keep-alive'
|
|
@@ -182,14 +170,13 @@ export function handleDeleteUserWithProgress(
|
|
|
182
170
|
export async function handleAddCases(
|
|
183
171
|
request: Request,
|
|
184
172
|
env: Env,
|
|
185
|
-
userUid: string
|
|
186
|
-
corsHeaders: ResponseHeaders
|
|
173
|
+
userUid: string
|
|
187
174
|
): Promise<Response> {
|
|
188
175
|
try {
|
|
189
176
|
const { cases = [] }: AddCasesRequest = await request.json();
|
|
190
177
|
const userData = await readUserRecord(env, userUid);
|
|
191
178
|
if (!userData) {
|
|
192
|
-
return createTextResponse('User not found',
|
|
179
|
+
return createTextResponse('User not found', 404);
|
|
193
180
|
}
|
|
194
181
|
|
|
195
182
|
const existingCases = userData.cases || [];
|
|
@@ -201,31 +188,30 @@ export async function handleAddCases(
|
|
|
201
188
|
userData.updatedAt = new Date().toISOString();
|
|
202
189
|
await writeUserRecord(env, userUid, userData);
|
|
203
190
|
|
|
204
|
-
return createJsonResponse(userData
|
|
191
|
+
return createJsonResponse(userData);
|
|
205
192
|
} catch {
|
|
206
|
-
return createTextResponse('Failed to add cases',
|
|
193
|
+
return createTextResponse('Failed to add cases', 500);
|
|
207
194
|
}
|
|
208
195
|
}
|
|
209
196
|
|
|
210
197
|
export async function handleDeleteCases(
|
|
211
198
|
request: Request,
|
|
212
199
|
env: Env,
|
|
213
|
-
userUid: string
|
|
214
|
-
corsHeaders: ResponseHeaders
|
|
200
|
+
userUid: string
|
|
215
201
|
): Promise<Response> {
|
|
216
202
|
try {
|
|
217
203
|
const { casesToDelete }: DeleteCasesRequest = await request.json();
|
|
218
204
|
const userData = await readUserRecord(env, userUid);
|
|
219
205
|
if (!userData) {
|
|
220
|
-
return createTextResponse('User not found',
|
|
206
|
+
return createTextResponse('User not found', 404);
|
|
221
207
|
}
|
|
222
208
|
|
|
223
209
|
userData.cases = userData.cases.filter((caseItem) => !casesToDelete.includes(caseItem.caseNumber));
|
|
224
210
|
userData.updatedAt = new Date().toISOString();
|
|
225
211
|
await writeUserRecord(env, userUid, userData);
|
|
226
212
|
|
|
227
|
-
return createJsonResponse(userData
|
|
213
|
+
return createJsonResponse(userData);
|
|
228
214
|
} catch {
|
|
229
|
-
return createTextResponse('Failed to delete cases',
|
|
215
|
+
return createTextResponse('Failed to delete cases', 500);
|
|
230
216
|
}
|
|
231
217
|
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
export interface Env {
|
|
2
|
-
USER_DB_AUTH: string;
|
|
3
2
|
USER_DB: KVNamespace;
|
|
4
3
|
STRIAE_DATA: R2Bucket;
|
|
5
4
|
STRIAE_FILES: R2Bucket;
|
|
6
|
-
R2_KEY_SECRET: string;
|
|
7
5
|
DATA_AT_REST_ENCRYPTION_PRIVATE_KEY?: string;
|
|
8
6
|
DATA_AT_REST_ENCRYPTION_KEY_ID?: string;
|
|
9
7
|
DATA_AT_REST_ENCRYPTION_KEYS_JSON?: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { requireUserKvReadConfig, requireUserKvWriteConfig } from './auth';
|
|
2
2
|
import { USER_CASES_SEGMENT } from './config';
|
|
3
3
|
import {
|
|
4
4
|
handleAddCases,
|
|
@@ -10,31 +10,16 @@ import {
|
|
|
10
10
|
} from './handlers/user-routes';
|
|
11
11
|
import type { Env } from './types';
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
'Access-Control-Allow-Origin': 'PAGES_CUSTOM_DOMAIN',
|
|
15
|
-
'Access-Control-Allow-Methods': 'GET, PUT, DELETE, OPTIONS',
|
|
16
|
-
'Access-Control-Allow-Headers': 'Content-Type, X-Custom-Auth-Key'
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
function createTextResponse(message: string, status: number, headers: Record<string, string>): Response {
|
|
13
|
+
function createTextResponse(message: string, status: number): Response {
|
|
20
14
|
return new Response(message, {
|
|
21
15
|
status,
|
|
22
|
-
headers: {
|
|
23
|
-
...headers,
|
|
24
|
-
'Content-Type': 'text/plain; charset=utf-8'
|
|
25
|
-
}
|
|
16
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
|
|
26
17
|
});
|
|
27
18
|
}
|
|
28
19
|
|
|
29
20
|
export default {
|
|
30
21
|
async fetch(request: Request, env: Env): Promise<Response> {
|
|
31
|
-
if (request.method === 'OPTIONS') {
|
|
32
|
-
return new Response(null, { headers: corsHeaders });
|
|
33
|
-
}
|
|
34
|
-
|
|
35
22
|
try {
|
|
36
|
-
await authenticate(request, env);
|
|
37
|
-
|
|
38
23
|
// DELETE can mutate user KV data (for example /:uid/cases), so non-GET methods require write config.
|
|
39
24
|
if (request.method === 'GET') {
|
|
40
25
|
requireUserKvReadConfig(env);
|
|
@@ -48,15 +33,15 @@ export default {
|
|
|
48
33
|
const isCasesEndpoint = parts[2] === USER_CASES_SEGMENT;
|
|
49
34
|
|
|
50
35
|
if (!userUid) {
|
|
51
|
-
return createTextResponse('Not Found', 404
|
|
36
|
+
return createTextResponse('Not Found', 404);
|
|
52
37
|
}
|
|
53
38
|
|
|
54
39
|
// Handle regular cases endpoint
|
|
55
40
|
if (isCasesEndpoint) {
|
|
56
41
|
switch (request.method) {
|
|
57
|
-
case 'PUT': return handleAddCases(request, env, userUid
|
|
58
|
-
case 'DELETE': return handleDeleteCases(request, env, userUid
|
|
59
|
-
default: return createTextResponse('Method not allowed', 405
|
|
42
|
+
case 'PUT': return handleAddCases(request, env, userUid);
|
|
43
|
+
case 'DELETE': return handleDeleteCases(request, env, userUid);
|
|
44
|
+
default: return createTextResponse('Method not allowed', 405);
|
|
60
45
|
}
|
|
61
46
|
}
|
|
62
47
|
|
|
@@ -65,24 +50,24 @@ export default {
|
|
|
65
50
|
const streamProgress = url.searchParams.get('stream') === 'true' || acceptsEventStream;
|
|
66
51
|
|
|
67
52
|
switch (request.method) {
|
|
68
|
-
case 'GET': return handleGetUser(env, userUid
|
|
69
|
-
case 'PUT': return handleAddUser(request, env, userUid
|
|
53
|
+
case 'GET': return handleGetUser(env, userUid);
|
|
54
|
+
case 'PUT': return handleAddUser(request, env, userUid);
|
|
70
55
|
case 'DELETE': return streamProgress
|
|
71
|
-
? handleDeleteUserWithProgress(env, userUid
|
|
72
|
-
: handleDeleteUser(env, userUid
|
|
73
|
-
default: return createTextResponse('Method not allowed', 405
|
|
56
|
+
? handleDeleteUserWithProgress(env, userUid)
|
|
57
|
+
: handleDeleteUser(env, userUid);
|
|
58
|
+
default: return createTextResponse('Method not allowed', 405);
|
|
74
59
|
}
|
|
75
60
|
} catch (error) {
|
|
76
61
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
77
62
|
if (errorMessage === 'Unauthorized') {
|
|
78
|
-
return createTextResponse('Forbidden', 403
|
|
63
|
+
return createTextResponse('Forbidden', 403);
|
|
79
64
|
}
|
|
80
65
|
|
|
81
66
|
if (errorMessage === 'User KV encryption is not fully configured') {
|
|
82
|
-
return createTextResponse(errorMessage, 500
|
|
67
|
+
return createTextResponse(errorMessage, 500);
|
|
83
68
|
}
|
|
84
69
|
|
|
85
|
-
return createTextResponse('Internal Server Error', 500
|
|
70
|
+
return createTextResponse('Internal Server Error', 500);
|
|
86
71
|
}
|
|
87
72
|
}
|
|
88
73
|
};
|
package/wrangler.toml.example
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
#:schema node_modules/wrangler/config-schema.json
|
|
2
2
|
name = "PAGES_PROJECT_NAME"
|
|
3
|
-
compatibility_date = "2026-04-
|
|
3
|
+
compatibility_date = "2026-04-21"
|
|
4
4
|
compatibility_flags = ["nodejs_compat"]
|
|
5
5
|
pages_build_output_dir = "./build/client"
|
|
6
6
|
|
|
7
7
|
[placement]
|
|
8
|
-
mode = "smart"
|
|
8
|
+
mode = "smart"
|
|
9
|
+
|
|
10
|
+
[[services]]
|
|
11
|
+
binding = "USER_WORKER"
|
|
12
|
+
service = "USER_WORKER_NAME"
|
|
13
|
+
|
|
14
|
+
[[services]]
|
|
15
|
+
binding = "DATA_WORKER"
|
|
16
|
+
service = "DATA_WORKER_NAME"
|
|
17
|
+
|
|
18
|
+
[[services]]
|
|
19
|
+
binding = "AUDIT_WORKER"
|
|
20
|
+
service = "AUDIT_WORKER_NAME"
|
|
21
|
+
|
|
22
|
+
[[services]]
|
|
23
|
+
binding = "IMAGE_WORKER"
|
|
24
|
+
service = "IMAGES_WORKER_NAME"
|
|
25
|
+
|
|
26
|
+
[[services]]
|
|
27
|
+
binding = "PDF_WORKER"
|
|
28
|
+
service = "PDF_WORKER_NAME"
|