@fungi.computer/oyster 0.5.0 → 0.7.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 (185) hide show
  1. package/README.md +155 -63
  2. package/dist/agent-archive-projection.d.ts +8 -0
  3. package/dist/agent-archive-projection.js +49 -0
  4. package/dist/agent-schedules.d.ts +50 -0
  5. package/dist/agent-schedules.js +17 -0
  6. package/dist/agents.d.ts +35 -14
  7. package/dist/agents.js +97 -144
  8. package/dist/app-builds.d.ts +7 -2
  9. package/dist/app-builds.js +19 -2
  10. package/dist/approval-capability.d.ts +1 -1
  11. package/dist/approval-capability.js +11 -16
  12. package/dist/approval-home.d.ts +2 -1
  13. package/dist/approval-home.js +5 -3
  14. package/dist/apps/catalog.js +1 -1
  15. package/dist/apps/gate.d.ts +4 -7
  16. package/dist/apps/gate.js +14 -29
  17. package/dist/apps/index.d.ts +1 -1
  18. package/dist/apps/installation.d.ts +4 -42
  19. package/dist/apps/installation.js +7 -75
  20. package/dist/apps/operations.js +0 -10
  21. package/dist/apps/permissions.d.ts +18 -8
  22. package/dist/apps/permissions.js +7 -1
  23. package/dist/auth.js +9 -1
  24. package/dist/authorization.d.ts +307 -0
  25. package/dist/authorization.js +542 -0
  26. package/dist/boot-host-script.js +1 -1
  27. package/dist/build/build-jobs.d.ts +2 -2
  28. package/dist/build/candidate-builder.d.ts +2 -0
  29. package/dist/build/candidate-builder.js +5 -1
  30. package/dist/build/candidate-cli.js +3 -0
  31. package/dist/build/candidate-cli.mjs +67 -22
  32. package/dist/build/job-log.d.ts +23 -0
  33. package/dist/build/job-log.js +88 -0
  34. package/dist/build/local-build.d.ts +2 -0
  35. package/dist/build/local-build.js +11 -9
  36. package/dist/channels/binding.d.ts +8 -1
  37. package/dist/channels/binding.js +12 -0
  38. package/dist/channels/conversations.d.ts +16 -5
  39. package/dist/channels/visitor-channel.d.ts +53 -0
  40. package/dist/channels/visitor-channel.js +124 -0
  41. package/dist/channels/visitor-edge.d.ts +115 -0
  42. package/dist/channels/visitor-edge.js +582 -0
  43. package/dist/channels/visitor-request.d.ts +7 -0
  44. package/dist/channels/visitor-request.js +17 -0
  45. package/dist/channels/visitor-stream-slots.d.ts +2 -0
  46. package/dist/channels/visitor-stream-slots.js +33 -0
  47. package/dist/channels/visitors.d.ts +47 -0
  48. package/dist/channels/visitors.js +181 -0
  49. package/dist/cloudflare.d.ts +11 -2
  50. package/dist/cloudflare.js +27 -1
  51. package/dist/{page/computer-target.d.ts → computer-target.d.ts} +2 -0
  52. package/dist/{page/computer-target.js → computer-target.js} +3 -1
  53. package/dist/files.js +9 -8
  54. package/dist/git-admission.d.ts +3 -3
  55. package/dist/git-admission.js +4 -6
  56. package/dist/git-door.d.ts +8 -7
  57. package/dist/git-door.js +18 -17
  58. package/dist/git.d.ts +1 -1
  59. package/dist/host.d.ts +38 -42
  60. package/dist/host.js +182 -239
  61. package/dist/installation-execution.js +10 -5
  62. package/dist/installation-operations.d.ts +4 -180
  63. package/dist/installation-operations.js +1 -5
  64. package/dist/installation-store-generation.d.ts +4 -1
  65. package/dist/installation-store-generation.js +59 -6
  66. package/dist/internal/delivery.js +0 -1
  67. package/dist/internal/host-gate.d.ts +9 -6
  68. package/dist/internal/host-gate.js +19 -25
  69. package/dist/internal/invocation-timing.d.ts +16 -0
  70. package/dist/internal/invocation-timing.js +23 -0
  71. package/dist/internal/registry-installations.js +1 -0
  72. package/dist/invocation.d.ts +19 -11
  73. package/dist/invocation.js +171 -96
  74. package/dist/keys.d.ts +2 -19
  75. package/dist/keys.js +1 -37
  76. package/dist/mcp.d.ts +15 -27
  77. package/dist/mcp.js +105 -155
  78. package/dist/page/agent-browser.d.ts +15 -0
  79. package/dist/page/agent-browser.js +162 -0
  80. package/dist/page/app-permissions-port.d.ts +3 -0
  81. package/dist/page/app-permissions-port.js +43 -0
  82. package/dist/page/app-permissions.d.ts +11 -0
  83. package/dist/page/app-permissions.js +296 -0
  84. package/dist/page/capabilities.js +1 -1
  85. package/dist/page/child-selection.d.ts +27 -0
  86. package/dist/page/child-selection.js +38 -0
  87. package/dist/page/isolated-page-theme.d.ts +2 -0
  88. package/dist/page/isolated-page-theme.js +11 -0
  89. package/dist/page/mount.d.ts +2 -13
  90. package/dist/page/mount.js +8 -94
  91. package/dist/page-script.d.ts +0 -3
  92. package/dist/page-script.js +3 -4
  93. package/dist/release.d.ts +13 -0
  94. package/dist/release.js +10 -0
  95. package/dist/request-origin.d.ts +4 -0
  96. package/dist/request-origin.js +10 -0
  97. package/dist/standalone/agent-archive-owner.d.ts +42 -0
  98. package/dist/standalone/agent-archive-owner.js +116 -0
  99. package/dist/standalone/agent-archive-schedules.d.ts +6 -0
  100. package/dist/standalone/agent-archive-schedules.js +76 -0
  101. package/dist/standalone/agent-archive.d.ts +31 -0
  102. package/dist/standalone/agent-archive.js +67 -0
  103. package/dist/standalone/agent-computers.d.ts +7 -6
  104. package/dist/standalone/agent-computers.js +4 -7
  105. package/dist/standalone/agent-mail.d.ts +2 -2
  106. package/dist/standalone/agents.d.ts +157 -20
  107. package/dist/standalone/agents.js +242 -38
  108. package/dist/standalone/app-builds-binding.js +1 -1
  109. package/dist/standalone/app-set.js +2 -54
  110. package/dist/standalone/app-source.d.ts +3 -4
  111. package/dist/standalone/app-source.js +12 -9
  112. package/dist/standalone/builds.d.ts +2 -14
  113. package/dist/standalone/builds.js +15 -13
  114. package/dist/standalone/computer-fleet.d.ts +4 -4
  115. package/dist/standalone/computer-fleet.js +20 -43
  116. package/dist/standalone/computer.d.ts +4 -3
  117. package/dist/standalone/computer.js +52 -53
  118. package/dist/standalone/dashboard-approval.d.ts +10 -0
  119. package/dist/standalone/dashboard-approval.js +66 -0
  120. package/dist/standalone/machine-link.d.ts +4 -221
  121. package/dist/standalone/machine-link.js +21 -15
  122. package/dist/standalone/media.d.ts +1 -0
  123. package/dist/standalone/media.js +20 -14
  124. package/dist/standalone/owner-routes.d.ts +16 -0
  125. package/dist/standalone/owner-routes.js +21 -0
  126. package/dist/standalone/permission-resources.d.ts +5 -0
  127. package/dist/standalone/permission-resources.js +31 -0
  128. package/dist/standalone/registry.d.ts +7 -0
  129. package/dist/standalone/registry.js +3 -0
  130. package/dist/standalone/tenancy.js +3 -1
  131. package/dist/standalone/worker-routes.d.ts +19 -0
  132. package/dist/standalone/worker-routes.js +9 -0
  133. package/dist/standalone/worker.d.ts +1 -1
  134. package/dist/standalone/worker.js +27 -35
  135. package/dist/standalone/workflow-binding.d.ts +8 -3
  136. package/dist/standalone/workflow-binding.js +14 -5
  137. package/dist/standalone/workflow-git-capture.js +2 -1
  138. package/dist/standalone/workflow-host.d.ts +8 -103
  139. package/dist/standalone/workflow-host.js +1 -1
  140. package/dist/standalone/workflow-node-capture.js +2 -2
  141. package/dist/standalone/workflow-root-authority.d.ts +1 -1
  142. package/dist/standalone/workflow-root-authority.js +1 -1
  143. package/dist/standalone/workflow-schedules.d.ts +14 -5
  144. package/dist/standalone/workflow-schedules.js +11 -5
  145. package/dist/standalone/workflow-webhooks.d.ts +4 -18
  146. package/dist/standalone/workflow-webhooks.js +5 -5
  147. package/dist/standalone/workflows.d.ts +36 -28
  148. package/dist/standalone/workflows.js +6 -3
  149. package/dist/storage-frame.d.ts +6 -6
  150. package/dist/testing/capabilities.d.ts +7 -6
  151. package/dist/testing/capabilities.js +3 -6
  152. package/dist/testing/installation.d.ts +0 -1
  153. package/dist/testing/installation.js +2 -5
  154. package/dist/turnstile.d.ts +27 -0
  155. package/dist/turnstile.js +51 -0
  156. package/dist/workers-git.d.ts +12 -4
  157. package/dist/workers-git.js +58 -36
  158. package/dist/workers.d.ts +9 -1
  159. package/dist/workers.js +59 -20
  160. package/dist/workflows.d.ts +2 -0
  161. package/dist/workflows.js +1 -0
  162. package/docs/api/README.md +7 -0
  163. package/docs/api/agent-schedules.md +175 -0
  164. package/docs/api/agents.md +34 -13
  165. package/docs/api/app-storage.md +1 -1
  166. package/docs/api/apps.md +323 -225
  167. package/docs/api/authorization.md +1300 -0
  168. package/docs/api/build-contract.md +2 -2
  169. package/docs/api/build.md +40 -37
  170. package/docs/api/cloudflare.md +27 -2
  171. package/docs/api/git.md +20 -11
  172. package/docs/api/host.md +80 -199
  173. package/docs/api/index.md +1 -1
  174. package/docs/api/invocation.md +15 -11
  175. package/docs/api/keys.md +1 -98
  176. package/docs/api/mcp.md +43 -40
  177. package/docs/api/modules.md +2 -0
  178. package/docs/api/page/mount.md +16 -19
  179. package/docs/api/workers-git.md +46 -12
  180. package/docs/api/workers.md +125 -235
  181. package/docs/site/agent-quickstart.mdx +6 -2
  182. package/examples/standalone/wrangler.celld.jsonc +66 -0
  183. package/package.json +64 -21
  184. package/dist/standalone/channel-session-door.d.ts +0 -4
  185. package/dist/standalone/channel-session-door.js +0 -42
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+ import { type ConversationBinding } from "./binding.js";
3
+ export type VisitorSql = Readonly<{
4
+ exec: (statement: string, ...bindings: (ArrayBuffer | string | number | null)[]) => Readonly<{
5
+ toArray: () => readonly unknown[];
6
+ }>;
7
+ }>;
8
+ /** Credentials handed to one anonymous visitor's iframe. */
9
+ export type VisitorCredentials = Readonly<{
10
+ visitorId: string;
11
+ accessToken: string;
12
+ accessExpiresAt: number;
13
+ refreshSecret: string;
14
+ }>;
15
+ /** Closed mint/refresh result. */
16
+ export type VisitorGrant = Readonly<{
17
+ kind: "granted";
18
+ credentials: VisitorCredentials;
19
+ }> | Readonly<{
20
+ kind: "denied";
21
+ reason: "unmapped" | "suspended" | "new_party_limit" | "address_limit" | "refresh_invalid" | "refresh_reused";
22
+ }>;
23
+ /** Bearer grammar: `vt_` or `vr_` plus 43 base64url characters. */
24
+ export declare const visitorBearerSchema: z.ZodString;
25
+ /**
26
+ * Anonymous visitor identity for web bindings. A visitor principal
27
+ * names exactly one binding and one visitor, so it can reach exactly that
28
+ * visitor's one conversation Session and nothing else. Only token hashes are
29
+ * stored. Reusing a spent refresh secret revokes the whole family.
30
+ */
31
+ export declare function createChannelVisitors(options: Readonly<{
32
+ sql: VisitorSql;
33
+ transactionSync: <T>(body: () => T) => T;
34
+ now: () => number;
35
+ }>): {
36
+ ensure: () => void;
37
+ mint: (binding: ConversationBinding | null, siteKey: string, addressHash: string) => Promise<VisitorGrant>;
38
+ refresh: (binding: ConversationBinding | null, refreshSecret: string) => Promise<VisitorGrant>;
39
+ authenticate: (bindingId: string, accessToken: string) => Promise<Readonly<{
40
+ visitorId: string;
41
+ expiresAt: number;
42
+ addressHash: string;
43
+ }> | null>;
44
+ forget: (visitorId: string) => void;
45
+ };
46
+ /** Anonymous visitor identity owner. */
47
+ export type ChannelVisitors = ReturnType<typeof createChannelVisitors>;
@@ -0,0 +1,181 @@
1
+ import { z } from "zod";
2
+ import { DEFAULT_BINDING_LIMITS } from "./binding.js";
3
+ import { channelIds } from "./ids.js";
4
+ /** Access tokens live one hour; refresh secrets seven days, rotated on use. */
5
+ const VISITOR_ACCESS_MS = 3_600_000;
6
+ const VISITOR_REFRESH_MS = 7 * 86_400_000;
7
+ /**
8
+ * A visitor identity (one conversation) ends thirty days after it was
9
+ * minted however often it refreshes: no token outlives it.
10
+ */
11
+ const VISITOR_LIFETIME_MS = 30 * 86_400_000;
12
+ /** New visitors one network address may mint per hour on one binding. */
13
+ const VISITORS_PER_ADDRESS_PER_HOUR = 10;
14
+ const HOUR_MS = 3_600_000;
15
+ /** Bearer grammar: `vt_` or `vr_` plus 43 base64url characters. */
16
+ export const visitorBearerSchema = z
17
+ .string()
18
+ .regex(/^v[tr]_[A-Za-z0-9_-]{43}$/u);
19
+ const tokenRow = z.strictObject({
20
+ visitor_id: z.string(),
21
+ family: z.string(),
22
+ kind: z.enum(["access", "refresh"]),
23
+ expires_at: z.number().int(),
24
+ used: z.number().int(),
25
+ });
26
+ const countRow = z.strictObject({ count: z.number().int() });
27
+ function randomBearer(prefix) {
28
+ const bytes = crypto.getRandomValues(new Uint8Array(32));
29
+ let binary = "";
30
+ for (const byte of bytes)
31
+ binary += String.fromCharCode(byte);
32
+ return (prefix +
33
+ btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/u, ""));
34
+ }
35
+ async function digest(value) {
36
+ const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
37
+ return [...new Uint8Array(hash)]
38
+ .map((byte) => byte.toString(16).padStart(2, "0"))
39
+ .join("");
40
+ }
41
+ /**
42
+ * Anonymous visitor identity for web bindings. A visitor principal
43
+ * names exactly one binding and one visitor, so it can reach exactly that
44
+ * visitor's one conversation Session and nothing else. Only token hashes are
45
+ * stored. Reusing a spent refresh secret revokes the whole family.
46
+ */
47
+ export function createChannelVisitors(options) {
48
+ const { sql } = options;
49
+ const ensure = () => {
50
+ sql.exec(`CREATE TABLE IF NOT EXISTS hub_channel_visitors (
51
+ visitor_id TEXT PRIMARY KEY,
52
+ binding_id TEXT NOT NULL,
53
+ address_hash TEXT NOT NULL,
54
+ created_at INTEGER NOT NULL,
55
+ revoked INTEGER NOT NULL DEFAULT 0
56
+ ) STRICT`);
57
+ sql.exec("CREATE INDEX IF NOT EXISTS hub_channel_visitors_binding ON hub_channel_visitors(binding_id, created_at)");
58
+ sql.exec(`CREATE TABLE IF NOT EXISTS hub_channel_visitor_tokens (
59
+ token_hash TEXT PRIMARY KEY,
60
+ visitor_id TEXT NOT NULL,
61
+ binding_id TEXT NOT NULL,
62
+ family TEXT NOT NULL,
63
+ kind TEXT NOT NULL CHECK (kind IN ('access','refresh')),
64
+ expires_at INTEGER NOT NULL,
65
+ used INTEGER NOT NULL DEFAULT 0
66
+ ) STRICT`);
67
+ };
68
+ const count = (statement, ...bindings) => countRow.parse(sql.exec(statement, ...bindings).toArray()[0]).count;
69
+ const issue = async (bindingId, visitorId, family, endsAt) => {
70
+ const accessToken = randomBearer("vt_");
71
+ const refreshSecret = randomBearer("vr_");
72
+ const now = options.now();
73
+ const accessExpiresAt = Math.min(now + VISITOR_ACCESS_MS, endsAt);
74
+ const [accessHash, refreshHash] = await Promise.all([
75
+ digest(accessToken),
76
+ digest(refreshSecret),
77
+ ]);
78
+ options.transactionSync(() => {
79
+ sql.exec("INSERT INTO hub_channel_visitor_tokens VALUES (?,?,?,?,'access',?,0)", accessHash, visitorId, bindingId, family, accessExpiresAt);
80
+ sql.exec("INSERT INTO hub_channel_visitor_tokens VALUES (?,?,?,?,'refresh',?,0)", refreshHash, visitorId, bindingId, family, Math.min(now + VISITOR_REFRESH_MS, endsAt));
81
+ });
82
+ return { visitorId, accessToken, accessExpiresAt, refreshSecret };
83
+ };
84
+ /** Mint a new anonymous visitor on one active web binding. */
85
+ const mint = async (binding, siteKey, addressHash) => {
86
+ ensure();
87
+ if (binding?.selector.provider !== "web" ||
88
+ binding.selector.siteKey !== siteKey)
89
+ return { kind: "denied", reason: "unmapped" };
90
+ if (binding.lifecycle !== "active")
91
+ return { kind: "denied", reason: "suspended" };
92
+ const limits = binding.limits ?? DEFAULT_BINDING_LIMITS;
93
+ const now = options.now();
94
+ const visitorId = channelIds.visitor.generate();
95
+ const admitted = options.transactionSync(() => {
96
+ if (count("SELECT COUNT(*) AS count FROM hub_channel_visitors WHERE binding_id=? AND created_at>?", binding.bindingId, now - HOUR_MS) >= limits.newPartiesPerHour)
97
+ return "new_party_limit";
98
+ if (count("SELECT COUNT(*) AS count FROM hub_channel_visitors WHERE binding_id=? AND address_hash=? AND created_at>?", binding.bindingId, addressHash, now - HOUR_MS) >= VISITORS_PER_ADDRESS_PER_HOUR)
99
+ return "address_limit";
100
+ sql.exec("INSERT INTO hub_channel_visitors (visitor_id,binding_id,address_hash,created_at) VALUES (?,?,?,?)", visitorId, binding.bindingId, addressHash, now);
101
+ return null;
102
+ });
103
+ if (admitted !== null)
104
+ return { kind: "denied", reason: admitted };
105
+ return {
106
+ kind: "granted",
107
+ credentials: await issue(binding.bindingId, visitorId, visitorId, now + VISITOR_LIFETIME_MS),
108
+ };
109
+ };
110
+ const lookup = async (bearer) => {
111
+ ensure();
112
+ const raw = sql
113
+ .exec("SELECT t.visitor_id,t.family,t.kind,t.expires_at,t.used FROM hub_channel_visitor_tokens t JOIN hub_channel_visitors v ON v.visitor_id=t.visitor_id WHERE t.token_hash=? AND v.revoked=0", await digest(bearer))
114
+ .toArray()[0];
115
+ return raw === undefined ? null : tokenRow.parse(raw);
116
+ };
117
+ /** Rotate a refresh secret; a reused one revokes the visitor's family. */
118
+ const refresh = async (binding, refreshSecret) => {
119
+ if (binding?.selector.provider !== "web")
120
+ return { kind: "denied", reason: "unmapped" };
121
+ if (binding.lifecycle !== "active")
122
+ return { kind: "denied", reason: "suspended" };
123
+ const hash = await digest(refreshSecret);
124
+ const row = await lookup(refreshSecret);
125
+ if (row?.kind !== "refresh" ||
126
+ row.expires_at <= options.now() ||
127
+ !sql
128
+ .exec("SELECT 1 FROM hub_channel_visitor_tokens WHERE token_hash=? AND binding_id=?", hash, binding.bindingId)
129
+ .toArray().length)
130
+ return { kind: "denied", reason: "refresh_invalid" };
131
+ const claimed = options.transactionSync(() => {
132
+ if (row.used !== 0) {
133
+ sql.exec("UPDATE hub_channel_visitors SET revoked=1 WHERE visitor_id=?", row.visitor_id);
134
+ return false;
135
+ }
136
+ sql.exec("UPDATE hub_channel_visitor_tokens SET used=1 WHERE token_hash=?", hash);
137
+ return true;
138
+ });
139
+ if (!claimed)
140
+ return { kind: "denied", reason: "refresh_reused" };
141
+ const created = z
142
+ .strictObject({ created_at: z.number().int() })
143
+ .parse(sql
144
+ .exec("SELECT created_at FROM hub_channel_visitors WHERE visitor_id=?", row.visitor_id)
145
+ .toArray()[0]).created_at;
146
+ const endsAt = created + VISITOR_LIFETIME_MS;
147
+ if (endsAt <= options.now())
148
+ return { kind: "denied", reason: "refresh_invalid" };
149
+ return {
150
+ kind: "granted",
151
+ credentials: await issue(binding.bindingId, row.visitor_id, row.family, endsAt),
152
+ };
153
+ };
154
+ /** The one visitor an access token names on this binding, or null. */
155
+ const authenticate = async (bindingId, accessToken) => {
156
+ const hash = await digest(accessToken);
157
+ const row = await lookup(accessToken);
158
+ if (row?.kind !== "access" || row.expires_at <= options.now())
159
+ return null;
160
+ const scoped = sql
161
+ .exec("SELECT v.address_hash FROM hub_channel_visitor_tokens t JOIN hub_channel_visitors v ON v.visitor_id=t.visitor_id WHERE t.token_hash=? AND t.binding_id=? AND v.revoked=0", hash, bindingId)
162
+ .toArray()[0];
163
+ if (scoped === undefined)
164
+ return null;
165
+ const address = z.strictObject({ address_hash: z.string() }).parse(scoped);
166
+ return {
167
+ visitorId: row.visitor_id,
168
+ expiresAt: row.expires_at,
169
+ addressHash: address.address_hash,
170
+ };
171
+ };
172
+ /** Revoke a swept visitor and drop its token hashes. */
173
+ const forget = (visitorId) => {
174
+ ensure();
175
+ options.transactionSync(() => {
176
+ sql.exec("UPDATE hub_channel_visitors SET revoked=1 WHERE visitor_id=?", visitorId);
177
+ sql.exec("DELETE FROM hub_channel_visitor_tokens WHERE visitor_id=?", visitorId);
178
+ });
179
+ };
180
+ return { ensure, mint, refresh, authenticate, forget };
181
+ }
@@ -4,16 +4,23 @@ export type { BackgroundFrameState } from "./invocation.js";
4
4
  import { WorkerEntrypoint } from "cloudflare:workers";
