service status on atproto

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

ptr.pet f6eac273 92c567b1

verified
Changed files
+205 -31
lib
src
proxy
+25
lib/src/index.ts
···
export * from "./lexicons/index.js";
···
+
import {
+
SystemsGazeBarometerCheck,
+
SystemsGazeBarometerHost,
+
SystemsGazeBarometerService,
+
SystemsGazeBarometerState,
+
} from "./lexicons/index.js";
+
export * from "./lexicons/index.js";
+
+
export const schemas = {
+
"systems.gaze.barometer.host": SystemsGazeBarometerHost.mainSchema,
+
"systems.gaze.barometer.service": SystemsGazeBarometerService.mainSchema,
+
"systems.gaze.barometer.check": SystemsGazeBarometerCheck.mainSchema,
+
"systems.gaze.barometer.state": SystemsGazeBarometerState.mainSchema,
+
};
+
export const nsid = <
+
T extends
+
| typeof SystemsGazeBarometerHost
+
| typeof SystemsGazeBarometerService
+
| typeof SystemsGazeBarometerCheck
+
| typeof SystemsGazeBarometerState,
+
>(
+
lex: T,
+
): (typeof schemas)[typeof lex.mainSchema.object.shape.$type.expected] => {
+
return schemas[lex.mainSchema.object.shape.$type.expected];
+
};
+8 -3
proxy/src/config.ts
···
interface Config {
repoDid: AtprotoDid;
appPass: string;
}
const getConfig = (prefix: string): Config => {
const get = <Value>(
name: string,
-
check: (value: unknown) => boolean = (value) =>
-
typeof value !== "undefined",
): Value => {
const value = env[`${prefix}${name}`];
if (check(value)) {
···
};
return {
repoDid: get("REPO_DID", isDid),
-
appPass: get("APP_PASSWORD"),
};
};
···
interface Config {
repoDid: AtprotoDid;
appPass: string;
+
hostName?: string;
+
hostDescription?: string;
}
+
const required = (value: unknown) => typeof value !== "undefined";
+
const getConfig = (prefix: string): Config => {
const get = <Value>(
name: string,
+
check: (value: unknown) => boolean = () => true,
): Value => {
const value = env[`${prefix}${name}`];
if (check(value)) {
···
};
return {
repoDid: get("REPO_DID", isDid),
+
appPass: get("APP_PASSWORD", required),
+
hostName: get("HOST_NAME"),
+
hostDescription: get("HOST_DESCRIPTION"),
};
};
+97 -28
proxy/src/index.ts
···
-
import {
-
Client,
-
CredentialManager,
-
ok,
-
simpleFetchHandler,
-
} from "@atcute/client";
import { getPdsEndpoint } from "@atcute/identity";
import {
CompositeDidDocumentResolver,
PlcDidDocumentResolver,
WebDidDocumentResolver,
} from "@atcute/identity-resolver";
-
import { isDid, type AtprotoDid } from "@atcute/lexicons/syntax";
import { config } from "./config";
import type {} from "@atcute/atproto";
-
import {} from "barometer-lexicon";
-
import { SystemsGazeBarometerState } from "barometer-lexicon";
-
import { now as generateTid } from "@atcute/tid";
-
import { is, safeParse } from "@atcute/lexicons";
const docResolver = new CompositeDidDocumentResolver({
methods: {
···
identifier: config.repoDid,
password: config.appPass,
});
-
const atpClient = new Client({ handler: creds });
const server = Bun.serve({
routes: {
"/push": {
···
await req.json(),
);
if (!maybeState.ok) {
-
return new Response(
-
JSON.stringify({
-
msg: `invalid state: ${maybeState.message}`,
-
issues: maybeState.issues,
-
}),
-
{ status: 400 },
-
);
}
const state = maybeState.value;
-
const result = await ok(
-
atpClient.post("com.atproto.repo.putRecord", {
-
input: {
-
collection: state.$type,
-
record: state,
-
repo: config.repoDid,
-
rkey: generateTid(),
-
},
-
}),
);
return new Response(
JSON.stringify({ cid: result.cid, uri: result.uri }),
);
···
+
import os from "os";
+
+
import { Client, CredentialManager } from "@atcute/client";
import { getPdsEndpoint } from "@atcute/identity";
import {
CompositeDidDocumentResolver,
PlcDidDocumentResolver,
WebDidDocumentResolver,
} from "@atcute/identity-resolver";
+
import {
+
parseCanonicalResourceUri,
+
parseResourceUri,
+
type RecordKey,
+
} from "@atcute/lexicons/syntax";
import { config } from "./config";
import type {} from "@atcute/atproto";
+
import { safeParse } from "@atcute/lexicons";
+
import {
+
SystemsGazeBarometerState,
+
SystemsGazeBarometerHost,
+
SystemsGazeBarometerService,
+
SystemsGazeBarometerCheck,
+
} from "barometer-lexicon";
+
import { expect, getRecord, putRecord } from "./utils";
+
+
interface Check {
+
record: SystemsGazeBarometerCheck.Main;
+
}
+
interface Service {
+
checks: Map<RecordKey, Check>;
+
record: SystemsGazeBarometerService.Main;
+
}
+
const services = new Map<RecordKey, Service>();
+
let host: SystemsGazeBarometerHost.Main | null = null;
const docResolver = new CompositeDidDocumentResolver({
methods: {
···
identifier: config.repoDid,
password: config.appPass,
});
+
export const atpClient = new Client({ handler: creds });
+
+
// fetch host record for this host
+
const maybeRecord = await getRecord(
+
"systems.gaze.barometer.host",
+
os.hostname(),
+
);
+
if (maybeRecord.ok) {
+
host = maybeRecord.value;
+
}
+
+
// if it doesnt exist we make a new one
+
if (host === null) {
+
const hostname = os.hostname();
+
await putRecord(
+
{
+
$type: "systems.gaze.barometer.host",
+
name: config.hostName ?? hostname,
+
description: config.hostDescription,
+
os: os.platform(),
+
},
+
hostname,
+
);
+
}
+
const badRequest = <Error extends { msg: string }>(error: Error) => {
+
return new Response(JSON.stringify(error), { status: 400 });
+
};
const server = Bun.serve({
routes: {
"/push": {
···
await req.json(),
);
if (!maybeState.ok) {
+
return badRequest({
+
msg: `invalid state: ${maybeState.message}`,
+
issues: maybeState.issues,
+
});
}
const state = maybeState.value;
+
+
const serviceAtUri = expect(
+
parseCanonicalResourceUri(state.forService),
);
+
let service = services.get(serviceAtUri.rkey);
+
if (!service) {
+
const serviceRecord = await getRecord(
+
"systems.gaze.barometer.service",
+
serviceAtUri.rkey,
+
);
+
if (!serviceRecord.ok) {
+
return badRequest({ msg: "service was not found" });
+
}
+
service = {
+
record: serviceRecord.value,
+
checks: new Map(),
+
};
+
services.set(serviceAtUri.rkey, service);
+
}
+
+
if (state.generatedBy) {
+
const checkAtUri = expect(
+
parseCanonicalResourceUri(state.generatedBy),
+
);
+
let check = service.checks.get(checkAtUri.rkey);
+
if (!check) {
+
let checkRecord = await getRecord(
+
"systems.gaze.barometer.check",
+
checkAtUri.rkey,
+
);
+
if (!checkRecord.ok) {
+
return badRequest({ msg: "check record not found" });
+
}
+
check = {
+
record: checkRecord.value,
+
};
+
service.checks.set(checkAtUri.rkey, check);
+
}
+
}
+
+
const result = await putRecord(state);
return new Response(
JSON.stringify({ cid: result.cid, uri: result.uri }),
);
+75
proxy/src/utils.ts
···
···
+
import { safeParse, type InferOutput, type RecordKey } from "@atcute/lexicons";
+
import { schemas as BarometerSchemas } from "barometer-lexicon";
+
import { config } from "./config";
+
import { ok } from "@atcute/client";
+
import { now as generateTid } from "@atcute/tid";
+
import { atpClient } from ".";
+
+
export type Result<T, E> =
+
| {
+
ok: true;
+
value: T;
+
}
+
| {
+
ok: false;
+
error: E;
+
};
+
+
export const expect = <T, E>(
+
v: Result<T, E>,
+
msg: string = "expected result to not be error",
+
) => {
+
if (v.ok) {
+
return v.value;
+
}
+
throw msg;
+
};
+
+
export const getRecord = async <
+
Collection extends keyof typeof BarometerSchemas,
+
>(
+
collection: Collection,
+
rkey: RecordKey,
+
): Promise<
+
Result<InferOutput<(typeof BarometerSchemas)[Collection]>, string>
+
> => {
+
let maybeRecord = await atpClient.get("com.atproto.repo.getRecord", {
+
params: {
+
collection,
+
repo: config.repoDid,
+
rkey,
+
},
+
});
+
if (!maybeRecord.ok) {
+
return {
+
ok: false,
+
error: maybeRecord.data.message ?? maybeRecord.data.error,
+
};
+
}
+
const maybeTyped = safeParse(
+
BarometerSchemas[collection],
+
maybeRecord.data.value,
+
);
+
if (!maybeTyped.ok) {
+
return { ok: false, error: maybeTyped.message };
+
}
+
return maybeTyped;
+
};
+
+
export const putRecord = async <
+
Collection extends keyof typeof BarometerSchemas,
+
>(
+
record: InferOutput<(typeof BarometerSchemas)[Collection]>,
+
rkey?: RecordKey,
+
) => {
+
return await ok(
+
atpClient.post("com.atproto.repo.putRecord", {
+
input: {
+
collection: record["$type"],
+
repo: config.repoDid,
+
record,
+
rkey: rkey ?? generateTid(),
+
},
+
}),
+
);
+
};