@atiproto/record-resolver 0.1.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.
@@ -0,0 +1,20 @@
1
+ import { type RecordMap } from "./types.js";
2
+ /**
3
+ * Minimal XRPC-shaped agent. Matches `@atiproto/agent.Agent`,
4
+ * `@atproto/api.Agent`, and bare `@atproto/xrpc.XrpcClient`.
5
+ */
6
+ export interface RecordResolverAgent {
7
+ call(nsid: string, params?: unknown, data?: unknown): Promise<{
8
+ data: unknown;
9
+ }>;
10
+ }
11
+ /**
12
+ * Resolves `at://` URIs to record values via an existing XRPC client.
13
+ * Auth, retries, and proxying live on the client — this resolver just
14
+ * routes the `com.atproto.repo.getRecord` call through it.
15
+ */
16
+ export declare class AgentRecordResolver {
17
+ private readonly agent;
18
+ constructor(agent: RecordResolverAgent);
19
+ resolve: (uri: string) => Promise<RecordMap>;
20
+ }
@@ -0,0 +1,27 @@
1
+ import type { SimpleStore } from "@atproto-labs/simple-store";
2
+ import { type FetchRecordResolverOptions } from "./FetchRecordResolver.js";
3
+ import type { RecordMap } from "./types.js";
4
+ export interface EdgeRecordResolverOptions extends FetchRecordResolverOptions {
5
+ /**
6
+ * Cache keyed by full `at://` URI. Defaults to an in-memory store
7
+ * with a 24-hour TTL. For Cloudflare Workers, pass a tiered store
8
+ * (e.g. memory + CacheApiStore from `@atiproto/edge-resolver-cache`)
9
+ * so proof records persist across requests within a colo.
10
+ *
11
+ * Caching is safe because proof records are content-addressed by
12
+ * their strongRef CID — once written, the value at a URI never
13
+ * changes.
14
+ */
15
+ cache?: SimpleStore<string, RecordMap>;
16
+ }
17
+ /**
18
+ * `FetchRecordResolver` with a `SimpleStore` cache in front of the
19
+ * `getRecord` call. The default cache is in-memory; pass a tiered
20
+ * store for cross-request persistence on the edge.
21
+ */
22
+ export declare class EdgeRecordResolver {
23
+ private readonly fetcher;
24
+ private readonly cache;
25
+ constructor(options?: EdgeRecordResolverOptions);
26
+ resolve: (uri: string) => Promise<RecordMap>;
27
+ }
@@ -0,0 +1,27 @@
1
+ import { type RecordMap } from "./types.js";
2
+ export interface FetchRecordResolverOptions {
3
+ /**
4
+ * Base URL of the relay (or PDS) used to fetch records. Calls land on
5
+ * `${relay}/xrpc/com.atproto.repo.getRecord`. Default:
6
+ * `https://bsky.network`.
7
+ */
8
+ relay?: string;
9
+ /** Request timeout in milliseconds. Default: `3000`. */
10
+ timeout?: number;
11
+ /** Override the global `fetch` (for tests, custom transports). */
12
+ fetch?: typeof fetch;
13
+ }
14
+ /**
15
+ * Resolves `at://` URIs to record values by calling
16
+ * `com.atproto.repo.getRecord` on a configurable relay.
17
+ *
18
+ * Suitable for verifying remote attestations from anywhere with HTTPS —
19
+ * no auth required since proof records are public.
20
+ */
21
+ export declare class FetchRecordResolver {
22
+ private readonly relay;
23
+ private readonly timeout;
24
+ private readonly fetchImpl;
25
+ constructor(options?: FetchRecordResolverOptions);
26
+ resolve: (uri: string) => Promise<RecordMap>;
27
+ }
@@ -0,0 +1,15 @@
1
+ import { type RecordResolver } from "./types.js";
2
+ /**
3
+ * Minimal XRPC-shaped agent. Matches `@atiproto/agent.Agent`,
4
+ * `@atproto/api.Agent`, and bare `@atproto/xrpc.XrpcClient`.
5
+ */
6
+ export interface RecordResolverAgent {
7
+ call(nsid: string, params?: unknown, data?: unknown): Promise<{
8
+ data: unknown;
9
+ }>;
10
+ }
11
+ /**
12
+ * Builds a RecordResolver that routes `getRecord` through an existing
13
+ * XRPC client. Auth, retries, and proxying live on the client.
14
+ */
15
+ export declare function createAgentRecordResolver(agent: RecordResolverAgent): RecordResolver;
@@ -0,0 +1,18 @@
1
+ import type { SimpleStore } from "@atproto-labs/simple-store";
2
+ import { type FetchRecordResolverOptions } from "./createFetchRecordResolver.js";
3
+ import type { RecordMap, RecordResolver } from "./types.js";
4
+ export interface CachedRecordResolverOptions extends FetchRecordResolverOptions {
5
+ /**
6
+ * Cache keyed by full `at://` URI. Defaults to an in-memory store
7
+ * with a 24-hour TTL bounded at 1000 entries. Proof records are
8
+ * content-addressed by their strongRef CID, so caching by URI is
9
+ * safe for an unlimited duration.
10
+ */
11
+ cache?: SimpleStore<string, RecordMap>;
12
+ }
13
+ /**
14
+ * Builds a RecordResolver that wraps `createFetchRecordResolver` with
15
+ * a `SimpleStore` cache. Safe by virtue of content-addressed
16
+ * strongRefs.
17
+ */
18
+ export declare function createCachedRecordResolver(options?: CachedRecordResolverOptions): RecordResolver;
@@ -0,0 +1,18 @@
1
+ import type { SimpleStore } from "@atproto-labs/simple-store";
2
+ import { type FetchRecordResolverOptions } from "./createFetchRecordResolver.js";
3
+ import type { RecordMap, RecordResolver } from "./types.js";
4
+ export interface EdgeRecordResolverOptions extends FetchRecordResolverOptions {
5
+ /**
6
+ * Cache keyed by full `at://` URI. Defaults to an in-memory store
7
+ * with a 24-hour TTL bounded at 1000 entries. Proof records are
8
+ * content-addressed by their strongRef CID, so caching by URI is
9
+ * safe for an unlimited duration.
10
+ */
11
+ cache?: SimpleStore<string, RecordMap>;
12
+ }
13
+ /**
14
+ * Builds a RecordResolver that wraps `createFetchRecordResolver` with
15
+ * a `SimpleStore` cache. Safe by virtue of content-addressed
16
+ * strongRefs.
17
+ */
18
+ export declare function createEdgeRecordResolver(options?: EdgeRecordResolverOptions): RecordResolver;
@@ -0,0 +1,18 @@
1
+ import { type RecordResolver } from "./types.js";
2
+ export interface FetchRecordResolverOptions {
3
+ /**
4
+ * Base URL of the relay (or PDS) used to fetch records. Calls land
5
+ * on `${relay}/xrpc/com.atproto.repo.getRecord`. Default:
6
+ * `https://bsky.network`.
7
+ */
8
+ relay?: string;
9
+ /** Request timeout in milliseconds. Default: `3000`. */
10
+ timeout?: number;
11
+ /** Override the global `fetch` (for tests, custom transports). */
12
+ fetch?: typeof fetch;
13
+ }
14
+ /**
15
+ * Builds a RecordResolver that calls `com.atproto.repo.getRecord` on a
16
+ * configurable relay (or PDS) over plain `fetch`. No auth.
17
+ */
18
+ export declare function createFetchRecordResolver(options?: FetchRecordResolverOptions): RecordResolver;
@@ -0,0 +1,5 @@
1
+ export { createFetchRecordResolver, type FetchRecordResolverOptions, } from "./createFetchRecordResolver.js";
2
+ export { createAgentRecordResolver, type RecordResolverAgent, } from "./createAgentRecordResolver.js";
3
+ export { createCachedRecordResolver, type CachedRecordResolverOptions, } from "./createCachedRecordResolver.js";
4
+ export { parseAtUri, type ParsedAtUri } from "./types.js";
5
+ export type { RecordMap, RecordResolver } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ createAgentRecordResolver: () => createAgentRecordResolver,
24
+ createCachedRecordResolver: () => createCachedRecordResolver,
25
+ createFetchRecordResolver: () => createFetchRecordResolver,
26
+ parseAtUri: () => parseAtUri
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+
30
+ // src/types.ts
31
+ function parseAtUri(uri) {
32
+ if (!uri.startsWith("at://")) {
33
+ throw new Error(`Not an at:// URI: ${uri}`);
34
+ }
35
+ const [repo, collection, rkey] = uri.slice("at://".length).split("/");
36
+ if (!repo || !collection || !rkey) {
37
+ throw new Error(`Malformed at:// URI: ${uri}`);
38
+ }
39
+ return { repo, collection, rkey };
40
+ }
41
+
42
+ // src/createFetchRecordResolver.ts
43
+ var DEFAULT_RELAY = "https://bsky.network";
44
+ var DEFAULT_TIMEOUT = 3e3;
45
+ function createFetchRecordResolver(options = {}) {
46
+ const relay = options.relay ?? DEFAULT_RELAY;
47
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT;
48
+ const fetchImpl = options.fetch ?? fetch;
49
+ return async (uri) => {
50
+ const { repo, collection, rkey } = parseAtUri(uri);
51
+ const url = new URL("/xrpc/com.atproto.repo.getRecord", relay);
52
+ url.searchParams.set("repo", repo);
53
+ url.searchParams.set("collection", collection);
54
+ url.searchParams.set("rkey", rkey);
55
+ const controller = new AbortController();
56
+ const timer = setTimeout(() => controller.abort(), timeout);
57
+ try {
58
+ const res = await fetchImpl(url, {
59
+ signal: controller.signal,
60
+ headers: { accept: "application/json" }
61
+ });
62
+ if (!res.ok) {
63
+ throw new Error(
64
+ `getRecord ${repo}/${collection}/${rkey} -> ${res.status}`
65
+ );
66
+ }
67
+ const body = await res.json();
68
+ if (!body.value || typeof body.value !== "object") {
69
+ throw new Error(`getRecord response missing value: ${uri}`);
70
+ }
71
+ return body.value;
72
+ } finally {
73
+ clearTimeout(timer);
74
+ }
75
+ };
76
+ }
77
+
78
+ // src/createAgentRecordResolver.ts
79
+ function createAgentRecordResolver(agent) {
80
+ return async (uri) => {
81
+ const { repo, collection, rkey } = parseAtUri(uri);
82
+ const res = await agent.call("com.atproto.repo.getRecord", {
83
+ repo,
84
+ collection,
85
+ rkey
86
+ });
87
+ const data = res.data;
88
+ if (!data?.value || typeof data.value !== "object") {
89
+ throw new Error(`getRecord response missing value: ${uri}`);
90
+ }
91
+ return data.value;
92
+ };
93
+ }
94
+
95
+ // src/createCachedRecordResolver.ts
96
+ var import_simple_store_memory = require("@atproto-labs/simple-store-memory");
97
+ function createCachedRecordResolver(options = {}) {
98
+ const fetcher = createFetchRecordResolver(options);
99
+ const cache = options.cache ?? new import_simple_store_memory.SimpleStoreMemory({
100
+ ttl: 864e5,
101
+ ttlAutopurge: true,
102
+ max: 1e3
103
+ });
104
+ return async (uri) => {
105
+ const cached = await cache.get(uri);
106
+ if (cached) return cached;
107
+ const record = await fetcher(uri);
108
+ void Promise.resolve(cache.set(uri, record)).catch(() => {
109
+ });
110
+ return record;
111
+ };
112
+ }
113
+ // Annotate the CommonJS export names for ESM import in node:
114
+ 0 && (module.exports = {
115
+ createAgentRecordResolver,
116
+ createCachedRecordResolver,
117
+ createFetchRecordResolver,
118
+ parseAtUri
119
+ });
120
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/types.ts","../src/createFetchRecordResolver.ts","../src/createAgentRecordResolver.ts","../src/createCachedRecordResolver.ts"],"sourcesContent":["export {\n createFetchRecordResolver,\n type FetchRecordResolverOptions,\n} from \"./createFetchRecordResolver.js\";\nexport {\n createAgentRecordResolver,\n type RecordResolverAgent,\n} from \"./createAgentRecordResolver.js\";\nexport {\n createCachedRecordResolver,\n type CachedRecordResolverOptions,\n} from \"./createCachedRecordResolver.js\";\nexport { parseAtUri, type ParsedAtUri } from \"./types.js\";\nexport type { RecordMap, RecordResolver } from \"./types.js\";\n","import type { RecordMap, RecordResolver } from \"@atiproto/atproto-attestation\";\n\nexport type { RecordMap, RecordResolver };\n\n/**\n * Parsed pieces of an `at://{repo}/{collection}/{rkey}` URI.\n */\nexport interface ParsedAtUri {\n repo: string;\n collection: string;\n rkey: string;\n}\n\nexport function parseAtUri(uri: string): ParsedAtUri {\n if (!uri.startsWith(\"at://\")) {\n throw new Error(`Not an at:// URI: ${uri}`);\n }\n const [repo, collection, rkey] = uri.slice(\"at://\".length).split(\"/\");\n if (!repo || !collection || !rkey) {\n throw new Error(`Malformed at:// URI: ${uri}`);\n }\n return { repo, collection, rkey };\n}\n","import { parseAtUri, type RecordMap, type RecordResolver } from \"./types.js\";\n\nexport interface FetchRecordResolverOptions {\n /**\n * Base URL of the relay (or PDS) used to fetch records. Calls land\n * on `${relay}/xrpc/com.atproto.repo.getRecord`. Default:\n * `https://bsky.network`.\n */\n relay?: string;\n /** Request timeout in milliseconds. Default: `3000`. */\n timeout?: number;\n /** Override the global `fetch` (for tests, custom transports). */\n fetch?: typeof fetch;\n}\n\nconst DEFAULT_RELAY = \"https://bsky.network\";\nconst DEFAULT_TIMEOUT = 3000;\n\n/**\n * Builds a RecordResolver that calls `com.atproto.repo.getRecord` on a\n * configurable relay (or PDS) over plain `fetch`. No auth.\n */\nexport function createFetchRecordResolver(\n options: FetchRecordResolverOptions = {},\n): RecordResolver {\n const relay = options.relay ?? DEFAULT_RELAY;\n const timeout = options.timeout ?? DEFAULT_TIMEOUT;\n const fetchImpl = options.fetch ?? fetch;\n\n return async (uri) => {\n const { repo, collection, rkey } = parseAtUri(uri);\n\n const url = new URL(\"/xrpc/com.atproto.repo.getRecord\", relay);\n url.searchParams.set(\"repo\", repo);\n url.searchParams.set(\"collection\", collection);\n url.searchParams.set(\"rkey\", rkey);\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeout);\n try {\n const res = await fetchImpl(url, {\n signal: controller.signal,\n headers: { accept: \"application/json\" },\n });\n if (!res.ok) {\n throw new Error(\n `getRecord ${repo}/${collection}/${rkey} -> ${res.status}`,\n );\n }\n const body = (await res.json()) as { value?: RecordMap };\n if (!body.value || typeof body.value !== \"object\") {\n throw new Error(`getRecord response missing value: ${uri}`);\n }\n return body.value;\n } finally {\n clearTimeout(timer);\n }\n };\n}\n","import { parseAtUri, type RecordMap, type RecordResolver } from \"./types.js\";\n\n/**\n * Minimal XRPC-shaped agent. Matches `@atiproto/agent.Agent`,\n * `@atproto/api.Agent`, and bare `@atproto/xrpc.XrpcClient`.\n */\nexport interface RecordResolverAgent {\n call(\n nsid: string,\n params?: unknown,\n data?: unknown,\n ): Promise<{ data: unknown }>;\n}\n\n/**\n * Builds a RecordResolver that routes `getRecord` through an existing\n * XRPC client. Auth, retries, and proxying live on the client.\n */\nexport function createAgentRecordResolver(\n agent: RecordResolverAgent,\n): RecordResolver {\n return async (uri) => {\n const { repo, collection, rkey } = parseAtUri(uri);\n const res = await agent.call(\"com.atproto.repo.getRecord\", {\n repo,\n collection,\n rkey,\n });\n const data = res.data as { value?: RecordMap } | undefined;\n if (!data?.value || typeof data.value !== \"object\") {\n throw new Error(`getRecord response missing value: ${uri}`);\n }\n return data.value;\n };\n}\n","import type { SimpleStore } from \"@atproto-labs/simple-store\";\nimport { SimpleStoreMemory } from \"@atproto-labs/simple-store-memory\";\nimport {\n createFetchRecordResolver,\n type FetchRecordResolverOptions,\n} from \"./createFetchRecordResolver.js\";\nimport type { RecordMap, RecordResolver } from \"./types.js\";\n\nexport interface CachedRecordResolverOptions\n extends FetchRecordResolverOptions {\n /**\n * Cache keyed by full `at://` URI. Defaults to an in-memory store\n * with a 24-hour TTL bounded at 1000 entries. Proof records are\n * content-addressed by their strongRef CID, so caching by URI is\n * safe for an unlimited duration.\n */\n cache?: SimpleStore<string, RecordMap>;\n}\n\n/**\n * Builds a RecordResolver that wraps `createFetchRecordResolver` with\n * a `SimpleStore` cache. Safe by virtue of content-addressed\n * strongRefs.\n */\nexport function createCachedRecordResolver(\n options: CachedRecordResolverOptions = {},\n): RecordResolver {\n const fetcher = createFetchRecordResolver(options);\n const cache =\n options.cache ??\n new SimpleStoreMemory<string, RecordMap>({\n ttl: 86_400_000,\n ttlAutopurge: true,\n max: 1000,\n });\n\n return async (uri) => {\n const cached = await cache.get(uri);\n if (cached) return cached;\n const record = await fetcher(uri);\n void Promise.resolve(cache.set(uri, record)).catch(() => {});\n return record;\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACaO,SAAS,WAAW,KAA0B;AACnD,MAAI,CAAC,IAAI,WAAW,OAAO,GAAG;AAC5B,UAAM,IAAI,MAAM,qBAAqB,GAAG,EAAE;AAAA,EAC5C;AACA,QAAM,CAAC,MAAM,YAAY,IAAI,IAAI,IAAI,MAAM,QAAQ,MAAM,EAAE,MAAM,GAAG;AACpE,MAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM;AACjC,UAAM,IAAI,MAAM,wBAAwB,GAAG,EAAE;AAAA,EAC/C;AACA,SAAO,EAAE,MAAM,YAAY,KAAK;AAClC;;;ACPA,IAAM,gBAAgB;AACtB,IAAM,kBAAkB;AAMjB,SAAS,0BACd,UAAsC,CAAC,GACvB;AAChB,QAAM,QAAQ,QAAQ,SAAS;AAC/B,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,YAAY,QAAQ,SAAS;AAEnC,SAAO,OAAO,QAAQ;AACpB,UAAM,EAAE,MAAM,YAAY,KAAK,IAAI,WAAW,GAAG;AAEjD,UAAM,MAAM,IAAI,IAAI,oCAAoC,KAAK;AAC7D,QAAI,aAAa,IAAI,QAAQ,IAAI;AACjC,QAAI,aAAa,IAAI,cAAc,UAAU;AAC7C,QAAI,aAAa,IAAI,QAAQ,IAAI;AAEjC,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,OAAO;AAC1D,QAAI;AACF,YAAM,MAAM,MAAM,UAAU,KAAK;AAAA,QAC/B,QAAQ,WAAW;AAAA,QACnB,SAAS,EAAE,QAAQ,mBAAmB;AAAA,MACxC,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI;AAAA,UACR,aAAa,IAAI,IAAI,UAAU,IAAI,IAAI,OAAO,IAAI,MAAM;AAAA,QAC1D;AAAA,MACF;AACA,YAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,UAAI,CAAC,KAAK,SAAS,OAAO,KAAK,UAAU,UAAU;AACjD,cAAM,IAAI,MAAM,qCAAqC,GAAG,EAAE;AAAA,MAC5D;AACA,aAAO,KAAK;AAAA,IACd,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF;AACF;;;ACxCO,SAAS,0BACd,OACgB;AAChB,SAAO,OAAO,QAAQ;AACpB,UAAM,EAAE,MAAM,YAAY,KAAK,IAAI,WAAW,GAAG;AACjD,UAAM,MAAM,MAAM,MAAM,KAAK,8BAA8B;AAAA,MACzD;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AACD,UAAM,OAAO,IAAI;AACjB,QAAI,CAAC,MAAM,SAAS,OAAO,KAAK,UAAU,UAAU;AAClD,YAAM,IAAI,MAAM,qCAAqC,GAAG,EAAE;AAAA,IAC5D;AACA,WAAO,KAAK;AAAA,EACd;AACF;;;ACjCA,iCAAkC;AAuB3B,SAAS,2BACd,UAAuC,CAAC,GACxB;AAChB,QAAM,UAAU,0BAA0B,OAAO;AACjD,QAAM,QACJ,QAAQ,SACR,IAAI,6CAAqC;AAAA,IACvC,KAAK;AAAA,IACL,cAAc;AAAA,IACd,KAAK;AAAA,EACP,CAAC;AAEH,SAAO,OAAO,QAAQ;AACpB,UAAM,SAAS,MAAM,MAAM,IAAI,GAAG;AAClC,QAAI,OAAQ,QAAO;AACnB,UAAM,SAAS,MAAM,QAAQ,GAAG;AAChC,SAAK,QAAQ,QAAQ,MAAM,IAAI,KAAK,MAAM,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAC3D,WAAO;AAAA,EACT;AACF;","names":[]}
package/dist/index.mjs ADDED
@@ -0,0 +1,90 @@
1
+ // src/types.ts
2
+ function parseAtUri(uri) {
3
+ if (!uri.startsWith("at://")) {
4
+ throw new Error(`Not an at:// URI: ${uri}`);
5
+ }
6
+ const [repo, collection, rkey] = uri.slice("at://".length).split("/");
7
+ if (!repo || !collection || !rkey) {
8
+ throw new Error(`Malformed at:// URI: ${uri}`);
9
+ }
10
+ return { repo, collection, rkey };
11
+ }
12
+
13
+ // src/createFetchRecordResolver.ts
14
+ var DEFAULT_RELAY = "https://bsky.network";
15
+ var DEFAULT_TIMEOUT = 3e3;
16
+ function createFetchRecordResolver(options = {}) {
17
+ const relay = options.relay ?? DEFAULT_RELAY;
18
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT;
19
+ const fetchImpl = options.fetch ?? fetch;
20
+ return async (uri) => {
21
+ const { repo, collection, rkey } = parseAtUri(uri);
22
+ const url = new URL("/xrpc/com.atproto.repo.getRecord", relay);
23
+ url.searchParams.set("repo", repo);
24
+ url.searchParams.set("collection", collection);
25
+ url.searchParams.set("rkey", rkey);
26
+ const controller = new AbortController();
27
+ const timer = setTimeout(() => controller.abort(), timeout);
28
+ try {
29
+ const res = await fetchImpl(url, {
30
+ signal: controller.signal,
31
+ headers: { accept: "application/json" }
32
+ });
33
+ if (!res.ok) {
34
+ throw new Error(
35
+ `getRecord ${repo}/${collection}/${rkey} -> ${res.status}`
36
+ );
37
+ }
38
+ const body = await res.json();
39
+ if (!body.value || typeof body.value !== "object") {
40
+ throw new Error(`getRecord response missing value: ${uri}`);
41
+ }
42
+ return body.value;
43
+ } finally {
44
+ clearTimeout(timer);
45
+ }
46
+ };
47
+ }
48
+
49
+ // src/createAgentRecordResolver.ts
50
+ function createAgentRecordResolver(agent) {
51
+ return async (uri) => {
52
+ const { repo, collection, rkey } = parseAtUri(uri);
53
+ const res = await agent.call("com.atproto.repo.getRecord", {
54
+ repo,
55
+ collection,
56
+ rkey
57
+ });
58
+ const data = res.data;
59
+ if (!data?.value || typeof data.value !== "object") {
60
+ throw new Error(`getRecord response missing value: ${uri}`);
61
+ }
62
+ return data.value;
63
+ };
64
+ }
65
+
66
+ // src/createCachedRecordResolver.ts
67
+ import { SimpleStoreMemory } from "@atproto-labs/simple-store-memory";
68
+ function createCachedRecordResolver(options = {}) {
69
+ const fetcher = createFetchRecordResolver(options);
70
+ const cache = options.cache ?? new SimpleStoreMemory({
71
+ ttl: 864e5,
72
+ ttlAutopurge: true,
73
+ max: 1e3
74
+ });
75
+ return async (uri) => {
76
+ const cached = await cache.get(uri);
77
+ if (cached) return cached;
78
+ const record = await fetcher(uri);
79
+ void Promise.resolve(cache.set(uri, record)).catch(() => {
80
+ });
81
+ return record;
82
+ };
83
+ }
84
+ export {
85
+ createAgentRecordResolver,
86
+ createCachedRecordResolver,
87
+ createFetchRecordResolver,
88
+ parseAtUri
89
+ };
90
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/types.ts","../src/createFetchRecordResolver.ts","../src/createAgentRecordResolver.ts","../src/createCachedRecordResolver.ts"],"sourcesContent":["import type { RecordMap, RecordResolver } from \"@atiproto/atproto-attestation\";\n\nexport type { RecordMap, RecordResolver };\n\n/**\n * Parsed pieces of an `at://{repo}/{collection}/{rkey}` URI.\n */\nexport interface ParsedAtUri {\n repo: string;\n collection: string;\n rkey: string;\n}\n\nexport function parseAtUri(uri: string): ParsedAtUri {\n if (!uri.startsWith(\"at://\")) {\n throw new Error(`Not an at:// URI: ${uri}`);\n }\n const [repo, collection, rkey] = uri.slice(\"at://\".length).split(\"/\");\n if (!repo || !collection || !rkey) {\n throw new Error(`Malformed at:// URI: ${uri}`);\n }\n return { repo, collection, rkey };\n}\n","import { parseAtUri, type RecordMap, type RecordResolver } from \"./types.js\";\n\nexport interface FetchRecordResolverOptions {\n /**\n * Base URL of the relay (or PDS) used to fetch records. Calls land\n * on `${relay}/xrpc/com.atproto.repo.getRecord`. Default:\n * `https://bsky.network`.\n */\n relay?: string;\n /** Request timeout in milliseconds. Default: `3000`. */\n timeout?: number;\n /** Override the global `fetch` (for tests, custom transports). */\n fetch?: typeof fetch;\n}\n\nconst DEFAULT_RELAY = \"https://bsky.network\";\nconst DEFAULT_TIMEOUT = 3000;\n\n/**\n * Builds a RecordResolver that calls `com.atproto.repo.getRecord` on a\n * configurable relay (or PDS) over plain `fetch`. No auth.\n */\nexport function createFetchRecordResolver(\n options: FetchRecordResolverOptions = {},\n): RecordResolver {\n const relay = options.relay ?? DEFAULT_RELAY;\n const timeout = options.timeout ?? DEFAULT_TIMEOUT;\n const fetchImpl = options.fetch ?? fetch;\n\n return async (uri) => {\n const { repo, collection, rkey } = parseAtUri(uri);\n\n const url = new URL(\"/xrpc/com.atproto.repo.getRecord\", relay);\n url.searchParams.set(\"repo\", repo);\n url.searchParams.set(\"collection\", collection);\n url.searchParams.set(\"rkey\", rkey);\n\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeout);\n try {\n const res = await fetchImpl(url, {\n signal: controller.signal,\n headers: { accept: \"application/json\" },\n });\n if (!res.ok) {\n throw new Error(\n `getRecord ${repo}/${collection}/${rkey} -> ${res.status}`,\n );\n }\n const body = (await res.json()) as { value?: RecordMap };\n if (!body.value || typeof body.value !== \"object\") {\n throw new Error(`getRecord response missing value: ${uri}`);\n }\n return body.value;\n } finally {\n clearTimeout(timer);\n }\n };\n}\n","import { parseAtUri, type RecordMap, type RecordResolver } from \"./types.js\";\n\n/**\n * Minimal XRPC-shaped agent. Matches `@atiproto/agent.Agent`,\n * `@atproto/api.Agent`, and bare `@atproto/xrpc.XrpcClient`.\n */\nexport interface RecordResolverAgent {\n call(\n nsid: string,\n params?: unknown,\n data?: unknown,\n ): Promise<{ data: unknown }>;\n}\n\n/**\n * Builds a RecordResolver that routes `getRecord` through an existing\n * XRPC client. Auth, retries, and proxying live on the client.\n */\nexport function createAgentRecordResolver(\n agent: RecordResolverAgent,\n): RecordResolver {\n return async (uri) => {\n const { repo, collection, rkey } = parseAtUri(uri);\n const res = await agent.call(\"com.atproto.repo.getRecord\", {\n repo,\n collection,\n rkey,\n });\n const data = res.data as { value?: RecordMap } | undefined;\n if (!data?.value || typeof data.value !== \"object\") {\n throw new Error(`getRecord response missing value: ${uri}`);\n }\n return data.value;\n };\n}\n","import type { SimpleStore } from \"@atproto-labs/simple-store\";\nimport { SimpleStoreMemory } from \"@atproto-labs/simple-store-memory\";\nimport {\n createFetchRecordResolver,\n type FetchRecordResolverOptions,\n} from \"./createFetchRecordResolver.js\";\nimport type { RecordMap, RecordResolver } from \"./types.js\";\n\nexport interface CachedRecordResolverOptions\n extends FetchRecordResolverOptions {\n /**\n * Cache keyed by full `at://` URI. Defaults to an in-memory store\n * with a 24-hour TTL bounded at 1000 entries. Proof records are\n * content-addressed by their strongRef CID, so caching by URI is\n * safe for an unlimited duration.\n */\n cache?: SimpleStore<string, RecordMap>;\n}\n\n/**\n * Builds a RecordResolver that wraps `createFetchRecordResolver` with\n * a `SimpleStore` cache. Safe by virtue of content-addressed\n * strongRefs.\n */\nexport function createCachedRecordResolver(\n options: CachedRecordResolverOptions = {},\n): RecordResolver {\n const fetcher = createFetchRecordResolver(options);\n const cache =\n options.cache ??\n new SimpleStoreMemory<string, RecordMap>({\n ttl: 86_400_000,\n ttlAutopurge: true,\n max: 1000,\n });\n\n return async (uri) => {\n const cached = await cache.get(uri);\n if (cached) return cached;\n const record = await fetcher(uri);\n void Promise.resolve(cache.set(uri, record)).catch(() => {});\n return record;\n };\n}\n"],"mappings":";AAaO,SAAS,WAAW,KAA0B;AACnD,MAAI,CAAC,IAAI,WAAW,OAAO,GAAG;AAC5B,UAAM,IAAI,MAAM,qBAAqB,GAAG,EAAE;AAAA,EAC5C;AACA,QAAM,CAAC,MAAM,YAAY,IAAI,IAAI,IAAI,MAAM,QAAQ,MAAM,EAAE,MAAM,GAAG;AACpE,MAAI,CAAC,QAAQ,CAAC,cAAc,CAAC,MAAM;AACjC,UAAM,IAAI,MAAM,wBAAwB,GAAG,EAAE;AAAA,EAC/C;AACA,SAAO,EAAE,MAAM,YAAY,KAAK;AAClC;;;ACPA,IAAM,gBAAgB;AACtB,IAAM,kBAAkB;AAMjB,SAAS,0BACd,UAAsC,CAAC,GACvB;AAChB,QAAM,QAAQ,QAAQ,SAAS;AAC/B,QAAM,UAAU,QAAQ,WAAW;AACnC,QAAM,YAAY,QAAQ,SAAS;AAEnC,SAAO,OAAO,QAAQ;AACpB,UAAM,EAAE,MAAM,YAAY,KAAK,IAAI,WAAW,GAAG;AAEjD,UAAM,MAAM,IAAI,IAAI,oCAAoC,KAAK;AAC7D,QAAI,aAAa,IAAI,QAAQ,IAAI;AACjC,QAAI,aAAa,IAAI,cAAc,UAAU;AAC7C,QAAI,aAAa,IAAI,QAAQ,IAAI;AAEjC,UAAM,aAAa,IAAI,gBAAgB;AACvC,UAAM,QAAQ,WAAW,MAAM,WAAW,MAAM,GAAG,OAAO;AAC1D,QAAI;AACF,YAAM,MAAM,MAAM,UAAU,KAAK;AAAA,QAC/B,QAAQ,WAAW;AAAA,QACnB,SAAS,EAAE,QAAQ,mBAAmB;AAAA,MACxC,CAAC;AACD,UAAI,CAAC,IAAI,IAAI;AACX,cAAM,IAAI;AAAA,UACR,aAAa,IAAI,IAAI,UAAU,IAAI,IAAI,OAAO,IAAI,MAAM;AAAA,QAC1D;AAAA,MACF;AACA,YAAM,OAAQ,MAAM,IAAI,KAAK;AAC7B,UAAI,CAAC,KAAK,SAAS,OAAO,KAAK,UAAU,UAAU;AACjD,cAAM,IAAI,MAAM,qCAAqC,GAAG,EAAE;AAAA,MAC5D;AACA,aAAO,KAAK;AAAA,IACd,UAAE;AACA,mBAAa,KAAK;AAAA,IACpB;AAAA,EACF;AACF;;;ACxCO,SAAS,0BACd,OACgB;AAChB,SAAO,OAAO,QAAQ;AACpB,UAAM,EAAE,MAAM,YAAY,KAAK,IAAI,WAAW,GAAG;AACjD,UAAM,MAAM,MAAM,MAAM,KAAK,8BAA8B;AAAA,MACzD;AAAA,MACA;AAAA,MACA;AAAA,IACF,CAAC;AACD,UAAM,OAAO,IAAI;AACjB,QAAI,CAAC,MAAM,SAAS,OAAO,KAAK,UAAU,UAAU;AAClD,YAAM,IAAI,MAAM,qCAAqC,GAAG,EAAE;AAAA,IAC5D;AACA,WAAO,KAAK;AAAA,EACd;AACF;;;ACjCA,SAAS,yBAAyB;AAuB3B,SAAS,2BACd,UAAuC,CAAC,GACxB;AAChB,QAAM,UAAU,0BAA0B,OAAO;AACjD,QAAM,QACJ,QAAQ,SACR,IAAI,kBAAqC;AAAA,IACvC,KAAK;AAAA,IACL,cAAc;AAAA,IACd,KAAK;AAAA,EACP,CAAC;AAEH,SAAO,OAAO,QAAQ;AACpB,UAAM,SAAS,MAAM,MAAM,IAAI,GAAG;AAClC,QAAI,OAAQ,QAAO;AACnB,UAAM,SAAS,MAAM,QAAQ,GAAG;AAChC,SAAK,QAAQ,QAAQ,MAAM,IAAI,KAAK,MAAM,CAAC,EAAE,MAAM,MAAM;AAAA,IAAC,CAAC;AAC3D,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -0,0 +1,11 @@
1
+ import type { RecordMap, RecordResolver } from "@atiproto/atproto-attestation";
2
+ export type { RecordMap, RecordResolver };
3
+ /**
4
+ * Parsed pieces of an `at://{repo}/{collection}/{rkey}` URI.
5
+ */
6
+ export interface ParsedAtUri {
7
+ repo: string;
8
+ collection: string;
9
+ rkey: string;
10
+ }
11
+ export declare function parseAtUri(uri: string): ParsedAtUri;
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@atiproto/record-resolver",
3
+ "version": "0.1.0",
4
+ "description": "RecordResolver implementations for @atiproto/atproto-attestation: fetch-backed XRPC, agent-backed, and cached variants",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.mjs",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "source": "./src/index.ts",
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.mjs",
13
+ "require": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsup && tsc --emitDeclarationOnly --declaration",
21
+ "prepack": "npm run build",
22
+ "test": "vitest run"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/Yakrware/atiproto.git",
27
+ "directory": "packages/record-resolver"
28
+ },
29
+ "homepage": "https://atiproto.com/docs/record-resolver",
30
+ "bugs": "https://github.com/Yakrware/atiproto/issues",
31
+ "license": "MIT",
32
+ "publishConfig": {
33
+ "tag": "beta",
34
+ "access": "public"
35
+ },
36
+ "dependencies": {
37
+ "@atiproto/atproto-attestation": "^0.1.0",
38
+ "@atproto-labs/simple-store": "^0.3.0",
39
+ "@atproto-labs/simple-store-memory": "^0.1.4"
40
+ },
41
+ "devDependencies": {
42
+ "tsup": "^8.5.1",
43
+ "typescript": "^6.0.2",
44
+ "vitest": "^4.1.4"
45
+ }
46
+ }