@fungi.computer/oyster 0.4.1 → 0.6.0

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.
Files changed (86) hide show
  1. package/README.md +46 -8
  2. package/dist/agents.d.ts +2 -1
  3. package/dist/agents.js +67 -8
  4. package/dist/apps/gate.d.ts +4 -0
  5. package/dist/apps/gate.js +3 -0
  6. package/dist/auth.d.ts +24 -1
  7. package/dist/auth.js +38 -0
  8. package/dist/boot-host-script.js +1 -1
  9. package/dist/build/candidate-cli.mjs +27 -5
  10. package/dist/channels/binding.d.ts +70 -0
  11. package/dist/channels/binding.js +97 -0
  12. package/dist/channels/conversations.d.ts +246 -0
  13. package/dist/channels/conversations.js +705 -0
  14. package/dist/channels/ids.d.ts +8 -0
  15. package/dist/channels/ids.js +15 -0
  16. package/dist/channels/visitor-channel.d.ts +53 -0
  17. package/dist/channels/visitor-channel.js +124 -0
  18. package/dist/channels/visitor-edge.d.ts +115 -0
  19. package/dist/channels/visitor-edge.js +582 -0
  20. package/dist/channels/visitor-request.d.ts +7 -0
  21. package/dist/channels/visitor-request.js +17 -0
  22. package/dist/channels/visitor-stream-slots.d.ts +2 -0
  23. package/dist/channels/visitor-stream-slots.js +33 -0
  24. package/dist/channels/visitors.d.ts +47 -0
  25. package/dist/channels/visitors.js +181 -0
  26. package/dist/cloudflare.d.ts +8 -2
  27. package/dist/cloudflare.js +27 -1
  28. package/dist/computer-target.d.ts +5 -0
  29. package/dist/computer-target.js +11 -0
  30. package/dist/host.d.ts +7 -4
  31. package/dist/host.js +96 -17
  32. package/dist/internal/host-gate.d.ts +6 -4
  33. package/dist/internal/host-gate.js +14 -7
  34. package/dist/invocation.d.ts +11 -4
  35. package/dist/invocation.js +134 -91
  36. package/dist/keys.d.ts +4 -1
  37. package/dist/keys.js +15 -4
  38. package/dist/machine-link/authority.d.ts +7 -0
  39. package/dist/machine-link/delivery.d.ts +12 -0
  40. package/dist/machine-link/delivery.js +22 -1
  41. package/dist/machine-link/disposition.d.ts +16 -0
  42. package/dist/machine-link/disposition.js +65 -0
  43. package/dist/machine-link/host.d.ts +11 -2
  44. package/dist/machine-link/host.js +125 -5
  45. package/dist/machine-link/link.d.ts +4 -0
  46. package/dist/machine-link/link.js +47 -1
  47. package/dist/page/capabilities.js +1 -1
  48. package/dist/page/child-selection.d.ts +27 -0
  49. package/dist/page/child-selection.js +38 -0
  50. package/dist/page/mount.js +1 -1
  51. package/dist/page-script.js +4 -4
  52. package/dist/release.d.ts +13 -0
  53. package/dist/release.js +10 -0
  54. package/dist/request-origin.d.ts +4 -0
  55. package/dist/request-origin.js +10 -0
  56. package/dist/standalone/agent-mail.js +4 -1
  57. package/dist/standalone/agents.d.ts +8 -0
  58. package/dist/standalone/agents.js +30 -11
  59. package/dist/standalone/channel-session-door.d.ts +4 -0
  60. package/dist/standalone/channel-session-door.js +72 -0
  61. package/dist/standalone/computer-fleet.js +7 -1
  62. package/dist/standalone/machine-link.d.ts +2 -0
  63. package/dist/standalone/machine-link.js +34 -1
  64. package/dist/standalone/tenancy.d.ts +3 -0
  65. package/dist/standalone/tenancy.js +7 -2
  66. package/dist/testing/capabilities.d.ts +2 -0
  67. package/dist/testing/capabilities.js +3 -1
  68. package/dist/turnstile.d.ts +27 -0
  69. package/dist/turnstile.js +51 -0
  70. package/dist/workers-git.js +21 -2
  71. package/dist/workers.d.ts +3 -0
  72. package/dist/workers.js +15 -12
  73. package/docs/api/agents.md +8 -4
  74. package/docs/api/apps.md +8 -4
  75. package/docs/api/auth.md +55 -0
  76. package/docs/api/browser.md +40 -16
  77. package/docs/api/build-contract.md +2 -2
  78. package/docs/api/cloudflare.md +26 -1
  79. package/docs/api/host.md +22 -13
  80. package/docs/api/invocation.md +7 -3
  81. package/docs/api/keys.md +25 -3
  82. package/docs/api/workers.md +38 -30
  83. package/docs/site/agent-quickstart.mdx +29 -2
  84. package/examples/standalone/wrangler.celld.jsonc +66 -0
  85. package/examples/standalone/wrangler.jsonc +3 -4
  86. package/package.json +53 -17
