blok0 0.1.0 → 0.1.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.
@@ -0,0 +1,154 @@
1
+ import * as keytar from 'keytar';
2
+ import * as os from 'os';
3
+ import * as path from 'path';
4
+ import * as fs from 'fs';
5
+
6
+ const SERVICE_NAME = 'blok0';
7
+ const CONFIG_DIR = path.join(os.homedir(), '.blok0');
8
+ const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
9
+
10
+ export interface AuthConfig {
11
+ apiEndpoint?: string;
12
+ refreshToken?: string;
13
+ tokenExpiry?: number;
14
+ }
15
+
16
+ export interface AuthCallback {
17
+ token: string;
18
+ expires_in?: number;
19
+ refresh_token?: string;
20
+ }
21
+
22
+ export interface AuthServerOptions {
23
+ port?: number;
24
+ timeout?: number;
25
+ state?: string;
26
+ }
27
+
28
+ export interface TokenResponse {
29
+ access_token: string;
30
+ refresh_token?: string;
31
+ expires_in?: number;
32
+ token_type?: string;
33
+ }
34
+
35
+ /**
36
+ * Ensure config directory exists
37
+ */
38
+ function ensureConfigDir(): void {
39
+ if (!fs.existsSync(CONFIG_DIR)) {
40
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Load auth configuration from file
46
+ */
47
+ export function loadAuthConfig(): AuthConfig {
48
+ try {
49
+ if (fs.existsSync(CONFIG_FILE)) {
50
+ const data = fs.readFileSync(CONFIG_FILE, 'utf-8');
51
+ return JSON.parse(data);
52
+ }
53
+ } catch (error) {
54
+ console.warn('Failed to load auth config:', error);
55
+ }
56
+ return {};
57
+ }
58
+
59
+ /**
60
+ * Save auth configuration to file
61
+ */
62
+ export function saveAuthConfig(config: AuthConfig): void {
63
+ ensureConfigDir();
64
+ fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
65
+ }
66
+
67
+ /**
68
+ * Store access token securely
69
+ */
70
+ export async function storeAccessToken(token: string): Promise<void> {
71
+ try {
72
+ await keytar.setPassword(SERVICE_NAME, 'access_token', token);
73
+ } catch (error) {
74
+ console.error('Failed to store access token:', error);
75
+ throw new Error('Unable to securely store access token');
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Get stored access token
81
+ */
82
+ export async function getAccessToken(): Promise<string | null> {
83
+ try {
84
+ return await keytar.getPassword(SERVICE_NAME, 'access_token');
85
+ } catch (error) {
86
+ console.error('Failed to retrieve access token:', error);
87
+ return null;
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Store refresh token securely
93
+ */
94
+ export async function storeRefreshToken(token: string): Promise<void> {
95
+ try {
96
+ await keytar.setPassword(SERVICE_NAME, 'refresh_token', token);
97
+ } catch (error) {
98
+ console.error('Failed to store refresh token:', error);
99
+ throw new Error('Unable to securely store refresh token');
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Get stored refresh token
105
+ */
106
+ export async function getRefreshToken(): Promise<string | null> {
107
+ try {
108
+ return await keytar.getPassword(SERVICE_NAME, 'refresh_token');
109
+ } catch (error) {
110
+ console.error('Failed to retrieve refresh token:', error);
111
+ return null;
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Clear all stored credentials
117
+ */
118
+ export async function clearCredentials(): Promise<void> {
119
+ try {
120
+ await keytar.deletePassword(SERVICE_NAME, 'access_token');
121
+ await keytar.deletePassword(SERVICE_NAME, 'refresh_token');
122
+
123
+ if (fs.existsSync(CONFIG_FILE)) {
124
+ fs.unlinkSync(CONFIG_FILE);
125
+ }
126
+ } catch (error) {
127
+ console.error('Failed to clear credentials:', error);
128
+ throw new Error('Unable to clear stored credentials');
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Check if user is authenticated
134
+ */
135
+ export async function isAuthenticated(): Promise<boolean> {
136
+ const token = await getAccessToken();
137
+ return token !== null;
138
+ }
139
+
140
+ /**
141
+ * Validate token expiry (basic check)
142
+ */
143
+ export function isTokenExpired(expiry?: number): boolean {
144
+ if (!expiry) return false;
145
+ return Date.now() >= expiry;
146
+ }
147
+
148
+ /**
149
+ * Get authorization header for API requests
150
+ */
151
+ export async function getAuthHeader(): Promise<string | null> {
152
+ const token = await getAccessToken();
153
+ return token ? `Bearer ${token}` : null;
154
+ }
@@ -0,0 +1,240 @@
1
+ import * as http from 'http';
2
+ import { EventEmitter } from 'events';
3
+ import { randomBytes } from 'crypto';
4
+ import { AuthCallback, AuthServerOptions } from './index';
5
+ import {
6
+ AUTH_BASE_URL,
7
+ AUTHORIZE_ENDPOINT,
8
+ DEFAULT_TIMEOUT,
9
+ PORT_RANGE,
10
+ CALLBACK_PATH,
11
+ SUCCESS_HTML,
12
+ ERROR_HTML,
13
+ TIMEOUT_HTML
14
+ } from './constants';
15
+
16
+ export class AuthServer extends EventEmitter {
17
+ private server?: http.Server;
18
+ private port?: number;
19
+ private state: string;
20
+ private timeoutId?: NodeJS.Timeout;
21
+ private resolveCallback?: (value: AuthCallback) => void;
22
+ private rejectCallback?: (error: Error) => void;
23
+
24
+ constructor(options: AuthServerOptions = {}) {
25
+ super();
26
+ this.state = options.state || this.generateState();
27
+ }
28
+
29
+ /**
30
+ * Generate a random state parameter for CSRF protection
31
+ */
32
+ private generateState(): string {
33
+ return randomBytes(32).toString('hex');
34
+ }
35
+
36
+ /**
37
+ * Find an available port in the configured range
38
+ */
39
+ private async findAvailablePort(): Promise<number> {
40
+ const { min, max } = PORT_RANGE;
41
+
42
+ for (let port = min; port <= max; port++) {
43
+ if (await this.isPortAvailable(port)) {
44
+ return port;
45
+ }
46
+ }
47
+
48
+ throw new Error('No available ports found in range');
49
+ }
50
+
51
+ /**
52
+ * Check if a port is available
53
+ */
54
+ private async isPortAvailable(port: number): Promise<boolean> {
55
+ return new Promise((resolve) => {
56
+ const testServer = http.createServer();
57
+ testServer.listen(port, '127.0.0.1', () => {
58
+ testServer.close(() => resolve(true));
59
+ });
60
+ testServer.on('error', () => resolve(false));
61
+ });
62
+ }
63
+
64
+ /**
65
+ * Handle incoming HTTP requests
66
+ */
67
+ private handleRequest = (req: http.IncomingMessage, res: http.ServerResponse): void => {
68
+ try {
69
+ const fullUrl = `http://localhost:${this.port}${req.url}`;
70
+ const parsedUrl = new URL(fullUrl);
71
+ const pathname = parsedUrl.pathname;
72
+
73
+ // Handle callback endpoint
74
+ if (pathname === CALLBACK_PATH && req.method === 'GET') {
75
+ this.handleCallback(parsedUrl, res);
76
+ return;
77
+ }
78
+
79
+ // Handle any other request with a 404
80
+ res.writeHead(404, { 'Content-Type': 'text/plain' });
81
+ res.end('Not found');
82
+ } catch (error) {
83
+ console.error('Error parsing request URL:', error);
84
+ res.writeHead(400, { 'Content-Type': 'text/plain' });
85
+ res.end('Bad request');
86
+ }
87
+ };
88
+
89
+ /**
90
+ * Handle the OAuth callback
91
+ */
92
+ private handleCallback(parsedUrl: URL, res: http.ServerResponse): void {
93
+ const searchParams = parsedUrl.searchParams;
94
+ const state = searchParams.get('state');
95
+ const token = searchParams.get('token');
96
+ const error = searchParams.get('error');
97
+
98
+ // Validate state parameter
99
+ if (!state || state !== this.state) {
100
+ console.error('State parameter mismatch - possible CSRF attack');
101
+ console.error(`Expected: ${this.state}, Received: ${state}`);
102
+ res.writeHead(400, { 'Content-Type': 'text/html' });
103
+ res.end(ERROR_HTML);
104
+ this.emit('error', new Error('Invalid state parameter'));
105
+ return;
106
+ }
107
+
108
+ // Handle error from authorization server
109
+ if (error) {
110
+ console.error('Authorization error:', error);
111
+ res.writeHead(400, { 'Content-Type': 'text/html' });
112
+ res.end(ERROR_HTML);
113
+ this.emit('error', new Error(`Authorization failed: ${error}`));
114
+ return;
115
+ }
116
+
117
+ // Handle successful authorization
118
+ if (token) {
119
+ const authCallback: AuthCallback = { token };
120
+ res.writeHead(200, { 'Content-Type': 'text/html' });
121
+ res.end(SUCCESS_HTML);
122
+ this.emit('success', authCallback);
123
+ return;
124
+ }
125
+
126
+ // Handle missing token
127
+ console.error('No token received in callback');
128
+ res.writeHead(400, { 'Content-Type': 'text/html' });
129
+ res.end(ERROR_HTML);
130
+ this.emit('error', new Error('No token received'));
131
+ }
132
+
133
+ /**
134
+ * Initialize the server by finding an available port
135
+ */
136
+ async initialize(): Promise<void> {
137
+ if (!this.port) {
138
+ this.port = await this.findAvailablePort();
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Start the authentication server
144
+ */
145
+ async start(): Promise<AuthCallback> {
146
+ return new Promise(async (resolve, reject) => {
147
+ this.resolveCallback = resolve;
148
+ this.rejectCallback = reject;
149
+
150
+ try {
151
+ // Initialize (find port) if not already done
152
+ await this.initialize();
153
+
154
+ // Create server
155
+ this.server = http.createServer(this.handleRequest);
156
+
157
+ // Set up event listeners
158
+ this.on('success', (callback: AuthCallback) => {
159
+ this.cleanup();
160
+ this.resolveCallback!(callback);
161
+ });
162
+
163
+ this.on('error', (error: Error) => {
164
+ this.cleanup();
165
+ this.rejectCallback!(error);
166
+ });
167
+
168
+ // Set timeout
169
+ this.timeoutId = setTimeout(() => {
170
+ this.handleTimeout();
171
+ }, DEFAULT_TIMEOUT);
172
+
173
+ // Start server
174
+ await new Promise<void>((resolveServer, rejectServer) => {
175
+ this.server!.listen(this.port, '127.0.0.1', () => {
176
+ console.log(`🔐 Authentication server started on http://localhost:${this.port}`);
177
+ resolveServer();
178
+ });
179
+ this.server!.on('error', rejectServer);
180
+ });
181
+
182
+ } catch (error) {
183
+ this.cleanup();
184
+ reject(error);
185
+ }
186
+ });
187
+ }
188
+
189
+ /**
190
+ * Handle authentication timeout
191
+ */
192
+ private handleTimeout(): void {
193
+ console.log('⏱️ Authentication timed out');
194
+
195
+ // Send timeout page to any open browser windows
196
+ if (this.server) {
197
+ // Note: In a real implementation, we'd track active connections
198
+ // For now, we'll just emit the error
199
+ }
200
+
201
+ this.emit('error', new Error('Authentication timed out'));
202
+ }
203
+
204
+ /**
205
+ * Get the authorization URL to open in browser
206
+ */
207
+ getAuthorizationUrl(): string {
208
+ const redirectUri = `http://localhost:${this.port}${CALLBACK_PATH}`;
209
+ const params = new URLSearchParams({
210
+ redirect_uri: redirectUri,
211
+ state: this.state,
212
+ });
213
+
214
+ return `${AUTH_BASE_URL}${AUTHORIZE_ENDPOINT}?${params.toString()}`;
215
+ }
216
+
217
+ /**
218
+ * Clean up server resources
219
+ */
220
+ private cleanup(): void {
221
+ if (this.timeoutId) {
222
+ clearTimeout(this.timeoutId);
223
+ this.timeoutId = undefined;
224
+ }
225
+
226
+ if (this.server) {
227
+ this.server.close();
228
+ this.server = undefined;
229
+ }
230
+
231
+ this.removeAllListeners();
232
+ }
233
+
234
+ /**
235
+ * Stop the server
236
+ */
237
+ stop(): void {
238
+ this.cleanup();
239
+ }
240
+ }
@@ -0,0 +1,186 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ import { BlockEntry } from '../registry';
4
+ import { CodeFile } from '../api';
5
+
6
+ /**
7
+ * Convert slug to PascalCase identifier
8
+ */
9
+ export function slugToIdentifier(slug: string): string {
10
+ return slug
11
+ .split('-')
12
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1))
13
+ .join('');
14
+ }
15
+
16
+ /**
17
+ * Convert slug to folder name (direct mapping)
18
+ */
19
+ export function slugToFolderName(slug: string): string {
20
+ return slug;
21
+ }
22
+
23
+ /**
24
+ * Validate that a directory contains a valid block
25
+ */
26
+ export function validateBlockDirectory(dirPath: string): { valid: boolean; errors: string[] } {
27
+ const errors: string[] = [];
28
+
29
+ if (!fs.existsSync(dirPath)) {
30
+ errors.push('Block directory does not exist');
31
+ return { valid: false, errors };
32
+ }
33
+
34
+ const configPath = path.join(dirPath, 'config.ts');
35
+ if (!fs.existsSync(configPath)) {
36
+ errors.push('config.ts file is missing');
37
+ }
38
+
39
+ // Check if config.ts exports a valid block configuration
40
+ if (fs.existsSync(configPath)) {
41
+ try {
42
+ const configContent = fs.readFileSync(configPath, 'utf-8');
43
+
44
+ // Basic validation - check for required exports
45
+ if (!configContent.includes('export') || !configContent.includes('Block')) {
46
+ errors.push('config.ts does not appear to export a valid block configuration');
47
+ }
48
+ } catch (error) {
49
+ errors.push(`Failed to read config.ts: ${(error as Error).message}`);
50
+ }
51
+ }
52
+
53
+ return { valid: errors.length === 0, errors };
54
+ }
55
+
56
+ /**
57
+ * Create block directory structure and write files
58
+ */
59
+ export function createBlockDirectory(
60
+ baseDir: string,
61
+ slug: string,
62
+ files: CodeFile[]
63
+ ): { dir: string; configPath: string; componentPath: string } {
64
+ const blockDir = path.join(baseDir, slugToFolderName(slug));
65
+
66
+ // Check if directory already exists
67
+ if (fs.existsSync(blockDir)) {
68
+ throw new Error(`Block directory already exists: ${blockDir}`);
69
+ }
70
+
71
+ // Create directory
72
+ fs.mkdirSync(blockDir, { recursive: true });
73
+
74
+ let configPath = '';
75
+ let componentPath = '';
76
+
77
+ // Write files
78
+ for (const file of files) {
79
+ const filePath = path.join(blockDir, file.name);
80
+ fs.writeFileSync(filePath, file.content);
81
+
82
+ if (file.name === 'config.ts') {
83
+ configPath = filePath;
84
+ } else if (file.name === 'Component.tsx') {
85
+ componentPath = filePath;
86
+ }
87
+ }
88
+
89
+ if (!configPath) {
90
+ throw new Error('config.ts file was not found in downloaded files');
91
+ }
92
+
93
+ if (!componentPath) {
94
+ throw new Error('Component.tsx file was not found in downloaded files');
95
+ }
96
+
97
+ return { dir: blockDir, configPath, componentPath };
98
+ }
99
+
100
+ /**
101
+ * Remove block directory
102
+ */
103
+ export function removeBlockDirectory(dirPath: string): void {
104
+ if (fs.existsSync(dirPath)) {
105
+ fs.rmSync(dirPath, { recursive: true, force: true });
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Create block entry from metadata and file paths
111
+ */
112
+ export function createBlockEntry(
113
+ metadata: { id: number; name: string; slug: string; sourceUrl: string },
114
+ dir: string,
115
+ configPath: string,
116
+ componentPath: string,
117
+ checksums: { [filename: string]: string }
118
+ ): BlockEntry {
119
+ return {
120
+ id: metadata.id,
121
+ name: metadata.name,
122
+ slug: metadata.slug,
123
+ dir,
124
+ configPath,
125
+ componentPath,
126
+ source: {
127
+ url: metadata.sourceUrl,
128
+ id: metadata.id,
129
+ fetchedAt: new Date().toISOString()
130
+ },
131
+ checksums
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Get all block directories in src/blocks
137
+ */
138
+ export function getBlockDirectories(blocksDir: string = 'src/blocks'): string[] {
139
+ const fullBlocksDir = path.join(process.cwd(), blocksDir);
140
+
141
+ if (!fs.existsSync(fullBlocksDir)) {
142
+ return [];
143
+ }
144
+
145
+ const entries = fs.readdirSync(fullBlocksDir, { withFileTypes: true });
146
+ return entries
147
+ .filter(entry => entry.isDirectory())
148
+ .map(entry => path.join(fullBlocksDir, entry.name));
149
+ }
150
+
151
+ /**
152
+ * Find blocks by scanning filesystem (fallback when registry is unavailable)
153
+ */
154
+ export function discoverBlocksFromFilesystem(): Array<{ slug: string; dir: string; hasConfig: boolean }> {
155
+ const blocksDir = path.join(process.cwd(), 'src/blocks');
156
+
157
+ if (!fs.existsSync(blocksDir)) {
158
+ return [];
159
+ }
160
+
161
+ const blockDirs = getBlockDirectories();
162
+ const blocks: Array<{ slug: string; dir: string; hasConfig: boolean }> = [];
163
+
164
+ for (const dir of blockDirs) {
165
+ const slug = path.basename(dir);
166
+ const configPath = path.join(dir, 'config.ts');
167
+ const hasConfig = fs.existsSync(configPath);
168
+
169
+ blocks.push({ slug, dir, hasConfig });
170
+ }
171
+
172
+ return blocks;
173
+ }
174
+
175
+ /**
176
+ * Ensure src/blocks directory exists
177
+ */
178
+ export function ensureBlocksDirectory(): string {
179
+ const blocksDir = path.join(process.cwd(), 'src/blocks');
180
+
181
+ if (!fs.existsSync(blocksDir)) {
182
+ fs.mkdirSync(blocksDir, { recursive: true });
183
+ }
184
+
185
+ return blocksDir;
186
+ }
package/src/detectors.ts CHANGED
@@ -1,22 +1,22 @@
1
- import { existsSync } from 'fs';
2
- import { join } from 'path';
3
-
4
- export function checkEmptyDirectory(): boolean {
5
- const cwd = process.cwd();
6
-
7
- const pkgPath = join(cwd, 'package.json');
8
- const configJs = join(cwd, 'payload.config.js');
9
- const configTs = join(cwd, 'payload.config.ts');
10
-
11
- if (existsSync(pkgPath)) {
12
- console.error('Error: package.json already exists. Please run in an empty directory.');
13
- return false;
14
- }
15
-
16
- if (existsSync(configJs) || existsSync(configTs)) {
17
- console.error('Error: Payload config file already exists. Please run in an empty directory.');
18
- return false;
19
- }
20
-
21
- return true;
22
- }
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+
4
+ export function checkEmptyDirectory(): boolean {
5
+ const cwd = process.cwd();
6
+
7
+ const pkgPath = join(cwd, 'package.json');
8
+ const configJs = join(cwd, 'payload.config.js');
9
+ const configTs = join(cwd, 'payload.config.ts');
10
+
11
+ if (existsSync(pkgPath)) {
12
+ console.error('Error: package.json already exists. Please run in an empty directory.');
13
+ return false;
14
+ }
15
+
16
+ if (existsSync(configJs) || existsSync(configTs)) {
17
+ console.error('Error: Payload config file already exists. Please run in an empty directory.');
18
+ return false;
19
+ }
20
+
21
+ return true;
22
+ }