5
5
  import { type GuestLimits } from "./guest-policy.js";
6
6
  export type { GuestLimits } from "./guest-policy.js";
7
- import type { ArtifactHeads, ArtifactStore, ArtifactStreams } from "./release.js";
7
+ import type { ArtifactHeads, ArtifactStore, ArtifactStreams, VerifiedObjectCache } from "./release.js";
8
8
  import { type AppBackendBindingName } from "@fungi.computer/puffball/app";
9
9
  import { type InvocationFrame, type InvocationGate } from "./invocation.js";
10
10
  import * as Schema from "effect/Schema";
11
11
  import { z } from "zod";
12
+ import type { ResourceRef } from "./apps/installation.js";
12
13
  import type { Release } from "@fungi.computer/puffball";
13
14
  import { type UiBootCheckOutcome } from "./boot-gate.js";
14
15
  export { checkBackendConstruction } from "./boot-construction.js";
15
16
  import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
16
17
  import { AppInstanceError, type AppInstance, type InstancePorts } from "./index.js";
18
+ /**
19
+ * The zone's edge cache as a {@link VerifiedObjectCache}: each verified object
20
+ * under a digest-named key on `origin` that no route serves. A failed write is
21
+ * logged and answers `unstored`; the next reader verifies from the store again.
22
+ */
23
+ export declare function edgeVerifiedObjectCache(cache: Pick<Cache, "match" | "put">, origin: string): VerifiedObjectCache;
17
24
  /** Store Release artifacts without loading the Worker ownership barrel. */
