@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 +10 -36
- package/bin/dalus.js +2 -11
- package/package.json +2 -2
- package/publish-dalus.bat +1 -1
- package/src/commands/login.js +1 -1
- package/src/cors.js +14 -29
- package/src/forgers/storage.js +30 -63
- package/src/handlers/login-callback.js +143 -0
- package/src/handlers/services.js +45 -0
- package/src/index.js +22 -101
- package/src/auth.js +0 -78
- package/src/commands/auth.js +0 -78
- package/src/commands/cors.js +0 -34
- package/src/commands/index.js +0 -115
- package/src/commands/storage.js +0 -72
- package/src/commands/utils.js +0 -7
- package/src/commands/validators.js +0 -101
- package/src/forgers/auth.js +0 -78
- package/src/forgers/cors.js +0 -34
- package/src/forgers/index.js +0 -115
- package/src/forgers/login.js +0 -288
- package/src/forgers/utils.js +0 -7
- package/src/forgers/validators.js +0 -101
- package/src/login.js +0 -288
- package/src/storage.js +0 -72
- package/src/utils.js +0 -7
- package/src/validators.js +0 -101
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
|
-
|
|
40
|
-
cobinar
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 [--
|
|
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
|
-
|
|
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.
|
|
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
package/src/commands/login.js
CHANGED
|
@@ -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://
|
|
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
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
29
|
-
|
|
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
|
}
|
package/src/forgers/storage.js
CHANGED
|
@@ -1,72 +1,39 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
1
|
+
'use strict';
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { walkFiles, guessContentType } = require('../fs-utils');
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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 —
|
|
3
|
-
// Created by Marc-Arthur Samuel Dalus · Cobinar Systems
|
|
2
|
+
// src/index.js — dalus.cobinar.com · Entry Point
|
|
4
3
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
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
|
-
//
|
|
44
|
-
//
|
|
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
|
|
48
|
-
import {
|
|
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
|
|
58
|
-
const url
|
|
59
|
-
const
|
|
60
|
-
const method = request
|
|
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
|
-
|
|
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
|
-
|
|
88
|
-
|
|
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
|
-
//
|