@atproto/tap 0.0.2

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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # @atproto/tap
2
+
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - [#4290](https://github.com/bluesky-social/atproto/pull/4290) [`b4a76ba`](https://github.com/bluesky-social/atproto/commit/b4a76bae7bef1189302488d43ce49a03fd61f957) Thanks [@dholms](https://github.com/dholms)! - Initial version of package
8
+
9
+ - Updated dependencies [[`b4a76ba`](https://github.com/bluesky-social/atproto/commit/b4a76bae7bef1189302488d43ce49a03fd61f957)]:
10
+ - @atproto/ws-client@0.0.4
package/LICENSE.txt ADDED
@@ -0,0 +1,7 @@
1
+ Dual MIT/Apache-2.0 License
2
+
3
+ Copyright (c) 2022-2025 Bluesky Social PBC, and Contributors
4
+
5
+ Except as otherwise noted in individual files, this software is licensed under the MIT license (<http://opensource.org/licenses/MIT>), or the Apache License, Version 2.0 (<http://www.apache.org/licenses/LICENSE-2.0>).
6
+
7
+ Downstream projects and end users may chose either license individually, or both together, at their discretion. The motivation for this dual-licensing is the additional software patent assurance provided by Apache 2.0.
package/README.md ADDED
@@ -0,0 +1,221 @@
1
+ # @atproto/tap
2
+
3
+ TypeScript client library for [Tap](https://github.com/bluesky-social/indigo/tree/main/cmd/tap/README.md), a sync utility for the AT Protocol network.
4
+
5
+ Tap handles firehose connections, cryptographic verification, backfill, and filtering. This client library lets you connect to a Tap instance and receive simple JSON events for the repos you care about.
6
+
7
+ [![NPM](https://img.shields.io/npm/v/@atproto/tap)](https://www.npmjs.com/package/@atproto/tap)
8
+ [![Github CI Status](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml/badge.svg)](https://github.com/bluesky-social/atproto/actions/workflows/repo.yaml)
9
+
10
+ ## Quick Start
11
+
12
+ ```bash
13
+ npm install @atproto/tap
14
+ ```
15
+
16
+ ```ts
17
+ import { Tap, SimpleIndexer } from '@atproto/tap'
18
+
19
+ const tap = new Tap('http://localhost:2480', { adminPassword: 'secret' })
20
+
21
+ const indexer = new SimpleIndexer()
22
+
23
+ indexer.identity(async (evt) => {
24
+ console.log(`${evt.did} updated identity: ${evt.handle} (${evt.status})`)
25
+ })
26
+
27
+ indexer.record(async (evt) => {
28
+ const uri = `at://${evt.did}/${evt.collection}/${evt.rkey}`
29
+ if (evt.action === 'create' || evt.action === 'update') {
30
+ console.log(`${evt.action}: ${uri}`)
31
+ } else {
32
+ console.log(`deleted: ${uri}`)
33
+ }
34
+ })
35
+
36
+ indexer.error((err) => console.error(err))
37
+
38
+ const channel = tap.channel(indexer)
39
+ channel.start()
40
+
41
+ await tap.addRepos(['did:plc:ewvi7nxzyoun6zhxrhs64oiz'])
42
+
43
+ // On shutdown
44
+ await channel.destroy()
45
+ ```
46
+
47
+ ## Running Tap
48
+
49
+ See the [Tap README](https://github.com/bluesky-social/indigo/tree/main/cmd/tap/README.md) for details getting Tap up and running. Your app can communicate with it either locally or over the internet.
50
+
51
+ This library is intended to be used with Tap running in the default mode of "WebScoket with acks". In this mode, Tap provides:
52
+
53
+ - **At-least-once delivery**: Events may be redelivered if the connection drops before an ack is received
54
+ - **Per-repo ordering**: Events for the same repo are delivered in order
55
+ - **Backfill**: When you add a repo, historical events are delivered before live events
56
+
57
+ ## API
58
+
59
+ ### `Tap`
60
+
61
+ The main client for interacting with a Tap server.
62
+
63
+ ```ts
64
+ const tap = new Tap(url: string, config?: TapConfig)
65
+ ```
66
+
67
+ **Config options:**
68
+
69
+ - `adminPassword?: string` - Password for Basic auth (required if Tap server has auth enabled)
70
+
71
+ **Methods:**
72
+
73
+ - `channel(handler: TapHandler, opts?: TapWebsocketOptions): TapChannel` - Create a WebSocket channel to receive events
74
+ - `addRepos(dids: string[]): Promise<void>` - Add repos to track (triggers backfill)
75
+ - `removeRepos(dids: string[]): Promise<void>` - Stop tracking repos
76
+ - `resolveDid(did: string): Promise<DidDocument | null>` - Resolve a DID to its DID document
77
+ - `getRepoInfo(did: string): Promise<RepoInfo>` - Get info about a tracked repo
78
+
79
+ ### `TapChannel`
80
+
81
+ WebSocket connection for receiving events. Created via `tap.channel()`.
82
+
83
+ ```ts
84
+ const channel = tap.channel(handler, opts?)
85
+ ```
86
+
87
+ **Methods:**
88
+
89
+ - `start(): Promise<void>` - Start receiving events. Returns a promise that resolves when the connection is destroyed or errors.
90
+ - `destroy(): Promise<void>` - Close the connection
91
+
92
+ The channel automatically handles reconnection and keepalive. Events are automatically acknowledged after your handler completes successfully.
93
+
94
+ ### `SimpleIndexer`
95
+
96
+ A convenience class for handling events by type. Passed into `tap.channel()` when opening a channel with Tap.
97
+
98
+ ```ts
99
+ const indexer = new SimpleIndexer()
100
+
101
+ indexer.identity(async (evt: IdentityEvent) => { ... })
102
+ indexer.record(async (evt: RecordEvent) => { ... })
103
+ indexer.error((err: Error) => { ... })
104
+ ```
105
+
106
+ If no error handler is registered, errors will throw as unhandled exceptions.
107
+
108
+ ### `TapHandler`
109
+
110
+ You can create your own custom handler by creating a class that implements the `TapHandler` interface:
111
+
112
+ ```ts
113
+ interface TapHandler {
114
+ onEvent: (evt: TapEvent, opts: HandlerOpts) => void | Promise<void>
115
+ onError: (err: Error) => void
116
+ }
117
+
118
+ interface HandlerOpts {
119
+ signal: AbortSignal
120
+ ack: () => Promise<void>
121
+ }
122
+ ```
123
+
124
+ When implementing a custom handler, be sure to call `ack()` when you're done processing the event.
125
+
126
+ ## Event Types
127
+
128
+ ### `RecordEvent`
129
+
130
+ ```ts
131
+ type RecordEvent = {
132
+ id: number
133
+ type: 'record'
134
+ action: 'create' | 'update' | 'delete'
135
+ did: string
136
+ rev: string
137
+ collection: string
138
+ rkey: string
139
+ record?: Record<string, unknown> // present for create/update
140
+ cid?: string // present for create/update
141
+ live: boolean // true if from firehose, false if from backfill
142
+ }
143
+ ```
144
+
145
+ ### `IdentityEvent`
146
+
147
+ ```ts
148
+ type IdentityEvent = {
149
+ id: number
150
+ type: 'identity'
151
+ did: string
152
+ handle: string
153
+ isActive: boolean
154
+ status: 'active' | 'takendown' | 'suspended' | 'deactivated' | 'deleted'
155
+ }
156
+ ```
157
+
158
+ ## Webhook Mode
159
+
160
+ If your Tap server is configured for webhook delivery, you can use `parseTapEvent` to validate incoming webhook payloads:
161
+
162
+ ```ts
163
+ import express from 'express'
164
+ import { parseTapEvent, assureAdminAuth } from '@atproto/tap'
165
+
166
+ const ADMIN_PASSWORD = process.env.ADMIN_PASSWORD
167
+
168
+ const app = express()
169
+ app.use(express.json())
170
+
171
+ app.post('/webhook', async (req, res) => {
172
+ try {
173
+ assureAdminAuth(ADMIN_PASSWORD, req.headers.authorization)
174
+ } catch {
175
+ return res.status(401).json({ error: 'Unauthorized' })
176
+ }
177
+
178
+ try {
179
+ const evt = parseTapEvent(req.body)
180
+ // handle event...
181
+ res.sendStatus(200)
182
+ } catch (err) {
183
+ console.error('Failed to process event:', err)
184
+ res.status(500).json({ error: 'Failed to process event' })
185
+ }
186
+ })
187
+ ```
188
+
189
+ ## Utilities
190
+
191
+ ### Auth helpers
192
+
193
+ ```ts
194
+ import {
195
+ formatAdminAuthHeader,
196
+ parseAdminAuthHeader,
197
+ assureAdminAuth,
198
+ } from '@atproto/tap'
199
+
200
+ // Format a password into a Basic auth header value
201
+ const header = formatAdminAuthHeader('secret')
202
+ // => 'Basic YWRtaW46c2VjcmV0'
203
+
204
+ // Parse an auth header to extract the password (throws if invalid)
205
+ const password = parseAdminAuthHeader(header)
206
+
207
+ // Verify auth header matches expected password (timing-safe, throws if invalid)
208
+ assureAdminAuth('secret', req.headers.authorization)
209
+ ```
210
+
211
+ ### Event parsing
212
+
213
+ ```ts
214
+ import { parseTapEvent } from '@atproto/tap'
215
+
216
+ const evt = parseTapEvent(jsonData) // validates and returns typed TapEvent
217
+ ```
218
+
219
+ ## License
220
+
221
+ MIT
@@ -0,0 +1,32 @@
1
+ import { ClientOptions } from 'ws';
2
+ import { TapEvent } from './types';
3
+ export interface HandlerOpts {
4
+ signal: AbortSignal;
5
+ ack: () => Promise<void>;
6
+ }
7
+ export interface TapHandler {
8
+ onEvent: (evt: TapEvent, opts: HandlerOpts) => void | Promise<void>;
9
+ onError: (err: Error) => void;
10
+ }
11
+ export type TapWebsocketOptions = ClientOptions & {
12
+ adminPassword?: string;
13
+ maxReconnectSeconds?: number;
14
+ heartbeatIntervalMs?: number;
15
+ onReconnectError?: (error: unknown, n: number, initialSetup: boolean) => void;
16
+ };
17
+ export declare class TapChannel {
18
+ private ws;
19
+ private handler;
20
+ private readonly abortController;
21
+ private readonly destroyDefer;
22
+ private bufferedAcks;
23
+ constructor(url: string, handler: TapHandler, wsOpts?: TapWebsocketOptions);
24
+ ackEvent(id: number): Promise<void>;
25
+ private sendAck;
26
+ private bufferAndSendAck;
27
+ private flushBufferedAcks;
28
+ start(): Promise<void>;
29
+ private processWsEvent;
30
+ destroy(): Promise<void>;
31
+ }
32
+ //# sourceMappingURL=channel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,IAAI,CAAA;AAGlC,OAAO,EAAE,QAAQ,EAAiB,MAAM,SAAS,CAAA;AAGjD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,WAAW,CAAA;IACnB,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CACzB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnE,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;CAC9B;AAED,MAAM,MAAM,mBAAmB,GAAG,aAAa,GAAG;IAChD,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAC5B,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,KAAK,IAAI,CAAA;CAC9E,CAAA;AAOD,qBAAa,UAAU;IACrB,OAAO,CAAC,EAAE,CAAoB;IAC9B,OAAO,CAAC,OAAO,CAAY;IAE3B,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAyC;IACzE,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAiC;IAE9D,OAAO,CAAC,YAAY,CAAoB;gBAGtC,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,UAAU,EACnB,MAAM,GAAE,mBAAwB;IAoB5B,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAY3B,OAAO;YAKP,gBAAgB;YAShB,iBAAiB;IAqBzB,KAAK;YAcG,cAAc;IA0BtB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAI/B"}
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TapChannel = void 0;
4
+ const common_1 = require("@atproto/common");
5
+ const ws_client_1 = require("@atproto/ws-client");
6
+ const types_1 = require("./types");
7
+ const util_1 = require("./util");
8
+ class TapChannel {
9
+ constructor(url, handler, wsOpts = {}) {
10
+ Object.defineProperty(this, "ws", {
11
+ enumerable: true,
12
+ configurable: true,
13
+ writable: true,
14
+ value: void 0
15
+ });
16
+ Object.defineProperty(this, "handler", {
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true,
20
+ value: void 0
21
+ });
22
+ Object.defineProperty(this, "abortController", {
23
+ enumerable: true,
24
+ configurable: true,
25
+ writable: true,
26
+ value: new AbortController()
27
+ });
28
+ Object.defineProperty(this, "destroyDefer", {
29
+ enumerable: true,
30
+ configurable: true,
31
+ writable: true,
32
+ value: (0, common_1.createDeferrable)()
33
+ });
34
+ Object.defineProperty(this, "bufferedAcks", {
35
+ enumerable: true,
36
+ configurable: true,
37
+ writable: true,
38
+ value: []
39
+ });
40
+ this.handler = handler;
41
+ const { adminPassword, ...rest } = wsOpts;
42
+ let headers = rest.headers;
43
+ if (adminPassword) {
44
+ headers ?? (headers = {});
45
+ headers['Authorization'] = (0, util_1.formatAdminAuthHeader)(adminPassword);
46
+ }
47
+ this.ws = new ws_client_1.WebSocketKeepAlive({
48
+ getUrl: async () => url,
49
+ onReconnect: () => {
50
+ this.flushBufferedAcks();
51
+ },
52
+ signal: this.abortController.signal,
53
+ ...rest,
54
+ headers,
55
+ });
56
+ }
57
+ async ackEvent(id) {
58
+ if (this.ws.isConnected()) {
59
+ try {
60
+ await this.sendAck(id);
61
+ }
62
+ catch {
63
+ await this.bufferAndSendAck(id);
64
+ }
65
+ }
66
+ else {
67
+ await this.bufferAndSendAck(id);
68
+ }
69
+ }
70
+ async sendAck(id) {
71
+ await this.ws.send(JSON.stringify({ type: 'ack', id }));
72
+ }
73
+ // resolves after the ack has been actually sent
74
+ async bufferAndSendAck(id) {
75
+ const defer = (0, common_1.createDeferrable)();
76
+ this.bufferedAcks.push({
77
+ id,
78
+ defer,
79
+ });
80
+ await defer.complete;
81
+ }
82
+ async flushBufferedAcks() {
83
+ while (this.bufferedAcks.length > 0) {
84
+ try {
85
+ const ack = this.bufferedAcks.at(0);
86
+ if (!ack) {
87
+ return;
88
+ }
89
+ await this.sendAck(ack.id);
90
+ ack.defer.resolve();
91
+ this.bufferedAcks = this.bufferedAcks.slice(1);
92
+ }
93
+ catch (err) {
94
+ this.handler.onError(new Error(`failed to send ack for event ${this.bufferedAcks[0]}`, {
95
+ cause: err,
96
+ }));
97
+ return;
98
+ }
99
+ }
100
+ }
101
+ async start() {
102
+ try {
103
+ for await (const chunk of this.ws) {
104
+ await this.processWsEvent(chunk);
105
+ }
106
+ }
107
+ catch (err) {
108
+ if ((0, common_1.isErrnoException)(err) && err.name === 'AbortError') {
109
+ this.destroyDefer.resolve();
110
+ }
111
+ else {
112
+ throw err;
113
+ }
114
+ }
115
+ }
116
+ async processWsEvent(chunk) {
117
+ let evt;
118
+ try {
119
+ const data = chunk.toString();
120
+ evt = (0, types_1.parseTapEvent)(JSON.parse(data));
121
+ }
122
+ catch (err) {
123
+ this.handler.onError(new Error('Failed to parse message', { cause: err }));
124
+ return;
125
+ }
126
+ try {
127
+ await this.handler.onEvent(evt, {
128
+ signal: this.abortController.signal,
129
+ ack: async () => {
130
+ await this.ackEvent(evt.id);
131
+ },
132
+ });
133
+ }
134
+ catch (err) {
135
+ // Don't ack on error - let Tap retry
136
+ this.handler.onError(new Error(`Failed to process event ${evt.id}`, { cause: err }));
137
+ return;
138
+ }
139
+ }
140
+ async destroy() {
141
+ this.abortController.abort();
142
+ await this.destroyDefer.complete;
143
+ }
144
+ }
145
+ exports.TapChannel = TapChannel;
146
+ //# sourceMappingURL=channel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel.js","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"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","sourcesContent":["import { ClientOptions } from 'ws'\nimport { Deferrable, createDeferrable, isErrnoException } from '@atproto/common'\nimport { WebSocketKeepAlive } from '@atproto/ws-client'\nimport { TapEvent, parseTapEvent } from './types'\nimport { formatAdminAuthHeader } from './util'\n\nexport interface HandlerOpts {\n signal: AbortSignal\n ack: () => Promise<void>\n}\n\nexport interface TapHandler {\n onEvent: (evt: TapEvent, opts: HandlerOpts) => void | Promise<void>\n onError: (err: Error) => void\n}\n\nexport type TapWebsocketOptions = ClientOptions & {\n adminPassword?: string\n maxReconnectSeconds?: number\n heartbeatIntervalMs?: number\n onReconnectError?: (error: unknown, n: number, initialSetup: boolean) => void\n}\n\ntype BufferedAck = {\n id: number\n defer: Deferrable\n}\n\nexport class TapChannel {\n private ws: WebSocketKeepAlive\n private handler: TapHandler\n\n private readonly abortController: AbortController = new AbortController()\n private readonly destroyDefer: Deferrable = createDeferrable()\n\n private bufferedAcks: BufferedAck[] = []\n\n constructor(\n url: string,\n handler: TapHandler,\n wsOpts: TapWebsocketOptions = {},\n ) {\n this.handler = handler\n const { adminPassword, ...rest } = wsOpts\n let headers = rest.headers\n if (adminPassword) {\n headers ??= {}\n headers['Authorization'] = formatAdminAuthHeader(adminPassword)\n }\n this.ws = new WebSocketKeepAlive({\n getUrl: async () => url,\n onReconnect: () => {\n this.flushBufferedAcks()\n },\n signal: this.abortController.signal,\n ...rest,\n headers,\n })\n }\n\n async ackEvent(id: number): Promise<void> {\n if (this.ws.isConnected()) {\n try {\n await this.sendAck(id)\n } catch {\n await this.bufferAndSendAck(id)\n }\n } else {\n await this.bufferAndSendAck(id)\n }\n }\n\n private async sendAck(id: number): Promise<void> {\n await this.ws.send(JSON.stringify({ type: 'ack', id }))\n }\n\n // resolves after the ack has been actually sent\n private async bufferAndSendAck(id: number): Promise<void> {\n const defer = createDeferrable()\n this.bufferedAcks.push({\n id,\n defer,\n })\n await defer.complete\n }\n\n private async flushBufferedAcks(): Promise<void> {\n while (this.bufferedAcks.length > 0) {\n try {\n const ack = this.bufferedAcks.at(0)\n if (!ack) {\n return\n }\n await this.sendAck(ack.id)\n ack.defer.resolve()\n this.bufferedAcks = this.bufferedAcks.slice(1)\n } catch (err) {\n this.handler.onError(\n new Error(`failed to send ack for event ${this.bufferedAcks[0]}`, {\n cause: err,\n }),\n )\n return\n }\n }\n }\n\n async start() {\n try {\n for await (const chunk of this.ws) {\n await this.processWsEvent(chunk)\n }\n } catch (err) {\n if (isErrnoException(err) && err.name === 'AbortError') {\n this.destroyDefer.resolve()\n } else {\n throw err\n }\n }\n }\n\n private async processWsEvent(chunk: Uint8Array) {\n let evt: TapEvent\n try {\n const data = chunk.toString()\n evt = parseTapEvent(JSON.parse(data))\n } catch (err) {\n this.handler.onError(new Error('Failed to parse message', { cause: err }))\n return\n }\n\n try {\n await this.handler.onEvent(evt, {\n signal: this.abortController.signal,\n ack: async () => {\n await this.ackEvent(evt.id)\n },\n })\n } catch (err) {\n // Don't ack on error - let Tap retry\n this.handler.onError(\n new Error(`Failed to process event ${evt.id}`, { cause: err }),\n )\n return\n }\n }\n\n async destroy(): Promise<void> {\n this.abortController.abort()\n await this.destroyDefer.complete\n }\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import { DidDocument } from '@atproto/common';
2
+ import { TapChannel, TapHandler, TapWebsocketOptions } from './channel';
3
+ import { RepoInfo } from './types';
4
+ export interface TapConfig {
5
+ adminPassword?: string;
6
+ }
7
+ export declare class Tap {
8
+ url: string;
9
+ private adminPassword?;
10
+ private authHeader?;
11
+ constructor(url: string, config?: TapConfig);
12
+ private getHeaders;
13
+ channel(handler: TapHandler, opts?: TapWebsocketOptions): TapChannel;
14
+ addRepos(dids: string[]): Promise<void>;
15
+ removeRepos(dids: string[]): Promise<void>;
16
+ resolveDid(did: string): Promise<DidDocument | null>;
17
+ getRepoInfo(did: string): Promise<RepoInfo>;
18
+ }
19
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAe,MAAM,iBAAiB,CAAA;AAC1D,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AACvE,OAAO,EAAE,QAAQ,EAAkB,MAAM,SAAS,CAAA;AAGlD,MAAM,WAAW,SAAS;IACxB,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAED,qBAAa,GAAG;IACd,GAAG,EAAE,MAAM,CAAA;IACX,OAAO,CAAC,aAAa,CAAC,CAAQ;IAC9B,OAAO,CAAC,UAAU,CAAC,CAAQ;gBAEf,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,SAAc;IAW/C,OAAO,CAAC,UAAU;IAUlB,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,IAAI,CAAC,EAAE,mBAAmB,GAAG,UAAU;IAU9D,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAavC,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,IAAI,CAAC;IAa1C,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC;IAepD,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;CAalD"}
package/dist/client.js ADDED
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Tap = void 0;
4
+ const common_1 = require("@atproto/common");
5
+ const channel_1 = require("./channel");
6
+ const types_1 = require("./types");
7
+ const util_1 = require("./util");
8
+ class Tap {
9
+ constructor(url, config = {}) {
10
+ Object.defineProperty(this, "url", {
11
+ enumerable: true,
12
+ configurable: true,
13
+ writable: true,
14
+ value: void 0
15
+ });
16
+ Object.defineProperty(this, "adminPassword", {
17
+ enumerable: true,
18
+ configurable: true,
19
+ writable: true,
20
+ value: void 0
21
+ });
22
+ Object.defineProperty(this, "authHeader", {
23
+ enumerable: true,
24
+ configurable: true,
25
+ writable: true,
26
+ value: void 0
27
+ });
28
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
29
+ throw new Error('Invalid URL, expected http:// or https://');
30
+ }
31
+ this.url = url;
32
+ this.adminPassword = config.adminPassword;
33
+ if (this.adminPassword) {
34
+ this.authHeader = (0, util_1.formatAdminAuthHeader)(this.adminPassword);
35
+ }
36
+ }
37
+ getHeaders() {
38
+ const headers = {
39
+ 'Content-Type': 'application/json',
40
+ };
41
+ if (this.authHeader) {
42
+ headers['Authorization'] = this.authHeader;
43
+ }
44
+ return headers;
45
+ }
46
+ channel(handler, opts) {
47
+ const url = new URL(this.url);
48
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
49
+ url.pathname = '/channel';
50
+ return new channel_1.TapChannel(url.toString(), handler, {
51
+ adminPassword: this.adminPassword,
52
+ ...opts,
53
+ });
54
+ }
55
+ async addRepos(dids) {
56
+ const response = await fetch(`${this.url}/repos/add`, {
57
+ method: 'POST',
58
+ headers: this.getHeaders(),
59
+ body: JSON.stringify({ dids }),
60
+ });
61
+ await response.body?.cancel(); // expect empty body
62
+ if (!response.ok) {
63
+ throw new Error(`Failed to add repos: ${response.statusText}`);
64
+ }
65
+ }
66
+ async removeRepos(dids) {
67
+ const response = await fetch(`${this.url}/repos/remove`, {
68
+ method: 'POST',
69
+ headers: this.getHeaders(),
70
+ body: JSON.stringify({ dids }),
71
+ });
72
+ await response.body?.cancel(); // expect empty body
73
+ if (!response.ok) {
74
+ throw new Error(`Failed to remove repos: ${response.statusText}`);
75
+ }
76
+ }
77
+ async resolveDid(did) {
78
+ const response = await fetch(`${this.url}/resolve/${did}`, {
79
+ method: 'GET',
80
+ headers: this.getHeaders(),
81
+ });
82
+ if (response.status === 404) {
83
+ return null;
84
+ }
85
+ else if (!response.ok) {
86
+ await response.body?.cancel();
87
+ throw new Error(`Failed to resolve DID: ${response.statusText}`);
88
+ }
89
+ return common_1.didDocument.parse(await response.json());
90
+ }
91
+ async getRepoInfo(did) {
92
+ const response = await fetch(`${this.url}/info/${did}`, {
93
+ method: 'GET',
94
+ headers: this.getHeaders(),
95
+ });
96
+ if (!response.ok) {
97
+ await response.body?.cancel();
98
+ throw new Error(`Failed to get repo info: ${response.statusText}`);
99
+ }
100
+ return types_1.repoInfoSchema.parse(await response.json());
101
+ }
102
+ }
103
+ exports.Tap = Tap;
104
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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","sourcesContent":["import { DidDocument, didDocument } from '@atproto/common'\nimport { TapChannel, TapHandler, TapWebsocketOptions } from './channel'\nimport { RepoInfo, repoInfoSchema } from './types'\nimport { formatAdminAuthHeader } from './util'\n\nexport interface TapConfig {\n adminPassword?: string\n}\n\nexport class Tap {\n url: string\n private adminPassword?: string\n private authHeader?: string\n\n constructor(url: string, config: TapConfig = {}) {\n if (!url.startsWith('http://') && !url.startsWith('https://')) {\n throw new Error('Invalid URL, expected http:// or https://')\n }\n this.url = url\n this.adminPassword = config.adminPassword\n if (this.adminPassword) {\n this.authHeader = formatAdminAuthHeader(this.adminPassword)\n }\n }\n\n private getHeaders(): Record<string, string> {\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n }\n if (this.authHeader) {\n headers['Authorization'] = this.authHeader\n }\n return headers\n }\n\n channel(handler: TapHandler, opts?: TapWebsocketOptions): TapChannel {\n const url = new URL(this.url)\n url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'\n url.pathname = '/channel'\n return new TapChannel(url.toString(), handler, {\n adminPassword: this.adminPassword,\n ...opts,\n })\n }\n\n async addRepos(dids: string[]): Promise<void> {\n const response = await fetch(`${this.url}/repos/add`, {\n method: 'POST',\n headers: this.getHeaders(),\n body: JSON.stringify({ dids }),\n })\n await response.body?.cancel() // expect empty body\n\n if (!response.ok) {\n throw new Error(`Failed to add repos: ${response.statusText}`)\n }\n }\n\n async removeRepos(dids: string[]): Promise<void> {\n const response = await fetch(`${this.url}/repos/remove`, {\n method: 'POST',\n headers: this.getHeaders(),\n body: JSON.stringify({ dids }),\n })\n await response.body?.cancel() // expect empty body\n\n if (!response.ok) {\n throw new Error(`Failed to remove repos: ${response.statusText}`)\n }\n }\n\n async resolveDid(did: string): Promise<DidDocument | null> {\n const response = await fetch(`${this.url}/resolve/${did}`, {\n method: 'GET',\n headers: this.getHeaders(),\n })\n\n if (response.status === 404) {\n return null\n } else if (!response.ok) {\n await response.body?.cancel()\n throw new Error(`Failed to resolve DID: ${response.statusText}`)\n }\n return didDocument.parse(await response.json())\n }\n\n async getRepoInfo(did: string): Promise<RepoInfo> {\n const response = await fetch(`${this.url}/info/${did}`, {\n method: 'GET',\n headers: this.getHeaders(),\n })\n\n if (!response.ok) {\n await response.body?.cancel()\n throw new Error(`Failed to get repo info: ${response.statusText}`)\n }\n\n return repoInfoSchema.parse(await response.json())\n }\n}\n"]}
@@ -0,0 +1,6 @@
1
+ export * from './types';
2
+ export * from './client';
3
+ export * from './channel';
4
+ export * from './simple-indexer';
5
+ export * from './util';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAA;AACvB,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,kBAAkB,CAAA;AAChC,cAAc,QAAQ,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./types"), exports);
18
+ __exportStar(require("./client"), exports);
19
+ __exportStar(require("./channel"), exports);
20
+ __exportStar(require("./simple-indexer"), exports);
21
+ __exportStar(require("./util"), exports);
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,0CAAuB;AACvB,2CAAwB;AACxB,4CAAyB;AACzB,mDAAgC;AAChC,yCAAsB","sourcesContent":["export * from './types'\nexport * from './client'\nexport * from './channel'\nexport * from './simple-indexer'\nexport * from './util'\n"]}
@@ -0,0 +1,17 @@
1
+ import { HandlerOpts, TapHandler } from './channel';
2
+ import { IdentityEvent, RecordEvent, TapEvent } from './types';
3
+ type IdentityEventHandler = (evt: IdentityEvent, opts?: HandlerOpts) => Promise<void>;
4
+ type RecordEventHandler = (evt: RecordEvent, opts?: HandlerOpts) => Promise<void>;
5
+ type ErrorHandler = (err: Error) => void;
6
+ export declare class SimpleIndexer implements TapHandler {
7
+ private identityHandler;
8
+ private recordHandler;
9
+ private errorHandler;
10
+ identity(fn: IdentityEventHandler): void;
11
+ record(fn: RecordEventHandler): void;
12
+ error(fn: ErrorHandler): void;
13
+ onEvent(evt: TapEvent, opts: HandlerOpts): Promise<void>;
14
+ onError(err: Error): void;
15
+ }
16
+ export {};
17
+ //# sourceMappingURL=simple-indexer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"simple-indexer.d.ts","sourceRoot":"","sources":["../src/simple-indexer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,WAAW,CAAA;AACnD,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAE9D,KAAK,oBAAoB,GAAG,CAC1B,GAAG,EAAE,aAAa,EAClB,IAAI,CAAC,EAAE,WAAW,KACf,OAAO,CAAC,IAAI,CAAC,CAAA;AAClB,KAAK,kBAAkB,GAAG,CACxB,GAAG,EAAE,WAAW,EAChB,IAAI,CAAC,EAAE,WAAW,KACf,OAAO,CAAC,IAAI,CAAC,CAAA;AAClB,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,KAAK,KAAK,IAAI,CAAA;AAExC,qBAAa,aAAc,YAAW,UAAU;IAC9C,OAAO,CAAC,eAAe,CAAkC;IACzD,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,YAAY,CAA0B;IAE9C,QAAQ,CAAC,EAAE,EAAE,oBAAoB;IAIjC,MAAM,CAAC,EAAE,EAAE,kBAAkB;IAI7B,KAAK,CAAC,EAAE,EAAE,YAAY;IAIhB,OAAO,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAS9D,OAAO,CAAC,GAAG,EAAE,KAAK;CAOnB"}