@cobinar/dalus 0.1.8 → 0.1.10

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/README.md CHANGED
@@ -18,53 +18,27 @@ relying on this day to day.
18
18
 
19
19
  ## Install
20
20
 
21
- ```
22
- npm install -g @cobinar/dalus
23
- ```
24
-
25
- Or from source, for working on dalus itself:
26
-
27
21
  ```
28
22
  cd dalus
29
23
  npm install
30
24
  npm link # makes the `dalus` command available globally
31
25
  ```
32
26
 
27
+ (Not published to npm — this is source you own and can change.)
28
+
33
29
  ## Log in
34
30
 
35
31
  ```
36
- dalus login
32
+ dalus login --api-base https://worker.lobby.cobinar.com --token <your bearer token>
37
33
  ```
38
34
 
39
- Opens your browser to sign in with Cobinar (the same sign-in every
40
- cobinar.com visitor uses dalus doesn't have, and doesn't need, its own
41
- separate login), then stores what it gets back in
42
- `~/.dalus/credentials.json` (mode 0600), never inside a project
43
- directory. You'll also be asked for your dashboard-worker URL the first
44
- time `dalus login --api-base <url>` to skip that prompt.
45
-
46
- Mechanically: dalus starts a one-shot local server on an ephemeral port,
47
- opens `https://cobinar.com/cli-login?port=<port>&ls=<random>`, and waits.
48
- That page runs the exact same auth.cobinar.com round trip as a normal
49
- sign-in; once it succeeds, cobinar.com's own `/auth/callback` page POSTs a
50
- short-lived, single-use code to dalus's local server (proving it's
51
- genuinely that page, not some other local process, via the `ls` value).
52
- dalus then redeems that code itself, directly against
53
- `cobinar-developers-worker`'s `/auth/redeem-code` (override with
54
- `--developers-base` for staging), which is what actually mints the
55
- signed token dashboard-worker checks — cobinar.com's callback never
56
- holds or sees that token, only the one-time code. No separate OAuth
57
- client, no manually copying anything out of a browser session.
58
-
59
- The stored token lasts 2 hours (`cobinar-developers-worker` sets that
60
- expiry) — `dalus login` again once a command starts failing with a
61
- session-expired error.
62
-
63
- `--token <token> [--api-base <url>]` is kept as a manual fallback for a
64
- CI runner or a headless box with no browser to open — get a token some
65
- other way and hand it to dalus directly, the same way `gh auth login
66
- --with-token` works. A real headless flow (a one-time code, email
67
- verification) is planned but not built yet.
35
+ dalus doesn't implement Cobinar's own signup/login flow that lives in
36
+ `cobinar-developers-worker`, a separate system. This command stores a
37
+ token you've already obtained from wherever that flow gives you one, the
38
+ same way `gh auth login --with-token` accepts a token instead of
39
+ reimplementing GitHub's own login. Omit `--api-base`/`--token` to be
40
+ prompted instead. Credentials are stored in `~/.dalus/credentials.json`
41
+ (mode 0600), never inside a project directory.
68
42
 
69
43
  `dalus login --whoami` shows what's currently stored (token
70
44
  redacted). `dalus login --logout` clears it.
