@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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/1-services/4-inboxes.ts"],
4
- "sourcesContent": ["import type { JSONSchema, GraffitiObject } from \"@graffiti-garden/api\";\nimport {\n getAuthorizationEndpoint,\n fetchWithErrorHandling,\n verifyHTTPSEndpoint,\n} from \"./utilities\";\nimport {\n compileGraffitiObjectSchema,\n GraffitiErrorCursorExpired,\n GraffitiErrorNotFound,\n} from \"@graffiti-garden/api\";\nimport {\n encode as dagCborEncode,\n decode as dagCborDecode,\n} from \"@ipld/dag-cbor\";\nimport {\n type infer as infer_,\n string,\n url,\n array,\n optional,\n nullable,\n strictObject,\n looseObject,\n nonnegative,\n int,\n boolean,\n custom,\n number,\n union,\n} from \"zod/mini\";\n\nexport class Inboxes {\n getAuthorizationEndpoint = getAuthorizationEndpoint;\n protected cache_: Promise<Cache> | null = null;\n protected get cache() {\n if (!this.cache_) {\n this.cache_ = createCache();\n }\n return this.cache_;\n }\n\n async send(inboxUrl: string, message: Message<{}>): Promise<string> {\n verifyHTTPSEndpoint(inboxUrl);\n const url = `${inboxUrl}/send`;\n\n const response = await fetchWithErrorHandling(url, {\n method: \"PUT\",\n headers: {\n \"Content-Type\": \"application/cbor\",\n },\n body: new Uint8Array(dagCborEncode({ m: message })),\n });\n\n const blob = await response.blob();\n const cbor = dagCborDecode(await blob.arrayBuffer());\n const parsed = SendResponseSchema.parse(cbor);\n return parsed.id;\n }\n\n async get(\n inboxUrl: string,\n messageId: string,\n inboxToken?: string | null,\n ): Promise<LabeledMessageBase | null> {\n const messageCacheKey = getMessageCacheKey(inboxUrl, messageId);\n const cache = await this.cache;\n const cached = await cache.messages.get(messageCacheKey);\n if (cached !== undefined) return cached;\n\n const url = `${inboxUrl}/message/${messageId}`;\n let response: Response | null = null;\n try {\n response = await fetchWithErrorHandling(url, {\n method: \"GET\",\n headers: {\n ...(inboxToken\n ? {\n Authorization: `Bearer ${inboxToken}`,\n }\n : {}),\n },\n });\n } catch (e) {\n if (e instanceof GraffitiErrorNotFound) {\n await cache.messages.set(messageCacheKey, null);\n return null;\n }\n throw e;\n }\n\n const blob = await response.blob();\n const cbor = dagCborDecode(await blob.arrayBuffer());\n const parsed = LabeledMessageBaseSchema.parse(cbor);\n\n await cache.messages.set(messageCacheKey, parsed);\n return parsed;\n }\n\n async label(\n inboxUrl: string,\n messageId: string,\n label: number,\n inboxToken?: string | null,\n ): Promise<void> {\n verifyHTTPSEndpoint(inboxUrl);\n\n if (inboxToken) {\n const url = `${inboxUrl}/label/${messageId}`;\n\n await fetchWithErrorHandling(url, {\n method: \"PUT\",\n headers: {\n \"Content-Type\": \"application/cbor\",\n Authorization: `Bearer ${inboxToken}`,\n },\n body: new Uint8Array(dagCborEncode({ l: label })),\n });\n }\n\n // Update the cache, even if no token.\n // Therefore people not logged in do not need to\n // repeatedly re-validate objects.\n const cache = await this.cache;\n const messageCacheKey = getMessageCacheKey(inboxUrl, messageId);\n const result = await cache.messages.get(messageCacheKey);\n if (result) {\n await cache.messages.set(messageCacheKey, {\n ...result,\n l: label,\n });\n }\n }\n\n protected async fetchMessageBatch(\n inboxUrl: string,\n type: \"query\" | \"export\",\n body: Uint8Array<ArrayBuffer> | undefined,\n inboxToken?: string | null,\n cursor?: string,\n signal?: AbortSignal,\n ) {\n const response = await fetchWithErrorHandling(\n `${inboxUrl}/${type}${cursor ? `?cursor=${encodeURIComponent(cursor)}` : \"\"}`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/cbor\",\n ...(inboxToken\n ? {\n Authorization: `Bearer ${inboxToken}`,\n }\n : {}),\n },\n body,\n signal,\n },\n );\n const retryAfterHeader = response.headers.get(\"Retry-After\");\n const retryAfter = retryAfterHeader\n ? parseInt(retryAfterHeader)\n : undefined;\n\n const waitTil =\n retryAfter && Number.isFinite(retryAfter)\n ? Date.now() + retryAfter * 1000\n : undefined;\n\n return { response, waitTil };\n }\n\n protected async *yieldFromCache(\n cache: Cache,\n inboxUrl: string,\n messageIdsCacheKey: string,\n cachedMessageIds: CacheQueryValue,\n cacheNumSeen: number = 0,\n ): AsyncGenerator<LabeledMessageBase> {\n // Filter out all messageIds before\n // the number already seen\n const messageIds = cachedMessageIds.messageIds.slice(cacheNumSeen);\n\n // Get all the messages pointed to in the cache\n const messages = await Promise.all(\n messageIds.map(async (id) => {\n const message = await cache.messages.get(\n getMessageCacheKey(inboxUrl, id),\n );\n if (!message) {\n // Something is very wrong with the cache,\n // it refers to message IDs that are not cached\n try {\n await cache.messageIds.del(messageIdsCacheKey);\n } catch {}\n throw new Error(\"Cache out of sync - perhaps clear browser storage\");\n }\n return message;\n }),\n );\n\n yield* messages;\n }\n\n protected async *lockedMessageStreamer<Schema extends JSONSchema>(\n ...args: Parameters<typeof this.messageStreamer<Schema>>\n ): MessageStream<Schema> {\n if (typeof window === \"undefined\" || !(await canUseIDB())) {\n // TODO: implement locking in node as well, but not\n // high priority since most use will be in browser\n const streamer = this.messageStreamer<Schema>(...args);\n while (true) {\n const next = await streamer.next();\n if (next.done) return next.value;\n yield next.value;\n }\n }\n\n // Request the lock\n const messageIdsCacheKey = await args[0];\n const lockKey = `graffiti:inbox:${messageIdsCacheKey}`;\n let releaseLock = () => {};\n let hasLock: boolean = false;\n await new Promise<void>((resolvehasLock) => {\n window.navigator.locks.request(\n lockKey,\n {\n mode: \"exclusive\",\n ifAvailable: true,\n },\n async (lock) => {\n // Immediately return whether we\n // acquired the lock or not\n hasLock = !!lock;\n resolvehasLock();\n\n // Then wait for the release to be called\n await new Promise<void>((r) => (releaseLock = r));\n },\n );\n });\n if (hasLock) {\n // If we have the lock, simply proceed with the regular streamer\n try {\n const streamer = this.messageStreamer<Schema>(...args);\n while (true) {\n const next = await streamer.next();\n if (next.done) return next.value;\n yield next.value;\n }\n } finally {\n // Release the lock when all done\n releaseLock();\n }\n }\n\n // Someone else has the lock,\n // so wait until the lock is released,\n // then just return from the cache\n releaseLock();\n await window.navigator.locks.request(lockKey, () => {});\n\n // TODO: the arguments here are brittle\n // at some point, refactor things\n const inboxUrl = args[1];\n const objectSchema = args[5] ?? {};\n const cacheVersion = args[6];\n const cacheNumSeen = args[7];\n\n const cache = await this.cache;\n const cachedMessageIds = await cache.messageIds.get(messageIdsCacheKey);\n if (!cachedMessageIds) {\n throw new Error(\"Cache not found\");\n }\n if (\n cacheVersion !== undefined &&\n cacheVersion !== cachedMessageIds.version\n ) {\n throw new GraffitiErrorCursorExpired(\"Cursor is stale\");\n }\n\n const iterator = this.yieldFromCache(\n cache,\n inboxUrl,\n messageIdsCacheKey,\n cachedMessageIds,\n cacheNumSeen,\n );\n for await (const m of iterator) yield m as LabeledMessage<Schema>;\n\n const outputCursor: infer_<typeof CursorSchema> = {\n numSeen: cachedMessageIds.messageIds.length,\n version: cachedMessageIds.version,\n messageIdsCacheKey,\n objectSchema,\n };\n\n return JSON.stringify(outputCursor);\n }\n\n protected async *messageStreamer<Schema extends JSONSchema>(\n messageIdsCacheKey_: Promise<string>,\n inboxUrl: string,\n type: \"export\" | \"query\",\n body: Uint8Array<ArrayBuffer> | undefined,\n inboxToken?: string | null,\n objectSchema: Schema = {} as Schema,\n cacheVersion?: string,\n cacheNumSeen: number = 0,\n ): MessageStream<Schema> {\n const validator = await compileGraffitiObjectSchema(objectSchema);\n const messageIdsCacheKey = await messageIdsCacheKey_;\n const cache = await this.cache;\n\n let cachedMessageIds = await cache.messageIds.get(messageIdsCacheKey);\n if (\n cacheVersion !== undefined &&\n cacheVersion !== cachedMessageIds?.version\n ) {\n throw new GraffitiErrorCursorExpired(\"Cursor is stale\");\n }\n\n // If we are rate-limited, wait\n let waitTil = cachedMessageIds?.waitTil;\n await waitFor(waitTil);\n\n // See if the cursor is still active by\n // requesting an initial batch of messages\n const cachedCursor = cachedMessageIds?.cursor;\n let firstResponse: Response | undefined = undefined;\n try {\n const out = await this.fetchMessageBatch(\n inboxUrl,\n type,\n body,\n inboxToken,\n cachedCursor,\n );\n firstResponse = out.response;\n waitTil = out.waitTil;\n } catch (e) {\n if (!(e instanceof GraffitiErrorCursorExpired && cachedCursor)) {\n console.error(\n \"Unexpected error in stream, waiting 5 seconds before continuing...\",\n );\n await new Promise((resolve) => setTimeout(resolve, 5000));\n throw e;\n }\n\n // The cursor is stale\n await cache.messageIds.del(messageIdsCacheKey);\n if (cacheVersion === undefined) {\n // The query is not a continuation\n // so we can effectively ignore the error\n cachedMessageIds = undefined;\n } else {\n // Otherwise propogate it up so the\n // consumer can clear their message history\n throw e;\n }\n }\n\n if (firstResponse !== undefined && cachedMessageIds) {\n // Cursor is valid! Yield from the cache\n const iterator = this.yieldFromCache(\n cache,\n inboxUrl,\n messageIdsCacheKey,\n cachedMessageIds,\n cacheNumSeen,\n );\n for await (const m of iterator) yield m as LabeledMessage<Schema>;\n }\n\n if (firstResponse === undefined) {\n // The cursor was stale: try again\n const out = await this.fetchMessageBatch(\n inboxUrl,\n type,\n body,\n inboxToken,\n );\n firstResponse = out.response;\n waitTil = out.waitTil;\n }\n\n // Continue streaming results\n let response = firstResponse;\n let cursor: string;\n const version = cachedMessageIds?.version ?? crypto.randomUUID();\n let messageIds = cachedMessageIds?.messageIds ?? [];\n while (true) {\n const blob = await response.blob();\n const decoded = dagCborDecode(await blob.arrayBuffer());\n const {\n results,\n hasMore,\n cursor: nextCursor,\n } = MessageResultSchema.parse(decoded);\n cursor = nextCursor;\n\n const labeledMessages: LabeledMessage<Schema>[] = results.map(\n (result) => {\n const object =\n result[LABELED_MESSAGE_MESSAGE_KEY][MESSAGE_OBJECT_KEY];\n if (!validator(object)) {\n throw new Error(\"Server returned data that does not match schema\");\n }\n return {\n ...result,\n [LABELED_MESSAGE_MESSAGE_KEY]: {\n ...result[LABELED_MESSAGE_MESSAGE_KEY],\n [MESSAGE_OBJECT_KEY]: object,\n },\n };\n },\n );\n\n // First cache the messages with their labels\n await cache.messages.setMany(\n labeledMessages.map((m: LabeledMessageBase) => [\n getMessageCacheKey(inboxUrl, m[LABELED_MESSAGE_ID_KEY]),\n m,\n ]),\n );\n // Then store all the messageids\n messageIds = [\n ...messageIds,\n ...labeledMessages.map(\n (m: LabeledMessageBase) => m[LABELED_MESSAGE_ID_KEY],\n ),\n ];\n await cache.messageIds.set(messageIdsCacheKey, {\n cursor,\n version,\n messageIds,\n waitTil,\n });\n\n // Update how many we've seen\n cacheNumSeen += labeledMessages.length;\n\n // Fetch the next page while the caller processes the current page.\n const prefetchController = new AbortController();\n // Otherwise get another response (after waiting for rate-limit)\n const nextPage = hasMore\n ? waitFor(waitTil)\n .then(() => this.fetchMessageBatch(\n inboxUrl,\n type,\n undefined, // Body is never past the first time\n inboxToken,\n cursor,\n prefetchController.signal,\n ))\n // A consumer may stop before awaiting this request.\n .then((value) => ({ value }), (error) => ({ error }))\n : null;\n\n // Return the values\n let consumedPage = false;\n try {\n for (const m of labeledMessages) yield m;\n consumedPage = true;\n } finally {\n if (!consumedPage) prefetchController.abort();\n }\n\n if (!hasMore) break;\n\n const out = await nextPage!;\n if (\"error\" in out) throw out.error;\n response = out.value.response;\n waitTil = out.value.waitTil;\n }\n\n const outputCursor: infer_<typeof CursorSchema> = {\n numSeen: cacheNumSeen,\n version,\n messageIdsCacheKey,\n objectSchema,\n };\n\n return JSON.stringify(outputCursor);\n }\n\n query<Schema extends JSONSchema>(\n inboxUrl: string,\n tags: Uint8Array[],\n objectSchema: Schema,\n inboxToken?: string | null,\n ): MessageStream<Schema> {\n verifyHTTPSEndpoint(inboxUrl);\n\n const body = dagCborEncode({\n tags,\n schema: objectSchema,\n });\n\n const messageIdsCacheKey = getMessageIdsCacheKey(inboxUrl, \"query\", body);\n return this.lockedMessageStreamer<Schema>(\n messageIdsCacheKey,\n inboxUrl,\n \"query\",\n new Uint8Array(body),\n inboxToken,\n objectSchema,\n );\n }\n\n continueQuery(\n inboxUrl: string,\n cursor: string,\n inboxToken?: string | null,\n ): MessageStream<{}> {\n verifyHTTPSEndpoint(inboxUrl);\n\n const decodedCursor = JSON.parse(cursor);\n const { messageIdsCacheKey, numSeen, objectSchema, version } =\n CursorSchema.parse(decodedCursor);\n\n return this.lockedMessageStreamer<{}>(\n Promise.resolve(messageIdsCacheKey),\n inboxUrl,\n \"query\",\n undefined,\n inboxToken,\n objectSchema,\n version,\n numSeen,\n );\n }\n\n export(inboxUrl: string, inboxToken: string): MessageStream<{}> {\n verifyHTTPSEndpoint(inboxUrl);\n const messageIdsCacheKey = getMessageIdsCacheKey(inboxUrl, \"export\");\n return this.lockedMessageStreamer<{}>(\n messageIdsCacheKey,\n inboxUrl,\n \"export\",\n undefined,\n inboxToken,\n );\n }\n}\n\nconst GraffitiObjectSchema = strictObject({\n value: looseObject({}),\n channels: array(string()),\n allowed: optional(nullable(array(url()))),\n url: url(),\n actor: url(),\n});\nexport const Uint8ArraySchema = custom<Uint8Array>(\n (v): v is Uint8Array => v instanceof Uint8Array,\n);\nexport const TagsSchema = array(Uint8ArraySchema);\n\nexport const MESSAGE_TAGS_KEY = \"t\";\nexport const MESSAGE_OBJECT_KEY = \"o\";\nexport const MESSAGE_METADATA_KEY = \"m\";\nexport const MessageBaseSchema = strictObject({\n [MESSAGE_TAGS_KEY]: TagsSchema,\n [MESSAGE_OBJECT_KEY]: GraffitiObjectSchema,\n [MESSAGE_METADATA_KEY]: Uint8ArraySchema,\n});\ntype MessageBase = infer_<typeof MessageBaseSchema>;\n\nexport const LABELED_MESSAGE_ID_KEY = \"id\";\nexport const LABELED_MESSAGE_MESSAGE_KEY = \"m\";\nexport const LABELED_MESSAGE_LABEL_KEY = \"l\";\nexport const LabeledMessageBaseSchema = strictObject({\n [LABELED_MESSAGE_ID_KEY]: string(),\n [LABELED_MESSAGE_MESSAGE_KEY]: MessageBaseSchema,\n [LABELED_MESSAGE_LABEL_KEY]: number(),\n});\ntype LabeledMessageBase = infer_<typeof LabeledMessageBaseSchema>;\n\nexport type Message<Schema extends JSONSchema> = MessageBase & {\n [MESSAGE_OBJECT_KEY]: GraffitiObject<Schema>;\n};\nexport type LabeledMessage<Schema extends JSONSchema> = LabeledMessageBase & {\n [LABELED_MESSAGE_MESSAGE_KEY]: {\n [MESSAGE_OBJECT_KEY]: GraffitiObject<Schema>;\n };\n};\n\nconst SendResponseSchema = strictObject({ id: string() });\n\nconst MessageResultSchema = strictObject({\n results: array(LabeledMessageBaseSchema),\n hasMore: boolean(),\n cursor: string(),\n});\n\nconst CursorSchema = strictObject({\n messageIdsCacheKey: string(),\n version: string(),\n numSeen: int().check(nonnegative()),\n objectSchema: union([looseObject({}), boolean()]),\n});\n\nexport interface MessageStream<\n Schema extends JSONSchema,\n> extends AsyncGenerator<LabeledMessage<Schema>, string> {}\n\ntype CacheQueryValue = {\n cursor: string;\n version: string;\n messageIds: string[];\n waitTil?: number;\n};\n\nasync function canUseIDB(): Promise<boolean> {\n try {\n if (!globalThis.indexedDB) return false;\n\n // Small probe database\n await new Promise<void>((resolve, reject) => {\n const req = indexedDB.open(\"__idb_probe__\", 1);\n req.onupgradeneeded = () => req.result.createObjectStore(\"k\");\n req.onsuccess = () => {\n req.result.close();\n resolve();\n };\n req.onerror = () => reject(req.error);\n });\n\n return true;\n } catch {\n return false;\n }\n}\n\ntype Cache = {\n messages: {\n get(k: string): Promise<LabeledMessageBase | null | undefined>;\n set(k: string, value: LabeledMessageBase | null): Promise<void>;\n setMany(entries: [string, LabeledMessageBase][]): Promise<void>;\n del(k: string): Promise<void>;\n };\n messageIds: {\n get(k: string): Promise<CacheQueryValue | undefined>;\n set(k: string, value: CacheQueryValue): Promise<void>;\n del(k: string): Promise<void>;\n };\n};\n\nfunction getMessageCacheKey(inboxUrl: string, messageId: string) {\n return `${encodeURIComponent(inboxUrl)}:${encodeURIComponent(messageId)}`;\n}\nasync function getMessageIdsCacheKey(\n inboxUrl: string,\n type: \"query\" | \"export\",\n body?: Uint8Array,\n): Promise<string> {\n const cacheIdData = dagCborEncode({\n inboxUrl,\n type,\n body: body ?? null,\n });\n return crypto.subtle\n .digest(\"SHA-256\", new Uint8Array(cacheIdData))\n .then((bytes) =>\n Array.from(new Uint8Array(bytes))\n .map((byte) => byte.toString(16).padStart(2, \"0\"))\n .join(\"\"),\n );\n}\n\nasync function resetCacheDB() {\n await new Promise<void>((resolve) => {\n const req = indexedDB.deleteDatabase(\"graffiti-inbox-cache\");\n req.onsuccess = () => resolve();\n req.onerror = () => resolve(); // best effort\n req.onblocked = () => resolve(); // best effort\n });\n}\n\nasync function createCache(): Promise<Cache> {\n if (await canUseIDB()) {\n const { openDB } = await import(\"idb\");\n const db = await openDB(\"graffiti-inbox-cache\", 1, {\n upgrade(db) {\n if (!db.objectStoreNames.contains(\"m\")) db.createObjectStore(\"m\");\n if (!db.objectStoreNames.contains(\"q\")) db.createObjectStore(\"q\");\n },\n });\n\n return {\n messages: {\n get: async (k) => {\n try {\n return db.get(\"m\", k);\n } catch (error) {\n console.error(\"Error getting message from cache:\", error);\n console.error(\"resetting cache...\");\n await resetCacheDB();\n return undefined;\n }\n },\n set: async (k, v) => {\n await db.put(\"m\", v, k);\n },\n setMany: async (entries) => {\n if (entries.length === 0) return;\n const tx = db.transaction(\"m\", \"readwrite\");\n await Promise.all([\n ...entries.map(([k, v]) => tx.store.put(v, k)),\n tx.done,\n ]);\n },\n del: (k) => db.delete(\"m\", k),\n },\n messageIds: {\n get: async (k) => {\n try {\n return await db.get(\"q\", k);\n } catch (error) {\n console.error(\"Error getting message IDs from cache:\", error);\n console.error(\"resetting cache...\");\n await resetCacheDB();\n return undefined;\n }\n },\n set: async (k, v) => {\n await db.put(\"q\", v, k);\n },\n del: (k) => db.delete(\"q\", k),\n },\n };\n }\n\n const m = new Map<string, LabeledMessageBase | null>();\n const q = new Map<string, CacheQueryValue>();\n\n return {\n messages: {\n get: async (k) => m.get(k),\n set: async (k, v) => void m.set(k, v),\n setMany: async (entries) => {\n for (const [k, v] of entries) m.set(k, v);\n },\n del: async (k) => void m.delete(k),\n },\n messageIds: {\n get: async (k) => q.get(k),\n set: async (k, v) => void q.set(k, v),\n del: async (k) => void q.delete(k),\n },\n };\n}\n\nasync function waitFor(waitTil?: number) {\n if (waitTil !== undefined) {\n const waitFor = waitTil - Date.now();\n if (waitFor > 0) {\n await new Promise((resolve) => setTimeout(resolve, waitFor));\n }\n }\n}\n"],
5
- "mappings": "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",
6
- "names": ["url", "dagCborEncode", "dagCborDecode", "db", "waitFor"]
4
+ "sourcesContent": ["import type { JSONSchema } from \"@graffiti-garden/api\";\nimport {\n getAuthorizationEndpoint,\n fetchWithErrorHandling,\n verifyHTTPSEndpoint,\n} from \"./utilities\";\nimport {\n compileGraffitiObjectSchema,\n GraffitiErrorCursorExpired,\n GraffitiErrorNotFound,\n} from \"@graffiti-garden/api\";\nimport {\n encode as dagCborEncode,\n decode as dagCborDecode,\n} from \"@ipld/dag-cbor\";\nimport {\n type infer as infer_,\n string,\n strictObject,\n looseObject,\n nonnegative,\n int,\n boolean,\n union,\n} from \"zod/mini\";\nimport {\n LabeledMessageBaseSchema,\n SendResponseSchema,\n MessageResultSchema,\n LABELED_MESSAGE_MESSAGE_KEY,\n MESSAGE_OBJECT_KEY,\n} from \"./4-inboxes/messages\";\nimport type {\n LabeledMessageBase,\n LabeledMessage,\n Message,\n} from \"./4-inboxes/messages\";\nimport {\n canUseIDB,\n createCache,\n getMessageCacheKey,\n getMessageIdsCacheKey,\n replayQuery,\n storeQueryPage,\n} from \"./4-inboxes/cache\";\nimport type { Cache, CacheQueryValue } from \"./4-inboxes/cache\";\n\nexport {\n Uint8ArraySchema,\n TagsSchema,\n MESSAGE_TAGS_KEY,\n MESSAGE_OBJECT_KEY,\n MESSAGE_METADATA_KEY,\n MessageBaseSchema,\n LABELED_MESSAGE_ID_KEY,\n LABELED_MESSAGE_MESSAGE_KEY,\n LABELED_MESSAGE_LABEL_KEY,\n LabeledMessageBaseSchema,\n} from \"./4-inboxes/messages\";\nexport type { Message, LabeledMessage } from \"./4-inboxes/messages\";\n\n// One batch may be slow on a mobile connection. This bounds the network request\n// and response-body read, not IndexedDB or time spent yielding messages.\nconst INBOX_REQUEST_TIMEOUT_MS = 30_000;\n// A stalled tab should not make peers wait indefinitely. This only bounds\n// lock acquisition; the request above bounds work after acquisition.\nconst INBOX_LOCK_WAIT_TIMEOUT_MS = 30_000;\n\ntype StreamSource<Schema extends JSONSchema> =\n | {\n kind: \"query\";\n inboxUrl: string;\n body: Uint8Array<ArrayBuffer>;\n schema: Schema;\n inboxToken?: string | null;\n }\n | {\n kind: \"continue\";\n inboxUrl: string;\n cursor: infer_<typeof CursorSchema>;\n inboxToken?: string | null;\n }\n | {\n kind: \"export\";\n inboxUrl: string;\n inboxToken: string;\n };\n\nexport class Inboxes {\n getAuthorizationEndpoint = getAuthorizationEndpoint;\n protected cache_: Promise<Cache> | null = null;\n protected get cache() {\n if (!this.cache_) {\n this.cache_ = createCache();\n }\n return this.cache_;\n }\n\n async send(inboxUrl: string, message: Message<{}>): Promise<string> {\n verifyHTTPSEndpoint(inboxUrl);\n const url = `${inboxUrl}/send`;\n\n const response = await fetchWithErrorHandling(url, {\n method: \"PUT\",\n headers: {\n \"Content-Type\": \"application/cbor\",\n },\n body: new Uint8Array(dagCborEncode({ m: message })),\n });\n\n const blob = await response.blob();\n const cbor = dagCborDecode(await blob.arrayBuffer());\n const parsed = SendResponseSchema.parse(cbor);\n return parsed.id;\n }\n\n async get(\n inboxUrl: string,\n messageId: string,\n inboxToken?: string | null,\n ): Promise<LabeledMessageBase | null> {\n const messageCacheKey = getMessageCacheKey(inboxUrl, messageId);\n const cache = await this.cache;\n const cached = await cache.messages.get(messageCacheKey);\n if (cached !== undefined) return cached;\n\n const url = `${inboxUrl}/message/${messageId}`;\n let response: Response | null = null;\n try {\n response = await fetchWithErrorHandling(url, {\n method: \"GET\",\n headers: {\n ...(inboxToken\n ? {\n Authorization: `Bearer ${inboxToken}`,\n }\n : {}),\n },\n });\n } catch (e) {\n if (e instanceof GraffitiErrorNotFound) {\n await cache.messages.set(messageCacheKey, null);\n return null;\n }\n throw e;\n }\n\n const blob = await response.blob();\n const cbor = dagCborDecode(await blob.arrayBuffer());\n const parsed = LabeledMessageBaseSchema.parse(cbor);\n\n await cache.messages.set(messageCacheKey, parsed);\n return parsed;\n }\n\n async label(\n inboxUrl: string,\n messageId: string,\n label: number,\n inboxToken?: string | null,\n ): Promise<void> {\n verifyHTTPSEndpoint(inboxUrl);\n\n if (inboxToken) {\n const url = `${inboxUrl}/label/${messageId}`;\n\n await fetchWithErrorHandling(url, {\n method: \"PUT\",\n headers: {\n \"Content-Type\": \"application/cbor\",\n Authorization: `Bearer ${inboxToken}`,\n },\n body: new Uint8Array(dagCborEncode({ l: label })),\n });\n }\n\n // Update the cache, even if no token.\n // Therefore people not logged in do not need to\n // repeatedly re-validate objects.\n const cache = await this.cache;\n const messageCacheKey = getMessageCacheKey(inboxUrl, messageId);\n const result = await cache.messages.get(messageCacheKey);\n if (result) {\n await cache.messages.set(messageCacheKey, {\n ...result,\n l: label,\n });\n }\n }\n\n protected async fetchMessageBatch(\n inboxUrl: string,\n type: \"query\" | \"export\",\n body: Uint8Array<ArrayBuffer> | undefined,\n inboxToken?: string | null,\n cursor?: string,\n signal?: AbortSignal,\n ) {\n // Keep the deadline active while the response body is being read, too.\n const timeout = AbortSignal.timeout(INBOX_REQUEST_TIMEOUT_MS);\n const response = await fetchWithErrorHandling(\n `${inboxUrl}/${type}${cursor ? `?cursor=${encodeURIComponent(cursor)}` : \"\"}`,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/cbor\",\n ...(inboxToken\n ? {\n Authorization: `Bearer ${inboxToken}`,\n }\n : {}),\n },\n body,\n signal: signal ? AbortSignal.any([signal, timeout]) : timeout,\n },\n );\n const retryAfterHeader = response.headers.get(\"Retry-After\");\n const retryAfter = retryAfterHeader\n ? parseInt(retryAfterHeader)\n : undefined;\n\n const waitTil =\n retryAfter && Number.isFinite(retryAfter)\n ? Date.now() + retryAfter * 1000\n : undefined;\n\n return { response, waitTil };\n }\n\n protected async withQueryLock<T>(\n messageIdsCacheKey: string,\n useLock: boolean,\n work: () => Promise<T>,\n signal?: AbortSignal,\n ): Promise<T> {\n if (!useLock) return work();\n\n // The lock follows one page of network and cache work; yielding cannot\n // prolong it.\n const timeout = AbortSignal.timeout(INBOX_LOCK_WAIT_TIMEOUT_MS);\n let acquired = false;\n try {\n return await window.navigator.locks.request(\n `graffiti:inbox:${messageIdsCacheKey}`,\n {\n mode: \"exclusive\",\n signal: signal ? AbortSignal.any([signal, timeout]) : timeout,\n },\n async () => {\n acquired = true;\n return work();\n },\n );\n } catch (error) {\n if (!acquired && timeout.aborted) {\n throw new Error(\"Timed out waiting for another tab's inbox query\", {\n cause: error,\n });\n }\n throw error;\n }\n }\n\n protected async *messageStreamer<Schema extends JSONSchema>(\n source: StreamSource<Schema>,\n ): MessageStream<Schema> {\n const { inboxUrl, inboxToken } = source;\n const type = source.kind === \"export\" ? \"export\" : \"query\";\n // Continuations carry their validated schema in the cursor; export uses\n // the empty schema. Both callers instantiate this stream with Schema = {}.\n const objectSchema = (\n source.kind === \"query\"\n ? source.schema\n : source.kind === \"continue\" ? source.cursor.objectSchema : {}\n ) as Schema;\n const validator = await compileGraffitiObjectSchema(objectSchema);\n const body = source.kind === \"query\" ? source.body : undefined;\n const messageIdsCacheKey = source.kind === \"continue\"\n ? source.cursor.messageIdsCacheKey\n : await getMessageIdsCacheKey(inboxUrl, type, body);\n const cacheVersion = source.kind === \"continue\"\n ? source.cursor.version\n : undefined;\n // Opening IndexedDB can wait on another tab, so do it before holding a lock.\n const cache = await this.cache;\n // TODO: implement locking in node as well, but not\n // high priority since most use will be in browser\n const useLock = typeof window !== \"undefined\" && (await canUseIDB());\n const initialQuery = await cache.messageIds.get(messageIdsCacheKey);\n // Version identifies a query history; generation changes on each page\n // written to that history, including an empty page.\n let seenGeneration = initialQuery?.generation;\n let seenVersion = initialQuery?.version;\n let numSeen = source.kind === \"continue\" ? source.cursor.numSeen : 0;\n let firstPage = true;\n\n // Advance one page at most. Recheck the cache under the lock so a tab\n // waiting for another tab's fetch can reuse its result.\n const fetchPage = async (signal?: AbortSignal): Promise<CacheQueryValue> => {\n while (true) {\n signal?.throwIfAborted();\n const step = await this.withQueryLock(\n messageIdsCacheKey,\n useLock,\n async () => {\n signal?.throwIfAborted();\n // A different tab may have fetched a page while we waited for the lock.\n let query = await cache.messageIds.get(messageIdsCacheKey);\n if (cacheVersion !== undefined && cacheVersion !== query?.version) {\n throw new GraffitiErrorCursorExpired(\"Cursor is stale\");\n }\n if (query?.version !== seenVersion) {\n // A cache reset after this stream yielded messages would replay\n // them. Report the stale cursor as a midstream expiry instead.\n if (!firstPage) throw new GraffitiErrorCursorExpired(\"Cursor is stale\");\n numSeen = 0;\n }\n if (query?.generation && query.generation !== seenGeneration) {\n return { query };\n }\n\n // A cached Retry-After should not keep other tabs waiting on this lock.\n if (query?.waitTil !== undefined && query.waitTil > Date.now()) {\n return { waitTil: query.waitTil };\n }\n\n // See if the cursor is still active by requesting the next batch.\n let batch: { response: Response; waitTil?: number };\n try {\n batch = await this.fetchMessageBatch(\n inboxUrl,\n type,\n firstPage ? body : undefined, // Later pages send no body.\n inboxToken,\n query?.cursor,\n signal,\n );\n } catch (error) {\n if (\n !(error instanceof GraffitiErrorCursorExpired && query?.cursor)\n ) {\n // Preserve the old initial-page error backoff, but wait only\n // after releasing the Web Lock.\n if (firstPage && !signal?.aborted) {\n return { initialFetchError: error };\n }\n throw error;\n }\n // Only an initial cached cursor is retried. A cursor that\n // expires midstream must be reported to the caller.\n if (!firstPage) throw error;\n\n // The cursor is stale. A continuation must report that to its caller.\n await cache.messageIds.del(messageIdsCacheKey);\n if (cacheVersion !== undefined) throw error;\n query = undefined;\n numSeen = 0;\n batch = await this.fetchMessageBatch(\n inboxUrl,\n type,\n body,\n inboxToken,\n undefined,\n signal,\n );\n }\n\n const decoded = dagCborDecode(\n new Uint8Array(await batch.response.arrayBuffer()),\n );\n const { results, hasMore, cursor } =\n MessageResultSchema.parse(decoded);\n const labeledMessages: LabeledMessage<Schema>[] = results.map(\n (result) => {\n const object =\n result[LABELED_MESSAGE_MESSAGE_KEY][MESSAGE_OBJECT_KEY];\n if (!validator(object)) {\n throw new Error(\"Server returned data that does not match schema\");\n }\n return {\n ...result,\n [LABELED_MESSAGE_MESSAGE_KEY]: {\n ...result[LABELED_MESSAGE_MESSAGE_KEY],\n [MESSAGE_OBJECT_KEY]: object,\n },\n };\n },\n );\n\n query = await storeQueryPage(\n cache,\n inboxUrl,\n messageIdsCacheKey,\n query,\n {\n messages: labeledMessages,\n cursor,\n hasMore,\n waitTil: batch.waitTil,\n },\n );\n return { query };\n },\n signal,\n );\n\n if (\"initialFetchError\" in step) {\n console.error(\n \"Unexpected error in stream, waiting 5 seconds before continuing...\",\n );\n await new Promise((resolve) => setTimeout(resolve, 5000));\n throw step.initialFetchError;\n }\n if (\"waitTil\" in step) {\n await waitFor(step.waitTil);\n continue;\n }\n return step.query;\n }\n };\n\n type PageResult = { query: CacheQueryValue } | { error: unknown };\n let nextPage: Promise<PageResult> | undefined;\n while (true) {\n const result = nextPage\n ? await nextPage\n : await fetchPage().then(\n (query) => ({ query }),\n (error) => ({ error }),\n );\n nextPage = undefined;\n if (\"error\" in result) throw result.error;\n\n const query = result.query;\n // Replay all unseen messages after releasing the lock.\n const messages = await replayQuery(\n cache,\n inboxUrl,\n messageIdsCacheKey,\n query,\n numSeen,\n );\n numSeen = query.messageIds.length;\n seenGeneration = query.generation;\n seenVersion = query.version;\n firstPage = false;\n // The current page's lock is released. Fetch the next page while the\n // caller processes these messages; closing the stream aborts that fetch.\n const prefetchController = new AbortController();\n if (query.hasMore) {\n // The caller may close before awaiting this request; handle rejection\n // here so an aborted prefetch does not become unhandled.\n nextPage = fetchPage(prefetchController.signal).then(\n (query) => ({ query }),\n (error) => ({ error }),\n );\n }\n let consumedPage = false;\n try {\n for (const message of messages) {\n yield message as LabeledMessage<Schema>;\n }\n consumedPage = true;\n } finally {\n if (!consumedPage) prefetchController.abort();\n }\n\n if (!query.hasMore) {\n const outputCursor: infer_<typeof CursorSchema> = {\n numSeen,\n version: query.version,\n messageIdsCacheKey,\n objectSchema,\n };\n return JSON.stringify(outputCursor);\n }\n }\n }\n\n query<Schema extends JSONSchema>(\n inboxUrl: string,\n tags: Uint8Array[],\n objectSchema: Schema,\n inboxToken?: string | null,\n ): MessageStream<Schema> {\n verifyHTTPSEndpoint(inboxUrl);\n const body = new Uint8Array(dagCborEncode({\n tags,\n schema: objectSchema,\n }));\n return this.messageStreamer<Schema>({\n kind: \"query\",\n inboxUrl,\n body,\n schema: objectSchema,\n inboxToken,\n });\n }\n\n continueQuery(\n inboxUrl: string,\n cursor: string,\n inboxToken?: string | null,\n ): MessageStream<{}> {\n verifyHTTPSEndpoint(inboxUrl);\n\n const decodedCursor = JSON.parse(cursor);\n const parsedCursor = CursorSchema.parse(decodedCursor);\n return this.messageStreamer<{}>({\n kind: \"continue\",\n inboxUrl,\n cursor: parsedCursor,\n inboxToken,\n });\n }\n\n export(inboxUrl: string, inboxToken: string): MessageStream<{}> {\n verifyHTTPSEndpoint(inboxUrl);\n return this.messageStreamer<{}>({\n kind: \"export\",\n inboxUrl,\n inboxToken,\n });\n }\n}\n\nconst CursorSchema = strictObject({\n messageIdsCacheKey: string(),\n version: string(),\n numSeen: int().check(nonnegative()),\n objectSchema: union([looseObject({}), boolean()]),\n});\n\nexport interface MessageStream<\n Schema extends JSONSchema,\n> extends AsyncGenerator<LabeledMessage<Schema>, string> {}\n\nasync function waitFor(waitTil?: number) {\n if (waitTil !== undefined) {\n const waitFor = waitTil - Date.now();\n if (waitFor > 0) {\n await new Promise((resolve) => setTimeout(resolve, waitFor));\n }\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["import_messages", "dagCborEncode", "dagCborDecode", "query", "waitFor"]
7
7
  }
