@surph_ai/cli 0.0.13 → 0.0.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@surph_ai/cli",
3
- "version": "0.0.13",
3
+ "version": "0.0.14",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "types": "./types.d.ts",
package/toolserver/app.js CHANGED
@@ -12,10 +12,12 @@ configureApp(app, {
12
12
 
13
13
  app.use(config)
14
14
 
15
+ const rpc = require('./routes/rpc')
15
16
  const api = require('./routes/api')
16
17
  const tool = require('./routes/tool')
17
18
  const index = require('./routes/index')
18
19
 
20
+ app.use('/rpc', rpc)
19
21
  app.use('/api', api)
20
22
  app.use('/tools', tool)
21
23
  app.use('/', index)
@@ -7,6 +7,12 @@ const BASE_URL = 'https://surph.ai'
7
7
  // const BASE_URL = 'http://localhost:3005' // for local testing
8
8
  const PATH_RE = /^\/[a-zA-Z0-9_\-/]+$/
9
9
 
10
+ // `req.url` includes the query string (e.g. `/tides?slug=rss-reader`) while
11
+ // `req.path` drops it. Upstream handlers like /api/tides key off `?slug=…`;
12
+ // dropping the query silently returned the default (first) record, which
13
+ // mismatched every non-first tide the user clicked.
14
+ const buildUpstreamUrl = (req) => `${BASE_URL}/api${req.url}`
15
+
10
16
  router.post('*', async (req, res, next) => {
11
17
  const path = req.path
12
18
  if (!PATH_RE.test(path) || path.includes('//') || path.includes('..')) {
@@ -15,7 +21,7 @@ router.post('*', async (req, res, next) => {
15
21
  }
16
22
 
17
23
  try {
18
- const url = `${BASE_URL}/api${path}`
24
+ const url = buildUpstreamUrl(req)
19
25
  const upstream = await fetch(`${url}`, {
20
26
  method: 'POST',
21
27
  body: JSON.stringify(req.body || {}),
@@ -43,7 +49,7 @@ router.get('*', async (req, res, next) => {
43
49
  }
44
50
 
45
51
  try {
46
- const url = `${BASE_URL}/api${path}`
52
+ const url = buildUpstreamUrl(req)
47
53
  const upstream = await fetch(`${url}`, {
48
54
  method: 'GET',
49
55
  headers: {
@@ -2,8 +2,13 @@ const { Router } = require('express')
2
2
  const os = require('os')
3
3
  const path = require('path')
4
4
  const fs = require('fs')
5
- const router = Router()
5
+ const userConfiguration = require('../../utils/userConfiguration')
6
6
  const manifest = require('../../../../../manifest.json')
7
+ const { getCanvasPickerSource } = require('../../utils/canvasPickerSource')
8
+
9
+ const SURPH_S3_URL = 'https://surph.s3.us-east-1.amazonaws.com'
10
+
11
+ const router = Router()
7
12
 
8
13
  // Written by `surph login` — user-scoped, cross-platform via os.homedir().
9
14
  // Resolves to ~/.surph/session.json on mac/linux and C:\Users\<name>\.surph\
@@ -22,20 +27,6 @@ const readSurphSession = () => {
22
27
  }
23
28
  }
24
29
 
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
30
  router.get('/', async (req, res, next) => {
40
31
  try {
41
32
  const toolSlugs = Object.keys(manifest)
@@ -44,14 +35,37 @@ router.get('/', async (req, res, next) => {
44
35
  // production shape the web app consumes — useSettings.js reads
45
36
  // `preloaded.user` and downstream code branches on `_id`.
46
37
  const session = readSurphSession()
47
- const user = session?.userId? await fetchUser(session.username) : null
38
+ const { user, contexts, tides, tools } = session?.userId? await userConfiguration({ id: session.userId }) : null
39
+ const creator = {
40
+ _id: user._id,
41
+ username: user.username,
42
+ avatar: user.avatar
43
+ }
44
+
45
+ const localTools = toolSlugs.map(slug => {
46
+ const tool = manifest[slug]
47
+ let isTideTool = false
48
+ tides.forEach(t => {
49
+ if (!t.tools)
50
+ return
51
+
52
+ if (t.tools.indexOf(slug) > -1) {
53
+ isTideTool = true
54
+ }
55
+ })
56
+
57
+ return isTideTool ? { ...tool, creator, origin: 'tide' } : { ...tool, creator }
58
+ })
48
59
 
49
60
  const context = {
50
61
  env: 'sandbox',
51
62
  mode: 'desktop',
52
- config: { ...req.config, entry: 'web' },
53
- tools: toolSlugs.map(slug => (manifest[slug])),
54
63
  user,
64
+ config: { ...req.config, entry: 'web' },
65
+ contexts,
66
+ tides,
67
+ tools: localTools // sdk mode is for local tool development only, do not clutter with live tools.
68
+ // tools: [ ...tools, ...localTools ]
55
69
  }
56
70
 
57
71
  res.render('index', {
@@ -66,4 +80,81 @@ router.get('/', async (req, res, next) => {
66
80
  }
67
81
  })
68
82
 
83
+
84
+ // Only surph-bucket URLs are fetchable via `?src`. Prevents SSRF (blind
85
+ // requests to internal metadata endpoints, etc). Keep this narrow — widening
86
+ // the allowlist is trivial later; narrowing after external URLs go live is not.
87
+ const TEMPLATE_SRC_ORIGIN = 'https://surph.s3.us-east-1.amazonaws.com/'
88
+
89
+ router.get('/template/:tide/:slug', async (req, res, next) => {
90
+ try {
91
+ const resp = await fetch(`${SURPH_S3_URL}/tides/${req.params.tide}/templates/${req.params.slug}.json`, {
92
+ method: 'GET',
93
+ headers: { 'Content-Type': 'application/json' },
94
+ })
95
+
96
+ if (!resp.ok) {
97
+ throw new Error(`HTTP ${resp.status}: ${resp.statusText}`)
98
+ }
99
+
100
+ const template = await resp.json();
101
+ // Optional static-render mode: `?src=<s3-json-url>` fetches the JSON
102
+ // server-side and splices it into `template.sampleData`, which the
103
+ // existing canvas.html hydration path already reads. Turns any authored
104
+ // template into a URL-driven static view with zero component changes.
105
+ // Fetch runs server-side so no CORS setup on S3, and the origin allowlist
106
+ // above blocks SSRF.
107
+ const src = typeof req.query?.src === 'string' ? req.query.src : null;
108
+ if (src) {
109
+ if (!src.startsWith(TEMPLATE_SRC_ORIGIN)) {
110
+ res.status(400).json({
111
+ success: false,
112
+ error: `src must start with ${TEMPLATE_SRC_ORIGIN}`,
113
+ });
114
+ return;
115
+ }
116
+ try {
117
+ const dataResp = await fetch(src, {
118
+ method: 'GET',
119
+ headers: { 'Accept': 'application/json' },
120
+ });
121
+ if (dataResp.ok) {
122
+ const data = await dataResp.json();
123
+ if (data && typeof data === 'object' && !Array.isArray(data)) {
124
+ template.sampleData = data;
125
+ }
126
+ } else {
127
+ console.warn(`[template] src fetch non-OK: HTTP ${dataResp.status} for ${src}`);
128
+ }
129
+ } catch (err) {
130
+ console.warn(`[template] src fetch failed for ${src}: ${err?.message}`);
131
+ }
132
+ // Whole point of ?src is freshness — never cache the host page.
133
+ res.set('Cache-Control', 'no-store');
134
+ }
135
+
136
+ // Mustache renders objects as `[object Object]`, so pre-stringify for the
137
+ // inline script hydration. Escape `<` to `\u003c` so a stray `</script>`
138
+ // in template.code (arbitrary user JS) can't terminate the script tag.
139
+ const templateJson = JSON.stringify(template).replace(/</g, '\\u003c');
140
+ const tideSlug = JSON.stringify(req.params.tide);
141
+
142
+ res.render('canvas', {
143
+ template,
144
+ templateJson,
145
+ tideSlug,
146
+ CDN: 'https://surph.s3.us-east-1.amazonaws.com/webapp/static'
147
+ // pickerSource: getCanvasPickerSource()
148
+ })
149
+ } catch (error) {
150
+ console.error('Error fetching template: ', error.message)
151
+
152
+ res.status(500).json({
153
+ success: false,
154
+ error: error.message,
155
+ })
156
+ }
157
+ })
158
+
159
+
69
160
  module.exports = router
@@ -0,0 +1,28 @@
1
+ const { Router } = require('express');
2
+ const router = Router();
3
+ const SURPH_RPC_URL = 'https://rpc.surph.ai';
4
+ // const SURPH_RPC_URL = 'http://localhost:5000' // for local testing
5
+ const PATH_RE = /^\/[a-zA-Z0-9_\-/]+$/;
6
+ router.post('*', async (req, res) => {
7
+ const path = req.path;
8
+ if (!PATH_RE.test(path) || path.includes('//') || path.includes('..')) {
9
+ res.status(400).json({ error: 'invalid path' });
10
+ return;
11
+ }
12
+ try {
13
+ const upstream = await fetch(`${SURPH_RPC_URL}${path}`, {
14
+ method: 'POST',
15
+ headers: { 'Content-Type': 'application/json' },
16
+ body: JSON.stringify(req.body || {}),
17
+ });
18
+ const body = await upstream.text();
19
+ const contentType = upstream.headers.get('content-type');
20
+ if (contentType)
21
+ res.set('Content-Type', contentType);
22
+ res.status(upstream.status).send(body);
23
+ }
24
+ catch (e) {
25
+ res.status(502).json({ error: e?.message || 'upstream RPC failed' });
26
+ }
27
+ });
28
+ module.exports = router;
@@ -0,0 +1,212 @@
1
+ <!DOCTYPE html>
2
+ <html dir="ltr" lang="en-US">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
7
+ <meta http-equiv="Content-Language" content="en" />
8
+
9
+ <title>{{ template.name }}</title>
10
+ <link rel="shortcut icon" href="https://surph.s3.us-east-1.amazonaws.com/admin/assets/icon-green.svg">
11
+
12
+ <!-- Keep in sync with TAILWIND_STYLESHEET in src/features/tide/components/TemplateCanvas.jsx -->
13
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css">
14
+
15
+ <style>
16
+ html, body {
17
+ margin: 0;
18
+ padding: 0;
19
+ min-height: 100vh;
20
+ background: #ffffff;
21
+ font-family: system-ui, -apple-system, Inter, Arial, sans-serif;
22
+ color: #1e0a3c;
23
+ }
24
+ #canvas-mount {
25
+ max-width: 100%;
26
+ margin: 0px auto;
27
+ padding: 0 0px 0px 0;
28
+ }
29
+ #canvas-error {
30
+ max-width: 100%;
31
+ margin: 40px auto;
32
+ padding: 16px;
33
+ background: #fff;
34
+ border: 1px solid #e5b5b5;
35
+ border-radius: 8px;
36
+ color: #a03030;
37
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
38
+ font-size: 0.85rem;
39
+ white-space: pre-wrap;
40
+ }
41
+ </style>
42
+ </head>
43
+ <body>
44
+ <div id="canvas-mount"></div>
45
+
46
+ <script type="text/javascript">
47
+ window.__TEMPLATE__ = {{{ templateJson }}};
48
+ window.__CANVAS_TIDE__ = {{{ tideSlug }}};
49
+ </script>
50
+ <script type="text/javascript">
51
+ (function () {
52
+ var mount = document.getElementById('canvas-mount');
53
+ var showError = function (msg) {
54
+ var box = document.createElement('div');
55
+ box.id = 'canvas-error';
56
+ box.textContent = msg;
57
+ mount.parentNode.insertBefore(box, mount);
58
+ };
59
+
60
+ var template = window.__TEMPLATE__;
61
+ if (!template || !template.slug || !template.code) {
62
+ showError('Missing template payload.');
63
+ return;
64
+ }
65
+
66
+ // Evaluate template.code in a fresh function scope. The code registers
67
+ // a factory at `window.__tideFactories[template.slug]` — no DOM side
68
+ // effects until we call the factory below.
69
+ try {
70
+ (new Function(template.code))();
71
+ } catch (err) {
72
+ console.error('[canvas] template code failed:', err);
73
+ showError('Template code failed to evaluate: ' + (err && err.message ? err.message : err));
74
+ return;
75
+ }
76
+
77
+ var factory = window.__tideFactories && window.__tideFactories[template.slug];
78
+ if (typeof factory !== 'function') {
79
+ showError('Template slug "' + template.slug + '" did not register a factory. Expected `window.__tideFactories["' + template.slug + '"]` to be a function.');
80
+ return;
81
+ }
82
+
83
+ // Build the container div, stamp all fetch/scope attrs on it BEFORE
84
+ // calling the factory. The factory reads these attributes inside
85
+ // `_fetchState` to decide between "seed from initialState" (draft /
86
+ // preview) and "fetch from S3" (live). Board slug wins over user id
87
+ // when both are present — server-side saveTemplateData uses the same
88
+ // board-wins rule.
89
+ var container = document.createElement('div');
90
+ container.id = 'tpl-' + template.slug;
91
+ container.setAttribute('data-tpl', template.slug);
92
+
93
+ var qs = new URLSearchParams(window.location.search);
94
+ var userId = qs.get('u');
95
+ var boardSlug = qs.get('b');
96
+ var tideParam = qs.get('t') || (window.__CANVAS_TIDE__ || null);
97
+ // Live mode: template will self-fetch from S3. Test mode counts as live
98
+ // (author's own user id is present) — sampleData must not leak in either.
99
+ // Draft/preview (no user, no board) is the only mode where sampleData is
100
+ // the intended first-open content.
101
+ var isLiveMode = !!(tideParam && template.dataKey && (boardSlug || userId));
102
+ if (isLiveMode) {
103
+ container.setAttribute('data-tide-slug', tideParam);
104
+ container.setAttribute('data-data-key', template.dataKey);
105
+ container.setAttribute('data-template-slug', template.slug);
106
+ if (userId) container.setAttribute('data-user-id', userId);
107
+ if (boardSlug) container.setAttribute('data-board-slug', boardSlug);
108
+ }
109
+ // For tides with dynamicContext: which subscribed context is currently
110
+ // selected in the parent chat. RPC handlers read it via
111
+ // `root.getAttribute('data-active-context-slug')` when they need to
112
+ // scope a call to whatever context the user is looking at.
113
+ var activeContextSlug = qs.get('c');
114
+ if (activeContextSlug) container.setAttribute('data-active-context-slug', activeContextSlug);
115
+
116
+ mount.appendChild(container);
117
+
118
+ var initialState = (!isLiveMode && template.sampleData && typeof template.sampleData === 'object') ? template.sampleData : {};
119
+ var instance;
120
+ try {
121
+ instance = factory(container, initialState);
122
+ } catch (err) {
123
+ console.error('[canvas] factory failed:', err);
124
+ showError('Template factory failed to run: ' + (err && err.message ? err.message : err));
125
+ return;
126
+ }
127
+ if (!instance || typeof instance.setState !== 'function') {
128
+ showError('Template factory did not return a valid instance handle.');
129
+ return;
130
+ }
131
+ // One instance per canvas iframe. Parent bridges through here.
132
+ window.__tide = instance;
133
+
134
+ // ---------------------------------------------------------------
135
+ // postMessage adapter — parent ↔ template bridge
136
+ // ---------------------------------------------------------------
137
+ // Convention:
138
+ // parent → template: postMessage({ kind: 'surph-tide-message', payload })
139
+ // → calls `window.__tide.setState(payload)`. Also dispatched as a
140
+ // `surph-tide-message` CustomEvent on the container for templates
141
+ // that want non-state reactions.
142
+ // template → parent: container dispatches a bubbling `surph-event`
143
+ // (via the composer's dispatchSurphEvent helper) → forwarded to
144
+ // parent as postMessage({ kind: 'surph-tide-event', slug, detail }).
145
+ //
146
+ // Origin is locked to the canvas's own origin — parent and canvas are
147
+ // served from the same host today. Broaden if that changes.
148
+ var TRUSTED_ORIGIN = window.location.origin;
149
+ var parentTarget =
150
+ (window.opener && window.opener !== window) ? window.opener :
151
+ (window.parent && window.parent !== window) ? window.parent :
152
+ null;
153
+
154
+ window.addEventListener('message', function (event) {
155
+ if (event.origin !== TRUSTED_ORIGIN) return;
156
+ var data = event.data;
157
+ if (!data) return;
158
+
159
+ // Refresh signal from parent — saveTemplateData just wrote to this
160
+ // template's data file and the parent is telling us to re-read it.
161
+ // Filter by dataKey so an invalidation for a sibling template on the
162
+ // same tide doesn't refetch us.
163
+ if (data.kind === 'surph-tide-refresh') {
164
+ if (data.dataKey && template.dataKey && data.dataKey !== template.dataKey) return;
165
+ if (typeof instance.refresh === 'function') {
166
+ try { instance.refresh(); }
167
+ catch (err) { console.error('[canvas] refresh() failed:', err); }
168
+ }
169
+ return;
170
+ }
171
+
172
+ if (data.kind !== 'surph-tide-message') return;
173
+ var payload = data.payload != null ? data.payload : {};
174
+ // Canonical hydration path: replace the mount's state via setState.
175
+ // This is the ONLY thing that overrides the sampleData/fetched
176
+ // initial state — if we relied on the template to opt in via a
177
+ // custom event listener, any template that forgot to listen would
178
+ // render sampleData forever.
179
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
180
+ try { instance.setState(payload); }
181
+ catch (err) { console.error('[canvas] setState from surph-tide-message failed:', err); }
182
+ }
183
+ container.dispatchEvent(new CustomEvent('surph-tide-message', {
184
+ detail: payload,
185
+ bubbles: true,
186
+ }));
187
+ });
188
+
189
+ // `surph-event` bubbles from the container to window (no shadow-DOM
190
+ // boundary anymore). Listen on window to catch any re-parenting.
191
+ window.addEventListener('surph-event', function (event) {
192
+ if (!parentTarget) return;
193
+ parentTarget.postMessage({
194
+ kind: 'surph-tide-event',
195
+ slug: template.slug,
196
+ detail: event.detail != null ? event.detail : {}
197
+ }, TRUSTED_ORIGIN);
198
+ });
199
+
200
+ // Ready handshake — lets the parent know the template is mounted and
201
+ // listening. Parent can queue outbound messages until this arrives.
202
+ if (parentTarget) {
203
+ parentTarget.postMessage({
204
+ kind: 'surph-tide-ready',
205
+ slug: template.slug
206
+ }, TRUSTED_ORIGIN);
207
+ }
208
+ })();
209
+ </script>
210
+ <script src="{{{CDN}}}/js/canvasPickerSource.js"></script>
211
+ </body>
212
+ </html>
@@ -0,0 +1,29 @@
1
+ const REST_API_URL = 'https://surph-mongo-http-19075ff8d93f.herokuapp.com'
2
+
3
+ // fetch user contexts, threads, tides, etc.
4
+ const userConfiguration = async (user, includeTides = true) => {
5
+ try {
6
+ const userId = user?._id || user?.id || null;
7
+ if (!userId) return null;
8
+
9
+ const response = await fetch(`${REST_API_URL}/queries/user_configuration`, {
10
+ method: 'POST',
11
+ body: JSON.stringify({ user: userId}),
12
+ headers: {
13
+ 'Accept': 'application/json',
14
+ 'Content-type': 'application/json',
15
+ 'x-surph-client': 'surph-webapp',
16
+ }
17
+ })
18
+
19
+ const { payload } = await response.json()
20
+ const { user: _user, contexts, tides, tools } = payload
21
+ // console.log('PAYLOAD: ' + JSON.stringify({ contexts, tides, tools }))
22
+
23
+ return { user: _user, contexts, tides, tools }
24
+ } catch (error) {
25
+ throw error
26
+ }
27
+ }
28
+
29
+ module.exports = userConfiguration;