@apertoid/core 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.
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "@apertoid/core",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "import": "./src/index.ts",
10
+ "types": "./src/index.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "dependencies": {
15
+ "zod": "^3.23.8"
16
+ }
17
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Verifies that a domain owns the given public key by checking standard DNS TXT records.
3
+ * Uses DNS-over-HTTPS (DoH) to work in Cloudflare Workers, Node.js, and Bun.
4
+ */
5
+ export async function verifyDomainAgentIdentity(domain: string, assertedPublicKey: string): Promise<boolean> {
6
+ try {
7
+ // We try to resolve standard _apertoid record first, then fallback to standard _ans record.
8
+ const txtRecords = await fetchTxtRecords(domain);
9
+ if (txtRecords.length === 0) {
10
+ console.warn(`No DNS TXT records found for agent under domain: ${domain}`);
11
+ return false;
12
+ }
13
+
14
+ // Look for any record that declares the protocol format (e.g. v=apertoid1 or v=ansv2)
15
+ const matchingRecord = txtRecords.find(
16
+ (record) => record.startsWith("v=apertoid1") || record.startsWith("v=ansv2")
17
+ );
18
+
19
+ if (!matchingRecord) {
20
+ console.warn(`No valid apertoid or ansv2 TXT protocol configuration found under domain: ${domain}`);
21
+ return false;
22
+ }
23
+
24
+ // Parse attributes: e.g., v=apertoid1; k=ed25519; p=PUBLIC_KEY_HERE
25
+ // Convert semicolon delimited format to a standard query-string layout for easy parsing
26
+ const normalizedQuery = matchingRecord.replace(/;\s*/g, "&");
27
+ const params = new URLSearchParams(normalizedQuery);
28
+ const registeredKey = params.get("p");
29
+
30
+ if (!registeredKey) {
31
+ console.warn(`Protocol record found but missing 'p' parameter under domain: ${domain}`);
32
+ return false;
33
+ }
34
+
35
+ // Validate that the key matches
36
+ return registeredKey === assertedPublicKey;
37
+ } catch (error) {
38
+ console.error(`DNS Resolution failure on domain: ${domain}`, error);
39
+ return false;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Fetches DNS TXT records from both '_apertoid.<domain>' and '_ans.<domain>' subdomains
45
+ * using Cloudflare's secure DNS-over-HTTPS API.
46
+ */
47
+ async function fetchTxtRecords(domain: string): Promise<string[]> {
48
+ const targets = [`_apertoid.${domain}`, `_ans.${domain}`];
49
+ const records: string[] = [];
50
+
51
+ for (const target of targets) {
52
+ try {
53
+ const url = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(target)}&type=TXT`;
54
+ const response = await fetch(url, {
55
+ method: "GET",
56
+ headers: {
57
+ "Accept": "application/dns-json",
58
+ },
59
+ });
60
+
61
+ if (!response.ok) {
62
+ continue;
63
+ }
64
+
65
+ const data = (await response.json()) as {
66
+ Answer?: Array<{ type: number; data: string }>;
67
+ };
68
+
69
+ if (data.Answer && Array.isArray(data.Answer)) {
70
+ for (const ans of data.Answer) {
71
+ // TXT record type in DNS is 16
72
+ if (ans.type === 16 && ans.data) {
73
+ // DoH data fields often wrap the string in double quotes. Clean them up.
74
+ const cleaned = ans.data.replace(/^"|"$/g, "");
75
+ records.push(cleaned);
76
+ }
77
+ }
78
+ }
79
+ } catch (err) {
80
+ console.error(`DoH lookup error for ${target}:`, err);
81
+ }
82
+ }
83
+
84
+ return records;
85
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from "./schemas";
2
+ export * from "./dns-parser";
package/src/schemas.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { z } from "zod";
2
+
3
+ // Validates the agent's identity claims according to ApertoID/ANSv2 formats
4
+ export const AgentSchema = z.object({
5
+ ans_name: z.string().url().regex(/^agent:\/\/(\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}/, {
6
+ message: "ans_name must match 'agent://domain.com' or 'agent://://domain.com'"
7
+ }),
8
+ mcp_endpoint: z.string().url(),
9
+ public_key: z.string().min(10, { message: "Public key must be at least 10 characters long" }),
10
+ });
11
+
12
+ // Matches individual tools exposed by the MCP server
13
+ export const ToolSchema = z.object({
14
+ name: z.string().min(1),
15
+ description: z.string().optional(),
16
+ inputSchema: z.record(z.any()).optional().default({}),
17
+ });
18
+
19
+ // Full configuration profile retrieved from target nodes (e.g. mcp.json)
20
+ export const RawMcpManifestSchema = AgentSchema.extend({
21
+ tools: z.array(ToolSchema).optional().default([]),
22
+ });
23
+
24
+ export type Agent = z.infer<typeof AgentSchema>;
25
+ export type Tool = z.infer<typeof ToolSchema>;
26
+ export type RawMcpManifest = z.infer<typeof RawMcpManifestSchema>;
package/tsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }