this repo has no description
0
fork

Configure Feed

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

Implement queryLabels endpoint and label signing

futurGH ff12d41f b54efd85

+174 -30
+1
.eslintrc.json
··· 10 10 "@typescript-eslint/no-misused-promises": "off", 11 11 "@typescript-eslint/no-non-null-assertion": "off", 12 12 "@typescript-eslint/no-unnecessary-condition": "off", 13 + "@typescript-eslint/no-unsafe-argument": "off", 13 14 "@typescript-eslint/no-unsafe-assignment": "off", 14 15 "@typescript-eslint/no-unsafe-call": "off", 15 16 "@typescript-eslint/no-unsafe-member-access": "off",
+2 -1
package.json
··· 35 35 "@typescript-eslint/parser": "^6.7.4", 36 36 "dprint": "^0.41.0", 37 37 "eslint": "^8.50.0", 38 - "typescript": "^5.2.2" 38 + "typescript": "^5.5.4" 39 39 }, 40 40 "dependencies": { 41 + "@atcute/cbor": "^1.0.0", 41 42 "@atproto/crypto": "^0.4.0", 42 43 "@atproto/xrpc-server": "^0.6.2", 43 44 "better-sqlite3": "^11.1.2",
+57 -27
pnpm-lock.yaml
··· 5 5 excludeLinksFromLockfile: false 6 6 7 7 dependencies: 8 + '@atcute/cbor': 9 + specifier: ^1.0.0 10 + version: 1.0.0 8 11 '@atproto/crypto': 9 12 specifier: ^0.4.0 10 13 version: 0.4.0 ··· 45 48 version: 8.5.12 46 49 '@typescript-eslint/eslint-plugin': 47 50 specifier: ^6.7.4 48 - version: 6.7.4(@typescript-eslint/parser@6.7.4)(eslint@8.50.0)(typescript@5.2.2) 51 + version: 6.7.4(@typescript-eslint/parser@6.7.4)(eslint@8.50.0)(typescript@5.5.4) 49 52 '@typescript-eslint/parser': 50 53 specifier: ^6.7.4 51 - version: 6.7.4(eslint@8.50.0)(typescript@5.2.2) 54 + version: 6.7.4(eslint@8.50.0)(typescript@5.5.4) 52 55 dprint: 53 56 specifier: ^0.41.0 54 57 version: 0.41.0 ··· 56 59 specifier: ^8.50.0 57 60 version: 8.50.0 58 61 typescript: 59 - specifier: ^5.2.2 60 - version: 5.2.2 62 + specifier: ^5.5.4 63 + version: 5.5.4 61 64 62 65 packages: 63 66 ··· 65 68 resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} 66 69 engines: {node: '>=0.10.0'} 67 70 dev: true 71 + 72 + /@atcute/base32@1.0.0: 73 + resolution: {integrity: sha512-Mbjsv6kd/ymvDMGjCoh9eqhlpFsoJ6zYguU6xtKxqh1wGhe5rvBOfMRXsEqcp7srn8Bfp8QhevqLgmwrWvzqrA==} 74 + dev: false 75 + 76 + /@atcute/cbor@1.0.0: 77 + resolution: {integrity: sha512-aHbURHim6cem7ZRLYg+Q9CkbGAPAV9P2pms7V/p5OkpP/dAb7RgoFwf49vg1454xrCtfFOhCtheUnmxLROdG3Q==} 78 + dependencies: 79 + '@atcute/base32': 1.0.0 80 + '@atcute/cid': 1.0.0 81 + fp16: 0.3.0 82 + dev: false 83 + 84 + /@atcute/cid@1.0.0: 85 + resolution: {integrity: sha512-JnWv3sg48zDBP318ErPYPI482Vw1Nm7e7WG+VYGSLRLp56b9LgcIh28p28gEmPtmsnM9hTAkKvJdi+CAkNDQUA==} 86 + dependencies: 87 + '@atcute/base32': 1.0.0 88 + '@atcute/varint': 1.0.0 89 + dev: false 90 + 91 + /@atcute/varint@1.0.0: 92 + resolution: {integrity: sha512-NEBOGkdaDY8cjlDg49kefIsRM7iv/4oReEnOr3bN4tF3IxBGdc6Io1NCJz1xNBNdUL+3VDG3CKHiRji91HXaTg==} 93 + dev: false 68 94 69 95 /@atproto/api@0.13.1: 70 96 resolution: {integrity: sha512-DL3iBfavn8Nnl48FmnAreQB0k0cIkW531DJ5JAHUCQZo10Nq0ZLk2/WFxcs0KuBG5wuLnGUdo+Y6/GQPVq8dYw==} ··· 432 458 '@types/node': 20.11.19 433 459 dev: true 434 460 435 - /@typescript-eslint/eslint-plugin@6.7.4(@typescript-eslint/parser@6.7.4)(eslint@8.50.0)(typescript@5.2.2): 461 + /@typescript-eslint/eslint-plugin@6.7.4(@typescript-eslint/parser@6.7.4)(eslint@8.50.0)(typescript@5.5.4): 436 462 resolution: {integrity: sha512-DAbgDXwtX+pDkAHwiGhqP3zWUGpW49B7eqmgpPtg+BKJXwdct79ut9+ifqOFPJGClGKSHXn2PTBatCnldJRUoA==} 437 463 engines: {node: ^16.0.0 || >=18.0.0} 438 464 peerDependencies: ··· 444 470 optional: true 445 471 dependencies: 446 472 '@eslint-community/regexpp': 4.10.0 447 - '@typescript-eslint/parser': 6.7.4(eslint@8.50.0)(typescript@5.2.2) 473 + '@typescript-eslint/parser': 6.7.4(eslint@8.50.0)(typescript@5.5.4) 448 474 '@typescript-eslint/scope-manager': 6.7.4 449 - '@typescript-eslint/type-utils': 6.7.4(eslint@8.50.0)(typescript@5.2.2) 450 - '@typescript-eslint/utils': 6.7.4(eslint@8.50.0)(typescript@5.2.2) 475 + '@typescript-eslint/type-utils': 6.7.4(eslint@8.50.0)(typescript@5.5.4) 476 + '@typescript-eslint/utils': 6.7.4(eslint@8.50.0)(typescript@5.5.4) 451 477 '@typescript-eslint/visitor-keys': 6.7.4 452 478 debug: 4.3.4 453 479 eslint: 8.50.0 ··· 455 481 ignore: 5.3.1 456 482 natural-compare: 1.4.0 457 483 semver: 7.6.0 458 - ts-api-utils: 1.2.1(typescript@5.2.2) 459 - typescript: 5.2.2 484 + ts-api-utils: 1.2.1(typescript@5.5.4) 485 + typescript: 5.5.4 460 486 transitivePeerDependencies: 461 487 - supports-color 462 488 dev: true 463 489 464 - /@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.2.2): 490 + /@typescript-eslint/parser@6.7.4(eslint@8.50.0)(typescript@5.5.4): 465 491 resolution: {integrity: sha512-I5zVZFY+cw4IMZUeNCU7Sh2PO5O57F7Lr0uyhgCJmhN/BuTlnc55KxPonR4+EM3GBdfiCyGZye6DgMjtubQkmA==} 466 492 engines: {node: ^16.0.0 || >=18.0.0} 467 493 peerDependencies: ··· 473 499 dependencies: 474 500 '@typescript-eslint/scope-manager': 6.7.4 475 501 '@typescript-eslint/types': 6.7.4 476 - '@typescript-eslint/typescript-estree': 6.7.4(typescript@5.2.2) 502 + '@typescript-eslint/typescript-estree': 6.7.4(typescript@5.5.4) 477 503 '@typescript-eslint/visitor-keys': 6.7.4 478 504 debug: 4.3.4 479 505 eslint: 8.50.0 480 - typescript: 5.2.2 506 + typescript: 5.5.4 481 507 transitivePeerDependencies: 482 508 - supports-color 483 509 dev: true ··· 490 516 '@typescript-eslint/visitor-keys': 6.7.4 491 517 dev: true 492 518 493 - /@typescript-eslint/type-utils@6.7.4(eslint@8.50.0)(typescript@5.2.2): 519 + /@typescript-eslint/type-utils@6.7.4(eslint@8.50.0)(typescript@5.5.4): 494 520 resolution: {integrity: sha512-n+g3zi1QzpcAdHFP9KQF+rEFxMb2KxtnJGID3teA/nxKHOVi3ylKovaqEzGBbVY2pBttU6z85gp0D00ufLzViQ==} 495 521 engines: {node: ^16.0.0 || >=18.0.0} 496 522 peerDependencies: ··· 500 526 typescript: 501 527 optional: true 502 528 dependencies: 503 - '@typescript-eslint/typescript-estree': 6.7.4(typescript@5.2.2) 504 - '@typescript-eslint/utils': 6.7.4(eslint@8.50.0)(typescript@5.2.2) 529 + '@typescript-eslint/typescript-estree': 6.7.4(typescript@5.5.4) 530 + '@typescript-eslint/utils': 6.7.4(eslint@8.50.0)(typescript@5.5.4) 505 531 debug: 4.3.4 506 532 eslint: 8.50.0 507 - ts-api-utils: 1.2.1(typescript@5.2.2) 508 - typescript: 5.2.2 533 + ts-api-utils: 1.2.1(typescript@5.5.4) 534 + typescript: 5.5.4 509 535 transitivePeerDependencies: 510 536 - supports-color 511 537 dev: true ··· 515 541 engines: {node: ^16.0.0 || >=18.0.0} 516 542 dev: true 517 543 518 - /@typescript-eslint/typescript-estree@6.7.4(typescript@5.2.2): 544 + /@typescript-eslint/typescript-estree@6.7.4(typescript@5.5.4): 519 545 resolution: {integrity: sha512-ty8b5qHKatlNYd9vmpHooQz3Vki3gG+3PchmtsA4TgrZBKWHNjWfkQid7K7xQogBqqc7/BhGazxMD5vr6Ha+iQ==} 520 546 engines: {node: ^16.0.0 || >=18.0.0} 521 547 peerDependencies: ··· 530 556 globby: 11.1.0 531 557 is-glob: 4.0.3 532 558 semver: 7.6.0 533 - ts-api-utils: 1.2.1(typescript@5.2.2) 534 - typescript: 5.2.2 559 + ts-api-utils: 1.2.1(typescript@5.5.4) 560 + typescript: 5.5.4 535 561 transitivePeerDependencies: 536 562 - supports-color 537 563 dev: true 538 564 539 - /@typescript-eslint/utils@6.7.4(eslint@8.50.0)(typescript@5.2.2): 565 + /@typescript-eslint/utils@6.7.4(eslint@8.50.0)(typescript@5.5.4): 540 566 resolution: {integrity: sha512-PRQAs+HUn85Qdk+khAxsVV+oULy3VkbH3hQ8hxLRJXWBEd7iI+GbQxH5SEUSH7kbEoTp6oT1bOwyga24ELALTA==} 541 567 engines: {node: ^16.0.0 || >=18.0.0} 542 568 peerDependencies: ··· 547 573 '@types/semver': 7.5.7 548 574 '@typescript-eslint/scope-manager': 6.7.4 549 575 '@typescript-eslint/types': 6.7.4 550 - '@typescript-eslint/typescript-estree': 6.7.4(typescript@5.2.2) 576 + '@typescript-eslint/typescript-estree': 6.7.4(typescript@5.5.4) 551 577 eslint: 8.50.0 552 578 semver: 7.6.0 553 579 transitivePeerDependencies: ··· 1199 1225 /forwarded@0.2.0: 1200 1226 resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} 1201 1227 engines: {node: '>= 0.6'} 1228 + dev: false 1229 + 1230 + /fp16@0.3.0: 1231 + resolution: {integrity: sha512-Iw6hLaH345EETQGU56NiAcJ173IraDRdJrVy+0fBMPdkZhzKNN/k3eqXFyNmnyx2/jnIzF2nr0mHh5nHrQBMEQ==} 1202 1232 dev: false 1203 1233 1204 1234 /fresh@0.5.2: ··· 2026 2056 engines: {node: '>=0.6'} 2027 2057 dev: false 2028 2058 2029 - /ts-api-utils@1.2.1(typescript@5.2.2): 2059 + /ts-api-utils@1.2.1(typescript@5.5.4): 2030 2060 resolution: {integrity: sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==} 2031 2061 engines: {node: '>=16'} 2032 2062 peerDependencies: 2033 2063 typescript: '>=4.2.0' 2034 2064 dependencies: 2035 - typescript: 5.2.2 2065 + typescript: 5.5.4 2036 2066 dev: true 2037 2067 2038 2068 /tunnel-agent@0.6.0: ··· 2061 2091 mime-types: 2.1.35 2062 2092 dev: false 2063 2093 2064 - /typescript@5.2.2: 2065 - resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} 2094 + /typescript@5.5.4: 2095 + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} 2066 2096 engines: {node: '>=14.17'} 2067 2097 hasBin: true 2068 2098 dev: true
+107 -2
src/LabelerServer.ts
··· 1 + import { encode as cborEncode } from "@atcute/cbor"; 1 2 import type { ComAtprotoLabelDefs } from "@atproto/api"; 2 - import { ErrorFrame, MessageFrame } from "@atproto/xrpc-server"; 3 + import { Keypair, Secp256k1Keypair } from "@atproto/crypto"; 4 + import { ErrorFrame, InvalidRequestError, MessageFrame, XRPCError } from "@atproto/xrpc-server"; 3 5 import Database, { type Database as SQLiteDatabase } from "better-sqlite3"; 4 6 import express from "express"; 5 7 import expressWs, { type Application, WebsocketRequestHandler } from "express-ws"; 6 8 import { Server } from "node:http"; 9 + import { fromString as ui8FromString } from "uint8arrays"; 7 10 import type { WebSocket } from "ws"; 11 + import { SignedLabel, StrictPartial } from "./util.js"; 12 + 13 + const LABEL_VERSION = 1; 8 14 9 15 export interface LabelerOptions { 10 16 did: string; 17 + signingKey: string; 11 18 dbFile?: string; 12 19 } 13 20 ··· 19 26 db: SQLiteDatabase; 20 27 21 28 did: string; 29 + 30 + private signingKey: Keypair; 22 31 23 32 private subscriptions = new Set<WebSocket>(); 24 33 25 34 constructor(options: LabelerOptions) { 26 35 this.did = options.did; 36 + this.signingKey = new Secp256k1Keypair(ui8FromString(options.signingKey, "hex"), false); 27 37 28 38 this.db = new Database(options.dbFile ?? "labels.db"); 29 39 this.db.pragma("journal_mode = WAL"); ··· 42 52 `); 43 53 44 54 this.app = expressWs(express().use(express.json())).app; 55 + this.app.get("/xrpc/com.atproto.label.queryLabels", this.queryLabelsHandler); 45 56 this.app.ws("/xrpc/com.atproto.label.subscribeLabels", this.subscribeLabelsHandler); 46 57 } 47 58 ··· 53 64 if (this.server?.listening) this.server?.close(callback); 54 65 } 55 66 56 - subscribeLabelsHandler: WebsocketRequestHandler = async (ws, req) => { 67 + async signLabel(label: ComAtprotoLabelDefs.Label): Promise<SignedLabel> { 68 + const toSign = this.formatLabel(label); 69 + const bytes = cborEncode(toSign); 70 + const sig = await this.signingKey.sign(bytes); 71 + return { ...toSign, sig }; 72 + } 73 + 74 + private formatLabel<T extends ComAtprotoLabelDefs.Label>(label: T): StrictPartial<T> { 75 + const { src, uri, cid, val, neg, cts, exp } = label; 76 + return { 77 + ver: LABEL_VERSION, 78 + src, 79 + uri, 80 + ...(cid ? { cid } : {}), 81 + val, 82 + ...(neg ? { neg } : {}), 83 + cts, 84 + ...(exp ? { exp } : {}), 85 + } as never; 86 + } 87 + 88 + private async ensureSignedLabel(label: ComAtprotoLabelDefs.Label): Promise<SignedLabel> { 89 + if (!label.sig) { 90 + const signed = await this.signLabel(label); 91 + const stmt = this.db.prepare(` 92 + UPDATE labels 93 + SET sig = ? 94 + WHERE id = ? 95 + `).run(signed.sig, label.id); 96 + if (!stmt.changes) throw new Error("Failed to update label with signature"); 97 + return signed; 98 + } 99 + return this.formatLabel(label) as ComAtprotoLabelDefs.Label & { sig: Uint8Array }; 100 + } 101 + 102 + queryLabelsHandler: express.RequestHandler = async (req, res) => { 103 + try { 104 + const { uriPatterns, sources, limit: limitStr, cursor: cursorStr } = req.query as { 105 + uriPatterns?: Array<string>; 106 + sources?: Array<string>; 107 + limit?: string; 108 + cursor?: string; 109 + }; 110 + 111 + const cursor = cursorStr ? parseInt(cursorStr, 10) : undefined; 112 + if (cursor && Number.isNaN(cursor)) { 113 + throw new InvalidRequestError("Cursor must be an integer"); 114 + } 115 + 116 + const limit = parseInt(limitStr ?? "50", 10); 117 + if (Number.isNaN(limit) || limit < 1 || limit > 250) { 118 + throw new InvalidRequestError("Limit must be an integer between 1 and 250"); 119 + } 120 + 121 + const patterns = uriPatterns?.includes("*") 122 + ? undefined 123 + : uriPatterns?.map((pattern) => { 124 + if (pattern.indexOf("*") !== pattern.length - 1) { 125 + throw new InvalidRequestError( 126 + "Only trailing wildcards are supported in uriPatterns", 127 + ); 128 + } 129 + return pattern.replaceAll(/%/g, "").replaceAll(/_/g, "\\_").slice(0, -1) + "%"; 130 + }); 131 + 132 + const stmt = this.db.prepare<unknown[], ComAtprotoLabelDefs.Label>(` 133 + SELECT * FROM labels 134 + ${patterns?.length ? patterns.map(() => "WHERE uri LIKE ?").join(" OR ") : ""} 135 + ${sources?.length ? "AND src IN (?)" : ""} 136 + ${cursor ? "AND id > ?" : ""} 137 + ORDER BY id ASC 138 + LIMIT ? 139 + `); 140 + 141 + const rows = stmt.all([...(patterns ?? []), sources ?? [], cursor ?? 0, limit]); 142 + 143 + const labels = await Promise.all(rows.map((row) => this.ensureSignedLabel(row))); 144 + const nextCursor = rows[rows.length - 1]?.id ?? 0; 145 + 146 + res.json({ cursor: nextCursor, labels }); 147 + } catch (e) { 148 + if (e instanceof XRPCError) { 149 + res.status(e.type).json(e.payload); 150 + } else { 151 + console.error(e); 152 + res.status(500).json({ 153 + error: "InternalError", 154 + message: e instanceof Error ? e.message : "An unknown error occurred", 155 + }); 156 + } 157 + return; 158 + } 159 + }; 160 + 161 + subscribeLabelsHandler: WebsocketRequestHandler = (ws, req) => { 57 162 const cursor = parseInt(req.params.cursor); 58 163 59 164 if (cursor && !Number.isNaN(cursor)) {
+7
src/util.ts
··· 1 + import type { ComAtprotoLabelDefs } from "@atproto/api"; 2 + 3 + export type StrictPartial<T> = 4 + & { [K in keyof T as undefined extends T[K] ? never : K]: T[K] } 5 + & { [K in keyof T as undefined extends T[K] ? K : never]?: T[K] }; 6 + 7 + export type SignedLabel = ComAtprotoLabelDefs.Label & { sig: Uint8Array };