@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,8 @@
1
+ /** Server- and widget-minted channel identities, shared by both Oyster hosts. */
2
+ export declare const channelIds: Readonly<{
3
+ readonly binding: import("@fungi.computer/cairn").IdCodec<"channel", "binding", 1>;
4
+ readonly site: import("@fungi.computer/cairn").IdCodec<"channel", "site", 1>;
5
+ readonly endpoint: import("@fungi.computer/cairn").IdCodec<"channel", "endpoint", 1>;
6
+ readonly visitor: import("@fungi.computer/cairn").IdCodec<"channel", "visitor", 1>;
7
+ readonly message: import("@fungi.computer/cairn").IdCodec<"channel", "message", 1>;
8
+ }>;
@@ -0,0 +1,15 @@
1
+ import { defineIdFamily } from "@fungi.computer/cairn/id";
2
+ /** Server- and widget-minted channel identities, shared by both Oyster hosts. */
3
+ export const channelIds = defineIdFamily({
4
+ family: "channel",
5
+ version: 1,
6
+ kinds: {
7
+ binding: { prefix: "chb" },
8
+ site: { prefix: "site_pk" },
9
+ /** The public, unguessable address of one incoming-events binding. */
10
+ endpoint: { prefix: "evt_ep" },
11
+ visitor: { prefix: "v" },
12
+ /** One visitor message; the widget mints it so a resend replays. */
13
+ message: { prefix: "msg" },
14
+ },
15
+ });
@@ -0,0 +1,53 @@
1
+ import { type ConversationBinding } from "./binding.js";
2
+ import { type ChannelConversations, type ChannelWithdrawResult, type InboundEvent, type IntakeResult, type SessionDoor, type VisitorPendingPrompt, type VisitorReadOptions } from "./conversations.js";
3
+ import type { ChannelVisitors, VisitorGrant } from "./visitors.js";
4
+ /**
5
+ * Web visitor sequencing only. The host owns authorization, its admission
6
+ * receipt and the Session implementation; identity and conversations can live
7
+ * in different stores (and, in standalone, different Agents).
8
+ */
9
+ export declare function createVisitorChannel(options: Readonly<{
10
+ binding: (bindingId: string) => ConversationBinding | null;
11
+ visitors: () => ChannelVisitors;
12
+ conversations: () => ChannelConversations;
13
+ sessionDoor: () => SessionDoor;
14
+ intake: (event: InboundEvent) => Promise<IntakeResult>;
15
+ acquireStream: (bindingId: string, visitorId: string, addressHash: string) => (() => void) | null;
16
+ now: () => number;
17
+ }>): {
18
+ mint: (bindingId: string, siteKey: string, addressHash: string) => Promise<VisitorGrant>;
19
+ refresh: (bindingId: string, secret: string) => Promise<VisitorGrant>;
20
+ rotate: (bindingId: string, siteKey: string, token: string, addressHash: string) => Promise<VisitorGrant | Readonly<{
21
+ kind: "unauthorized";
22
+ }>>;
23
+ send: (bindingId: string, token: string, requestId: string, text: string) => Promise<IntakeResult | Readonly<{
24
+ kind: "unauthorized";
25
+ }>>;
26
+ withdraw: (bindingId: string, token: string, rawRequestId: string) => Promise<ChannelWithdrawResult | Readonly<{
27
+ kind: "unauthorized";
28
+ }> | Readonly<{
29
+ kind: "denied";
30
+ reason: "event_conflict" | "new_party_limit" | "banned";
31
+ }>>;
32
+ latestPending: (bindingId: string, token: string) => Promise<VisitorPendingPrompt | null | Readonly<{
33
+ kind: "unauthorized";
34
+ }>>;
35
+ open: (bindingId: string, token: string, read: VisitorReadOptions) => Promise<Readonly<{
36
+ kind: "session";
37
+ snapshot: string;
38
+ }> | Readonly<{
39
+ kind: "unauthorized";
40
+ }> | Readonly<{
41
+ kind: "denied";
42
+ reason: "new_party_limit" | "banned";
43
+ }>>;
44
+ watch: (bindingId: string, token: string, canRead: (visitorId: string) => Promise<boolean>) => Promise<Readonly<{
45
+ kind: "stream";
46
+ body: ReadableStream<Uint8Array>;
47
+ }> | Readonly<{
48
+ kind: "empty" | "unauthorized";
49
+ }> | Readonly<{
50
+ kind: "denied";
51
+ reason: "suspended" | "stream_limit";
52
+ }>>;
53
+ };
@@ -0,0 +1,124 @@
1
+ import { partyLabel } from "./binding.js";
2
+ import { channelRequestId, } from "./conversations.js";
3
+ import { visitorMessageId, visitorRequestSchema } from "./visitor-request.js";
4
+ /**
5
+ * Web visitor sequencing only. The host owns authorization, its admission
6
+ * receipt and the Session implementation; identity and conversations can live
7
+ * in different stores (and, in standalone, different Agents).
8
+ */
9
+ export function createVisitorChannel(options) {
10
+ const active = (bindingId) => {
11
+ const binding = options.binding(bindingId);
12
+ return binding?.lifecycle === "active" &&
13
+ binding.selector.provider === "web"
14
+ ? binding
15
+ : null;
16
+ };
17
+ const authenticate = (bindingId, token) => options.visitors().authenticate(bindingId, token);
18
+ return {
19
+ mint: (bindingId, siteKey, addressHash) => options.visitors().mint(options.binding(bindingId), siteKey, addressHash),
20
+ refresh: (bindingId, secret) => options.visitors().refresh(options.binding(bindingId), secret),
21
+ rotate: async (bindingId, siteKey, token, addressHash) => {
22
+ const visitors = options.visitors();
23
+ const visitor = await visitors.authenticate(bindingId, token);
24
+ if (visitor === null)
25
+ return { kind: "unauthorized" };
26
+ visitors.forget(visitor.visitorId);
27
+ return await visitors.mint(options.binding(bindingId), siteKey, addressHash);
28
+ },
29
+ send: async (bindingId, token, requestId, text) => {
30
+ const visitor = await authenticate(bindingId, token);
31
+ if (visitor === null)
32
+ return { kind: "unauthorized" };
33
+ const { visitorId } = visitor;
34
+ return options.intake({
35
+ bindingId,
36
+ conversation: visitorId,
37
+ eventId: await visitorMessageId(requestId),
38
+ party: { kind: "visitor", visitorId },
39
+ text,
40
+ sourceRequestId: requestId,
41
+ });
42
+ },
43
+ withdraw: async (bindingId, token, rawRequestId) => {
44
+ const binding = active(bindingId);
45
+ const visitor = await authenticate(bindingId, token);
46
+ if (visitor === null || binding === null)
47
+ return { kind: "unauthorized" };
48
+ const { requestId } = visitorRequestSchema.parse({
49
+ requestId: rawRequestId,
50
+ });
51
+ const eventId = await visitorMessageId(requestId);
52
+ const conversations = options.conversations();
53
+ if (conversations.visitorEventConflict(bindingId, visitor.visitorId, eventId))
54
+ return { kind: "denied", reason: "event_conflict" };
55
+ const opened = await conversations.open(binding, visitor.visitorId, {
56
+ kind: "visitor",
57
+ visitorId: visitor.visitorId,
58
+ });
59
+ if (opened.kind === "denied")
60
+ return opened;
61
+ return options
62
+ .sessionDoor()
63
+ .withdraw(opened.conversation.sessionId, channelRequestId(binding.bindingId, eventId));
64
+ },
65
+ latestPending: async (bindingId, token) => {
66
+ const binding = active(bindingId);
67
+ const visitor = await authenticate(bindingId, token);
68
+ if (visitor === null || binding === null)
69
+ return { kind: "unauthorized" };
70
+ const { visitorId } = visitor;
71
+ const conversation = options.conversations().find(bindingId, visitorId);
72
+ if (conversation === null)
73
+ return null;
74
+ return await options.sessionDoor().latestPending(conversation.sessionId, {
75
+ id: visitorId,
76
+ name: partyLabel({ kind: "visitor", visitorId }),
77
+ });
78
+ },
79
+ open: async (bindingId, token, read) => {
80
+ const binding = active(bindingId);
81
+ const visitor = await authenticate(bindingId, token);
82
+ if (visitor === null || binding === null)
83
+ return { kind: "unauthorized" };
84
+ const { visitorId } = visitor;
85
+ const opened = await options.conversations().open(binding, visitorId, {
86
+ kind: "visitor",
87
+ visitorId,
88
+ });
89
+ if (opened.kind === "denied")
90
+ return opened;
91
+ return {
92
+ kind: "session",
93
+ snapshot: await options
94
+ .sessionDoor()
95
+ .participantDocument(opened.conversation.sessionId, visitorId, read),
96
+ };
97
+ },
98
+ watch: async (bindingId, token, canRead) => {
99
+ const visitor = await authenticate(bindingId, token);
100
+ if (visitor === null)
101
+ return { kind: "unauthorized" };
102
+ if (options.binding(bindingId)?.lifecycle !== "active")
103
+ return { kind: "denied", reason: "suspended" };
104
+ const { visitorId } = visitor;
105
+ const conversation = options.conversations().find(bindingId, visitorId);
106
+ if (conversation === null)
107
+ return { kind: "empty" };
108
+ const release = options.acquireStream(bindingId, visitorId, visitor.addressHash);
109
+ if (release === null)
110
+ return { kind: "denied", reason: "stream_limit" };
111
+ return {
112
+ kind: "stream",
113
+ body: await options
114
+ .sessionDoor()
115
+ .participantStream(conversation.sessionId, visitorId, {
116
+ expiresAt: visitor.expiresAt,
117
+ now: options.now,
118
+ release,
119
+ canRead: () => canRead(visitorId),
120
+ }),
121
+ };
122
+ },
123
+ };
124
+ }
@@ -0,0 +1,115 @@
1
+ import { Hono, type Context, type Env as HonoEnv } from "hono";
2
+ import type { ChannelWithdrawResult, IntakeResult, VisitorPendingPrompt, VisitorReadOptions } from "./conversations.js";
3
+ import { type VisitorGrant } from "./visitors.js";
4
+ import type { Avatar } from "@fungi.computer/sprite/avatar";
5
+ /** Public address facts the host supplies after looking up an embed site. */
6
+ export type VisitorSiteAddress = Readonly<{
7
+ bindingId: string;
8
+ }>;
9
+ /** Embed-safe site chrome returned by the host after binding admission. */
10
+ export type VisitorSiteFacts = Readonly<{
11
+ accent?: string;
12
+ name: string;
13
+ avatar: Avatar;
14
+ origins: readonly string[];
15
+ }>;
16
+ /** An opened host door, disposed after the edge has returned a response. */
17
+ export type VisitorEdgeChannel = Readonly<{
18
+ [Symbol.dispose]: () => void;
19
+ readChannelSite: (input: Readonly<{
20
+ bindingId: string;
21
+ siteKey: string;
22
+ }>) => Promise<VisitorSiteFacts | null>;
23
+ mintChannelVisitor: (input: Readonly<{
24
+ bindingId: string;
25
+ siteKey: string;
26
+ addressHash: string;
27
+ }>) => Promise<VisitorGrant>;
28
+ refreshChannelVisitor: (input: Readonly<{
29
+ bindingId: string;
30
+ refreshSecret: string;
31
+ }>) => Promise<VisitorGrant>;
32
+ rotateChannelVisitor: (input: Readonly<{
33
+ bindingId: string;
34
+ siteKey: string;
35
+ accessToken: string;
36
+ addressHash: string;
37
+ }>) => Promise<VisitorGrant | Readonly<{
38
+ kind: "unauthorized";
39
+ }>>;
40
+ sendAsChannelVisitor: (input: Readonly<{
41
+ bindingId: string;
42
+ accessToken: string;
43
+ requestId: string;
44
+ text: string;
45
+ }>) => Promise<IntakeResult | Readonly<{
46
+ kind: "unauthorized";
47
+ }>>;
48
+ withdrawAsChannelVisitor: (input: Readonly<{
49
+ bindingId: string;
50
+ accessToken: string;
51
+ requestId: string;
52
+ }>) => Promise<ChannelWithdrawResult | Readonly<{
53
+ kind: "unauthorized";
54
+ }> | Readonly<{
55
+ kind: "denied";
56
+ reason: "event_conflict" | "new_party_limit" | "banned";
57
+ }>>;
58
+ latestPendingAsChannelVisitor: (input: Readonly<{
59
+ bindingId: string;
60
+ accessToken: string;
61
+ }>) => Promise<VisitorPendingPrompt | null | Readonly<{
62
+ kind: "unauthorized";
63
+ }>>;
64
+ watchAsChannelVisitor: (input: Readonly<{
65
+ bindingId: string;
66
+ accessToken: string;
67
+ }>) => Promise<Readonly<{
68
+ kind: "stream";
69
+ body: ReadableStream<Uint8Array>;
70
+ }> | Readonly<{
71
+ kind: "empty";
72
+ }> | Readonly<{
73
+ kind: "unauthorized";
74
+ }> | Readonly<{
75
+ kind: "denied";
76
+ reason: "suspended" | "stream_limit";
77
+ }>>;
78
+ openVisitorSession: (input: Readonly<{
79
+ bindingId: string;
80
+ accessToken: string;
81
+ options: VisitorReadOptions;
82
+ }>) => Promise<Readonly<{
83
+ kind: "session";
84
+ snapshot: string;
85
+ }> | Readonly<{
86
+ kind: "unauthorized";
87
+ }> | Readonly<{
88
+ kind: "denied";
89
+ reason: "new_party_limit" | "banned";
90
+ }>>;
91
+ readAsChannelVisitor: (input: Readonly<{
92
+ bindingId: string;
93
+ accessToken: string;
94
+ }>) => Promise<Readonly<{
95
+ kind: "view";
96
+ view: unknown;
97
+ }> | Readonly<{
98
+ kind: "unauthorized";
99
+ }>>;
100
+ }>;
101
+ /**
102
+ * Address lookup and mint/refresh use the site owner; conversation work must
103
+ * receive a verified visitor first. A host may route different visitors to
104
+ * different Agents without teaching the conversation core about its registry.
105
+ */
106
+ export type VisitorEdgeHost<E extends HonoEnv, Site extends VisitorSiteAddress, Visitor extends object> = Readonly<{
107
+ resolveSite: (context: Context<E>, siteKey: string) => Promise<Site | null>;
108
+ openSite: (context: Context<E>, site: Site) => Promise<VisitorEdgeChannel>;
109
+ authenticateVisitor: (context: Context<E>, site: Site, token: string) => Promise<Visitor | null>;
110
+ openChannel: (context: Context<E>, site: Site, visitor: Visitor) => Promise<VisitorEdgeChannel>;
111
+ addressHash: (context: Context<E>) => Promise<string>;
112
+ assetPath: (kind: "widget" | "styles") => string;
113
+ onError: (error: Error, context: Context<E>) => Response | Promise<Response>;
114
+ }>;
115
+ export declare function createVisitorEdge<E extends HonoEnv, Site extends VisitorSiteAddress, Visitor extends object>(host: VisitorEdgeHost<E, Site, Visitor>): Hono<E>;