18
25
  export declare function r2ArtifactStore(bucket: R2Bucket): ArtifactStore & ArtifactHeads & ArtifactStreams;
19
26
  /** Encode supervisor refusals before native RPC can discard Error properties. */
@@ -33,6 +40,8 @@ export type FrameBindingMethod<Result> = Readonly<{
33
40
  decodeResult: z.ZodType<Result>["parse"];
34
41
  /** Determine the declared operations needed for these decoded arguments. */
35
42
  requiredOperations: (args: readonly unknown[]) => readonly string[];
43
+ /** Select a concrete host resource after decoding, before provider dispatch. */
44
+ requestedResource?: (args: readonly unknown[]) => ResourceRef;
36
45
  }>;
37
46
  /** Method descriptors for the closed App binding vocabulary. */
38
47
  export type FrameBindingMethods<Result> = Readonly<Partial<Record<AppBackendBindingName, Readonly<Record<string, FrameBindingMethod<Result>>>>>>;
@@ -113,7 +122,7 @@ export type AppInstanceLoader<FacetClass> = Readonly<{
113
122
  getDurableObjectClass: (exportName: string) => FacetClass;
114
123
  }>;
115
124
  }>;
116
- /** End the native App facet before shared frame admission releases its permit. */
125
+ /** Stop the shared native App facet at cohort drain or revocation. */
117
126
  export declare function createCloudflareInvocationGate<Host, FacetClass>(ctx: Pick<AppInstanceContext<FacetClass>, "facets">, close: (frame: InvocationFrame<Host>) => Promise<void>): InvocationGate<Host>;
