arcway 0.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/LICENSE +21 -0
- package/README.md +711 -0
- package/client/env.js +55 -0
- package/client/fetcher.js +50 -0
- package/client/graphql.js +35 -0
- package/client/head.js +140 -0
- package/client/hooks/use-api.js +80 -0
- package/client/hooks/use-debounce.js +12 -0
- package/client/hooks/use-form.js +86 -0
- package/client/hooks/use-graphql.js +30 -0
- package/client/hooks/use-interval.js +12 -0
- package/client/hooks/use-mutation.js +27 -0
- package/client/hooks/use-query.js +45 -0
- package/client/hooks/web/use-click-outside.js +22 -0
- package/client/hooks/web/use-local-storage.js +42 -0
- package/client/index.js +62 -0
- package/client/page-loader.js +155 -0
- package/client/provider.js +53 -0
- package/client/query.js +13 -0
- package/client/router.jsx +303 -0
- package/client/ui/accordion.jsx +65 -0
- package/client/ui/accordion.stories.jsx +48 -0
- package/client/ui/alert-dialog.jsx +122 -0
- package/client/ui/alert-dialog.stories.jsx +44 -0
- package/client/ui/alert.jsx +52 -0
- package/client/ui/alert.stories.jsx +31 -0
- package/client/ui/app-shell.jsx +39 -0
- package/client/ui/app-shell.stories.jsx +51 -0
- package/client/ui/aspect-ratio.jsx +6 -0
- package/client/ui/aspect-ratio.stories.jsx +69 -0
- package/client/ui/avatar.jsx +78 -0
- package/client/ui/avatar.stories.jsx +62 -0
- package/client/ui/badge.jsx +34 -0
- package/client/ui/badge.stories.js +32 -0
- package/client/ui/breadcrumb.jsx +86 -0
- package/client/ui/breadcrumb.stories.jsx +43 -0
- package/client/ui/button-group.jsx +58 -0
- package/client/ui/button-group.stories.jsx +67 -0
- package/client/ui/button.jsx +46 -0
- package/client/ui/button.stories.js +72 -0
- package/client/ui/calendar.jsx +172 -0
- package/client/ui/card.jsx +57 -0
- package/client/ui/card.stories.jsx +33 -0
- package/client/ui/carousel.jsx +167 -0
- package/client/ui/chart.jsx +244 -0
- package/client/ui/checkbox.jsx +24 -0
- package/client/ui/checkbox.stories.js +33 -0
- package/client/ui/collapsible.jsx +12 -0
- package/client/ui/collapsible.stories.jsx +42 -0
- package/client/ui/combobox.jsx +223 -0
- package/client/ui/command.jsx +128 -0
- package/client/ui/context-menu.jsx +170 -0
- package/client/ui/context-menu.stories.jsx +35 -0
- package/client/ui/dialog.jsx +109 -0
- package/client/ui/dialog.stories.jsx +37 -0
- package/client/ui/direction.jsx +9 -0
- package/client/ui/drawer.jsx +87 -0
- package/client/ui/dropdown-menu.jsx +172 -0
- package/client/ui/dropdown-menu.stories.jsx +34 -0
- package/client/ui/empty.jsx +76 -0
- package/client/ui/empty.stories.jsx +64 -0
- package/client/ui/field.jsx +174 -0
- package/client/ui/field.stories.jsx +118 -0
- package/client/ui/form.jsx +17 -0
- package/client/ui/hooks/use-mobile.js +16 -0
- package/client/ui/hover-card.jsx +26 -0
- package/client/ui/hover-card.stories.jsx +28 -0
- package/client/ui/index.js +649 -0
- package/client/ui/input-group.jsx +116 -0
- package/client/ui/input-group.stories.jsx +65 -0
- package/client/ui/input-otp.jsx +62 -0
- package/client/ui/input.jsx +16 -0
- package/client/ui/input.stories.js +27 -0
- package/client/ui/item.jsx +155 -0
- package/client/ui/item.stories.jsx +118 -0
- package/client/ui/kbd.jsx +24 -0
- package/client/ui/kbd.stories.jsx +32 -0
- package/client/ui/label.jsx +16 -0
- package/client/ui/label.stories.js +25 -0
- package/client/ui/lib/utils.js +6 -0
- package/client/ui/main-content.jsx +30 -0
- package/client/ui/menubar.jsx +189 -0
- package/client/ui/menubar.stories.jsx +43 -0
- package/client/ui/native-select.jsx +34 -0
- package/client/ui/native-select.stories.jsx +67 -0
- package/client/ui/navigation-menu.jsx +120 -0
- package/client/ui/navigation-menu.stories.jsx +45 -0
- package/client/ui/pagination.jsx +92 -0
- package/client/ui/pagination.stories.jsx +52 -0
- package/client/ui/panel.jsx +66 -0
- package/client/ui/popover.jsx +54 -0
- package/client/ui/popover.stories.jsx +27 -0
- package/client/ui/progress.jsx +19 -0
- package/client/ui/progress.stories.js +34 -0
- package/client/ui/radio-group.jsx +33 -0
- package/client/ui/radio-group.stories.jsx +49 -0
- package/client/ui/resizable.jsx +33 -0
- package/client/ui/scroll-area.jsx +41 -0
- package/client/ui/scroll-area.stories.jsx +43 -0
- package/client/ui/select.jsx +145 -0
- package/client/ui/select.stories.jsx +80 -0
- package/client/ui/separator.jsx +18 -0
- package/client/ui/separator.stories.jsx +37 -0
- package/client/ui/sheet.jsx +95 -0
- package/client/ui/sheet.stories.jsx +56 -0
- package/client/ui/sidebar.jsx +544 -0
- package/client/ui/skeleton.jsx +8 -0
- package/client/ui/skeleton.stories.js +23 -0
- package/client/ui/slider.jsx +41 -0
- package/client/ui/slider.stories.js +31 -0
- package/client/ui/sonner.jsx +37 -0
- package/client/ui/spinner.jsx +14 -0
- package/client/ui/spinner.stories.js +16 -0
- package/client/ui/style-mira.css +1316 -0
- package/client/ui/switch.jsx +22 -0
- package/client/ui/switch.stories.js +44 -0
- package/client/ui/table.jsx +33 -0
- package/client/ui/table.stories.jsx +42 -0
- package/client/ui/tabs.jsx +63 -0
- package/client/ui/tabs.stories.jsx +45 -0
- package/client/ui/textarea.jsx +15 -0
- package/client/ui/textarea.stories.js +33 -0
- package/client/ui/theme.css +459 -0
- package/client/ui/toggle-group.jsx +62 -0
- package/client/ui/toggle-group.stories.jsx +68 -0
- package/client/ui/toggle.jsx +34 -0
- package/client/ui/toggle.stories.js +46 -0
- package/client/ui/tooltip.jsx +37 -0
- package/client/ui/tooltip.stories.jsx +32 -0
- package/client/ui/use-transition.js +35 -0
- package/client/ws.js +132 -0
- package/package.json +134 -0
- package/server/bin/cli.js +42 -0
- package/server/bin/commands/build.js +23 -0
- package/server/bin/commands/dev.js +57 -0
- package/server/bin/commands/docs.js +30 -0
- package/server/bin/commands/graphql-schema.js +32 -0
- package/server/bin/commands/lint.js +35 -0
- package/server/bin/commands/mcp.js +26 -0
- package/server/bin/commands/migrate.js +82 -0
- package/server/bin/commands/schema.js +41 -0
- package/server/bin/commands/seed.js +36 -0
- package/server/bin/commands/start.js +31 -0
- package/server/bin/commands/test.js +20 -0
- package/server/bin/solo.js +4 -0
- package/server/boot/index.js +150 -0
- package/server/boot.js +2 -0
- package/server/build.js +23 -0
- package/server/cache/drivers/memory.js +23 -0
- package/server/cache/drivers/redis.js +28 -0
- package/server/cache/index.js +69 -0
- package/server/config/loader.js +89 -0
- package/server/config/modules/api.js +17 -0
- package/server/config/modules/build.js +9 -0
- package/server/config/modules/cache.js +10 -0
- package/server/config/modules/database.js +29 -0
- package/server/config/modules/events.js +15 -0
- package/server/config/modules/files.js +15 -0
- package/server/config/modules/jobs.js +20 -0
- package/server/config/modules/logger.js +9 -0
- package/server/config/modules/mail.js +11 -0
- package/server/config/modules/mcp.js +9 -0
- package/server/config/modules/pages.js +20 -0
- package/server/config/modules/queue.js +10 -0
- package/server/config/modules/redis.js +9 -0
- package/server/config/modules/server.js +30 -0
- package/server/config/modules/session.js +9 -0
- package/server/config/modules/websocket.js +11 -0
- package/server/constants.js +67 -0
- package/server/context.js +15 -0
- package/server/db/index.js +87 -0
- package/server/db/schema/drivers/mysql.js +28 -0
- package/server/db/schema/drivers/pg.js +34 -0
- package/server/db/schema/drivers/sqlite.js +22 -0
- package/server/db/schema/index.js +78 -0
- package/server/db/seeds.js +22 -0
- package/server/discovery.js +67 -0
- package/server/docs/openapi.js +153 -0
- package/server/env.js +17 -0
- package/server/events/drivers/memory.js +45 -0
- package/server/events/drivers/redis.js +64 -0
- package/server/events/handler.js +67 -0
- package/server/events/index.js +35 -0
- package/server/events/pattern.js +5 -0
- package/server/files/drivers/local.js +83 -0
- package/server/files/drivers/s3.js +113 -0
- package/server/files/index.js +57 -0
- package/server/filewatcher/index.js +156 -0
- package/server/glob.js +6 -0
- package/server/graphql/discovery.js +70 -0
- package/server/graphql/handler.js +41 -0
- package/server/graphql/index.js +13 -0
- package/server/graphql/loaders.js +19 -0
- package/server/graphql/merge.js +48 -0
- package/server/graphql/subscriptions.js +43 -0
- package/server/health.js +34 -0
- package/server/helpers.js +9 -0
- package/server/index.js +55 -0
- package/server/internals.js +139 -0
- package/server/jobs/cron.js +10 -0
- package/server/jobs/drivers/knex-queue.js +207 -0
- package/server/jobs/drivers/lease.js +148 -0
- package/server/jobs/drivers/memory-queue.js +134 -0
- package/server/jobs/queue.js +27 -0
- package/server/jobs/runner.js +197 -0
- package/server/jobs/throughput.js +63 -0
- package/server/lib/vault/encrypt.js +40 -0
- package/server/lib/vault/ids.js +9 -0
- package/server/lib/vault/index.js +14 -0
- package/server/lib/vault/jwt.js +55 -0
- package/server/lib/vault/password.js +10 -0
- package/server/lint/boundaries.js +77 -0
- package/server/logger/index.js +130 -0
- package/server/mail/drivers/console.js +31 -0
- package/server/mail/drivers/smtp.js +34 -0
- package/server/mail/imap.js +105 -0
- package/server/mail/inbound-store.js +58 -0
- package/server/mail/inbound.js +79 -0
- package/server/mail/index.js +112 -0
- package/server/mcp/debug-api.js +137 -0
- package/server/mcp/helpers.js +30 -0
- package/server/mcp/index.js +77 -0
- package/server/mcp/runtime.js +7 -0
- package/server/mcp/server.js +19 -0
- package/server/mcp/tools/debugging.js +133 -0
- package/server/mcp/tools/introspection.js +87 -0
- package/server/middlewares/cors.js +30 -0
- package/server/middlewares/index.js +3 -0
- package/server/middlewares/require-session.js +15 -0
- package/server/module-loader.js +9 -0
- package/server/pages/build-client.js +187 -0
- package/server/pages/build-css.js +47 -0
- package/server/pages/build-manifest.js +55 -0
- package/server/pages/build-plugins.js +75 -0
- package/server/pages/build-server.js +115 -0
- package/server/pages/build.js +116 -0
- package/server/pages/discovery.js +120 -0
- package/server/pages/fonts.js +128 -0
- package/server/pages/handler.js +276 -0
- package/server/pages/hmr.js +176 -0
- package/server/pages/pages-router.js +78 -0
- package/server/pages/ssr.js +276 -0
- package/server/pages/static.js +92 -0
- package/server/pages/watcher.js +90 -0
- package/server/queue/drivers/knex.js +67 -0
- package/server/queue/drivers/redis.js +91 -0
- package/server/queue/index.js +61 -0
- package/server/rate-limit/consume.js +21 -0
- package/server/rate-limit/drivers/memory.js +24 -0
- package/server/rate-limit/drivers/redis.js +32 -0
- package/server/rate-limit/index.js +33 -0
- package/server/redis/index.js +67 -0
- package/server/ring-buffer.js +44 -0
- package/server/route.js +4 -0
- package/server/router/api-router.js +317 -0
- package/server/router/cors.js +31 -0
- package/server/router/middleware.js +91 -0
- package/server/router/routes.js +132 -0
- package/server/server.js +35 -0
- package/server/session/helpers.js +21 -0
- package/server/session/index.js +89 -0
- package/server/static/index.js +36 -0
- package/server/system-jobs/index.js +50 -0
- package/server/system-routes/index.js +84 -0
- package/server/testing/index.js +263 -0
- package/server/validation.js +41 -0
- package/server/watcher.js +34 -0
- package/server/web-server.js +231 -0
- package/server/ws/discovery.js +54 -0
- package/server/ws/index.js +14 -0
- package/server/ws/realtime.js +318 -0
- package/server/ws/registry.js +17 -0
- package/server/ws/server.js +152 -0
- package/server/ws/ws-router.js +335 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { discoverModules } from '../discovery.js';
|
|
3
|
+
import { validateRequestSchema } from '../validation.js';
|
|
4
|
+
function isMiddlewareConfig(value) {
|
|
5
|
+
return (
|
|
6
|
+
typeof value === 'object' &&
|
|
7
|
+
value !== null &&
|
|
8
|
+
'handler' in value &&
|
|
9
|
+
typeof value.handler === 'function'
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
function wrapMiddlewareConfig(config) {
|
|
13
|
+
const { schema, handler } = config;
|
|
14
|
+
if (!schema) return handler;
|
|
15
|
+
return async (ctx) => {
|
|
16
|
+
const validated = validateRequestSchema(schema, ctx.req.query, ctx.req.body);
|
|
17
|
+
if (validated.error) {
|
|
18
|
+
return { status: 400, error: validated.error };
|
|
19
|
+
}
|
|
20
|
+
ctx.req.query = validated.query;
|
|
21
|
+
ctx.req.body = validated.body;
|
|
22
|
+
return handler(ctx);
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async function discoverMiddleware(apiDir) {
|
|
26
|
+
const middleware = [];
|
|
27
|
+
const entries = await discoverModules(apiDir, {
|
|
28
|
+
recursive: true,
|
|
29
|
+
skipUnderscore: false,
|
|
30
|
+
filter: (name) => name === '_middleware.js',
|
|
31
|
+
label: 'middleware',
|
|
32
|
+
});
|
|
33
|
+
for (const { filePath, relativePath, module } of entries) {
|
|
34
|
+
const parts = relativePath.split(path.sep);
|
|
35
|
+
const dirParts = parts.slice(0, -1);
|
|
36
|
+
let pathPrefix = '/';
|
|
37
|
+
if (dirParts.length > 0) {
|
|
38
|
+
const converted = dirParts.map((p) => p.replace(/\[([^\]]+)\]/g, ':$1'));
|
|
39
|
+
pathPrefix = '/' + converted.join('/');
|
|
40
|
+
}
|
|
41
|
+
{
|
|
42
|
+
const exported = module.default;
|
|
43
|
+
if (!exported) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Middleware file "${filePath}" must have a default export (MiddlewareConfig or array of MiddlewareConfig)`,
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const items = Array.isArray(exported) ? exported : [exported];
|
|
49
|
+
const fns = [];
|
|
50
|
+
for (let i = 0; i < items.length; i++) {
|
|
51
|
+
const item = items[i];
|
|
52
|
+
if (isMiddlewareConfig(item)) {
|
|
53
|
+
fns.push(wrapMiddlewareConfig(item));
|
|
54
|
+
} else {
|
|
55
|
+
throw new Error(
|
|
56
|
+
`Middleware file "${filePath}" default export${items.length > 1 ? `[${i}]` : ''} must be a MiddlewareConfig object with a handler function. Got ${typeof item}.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
middleware.push({ pathPrefix, fns });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
middleware.sort((a, b) => a.pathPrefix.length - b.pathPrefix.length);
|
|
64
|
+
return middleware;
|
|
65
|
+
}
|
|
66
|
+
function getMiddlewareForRoute(allMiddleware, routePattern) {
|
|
67
|
+
const matching = [];
|
|
68
|
+
for (const mw of allMiddleware) {
|
|
69
|
+
if (mw.pathPrefix === '/') {
|
|
70
|
+
matching.push(...mw.fns);
|
|
71
|
+
} else if (routePattern === mw.pathPrefix || routePattern.startsWith(mw.pathPrefix + '/')) {
|
|
72
|
+
matching.push(...mw.fns);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return matching;
|
|
76
|
+
}
|
|
77
|
+
function buildMiddlewareChain(middlewareFns, handler) {
|
|
78
|
+
if (middlewareFns.length === 0) {
|
|
79
|
+
return async (ctx) => handler(ctx);
|
|
80
|
+
}
|
|
81
|
+
return async (ctx) => {
|
|
82
|
+
for (const mw of middlewareFns) {
|
|
83
|
+
const result = await mw(ctx);
|
|
84
|
+
if (result !== void 0 && result !== null) {
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return handler(ctx);
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export { buildMiddlewareChain, discoverMiddleware, getMiddlewareForRoute };
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { discoverModules } from '../discovery.js';
|
|
2
|
+
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
|
|
3
|
+
function filePathToPattern(relativePath) {
|
|
4
|
+
let route = relativePath.replace(/\\/g, '/');
|
|
5
|
+
route = route.replace(/\.jsx?$/, '');
|
|
6
|
+
route = route.replace(/\[\[\.\.\.([^\]]+)\]\]/g, '*$1?');
|
|
7
|
+
route = route.replace(/\[\.\.\.([^\]]+)\]/g, '*$1');
|
|
8
|
+
route = route.replace(/\[([^\]]+)\]/g, ':$1');
|
|
9
|
+
route = route.replace(/\/index$/, '');
|
|
10
|
+
if (route === 'index') {
|
|
11
|
+
route = '';
|
|
12
|
+
}
|
|
13
|
+
return '/' + route;
|
|
14
|
+
}
|
|
15
|
+
function sortBySpecificity(items) {
|
|
16
|
+
items.sort((a, b) => {
|
|
17
|
+
const aCatchAll = a.catchAllParam ? 1 : 0;
|
|
18
|
+
const bCatchAll = b.catchAllParam ? 1 : 0;
|
|
19
|
+
if (aCatchAll !== bCatchAll) return aCatchAll - bCatchAll;
|
|
20
|
+
const aParams = a.paramNames.length;
|
|
21
|
+
const bParams = b.paramNames.length;
|
|
22
|
+
if (aParams !== bParams) return aParams - bParams;
|
|
23
|
+
return a.pattern.localeCompare(b.pattern);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function matchPattern(items, pathname) {
|
|
27
|
+
for (const item of items) {
|
|
28
|
+
const m = item.regex.exec(pathname);
|
|
29
|
+
if (!m) continue;
|
|
30
|
+
const params = {};
|
|
31
|
+
try {
|
|
32
|
+
for (let i = 0; i < item.paramNames.length; i++) {
|
|
33
|
+
const name = item.paramNames[i];
|
|
34
|
+
const raw = m[i + 1];
|
|
35
|
+
if (name === item.catchAllParam) {
|
|
36
|
+
params[name] = raw ? raw.split('/').map(decodeURIComponent) : [];
|
|
37
|
+
} else {
|
|
38
|
+
params[name] = decodeURIComponent(raw);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
return { match: item, params };
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
function compilePattern(pattern) {
|
|
49
|
+
const paramNames = [];
|
|
50
|
+
let catchAllParam;
|
|
51
|
+
let isOptionalCatchAll = false;
|
|
52
|
+
const regexStr = pattern.replace(
|
|
53
|
+
/\/:([^/]+)|\/\*(\w+)\?|\/\*(\w+)/g,
|
|
54
|
+
(match, singleParam, optCatchAll, reqCatchAll) => {
|
|
55
|
+
if (singleParam) {
|
|
56
|
+
paramNames.push(singleParam);
|
|
57
|
+
return '/([^/]+)';
|
|
58
|
+
}
|
|
59
|
+
if (optCatchAll) {
|
|
60
|
+
paramNames.push(optCatchAll);
|
|
61
|
+
catchAllParam = optCatchAll;
|
|
62
|
+
isOptionalCatchAll = true;
|
|
63
|
+
return '(?:/(.+))?';
|
|
64
|
+
}
|
|
65
|
+
if (reqCatchAll) {
|
|
66
|
+
paramNames.push(reqCatchAll);
|
|
67
|
+
catchAllParam = reqCatchAll;
|
|
68
|
+
return '/(.+)';
|
|
69
|
+
}
|
|
70
|
+
return match;
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
const regex = new RegExp(`^${regexStr}/?$`);
|
|
74
|
+
return { regex, paramNames, catchAllParam, isOptionalCatchAll };
|
|
75
|
+
}
|
|
76
|
+
async function discoverRoutes(apiDir) {
|
|
77
|
+
const entries = await discoverModules(apiDir, { recursive: true, label: 'route file' });
|
|
78
|
+
const routes = [];
|
|
79
|
+
for (const { filePath, relativePath, module } of entries) {
|
|
80
|
+
const urlPattern = filePathToPattern(relativePath);
|
|
81
|
+
const { regex, paramNames, catchAllParam } = compilePattern(urlPattern);
|
|
82
|
+
for (const method of HTTP_METHODS) {
|
|
83
|
+
const config = module[method];
|
|
84
|
+
if (!config) continue;
|
|
85
|
+
if (typeof config.handler !== 'function') {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Route ${method} ${urlPattern} (${filePath}) must export a handler function`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
routes.push({
|
|
91
|
+
method,
|
|
92
|
+
pattern: urlPattern,
|
|
93
|
+
regex,
|
|
94
|
+
paramNames,
|
|
95
|
+
...(catchAllParam ? { catchAllParam } : {}),
|
|
96
|
+
config,
|
|
97
|
+
...(method === 'GET' && typeof config.ws === 'function' ? { wsEnabled: true } : {}),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
sortBySpecificity(routes);
|
|
102
|
+
return routes;
|
|
103
|
+
}
|
|
104
|
+
function matchRoute(routes, method, pathname) {
|
|
105
|
+
const upperMethod = method.toUpperCase();
|
|
106
|
+
const methodRoutes = routes.filter((r) => r.method === upperMethod);
|
|
107
|
+
const result = matchPattern(methodRoutes, pathname);
|
|
108
|
+
if (!result) return null;
|
|
109
|
+
return { route: result.match, params: result.params };
|
|
110
|
+
}
|
|
111
|
+
function matchRoutesByPath(routes, pathname) {
|
|
112
|
+
const matched = new Map();
|
|
113
|
+
let params = null;
|
|
114
|
+
for (const method of HTTP_METHODS) {
|
|
115
|
+
const result = matchRoute(routes, method, pathname);
|
|
116
|
+
if (result) {
|
|
117
|
+
matched.set(method, result.route);
|
|
118
|
+
if (!params) params = result.params;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (matched.size === 0 || !params) return null;
|
|
122
|
+
return { routes: matched, params };
|
|
123
|
+
}
|
|
124
|
+
export {
|
|
125
|
+
compilePattern,
|
|
126
|
+
discoverRoutes,
|
|
127
|
+
filePathToPattern,
|
|
128
|
+
matchPattern,
|
|
129
|
+
matchRoute,
|
|
130
|
+
matchRoutesByPath,
|
|
131
|
+
sortBySpecificity,
|
|
132
|
+
};
|
package/server/server.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createServer } from 'node:http';
|
|
2
|
+
function createHttpServer(handler) {
|
|
3
|
+
const server = createServer(handler);
|
|
4
|
+
const connections = new Set();
|
|
5
|
+
server.on('connection', (socket) => {
|
|
6
|
+
connections.add(socket);
|
|
7
|
+
socket.once('close', () => connections.delete(socket));
|
|
8
|
+
});
|
|
9
|
+
server.__trackedConnections = connections;
|
|
10
|
+
return server;
|
|
11
|
+
}
|
|
12
|
+
function listen(server, port, host = '0.0.0.0') {
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
server.once('error', reject);
|
|
15
|
+
server.listen(port, host, () => {
|
|
16
|
+
server.removeListener('error', reject);
|
|
17
|
+
resolve();
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function closeServer(server) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
server.close((err) => {
|
|
24
|
+
if (err) reject(err);
|
|
25
|
+
else resolve();
|
|
26
|
+
});
|
|
27
|
+
const connections = server.__trackedConnections;
|
|
28
|
+
if (connections) {
|
|
29
|
+
for (const socket of connections) {
|
|
30
|
+
socket.destroy();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export { closeServer, createHttpServer, listen };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { parse as parseCookieHeader } from 'cookie';
|
|
2
|
+
import { unsealSession } from './index.js';
|
|
3
|
+
function parseCookies(req) {
|
|
4
|
+
return req.headers['cookie'] ? parseCookieHeader(req.headers['cookie']) : {};
|
|
5
|
+
}
|
|
6
|
+
function parseCookiesFromHeader(cookieHeader) {
|
|
7
|
+
return cookieHeader ? parseCookieHeader(cookieHeader) : {};
|
|
8
|
+
}
|
|
9
|
+
async function resolveSession(cookies, sessionConfig) {
|
|
10
|
+
if (!sessionConfig) return {};
|
|
11
|
+
return unsealSession(cookies[sessionConfig.cookieName], sessionConfig);
|
|
12
|
+
}
|
|
13
|
+
function flattenHeaders(raw) {
|
|
14
|
+
const result = {};
|
|
15
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
16
|
+
if (value === void 0) continue;
|
|
17
|
+
result[key] = Array.isArray(value) ? value.join(', ') : value;
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
}
|
|
21
|
+
export { flattenHeaders, parseCookies, parseCookiesFromHeader, resolveSession };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { sealData, unsealData } from 'iron-session';
|
|
2
|
+
import { serialize as serializeCookie } from 'cookie';
|
|
3
|
+
import { SESSION_COOKIE_MAX_AGE_SKEW } from '../constants.js';
|
|
4
|
+
import { toErrorMessage } from '../helpers.js';
|
|
5
|
+
const MIN_PASSWORD_LENGTH = 32;
|
|
6
|
+
function resolveSessionConfig(config, mode) {
|
|
7
|
+
if (!config.password) {
|
|
8
|
+
throw new Error(
|
|
9
|
+
`Session "password" is required but was not provided (got ${config.password === void 0 ? 'undefined' : String(config.password)}). Set a password of at least 32 characters, e.g. via SESSION_SECRET env variable.`,
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
if (typeof config.password === 'string') {
|
|
13
|
+
if (config.password.length < MIN_PASSWORD_LENGTH) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
`Session password must be at least ${MIN_PASSWORD_LENGTH} characters (got ${config.password.length}). Short passwords produce weak encryption.`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
} else if (typeof config.password === 'object') {
|
|
19
|
+
for (const [id, pw] of Object.entries(config.password)) {
|
|
20
|
+
if (typeof pw === 'string' && pw.length < MIN_PASSWORD_LENGTH) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Session password id=${id} must be at least ${MIN_PASSWORD_LENGTH} characters (got ${pw.length}). Short passwords produce weak encryption.`,
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
password: config.password,
|
|
29
|
+
cookieName: config.cookieName ?? 'arcway.session',
|
|
30
|
+
ttl: config.ttl ?? 14 * 24 * 3600,
|
|
31
|
+
cookie: {
|
|
32
|
+
httpOnly: config.cookie?.httpOnly ?? true,
|
|
33
|
+
secure: config.cookie?.secure ?? mode === 'production',
|
|
34
|
+
sameSite: config.cookie?.sameSite ?? 'lax',
|
|
35
|
+
path: config.cookie?.path ?? '/',
|
|
36
|
+
domain: config.cookie?.domain,
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function unsealSession(cookieValue, config) {
|
|
41
|
+
if (!cookieValue) return {};
|
|
42
|
+
try {
|
|
43
|
+
const data = await unsealData(cookieValue, {
|
|
44
|
+
password: config.password,
|
|
45
|
+
ttl: config.ttl,
|
|
46
|
+
});
|
|
47
|
+
return data && typeof data === 'object' ? data : {};
|
|
48
|
+
} catch (err) {
|
|
49
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
50
|
+
const reason = toErrorMessage(err);
|
|
51
|
+
console.warn(`[arcway] Failed to unseal session cookie: ${reason}`);
|
|
52
|
+
}
|
|
53
|
+
return {};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function sealSession(data, config) {
|
|
57
|
+
return sealData(data, {
|
|
58
|
+
password: config.password,
|
|
59
|
+
ttl: config.ttl,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function buildSessionSetCookie(sealedValue, config) {
|
|
63
|
+
const maxAge = (config.ttl === 0 ? 2147483647 : config.ttl) - SESSION_COOKIE_MAX_AGE_SKEW;
|
|
64
|
+
return serializeCookie(config.cookieName, sealedValue, {
|
|
65
|
+
httpOnly: config.cookie.httpOnly,
|
|
66
|
+
secure: config.cookie.secure,
|
|
67
|
+
sameSite: config.cookie.sameSite,
|
|
68
|
+
path: config.cookie.path,
|
|
69
|
+
domain: config.cookie.domain,
|
|
70
|
+
maxAge,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
function buildSessionClearCookie(config) {
|
|
74
|
+
return serializeCookie(config.cookieName, '', {
|
|
75
|
+
httpOnly: config.cookie.httpOnly,
|
|
76
|
+
secure: config.cookie.secure,
|
|
77
|
+
sameSite: config.cookie.sameSite,
|
|
78
|
+
path: config.cookie.path,
|
|
79
|
+
domain: config.cookie.domain,
|
|
80
|
+
maxAge: 0,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
export {
|
|
84
|
+
buildSessionClearCookie,
|
|
85
|
+
buildSessionSetCookie,
|
|
86
|
+
resolveSessionConfig,
|
|
87
|
+
sealSession,
|
|
88
|
+
unsealSession,
|
|
89
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { serveStaticAsset, servePublicFile } from '../pages/static.js';
|
|
4
|
+
|
|
5
|
+
class StaticRouter {
|
|
6
|
+
outDir;
|
|
7
|
+
publicDir;
|
|
8
|
+
hasPublicDir;
|
|
9
|
+
|
|
10
|
+
constructor({ outDir, publicDir }) {
|
|
11
|
+
this.outDir = outDir;
|
|
12
|
+
this.publicDir = publicDir;
|
|
13
|
+
this.hasPublicDir = publicDir ? fs.existsSync(publicDir) : false;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
handle(req, res) {
|
|
17
|
+
const method = req.method ?? 'GET';
|
|
18
|
+
if (method !== 'GET') return false;
|
|
19
|
+
|
|
20
|
+
const pathname = (req.url ?? '/').split('?')[0];
|
|
21
|
+
|
|
22
|
+
// Built bundles: /static/client/*
|
|
23
|
+
if (pathname.startsWith('/static/client/')) {
|
|
24
|
+
return serveStaticAsset(pathname, res, this.outDir);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Public files: /* (fallback)
|
|
28
|
+
if (this.hasPublicDir) {
|
|
29
|
+
return servePublicFile(pathname, res, this.publicDir);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { StaticRouter };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const SYSTEM_JOB_DOMAIN = '__system';
|
|
2
|
+
const systemJobRegistry = [
|
|
3
|
+
// Future system jobs are added here. Examples:
|
|
4
|
+
//
|
|
5
|
+
// {
|
|
6
|
+
// definition: { name: 'process-mail', handler: processMailHandler, schedule: '* * * * *' },
|
|
7
|
+
// shouldRegister: (config) => !!config.mail,
|
|
8
|
+
// description: 'Process inbound email via IMAP polling',
|
|
9
|
+
// },
|
|
10
|
+
];
|
|
11
|
+
function getApplicableSystemJobs(config) {
|
|
12
|
+
return systemJobRegistry.filter((entry) => entry.shouldRegister(config));
|
|
13
|
+
}
|
|
14
|
+
function registerSystemJobs(config, queue, store) {
|
|
15
|
+
const applicable = getApplicableSystemJobs(config);
|
|
16
|
+
const registered = [];
|
|
17
|
+
for (const entry of applicable) {
|
|
18
|
+
queue.register(SYSTEM_JOB_DOMAIN, entry.definition, store);
|
|
19
|
+
registered.push({
|
|
20
|
+
domainName: SYSTEM_JOB_DOMAIN,
|
|
21
|
+
jobName: entry.definition.name,
|
|
22
|
+
schedule: entry.definition.schedule,
|
|
23
|
+
description: entry.description,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return registered;
|
|
27
|
+
}
|
|
28
|
+
function validateNoSystemJobCollision(domainNames) {
|
|
29
|
+
for (const name of domainNames) {
|
|
30
|
+
if (name === SYSTEM_JOB_DOMAIN || name.startsWith(`${SYSTEM_JOB_DOMAIN}/`)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Domain name "${name}" uses the reserved "${SYSTEM_JOB_DOMAIN}" prefix. This prefix is reserved for framework-internal system jobs.`,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function addSystemJobEntry(entry) {
|
|
38
|
+
systemJobRegistry.push(entry);
|
|
39
|
+
}
|
|
40
|
+
function _resetSystemJobRegistry() {
|
|
41
|
+
systemJobRegistry.length = 0;
|
|
42
|
+
}
|
|
43
|
+
export {
|
|
44
|
+
SYSTEM_JOB_DOMAIN,
|
|
45
|
+
_resetSystemJobRegistry,
|
|
46
|
+
addSystemJobEntry,
|
|
47
|
+
getApplicableSystemJobs,
|
|
48
|
+
registerSystemJobs,
|
|
49
|
+
validateNoSystemJobCollision,
|
|
50
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { checkHealth } from '../health.js';
|
|
2
|
+
|
|
3
|
+
const SYSTEM_PREFIX = '/_system/api';
|
|
4
|
+
|
|
5
|
+
class SystemRouter {
|
|
6
|
+
routes = [];
|
|
7
|
+
_healthDeps = null;
|
|
8
|
+
|
|
9
|
+
constructor({ healthDeps } = {}) {
|
|
10
|
+
this._healthDeps = healthDeps ?? null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
register(route) {
|
|
14
|
+
this.routes.push(route);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async handle(req, res) {
|
|
18
|
+
const url = req.url ?? '';
|
|
19
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
20
|
+
|
|
21
|
+
// ── /_system/health ──
|
|
22
|
+
if (method === 'GET' && url.split('?')[0] === '/_system/health') {
|
|
23
|
+
return this._handleHealth(res);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!url.startsWith(SYSTEM_PREFIX)) return false;
|
|
27
|
+
const fullPath = url.split('?')[0];
|
|
28
|
+
const routePath = fullPath.slice(SYSTEM_PREFIX.length) || '/';
|
|
29
|
+
|
|
30
|
+
for (const route of this.routes) {
|
|
31
|
+
if (!routePath.startsWith(route.path)) continue;
|
|
32
|
+
const methods = Array.isArray(route.method) ? route.method : [route.method];
|
|
33
|
+
const methodMatch = methods.some((m) => m === '*' || m.toUpperCase() === method);
|
|
34
|
+
if (!methodMatch) continue;
|
|
35
|
+
const query = new URL(url, 'http://localhost').searchParams;
|
|
36
|
+
const remainingPath = routePath.slice(route.path.length) || '/';
|
|
37
|
+
try {
|
|
38
|
+
await route.handler(req, res, {
|
|
39
|
+
path: remainingPath,
|
|
40
|
+
query,
|
|
41
|
+
method,
|
|
42
|
+
});
|
|
43
|
+
} catch (err) {
|
|
44
|
+
console.error(`[system-route] Error handling ${method} ${fullPath}:`, err);
|
|
45
|
+
if (!res.headersSent) {
|
|
46
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
47
|
+
res.end(
|
|
48
|
+
JSON.stringify({
|
|
49
|
+
error: { code: 'INTERNAL_ERROR', message: 'System route handler error' },
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
57
|
+
res.end(
|
|
58
|
+
JSON.stringify({
|
|
59
|
+
error: { code: 'NOT_FOUND', message: `System route not found: ${routePath}` },
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async _handleHealth(res) {
|
|
66
|
+
if (this._healthDeps) {
|
|
67
|
+
try {
|
|
68
|
+
const result = await checkHealth(this._healthDeps);
|
|
69
|
+
const status = result.status === 'ok' ? 200 : 503;
|
|
70
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
71
|
+
res.end(JSON.stringify(result));
|
|
72
|
+
} catch {
|
|
73
|
+
res.writeHead(503, { 'Content-Type': 'application/json' });
|
|
74
|
+
res.end(JSON.stringify({ status: 'degraded', components: {} }));
|
|
75
|
+
}
|
|
76
|
+
} else {
|
|
77
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
78
|
+
res.end(JSON.stringify({ status: 'ok', timestamp: new Date().toISOString() }));
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export { SystemRouter };
|