@luckydraw/cumulus 1.0.0 → 1.0.1
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/CHANGELOG.md +9 -0
- package/dist/gateway/adapters/webchat.d.ts +13 -0
- package/dist/gateway/adapters/webchat.d.ts.map +1 -1
- package/dist/gateway/adapters/webchat.js +56 -3
- package/dist/gateway/adapters/webchat.js.map +1 -1
- package/dist/gateway/static/blex-render.js +341 -0
- package/dist/gateway/static/chat.html +1 -0
- package/dist/gateway/static/widget.js +112 -127
- package/docs/web-app-agent-guide.md +40 -1
- package/examples/web-app-agent/README.md +161 -14
- package/examples/web-app-agent/gateway.config.example.json +22 -4
- package/examples/web-app-agent/public/agent/blex-mount.js +136 -0
- package/examples/web-app-agent/public/agent/bridge-mount.js +19 -3
- package/examples/web-app-agent/public/agent/commands.js +10 -4
- package/examples/web-app-agent/public/agent/device-thread.js +5 -5
- package/examples/web-app-agent/public/agent/panel.css +6 -0
- package/examples/web-app-agent/public/agent/panel.js +27 -4
- package/examples/web-app-agent/public/index.html +15 -0
- package/examples/web-app-agent/server.js +148 -11
- package/package.json +1 -1
|
@@ -98,6 +98,20 @@
|
|
|
98
98
|
<ul class="notes" id="note-list" data-testid="note-list" data-loading="false"></ul>
|
|
99
99
|
</main>
|
|
100
100
|
|
|
101
|
+
<!-- Asset versions. server.js rewrites the empty map at serve time (stampHtml),
|
|
102
|
+
and stamps the src/href attributes below with the same hashes. The map
|
|
103
|
+
exists for assets that are loaded from inside JavaScript — panel.css and
|
|
104
|
+
the blex pair — because an HTML-level stamp cannot see a URL that only
|
|
105
|
+
appears in a script. Left as {} by a server that does not do the rewrite,
|
|
106
|
+
in which case agentAsset() returns URLs unchanged. -->
|
|
107
|
+
<script>
|
|
108
|
+
window.__AGENT_ASSET_V = {};
|
|
109
|
+
window.agentAsset = function (url) {
|
|
110
|
+
var v = window.__AGENT_ASSET_V[url];
|
|
111
|
+
return v ? url + '?v=' + v : url;
|
|
112
|
+
};
|
|
113
|
+
</script>
|
|
114
|
+
|
|
101
115
|
<!-- Load order matters: the registry and its dependencies must exist before
|
|
102
116
|
bridge-mount runs. bridge-mount is a module (it imports the cumulus
|
|
103
117
|
BridgeClient), so it is deferred automatically. -->
|
|
@@ -105,6 +119,7 @@
|
|
|
105
119
|
<script src="/agent/device-thread.js"></script>
|
|
106
120
|
<script src="/agent/commands.js"></script>
|
|
107
121
|
<script src="/agent/chat-client.js"></script>
|
|
122
|
+
<script src="/agent/blex-mount.js"></script>
|
|
108
123
|
<script src="/agent/panel.js"></script>
|
|
109
124
|
<script type="module" src="/agent/bridge-mount.js"></script>
|
|
110
125
|
</body>
|
|
@@ -101,6 +101,114 @@ function resolveBridgeDir() {
|
|
|
101
101
|
}
|
|
102
102
|
const BRIDGE_DIR = resolveBridgeDir();
|
|
103
103
|
|
|
104
|
+
/* ---- where the browser's blex renderer comes from -------------------------
|
|
105
|
+
Same rule as the bridge client above, and for a sharper reason than symmetry.
|
|
106
|
+
The obvious alternative — point a <script> at GATEWAY_ORIGIN + '/blex.min.js'
|
|
107
|
+
— is only correct for an app on a DIFFERENT origin from the gateway. The
|
|
108
|
+
common production shape is the opposite: GATEWAY_ORIGIN is your own hostname
|
|
109
|
+
and an edge (Caddy/Cloudflare) routes just `/bridge*` and `/api/thread/*` to
|
|
110
|
+
the gateway. There is no `/blex.min.js` on your hostname, so the cross-origin
|
|
111
|
+
recipe 404s and blex silently degrades to plain text — a failure that reads
|
|
112
|
+
like "my route is broken" rather than "my asset is missing". Serving the
|
|
113
|
+
assets ourselves is origin-agnostic and still not a vendored copy.
|
|
114
|
+
(Measured by @cdda, which hit exactly this.) */
|
|
115
|
+
function resolveStaticDir() {
|
|
116
|
+
const inRepo = path.join(ROOT, '..', '..', 'dist', 'gateway', 'static');
|
|
117
|
+
if (fs.existsSync(path.join(inRepo, 'blex-render.js'))) return inRepo;
|
|
118
|
+
try {
|
|
119
|
+
const require = createRequire(import.meta.url);
|
|
120
|
+
return path.dirname(require.resolve('@luckydraw/cumulus/dist/gateway/static/blex-render.js'));
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const STATIC_DIR = resolveStaticDir();
|
|
126
|
+
|
|
127
|
+
/* ---- asset versioning -----------------------------------------------------
|
|
128
|
+
Without this, a CDN in front of your app decides how long your agent surface
|
|
129
|
+
stays stale. Measured at a real Cloudflare edge: it overrides an origin's
|
|
130
|
+
`no-cache` with `max-age=14400`, so an edit to commands.js — the file you
|
|
131
|
+
change most — can take four hours to reach a browser, and the panel's files
|
|
132
|
+
expire on independent clocks, so a visitor can hold two files from DIFFERENT
|
|
133
|
+
deploys. `?v=<content hash>` makes the URL change when the bytes change,
|
|
134
|
+
which no cache policy can override.
|
|
135
|
+
|
|
136
|
+
Computed at SERVE time, not build time: this kit has no build step, and its
|
|
137
|
+
blex/bridge assets come out of the installed cumulus package, so a build-time
|
|
138
|
+
hash would describe the wrong bytes after `npm i`. */
|
|
139
|
+
const assetStampCache = new Map();
|
|
140
|
+
|
|
141
|
+
/** First 8 hex of sha1, cached and invalidated on mtime+size — so an edit is
|
|
142
|
+
picked up with no restart. undefined if the file cannot be read. */
|
|
143
|
+
function assetVersion(file) {
|
|
144
|
+
if (!file) return undefined;
|
|
145
|
+
try {
|
|
146
|
+
const stat = fs.statSync(file);
|
|
147
|
+
const hit = assetStampCache.get(file);
|
|
148
|
+
if (hit && hit.mtimeMs === stat.mtimeMs && hit.size === stat.size) return hit.hash;
|
|
149
|
+
const hash = crypto.createHash('sha1').update(fs.readFileSync(file)).digest('hex').slice(0, 8);
|
|
150
|
+
assetStampCache.set(file, { hash, mtimeMs: stat.mtimeMs, size: stat.size });
|
|
151
|
+
return hash;
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** URL path -> file on disk, or null if it is not an asset we serve. ONE owner
|
|
158
|
+
for this mapping: the routes below and the stamp both go through it, so a
|
|
159
|
+
URL can never be versioned as one file and served as another. The basename
|
|
160
|
+
allowlists are the whole traversal defence for the two directories outside
|
|
161
|
+
PUBLIC — do not loosen them to a prefix match. */
|
|
162
|
+
function assetUrlToFile(pathname) {
|
|
163
|
+
if (pathname.startsWith('/agent/bridge-client/')) {
|
|
164
|
+
const name = path.basename(pathname);
|
|
165
|
+
if (!BRIDGE_DIR || !/^(client|protocol)\.js$/.test(name)) return null;
|
|
166
|
+
return path.join(BRIDGE_DIR, name);
|
|
167
|
+
}
|
|
168
|
+
// blex-chart.min.js is an OPT-IN companion global (blex.min.js inlines Chart.js
|
|
169
|
+
// and never fetches it). Nothing requests it today; it is allowlisted so an
|
|
170
|
+
// adopter who wants it can add one script tag instead of editing this server.
|
|
171
|
+
if (pathname.startsWith('/agent/blex/')) {
|
|
172
|
+
const name = path.basename(pathname);
|
|
173
|
+
if (!STATIC_DIR || !/^(blex\.min|blex-render|blex-chart\.min)\.js$/.test(name)) return null;
|
|
174
|
+
return path.join(STATIC_DIR, name);
|
|
175
|
+
}
|
|
176
|
+
const rel = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, '');
|
|
177
|
+
const file = path.join(PUBLIC, rel);
|
|
178
|
+
return file === PUBLIC || file.startsWith(PUBLIC + path.sep) ? file : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* Assets loaded from INSIDE JavaScript — a <link> built at runtime, a script
|
|
182
|
+
element appended by blex-mount. An HTML-level stamp cannot see a URL that
|
|
183
|
+
only exists in JS, so these get their hashes through the map below instead.
|
|
184
|
+
(This is the "a loader that fetches its own dependencies must propagate the
|
|
185
|
+
version token" case, applied where it is actually true.) */
|
|
186
|
+
const RUNTIME_LOADED = [
|
|
187
|
+
'/agent/panel.css',
|
|
188
|
+
'/agent/blex/blex.min.js',
|
|
189
|
+
'/agent/blex/blex-render.js',
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const ASSET_MAP_MARKER = 'window.__AGENT_ASSET_V = {};';
|
|
193
|
+
|
|
194
|
+
/** Stamp served HTML: `?v=` on every same-origin .js/.css src/href, plus the
|
|
195
|
+
version map for the runtime-loaded set. */
|
|
196
|
+
function stampHtml(html) {
|
|
197
|
+
const stamped = html.replace(
|
|
198
|
+
/(\s(?:src|href)=")(\/[^"?#]+\.(?:js|css))(")/g,
|
|
199
|
+
(m, pre, url, post) => {
|
|
200
|
+
const v = assetVersion(assetUrlToFile(url));
|
|
201
|
+
return v ? `${pre}${url}?v=${v}${post}` : m;
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
const map = {};
|
|
205
|
+
for (const url of RUNTIME_LOADED) {
|
|
206
|
+
const v = assetVersion(assetUrlToFile(url));
|
|
207
|
+
if (v) map[url] = v;
|
|
208
|
+
}
|
|
209
|
+
return stamped.replace(ASSET_MAP_MARKER, `window.__AGENT_ASSET_V = ${JSON.stringify(map)};`);
|
|
210
|
+
}
|
|
211
|
+
|
|
104
212
|
/* ---- sessions (in-memory; your app has real ones) ------------------------ */
|
|
105
213
|
const sessions = new Set();
|
|
106
214
|
|
|
@@ -141,14 +249,27 @@ function readBody(req) {
|
|
|
141
249
|
});
|
|
142
250
|
}
|
|
143
251
|
|
|
144
|
-
function sendFile(res, file) {
|
|
252
|
+
function sendFile(res, file, requestedVersion) {
|
|
145
253
|
fs.readFile(file, (err, buf) => {
|
|
146
254
|
if (err) {
|
|
147
255
|
res.writeHead(404);
|
|
148
256
|
return res.end('not found');
|
|
149
257
|
}
|
|
150
|
-
|
|
151
|
-
|
|
258
|
+
const ext = path.extname(file);
|
|
259
|
+
const body = ext === '.html' ? Buffer.from(stampHtml(buf.toString('utf8')), 'utf8') : buf;
|
|
260
|
+
// A ?v= that matches the CURRENT hash is content-addressed, so it is safe to
|
|
261
|
+
// cache forever. A stale or forged one must NOT be — that would pin today's
|
|
262
|
+
// bytes under a key that no longer describes them. HTML is never immutable:
|
|
263
|
+
// it is what carries the stamps.
|
|
264
|
+
const contentAddressed =
|
|
265
|
+
ext !== '.html' && requestedVersion !== undefined && requestedVersion === assetVersion(file);
|
|
266
|
+
res.writeHead(200, {
|
|
267
|
+
'content-type': MIME[ext] || 'application/octet-stream',
|
|
268
|
+
'cache-control': contentAddressed
|
|
269
|
+
? 'public, max-age=31536000, immutable'
|
|
270
|
+
: 'no-cache, must-revalidate',
|
|
271
|
+
});
|
|
272
|
+
res.end(body);
|
|
152
273
|
});
|
|
153
274
|
}
|
|
154
275
|
|
|
@@ -205,24 +326,38 @@ const server = http.createServer(async (req, res) => {
|
|
|
205
326
|
});
|
|
206
327
|
}
|
|
207
328
|
|
|
329
|
+
// Static assets. assetUrlToFile owns the URL -> disk mapping (and the
|
|
330
|
+
// allowlists for the two directories outside PUBLIC); the routes below only
|
|
331
|
+
// pick the right 404 message when it declines.
|
|
332
|
+
const version = url.searchParams.get('v') ?? undefined;
|
|
333
|
+
|
|
208
334
|
// Bridge client, served from the cumulus package rather than a local copy.
|
|
209
335
|
if (p.startsWith('/agent/bridge-client/')) {
|
|
210
|
-
const
|
|
211
|
-
if (!
|
|
336
|
+
const file = assetUrlToFile(p);
|
|
337
|
+
if (!file) {
|
|
212
338
|
res.writeHead(404);
|
|
213
339
|
return res.end('bridge client unavailable — build cumulus or npm i @luckydraw/cumulus');
|
|
214
340
|
}
|
|
215
|
-
return sendFile(res,
|
|
341
|
+
return sendFile(res, file, version);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Blex renderer + library, likewise served from the package.
|
|
345
|
+
if (p.startsWith('/agent/blex/')) {
|
|
346
|
+
const file = assetUrlToFile(p);
|
|
347
|
+
if (!file) {
|
|
348
|
+
res.writeHead(404);
|
|
349
|
+
return res.end('blex unavailable — build cumulus or npm i @luckydraw/cumulus');
|
|
350
|
+
}
|
|
351
|
+
return sendFile(res, file, version);
|
|
216
352
|
}
|
|
217
353
|
|
|
218
354
|
if (req.method === 'GET') {
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
if (!file.startsWith(PUBLIC)) {
|
|
355
|
+
const file = assetUrlToFile(p);
|
|
356
|
+
if (!file) {
|
|
222
357
|
res.writeHead(403);
|
|
223
358
|
return res.end('forbidden');
|
|
224
359
|
}
|
|
225
|
-
return sendFile(res, file);
|
|
360
|
+
return sendFile(res, file, version);
|
|
226
361
|
}
|
|
227
362
|
|
|
228
363
|
res.writeHead(404);
|
|
@@ -230,7 +365,9 @@ const server = http.createServer(async (req, res) => {
|
|
|
230
365
|
});
|
|
231
366
|
|
|
232
367
|
server.listen(PORT, '127.0.0.1', () => {
|
|
233
|
-
|
|
368
|
+
// The bound port, not the requested one — PORT=0 means "pick one for me".
|
|
369
|
+
const bound = server.address().port;
|
|
370
|
+
console.log(`demo app: http://127.0.0.1:${bound} (password: ${PASSWORD})`);
|
|
234
371
|
console.log(
|
|
235
372
|
`bridge client: ${BRIDGE_DIR ?? 'NOT FOUND — run `npm run build` in the cumulus repo'}`
|
|
236
373
|
);
|