@graffiti-garden/implementation-decentralized 0.1.3 → 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 (41) 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/4-graffiti.d.ts +1 -0
  8. package/dist/3-protocol/4-graffiti.d.ts.map +1 -1
  9. package/dist/browser/index.js +7 -7
  10. package/dist/browser/index.js.map +4 -4
  11. package/dist/cjs/1-services/4-inboxes/cache.js +186 -0
  12. package/dist/cjs/1-services/4-inboxes/cache.js.map +7 -0
  13. package/dist/cjs/1-services/4-inboxes/messages.js +69 -0
  14. package/dist/cjs/1-services/4-inboxes/messages.js.map +7 -0
  15. package/dist/cjs/1-services/4-inboxes-tests.js +339 -9
  16. package/dist/cjs/1-services/4-inboxes-tests.js.map +3 -3
  17. package/dist/cjs/1-services/4-inboxes.js +210 -399
  18. package/dist/cjs/1-services/4-inboxes.js.map +3 -3
  19. package/dist/cjs/3-protocol/4-graffiti.js +19 -10
  20. package/dist/cjs/3-protocol/4-graffiti.js.map +2 -2
  21. package/dist/cjs/index.spec.js +1 -0
  22. package/dist/cjs/index.spec.js.map +2 -2
  23. package/dist/esm/1-services/4-inboxes/cache.js +156 -0
  24. package/dist/esm/1-services/4-inboxes/cache.js.map +7 -0
  25. package/dist/esm/1-services/4-inboxes/messages.js +60 -0
  26. package/dist/esm/1-services/4-inboxes/messages.js.map +7 -0
  27. package/dist/esm/1-services/4-inboxes-tests.js +340 -10
  28. package/dist/esm/1-services/4-inboxes-tests.js.map +3 -3
  29. package/dist/esm/1-services/4-inboxes.js +222 -383
  30. package/dist/esm/1-services/4-inboxes.js.map +3 -3
  31. package/dist/esm/3-protocol/4-graffiti.js +19 -10
  32. package/dist/esm/3-protocol/4-graffiti.js.map +2 -2
  33. package/dist/esm/index.spec.js +2 -1
  34. package/dist/esm/index.spec.js.map +2 -2
  35. package/package.json +1 -1
  36. package/src/1-services/4-inboxes/cache.ts +227 -0
  37. package/src/1-services/4-inboxes/messages.ts +63 -0
  38. package/src/1-services/4-inboxes-tests.ts +387 -2
  39. package/src/1-services/4-inboxes.ts +297 -512
  40. package/src/3-protocol/4-graffiti.ts +26 -10
  41. package/src/index.spec.ts +3 -1
@@ -13,7 +13,7 @@ import { decode as dagCborDecode, encode as dagCborEncode } from "@ipld/dag-cbor
13
13
  import { Handles } from "./3-protocol/2-handles";
14
14
  import { didTests } from "./1-services/2-dids-tests";
15
15
  import { storageBucketTests } from "./1-services/3-storage-buckets-tests";
16
- import { inboxTests } from "./1-services/4-inboxes-tests";
16
+ import { inboxTests, inboxUnitTests } from "./1-services/4-inboxes-tests";
17
17
  import { stringEncodingTests } from "./2-primitives/1-string-encoding-tests";
18
18
  import { contentAddressesTests } from "./2-primitives/2-content-addresses-tests";
19
19
  import { channelAttestationTests } from "./2-primitives/3-channel-attestations-tests";
@@ -236,4 +236,5 @@ describe("GraffitiDecentralized Tests", () => {
236
236
  });
237
237
  }
238
238
  });
239
+ inboxUnitTests();
239
240
  //# sourceMappingURL=index.spec.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/index.spec.ts"],
