cogsbox-sync 0.0.1
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/dist/bin/cli.d.ts +1 -0
- package/dist/bin/cli.js +61 -0
- package/dist/index.d.ts +903 -0
- package/dist/index.js +2755 -0
- package/package.json +36 -0
- package/templates/worker/.editorconfig +12 -0
- package/templates/worker/.prettierrc +6 -0
- package/templates/worker/bin/init.ts +53 -0
- package/templates/worker/package-lock.json +4087 -0
- package/templates/worker/package.json +30 -0
- package/templates/worker/schema-processor/Dockerfile +16 -0
- package/templates/worker/schema-processor/package.json +10 -0
- package/templates/worker/schema-processor/server.js +483 -0
- package/templates/worker/schema-processor/serverOldExpress.js +488 -0
- package/templates/worker/src/UserObject.ts +40 -0
- package/templates/worker/src/auth.ts +58 -0
- package/templates/worker/src/index.ts +1860 -0
- package/templates/worker/src/utility.ts +101 -0
- package/templates/worker/test/index.spec.ts +25 -0
- package/templates/worker/test/tsconfig.json +8 -0
- package/templates/worker/tsconfig.json +46 -0
- package/templates/worker/vitest.config.mts +11 -0
- package/templates/worker/worker-configuration.d.ts +7954 -0
- package/templates/worker/wrangler.jsonc +90 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
// schema-processor/server.js
|
|
2
|
+
const express = require('express');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
const app = express();
|
|
6
|
+
|
|
7
|
+
app.use(express.json({ limit: '10mb' }));
|
|
8
|
+
|
|
9
|
+
// In-memory cache for schemas per tenant
|
|
10
|
+
const schemaCache = new Map();
|
|
11
|
+
|
|
12
|
+
// Health check endpoint
|
|
13
|
+
app.get('/health', (req, res) => {
|
|
14
|
+
res.json({
|
|
15
|
+
status: 'healthy',
|
|
16
|
+
timestamp: new Date().toISOString(),
|
|
17
|
+
cachedSchemas: Array.from(schemaCache.keys())
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Utility function to safely import schema module
|
|
22
|
+
async function importSchemaModule(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
// Clear Node.js module cache to ensure fresh import
|
|
25
|
+
delete require.cache[filePath];
|
|
26
|
+
|
|
27
|
+
const schemaModule = await import(`file://${filePath}?t=${Date.now()}`);
|
|
28
|
+
|
|
29
|
+
if (!schemaModule.syncSchema) {
|
|
30
|
+
throw new Error('syncSchema not found in module');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return schemaModule;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(`Failed to import schema module from ${filePath}:`, error.message);
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Load schema for a tenant and cache it
|
|
41
|
+
async function loadSchemaForTenant(tenantId) {
|
|
42
|
+
console.log(`[loadSchemaForTenant] Starting load for tenant ${tenantId}`);
|
|
43
|
+
|
|
44
|
+
if (schemaCache.has(tenantId)) {
|
|
45
|
+
console.log(`[loadSchemaForTenant] Schema already cached for tenant ${tenantId}`);
|
|
46
|
+
return schemaCache.get(tenantId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Load from file system cache first
|
|
50
|
+
const cacheFile = `/tmp/schema-${tenantId}.mjs`;
|
|
51
|
+
console.log(`[loadSchemaForTenant] Checking cache file: ${cacheFile}`);
|
|
52
|
+
|
|
53
|
+
if (fs.existsSync(cacheFile)) {
|
|
54
|
+
try {
|
|
55
|
+
console.log(`[loadSchemaForTenant] Cache file exists, importing...`);
|
|
56
|
+
const schemaModule = await importSchemaModule(cacheFile);
|
|
57
|
+
|
|
58
|
+
schemaCache.set(tenantId, schemaModule.syncSchema);
|
|
59
|
+
console.log(`[loadSchemaForTenant] Schema cached successfully for tenant ${tenantId}`);
|
|
60
|
+
return schemaModule.syncSchema;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error(`[loadSchemaForTenant] Cache file import failed:`, error.message);
|
|
63
|
+
console.log('Cache file invalid, will reload:', error.message);
|
|
64
|
+
try {
|
|
65
|
+
fs.unlinkSync(cacheFile);
|
|
66
|
+
console.log(`[loadSchemaForTenant] Removed invalid cache file`);
|
|
67
|
+
} catch (unlinkError) {
|
|
68
|
+
console.error(`[loadSchemaForTenant] Failed to remove cache file:`, unlinkError.message);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
} else {
|
|
72
|
+
console.log(`[loadSchemaForTenant] Cache file does not exist`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log(`[loadSchemaForTenant] No schema available for tenant ${tenantId}`);
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// GET endpoint to check schema status
|
|
80
|
+
app.get('/load-schema', async (req, res) => {
|
|
81
|
+
try {
|
|
82
|
+
const { tenantId } = req.query;
|
|
83
|
+
|
|
84
|
+
if (!tenantId) {
|
|
85
|
+
return res.status(400).json({ error: 'tenantId is required' });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const schema = await loadSchemaForTenant(tenantId);
|
|
89
|
+
|
|
90
|
+
// Handle both schema formats - direct schemas or nested under 'schemas'
|
|
91
|
+
let schemaKeys = [];
|
|
92
|
+
if (schema) {
|
|
93
|
+
if (schema.schemas) {
|
|
94
|
+
schemaKeys = Object.keys(schema.schemas);
|
|
95
|
+
} else {
|
|
96
|
+
schemaKeys = Object.keys(schema);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
res.json({
|
|
101
|
+
hasSchema: !!schema,
|
|
102
|
+
schemaKeys: schemaKeys
|
|
103
|
+
});
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Error in GET /load-schema:', error);
|
|
106
|
+
res.status(500).json({ error: error.message });
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// POST endpoint to load/update schema
|
|
111
|
+
app.post('/load-schema', async (req, res) => {
|
|
112
|
+
try {
|
|
113
|
+
const { tenantId } = req.query;
|
|
114
|
+
const { bundledCode } = req.body;
|
|
115
|
+
|
|
116
|
+
if (!tenantId) {
|
|
117
|
+
return res.status(400).json({ error: 'tenantId is required' });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (bundledCode) {
|
|
121
|
+
// Save and cache new schema
|
|
122
|
+
const cacheFile = `/tmp/schema-${tenantId}.mjs`;
|
|
123
|
+
fs.writeFileSync(cacheFile, bundledCode);
|
|
124
|
+
|
|
125
|
+
const schemaModule = await importSchemaModule(cacheFile);
|
|
126
|
+
schemaCache.set(tenantId, schemaModule.syncSchema);
|
|
127
|
+
|
|
128
|
+
console.log(`Schema loaded and cached for tenant ${tenantId}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const schema = await loadSchemaForTenant(tenantId);
|
|
132
|
+
|
|
133
|
+
// Handle both schema formats
|
|
134
|
+
let schemaKeys = [];
|
|
135
|
+
if (schema) {
|
|
136
|
+
if (schema.schemas) {
|
|
137
|
+
schemaKeys = Object.keys(schema.schemas);
|
|
138
|
+
} else {
|
|
139
|
+
schemaKeys = Object.keys(schema);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
res.json({
|
|
144
|
+
success: true,
|
|
145
|
+
hasSchema: !!schema,
|
|
146
|
+
schemaKeys: schemaKeys
|
|
147
|
+
});
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('Error loading schema:', error);
|
|
150
|
+
res.status(500).json({ error: error.message });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
app.post('/validate', async (req, res) => {
|
|
155
|
+
try {
|
|
156
|
+
const { data, schemaKey, bundledCode, context } = req.body;
|
|
157
|
+
const { tenantId } = req.query;
|
|
158
|
+
|
|
159
|
+
if (!tenantId || !schemaKey || data === undefined || !bundledCode) {
|
|
160
|
+
return res.status(400).json({
|
|
161
|
+
valid: false,
|
|
162
|
+
errors: [{ path: [], message: 'Missing tenantId, schemaKey, data, or bundledCode' }]
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const tempFile = `/tmp/schema-${tenantId}-${Date.now()}.mjs`;
|
|
167
|
+
fs.writeFileSync(tempFile, bundledCode);
|
|
168
|
+
|
|
169
|
+
const schemaModule = await importSchemaModule(tempFile);
|
|
170
|
+
fs.unlinkSync(tempFile);
|
|
171
|
+
|
|
172
|
+
let schemaDefinition = null;
|
|
173
|
+
if (schemaModule.syncSchema && schemaModule.syncSchema.schemas && schemaModule.syncSchema.schemas[schemaKey]) {
|
|
174
|
+
schemaDefinition = schemaModule.syncSchema.schemas[schemaKey];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (!schemaDefinition) {
|
|
178
|
+
return res.status(404).json({
|
|
179
|
+
valid: false,
|
|
180
|
+
errors: [{ path: [], message: `Schema '${schemaKey}' not found within the provided bundle.` }]
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const validator = schemaDefinition.validate;
|
|
185
|
+
if (typeof validator !== 'function') {
|
|
186
|
+
throw new Error(`Validator function not found for schema '${schemaKey}'`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Pass the context from the request
|
|
190
|
+
const result = validator(data, context || {});
|
|
191
|
+
|
|
192
|
+
// --- CORRECTED LOGIC ---
|
|
193
|
+
if (!result.success) {
|
|
194
|
+
// The correct property name from Zod is .issues, not .errors
|
|
195
|
+
return res.status(422).json({
|
|
196
|
+
valid: false,
|
|
197
|
+
// Using the correct "issues" property from the Zod error object
|
|
198
|
+
errors: result.error.issues
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
// --- END CORRECTION ---
|
|
202
|
+
|
|
203
|
+
res.json({ valid: true, errors: [] });
|
|
204
|
+
|
|
205
|
+
} catch (error) {
|
|
206
|
+
console.error('[Container] /validate Error:', error);
|
|
207
|
+
// Also correct the property name here in the catch block for safety
|
|
208
|
+
if (error.name === 'ZodError') {
|
|
209
|
+
return res.status(422).json({
|
|
210
|
+
valid: false,
|
|
211
|
+
errors: error.issues // Use .issues here as well
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
res.status(500).json({ valid: false, errors: [{ path: [], message: error.message }] });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
app.post('/validate-path', async (req, res) => {
|
|
218
|
+
try {
|
|
219
|
+
const { path, value, schemaKey, bundledCode } = req.body;
|
|
220
|
+
const { tenantId } = req.query;
|
|
221
|
+
|
|
222
|
+
if (!tenantId || !schemaKey || !bundledCode || !path) {
|
|
223
|
+
return res.status(400).json({
|
|
224
|
+
valid: false,
|
|
225
|
+
errors: [{ path: [], message: 'Missing required parameters' }]
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Write and load schema
|
|
230
|
+
const tempFile = `/tmp/schema-${tenantId}-${Date.now()}.mjs`;
|
|
231
|
+
fs.writeFileSync(tempFile, bundledCode);
|
|
232
|
+
const schemaModule = await importSchemaModule(tempFile);
|
|
233
|
+
fs.unlinkSync(tempFile);
|
|
234
|
+
|
|
235
|
+
// Get the specific schema from the schemas collection
|
|
236
|
+
const schemaDefinition = schemaModule.syncSchema?.schemas?.[schemaKey];
|
|
237
|
+
|
|
238
|
+
if (!schemaDefinition) {
|
|
239
|
+
return res.status(404).json({
|
|
240
|
+
valid: false,
|
|
241
|
+
errors: [{ path: [], message: `Schema '${schemaKey}' not found` }]
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// The validation schema is at schemaDefinition.schemas.validation
|
|
246
|
+
// NOT schemaDefinition.schemas?.validation (which would be looking for a nested schemas.schemas)
|
|
247
|
+
if (!schemaDefinition.schemas || !schemaDefinition.schemas.validation) {
|
|
248
|
+
return res.status(404).json({
|
|
249
|
+
valid: false,
|
|
250
|
+
errors: [{ path: [], message: `Validation schema not found for '${schemaKey}'` }]
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Walk to the target field
|
|
255
|
+
let fieldValidator = schemaDefinition.schemas.validation;
|
|
256
|
+
|
|
257
|
+
for (let i = 0; i < path.length; i++) {
|
|
258
|
+
const segment = path[i];
|
|
259
|
+
|
|
260
|
+
if (typeof segment === 'number' || segment.startsWith('id:')) {
|
|
261
|
+
// Array element
|
|
262
|
+
if (fieldValidator._def?.typeName === 'ZodArray') {
|
|
263
|
+
fieldValidator = fieldValidator._def.type || fieldValidator.element;
|
|
264
|
+
} else {
|
|
265
|
+
return res.json({ valid: true }); // Path doesn't exist in schema
|
|
266
|
+
}
|
|
267
|
+
} else {
|
|
268
|
+
// Object property
|
|
269
|
+
if (fieldValidator.shape) {
|
|
270
|
+
fieldValidator = fieldValidator.shape[segment];
|
|
271
|
+
} else if (fieldValidator._def?.shape) {
|
|
272
|
+
const shape = fieldValidator._def.shape();
|
|
273
|
+
fieldValidator = shape[segment];
|
|
274
|
+
} else {
|
|
275
|
+
return res.json({ valid: true }); // Path doesn't exist in schema
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (!fieldValidator) {
|
|
280
|
+
return res.json({ valid: true }); // No validator for this path
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Validate the value
|
|
285
|
+
const result = fieldValidator.safeParse(value);
|
|
286
|
+
|
|
287
|
+
if (!result.success) {
|
|
288
|
+
return res.status(422).json({
|
|
289
|
+
valid: false,
|
|
290
|
+
errors: result.error.issues.map(issue => ({
|
|
291
|
+
path: issue.path, // Just the relative path from Zod
|
|
292
|
+
message: issue.message,
|
|
293
|
+
code: issue.code
|
|
294
|
+
}))
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
res.json({ valid: true, errors: [] });
|
|
299
|
+
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.error('[Container] /validate-path Error:', error);
|
|
302
|
+
res.status(500).json({
|
|
303
|
+
valid: false,
|
|
304
|
+
errors: [{ path: [], message: error.message }]
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
app.post('/get-api-routes', async (req, res) => {
|
|
309
|
+
try {
|
|
310
|
+
// Destructure params from the body, which will be sent by the DO
|
|
311
|
+
const { tenantId, schemaKey, bundledCode, params } = req.body;
|
|
312
|
+
|
|
313
|
+
if (!tenantId || !schemaKey || !bundledCode) {
|
|
314
|
+
return res.status(400).json({ error: 'Missing tenantId, schemaKey, or bundledCode' });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const justSchemaKey = schemaKey.split('[')[0];
|
|
318
|
+
const tempFile = `/tmp/schema-${tenantId}-${Date.now()}.mjs`;
|
|
319
|
+
fs.writeFileSync(tempFile, bundledCode);
|
|
320
|
+
|
|
321
|
+
const schemaModule = await importSchemaModule(tempFile);
|
|
322
|
+
fs.unlinkSync(tempFile);
|
|
323
|
+
|
|
324
|
+
const schemaDefinition = schemaModule.syncSchema?.schemas?.[justSchemaKey];
|
|
325
|
+
|
|
326
|
+
if (!schemaDefinition) {
|
|
327
|
+
return res.status(404).json({ error: `Schema '${justSchemaKey}' not found in bundle.` });
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const apiConfig = schemaDefinition.api || {};
|
|
331
|
+
|
|
332
|
+
// Process both queryData and mutateData with the same logic
|
|
333
|
+
const processApiConfig = (config) => {
|
|
334
|
+
if (!config) return null;
|
|
335
|
+
|
|
336
|
+
// Check if it's an apiWithParams object
|
|
337
|
+
if (config && typeof config.handler === 'function') {
|
|
338
|
+
if (!params || typeof params !== 'object') {
|
|
339
|
+
throw new Error('Missing or invalid params object for parameterized route.');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Validate the params object directly with Zod
|
|
343
|
+
const validationResult = schemaDefinition.validateApiParams(config, params);
|
|
344
|
+
|
|
345
|
+
if (!validationResult.success) {
|
|
346
|
+
throw new Error(`API parameter validation failed: ${JSON.stringify(validationResult.error.flatten())}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Execute handler with the validated params object
|
|
350
|
+
const result = config.handler(validationResult.data);
|
|
351
|
+
|
|
352
|
+
// Determine if it's GET or POST based on return type
|
|
353
|
+
if (typeof result === 'string') {
|
|
354
|
+
// Just a URL = GET request
|
|
355
|
+
return { url: result, method: 'GET' };
|
|
356
|
+
} else if (result && typeof result === 'object' && result.url) {
|
|
357
|
+
// Object with url and optional data = POST if data exists, otherwise GET
|
|
358
|
+
return {
|
|
359
|
+
url: result.url,
|
|
360
|
+
method: result.data ? 'POST' : 'GET',
|
|
361
|
+
data: result.data
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
throw new Error('Invalid handler return value');
|
|
366
|
+
} else if (typeof config === 'string') {
|
|
367
|
+
// Static string = GET request
|
|
368
|
+
return { url: config, method: 'GET' };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return null;
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const queryConfig = processApiConfig(apiConfig.queryData);
|
|
376
|
+
const mutateConfig = processApiConfig(apiConfig.mutateData);
|
|
377
|
+
|
|
378
|
+
res.json({
|
|
379
|
+
queryData: queryConfig,
|
|
380
|
+
mutateData: mutateConfig
|
|
381
|
+
});
|
|
382
|
+
} catch (error) {
|
|
383
|
+
return res.status(400).json({ error: error.message });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
} catch (error) {
|
|
387
|
+
console.error('[Container] /get-api-routes Error:', error);
|
|
388
|
+
res.status(500).json({ error: error.message });
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
app.post('/process-notifications-batch', async (req, res) => {
|
|
394
|
+
try {
|
|
395
|
+
// 1. Accept the new batch payload structure
|
|
396
|
+
const { bundledCode, fullState, contexts, operation } = req.body;
|
|
397
|
+
const { tenantId } = req.query;
|
|
398
|
+
|
|
399
|
+
// Validate the incoming batch payload
|
|
400
|
+
if (!tenantId || !bundledCode || !fullState || !Array.isArray(contexts)) {
|
|
401
|
+
return res.status(400).json({ error: 'Missing tenantId, bundledCode, fullState, or contexts array' });
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// 2. Use the stateless pattern (UNCHANGED): write, import, and delete the temp file.
|
|
405
|
+
const tempFile = `/tmp/schema-${tenantId}-${Date.now()}.mjs`;
|
|
406
|
+
fs.writeFileSync(tempFile, bundledCode);
|
|
407
|
+
const schemaModule = await importSchemaModule(tempFile);
|
|
408
|
+
fs.unlinkSync(tempFile);
|
|
409
|
+
|
|
410
|
+
// 3. Get the schema definition from the imported module (UNCHANGED).
|
|
411
|
+
const schema = schemaModule.syncSchema;
|
|
412
|
+
if (!schema) {
|
|
413
|
+
return res.status(404).json({ error: 'syncSchema not found in the provided bundle.' });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 4. The notification functions are attached to the top-level schema object (UNCHANGED).
|
|
417
|
+
const notificationFunctions = schema.notifications;
|
|
418
|
+
if (!notificationFunctions) {
|
|
419
|
+
// No notifications defined, return an empty object.
|
|
420
|
+
return res.json({});
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// This will become the final response object: { userId: [notifications] }
|
|
424
|
+
const notificationsByUserId = {};
|
|
425
|
+
|
|
426
|
+
// 5. BATCH PROCESSING LOOP: Iterate over the array of contexts.
|
|
427
|
+
for (const context of contexts) {
|
|
428
|
+
// Ensure context is valid before processing
|
|
429
|
+
if (!context || !context.userId) continue;
|
|
430
|
+
|
|
431
|
+
const matchedNotificationsForUser = [];
|
|
432
|
+
|
|
433
|
+
// 6. Execute all notification functions for this specific context.
|
|
434
|
+
for (const channelName in notificationFunctions) {
|
|
435
|
+
try {
|
|
436
|
+
const notificationFn = notificationFunctions[channelName];
|
|
437
|
+
|
|
438
|
+
// Execute the function with the full state and the current user's context
|
|
439
|
+
const result = notificationFn( context,fullState);
|
|
440
|
+
|
|
441
|
+
if (result !== null && result !== undefined) {
|
|
442
|
+
matchedNotificationsForUser.push({
|
|
443
|
+
channel: channelName,
|
|
444
|
+
payload: { ...result },
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
} catch (fnError) {
|
|
448
|
+
console.error(`[Container] Error executing notification function '${channelName}' for user '${context.userId}':`, fnError);
|
|
449
|
+
// Continue to the next notification function, do not stop the batch
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 7. If any notifications were generated for this user, add them to the results map.
|
|
454
|
+
if (matchedNotificationsForUser.length > 0) {
|
|
455
|
+
notificationsByUserId[context.userId] = matchedNotificationsForUser;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// 8. Send the single, consolidated response back to the Durable Object.
|
|
460
|
+
res.json(notificationsByUserId);
|
|
461
|
+
|
|
462
|
+
} catch (error) {
|
|
463
|
+
console.error('[Container] /process-notifications-batch Error:', error);
|
|
464
|
+
res.status(500).json({ error: 'Internal server error processing notification batch', details: error.message });
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
// Error handling middleware
|
|
469
|
+
app.use((error, req, res, next) => {
|
|
470
|
+
console.error('Unhandled error:', error);
|
|
471
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
// Graceful shutdown
|
|
475
|
+
process.on('SIGTERM', () => {
|
|
476
|
+
console.log('Received SIGTERM, shutting down gracefully');
|
|
477
|
+
process.exit(0);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
process.on('SIGINT', () => {
|
|
481
|
+
console.log('Received SIGINT, shutting down gracefully');
|
|
482
|
+
process.exit(0);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const PORT = process.env.PORT || 8080;
|
|
486
|
+
app.listen(PORT, '0.0.0.0', () => {
|
|
487
|
+
console.log(`Schema processor listening on port ${PORT}`);
|
|
488
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { DurableObject } from 'cloudflare:workers';
|
|
2
|
+
|
|
3
|
+
export interface UserPresence {
|
|
4
|
+
forwardNotification(message: object): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class UserPresenceDO extends DurableObject implements UserPresence {
|
|
8
|
+
async fetch(request: Request) {
|
|
9
|
+
if (request.headers.get('Upgrade') !== 'websocket') {
|
|
10
|
+
return new Response('Expected WebSocket', { status: 400 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const pair = new WebSocketPair();
|
|
14
|
+
const [client, server] = Object.values(pair);
|
|
15
|
+
|
|
16
|
+
this.ctx.acceptWebSocket(server);
|
|
17
|
+
|
|
18
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// This method will be called by other Durable Objects (the WebSocketSyncEngine)
|
|
22
|
+
async forwardNotification(message: object): Promise<void> {
|
|
23
|
+
console.log(`[UserPresenceDO] Forwarding notification:`, message);
|
|
24
|
+
const messageString = JSON.stringify(message);
|
|
25
|
+
|
|
26
|
+
// Broadcast to all sessions for this user (e.g., laptop and phone)
|
|
27
|
+
for (const ws of this.ctx.getWebSockets()) {
|
|
28
|
+
try {
|
|
29
|
+
ws.send(messageString);
|
|
30
|
+
} catch (e) {
|
|
31
|
+
// The socket might be closing. This is normal.
|
|
32
|
+
console.warn('[UserPresenceDO] Failed to send to a closing socket.');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async webSocketClose(ws: WebSocket, code: number, reason: string, wasClean: boolean): Promise<void> {
|
|
38
|
+
console.log(`[UserPresenceDO] Socket closed. Code: ${code}, Reason: ${reason}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { sign, verify } from '@tsndr/cloudflare-worker-jwt';
|
|
2
|
+
|
|
3
|
+
export class AuthError extends Error {
|
|
4
|
+
constructor(
|
|
5
|
+
message: string,
|
|
6
|
+
public status: number,
|
|
7
|
+
) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'AuthError';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type AuthResult = {
|
|
14
|
+
success: boolean;
|
|
15
|
+
valid: boolean;
|
|
16
|
+
tenantId: number;
|
|
17
|
+
serviceId: number;
|
|
18
|
+
scopes: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class AuthService {
|
|
22
|
+
constructor(
|
|
23
|
+
private jwtSecret: string,
|
|
24
|
+
private authSecret: string,
|
|
25
|
+
) {}
|
|
26
|
+
|
|
27
|
+
async validateApiKey(apiKey: string): Promise<AuthResult> {
|
|
28
|
+
if (apiKey !== this.authSecret) {
|
|
29
|
+
return { success: false, valid: false, tenantId: 0, serviceId: 0, scopes: [] };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
success: true,
|
|
34
|
+
valid: true,
|
|
35
|
+
tenantId: 1,
|
|
36
|
+
serviceId: 1,
|
|
37
|
+
scopes: ['*'],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
extractBearerToken(request: Request): string {
|
|
42
|
+
const authHeader = request.headers.get('Authorization');
|
|
43
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
44
|
+
throw new AuthError('Missing or invalid authorization header', 401);
|
|
45
|
+
}
|
|
46
|
+
return authHeader.substring(7);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async verifyToken(token: string): Promise<any> {
|
|
50
|
+
const isValid = await verify(token, this.jwtSecret);
|
|
51
|
+
if (!isValid) throw new AuthError('Invalid token', 401);
|
|
52
|
+
return JSON.parse(atob(token.split('.')[1]));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async signToken(payload: Record<string, any>): Promise<string> {
|
|
56
|
+
return sign(payload, this.jwtSecret);
|
|
57
|
+
}
|
|
58
|
+
}
|