@graffiti-garden/implementation-decentralized 0.1.2 → 0.1.4

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 (61) hide show
  1. package/dist/1-services/4-inboxes/cache.d.ts +34 -0
  2. package/dist/1-services/4-inboxes/cache.d.ts.map +1 -0
  3. package/dist/1-services/4-inboxes/messages.d.ts +70 -0
  4. package/dist/1-services/4-inboxes/messages.d.ts.map +1 -0
  5. package/dist/1-services/4-inboxes.d.ts +28 -67
  6. package/dist/1-services/4-inboxes.d.ts.map +1 -1
  7. package/dist/3-protocol/2-handles.d.ts.map +1 -1
  8. package/dist/3-protocol/4-graffiti.d.ts +2 -2
  9. package/dist/3-protocol/4-graffiti.d.ts.map +1 -1
  10. package/dist/3-protocol/login-dialog.html.d.ts +1 -1
  11. package/dist/3-protocol/login-dialog.html.d.ts.map +1 -1
  12. package/dist/browser/index.js +7 -7
  13. package/dist/browser/index.js.map +4 -4
  14. package/dist/browser/{login-dialog.html-E6VRKMXH.js → login-dialog.html-WIDO3XZF.js} +18 -4
  15. package/dist/browser/{login-dialog.html-E6VRKMXH.js.map → login-dialog.html-WIDO3XZF.js.map} +2 -2
  16. package/dist/cjs/1-services/4-inboxes/cache.js +186 -0
  17. package/dist/cjs/1-services/4-inboxes/cache.js.map +7 -0
  18. package/dist/cjs/1-services/4-inboxes/messages.js +69 -0
  19. package/dist/cjs/1-services/4-inboxes/messages.js.map +7 -0
  20. package/dist/cjs/1-services/4-inboxes-tests.js +339 -9
  21. package/dist/cjs/1-services/4-inboxes-tests.js.map +3 -3
  22. package/dist/cjs/1-services/4-inboxes.js +210 -399
  23. package/dist/cjs/1-services/4-inboxes.js.map +3 -3
  24. package/dist/cjs/3-protocol/2-handles-tests.js +8 -0
  25. package/dist/cjs/3-protocol/2-handles-tests.js.map +2 -2
  26. package/dist/cjs/3-protocol/2-handles.js +3 -4
  27. package/dist/cjs/3-protocol/2-handles.js.map +2 -2
  28. package/dist/cjs/3-protocol/4-graffiti.js +49 -53
  29. package/dist/cjs/3-protocol/4-graffiti.js.map +2 -2
  30. package/dist/cjs/3-protocol/login-dialog.html.js +17 -3
  31. package/dist/cjs/3-protocol/login-dialog.html.js.map +2 -2
  32. package/dist/cjs/index.spec.js +90 -0
  33. package/dist/cjs/index.spec.js.map +3 -3
  34. package/dist/esm/1-services/4-inboxes/cache.js +156 -0
  35. package/dist/esm/1-services/4-inboxes/cache.js.map +7 -0
  36. package/dist/esm/1-services/4-inboxes/messages.js +60 -0
  37. package/dist/esm/1-services/4-inboxes/messages.js.map +7 -0
  38. package/dist/esm/1-services/4-inboxes-tests.js +340 -10
  39. package/dist/esm/1-services/4-inboxes-tests.js.map +3 -3
  40. package/dist/esm/1-services/4-inboxes.js +222 -383
  41. package/dist/esm/1-services/4-inboxes.js.map +3 -3
  42. package/dist/esm/3-protocol/2-handles-tests.js +8 -0
  43. package/dist/esm/3-protocol/2-handles-tests.js.map +2 -2
  44. package/dist/esm/3-protocol/2-handles.js +3 -4
  45. package/dist/esm/3-protocol/2-handles.js.map +2 -2
  46. package/dist/esm/3-protocol/4-graffiti.js +49 -53
  47. package/dist/esm/3-protocol/4-graffiti.js.map +2 -2
  48. package/dist/esm/3-protocol/login-dialog.html.js +17 -3
  49. package/dist/esm/3-protocol/login-dialog.html.js.map +2 -2
  50. package/dist/esm/index.spec.js +92 -2
  51. package/dist/esm/index.spec.js.map +2 -2
  52. package/package.json +1 -1
  53. package/src/1-services/4-inboxes/cache.ts +227 -0
  54. package/src/1-services/4-inboxes/messages.ts +63 -0
  55. package/src/1-services/4-inboxes-tests.ts +387 -2
  56. package/src/1-services/4-inboxes.ts +297 -512
  57. package/src/3-protocol/2-handles-tests.ts +12 -0
  58. package/src/3-protocol/2-handles.ts +3 -5
  59. package/src/3-protocol/4-graffiti.ts +69 -83
  60. package/src/3-protocol/login-dialog.html.ts +17 -3
  61. package/src/index.spec.ts +106 -2