4
- "sourcesContent": ["import type {\n GraffitiLoginEvent,\n GraffitiLogoutEvent,\n GraffitiSession,\n} from \"@graffiti-garden/api\";\nimport {\n graffitiCRUDTests,\n graffitiDiscoverTests,\n graffitiMediaTests,\n} from \"@graffiti-garden/api/tests\";\nimport { DecentralizedIdentifiers } from \"./1-services/2-dids\";\nimport { Authorization } from \"./1-services/1-authorization\";\nimport { StorageBuckets } from \"./1-services/3-storage-buckets\";\nimport { Inboxes } from \"./1-services/4-inboxes\";\nimport { Sessions } from \"./3-protocol/1-sessions\";\nimport { afterAll, beforeAll, describe, expect, test } from \"vitest\";\nimport { decode as dagCborDecode, encode as dagCborEncode } from \"@ipld/dag-cbor\";\nimport { Handles } from \"./3-protocol/2-handles\";\nimport { didTests } from \"./1-services/2-dids-tests\";\nimport { storageBucketTests } from \"./1-services/3-storage-buckets-tests\";\nimport { inboxTests } from \"./1-services/4-inboxes-tests\";\nimport { stringEncodingTests } from \"./2-primitives/1-string-encoding-tests\";\nimport { contentAddressesTests } from \"./2-primitives/2-content-addresses-tests\";\nimport { channelAttestationTests } from \"./2-primitives/3-channel-attestations-tests\";\nimport { allowedAttestationTests } from \"./2-primitives/4-allowed-attestations-tests\";\nimport { handleTests } from \"./3-protocol/2-handles-tests\";\nimport { objectEncodingTests } from \"./3-protocol/3-object-encoding-tests\";\nimport { GraffitiDecentralized } from \"./3-protocol/4-graffiti\";\nimport {\n DecentralizedTestEnvironment,\n decentralizedSharedInbox,\n decentralizedTestUsers,\n} from \"../test/environment\";\n\ndescribe(\"GraffitiDecentralized Tests\", () => {\n const environment = new DecentralizedTestEnvironment();\n const handles = decentralizedTestUsers.map((user) => user.handle);\n let sessions: GraffitiSession[] = [];\n let sessionMethods: Sessions;\n let handleMethods: Handles;\n\n beforeAll(async () => {\n await environment.start();\n\n const dids = new DecentralizedIdentifiers();\n sessionMethods = new Sessions({\n dids,\n authorization: new Authorization(),\n inboxes: new Inboxes(),\n storageBuckets: new StorageBuckets(),\n });\n handleMethods = new Handles({ dids });\n\n for (const handle of handles) {\n sessions.push(await login(handle));\n }\n });\n\n afterAll(async () => {\n try {\n for (const session of sessions) {\n await logout(session.actor);\n }\n } finally {\n await environment.stop();\n }\n });\n\n // Service tests\n didTests();\n storageBucketTests(\n decentralizedTestUsers[0].bucketEndpoint,\n decentralizedTestUsers[0].token,\n );\n inboxTests(\n decentralizedTestUsers[0].inboxEndpoint,\n decentralizedTestUsers[0].token,\n );\n\n // Primitive tests\n stringEncodingTests();\n contentAddressesTests();\n channelAttestationTests();\n allowedAttestationTests();\n\n // Protocol tests\n handleTests(handles[0]);\n objectEncodingTests();\n\n const useGraffiti = () =>\n new GraffitiDecentralized({\n defaultInboxEndpoints: [decentralizedSharedInbox],\n });\n graffitiCRUDTests(\n useGraffiti,\n () => sessions[0],\n () => sessions[1],\n );\n graffitiMediaTests(\n useGraffiti,\n () => sessions[0],\n () => sessions[1],\n );\n graffitiDiscoverTests(\n useGraffiti,\n () => sessions[0],\n () => sessions[1],\n );\n\n test(\"OAuth codes require approval from the selected account\", async () => {\n const account = decentralizedTestUsers[0];\n const redirectUri = \"https://client.example/callback\";\n const endpoint = \"https://localhost:5173/app/oauth/authorize\";\n const cookie = `account_${account.accountId}=${account.token}`;\n const headers = {\n Cookie: cookie,\n \"X-Graffiti-Account\": String(account.accountId),\n \"Content-Type\": \"application/json\",\n Origin: \"https://localhost:5173\",\n };\n const request = (scope: string[], requestHeaders: HeadersInit = headers) =>\n fetch(endpoint, {\n method: \"POST\",\n headers: requestHeaders,\n body: JSON.stringify({ redirect_uri: redirectUri, state: \"test-state\", scope }),\n });\n\n const navigation = await fetch(\n `${endpoint}?account=${account.accountId}&redirect_uri=${encodeURIComponent(redirectUri)}`,\n { headers: { Cookie: cookie } },\n );\n expect(navigation.status).toBe(404);\n\n expect((await request([account.bucketEndpoint], { Cookie: cookie })).status).toBe(401);\n expect((await request([account.bucketEndpoint], {\n ...headers,\n Origin: \"https://attacker.example\",\n })).status).toBe(403);\n expect((await request([decentralizedTestUsers[1].bucketEndpoint])).status).toBe(403);\n\n const approval = await request([account.bucketEndpoint]);\n expect(approval.status).toBe(200);\n const { redirectUri: callback } = await approval.json() as { redirectUri: string };\n const code = new URL(callback).searchParams.get(\"code\");\n expect(code).toBeTruthy();\n expect(new URL(callback).searchParams.get(\"state\")).toBe(\"test-state\");\n\n const exchange = await fetch(\"https://localhost:5173/app/oauth/token\", {\n method: \"POST\",\n body: new URLSearchParams({ code: code!, redirect_uri: redirectUri }),\n });\n expect(exchange.status).toBe(200);\n expect((await exchange.json()).access_token).toBeTypeOf(\"string\");\n });\n\n test(\"deleting a bucket removes its values and rejects requests to its old URL\", async () => {\n const account = decentralizedTestUsers[0];\n const smallKey = crypto.randomUUID();\n const largeKey = crypto.randomUUID();\n const largeValue = new Uint8Array(32 * 1024 + 1).fill(7);\n const authorization = { Authorization: `Bearer ${account.token}` };\n\n for (const [key, value] of [\n [smallKey, new Uint8Array([1, 2, 3])],\n [largeKey, largeValue],\n ] as const) {\n const response = await fetch(`${account.bucketEndpoint}/value/${key}`, {\n method: \"PUT\",\n headers: authorization,\n body: value,\n });\n expect(response.status).toBe(201);\n }\n\n const r2Key = `${account.bucketId}/${largeKey}`;\n expect(await environment.storedR2ObjectExists(r2Key)).toBe(true);\n\n const deletion = await fetch(\n `https://localhost:5173/app/service-instances/bucket/service/${account.bucketId}`,\n {\n method: \"DELETE\",\n headers: {\n Cookie: `account_${account.accountId}=${account.token}`,\n \"X-Graffiti-Account\": String(account.accountId),\n },\n },\n );\n expect(deletion.status).toBe(200);\n\n expect(await environment.storedR2ObjectExists(r2Key)).toBe(false);\n for (const key of [smallKey, largeKey]) {\n const response = await fetch(`${account.bucketEndpoint}/value/${key}`);\n expect(response.status).toBe(404);\n }\n const batch = await fetch(`${account.bucketEndpoint}/values`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/cbor\" },\n body: dagCborEncode({ keys: [smallKey, largeKey], maxValueBytes: largeValue.length }).slice(),\n });\n expect((dagCborDecode(await batch.arrayBuffer()) as { results: { status: number }[] }).results)\n .toEqual([{ status: 404 }, { status: 404 }]);\n\n const putAfterDeletion = await fetch(`${account.bucketEndpoint}/value/${smallKey}`, {\n method: \"PUT\",\n headers: authorization,\n body: new Uint8Array([4]),\n });\n expect(putAfterDeletion.status).toBe(404);\n });\n\n // How to log in/out vvv\n async function login(handle: string) {\n const actor = await handleMethods.handleToActor(handle);\n\n return await new Promise<GraffitiSession>((resolve, reject) => {\n let timeout: ReturnType<typeof setTimeout>;\n const cleanup = () => {\n clearTimeout(timeout);\n sessionMethods.sessionEvents.removeEventListener(\"login\", listener);\n };\n const listener = (e: unknown) => {\n if (!(e instanceof CustomEvent)) return;\n const detail = e.detail as GraffitiLoginEvent[\"detail\"];\n if (detail.session?.actor !== actor) return;\n cleanup();\n if (detail.error) {\n reject(detail.error);\n } else {\n resolve(detail.session);\n }\n };\n sessionMethods.sessionEvents.addEventListener(\"login\", listener);\n\n timeout = setTimeout(\n () => {\n cleanup();\n reject(new Error(\"Authorization timed out\"));\n },\n 30 * 1000,\n );\n\n void sessionMethods.login(actor);\n });\n }\n\n async function logout(actor: string) {\n return await new Promise<void>((resolve, reject) => {\n let timeout: ReturnType<typeof setTimeout>;\n const cleanup = () => {\n clearTimeout(timeout);\n sessionMethods.sessionEvents.removeEventListener(\"logout\", listener);\n };\n const listener = (e: unknown) => {\n if (!(e instanceof CustomEvent)) return;\n const detail = e.detail as GraffitiLogoutEvent[\"detail\"];\n if (detail.actor !== actor) return;\n cleanup();\n\n if (detail.error) {\n reject(detail.error);\n } else {\n resolve();\n }\n };\n sessionMethods.sessionEvents.addEventListener(\"logout\", listener);\n\n timeout = setTimeout(\n () => {\n cleanup();\n reject(new Error(\"Logout timed out\"));\n },\n 30 * 1000,\n );\n\n void sessionMethods.logout(actor);\n });\n }\n});\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type {\n GraffitiLoginEvent,\n GraffitiLogoutEvent,\n GraffitiSession,\n} from \"@graffiti-garden/api\";\nimport {\n graffitiCRUDTests,\n graffitiDiscoverTests,\n graffitiMediaTests,\n} from \"@graffiti-garden/api/tests\";\nimport { DecentralizedIdentifiers } from \"./1-services/2-dids\";\nimport { Authorization } from \"./1-services/1-authorization\";\nimport { StorageBuckets } from \"./1-services/3-storage-buckets\";\nimport { Inboxes } from \"./1-services/4-inboxes\";\nimport { Sessions } from \"./3-protocol/1-sessions\";\nimport { afterAll, beforeAll, describe, expect, test } from \"vitest\";\nimport { decode as dagCborDecode, encode as dagCborEncode } from \"@ipld/dag-cbor\";\nimport { Handles } from \"./3-protocol/2-handles\";\nimport { didTests } from \"./1-services/2-dids-tests\";\nimport { storageBucketTests } from \"./1-services/3-storage-buckets-tests\";\nimport { inboxTests, inboxUnitTests } from \"./1-services/4-inboxes-tests\";\nimport { stringEncodingTests } from \"./2-primitives/1-string-encoding-tests\";\nimport { contentAddressesTests } from \"./2-primitives/2-content-addresses-tests\";\nimport { channelAttestationTests } from \"./2-primitives/3-channel-attestations-tests\";\nimport { allowedAttestationTests } from \"./2-primitives/4-allowed-attestations-tests\";\nimport { handleTests } from \"./3-protocol/2-handles-tests\";\nimport { objectEncodingTests } from \"./3-protocol/3-object-encoding-tests\";\nimport { GraffitiDecentralized } from \"./3-protocol/4-graffiti\";\nimport {\n DecentralizedTestEnvironment,\n decentralizedSharedInbox,\n decentralizedTestUsers,\n} from \"../test/environment\";\n\ndescribe(\"GraffitiDecentralized Tests\", () => {\n const environment = new DecentralizedTestEnvironment();\n const handles = decentralizedTestUsers.map((user) => user.handle);\n let sessions: GraffitiSession[] = [];\n let sessionMethods: Sessions;\n let handleMethods: Handles;\n\n beforeAll(async () => {\n await environment.start();\n\n const dids = new DecentralizedIdentifiers();\n sessionMethods = new Sessions({\n dids,\n authorization: new Authorization(),\n inboxes: new Inboxes(),\n storageBuckets: new StorageBuckets(),\n });\n handleMethods = new Handles({ dids });\n\n for (const handle of handles) {\n sessions.push(await login(handle));\n }\n });\n\n afterAll(async () => {\n try {\n for (const session of sessions) {\n await logout(session.actor);\n }\n } finally {\n await environment.stop();\n }\n });\n\n // Service tests\n didTests();\n storageBucketTests(\n decentralizedTestUsers[0].bucketEndpoint,\n decentralizedTestUsers[0].token,\n );\n inboxTests(\n decentralizedTestUsers[0].inboxEndpoint,\n decentralizedTestUsers[0].token,\n );\n\n // Primitive tests\n stringEncodingTests();\n contentAddressesTests();\n channelAttestationTests();\n allowedAttestationTests();\n\n // Protocol tests\n handleTests(handles[0]);\n objectEncodingTests();\n\n const useGraffiti = () =>\n new GraffitiDecentralized({\n defaultInboxEndpoints: [decentralizedSharedInbox],\n });\n graffitiCRUDTests(\n useGraffiti,\n () => sessions[0],\n () => sessions[1],\n );\n graffitiMediaTests(\n useGraffiti,\n () => sessions[0],\n () => sessions[1],\n );\n graffitiDiscoverTests(\n useGraffiti,\n () => sessions[0],\n () => sessions[1],\n );\n\n test(\"OAuth codes require approval from the selected account\", async () => {\n const account = decentralizedTestUsers[0];\n const redirectUri = \"https://client.example/callback\";\n const endpoint = \"https://localhost:5173/app/oauth/authorize\";\n const cookie = `account_${account.accountId}=${account.token}`;\n const headers = {\n Cookie: cookie,\n \"X-Graffiti-Account\": String(account.accountId),\n \"Content-Type\": \"application/json\",\n Origin: \"https://localhost:5173\",\n };\n const request = (scope: string[], requestHeaders: HeadersInit = headers) =>\n fetch(endpoint, {\n method: \"POST\",\n headers: requestHeaders,\n body: JSON.stringify({ redirect_uri: redirectUri, state: \"test-state\", scope }),\n });\n\n const navigation = await fetch(\n `${endpoint}?account=${account.accountId}&redirect_uri=${encodeURIComponent(redirectUri)}`,\n { headers: { Cookie: cookie } },\n );\n expect(navigation.status).toBe(404);\n\n expect((await request([account.bucketEndpoint], { Cookie: cookie })).status).toBe(401);\n expect((await request([account.bucketEndpoint], {\n ...headers,\n Origin: \"https://attacker.example\",\n })).status).toBe(403);\n expect((await request([decentralizedTestUsers[1].bucketEndpoint])).status).toBe(403);\n\n const approval = await request([account.bucketEndpoint]);\n expect(approval.status).toBe(200);\n const { redirectUri: callback } = await approval.json() as { redirectUri: string };\n const code = new URL(callback).searchParams.get(\"code\");\n expect(code).toBeTruthy();\n expect(new URL(callback).searchParams.get(\"state\")).toBe(\"test-state\");\n\n const exchange = await fetch(\"https://localhost:5173/app/oauth/token\", {\n method: \"POST\",\n body: new URLSearchParams({ code: code!, redirect_uri: redirectUri }),\n });\n expect(exchange.status).toBe(200);\n expect((await exchange.json()).access_token).toBeTypeOf(\"string\");\n });\n\n test(\"deleting a bucket removes its values and rejects requests to its old URL\", async () => {\n const account = decentralizedTestUsers[0];\n const smallKey = crypto.randomUUID();\n const largeKey = crypto.randomUUID();\n const largeValue = new Uint8Array(32 * 1024 + 1).fill(7);\n const authorization = { Authorization: `Bearer ${account.token}` };\n\n for (const [key, value] of [\n [smallKey, new Uint8Array([1, 2, 3])],\n [largeKey, largeValue],\n ] as const) {\n const response = await fetch(`${account.bucketEndpoint}/value/${key}`, {\n method: \"PUT\",\n headers: authorization,\n body: value,\n });\n expect(response.status).toBe(201);\n }\n\n const r2Key = `${account.bucketId}/${largeKey}`;\n expect(await environment.storedR2ObjectExists(r2Key)).toBe(true);\n\n const deletion = await fetch(\n `https://localhost:5173/app/service-instances/bucket/service/${account.bucketId}`,\n {\n method: \"DELETE\",\n headers: {\n Cookie: `account_${account.accountId}=${account.token}`,\n \"X-Graffiti-Account\": String(account.accountId),\n },\n },\n );\n expect(deletion.status).toBe(200);\n\n expect(await environment.storedR2ObjectExists(r2Key)).toBe(false);\n for (const key of [smallKey, largeKey]) {\n const response = await fetch(`${account.bucketEndpoint}/value/${key}`);\n expect(response.status).toBe(404);\n }\n const batch = await fetch(`${account.bucketEndpoint}/values`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/cbor\" },\n body: dagCborEncode({ keys: [smallKey, largeKey], maxValueBytes: largeValue.length }).slice(),\n });\n expect((dagCborDecode(await batch.arrayBuffer()) as { results: { status: number }[] }).results)\n .toEqual([{ status: 404 }, { status: 404 }]);\n\n const putAfterDeletion = await fetch(`${account.bucketEndpoint}/value/${smallKey}`, {\n method: \"PUT\",\n headers: authorization,\n body: new Uint8Array([4]),\n });\n expect(putAfterDeletion.status).toBe(404);\n });\n\n // How to log in/out vvv\n async function login(handle: string) {\n const actor = await handleMethods.handleToActor(handle);\n\n return await new Promise<GraffitiSession>((resolve, reject) => {\n let timeout: ReturnType<typeof setTimeout>;\n const cleanup = () => {\n clearTimeout(timeout);\n sessionMethods.sessionEvents.removeEventListener(\"login\", listener);\n };\n const listener = (e: unknown) => {\n if (!(e instanceof CustomEvent)) return;\n const detail = e.detail as GraffitiLoginEvent[\"detail\"];\n if (detail.session?.actor !== actor) return;\n cleanup();\n if (detail.error) {\n reject(detail.error);\n } else {\n resolve(detail.session);\n }\n };\n sessionMethods.sessionEvents.addEventListener(\"login\", listener);\n\n timeout = setTimeout(\n () => {\n cleanup();\n reject(new Error(\"Authorization timed out\"));\n },\n 30 * 1000,\n );\n\n void sessionMethods.login(actor);\n });\n }\n\n async function logout(actor: string) {\n return await new Promise<void>((resolve, reject) => {\n let timeout: ReturnType<typeof setTimeout>;\n const cleanup = () => {\n clearTimeout(timeout);\n sessionMethods.sessionEvents.removeEventListener(\"logout\", listener);\n };\n const listener = (e: unknown) => {\n if (!(e instanceof CustomEvent)) return;\n const detail = e.detail as GraffitiLogoutEvent[\"detail\"];\n if (detail.actor !== actor) return;\n cleanup();\n\n if (detail.error) {\n reject(detail.error);\n } else {\n resolve();\n }\n };\n sessionMethods.sessionEvents.addEventListener(\"logout\", listener);\n\n timeout = setTimeout(\n () => {\n cleanup();\n reject(new Error(\"Logout timed out\"));\n },\n 30 * 1000,\n );\n\n void sessionMethods.logout(actor);\n });\n }\n});\n\ninboxUnitTests();\n"],
5
+ "mappings": "AAKA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gCAAgC;AACzC,SAAS,qBAAqB;AAC9B,SAAS,sBAAsB;AAC/B,SAAS,eAAe;AACxB,SAAS,gBAAgB;AACzB,SAAS,UAAU,WAAW,UAAU,QAAQ,YAAY;AAC5D,SAAS,UAAU,eAAe,UAAU,qBAAqB;AACjE,SAAS,eAAe;AACxB,SAAS,gBAAgB;AACzB,SAAS,0BAA0B;AACnC,SAAS,YAAY,sBAAsB;AAC3C,SAAS,2BAA2B;AACpC,SAAS,6BAA6B;AACtC,SAAS,+BAA+B;AACxC,SAAS,+BAA+B;AACxC,SAAS,mBAAmB;AAC5B,SAAS,2BAA2B;AACpC,SAAS,6BAA6B;AACtC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,+BAA+B,MAAM;AAC5C,QAAM,cAAc,IAAI,6BAA6B;AACrD,QAAM,UAAU,uBAAuB,IAAI,CAAC,SAAS,KAAK,MAAM;AAChE,MAAI,WAA8B,CAAC;AACnC,MAAI;AACJ,MAAI;AAEJ,YAAU,YAAY;AACpB,UAAM,YAAY,MAAM;AAExB,UAAM,OAAO,IAAI,yBAAyB;AAC1C,qBAAiB,IAAI,SAAS;AAAA,MAC5B;AAAA,MACA,eAAe,IAAI,cAAc;AAAA,MACjC,SAAS,IAAI,QAAQ;AAAA,MACrB,gBAAgB,IAAI,eAAe;AAAA,IACrC,CAAC;AACD,oBAAgB,IAAI,QAAQ,EAAE,KAAK,CAAC;AAEpC,eAAW,UAAU,SAAS;AAC5B,eAAS,KAAK,MAAM,MAAM,MAAM,CAAC;AAAA,IACnC;AAAA,EACF,CAAC;AAED,WAAS,YAAY;AACnB,QAAI;AACF,iBAAW,WAAW,UAAU;AAC9B,cAAM,OAAO,QAAQ,KAAK;AAAA,MAC5B;AAAA,IACF,UAAE;AACA,YAAM,YAAY,KAAK;AAAA,IACzB;AAAA,EACF,CAAC;AAGD,WAAS;AACT;AAAA,IACE,uBAAuB,CAAC,EAAE;AAAA,IAC1B,uBAAuB,CAAC,EAAE;AAAA,EAC5B;AACA;AAAA,IACE,uBAAuB,CAAC,EAAE;AAAA,IAC1B,uBAAuB,CAAC,EAAE;AAAA,EAC5B;AAGA,sBAAoB;AACpB,wBAAsB;AACtB,0BAAwB;AACxB,0BAAwB;AAGxB,cAAY,QAAQ,CAAC,CAAC;AACtB,sBAAoB;AAEpB,QAAM,cAAc,MAClB,IAAI,sBAAsB;AAAA,IACxB,uBAAuB,CAAC,wBAAwB;AAAA,EAClD,CAAC;AACH;AAAA,IACE;AAAA,IACA,MAAM,SAAS,CAAC;AAAA,IAChB,MAAM,SAAS,CAAC;AAAA,EAClB;AACA;AAAA,IACE;AAAA,IACA,MAAM,SAAS,CAAC;AAAA,IAChB,MAAM,SAAS,CAAC;AAAA,EAClB;AACA;AAAA,IACE;AAAA,IACA,MAAM,SAAS,CAAC;AAAA,IAChB,MAAM,SAAS,CAAC;AAAA,EAClB;AAEA,OAAK,0DAA0D,YAAY;AACzE,UAAM,UAAU,uBAAuB,CAAC;AACxC,UAAM,cAAc;AACpB,UAAM,WAAW;AACjB,UAAM,SAAS,WAAW,QAAQ,SAAS,IAAI,QAAQ,KAAK;AAC5D,UAAM,UAAU;AAAA,MACd,QAAQ;AAAA,MACR,sBAAsB,OAAO,QAAQ,SAAS;AAAA,MAC9C,gBAAgB;AAAA,MAChB,QAAQ;AAAA,IACV;AACA,UAAM,UAAU,CAAC,OAAiB,iBAA8B,YAC9D,MAAM,UAAU;AAAA,MACd,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,MAAM,KAAK,UAAU,EAAE,cAAc,aAAa,OAAO,cAAc,MAAM,CAAC;AAAA,IAChF,CAAC;AAEH,UAAM,aAAa,MAAM;AAAA,MACvB,GAAG,QAAQ,YAAY,QAAQ,SAAS,iBAAiB,mBAAmB,WAAW,CAAC;AAAA,MACxF,EAAE,SAAS,EAAE,QAAQ,OAAO,EAAE;AAAA,IAChC;AACA,WAAO,WAAW,MAAM,EAAE,KAAK,GAAG;AAElC,YAAQ,MAAM,QAAQ,CAAC,QAAQ,cAAc,GAAG,EAAE,QAAQ,OAAO,CAAC,GAAG,MAAM,EAAE,KAAK,GAAG;AACrF,YAAQ,MAAM,QAAQ,CAAC,QAAQ,cAAc,GAAG;AAAA,MAC9C,GAAG;AAAA,MACH,QAAQ;AAAA,IACV,CAAC,GAAG,MAAM,EAAE,KAAK,GAAG;AACpB,YAAQ,MAAM,QAAQ,CAAC,uBAAuB,CAAC,EAAE,cAAc,CAAC,GAAG,MAAM,EAAE,KAAK,GAAG;AAEnF,UAAM,WAAW,MAAM,QAAQ,CAAC,QAAQ,cAAc,CAAC;AACvD,WAAO,SAAS,MAAM,EAAE,KAAK,GAAG;AAChC,UAAM,EAAE,aAAa,SAAS,IAAI,MAAM,SAAS,KAAK;AACtD,UAAM,OAAO,IAAI,IAAI,QAAQ,EAAE,aAAa,IAAI,MAAM;AACtD,WAAO,IAAI,EAAE,WAAW;AACxB,WAAO,IAAI,IAAI,QAAQ,EAAE,aAAa,IAAI,OAAO,CAAC,EAAE,KAAK,YAAY;AAErE,UAAM,WAAW,MAAM,MAAM,0CAA0C;AAAA,MACrE,QAAQ;AAAA,MACR,MAAM,IAAI,gBAAgB,EAAE,MAAa,cAAc,YAAY,CAAC;AAAA,IACtE,CAAC;AACD,WAAO,SAAS,MAAM,EAAE,KAAK,GAAG;AAChC,YAAQ,MAAM,SAAS,KAAK,GAAG,YAAY,EAAE,WAAW,QAAQ;AAAA,EAClE,CAAC;AAED,OAAK,4EAA4E,YAAY;AAC3F,UAAM,UAAU,uBAAuB,CAAC;AACxC,UAAM,WAAW,OAAO,WAAW;AACnC,UAAM,WAAW,OAAO,WAAW;AACnC,UAAM,aAAa,IAAI,WAAW,KAAK,OAAO,CAAC,EAAE,KAAK,CAAC;AACvD,UAAM,gBAAgB,EAAE,eAAe,UAAU,QAAQ,KAAK,GAAG;AAEjE,eAAW,CAAC,KAAK,KAAK,KAAK;AAAA,MACzB,CAAC,UAAU,IAAI,WAAW,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;AAAA,MACpC,CAAC,UAAU,UAAU;AAAA,IACvB,GAAY;AACV,YAAM,WAAW,MAAM,MAAM,GAAG,QAAQ,cAAc,UAAU,GAAG,IAAI;AAAA,QACrE,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,MAAM;AAAA,MACR,CAAC;AACD,aAAO,SAAS,MAAM,EAAE,KAAK,GAAG;AAAA,IAClC;AAEA,UAAM,QAAQ,GAAG,QAAQ,QAAQ,IAAI,QAAQ;AAC7C,WAAO,MAAM,YAAY,qBAAqB,KAAK,CAAC,EAAE,KAAK,IAAI;AAE/D,UAAM,WAAW,MAAM;AAAA,MACrB,+DAA+D,QAAQ,QAAQ;AAAA,MAC/E;AAAA,QACE,QAAQ;AAAA,QACR,SAAS;AAAA,UACP,QAAQ,WAAW,QAAQ,SAAS,IAAI,QAAQ,KAAK;AAAA,UACrD,sBAAsB,OAAO,QAAQ,SAAS;AAAA,QAChD;AAAA,MACF;AAAA,IACF;AACA,WAAO,SAAS,MAAM,EAAE,KAAK,GAAG;AAEhC,WAAO,MAAM,YAAY,qBAAqB,KAAK,CAAC,EAAE,KAAK,KAAK;AAChE,eAAW,OAAO,CAAC,UAAU,QAAQ,GAAG;AACtC,YAAM,WAAW,MAAM,MAAM,GAAG,QAAQ,cAAc,UAAU,GAAG,EAAE;AACrE,aAAO,SAAS,MAAM,EAAE,KAAK,GAAG;AAAA,IAClC;AACA,UAAM,QAAQ,MAAM,MAAM,GAAG,QAAQ,cAAc,WAAW;AAAA,MAC5D,QAAQ;AAAA,MACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,MAC9C,MAAM,cAAc,EAAE,MAAM,CAAC,UAAU,QAAQ,GAAG,eAAe,WAAW,OAAO,CAAC,EAAE,MAAM;AAAA,IAC9F,CAAC;AACD,WAAQ,cAAc,MAAM,MAAM,YAAY,CAAC,EAAwC,OAAO,EAC3F,QAAQ,CAAC,EAAE,QAAQ,IAAI,GAAG,EAAE,QAAQ,IAAI,CAAC,CAAC;AAE7C,UAAM,mBAAmB,MAAM,MAAM,GAAG,QAAQ,cAAc,UAAU,QAAQ,IAAI;AAAA,MAClF,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,MAAM,IAAI,WAAW,CAAC,CAAC,CAAC;AAAA,IAC1B,CAAC;AACD,WAAO,iBAAiB,MAAM,EAAE,KAAK,GAAG;AAAA,EAC1C,CAAC;AAGD,iBAAe,MAAM,QAAgB;AACnC,UAAM,QAAQ,MAAM,cAAc,cAAc,MAAM;AAEtD,WAAO,MAAM,IAAI,QAAyB,CAAC,SAAS,WAAW;AAC7D,UAAI;AACJ,YAAM,UAAU,MAAM;AACpB,qBAAa,OAAO;AACpB,uBAAe,cAAc,oBAAoB,SAAS,QAAQ;AAAA,MACpE;AACA,YAAM,WAAW,CAAC,MAAe;AAC/B,YAAI,EAAE,aAAa,aAAc;AACjC,cAAM,SAAS,EAAE;AACjB,YAAI,OAAO,SAAS,UAAU,MAAO;AACrC,gBAAQ;AACR,YAAI,OAAO,OAAO;AAChB,iBAAO,OAAO,KAAK;AAAA,QACrB,OAAO;AACL,kBAAQ,OAAO,OAAO;AAAA,QACxB;AAAA,MACF;AACA,qBAAe,cAAc,iBAAiB,SAAS,QAAQ;AAE/D,gBAAU;AAAA,QACR,MAAM;AACJ,kBAAQ;AACR,iBAAO,IAAI,MAAM,yBAAyB,CAAC;AAAA,QAC7C;AAAA,QACA,KAAK;AAAA,MACP;AAEA,WAAK,eAAe,MAAM,KAAK;AAAA,IACjC,CAAC;AAAA,EACH;AAEA,iBAAe,OAAO,OAAe;AACnC,WAAO,MAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAClD,UAAI;AACJ,YAAM,UAAU,MAAM;AACpB,qBAAa,OAAO;AACpB,uBAAe,cAAc,oBAAoB,UAAU,QAAQ;AAAA,MACrE;AACA,YAAM,WAAW,CAAC,MAAe;AAC/B,YAAI,EAAE,aAAa,aAAc;AACjC,cAAM,SAAS,EAAE;AACjB,YAAI,OAAO,UAAU,MAAO;AAC5B,gBAAQ;AAER,YAAI,OAAO,OAAO;AAChB,iBAAO,OAAO,KAAK;AAAA,QACrB,OAAO;AACL,kBAAQ;AAAA,QACV;AAAA,MACF;AACA,qBAAe,cAAc,iBAAiB,UAAU,QAAQ;AAEhE,gBAAU;AAAA,QACR,MAAM;AACJ,kBAAQ;AACR,iBAAO,IAAI,MAAM,kBAAkB,CAAC;AAAA,QACtC;AAAA,QACA,KAAK;AAAA,MACP;AAEA,WAAK,eAAe,OAAO,KAAK;AAAA,IAClC,CAAC;AAAA,EACH;AACF,CAAC;AAED,eAAe;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@graffiti-garden/implementation-decentralized",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "A client for a decentralized implementation of the Graffiti API",
5
5
  "types": "./dist/index.d.ts",