package/bin/dalus.js CHANGED
@@ -8,18 +8,13 @@ const { initCommand } = require('../src/commands/init');
8
8
  const HELP = `dalus — deploy Cobinar projects from the command line
9
9
 
10
10
  Usage:
11
- dalus login [--web-base <url>] [--api-base <url>]
12
- dalus login --token <token> [--api-base <url>]
11
+ dalus login [--api-base <url>] [--token <token>]
13
12
  dalus login --whoami
14
13
  dalus login --logout
15
14
  dalus init [--force]
16
15
  dalus forge [--pages] [--workers] [--storage] [--database] [--vault]
17
16
  [--env <name>] [--name <resourceName>] [--yes]
18
17
 
19
- Bare "dalus login" opens your browser to sign in with Cobinar and stores
20
- what it gets back. --token accepts one you already have some other way
21
- (CI, a headless box with no browser) instead.
22
-
23
18
  Bare "dalus forge" deploys every resource in dalus.jsonc / dalus.toml /
24
19
  dalus.json found in the current directory. Passing one or more of
25
20
  --pages/--workers/--storage/--database/--vault narrows it to just
@@ -60,10 +55,6 @@ async function main() {
60
55
  }
61
56
 
62
57
  main().catch((err) => {
63
- if (err && err.status === 401) {
64
- console.error('Your Cobinar session expired or was rejected. Run "dalus login" again.');
65
- } else {
66
- console.error(err.message || err);
67
- }
58
+ console.error(err.message || err);
68
59
  process.exitCode = 1;
69
60
  });
package/package.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@cobinar/dalus",
3
- "version": "0.1.8",
4
- "type": "commonjs",
3
+ "version": "0.1.10",
5
4
  "description": "Deploy Cobinar projects (Edge Compute, Static Hosting, Object Storage, Document DB, Vault) from the command line.",
6
5
  "bin": {
7
6
  "dalus": "./bin/dalus.js"
8
7
  },
9
8
  "main": "./src/index.js",
9
+ "type": "commonjs",
10
10
  "engines": {
11
11
  "node": ">=18.0.0"
12
12
  },
package/publish-dalus.bat CHANGED
@@ -2,7 +2,7 @@
2
2
  echo 🚀 Preparing to publish new @cobinar/dalus version...
3
3
 
4
4
  :: Navigate to your exact project directory
5
- cd /d "D:\Downloads\Cobinar-Lobby + Dashboard + Da;us CLI\dalus"
5
+ cd /d "D:\Downloads\daluss"
6
6
 
7
7
  echo.
8
8
  echo 🔐 Checking npm authentication...
@@ -10,7 +10,7 @@ const { saveCredentials, clearCredentials, loadCredentials, CREDENTIALS_PATH } =
10
10
  // That's the one, deliberately-fixed anchor point everything else in this
11
11
  // file is relative to; if IT ever needs to move, that's a cobinar.com
12
12
  // redeploy, not a new npm release everyone has to go update.
13
- const SERVICES_ENDPOINT = 'https://8bnk.dalus.cobinar.com';
13
+ const SERVICES_ENDPOINT = 'https://cobinar.com/api/services';
14
14
  // Only used if the discovery call above fails outright (a network hiccup,
15
15
  // cobinar.com briefly unreachable) -- not the source of truth, just keeps
16
16
  // `dalus login` from being completely dead during a transient outage of
package/src/cors.js CHANGED
@@ -1,34 +1,19 @@
1
- // src/cors.js
2
-
3
- export function getAllowedOrigins(env) {
4
- return (env.ALLOWED_ORIGINS || '')
5
- .split(/[,\n\r]+/)
6
- .map((o) => o.trim())
7
- .map((o) => o.replace(/[\r\n\t\x00-\x1F\x7F]/g, ''))
8
- .filter((o) => /^https?:\/\/[a-zA-Z0-9.-]+(:\d+)?$/.test(o));
9
- }
10
-
11
- export function corsHeaders(env, requestOrigin = '') {
12
- const allowed = getAllowedOrigins(env);
13
- const origin = allowed.includes(requestOrigin) ? requestOrigin : allowed[0] || '';
14
- const headers = {
15
- 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',
16
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With',
17
- 'Access-Control-Expose-Headers': 'X-Upload-Key, X-Served-By',
1
+ // Minimal, self-contained — this worker doesn't import anything from
2
+ // cobinar-main's project. The only cross-origin call this API ever needs to
3
+ // allow is the browser's own /login/callback page posting to itself, which
4
+ // is same-origin anyway; this exists mainly so a misconfigured or future
5
+ // caller gets a clear CORS response instead of a silent browser block.
6
+ export function corsHeaders(request) {
7
+ const origin = request.headers.get('Origin') || '';
8
+ const allowed = origin.endsWith('.cobinar.com') || origin === 'https://cobinar.com';
9
+ return {
10
+ 'Access-Control-Allow-Origin': allowed ? origin : 'https://dalus.cobinar.com',
11
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
12
+ 'Access-Control-Allow-Headers': 'Content-Type',
18
13
  Vary: 'Origin',
19
14
  };
20
- if (origin) {
21
- headers['Access-Control-Allow-Origin'] = origin;
22
- headers['Access-Control-Allow-Credentials'] = 'true';
23
- headers['Access-Control-Max-Age'] = '86400';
24
- }
25
- return headers;
26
15
  }
27
16
 
28
- export function handleCors(request, env) {
29
- const origin = request.headers.get('Origin') || '';
30
- return new Response(null, {
31
- status: 204,
32
- headers: corsHeaders(env, origin),
33
- });
17
+ export function handleCors(request) {
18
+ return new Response(null, { status: 204, headers: corsHeaders(request) });
34
19
  }
@@ -1,72 +1,39 @@
1
- // src/storage.js
2
- const { generateId } = require('./utils.js');
1
+ 'use strict';
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { walkFiles, guessContentType } = require('../fs-utils');
3
5
 
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 };
6
+ /** Deploys one Object Storage bucket: find-or-create by name, flips
7
+ * public access if configured, then PUTs every file under `dir` as an
8
+ * object keyed by its relative path — same "re-upload everything every
9
+ * run" behavior as the Pages forger, for the same reason (matching a
10
+ * deploy tool's usual "this directory IS the desired state" model). */
11
+ async function forgeStorage(api, log, entry) {
12
+ log(`Object Storage: ${entry.name}`);
8
13
 
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()}`;
14
+ let bucket = await api.findByName('/storage/buckets', entry.name);
15
+ if (!bucket) {
16
+ log(` creating bucket...`);
17
+ bucket = await api.post('/storage/buckets', { name: entry.name });
13
18
  }
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
19
 
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
- });
20
+ if (typeof entry.public === 'boolean' && entry.public !== bucket.publicEnabled) {
21
+ bucket = await api.patch(`/storage/buckets/${bucket.id}/public`, { enabled: entry.public });
22
+ log(` public access -> ${entry.public}`);
44
23
  }
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
24
 
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);
25
+ if (entry.dir) {
26
+ const files = walkFiles(entry.dir);
27
+ for (const relPath of files) {
28
+ const fullPath = path.join(entry.dir, relPath);
29
+ const contentType = guessContentType(relPath);
30
+ const body = fs.readFileSync(fullPath);
31
+ const result = await api.putRaw(`/storage/buckets/${bucket.id}/objects/${relPath}`, body, contentType);
32
+ log(` uploaded ${relPath}${result && result.url ? ` -> ${result.url}` : ''}`);
69
33
  }
70
34
  }
