a collection of lightweight TypeScript packages for AT Protocol, the protocol powering Bluesky
atproto bluesky typescript npm

feat(tap): initial commit

mary.my.id 4e4877c0 8ce48960

verified
+2
README.md
··· 36 36 | [`client`](./packages/clients/client): XRPC HTTP client | 37 37 | [`firehose`](./packages/clients/firehose): XRPC subscription client | 38 38 | [`jetstream`](./packages/clients/jetstream): Jetstream WebSocket client | 39 + | [`tap`](./packages/clients/tap): Tap WebSocket client | 39 40 | [`cache`](./packages/clients/cache): normalized cache store | 40 41 | **Server packages** | 41 42 | [`xrpc-server`](./packages/servers/xrpc-server): XRPC web framework | ··· 45 46 | [`xrpc-server-node`](./packages/servers/xrpc-server-node): Node.js WebSocket adapter | 46 47 | **OAuth packages** | 47 48 | [`oauth-browser-client`](./packages/oauth/browser-client): minimal OAuth client for SPAs | 49 + | [`oauth-node-client`](./packages/oauth/node-client): OAuth client for Node.js | 48 50 | **Lexicon packages** | 49 51 | [`lex-cli`](./packages/lexicons/lex-cli): generate TypeScript from lexicon schemas | 50 52 | [`lexicon-doc`](./packages/lexicons/lexicon-doc): parse and author lexicon documents |
+33
packages/clients/tap/README.md
··· 1 + # @atcute/tap 2 + 3 + an atproto Tap client. 4 + 5 + ```sh 6 + npm install @atcute/tap 7 + ``` 8 + 9 + Tap is a sync utility that connects to the relay firehose, verifies data, performs backfill, and 10 + streams simple JSON events to clients over a local websocket. 11 + 12 + ## usage 13 + 14 + ```ts 15 + import { TapClient } from '@atcute/tap'; 16 + 17 + const tap = new TapClient({ 18 + url: 'http://localhost:2480', 19 + // adminPassword: process.env.TAP_ADMIN_PASSWORD, 20 + }); 21 + 22 + await tap.addRepos(['did:plc:...']); 23 + 24 + for await (const { event, ack } of tap.subscribe()) { 25 + // process event idempotently 26 + await ack(); 27 + } 28 + ``` 29 + 30 + ## acks 31 + 32 + Tap’s default mode requires that clients acknowledge each event after it has been processed. if you 33 + don’t call `ack()`, Tap will retry delivery later.
+24
packages/clients/tap/lib/dev-index.ts
··· 1 + import { TapClient } from './tap-client.js'; 2 + 3 + const tap = new TapClient({ url: 'http://localhost:2480' }); 4 + 5 + const subscription = tap.subscribe({ 6 + onConnectionOpen() { 7 + console.log(`ws open`); 8 + }, 9 + onConnectionClose() { 10 + console.log(`ws close`); 11 + }, 12 + onConnectionError(ev) { 13 + console.log(`ws error`, ev); 14 + }, 15 + onError(err) { 16 + console.error('tap subscription error', err); 17 + }, 18 + }); 19 + 20 + for await (const { event, ack } of subscription) { 21 + console.log(event); 22 + 23 + await ack(); 24 + }
+5
packages/clients/tap/lib/index.ts
··· 1 + export { TapClient } from './tap-client.js'; 2 + export { TapSubscription } from './tap-subscription.js'; 3 + 4 + export * as defs from './typedefs.js'; 5 + export type * from './types.js';
+114
packages/clients/tap/lib/tap-client.ts
··· 1 + import { defs as identityDefs, type DidDocument } from '@atcute/identity'; 2 + 3 + import type { Did } from '@atcute/lexicons'; 4 + 5 + import { TapSubscription } from './tap-subscription.js'; 6 + import { repoInfoSchema } from './typedefs.js'; 7 + import type { RepoInfo, TapClientOptions, TapSubscribeOptions } from './types.js'; 8 + import { formatAdminAuthHeader } from './utils.js'; 9 + 10 + export class TapClient { 11 + #url: URL; 12 + #fetch: typeof globalThis.fetch; 13 + #adminPassword?: string; 14 + #authHeader?: string; 15 + 16 + constructor(options: TapClientOptions) { 17 + const url = typeof options.url === 'string' ? new URL(options.url) : new URL(options.url); 18 + 19 + if (url.protocol !== 'http:' && url.protocol !== 'https:') { 20 + throw new Error(`invalid url protocol, expected http: or https:, got ${url.protocol}`); 21 + } 22 + 23 + this.#url = url; 24 + this.#fetch = options.fetch ?? fetch; 25 + 26 + if (options.adminPassword) { 27 + this.#adminPassword = options.adminPassword; 28 + this.#authHeader = formatAdminAuthHeader(options.adminPassword); 29 + } 30 + } 31 + 32 + subscribe(options?: TapSubscribeOptions): TapSubscription { 33 + const wsUrl = new URL(this.#url); 34 + wsUrl.protocol = wsUrl.protocol === 'https:' ? 'wss:' : 'ws:'; 35 + wsUrl.pathname = '/channel'; 36 + 37 + return new TapSubscription({ 38 + url: wsUrl.toString(), 39 + adminPassword: this.#adminPassword, 40 + ...options, 41 + }); 42 + } 43 + 44 + async addRepos(dids: Did[]): Promise<void> { 45 + const response = await this.#fetch(new URL('/repos/add', this.#url), { 46 + method: 'POST', 47 + headers: this.#getHeaders(), 48 + body: JSON.stringify({ dids }), 49 + }); 50 + 51 + await response.body?.cancel(); 52 + if (!response.ok) { 53 + throw new Error(`failed to add repos: ${response.status} ${response.statusText}`); 54 + } 55 + } 56 + 57 + async removeRepos(dids: Did[]): Promise<void> { 58 + const response = await this.#fetch(new URL('/repos/remove', this.#url), { 59 + method: 'POST', 60 + headers: this.#getHeaders(), 61 + body: JSON.stringify({ dids }), 62 + }); 63 + 64 + await response.body?.cancel(); 65 + if (!response.ok) { 66 + throw new Error(`failed to remove repos: ${response.status} ${response.statusText}`); 67 + } 68 + } 69 + 70 + async resolveDid(did: Did): Promise<DidDocument | null> { 71 + const response = await this.#fetch(new URL(`/resolve/${did}`, this.#url), { 72 + method: 'GET', 73 + headers: this.#getHeaders(), 74 + }); 75 + 76 + if (response.status === 404) { 77 + await response.body?.cancel(); 78 + return null; 79 + } 80 + 81 + if (!response.ok) { 82 + await response.body?.cancel(); 83 + throw new Error(`failed to resolve did: ${response.status} ${response.statusText}`); 84 + } 85 + 86 + return identityDefs.didDocument.parse(await response.json()); 87 + } 88 + 89 + async getRepoInfo(did: Did): Promise<RepoInfo> { 90 + const response = await this.#fetch(new URL(`/info/${did}`, this.#url), { 91 + method: 'GET', 92 + headers: this.#getHeaders(), 93 + }); 94 + 95 + if (!response.ok) { 96 + await response.body?.cancel(); 97 + throw new Error(`failed to get repo info: ${response.status} ${response.statusText}`); 98 + } 99 + 100 + return repoInfoSchema.parse(await response.json()); 101 + } 102 + 103 + #getHeaders(): Record<string, string> { 104 + const headers: Record<string, string> = { 105 + 'Content-Type': 'application/json', 106 + }; 107 + 108 + if (this.#authHeader) { 109 + headers['Authorization'] = this.#authHeader; 110 + } 111 + 112 + return headers; 113 + } 114 + }
+95
packages/clients/tap/lib/tap-subscription.test.ts
··· 1 + import { describe, expect, it } from 'vitest'; 2 + import { WebSocketServer, type RawData, type WebSocket } from 'ws'; 3 + 4 + import * as v from '@badrap/valita'; 5 + import { TapSubscription } from './tap-subscription.js'; 6 + import { tapRecordEventWireSchema } from './typedefs.js'; 7 + 8 + import { decodeUtf8From } from '@atcute/uint8array'; 9 + 10 + type RecordEventWire = v.Infer<typeof tapRecordEventWireSchema>; 11 + 12 + const createRecordEvent = (id: number): RecordEventWire => ({ 13 + id, 14 + type: 'record', 15 + record: { 16 + did: 'did:plc:ewvi7nxzyoun6zhxrhs64oiz', 17 + rev: '3k3m5z2zq2f2x', 18 + collection: 'app.bsky.feed.post', 19 + rkey: '3k3m5z2zq2f2x', 20 + action: 'create', 21 + record: { text: 'hello', $type: 'app.bsky.feed.post' }, 22 + cid: 'bafyreigkq6j3o7v2xq2xq2xq2xq2xq2xq2xq2xq2xq2xq2xq2xq2x', 23 + live: true, 24 + }, 25 + }); 26 + 27 + describe('tap subscription', () => { 28 + it('receives events and sends acks', async () => { 29 + const server = new WebSocketServer({ port: 0 }); 30 + const address = server.address(); 31 + if (typeof address === 'string' || address === null) { 32 + throw new Error(`unexpected ws address`); 33 + } 34 + 35 + const receivedAcks: number[] = []; 36 + 37 + server.on('connection', (socket: WebSocket) => { 38 + socket.send(JSON.stringify(createRecordEvent(42))); 39 + socket.on('message', (data: RawData) => { 40 + const msg = JSON.parse(typeof data === 'string' ? data : decodeUtf8From(data as Uint8Array)); 41 + if (msg.type === 'ack') { 42 + receivedAcks.push(msg.id); 43 + socket.close(); 44 + } 45 + }); 46 + }); 47 + 48 + const subscription = new TapSubscription({ 49 + url: `ws://127.0.0.1:${address.port}/channel`, 50 + }); 51 + 52 + const iterator = subscription[Symbol.asyncIterator](); 53 + const next = await iterator.next(); 54 + if (next.done) { 55 + throw new Error(`expected message`); 56 + } 57 + 58 + expect(next.value.event.type).toBe('record'); 59 + await next.value.ack(); 60 + 61 + await iterator.return?.(); 62 + 63 + await new Promise<void>((resolve) => server.close(() => resolve())); 64 + expect(receivedAcks).toEqual([42]); 65 + }); 66 + 67 + it('drops malformed messages', async () => { 68 + const server = new WebSocketServer({ port: 0 }); 69 + const address = server.address(); 70 + if (typeof address === 'string' || address === null) { 71 + throw new Error(`unexpected ws address`); 72 + } 73 + 74 + let errors = 0; 75 + 76 + server.on('connection', (socket: WebSocket) => { 77 + socket.send('not json'); 78 + setTimeout(() => socket.close(), 25); 79 + }); 80 + 81 + const subscription = new TapSubscription({ 82 + url: `ws://127.0.0.1:${address.port}/channel`, 83 + onError: () => { 84 + errors++; 85 + }, 86 + }); 87 + 88 + const iterator = subscription[Symbol.asyncIterator](); 89 + await new Promise((resolve) => setTimeout(resolve, 75)); 90 + await iterator.return?.(); 91 + 92 + await new Promise<void>((resolve) => server.close(() => resolve())); 93 + expect(errors).toBeGreaterThan(0); 94 + }); 95 + });
+267
packages/clients/tap/lib/tap-subscription.ts
··· 1 + import { EventIterator } from '@mary-ext/event-iterator'; 2 + import { SimpleEventEmitter } from '@mary-ext/simple-event-emitter'; 3 + import { WebSocket as ReconnectingWebSocket } from 'partysocket'; 4 + 5 + import type { ReadonlyDeep } from 'type-fest'; 6 + 7 + import { decodeUtf8From } from '@atcute/uint8array'; 8 + 9 + import { flattenTapEvent, tapEventWireSchema } from './typedefs.js'; 10 + import type { TapEvent, TapSubscribeOptions, TapSubscriptionMessage } from './types.js'; 11 + import { formatAdminAuthHeader } from './utils.js'; 12 + 13 + export interface TapSubscriptionOptions extends TapSubscribeOptions { 14 + url: string; 15 + adminPassword?: string; 16 + } 17 + 18 + type BufferedAck = { 19 + id: number; 20 + promise: Promise<void>; 21 + resolve: (value: void) => void; 22 + reject: (reason?: unknown) => void; 23 + }; 24 + 25 + const PARSE_OPTIONS = { mode: 'passthrough' } as const; 26 + 27 + export class TapSubscription { 28 + #listening = 0; 29 + #ws?: ReconnectingWebSocket; 30 + 31 + #emitter = new SimpleEventEmitter<[message: TapSubscriptionMessage]>(); 32 + #bufferedAcks: BufferedAck[] = []; 33 + 34 + #options: TapSubscriptionOptions; 35 + #closed = false; 36 + 37 + constructor(options: TapSubscriptionOptions) { 38 + this.#options = options; 39 + } 40 + 41 + #sendAck(id: number): boolean { 42 + const ws = this.#ws; 43 + if (ws === undefined) { 44 + return false; 45 + } 46 + 47 + if (ws.readyState !== 1) { 48 + return false; 49 + } 50 + 51 + ws.send(JSON.stringify({ type: 'ack', id })); 52 + return true; 53 + } 54 + 55 + async #ackEvent(id: number): Promise<void> { 56 + if (this.#closed) { 57 + throw new Error(`tap subscription is closed`); 58 + } 59 + 60 + try { 61 + if (this.#sendAck(id)) { 62 + return; 63 + } 64 + } catch { 65 + // fall through to buffering 66 + } 67 + 68 + const { promise, resolve, reject } = Promise.withResolvers<void>(); 69 + this.#bufferedAcks.push({ id, promise, resolve, reject }); 70 + return await promise; 71 + } 72 + 73 + #flushBufferedAcks() { 74 + while (this.#bufferedAcks.length > 0) { 75 + const ack = this.#bufferedAcks[0]; 76 + if (ack === undefined) { 77 + return; 78 + } 79 + 80 + try { 81 + if (!this.#sendAck(ack.id)) { 82 + return; 83 + } 84 + 85 + ack.resolve(undefined); 86 + this.#bufferedAcks = this.#bufferedAcks.slice(1); 87 + } catch (err) { 88 + this.#options.onError?.(err); 89 + return; 90 + } 91 + } 92 + } 93 + 94 + #create() { 95 + if (this.#ws !== undefined) { 96 + return; 97 + } 98 + 99 + const { 100 + url, 101 + adminPassword, 102 + ws: wsOptions, 103 + validateEvents = true, 104 + onConnectionClose, 105 + onConnectionError, 106 + onConnectionOpen, 107 + onError, 108 + } = this.#options; 109 + 110 + const emitter = this.#emitter; 111 + 112 + const authHeader = adminPassword ? formatAdminAuthHeader(adminPassword) : undefined; 113 + 114 + const mergedWsOptions = 115 + authHeader !== undefined && wsOptions?.WebSocket === undefined 116 + ? { 117 + ...wsOptions, 118 + WebSocket: createAuthedWebSocket(authHeader), 119 + } 120 + : wsOptions; 121 + 122 + const ws = new ReconnectingWebSocket(() => url, null, mergedWsOptions); 123 + this.#ws = ws; 124 + 125 + ws.binaryType = 'arraybuffer'; 126 + 127 + ws.onclose = onConnectionClose ?? null; 128 + ws.onerror = onConnectionError ?? null; 129 + 130 + ws.onopen = (ev) => { 131 + this.#flushBufferedAcks(); 132 + onConnectionOpen?.(ev); 133 + }; 134 + 135 + ws.onmessage = (ev) => { 136 + let raw: unknown; 137 + try { 138 + const data = toMessageText(ev.data); 139 + raw = JSON.parse(data); 140 + } catch (err) { 141 + onError?.(new Error(`failed to parse tap message`, { cause: err })); 142 + return; 143 + } 144 + 145 + let evt: TapEvent; 146 + if (validateEvents) { 147 + const result = tapEventWireSchema.try(raw, PARSE_OPTIONS); 148 + if (!result.ok) { 149 + onError?.(result); 150 + return; 151 + } 152 + 153 + evt = flattenTapEvent(result.value); 154 + } else { 155 + try { 156 + evt = flattenTapEvent(raw as any); 157 + } catch (err) { 158 + onError?.(err); 159 + return; 160 + } 161 + } 162 + 163 + let acked = false; 164 + let ackPromise: Promise<void> | undefined; 165 + 166 + emitter.emit({ 167 + event: evt, 168 + ack: () => { 169 + if (!acked) { 170 + acked = true; 171 + ackPromise = this.#ackEvent(evt.id); 172 + } 173 + return ackPromise!; 174 + }, 175 + }); 176 + }; 177 + } 178 + 179 + #destroy() { 180 + const ws = this.#ws; 181 + if (ws) { 182 + ws.close(); 183 + this.#ws = undefined; 184 + } 185 + 186 + this.#closed = true; 187 + 188 + if (this.#bufferedAcks.length > 0) { 189 + const err = new Error(`tap subscription closed before ack was sent`); 190 + for (const ack of this.#bufferedAcks) { 191 + ack.reject(err); 192 + } 193 + this.#bufferedAcks = []; 194 + } 195 + } 196 + 197 + [Symbol.asyncIterator]() { 198 + return new EventIterator<TapSubscriptionMessage>((emit) => { 199 + if (this.#listening === 0) { 200 + this.#closed = false; 201 + this.#create(); 202 + } 203 + 204 + this.#listening++; 205 + this.#emitter.subscribe(emit); 206 + 207 + return () => { 208 + if (this.#listening === 1) { 209 + this.#destroy(); 210 + } 211 + 212 + this.#listening--; 213 + this.#emitter.unsubscribe(emit); 214 + }; 215 + }); 216 + } 217 + 218 + getOptions(): ReadonlyDeep<TapSubscriptionOptions> { 219 + return this.#options; 220 + } 221 + 222 + updateOptions(options: Partial<TapSubscriptionOptions>): void { 223 + this.#options = { ...this.#options, ...options }; 224 + 225 + if (this.#ws !== undefined) { 226 + this.#destroy(); 227 + this.#closed = false; 228 + this.#create(); 229 + } 230 + } 231 + } 232 + 233 + const toMessageText = (data: unknown): string => { 234 + if (typeof data === 'string') { 235 + return data; 236 + } 237 + 238 + if (data instanceof ArrayBuffer) { 239 + return decodeUtf8From(new Uint8Array(data)); 240 + } 241 + 242 + if (ArrayBuffer.isView(data)) { 243 + return decodeUtf8From(new Uint8Array(data.buffer, data.byteOffset, data.byteLength)); 244 + } 245 + 246 + return String(data); 247 + }; 248 + 249 + const createAuthedWebSocket = (authorization: string) => { 250 + const WebSocketCtor = WebSocket as unknown as { 251 + new ( 252 + url: string | URL, 253 + protocols?: string | string[], 254 + options?: { headers?: Record<string, string> }, 255 + ): WebSocket; 256 + }; 257 + 258 + return class AuthedWebSocket extends WebSocketCtor { 259 + constructor(url: string | URL, protocols?: string | string[]) { 260 + super(url, protocols as any, { 261 + headers: { 262 + Authorization: authorization, 263 + }, 264 + }); 265 + } 266 + }; 267 + };
+102
packages/clients/tap/lib/typedefs.ts
··· 1 + import * as v from '@badrap/valita'; 2 + 3 + import { isDid, isHandle, isNsid, isRecordKey, isTid } from '@atcute/lexicons/syntax'; 4 + 5 + import type * as t from './types.js'; 6 + 7 + const didString = v.string().assert(isDid, `must be a did`); 8 + const handleString = v.string().assert(isHandle, `must be a handle`); 9 + const nsidString = v.string().assert(isNsid, `must be an nsid`); 10 + const rkeyString = v.string().assert(isRecordKey, `must be a record key`); 11 + const tidString = v.string().assert(isTid, `must be a tid`); 12 + 13 + const integer = v 14 + .number() 15 + .assert((input) => input >= 0 && Number.isSafeInteger(input), `must be a nonnegative integer`); 16 + 17 + const recordEventDataSchema = v.object({ 18 + did: didString, 19 + rev: tidString, 20 + collection: nsidString, 21 + rkey: rkeyString, 22 + action: v.union(v.literal('create'), v.literal('update'), v.literal('delete')), 23 + record: v.record(v.unknown()).optional(), 24 + cid: v.string().optional(), 25 + live: v.boolean(), 26 + }); 27 + 28 + const identityEventDataSchema = v.object({ 29 + did: didString, 30 + handle: handleString, 31 + is_active: v.boolean(), 32 + status: v.union( 33 + v.literal('active'), 34 + v.literal('takendown'), 35 + v.literal('suspended'), 36 + v.literal('deactivated'), 37 + v.literal('deleted'), 38 + ), 39 + }); 40 + 41 + export const tapRecordEventWireSchema = v.object({ 42 + id: integer, 43 + type: v.literal('record'), 44 + record: recordEventDataSchema, 45 + }); 46 + 47 + export const tapIdentityEventWireSchema = v.object({ 48 + id: integer, 49 + type: v.literal('identity'), 50 + identity: identityEventDataSchema, 51 + }); 52 + 53 + export const tapEventWireSchema = v.union(tapRecordEventWireSchema, tapIdentityEventWireSchema); 54 + 55 + export const repoInfoSchema: v.Type<t.RepoInfo> = v.object({ 56 + did: didString, 57 + handle: handleString, 58 + state: v.string(), 59 + rev: tidString, 60 + records: integer, 61 + error: v.string().optional(), 62 + retries: integer.optional(), 63 + }); 64 + 65 + export const flattenTapEvent = (wire: v.Infer<typeof tapEventWireSchema>): t.TapEvent => { 66 + switch (wire.type) { 67 + case 'identity': { 68 + return { 69 + id: wire.id, 70 + type: 'identity', 71 + 72 + did: wire.identity.did, 73 + handle: wire.identity.handle, 74 + isActive: wire.identity.is_active, 75 + status: wire.identity.status, 76 + }; 77 + } 78 + 79 + case 'record': { 80 + return { 81 + id: wire.id, 82 + type: 'record', 83 + live: wire.record.live, 84 + 85 + rev: wire.record.rev, 86 + did: wire.record.did, 87 + collection: wire.record.collection, 88 + rkey: wire.record.rkey, 89 + cid: wire.record.cid, 90 + action: wire.record.action, 91 + record: wire.record.record, 92 + }; 93 + } 94 + 95 + default: { 96 + wire satisfies never; 97 + 98 + const obj = wire as any; 99 + throw new Error(`unknown "${obj.type}" type`); 100 + } 101 + } 102 + };
+68
packages/clients/tap/lib/types.ts
··· 1 + import type { Did, Handle, Nsid, RecordKey, Tid } from '@atcute/lexicons/syntax'; 2 + import type { CloseEvent, ErrorEvent, Options } from 'partysocket/ws'; 3 + 4 + export type TapRecordAction = 'create' | 'update' | 'delete'; 5 + 6 + export type TapRepoStatus = 'active' | 'takendown' | 'suspended' | 'deactivated' | 'deleted'; 7 + 8 + export interface TapRecordEvent { 9 + id: number; 10 + type: 'record'; 11 + 12 + live: boolean; 13 + did: Did; 14 + rev: Tid; 15 + collection: Nsid; 16 + rkey: RecordKey; 17 + action: TapRecordAction; 18 + record?: Record<string, unknown>; 19 + cid?: string; 20 + } 21 + 22 + export interface TapIdentityEvent { 23 + id: number; 24 + type: 'identity'; 25 + 26 + did: Did; 27 + handle: Handle; 28 + isActive: boolean; 29 + status: TapRepoStatus; 30 + } 31 + 32 + export type TapEvent = TapRecordEvent | TapIdentityEvent; 33 + 34 + export interface RepoInfo { 35 + did: Did; 36 + handle: Handle; 37 + state: string; 38 + rev: Tid; 39 + records: number; 40 + error?: string; 41 + retries?: number; 42 + } 43 + 44 + export interface TapClientOptions { 45 + url: string | URL; 46 + adminPassword?: string; 47 + fetch?: typeof globalThis.fetch; 48 + } 49 + 50 + export interface TapSubscribeOptions { 51 + /** 52 + * whether to validate incoming events. 53 + * @default true 54 + */ 55 + validateEvents?: boolean; 56 + 57 + onConnectionOpen?: (event: Event) => void; 58 + onConnectionClose?: (event: CloseEvent) => void; 59 + onConnectionError?: (event: ErrorEvent) => void; 60 + onError?: (error: unknown) => void; 61 + 62 + ws?: Options; 63 + } 64 + 65 + export interface TapSubscriptionMessage { 66 + event: TapEvent; 67 + ack: () => Promise<void>; 68 + }
+6
packages/clients/tap/lib/utils.ts
··· 1 + import { toBase64Pad } from '@atcute/multibase'; 2 + import { encodeUtf8 } from '@atcute/uint8array'; 3 + 4 + export const formatAdminAuthHeader = (password: string): string => { 5 + return `Basic ${toBase64Pad(encodeUtf8(`admin:${password}`))}`; 6 + };
+41
packages/clients/tap/package.json
··· 1 + { 2 + "type": "module", 3 + "name": "@atcute/tap", 4 + "version": "0.1.0", 5 + "description": "an atproto Tap client", 6 + "license": "0BSD", 7 + "repository": { 8 + "url": "https://github.com/mary-ext/atcute", 9 + "directory": "packages/clients/tap" 10 + }, 11 + "files": [ 12 + "dist/", 13 + "lib/", 14 + "!lib/**/*.bench.ts", 15 + "!lib/**/*.test.ts" 16 + ], 17 + "exports": { 18 + ".": "./dist/index.js" 19 + }, 20 + "scripts": { 21 + "build": "tsgo --project tsconfig.build.json", 22 + "test": "vitest run", 23 + "prepublish": "rm -rf dist; pnpm run build" 24 + }, 25 + "dependencies": { 26 + "@atcute/multibase": "workspace:^", 27 + "@atcute/uint8array": "workspace:^", 28 + "@atcute/identity": "workspace:^", 29 + "@atcute/lexicons": "workspace:^", 30 + "@badrap/valita": "^0.4.6", 31 + "@mary-ext/event-iterator": "^1.0.0", 32 + "@mary-ext/simple-event-emitter": "^1.0.0", 33 + "partysocket": "^1.1.6", 34 + "type-fest": "^4.41.0" 35 + }, 36 + "devDependencies": { 37 + "@types/ws": "^8.18.1", 38 + "vitest": "^4.0.14", 39 + "ws": "^8.18.3" 40 + } 41 + }
+5
packages/clients/tap/tsconfig.build.json
··· 1 + { 2 + "extends": "./tsconfig.json", 3 + "exclude": ["**/*.test.ts"] 4 + } 5 +
+25
packages/clients/tap/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "outDir": "dist/", 4 + "esModuleInterop": true, 5 + "skipLibCheck": true, 6 + "target": "ESNext", 7 + "allowJs": true, 8 + "resolveJsonModule": true, 9 + "moduleDetection": "force", 10 + "isolatedModules": true, 11 + "verbatimModuleSyntax": true, 12 + "strict": true, 13 + "noImplicitOverride": true, 14 + "noUnusedLocals": true, 15 + "noUnusedParameters": true, 16 + "useDefineForClassFields": false, 17 + "noFallthroughCasesInSwitch": true, 18 + "module": "NodeNext", 19 + "sourceMap": true, 20 + "declaration": true, 21 + "declarationMap": true 22 + }, 23 + "include": ["lib"] 24 + } 25 +
+7
packages/clients/tap/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config'; 2 + 3 + export default defineConfig({ 4 + test: { 5 + include: ['lib/**/*.test.ts'], 6 + }, 7 + });
+85
pnpm-lock.yaml
··· 206 206 specifier: ^4.41.0 207 207 version: 4.41.0 208 208 209 + packages/clients/tap: 210 + dependencies: 211 + '@atcute/identity': 212 + specifier: workspace:^ 213 + version: link:../../identity/identity 214 + '@atcute/lexicons': 215 + specifier: workspace:^ 216 + version: link:../../lexicons/lexicons 217 + '@atcute/multibase': 218 + specifier: workspace:^ 219 + version: link:../../utilities/multibase 220 + '@atcute/uint8array': 221 + specifier: workspace:^ 222 + version: link:../../misc/uint8array 223 + '@badrap/valita': 224 + specifier: ^0.4.6 225 + version: 0.4.6 226 + '@mary-ext/event-iterator': 227 + specifier: ^1.0.0 228 + version: 1.0.0 229 + '@mary-ext/simple-event-emitter': 230 + specifier: ^1.0.0 231 + version: 1.0.0 232 + partysocket: 233 + specifier: ^1.1.6 234 + version: 1.1.6 235 + type-fest: 236 + specifier: ^4.41.0 237 + version: 4.41.0 238 + devDependencies: 239 + '@types/ws': 240 + specifier: ^8.18.1 241 + version: 8.18.1 242 + vitest: 243 + specifier: ^4.0.14 244 + version: 4.0.15(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.0) 245 + ws: 246 + specifier: ^8.18.3 247 + version: 8.18.3 248 + 209 249 packages/definitions/atproto: 210 250 dependencies: 211 251 '@atcute/lexicons': ··· 6159 6199 optionalDependencies: 6160 6200 vite: 7.2.4(@types/node@22.19.1)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.0) 6161 6201 6202 + '@vitest/mocker@4.0.15(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.0))': 6203 + dependencies: 6204 + '@vitest/spy': 4.0.15 6205 + estree-walker: 3.0.3 6206 + magic-string: 0.30.21 6207 + optionalDependencies: 6208 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.0) 6209 + 6162 6210 '@vitest/pretty-format@4.0.14': 6163 6211 dependencies: 6164 6212 tinyrainbow: 3.0.3 ··· 7873 7921 why-is-node-running: 2.3.0 7874 7922 optionalDependencies: 7875 7923 '@types/node': 22.19.1 7924 + transitivePeerDependencies: 7925 + - jiti 7926 + - less 7927 + - lightningcss 7928 + - msw 7929 + - sass 7930 + - sass-embedded 7931 + - stylus 7932 + - sugarss 7933 + - terser 7934 + - tsx 7935 + - yaml 7936 + 7937 + vitest@4.0.15(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.0): 7938 + dependencies: 7939 + '@vitest/expect': 4.0.15 7940 + '@vitest/mocker': 4.0.15(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.0)) 7941 + '@vitest/pretty-format': 4.0.15 7942 + '@vitest/runner': 4.0.15 7943 + '@vitest/snapshot': 4.0.15 7944 + '@vitest/spy': 4.0.15 7945 + '@vitest/utils': 4.0.15 7946 + es-module-lexer: 1.7.0 7947 + expect-type: 1.2.2 7948 + magic-string: 0.30.21 7949 + obug: 2.1.1 7950 + pathe: 2.0.3 7951 + picomatch: 4.0.3 7952 + std-env: 3.10.0 7953 + tinybench: 2.9.0 7954 + tinyexec: 1.0.2 7955 + tinyglobby: 0.2.15 7956 + tinyrainbow: 3.0.3 7957 + vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(tsx@4.20.6)(yaml@2.8.0) 7958 + why-is-node-running: 2.3.0 7959 + optionalDependencies: 7960 + '@types/node': 24.10.1 7876 7961 transitivePeerDependencies: 7877 7962 - jiti 7878 7963 - less