@cobinar/dalus 0.1.8 → 0.1.11

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.
Files changed (41) hide show
  1. package/README.md +10 -36
  2. package/bin/{dalus.js → dalus.mjs} +5 -14
  3. package/convert_to_mjs.py +59 -0
  4. package/fix_requires.py +57 -0
  5. package/package.json +4 -4
  6. package/publish-dalus.bat +1 -1
  7. package/src/{api-client.js → api-client.mjs} +67 -67
  8. package/src/commands/{forge.js → forge.mjs} +104 -104
  9. package/src/commands/{init.js → init.mjs} +53 -53
  10. package/src/{forgers/login.js → commands/login.mjs} +288 -288
  11. package/src/{config.js → config.mjs} +147 -147
  12. package/src/cors.mjs +19 -0
  13. package/src/forgers/{database.js → database.mjs} +34 -34
  14. package/src/forgers/{pages.js → pages.mjs} +35 -35
  15. package/src/forgers/storage.mjs +39 -0
  16. package/src/forgers/{vault.js → vault.mjs} +27 -27
  17. package/src/forgers/{workers.js → workers.mjs} +85 -85
  18. package/src/{fs-utils.js → fs-utils.mjs} +36 -36
  19. package/src/handlers/login-callback.mjs +143 -0
  20. package/src/handlers/services.mjs +45 -0
  21. package/src/index.mjs +36 -0
  22. package/src/auth.js +0 -78
  23. package/src/commands/auth.js +0 -78
  24. package/src/commands/cors.js +0 -34
  25. package/src/commands/index.js +0 -115
  26. package/src/commands/login.js +0 -288
  27. package/src/commands/storage.js +0 -72
  28. package/src/commands/utils.js +0 -7
  29. package/src/commands/validators.js +0 -101
  30. package/src/cors.js +0 -34
  31. package/src/forgers/auth.js +0 -78
  32. package/src/forgers/cors.js +0 -34
  33. package/src/forgers/index.js +0 -115
  34. package/src/forgers/storage.js +0 -72
  35. package/src/forgers/utils.js +0 -7
  36. package/src/forgers/validators.js +0 -101
  37. package/src/index.js +0 -115
  38. package/src/login.js +0 -288
  39. package/src/storage.js +0 -72
  40. package/src/utils.js +0 -7
  41. package/src/validators.js +0 -101
