@atcute/xrpc-server 0.1.12 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +81 -20
  2. package/dist/auth/jwt-creator.d.ts +4 -2
  3. package/dist/auth/jwt-creator.d.ts.map +1 -1
  4. package/dist/auth/jwt-creator.js +1 -1
  5. package/dist/auth/jwt-creator.js.map +1 -1
  6. package/dist/auth/jwt-verifier.d.ts +69 -8
  7. package/dist/auth/jwt-verifier.d.ts.map +1 -1
  8. package/dist/auth/jwt-verifier.js +130 -19
  9. package/dist/auth/jwt-verifier.js.map +1 -1
  10. package/dist/auth/jwt.d.ts +7 -2
  11. package/dist/auth/jwt.d.ts.map +1 -1
  12. package/dist/auth/jwt.js +25 -25
  13. package/dist/auth/jwt.js.map +1 -1
  14. package/dist/main/router.d.ts +32 -7
  15. package/dist/main/router.d.ts.map +1 -1
  16. package/dist/main/router.js +63 -21
  17. package/dist/main/router.js.map +1 -1
  18. package/dist/main/types/operation.d.ts +8 -0
  19. package/dist/main/types/operation.d.ts.map +1 -1
  20. package/dist/main/types/websocket.d.ts +8 -0
  21. package/dist/main/types/websocket.d.ts.map +1 -1
  22. package/dist/main/utils/websocket-mock.d.ts.map +1 -1
  23. package/dist/main/utils/websocket-mock.js +3 -0
  24. package/dist/main/utils/websocket-mock.js.map +1 -1
  25. package/dist/main/xrpc-error.d.ts +55 -15
  26. package/dist/main/xrpc-error.d.ts.map +1 -1
  27. package/dist/main/xrpc-error.js +66 -26
  28. package/dist/main/xrpc-error.js.map +1 -1
  29. package/lib/auth/jwt-creator.ts +5 -3
  30. package/lib/auth/jwt-verifier.ts +205 -25
  31. package/lib/auth/jwt.ts +42 -29
  32. package/lib/main/router.ts +95 -42
  33. package/lib/main/types/operation.ts +8 -0
  34. package/lib/main/types/websocket.ts +8 -0
  35. package/lib/main/utils/websocket-mock.ts +3 -0
  36. package/lib/main/xrpc-error.ts +107 -44
  37. package/package.json +20 -16