@@ -34,6 +34,14 @@ function handleTests(handle) {
34
34
  const resolvedHandle = await handles.actorToHandle(actor);
35
35
  (0, import_vitest.expect)(resolvedHandle).toBe(handle);
36
36
  });
37
+ (0, import_vitest.test)("actorToHandle uses the first did:web alias", async () => {
38
+ const actor = "did:plc:example";
39
+ const handleDid = "did:web:alice.example";
40
+ const dids = {
41
+ resolve: async (did) => did === actor ? { alsoKnownAs: ["at://alice.example", handleDid, "did:web:other.example"] } : { alsoKnownAs: [actor] }
42
+ };
43
+ (0, import_vitest.expect)(await new import_handles.Handles({ dids }).actorToHandle(actor)).toBe("alice.example");
44
+ });
37
45
  });
38
46
  }
39
47
  //# sourceMappingURL=2-handles-tests.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/3-protocol/2-handles-tests.ts"],
4
- "sourcesContent": ["import { describe, expect, test } from \"vitest\";\nimport { Handles } from \"./2-handles\";\nimport { DecentralizedIdentifiers } from \"../1-services/2-dids\";\n\nexport function handleTests(handle: string) {\n describe(\"Handles\", async () => {\n const handles = new Handles({\n dids: new DecentralizedIdentifiers(),\n });\n\n test(\"handleToActor and actorToHandle\", async () => {\n const actor = await handles.handleToActor(handle);\n const resolvedHandle = await handles.actorToHandle(actor);\n expect(resolvedHandle).toBe(handle);\n });\n });\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAuC;AACvC,qBAAwB;AACxB,kBAAyC;AAElC,SAAS,YAAY,QAAgB;AAC1C,8BAAS,WAAW,YAAY;AAC9B,UAAM,UAAU,IAAI,uBAAQ;AAAA,MAC1B,MAAM,IAAI,qCAAyB;AAAA,IACrC,CAAC;AAED,4BAAK,mCAAmC,YAAY;AAClD,YAAM,QAAQ,MAAM,QAAQ,cAAc,MAAM;AAChD,YAAM,iBAAiB,MAAM,QAAQ,cAAc,KAAK;AACxD,gCAAO,cAAc,EAAE,KAAK,MAAM;AAAA,IACpC,CAAC;AAAA,EACH,CAAC;AACH;",
4
+ "sourcesContent": ["import { describe, expect, test } from \"vitest\";\nimport { Handles } from \"./2-handles\";\nimport { DecentralizedIdentifiers } from \"../1-services/2-dids\";\n\nexport function handleTests(handle: string) {\n describe(\"Handles\", async () => {\n const handles = new Handles({\n dids: new DecentralizedIdentifiers(),\n });\n\n test(\"handleToActor and actorToHandle\", async () => {\n const actor = await handles.handleToActor(handle);\n const resolvedHandle = await handles.actorToHandle(actor);\n expect(resolvedHandle).toBe(handle);\n });\n\n test(\"actorToHandle uses the first did:web alias\", async () => {\n const actor = \"did:plc:example\";\n const handleDid = \"did:web:alice.example\";\n const dids = {\n resolve: async (did: string) => did === actor\n ? { alsoKnownAs: [\"at://alice.example\", handleDid, \"did:web:other.example\"] }\n : { alsoKnownAs: [actor] },\n } as DecentralizedIdentifiers;\n expect(await new Handles({ dids }).actorToHandle(actor)).toBe(\"alice.example\");\n });\n\n });\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAAuC;AACvC,qBAAwB;AACxB,kBAAyC;AAElC,SAAS,YAAY,QAAgB;AAC1C,8BAAS,WAAW,YAAY;AAC9B,UAAM,UAAU,IAAI,uBAAQ;AAAA,MAC1B,MAAM,IAAI,qCAAyB;AAAA,IACrC,CAAC;AAED,4BAAK,mCAAmC,YAAY;AAClD,YAAM,QAAQ,MAAM,QAAQ,cAAc,MAAM;AAChD,YAAM,iBAAiB,MAAM,QAAQ,cAAc,KAAK;AACxD,gCAAO,cAAc,EAAE,KAAK,MAAM;AAAA,IACpC,CAAC;AAED,4BAAK,8CAA8C,YAAY;AAC7D,YAAM,QAAQ;AACd,YAAM,YAAY;AAClB,YAAM,OAAO;AAAA,QACX,SAAS,OAAO,QAAgB,QAAQ,QACpC,EAAE,aAAa,CAAC,sBAAsB,WAAW,uBAAuB,EAAE,IAC1E,EAAE,aAAa,CAAC,KAAK,EAAE;AAAA,MAC7B;AACA,gCAAO,MAAM,IAAI,uBAAQ,EAAE,KAAK,CAAC,EAAE,cAAc,KAAK,CAAC,EAAE,KAAK,eAAe;AAAA,IAC/E,CAAC;AAAA,EAEH,CAAC;AACH;",
6
6
  "names": []
7
7
  }
@@ -31,15 +31,14 @@ class Handles {
31
31
  services;
32
32
  actorToHandle = async (actor) => {
33
33
  const actorDocument = await this.services.dids.resolve(actor);
34
- const handleDid = actorDocument.alsoKnownAs?.at(0);
34
+ const handleDid = actorDocument.alsoKnownAs?.find(
35
+ (alias) => alias.startsWith(HANDLE_DID_PREFIX)
36
+ );
35
37
  if (!handleDid) {
36
38
  throw new import_api.GraffitiErrorNotFound(
37
39
  `Handle for actor DID ${actor} not found`
38
40
  );
39
41
  }
40
- if (!handleDid.startsWith(HANDLE_DID_PREFIX)) {
41
- throw new Error(`Handle DID ${handleDid} is not a valid handle`);
42
- }
43
42
  const handle = handleDid.slice(HANDLE_DID_PREFIX.length);
44
43
  const handleDocument = await this.services.dids.resolve(handleDid);
45
44
  if (!handleDocument.alsoKnownAs || !handleDocument.alsoKnownAs.includes(actor)) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/3-protocol/2-handles.ts"],
4
- "sourcesContent": ["import type { Graffiti } from \"@graffiti-garden/api\";\nimport { GraffitiErrorNotFound } from \"@graffiti-garden/api\";\nimport { DecentralizedIdentifiers } from \"../1-services/2-dids\";\n\n// Handles used a fixed method\nconst HANDLE_DID_PREFIX = \"did:web:\";\n\nexport class Handles {\n constructor(\n protected readonly services: { dids: DecentralizedIdentifiers },\n ) {}\n\n actorToHandle: Graffiti[\"actorToHandle\"] = async (actor) => {\n const actorDocument = await this.services.dids.resolve(actor);\n\n const handleDid = actorDocument.alsoKnownAs?.at(0);\n if (!handleDid) {\n throw new GraffitiErrorNotFound(\n `Handle for actor DID ${actor} not found`,\n );\n }\n if (!handleDid.startsWith(HANDLE_DID_PREFIX)) {\n throw new Error(`Handle DID ${handleDid} is not a valid handle`);\n }\n\n const handle = handleDid.slice(HANDLE_DID_PREFIX.length);\n\n const handleDocument = await this.services.dids.resolve(handleDid);\n if (\n !handleDocument.alsoKnownAs ||\n !handleDocument.alsoKnownAs.includes(actor)\n ) {\n throw new Error(`Handle ${handle} does not match actor ${actor}`);\n }\n\n return handle;\n };\n\n handleToActor: Graffiti[\"handleToActor\"] = async (handle) => {\n const handleDid = `${HANDLE_DID_PREFIX}${handle}`;\n const handleDocument = await this.services.dids.resolve(handleDid);\n\n const actor = handleDocument.alsoKnownAs?.at(0);\n if (!actor) {\n throw new GraffitiErrorNotFound(\n `Actor for handle DID ${handleDid} not found`,\n );\n }\n\n const actorDocument = await this.services.dids.resolve(actor);\n if (\n !actorDocument.alsoKnownAs ||\n !actorDocument.alsoKnownAs.includes(handleDid)\n ) {\n throw new Error(`Actor ${actor} does not match handle ${handle}`);\n }\n\n return actor;\n };\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,iBAAsC;AACtC,kBAAyC;AAGzC,MAAM,oBAAoB;AAEnB,MAAM,QAAQ;AAAA,EACnB,YACqB,UACnB;AADmB;AAAA,EAClB;AAAA,EADkB;AAAA,EAGrB,gBAA2C,OAAO,UAAU;AAC1D,UAAM,gBAAgB,MAAM,KAAK,SAAS,KAAK,QAAQ,KAAK;AAE5D,UAAM,YAAY,cAAc,aAAa,GAAG,CAAC;AACjD,QAAI,CAAC,WAAW;AACd,YAAM,IAAI;AAAA,QACR,wBAAwB,KAAK;AAAA,MAC/B;AAAA,IACF;AACA,QAAI,CAAC,UAAU,WAAW,iBAAiB,GAAG;AAC5C,YAAM,IAAI,MAAM,cAAc,SAAS,wBAAwB;AAAA,IACjE;AAEA,UAAM,SAAS,UAAU,MAAM,kBAAkB,MAAM;AAEvD,UAAM,iBAAiB,MAAM,KAAK,SAAS,KAAK,QAAQ,SAAS;AACjE,QACE,CAAC,eAAe,eAChB,CAAC,eAAe,YAAY,SAAS,KAAK,GAC1C;AACA,YAAM,IAAI,MAAM,UAAU,MAAM,yBAAyB,KAAK,EAAE;AAAA,IAClE;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,gBAA2C,OAAO,WAAW;AAC3D,UAAM,YAAY,GAAG,iBAAiB,GAAG,MAAM;AAC/C,UAAM,iBAAiB,MAAM,KAAK,SAAS,KAAK,QAAQ,SAAS;AAEjE,UAAM,QAAQ,eAAe,aAAa,GAAG,CAAC;AAC9C,QAAI,CAAC,OAAO;AACV,YAAM,IAAI;AAAA,QACR,wBAAwB,SAAS;AAAA,MACnC;AAAA,IACF;AAEA,UAAM,gBAAgB,MAAM,KAAK,SAAS,KAAK,QAAQ,KAAK;AAC5D,QACE,CAAC,cAAc,eACf,CAAC,cAAc,YAAY,SAAS,SAAS,GAC7C;AACA,YAAM,IAAI,MAAM,SAAS,KAAK,0BAA0B,MAAM,EAAE;AAAA,IAClE;AAEA,WAAO;AAAA,EACT;AACF;",
4
+ "sourcesContent": ["import type { Graffiti } from \"@graffiti-garden/api\";\nimport { GraffitiErrorNotFound } from \"@graffiti-garden/api\";\nimport { DecentralizedIdentifiers } from \"../1-services/2-dids\";\n\n// Handles used a fixed method\nconst HANDLE_DID_PREFIX = \"did:web:\";\n\nexport class Handles {\n constructor(\n protected readonly services: { dids: DecentralizedIdentifiers },\n ) {}\n\n actorToHandle: Graffiti[\"actorToHandle\"] = async (actor) => {\n const actorDocument = await this.services.dids.resolve(actor);\n\n const handleDid = actorDocument.alsoKnownAs?.find((alias) =>\n alias.startsWith(HANDLE_DID_PREFIX),\n );\n if (!handleDid) {\n throw new GraffitiErrorNotFound(\n `Handle for actor DID ${actor} not found`,\n );\n }\n const handle = handleDid.slice(HANDLE_DID_PREFIX.length);\n\n const handleDocument = await this.services.dids.resolve(handleDid);\n if (\n !handleDocument.alsoKnownAs ||\n !handleDocument.alsoKnownAs.includes(actor)\n ) {\n throw new Error(`Handle ${handle} does not match actor ${actor}`);\n }\n\n return handle;\n };\n\n handleToActor: Graffiti[\"handleToActor\"] = async (handle) => {\n const handleDid = `${HANDLE_DID_PREFIX}${handle}`;\n const handleDocument = await this.services.dids.resolve(handleDid);\n\n const actor = handleDocument.alsoKnownAs?.at(0);\n if (!actor) {\n throw new GraffitiErrorNotFound(\n `Actor for handle DID ${handleDid} not found`,\n );\n }\n\n const actorDocument = await this.services.dids.resolve(actor);\n if (\n !actorDocument.alsoKnownAs ||\n !actorDocument.alsoKnownAs.includes(handleDid)\n ) {\n throw new Error(`Actor ${actor} does not match handle ${handle}`);\n }\n\n return actor;\n };\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,iBAAsC;AACtC,kBAAyC;AAGzC,MAAM,oBAAoB;AAEnB,MAAM,QAAQ;AAAA,EACnB,YACqB,UACnB;AADmB;AAAA,EAClB;AAAA,EADkB;AAAA,EAGrB,gBAA2C,OAAO,UAAU;AAC1D,UAAM,gBAAgB,MAAM,KAAK,SAAS,KAAK,QAAQ,KAAK;AAE5D,UAAM,YAAY,cAAc,aAAa;AAAA,MAAK,CAAC,UACjD,MAAM,WAAW,iBAAiB;AAAA,IACpC;AACA,QAAI,CAAC,WAAW;AACd,YAAM,IAAI;AAAA,QACR,wBAAwB,KAAK;AAAA,MAC/B;AAAA,IACF;AACA,UAAM,SAAS,UAAU,MAAM,kBAAkB,MAAM;AAEvD,UAAM,iBAAiB,MAAM,KAAK,SAAS,KAAK,QAAQ,SAAS;AACjE,QACE,CAAC,eAAe,eAChB,CAAC,eAAe,YAAY,SAAS,KAAK,GAC1C;AACA,YAAM,IAAI,MAAM,UAAU,MAAM,yBAAyB,KAAK,EAAE;AAAA,IAClE;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,gBAA2C,OAAO,WAAW;AAC3D,UAAM,YAAY,GAAG,iBAAiB,GAAG,MAAM;AAC/C,UAAM,iBAAiB,MAAM,KAAK,SAAS,KAAK,QAAQ,SAAS;AAEjE,UAAM,QAAQ,eAAe,aAAa,GAAG,CAAC;AAC9C,QAAI,CAAC,OAAO;AACV,YAAM,IAAI;AAAA,QACR,wBAAwB,SAAS;AAAA,MACnC;AAAA,IACF;AAEA,UAAM,gBAAgB,MAAM,KAAK,SAAS,KAAK,QAAQ,KAAK;AAC5D,QACE,CAAC,cAAc,eACf,CAAC,cAAc,YAAY,SAAS,SAAS,GAC7C;AACA,YAAM,IAAI,MAAM,SAAS,KAAK,0BAA0B,MAAM,EAAE;AAAA,IAClE;AAEA,WAAO;AAAA,EACT;AACF;",
6
6
  "names": []
7
7
  }
@@ -157,7 +157,7 @@ class GraffitiDecentralized {
157
157
  console.error("Failed to load handle from actor:", error);
158
158
  }
159
159
  if (handle) {
160
- await this.rememberHandleInPasswordManager(handle, actor);
160
+ this.rememberHandleInBrowserStorage(handle);
161
161
  }
162
162
  });
163
163
  for (const method of [
@@ -209,7 +209,7 @@ class GraffitiDecentralized {
209
209
  const input = template?.querySelector(
210
210
  "#username"
211
211
  );
212
- const rememberedHandle = proposedHandle || await this.getRememberedHandleFromPasswordManager();
212
+ const rememberedHandle = proposedHandle || this.getRememberedHandleFromBrowserStorage();
213
213
  if (input) input.value = rememberedHandle ?? "";
214
214
  input?.addEventListener("focus", () => input?.select());
215
215
  setTimeout(() => input?.focus(), 0);
@@ -247,6 +247,10 @@ class GraffitiDecentralized {
247
247
  this.login_(handle);
248
248
  }
249
249
  });
250
+ template?.querySelector("#graffiti-login-forgot")?.addEventListener("click", (e) => {
251
+ e.preventDefault();
252
+ void this.loginWithoutHandle();
253
+ });
250
254
  } else {
251
255
  template = await this.modal?.displayTemplate("graffiti-login-welcome");
252
256
  template?.querySelector("#graffiti-login-existing")?.addEventListener("click", (e) => {
@@ -297,48 +301,31 @@ class GraffitiDecentralized {
297
301
  await this.sessions.login(actor);
298
302
  }
299
303
  }
300
- async rememberHandleInPasswordManager(handle, actor) {
301
- if (typeof window === "undefined") return;
302
- const credentials = window.navigator.credentials;
303
- const PasswordCredentialConstructor = window.PasswordCredential;
304
- if (!credentials?.store || !PasswordCredentialConstructor) {
305
- this.rememberHandleInBrowserStorage(handle);
306
- return;
307
- }
308
- try {
309
- const credential = new PasswordCredentialConstructor({
310
- id: handle,
311
- // Graffiti logins use external authorization; this stable placeholder
312
- // allows browsers/password managers to persist the handle.
313
- password: "no-password",
314
- name: handle
315
- });
316
- await credentials.store(credential);
317
- } catch (error) {
318
- console.error("Failed to remember handle in password manager:", error);
319
- this.rememberHandleInBrowserStorage(handle);
320
- }
321
- }
322
- async getRememberedHandleFromPasswordManager() {
323
- if (typeof window === "undefined") return void 0;
324
- const credentials = window.navigator.credentials;
325
- const PasswordCredentialConstructor = window.PasswordCredential;
326
- if (!credentials?.get || !PasswordCredentialConstructor) {
327
- return this.getRememberedHandleFromBrowserStorage();
328
- }
329
- try {
330
- const credential = await credentials.get({
331
- password: true,
332
- mediation: "optional"
333
- });
334
- if (credential && "id" in credential && typeof credential.id === "string") {
335
- return credential.id;
304
+ async loginWithoutHandle() {
305
+ const template = await this.modal?.displayTemplate("graffiti-login-provider");
306
+ const defaultProvider = new URL("/", this.identityCreatorEndpoint);
307
+ const provider = template?.querySelector("#graffiti-provider");
308
+ if (provider) provider.value = defaultProvider.host;
309
+ provider?.addEventListener("focus", () => provider.select());
310
+ setTimeout(() => provider?.focus(), 0);
311
+ template?.querySelector("#graffiti-login-provider-form")?.addEventListener("submit", (event) => {
312
+ event.preventDefault();
313
+ try {
314
+ const domain = provider?.value.trim() ?? "";
315
+ const origin = domain === defaultProvider.host ? defaultProvider : new URL(`https://${domain}`);
316
+ if (!origin.hostname || origin.username || origin.password || origin.pathname !== "/" || origin.search || origin.hash) throw new Error("Invalid provider domain");
317
+ const url2 = new URL("/forgot-handle", origin);
318
+ url2.searchParams.set("redirect_uri", window.location.toString());
319
+ window.location.assign(url2.toString());
320
+ } catch {
321
+ alert("Enter a valid provider domain.");
336
322
  }
337
- } catch (error) {
338
- console.error("Failed to load handle from password manager:", error);
339
- return this.getRememberedHandleFromBrowserStorage();
340
- }
341
- return void 0;
323
+ });
324
+ template?.querySelector("#graffiti-login-provider-back")?.addEventListener("click", (event) => {
325
+ event.preventDefault();
326
+ void this.login_("");
327
+ });
328
+ await this.modal?.open();
342
329
  }
343
330
  rememberHandleInBrowserStorage(handle) {
344
331
  if (typeof window === "undefined") return;
@@ -898,6 +885,13 @@ class GraffitiDecentralized {
898
885
  await iterator.return("");
899
886
  }
900
887
  }
888
+ // Labeling is advisory and should not hold up discovery, but its failure
889
+ // must be handled even if the caller has already stopped reading the stream.
890
+ labelInboxMessage(inboxEndpoint, messageId, label, inboxToken) {
891
+ void this.inboxes.label(inboxEndpoint, messageId, label, inboxToken).catch(
892
+ (error) => console.error("Could not label inbox message:", error)
893
+ );
894
+ }
901
895
  async processOneLabeledMessage(inboxEndpoint, result, inboxToken, recipient) {
902
896
  const label = result.l;
903
897
  if (label !== MESSAGE_LABEL_VALID && label !== MESSAGE_LABEL_UNLABELED && label !== MESSAGE_LABEL_TRASH)
@@ -909,7 +903,7 @@ class GraffitiDecentralized {
909
903
  const metadataRaw = (0, import_dag_cbor.decode)(metadataBytes);
910
904
  metadata = MessageMetadataSchema.parse(metadataRaw);
911
905
  } catch (e) {
912
- this.inboxes.label(
906
+ this.labelInboxMessage(
913
907
  inboxEndpoint,
914
908
  messageId,
915
909
  MESSAGE_LABEL_INVALID,
@@ -942,7 +936,7 @@ class GraffitiDecentralized {
942
936
  if (!past || past[import_inboxes.LABELED_MESSAGE_MESSAGE_KEY][import_inboxes.MESSAGE_OBJECT_KEY].url !== object.url)
943
937
  return;
944
938
  if (past[import_inboxes.LABELED_MESSAGE_LABEL_KEY] !== MESSAGE_LABEL_TRASH) {
945
- this.inboxes.label(
939
+ this.labelInboxMessage(
946
940
  inboxEndpoint,
947
941
  tombstonedMessageId,
948
942
  MESSAGE_LABEL_TRASH,
@@ -1003,26 +997,28 @@ class GraffitiDecentralized {
1003
997
  }
1004
998
  if (tombstonedMessageId) {
1005
999
  if (validationError instanceof import_api.GraffitiErrorNotFound) {
1006
- this.inboxes.get(inboxEndpoint, tombstonedMessageId, inboxToken).then((result2) => {
1000
+ void this.inboxes.get(inboxEndpoint, tombstonedMessageId, inboxToken).then((result2) => {
1007
1001
  if (
1008
1002
  // Make sure that it actually references the object being deleted
1009
1003
  result2 && result2[import_inboxes.LABELED_MESSAGE_MESSAGE_KEY][import_inboxes.MESSAGE_OBJECT_KEY].url === object.url && // And that the object is not already marked as trash
1010
1004
  result2[import_inboxes.LABELED_MESSAGE_LABEL_KEY] !== MESSAGE_LABEL_TRASH
1011
1005
  ) {
1012
- this.inboxes.label(
1006
+ this.labelInboxMessage(
1013
1007
  inboxEndpoint,
1014
1008
  tombstonedMessageId,
1015
1009
  MESSAGE_LABEL_TRASH,
1016
1010
  inboxToken
1017
1011
  );
1018
1012
  }
1019
- this.inboxes.label(
1013
+ this.labelInboxMessage(
1020
1014
  inboxEndpoint,
1021
1015
  messageId,
1022
1016
  MESSAGE_LABEL_TRASH,
1023
1017
  inboxToken
1024
1018
  );
1025
- });
1019
+ }).catch(
1020
+ (error) => console.error("Could not inspect tombstone message:", error)
1021
+ );
1026
1022
  return {
1027
1023
  messageId,
1028
1024
  tombstone: true,
@@ -1035,7 +1031,7 @@ class GraffitiDecentralized {
1035
1031
  } else {
1036
1032
  console.error("Recieved an incorrect tombstone object");
1037
1033
  console.error(validationError);
1038
- this.inboxes.label(
1034
+ this.labelInboxMessage(
1039
1035
  inboxEndpoint,
1040
1036
  messageId,
1041
1037
  MESSAGE_LABEL_INVALID,
@@ -1044,7 +1040,7 @@ class GraffitiDecentralized {
1044
1040
  }
1045
1041
  } else {
1046
1042
  if (validationError === void 0) {
1047
- this.inboxes.label(
1043
+ this.labelInboxMessage(
1048
1044
  inboxEndpoint,
1049
1045
  messageId,
1050
1046
  MESSAGE_LABEL_VALID,
@@ -1059,7 +1055,7 @@ class GraffitiDecentralized {
1059
1055
  announcements
1060
1056
  };
1061
1057
  } else if (validationError instanceof import_api.GraffitiErrorNotFound) {
1062
- this.inboxes.label(
1058
+ this.labelInboxMessage(
1063
1059
  inboxEndpoint,
1064
1060
  messageId,
1065
1061
  MESSAGE_LABEL_TRASH,
@@ -1068,7 +1064,7 @@ class GraffitiDecentralized {
1068
1064
  } else {
1069
1065
  console.error("Recieved an incorrect object");
1070
1066
  console.error(validationError);
1071
- this.inboxes.label(
1067
+ this.labelInboxMessage(
1072
1068
  inboxEndpoint,
1073
1069
  messageId,
1074
1070
  MESSAGE_LABEL_INVALID,