@syncular/console 0.0.6-82 → 0.0.6-84

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syncular/console",
3
- "version": "0.0.6-82",
3
+ "version": "0.0.6-84",
4
4
  "description": "Embeddable Syncular console UI",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Benjamin Kniffler",
@@ -68,11 +68,11 @@
68
68
  "release": "bunx syncular-publish"
69
69
  },
70
70
  "dependencies": {
71
- "@syncular/observability-sentry": "0.0.6-82",
72
- "@syncular/transport-http": "0.0.6-82",
73
- "@syncular/ui": "0.0.6-82",
71
+ "@syncular/observability-sentry": "0.0.6-84",
72
+ "@syncular/transport-http": "0.0.6-84",
73
+ "@syncular/ui": "0.0.6-84",
74
74
  "@tanstack/react-query": "^5.90.21",
75
- "@tanstack/react-router": "^1.161.3",
75
+ "@tanstack/react-router": "^1.162.9",
76
76
  "lucide-react": "^0.575.0"
77
77
  },
78
78
  "peerDependencies": {
@@ -81,13 +81,13 @@
81
81
  },
82
82
  "devDependencies": {
83
83
  "@syncular/config": "0.0.0",
84
- "@tailwindcss/vite": "^4.2.0",
84
+ "@tailwindcss/vite": "^4.2.1",
85
85
  "@types/react": "^19",
86
86
  "@types/react-dom": "^19",
87
87
  "@vitejs/plugin-react": "^5.1.4",
88
88
  "react": "^19.2.4",
89
89
  "react-dom": "^19.2.4",
90
- "tailwindcss": "^4.2.0",
90
+ "tailwindcss": "^4.2.1",
91
91
  "vite": "^7.3.1"
92
92
  },
