@surph_ai/cli 0.0.9 → 0.0.12
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/cli.js +178 -5
- package/package.json +1 -1
- package/scaffold/tool/package.json +2 -1
- package/toolserver/app.js +9 -11
- package/toolserver/middleware/config.js +40 -0
- package/toolserver/middleware/configureApp.js +63 -0
- package/toolserver/middleware/index.js +7 -0
- package/toolserver/routes/api.js +66 -0
- package/toolserver/routes/index.js +63 -2
- package/toolserver/templates/index.html +7 -6
package/cli.js
CHANGED
|
@@ -9,6 +9,10 @@
|
|
|
9
9
|
const { Command } = require('commander')
|
|
10
10
|
const path = require('path')
|
|
11
11
|
const fs = require('fs')
|
|
12
|
+
const os = require('os')
|
|
13
|
+
const http = require('http')
|
|
14
|
+
const crypto = require('crypto')
|
|
15
|
+
const { exec } = require('child_process')
|
|
12
16
|
const dotenv = require('dotenv')
|
|
13
17
|
const prompt = require('prompt')
|
|
14
18
|
const shell = require('shelljs')
|
|
@@ -22,9 +26,19 @@ const utils = require('./utils')
|
|
|
22
26
|
const packageJson = require('./package.json')
|
|
23
27
|
|
|
24
28
|
const VERSION = packageJson.version
|
|
25
|
-
|
|
29
|
+
// Env overrides let the flow point at a local surph.ai / REST during dev
|
|
30
|
+
// without editing this file. Defaults are production.
|
|
31
|
+
const SURPH_REST_URL = process.env.SURPH_REST_URL || 'https://surph-mongo-http-19075ff8d93f.herokuapp.com'
|
|
32
|
+
|
|
33
|
+
const SURPH_WEB_URL = 'https://surph.ai'
|
|
26
34
|
// const RPC_URL = 'https://e3lunom2hka3u6iiotrlzrcez40uwdnk.lambda-url.us-east-1.on.aws'
|
|
27
35
|
|
|
36
|
+
// User-scoped session — one login covers every project on this machine,
|
|
37
|
+
// matching gh / gcloud / claude. Was previously project-scoped
|
|
38
|
+
// (../../../.surph/session.json inside the tool project).
|
|
39
|
+
const SESSION_PATH = path.join(os.homedir(), '.surph', 'session.json')
|
|
40
|
+
const CLI_LOGIN_TIMEOUT_MS = 2 * 60 * 1000
|
|
41
|
+
|
|
28
42
|
// FUTURE: Export as a referenceable type
|
|
29
43
|
const ERRORS = {
|
|
30
44
|
// General errors
|
|
@@ -40,13 +54,12 @@ const ERRORS = {
|
|
|
40
54
|
|
|
41
55
|
const surphSession = async () => {
|
|
42
56
|
try {
|
|
43
|
-
|
|
44
|
-
if (!fs.existsSync(userSessionPath)) {
|
|
57
|
+
if (!fs.existsSync(SESSION_PATH)) {
|
|
45
58
|
console.log('Not logged in.')
|
|
46
59
|
return null
|
|
47
60
|
}
|
|
48
|
-
|
|
49
|
-
const file = fs.readFileSync(
|
|
61
|
+
|
|
62
|
+
const file = fs.readFileSync(SESSION_PATH, 'utf-8')
|
|
50
63
|
const session = JSON.parse(file)
|
|
51
64
|
return session
|
|
52
65
|
} catch (error) {
|
|
@@ -55,6 +68,45 @@ const surphSession = async () => {
|
|
|
55
68
|
}
|
|
56
69
|
}
|
|
57
70
|
|
|
71
|
+
// Cross-platform browser opener. Falls back to printing the URL — matters for
|
|
72
|
+
// SSH / headless / broken default handlers, where `open` silently no-ops.
|
|
73
|
+
const openBrowser = (url) => {
|
|
74
|
+
const cmd = process.platform === 'darwin' ? `open "${url}"`
|
|
75
|
+
: process.platform === 'win32' ? `start "" "${url}"`
|
|
76
|
+
: `xdg-open "${url}"`
|
|
77
|
+
exec(cmd, (err) => {
|
|
78
|
+
if (err) {
|
|
79
|
+
console.log(colors.yellow(`\nCouldn't open browser automatically. Open this URL manually:\n`))
|
|
80
|
+
console.log(colors.cyan(url) + '\n')
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const loginSuccessHtml = (username) => `<!doctype html>
|
|
86
|
+
<html>
|
|
87
|
+
<head><meta charset="utf-8"><title>Surph — Logged in</title>
|
|
88
|
+
<style>
|
|
89
|
+
html,body{margin:0;height:100%;background:#0b0b0b;color:#f5f5f5;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif}
|
|
90
|
+
.wrap{display:flex;align-items:center;justify-content:center;height:100%}
|
|
91
|
+
.card{text-align:center;max-width:420px;padding:2rem}
|
|
92
|
+
h1{font-weight:500;margin:0 0 .5rem;font-size:1.5rem}
|
|
93
|
+
p{margin:0;color:rgba(245,245,245,.65);line-height:1.5}
|
|
94
|
+
.ok{color:#4ade80;font-size:2rem;margin-bottom:1rem}
|
|
95
|
+
</style></head>
|
|
96
|
+
<body><div class="wrap"><div class="card">
|
|
97
|
+
<div class="ok">✓</div>
|
|
98
|
+
<h1>Logged in${username ? ` as @${username}` : ''}</h1>
|
|
99
|
+
<p>You can close this tab and return to your terminal.</p>
|
|
100
|
+
</div></div></body></html>`
|
|
101
|
+
|
|
102
|
+
const loginErrorHtml = (message) => `<!doctype html>
|
|
103
|
+
<html><head><meta charset="utf-8"><title>Surph — Login failed</title></head>
|
|
104
|
+
<body style="font-family:-apple-system,sans-serif;padding:2rem">
|
|
105
|
+
<h1>Login failed</h1>
|
|
106
|
+
<p>${message}</p>
|
|
107
|
+
<p>You may close this tab and try again from the terminal.</p>
|
|
108
|
+
</body></html>`
|
|
109
|
+
|
|
58
110
|
const program = new Command()
|
|
59
111
|
|
|
60
112
|
program.version(VERSION, '-v, --version', 'output the current version')
|
|
@@ -77,6 +129,127 @@ program.command('version')
|
|
|
77
129
|
}
|
|
78
130
|
})
|
|
79
131
|
|
|
132
|
+
program.command('login')
|
|
133
|
+
.description('log in to Surph')
|
|
134
|
+
.action(async () => {
|
|
135
|
+
let server
|
|
136
|
+
try {
|
|
137
|
+
// State nonce binds the browser round-trip to this specific CLI
|
|
138
|
+
// invocation — rejects stray callbacks (stale tab, replay, CSRF).
|
|
139
|
+
const state = crypto.randomBytes(16).toString('hex')
|
|
140
|
+
|
|
141
|
+
let resolveCallback
|
|
142
|
+
let rejectCallback
|
|
143
|
+
const callbackPromise = new Promise((resolve, reject) => {
|
|
144
|
+
resolveCallback = resolve
|
|
145
|
+
rejectCallback = reject
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
server = http.createServer((req, res) => {
|
|
149
|
+
// Ignore anything except the /callback path. Browsers ping
|
|
150
|
+
// /favicon.ico etc. — respond quietly so they don't fill the terminal.
|
|
151
|
+
const reqUrl = new URL(req.url, 'http://127.0.0.1')
|
|
152
|
+
if (reqUrl.pathname !== '/callback') {
|
|
153
|
+
res.writeHead(404)
|
|
154
|
+
res.end()
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const token = reqUrl.searchParams.get('token')
|
|
159
|
+
const returnedState = reqUrl.searchParams.get('state')
|
|
160
|
+
const errParam = reqUrl.searchParams.get('error')
|
|
161
|
+
|
|
162
|
+
if (errParam) {
|
|
163
|
+
res.writeHead(400, { 'Content-Type': 'text/html' })
|
|
164
|
+
res.end(loginErrorHtml(errParam))
|
|
165
|
+
rejectCallback(new Error(errParam))
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
if (!token || !returnedState || returnedState !== state) {
|
|
169
|
+
res.writeHead(400, { 'Content-Type': 'text/html' })
|
|
170
|
+
res.end(loginErrorHtml('Invalid state or missing token.'))
|
|
171
|
+
rejectCallback(new Error('invalid state or missing token'))
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Success page is written first; user lookup happens after.
|
|
176
|
+
// Response is intentionally best-effort — we already have the token.
|
|
177
|
+
res.writeHead(200, { 'Content-Type': 'text/html' })
|
|
178
|
+
res.end(loginSuccessHtml(null))
|
|
179
|
+
resolveCallback(token)
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// Bind port 0 = pick any free port. 127.0.0.1 (not 0.0.0.0) so nothing
|
|
183
|
+
// on the LAN can hit the callback and race for the token.
|
|
184
|
+
await new Promise((resolve, reject) => {
|
|
185
|
+
server.on('error', reject)
|
|
186
|
+
server.listen(0, '127.0.0.1', resolve)
|
|
187
|
+
})
|
|
188
|
+
const { port } = server.address()
|
|
189
|
+
const callback = `http://127.0.0.1:${port}/callback`
|
|
190
|
+
const authUrl = `${SURPH_WEB_URL}/auth/cli/login?callback=${encodeURIComponent(callback)}&state=${state}`
|
|
191
|
+
|
|
192
|
+
console.log(colors.cyan('\nOpening browser to complete login...\n'))
|
|
193
|
+
console.log(colors.gray(`If it doesn't open, visit this URL manually:\n${authUrl}\n`))
|
|
194
|
+
openBrowser(authUrl)
|
|
195
|
+
|
|
196
|
+
// Hard timeout — otherwise a user who never completes login leaves the
|
|
197
|
+
// CLI hanging forever with a bound port.
|
|
198
|
+
const timeoutId = setTimeout(
|
|
199
|
+
() => rejectCallback(new Error(`login timed out after ${CLI_LOGIN_TIMEOUT_MS / 1000}s`)),
|
|
200
|
+
CLI_LOGIN_TIMEOUT_MS
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
let token
|
|
204
|
+
try {
|
|
205
|
+
token = await callbackPromise
|
|
206
|
+
} finally {
|
|
207
|
+
clearTimeout(timeoutId)
|
|
208
|
+
server.close()
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Exchange the token for the user record. Hits the web server (same
|
|
212
|
+
// origin as the login redirect), not the mongo-http REST — the token
|
|
213
|
+
// is signed by the web server's HMAC secret and only it can verify.
|
|
214
|
+
const resp = await fetch(`${SURPH_WEB_URL}/auth/cli/whoami`, {
|
|
215
|
+
method: 'GET',
|
|
216
|
+
headers: {
|
|
217
|
+
Accept: 'application/json',
|
|
218
|
+
Authorization: `Bearer ${token}`,
|
|
219
|
+
'x-surph-client': 'surph-sdk'
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
if (!resp.ok) {
|
|
223
|
+
throw new Error(`whoami request failed: HTTP ${resp.status}`)
|
|
224
|
+
}
|
|
225
|
+
const body = await resp.json()
|
|
226
|
+
const user = body?.payload || body?.user || body
|
|
227
|
+
if (!user?._id) {
|
|
228
|
+
throw new Error('whoami response missing user _id')
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const session = {
|
|
232
|
+
token,
|
|
233
|
+
userId: user._id,
|
|
234
|
+
username: user.username || null,
|
|
235
|
+
avatar: user.avatar || null,
|
|
236
|
+
createdAt: new Date().toISOString()
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
fs.mkdirSync(path.dirname(SESSION_PATH), { recursive: true })
|
|
240
|
+
// Restrict to owner rw so a shared machine doesn't leak the token to
|
|
241
|
+
// other local users. No-op on Windows.
|
|
242
|
+
fs.writeFileSync(SESSION_PATH, JSON.stringify(session, null, 2) + '\n', { mode: 0o600 })
|
|
243
|
+
|
|
244
|
+
const who = session.username ? `@${session.username}` : session.userId
|
|
245
|
+
console.log(colors.brightGreen(`\n✓ Logged in as ${who}\n`))
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (server) { try { server.close() } catch (_) {} }
|
|
248
|
+
console.log(colors.red(`\nLogin failed: ${error.message}\n`))
|
|
249
|
+
process.exit(1)
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
|
|
80
253
|
program.command('profile')
|
|
81
254
|
.description('Current Surph user')
|
|
82
255
|
.action(async (cmd, options) => {
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "
|
|
2
|
+
"name": "test5",
|
|
3
3
|
"version": "0.0.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"test": "",
|
|
9
9
|
"build": "rimraf dist && tsc",
|
|
10
10
|
"watch": "tsc -w",
|
|
11
|
+
"login": "surph login",
|
|
11
12
|
"manifest": "surph manifest",
|
|
12
13
|
"publish": "npm run build && surph publish",
|
|
13
14
|
"deploy": "npm run build && surph deploy"
|
package/toolserver/app.js
CHANGED
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
const express = require('express')
|
|
2
|
+
const path = require('path')
|
|
2
3
|
const vinely = require('@vinely-ai/sdk')
|
|
4
|
+
const { config, configureApp } = require('./middleware/index.js')
|
|
3
5
|
|
|
4
6
|
const app = express()
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
views: '
|
|
8
|
+
configureApp(app, {
|
|
9
|
+
views: path.join(__dirname, 'templates'),
|
|
8
10
|
static: 'public',
|
|
9
11
|
})
|
|
10
12
|
|
|
11
|
-
app.use(
|
|
12
|
-
res.setHeader('Access-Control-Allow-Origin', '*')
|
|
13
|
-
res.header('Access-Control-Allow-Headers', 'x-surph-user, Content-Type');
|
|
14
|
-
res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
15
|
-
if (req.method === 'OPTIONS') return res.sendStatus(204);
|
|
16
|
-
next()
|
|
17
|
-
})
|
|
13
|
+
app.use(config)
|
|
18
14
|
|
|
19
|
-
const
|
|
15
|
+
const api = require('./routes/api')
|
|
20
16
|
const tool = require('./routes/tool')
|
|
17
|
+
const index = require('./routes/index')
|
|
21
18
|
|
|
22
|
-
app.use('/',
|
|
19
|
+
app.use('/api', api)
|
|
23
20
|
app.use('/tools', tool)
|
|
21
|
+
app.use('/', index)
|
|
24
22
|
|
|
25
23
|
module.exports.entry = vinely.server(app)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// this is meant to mimic production environment
|
|
2
|
+
// so no need to distinguish between local and production
|
|
3
|
+
|
|
4
|
+
let config = null;
|
|
5
|
+
|
|
6
|
+
const CDN = 'https://surph.s3.us-east-1.amazonaws.com/webapp/static';
|
|
7
|
+
|
|
8
|
+
const fetchDesktopConfig = async () => {
|
|
9
|
+
const response = await fetch('https://surph.s3.us-east-1.amazonaws.com/admin/desktop/config.json');
|
|
10
|
+
const data = await response.json();
|
|
11
|
+
return data;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const fetchWebConfig = async () => {
|
|
15
|
+
const response = await fetch('https://surph.s3.us-east-1.amazonaws.com/webapp/static/dist/prod/imports.json');
|
|
16
|
+
const data = await response.json();
|
|
17
|
+
return { imports: data }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = async (req, res, next) => {
|
|
21
|
+
const webConfig = await fetchWebConfig(); // always fetch the latest imports.json
|
|
22
|
+
|
|
23
|
+
if (config !== null) {
|
|
24
|
+
req.config = { ...config, ...webConfig };
|
|
25
|
+
// console.log('CACHED CONFIG: ', req.config);
|
|
26
|
+
return next();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const desktopConfig = await fetchDesktopConfig();
|
|
30
|
+
config = {
|
|
31
|
+
...desktopConfig,
|
|
32
|
+
...webConfig,
|
|
33
|
+
isLocal: false,
|
|
34
|
+
isDev: true, // this is undefined in production.
|
|
35
|
+
CDN,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
req.config = config;
|
|
39
|
+
next();
|
|
40
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const express = require('express')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
// const logger = require('morgan')
|
|
4
|
+
const cookieParser = require('cookie-parser')
|
|
5
|
+
const bodyParser = require('body-parser')
|
|
6
|
+
const sessions = require('client-sessions')
|
|
7
|
+
const { renderFile } = require('ejs')
|
|
8
|
+
const engines = require('consolidate')
|
|
9
|
+
|
|
10
|
+
const BASE_PATH = process.cwd()
|
|
11
|
+
|
|
12
|
+
const configureSession = (app) => {
|
|
13
|
+
const opts = app.opts
|
|
14
|
+
if (!opts.session)
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
const { session } = opts
|
|
18
|
+
const cookieName = session.cookieName || 'surphSession'
|
|
19
|
+
const secret = session.secret || process.env.SESSION_SECRET
|
|
20
|
+
const duration = session.duration || 24 * 60 * 60 * 1000
|
|
21
|
+
const activeDuration = session.activeDuration || 30 * 60 * 1000
|
|
22
|
+
|
|
23
|
+
app.use(sessions({
|
|
24
|
+
cookieName,
|
|
25
|
+
secret,
|
|
26
|
+
duration,
|
|
27
|
+
activeDuration,
|
|
28
|
+
}))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = (app, opts) => { // opts is optional
|
|
32
|
+
if (!app) // should never happen
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
app.opts = opts || {}
|
|
36
|
+
app.use(bodyParser.json({ limit: '10mb' }))
|
|
37
|
+
app.use(bodyParser.urlencoded({extended: false}))
|
|
38
|
+
app.use(cookieParser())
|
|
39
|
+
// app.use(logger('dev'))
|
|
40
|
+
configureSession(app)
|
|
41
|
+
|
|
42
|
+
const viewsPath = app.opts.views
|
|
43
|
+
? path.resolve(BASE_PATH, app.opts.views)
|
|
44
|
+
: path.join(__dirname, '..', 'templates')
|
|
45
|
+
app.set('views', viewsPath)
|
|
46
|
+
|
|
47
|
+
// default to mustache template engine:
|
|
48
|
+
const viewEngine = app.opts.viewEngine || 'mustache'
|
|
49
|
+
|
|
50
|
+
if (viewEngine === 'mustache') {
|
|
51
|
+
app.engine('html', engines.mustache)
|
|
52
|
+
app.set('view engine', 'html')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (viewEngine === 'ejs') {
|
|
56
|
+
app.set('view engine', 'html')
|
|
57
|
+
app.engine('html', renderFile)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const staticDir = app.opts.static || 'public'
|
|
61
|
+
const staticPath = path.resolve(BASE_PATH, staticDir)
|
|
62
|
+
app.use(express.static(staticPath, {index: false })) // { index:false } prevents index.html from overriding template engine
|
|
63
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const { Router } = require('express')
|
|
2
|
+
// import { REST_API_URL } from '../utils/constants.js'
|
|
3
|
+
|
|
4
|
+
const router = Router()
|
|
5
|
+
|
|
6
|
+
const BASE_URL = 'https://surph.ai'
|
|
7
|
+
// const BASE_URL = 'http://localhost:3005' // for local testing
|
|
8
|
+
const PATH_RE = /^\/[a-zA-Z0-9_\-/]+$/
|
|
9
|
+
|
|
10
|
+
router.post('*', async (req, res, next) => {
|
|
11
|
+
const path = req.path
|
|
12
|
+
if (!PATH_RE.test(path) || path.includes('//') || path.includes('..')) {
|
|
13
|
+
res.status(400).json({ error: 'invalid path' })
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const url = `${BASE_URL}/api${path}`
|
|
19
|
+
const upstream = await fetch(`${url}`, {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
body: JSON.stringify(req.body || {}),
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
'Accept': 'application/json',
|
|
25
|
+
'x-surph-client': 'surph-webapp'
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const body = await upstream.json()
|
|
30
|
+
const contentType = upstream.headers.get('content-type')
|
|
31
|
+
if (contentType) res.set('Content-Type', contentType)
|
|
32
|
+
res.status(upstream.status).json(body)
|
|
33
|
+
} catch (e) {
|
|
34
|
+
res.status(502).json({ error: e?.message || 'upstream api failed' })
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
router.get('*', async (req, res, next) => {
|
|
39
|
+
const path = req.path
|
|
40
|
+
if (!PATH_RE.test(path) || path.includes('//') || path.includes('..')) {
|
|
41
|
+
res.status(400).json({ error: 'invalid path' })
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const url = `${BASE_URL}/api${path}`
|
|
47
|
+
const upstream = await fetch(`${url}`, {
|
|
48
|
+
method: 'GET',
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
'Accept': 'application/json',
|
|
52
|
+
'x-surph-client': 'surph-webapp'
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const body = await upstream.json()
|
|
57
|
+
const contentType = upstream.headers.get('content-type')
|
|
58
|
+
if (contentType) res.set('Content-Type', contentType)
|
|
59
|
+
res.status(upstream.status).json(body)
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.log('ERR: ' + e.message)
|
|
62
|
+
res.status(502).json({ error: e?.message || 'upstream api failed' })
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
module.exports = router
|
|
@@ -1,8 +1,69 @@
|
|
|
1
1
|
const { Router } = require('express')
|
|
2
|
+
const os = require('os')
|
|
3
|
+
const path = require('path')
|
|
4
|
+
const fs = require('fs')
|
|
2
5
|
const router = Router()
|
|
6
|
+
const manifest = require('../../../../../manifest.json')
|
|
3
7
|
|
|
4
|
-
|
|
5
|
-
|
|
8
|
+
// Written by `surph login` — user-scoped, cross-platform via os.homedir().
|
|
9
|
+
// Resolves to ~/.surph/session.json on mac/linux and C:\Users\<name>\.surph\
|
|
10
|
+
// session.json on windows.
|
|
11
|
+
const SESSION_PATH = path.join(os.homedir(), '.surph', 'session.json')
|
|
12
|
+
|
|
13
|
+
// Re-read on every request so `surph login` / `surph logout` take effect
|
|
14
|
+
// on the next browser refresh without needing to restart the toolserver.
|
|
15
|
+
// Cheap: single sync fs read of a tiny JSON file on localhost.
|
|
16
|
+
const readSurphSession = () => {
|
|
17
|
+
try {
|
|
18
|
+
if (!fs.existsSync(SESSION_PATH)) return null
|
|
19
|
+
return JSON.parse(fs.readFileSync(SESSION_PATH, 'utf-8'))
|
|
20
|
+
} catch {
|
|
21
|
+
return null
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const fetchUser = async (username) => {
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(`https://surph.ai/api/users?username=${username}`)
|
|
28
|
+
const { payload: users } = await response.json()
|
|
29
|
+
if (users.length === 0) {
|
|
30
|
+
throw new Error(`User ${username} not found.`)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return users[0]
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return null
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
router.get('/', async (req, res, next) => {
|
|
40
|
+
try {
|
|
41
|
+
const toolSlugs = Object.keys(manifest)
|
|
42
|
+
|
|
43
|
+
// Mirror `userId` to `_id` (and `id`) so preloaded.user matches the
|
|
44
|
+
// production shape the web app consumes — useSettings.js reads
|
|
45
|
+
// `preloaded.user` and downstream code branches on `_id`.
|
|
46
|
+
const session = readSurphSession()
|
|
47
|
+
const user = session?.userId? await fetchUser(session.username) : null
|
|
48
|
+
|
|
49
|
+
const context = {
|
|
50
|
+
env: 'sandbox',
|
|
51
|
+
mode: 'desktop',
|
|
52
|
+
config: { ...req.config, entry: 'web' },
|
|
53
|
+
tools: toolSlugs.map(slug => (manifest[slug])),
|
|
54
|
+
user,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
res.render('index', {
|
|
58
|
+
...context,
|
|
59
|
+
CDN: req.config.CDN,
|
|
60
|
+
preloaded: JSON.stringify(context)
|
|
61
|
+
})
|
|
62
|
+
} catch (error) {
|
|
63
|
+
res.json({
|
|
64
|
+
error: `something went wrong: ${error.message}`
|
|
65
|
+
})
|
|
66
|
+
}
|
|
6
67
|
})
|
|
7
68
|
|
|
8
69
|
module.exports = router
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
|
|
1
2
|
<!DOCTYPE html>
|
|
2
3
|
<html dir="ltr" lang="en-US">
|
|
3
4
|
<head>
|
|
@@ -7,14 +8,14 @@
|
|
|
7
8
|
<meta http-equiv="Content-Language" content="en" />
|
|
8
9
|
|
|
9
10
|
<title>Surph AI</title>
|
|
10
|
-
<link rel="shortcut icon" href="https://
|
|
11
|
-
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css">
|
|
12
|
-
<link rel="stylesheet" href="https://cdn.turbo360-dev.com/admin/assets/scaffold/css/style.min.css" />
|
|
11
|
+
<link rel="shortcut icon" href="https://surph.s3.us-east-1.amazonaws.com/admin/assets/icon-green.svg">
|
|
13
12
|
</head>
|
|
14
13
|
|
|
15
14
|
<body>
|
|
16
|
-
<div
|
|
17
|
-
|
|
18
|
-
</
|
|
15
|
+
<div id="root"></div>
|
|
16
|
+
|
|
17
|
+
<script type="text/javascript">window.__PRELOADED__ = {{{ preloaded }}}</script>
|
|
18
|
+
<script defer="defer" src="{{{ CDN }}}/dist/prod/{{{ config.imports.commons }}}"></script>
|
|
19
|
+
<script defer="defer" src="{{{ CDN }}}/dist/prod/{{{ config.imports.app }}}"></script>
|
|
19
20
|
</body>
|
|
20
21
|
</html>
|