6
6
  "module": "./dist/esm/index.js",
@@ -0,0 +1,227 @@
1
+ import { encode as dagCborEncode } from "@ipld/dag-cbor";
2
+ import { LABELED_MESSAGE_ID_KEY, type LabeledMessageBase } from "./messages";
3
+
4
+ // One record per inbox query. Messages live in the separate message cache;
5
+ // this record tracks their IDs and where to continue fetching from the server.
6
+ export type CacheQueryValue = {
7
+ // Server continuation state for the next page.
8
+ cursor: string;
9
+ // Identifies this local query history, not a server version. It stays the
10
+ // same as pages arrive; a reset creates a new one so old caller cursors expire.
11
+ version: string;
12
+ // All message IDs fetched for this query so far, in order. A caller's
13
+ // numSeen cursor indexes into this list to replay only unseen messages.
14
+ messageIds: string[];
15
+ waitTil?: number;
16
+ // Changes on every cached page write, even if the page is empty and neither
17
+ // messageIds nor cursor changes. A tab waiting on the query lock can use this
18
+ // to detect and reuse a page fetched by another tab. Older entries lack it.
19
+ generation?: string;
20
+ // Whether the server has another page; needed when reusing another tab's
21
+ // cached page. Older cache entries lack this and are refreshed on next query.
22
+ hasMore?: boolean;
23
+ };
24
+
25
+ export async function canUseIDB(): Promise<boolean> {
26
+ try {
27
+ if (!globalThis.indexedDB) return false;
28
+
29
+ // Small probe database
30
+ await new Promise<void>((resolve, reject) => {
31
+ const req = indexedDB.open("__idb_probe__", 1);
32
+ req.onupgradeneeded = () => req.result.createObjectStore("k");
33
+ req.onsuccess = () => {
34
+ req.result.close();
35
+ resolve();
36
+ };
37
+ req.onerror = () => reject(req.error);
38
+ });
39
+
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ export type Cache = {
47
+ messages: {
48
+ get(k: string): Promise<LabeledMessageBase | null | undefined>;
49
+ set(k: string, value: LabeledMessageBase | null): Promise<void>;
50
+ setMany(entries: [string, LabeledMessageBase][]): Promise<void>;
51
+ del(k: string): Promise<void>;
52
+ };
53
+ messageIds: {
54
+ get(k: string): Promise<CacheQueryValue | undefined>;
55
+ set(k: string, value: CacheQueryValue): Promise<void>;
56
+ del(k: string): Promise<void>;
57
+ };
58
+ };
59
+
60
+ export function getMessageCacheKey(inboxUrl: string, messageId: string) {
61
+ return `${encodeURIComponent(inboxUrl)}:${encodeURIComponent(messageId)}`;
62
+ }
63
+
64
+ export async function replayQuery(
65
+ cache: Cache,
66
+ inboxUrl: string,
67
+ messageIdsCacheKey: string,
68
+ query: CacheQueryValue,
69
+ numSeen = 0,
70
+ ): Promise<LabeledMessageBase[]> {
71
+ // Filter out all messageIds before
72
+ // the number already seen
73
+ const messageIds = query.messageIds.slice(numSeen);
74
+
75
+ // Get all the messages pointed to in the cache
76
+ return Promise.all(
77
+ messageIds.map(async (id) => {
78
+ const message = await cache.messages.get(getMessageCacheKey(inboxUrl, id));
79
+ if (!message) {
80
+ // Something is very wrong with the cache,
81
+ // it refers to message IDs that are not cached
82
+ try {
83
+ await cache.messageIds.del(messageIdsCacheKey);
84
+ } catch {}
85
+ throw new Error("Cache out of sync - perhaps clear browser storage");
86
+ }
87
+ return message;
88
+ }),
89
+ );
90
+ }
91
+
92
+ export async function storeQueryPage(
93
+ cache: Cache,
94
+ inboxUrl: string,
95
+ messageIdsCacheKey: string,
96
+ previous: CacheQueryValue | undefined,
97
+ page: {
98
+ messages: LabeledMessageBase[];
99
+ cursor: string;
100
+ hasMore: boolean;
101
+ waitTil?: number;
102
+ },
103
+ ): Promise<CacheQueryValue> {
104
+ // First cache the messages with their labels
105
+ await cache.messages.setMany(
106
+ page.messages.map((message) => [
107
+ getMessageCacheKey(inboxUrl, message[LABELED_MESSAGE_ID_KEY]),
108
+ message,
109
+ ]),
110
+ );
111
+ // Then store all the messageids
112
+ const query = {
113
+ cursor: page.cursor,
114
+ version: previous?.version ?? crypto.randomUUID(),
115
+ messageIds: [
116
+ ...(previous?.messageIds ?? []),
117
+ ...page.messages.map((message) => message[LABELED_MESSAGE_ID_KEY]),
118
+ ],
119
+ generation: crypto.randomUUID(),
120
+ hasMore: page.hasMore,
121
+ waitTil: page.waitTil,
122
+ };
123
+ await cache.messageIds.set(messageIdsCacheKey, query);
124
+ return query;
125
+ }
126
+
127
+ export async function getMessageIdsCacheKey(
128
+ inboxUrl: string,
129
+ type: "query" | "export",
130
+ body?: Uint8Array,
131
+ ): Promise<string> {
132
+ const cacheIdData = dagCborEncode({
133
+ inboxUrl,
134
+ type,
135
+ body: body ?? null,
136
+ });
137
+ return crypto.subtle
138
+ .digest("SHA-256", new Uint8Array(cacheIdData))
139
+ .then((bytes) =>
140
+ Array.from(new Uint8Array(bytes))
141
+ .map((byte) => byte.toString(16).padStart(2, "0"))
142
+ .join(""),
143
+ );
144
+ }
145
+
146
+ async function resetCacheDB() {
147
+ await new Promise<void>((resolve) => {
148
+ const req = indexedDB.deleteDatabase("graffiti-inbox-cache");
149
+ req.onsuccess = () => resolve();
150
+ req.onerror = () => resolve(); // best effort
151
+ req.onblocked = () => resolve(); // best effort
152
+ });
153
+ }
154
+
155
+ export async function createCache(): Promise<Cache> {
156
+ if (await canUseIDB()) {
157
+ const { openDB } = await import("idb");
158
+ const db = await openDB("graffiti-inbox-cache", 1, {
159
+ upgrade(db) {
160
+ if (!db.objectStoreNames.contains("m")) db.createObjectStore("m");
161
+ if (!db.objectStoreNames.contains("q")) db.createObjectStore("q");
162
+ },
163
+ });
164
+
165
+ return {
166
+ messages: {
167
+ get: async (k) => {
168
+ try {
169
+ return db.get("m", k);
170
+ } catch (error) {
171
+ console.error("Error getting message from cache:", error);
172
+ console.error("resetting cache...");
173
+ await resetCacheDB();
174
+ return undefined;
175
+ }
176
+ },
177
+ set: async (k, v) => {
178
+ await db.put("m", v, k);
179
+ },
180
+ setMany: async (entries) => {
181
+ if (entries.length === 0) return;
182
+ const tx = db.transaction("m", "readwrite");
183
+ await Promise.all([
184
+ ...entries.map(([k, v]) => tx.store.put(v, k)),
185
+ tx.done,
186
+ ]);
187
+ },
188
+ del: (k) => db.delete("m", k),
189
+ },
190
+ messageIds: {
191
+ get: async (k) => {
192
+ try {
193
+ return await db.get("q", k);
194
+ } catch (error) {
195
+ console.error("Error getting message IDs from cache:", error);
196
+ console.error("resetting cache...");
197
+ await resetCacheDB();
198
+ return undefined;
199
+ }
200
+ },
201
+ set: async (k, v) => {
202
+ await db.put("q", v, k);
203
+ },
204
+ del: (k) => db.delete("q", k),
205
+ },
206
+ };
207
+ }
208
+
209
+ const m = new Map<string, LabeledMessageBase | null>();
210
+ const q = new Map<string, CacheQueryValue>();
211
+
212
+ return {
213
+ messages: {
214
+ get: async (k) => m.get(k),
215
+ set: async (k, v) => void m.set(k, v),
216
+ setMany: async (entries) => {
217
+ for (const [k, v] of entries) m.set(k, v);
218
+ },
219
+ del: async (k) => void m.delete(k),
220
+ },
221
+ messageIds: {
222
+ get: async (k) => q.get(k),
223
+ set: async (k, v) => void q.set(k, v),
224
+ del: async (k) => void q.delete(k),
225
+ },
226
+ };
227
+ }
@@ -0,0 +1,63 @@
1
+ import type { JSONSchema, GraffitiObject } from "@graffiti-garden/api";
2
+ import {
3
+ type infer as infer_,
4
+ string,
5
+ url,
6
+ array,
7
+ optional,
8
+ nullable,
9
+ strictObject,
10
+ looseObject,
11
+ custom,
12
+ number,
13
+ boolean,
14
+ } from "zod/mini";
15
+
16
+ const GraffitiObjectSchema = strictObject({
17
+ value: looseObject({}),
18
+ channels: array(string()),
19
+ allowed: optional(nullable(array(url()))),
20
+ url: url(),
21
+ actor: url(),
22
+ });
23
+ export const Uint8ArraySchema = custom<Uint8Array>(
24
+ (v): v is Uint8Array => v instanceof Uint8Array,
25
+ );
26
+ export const TagsSchema = array(Uint8ArraySchema);
27
+
28
+ export const MESSAGE_TAGS_KEY = "t";
29
+ export const MESSAGE_OBJECT_KEY = "o";
30
+ export const MESSAGE_METADATA_KEY = "m";
31
+ export const MessageBaseSchema = strictObject({
32
+ [MESSAGE_TAGS_KEY]: TagsSchema,
33
+ [MESSAGE_OBJECT_KEY]: GraffitiObjectSchema,
34
+ [MESSAGE_METADATA_KEY]: Uint8ArraySchema,
35
+ });
36
+ type MessageBase = infer_<typeof MessageBaseSchema>;
37
+
38
+ export const LABELED_MESSAGE_ID_KEY = "id";
39
+ export const LABELED_MESSAGE_MESSAGE_KEY = "m";
40
+ export const LABELED_MESSAGE_LABEL_KEY = "l";
41
+ export const LabeledMessageBaseSchema = strictObject({
42
+ [LABELED_MESSAGE_ID_KEY]: string(),
43
+ [LABELED_MESSAGE_MESSAGE_KEY]: MessageBaseSchema,
44
+ [LABELED_MESSAGE_LABEL_KEY]: number(),
45
+ });
46
+ export type LabeledMessageBase = infer_<typeof LabeledMessageBaseSchema>;
47
+
48
+ export type Message<Schema extends JSONSchema> = MessageBase & {
49
+ [MESSAGE_OBJECT_KEY]: GraffitiObject<Schema>;
50
+ };
51
+ export type LabeledMessage<Schema extends JSONSchema> = LabeledMessageBase & {
52
+ [LABELED_MESSAGE_MESSAGE_KEY]: {
53
+ [MESSAGE_OBJECT_KEY]: GraffitiObject<Schema>;
54
+ };
55
+ };
56
+
57
+ export const SendResponseSchema = strictObject({ id: string() });
58
+
59
+ export const MessageResultSchema = strictObject({
60
+ results: array(LabeledMessageBaseSchema),
61
+ hasMore: boolean(),
62
+ cursor: string(),
63
+ });