blok0 0.1.0 → 0.1.2

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.
@@ -0,0 +1,177 @@
1
+ import axios, { AxiosInstance, AxiosResponse } from 'axios';
2
+ import { getAuthHeader } from '../auth';
3
+
4
+ export interface BlockMetadata {
5
+ id: number;
6
+ name: string;
7
+ slug: string;
8
+ codeFiles: Array<{
9
+ sourceCode?: {
10
+ name: string;
11
+ url: string;
12
+ };
13
+ }>;
14
+ _status: 'published' | 'draft';
15
+ }
16
+
17
+ export interface CodeFile {
18
+ name: string;
19
+ content: string;
20
+ }
21
+
22
+ class APIClient {
23
+ private client: AxiosInstance;
24
+ private baseURL: string;
25
+
26
+ constructor(baseURL: string = 'https://www.blok0.xyz') {
27
+ this.baseURL = baseURL;
28
+ this.client = axios.create({
29
+ baseURL,
30
+ timeout: 30000,
31
+ headers: {
32
+ 'User-Agent': 'blok0-cli/1.0.0'
33
+ }
34
+ });
35
+
36
+ // Add auth header to all requests
37
+ this.client.interceptors.request.use(async (config) => {
38
+ const authHeader = await getAuthHeader();
39
+ if (authHeader) {
40
+ config.headers.Authorization = authHeader;
41
+ console.log(`🔐 API Request: ${config.method?.toUpperCase()} ${config.url}`);
42
+ console.log(`🔑 Authorization: ${authHeader}`);
43
+ } else {
44
+ console.log(`🚫 API Request: ${config.method?.toUpperCase()} ${config.url} (No auth)`);
45
+ }
46
+ return config;
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Fetch block metadata from URL
52
+ */
53
+ async fetchBlockMetadata(url: string): Promise<BlockMetadata> {
54
+ try {
55
+ const response: AxiosResponse<BlockMetadata> = await this.client.get(url);
56
+ return response.data;
57
+ } catch (error) {
58
+ if (axios.isAxiosError(error)) {
59
+ if (error.response?.status === 401) {
60
+ throw new Error('Authentication required. Please run `blok0 login` first.');
61
+ }
62
+ if (error.response?.status === 404) {
63
+ throw new Error(`Block not found at URL: ${url}`);
64
+ }
65
+ throw new Error(`API request failed: ${error.response?.data?.message || error.message}`);
66
+ }
67
+ throw error;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Download source code file
73
+ */
74
+ async downloadSourceCode(url: string): Promise<string> {
75
+ try {
76
+ const response: AxiosResponse<string> = await this.client.get(url, {
77
+ responseType: 'text'
78
+ });
79
+ return response.data;
80
+ } catch (error) {
81
+ if (axios.isAxiosError(error)) {
82
+ throw new Error(`Failed to download source code from ${url}: ${error.message}`);
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Validate block metadata
90
+ */
91
+ validateBlockMetadata(metadata: BlockMetadata): void {
92
+ if (!metadata.id || typeof metadata.id !== 'number') {
93
+ throw new Error('Invalid block metadata: missing or invalid id');
94
+ }
95
+
96
+ if (!metadata.name || typeof metadata.name !== 'string') {
97
+ throw new Error('Invalid block metadata: missing or invalid name');
98
+ }
99
+
100
+ if (!metadata.slug || typeof metadata.slug !== 'string') {
101
+ throw new Error('Invalid block metadata: missing or invalid slug');
102
+ }
103
+
104
+ // Validate slug format
105
+ const slugRegex = /^[a-z0-9-]+$/;
106
+ if (!slugRegex.test(metadata.slug)) {
107
+ throw new Error('Invalid block slug format. Must contain only lowercase letters, numbers, and dashes.');
108
+ }
109
+
110
+ if (!metadata.codeFiles || !Array.isArray(metadata.codeFiles)) {
111
+ throw new Error('Invalid block metadata: missing or invalid codeFiles');
112
+ }
113
+
114
+ // Filter out malformed codeFiles entries
115
+ const validCodeFiles = metadata.codeFiles.filter(file => file.sourceCode && file.sourceCode.name && file.sourceCode.url);
116
+
117
+ if (validCodeFiles.length === 0) {
118
+ throw new Error('Invalid block metadata: no valid code files specified');
119
+ }
120
+
121
+ // Check for required files
122
+ const hasConfig = validCodeFiles.some(file => file.sourceCode!.name === 'config.ts');
123
+ if (!hasConfig) {
124
+ throw new Error('Invalid block metadata: config.ts file is required');
125
+ }
126
+
127
+ if (metadata._status !== 'published') {
128
+ throw new Error('Block is not published and cannot be installed');
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Fetch complete block data including source files
134
+ */
135
+ async fetchBlockData(url: string): Promise<{ metadata: BlockMetadata; files: CodeFile[] }> {
136
+ const metadata = await this.fetchBlockMetadata(url);
137
+ this.validateBlockMetadata(metadata);
138
+
139
+ const files: CodeFile[] = [];
140
+
141
+ // Filter out malformed codeFiles entries (same logic as validation)
142
+ const validCodeFiles = metadata.codeFiles.filter(file => file.sourceCode && file.sourceCode.name && file.sourceCode.url);
143
+
144
+ for (const fileInfo of validCodeFiles) {
145
+ const { name, url: fileUrl } = fileInfo.sourceCode!;
146
+
147
+ // Resolve relative URLs
148
+ const resolvedUrl = fileUrl.startsWith('http') ? fileUrl : `${this.baseURL}${fileUrl}`;
149
+
150
+ try {
151
+ const content = await this.downloadSourceCode(resolvedUrl);
152
+ files.push({ name, content });
153
+ } catch (error) {
154
+ throw new Error(`Failed to download ${name}: ${(error as Error).message}`);
155
+ }
156
+ }
157
+
158
+ return { metadata, files };
159
+ }
160
+
161
+ /**
162
+ * Test API connectivity and authentication
163
+ */
164
+ async testConnection(): Promise<boolean> {
165
+ try {
166
+ // Try to access a test endpoint or just check auth header
167
+ const authHeader = await getAuthHeader();
168
+ return authHeader !== null;
169
+ } catch (error) {
170
+ return false;
171
+ }
172
+ }
173
+ }
174
+
175
+ // Export singleton instance
176
+ export const apiClient = new APIClient();
177
+ export { APIClient };
@@ -0,0 +1,368 @@
1
+ import { Project, SyntaxKind, Node, ImportDeclaration, ObjectLiteralExpression, PropertyAssignment, ArrayLiteralExpression, StringLiteral, Identifier } from 'ts-morph';
2
+
3
+ /**
4
+ * Update Payload page collection config to include new block
5
+ */
6
+ export function updatePageCollectionConfig(pagesCollectionPath: string, blockConfigPath: string, blockName: string): void {
7
+ const project = new Project();
8
+ const sourceFile = project.addSourceFileAtPath(pagesCollectionPath);
9
+
10
+ // Check if block is already imported (do this first before any modifications)
11
+ const importDeclarations = sourceFile.getImportDeclarations();
12
+ const existingImport = importDeclarations.find(imp =>
13
+ imp.getModuleSpecifier().getLiteralValue() === blockConfigPath
14
+ );
15
+
16
+ let needsImport = !existingImport;
17
+ if (needsImport) {
18
+ // Add import statement
19
+ const lastImport = importDeclarations[importDeclarations.length - 1];
20
+ const importText = `\nimport { ${blockName} } from '${blockConfigPath}';\n`;
21
+ sourceFile.insertText(lastImport.getEnd(), importText);
22
+ sourceFile.saveSync(); // Save the import addition
23
+ }
24
+
25
+ // Re-load the source file after modification to get fresh AST
26
+ const updatedProject = new Project();
27
+ const updatedSourceFile = updatedProject.addSourceFileAtPath(pagesCollectionPath);
28
+
29
+ // Find the Pages collection export (fresh references)
30
+ const pagesExport = updatedSourceFile.getVariableDeclaration('Pages');
31
+ if (!pagesExport) {
32
+ throw new Error('Could not find Pages collection export');
33
+ }
34
+
35
+ const pagesObject = pagesExport.getInitializer();
36
+ if (!pagesObject || !Node.isObjectLiteralExpression(pagesObject)) {
37
+ throw new Error('Could not find Pages collection object');
38
+ }
39
+
40
+ // Find the fields array
41
+ const fieldsProperty = pagesObject.getProperty('fields');
42
+ if (!fieldsProperty || !Node.isPropertyAssignment(fieldsProperty)) {
43
+ throw new Error('Could not find fields property in Pages collection');
44
+ }
45
+
46
+ const fieldsArray = fieldsProperty.getInitializer();
47
+ if (!fieldsArray || !Node.isArrayLiteralExpression(fieldsArray)) {
48
+ throw new Error('Could not find fields array in Pages collection');
49
+ }
50
+
51
+ // Find the tabs array within fields
52
+ const tabsField = fieldsArray.getElements().find(element => {
53
+ if (Node.isObjectLiteralExpression(element)) {
54
+ const typeProperty = element.getProperty('type');
55
+ if (Node.isPropertyAssignment(typeProperty)) {
56
+ const initializer = typeProperty.getInitializer();
57
+ return Node.isStringLiteral(initializer) && initializer.getLiteralValue() === 'tabs';
58
+ }
59
+ }
60
+ return false;
61
+ });
62
+
63
+ if (!tabsField || !Node.isObjectLiteralExpression(tabsField)) {
64
+ throw new Error('Could not find tabs field in Pages collection');
65
+ }
66
+
67
+ const tabsProperty = tabsField.getProperty('tabs');
68
+ if (!tabsProperty || !Node.isPropertyAssignment(tabsProperty)) {
69
+ throw new Error('Could not find tabs property');
70
+ }
71
+
72
+ const tabsArray = tabsProperty.getInitializer();
73
+ if (!tabsArray || !Node.isArrayLiteralExpression(tabsArray)) {
74
+ throw new Error('Could not find tabs array');
75
+ }
76
+
77
+ // Find the "Content" tab (which contains the layout)
78
+ const contentTab = tabsArray.getElements().find(element => {
79
+ if (Node.isObjectLiteralExpression(element)) {
80
+ const labelProperty = element.getProperty('label');
81
+ if (Node.isPropertyAssignment(labelProperty)) {
82
+ const initializer = labelProperty.getInitializer();
83
+ return Node.isStringLiteral(initializer) && initializer.getLiteralValue() === 'Content';
84
+ }
85
+ }
86
+ return false;
87
+ });
88
+
89
+ if (!contentTab || !Node.isObjectLiteralExpression(contentTab)) {
90
+ throw new Error('Could not find Content tab in Pages collection');
91
+ }
92
+
93
+ // Find the layout field within the Content tab
94
+ const contentFields = contentTab.getProperty('fields');
95
+ if (!contentFields || !Node.isPropertyAssignment(contentFields)) {
96
+ throw new Error('Could not find fields in Content tab');
97
+ }
98
+
99
+ const contentFieldsArray = contentFields.getInitializer();
100
+ if (!contentFieldsArray || !Node.isArrayLiteralExpression(contentFieldsArray)) {
101
+ throw new Error('Could not find fields array in Content tab');
102
+ }
103
+
104
+ // Find the layout field
105
+ const layoutField = contentFieldsArray.getElements().find(element => {
106
+ if (Node.isObjectLiteralExpression(element)) {
107
+ const nameProperty = element.getProperty('name');
108
+ if (Node.isPropertyAssignment(nameProperty)) {
109
+ const initializer = nameProperty.getInitializer();
110
+ return Node.isStringLiteral(initializer) && initializer.getLiteralValue() === 'layout';
111
+ }
112
+ }
113
+ return false;
114
+ });
115
+
116
+ if (!layoutField || !Node.isObjectLiteralExpression(layoutField)) {
117
+ throw new Error('Could not find layout field');
118
+ }
119
+
120
+ // Find the blocks array
121
+ const blocksProperty = layoutField.getProperty('blocks');
122
+ if (!blocksProperty || !Node.isPropertyAssignment(blocksProperty)) {
123
+ throw new Error('Could not find blocks property in layout');
124
+ }
125
+
126
+ const blocksArray = blocksProperty.getInitializer();
127
+ if (!blocksArray || !Node.isArrayLiteralExpression(blocksArray)) {
128
+ throw new Error('Could not find blocks array in layout');
129
+ }
130
+
131
+ // Check if block is already in array
132
+ const existingElements = blocksArray.getElements();
133
+ const alreadyExists = existingElements.some(element => {
134
+ if (Node.isIdentifier(element)) {
135
+ return element.getText() === blockName;
136
+ }
137
+ return false;
138
+ });
139
+
140
+ if (!alreadyExists) {
141
+ // Add block to array
142
+ const lastElement = existingElements[existingElements.length - 1];
143
+ if (lastElement) {
144
+ blocksArray.insertElement(existingElements.length, blockName);
145
+ } else {
146
+ blocksArray.addElement(blockName);
147
+ }
148
+ }
149
+
150
+ updatedSourceFile.saveSync();
151
+ }
152
+
153
+ /**
154
+ * Update RenderBlocks.tsx to include new block component
155
+ */
156
+ export function updateRenderBlocksComponent(componentPath: string, blockSlug: string, blockComponentPath: string): void {
157
+ // Extract the actual component name from the Component.tsx file
158
+ const fullComponentPath = require('path').resolve(blockComponentPath.replace('./', 'src/blocks/') + '.tsx');
159
+ const componentName = extractComponentName(fullComponentPath);
160
+
161
+ if (!componentName) {
162
+ throw new Error(`Could not extract component name from ${fullComponentPath}`);
163
+ }
164
+
165
+ // Convert slug to blockType key (camelCase)
166
+ const blockTypeKey = blockSlug.split('-').map((word, index) =>
167
+ index === 0 ? word : word.charAt(0).toUpperCase() + word.slice(1)
168
+ ).join('');
169
+
170
+ const project = new Project();
171
+ const sourceFile = project.addSourceFileAtPath(componentPath);
172
+
173
+ // Find the blockComponents object
174
+ const blockComponents = sourceFile.getVariableDeclaration('blockComponents')?.getInitializer();
175
+ if (!blockComponents || !Node.isObjectLiteralExpression(blockComponents)) {
176
+ throw new Error('Could not find blockComponents object in RenderBlocks.tsx');
177
+ }
178
+
179
+ // Check if component is already imported (check both default and named imports)
180
+ const importDeclarations = sourceFile.getImportDeclarations();
181
+ const existingImport = importDeclarations.find(imp => {
182
+ // Check default import
183
+ const defaultImport = imp.getDefaultImport();
184
+ if (defaultImport && defaultImport.getText() === componentName) {
185
+ return true;
186
+ }
187
+
188
+ // Check named imports
189
+ const namedImports = imp.getNamedImports();
190
+ return namedImports.some(namedImport => namedImport.getName() === componentName);
191
+ });
192
+
193
+ let needsImport = !existingImport;
194
+ if (needsImport) {
195
+ // Add import statement (named import)
196
+ const lastImport = importDeclarations[importDeclarations.length - 1];
197
+ const importText = `\nimport { ${componentName} } from '${blockComponentPath}';\n`;
198
+ sourceFile.insertText(lastImport.getEnd(), importText);
199
+ sourceFile.saveSync(); // Save the import addition
200
+ }
201
+
202
+ // Re-load the source file after modification to get fresh AST
203
+ const updatedProject = new Project();
204
+ const updatedSourceFile = updatedProject.addSourceFileAtPath(componentPath);
205
+ const updatedBlockComponents = updatedSourceFile.getVariableDeclaration('blockComponents')?.getInitializer();
206
+ if (!updatedBlockComponents || !Node.isObjectLiteralExpression(updatedBlockComponents)) {
207
+ throw new Error('Could not find blockComponents object in RenderBlocks.tsx after reload');
208
+ }
209
+
210
+ // Check if property already exists
211
+ const existingProperties = updatedBlockComponents.getProperties();
212
+ const propertyExists = existingProperties.some(prop => {
213
+ if (Node.isPropertyAssignment(prop)) {
214
+ const name = prop.getName();
215
+ return name === `'${blockTypeKey}'` || name === `"${blockTypeKey}"`;
216
+ }
217
+ return false;
218
+ });
219
+
220
+ if (!propertyExists) {
221
+ // Add new property to object
222
+ const lastProperty = existingProperties[existingProperties.length - 1];
223
+ if (lastProperty) {
224
+ const insertPos = lastProperty.getEnd();
225
+ const newPropertyText = `,\n '${blockTypeKey}': ${componentName}`;
226
+ updatedSourceFile.insertText(insertPos, newPropertyText);
227
+ } else {
228
+ // Object is empty, add first property
229
+ const objectStart = updatedBlockComponents.getStart() + 1; // After opening brace
230
+ updatedSourceFile.insertText(objectStart, `\n '${blockTypeKey}': ${componentName}\n`);
231
+ }
232
+ }
233
+
234
+ updatedSourceFile.saveSync();
235
+ }
236
+
237
+ /**
238
+ * Validate that a file can be parsed as TypeScript
239
+ */
240
+ export function validateTypeScriptFile(filePath: string): { valid: boolean; errors: string[] } {
241
+ const errors: string[] = [];
242
+
243
+ try {
244
+ const project = new Project();
245
+ const sourceFile = project.addSourceFileAtPath(filePath);
246
+
247
+ // Check for syntax errors
248
+ const diagnostics = sourceFile.getPreEmitDiagnostics();
249
+ for (const diagnostic of diagnostics) {
250
+ errors.push(diagnostic.getMessageText().toString());
251
+ }
252
+ } catch (error) {
253
+ errors.push(`Failed to parse TypeScript file: ${(error as Error).message}`);
254
+ }
255
+
256
+ return { valid: errors.length === 0, errors };
257
+ }
258
+
259
+ /**
260
+ * Find Payload config file in project
261
+ */
262
+ export function findPayloadConfig(): string | null {
263
+ const possiblePaths = [
264
+ 'payload.config.ts',
265
+ 'payload.config.js',
266
+ 'src/payload.config.ts',
267
+ 'src/payload.config.js'
268
+ ];
269
+
270
+ for (const path of possiblePaths) {
271
+ if (require('fs').existsSync(path)) {
272
+ return path;
273
+ }
274
+ }
275
+
276
+ return null;
277
+ }
278
+
279
+ /**
280
+ * Find RenderBlocks component file
281
+ */
282
+ export function findRenderBlocksComponent(): string | null {
283
+ const possiblePaths = [
284
+ 'src/blocks/RenderBlocks.tsx',
285
+ 'src/blocks/RenderBlocks.ts',
286
+ 'src/components/RenderBlocks.tsx',
287
+ 'src/components/RenderBlocks.ts'
288
+ ];
289
+
290
+ for (const path of possiblePaths) {
291
+ if (require('fs').existsSync(path)) {
292
+ return path;
293
+ }
294
+ }
295
+
296
+ return null;
297
+ }
298
+
299
+ /**
300
+ * Find Pages collection file in project
301
+ */
302
+ export function findPagesCollection(): string | null {
303
+ const possiblePaths = [
304
+ 'src/collections/Pages/index.ts',
305
+ 'src/collections/Pages.ts',
306
+ 'src/collections/pages/index.ts',
307
+ 'src/collections/pages.ts'
308
+ ];
309
+
310
+ for (const path of possiblePaths) {
311
+ if (require('fs').existsSync(path)) {
312
+ return path;
313
+ }
314
+ }
315
+
316
+ return null;
317
+ }
318
+
319
+ /**
320
+ * Extract the component name from a Component.tsx file
321
+ */
322
+ export function extractComponentName(componentPath: string): string | null {
323
+ try {
324
+ const project = new Project();
325
+ const sourceFile = project.addSourceFileAtPath(componentPath);
326
+
327
+ // Look for named exports like "export const ComponentName"
328
+ const variableDeclarations = sourceFile.getVariableDeclarations();
329
+ for (const declaration of variableDeclarations) {
330
+ if (declaration.isExported()) {
331
+ const name = declaration.getName();
332
+ // Check if it's a React component (function or const with JSX)
333
+ const initializer = declaration.getInitializer();
334
+ if (initializer) {
335
+ // Look for React.FC or function patterns
336
+ const type = declaration.getType();
337
+ const typeText = type.getText();
338
+ if (typeText.includes('React.FC') || typeText.includes('FC<') ||
339
+ initializer.getText().includes('React.FC') || initializer.getText().includes('FC<')) {
340
+ return name;
341
+ }
342
+ }
343
+ }
344
+ }
345
+
346
+ // Look for export default statements
347
+ const defaultExport = sourceFile.getDefaultExportSymbol();
348
+ if (defaultExport) {
349
+ return defaultExport.getName();
350
+ }
351
+
352
+ // Look for export default expressions
353
+ const exportAssignments = sourceFile.getExportAssignments();
354
+ for (const assignment of exportAssignments) {
355
+ if (assignment.isExportEquals() === false) { // export default
356
+ const expression = assignment.getExpression();
357
+ if (expression) {
358
+ return expression.getText();
359
+ }
360
+ }
361
+ }
362
+
363
+ return null;
364
+ } catch (error) {
365
+ console.error('Error extracting component name:', error);
366
+ return null;
367
+ }
368
+ }
@@ -0,0 +1,155 @@
1
+ export const AUTH_BASE_URL = 'https://www.blok0.xyz';
2
+ export const AUTHORIZE_ENDPOINT = '/api/authorize/cli';
3
+ export const DEFAULT_TIMEOUT = 5 * 60 * 1000; // 5 minutes
4
+ export const PORT_RANGE = { min: 3000, max: 4000 };
5
+
6
+ export const CALLBACK_PATH = '/callback';
7
+ export const SUCCESS_HTML = `
8
+ <!DOCTYPE html>
9
+ <html>
10
+ <head>
11
+ <title>Authentication Successful</title>
12
+ <style>
13
+ body {
14
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
15
+ background: #f5f5f5;
16
+ margin: 0;
17
+ padding: 0;
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: center;
21
+ height: 100vh;
22
+ }
23
+ .container {
24
+ background: white;
25
+ padding: 2rem;
26
+ border-radius: 8px;
27
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
28
+ text-align: center;
29
+ max-width: 400px;
30
+ }
31
+ .success-icon {
32
+ color: #10b981;
33
+ font-size: 3rem;
34
+ margin-bottom: 1rem;
35
+ }
36
+ h1 {
37
+ color: #1f2937;
38
+ margin: 0 0 0.5rem 0;
39
+ font-size: 1.5rem;
40
+ }
41
+ p {
42
+ color: #6b7280;
43
+ margin: 0;
44
+ }
45
+ </style>
46
+ </head>
47
+ <body>
48
+ <div class="container">
49
+ <div class="success-icon">✓</div>
50
+ <h1>Authentication Successful!</h1>
51
+ <p>You can now close this window and return to your terminal.</p>
52
+ </div>
53
+ </body>
54
+ </html>
55
+ `;
56
+
57
+ export const ERROR_HTML = `
58
+ <!DOCTYPE html>
59
+ <html>
60
+ <head>
61
+ <title>Authentication Failed</title>
62
+ <style>
63
+ body {
64
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
65
+ background: #f5f5f5;
66
+ margin: 0;
67
+ padding: 0;
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: center;
71
+ height: 100vh;
72
+ }
73
+ .container {
74
+ background: white;
75
+ padding: 2rem;
76
+ border-radius: 8px;
77
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
78
+ text-align: center;
79
+ max-width: 400px;
80
+ }
81
+ .error-icon {
82
+ color: #ef4444;
83
+ font-size: 3rem;
84
+ margin-bottom: 1rem;
85
+ }
86
+ h1 {
87
+ color: #1f2937;
88
+ margin: 0 0 0.5rem 0;
89
+ font-size: 1.5rem;
90
+ }
91
+ p {
92
+ color: #6b7280;
93
+ margin: 0;
94
+ }
95
+ </style>
96
+ </head>
97
+ <body>
98
+ <div class="container">
99
+ <div class="error-icon">✗</div>
100
+ <h1>Authentication Failed</h1>
101
+ <p>Please try again or contact support if the problem persists.</p>
102
+ </div>
103
+ </body>
104
+ </html>
105
+ `;
106
+
107
+ export const TIMEOUT_HTML = `
108
+ <!DOCTYPE html>
109
+ <html>
110
+ <head>
111
+ <title>Authentication Timeout</title>
112
+ <style>
113
+ body {
114
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
115
+ background: #f5f5f5;
116
+ margin: 0;
117
+ padding: 0;
118
+ display: flex;
119
+ align-items: center;
120
+ justify-content: center;
121
+ height: 100vh;
122
+ }
123
+ .container {
124
+ background: white;
125
+ padding: 2rem;
126
+ border-radius: 8px;
127
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
128
+ text-align: center;
129
+ max-width: 400px;
130
+ }
131
+ .timeout-icon {
132
+ color: #f59e0b;
133
+ font-size: 3rem;
134
+ margin-bottom: 1rem;
135
+ }
136
+ h1 {
137
+ color: #1f2937;
138
+ margin: 0 0 0.5rem 0;
139
+ font-size: 1.5rem;
140
+ }
141
+ p {
142
+ color: #6b7280;
143
+ margin: 0;
144
+ }
145
+ </style>
146
+ </head>
147
+ <body>
148
+ <div class="container">
149
+ <div class="timeout-icon">⏱</div>
150
+ <h1>Authentication Timeout</h1>
151
+ <p>The authentication request has timed out. Please try again.</p>
152
+ </div>
153
+ </body>
154
+ </html>
155
+ `;