@@ -0,0 +1,582 @@
1
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
2
+ if (value !== null && value !== void 0) {
3
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
4
+ var dispose, inner;
5
+ if (async) {
6
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
7
+ dispose = value[Symbol.asyncDispose];
8
+ }
9
+ if (dispose === void 0) {
10
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
11
+ dispose = value[Symbol.dispose];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
15
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
16
+ env.stack.push({ value: value, dispose: dispose, async: async });
17
+ }
18
+ else if (async) {
19
+ env.stack.push({ async: true });
20
+ }
21
+ return value;
22
+ };
23
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
24
+ return function (env) {
25
+ function fail(e) {
26
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
27
+ env.hasError = true;
28
+ }
29
+ var r, s = 0;
30
+ function next() {
31
+ while (r = env.stack.pop()) {
32
+ try {
33
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
34
+ if (r.dispose) {
35
+ var result = r.dispose.call(r.value);
36
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
37
+ }
38
+ else s |= 1;
39
+ }
40
+ catch (e) {
41
+ fail(e);
42
+ }
43
+ }
44
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
45
+ if (env.hasError) throw env.error;
46
+ }
47
+ return next();
48
+ };
49
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
50
+ var e = new Error(message);
51
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
52
+ });
53
+ import { Hono } from "hono";
54
+ import { html } from "hono/html";
55
+ import * as Schema from "effect/Schema";
56
+ import * as Option from "effect/Option";
57
+ import { z } from "zod";
58
+ import { sameOriginReadRequest, sameOriginRequest } from "../request-origin.js";
59
+ import { channelIds } from "./ids.js";
60
+ import { originAllowed } from "./binding.js";
61
+ import { visitorBearerSchema } from "./visitors.js";
62
+ import { visitorRequestSchema } from "./visitor-request.js";
63
+ /**
64
+ * The public web-channel edge. The host owns address lookup, visitor identity
65
+ * and Agent selection. Current conversation operations authenticate their
66
+ * bearer before asking the host to resolve an Agent for that visitor. The
67
+ * retained legacy GET /conversation keeps its site-owner path.
68
+ */
69
+ const siteKeySchema = z
70
+ .string()
71
+ .refine((value) => channelIds.site.is(value));
72
+ const messageSchema = visitorRequestSchema.extend({
73
+ text: z.string().trim().min(1).max(16_000),
74
+ });
75
+ const refreshSchema = z.strictObject({ refreshSecret: visitorBearerSchema });
76
+ const sessionReadSchema = z
77
+ .strictObject({
78
+ limit: z.number().int().min(1).max(200).optional(),
79
+ before: z.string().min(1).max(2048).optional(),
80
+ })
81
+ .transform((value) => {
82
+ if (value.limit === undefined)
83
+ return value.before === undefined ? {} : { before: value.before };
84
+ if (value.before === undefined)
85
+ return { limit: value.limit };
86
+ return { limit: value.limit, before: value.before };
87
+ });
88
+ const decodeJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json));
89
+ const DENIAL_STATUS = {
90
+ unmapped: 404,
91
+ suspended: 403,
92
+ banned: 403,
93
+ too_long: 413,
94
+ party_daily_limit: 429,
95
+ new_party_limit: 429,
96
+ rate_limited: 429,
97
+ address_limit: 429,
98
+ stream_limit: 429,
99
+ event_conflict: 409,
100
+ refresh_invalid: 401,
101
+ refresh_reused: 401,
102
+ };
103
+ function denied(reason) {
104
+ return Response.json({ error: reason }, { status: DENIAL_STATUS[reason], headers: { "cache-control": "no-store" } });
105
+ }
106
+ /** Browsers always send Origin on POST; only the embed document may call. */
107
+ function sameOrigin(context) {
108
+ return sameOriginRequest(context.req.raw);
109
+ }
110
+ function bearer(context) {
111
+ const header = context.req.header("authorization") ?? "";
112
+ const parsed = visitorBearerSchema.safeParse(header.startsWith("Bearer ") ? header.slice("Bearer ".length) : "");
113
+ return parsed.success && parsed.data.startsWith("vt_") ? parsed.data : null;
114
+ }
115
+ function intakeResponse(result) {
116
+ return result.kind === "accepted" || result.kind === "held"
117
+ ? Response.json(result.kind === "accepted"
118
+ ? {
119
+ status: "accepted",
120
+ commandId: result.commandId,
121
+ delivery: result.delivery,
122
+ }
123
+ : { status: "recorded", delivery: result.delivery }, { status: 202, headers: { "cache-control": "no-store" } })
124
+ : denied(result.reason);
125
+ }
126
+ /** Only exact embed-safe origins reach the CSP header. */
127
+ function frameAncestors(origins) {
128
+ return origins
129
+ .filter((origin) => /^https?:\/\/(\*\.)?[a-z0-9.:[\]-]+$/u.test(origin))
130
+ .join(" ");
131
+ }
132
+ /** Both values are already parsed: a site key and an optional `#rrggbb`. */
133
+ async function embedDocument(siteKey, facts, assets) {
134
+ return (await html `<!doctype html>
135
+ <html lang="en">
136
+ <head>
137
+ <meta charset="utf-8" />
138
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
139
+ <meta name="referrer" content="no-referrer" />
140
+ <title>${facts.name}</title>
141
+ <link rel="stylesheet" href="${assets("styles")}" />
142
+ </head>
143
+ <body class="caps-embed">
144
+ <div
145
+ id="fungi-embed"
146
+ data-site="${siteKey}"
147
+ data-accent="${facts.accent ?? ""}"
148
+ data-name="${facts.name}"
149
+ data-avatar="${JSON.stringify(facts.avatar)}"
150
+ ></div>
151
+ <script type="module" src="${assets("widget")}"></script>
152
+ </body>
153
+ </html>`).toString();
154
+ }
155
+ export function createVisitorEdge(host) {
156
+ async function route(context) {
157
+ const siteKey = siteKeySchema.safeParse(context.req.param("siteKey"));
158
+ if (!siteKey.success)
159
+ return null;
160
+ return host.resolveSite(context, siteKey.data);
161
+ }
162
+ async function visitorChannel(context, site, token) {
163
+ const visitor = await host.authenticateVisitor(context, site, token);
164
+ if (visitor === null)
165
+ return null;
166
+ return host.openChannel(context, site, visitor);
167
+ }
168
+ return new Hono()
169
+ .onError(host.onError)
170
+ .get("/embed/v1/s/:siteKey", async (context) => {
171
+ const env_1 = { stack: [], error: void 0, hasError: false };
172
+ try {
173
+ const site = await route(context);
174
+ if (site === null)
175
+ return context.text("Not found", 404);
176
+ const siteKey = context.req.param("siteKey");
177
+ const channels = __addDisposableResource(env_1, await host.openSite(context, site), false);
178
+ const facts = await channels.readChannelSite({
179
+ bindingId: site.bindingId,
180
+ siteKey,
181
+ });
182
+ if (facts === null)
183
+ return context.text("Not found", 404);
184
+ return new Response(await embedDocument(siteKey, facts, host.assetPath), {
185
+ headers: {
186
+ "content-type": "text/html; charset=utf-8",
187
+ "cache-control": "no-store",
188
+ "referrer-policy": "no-referrer",
189
+ "x-content-type-options": "nosniff",
190
+ "content-security-policy": [
191
+ "default-src 'none'",
192
+ "script-src 'self'",
193
+ "style-src 'self'",
194
+ "font-src 'self'",
195
+ "img-src 'self' data:",
196
+ "connect-src 'self'",
197
+ "base-uri 'none'",
198
+ "form-action 'none'",
199
+ `frame-ancestors ${frameAncestors(facts.origins) || "'none'"}`,
200
+ ].join("; "),
201
+ },
202
+ });
203
+ }
204
+ catch (e_1) {
205
+ env_1.error = e_1;
206
+ env_1.hasError = true;
207
+ }
208
+ finally {
209
+ __disposeResources(env_1);
210
+ }
211
+ })
212
+ .get("/api/embed/v1/sites/:siteKey/theme", async (context) => {
213
+ const env_2 = { stack: [], error: void 0, hasError: false };
214
+ try {
215
+ // Public chrome and an advisory address check. CSP still admits the frame.
216
+ const headers = {
217
+ "access-control-allow-origin": "*",
218
+ "cache-control": "no-store",
219
+ vary: "Origin",
220
+ };
221
+ const site = await route(context);
222
+ if (site === null)
223
+ return context.json({ error: "unmapped" }, 404, headers);
224
+ const channels = __addDisposableResource(env_2, await host.openSite(context, site), false);
225
+ const facts = await channels.readChannelSite({
226
+ bindingId: site.bindingId,
227
+ siteKey: context.req.param("siteKey"),
228
+ });
229
+ return facts === null
230
+ ? context.json({ error: "unmapped" }, 404, headers)
231
+ : context.json({
232
+ accent: facts.accent ?? null,
233
+ originAllowed: originAllowed(facts.origins, context.req.header("origin") ?? new URL(context.req.url).origin),
234
+ }, 200, headers);
235
+ }
236
+ catch (e_2) {
237
+ env_2.error = e_2;
238
+ env_2.hasError = true;
239
+ }
240
+ finally {
241
+ __disposeResources(env_2);
242
+ }
243
+ })
244
+ .post("/api/embed/v1/sites/:siteKey/visitors", async (context) => {
245
+ const env_3 = { stack: [], error: void 0, hasError: false };
246
+ try {
247
+ if (!sameOrigin(context))
248
+ return context.json({ error: "forbidden" }, 403);
249
+ // Validate the host's address-key configuration before touching owners.
250
+ const hash = await host.addressHash(context);
251
+ const site = await route(context);
252
+ if (site === null)
253
+ return denied("unmapped");
254
+ const channels = __addDisposableResource(env_3, await host.openSite(context, site), false);
255
+ const grant = await channels.mintChannelVisitor({
256
+ bindingId: site.bindingId,
257
+ siteKey: context.req.param("siteKey"),
258
+ addressHash: hash,
259
+ });
260
+ return grant.kind === "granted"
261
+ ? Response.json(grant.credentials, {
262
+ status: 201,
263
+ headers: { "cache-control": "no-store" },
264
+ })
265
+ : denied(grant.reason);
266
+ }
267
+ catch (e_3) {
268
+ env_3.error = e_3;
269
+ env_3.hasError = true;
270
+ }
271
+ finally {
272
+ __disposeResources(env_3);
273
+ }
274
+ })
275
+ .post("/api/embed/v1/sites/:siteKey/visitors/refresh", async (context) => {
276
+ const env_4 = { stack: [], error: void 0, hasError: false };
277
+ try {
278
+ if (!sameOrigin(context))
279
+ return context.json({ error: "forbidden" }, 403);
280
+ const site = await route(context);
281
+ if (site === null)
282
+ return denied("unmapped");
283
+ const decoded = await context.req.json().then((value) => ({
284
+ kind: "decoded",
285
+ result: refreshSchema.safeParse(value),
286
+ }), (cause) => ({ kind: "malformed", cause }));
287
+ if (decoded.kind === "malformed" || !decoded.result.success)
288
+ return context.json({ error: "invalid_input" }, 400);
289
+ const body = decoded.result;
290
+ const channels = __addDisposableResource(env_4, await host.openSite(context, site), false);
291
+ const grant = await channels.refreshChannelVisitor({
292
+ bindingId: site.bindingId,
293
+ refreshSecret: body.data.refreshSecret,
294
+ });
295
+ return grant.kind === "granted"
296
+ ? Response.json(grant.credentials, {
297
+ headers: { "cache-control": "no-store" },
298
+ })
299
+ : denied(grant.reason);
300
+ }
301
+ catch (e_4) {
302
+ env_4.error = e_4;
303
+ env_4.hasError = true;
304
+ }
305
+ finally {
306
+ __disposeResources(env_4);
307
+ }
308
+ })
309
+ .post("/api/embed/v1/sites/:siteKey/visitors/rotate", async (context) => {
310
+ const env_5 = { stack: [], error: void 0, hasError: false };
311
+ try {
312
+ if (!sameOrigin(context))
313
+ return context.json({ error: "forbidden" }, 403);
314
+ const token = bearer(context);
315
+ if (token === null)
316
+ return context.json({ error: "unauthorized" }, 401);
317
+ const site = await route(context);
318
+ if (site === null)
319
+ return denied("unmapped");
320
+ const channels = __addDisposableResource(env_5, await host.openSite(context, site), false);
321
+ const grant = await channels.rotateChannelVisitor({
322
+ bindingId: site.bindingId,
323
+ siteKey: context.req.param("siteKey"),
324
+ accessToken: token,
325
+ addressHash: await host.addressHash(context),
326
+ });
327
+ if (grant.kind === "unauthorized")
328
+ return context.json({ error: "unauthorized" }, 401);
329
+ return grant.kind === "granted"
330
+ ? Response.json(grant.credentials, {
331
+ status: 201,
332
+ headers: { "cache-control": "no-store" },
333
+ })
334
+ : denied(grant.reason);
335
+ }
336
+ catch (e_5) {
337
+ env_5.error = e_5;
338
+ env_5.hasError = true;
339
+ }
340
+ finally {
341
+ __disposeResources(env_5);
342
+ }
343
+ })
344
+ .post("/api/embed/v1/sites/:siteKey/messages", async (context) => {
345
+ const env_6 = { stack: [], error: void 0, hasError: false };
346
+ try {
347
+ if (!sameOrigin(context))
348
+ return context.json({ error: "forbidden" }, 403);
349
+ const token = bearer(context);
350
+ if (token === null)
351
+ return context.json({ error: "unauthorized" }, 401);
352
+ const site = await route(context);
353
+ if (site === null)
354
+ return denied("unmapped");
355
+ const raw = await context.req.text();
356
+ if (raw.length > 32_000)
357
+ return denied("too_long");
358
+ const json = decodeJson(raw);
359
+ if (Option.isNone(json))
360
+ return context.json({ error: "invalid_input" }, 400);
361
+ const body = messageSchema.safeParse(json.value);
362
+ if (!body.success)
363
+ return context.json({ error: "invalid_input" }, 400);
364
+ const opened = await visitorChannel(context, site, token);
365
+ if (opened === null)
366
+ return context.json({ error: "unauthorized" }, 401);
367
+ const channels = __addDisposableResource(env_6, opened, false);
368
+ const result = await channels.sendAsChannelVisitor({
369
+ bindingId: site.bindingId,
370
+ accessToken: token,
371
+ requestId: body.data.requestId,
372
+ text: body.data.text,
373
+ });
374
+ return result.kind === "unauthorized"
375
+ ? context.json({ error: "unauthorized" }, 401)
376
+ : intakeResponse(result);
377
+ }
378
+ catch (e_6) {
379
+ env_6.error = e_6;
380
+ env_6.hasError = true;
381
+ }
382
+ finally {
383
+ __disposeResources(env_6);
384
+ }
385
+ })
386
+ .post("/api/embed/v1/sites/:siteKey/requests/withdraw", async (context) => {
387
+ const env_7 = { stack: [], error: void 0, hasError: false };
388
+ try {
389
+ if (!sameOrigin(context))
390
+ return context.json({ error: "forbidden" }, 403);
391
+ const token = bearer(context);
392
+ if (token === null)
393
+ return context.json({ error: "unauthorized" }, 401);
394
+ const site = await route(context);
395
+ if (site === null)
396
+ return denied("unmapped");
397
+ const raw = await context.req.text();
398
+ if (raw.length > 2_048)
399
+ return denied("too_long");
400
+ const json = decodeJson(raw);
401
+ if (Option.isNone(json))
402
+ return context.json({ error: "invalid_input" }, 400);
403
+ const body = visitorRequestSchema.safeParse(json.value);
404
+ if (!body.success)
405
+ return context.json({ error: "invalid_input" }, 400);
406
+ const opened = await visitorChannel(context, site, token);
407
+ if (opened === null)
408
+ return context.json({ error: "unauthorized" }, 401);
409
+ const channels = __addDisposableResource(env_7, opened, false);
410
+ const result = await channels.withdrawAsChannelVisitor({
411
+ bindingId: site.bindingId,
412
+ accessToken: token,
413
+ requestId: body.data.requestId,
414
+ });
415
+ if ("kind" in result)
416
+ return result.kind === "unauthorized"
417
+ ? context.json({ error: "unauthorized" }, 401)
418
+ : denied(result.reason);
419
+ return Response.json(result, {
420
+ headers: { "cache-control": "no-store" },
421
+ });
422
+ }
423
+ catch (e_7) {
424
+ env_7.error = e_7;
425
+ env_7.hasError = true;
426
+ }
427
+ finally {
428
+ __disposeResources(env_7);
429
+ }
430
+ })
431
+ .get("/api/embed/v1/sites/:siteKey/conversation/pending/latest", async (context) => {
432
+ const env_8 = { stack: [], error: void 0, hasError: false };
433
+ try {
434
+ if (!sameOriginReadRequest(context.req.raw))
435
+ return context.json({ error: "forbidden" }, 403);
436
+ const token = bearer(context);
437
+ if (token === null)
438
+ return context.json({ error: "unauthorized" }, 401);
439
+ const site = await route(context);
440
+ if (site === null)
441
+ return denied("unmapped");
442
+ const opened = await visitorChannel(context, site, token);
443
+ if (opened === null)
444
+ return context.json({ error: "unauthorized" }, 401);
445
+ const channels = __addDisposableResource(env_8, opened, false);
446
+ const result = await channels.latestPendingAsChannelVisitor({
447
+ bindingId: site.bindingId,
448
+ accessToken: token,
449
+ });
450
+ if (result !== null && "kind" in result)
451
+ return context.json({ error: "unauthorized" }, 401);
452
+ return Response.json(result, {
453
+ headers: { "cache-control": "no-store" },
454
+ });
455
+ }
456
+ catch (e_8) {
457
+ env_8.error = e_8;
458
+ env_8.hasError = true;
459
+ }
460
+ finally {
461
+ __disposeResources(env_8);
462
+ }
463
+ })
464
+ .get("/api/embed/v1/sites/:siteKey/conversation/events", async (context) => {
465
+ const env_9 = { stack: [], error: void 0, hasError: false };
466
+ try {
467
+ if (!sameOriginReadRequest(context.req.raw))
468
+ return context.json({ error: "forbidden" }, 403);
469
+ const token = bearer(context);
470
+ if (token === null)
471
+ return context.json({ error: "unauthorized" }, 401);
472
+ const site = await route(context);
473
+ if (site === null)
474
+ return denied("unmapped");
475
+ const channel = await visitorChannel(context, site, token);
476
+ if (channel === null)
477
+ return context.json({ error: "unauthorized" }, 401);
478
+ const channels = __addDisposableResource(env_9, channel, false);
479
+ const opened = await channels.watchAsChannelVisitor({
480
+ bindingId: site.bindingId,
481
+ accessToken: token,
482
+ });
483
+ if (opened.kind === "unauthorized")
484
+ return context.json({ error: "unauthorized" }, 401);
485
+ if (opened.kind === "denied")
486
+ return denied(opened.reason);
487
+ if (opened.kind === "empty")
488
+ return context.body(null, 204);
489
+ return new Response(opened.body, {
490
+ headers: {
491
+ "content-type": "text/event-stream",
492
+ "cache-control": "no-store",
493
+ "x-accel-buffering": "no",
494
+ },
495
+ });
496
+ }
497
+ catch (e_9) {
498
+ env_9.error = e_9;
499
+ env_9.hasError = true;
500
+ }
501
+ finally {
502
+ __disposeResources(env_9);
503
+ }
504
+ })
505
+ .post("/api/embed/v1/sites/:siteKey/session", async (context) => {
506
+ const env_10 = { stack: [], error: void 0, hasError: false };
507
+ try {
508
+ if (!sameOrigin(context))
509
+ return context.json({ error: "forbidden" }, 403);
510
+ const token = bearer(context);
511
+ if (token === null)
512
+ return context.json({ error: "unauthorized" }, 401);
513
+ const site = await route(context);
514
+ if (site === null)
515
+ return denied("unmapped");
516
+ const raw = await context.req.text();
517
+ if (raw.length > 4096)
518
+ return context.json({ error: "invalid_input" }, 400);
519
+ const json = decodeJson(raw);
520
+ if (Option.isNone(json))
521
+ return context.json({ error: "invalid_input" }, 400);
522
+ const decoded = sessionReadSchema.safeParse(json.value);
523
+ if (!decoded.success)
524
+ return context.json({ error: "invalid_input" }, 400);
525
+ const channel = await visitorChannel(context, site, token);
526
+ if (channel === null)
527
+ return context.json({ error: "unauthorized" }, 401);
528
+ const channels = __addDisposableResource(env_10, channel, false);
529
+ const opened = await channels.openVisitorSession({
530
+ bindingId: site.bindingId,
531
+ accessToken: token,
532
+ options: decoded.data,
533
+ });
534
+ if (opened.kind === "unauthorized")
535
+ return context.json({ error: "unauthorized" }, 401);
536
+ if (opened.kind === "denied")
537
+ return denied(opened.reason);
538
+ return new Response(opened.snapshot, {
539
+ headers: {
540
+ "content-type": "application/json",
541
+ "cache-control": "no-store",
542
+ },
543
+ });
544
+ }
545
+ catch (e_10) {
546
+ env_10.error = e_10;
547
+ env_10.hasError = true;
548
+ }
549
+ finally {
550
+ __disposeResources(env_10);
551
+ }
552
+ })
553
+ .get("/api/embed/v1/sites/:siteKey/conversation", async (context) => {
554
+ const env_11 = { stack: [], error: void 0, hasError: false };
555
+ try {
556
+ const token = bearer(context);
557
+ if (token === null)
558
+ return context.json({ error: "unauthorized" }, 401);
559
+ const site = await route(context);
560
+ if (site === null)
561
+ return denied("unmapped");
562
+ // Retained legacy read: do not change its site-owner path in this slice.
563
+ const channels = __addDisposableResource(env_11, await host.openSite(context, site), false);
564
+ const result = await channels.readAsChannelVisitor({
565
+ bindingId: site.bindingId,
566
+ accessToken: token,
567
+ });
568
+ return result.kind === "unauthorized"
569
+ ? context.json({ error: "unauthorized" }, 401)
570
+ : Response.json(result.view, {
571
+ headers: { "cache-control": "no-store" },
572
+ });
573
+ }
574
+ catch (e_11) {
575
+ env_11.error = e_11;
576
+ env_11.hasError = true;
577
+ }
578
+ finally {
579
+ __disposeResources(env_11);
580
+ }
581
+ });
582
+ }
@@ -0,0 +1,7 @@
1
+ import { z } from "zod";
2
+ /** The original caller key, carried separately from channel and owner identities. */
3
+ export declare const visitorRequestSchema: z.ZodObject<{
4
+ requestId: z.ZodLazy<z.ZodString>;
5
+ }, z.core.$strict>;
6
+ /** Native hashing keeps the existing channel identity; it is never an inverse correlation. */
7
+ export declare function visitorMessageId(requestId: string): Promise<string>;
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ import { FungiMessageMetaSchema } from "@fungi.computer/agar/agents";
3
+ import { channelIds } from "./ids.js";
4
+ /** The original caller key, carried separately from channel and owner identities. */
5
+ export const visitorRequestSchema = z.strictObject({
6
+ requestId: FungiMessageMetaSchema.shape.sourceRequestId.unwrap(),
7
+ });
8
+ /** Native hashing keeps the existing channel identity; it is never an inverse correlation. */
9
+ export async function visitorMessageId(requestId) {
10
+ const input = visitorRequestSchema.parse({ requestId });
11
+ const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(input.requestId));
12
+ const suffix = BigInt(`0x${Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join("")}`)
13
+ .toString(36)
14
+ .padStart(21, "0")
15
+ .slice(0, 21);
16
+ return channelIds.message.schema.parse(`msg_${suffix}`);
17
+ }
@@ -0,0 +1,2 @@
1
+ /** Shared stream caps, independent of the host's Session watch and SSE pump. */
2
+ export declare function createVisitorStreamSlots(): (bindingId: string, visitorId: string, addressHash: string) => (() => void) | null;
@@ -0,0 +1,33 @@
1
+ /** Shared stream caps, independent of the host's Session watch and SSE pump. */
2
+ export function createVisitorStreamSlots() {
3
+ const visitors = new Map();
4
+ const bindings = new Map();
5
+ const addresses = new Map();
6
+ return (bindingId, visitorId, addressHash) => {
7
+ const address = bindingId + ":" + addressHash;
8
+ if ((visitors.get(visitorId) ?? 0) >= 2 ||
9
+ (bindings.get(bindingId) ?? 0) >= 32 ||
10
+ (addresses.get(address) ?? 0) >= 8)
11
+ return null;
12
+ const entries = [
13
+ [visitors, visitorId],
14
+ [bindings, bindingId],
15
+ [addresses, address],
16
+ ];
17
+ for (const [counts, key] of entries)
18
+ counts.set(key, (counts.get(key) ?? 0) + 1);
19
+ let released = false;
20
+ return () => {
21
+ if (released)
22
+ return;
23
+ released = true;
24
+ for (const [counts, key] of entries) {
25
+ const remaining = (counts.get(key) ?? 1) - 1;
26
+ if (remaining === 0)
27
+ counts.delete(key);
28
+ else
29
+ counts.set(key, remaining);
30
+ }
31
+ };
32
+ };
33
+ }