package/src/login.js DELETED
@@ -1,288 +0,0 @@
1
- 'use strict';
2
- const http = require('http');
3
- const crypto = require('crypto');
4
- const readline = require('readline');
5
- const { execFile } = require('child_process');
6
- const { saveCredentials, clearCredentials, loadCredentials, CREDENTIALS_PATH } = require('../config');
7
-
8
- // Where dalus's OWN worker (the login page this opens) currently lives is
9
- // looked up here, not hardcoded — see cobinar's src/handlers/services.js.
10
- // That's the one, deliberately-fixed anchor point everything else in this
11
- // file is relative to; if IT ever needs to move, that's a cobinar.com
12
- // redeploy, not a new npm release everyone has to go update.
13
- const SERVICES_ENDPOINT = 'https://cobinar.com/api/services';
14
- // Only used if the discovery call above fails outright (a network hiccup,
15
- // cobinar.com briefly unreachable) -- not the source of truth, just keeps
16
- // `dalus login` from being completely dead during a transient outage of
17
- // that one lookup. --web-base always wins over both.
18
- const FALLBACK_DALUS_BASE = 'https://8bnk.dalus.cobinar.com';
19
- // cobinar-developers-worker — mints the actual bearer token dashboard-worker
20
- // checks (a signed "workerToken"), from the one-time code dalus's own
21
- // worker hands us. Not the same thing as dalus.cobinar.com itself or as
22
- // auth.cobinar.com (Cobinar's general sign-in provider, which dalus's
23
- // worker is one client of) — those are three separate services. See the
24
- // comment on exchangeSsoCode below for the exact handoff.
25
- const DEFAULT_DEVELOPERS_BASE = 'https://worker.dashboard.cobinar.com';
26
- const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
27
-
28
- function ask(question) {
29
- return new Promise((resolve) => {
30
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
31
- rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); });
32
- });
33
- }
34
-
35
- function openBrowser(url) {
36
- // execFile, not exec — no shell involved for the process WE spawn. That
37
- // alone isn't quite enough on Windows: `cmd /c start` hands its arguments
38
- // to cmd.exe's OWN re-parsing of the whole line as a new command, where an
39
- // unescaped "&" means "run two commands", not "a literal character in a
40
- // URL" — which is exactly what truncated a real sign-in URL at its first
41
- // "&" here before. rundll32 is a normal Win32 program, not a shell, so it
42
- // never re-interprets "&" (or anything else) in its argument at all.
43
- const done = () => {}; // best-effort; the URL is always printed too
44
- if (process.platform === 'darwin') execFile('open', [url], done);
45
- else if (process.platform === 'win32') execFile('rundll32', ['url.dll,FileProtocolHandler', url], done);
46
- else execFile('xdg-open', [url], done);
47
- }
48
-
49
- const REQUEST_TIMEOUT_MS = 8000;
50
-
51
- function getJson(urlString) {
52
- return new Promise((resolve, reject) => {
53
- const url = new URL(urlString);
54
- const req = require(url.protocol === 'http:' ? 'http' : 'https')
55
- .get(url, (res) => {
56
- let out = '';
57
- res.on('data', (c) => (out += c));
58
- res.on('end', () => {
59
- let parsed = null;
60
- try { parsed = JSON.parse(out); } catch { /* leaves parsed null below */ }
61
- resolve({ status: res.statusCode, json: parsed });
62
- });
63
- })
64
- .on('error', reject);
65
- req.setTimeout(REQUEST_TIMEOUT_MS, () => req.destroy(new Error('Request timed out')));
66
- });
67
- }
68
-
69
- // The one lookup this whole file depends on: where does dalus's own worker
70
- // (the login page browserLogin below sends people to) currently live. See
71
- // SERVICES_ENDPOINT's comment above for why this is a runtime fetch and not
72
- // a constant.
73
- async function discoverDalusBase() {
74
- try {
75
- const { status, json } = await getJson(SERVICES_ENDPOINT);
76
- if (status === 200 && json && typeof json.dalus === 'string' && json.dalus) {
77
- return json.dalus;
78
- }
79
- } catch {
80
- // Falls through to FALLBACK_DALUS_BASE below.
81
- }
82
- return FALLBACK_DALUS_BASE;
83
- }
84
-
85
- function postJson(urlString, body) {
86
- return new Promise((resolve, reject) => {
87
- const url = new URL(urlString);
88
- const data = JSON.stringify(body);
89
- const req = require(url.protocol === 'http:' ? 'http' : 'https').request(
90
- {
91
- hostname: url.hostname,
92
- port: url.port || (url.protocol === 'http:' ? 80 : 443),
93
- path: url.pathname + url.search,
94
- method: 'POST',
95
- headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) },
96
- },
97
- (res) => {
98
- let out = '';
99
- res.on('data', (c) => (out += c));
100
- res.on('end', () => {
101
- let parsed = null;
102
- try { parsed = JSON.parse(out); } catch { /* leaves parsed null below */ }
103
- resolve({ status: res.statusCode, json: parsed });
104
- });
105
- },
106
- );
107
- req.on('error', reject);
108
- req.setTimeout(REQUEST_TIMEOUT_MS, () => req.destroy(new Error('Request timed out')));
109
- req.write(data);
110
- req.end();
111
- });
112
- }
113
-
114
- // The other half of cobinar-developers-worker's POST /auth/redeem-code:
115
- // cobinar.com's callback page writes { uid, email, name, picture } to a KV
116
- // namespace it shares with cobinar-developers-worker, keyed by a one-time
117
- // code, and hands US that code (over the loopback server below, once the
118
- // browser gets there). Redeeming it here — not on cobinar.com's server, and
119
- // not in the browser — is what actually mints the workerToken dashboard-
120
- // worker will accept; nothing before this point has needed dashboard-worker
121
- // to trust anything, since a workerToken is the first credential in this
122
- // whole flow it actually checks.
123
- async function exchangeSsoCode(developersBase, code) {
124
- const { status, json } = await postJson(`${developersBase}/auth/redeem-code`, { code });
125
- if (status !== 200 || !json || typeof json.workerToken !== 'string') {
126
- const detail = json && json.error ? json.error : `HTTP ${status}`;
127
- throw new Error(`Could not finish signing in (${detail}). Run "dalus login" again.`);
128
- }
129
- return json;
130
- }
131
-
132
- // Starts a one-shot local server, opens dalus's own worker (wherever
133
- // discoverDalusBase() says it currently lives) to sign in, and waits for
134
- // its /login/callback page to POST a one-time SSO code back here once the
135
- // auth.cobinar.com round trip finishes. dalus's worker has its own
136
- // client_id/secret, registered independently of cobinar.com's — see that
137
- // worker's public/login/index.html and public/login/callback.html for the
138
- // other half of this handshake, and exchangeSsoCode above for what happens
139
- // to the code once it arrives here.
140
- function browserLogin(webBase, developersBase) {
141
- // "ls" (local state): proves the browser tab that finishes is the one
142
- // THIS process opened, not some other local process guessing the port
143
- // and racing it — the port alone isn't a secret (any local process can
144
- // see what's listening), this is.
145
- const ls = crypto.randomBytes(24).toString('hex');
146
- const allowedOrigin = new URL(webBase).origin;
147
-
148
- return new Promise((resolve, reject) => {
149
- let settled = false;
150
-
151
- const server = http.createServer((req, res) => {
152
- const url = new URL(req.url, 'http://127.0.0.1');
153
- res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
154
- res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS');
155
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
156
-
157
- if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
158
- if (url.pathname !== '/complete' || req.method !== 'POST') { res.writeHead(404); res.end(); return; }
159
-
160
- let body = '';
161
- req.on('data', (chunk) => {
162
- body += chunk;
163
- if (body.length > 1e6) req.destroy(); // this payload is a short code + a small profile, never anywhere near this size
164
- });
165
- req.on('end', async () => {
166
- let parsed = null;
167
- try { parsed = JSON.parse(body); } catch { /* falls through to the ok:false below */ }
168
-
169
- if (!parsed || parsed.ls !== ls || typeof parsed.code !== 'string' || !parsed.code) {
170
- res.writeHead(400, { 'Content-Type': 'application/json' });
171
- res.end(JSON.stringify({ ok: false }));
172
- return; // deliberately not finish()/reject() here — a stray or malicious request to this port shouldn't cancel a login still in progress
173
- }
174
-
175
- try {
176
- const redeemed = await exchangeSsoCode(developersBase, parsed.code);
177
- res.writeHead(200, { 'Content-Type': 'application/json' });
178
- res.end(JSON.stringify({ ok: true }));
179
- finish(null, {
180
- token: redeemed.workerToken,
181
- user: parsed.user || { email: redeemed.cobinarEmail, name: redeemed.displayName, picture: redeemed.photoURL },
182
- });
183
- } catch (err) {
184
- res.writeHead(502, { 'Content-Type': 'application/json' });
185
- res.end(JSON.stringify({ ok: false, error: err.message }));
186
- finish(err);
187
- }
188
- });
189
- });
190
-
191
- function finish(err, result) {
192
- if (settled) return;
193
- settled = true;
194
- clearTimeout(timer);
195
- server.close();
196
- if (err) reject(err); else resolve(result);
197
- }
198
-
199
- const timer = setTimeout(() => {
200
- finish(new Error(`Timed out waiting for sign-in (${LOGIN_TIMEOUT_MS / 1000}s). Run "dalus login" again.`));
201
- }, LOGIN_TIMEOUT_MS);
202
-
203
- server.on('error', (err) => finish(err));
204
-
205
- server.listen(0, '127.0.0.1', () => {
206
- const port = server.address().port;
207
- // port and ls travel together as one opaque value, not two params
208
- // joined by "&" — see openBrowser's comment above for exactly why
209
- // that character caused real trouble here. Neither value can ever
210
- // contain a "." (port is digits, ls is hex), so joining/splitting on
211
- // it is unambiguous. The trailing slash on /login/ is deliberate
212
- // too: requesting the bare path and hoping a redirect adds the slash
213
- // back is one more hop that could, in principle, drop the query
214
- // string — this just starts at the real address.
215
- const loginUrl = `${webBase}/login/?d=${port}.${ls}`;
216
- console.log('Opening your browser to sign in with Cobinar...');
217
- console.log(`If it doesn't open automatically, visit:\n ${loginUrl}\n`);
218
- openBrowser(loginUrl);
219
- });
220
- });
221
- }
222
-
223
- async function loginCommand(args) {
224
- if (args.includes('--logout')) {
225
- clearCredentials();
226
- console.log('Logged out.');
227
- return;
228
- }
229
- if (args.includes('--whoami')) {
230
- const creds = loadCredentials();
231
- if (!creds) { console.log('Not logged in.'); return; }
232
- console.log(`API base: ${creds.apiBase}`);
233
- if (creds.user && creds.user.email) console.log(`Signed in as: ${creds.user.email}`);
234
- console.log(`Token: ${creds.token.slice(0, 8)}...${creds.token.slice(-4)} (stored in ${CREDENTIALS_PATH})`);
235
- console.log('Note: this token expires 2 hours after signing in — "dalus login" again once it does.');
236
- return;
237
- }
238
-
239
- const tokenArgIdx = args.indexOf('--token');
240
- const apiBaseArgIdx = args.indexOf('--api-base');
241
-
242
- // --token stays as a manual escape hatch for CI/headless boxes that
243
- // can't open a browser at all — not the default anymore, but not worth
244
- // removing until there's a real headless flow (one-time code / email,
245
- // planned later) to replace it with.
246
- if (tokenArgIdx !== -1) {
247
- const token = args[tokenArgIdx + 1];
248
- const apiBase = apiBaseArgIdx !== -1 ? args[apiBaseArgIdx + 1] : await ask('Cobinar dashboard-worker URL: ');
249
- if (!token || !apiBase) {
250
- console.error('Both a worker URL and a token are required.');
251
- process.exitCode = 1;
252
- return;
253
- }
254
- saveCredentials({ apiBase: apiBase.replace(/\/$/, ''), token });
255
- console.log(`Saved to ${CREDENTIALS_PATH}. Run "dalus forge" from a project directory to deploy.`);
256
- return;
257
- }
258
-
259
- const webBaseArgIdx = args.indexOf('--web-base');
260
- const webBase = (webBaseArgIdx !== -1 ? args[webBaseArgIdx + 1] : await discoverDalusBase()).replace(/\/$/, '');
261
- const developersBaseArgIdx = args.indexOf('--developers-base');
262
- const developersBase = (developersBaseArgIdx !== -1 ? args[developersBaseArgIdx + 1] : DEFAULT_DEVELOPERS_BASE).replace(/\/$/, '');
263
-
264
- let result;
265
- try {
266
- result = await browserLogin(webBase, developersBase);
267
- } catch (err) {
268
- console.error(err.message);
269
- process.exitCode = 1;
270
- return;
271
- }
272
-
273
- const apiBase = apiBaseArgIdx !== -1
274
- ? args[apiBaseArgIdx + 1]
275
- : await ask('Cobinar dashboard-worker URL (e.g. https://worker.lobby.cobinar.com): ');
276
- if (!apiBase) {
277
- console.error('A dashboard-worker URL is required.');
278
- process.exitCode = 1;
279
- return;
280
- }
281
-
282
- saveCredentials({ apiBase: apiBase.replace(/\/$/, ''), token: result.token, user: result.user || null });
283
- console.log(`Signed in${result.user && result.user.email ? ' as ' + result.user.email : ''}. Saved to ${CREDENTIALS_PATH}.`);
284
- console.log('This session lasts 2 hours — "dalus login" again once it expires.');
285
- console.log('Run "dalus forge" from a project directory to deploy.');
286
- }
287
-
288
- module.exports = { loginCommand, browserLogin, exchangeSsoCode, discoverDalusBase };
package/src/storage.js DELETED
@@ -1,72 +0,0 @@
1
- // src/storage.js
2
- import { generateId } from './utils.js';
3
-
4
- const FOLDER_COLLECTIONS = new Set(['posts', 'products', 'projects']);
5
- const FLAT_COLLECTIONS = new Set(['images', 'files']);
6
- export const ALL_COLLECTIONS = new Set([...FOLDER_COLLECTIONS, ...FLAT_COLLECTIONS]);
7
- export { FOLDER_COLLECTIONS, FLAT_COLLECTIONS };
8
-
9
- export function generateUploadPath(collection, entityId, ext, slot) {
10
- const uid = generateId();
11
- if (FLAT_COLLECTIONS.has(collection)) {
12
- return `cobinar/${collection}/${uid}.${ext.toLowerCase()}`;
13
- }
14
- const prefix = slot ? `${slot}-${uid}` : uid;
15
- return `cobinar/${collection}/${entityId}/${prefix}.${ext.toLowerCase()}`;
16
- }
17
-
18
- export async function uploadFile(bucket, key, body, contentType, metadata = {}) {
19
- await bucket.put(key, body, {
20
- httpMetadata: {
21
- contentType,
22
- cacheControl: 'public, max-age=31536000, immutable',
23
- },
24
- customMetadata: {
25
- uploadedAt: new Date().toISOString(),
26
- ...sanitizeMetadata(metadata),
27
- },
28
- });
29
- return { key };
30
- }
31
-
32
- export async function deleteFiles(bucket, keys) {
33
- if (!keys || keys.length === 0) return;
34
- await bucket.delete(keys);
35
- }
36
-
37
- export async function streamFile(bucket, key, extraHeaders = {}) {
38
- const object = await bucket.get(key);
39
- if (!object) {
40
- return new Response(JSON.stringify({ ok: false, error: 'File not found' }), {
41
- status: 404,
42
- headers: { 'Content-Type': 'application/json' },
43
- });
44
- }
45
- const headers = new Headers({
46
- 'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',
47
- 'Cache-Control': 'public, max-age=31536000, immutable',
48
- ETag: object.etag,
49
- 'Last-Modified': object.uploaded?.toUTCString() || '',
50
- 'X-Served-By': 'cobinar-r2',
51
- ...extraHeaders,
52
- });
53
- return new Response(object.body, { headers });
54
- }
55
-
56
- export function getPublicUrl(key, env) {
57
- if (env.R2_PUBLIC_DOMAIN) {
58
- return `https://${env.R2_PUBLIC_DOMAIN}/${key}`;
59
- }
60
- const domain = env.WORKER_DOMAIN || 'cobinar-r2.workers.dev';
61
- return `https://${domain}/assets/${key}`;
62
- }
63
-
64
- function sanitizeMetadata(obj) {
65
- const out = {};
66
- for (const [k, v] of Object.entries(obj)) {
67
- if (v !== null && v !== undefined) {
68
- out[k] = String(v);
69
- }
70
- }
71
- return out;
72
- }
package/src/utils.js DELETED
@@ -1,7 +0,0 @@
1
- // src/utils.js
2
-
3
- export function generateId() {
4
- const buf = new Uint8Array(8);
5
- crypto.getRandomValues(buf);
6
- return Array.from(buf, (b) => b.toString(16).padStart(2, '0')).join('').slice(0, 12);
7
- }
package/src/validators.js DELETED
@@ -1,101 +0,0 @@
1
- // src/validators.js
2
-
3
- export const ALLOWED_TYPES = {
4
- image: {
5
- mimes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/avif', 'image/svg+xml'],
6
- extensions: ['jpg', 'jpeg', 'png', 'webp', 'gif', 'avif', 'svg'],
7
- maxBytes: 25 * 1024 * 1024,
8
- label: 'Image',
9
- },
10
- projectImage: {
11
- mimes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif'],
12
- extensions: ['jpg', 'jpeg', 'png', 'webp', 'avif'],
13
- maxBytes: 10 * 1024 * 1024,
14
- label: 'Project image',
15
- },
16
- productImage: {
17
- mimes: ['image/jpeg', 'image/png', 'image/webp', 'image/avif'],
18
- extensions: ['jpg', 'jpeg', 'png', 'webp', 'avif'],
19
- maxBytes: 10 * 1024 * 1024,
20
- label: 'Product image',
21
- },
22
- postMedia: {
23
- mimes: [
24
- 'image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/avif',
25
- 'video/mp4', 'video/webm', 'video/quicktime', 'application/pdf',
26
- ],
27
- extensions: ['jpg', 'jpeg', 'png', 'webp', 'gif', 'avif', 'mp4', 'webm', 'mov', 'pdf'],
28
- maxBytes: 50 * 1024 * 1024,
29
- label: 'Post media',
30
- },
31
- file: {
32
- mimes: [
33
- 'application/pdf', 'application/zip', 'application/x-zip-compressed',
34
- 'application/octet-stream', 'text/plain', 'text/csv',
35
- 'application/vnd.ms-excel',
36
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
37
- 'application/msword',
38
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
39
- 'application/json',
40
- ],
41
- extensions: ['pdf', 'zip', 'txt', 'csv', 'xls', 'xlsx', 'doc', 'docx', 'json', 'bin'],
42
- maxBytes: 100 * 1024 * 1024,
43
- label: 'File',
44
- },
45
- };
46
-
47
- export const DEFAULT_RULE = {
48
- posts: 'postMedia',
49
- products: 'productImage',
50
- projects: 'projectImage',
51
- images: 'image',
52
- files: 'file',
53
- };
54
-
55
- export function validateFile(mimeType, sizeBytes, filename, ruleName) {
56
- const rule = ALLOWED_TYPES[ruleName];
57
- if (!rule) return { valid: false, error: `Unknown validation rule: "${ruleName}"` };
58
-
59
- const baseMime = mimeType.split(';')[0].trim().toLowerCase();
60
- if (!rule.mimes.includes(baseMime)) {
61
- return { valid: false, error: `${rule.label} must be one of: ${rule.mimes.join(', ')}. Got: ${baseMime}` };
62
- }
63
-
64
- const rawExt = (filename.split('.').pop() || '').toLowerCase();
65
- if (!rule.extensions.includes(rawExt)) {
66
- return { valid: false, error: `${rule.label} extension must be .${rule.extensions.join(', .')}. Got: .${rawExt}` };
67
- }
68
-
69
- if (sizeBytes > rule.maxBytes) {
70
- const maxMb = (rule.maxBytes / 1024 / 1024).toFixed(0);
71
- const gotMb = (sizeBytes / 1024 / 1024).toFixed(1);
72
- return { valid: false, error: `${rule.label} must be under ${maxMb} MB. Got: ${gotMb} MB` };
73
- }
74
-
75
- return { valid: true, ext: rawExt };
76
- }
77
-
78
- export function extFromMime(mimeType) {
79
- const map = {
80
- 'image/jpeg': 'jpg',
81
- 'image/png': 'png',
82
- 'image/webp': 'webp',
83
- 'image/gif': 'gif',
84
- 'image/avif': 'avif',
85
- 'image/svg+xml': 'svg',
86
- 'video/mp4': 'mp4',
87
- 'video/webm': 'webm',
88
- 'video/quicktime': 'mov',
89
- 'application/pdf': 'pdf',
90
- 'application/zip': 'zip',
91
- 'application/x-zip-compressed': 'zip',
92
- 'text/plain': 'txt',
93
- 'text/csv': 'csv',
94
- 'application/vnd.ms-excel': 'xls',
95
- 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': 'xlsx',
96
- 'application/msword': 'doc',
97
- 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': 'docx',
98
- 'application/json': 'json',
99
- };
100
- return map[mimeType.split(';')[0].trim().toLowerCase()] || 'bin';
101
- }