service status on atproto

feat: create host record automatically, check for service and check records when pushing state

ptr.pet f6eac273 92c567b1

verified
+205 -31
+25
lib/src/index.ts
··· 1 + import { 2 + SystemsGazeBarometerCheck, 3 + SystemsGazeBarometerHost, 4 + SystemsGazeBarometerService, 5 + SystemsGazeBarometerState, 6 + } from "./lexicons/index.js"; 7 + 1 8 export * from "./lexicons/index.js"; 9 + 10 + export const schemas = { 11 + "systems.gaze.barometer.host": SystemsGazeBarometerHost.mainSchema, 12 + "systems.gaze.barometer.service": SystemsGazeBarometerService.mainSchema, 13 + "systems.gaze.barometer.check": SystemsGazeBarometerCheck.mainSchema, 14 + "systems.gaze.barometer.state": SystemsGazeBarometerState.mainSchema, 15 + }; 16 + export const nsid = < 17 + T extends 18 + | typeof SystemsGazeBarometerHost 19 + | typeof SystemsGazeBarometerService 20 + | typeof SystemsGazeBarometerCheck 21 + | typeof SystemsGazeBarometerState, 22 + >( 23 + lex: T, 24 + ): (typeof schemas)[typeof lex.mainSchema.object.shape.$type.expected] => { 25 + return schemas[lex.mainSchema.object.shape.$type.expected]; 26 + };
+8 -3
proxy/src/config.ts
··· 4 4 interface Config { 5 5 repoDid: AtprotoDid; 6 6 appPass: string; 7 + hostName?: string; 8 + hostDescription?: string; 7 9 } 8 10 11 + const required = (value: unknown) => typeof value !== "undefined"; 12 + 9 13 const getConfig = (prefix: string): Config => { 10 14 const get = <Value>( 11 15 name: string, 12 - check: (value: unknown) => boolean = (value) => 13 - typeof value !== "undefined", 16 + check: (value: unknown) => boolean = () => true, 14 17 ): Value => { 15 18 const value = env[`${prefix}${name}`]; 16 19 if (check(value)) { ··· 20 23 }; 21 24 return { 22 25 repoDid: get("REPO_DID", isDid), 23 - appPass: get("APP_PASSWORD"), 26 + appPass: get("APP_PASSWORD", required), 27 + hostName: get("HOST_NAME"), 28 + hostDescription: get("HOST_DESCRIPTION"), 24 29 }; 25 30 }; 26 31
+97 -28
proxy/src/index.ts
··· 1 - import { 2 - Client, 3 - CredentialManager, 4 - ok, 5 - simpleFetchHandler, 6 - } from "@atcute/client"; 1 + import os from "os"; 2 + 3 + import { Client, CredentialManager } from "@atcute/client"; 7 4 import { getPdsEndpoint } from "@atcute/identity"; 8 5 import { 9 6 CompositeDidDocumentResolver, 10 7 PlcDidDocumentResolver, 11 8 WebDidDocumentResolver, 12 9 } from "@atcute/identity-resolver"; 13 - import { isDid, type AtprotoDid } from "@atcute/lexicons/syntax"; 10 + import { 11 + parseCanonicalResourceUri, 12 + parseResourceUri, 13 + type RecordKey, 14 + } from "@atcute/lexicons/syntax"; 14 15 import { config } from "./config"; 15 16 import type {} from "@atcute/atproto"; 16 - import {} from "barometer-lexicon"; 17 - import { SystemsGazeBarometerState } from "barometer-lexicon"; 18 - import { now as generateTid } from "@atcute/tid"; 19 - import { is, safeParse } from "@atcute/lexicons"; 17 + import { safeParse } from "@atcute/lexicons"; 18 + import { 19 + SystemsGazeBarometerState, 20 + SystemsGazeBarometerHost, 21 + SystemsGazeBarometerService, 22 + SystemsGazeBarometerCheck, 23 + } from "barometer-lexicon"; 24 + import { expect, getRecord, putRecord } from "./utils"; 25 + 26 + interface Check { 27 + record: SystemsGazeBarometerCheck.Main; 28 + } 29 + interface Service { 30 + checks: Map<RecordKey, Check>; 31 + record: SystemsGazeBarometerService.Main; 32 + } 33 + const services = new Map<RecordKey, Service>(); 34 + let host: SystemsGazeBarometerHost.Main | null = null; 20 35 21 36 const docResolver = new CompositeDidDocumentResolver({ 22 37 methods: { ··· 36 51 identifier: config.repoDid, 37 52 password: config.appPass, 38 53 }); 39 - const atpClient = new Client({ handler: creds }); 54 + export const atpClient = new Client({ handler: creds }); 55 + 56 + // fetch host record for this host 57 + const maybeRecord = await getRecord( 58 + "systems.gaze.barometer.host", 59 + os.hostname(), 60 + ); 61 + if (maybeRecord.ok) { 62 + host = maybeRecord.value; 63 + } 64 + 65 + // if it doesnt exist we make a new one 66 + if (host === null) { 67 + const hostname = os.hostname(); 68 + await putRecord( 69 + { 70 + $type: "systems.gaze.barometer.host", 71 + name: config.hostName ?? hostname, 72 + description: config.hostDescription, 73 + os: os.platform(), 74 + }, 75 + hostname, 76 + ); 77 + } 40 78 79 + const badRequest = <Error extends { msg: string }>(error: Error) => { 80 + return new Response(JSON.stringify(error), { status: 400 }); 81 + }; 41 82 const server = Bun.serve({ 42 83 routes: { 43 84 "/push": { ··· 47 88 await req.json(), 48 89 ); 49 90 if (!maybeState.ok) { 50 - return new Response( 51 - JSON.stringify({ 52 - msg: `invalid state: ${maybeState.message}`, 53 - issues: maybeState.issues, 54 - }), 55 - { status: 400 }, 56 - ); 91 + return badRequest({ 92 + msg: `invalid state: ${maybeState.message}`, 93 + issues: maybeState.issues, 94 + }); 57 95 } 58 96 const state = maybeState.value; 59 - const result = await ok( 60 - atpClient.post("com.atproto.repo.putRecord", { 61 - input: { 62 - collection: state.$type, 63 - record: state, 64 - repo: config.repoDid, 65 - rkey: generateTid(), 66 - }, 67 - }), 97 + 98 + const serviceAtUri = expect( 99 + parseCanonicalResourceUri(state.forService), 68 100 ); 101 + let service = services.get(serviceAtUri.rkey); 102 + if (!service) { 103 + const serviceRecord = await getRecord( 104 + "systems.gaze.barometer.service", 105 + serviceAtUri.rkey, 106 + ); 107 + if (!serviceRecord.ok) { 108 + return badRequest({ msg: "service was not found" }); 109 + } 110 + service = { 111 + record: serviceRecord.value, 112 + checks: new Map(), 113 + }; 114 + services.set(serviceAtUri.rkey, service); 115 + } 116 + 117 + if (state.generatedBy) { 118 + const checkAtUri = expect( 119 + parseCanonicalResourceUri(state.generatedBy), 120 + ); 121 + let check = service.checks.get(checkAtUri.rkey); 122 + if (!check) { 123 + let checkRecord = await getRecord( 124 + "systems.gaze.barometer.check", 125 + checkAtUri.rkey, 126 + ); 127 + if (!checkRecord.ok) { 128 + return badRequest({ msg: "check record not found" }); 129 + } 130 + check = { 131 + record: checkRecord.value, 132 + }; 133 + service.checks.set(checkAtUri.rkey, check); 134 + } 135 + } 136 + 137 + const result = await putRecord(state); 69 138 return new Response( 70 139 JSON.stringify({ cid: result.cid, uri: result.uri }), 71 140 );
+75
proxy/src/utils.ts
··· 1 + import { safeParse, type InferOutput, type RecordKey } from "@atcute/lexicons"; 2 + import { schemas as BarometerSchemas } from "barometer-lexicon"; 3 + import { config } from "./config"; 4 + import { ok } from "@atcute/client"; 5 + import { now as generateTid } from "@atcute/tid"; 6 + import { atpClient } from "."; 7 + 8 + export type Result<T, E> = 9 + | { 10 + ok: true; 11 + value: T; 12 + } 13 + | { 14 + ok: false; 15 + error: E; 16 + }; 17 + 18 + export const expect = <T, E>( 19 + v: Result<T, E>, 20 + msg: string = "expected result to not be error", 21 + ) => { 22 + if (v.ok) { 23 + return v.value; 24 + } 25 + throw msg; 26 + }; 27 + 28 + export const getRecord = async < 29 + Collection extends keyof typeof BarometerSchemas, 30 + >( 31 + collection: Collection, 32 + rkey: RecordKey, 33 + ): Promise< 34 + Result<InferOutput<(typeof BarometerSchemas)[Collection]>, string> 35 + > => { 36 + let maybeRecord = await atpClient.get("com.atproto.repo.getRecord", { 37 + params: { 38 + collection, 39 + repo: config.repoDid, 40 + rkey, 41 + }, 42 + }); 43 + if (!maybeRecord.ok) { 44 + return { 45 + ok: false, 46 + error: maybeRecord.data.message ?? maybeRecord.data.error, 47 + }; 48 + } 49 + const maybeTyped = safeParse( 50 + BarometerSchemas[collection], 51 + maybeRecord.data.value, 52 + ); 53 + if (!maybeTyped.ok) { 54 + return { ok: false, error: maybeTyped.message }; 55 + } 56 + return maybeTyped; 57 + }; 58 + 59 + export const putRecord = async < 60 + Collection extends keyof typeof BarometerSchemas, 61 + >( 62 + record: InferOutput<(typeof BarometerSchemas)[Collection]>, 63 + rkey?: RecordKey, 64 + ) => { 65 + return await ok( 66 + atpClient.post("com.atproto.repo.putRecord", { 67 + input: { 68 + collection: record["$type"], 69 + repo: config.repoDid, 70 + record, 71 + rkey: rkey ?? generateTid(), 72 + }, 73 + }), 74 + ); 75 + };