118
127
  /**
119
128
  * Run an App Instance inside a SQLite-backed Durable Object on workerd, celld
@@ -20,6 +20,32 @@ import { BootGateError } from "./boot-gate.js";
20
20
  export { checkBackendConstruction } from "./boot-construction.js";
21
21
  import { createAppInstance, AppInstanceError, instanceIdSchema, instanceSchema, appInstanceErrorCodeSchema, } from "./index.js";
22
22
  const bindingWire = createFrameBindingWire(appInstanceErrorCodeSchema.literals);
23
+ /**
24
+ * The zone's edge cache as a {@link VerifiedObjectCache}: each verified object
25
+ * under a digest-named key on `origin` that no route serves. A failed write is
26
+ * logged and answers `unstored`; the next reader verifies from the store again.
27
+ */
28
+ export function edgeVerifiedObjectCache(cache, origin) {
29
+ const key = (digest) => new Request(new URL(`/_oyster/verified/${digest}`, origin));
30
+ return {
31
+ match: async (digest) => (await cache.match(key(digest)))?.body ?? null,
32
+ put: async (digest, bytes) => {
33
+ try {
34
+ await cache.put(key(digest), new Response(new Uint8Array(bytes), {
35
+ headers: { "Cache-Control": "public, max-age=31536000, immutable" },
36
+ }));
37
+ return "stored";
38
+ }
39
+ catch (cause) {
40
+ console.error("Verified object cache write failed", {
41
+ digest,
42
+ failure: cause instanceof Error ? cause.name : "non-error rejection",
43
+ });
44
+ return "unstored";
45
+ }
46
+ },
47
+ };
48
+ }
23
49
  /** Store Release artifacts without loading the Worker ownership barrel. */
