@objectstack/plugin-hono-server 4.0.3 → 4.0.4
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +8 -0
- package/README.md +71 -1
- package/dist/index.d.mts +44 -40
- package/dist/index.d.ts +44 -40
- package/dist/index.js +82 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +82 -14
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/adapter.ts +13 -7
- package/src/hono-plugin.test.ts +123 -0
- package/src/hono-plugin.ts +171 -43
- package/src/pattern-matcher.test.ts +180 -0
- package/vitest.config.ts +22 -0
package/src/hono-plugin.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
2
|
|
|
3
|
-
import { Plugin, PluginContext, IHttpServer } from '@objectstack/core';
|
|
3
|
+
import { Plugin, PluginContext, IHttpServer, IDataEngine } from '@objectstack/core';
|
|
4
4
|
import {
|
|
5
5
|
RestServerConfig,
|
|
6
6
|
} from '@objectstack/spec/api';
|
|
7
|
-
import { HonoHttpServer } from './adapter';
|
|
7
|
+
import { HonoHttpServer, HonoCorsOptions } from './adapter';
|
|
8
|
+
import { cors } from 'hono/cors';
|
|
8
9
|
import { serveStatic } from '@hono/node-server/serve-static';
|
|
9
10
|
import * as fs from 'fs';
|
|
10
11
|
import * as path from 'path';
|
|
@@ -45,33 +46,97 @@ export interface HonoPluginOptions {
|
|
|
45
46
|
* @default false
|
|
46
47
|
*/
|
|
47
48
|
spaFallback?: boolean;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* CORS configuration. Set to `false` to disable entirely.
|
|
52
|
+
* Enabled by default with origin '*'.
|
|
53
|
+
* Can also be controlled via environment variables:
|
|
54
|
+
* CORS_ENABLED, CORS_ORIGIN, CORS_CREDENTIALS, CORS_MAX_AGE
|
|
55
|
+
*/
|
|
56
|
+
cors?: HonoCorsOptions | false;
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
/**
|
|
51
60
|
* Hono Server Plugin
|
|
52
|
-
*
|
|
61
|
+
*
|
|
53
62
|
* Provides HTTP server capabilities using Hono framework.
|
|
54
63
|
* Registers the IHttpServer service so other plugins can register routes.
|
|
55
|
-
*
|
|
64
|
+
*
|
|
56
65
|
* Route registration is handled by plugins:
|
|
57
66
|
* - `@objectstack/rest` → CRUD, metadata, discovery, UI, batch
|
|
58
67
|
* - `createDispatcherPlugin()` → auth, graphql, analytics, packages, etc.
|
|
59
68
|
*/
|
|
69
|
+
/**
|
|
70
|
+
* Check if an origin matches a pattern with wildcards.
|
|
71
|
+
* Supports patterns like:
|
|
72
|
+
* - "https://*.example.com" - matches any subdomain
|
|
73
|
+
* - "http://localhost:*" - matches any port
|
|
74
|
+
* - "https://*.objectui.org,https://*.objectstack.ai" - comma-separated patterns
|
|
75
|
+
*
|
|
76
|
+
* @param origin The origin to check (e.g., "https://app.example.com")
|
|
77
|
+
* @param pattern The pattern to match against (supports * wildcard)
|
|
78
|
+
* @returns true if origin matches the pattern
|
|
79
|
+
*/
|
|
80
|
+
function matchOriginPattern(origin: string, pattern: string): boolean {
|
|
81
|
+
if (pattern === '*') return true;
|
|
82
|
+
if (pattern === origin) return true;
|
|
83
|
+
|
|
84
|
+
// Convert wildcard pattern to regex
|
|
85
|
+
// Escape special regex characters except *
|
|
86
|
+
const regexPattern = pattern
|
|
87
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape special chars
|
|
88
|
+
.replace(/\*/g, '.*'); // Convert * to .*
|
|
89
|
+
|
|
90
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
91
|
+
return regex.test(origin);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create a CORS origin matcher function that supports wildcard patterns.
|
|
96
|
+
*
|
|
97
|
+
* @param patterns Single pattern, array of patterns, or comma-separated patterns
|
|
98
|
+
* @returns Function that returns the origin if it matches, or null/undefined
|
|
99
|
+
*/
|
|
100
|
+
function createOriginMatcher(
|
|
101
|
+
patterns: string | string[]
|
|
102
|
+
): (origin: string) => string | undefined | null {
|
|
103
|
+
// Normalize to array
|
|
104
|
+
let patternList: string[];
|
|
105
|
+
if (typeof patterns === 'string') {
|
|
106
|
+
// Handle comma-separated patterns
|
|
107
|
+
patternList = patterns.includes(',')
|
|
108
|
+
? patterns.split(',').map(s => s.trim()).filter(Boolean)
|
|
109
|
+
: [patterns];
|
|
110
|
+
} else {
|
|
111
|
+
patternList = patterns;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Return matcher function
|
|
115
|
+
return (requestOrigin: string) => {
|
|
116
|
+
for (const pattern of patternList) {
|
|
117
|
+
if (matchOriginPattern(requestOrigin, pattern)) {
|
|
118
|
+
return requestOrigin;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
60
125
|
export class HonoServerPlugin implements Plugin {
|
|
61
126
|
name = 'com.objectstack.server.hono';
|
|
62
127
|
type = 'server';
|
|
63
128
|
version = '0.9.0';
|
|
64
|
-
|
|
129
|
+
|
|
65
130
|
// Constants
|
|
66
131
|
private static readonly DEFAULT_ENDPOINT_PRIORITY = 100;
|
|
67
132
|
private static readonly CORE_ENDPOINT_PRIORITY = 950;
|
|
68
133
|
private static readonly DISCOVERY_ENDPOINT_PRIORITY = 900;
|
|
69
|
-
|
|
134
|
+
|
|
70
135
|
private options: HonoPluginOptions;
|
|
71
136
|
private server: HonoHttpServer;
|
|
72
137
|
|
|
73
138
|
constructor(options: HonoPluginOptions = {}) {
|
|
74
|
-
this.options = {
|
|
139
|
+
this.options = {
|
|
75
140
|
port: 3000,
|
|
76
141
|
registerStandardEndpoints: true,
|
|
77
142
|
useApiRegistry: true,
|
|
@@ -86,17 +151,76 @@ export class HonoServerPlugin implements Plugin {
|
|
|
86
151
|
* Init phase - Setup HTTP server and register as service
|
|
87
152
|
*/
|
|
88
153
|
init = async (ctx: PluginContext) => {
|
|
89
|
-
ctx.logger.debug('Initializing Hono server plugin', {
|
|
154
|
+
ctx.logger.debug('Initializing Hono server plugin', {
|
|
90
155
|
port: this.options.port,
|
|
91
|
-
staticRoot: this.options.staticRoot
|
|
156
|
+
staticRoot: this.options.staticRoot
|
|
92
157
|
});
|
|
93
|
-
|
|
158
|
+
|
|
94
159
|
// Register HTTP server service as IHttpServer
|
|
95
160
|
// Register as 'http.server' to match core requirements
|
|
96
161
|
ctx.registerService('http.server', this.server);
|
|
97
162
|
// Alias 'http-server' for backward compatibility
|
|
98
163
|
ctx.registerService('http-server', this.server);
|
|
99
164
|
ctx.logger.debug('HTTP server service registered', { serviceName: 'http.server' });
|
|
165
|
+
|
|
166
|
+
// ─── CORS Middleware ──────────────────────────────────────────────────
|
|
167
|
+
// Enabled by default. Controlled via options.cors or environment variables.
|
|
168
|
+
const corsDisabledByEnv = process.env.CORS_ENABLED === 'false';
|
|
169
|
+
if (this.options.cors !== false && !corsDisabledByEnv) {
|
|
170
|
+
const corsOpts = typeof this.options.cors === 'object' ? this.options.cors : {};
|
|
171
|
+
const enabled = corsOpts.enabled ?? true;
|
|
172
|
+
|
|
173
|
+
if (enabled) {
|
|
174
|
+
let configuredOrigin: string | string[];
|
|
175
|
+
if (corsOpts.origins) {
|
|
176
|
+
configuredOrigin = corsOpts.origins;
|
|
177
|
+
} else if (process.env.CORS_ORIGIN) {
|
|
178
|
+
const envOrigin = process.env.CORS_ORIGIN.trim();
|
|
179
|
+
configuredOrigin = envOrigin.includes(',') ? envOrigin.split(',').map(s => s.trim()) : envOrigin;
|
|
180
|
+
} else {
|
|
181
|
+
configuredOrigin = '*';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const credentials = corsOpts.credentials ?? (process.env.CORS_CREDENTIALS !== 'false');
|
|
185
|
+
const maxAge = corsOpts.maxAge ?? (process.env.CORS_MAX_AGE ? parseInt(process.env.CORS_MAX_AGE, 10) : 86400);
|
|
186
|
+
|
|
187
|
+
// Determine origin handler based on configuration
|
|
188
|
+
let origin: string | string[] | ((origin: string) => string | undefined | null);
|
|
189
|
+
|
|
190
|
+
// Check if patterns contain wildcards (*, subdomain patterns, port patterns)
|
|
191
|
+
const hasWildcard = (patterns: string | string[]): boolean => {
|
|
192
|
+
const list = Array.isArray(patterns) ? patterns : [patterns];
|
|
193
|
+
return list.some(p => p.includes('*'));
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// When credentials is true, browsers reject wildcard '*' for Access-Control-Allow-Origin.
|
|
197
|
+
// For wildcard patterns (like "https://*.example.com"), always use a matcher function.
|
|
198
|
+
// For exact origins, we can pass them directly as string/array.
|
|
199
|
+
if (configuredOrigin === '*' && credentials) {
|
|
200
|
+
// Credentials mode with '*' - reflect the request origin
|
|
201
|
+
origin = (requestOrigin: string) => requestOrigin || '*';
|
|
202
|
+
} else if (hasWildcard(configuredOrigin)) {
|
|
203
|
+
// Wildcard patterns (including better-auth style patterns like "https://*.objectui.org")
|
|
204
|
+
// Use pattern matcher to support subdomain and port wildcards
|
|
205
|
+
origin = createOriginMatcher(configuredOrigin);
|
|
206
|
+
} else {
|
|
207
|
+
// Exact origin(s) - pass through as-is
|
|
208
|
+
origin = configuredOrigin;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const rawApp = this.server.getRawApp();
|
|
212
|
+
rawApp.use('*', cors({
|
|
213
|
+
origin: origin as any,
|
|
214
|
+
allowMethods: corsOpts.methods || ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'],
|
|
215
|
+
allowHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
|
|
216
|
+
exposeHeaders: [],
|
|
217
|
+
credentials,
|
|
218
|
+
maxAge,
|
|
219
|
+
}));
|
|
220
|
+
|
|
221
|
+
ctx.logger.debug('CORS middleware enabled', { origin: configuredOrigin, credentials });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
100
224
|
}
|
|
101
225
|
|
|
102
226
|
/**
|
|
@@ -112,8 +236,8 @@ export class HonoServerPlugin implements Plugin {
|
|
|
112
236
|
try {
|
|
113
237
|
const rawKernel = ctx.getKernel() as any;
|
|
114
238
|
if (rawKernel.plugins) {
|
|
115
|
-
const loadedPlugins = rawKernel.plugins instanceof Map
|
|
116
|
-
? Array.from(rawKernel.plugins.values())
|
|
239
|
+
const loadedPlugins = rawKernel.plugins instanceof Map
|
|
240
|
+
? Array.from(rawKernel.plugins.values())
|
|
117
241
|
: Array.isArray(rawKernel.plugins) ? rawKernel.plugins : Object.values(rawKernel.plugins);
|
|
118
242
|
|
|
119
243
|
for (const plugin of (loadedPlugins as any[])) {
|
|
@@ -123,10 +247,10 @@ export class HonoServerPlugin implements Plugin {
|
|
|
123
247
|
// Derive base route from name: @org/console -> console
|
|
124
248
|
const slug = plugin.slug || plugin.name.split('/').pop();
|
|
125
249
|
const baseRoute = `/${slug}`;
|
|
126
|
-
|
|
127
|
-
ctx.logger.debug(`Auto-mounting UI Plugin: ${plugin.name}`, {
|
|
128
|
-
path: baseRoute,
|
|
129
|
-
root: plugin.staticPath
|
|
250
|
+
|
|
251
|
+
ctx.logger.debug(`Auto-mounting UI Plugin: ${plugin.name}`, {
|
|
252
|
+
path: baseRoute,
|
|
253
|
+
root: plugin.staticPath
|
|
130
254
|
});
|
|
131
255
|
|
|
132
256
|
mounts.push({
|
|
@@ -161,7 +285,7 @@ export class HonoServerPlugin implements Plugin {
|
|
|
161
285
|
|
|
162
286
|
if (mounts.length > 0) {
|
|
163
287
|
const rawApp = this.server.getRawApp();
|
|
164
|
-
|
|
288
|
+
|
|
165
289
|
for (const mount of mounts) {
|
|
166
290
|
const mountRoot = path.resolve(process.cwd(), mount.root);
|
|
167
291
|
|
|
@@ -173,22 +297,22 @@ export class HonoServerPlugin implements Plugin {
|
|
|
173
297
|
const mountPath = mount.path || '/';
|
|
174
298
|
const normalizedPath = mountPath.startsWith('/') ? mountPath : `/${mountPath}`;
|
|
175
299
|
const routePattern = normalizedPath === '/' ? '/*' : `${normalizedPath.replace(/\/$/, '')}/*`;
|
|
176
|
-
|
|
300
|
+
|
|
177
301
|
// Routes to register: both /mount and /mount/*
|
|
178
302
|
const routes = normalizedPath === '/' ? [routePattern] : [normalizedPath, routePattern];
|
|
179
303
|
|
|
180
|
-
ctx.logger.debug('Mounting static files', {
|
|
181
|
-
to: routes,
|
|
182
|
-
from: mountRoot,
|
|
183
|
-
rewrite: mount.rewrite,
|
|
184
|
-
spa: mount.spa
|
|
304
|
+
ctx.logger.debug('Mounting static files', {
|
|
305
|
+
to: routes,
|
|
306
|
+
from: mountRoot,
|
|
307
|
+
rewrite: mount.rewrite,
|
|
308
|
+
spa: mount.spa
|
|
185
309
|
});
|
|
186
310
|
|
|
187
311
|
routes.forEach(route => {
|
|
188
312
|
// 1. Serve Static Files
|
|
189
313
|
rawApp.get(
|
|
190
|
-
route,
|
|
191
|
-
serveStatic({
|
|
314
|
+
route,
|
|
315
|
+
serveStatic({
|
|
192
316
|
root: mount.root,
|
|
193
317
|
rewriteRequestPath: (reqPath) => {
|
|
194
318
|
if (mount.rewrite && normalizedPath !== '/') {
|
|
@@ -208,12 +332,12 @@ export class HonoServerPlugin implements Plugin {
|
|
|
208
332
|
// Skip if API path check
|
|
209
333
|
const config = this.options.restConfig || {};
|
|
210
334
|
const basePath = config.api?.basePath || '/api';
|
|
211
|
-
|
|
335
|
+
|
|
212
336
|
if (c.req.path.startsWith(basePath)) {
|
|
213
337
|
return next();
|
|
214
338
|
}
|
|
215
339
|
|
|
216
|
-
return serveStatic({
|
|
340
|
+
return serveStatic({
|
|
217
341
|
root: mount.root,
|
|
218
342
|
rewriteRequestPath: () => 'index.html'
|
|
219
343
|
})(c, next);
|
|
@@ -232,7 +356,7 @@ export class HonoServerPlugin implements Plugin {
|
|
|
232
356
|
|
|
233
357
|
const port = this.options.port ?? 3000;
|
|
234
358
|
ctx.logger.debug('Starting HTTP server', { port });
|
|
235
|
-
|
|
359
|
+
|
|
236
360
|
await this.server.listen(port);
|
|
237
361
|
|
|
238
362
|
const actualPort = this.server.getPort();
|
|
@@ -281,37 +405,41 @@ export class HonoServerPlugin implements Plugin {
|
|
|
281
405
|
|
|
282
406
|
ctx.logger.info('Registered discovery endpoints', { prefix });
|
|
283
407
|
|
|
284
|
-
// Basic CRUD data endpoints — delegate to
|
|
285
|
-
const
|
|
408
|
+
// Basic CRUD data endpoints — delegate to ObjectQL service directly
|
|
409
|
+
const getObjectQL = () => ctx.getService<IDataEngine>('objectql');
|
|
286
410
|
|
|
287
411
|
// Create
|
|
288
412
|
rawApp.post(`${prefix}/data/:object`, async (c: any) => {
|
|
289
|
-
const
|
|
290
|
-
if (!
|
|
413
|
+
const ql = getObjectQL();
|
|
414
|
+
if (!ql) return c.json({ error: 'Data service not available' }, 503);
|
|
291
415
|
const object = c.req.param('object');
|
|
292
416
|
const data = await c.req.json().catch(() => ({}));
|
|
293
|
-
const
|
|
294
|
-
|
|
417
|
+
const res = await ql.insert(object, data);
|
|
418
|
+
const record = { ...data, ...res };
|
|
419
|
+
return c.json({ object, id: record.id, record });
|
|
295
420
|
});
|
|
296
421
|
|
|
297
422
|
// Get by ID
|
|
298
423
|
rawApp.get(`${prefix}/data/:object/:id`, async (c: any) => {
|
|
299
|
-
const
|
|
300
|
-
if (!
|
|
424
|
+
const ql = getObjectQL();
|
|
425
|
+
if (!ql) return c.json({ error: 'Data service not available' }, 503);
|
|
301
426
|
const object = c.req.param('object');
|
|
302
427
|
const id = c.req.param('id');
|
|
303
|
-
|
|
304
|
-
|
|
428
|
+
let all = await ql.find(object);
|
|
429
|
+
if (!all) all = [];
|
|
430
|
+
const match = all.find((i: any) => i.id === id);
|
|
431
|
+
return match ? c.json({ object, id, record: match }) : c.json({ error: 'Not found' }, 404);
|
|
305
432
|
});
|
|
306
433
|
|
|
307
434
|
// Find / List
|
|
308
435
|
rawApp.get(`${prefix}/data/:object`, async (c: any) => {
|
|
309
|
-
const
|
|
310
|
-
if (!
|
|
436
|
+
const ql = getObjectQL();
|
|
437
|
+
if (!ql) return c.json({ error: 'Data service not available' }, 503);
|
|
311
438
|
const object = c.req.param('object');
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
439
|
+
let all = await ql.find(object);
|
|
440
|
+
if (!Array.isArray(all) && all && (all as any).value) all = (all as any).value;
|
|
441
|
+
if (!all) all = [];
|
|
442
|
+
return c.json({ object, records: all, total: all.length });
|
|
315
443
|
});
|
|
316
444
|
|
|
317
445
|
ctx.logger.debug('Registered standard CRUD data endpoints', { prefix });
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check if an origin matches a pattern with wildcards.
|
|
5
|
+
* Supports patterns like:
|
|
6
|
+
* - "https://*.example.com" - matches any subdomain
|
|
7
|
+
* - "http://localhost:*" - matches any port
|
|
8
|
+
* - "https://*.objectui.org,https://*.objectstack.ai" - comma-separated patterns
|
|
9
|
+
*
|
|
10
|
+
* @param origin The origin to check (e.g., "https://app.example.com")
|
|
11
|
+
* @param pattern The pattern to match against (supports * wildcard)
|
|
12
|
+
* @returns true if origin matches the pattern
|
|
13
|
+
*/
|
|
14
|
+
function matchOriginPattern(origin: string, pattern: string): boolean {
|
|
15
|
+
if (pattern === '*') return true;
|
|
16
|
+
if (pattern === origin) return true;
|
|
17
|
+
|
|
18
|
+
// Convert wildcard pattern to regex
|
|
19
|
+
// Escape special regex characters except *
|
|
20
|
+
const regexPattern = pattern
|
|
21
|
+
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape special chars
|
|
22
|
+
.replace(/\*/g, '.*'); // Convert * to .*
|
|
23
|
+
|
|
24
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
25
|
+
return regex.test(origin);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a CORS origin matcher function that supports wildcard patterns.
|
|
30
|
+
*
|
|
31
|
+
* @param patterns Single pattern, array of patterns, or comma-separated patterns
|
|
32
|
+
* @returns Function that returns the origin if it matches, or null/undefined
|
|
33
|
+
*/
|
|
34
|
+
function createOriginMatcher(
|
|
35
|
+
patterns: string | string[]
|
|
36
|
+
): (origin: string) => string | undefined | null {
|
|
37
|
+
// Normalize to array
|
|
38
|
+
let patternList: string[];
|
|
39
|
+
if (typeof patterns === 'string') {
|
|
40
|
+
// Handle comma-separated patterns
|
|
41
|
+
patternList = patterns.includes(',')
|
|
42
|
+
? patterns.split(',').map(s => s.trim()).filter(Boolean)
|
|
43
|
+
: [patterns];
|
|
44
|
+
} else {
|
|
45
|
+
patternList = patterns;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Return matcher function
|
|
49
|
+
return (requestOrigin: string) => {
|
|
50
|
+
for (const pattern of patternList) {
|
|
51
|
+
if (matchOriginPattern(requestOrigin, pattern)) {
|
|
52
|
+
return requestOrigin;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
describe('matchOriginPattern', () => {
|
|
60
|
+
describe('exact matching', () => {
|
|
61
|
+
it('should match exact origin', () => {
|
|
62
|
+
expect(matchOriginPattern('https://app.example.com', 'https://app.example.com')).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('should not match different origins', () => {
|
|
66
|
+
expect(matchOriginPattern('https://app.example.com', 'https://api.example.com')).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should match wildcard "*"', () => {
|
|
70
|
+
expect(matchOriginPattern('https://any.domain.com', '*')).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('subdomain wildcard matching', () => {
|
|
75
|
+
it('should match subdomain with wildcard pattern', () => {
|
|
76
|
+
expect(matchOriginPattern('https://app.objectui.org', 'https://*.objectui.org')).toBe(true);
|
|
77
|
+
expect(matchOriginPattern('https://api.objectui.org', 'https://*.objectui.org')).toBe(true);
|
|
78
|
+
expect(matchOriginPattern('https://studio.objectui.org', 'https://*.objectui.org')).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should match multi-level subdomains', () => {
|
|
82
|
+
expect(matchOriginPattern('https://app.dev.objectui.org', 'https://*.objectui.org')).toBe(true);
|
|
83
|
+
expect(matchOriginPattern('https://api.staging.objectui.org', 'https://*.objectui.org')).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should not match different domain', () => {
|
|
87
|
+
expect(matchOriginPattern('https://app.example.com', 'https://*.objectui.org')).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should not match different protocol', () => {
|
|
91
|
+
expect(matchOriginPattern('http://app.objectui.org', 'https://*.objectui.org')).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('port wildcard matching', () => {
|
|
96
|
+
it('should match localhost with any port', () => {
|
|
97
|
+
expect(matchOriginPattern('http://localhost:3000', 'http://localhost:*')).toBe(true);
|
|
98
|
+
expect(matchOriginPattern('http://localhost:8080', 'http://localhost:*')).toBe(true);
|
|
99
|
+
expect(matchOriginPattern('http://localhost:5173', 'http://localhost:*')).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should not match different host', () => {
|
|
103
|
+
expect(matchOriginPattern('http://example.com:3000', 'http://localhost:*')).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
describe('multiple wildcard patterns', () => {
|
|
108
|
+
it('should match wildcard in multiple positions', () => {
|
|
109
|
+
expect(matchOriginPattern('https://app.objectui.org', 'https://*.objectui.*')).toBe(true);
|
|
110
|
+
expect(matchOriginPattern('https://api.objectui.com', 'https://*.objectui.*')).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('createOriginMatcher', () => {
|
|
116
|
+
describe('single pattern', () => {
|
|
117
|
+
it('should create matcher for single string pattern', () => {
|
|
118
|
+
const matcher = createOriginMatcher('https://*.objectui.org');
|
|
119
|
+
|
|
120
|
+
expect(matcher('https://app.objectui.org')).toBe('https://app.objectui.org');
|
|
121
|
+
expect(matcher('https://api.objectui.org')).toBe('https://api.objectui.org');
|
|
122
|
+
expect(matcher('https://example.com')).toBe(null);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('array of patterns', () => {
|
|
127
|
+
it('should create matcher for array of patterns', () => {
|
|
128
|
+
const matcher = createOriginMatcher([
|
|
129
|
+
'https://*.objectui.org',
|
|
130
|
+
'https://*.objectstack.ai'
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
expect(matcher('https://app.objectui.org')).toBe('https://app.objectui.org');
|
|
134
|
+
expect(matcher('https://api.objectstack.ai')).toBe('https://api.objectstack.ai');
|
|
135
|
+
expect(matcher('https://example.com')).toBe(null);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('comma-separated patterns', () => {
|
|
140
|
+
it('should parse comma-separated patterns', () => {
|
|
141
|
+
const matcher = createOriginMatcher('https://*.objectui.org,https://*.objectstack.ai');
|
|
142
|
+
|
|
143
|
+
expect(matcher('https://app.objectui.org')).toBe('https://app.objectui.org');
|
|
144
|
+
expect(matcher('https://api.objectstack.ai')).toBe('https://api.objectstack.ai');
|
|
145
|
+
expect(matcher('https://example.com')).toBe(null);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should handle whitespace in comma-separated patterns', () => {
|
|
149
|
+
const matcher = createOriginMatcher('https://*.objectui.org, https://*.objectstack.ai , http://localhost:*');
|
|
150
|
+
|
|
151
|
+
expect(matcher('https://app.objectui.org')).toBe('https://app.objectui.org');
|
|
152
|
+
expect(matcher('https://api.objectstack.ai')).toBe('https://api.objectstack.ai');
|
|
153
|
+
expect(matcher('http://localhost:3000')).toBe('http://localhost:3000');
|
|
154
|
+
expect(matcher('https://example.com')).toBe(null);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
describe('mixed exact and wildcard patterns', () => {
|
|
159
|
+
it('should match both exact and wildcard patterns', () => {
|
|
160
|
+
const matcher = createOriginMatcher([
|
|
161
|
+
'https://app.example.com',
|
|
162
|
+
'https://*.objectui.org'
|
|
163
|
+
]);
|
|
164
|
+
|
|
165
|
+
expect(matcher('https://app.example.com')).toBe('https://app.example.com');
|
|
166
|
+
expect(matcher('https://dev.objectui.org')).toBe('https://dev.objectui.org');
|
|
167
|
+
expect(matcher('https://other.com')).toBe(null);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe('localhost patterns', () => {
|
|
172
|
+
it('should match localhost with port wildcard', () => {
|
|
173
|
+
const matcher = createOriginMatcher('http://localhost:*');
|
|
174
|
+
|
|
175
|
+
expect(matcher('http://localhost:3000')).toBe('http://localhost:3000');
|
|
176
|
+
expect(matcher('http://localhost:8080')).toBe('http://localhost:8080');
|
|
177
|
+
expect(matcher('http://127.0.0.1:3000')).toBe(null);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
+
|
|
3
|
+
import { defineConfig } from 'vitest/config';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
test: {
|
|
8
|
+
globals: true,
|
|
9
|
+
environment: 'node',
|
|
10
|
+
},
|
|
11
|
+
resolve: {
|
|
12
|
+
alias: {
|
|
13
|
+
'@objectstack/core': path.resolve(__dirname, '../../core/src/index.ts'),
|
|
14
|
+
'@objectstack/spec/api': path.resolve(__dirname, '../../spec/src/api/index.ts'),
|
|
15
|
+
'@objectstack/spec/contracts': path.resolve(__dirname, '../../spec/src/contracts/index.ts'),
|
|
16
|
+
'@objectstack/spec/data': path.resolve(__dirname, '../../spec/src/data/index.ts'),
|
|
17
|
+
'@objectstack/spec/kernel': path.resolve(__dirname, '../../spec/src/kernel/index.ts'),
|
|
18
|
+
'@objectstack/spec/system': path.resolve(__dirname, '../../spec/src/system/index.ts'),
|
|
19
|
+
'@objectstack/spec': path.resolve(__dirname, '../../spec/src/index.ts'),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
});
|