@@ -13,5 +13,17 @@ export function handleTests(handle: string) {
13
13
  const resolvedHandle = await handles.actorToHandle(actor);
14
14
  expect(resolvedHandle).toBe(handle);
15
15
  });
16
+
17
+ test("actorToHandle uses the first did:web alias", async () => {
18
+ const actor = "did:plc:example";
19
+ const handleDid = "did:web:alice.example";
20
+ const dids = {
21
+ resolve: async (did: string) => did === actor
22
+ ? { alsoKnownAs: ["at://alice.example", handleDid, "did:web:other.example"] }
23
+ : { alsoKnownAs: [actor] },
24
+ } as DecentralizedIdentifiers;
25
+ expect(await new Handles({ dids }).actorToHandle(actor)).toBe("alice.example");
26
+ });
27
+
16
28
  });
17
29
  }
@@ -13,16 +13,14 @@ export class Handles {
13
13
  actorToHandle: Graffiti["actorToHandle"] = async (actor) => {
14
14
  const actorDocument = await this.services.dids.resolve(actor);
15
15
 
16
- const handleDid = actorDocument.alsoKnownAs?.at(0);
16
+ const handleDid = actorDocument.alsoKnownAs?.find((alias) =>
17
+ alias.startsWith(HANDLE_DID_PREFIX),
18
+ );
17
19
  if (!handleDid) {
18
20
  throw new GraffitiErrorNotFound(
19
21
  `Handle for actor DID ${actor} not found`,
20
22
  );
21
23
  }
22
- if (!handleDid.startsWith(HANDLE_DID_PREFIX)) {
23
- throw new Error(`Handle DID ${handleDid} is not a valid handle`);
24
- }
25
-
26
24
  const handle = handleDid.slice(HANDLE_DID_PREFIX.length);
27
25
 
28
26
  const handleDocument = await this.services.dids.resolve(handleDid);
@@ -220,7 +220,7 @@ export class GraffitiDecentralized implements Graffiti {
220
220
  }
221
221
 
222
222
  if (handle) {
223
- await this.rememberHandleInPasswordManager(handle, actor);
223
+ this.rememberHandleInBrowserStorage(handle);
224
224
  }
225
225
  });
226
226
 
@@ -282,8 +282,7 @@ export class GraffitiDecentralized implements Graffiti {
282
282
  "#username",
283
283
  ) as HTMLInputElement | null;
284
284
  const rememberedHandle =
285
- proposedHandle ||
286
- (await this.getRememberedHandleFromPasswordManager());
285
+ proposedHandle || this.getRememberedHandleFromBrowserStorage();
287
286
  if (input) input.value = rememberedHandle ?? "";
288
287
  input?.addEventListener("focus", () => input?.select());
289
288
  setTimeout(() => input?.focus(), 0);
@@ -328,6 +327,12 @@ export class GraffitiDecentralized implements Graffiti {
328
327
  this.login_(handle);
329
328
  }
330
329
  });
330
+ template
331
+ ?.querySelector("#graffiti-login-forgot")
332
+ ?.addEventListener("click", (e) => {
333
+ e.preventDefault();
334
+ void this.loginWithoutHandle();
335
+ });
331
336
  } else {
332
337
  template = await this.modal?.displayTemplate("graffiti-login-welcome");
333
338
  template
@@ -396,79 +401,44 @@ export class GraffitiDecentralized implements Graffiti {
396
401
  }
397
402
  }
398
403
 