@@ -38,8 +38,13 @@ type InternalRouteData = {
38
38
  export type FetchMiddleware = Middleware<[request: Request], Promise<Response>>;
39
39
 
40
40
  export type NotFoundHandler = (request: Request) => Promisable<Response>;
41
+ export type HealthCheckHandler = (request: Request) => Promisable<Response>;
41
42
  export type ExceptionHandler = (error: unknown, request: Request) => Promisable<Response>;
42
- export type SubscriptionExceptionHandler = (error: unknown, request: Request) => void;
43
+
44
+ /** telemetry hook invoked for unexpected HTTP handler errors; fire-and-forget. */
45
+ export type ErrorObserver = (ctx: { error: unknown; request: Request }) => void;
46
+ /** telemetry hook invoked for unexpected subscription errors; fire-and-forget. */
47
+ export type SocketErrorObserver = (ctx: { error: unknown; request: Request }) => void;
43
48
 
44
49
  export const defaultExceptionHandler: ExceptionHandler = (error: unknown) => {
45
50
  if (error instanceof XRPCError) {
@@ -60,23 +65,40 @@ export const defaultNotFoundHandler: NotFoundHandler = () => {
60
65
  return new Response('Not Found', { status: 404 });
61
66
  };
62
67
 
63
- export const defaultSubscriptionExceptionHandler: SubscriptionExceptionHandler = (error: unknown) => {
64
- throw error;
65
- };
66
-
67
68
  export interface XRPCRouterOptions {
68
69
  middlewares?: FetchMiddleware[];
69
70
  handleNotFound?: NotFoundHandler;
71
+ /**
72
+ * optional handler for `/xrpc/_health`. when provided, the router answers
73
+ * health-check requests by invoking this handler; when absent, the path
74
+ * falls through to `handleNotFound`. `_health` is not part of the atproto
75
+ * XRPC spec, so callers opt in explicitly.
76
+ */
77
+ handleHealthCheck?: HealthCheckHandler;
78
+ /** translates a thrown error into an HTTP response. */
70
79
  handleException?: ExceptionHandler;
71
- handleSubscriptionException?: SubscriptionExceptionHandler;
80
+ /**
81
+ * fire-and-forget telemetry hook for unexpected HTTP errors. not invoked for
82
+ * client-induced errors (aborted requests, `XRPCError` subclasses, thrown
83
+ * `Response` objects).
84
+ */
85
+ onError?: ErrorObserver;
86
+ /**
87
+ * fire-and-forget telemetry hook for unexpected subscription errors. not
88
+ * invoked for aborted signals or `XRPCSubscriptionError` (which is
89
+ * translated to an error frame).
90
+ */
91
+ onSocketError?: SocketErrorObserver;
72
92
  websocket?: WebSocketAdapter;
73
93
  }
74
94
 
75
95
  export class XRPCRouter {
76
96
  #handlers: Record<string, InternalRouteData> = {};
77
97
  #handleNotFound: NotFoundHandler;
98
+ #handleHealthCheck?: HealthCheckHandler;
78
99
  #handleException: ExceptionHandler;
79
- #handleSubscriptionException: SubscriptionExceptionHandler;
100
+ #onError?: ErrorObserver;
101
+ #onSocketError?: SocketErrorObserver;
80
102
  #websocket?: WebSocketAdapter;
81
103
 
82
104
  fetch: (request: Request) => Promise<Response>;
@@ -85,7 +107,9 @@ export class XRPCRouter {
85
107
  middlewares = [],
86
108
  handleException = defaultExceptionHandler,
87
109
  handleNotFound = defaultNotFoundHandler,
88
- handleSubscriptionException = defaultSubscriptionExceptionHandler,
110
+ handleHealthCheck,
111
+ onError,
112
+ onSocketError,
89
113
  websocket,
90
114
  }: XRPCRouterOptions = {}) {
91
115
  const runner = createAsyncMiddlewareRunner([...middlewares, (request) => this.#dispatch(request)]);
@@ -93,10 +117,46 @@ export class XRPCRouter {
93
117
  this.fetch = (request) => runner(request);
94
118
  this.#handleException = handleException;
95
119
  this.#handleNotFound = handleNotFound;
96
- this.#handleSubscriptionException = handleSubscriptionException;
120
+ this.#handleHealthCheck = handleHealthCheck;
121
+ this.#onError = onError;
122
+ this.#onSocketError = onSocketError;
97
123
  this.#websocket = websocket;
98
124
  }
99
125
 
126
+ #observeError(error: unknown, request: Request): void {
127
+ // client-induced errors are not bugs; skip telemetry
128
+ if (request.signal.aborted) {
129
+ return;
130
+ }
131
+ if (error instanceof XRPCError) {
132
+ return;
133
+ }
134
+ if (error instanceof Response) {
135
+ return;
136
+ }
137
+
138
+ try {
139
+ this.#onError?.({ error, request });
140
+ } catch {
141
+ // observer threw; swallow to keep response path deterministic
142
+ }
143
+ }
144
+
145
+ #observeSocketError(error: unknown, request: Request): void {
146
+ if (request.signal.aborted) {
147
+ return;
148
+ }
149
+ if (error instanceof XRPCSubscriptionError) {
150
+ return;
151
+ }
152
+
153
+ try {
154
+ this.#onSocketError?.({ error, request });
155
+ } catch {
156
+ // observer threw; swallow to keep socket close path deterministic
157
+ }
158
+ }
159
+
100
160
  async #dispatch(request: Request): Promise<Response> {
101
161
  const url = new URL(request.url);
102
162
  const pathname = url.pathname;
@@ -107,15 +167,31 @@ export class XRPCRouter {
107
167
 
108
168
  const nsid = pathname.slice('/xrpc/'.length);
109
169
 
170
+ if (nsid === '_health' && this.#handleHealthCheck !== undefined) {
171
+ try {
172
+ return await this.#handleHealthCheck(request);
173
+ } catch (err) {
174
+ if (request.signal.aborted) {
175
+ return new Response(null, { status: 499 });
176
+ }
177
+
178
+ this.#observeError(err, request);
179
+ return this.#handleException(err, request);
180
+ }
181
+ }
182
+
110
183
  const route = this.#handlers[nsid];
111
184
  if (route === undefined) {
112
185
  return this.#handleNotFound(request);
113
186
  }
114
187
 
115
- if (request.method !== route.method) {
188
+ // allow HEAD alongside GET; the runtime is responsible for stripping the
189
+ // response body per the Fetch API.
190
+ const allowed = request.method === route.method || (route.method === 'GET' && request.method === 'HEAD');
191
+ if (!allowed) {
116
192
  return Response.json(
117
- { error: 'InvalidHttpMethod', message: `invalid http method (expected ${route.method})` },
118
- { status: 405, headers: { allow: `${route.method}` } },
193
+ { error: 'InvalidRequest', message: `invalid http method (expected ${route.method})` },
194
+ { status: 405, headers: { allow: route.method === 'GET' ? 'GET, HEAD' : route.method } },
119
195
  );
120
196
  }
121
197
 
@@ -131,38 +207,11 @@ export class XRPCRouter {
131
207
  return new Response(null, { status: 499 });
132
208
  }
133
209
 
210
+ this.#observeError(err, request);
134
211
  return this.#handleException(err, request);
135
212
  }
136
213
  }
137
214
 
138
- /** @deprecated use `addQuery` and `addProcedure` instead */
139
- add<TQuery extends XRPCQueryMetadata>(
140
- query: TQuery | Namespaced<TQuery>,
141
- config: QueryConfig<TQuery>,
142
- ): void;
143
- add<TProcedure extends XRPCProcedureMetadata>(
144
- procedure: TProcedure | Namespaced<TProcedure>,
145
- config: ProcedureConfig<TProcedure>,
146
- ): void;
147
- add(
148
- operation:
149
- | XRPCQueryMetadata
150
- | XRPCProcedureMetadata
151
- | Namespaced<XRPCQueryMetadata | XRPCProcedureMetadata>,
152
- config: any,
153
- ): void {
154
- const schema = unwrapLxm(operation);
155
-
156
- switch (schema.type) {
157
- case 'xrpc_query': {
158
- return this.addQuery(schema, config);
159
- }
160
- case 'xrpc_procedure': {
161
- return this.addProcedure(schema, config);
162
- }
163
- }
164
- }
165
-
166
215
  addQuery<TQuery extends XRPCQueryMetadata, TConfig extends QueryConfig<TQuery>>(
167
216
  query: TQuery | Namespaced<TQuery>,
168
217
  config: TConfig,
@@ -350,12 +399,16 @@ export class XRPCRouter {
350
399
 
351
400
  const frame = encodeMessageFrame(body, type);
352
401
  await ws.send(frame);
402
+ const drained = ws.drain();
403
+ if (drained) {
404
+ await drained;
405
+ }
353
406
  }
354
407
 
355
408
  ws.close(1000);
356
409
  } catch (err) {
357
410
  if (err instanceof XRPCSubscriptionError) {
358
- const frame = encodeErrorFrame(err.error, err.description);
411
+ const frame = encodeErrorFrame(err.error, err.message || undefined);
359
412
 
360
413
  try {
361
414
  await ws.send(frame);
@@ -368,7 +421,7 @@ export class XRPCRouter {
368
421
  }
369
422
 
370
423
  ws.close(1011, `internal server error`);
371
- this.#handleSubscriptionException(err, request);
424
+ this.#observeSocketError(err, request);
372
425
  }
373
426
  });
374
427
 
@@ -12,6 +12,14 @@ import type {
12
12
  import type { Literal, Promisable } from '../../types/misc.ts';
13
13
  import type { JSONResponse } from '../response.ts';
14
14
 
15
+ /**
16
+ * untyped variant of {@link QueryContext} / {@link ProcedureContext}.
17
+ *
18
+ * `input` is set only when the lexicon declares a `lex` input body and the
19
+ * request JSON parsed successfully. for blob inputs (and for procedures that
20
+ * declare no input at all) it is `undefined`; handlers that expect a blob
21
+ * should stream from `request.body` directly.
22
+ */
15
23
  export type UnknownOperationContext = {
16
24
  request: Request;
17
25
  signal: AbortSignal;
@@ -3,6 +3,14 @@ import type { Promisable } from '../../types/misc.ts';
3
3
  export interface WebSocketConnection {
4
4
  signal: AbortSignal;
5
5
  send(data: Uint8Array): void | Promise<void>;
6
+ /**
7
+ * backpressure hook invoked by the router after every frame it sends.
8
+ * adapters that can observe the outgoing send buffer (Node `ws`, Bun, Deno)
9
+ * should resolve only once the buffer has drained below a healthy threshold.
10
+ * adapters without that visibility (e.g. Cloudflare Workers) should return
11
+ * synchronously.
12
+ */
13
+ drain(): void | Promise<void>;
6
14
  close(code?: number, reason?: string): void;
7
15
  }
8
16
 
@@ -79,6 +79,9 @@ export class MockWebSocketAdapter implements WebSocketAdapter {
79
79
  send(data) {
80
80
  onMessage.emit(data);
81
81
  },
82
+ drain() {
83
+ // tests have no outgoing buffer to observe
84
+ },
82
85
  close(code = 1000, reason = '') {
83
86
  if (!signal.aborted) {
84
87
  onClose.emit({ code, reason, wasClean: true });
@@ -1,7 +1,71 @@
1
+ /**
2
+ * a single WWW-Authenticate challenge. exactly one of `params` or `token68` may
3
+ * be provided. a bare scheme (no params, no token) is valid and renders as just
4
+ * the scheme name.
5
+ *
6
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc7235#section-4.1 | RFC 7235 §4.1}
7
+ */
8
+ export interface WWWAuthenticateChallenge {
9
+ /** authentication scheme, e.g. `Bearer`, `DPoP`, `Basic`. */
10
+ scheme: string;
11
+ /** auth-param pairs. entries whose value is `undefined` are omitted. */
12
+ params?: Record<string, string | undefined>;
13
+ /**
14
+ * token68 value for schemes that carry one instead of auth-params (e.g.
15
+ * `Basic`). mutually exclusive with `params`.
16
+ */
17
+ token68?: string;
18
+ }
19
+
20
+ /**
21
+ * formats one or more WWW-Authenticate challenges into a single header value.
22
+ *
23
+ * each challenge is emitted as `<scheme>` followed by its params or token68.
24
+ * multiple challenges are joined with `, `. auth-param values are quoted using
25
+ * `JSON.stringify` (RFC 7230 quoted-string semantics for ASCII content).
26
+ *
27
+ * @param challenges one challenge, or an ordered array of challenges
28
+ * @returns the formatted header value
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * formatWWWAuthenticate({ scheme: 'Bearer', params: { error: 'BadJwtSignature' } })
33
+ * // => `Bearer error="BadJwtSignature"`
34
+ * ```
35
+ */
36
+ export const formatWWWAuthenticate = (
37
+ challenges: WWWAuthenticateChallenge | WWWAuthenticateChallenge[],
38
+ ): string => {
39
+ const list = Array.isArray(challenges) ? challenges : [challenges];
40
+ return list.map(formatChallenge).join(', ');
41
+ };
42
+
43
+ const formatChallenge = (challenge: WWWAuthenticateChallenge): string => {
44
+ if (challenge.token68 !== undefined) {
45
+ return `${challenge.scheme} ${challenge.token68}`;
46
+ }
47
+
48
+ if (challenge.params !== undefined) {
49
+ const parts: string[] = [];
50
+ for (const name in challenge.params) {
51
+ const value = challenge.params[name];
52
+ if (value !== undefined) {
53
+ parts.push(`${name}=${JSON.stringify(value)}`);
54
+ }
55
+ }
56
+
57
+ if (parts.length > 0) {
58
+ return `${challenge.scheme} ${parts.join(', ')}`;
59
+ }
60
+ }
61
+
62
+ return challenge.scheme;
63
+ };
64
+
1
65
  export interface XRPCErrorOptions {
2
66
  status: number;
3
67
  error: string;
4
- description?: string;
68
+ message?: string;
5
69
  headers?: HeadersInit;
6
70
  }
7
71
 
@@ -11,54 +75,65 @@ export class XRPCError extends Error {
11
75
 
12
76
  /** error name */
13
77
  readonly error: string;
14
- /** error message */
15
- readonly description?: string;
16
78
  /** response headers */
17
79
  readonly headers?: HeadersInit;
18
80
 
19
- constructor({ status, error, description, headers }: XRPCErrorOptions) {
20
- super(`${error} > ${description ?? '(unspecified description)'}`);
81
+ constructor({ status, error, message, headers }: XRPCErrorOptions) {
82
+ super(message);
21
83
 
22
84
  this.status = status;
23
85
 
24
86
  this.error = error;
25
- this.description = description;
26
87
  this.headers = headers;
27
88
  }
28
89
 
29
90
  toResponse(): Response {
30
91
  return Response.json(
31
- { error: this.error, message: this.description },
92
+ { error: this.error, message: this.message || undefined },
32
93
  { status: this.status, headers: this.headers },
33
94
  );
34
95
  }
35
96
  }
36
97
 
37
98
  export class InvalidRequestError extends XRPCError {
38
- constructor({
39
- status = 400,
40
- error = 'InvalidRequest',
41
- description,
42
- headers,
43
- }: Partial<XRPCErrorOptions> = {}) {
44
- super({ status, error, description, headers });
99
+ constructor({ status = 400, error = 'InvalidRequest', message, headers }: Partial<XRPCErrorOptions> = {}) {
100
+ super({ status, error, message, headers });
45
101
  }
46
102
  }
47
103
 
104
+ export interface AuthRequiredErrorOptions extends Partial<XRPCErrorOptions> {
105
+ /**
106
+ * WWW-Authenticate challenge(s) to attach to the response. the formatted
107
+ * header is set on `headers` automatically, and `access-control-expose-headers`
108
+ * is appended so browsers can read it from CORS responses.
109
+ */
110
+ wwwAuthenticate?: WWWAuthenticateChallenge | WWWAuthenticateChallenge[];
111
+ }
112
+
48
113
  export class AuthRequiredError extends XRPCError {
49
114
  constructor({
50
115
  status = 401,
51
116
  error = 'AuthenticationRequired',
52
- description,
117
+ message,
53
118
  headers,
54
- }: Partial<XRPCErrorOptions> = {}) {
55
- super({ status, error, description, headers });
119
+ wwwAuthenticate,
120
+ }: AuthRequiredErrorOptions = {}) {
121
+ let mergedHeaders = headers;
122
+
123
+ if (wwwAuthenticate !== undefined) {
124
+ const target = new Headers(headers);
125
+ target.set('www-authenticate', formatWWWAuthenticate(wwwAuthenticate));
126
+ target.append('access-control-expose-headers', 'www-authenticate');
127
+ mergedHeaders = target;
128
+ }
129
+
130
+ super({ status, error, message, headers: mergedHeaders });
56
131
  }
57
132
  }
58
133
 
59
134
  export class ForbiddenError extends XRPCError {
60
- constructor({ status = 403, error = 'Forbidden', description, headers }: Partial<XRPCErrorOptions> = {}) {
61
- super({ status, error, description, headers });
135
+ constructor({ status = 403, error = 'Forbidden', message, headers }: Partial<XRPCErrorOptions> = {}) {
136
+ super({ status, error, message, headers });
62
137
  }
63
138
  }
64
139
 
@@ -66,10 +141,10 @@ export class RateLimitExceededError extends XRPCError {
66
141
  constructor({
67
142
  status = 429,
68
143
  error = 'RateLimitExceeded',
69
- description,
144
+ message,
70
145
  headers,
71
146
  }: Partial<XRPCErrorOptions> = {}) {
72
- super({ status, error, description, headers });
147
+ super({ status, error, message, headers });
73
148
  }
74
149
  }
75
150
 
@@ -77,21 +152,16 @@ export class InternalServerError extends XRPCError {
77
152
  constructor({
78
153
  status = 500,
79
154
  error = 'InternalServerError',
80
- description,
155
+ message,
81
156
  headers,
82
157
  }: Partial<XRPCErrorOptions> = {}) {
83
- super({ status, error, description, headers });
158
+ super({ status, error, message, headers });
84
159
  }
85
160
  }
86
161
 
87
162
  export class UpstreamFailureError extends XRPCError {
88
- constructor({
89
- status = 502,
90
- error = 'UpstreamFailure',
91
- description,
92
- headers,
93
- }: Partial<XRPCErrorOptions> = {}) {
94
- super({ status, error, description, headers });
163
+ constructor({ status = 502, error = 'UpstreamFailure', message, headers }: Partial<XRPCErrorOptions> = {}) {
164
+ super({ status, error, message, headers });
95
165
  }
96
166
  }
97
167
 
@@ -99,40 +169,33 @@ export class NotEnoughResourcesError extends XRPCError {
99
169
  constructor({
100
170
  status = 503,
101
171
  error = 'NotEnoughResources',
102
- description,
172
+ message,
103
173
  headers,
104
174
  }: Partial<XRPCErrorOptions> = {}) {
105
- super({ status, error, description, headers });
175
+ super({ status, error, message, headers });
106
176
  }
107
177
  }
108
178
 
109
179
  export class UpstreamTimeoutError extends XRPCError {
110
- constructor({
111
- status = 504,
112
- error = 'UpstreamTimeout',
113
- description,
114
- headers,
115
- }: Partial<XRPCErrorOptions> = {}) {
116
- super({ status, error, description, headers });
180
+ constructor({ status = 504, error = 'UpstreamTimeout', message, headers }: Partial<XRPCErrorOptions> = {}) {
181
+ super({ status, error, message, headers });
117
182
  }
118
183
  }
119
184
 
120
185
  export interface XRPCSubscriptionErrorOptions {
121
186
  closeCode?: number;
122
187
  error: string;
123
- description?: string;
188
+ message?: string;
124
189
  }
125
190
 
126
191
  export class XRPCSubscriptionError extends Error {
127
192
  readonly closeCode: number;
128
193
  readonly error: string;
129
- readonly description?: string;
130
194
 
131
- constructor({ closeCode = 1008, error, description }: XRPCSubscriptionErrorOptions) {
132
- super(`Subscription error: ${error}${description ? ` - ${description}` : ''}`);
195
+ constructor({ closeCode = 1008, error, message }: XRPCSubscriptionErrorOptions) {
196
+ super(message);
133
197
 
134
198
  this.closeCode = closeCode;
135
199
  this.error = error;
136
- this.description = description;
137
200
  }
138
201
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atcute/xrpc-server",
3
- "version": "0.1.12",
3
+ "version": "2.0.0",
4
4
  "description": "a small web framework for handling XRPC operations",
5
5
  "license": "0BSD",
6
6
  "repository": {
@@ -11,7 +11,8 @@
11
11
  "dist/",
12
12
  "lib/",
13
13
  "!lib/**/*.bench.ts",
14
- "!lib/**/*.test.ts"
14
+ "!lib/**/*.test.ts",
15
+ "!dist/**/*.{test,bench}.*"
15
16
  ],
16
17
  "type": "module",
17
18
  "exports": {
@@ -23,27 +24,30 @@
23
24
  "access": "public"
24
25
  },
25
26
  "dependencies": {
26
- "@badrap/valita": "^0.4.6",
27
- "nanoid": "^5.1.7",
27
+ "nanoid": "^5.1.11",
28
+ "valibot": "^1.4.0",
29
+ "@atcute/cbor": "^2.3.3",
28
30
  "@atcute/crypto": "^2.4.1",
29
- "@atcute/cbor": "^2.3.2",
30
- "@atcute/lexicons": "^1.2.9",
31
- "@atcute/identity-resolver": "^1.2.2",
32
- "@atcute/uint8array": "^1.1.1",
33
- "@atcute/identity": "^1.1.4",
34
- "@atcute/multibase": "^1.2.0"
31
+ "@atcute/identity": "^2.0.0",
32
+ "@atcute/identity-resolver": "^2.0.0",
33
+ "@atcute/lexicons": "^2.0.0",
34
+ "@atcute/multibase": "^1.2.0",
35
+ "@atcute/uint8array": "^1.1.1"
35
36
  },
36
37
  "devDependencies": {
37
38
  "@atcute/xrpc-server": "file:",
38
39
  "@mary-ext/simple-event-emitter": "^1.0.1",
39
- "@types/node": "^25.5.2",
40
- "@vitest/coverage-v8": "^4.1.2",
41
- "vitest": "^4.1.2",
42
- "@atcute/bluesky": "^3.3.0",
43
- "@atcute/atproto": "^3.1.10"
40
+ "@types/node": "^25.6.0",
41
+ "@vitest/coverage-v8": "^4.1.5",
42
+ "vitest": "^4.1.5",
43
+ "@atcute/bluesky": "^4.0.0",
44
+ "@atcute/atproto": "^4.0.0"
45
+ },
46
+ "peerDependencies": {
47
+ "@atcute/lexicons": "^2.0.0"
44
48
  },
45
49
  "scripts": {
46
- "build": "tsgo --project tsconfig.build.json",
50
+ "build": "tsgo",
47
51
  "test": "vitest --coverage",
48
52
  "prepublish": "rm -rf dist; pnpm run build"
49
53
  }