Suite of AT Protocol TypeScript libraries built on web standards
20
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: circular package dep, common web introduced

+1012 -1033
+201
common-web/async.ts
··· 1 + import { bailableWait } from "./util.ts"; 2 + 3 + export const readFromGenerator = async <T>( 4 + gen: AsyncGenerator<T>, 5 + isDone: (last?: T) => Promise<boolean> | boolean, 6 + waitFor: Promise<unknown> = Promise.resolve(), 7 + maxLength = Number.MAX_SAFE_INTEGER, 8 + ): Promise<T[]> => { 9 + const evts: T[] = []; 10 + let bail: undefined | (() => void); 11 + let hasBroke = false; 12 + const awaitDone = async () => { 13 + if (await isDone(evts.at(-1))) { 14 + return true; 15 + } 16 + const bailable = bailableWait(20); 17 + await bailable.wait(); 18 + bail = bailable.bail; 19 + if (hasBroke) return false; 20 + return await awaitDone(); 21 + }; 22 + const breakOn: Promise<void> = new Promise((resolve) => { 23 + waitFor.then(() => { 24 + awaitDone().then(() => resolve()); 25 + }); 26 + }); 27 + 28 + try { 29 + while (evts.length < maxLength) { 30 + const maybeEvt = await Promise.race([gen.next(), breakOn]); 31 + if (!maybeEvt) break; 32 + const evt = maybeEvt as IteratorResult<T>; 33 + if (evt.done) break; 34 + evts.push(evt.value); 35 + } 36 + } finally { 37 + hasBroke = true; 38 + bail && bail(); 39 + } 40 + return evts; 41 + }; 42 + 43 + export type Deferrable = { 44 + resolve: () => void; 45 + complete: Promise<void>; 46 + }; 47 + 48 + export const createDeferrable = (): Deferrable => { 49 + let resolve!: () => void; 50 + const promise: Promise<void> = new Promise((res) => { 51 + resolve = () => res(); 52 + }); 53 + return { resolve, complete: promise }; 54 + }; 55 + 56 + export const createDeferrables = (count: number): Deferrable[] => { 57 + const list: Deferrable[] = []; 58 + for (let i = 0; i < count; i++) { 59 + list.push(createDeferrable()); 60 + } 61 + return list; 62 + }; 63 + 64 + export const allComplete = async (deferrables: Deferrable[]): Promise<void> => { 65 + await Promise.all(deferrables.map((d) => d.complete)); 66 + }; 67 + 68 + export class AsyncBuffer<T> { 69 + private buffer: T[] = []; 70 + private promise: Promise<void>; 71 + private resolve: () => void; 72 + private closed = false; 73 + private toThrow: unknown | undefined; 74 + 75 + constructor(public maxSize?: number) { 76 + this.promise = Promise.resolve(); 77 + this.resolve = () => null; 78 + this.resetPromise(); 79 + } 80 + 81 + get curr(): T[] { 82 + return this.buffer; 83 + } 84 + 85 + get size(): number { 86 + return this.buffer.length; 87 + } 88 + 89 + get isClosed(): boolean { 90 + return this.closed; 91 + } 92 + 93 + resetPromise() { 94 + this.promise = new Promise<void>((r) => (this.resolve = r)); 95 + } 96 + 97 + push(item: T) { 98 + this.buffer.push(item); 99 + this.resolve(); 100 + } 101 + 102 + pushMany(items: T[]) { 103 + items.forEach((i) => this.buffer.push(i)); 104 + this.resolve(); 105 + } 106 + 107 + async *events(): AsyncGenerator<T> { 108 + while (true) { 109 + if (this.closed && this.buffer.length === 0) { 110 + if (this.toThrow) { 111 + throw this.toThrow; 112 + } else { 113 + return; 114 + } 115 + } 116 + await this.promise; 117 + if (this.toThrow) { 118 + throw this.toThrow; 119 + } 120 + if (this.maxSize && this.size > this.maxSize) { 121 + throw new AsyncBufferFullError(this.maxSize); 122 + } 123 + const [first, ...rest] = this.buffer; 124 + if (first) { 125 + this.buffer = rest; 126 + yield first; 127 + } else { 128 + this.resetPromise(); 129 + } 130 + } 131 + } 132 + 133 + throw(err: unknown) { 134 + this.toThrow = err; 135 + this.closed = true; 136 + this.resolve(); 137 + } 138 + 139 + close() { 140 + this.closed = true; 141 + this.resolve(); 142 + } 143 + } 144 + 145 + export class AsyncBufferFullError extends Error { 146 + constructor(maxSize: number) { 147 + super(`ReachedMaxBufferSize: ${maxSize}`); 148 + } 149 + } 150 + 151 + export function allFulfilled<T extends readonly unknown[] | []>( 152 + promises: T, 153 + ): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>; 154 + export function allFulfilled<T>( 155 + promises: Iterable<T | PromiseLike<T>>, 156 + ): Promise<Awaited<T>[]>; 157 + export function allFulfilled( 158 + promises: Iterable<Promise<unknown>>, 159 + ): Promise<unknown[]> { 160 + return Promise.allSettled(promises).then(handleAllSettledErrors); 161 + } 162 + 163 + export function handleAllSettledErrors< 164 + T extends readonly PromiseSettledResult<unknown>[] | [], 165 + >( 166 + results: T, 167 + ): { 168 + -readonly [P in keyof T]: T[P] extends PromiseSettledResult<infer U> ? U 169 + : never; 170 + }; 171 + export function handleAllSettledErrors<T>( 172 + results: PromiseSettledResult<T>[], 173 + ): T[]; 174 + export function handleAllSettledErrors( 175 + results: PromiseSettledResult<unknown>[], 176 + ): unknown[] { 177 + if (results.every(isFulfilledResult)) return results.map(extractValue); 178 + 179 + const errors = results.filter(isRejectedResult).map(extractReason); 180 + throw errors; 181 + } 182 + 183 + export function isRejectedResult( 184 + result: PromiseSettledResult<unknown>, 185 + ): result is PromiseRejectedResult { 186 + return result.status === "rejected"; 187 + } 188 + 189 + function extractReason(result: PromiseRejectedResult): unknown { 190 + return result.reason; 191 + } 192 + 193 + export function isFulfilledResult<T>( 194 + result: PromiseSettledResult<T>, 195 + ): result is PromiseFulfilledResult<T> { 196 + return result.status === "fulfilled"; 197 + } 198 + 199 + function extractValue<T>(result: PromiseFulfilledResult<T>): T { 200 + return result.value; 201 + }
+26
common-web/check.ts
··· 1 + export interface Checkable<T> { 2 + parse: (obj: unknown) => T; 3 + safeParse: ( 4 + obj: unknown, 5 + ) => { success: true; data: T } | { success: false; error: Error }; 6 + } 7 + 8 + export interface Def<T> { 9 + name: string; 10 + schema: Checkable<T>; 11 + } 12 + 13 + export const is = <T>(obj: unknown, def: Checkable<T>): obj is T => { 14 + return def.safeParse(obj).success; 15 + }; 16 + 17 + export const create = <T>(def: Checkable<T>) => (v: unknown): v is T => 18 + def.safeParse(v).success; 19 + 20 + export const assure = <T>(def: Checkable<T>, obj: unknown): T => { 21 + return def.parse(obj); 22 + }; 23 + 24 + export const isObject = (obj: unknown): obj is Record<string, unknown> => { 25 + return typeof obj === "object" && obj !== null; 26 + };
+118
common-web/dates.ts
··· 1 + export const SECOND = 1000; 2 + export const MINUTE = SECOND * 60; 3 + export const HOUR = MINUTE * 60; 4 + export const DAY = HOUR * 24; 5 + 6 + const SEPARATORS_TO_ESCAPE = new Set([ 7 + "\\", 8 + "^", 9 + "$", 10 + ".", 11 + "|", 12 + "?", 13 + "*", 14 + "+", 15 + "(", 16 + ")", 17 + "[", 18 + "]", 19 + "{", 20 + "}", 21 + ]); 22 + 23 + function getStringSeparator(dateString: string): string { 24 + const separator = /\D/.exec(dateString); 25 + return separator ? separator[0] : ""; 26 + } 27 + 28 + function getTimeStringSeparator(timeString: string): string { 29 + const matches = timeString.match(/([^Z+\-\d])(?=\d+\1)/); 30 + return Array.isArray(matches) ? matches[0] : ""; 31 + } 32 + 33 + export function isValidDate(date: string, s = "-"): boolean { 34 + if (SEPARATORS_TO_ESCAPE.has(s)) { 35 + s = `\\${s}`; 36 + } 37 + 38 + const validator = new RegExp( 39 + `^(?!0{4}${s}0{2}${s}0{2})((?=[0-9]{4}${s}(((0[^2])|1[0-2])|02(?=${s}(([0-1][0-9])|2[0-8])))${s}[0-9]{2})|(?=((([13579][26])|([2468][048])|(0[48]))0{2})|([0-9]{2}((((0|[2468])[48])|[2468][048])|([13579][26])))${s}02${s}29))([0-9]{4})${s}(?!((0[469])|11)${s}31)((0[1,3-9]|1[0-2])|(02(?!${s}3)))${s}(0[1-9]|[1-2][0-9]|3[0-1])$`, 40 + ); 41 + return validator.test(date); 42 + } 43 + 44 + function isValidTime( 45 + timeWithOffset: string, 46 + s = ":", 47 + isTimezoneCheckOn = false, 48 + ): boolean { 49 + const validator = new RegExp( 50 + `^([0-1]|2(?=([0-3])|4${s}00))[0-9]${s}[0-5][0-9](${s}([0-5]|6(?=0))[0-9])?(\.[0-9]{1,9})?$`, 51 + ); 52 + 53 + if (!isTimezoneCheckOn || !/[Z+\-]/.test(timeWithOffset)) { 54 + return validator.test(timeWithOffset); 55 + } 56 + 57 + if (/Z$/.test(timeWithOffset)) { 58 + return validator.test(timeWithOffset.replace("Z", "")); 59 + } 60 + 61 + const isPositiveTimezoneOffset = timeWithOffset.includes("+"); 62 + const [time, offset] = timeWithOffset.split(/[+-]/); 63 + 64 + return validator.test(time) && 65 + isValidZoneOffset( 66 + offset, 67 + isPositiveTimezoneOffset, 68 + getStringSeparator(offset), 69 + ); 70 + } 71 + 72 + function isValidZoneOffset( 73 + offset: string, 74 + isPositiveOffset: boolean, 75 + s = ":", 76 + ): boolean { 77 + const validator = new RegExp( 78 + isPositiveOffset 79 + ? `^(0(?!(2${s}4)|0${s}3)|1(?=([0-1]|2(?=${s}[04])|[34](?=${s}0))))([03469](?=${s}[03])|[17](?=${s}0)|2(?=${s}[04])|5(?=${s}[034])|8(?=${s}[04]))${s}([03](?=0)|4(?=5))[05]$` 80 + : `^(0(?=[^0])|1(?=[0-2]))([39](?=${s}[03])|[0-24-8](?=${s}00))${s}[03]0$`, 81 + ); 82 + return validator.test(offset); 83 + } 84 + 85 + export function isValidISODateString(dateString: string): boolean { 86 + const [date, timeWithOffset] = dateString.split("T"); 87 + const dateSeparator = getStringSeparator(date); 88 + const isDateValid = isValidDate(date, dateSeparator); 89 + 90 + if (!timeWithOffset) { 91 + return false; 92 + } 93 + 94 + const timeStringSeparator = getTimeStringSeparator(timeWithOffset); 95 + return isDateValid && isValidTime(timeWithOffset, timeStringSeparator, true); 96 + } 97 + 98 + export const lessThanAgoMs = (time: Date, range: number): boolean => { 99 + return Date.now() < time.getTime() + range; 100 + }; 101 + 102 + export const addHoursToDate = (hours: number, startingDate?: Date): Date => { 103 + const currentDate = startingDate ? new Date(startingDate) : new Date(); 104 + currentDate.setHours(currentDate.getHours() + hours); 105 + return currentDate; 106 + }; 107 + 108 + export function toSimplifiedISOSafe(dateStr: string): string { 109 + const date = new Date(dateStr); 110 + if (isNaN(date.getTime())) { 111 + return new Date(0).toISOString(); 112 + } 113 + const iso = date.toISOString(); 114 + if (!isValidISODateString(iso)) { 115 + return new Date(0).toISOString(); 116 + } 117 + return iso; 118 + }
+19
common-web/deno.json
··· 1 + { 2 + "name": "@atp/common-web", 3 + "version": "0.1.0-alpha.1", 4 + "exports": { 5 + ".": "./mod.ts", 6 + "./async": "./async.ts", 7 + "./check": "./check.ts", 8 + "./dates": "./dates.ts", 9 + "./did-doc": "./did-doc.ts", 10 + "./retry": "./retry.ts", 11 + "./strings": "./strings.ts", 12 + "./tid": "./tid.ts", 13 + "./util": "./util.ts" 14 + }, 15 + "license": "MIT", 16 + "imports": { 17 + "zod": "jsr:@zod/zod@^4.1.13" 18 + } 19 + }
+193
common-web/did-doc.ts
··· 1 + import { z } from "zod"; 2 + 3 + export const isValidDidDoc = (doc: unknown): doc is DidDocument => { 4 + return didDocument.safeParse(doc).success; 5 + }; 6 + 7 + export const getDid = (doc: DidDocument): string => { 8 + const id = doc.id; 9 + if (typeof id !== "string") { 10 + throw new Error("No `id` on document"); 11 + } 12 + return id; 13 + }; 14 + 15 + export const getHandle = (doc: DidDocument): string | undefined => { 16 + const aka = doc.alsoKnownAs; 17 + if (aka) { 18 + for (let i = 0; i < aka.length; i++) { 19 + const alias = aka[i]; 20 + if (alias.startsWith("at://")) { 21 + return alias.slice(5); 22 + } 23 + } 24 + } 25 + return undefined; 26 + }; 27 + 28 + export const getSigningKey = ( 29 + doc: DidDocument, 30 + ): { type: string; publicKeyMultibase: string } | undefined => { 31 + return getVerificationMaterial(doc, "atproto"); 32 + }; 33 + 34 + export const getVerificationMaterial = ( 35 + doc: DidDocument, 36 + keyId: string, 37 + ): { type: string; publicKeyMultibase: string } | undefined => { 38 + const key = findItemById(doc, "verificationMethod", `#${keyId}`); 39 + if (!key) { 40 + return undefined; 41 + } 42 + 43 + if (!key.publicKeyMultibase) { 44 + return undefined; 45 + } 46 + 47 + return { 48 + type: key.type, 49 + publicKeyMultibase: key.publicKeyMultibase, 50 + }; 51 + }; 52 + 53 + export const getSigningDidKey = (doc: DidDocument): string | undefined => { 54 + const parsed = getSigningKey(doc); 55 + if (!parsed) return; 56 + return `did:key:${parsed.publicKeyMultibase}`; 57 + }; 58 + 59 + export const getPdsEndpoint = (doc: DidDocument): string | undefined => { 60 + return getServiceEndpoint(doc, { 61 + id: "#atproto_pds", 62 + type: "AtprotoPersonalDataServer", 63 + }); 64 + }; 65 + 66 + export const getFeedGenEndpoint = (doc: DidDocument): string | undefined => { 67 + return getServiceEndpoint(doc, { 68 + id: "#bsky_fg", 69 + type: "BskyFeedGenerator", 70 + }); 71 + }; 72 + 73 + export const getNotifEndpoint = (doc: DidDocument): string | undefined => { 74 + return getServiceEndpoint(doc, { 75 + id: "#bsky_notif", 76 + type: "BskyNotificationService", 77 + }); 78 + }; 79 + 80 + export const getServiceEndpoint = ( 81 + doc: DidDocument, 82 + opts: { id: string; type?: string }, 83 + ): string | undefined => { 84 + const service = findItemById(doc, "service", opts.id); 85 + if (!service) { 86 + return undefined; 87 + } 88 + 89 + if (opts.type && service.type !== opts.type) { 90 + return undefined; 91 + } 92 + 93 + if (typeof service.serviceEndpoint !== "string") { 94 + return undefined; 95 + } 96 + 97 + return validateUrl(service.serviceEndpoint); 98 + }; 99 + 100 + function findItemById< 101 + D extends DidDocument, 102 + T extends "verificationMethod" | "service", 103 + >(doc: D, type: T, id: string): NonNullable<D[T]>[number] | undefined; 104 + function findItemById( 105 + doc: DidDocument, 106 + type: "verificationMethod" | "service", 107 + id: string, 108 + ) { 109 + const items = doc[type]; 110 + if (items) { 111 + for (let i = 0; i < items.length; i++) { 112 + const item = items[i]; 113 + const itemId = item.id; 114 + 115 + if ( 116 + itemId[0] === "#" 117 + ? itemId === id 118 + : itemId.length === doc.id.length + id.length && 119 + itemId[doc.id.length] === "#" && 120 + itemId.endsWith(id) && 121 + itemId.startsWith(doc.id) 122 + ) { 123 + return item; 124 + } 125 + } 126 + } 127 + return undefined; 128 + } 129 + 130 + const validateUrl = (urlStr: string): string | undefined => { 131 + if (!urlStr.startsWith("http://") && !urlStr.startsWith("https://")) { 132 + return undefined; 133 + } 134 + 135 + if (!canParseUrl(urlStr)) { 136 + return undefined; 137 + } 138 + 139 + return urlStr; 140 + }; 141 + 142 + const canParseUrl = URL.canParse ?? 143 + ((urlStr: string): boolean => { 144 + try { 145 + new URL(urlStr); 146 + return true; 147 + } catch { 148 + return false; 149 + } 150 + }); 151 + 152 + const verificationMethod: VerificationMethod = z.object({ 153 + id: z.string(), 154 + type: z.string(), 155 + controller: z.string(), 156 + publicKeyMultibase: z.string().optional(), 157 + }); 158 + 159 + type VerificationMethod = z.ZodObject<{ 160 + id: z.ZodString; 161 + type: z.ZodString; 162 + controller: z.ZodString; 163 + publicKeyMultibase: z.ZodOptional<z.ZodString>; 164 + }, z.core.$strip>; 165 + 166 + const service: Service = z.object({ 167 + id: z.string(), 168 + type: z.string(), 169 + serviceEndpoint: z.union([z.string(), z.record(z.string(), z.unknown())]), 170 + }); 171 + 172 + type Service = z.ZodObject<{ 173 + id: z.ZodString; 174 + type: z.ZodString; 175 + serviceEndpoint: z.ZodUnion< 176 + readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>] 177 + >; 178 + }, z.core.$strip>; 179 + 180 + export const didDocument: DidDocumentType = z.object({ 181 + id: z.string(), 182 + alsoKnownAs: z.array(z.string()).optional(), 183 + verificationMethod: z.array(verificationMethod).optional(), 184 + service: z.array(service).optional(), 185 + }); 186 + type DidDocumentType = z.ZodObject<{ 187 + id: z.ZodString; 188 + alsoKnownAs: z.ZodOptional<z.ZodArray<z.ZodString>>; 189 + verificationMethod: z.ZodOptional<z.ZodArray<VerificationMethod>>; 190 + service: z.ZodOptional<z.ZodArray<Service>>; 191 + }, z.core.$strip>; 192 + 193 + export type DidDocument = z.infer<DidDocumentType>;
+17
common-web/mod.ts
··· 1 + /** 2 + * # AT Protocol Common Utilities (Web) 3 + * 4 + * Shared web-safe TypeScript utilities for other @atp/* packages. 5 + * 6 + * @module 7 + */ 8 + export * as check from "./check.ts"; 9 + export * as util from "./util.ts"; 10 + 11 + export * from "./async.ts"; 12 + export * from "./dates.ts"; 13 + export * from "./did-doc.ts"; 14 + export * from "./retry.ts"; 15 + export * from "./strings.ts"; 16 + export * from "./tid.ts"; 17 + export * from "./util.ts";
+59
common-web/retry.ts
··· 1 + import { wait } from "./util.ts"; 2 + 3 + export type RetryOptions = { 4 + maxRetries?: number; 5 + getWaitMs?: (n: number) => number | null; 6 + }; 7 + 8 + export async function retry<T>( 9 + fn: () => Promise<T> | T, 10 + opts: RetryOptions & { 11 + retryable?: (err: unknown) => boolean; 12 + } = {}, 13 + ): Promise<T> { 14 + const { maxRetries = 3, retryable = () => true, getWaitMs = backoffMs } = 15 + opts; 16 + let retries = 0; 17 + let doneError: unknown; 18 + while (!doneError) { 19 + try { 20 + return await fn(); 21 + } catch (err) { 22 + const waitMs = getWaitMs(retries); 23 + const willRetry = retries < maxRetries && waitMs !== null && 24 + retryable(err); 25 + if (willRetry) { 26 + retries += 1; 27 + if (waitMs !== 0) { 28 + await wait(waitMs); 29 + } 30 + } else { 31 + doneError = err; 32 + } 33 + } 34 + } 35 + throw doneError; 36 + } 37 + 38 + export function createRetryable(retryable: (err: unknown) => boolean): { 39 + <T>(fn: () => Promise<T>, opts?: RetryOptions): Promise<T>; 40 + } { 41 + return <T>(fn: () => Promise<T>, opts?: RetryOptions) => 42 + retry(fn, { ...opts, retryable }); 43 + } 44 + 45 + export function backoffMs(n: number, multiplier = 100, max = 1000): number { 46 + const exponentialMs = Math.pow(2, n) * multiplier; 47 + const ms = Math.min(exponentialMs, max); 48 + return jitter(ms); 49 + } 50 + 51 + function jitter(value: number) { 52 + const delta = value * 0.15; 53 + return value + randomRange(-delta, delta); 54 + } 55 + 56 + function randomRange(from: number, to: number) { 57 + const rand = Math.random() * (to - from); 58 + return rand + from; 59 + }
+75
common-web/strings.ts
··· 1 + export const utf8Len = (str: string): number => { 2 + return new TextEncoder().encode(str).byteLength; 3 + }; 4 + 5 + export const graphemeLen = (str: string): number => { 6 + if (typeof Intl !== "undefined" && "Segmenter" in Intl) { 7 + const segmenter = new Intl.Segmenter(undefined, { 8 + granularity: "grapheme", 9 + }); 10 + return Array.from(segmenter.segment(str)).length; 11 + } 12 + 13 + return Array.from(str).length; 14 + }; 15 + 16 + export const utf8ToB64Url = (utf8: string): string => { 17 + const encoder = new TextEncoder(); 18 + const bytes = encoder.encode(utf8); 19 + return btoa(String.fromCharCode(...bytes)) 20 + .replace(/\+/g, "-") 21 + .replace(/\//g, "_") 22 + .replace(/=/g, ""); 23 + }; 24 + 25 + export const b64UrlToUtf8 = (b64: string): string => { 26 + const base64 = b64.replace(/-/g, "+").replace(/_/g, "/"); 27 + const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4); 28 + 29 + const binaryString = atob(padded); 30 + const bytes = new Uint8Array(binaryString.length); 31 + for (let i = 0; i < binaryString.length; i++) { 32 + bytes[i] = binaryString.charCodeAt(i); 33 + } 34 + 35 + const decoder = new TextDecoder(); 36 + return decoder.decode(bytes); 37 + }; 38 + 39 + export const parseLanguage = (langTag: string): LanguageTag | null => { 40 + const parsed = langTag.match(bcp47Regexp); 41 + if (!parsed?.groups) return null; 42 + const parts = parsed.groups; 43 + const result: LanguageTag = {}; 44 + 45 + if (parts.grandfathered) result.grandfathered = parts.grandfathered; 46 + if (parts.language) result.language = parts.language; 47 + if (parts.extlang) result.extlang = parts.extlang; 48 + if (parts.script) result.script = parts.script; 49 + if (parts.region) result.region = parts.region; 50 + if (parts.variant) result.variant = parts.variant; 51 + if (parts.extension) result.extension = parts.extension; 52 + if (parts.privateUseA || parts.privateUseB) { 53 + result.privateUse = parts.privateUseA || parts.privateUseB; 54 + } 55 + 56 + return result; 57 + }; 58 + 59 + export const validateLanguage = (langTag: string): boolean => { 60 + return bcp47Regexp.test(langTag); 61 + }; 62 + 63 + export type LanguageTag = { 64 + grandfathered?: string; 65 + language?: string; 66 + extlang?: string; 67 + script?: string; 68 + region?: string; 69 + variant?: string; 70 + extension?: string; 71 + privateUse?: string; 72 + }; 73 + 74 + const bcp47Regexp = 75 + /^((?<grandfathered>(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((?<language>([A-Za-z]{2,3}(-(?<extlang>[A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-(?<script>[A-Za-z]{4}))?(-(?<region>[A-Za-z]{2}|[0-9]{3}))?(-(?<variant>[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-(?<extension>[0-9A-WY-Za-wy-z](-[A-Za-z0-9]{2,8})+))*(-(?<privateUseA>x(-[A-Za-z0-9]{1,8})+))?)|(?<privateUseB>x(-[A-Za-z0-9]{1,8})+))$/;
+105
common-web/tid.ts
··· 1 + import { s32decode, s32encode } from "./util.ts"; 2 + 3 + const TID_LEN = 13; 4 + 5 + let lastTimestamp = 0; 6 + let timestampCount = 0; 7 + let clockid: number | null = null; 8 + 9 + function dedash(str: string): string { 10 + return str.replaceAll("-", ""); 11 + } 12 + 13 + export class TID { 14 + str: string; 15 + 16 + constructor(str: string) { 17 + const noDashes = dedash(str); 18 + if (noDashes.length !== TID_LEN) { 19 + throw new Error(`Poorly formatted TID: ${noDashes.length} length`); 20 + } 21 + this.str = noDashes; 22 + } 23 + 24 + static next(prev?: TID): TID { 25 + const time = Math.max(Date.now(), lastTimestamp); 26 + if (time === lastTimestamp) { 27 + timestampCount++; 28 + } 29 + lastTimestamp = time; 30 + const timestamp = time * 1000 + timestampCount; 31 + if (clockid === null) { 32 + clockid = Math.floor(Math.random() * 32); 33 + } 34 + const tid = TID.fromTime(timestamp, clockid); 35 + if (!prev || tid.newerThan(prev)) { 36 + return tid; 37 + } 38 + return TID.fromTime(prev.timestamp() + 1, clockid); 39 + } 40 + 41 + static nextStr(prev?: string): string { 42 + return TID.next(prev ? new TID(prev) : undefined).toString(); 43 + } 44 + 45 + static fromTime(timestamp: number, clockid: number): TID { 46 + const str = `${s32encode(timestamp)}${s32encode(clockid).padStart(2, "2")}`; 47 + return new TID(str); 48 + } 49 + 50 + static fromStr(str: string): TID { 51 + return new TID(str); 52 + } 53 + 54 + static oldestFirst(a: TID, b: TID): number { 55 + return a.compareTo(b); 56 + } 57 + 58 + static newestFirst(a: TID, b: TID): number { 59 + return b.compareTo(a); 60 + } 61 + 62 + static is(str: string): boolean { 63 + return dedash(str).length === TID_LEN; 64 + } 65 + 66 + timestamp(): number { 67 + return s32decode(this.str.slice(0, 11)); 68 + } 69 + 70 + clockid(): number { 71 + return s32decode(this.str.slice(11, 13)); 72 + } 73 + 74 + formatted(): string { 75 + const str = this.toString(); 76 + return `${str.slice(0, 4)}-${str.slice(4, 7)}-${ 77 + str.slice( 78 + 7, 79 + 11, 80 + ) 81 + }-${str.slice(11, 13)}`; 82 + } 83 + 84 + toString(): string { 85 + return this.str; 86 + } 87 + 88 + compareTo(other: TID): number { 89 + if (this.str > other.str) return 1; 90 + if (this.str < other.str) return -1; 91 + return 0; 92 + } 93 + 94 + equals(other: TID): boolean { 95 + return this.str === other.str; 96 + } 97 + 98 + newerThan(other: TID): boolean { 99 + return this.compareTo(other) > 0; 100 + } 101 + 102 + olderThan(other: TID): boolean { 103 + return this.compareTo(other) < 0; 104 + } 105 + }
+177
common-web/util.ts
··· 1 + export const noUndefinedVals = <T>( 2 + obj: Record<string, T | undefined>, 3 + ): Record<string, T> => { 4 + Object.keys(obj).forEach((k) => { 5 + if (obj[k] === undefined) { 6 + delete obj[k]; 7 + } 8 + }); 9 + return obj as Record<string, T>; 10 + }; 11 + 12 + export function omit< 13 + T extends undefined | null | Record<string, unknown>, 14 + K extends keyof NonNullable<T>, 15 + >( 16 + object: T, 17 + rejectedKeys: readonly K[], 18 + ): T extends undefined ? undefined : T extends null ? null : Omit<T, K>; 19 + export function omit( 20 + src: undefined | null | Record<string, unknown>, 21 + rejectedKeys: readonly string[], 22 + ): undefined | null | Record<string, unknown> { 23 + if (!src) return src; 24 + 25 + const dst: Record<string, unknown> = {}; 26 + const srcKeys = Object.keys(src); 27 + for (let i = 0; i < srcKeys.length; i++) { 28 + const key = srcKeys[i]; 29 + if (!rejectedKeys.includes(key)) { 30 + dst[key] = src[key]; 31 + } 32 + } 33 + return dst; 34 + } 35 + 36 + export const jitter = (maxMs: number): number => { 37 + return Math.round((Math.random() - 0.5) * maxMs * 2); 38 + }; 39 + 40 + export const wait = (ms: number): Promise<void> => { 41 + return new Promise((res) => setTimeout(res, ms)); 42 + }; 43 + 44 + export type BailableWait = { 45 + bail: () => void; 46 + wait: () => Promise<void>; 47 + }; 48 + 49 + export const bailableWait = (ms: number): BailableWait => { 50 + let bail!: () => void; 51 + const waitPromise = new Promise<void>((res) => { 52 + const timeout = setTimeout(res, ms); 53 + bail = () => { 54 + clearTimeout(timeout); 55 + res(); 56 + }; 57 + }); 58 + return { bail, wait: () => waitPromise }; 59 + }; 60 + 61 + export const flattenUint8Arrays = (arrs: Uint8Array[]): Uint8Array => { 62 + const length = arrs.reduce((acc, cur) => { 63 + return acc + cur.length; 64 + }, 0); 65 + const flattened = new Uint8Array(length); 66 + let offset = 0; 67 + arrs.forEach((arr) => { 68 + flattened.set(arr, offset); 69 + offset += arr.length; 70 + }); 71 + return flattened; 72 + }; 73 + 74 + export const streamToUI8Array = async ( 75 + stream: AsyncIterable<Uint8Array>, 76 + ): Promise<Uint8Array> => { 77 + const arrays: Uint8Array[] = []; 78 + for await (const chunk of stream) { 79 + arrays.push(chunk); 80 + } 81 + return flattenUint8Arrays(arrays); 82 + }; 83 + 84 + const S32_CHAR = "234567abcdefghijklmnopqrstuvwxyz"; 85 + 86 + export const s32encode = (i: number): string => { 87 + let s = ""; 88 + while (i) { 89 + const c = i % 32; 90 + i = Math.floor(i / 32); 91 + s = S32_CHAR.charAt(c) + s; 92 + } 93 + return s; 94 + }; 95 + 96 + export const s32decode = (s: string): number => { 97 + let i = 0; 98 + for (const c of s) { 99 + i = i * 32 + S32_CHAR.indexOf(c); 100 + } 101 + return i; 102 + }; 103 + 104 + export const asyncFilter = async <T>( 105 + arr: T[], 106 + fn: (t: T) => Promise<boolean>, 107 + ): Promise<T[]> => { 108 + const results = await Promise.all(arr.map((t) => fn(t))); 109 + return arr.filter((_, i) => results[i]); 110 + }; 111 + 112 + export const errHasMsg = (err: unknown, msg: string): boolean => { 113 + return !!err && typeof err === "object" && "message" in err && 114 + (err as { message: unknown }).message === msg; 115 + }; 116 + 117 + export const chunkArray = <T>(arr: T[], chunkSize: number): T[][] => { 118 + return arr.reduce((acc, cur, i) => { 119 + const chunkI = Math.floor(i / chunkSize); 120 + if (!acc[chunkI]) { 121 + acc[chunkI] = []; 122 + } 123 + acc[chunkI].push(cur); 124 + return acc; 125 + }, [] as T[][]); 126 + }; 127 + 128 + export const range = (num: number): number[] => { 129 + const nums: number[] = []; 130 + for (let i = 0; i < num; i++) { 131 + nums.push(i); 132 + } 133 + return nums; 134 + }; 135 + 136 + export const dedupeStrs = (strs: string[]): string[] => { 137 + return [...new Set(strs)]; 138 + }; 139 + 140 + export const parseIntWithFallback = <T>( 141 + value: string | undefined, 142 + fallback: T, 143 + ): number | T => { 144 + const parsed = parseInt(value || "", 10); 145 + return isNaN(parsed) ? fallback : parsed; 146 + }; 147 + 148 + export function ui8ToArrayBuffer(bytes: Uint8Array): ArrayBuffer { 149 + return bytes.buffer.slice( 150 + bytes.byteOffset, 151 + bytes.byteLength + bytes.byteOffset, 152 + ) as ArrayBuffer; 153 + } 154 + 155 + export function keyBy<T, K extends keyof T>( 156 + arr: readonly T[], 157 + key: K, 158 + ): Map<T[K], T> { 159 + return arr.reduce((acc, cur) => { 160 + acc.set(cur[key], cur); 161 + return acc; 162 + }, new Map<T[K], T>()); 163 + } 164 + 165 + export const mapDefined = <T, S>( 166 + arr: T[], 167 + fn: (obj: T) => S | undefined, 168 + ): S[] => { 169 + const output: S[] = []; 170 + for (const item of arr) { 171 + const val = fn(item); 172 + if (val !== undefined) { 173 + output.push(val); 174 + } 175 + } 176 + return output; 177 + };
+1 -210
common/async.ts
··· 1 - import { bailableWait } from "./util.ts"; 2 - 3 - // reads values from a generator into a list 4 - // breaks when isDone signals `true` AND `waitFor` completes OR when a max length is reached 5 - // NOTE: does not signal generator to close. it *will* continue to produce values 6 - export const readFromGenerator = async <T>( 7 - gen: AsyncGenerator<T>, 8 - isDone: (last?: T) => Promise<boolean> | boolean, 9 - waitFor: Promise<unknown> = Promise.resolve(), 10 - maxLength = Number.MAX_SAFE_INTEGER, 11 - ): Promise<T[]> => { 12 - const evts: T[] = []; 13 - let bail: undefined | (() => void); 14 - let hasBroke = false; 15 - const awaitDone = async () => { 16 - if (await isDone(evts.at(-1))) { 17 - return true; 18 - } 19 - const bailable = bailableWait(20); 20 - await bailable.wait(); 21 - bail = bailable.bail; 22 - if (hasBroke) return false; 23 - return await awaitDone(); 24 - }; 25 - const breakOn: Promise<void> = new Promise((resolve) => { 26 - waitFor.then(() => { 27 - awaitDone().then(() => resolve()); 28 - }); 29 - }); 30 - 31 - try { 32 - while (evts.length < maxLength) { 33 - const maybeEvt = await Promise.race([gen.next(), breakOn]); 34 - if (!maybeEvt) break; 35 - const evt = maybeEvt as IteratorResult<T>; 36 - if (evt.done) break; 37 - evts.push(evt.value); 38 - } 39 - } finally { 40 - hasBroke = true; 41 - bail && bail(); 42 - } 43 - return evts; 44 - }; 45 - 46 - export type Deferrable = { 47 - resolve: () => void; 48 - complete: Promise<void>; 49 - }; 50 - 51 - export const createDeferrable = (): Deferrable => { 52 - let resolve!: () => void; 53 - const promise: Promise<void> = new Promise((res) => { 54 - resolve = () => res(); 55 - }); 56 - return { resolve, complete: promise }; 57 - }; 58 - 59 - export const createDeferrables = (count: number): Deferrable[] => { 60 - const list: Deferrable[] = []; 61 - for (let i = 0; i < count; i++) { 62 - list.push(createDeferrable()); 63 - } 64 - return list; 65 - }; 66 - 67 - export const allComplete = async (deferrables: Deferrable[]): Promise<void> => { 68 - await Promise.all(deferrables.map((d) => d.complete)); 69 - }; 70 - 71 - export class AsyncBuffer<T> { 72 - private buffer: T[] = []; 73 - private promise: Promise<void>; 74 - private resolve: () => void; 75 - private closed = false; 76 - private toThrow: unknown | undefined; 77 - 78 - constructor(public maxSize?: number) { 79 - // Initializing to satisfy types/build, immediately reset by resetPromise() 80 - this.promise = Promise.resolve(); 81 - this.resolve = () => null; 82 - this.resetPromise(); 83 - } 84 - 85 - get curr(): T[] { 86 - return this.buffer; 87 - } 88 - 89 - get size(): number { 90 - return this.buffer.length; 91 - } 92 - 93 - get isClosed(): boolean { 94 - return this.closed; 95 - } 96 - 97 - resetPromise() { 98 - this.promise = new Promise<void>((r) => (this.resolve = r)); 99 - } 100 - 101 - push(item: T) { 102 - this.buffer.push(item); 103 - this.resolve(); 104 - } 105 - 106 - pushMany(items: T[]) { 107 - items.forEach((i) => this.buffer.push(i)); 108 - this.resolve(); 109 - } 110 - 111 - async *events(): AsyncGenerator<T> { 112 - while (true) { 113 - if (this.closed && this.buffer.length === 0) { 114 - if (this.toThrow) { 115 - throw this.toThrow; 116 - } else { 117 - return; 118 - } 119 - } 120 - await this.promise; 121 - if (this.toThrow) { 122 - throw this.toThrow; 123 - } 124 - if (this.maxSize && this.size > this.maxSize) { 125 - throw new AsyncBufferFullError(this.maxSize); 126 - } 127 - const [first, ...rest] = this.buffer; 128 - if (first) { 129 - this.buffer = rest; 130 - yield first; 131 - } else { 132 - this.resetPromise(); 133 - } 134 - } 135 - } 136 - 137 - throw(err: unknown) { 138 - this.toThrow = err; 139 - this.closed = true; 140 - this.resolve(); 141 - } 142 - 143 - close() { 144 - this.closed = true; 145 - this.resolve(); 146 - } 147 - } 148 - 149 - export class AsyncBufferFullError extends Error { 150 - constructor(maxSize: number) { 151 - super(`ReachedMaxBufferSize: ${maxSize}`); 152 - } 153 - } 154 - 155 - /** 156 - * Utility function that behaves like {@link Promise.allSettled} but returns the 157 - * same result as {@link Promise.all} in case every promise is fulfilled, and 158 - * throws an {@link AggregateError} if there are more than one errors. 159 - */ 160 - export function allFulfilled<T extends readonly unknown[] | []>( 161 - promises: T, 162 - ): Promise<{ -readonly [P in keyof T]: Awaited<T[P]> }>; 163 - export function allFulfilled<T>( 164 - promises: Iterable<T | PromiseLike<T>>, 165 - ): Promise<Awaited<T>[]>; 166 - export function allFulfilled( 167 - promises: Iterable<Promise<unknown>>, 168 - ): Promise<unknown[]> { 169 - return Promise.allSettled(promises).then(handleAllSettledErrors); 170 - } 171 - 172 - export function handleAllSettledErrors< 173 - T extends readonly PromiseSettledResult<unknown>[] | [], 174 - >( 175 - results: T, 176 - ): { 177 - -readonly [P in keyof T]: T[P] extends PromiseSettledResult<infer U> ? U 178 - : never; 179 - }; 180 - export function handleAllSettledErrors<T>( 181 - results: PromiseSettledResult<T>[], 182 - ): T[]; 183 - export function handleAllSettledErrors( 184 - results: PromiseSettledResult<unknown>[], 185 - ): unknown[] { 186 - if (results.every(isFulfilledResult)) return results.map(extractValue); 187 - 188 - const errors = results.filter(isRejectedResult).map(extractReason); 189 - throw errors; 190 - } 191 - 192 - export function isRejectedResult( 193 - result: PromiseSettledResult<unknown>, 194 - ): result is PromiseRejectedResult { 195 - return result.status === "rejected"; 196 - } 197 - 198 - function extractReason(result: PromiseRejectedResult): unknown { 199 - return result.reason; 200 - } 201 - 202 - export function isFulfilledResult<T>( 203 - result: PromiseSettledResult<T>, 204 - ): result is PromiseFulfilledResult<T> { 205 - return result.status === "fulfilled"; 206 - } 207 - 208 - function extractValue<T>(result: PromiseFulfilledResult<T>): T { 209 - return result.value; 210 - } 1 + export * from "@atp/common-web/async";
+1 -29
common/check.ts
··· 1 - // Explicitly not using "zod" types here to avoid mismatching types due to 2 - // version differences. 3 - 4 - export interface Checkable<T> { 5 - parse: (obj: unknown) => T; 6 - safeParse: ( 7 - obj: unknown, 8 - ) => { success: true; data: T } | { success: false; error: Error }; 9 - } 10 - 11 - export interface Def<T> { 12 - name: string; 13 - schema: Checkable<T>; 14 - } 15 - 16 - export const is = <T>(obj: unknown, def: Checkable<T>): obj is T => { 17 - return def.safeParse(obj).success; 18 - }; 19 - 20 - export const create = <T>(def: Checkable<T>) => (v: unknown): v is T => 21 - def.safeParse(v).success; 22 - 23 - export const assure = <T>(def: Checkable<T>, obj: unknown): T => { 24 - return def.parse(obj); 25 - }; 26 - 27 - export const isObject = (obj: unknown): obj is Record<string, unknown> => { 28 - return typeof obj === "object" && obj !== null; 29 - }; 1 + export * from "@atp/common-web/check";
+1 -129
common/dates.ts
··· 1 - // Time constants 2 - export const SECOND = 1000; 3 - export const MINUTE = SECOND * 60; 4 - export const HOUR = MINUTE * 60; 5 - export const DAY = HOUR * 24; 6 - 7 - // Characters that need escaping in regex 8 - const SEPARATORS_TO_ESCAPE = new Set([ 9 - "\\", 10 - "^", 11 - "$", 12 - ".", 13 - "|", 14 - "?", 15 - "*", 16 - "+", 17 - "(", 18 - ")", 19 - "[", 20 - "]", 21 - "{", 22 - "}", 23 - ]); 24 - 25 - // Helper Functions 26 - 27 - function getStringSeparator(dateString: string): string { 28 - const separator = /\D/.exec(dateString); 29 - return separator ? separator[0] : ""; 30 - } 31 - 32 - function getTimeStringSeparator(timeString: string): string { 33 - const matches = timeString.match(/([^Z+\-\d])(?=\d+\1)/); 34 - return Array.isArray(matches) ? matches[0] : ""; 35 - } 36 - 37 - // Validation Functions 38 - 39 - export function isValidDate(date: string, s = "-"): boolean { 40 - if (SEPARATORS_TO_ESCAPE.has(s)) { 41 - s = `\\${s}`; 42 - } 43 - 44 - const validator = new RegExp( 45 - `^(?!0{4}${s}0{2}${s}0{2})((?=[0-9]{4}${s}(((0[^2])|1[0-2])|02(?=${s}(([0-1][0-9])|2[0-8])))${s}[0-9]{2})|(?=((([13579][26])|([2468][048])|(0[48]))0{2})|([0-9]{2}((((0|[2468])[48])|[2468][048])|([13579][26])))${s}02${s}29))([0-9]{4})${s}(?!((0[469])|11)${s}31)((0[1,3-9]|1[0-2])|(02(?!${s}3)))${s}(0[1-9]|[1-2][0-9]|3[0-1])$`, 46 - ); 47 - return validator.test(date); 48 - } 49 - 50 - function isValidTime( 51 - timeWithOffset: string, 52 - s = ":", 53 - isTimezoneCheckOn = false, 54 - ): boolean { 55 - const validator = new RegExp( 56 - `^([0-1]|2(?=([0-3])|4${s}00))[0-9]${s}[0-5][0-9](${s}([0-5]|6(?=0))[0-9])?(\.[0-9]{1,9})?$`, 57 - ); 58 - 59 - if (!isTimezoneCheckOn || !/[Z+\-]/.test(timeWithOffset)) { 60 - return validator.test(timeWithOffset); 61 - } 62 - 63 - // Case we got time in Zulu tz 64 - if (/Z$/.test(timeWithOffset)) { 65 - return validator.test(timeWithOffset.replace("Z", "")); 66 - } 67 - 68 - const isPositiveTimezoneOffset = timeWithOffset.includes("+"); 69 - const [time, offset] = timeWithOffset.split(/[+-]/); 70 - 71 - return validator.test(time) && 72 - isValidZoneOffset( 73 - offset, 74 - isPositiveTimezoneOffset, 75 - getStringSeparator(offset), 76 - ); 77 - } 78 - 79 - function isValidZoneOffset( 80 - offset: string, 81 - isPositiveOffset: boolean, 82 - s = ":", 83 - ): boolean { 84 - const validator = new RegExp( 85 - isPositiveOffset 86 - ? `^(0(?!(2${s}4)|0${s}3)|1(?=([0-1]|2(?=${s}[04])|[34](?=${s}0))))([03469](?=${s}[03])|[17](?=${s}0)|2(?=${s}[04])|5(?=${s}[034])|8(?=${s}[04]))${s}([03](?=0)|4(?=5))[05]$` 87 - : `^(0(?=[^0])|1(?=[0-2]))([39](?=${s}[03])|[0-24-8](?=${s}00))${s}[03]0$`, 88 - ); 89 - return validator.test(offset); 90 - } 91 - 92 - export function isValidISODateString(dateString: string): boolean { 93 - const [date, timeWithOffset] = dateString.split("T"); 94 - const dateSeparator = getStringSeparator(date); 95 - const isDateValid = isValidDate(date, dateSeparator); 96 - 97 - if (!timeWithOffset) { 98 - return false; 99 - } 100 - 101 - const timeStringSeparator = getTimeStringSeparator(timeWithOffset); 102 - return isDateValid && isValidTime(timeWithOffset, timeStringSeparator, true); 103 - } 104 - 105 - // Utility Functions 106 - 107 - export const lessThanAgoMs = (time: Date, range: number): boolean => { 108 - return Date.now() < time.getTime() + range; 109 - }; 110 - 111 - export const addHoursToDate = (hours: number, startingDate?: Date): Date => { 112 - // When date is passed, clone before calling `setHours()` so that we are not mutating the original date 113 - const currentDate = startingDate ? new Date(startingDate) : new Date(); 114 - currentDate.setHours(currentDate.getHours() + hours); 115 - return currentDate; 116 - }; 117 - 118 - export function toSimplifiedISOSafe(dateStr: string): string { 119 - const date = new Date(dateStr); 120 - if (isNaN(date.getTime())) { 121 - return new Date(0).toISOString(); 122 - } 123 - const iso = date.toISOString(); 124 - if (!isValidISODateString(iso)) { 125 - // Occurs in rare cases, e.g. where resulting UTC year is negative. These also don't preserve lexical sort. 126 - return new Date(0).toISOString(); 127 - } 128 - return iso; // YYYY-MM-DDTHH:mm:ss.sssZ 129 - } 1 + export * from "@atp/common-web/dates";
+1 -210
common/did-doc.ts
··· 1 - import { z } from "zod"; 2 - 3 - // Parsing atproto data 4 - // -------- 5 - 6 - export const isValidDidDoc = (doc: unknown): doc is DidDocument => { 7 - return didDocument.safeParse(doc).success; 8 - }; 9 - 10 - export const getDid = (doc: DidDocument): string => { 11 - const id = doc.id; 12 - if (typeof id !== "string") { 13 - throw new Error("No `id` on document"); 14 - } 15 - return id; 16 - }; 17 - 18 - export const getHandle = (doc: DidDocument): string | undefined => { 19 - const aka = doc.alsoKnownAs; 20 - if (aka) { 21 - for (let i = 0; i < aka.length; i++) { 22 - const alias = aka[i]; 23 - if (alias.startsWith("at://")) { 24 - // strip off "at://" prefix 25 - return alias.slice(5); 26 - } 27 - } 28 - } 29 - return undefined; 30 - }; 31 - 32 - // @NOTE we parse to type/publicKeyMultibase to avoid the dependency on @atproto/crypto 33 - export const getSigningKey = ( 34 - doc: DidDocument, 35 - ): { type: string; publicKeyMultibase: string } | undefined => { 36 - return getVerificationMaterial(doc, "atproto"); 37 - }; 38 - 39 - export const getVerificationMaterial = ( 40 - doc: DidDocument, 41 - keyId: string, 42 - ): { type: string; publicKeyMultibase: string } | undefined => { 43 - // /!\ Hot path 44 - 45 - const key = findItemById(doc, "verificationMethod", `#${keyId}`); 46 - if (!key) { 47 - return undefined; 48 - } 49 - 50 - if (!key.publicKeyMultibase) { 51 - return undefined; 52 - } 53 - 54 - return { 55 - type: key.type, 56 - publicKeyMultibase: key.publicKeyMultibase, 57 - }; 58 - }; 59 - 60 - export const getSigningDidKey = (doc: DidDocument): string | undefined => { 61 - const parsed = getSigningKey(doc); 62 - if (!parsed) return; 63 - return `did:key:${parsed.publicKeyMultibase}`; 64 - }; 65 - 66 - export const getPdsEndpoint = (doc: DidDocument): string | undefined => { 67 - return getServiceEndpoint(doc, { 68 - id: "#atproto_pds", 69 - type: "AtprotoPersonalDataServer", 70 - }); 71 - }; 72 - 73 - export const getFeedGenEndpoint = (doc: DidDocument): string | undefined => { 74 - return getServiceEndpoint(doc, { 75 - id: "#bsky_fg", 76 - type: "BskyFeedGenerator", 77 - }); 78 - }; 79 - 80 - export const getNotifEndpoint = (doc: DidDocument): string | undefined => { 81 - return getServiceEndpoint(doc, { 82 - id: "#bsky_notif", 83 - type: "BskyNotificationService", 84 - }); 85 - }; 86 - 87 - export const getServiceEndpoint = ( 88 - doc: DidDocument, 89 - opts: { id: string; type?: string }, 90 - ): string | undefined => { 91 - // /!\ Hot path 92 - 93 - const service = findItemById(doc, "service", opts.id); 94 - if (!service) { 95 - return undefined; 96 - } 97 - 98 - if (opts.type && service.type !== opts.type) { 99 - return undefined; 100 - } 101 - 102 - if (typeof service.serviceEndpoint !== "string") { 103 - return undefined; 104 - } 105 - 106 - return validateUrl(service.serviceEndpoint); 107 - }; 108 - 109 - function findItemById< 110 - D extends DidDocument, 111 - T extends "verificationMethod" | "service", 112 - >(doc: D, type: T, id: string): NonNullable<D[T]>[number] | undefined; 113 - function findItemById( 114 - doc: DidDocument, 115 - type: "verificationMethod" | "service", 116 - id: string, 117 - ) { 118 - // /!\ Hot path 119 - 120 - const items = doc[type]; 121 - if (items) { 122 - for (let i = 0; i < items.length; i++) { 123 - const item = items[i]; 124 - const itemId = item.id; 125 - 126 - if ( 127 - itemId[0] === "#" 128 - ? itemId === id 129 - // Optimized version of: itemId === `${doc.id}${id}` 130 - : itemId.length === doc.id.length + id.length && 131 - itemId[doc.id.length] === "#" && 132 - itemId.endsWith(id) && 133 - itemId.startsWith(doc.id) // <== We could probably skip this check 134 - ) { 135 - return item; 136 - } 137 - } 138 - } 139 - return undefined; 140 - } 141 - 142 - // Check protocol and hostname to prevent potential SSRF 143 - const validateUrl = (urlStr: string): string | undefined => { 144 - if (!urlStr.startsWith("http://") && !urlStr.startsWith("https://")) { 145 - return undefined; 146 - } 147 - 148 - if (!canParseUrl(urlStr)) { 149 - return undefined; 150 - } 151 - 152 - return urlStr; 153 - }; 154 - 155 - const canParseUrl = URL.canParse ?? 156 - // URL.canParse is not available in Node.js < 18.17.0 157 - ((urlStr: string): boolean => { 158 - try { 159 - new URL(urlStr); 160 - return true; 161 - } catch { 162 - return false; 163 - } 164 - }); 165 - 166 - // Types 167 - // -------- 168 - 169 - const verificationMethod: VerificationMethod = z.object({ 170 - id: z.string(), 171 - type: z.string(), 172 - controller: z.string(), 173 - publicKeyMultibase: z.string().optional(), 174 - }); 175 - 176 - type VerificationMethod = z.ZodObject<{ 177 - id: z.ZodString; 178 - type: z.ZodString; 179 - controller: z.ZodString; 180 - publicKeyMultibase: z.ZodOptional<z.ZodString>; 181 - }, z.core.$strip>; 182 - 183 - const service: Service = z.object({ 184 - id: z.string(), 185 - type: z.string(), 186 - serviceEndpoint: z.union([z.string(), z.record(z.string(), z.unknown())]), 187 - }); 188 - 189 - type Service = z.ZodObject<{ 190 - id: z.ZodString; 191 - type: z.ZodString; 192 - serviceEndpoint: z.ZodUnion< 193 - readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodUnknown>] 194 - >; 195 - }, z.core.$strip>; 196 - 197 - export const didDocument: DidDocumentType = z.object({ 198 - id: z.string(), 199 - alsoKnownAs: z.array(z.string()).optional(), 200 - verificationMethod: z.array(verificationMethod).optional(), 201 - service: z.array(service).optional(), 202 - }); 203 - type DidDocumentType = z.ZodObject<{ 204 - id: z.ZodString; 205 - alsoKnownAs: z.ZodOptional<z.ZodArray<z.ZodString>>; 206 - verificationMethod: z.ZodOptional<z.ZodArray<VerificationMethod>>; 207 - service: z.ZodOptional<z.ZodArray<Service>>; 208 - }, z.core.$strip>; 209 - 210 - export type DidDocument = z.infer<DidDocumentType>; 1 + export * from "@atp/common-web/did-doc";
+1 -10
common/mod.ts
··· 8 8 * 9 9 * @module 10 10 */ 11 - export * as check from "./check.ts"; 12 - export * as util from "./util.ts"; 13 - 11 + export * from "@atp/common-web"; 14 12 export * from "./ipld.ts"; 15 13 export * from "./ipld-multi.ts"; 16 14 export * from "./obfuscate.ts"; 17 15 export * from "./streams.ts"; 18 - export * from "./async.ts"; 19 16 export * from "./types.ts"; 20 - export * from "./tid.ts"; 21 - export * from "./strings.ts"; 22 - export * from "./dates.ts"; 23 - export * from "./util.ts"; 24 - export * from "./retry.ts"; 25 - export * from "./did-doc.ts";
+1 -61
common/retry.ts
··· 1 - import { wait } from "./util.ts"; 2 - 3 - export type RetryOptions = { 4 - maxRetries?: number; 5 - getWaitMs?: (n: number) => number | null; 6 - }; 7 - 8 - export async function retry<T>( 9 - fn: () => Promise<T> | T, 10 - opts: RetryOptions & { 11 - retryable?: (err: unknown) => boolean; 12 - } = {}, 13 - ): Promise<T> { 14 - const { maxRetries = 3, retryable = () => true, getWaitMs = backoffMs } = 15 - opts; 16 - let retries = 0; 17 - let doneError: unknown; 18 - while (!doneError) { 19 - try { 20 - return await fn(); 21 - } catch (err) { 22 - const waitMs = getWaitMs(retries); 23 - const willRetry = retries < maxRetries && waitMs !== null && 24 - retryable(err); 25 - if (willRetry) { 26 - retries += 1; 27 - if (waitMs !== 0) { 28 - await wait(waitMs); 29 - } 30 - } else { 31 - doneError = err; 32 - } 33 - } 34 - } 35 - throw doneError; 36 - } 37 - 38 - export function createRetryable(retryable: (err: unknown) => boolean): { 39 - <T>(fn: () => Promise<T>, opts?: RetryOptions): Promise<T>; 40 - } { 41 - return <T>(fn: () => Promise<T>, opts?: RetryOptions) => 42 - retry(fn, { ...opts, retryable }); 43 - } 44 - 45 - // Waits exponential backoff with max and jitter: ~100, ~200, ~400, ~800, ~1000, ~1000, ... 46 - export function backoffMs(n: number, multiplier = 100, max = 1000): number { 47 - const exponentialMs = Math.pow(2, n) * multiplier; 48 - const ms = Math.min(exponentialMs, max); 49 - return jitter(ms); 50 - } 51 - 52 - // Adds randomness +/-15% of value 53 - function jitter(value: number) { 54 - const delta = value * 0.15; 55 - return value + randomRange(-delta, delta); 56 - } 57 - 58 - function randomRange(from: number, to: number) { 59 - const rand = Math.random() * (to - from); 60 - return rand + from; 61 - } 1 + export * from "@atp/common-web/retry";
+1 -83
common/strings.ts
··· 1 - // counts the number of bytes in a utf8 string 2 - export const utf8Len = (str: string): number => { 3 - return new TextEncoder().encode(str).byteLength; 4 - }; 5 - 6 - // counts the number of graphemes (user-displayed characters) in a string 7 - // Using Intl.Segmenter which is supported in Deno and modern browsers 8 - export const graphemeLen = (str: string): number => { 9 - if (typeof Intl !== "undefined" && "Segmenter" in Intl) { 10 - const segmenter = new Intl.Segmenter(undefined, { 11 - granularity: "grapheme", 12 - }); 13 - return Array.from(segmenter.segment(str)).length; 14 - } 15 - 16 - // Fallback for environments without Intl.Segmenter 17 - // This is a simplified approach that handles basic cases 18 - return Array.from(str).length; 19 - }; 20 - 21 - export const utf8ToB64Url = (utf8: string): string => { 22 - const encoder = new TextEncoder(); 23 - const bytes = encoder.encode(utf8); 24 - return btoa(String.fromCharCode(...bytes)) 25 - .replace(/\+/g, "-") 26 - .replace(/\//g, "_") 27 - .replace(/=/g, ""); 28 - }; 29 - 30 - export const b64UrlToUtf8 = (b64: string): string => { 31 - // Convert base64url to base64 32 - const base64 = b64.replace(/-/g, "+").replace(/_/g, "/"); 33 - // Add padding if needed 34 - const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4); 35 - 36 - const binaryString = atob(padded); 37 - const bytes = new Uint8Array(binaryString.length); 38 - for (let i = 0; i < binaryString.length; i++) { 39 - bytes[i] = binaryString.charCodeAt(i); 40 - } 41 - 42 - const decoder = new TextDecoder(); 43 - return decoder.decode(bytes); 44 - }; 45 - 46 - export const parseLanguage = (langTag: string): LanguageTag | null => { 47 - const parsed = langTag.match(bcp47Regexp); 48 - if (!parsed?.groups) return null; 49 - const parts = parsed.groups; 50 - const result: LanguageTag = {}; 51 - 52 - if (parts.grandfathered) result.grandfathered = parts.grandfathered; 53 - if (parts.language) result.language = parts.language; 54 - if (parts.extlang) result.extlang = parts.extlang; 55 - if (parts.script) result.script = parts.script; 56 - if (parts.region) result.region = parts.region; 57 - if (parts.variant) result.variant = parts.variant; 58 - if (parts.extension) result.extension = parts.extension; 59 - if (parts.privateUseA || parts.privateUseB) { 60 - result.privateUse = parts.privateUseA || parts.privateUseB; 61 - } 62 - 63 - return result; 64 - }; 65 - 66 - export const validateLanguage = (langTag: string): boolean => { 67 - return bcp47Regexp.test(langTag); 68 - }; 69 - 70 - export type LanguageTag = { 71 - grandfathered?: string; 72 - language?: string; 73 - extlang?: string; 74 - script?: string; 75 - region?: string; 76 - variant?: string; 77 - extension?: string; 78 - privateUse?: string; 79 - }; 80 - 81 - // Validates well-formed BCP 47 syntax: https://www.rfc-editor.org/rfc/rfc5646.html#section-2.1 82 - const bcp47Regexp = 83 - /^((?<grandfathered>(en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((?<language>([A-Za-z]{2,3}(-(?<extlang>[A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-(?<script>[A-Za-z]{4}))?(-(?<region>[A-Za-z]{2}|[0-9]{3}))?(-(?<variant>[A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-(?<extension>[0-9A-WY-Za-wy-z](-[A-Za-z0-9]{2,8})+))*(-(?<privateUseA>x(-[A-Za-z0-9]{1,8})+))?)|(?<privateUseB>x(-[A-Za-z0-9]{1,8})+))$/; 1 + export * from "@atp/common-web/strings";
+1 -111
common/tid.ts
··· 1 - import { s32decode, s32encode } from "./util.ts"; 2 - 3 - const TID_LEN = 13; 4 - 5 - let lastTimestamp = 0; 6 - let timestampCount = 0; 7 - let clockid: number | null = null; 8 - 9 - function dedash(str: string): string { 10 - return str.replaceAll("-", ""); 11 - } 12 - 13 - export class TID { 14 - str: string; 15 - 16 - constructor(str: string) { 17 - const noDashes = dedash(str); 18 - if (noDashes.length !== TID_LEN) { 19 - throw new Error(`Poorly formatted TID: ${noDashes.length} length`); 20 - } 21 - this.str = noDashes; 22 - } 23 - 24 - static next(prev?: TID): TID { 25 - // javascript does not have microsecond precision 26 - // instead, we append a counter to the timestamp to indicate if multiple timestamps were created within the same millisecond 27 - // take max of current time & last timestamp to prevent tids moving backwards if system clock drifts backwards 28 - const time = Math.max(Date.now(), lastTimestamp); 29 - if (time === lastTimestamp) { 30 - timestampCount++; 31 - } 32 - lastTimestamp = time; 33 - const timestamp = time * 1000 + timestampCount; 34 - // the bottom 32 clock ids can be randomized & are not guaranteed to be collision resistant 35 - // we use the same clockid for all tids coming from this machine 36 - if (clockid === null) { 37 - clockid = Math.floor(Math.random() * 32); 38 - } 39 - const tid = TID.fromTime(timestamp, clockid); 40 - if (!prev || tid.newerThan(prev)) { 41 - return tid; 42 - } 43 - return TID.fromTime(prev.timestamp() + 1, clockid); 44 - } 45 - 46 - static nextStr(prev?: string): string { 47 - return TID.next(prev ? new TID(prev) : undefined).toString(); 48 - } 49 - 50 - static fromTime(timestamp: number, clockid: number): TID { 51 - // base32 encode with encoding variant sort (s32) 52 - const str = `${s32encode(timestamp)}${s32encode(clockid).padStart(2, "2")}`; 53 - return new TID(str); 54 - } 55 - 56 - static fromStr(str: string): TID { 57 - return new TID(str); 58 - } 59 - 60 - static oldestFirst(a: TID, b: TID): number { 61 - return a.compareTo(b); 62 - } 63 - 64 - static newestFirst(a: TID, b: TID): number { 65 - return b.compareTo(a); 66 - } 67 - 68 - static is(str: string): boolean { 69 - return dedash(str).length === TID_LEN; 70 - } 71 - 72 - timestamp(): number { 73 - return s32decode(this.str.slice(0, 11)); 74 - } 75 - 76 - clockid(): number { 77 - return s32decode(this.str.slice(11, 13)); 78 - } 79 - 80 - formatted(): string { 81 - const str = this.toString(); 82 - return `${str.slice(0, 4)}-${str.slice(4, 7)}-${ 83 - str.slice( 84 - 7, 85 - 11, 86 - ) 87 - }-${str.slice(11, 13)}`; 88 - } 89 - 90 - toString(): string { 91 - return this.str; 92 - } 93 - 94 - compareTo(other: TID): number { 95 - if (this.str > other.str) return 1; 96 - if (this.str < other.str) return -1; 97 - return 0; 98 - } 99 - 100 - equals(other: TID): boolean { 101 - return this.str === other.str; 102 - } 103 - 104 - newerThan(other: TID): boolean { 105 - return this.compareTo(other) > 0; 106 - } 107 - 108 - olderThan(other: TID): boolean { 109 - return this.compareTo(other) < 0; 110 - } 111 - } 1 + export * from "@atp/common-web/tid";
+1 -183
common/util.ts
··· 1 - export const noUndefinedVals = <T>( 2 - obj: Record<string, T | undefined>, 3 - ): Record<string, T> => { 4 - Object.keys(obj).forEach((k) => { 5 - if (obj[k] === undefined) { 6 - delete obj[k]; 7 - } 8 - }); 9 - return obj as Record<string, T>; 10 - }; 11 - 12 - /** 13 - * Returns a shallow copy of the object without the specified keys. If the input 14 - * is nullish, it returns the input. 15 - */ 16 - export function omit< 17 - T extends undefined | null | Record<string, unknown>, 18 - K extends keyof NonNullable<T>, 19 - >( 20 - object: T, 21 - rejectedKeys: readonly K[], 22 - ): T extends undefined ? undefined : T extends null ? null : Omit<T, K>; 23 - export function omit( 24 - src: undefined | null | Record<string, unknown>, 25 - rejectedKeys: readonly string[], 26 - ): undefined | null | Record<string, unknown> { 27 - // Hot path 28 - 29 - if (!src) return src; 30 - 31 - const dst: Record<string, unknown> = {}; 32 - const srcKeys = Object.keys(src); 33 - for (let i = 0; i < srcKeys.length; i++) { 34 - const key = srcKeys[i]; 35 - if (!rejectedKeys.includes(key)) { 36 - dst[key] = src[key]; 37 - } 38 - } 39 - return dst; 40 - } 41 - 42 - export const jitter = (maxMs: number): number => { 43 - return Math.round((Math.random() - 0.5) * maxMs * 2); 44 - }; 45 - 46 - export const wait = (ms: number): Promise<void> => { 47 - return new Promise((res) => setTimeout(res, ms)); 48 - }; 49 - 50 - export type BailableWait = { 51 - bail: () => void; 52 - wait: () => Promise<void>; 53 - }; 54 - 55 - export const bailableWait = (ms: number): BailableWait => { 56 - let bail!: () => void; 57 - const waitPromise = new Promise<void>((res) => { 58 - const timeout = setTimeout(res, ms); 59 - bail = () => { 60 - clearTimeout(timeout); 61 - res(); 62 - }; 63 - }); 64 - return { bail, wait: () => waitPromise }; 65 - }; 66 - 67 - export const flattenUint8Arrays = (arrs: Uint8Array[]): Uint8Array => { 68 - const length = arrs.reduce((acc, cur) => { 69 - return acc + cur.length; 70 - }, 0); 71 - const flattened = new Uint8Array(length); 72 - let offset = 0; 73 - arrs.forEach((arr) => { 74 - flattened.set(arr, offset); 75 - offset += arr.length; 76 - }); 77 - return flattened; 78 - }; 79 - 80 - export const streamToUI8Array = async ( 81 - stream: AsyncIterable<Uint8Array>, 82 - ): Promise<Uint8Array> => { 83 - const arrays: Uint8Array[] = []; 84 - for await (const chunk of stream) { 85 - arrays.push(chunk); 86 - } 87 - return flattenUint8Arrays(arrays); 88 - }; 89 - 90 - const S32_CHAR = "234567abcdefghijklmnopqrstuvwxyz"; 91 - 92 - export const s32encode = (i: number): string => { 93 - let s = ""; 94 - while (i) { 95 - const c = i % 32; 96 - i = Math.floor(i / 32); 97 - s = S32_CHAR.charAt(c) + s; 98 - } 99 - return s; 100 - }; 101 - 102 - export const s32decode = (s: string): number => { 103 - let i = 0; 104 - for (const c of s) { 105 - i = i * 32 + S32_CHAR.indexOf(c); 106 - } 107 - return i; 108 - }; 109 - 110 - export const asyncFilter = async <T>( 111 - arr: T[], 112 - fn: (t: T) => Promise<boolean>, 113 - ): Promise<T[]> => { 114 - const results = await Promise.all(arr.map((t) => fn(t))); 115 - return arr.filter((_, i) => results[i]); 116 - }; 117 - 118 - export const errHasMsg = (err: unknown, msg: string): boolean => { 119 - return !!err && typeof err === "object" && "message" in err && 120 - (err as { message: unknown }).message === msg; 121 - }; 122 - 123 - export const chunkArray = <T>(arr: T[], chunkSize: number): T[][] => { 124 - return arr.reduce((acc, cur, i) => { 125 - const chunkI = Math.floor(i / chunkSize); 126 - if (!acc[chunkI]) { 127 - acc[chunkI] = []; 128 - } 129 - acc[chunkI].push(cur); 130 - return acc; 131 - }, [] as T[][]); 132 - }; 133 - 134 - export const range = (num: number): number[] => { 135 - const nums: number[] = []; 136 - for (let i = 0; i < num; i++) { 137 - nums.push(i); 138 - } 139 - return nums; 140 - }; 141 - 142 - export const dedupeStrs = (strs: string[]): string[] => { 143 - return [...new Set(strs)]; 144 - }; 145 - 146 - export const parseIntWithFallback = <T>( 147 - value: string | undefined, 148 - fallback: T, 149 - ): number | T => { 150 - const parsed = parseInt(value || "", 10); 151 - return isNaN(parsed) ? fallback : parsed; 152 - }; 153 - 154 - export function ui8ToArrayBuffer(bytes: Uint8Array): ArrayBuffer { 155 - return bytes.buffer.slice( 156 - bytes.byteOffset, 157 - bytes.byteLength + bytes.byteOffset, 158 - ) as ArrayBuffer; 159 - } 160 - 161 - export function keyBy<T, K extends keyof T>( 162 - arr: readonly T[], 163 - key: K, 164 - ): Map<T[K], T> { 165 - return arr.reduce((acc, cur) => { 166 - acc.set(cur[key], cur); 167 - return acc; 168 - }, new Map<T[K], T>()); 169 - } 170 - 171 - export const mapDefined = <T, S>( 172 - arr: T[], 173 - fn: (obj: T) => S | undefined, 174 - ): S[] => { 175 - const output: S[] = []; 176 - for (const item of arr) { 177 - const val = fn(item); 178 - if (val !== undefined) { 179 - output.push(val); 180 - } 181 - } 182 - return output; 183 - }; 1 + export * from "@atp/common-web/util";
+1
deno.json
··· 1 1 { 2 2 "workspace": [ 3 + "common-web", 3 4 "common", 4 5 "bytes", 5 6 "syntax",
+5
deno.lock
··· 1135 1135 "npm:multiformats@^13.4.1" 1136 1136 ] 1137 1137 }, 1138 + "common-web": { 1139 + "dependencies": [ 1140 + "jsr:@zod/zod@^4.1.13" 1141 + ] 1142 + }, 1138 1143 "crypto": { 1139 1144 "dependencies": [ 1140 1145 "jsr:@noble/curves@^2.0.1",
+1 -1
identity/did/atproto-data.ts
··· 5 5 getNotifEndpoint, 6 6 getPdsEndpoint, 7 7 getSigningKey, 8 - } from "@atp/common"; 8 + } from "@atp/common-web"; 9 9 import * as crypto from "@atp/crypto"; 10 10 import type { AtprotoData, DidDocument } from "../types.ts"; 11 11
+1 -1
identity/did/base-resolver.ts
··· 1 - import { check } from "@atp/common"; 1 + import { check } from "@atp/common-web"; 2 2 import * as crypto from "@atp/crypto"; 3 3 import { 4 4 DidNotFoundError,
+1 -1
identity/did/memory-cache.ts
··· 1 - import { DAY, HOUR } from "@atp/common"; 1 + import { DAY, HOUR } from "@atp/common-web"; 2 2 import type { CacheResult, DidCache, DidDocument } from "../types.ts"; 3 3 4 4 /**
+1 -1
identity/tests/did-cache_test.ts
··· 1 1 import * as plc from "@did-plc/lib"; 2 2 import { Database as DidPlcDb, PlcServer } from "@did-plc/server"; 3 3 import getPort from "get-port"; 4 - import { wait } from "@atp/common"; 4 + import { wait } from "@atp/common-web"; 5 5 // deno-lint-ignore no-import-prefix no-unversioned-import 6 6 import { Secp256k1Keypair } from "npm:@atproto/crypto"; 7 7 import { DidResolver } from "../mod.ts";
+3 -3
identity/types.ts
··· 1 - import type { DidDocument } from "@atp/common"; 1 + import type { DidDocument } from "@atp/common-web"; 2 2 3 - export { didDocument } from "@atp/common"; 4 - export type { DidDocument } from "@atp/common"; 3 + export { didDocument } from "@atp/common-web"; 4 + export type { DidDocument } from "@atp/common-web"; 5 5 6 6 /** 7 7 * Options for a combined handle and did resolver.