@fungi.computer/inkcap 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend/methods.ts +30 -11
- package/backend/test/gills.test.ts +1 -1
- package/backend/test/methods.test.ts +84 -18
- package/dist/assets/{FileTab-B9q_XVEC.js → FileTab-D5FlpG7C.js} +1 -1
- package/dist/assets/InkcapFileTree-B-gxxHjo.js +1 -0
- package/dist/assets/dialog-jssrzu-h.js +83 -0
- package/dist/assets/index-Bj0ZDQ9x.js +1267 -0
- package/dist/assets/index-C9u7iVEg.css +2 -0
- package/dist/assets/{useForm-CuiUTLkb.js → useForm-JWOEk10W.js} +1 -1
- package/dist/index.html +3 -3
- package/package.json +29 -11
- package/dist/assets/InkcapFileTree-CdYnx2Jy.js +0 -1
- package/dist/assets/dialog-CWwO4mBD.js +0 -71
- package/dist/assets/index-Iod2z-L_.js +0 -1279
- package/dist/assets/index-nLv8PMRJ.css +0 -2
package/backend/methods.ts
CHANGED
|
@@ -26,16 +26,16 @@ import {
|
|
|
26
26
|
listOutputSchema,
|
|
27
27
|
readBytesInputSchema,
|
|
28
28
|
writeOutputSchema,
|
|
29
|
-
} from "@fungi.computer/computer
|
|
29
|
+
} from "@fungi.computer/computer-contract";
|
|
30
30
|
export type InkcapBackendContext = {
|
|
31
31
|
activity: DocumentActivityStore;
|
|
32
32
|
documentsChanged(computerId: string, sequence: number): Promise<void>;
|
|
33
33
|
env: {
|
|
34
34
|
APP_STORAGE: Pick<AppStorageWire, "read" | "write">;
|
|
35
|
-
COMPUTER
|
|
36
|
-
list
|
|
37
|
-
read
|
|
38
|
-
write
|
|
35
|
+
COMPUTER: {
|
|
36
|
+
list(input: z.output<typeof listInput>): Promise<WhistleJsonValue>;
|
|
37
|
+
read(input: z.output<typeof readInput>): Promise<WhistleJsonValue>;
|
|
38
|
+
write(input: z.output<typeof writeInput>): Promise<WhistleJsonValue>;
|
|
39
39
|
};
|
|
40
40
|
};
|
|
41
41
|
};
|
|
@@ -103,6 +103,19 @@ const refusal = z.union([
|
|
|
103
103
|
const listReply = z.union([listOutputSchema, refusal]);
|
|
104
104
|
const readReply = z.union([readOutput, refusal]);
|
|
105
105
|
const writeReply = z.union([writeOutputSchema, refusal]);
|
|
106
|
+
// The host always supplies callable bindings; admission is checked on invocation.
|
|
107
|
+
const bindingNotGranted = z.object({ code: z.literal("binding_not_granted") });
|
|
108
|
+
async function computerCall(
|
|
109
|
+
call: () => Promise<WhistleJsonValue>,
|
|
110
|
+
): Promise<WhistleJsonValue> {
|
|
111
|
+
try {
|
|
112
|
+
return await call();
|
|
113
|
+
} catch (cause) {
|
|
114
|
+
if (bindingNotGranted.safeParse(cause).success)
|
|
115
|
+
return { error: "not_granted" };
|
|
116
|
+
throw cause;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
106
119
|
async function recordDocument(
|
|
107
120
|
input: z.output<typeof documentIdentitySchema>,
|
|
108
121
|
context: InkcapBackendContext,
|
|
@@ -164,9 +177,11 @@ export const appBackend = defineAppBackend({
|
|
|
164
177
|
input: listInput,
|
|
165
178
|
output: listReply,
|
|
166
179
|
handler: async (input, context: InkcapBackendContext, caller) =>
|
|
167
|
-
caller.computerId === null
|
|
180
|
+
caller.computerId === null
|
|
168
181
|
? refusal.parse({ error: "not_granted" })
|
|
169
|
-
: listReply.parse(
|
|
182
|
+
: listReply.parse(
|
|
183
|
+
await computerCall(() => context.env.COMPUTER.list(input)),
|
|
184
|
+
),
|
|
170
185
|
}),
|
|
171
186
|
readFile: defineAppMethod({
|
|
172
187
|
description:
|
|
@@ -176,9 +191,11 @@ export const appBackend = defineAppBackend({
|
|
|
176
191
|
input: readInput,
|
|
177
192
|
output: readReply,
|
|
178
193
|
handler: async (input, context: InkcapBackendContext, caller) =>
|
|
179
|
-
caller.computerId === null
|
|
194
|
+
caller.computerId === null
|
|
180
195
|
? refusal.parse({ error: "not_granted" })
|
|
181
|
-
: readReply.parse(
|
|
196
|
+
: readReply.parse(
|
|
197
|
+
await computerCall(() => context.env.COMPUTER.read(input)),
|
|
198
|
+
),
|
|
182
199
|
}),
|
|
183
200
|
writeFile: defineAppMethod({
|
|
184
201
|
description:
|
|
@@ -208,9 +225,11 @@ export const appBackend = defineAppBackend({
|
|
|
208
225
|
},
|
|
209
226
|
},
|
|
210
227
|
handler: async (input, context: InkcapBackendContext, caller) =>
|
|
211
|
-
caller.computerId === null
|
|
228
|
+
caller.computerId === null
|
|
212
229
|
? refusal.parse({ error: "not_granted" })
|
|
213
|
-
: writeReply.parse(
|
|
230
|
+
: writeReply.parse(
|
|
231
|
+
await computerCall(() => context.env.COMPUTER.write(input)),
|
|
232
|
+
),
|
|
214
233
|
}),
|
|
215
234
|
},
|
|
216
235
|
});
|
|
@@ -27,7 +27,7 @@ it("projects the saved Recent Files source with stable identities and its own cu
|
|
|
27
27
|
documentsChanged: () => Promise.resolve(),
|
|
28
28
|
env: {
|
|
29
29
|
APP_STORAGE: { read: unavailable, write: unavailable },
|
|
30
|
-
COMPUTER: { read: unavailable },
|
|
30
|
+
COMPUTER: { list: unavailable, read: unavailable, write: unavailable },
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
33
|
const caller = {
|
|
@@ -37,6 +37,7 @@ const unavailable = (): Promise<never> =>
|
|
|
37
37
|
Promise.reject(new Error("Computer calls must not access repositories"));
|
|
38
38
|
const unrelated = {
|
|
39
39
|
APP_STORAGE: { read: unavailable, write: unavailable },
|
|
40
|
+
COMPUTER: { list: unavailable, read: unavailable, write: unavailable },
|
|
40
41
|
};
|
|
41
42
|
const emptyMarker = () =>
|
|
42
43
|
Promise.resolve({
|
|
@@ -49,7 +50,7 @@ const emptyMarker = () =>
|
|
|
49
50
|
canWriteTeam: false,
|
|
50
51
|
},
|
|
51
52
|
});
|
|
52
|
-
type ComputerBinding =
|
|
53
|
+
type ComputerBinding = InkcapBackendContext["env"]["COMPUTER"];
|
|
53
54
|
|
|
54
55
|
const caller: Parameters<typeof appBackend.call>[3] = {
|
|
55
56
|
actor: { kind: "agent", agentId: "agent-a" },
|
|
@@ -69,9 +70,8 @@ it("lists files only through the admitted pinned Computer binding", async () =>
|
|
|
69
70
|
env: {
|
|
70
71
|
...unrelated,
|
|
71
72
|
COMPUTER: {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
) => {
|
|
73
|
+
...unrelated.COMPUTER,
|
|
74
|
+
list: (input: Parameters<ComputerBinding["list"]>[0]) => {
|
|
75
75
|
calls.push(input);
|
|
76
76
|
return Promise.resolve(reply);
|
|
77
77
|
},
|
|
@@ -95,6 +95,7 @@ it("does not dispatch when the caller has no pinned Computer", async () => {
|
|
|
95
95
|
env: {
|
|
96
96
|
...unrelated,
|
|
97
97
|
COMPUTER: {
|
|
98
|
+
...unrelated.COMPUTER,
|
|
98
99
|
read: () => {
|
|
99
100
|
calls++;
|
|
100
101
|
return Promise.resolve({});
|
|
@@ -116,7 +117,8 @@ it("forwards bounded byte reads and compare-and-swap writes", async () => {
|
|
|
116
117
|
env: {
|
|
117
118
|
...unrelated,
|
|
118
119
|
COMPUTER: {
|
|
119
|
-
|
|
120
|
+
...unrelated.COMPUTER,
|
|
121
|
+
read: (input: Parameters<ComputerBinding["read"]>[0]) => {
|
|
120
122
|
calls.push(input);
|
|
121
123
|
return Promise.resolve({
|
|
122
124
|
path: "/notes",
|
|
@@ -126,9 +128,7 @@ it("forwards bounded byte reads and compare-and-swap writes", async () => {
|
|
|
126
128
|
truncated: false,
|
|
127
129
|
});
|
|
128
130
|
},
|
|
129
|
-
write: (
|
|
130
|
-
input: Parameters<NonNullable<ComputerBinding["write"]>>[0],
|
|
131
|
-
) => {
|
|
131
|
+
write: (input: Parameters<ComputerBinding["write"]>[0]) => {
|
|
132
132
|
calls.push(input);
|
|
133
133
|
return Promise.resolve({ path: "/notes", bytes: 2 });
|
|
134
134
|
},
|
|
@@ -230,6 +230,7 @@ it("retains typed conflict and attachment budget refusals", async () => {
|
|
|
230
230
|
env: {
|
|
231
231
|
...unrelated,
|
|
232
232
|
COMPUTER: {
|
|
233
|
+
...unrelated.COMPUTER,
|
|
233
234
|
write: () => Promise.resolve(z.json().parse(response)),
|
|
234
235
|
},
|
|
235
236
|
},
|
|
@@ -239,16 +240,77 @@ it("retains typed conflict and attachment budget refusals", async () => {
|
|
|
239
240
|
).toEqual(response);
|
|
240
241
|
}
|
|
241
242
|
});
|
|
242
|
-
it(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
|
|
243
|
+
it.each([
|
|
244
|
+
["listFiles", {}],
|
|
245
|
+
["readFile", { path: "/notes" }],
|
|
246
|
+
["writeFile", { path: "/notes", content: "hi" }],
|
|
247
|
+
] as const)(
|
|
248
|
+
"maps the host's missing-grant refusal for %s",
|
|
249
|
+
async (method, input) => {
|
|
250
|
+
const denied = (): Promise<never> =>
|
|
251
|
+
Promise.reject(
|
|
252
|
+
Object.assign(new Error("App binding refused: binding_not_granted"), {
|
|
253
|
+
name: "AppInstanceError",
|
|
254
|
+
code: "binding_not_granted",
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
257
|
+
await expect(
|
|
258
|
+
appBackend.call(
|
|
259
|
+
method,
|
|
260
|
+
input,
|
|
261
|
+
{
|
|
262
|
+
activity,
|
|
263
|
+
documentsChanged,
|
|
264
|
+
env: {
|
|
265
|
+
...unrelated,
|
|
266
|
+
COMPUTER: { list: denied, read: denied, write: denied },
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
caller,
|
|
270
|
+
),
|
|
271
|
+
).resolves.toEqual({ error: "not_granted" });
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
it.each([
|
|
275
|
+
["listFiles", {}],
|
|
276
|
+
["readFile", { path: "/notes" }],
|
|
277
|
+
["writeFile", { path: "/notes", content: "hi" }],
|
|
278
|
+
] as const)(
|
|
279
|
+
"preserves other binding failures for %s",
|
|
280
|
+
async (method, input) => {
|
|
281
|
+
for (const failure of [
|
|
282
|
+
Object.assign(new Error("App binding refused: binding_unavailable"), {
|
|
283
|
+
name: "AppInstanceError",
|
|
284
|
+
code: "binding_unavailable",
|
|
285
|
+
}),
|
|
286
|
+
Object.assign(
|
|
287
|
+
new Error("App binding refused: invalid_invocation_facts"),
|
|
288
|
+
{
|
|
289
|
+
name: "AppInstanceError",
|
|
290
|
+
code: "invalid_invocation_facts",
|
|
291
|
+
},
|
|
292
|
+
),
|
|
293
|
+
new Error("binding_not_granted"),
|
|
294
|
+
]) {
|
|
295
|
+
const failed = (): Promise<never> => Promise.reject(failure);
|
|
296
|
+
await expect(
|
|
297
|
+
appBackend.call(
|
|
298
|
+
method,
|
|
299
|
+
input,
|
|
300
|
+
{
|
|
301
|
+
activity,
|
|
302
|
+
documentsChanged,
|
|
303
|
+
env: {
|
|
304
|
+
...unrelated,
|
|
305
|
+
COMPUTER: { list: failed, read: failed, write: failed },
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
caller,
|
|
309
|
+
),
|
|
310
|
+
).rejects.toBe(failure);
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
);
|
|
252
314
|
it("does not expose Computer methods through unauthenticated repository HTTP", async () => {
|
|
253
315
|
expect(
|
|
254
316
|
(await Inkcap.prototype.fetch(new Request("https://inkcap.test/"))).status,
|
|
@@ -274,6 +336,7 @@ it("persists admitted document activity and pages saved rows without provider ca
|
|
|
274
336
|
activity,
|
|
275
337
|
documentsChanged,
|
|
276
338
|
env: {
|
|
339
|
+
...unrelated,
|
|
277
340
|
APP_STORAGE: {
|
|
278
341
|
read: emptyMarker,
|
|
279
342
|
write: (input) => {
|
|
@@ -311,6 +374,7 @@ it("keeps cursor pages and activity private to the actor and pinned Computer", a
|
|
|
311
374
|
activity: createDocumentActivityStore(sql),
|
|
312
375
|
documentsChanged,
|
|
313
376
|
env: {
|
|
377
|
+
...unrelated,
|
|
314
378
|
APP_STORAGE: {
|
|
315
379
|
read: emptyMarker,
|
|
316
380
|
write: () =>
|
|
@@ -386,6 +450,7 @@ it("bounds retained documents and returns source continuation without trimming a
|
|
|
386
450
|
activity,
|
|
387
451
|
documentsChanged,
|
|
388
452
|
env: {
|
|
453
|
+
...unrelated,
|
|
389
454
|
APP_STORAGE: {
|
|
390
455
|
read: emptyMarker,
|
|
391
456
|
write: () =>
|
|
@@ -440,6 +505,7 @@ it("preserves the original marker failure without any provider or retry", async
|
|
|
440
505
|
activity,
|
|
441
506
|
documentsChanged,
|
|
442
507
|
env: {
|
|
508
|
+
...unrelated,
|
|
443
509
|
APP_STORAGE: {
|
|
444
510
|
read: emptyMarker,
|
|
445
511
|
write: () => {
|