24
50
  export function r2ArtifactStore(bucket) {
25
51
  const abortError = (signal) => {
@@ -241,7 +267,7 @@ const facetInvocationSchema = rpcHandleSchema({
241
267
  invoke: rpcMethod(z.union([z.json(), z.undefined()])).input(z.tuple([z.string(), z.array(z.unknown()), z.unknown()])),
242
268
  http: rpcMethod(facetHttpReplySchema).input(z.tuple([facetHttpInputSchema, z.unknown()])),
243
269
  });
244
- /** End the native App facet before shared frame admission releases its permit. */
270
+ /** Stop the shared native App facet at cohort drain or revocation. */
245
271
  export function createCloudflareInvocationGate(ctx, close) {
246
272
  return createInvocationGate({
247
273
  abortGuest: () => {
@@ -1,3 +1,5 @@
1
+ /** The query parameter that carries a window's Computer routing hint. */
2
+ export declare const computerTargetParam = "computerId";
1
3
  /** Serialize a window's routing hint. The server must admit it through the live
2
4
  * owner and fleet before constructing that request's workspace. */
3
5
  export declare function computerTargetUrl(path: string, computerId?: string): string;
@@ -1,9 +1,11 @@
1
+ /** The query parameter that carries a window's Computer routing hint. */
2
+ export const computerTargetParam = "computerId";
1
3
  /** Serialize a window's routing hint. The server must admit it through the live
2
4
  * owner and fleet before constructing that request's workspace. */
3
5
  export function computerTargetUrl(path, computerId) {
4
6
  if (computerId === undefined)
5
7
  return path;
6
8
  const url = new URL(path, "https://oyster.invalid");
7
- url.searchParams.set("computerId", computerId);
9
+ url.searchParams.set(computerTargetParam, computerId);
8
10
  return url.pathname + url.search;
9
11
  }
package/dist/files.js CHANGED
@@ -6,7 +6,7 @@
6
6
  * the Oyster's workspace unchanged. Other sources (a git repository) implement
7
7
  * the same `FileSource`.
8
8
  */
9
- import { Hono } from "hono";
9
+ import { computer, routes } from "./authorization.js";
10
10
  import * as Schema from "effect/Schema";
11
11
  import { computerList, computerReadFile, computerWriteFile, } from "@fungi.computer/agar/computer";
12
12
  const MAX_FILE_BYTES = 25 * 1024 * 1024;
@@ -66,10 +66,11 @@ export function fileCapabilities(source) {
66
66
  },
67
67
  ];
68
68
  }
69
+ /** The workspace is the Computer's: a key needs `computer.use` (see `authorization.ts`). */
69
70
  function createFileRoutes(source) {
70
- const routes = new Hono();
71
+ const table = routes();
71
72
  const pathOf = (raw) => normalizeFilePath(raw ?? "");
72
- routes.get("/list", async (context) => {
73
+ table.get("/list", computer(), async (context) => {
73
74
  const path = pathOf(context.req.query("path"));
74
75
  if (path === null)
75
76
  return refusal(400, "Invalid path");
@@ -80,7 +81,7 @@ function createFileRoutes(source) {
80
81
  ? refusal(404, "No such directory")
81
82
  : Response.json(listing);
82
83
  });
83
- routes.get("/content", async (context) => {
84
+ table.get("/content", computer(), async (context) => {
84
85
  const path = pathOf(context.req.query("path"));
85
86
  if (path === null)
86
87
  return refusal(400, "Invalid path");
@@ -91,7 +92,7 @@ function createFileRoutes(source) {
91
92
  headers: { "Content-Type": "application/octet-stream" },
92
93
  });
93
94
  });
94
- routes.put("/content", async (context) => {
95
+ table.put("/content", computer(), async (context) => {
95
96
  const path = pathOf(context.req.query("path"));
96
97
  if (path === null || path === "/")
97
98
  return refusal(400, "Invalid path");
@@ -104,7 +105,7 @@ function createFileRoutes(source) {
104
105
  await source.write(path, bytes);
105
106
  return new Response(null, { status: 204 });
106
107
  });
107
- routes.post("/mkdir", async (context) => {
108
+ table.post("/mkdir", computer(), async (context) => {
108
109
  const body = Schema.decodeUnknownOption(mkdirSchema)(await context.req.json());
109
110
  const path = body._tag === "Some" ? normalizeFilePath(body.value.path) : null;
110
111
  if (path === null || path === "/")
@@ -112,12 +113,12 @@ function createFileRoutes(source) {
112
113
  await source.mkdir(path);
113
114
  return new Response(null, { status: 204 });
114
115
  });
115
- routes.post("/replace", async (context) => {
116
+ table.post("/replace", computer(), async (context) => {
116
117
  const body = Schema.decodeUnknownOption(replaceSchema)(await context.req.json());
117
118
  const path = body._tag === "Some" ? normalizeFilePath(body.value.path) : null;
118
119
  if (body._tag === "None" || path === null)
119
120
  return refusal(400, "Invalid request");
120
121
  return Response.json(await source.replace(path, body.value.expectedContent, body.value.content));
121
122
  });
122
- return routes;
123
+ return table;
123
124
  }
@@ -5,10 +5,10 @@ import type { AppDeclaration } from "@fungi.computer/puffball/app";
5
5
  import type { Installation } from "./apps/installation.js";
6
6
  /** Installation-scoped storage; transport and credentials stay with its provider adapter. */
7
7
  export type GitStore = GitBinding;
8
- /** Live host authority, reopened before every provider call. Null is an anonymous reader. */
8
+ /** Live host authority, reopened before every provider call. */
9
9
  export type GitGrants = Readonly<{
10
10
  /** Reopen the host authority for this repository and requested scope. */
11
- admits(writer: GitWriter | null, repository: string, scope: "read" | "write"): Promise<boolean>;
11
+ admits(writer: GitWriter, repository: string, scope: "read" | "write"): Promise<boolean>;
12
12
  }>;
13
13
  /** Reuse the live installation/verified Release rule without opening provider storage. */
14
14
  export declare function createInstallationGitAdmission(ports: Readonly<{
@@ -36,7 +36,7 @@ export type AdmittedGit = Readonly<{
36
36
  /** Recheck host read authority before exchanging provider bytes. */
37
37
  read(writer: Extract<GitWriter, {
38
38
  kind: "actor";
39
- }> | null, repository: string, request: GitExchangeRequest): Promise<GitExchangeResponse>;
39
+ }>, repository: string, request: GitExchangeRequest): Promise<GitExchangeResponse>;
40
40
  /** Bind installation-authored operations; null attribution is lifecycle-only. */
41
41
  binding(onBehalfOf: HostActor | null): GitBinding;
42
42
  }>;
@@ -5,13 +5,12 @@ import { acceptedRefs, parseReceivePack, refusalReport, } from "@fungi.computer/
5
5
  import { defineIdFamily } from "@fungi.computer/cairn/id";
6
6
  /** Reuse the live installation/verified Release rule without opening provider storage. */
7
7
  export function createInstallationGitAdmission(ports) {
8
- return async (writer) => {
8
+ return async () => {
9
9
  const current = await ports.currentInstallation();
10
10
  if (current === null)
11
11
  return false;
12
12
  const declaration = await ports.declaration(current);
13
- return (declaration?.capabilities.includes("git.repository") === true &&
14
- (writer !== null || current.visibility === "public"));
13
+ return declaration?.capabilities.includes("git.repository") === true;
15
14
  };
16
15
  }
17
16
  const ids = defineIdFamily({
@@ -26,9 +25,8 @@ export function createAdmittedGit(ports) {
26
25
  const { store, grants, policy, journal } = ports;
27
26
  async function admit(writer, repository, scope) {
28
27
  gitRepositoryNameSchema.parse(repository);
29
- if (writer !== null)
30
- gitWriterSchema.parse(writer);
31
- if (writer?.kind === "installation" &&
28
+ gitWriterSchema.parse(writer);
29
+ if (writer.kind === "installation" &&
32
30
  writer.installationId !== ports.installationId)
33
31
  throw new Error("Git installation authority mismatch");
34
32
  if (!(await grants.admits(writer, repository, scope)))
@@ -1,5 +1,3 @@
1
- /** Smart-HTTP routing; all writes enter the same admitted Git path. */
2
- import { Hono } from "hono";
3
1
  import type { GitWriter } from "@fungi.computer/agar/git";
4
2
  import type { AdmittedGit } from "./git-admission.js";
5
3
  /** The repository and access one smart-HTTP request names. */
@@ -11,12 +9,10 @@ export type GitDoorTarget = Readonly<{
11
9
  /** Identity and selected installation come only from host-held ports. */
12
10
  export type GitDoorHost = Readonly<{
13
11
  /** Resolve a host-authenticated actor for this exact repository and scope;
14
- * null means an anonymous request. */
12
+ * null refuses the request. */
15
13
  authenticate(request: Request, target: GitDoorTarget): Promise<Extract<GitWriter, {
16
14
  kind: "actor";
17
15
  }> | null>;
18
- /** Decide whether anonymous reads may reach the selected installation. */
19
- isPublic(installationId: string): Promise<boolean>;
20
16
  /** Resolve the installation's admitted path without exposing provider credentials. */
21
17
  git(installationId: string): Promise<AdmittedGit | null>;
22
18
  }>;
@@ -31,5 +27,10 @@ export declare function smartHttpExchange(method: string, url: URL, suffix: stri
31
27
  export declare function gitDoorRoute(method: string, url: URL): (GitDoorTarget & Readonly<{
32
28
  advertise: boolean;
33
29
  }>) | null;
34
- /** Mount one smart-HTTP door with no provider credentials or alternate write branch. */
35
- export declare function createGitDoor(host: GitDoorHost): Hono;
30
+ /** One smart-HTTP exchange's answer; the host declares the route it serves at. */
31
+ export type GitDoor = Readonly<{
32
+ /** Answer one smart-HTTP request at `/git/...`; anything else is 404. */
33
+ fetch: (request: Request) => Promise<Response>;
34
+ }>;
35
+ /** One smart-HTTP door with no provider credentials or alternate write branch. */
36
+ export declare function createGitDoor(host: GitDoorHost): GitDoor;
package/dist/git-door.js CHANGED
@@ -1,5 +1,4 @@
1
1
  /** Smart-HTTP routing; all writes enter the same admitted Git path. */
2
- import { Hono } from "hono";
3
2
  import { z } from "zod";
4
3
  const route = /^\/git\/([a-zA-Z0-9_-]{1,256})\/([a-zA-Z0-9._-]{1,128}?)(?:\.git)?\/(info\/refs|git-upload-pack|git-receive-pack)$/u;
5
4
  /** One smart-HTTP exchange: the method, `service` query and path suffix a
@@ -59,34 +58,37 @@ async function bodyOf(request) {
59
58
  }
60
59
  return body;
61
60
  }
62
- /** Mount one smart-HTTP door with no provider credentials or alternate write branch. */
61
+ const text = (body, status) => new Response(body, {
62
+ status,
63
+ headers: { "Content-Type": "text/plain; charset=UTF-8" },
64
+ });
65
+ /** One smart-HTTP door with no provider credentials or alternate write branch. */
63
66
  export function createGitDoor(host) {
64
- const door = new Hono();
65
- const smartHttp = async (context) => {
66
- const routed = gitDoorRoute(context.req.method, new URL(context.req.url));
67
+ const smartHttp = async (incoming) => {
68
+ const routed = gitDoorRoute(incoming.method, new URL(incoming.url));
67
69
  if (routed === null)
68
- return context.text("Not a git route", 404);
70
+ return text("Not a git route", 404);
69
71
  const { advertise, ...target } = routed;
70
72
  const { installationId, repository } = target;
71
73
  const writing = target.scope === "write";
72
- const writer = await host.authenticate(context.req.raw, target);
73
- if (writer === null && (writing || !(await host.isPublic(installationId))))
74
+ const writer = await host.authenticate(incoming, target);
75
+ if (writer === null)
74
76
  return new Response("A key is required", {
75
77
  status: 401,
76
78
  headers: { "WWW-Authenticate": 'Basic realm="Oyster git"' },
77
79
  });
78
80
  const git = await host.git(installationId);
79
81
  if (git === null)
80
- return context.text("Installation Git unavailable", 403);
81
- if (context.req.header("content-encoding") !== undefined)
82
- return context.text("Compressed Git requests are not supported", 400);
83
- const body = advertise ? null : await bodyOf(context.req.raw);
82
+ return text("Installation Git unavailable", 403);
83
+ if (incoming.headers.get("content-encoding") !== null)
84
+ return text("Compressed Git requests are not supported", 400);
85
+ const body = advertise ? null : await bodyOf(incoming);
84
86
  if (body instanceof Response)
85
87
  return body;
86
88
  const headers = {};
87
89
  for (const name of ["content-type", "accept", "git-protocol"]) {
88
- const value = context.req.header(name);
89
- if (value !== undefined)
90
+ const value = incoming.headers.get(name);
91
+ if (value !== null)
90
92
  headers[name] = value;
91
93
  }
92
94
  const request = {
@@ -95,7 +97,7 @@ export function createGitDoor(host) {
95
97
  headers,
96
98
  body,
97
99
  };
98
- const response = writing && writer !== null
100
+ const response = writing
99
101
  ? await git.push(writer, repository, request)
100
102
  : await git.read(writer, repository, request);
101
103
  return new Response(new Uint8Array(response.body), {
@@ -103,6 +105,5 @@ export function createGitDoor(host) {
103
105
  headers: response.headers,
104
106
  });
105
107
  };
106
- door.all("/git/*", smartHttp);
107
- return door;
108
+ return { fetch: smartHttp };
108
109
  }
package/dist/git.d.ts CHANGED
@@ -23,7 +23,7 @@ export declare function signStandardWebhook(secret: string, id: string, timestam
23
23
  /** Headers for one signed delivery: Standard Webhooks plus `X-Hub-Signature-256`. */
24
24
  export declare function webhookHeaders(secret: string, id: string, type: string, timestamp: number, body: string): Promise<Record<string, string>>;
25
25
  export { createGitDoor, gitDoorRoute, smartHttpExchange } from "./git-door.js";
26
- export type { GitDoorHost, GitDoorTarget } from "./git-door.js";
26
+ export type { GitDoor, GitDoorHost, GitDoorTarget } from "./git-door.js";
27
27
  export { createAdmittedGit, createInstallationGitAdmission, } from "./git-admission.js";
28
28
  export type { AdmittedGit, GitStore, GitGrants, GitPolicy, GitJournal, } from "./git-admission.js";
29
29
  export { createGitPushJournal } from "./git-journal.js";