93
93
  "files": [
@@ -0,0 +1,193 @@
1
+ import { afterEach, describe, expect, it } from 'bun:test';
2
+ import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import path from 'node:path';
5
+ import {
6
+ CONSOLE_BASEPATH_META,
7
+ CONSOLE_SERVER_URL_META,
8
+ CONSOLE_TOKEN_META,
9
+ } from '../runtime-config';
10
+ import { createConsoleStaticResponder } from '../static-server';
11
+
12
+ const tempDirs: string[] = [];
13
+
14
+ function countMatches(value: string, pattern: RegExp): number {
15
+ return value.match(pattern)?.length ?? 0;
16
+ }
17
+
18
+ async function createStaticFixture(): Promise<string> {
19
+ const staticDir = await mkdtemp(path.join(tmpdir(), 'syncular-console-'));
20
+ tempDirs.push(staticDir);
21
+
22
+ await mkdir(path.join(staticDir, 'assets'), { recursive: true });
23
+ await writeFile(
24
+ path.join(staticDir, 'index.html'),
25
+ `<!doctype html>
26
+ <html>
27
+ <head>
28
+ <meta name="${CONSOLE_BASEPATH_META}" content="/old-basepath" />
29
+ <meta name="${CONSOLE_SERVER_URL_META}" content="https://old.example/api" />
30
+ <meta name="${CONSOLE_TOKEN_META}" content="old-token" />
31
+ <link rel="stylesheet" href="/assets/console.css" />
32
+ </head>
33
+ <body>
34
+ <script type="module" src="/assets/main.js"></script>
35
+ </body>
36
+ </html>`
37
+ );
38
+ await writeFile(
39
+ path.join(staticDir, 'assets', 'main.js'),
40
+ 'console.log(1);\n'
41
+ );
42
+ await writeFile(path.join(staticDir, 'assets', 'console.css'), 'body{}\n');
43
+
44
+ return staticDir;
45
+ }
46
+
47
+ afterEach(async () => {
48
+ await Promise.all(
49
+ tempDirs.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))
50
+ );
51
+ });
52
+
53
+ describe('createConsoleStaticResponder', () => {
54
+ it('serves index with prefilled meta tags and rewrites rooted asset paths', async () => {
55
+ const staticDir = await createStaticFixture();
56
+ const responder = createConsoleStaticResponder({
57
+ mountPath: '/console',
58
+ staticDir,
59
+ defaultPrefill: {
60
+ serverUrl: 'https://api.example.com',
61
+ token: 'default-token',
62
+ },
63
+ });
64
+
65
+ const response = await responder(new Request('http://localhost/console'));
66
+ if (!response) {
67
+ throw new Error('Expected console index response.');
68
+ }
69
+
70
+ const html = await response.text();
71
+ expect(response.status).toBe(200);
72
+ expect(response.headers.get('cache-control')).toBe('no-store');
73
+ expect(html).toContain(
74
+ `<meta name="${CONSOLE_BASEPATH_META}" content="/console" />`
75
+ );
76
+ expect(html).toContain(
77
+ `<meta name="${CONSOLE_SERVER_URL_META}" content="https://api.example.com" />`
78
+ );
79
+ expect(html).toContain(
80
+ `<meta name="${CONSOLE_TOKEN_META}" content="default-token" />`
81
+ );
82
+ expect(html).toContain('href="/console/assets/console.css"');
83
+ expect(html).toContain('src="/console/assets/main.js"');
84
+
85
+ expect(
86
+ countMatches(html, new RegExp(`name="${CONSOLE_BASEPATH_META}"`, 'g'))
87
+ ).toBe(1);
88
+ expect(
89
+ countMatches(html, new RegExp(`name="${CONSOLE_SERVER_URL_META}"`, 'g'))
90
+ ).toBe(1);
91
+ expect(
92
+ countMatches(html, new RegExp(`name="${CONSOLE_TOKEN_META}"`, 'g'))
93
+ ).toBe(1);
94
+ });
95
+
96
+ it('applies request-level prefill overrides when serving index', async () => {
97
+ const staticDir = await createStaticFixture();
98
+ const responder = createConsoleStaticResponder({
99
+ mountPath: '/console',
100
+ staticDir,
101
+ defaultPrefill: {
102
+ basePath: '/console',
103
+ serverUrl: 'https://default.example/api',
104
+ token: 'default-token',
105
+ },
106
+ });
107
+
108
+ const response = await responder(
109
+ new Request('http://localhost/console/app'),
110
+ {
111
+ prefill: {
112
+ basePath: '/ops',
113
+ serverUrl: 'https://ops.example/api',
114
+ token: 'request-token',
115
+ },
116
+ }
117
+ );
118
+ if (!response) {
119
+ throw new Error('Expected console index response.');
120
+ }
121
+
122
+ const html = await response.text();
123
+ expect(response.status).toBe(200);
124
+ expect(html).toContain(
125
+ `<meta name="${CONSOLE_BASEPATH_META}" content="/ops" />`
126
+ );
127
+ expect(html).toContain(
128
+ `<meta name="${CONSOLE_SERVER_URL_META}" content="https://ops.example/api" />`
129
+ );
130
+ expect(html).toContain(
131
+ `<meta name="${CONSOLE_TOKEN_META}" content="request-token" />`
132
+ );
133
+ expect(html).toContain('href="/ops/assets/console.css"');
134
+ expect(html).toContain('src="/ops/assets/main.js"');
135
+ });
136
+
137
+ it('serves static assets, blocks traversal, and handles missing assets', async () => {
138
+ const staticDir = await createStaticFixture();
139
+ const responder = createConsoleStaticResponder({
140
+ mountPath: '/console',
141
+ staticDir,
142
+ });
143
+
144
+ const assetResponse = await responder(
145
+ new Request('http://localhost/console/assets/main.js')
146
+ );
147
+ if (!assetResponse) {
148
+ throw new Error('Expected static asset response.');
149
+ }
150
+ expect(assetResponse.status).toBe(200);
151
+ expect(assetResponse.headers.get('content-type')).toBe(
152
+ 'text/javascript; charset=utf-8'
153
+ );
154
+ expect(assetResponse.headers.get('cache-control')).toBe(
155
+ 'public, max-age=31536000, immutable'
156
+ );
157
+ expect(await assetResponse.text()).toContain('console.log(1);');
158
+
159
+ const forbidden = await responder(
160
+ new Request('http://localhost/console/%2e%2e%2fsecret.js')
161
+ );
162
+ if (!forbidden) {
163
+ throw new Error('Expected forbidden response.');
164
+ }
165
+ expect(forbidden.status).toBe(403);
166
+
167
+ const missing = await responder(
168
+ new Request('http://localhost/console/assets/missing.js')
169
+ );
170
+ if (!missing) {
171
+ throw new Error('Expected missing response.');
172
+ }
173
+ expect(missing.status).toBe(404);
174
+ });
175
+
176
+ it('returns null for unsupported methods and non-matching mount paths', async () => {
177
+ const staticDir = await createStaticFixture();
178
+ const responder = createConsoleStaticResponder({
179
+ mountPath: '/console',
180
+ staticDir,
181
+ });
182
+
183
+ const postResponse = await responder(
184
+ new Request('http://localhost/console', { method: 'POST' })
185
+ );
186
+ const otherPathResponse = await responder(
187
+ new Request('http://localhost/admin')
188
+ );
189
+
190
+ expect(postResponse).toBeNull();
191
+ expect(otherPathResponse).toBeNull();
192
+ });
193
+ });