@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 +1 -1
- package/toolserver/app.js +2 -0
- package/toolserver/routes/api.js +8 -2
- package/toolserver/routes/index.js +109 -18
- package/toolserver/routes/rpc.js +28 -0
- package/toolserver/templates/canvas.html +212 -0
- package/utils/userConfiguration.js +29 -0
package/package.json
CHANGED
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)
|
package/toolserver/routes/api.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
|
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
|
|
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;
|