399
- protected async rememberHandleInPasswordManager(
400
- handle: string,
401
- actor: string,
402
- ) {
403
- if (typeof window === "undefined") return;
404
-
405
- const credentials = window.navigator.credentials;
406
-
407
- const PasswordCredentialConstructor = (
408
- window as unknown as {
409
- PasswordCredential?: new (data: {
410
- id: string;
411
- password: string;
412
- name?: string;
413
- }) => Credential;
414
- }
415
- ).PasswordCredential;
416
-
417
- if (!credentials?.store || !PasswordCredentialConstructor) {
418
- this.rememberHandleInBrowserStorage(handle);
419
- return;
420
- }
421
-
422
- try {
423
- const credential = new PasswordCredentialConstructor({
424
- id: handle,
425
- // Graffiti logins use external authorization; this stable placeholder
426
- // allows browsers/password managers to persist the handle.
427
- password: "no-password",
428
- name: handle,
404
+ protected async loginWithoutHandle() {
405
+ const template = await this.modal?.displayTemplate("graffiti-login-provider");
406
+ const defaultProvider = new URL("/", this.identityCreatorEndpoint);
407
+ const provider = template?.querySelector("#graffiti-provider") as HTMLInputElement | null;
408
+ if (provider) provider.value = defaultProvider.host;
409
+ provider?.addEventListener("focus", () => provider.select());
410
+ setTimeout(() => provider?.focus(), 0);
411
+
412
+ template
413
+ ?.querySelector("#graffiti-login-provider-form")
414
+ ?.addEventListener("submit", (event) => {
415
+ event.preventDefault();
416
+ try {
417
+ const domain = provider?.value.trim() ?? "";
418
+ const origin = domain === defaultProvider.host
419
+ ? defaultProvider
420
+ : new URL(`https://${domain}`);
421
+ if (
422
+ !origin.hostname || origin.username || origin.password ||
423
+ origin.pathname !== "/" || origin.search || origin.hash
424
+ ) throw new Error("Invalid provider domain");
425
+
426
+ const url = new URL("/forgot-handle", origin);
427
+ url.searchParams.set("redirect_uri", window.location.toString());
428
+ window.location.assign(url.toString());
429
+ } catch {
430
+ alert("Enter a valid provider domain.");
431
+ }
429
432
  });
430
- await credentials.store(credential);
431
- } catch (error) {
432
- console.error("Failed to remember handle in password manager:", error);
433
- this.rememberHandleInBrowserStorage(handle);
434
- }
435
- }
436
433
 
437
- protected async getRememberedHandleFromPasswordManager() {
438
- if (typeof window === "undefined") return undefined;
439
-
440
- const credentials = window.navigator.credentials;
441
- const PasswordCredentialConstructor = (
442
- window as unknown as {
443
- PasswordCredential?: new (data: {
444
- id: string;
445
- password: string;
446
- name?: string;
447
- }) => Credential;
448
- }
449
- ).PasswordCredential;
450
- if (!credentials?.get || !PasswordCredentialConstructor) {
451
- return this.getRememberedHandleFromBrowserStorage();
452
- }
453
-
454
- try {
455
- const credential = await credentials.get({
456
- password: true,
457
- mediation: "optional",
458
- } as any);
459
- if (
460
- credential &&
461
- "id" in credential &&
462
- typeof credential.id === "string"
463
- ) {
464
- return credential.id;
465
- }
466
- } catch (error) {
467
- console.error("Failed to load handle from password manager:", error);
468
- return this.getRememberedHandleFromBrowserStorage();
469
- }
434
+ template
435
+ ?.querySelector("#graffiti-login-provider-back")
436
+ ?.addEventListener("click", (event) => {
437
+ event.preventDefault();
438
+ void this.login_("");
439
+ });
470
440
 
471
- return undefined;
441
+ await this.modal?.open();
472
442
  }
473
443
 
474
444
  protected rememberHandleInBrowserStorage(handle: string) {
@@ -1221,6 +1191,19 @@ export class GraffitiDecentralized implements Graffiti {
1221
1191
  }
1222
1192
  }
1223
1193
 
1194
+ // Labeling is advisory and should not hold up discovery, but its failure
1195
+ // must be handled even if the caller has already stopped reading the stream.
1196
+ protected labelInboxMessage(
1197
+ inboxEndpoint: string,
1198
+ messageId: string,
1199
+ label: number,
1200
+ inboxToken?: string | null,
1201
+ ): void {
1202
+ void this.inboxes.label(inboxEndpoint, messageId, label, inboxToken).catch(
1203
+ (error) => console.error("Could not label inbox message:", error),
1204
+ );
1205
+ }
1206
+
1224
1207
  protected async processOneLabeledMessage<Schema extends JSONSchema>(
1225
1208
  inboxEndpoint: string,
1226
1209
  result: LabeledMessage<Schema>,
@@ -1244,7 +1227,7 @@ export class GraffitiDecentralized implements Graffiti {
1244
1227
  const metadataRaw = dagCborDecode(metadataBytes);
1245
1228
  metadata = MessageMetadataSchema.parse(metadataRaw);
1246
1229
  } catch (e) {
1247
- this.inboxes.label(
1230
+ this.labelInboxMessage(
1248
1231
  inboxEndpoint,
1249
1232
  messageId,
1250
1233
  MESSAGE_LABEL_INVALID,
@@ -1297,7 +1280,7 @@ export class GraffitiDecentralized implements Graffiti {
1297
1280
  // device and the device cache is out of date.
1298
1281
  if (past[LABELED_MESSAGE_LABEL_KEY] !== MESSAGE_LABEL_TRASH) {
1299
1282
  // Label the message as trash
1300
- this.inboxes.label(
1283
+ this.labelInboxMessage(
1301
1284
  inboxEndpoint,
1302
1285
  tombstonedMessageId,
1303
1286
  MESSAGE_LABEL_TRASH,
@@ -1373,7 +1356,7 @@ export class GraffitiDecentralized implements Graffiti {
1373
1356
  if (tombstonedMessageId) {
1374
1357
  if (validationError instanceof GraffitiErrorNotFound) {
1375
1358
  // Not found == The tombstone is correct
1376
- this.inboxes
1359
+ void this.inboxes
1377
1360
  // Get the referenced message
1378
1361
  .get(inboxEndpoint, tombstonedMessageId, inboxToken)
1379
1362
  .then((result) => {
@@ -1386,7 +1369,7 @@ export class GraffitiDecentralized implements Graffiti {
1386
1369
  result[LABELED_MESSAGE_LABEL_KEY] !== MESSAGE_LABEL_TRASH
1387
1370
  ) {
1388
1371
  // If valid but not yet trash, label the message as trash
1389
- this.inboxes.label(
1372
+ this.labelInboxMessage(
1390
1373
  inboxEndpoint,
1391
1374
  tombstonedMessageId,
1392
1375
  MESSAGE_LABEL_TRASH,
@@ -1395,13 +1378,16 @@ export class GraffitiDecentralized implements Graffiti {
1395
1378
  }
1396
1379
 
1397
1380
  // Then, label the tombstone message as trash
1398
- this.inboxes.label(
1381
+ this.labelInboxMessage(
1399
1382
  inboxEndpoint,
1400
1383
  messageId,
1401
1384
  MESSAGE_LABEL_TRASH,
1402
1385
  inboxToken,
1403
1386
  );
1404
- });
1387
+ })
1388
+ .catch((error) =>
1389
+ console.error("Could not inspect tombstone message:", error),
1390
+ );
1405
1391
 
1406
1392
  return {
1407
1393
  messageId,
@@ -1415,7 +1401,7 @@ export class GraffitiDecentralized implements Graffiti {
1415
1401
  } else {
1416
1402
  console.error("Recieved an incorrect tombstone object");
1417
1403
  console.error(validationError);
1418
- this.inboxes.label(
1404
+ this.labelInboxMessage(
1419
1405
  inboxEndpoint,
1420
1406
  messageId,
1421
1407
  MESSAGE_LABEL_INVALID,
@@ -1424,7 +1410,7 @@ export class GraffitiDecentralized implements Graffiti {
1424
1410
  }
1425
1411
  } else {
1426
1412
  if (validationError === undefined) {
1427
- this.inboxes.label(
1413
+ this.labelInboxMessage(
1428
1414
  inboxEndpoint,
1429
1415
  messageId,
1430
1416
  MESSAGE_LABEL_VALID,
@@ -1441,7 +1427,7 @@ export class GraffitiDecentralized implements Graffiti {
1441
1427
  } else if (validationError instanceof GraffitiErrorNotFound) {
1442
1428
  // Item was deleted before we got a chance to
1443
1429
  // validate it. Just label the message as trash.
1444
- this.inboxes.label(
1430
+ this.labelInboxMessage(
1445
1431
  inboxEndpoint,
1446
1432
  messageId,
1447
1433
  MESSAGE_LABEL_TRASH,
@@ -1450,7 +1436,7 @@ export class GraffitiDecentralized implements Graffiti {
1450
1436
  } else {
1451
1437
  console.error("Recieved an incorrect object");
1452
1438
  console.error(validationError);
1453
- this.inboxes.label(
1439
+ this.labelInboxMessage(
1454
1440
  inboxEndpoint,
1455
1441
  messageId,
1456
1442
  MESSAGE_LABEL_INVALID,
@@ -37,7 +37,21 @@ export const template = `<template id="graffiti-login-welcome">
37
37
  </button>
38
38
  </form>
39
39
 
40
- <p>
41
- Don't&nbsp;have&nbsp;a Graffiti&nbsp;handle? <a id="graffiti-login-new">Create&nbsp;one</a>.
42
- </p>
40
+ <div style="display: grid; gap: 0.5em">
41
+ <p><a href="#" id="graffiti-login-forgot">Forgot your handle?</a></p>
42
+ <p>Don't&nbsp;have&nbsp;a Graffiti&nbsp;handle? <a id="graffiti-login-new">Create&nbsp;one</a>.</p>
43
+ </div>
44
+ </template>
45
+
46
+ <template id="graffiti-login-provider">
47
+ <h1>
48
+ <a target="_blank" href="https://graffiti.garden">Graffiti Log&nbsp;In</a>
49
+ </h1>
50
+
51
+ <form id="graffiti-login-provider-form">
52
+ <label for="graffiti-provider">Where did you create your Graffiti account?</label>
53
+ <input id="graffiti-provider" type="text" autocapitalize="none" spellcheck="false" required>
54
+ <button type="submit">Continue to provider</button>
55
+ </form>
56
+ <p><a href="#" id="graffiti-login-provider-back">← Back</a></p>
43
57
  </template>`;
package/src/index.spec.ts CHANGED
@@ -13,11 +13,12 @@ import { Authorization } from "./1-services/1-authorization";
13
13
  import { StorageBuckets } from "./1-services/3-storage-buckets";
14
14
  import { Inboxes } from "./1-services/4-inboxes";
15
15
  import { Sessions } from "./3-protocol/1-sessions";
16
- import { afterAll, beforeAll, describe } from "vitest";
16
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
17
+ import { decode as dagCborDecode, encode as dagCborEncode } from "@ipld/dag-cbor";
17
18
  import { Handles } from "./3-protocol/2-handles";
18
19
  import { didTests } from "./1-services/2-dids-tests";
19
20
  import { storageBucketTests } from "./1-services/3-storage-buckets-tests";
20
- import { inboxTests } from "./1-services/4-inboxes-tests";
21
+ import { inboxTests, inboxUnitTests } from "./1-services/4-inboxes-tests";
21
22
  import { stringEncodingTests } from "./2-primitives/1-string-encoding-tests";
22
23
  import { contentAddressesTests } from "./2-primitives/2-content-addresses-tests";
23
24
  import { channelAttestationTests } from "./2-primitives/3-channel-attestations-tests";
@@ -106,6 +107,107 @@ describe("GraffitiDecentralized Tests", () => {
106
107
  () => sessions[1],
107
108
  );
108
109
 
110
+ test("OAuth codes require approval from the selected account", async () => {
111
+ const account = decentralizedTestUsers[0];
112
+ const redirectUri = "https://client.example/callback";
113
+ const endpoint = "https://localhost:5173/app/oauth/authorize";
114
+ const cookie = `account_${account.accountId}=${account.token}`;
115
+ const headers = {
116
+ Cookie: cookie,
117
+ "X-Graffiti-Account": String(account.accountId),
118
+ "Content-Type": "application/json",
119
+ Origin: "https://localhost:5173",
120
+ };
121
+ const request = (scope: string[], requestHeaders: HeadersInit = headers) =>
122
+ fetch(endpoint, {
123
+ method: "POST",
124
+ headers: requestHeaders,
125
+ body: JSON.stringify({ redirect_uri: redirectUri, state: "test-state", scope }),
126
+ });
127
+
128
+ const navigation = await fetch(
129
+ `${endpoint}?account=${account.accountId}&redirect_uri=${encodeURIComponent(redirectUri)}`,
130
+ { headers: { Cookie: cookie } },
131
+ );
132
+ expect(navigation.status).toBe(404);
133
+
134
+ expect((await request([account.bucketEndpoint], { Cookie: cookie })).status).toBe(401);
135
+ expect((await request([account.bucketEndpoint], {
136
+ ...headers,
137
+ Origin: "https://attacker.example",
138
+ })).status).toBe(403);
139
+ expect((await request([decentralizedTestUsers[1].bucketEndpoint])).status).toBe(403);
140
+
141
+ const approval = await request([account.bucketEndpoint]);
142
+ expect(approval.status).toBe(200);
143
+ const { redirectUri: callback } = await approval.json() as { redirectUri: string };
144
+ const code = new URL(callback).searchParams.get("code");
145
+ expect(code).toBeTruthy();
146
+ expect(new URL(callback).searchParams.get("state")).toBe("test-state");
147
+
148
+ const exchange = await fetch("https://localhost:5173/app/oauth/token", {
149
+ method: "POST",
150
+ body: new URLSearchParams({ code: code!, redirect_uri: redirectUri }),
151
+ });
152
+ expect(exchange.status).toBe(200);
153
+ expect((await exchange.json()).access_token).toBeTypeOf("string");
154
+ });
155
+
156
+ test("deleting a bucket removes its values and rejects requests to its old URL", async () => {
157
+ const account = decentralizedTestUsers[0];
158
+ const smallKey = crypto.randomUUID();
159
+ const largeKey = crypto.randomUUID();
160
+ const largeValue = new Uint8Array(32 * 1024 + 1).fill(7);
161
+ const authorization = { Authorization: `Bearer ${account.token}` };
162
+
163
+ for (const [key, value] of [
164
+ [smallKey, new Uint8Array([1, 2, 3])],
165
+ [largeKey, largeValue],
166
+ ] as const) {
167
+ const response = await fetch(`${account.bucketEndpoint}/value/${key}`, {
168
+ method: "PUT",
169
+ headers: authorization,
170
+ body: value,
171
+ });
172
+ expect(response.status).toBe(201);
173
+ }
174
+
175
+ const r2Key = `${account.bucketId}/${largeKey}`;
176
+ expect(await environment.storedR2ObjectExists(r2Key)).toBe(true);
177
+
178
+ const deletion = await fetch(
179
+ `https://localhost:5173/app/service-instances/bucket/service/${account.bucketId}`,
180
+ {
181
+ method: "DELETE",
182
+ headers: {
183
+ Cookie: `account_${account.accountId}=${account.token}`,
184
+ "X-Graffiti-Account": String(account.accountId),
185
+ },
186
+ },
187
+ );
188
+ expect(deletion.status).toBe(200);
189
+
190
+ expect(await environment.storedR2ObjectExists(r2Key)).toBe(false);
191
+ for (const key of [smallKey, largeKey]) {
192
+ const response = await fetch(`${account.bucketEndpoint}/value/${key}`);
193
+ expect(response.status).toBe(404);
194
+ }
195
+ const batch = await fetch(`${account.bucketEndpoint}/values`, {
196
+ method: "POST",
197
+ headers: { "Content-Type": "application/cbor" },
198
+ body: dagCborEncode({ keys: [smallKey, largeKey], maxValueBytes: largeValue.length }).slice(),
199
+ });
200
+ expect((dagCborDecode(await batch.arrayBuffer()) as { results: { status: number }[] }).results)
201
+ .toEqual([{ status: 404 }, { status: 404 }]);
202
+
203
+ const putAfterDeletion = await fetch(`${account.bucketEndpoint}/value/${smallKey}`, {
204
+ method: "PUT",
205
+ headers: authorization,
206
+ body: new Uint8Array([4]),
207
+ });
208
+ expect(putAfterDeletion.status).toBe(404);
209
+ });
210
+
109
211
  // How to log in/out vvv
110
212
  async function login(handle: string) {
111
213
  const actor = await handleMethods.handleToActor(handle);
@@ -174,3 +276,5 @@ describe("GraffitiDecentralized Tests", () => {
174
276
  });
175
277
  }
176
278
  });
279
+
280
+ inboxUnitTests();