@realtimex/realtimex-alchemy 1.0.74 → 1.0.76
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/dist/CHANGELOG.md +19 -0
- package/dist/api/index.js +162 -0
- package/dist/assets/index-B20f5cpj.js +180 -0
- package/dist/assets/{index-CT_DMn9k.css → index-DQzcu1YR.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/scripts/migrate.sh +26 -2
- package/dist/assets/index-DG7qPboL.js +0 -177
package/dist/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.76] - 2026-01-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Localization**: Added full localization support for the new "Zero-Config" setup flow. All setup steps, including project selection and provisioning logs, are now available in supported languages.
|
|
12
|
+
|
|
13
|
+
### Improved
|
|
14
|
+
- **Setup Wizard**: Replaced hardcoded strings in the setup wizard with translation keys to ensure a consistent localized experience.
|
|
15
|
+
|
|
16
|
+
## [1.0.75] - 2026-01-29
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- **Onboarding**: Implemented "Zero-Config Cloud Provisioning". Users can now create and configure a brand new Supabase project directly from the Setup Wizard using just their Personal Access Token.
|
|
20
|
+
- **Setup Wizard**: Added a new "Quick Launch" workflow that automates project creation, API key retrieval, and database initialization.
|
|
21
|
+
- **API**: Added `/api/setup/auto-provision` endpoint that handles the full lifecycle of Supabase project creation (Create -> Poll -> Configure -> DNS Wait).
|
|
22
|
+
|
|
23
|
+
### Improved
|
|
24
|
+
- **Migration**: Updated `migrate.sh` to support "Docker-less" Edge Function deployment via the Supabase Cloud API (`--use-api`), significantly reducing local dependency requirements.
|
|
25
|
+
- **Migration**: Added automatic `TOKEN_ENCRYPTION_KEY` generation during migration to ensure secure secret management out of the box.
|
|
26
|
+
|
|
8
27
|
## [1.0.74] - 2026-01-29
|
|
9
28
|
|
|
10
29
|
### Added
|
package/dist/api/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
+
import axios from 'axios';
|
|
2
3
|
import cors from 'cors';
|
|
4
|
+
import crypto from 'crypto';
|
|
3
5
|
import path from 'path';
|
|
4
6
|
import { fileURLToPath } from 'url';
|
|
5
7
|
import { spawn } from 'child_process';
|
|
@@ -127,6 +129,166 @@ app.post('/api/migrate', (req, res) => {
|
|
|
127
129
|
console.log('[Migrate] Client disconnected, but migration will continue');
|
|
128
130
|
});
|
|
129
131
|
});
|
|
132
|
+
// GET /api/setup/organizations - List Supabase organizations
|
|
133
|
+
app.get('/api/setup/organizations', async (req, res) => {
|
|
134
|
+
const authHeader = req.headers['authorization'];
|
|
135
|
+
if (!authHeader) {
|
|
136
|
+
return res.status(401).json({ error: 'Missing Authorization header' });
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const response = await axios.get('https://api.supabase.com/v1/organizations', {
|
|
140
|
+
headers: { 'Authorization': authHeader }
|
|
141
|
+
});
|
|
142
|
+
res.json(response.data);
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
console.error('[Setup] Failed to fetch organizations:', error.response?.data || error.message);
|
|
146
|
+
res.status(error.response?.status || 500).json({
|
|
147
|
+
error: error.response?.data?.message || 'Failed to fetch organizations'
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
// POST /api/setup/auto-provision - Create project and poll for readiness (SSE)
|
|
152
|
+
app.post('/api/setup/auto-provision', async (req, res) => {
|
|
153
|
+
const { orgId, projectName: customProjectName, region: customRegion } = req.body;
|
|
154
|
+
const authHeader = req.headers['authorization'];
|
|
155
|
+
if (!orgId) {
|
|
156
|
+
return res.status(400).json({ error: 'Missing required parameter (orgId)' });
|
|
157
|
+
}
|
|
158
|
+
if (!authHeader) {
|
|
159
|
+
return res.status(401).json({ error: 'Missing Authorization header' });
|
|
160
|
+
}
|
|
161
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
162
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
163
|
+
res.setHeader('Connection', 'keep-alive');
|
|
164
|
+
const sendEvent = (type, data) => {
|
|
165
|
+
res.write(`data: ${JSON.stringify({ type, data })}\n\n`);
|
|
166
|
+
};
|
|
167
|
+
try {
|
|
168
|
+
const projectName = customProjectName || `Alchemy-${crypto.randomBytes(2).toString('hex')}`;
|
|
169
|
+
const region = customRegion || 'us-east-1';
|
|
170
|
+
// Generate a secure DB password server-side
|
|
171
|
+
const dbPass = crypto.randomBytes(16).toString('base64')
|
|
172
|
+
.replace(/\+/g, 'a')
|
|
173
|
+
.replace(/\//g, 'b')
|
|
174
|
+
.replace(/=/g, 'c') + '1!Aa';
|
|
175
|
+
sendEvent('info', `🚀 Creating Supabase project: ${projectName} in ${region}...`);
|
|
176
|
+
// 1. Create Project
|
|
177
|
+
const createResponse = await axios.post('https://api.supabase.com/v1/projects', {
|
|
178
|
+
name: projectName,
|
|
179
|
+
organization_id: orgId,
|
|
180
|
+
region: region,
|
|
181
|
+
db_pass: dbPass
|
|
182
|
+
}, {
|
|
183
|
+
headers: { 'Authorization': authHeader },
|
|
184
|
+
timeout: 15000 // 15s timeout for creation
|
|
185
|
+
});
|
|
186
|
+
const project = createResponse.data;
|
|
187
|
+
const projectRef = project.id;
|
|
188
|
+
sendEvent('info', `📦 Project created! ID: ${projectRef}. Waiting for it to go live...`);
|
|
189
|
+
sendEvent('project_id', projectRef);
|
|
190
|
+
// 2. Poll for Readiness
|
|
191
|
+
let isReady = false;
|
|
192
|
+
let attempts = 0;
|
|
193
|
+
const maxAttempts = 60; // 5 minutes (5s interval)
|
|
194
|
+
while (!isReady && attempts < maxAttempts) {
|
|
195
|
+
attempts++;
|
|
196
|
+
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
197
|
+
try {
|
|
198
|
+
const statusResponse = await axios.get(`https://api.supabase.com/v1/projects/${projectRef}`, {
|
|
199
|
+
headers: { 'Authorization': authHeader },
|
|
200
|
+
timeout: 10000 // 10s timeout for status check
|
|
201
|
+
});
|
|
202
|
+
const status = statusResponse.data.status;
|
|
203
|
+
sendEvent('info', `⏳ Status: ${status} (Attempt ${attempts}/${maxAttempts})`);
|
|
204
|
+
if (status === 'ACTIVE_HEALTHY' || status === 'ACTIVE') {
|
|
205
|
+
isReady = true;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch (pollError) {
|
|
209
|
+
console.warn('[Setup] Polling error:', pollError.message);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (!isReady) {
|
|
213
|
+
throw new Error('Project provision timed out after 5 minutes.');
|
|
214
|
+
}
|
|
215
|
+
// 3. Get API Keys
|
|
216
|
+
sendEvent('info', '🔑 Retrieving API keys...');
|
|
217
|
+
let anonKey = '';
|
|
218
|
+
let keyAttempts = 0;
|
|
219
|
+
const maxKeyAttempts = 10;
|
|
220
|
+
while (!anonKey && keyAttempts < maxKeyAttempts) {
|
|
221
|
+
keyAttempts++;
|
|
222
|
+
if (keyAttempts > 1) {
|
|
223
|
+
sendEvent('info', `⏳ API keys not ready yet. Retrying (Attempt ${keyAttempts}/${maxKeyAttempts})...`);
|
|
224
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const keysResponse = await axios.get(`https://api.supabase.com/v1/projects/${projectRef}/api-keys`, {
|
|
228
|
+
headers: { 'Authorization': authHeader },
|
|
229
|
+
timeout: 10000 // 10s timeout for key retrieval
|
|
230
|
+
});
|
|
231
|
+
const keys = keysResponse.data;
|
|
232
|
+
if (Array.isArray(keys)) {
|
|
233
|
+
anonKey = keys.find((k) => k.name === 'anon')?.api_key;
|
|
234
|
+
if (anonKey) {
|
|
235
|
+
sendEvent('info', '✅ API keys retrieved successfully.');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
const errorMsg = err.response?.data?.message || err.message;
|
|
241
|
+
console.warn(`[Setup] Key retrieval attempt ${keyAttempts} failed:`, errorMsg);
|
|
242
|
+
sendEvent('info', `⚠️ Attempt ${keyAttempts} failed: ${errorMsg}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!anonKey) {
|
|
246
|
+
throw new Error('Could not find anonymous API key for the new project after several attempts.');
|
|
247
|
+
}
|
|
248
|
+
const supabaseUrl = `https://${projectRef}.supabase.co`;
|
|
249
|
+
// 4. Verification: Wait for DNS to propagate (Cloudflare/Supabase propagation)
|
|
250
|
+
sendEvent('info', '🌐 Waiting for DNS propagation (Supabase/Cloudflare)...');
|
|
251
|
+
let dnsReady = false;
|
|
252
|
+
let dnsAttempts = 0;
|
|
253
|
+
const maxDnsAttempts = 20;
|
|
254
|
+
while (!dnsReady && dnsAttempts < maxDnsAttempts) {
|
|
255
|
+
dnsAttempts++;
|
|
256
|
+
try {
|
|
257
|
+
// Try to ping the REST endpoint
|
|
258
|
+
const pingResponse = await axios.get(`${supabaseUrl}/rest/v1/`, {
|
|
259
|
+
timeout: 5000,
|
|
260
|
+
validateStatus: () => true // Accept any status (even 401/404) as DNS is resolved
|
|
261
|
+
});
|
|
262
|
+
if (pingResponse.status < 500) {
|
|
263
|
+
dnsReady = true;
|
|
264
|
+
sendEvent('info', '✨ DNS resolved! Project is fully accessible.');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
catch (pingError) {
|
|
268
|
+
console.log(`[Setup] DNS poll ${dnsAttempts}:`, pingError.message);
|
|
269
|
+
if (dnsAttempts % 5 === 0) {
|
|
270
|
+
sendEvent('info', '⏳ DNS still propagating... standby.');
|
|
271
|
+
}
|
|
272
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
sendEvent('success', {
|
|
276
|
+
url: supabaseUrl,
|
|
277
|
+
anonKey: anonKey,
|
|
278
|
+
projectId: projectRef,
|
|
279
|
+
dbPass: dbPass // Password generated on line 202
|
|
280
|
+
});
|
|
281
|
+
sendEvent('done', 'success');
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
console.error('[Setup] Auto-provision failed:', error.response?.data || error.message);
|
|
285
|
+
sendEvent('error', error.response?.data?.message || error.message || 'Auto-provisioning failed');
|
|
286
|
+
sendEvent('done', 'failed');
|
|
287
|
+
}
|
|
288
|
+
finally {
|
|
289
|
+
res.end();
|
|
290
|
+
}
|
|
291
|
+
});
|
|
130
292
|
// SSE Events
|
|
131
293
|
app.get('/events', (req, res) => {
|
|
132
294
|
res.setHeader('Content-Type', 'text/event-stream');
|