@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.
- package/README.md +46 -8
- package/dist/agents.d.ts +2 -1
- package/dist/agents.js +67 -8
- package/dist/apps/gate.d.ts +4 -0
- package/dist/apps/gate.js +3 -0
- package/dist/auth.d.ts +24 -1
- package/dist/auth.js +38 -0
- package/dist/boot-host-script.js +1 -1
- package/dist/build/candidate-cli.mjs +27 -5
- package/dist/channels/binding.d.ts +70 -0
- package/dist/channels/binding.js +97 -0
- package/dist/channels/conversations.d.ts +246 -0
- package/dist/channels/conversations.js +705 -0
- package/dist/channels/ids.d.ts +8 -0
- package/dist/channels/ids.js +15 -0
- package/dist/channels/visitor-channel.d.ts +53 -0
- package/dist/channels/visitor-channel.js +124 -0
- package/dist/channels/visitor-edge.d.ts +115 -0
- package/dist/channels/visitor-edge.js +582 -0
- package/dist/channels/visitor-request.d.ts +7 -0
- package/dist/channels/visitor-request.js +17 -0
- package/dist/channels/visitor-stream-slots.d.ts +2 -0
- package/dist/channels/visitor-stream-slots.js +33 -0
- package/dist/channels/visitors.d.ts +47 -0
- package/dist/channels/visitors.js +181 -0
- package/dist/cloudflare.d.ts +8 -2
- package/dist/cloudflare.js +27 -1
- package/dist/computer-target.d.ts +5 -0
- package/dist/computer-target.js +11 -0
- package/dist/host.d.ts +7 -4
- package/dist/host.js +96 -17
- package/dist/internal/host-gate.d.ts +6 -4
- package/dist/internal/host-gate.js +14 -7
- package/dist/invocation.d.ts +11 -4
- package/dist/invocation.js +134 -91
- package/dist/keys.d.ts +4 -1
- package/dist/keys.js +15 -4
- package/dist/machine-link/authority.d.ts +7 -0
- package/dist/machine-link/delivery.d.ts +12 -0
- package/dist/machine-link/delivery.js +22 -1
- package/dist/machine-link/disposition.d.ts +16 -0
- package/dist/machine-link/disposition.js +65 -0
- package/dist/machine-link/host.d.ts +11 -2
- package/dist/machine-link/host.js +125 -5
- package/dist/machine-link/link.d.ts +4 -0
- package/dist/machine-link/link.js +47 -1
- package/dist/page/capabilities.js +1 -1
- package/dist/page/child-selection.d.ts +27 -0
- package/dist/page/child-selection.js +38 -0
- package/dist/page/mount.js +1 -1
- package/dist/page-script.js +4 -4
- package/dist/release.d.ts +13 -0
- package/dist/release.js +10 -0
- package/dist/request-origin.d.ts +4 -0
- package/dist/request-origin.js +10 -0
- package/dist/standalone/agent-mail.js +4 -1
- package/dist/standalone/agents.d.ts +8 -0
- package/dist/standalone/agents.js +30 -11
- package/dist/standalone/channel-session-door.d.ts +4 -0
- package/dist/standalone/channel-session-door.js +72 -0
- package/dist/standalone/computer-fleet.js +7 -1
- package/dist/standalone/machine-link.d.ts +2 -0
- package/dist/standalone/machine-link.js +34 -1
- package/dist/standalone/tenancy.d.ts +3 -0
- package/dist/standalone/tenancy.js +7 -2
- package/dist/testing/capabilities.d.ts +2 -0
- package/dist/testing/capabilities.js +3 -1
- package/dist/turnstile.d.ts +27 -0
- package/dist/turnstile.js +51 -0
- package/dist/workers-git.js +21 -2
- package/dist/workers.d.ts +3 -0
- package/dist/workers.js +15 -12
- package/docs/api/agents.md +8 -4
- package/docs/api/apps.md +8 -4
- package/docs/api/auth.md +55 -0
- package/docs/api/browser.md +40 -16
- package/docs/api/build-contract.md +2 -2
- package/docs/api/cloudflare.md +26 -1
- package/docs/api/host.md +22 -13
- package/docs/api/invocation.md +7 -3
- package/docs/api/keys.md +25 -3
- package/docs/api/workers.md +38 -30
- package/docs/site/agent-quickstart.mdx +29 -2
- package/examples/standalone/wrangler.celld.jsonc +66 -0
- package/examples/standalone/wrangler.jsonc +3 -4
- package/package.json +53 -17
|
@@ -5,10 +5,12 @@ import { correlationKey } from "@fungi.computer/cairn";
|
|
|
5
5
|
import { createMachineLinkPostChannel, MachineLinkPostUnavailable, machineLinkAudience, machineLinkProtocolVersion, } from "@fungi.computer/seam/machine-link-protocol";
|
|
6
6
|
import { importSeamVerifiers, seamHeader, verifySeamRequest, } from "@fungi.computer/seam";
|
|
7
7
|
import { createPost, definePostRoute, PostDeliveryResultSchema, } from "@fungi.computer/carrier/post";
|
|
8
|
+
import { undeliverableReply } from "@fungi.computer/bird-dog";
|
|
8
9
|
import { BirdDogMailSchema } from "@fungi.computer/bird-dog/effect";
|
|
9
10
|
import { machineAgentOperationSchema } from "@fungi.computer/seam/machine-link-protocol";
|
|
10
11
|
import { Watchdog, } from "@fungi.computer/watchdog";
|
|
11
12
|
import { createMachineLinkSockets } from "./presence.js";
|
|
13
|
+
import { machineLinkDisposition } from "./disposition.js";
|
|
12
14
|
const attachmentSchema = z.strictObject({
|
|
13
15
|
deviceId: z.string().min(1).max(256),
|
|
14
16
|
fenceEpoch: z.int().nonnegative(),
|
|
@@ -48,6 +50,15 @@ const machineMailEnvelope = Schema.Struct({
|
|
|
48
50
|
address: custodyAddress,
|
|
49
51
|
event: BirdDogMailSchema,
|
|
50
52
|
});
|
|
53
|
+
const machineDispositionEnvelope = Schema.Struct({
|
|
54
|
+
key: Schema.String,
|
|
55
|
+
kind: Schema.Literal("machineLinkDisposition"),
|
|
56
|
+
address: custodyAddress,
|
|
57
|
+
event: Schema.Struct({
|
|
58
|
+
original: machineMailEnvelope,
|
|
59
|
+
reply: BirdDogMailSchema,
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
51
62
|
const machineAgentEnvelope = Schema.Struct({
|
|
52
63
|
key: Schema.String,
|
|
53
64
|
kind: Schema.Literal("machineAgent"),
|
|
@@ -87,6 +98,25 @@ export class MachineLinkHost {
|
|
|
87
98
|
}),
|
|
88
99
|
},
|
|
89
100
|
}),
|
|
101
|
+
machineLinkDisposition: definePostRoute({
|
|
102
|
+
addressKind: "machineLinkAgent",
|
|
103
|
+
address: custodyAddress,
|
|
104
|
+
event: machineDispositionEnvelope.fields.event,
|
|
105
|
+
name: ({ agentId }) => agentId,
|
|
106
|
+
namespace: {
|
|
107
|
+
getByName: () => ({
|
|
108
|
+
deliver: async (envelope) => {
|
|
109
|
+
const typed = Schema.decodeUnknownSync(machineDispositionEnvelope, { onExcessProperty: "error" })(envelope);
|
|
110
|
+
return await this.options.disposition(typed.event.original, {
|
|
111
|
+
key: typed.key,
|
|
112
|
+
kind: "agentMail",
|
|
113
|
+
address: typed.address,
|
|
114
|
+
event: typed.event.reply,
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
}),
|
|
118
|
+
},
|
|
119
|
+
}),
|
|
90
120
|
};
|
|
91
121
|
context.setWebSocketAutoResponse(new WebSocketRequestResponsePair("ping", "pong"));
|
|
92
122
|
this.router = new Hono().get("/api/teams/:teamId/local/mesh/:deviceId/connect", async (context) => {
|
|
@@ -141,6 +171,8 @@ export class MachineLinkHost {
|
|
|
141
171
|
await this.publishDirectory(socket, deviceId);
|
|
142
172
|
await this.releaseCustody();
|
|
143
173
|
await this.drainCustody();
|
|
174
|
+
await this.stageUndeliverableReplies();
|
|
175
|
+
await this.drainCustody();
|
|
144
176
|
}
|
|
145
177
|
async publishDirectory(socket, deviceId) {
|
|
146
178
|
this.deviceId = deviceId;
|
|
@@ -195,7 +227,7 @@ export class MachineLinkHost {
|
|
|
195
227
|
}
|
|
196
228
|
return channel;
|
|
197
229
|
}
|
|
198
|
-
async deliver(deviceId, envelope) {
|
|
230
|
+
async deliver(deviceId, envelope, options = {}) {
|
|
199
231
|
this.deviceId = deviceId;
|
|
200
232
|
await this.options.authorize(deviceId);
|
|
201
233
|
const links = createMachineLinkSockets(this.context, deviceId);
|
|
@@ -205,7 +237,7 @@ export class MachineLinkHost {
|
|
|
205
237
|
const typedEnvelope = Schema.decodeUnknownSync(Schema.Union([machineMailEnvelope, machineAgentEnvelope]))(envelope);
|
|
206
238
|
if (typedEnvelope.kind === "machineAgent")
|
|
207
239
|
return await this.deliverLiveOperation(deviceId, socket, typedEnvelope);
|
|
208
|
-
return await this.deliverCustodyMail(deviceId, socket, typedEnvelope);
|
|
240
|
+
return await this.deliverCustodyMail(deviceId, socket, typedEnvelope, options.refuseDuplicate === true);
|
|
209
241
|
}
|
|
210
242
|
async deliverLiveOperation(deviceId, socket, envelope) {
|
|
211
243
|
if (socket === undefined)
|
|
@@ -213,18 +245,31 @@ export class MachineLinkHost {
|
|
|
213
245
|
await this.publishDirectory(socket, deviceId);
|
|
214
246
|
return await this.channel(socket, deviceId).deliver(envelope);
|
|
215
247
|
}
|
|
216
|
-
async deliverCustodyMail(deviceId, socket, envelope) {
|
|
248
|
+
async deliverCustodyMail(deviceId, socket, envelope, refuseDuplicate = false) {
|
|
217
249
|
const { post } = await this.custody();
|
|
218
250
|
const existing = post.read(envelope.key);
|
|
219
251
|
if (existing !== undefined) {
|
|
252
|
+
if (refuseDuplicate)
|
|
253
|
+
return "refused";
|
|
220
254
|
this.owner.transactionSync(() => {
|
|
221
255
|
post.send(envelope);
|
|
222
256
|
});
|
|
223
257
|
return previousCustodyReceipt(existing);
|
|
224
258
|
}
|
|
225
|
-
this.owner.transactionSync(() => {
|
|
259
|
+
const duplicate = this.owner.transactionSync(() => {
|
|
260
|
+
if (post.read(envelope.key) !== undefined)
|
|
261
|
+
return true;
|
|
226
262
|
post.send(envelope);
|
|
263
|
+
return false;
|
|
227
264
|
});
|
|
265
|
+
if (duplicate) {
|
|
266
|
+
if (refuseDuplicate)
|
|
267
|
+
return "refused";
|
|
268
|
+
const raced = post.read(envelope.key);
|
|
269
|
+
return raced === undefined
|
|
270
|
+
? recipientOffline
|
|
271
|
+
: previousCustodyReceipt(raced);
|
|
272
|
+
}
|
|
228
273
|
await this.options.scheduleWake(post.nextWake() ?? Date.now());
|
|
229
274
|
if (socket === undefined)
|
|
230
275
|
return recipientOffline;
|
|
@@ -237,12 +282,87 @@ export class MachineLinkHost {
|
|
|
237
282
|
/** Run due Carrier work; expired rows remain visible as Carrier dead letters. */
|
|
238
283
|
async alarm() {
|
|
239
284
|
await this.drainCustody();
|
|
285
|
+
await this.stageUndeliverableReplies();
|
|
286
|
+
await this.drainCustody();
|
|
287
|
+
}
|
|
288
|
+
/** Turn each terminal incoming-mail row into a durable reply outbox row. */
|
|
289
|
+
async stageUndeliverableReplies() {
|
|
290
|
+
const { post } = await this.custody();
|
|
291
|
+
let cursor;
|
|
292
|
+
do {
|
|
293
|
+
const page = post.deadLetters(64, cursor);
|
|
294
|
+
for (const letter of page.letters)
|
|
295
|
+
this.stageUndeliverableReply(post, letter);
|
|
296
|
+
cursor = page.nextCursor;
|
|
297
|
+
} while (cursor !== undefined);
|
|
298
|
+
}
|
|
299
|
+
stageUndeliverableReply(post, letter) {
|
|
300
|
+
if ((letter.reason !== "exhausted" && letter.reason !== "expired") ||
|
|
301
|
+
letter.envelope.kind !== "agentMail")
|
|
302
|
+
return;
|
|
303
|
+
const original = Schema.decodeUnknownSync(machineMailEnvelope, {
|
|
304
|
+
onExcessProperty: "error",
|
|
305
|
+
})(letter.envelope);
|
|
306
|
+
if (original.event.reply !== undefined ||
|
|
307
|
+
original.event.from.kind !== "agent")
|
|
308
|
+
return;
|
|
309
|
+
const replyKey = `${original.key}:reply:0`;
|
|
310
|
+
if (post.read(replyKey) !== undefined)
|
|
311
|
+
return;
|
|
312
|
+
const reply = {
|
|
313
|
+
key: replyKey,
|
|
314
|
+
kind: "agentMail",
|
|
315
|
+
address: {
|
|
316
|
+
scopeId: original.address.scopeId,
|
|
317
|
+
agentId: original.event.from.agentId,
|
|
318
|
+
},
|
|
319
|
+
event: undeliverableReply(original.event, Date.now()),
|
|
320
|
+
};
|
|
321
|
+
const disposition = {
|
|
322
|
+
key: replyKey,
|
|
323
|
+
kind: "machineLinkDisposition",
|
|
324
|
+
address: reply.address,
|
|
325
|
+
event: { original, reply: reply.event },
|
|
326
|
+
};
|
|
327
|
+
this.owner.transactionSync(() => {
|
|
328
|
+
if (post.read(replyKey) === undefined)
|
|
329
|
+
post.send(disposition);
|
|
330
|
+
});
|
|
240
331
|
}
|
|
241
|
-
/** Expose terminal custody rows
|
|
332
|
+
/** Expose terminal custody rows for the device-owned diagnostic surface. */
|
|
242
333
|
async deadLetters(afterKey) {
|
|
243
334
|
const { post } = await this.custody();
|
|
244
335
|
return post.deadLetters(64, afterKey);
|
|
245
336
|
}
|
|
337
|
+
/** Prove a disposition is still present in this link's durable Post outbox. */
|
|
338
|
+
async hasStagedDisposition(deviceId, original, reply) {
|
|
339
|
+
this.deviceId = deviceId;
|
|
340
|
+
await this.options.authorize(deviceId);
|
|
341
|
+
const identity = { teamId: this.options.teamId, deviceId };
|
|
342
|
+
const requested = machineLinkDisposition(identity, original, reply);
|
|
343
|
+
if (requested === undefined)
|
|
344
|
+
return false;
|
|
345
|
+
const { post } = await this.custody();
|
|
346
|
+
const row = post.read(requested.reply.key);
|
|
347
|
+
if (row?.state !== "pending")
|
|
348
|
+
return false;
|
|
349
|
+
try {
|
|
350
|
+
const staged = Schema.decodeUnknownSync(machineDispositionEnvelope, {
|
|
351
|
+
onExcessProperty: "error",
|
|
352
|
+
})(row.envelope);
|
|
353
|
+
const stagedDisposition = machineLinkDisposition(identity, staged.event.original, {
|
|
354
|
+
key: staged.key,
|
|
355
|
+
kind: "agentMail",
|
|
356
|
+
address: staged.address,
|
|
357
|
+
event: staged.event.reply,
|
|
358
|
+
});
|
|
359
|
+
return (stagedDisposition !== undefined &&
|
|
360
|
+
JSON.stringify(stagedDisposition) === JSON.stringify(requested));
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
246
366
|
async deliverToSocket(envelope) {
|
|
247
367
|
const deviceId = this.deviceId;
|
|
248
368
|
if (deviceId === undefined)
|
|
@@ -29,6 +29,10 @@ export declare function createMachineLink<Readback>(state: MachineLinkState, opt
|
|
|
29
29
|
fetch: (request: Request) => Promise<Response>;
|
|
30
30
|
/** Mail addressed to this device's Agent, rechecked against its home. */
|
|
31
31
|
deliver(input: MachineLinkIdentity, caller: MachineLinkCaller, agentId: string, envelope: PostEnvelope): Promise<PostDeliveryResult>;
|
|
32
|
+
/** A relay's typed terminal reply to an Agent homed on this device. */
|
|
33
|
+
deliverRelayDisposition(input: MachineLinkIdentity, relayInput: MachineLinkIdentity, original: PostEnvelope, reply: PostEnvelope): Promise<PostDeliveryResult>;
|
|
34
|
+
/** Attest a still-pending disposition in this device's own Post outbox. */
|
|
35
|
+
hasStagedDisposition(input: MachineLinkIdentity, original: PostEnvelope, reply: PostEnvelope): Promise<boolean>;
|
|
32
36
|
/** One finite Agent operation on this device, returning its receipt. */
|
|
33
37
|
operation(input: MachineLinkIdentity, caller: MachineLinkCaller, agentId: string, raw: MachineAgentOperation): Promise<Schema.Json>;
|
|
34
38
|
/**
|
|
@@ -4,8 +4,16 @@ import * as Schema from "effect/Schema";
|
|
|
4
4
|
import { BirdDogMailSchema } from "@fungi.computer/bird-dog/effect";
|
|
5
5
|
import { agentMailAddressSchema, machineAgentOperationSchema, MachineLinkPostUnavailable, } from "@fungi.computer/seam/machine-link-protocol";
|
|
6
6
|
import { machineLinkFenceFactSchema, machineLinkIdentitySchema, machineLinkName, MachineLinkRefused, } from "./authority.js";
|
|
7
|
+
import { machineLinkDisposition } from "./disposition.js";
|
|
7
8
|
import { MachineLinkHost } from "./host.js";
|
|
8
9
|
const pinKey = "machine-link:identity";
|
|
10
|
+
function isRetainedRelayFence(target, relay, fence) {
|
|
11
|
+
return (relay.teamId === target.teamId &&
|
|
12
|
+
relay.deviceId !== target.deviceId &&
|
|
13
|
+
fence !== null &&
|
|
14
|
+
fence.teamId === relay.teamId &&
|
|
15
|
+
fence.deviceId === relay.deviceId);
|
|
16
|
+
}
|
|
9
17
|
/**
|
|
10
18
|
* One device's link inside its own Durable Object. The host's DO class
|
|
11
19
|
* delegates every entry point here. Identity is pinned on the first live
|
|
@@ -60,6 +68,12 @@ export function createMachineLink(state, options) {
|
|
|
60
68
|
})(envelope.event);
|
|
61
69
|
return { address, mail: body };
|
|
62
70
|
};
|
|
71
|
+
const deliverUndeliverable = async (identity, rawOriginal, rawReply) => {
|
|
72
|
+
const checked = machineLinkDisposition(identity, rawOriginal, rawReply);
|
|
73
|
+
if (checked === undefined)
|
|
74
|
+
return "refused";
|
|
75
|
+
return await authority.deliverUndeliverable(identity, checked.original, checked.reply);
|
|
76
|
+
};
|
|
63
77
|
/**
|
|
64
78
|
* The link's own sender check, whatever the host answered: the facts must
|
|
65
79
|
* name the caller and actor it saw, a hosted caller must be hosted, and a
|
|
@@ -94,6 +108,8 @@ export function createMachineLink(state, options) {
|
|
|
94
108
|
if (envelope.kind !== "agentMail")
|
|
95
109
|
return "refused";
|
|
96
110
|
const { address, mail: body } = mail(envelope);
|
|
111
|
+
if (body.from.kind !== "agent")
|
|
112
|
+
return "refused";
|
|
97
113
|
const caller = {
|
|
98
114
|
kind: "device",
|
|
99
115
|
deviceId: identity.deviceId,
|
|
@@ -118,6 +134,7 @@ export function createMachineLink(state, options) {
|
|
|
118
134
|
replay: openReplayStore,
|
|
119
135
|
directory: () => authority.directory(identity),
|
|
120
136
|
receive: (_device, envelope) => receive(identity, envelope),
|
|
137
|
+
disposition: (original, reply) => deliverUndeliverable(identity, original, reply),
|
|
121
138
|
scheduleWake: (at) => state.storage.setAlarm(at),
|
|
122
139
|
report: options.report,
|
|
123
140
|
}));
|
|
@@ -149,16 +166,45 @@ export function createMachineLink(state, options) {
|
|
|
149
166
|
return "refused";
|
|
150
167
|
const { address, mail: body } = mail(envelope);
|
|
151
168
|
const facts = await checkedTarget(device, caller, agentId, null);
|
|
169
|
+
const senderMatches = body.from.kind === "agent" && body.from.agentId === facts.callerAgentId;
|
|
170
|
+
const systemOfflineDisposition = body.from.kind === "system" &&
|
|
171
|
+
body.reply?.outcome === "undeliverable" &&
|
|
172
|
+
body.reply.reason === "recipient_offline" &&
|
|
173
|
+
body.to.agentId === facts.targetAgentId;
|
|
152
174
|
if (facts.targetHome.kind !== "machine" ||
|
|
153
175
|
facts.targetHome.deviceId !== device.deviceId ||
|
|
154
176
|
envelope.key !== body.key ||
|
|
155
177
|
address.scopeId !== facts.teamId ||
|
|
156
178
|
address.agentId !== facts.targetAgentId ||
|
|
157
179
|
body.to.agentId !== facts.targetAgentId ||
|
|
158
|
-
|
|
180
|
+
(!senderMatches && !systemOfflineDisposition))
|
|
159
181
|
return "refused";
|
|
160
182
|
return await hostFor(device).deliver(device.deviceId, envelope);
|
|
161
183
|
},
|
|
184
|
+
/** A relay's typed terminal reply to an Agent homed on this device. */
|
|
185
|
+
async deliverRelayDisposition(input, relayInput, original, reply) {
|
|
186
|
+
const device = await current(input);
|
|
187
|
+
const relay = machineLinkIdentitySchema.parse(relayInput);
|
|
188
|
+
const retainedRelay = await authority.currentFence(relay);
|
|
189
|
+
if (!isRetainedRelayFence(device, relay, retainedRelay))
|
|
190
|
+
return "refused";
|
|
191
|
+
const checked = machineLinkDisposition(device, original, reply);
|
|
192
|
+
if (checked === undefined)
|
|
193
|
+
return "refused";
|
|
194
|
+
if (!(await authority.verifyRelayDisposition(relay, checked.original, checked.reply)))
|
|
195
|
+
return "refused";
|
|
196
|
+
const home = await authority.agentHome(device, checked.reply.address.agentId);
|
|
197
|
+
if (home?.kind !== "machine" || home.deviceId !== device.deviceId)
|
|
198
|
+
return "refused";
|
|
199
|
+
return await hostFor(device).deliver(device.deviceId, checked.reply, {
|
|
200
|
+
refuseDuplicate: true,
|
|
201
|
+
});
|
|
202
|
+
},
|
|
203
|
+
/** Attest a still-pending disposition in this device's own Post outbox. */
|
|
204
|
+
async hasStagedDisposition(input, original, reply) {
|
|
205
|
+
const device = await current(input);
|
|
206
|
+
return await hostFor(device).hasStagedDisposition(device.deviceId, original, reply);
|
|
207
|
+
},
|
|
162
208
|
/** One finite Agent operation on this device, returning its receipt. */
|
|
163
209
|
async operation(input, caller, agentId, raw) {
|
|
164
210
|
const device = await current(input);
|
|
@@ -3,7 +3,7 @@ import { compile, parse, pathToRegexp } from "path-to-regexp";
|
|
|
3
3
|
import * as Schema from "effect/Schema";
|
|
4
4
|
import { AppError } from "@fungi.computer/agar";
|
|
5
5
|
import { parseJsonValue } from "@fungi.computer/whistle/wire";
|
|
6
|
-
import { computerTargetUrl } from "
|
|
6
|
+
import { computerTargetUrl } from "../computer-target.js";
|
|
7
7
|
const queryInput = Schema.Record(Schema.String, Schema.Union([Schema.String, Schema.Number, Schema.Boolean]));
|
|
8
8
|
const capabilityInputSchema = Schema.Union([Schema.Json, Schema.Undefined]);
|
|
9
9
|
function resolveRoute(path, input) {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The launch fields a window names; none of them is authority. */
|
|
2
|
+
export type ChildLaunch = Readonly<{
|
|
3
|
+
appId: string;
|
|
4
|
+
releaseId: string;
|
|
5
|
+
surfaceId: string;
|
|
6
|
+
href: string;
|
|
7
|
+
}>;
|
|
8
|
+
/** The identity the App's own configuration route answers with. */
|
|
9
|
+
export type ChildIdentity = Readonly<{
|
|
10
|
+
installationId: string;
|
|
11
|
+
appId: string;
|
|
12
|
+
releaseId: string;
|
|
13
|
+
computerId: string | null;
|
|
14
|
+
}>;
|
|
15
|
+
/**
|
|
16
|
+
* The installation page a launch names, or null when the address is not one.
|
|
17
|
+
* Only an `/apps/<installationId>/` page is ever asked for its configuration.
|
|
18
|
+
*/
|
|
19
|
+
export declare function childInstallationAddress(href: string, origin: string): URL | null;
|
|
20
|
+
/**
|
|
21
|
+
* The launch still names what the host serves at its address: the same
|
|
22
|
+
* installation, App and Release, under the admitted Computer. The host's
|
|
23
|
+
* configuration read is the one fresh answer; no directory read precedes it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function childMatchesLaunch(launch: ChildLaunch, pathname: string, config: ChildIdentity, computerTarget: string | undefined): boolean;
|
|
26
|
+
/** Concurrent callers share the one read in flight; a settled read is never reused. */
|
|
27
|
+
export declare function sharedRead<A>(read: () => Promise<A>): () => Promise<A>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { addressKey } from "@fungi.computer/nacre/app-directory";
|
|
2
|
+
const installationPath = /^\/apps\/[^/]+\/$/u;
|
|
3
|
+
/**
|
|
4
|
+
* The installation page a launch names, or null when the address is not one.
|
|
5
|
+
* Only an `/apps/<installationId>/` page is ever asked for its configuration.
|
|
6
|
+
*/
|
|
7
|
+
export function childInstallationAddress(href, origin) {
|
|
8
|
+
const address = new URL(href, origin);
|
|
9
|
+
return address.origin === origin && installationPath.test(address.pathname)
|
|
10
|
+
? address
|
|
11
|
+
: null;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The launch still names what the host serves at its address: the same
|
|
15
|
+
* installation, App and Release, under the admitted Computer. The host's
|
|
16
|
+
* configuration read is the one fresh answer; no directory read precedes it.
|
|
17
|
+
*/
|
|
18
|
+
export function childMatchesLaunch(launch, pathname, config, computerTarget) {
|
|
19
|
+
return (pathname === `/apps/${encodeURIComponent(config.installationId)}/` &&
|
|
20
|
+
launch.surfaceId ===
|
|
21
|
+
addressKey({
|
|
22
|
+
kind: "installation",
|
|
23
|
+
installationId: config.installationId,
|
|
24
|
+
}) &&
|
|
25
|
+
config.appId === launch.appId &&
|
|
26
|
+
config.releaseId === launch.releaseId &&
|
|
27
|
+
(computerTarget === undefined || config.computerId === computerTarget));
|
|
28
|
+
}
|
|
29
|
+
/** Concurrent callers share the one read in flight; a settled read is never reused. */
|
|
30
|
+
export function sharedRead(read) {
|
|
31
|
+
let inFlight;
|
|
32
|
+
return () => {
|
|
33
|
+
inFlight ??= read().finally(() => {
|
|
34
|
+
inFlight = undefined;
|
|
35
|
+
});
|
|
36
|
+
return inFlight;
|
|
37
|
+
};
|
|
38
|
+
}
|
package/dist/page/mount.js
CHANGED
|
@@ -9,7 +9,7 @@ import { RpcTarget, RpcStub } from "@fungi.computer/capnweb";
|
|
|
9
9
|
import { EventSource } from "eventsource";
|
|
10
10
|
import { connectBrowser, UiBrowserError, } from "@fungi.computer/knapsack/ui/browser";
|
|
11
11
|
import { createPageLifetime } from "./lifetime.js";
|
|
12
|
-
import { computerTargetUrl } from "
|
|
12
|
+
import { computerTargetUrl } from "../computer-target.js";
|
|
13
13
|
import * as Schema from "effect/Schema";
|
|
14
14
|
import { AppError, createCapabilityRegistry, } from "@fungi.computer/agar";
|
|
15
15
|
import { appStorage } from "@fungi.computer/agar/storage";
|