71
- return out;
35
+
36
+ log(` done`);
72
37
  }
38
+
39
+ module.exports = { forgeStorage };
@@ -0,0 +1,143 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // src/handlers/login-callback.js — dalus.cobinar.com's OAuth code exchange
3
+ //
4
+ // Fully self-contained: this worker's own client_id/secret, its own copy of
5
+ // the couple of small helpers it needs, nothing imported from cobinar-main's
6
+ // project. The only thing shared with the rest of Cobinar Systems is the
7
+ // identity provider itself (auth.cobinar.com — same one every Cobinar
8
+ // product is a client of) and the SSO-code-handoff CONTRACT: the KV key
9
+ // format cobinar-developers-worker's own /auth/redeem-code expects. That
10
+ // contract is about whose identity just signed in, not which worker or
11
+ // which OAuth client asked, so it needs no changes to work from here.
12
+ //
13
+ // Frontend contract:
14
+ // POST /api/login/callback
15
+ // { "code": "<authorization code from ?code=>",
16
+ // "redirect_uri": "<the exact redirect_uri used in /oauth/authorize>" }
17
+ // → 200 { ok:true, user:{ uid, email, name, picture }, ssoCode:"..." }
18
+ // → 4xx { ok:false, error:"human readable message" }
19
+ //
20
+ // Required bindings (see wrangler.jsonc / .dev.vars.example):
21
+ // DALUS_AUTH_CLIENT_ID, DALUS_AUTH_CLIENT_SECRET, DALUS_AUTH_BASE, SSO_KV
22
+ // ═══════════════════════════════════════════════════════════════════════════
23
+
24
+ import { corsHeaders } from '../cors.js';
25
+
26
+ const DEFAULT_AUTH_BASE = 'https://auth.cobinar.com';
27
+ const SSO_CODE_TTL_SECONDS = 120;
28
+
29
+ export async function handleLoginCallback(request, env) {
30
+ const headers = { 'Content-Type': 'application/json', ...corsHeaders(request) };
31
+ const fail = (message, status = 400) =>
32
+ new Response(JSON.stringify({ ok: false, error: message }), { status, headers });
33
+
34
+ const authBase = env.DALUS_AUTH_BASE || DEFAULT_AUTH_BASE;
35
+ const clientId = env.DALUS_AUTH_CLIENT_ID;
36
+ const clientSecret = env.DALUS_AUTH_CLIENT_SECRET;
37
+ if (!clientId || !clientSecret) {
38
+ return fail('Sign-in is not configured yet on the server.', 500);
39
+ }
40
+
41
+ let body;
42
+ try {
43
+ body = await request.json();
44
+ } catch {
45
+ return fail('Malformed request.');
46
+ }
47
+
48
+ const code = typeof body?.code === 'string' ? body.code.trim() : '';
49
+ if (!code) return fail('Missing authorization code.');
50
+
51
+ const redirectUri = typeof body?.redirect_uri === 'string' ? body.redirect_uri.trim() : '';
52
+ if (!/^https:\/\/[a-zA-Z0-9.-]+(\/[^\s]*)?$/.test(redirectUri)) {
53
+ return fail('Missing or invalid redirect_uri.');
54
+ }
55
+
56
+ // ── Step 1: exchange the code for tokens (server-side only) ───────────────
57
+ let tokenJson;
58
+ try {
59
+ const tokenRes = await fetch(`${authBase}/oauth/token`, {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json' },
62
+ body: JSON.stringify({
63
+ grant_type: 'authorization_code',
64
+ code,
65
+ client_id: clientId,
66
+ client_secret: clientSecret,
67
+ redirect_uri: redirectUri,
68
+ }),
69
+ });
70
+ tokenJson = await safeJson(tokenRes);
71
+ if (!tokenRes.ok || !tokenJson?.access_token) {
72
+ console.error('[login-callback] token exchange rejected:', tokenRes.status, JSON.stringify(tokenJson), 'redirect_uri sent:', redirectUri);
73
+ return fail(friendlyOAuthError(tokenJson), tokenRes.status === 401 ? 401 : 400);
74
+ }
75
+ } catch (err) {
76
+ console.error('[login-callback] token fetch threw:', err?.name, err?.message);
77
+ return fail('Could not reach the sign-in service. Please try again.', 502);
78
+ }
79
+
80
+ // ── Step 2: fetch the signed-in user's profile ─────────────────────────────
81
+ let userInfo;
82
+ try {
83
+ const userRes = await fetch(`${authBase}/oauth/userinfo`, {
84
+ headers: { Authorization: `Bearer ${tokenJson.access_token}` },
85
+ });
86
+ userInfo = await safeJson(userRes);
87
+ if (!userRes.ok || !userInfo) {
88
+ console.error('[login-callback] userinfo rejected:', userRes.status, JSON.stringify(userInfo));
89
+ return fail('Signed in, but could not load your profile. Please try again.', 502);
90
+ }
91
+ } catch (err) {
92
+ console.error('[login-callback] userinfo fetch threw:', err?.name, err?.message);
93
+ return fail('Signed in, but could not load your profile. Please try again.', 502);
94
+ }
95
+
96
+ const user = {
97
+ uid: userInfo.uid || userInfo.sub || null,
98
+ email: userInfo.email || userInfo.cobinarEmail || null,
99
+ name: userInfo.name || null,
100
+ picture: userInfo.picture || null,
101
+ };
102
+ if (!user.uid || !user.email) {
103
+ return fail('Sign-in succeeded but the profile was incomplete.', 502);
104
+ }
105
+
106
+ // ── Step 3: hand off an SSO code — cobinar-developers-worker redeems this
107
+ // for the workerToken dalus actually needs; this handler never mints
108
+ // one itself. ─────────────────────────────────────────────────────────
109
+ const ssoCode = crypto.randomUUID().replace(/-/g, '');
110
+ try {
111
+ await env.SSO_KV.put(
112
+ `sso:${ssoCode}`,
113
+ JSON.stringify({ uid: user.uid, email: user.email, name: user.name, picture: user.picture }),
114
+ { expirationTtl: SSO_CODE_TTL_SECONDS },
115
+ );
116
+ } catch (err) {
117
+ console.error('[login-callback] SSO_KV write failed:', err?.name, err?.message);
118
+ return fail('Signed in, but could not finish preparing your credentials. Please try again.', 502);
119
+ }
120
+
121
+ return new Response(JSON.stringify({ ok: true, user, ssoCode }), { status: 200, headers });
122
+ }
123
+
124
+ async function safeJson(res) {
125
+ try {
126
+ return await res.json();
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+
132
+ function friendlyOAuthError(tokenJson) {
133
+ switch (tokenJson?.error || '') {
134
+ case 'invalid_grant':
135
+ return 'That sign-in link expired or was already used. Please sign in again.';
136
+ case 'invalid_client':
137
+ return 'Sign-in is misconfigured on our end. Please try again later.';
138
+ case 'access_denied':
139
+ return 'Sign-in was cancelled.';
140
+ default:
141
+ return 'Could not complete sign-in. Please try again.';
142
+ }
143
+ }
@@ -0,0 +1,45 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // src/handlers/services.js — where do Cobinar's other workers live right now
3
+ //
4
+ // One JSON endpoint, backed by KV instead of source code, specifically so
5
+ // a worker's public URL can change WITHOUT redeploying whatever reads it —
6
+ // today that's the dalus CLI (an npm package, the slowest thing in this
7
+ // whole system to get everyone to update), so it looks this up once at the
8
+ // start of `dalus login` instead of shipping a fixed URL in the package.
9
+ //
10
+ // GET /api/services
11
+ // → 200 { "dalus": "https://8bnk.dalus.cobinar.com" }
12
+ //
13
+ // Values live in SSO_KV under a `service:<name>` key (the same shared
14
+ // namespace this worker already uses for the SSO code hand-off — reused
15
+ // here for a second, unrelated-but-convenient purpose: a permanent value
16
+ // instead of the hand-off's short-TTL ones, so give it its own key prefix
17
+ // to keep the two apart). Set/update one with:
18
+ //
19
+ // wrangler kv key put --binding=SSO_KV "service:dalus" "https://8bnk.dalus.cobinar.com"
20
+ //
21
+ // No redeploy of THIS worker needed either when a URL changes — that's the
22
+ // whole point. FALLBACK below is only so this endpoint returns something
23
+ // sane before that command has ever been run once.
24
+ // ═══════════════════════════════════════════════════════════════════════════
25
+
26
+ const FALLBACK = {
27
+ dalus: 'https://8bnk.dalus.cobinar.com',
28
+ };
29
+
30
+ export async function handleServices(request, env) {
31
+ const headers = { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=300' };
32
+
33
+ const result = {};
34
+ for (const name of Object.keys(FALLBACK)) {
35
+ let value = null;
36
+ try {
37
+ value = await env.SSO_KV.get(`service:${name}`);
38
+ } catch (err) {
39
+ console.error('[services] KV read failed for', name, ':', err?.message);
40
+ }
41
+ result[name] = value || FALLBACK[name];
42
+ }
43
+
44
+ return new Response(JSON.stringify(result), { status: 200, headers });
45
+ }
package/src/index.js CHANGED
@@ -1,115 +1,36 @@
1
1
  // ═══════════════════════════════════════════════════════════════════════════
2
- // src/index.js — Cobinar R2 Worker · Entry Point
3
- // Created by Marc-Arthur Samuel Dalus · Cobinar Systems
2
+ // src/index.js — dalus.cobinar.com · Entry Point
4
3
  //
5
- // Routes:
6
- // GET /health — Uptime check (no auth)
7
- // GET /assets/:key — Stream public R2 asset (CDN fallback)
8
- // POST /api/auth/callback — Cobinar Auth (OAuth) code exchange (public
9
- // this is how a visitor becomes authenticated)
10
- // GET /api/services — where Cobinar's other workers currently
11
- // live (public, read-only; see that
12
- // handler for why this is KV-backed)
13
- // POST /api/upload — Upload file to R2 (Firebase auth + admin only)
14
- // DELETE /api/delete — Delete R2 object (Firebase auth + admin only)
15
- // (anything else) — Delegated to env.ASSETS (the site in ./public)
16
- //
17
- // assets.run_worker_first is set to `true` (unconditional) rather than a
18
- // glob-pattern array scoped to /api/* — Cloudflare's asset-vs-worker
19
- // precedence for that project turned out to return 405 for every method
20
- // other than GET regardless of pattern, so this Worker now runs for every
21
- // request and explicitly hands off anything it doesn't recognize to
22
- // env.ASSETS.fetch(request) itself, rather than depending on that layer.
23
- //
24
- // Bindings (wrangler.toml):
25
- // COBINAR_R2 R2Bucket — cobinar bucket
26
- // FIREBASE_PROJECT_ID — cobinar-prod
27
- // ALLOWED_ORIGINS — CORS allowlist
28
- // WORKER_DOMAIN — worker.cobinar.com
29
- // R2_PUBLIC_DOMAIN — cdn.cobinar.com
30
- // ADMIN_EMAIL — cobinar@cobinar.com
31
- // MAX_UPLOAD_SIZE — 104857600 (100 MB)
32
- // COBINAR_AUTH_CLIENT_ID — Cobinar Auth OAuth client id (public)
33
- // COBINAR_AUTH_CLIENT_SECRET — SECRET — set via `wrangler secret put`, never in this repo
34
- // COBINAR_AUTH_BASE — https://auth.cobinar.com
35
- //
36
- // Folder layout in bucket:
37
- // cobinar/posts/{postId}/{slot}-{uuid}.{ext} ← post media
38
- // cobinar/products/{productId}/{slot}-{uuid}.{ext} ← product images
39
- // cobinar/projects/{projectId}/{slot}-{uuid}.{ext} ← project assets
40
- // cobinar/images/{uuid}.{ext} ← flat image store
41
- // cobinar/files/{uuid}.{ext} ← flat file store
4
+ // Everything about the dalus CLI that lives on the web: the landing page,
5
+ // docs, and the browser-based sign-in flow it opens (see public/login/).
6
+ // A standalone Worker, independent of cobinar-main — the only thing it
7
+ // shares with the rest of Cobinar Systems is auth.cobinar.com (the identity
8
+ // provider every product is a client of) and the SSO_KV binding used to
9
+ // hand a signed-in identity off to cobinar-developers-worker.
42
10
  //
43
- // CDN delivery (once cdn.cobinar.com R2 custom domain is configured):
44
- // https://cdn.cobinar.com/{key} → direct R2 edge (zero Worker cost)
11
+ // Routes:
12
+ // POST /api/login/callback — OAuth code exchange (public a visitor
13
+ // has no session yet, this is what
14
+ // creates the SSO code for one)
15
+ // (anything else) — env.ASSETS (index.html, docs, login
16
+ // pages, css)
45
17
  // ═══════════════════════════════════════════════════════════════════════════
46
18
 
47
- import { handleCors, corsHeaders } from './cors.js';
48
- import { verifyFirebaseToken } from './auth.js';
49
- import { handleUpload } from './handlers/upload.js';
50
- import { handleDelete } from './handlers/delete.js';
51
- import { handleServe } from './handlers/serve.js';
52
- import { handleHealth } from './handlers/health.js';
53
- import { handleAuthCallback } from './handlers/auth-callback.js';
54
- import { handleServices } from './handlers/services.js';
19
+ import { handleCors } from './cors.js';
20
+ import { handleLoginCallback } from './handlers/login-callback.js';
55
21
 
56
22
  export default {
57
- async fetch(request, env, ctx) {
58
- const url = new URL(request.url);
59
- const path = url.pathname;
60
- const method = request.method;
61
-
62
- // ── CORS preflight ──────────────────────────────────────────────────────
63
- if (method === 'OPTIONS') return handleCors(request, env);
64
-
65
- // ── Health check (no auth) ──────────────────────────────────────────────
66
- if (path === '/health' && method === 'GET') return handleHealth(request, env);
67
-
68
- // ── Public asset serving — CDN fallback until cdn.cobinar.com is live ───
69
- if (method === 'GET' && path.startsWith('/assets/')) {
70
- return handleServe(request, env, path.slice('/assets/'.length));
71
- }
72
-
73
- // ── Cobinar Auth code exchange — public. A visitor has no session yet;
74
- // this is the one request that creates one, so it can't require one. ──
75
- if (path === '/api/auth/callback' && method === 'POST') {
76
- return handleAuthCallback(request, env);
77
- }
78
- if (path === '/api/services' && method === 'GET') {
79
- return handleServices(request, env);
80
- }
23
+ async fetch(request, env) {
24
+ const url = new URL(request.url);
25
+ const { pathname } = url;
26
+ const { method } = request;
81
27
 
82
- // ── Authenticated API routes ────────────────────────────────────────────
83
- if (path.startsWith('/api/')) {
84
- const token = (request.headers.get('Authorization') || '').replace(/^Bearer\s+/i, '').trim();
85
- if (!token) return jsonError('Missing Authorization header', 401, env, request);
28
+ if (method === 'OPTIONS') return handleCors(request);
86
29
 
87
- let claims;
88
- try {
89
- claims = await verifyFirebaseToken(token, env.FIREBASE_PROJECT_ID);
90
- } catch (e) {
91
- return jsonError('Invalid or expired token: ' + e.message, 401, env, request);
92
- }
93
-
94
- if (path === '/api/upload' && method === 'POST') return handleUpload(request, env, claims);
95
- if (path === '/api/delete' && method === 'DELETE') return handleDelete(request, env, claims);
96
-
97
- return jsonError('Unknown API route', 404, env, request);
30
+ if (pathname === '/api/login/callback' && method === 'POST') {
31
+ return handleLoginCallback(request, env);
98
32
  }
99
33
 
100
- // ── Everything else: hand off to static assets (site pages, css, js, icons).
101
- // Explicit here rather than relying on Cloudflare's asset-vs-worker
102
- // precedence, since that's exactly the layer that's been unreliable. ──
103
34
  return env.ASSETS.fetch(request);
104
35
  },
105
36
  };
106
-
107
- function jsonError(message, status, env, request) {
108
- const origin = request?.headers?.get('Origin') || '';
109
- return new Response(JSON.stringify({ ok: false, error: message }), {
110
- status,
111
- headers: { 'Content-Type': 'application/json', ...corsHeaders(env, origin) },
112
- });
113
- }
114
- // Created by Marc-Arthur Samuel Dalus · Cobinar Systems
115
- //