@nkmc/gateway 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.
Files changed (130) hide show
  1. package/dist/chunk-56RA53VS.js +37 -0
  2. package/dist/chunk-CZJ75YTV.js +969 -0
  3. package/dist/chunk-QGM4M3NI.js +37 -0
  4. package/dist/http.cjs +1772 -0
  5. package/dist/http.d.cts +49 -0
  6. package/dist/http.d.ts +49 -0
  7. package/dist/http.js +748 -0
  8. package/dist/index.cjs +2436 -0
  9. package/dist/index.d.cts +436 -0
  10. package/dist/index.d.ts +436 -0
  11. package/dist/index.js +1434 -0
  12. package/dist/proxy-ClPcDgsO.d.cts +283 -0
  13. package/dist/proxy-qpda1ANS.d.ts +283 -0
  14. package/dist/proxy.cjs +148 -0
  15. package/dist/proxy.d.cts +6 -0
  16. package/dist/proxy.d.ts +6 -0
  17. package/dist/proxy.js +90 -0
  18. package/dist/testing.cjs +865 -0
  19. package/dist/testing.d.cts +12 -0
  20. package/dist/testing.d.ts +12 -0
  21. package/dist/testing.js +831 -0
  22. package/dist/tunnels-BviBEaih.d.cts +12 -0
  23. package/dist/tunnels-DFHNgmN7.d.ts +12 -0
  24. package/dist/types-C6JC9oTm.d.cts +21 -0
  25. package/dist/types-C6JC9oTm.d.ts +21 -0
  26. package/package.json +47 -0
  27. package/src/__tests__/sqlite-integration.test.ts +384 -0
  28. package/src/credential/d1-vault.ts +134 -0
  29. package/src/credential/memory-vault.ts +50 -0
  30. package/src/credential/types.ts +16 -0
  31. package/src/d1/__tests__/sqlite-adapter.test.ts +75 -0
  32. package/src/d1/sqlite-adapter.ts +59 -0
  33. package/src/d1/types.ts +22 -0
  34. package/src/federation/__tests__/d1-peer-store.test.ts +218 -0
  35. package/src/federation/__tests__/peer-client.test.ts +205 -0
  36. package/src/federation/__tests__/peer-store.test.ts +114 -0
  37. package/src/federation/d1-peer-store.ts +164 -0
  38. package/src/federation/peer-backend.ts +60 -0
  39. package/src/federation/peer-client.ts +122 -0
  40. package/src/federation/peer-store.ts +45 -0
  41. package/src/federation/types.ts +39 -0
  42. package/src/http/app.ts +152 -0
  43. package/src/http/lib/dns.ts +30 -0
  44. package/src/http/middleware/admin-auth.ts +18 -0
  45. package/src/http/middleware/agent-auth.ts +27 -0
  46. package/src/http/middleware/publish-auth.ts +39 -0
  47. package/src/http/routes/__tests__/federation.test.ts +364 -0
  48. package/src/http/routes/__tests__/peers.test.ts +290 -0
  49. package/src/http/routes/__tests__/proxy.test.ts +159 -0
  50. package/src/http/routes/auth.ts +39 -0
  51. package/src/http/routes/byok.ts +62 -0
  52. package/src/http/routes/credentials.ts +40 -0
  53. package/src/http/routes/domains.ts +174 -0
  54. package/src/http/routes/federation.ts +170 -0
  55. package/src/http/routes/fs.ts +89 -0
  56. package/src/http/routes/peers.ts +103 -0
  57. package/src/http/routes/proxy.ts +57 -0
  58. package/src/http/routes/registry.ts +222 -0
  59. package/src/http/routes/tunnels.ts +124 -0
  60. package/src/http.ts +9 -0
  61. package/src/index.ts +63 -0
  62. package/src/metering/d1-store.ts +123 -0
  63. package/src/metering/memory-store.ts +29 -0
  64. package/src/metering/pricing-guard.ts +68 -0
  65. package/src/metering/types.ts +25 -0
  66. package/src/onboard/apis-guru.ts +64 -0
  67. package/src/onboard/index.ts +4 -0
  68. package/src/onboard/manifest.ts +362 -0
  69. package/src/onboard/pipeline.ts +214 -0
  70. package/src/onboard/types.ts +72 -0
  71. package/src/proxy/__tests__/tool-registry.test.ts +93 -0
  72. package/src/proxy/tool-registry.ts +122 -0
  73. package/src/proxy.ts +12 -0
  74. package/src/registry/context7-backend.ts +93 -0
  75. package/src/registry/context7.ts +54 -0
  76. package/src/registry/d1-store.ts +242 -0
  77. package/src/registry/memory-store.ts +101 -0
  78. package/src/registry/openapi-compiler.ts +284 -0
  79. package/src/registry/resolver.ts +196 -0
  80. package/src/registry/rpc-compiler.ts +142 -0
  81. package/src/registry/skill-parser.ts +119 -0
  82. package/src/registry/skill-to-config.ts +239 -0
  83. package/src/registry/source-refresher.ts +83 -0
  84. package/src/registry/types.ts +129 -0
  85. package/src/registry/virtual-files.ts +76 -0
  86. package/src/testing/sqlite-d1.ts +64 -0
  87. package/src/testing.ts +2 -0
  88. package/src/tunnel/__tests__/cloudflare-provider.test.ts +255 -0
  89. package/src/tunnel/__tests__/tunnel.test.ts +542 -0
  90. package/src/tunnel/cloudflare-provider.ts +121 -0
  91. package/src/tunnel/memory-store.ts +30 -0
  92. package/src/tunnel/types.ts +28 -0
  93. package/test/credential/d1-vault.test.ts +127 -0
  94. package/test/credential/injection.test.ts +67 -0
  95. package/test/credential/memory-vault.test.ts +63 -0
  96. package/test/http/app.test.ts +300 -0
  97. package/test/http/byok-e2e.test.ts +240 -0
  98. package/test/http/byok.test.ts +115 -0
  99. package/test/http/credentials.test.ts +57 -0
  100. package/test/http/e2e.test.ts +260 -0
  101. package/test/integration/authenticated-apis.test.ts +185 -0
  102. package/test/integration/free-apis-e2e.test.ts +222 -0
  103. package/test/metering/d1-store.test.ts +82 -0
  104. package/test/metering/memory-store.test.ts +76 -0
  105. package/test/metering/pricing-guard.test.ts +108 -0
  106. package/test/onboard/apis-guru.test.ts +57 -0
  107. package/test/onboard/e2e.test.ts +70 -0
  108. package/test/onboard/pipeline.test.ts +318 -0
  109. package/test/onboard/real-apis.test.ts +483 -0
  110. package/test/registry/compilation-correctness.test.ts +132 -0
  111. package/test/registry/context7-backend.test.ts +88 -0
  112. package/test/registry/context7-e2e.test.ts +92 -0
  113. package/test/registry/context7.test.ts +73 -0
  114. package/test/registry/d1-store.test.ts +184 -0
  115. package/test/registry/integration.test.ts +129 -0
  116. package/test/registry/lazy-mount.test.ts +138 -0
  117. package/test/registry/memory-store.test.ts +171 -0
  118. package/test/registry/openapi-compiler.test.ts +267 -0
  119. package/test/registry/openapi-e2e.test.ts +154 -0
  120. package/test/registry/passthrough-e2e.test.ts +109 -0
  121. package/test/registry/resolver-peer.test.ts +299 -0
  122. package/test/registry/resolver.test.ts +228 -0
  123. package/test/registry/rpc-compiler.test.ts +112 -0
  124. package/test/registry/skill-parser.test.ts +151 -0
  125. package/test/registry/skill-to-config.test.ts +151 -0
  126. package/test/registry/skill-to-rpc-config.test.ts +142 -0
  127. package/test/registry/source-refresher.test.ts +90 -0
  128. package/test/registry/virtual-files.test.ts +96 -0
  129. package/tsconfig.json +4 -0
  130. package/tsup.config.ts +8 -0
@@ -0,0 +1,222 @@
1
+ import { Hono } from "hono";
2
+ import type { RegistryStore, EndpointRecord, SourceConfig } from "../../registry/types.js";
3
+ import { parseSkillMd } from "../../registry/skill-parser.js";
4
+ import { fetchAndCompile } from "../../registry/openapi-compiler.js";
5
+ import type { Env } from "../app.js";
6
+ import type { PublishAuthContext } from "../middleware/publish-auth.js";
7
+
8
+ /** Well-known paths to probe for OpenAPI specs */
9
+ const OPENAPI_PATHS = [
10
+ "/openapi.json",
11
+ "/openapi.yaml",
12
+ "/swagger.json",
13
+ "/swagger.yaml",
14
+ "/docs/openapi.json",
15
+ "/api-docs",
16
+ "/api/openapi.json",
17
+ "/.well-known/openapi.json",
18
+ "/.well-known/openapi.yaml",
19
+ ];
20
+
21
+ export interface RegistryRouteOptions {
22
+ store: RegistryStore;
23
+ }
24
+
25
+ export function registryRoutes(options: RegistryRouteOptions) {
26
+ const { store } = options;
27
+ const app = new Hono<Env>();
28
+
29
+ // Register a service (body = skill.md content)
30
+ app.post("/services", async (c) => {
31
+ const domain = c.req.query("domain");
32
+ if (!domain) {
33
+ return c.json({ error: "Missing ?domain= query parameter" }, 400);
34
+ }
35
+
36
+ // Publish token scope check: token must match the target domain
37
+ const auth = c.get("publishAuth" as never) as PublishAuthContext | undefined;
38
+ if (auth?.type === "publish" && auth.domain !== domain) {
39
+ return c.json(
40
+ { error: `Token is scoped to "${auth.domain}", cannot register "${domain}"` },
41
+ 403,
42
+ );
43
+ }
44
+
45
+ const contentType = c.req.header("Content-Type") ?? "";
46
+ let skillMd: string;
47
+
48
+ if (contentType.includes("text/markdown") || contentType.includes("text/plain")) {
49
+ skillMd = await c.req.text();
50
+ } else {
51
+ // Try JSON body with skillMd field (and optional pre-compiled endpoints)
52
+ const body = await c.req.json<{ skillMd: string; endpoints?: EndpointRecord[]; source?: SourceConfig }>().catch(() => null);
53
+ if (!body?.skillMd) {
54
+ return c.json({ error: "Body must be skill.md text or JSON with skillMd field" }, 400);
55
+ }
56
+ skillMd = body.skillMd;
57
+ // If pre-compiled endpoints are provided, use them after parsing
58
+ if (Array.isArray(body.endpoints) && body.endpoints.length > 0) {
59
+ if (!skillMd.trim()) {
60
+ return c.json({ error: "Empty skill.md content" }, 400);
61
+ }
62
+ const isFirstParty = c.req.query("first_party") === "true";
63
+ const authMode = c.req.query("auth_mode");
64
+ const record = parseSkillMd(domain, skillMd, { isFirstParty });
65
+ record.endpoints = body.endpoints;
66
+ if (body.source) {
67
+ record.source = body.source;
68
+ }
69
+ if (authMode === "nkmc-jwt") {
70
+ record.authMode = "nkmc-jwt";
71
+ }
72
+ await store.put(domain, record);
73
+ return c.json({ ok: true, domain, name: record.name }, 201);
74
+ }
75
+ }
76
+
77
+ if (!skillMd.trim()) {
78
+ return c.json({ error: "Empty skill.md content" }, 400);
79
+ }
80
+
81
+ const isFirstParty = c.req.query("first_party") === "true";
82
+ const authMode = c.req.query("auth_mode");
83
+ const record = parseSkillMd(domain, skillMd, { isFirstParty });
84
+ if (authMode === "nkmc-jwt") {
85
+ record.authMode = "nkmc-jwt";
86
+ }
87
+ await store.put(domain, record);
88
+
89
+ return c.json({ ok: true, domain, name: record.name }, 201);
90
+ });
91
+
92
+ // Discover: auto-detect OpenAPI spec from a running service URL and register
93
+ app.post("/services/discover", async (c) => {
94
+ const body = await c.req.json<{ url: string; domain?: string; specUrl?: string }>().catch(() => null);
95
+ if (!body?.url) {
96
+ return c.json({ error: "Missing 'url' field (base URL of the service)" }, 400);
97
+ }
98
+
99
+ const baseUrl = body.url.replace(/\/+$/, "");
100
+
101
+ // Derive domain from URL if not provided
102
+ let domain: string;
103
+ try {
104
+ domain = body.domain ?? new URL(baseUrl).hostname;
105
+ } catch {
106
+ return c.json({ error: "Invalid URL" }, 400);
107
+ }
108
+
109
+ // Publish token scope check
110
+ const auth = c.get("publishAuth" as never) as PublishAuthContext | undefined;
111
+ if (auth?.type === "publish" && auth.domain !== domain) {
112
+ return c.json(
113
+ { error: `Token is scoped to "${auth.domain}", cannot register "${domain}"` },
114
+ 403,
115
+ );
116
+ }
117
+
118
+ // If specUrl is provided, use it directly
119
+ if (body.specUrl) {
120
+ try {
121
+ const result = await fetchAndCompile(body.specUrl, { domain });
122
+ await store.put(domain, result.record);
123
+ return c.json({
124
+ ok: true,
125
+ domain,
126
+ name: result.record.name,
127
+ endpoints: result.record.endpoints.length,
128
+ source: body.specUrl,
129
+ }, 201);
130
+ } catch (err) {
131
+ return c.json({ error: `Failed to compile spec: ${err instanceof Error ? err.message : err}` }, 400);
132
+ }
133
+ }
134
+
135
+ // Auto-discover: probe well-known paths
136
+ for (const path of OPENAPI_PATHS) {
137
+ const specUrl = `${baseUrl}${path}`;
138
+ try {
139
+ const resp = await fetch(specUrl, { method: "GET", headers: { Accept: "application/json, application/yaml" } });
140
+ if (!resp.ok) continue;
141
+
142
+ const text = await resp.text();
143
+ if (!text.trim() || text.length < 20) continue;
144
+
145
+ // Try to compile — if it fails, try next path
146
+ try {
147
+ const result = await fetchAndCompile(specUrl, { domain });
148
+ await store.put(domain, result.record);
149
+ return c.json({
150
+ ok: true,
151
+ domain,
152
+ name: result.record.name,
153
+ endpoints: result.record.endpoints.length,
154
+ source: specUrl,
155
+ }, 201);
156
+ } catch {
157
+ continue;
158
+ }
159
+ } catch {
160
+ continue;
161
+ }
162
+ }
163
+
164
+ return c.json({
165
+ error: "Could not find OpenAPI spec",
166
+ probed: OPENAPI_PATHS.map((p) => `${baseUrl}${p}`),
167
+ hint: "Use --spec-url to provide the spec location directly",
168
+ }, 404);
169
+ });
170
+
171
+ // List all services
172
+ app.get("/services", async (c) => {
173
+ const query = c.req.query("q");
174
+ if (query) {
175
+ const results = await store.search(query);
176
+ return c.json(results);
177
+ }
178
+ const list = await store.list();
179
+ return c.json(list);
180
+ });
181
+
182
+ // Get service details
183
+ app.get("/services/:domain", async (c) => {
184
+ const domain = c.req.param("domain");
185
+ const record = await store.get(domain);
186
+ if (!record) {
187
+ return c.json({ error: "Service not found" }, 404);
188
+ }
189
+ return c.json(record);
190
+ });
191
+
192
+ // List versions of a service
193
+ app.get("/services/:domain/versions", async (c) => {
194
+ const domain = c.req.param("domain");
195
+ const versions = await store.listVersions(domain);
196
+ return c.json({ domain, versions });
197
+ });
198
+
199
+ // Get specific version of a service
200
+ app.get("/services/:domain/versions/:version", async (c) => {
201
+ const domain = c.req.param("domain");
202
+ const version = c.req.param("version");
203
+ const record = await store.getVersion(domain, version);
204
+ if (!record) {
205
+ return c.json({ error: "Version not found" }, 404);
206
+ }
207
+ return c.json(record);
208
+ });
209
+
210
+ // Delete a service
211
+ app.delete("/services/:domain", async (c) => {
212
+ const domain = c.req.param("domain");
213
+ const existing = await store.get(domain);
214
+ if (!existing) {
215
+ return c.json({ error: "Service not found" }, 404);
216
+ }
217
+ await store.delete(domain);
218
+ return c.json({ ok: true, domain });
219
+ });
220
+
221
+ return app;
222
+ }
@@ -0,0 +1,124 @@
1
+ import { Hono } from "hono";
2
+ import type { Env } from "../app.js";
3
+ import type { TunnelStore, TunnelProvider } from "../../tunnel/types.js";
4
+ import { nanoid } from "nanoid";
5
+
6
+ export interface TunnelRouteOptions {
7
+ tunnelStore: TunnelStore;
8
+ tunnelProvider: TunnelProvider;
9
+ tunnelDomain: string; // e.g. "tunnel.example.com"
10
+ }
11
+
12
+ export function tunnelRoutes(options: TunnelRouteOptions) {
13
+ const { tunnelStore, tunnelProvider, tunnelDomain } = options;
14
+ const app = new Hono<Env>();
15
+
16
+ // POST /tunnels/create — create a tunnel for the authenticated agent
17
+ app.post("/create", async (c) => {
18
+ const agent = c.get("agent");
19
+ const body = await c.req.json<{
20
+ advertisedDomains?: string[];
21
+ gatewayName?: string;
22
+ }>().catch(() => ({} as { advertisedDomains?: string[]; gatewayName?: string }));
23
+
24
+ // Check if agent already has a tunnel
25
+ const existing = await tunnelStore.getByAgent(agent.id);
26
+ if (existing && existing.status === "active") {
27
+ return c.json({
28
+ tunnelId: existing.id,
29
+ publicUrl: existing.publicUrl,
30
+ message: "Tunnel already exists",
31
+ });
32
+ }
33
+
34
+ const id = nanoid(12);
35
+ const hostname = `${id}.${tunnelDomain}`;
36
+ const publicUrl = `https://${hostname}`;
37
+
38
+ // Create via Cloudflare API
39
+ const { tunnelId, tunnelToken } = await tunnelProvider.create(
40
+ `nkmc-${agent.id}-${id}`,
41
+ hostname,
42
+ );
43
+
44
+ const now = Date.now();
45
+
46
+ // Store record
47
+ await tunnelStore.put({
48
+ id,
49
+ agentId: agent.id,
50
+ tunnelId,
51
+ publicUrl,
52
+ status: "active",
53
+ createdAt: now,
54
+ advertisedDomains: body.advertisedDomains ?? [],
55
+ gatewayName: body.gatewayName,
56
+ lastSeen: now,
57
+ });
58
+
59
+ return c.json({ tunnelId: id, tunnelToken, publicUrl }, 201);
60
+ });
61
+
62
+ // DELETE /tunnels/:id — delete a tunnel
63
+ app.delete("/:id", async (c) => {
64
+ const id = c.req.param("id");
65
+ const agent = c.get("agent");
66
+
67
+ const record = await tunnelStore.get(id);
68
+ if (!record) return c.json({ error: "Tunnel not found" }, 404);
69
+ if (record.agentId !== agent.id)
70
+ return c.json({ error: "Not your tunnel" }, 403);
71
+
72
+ await tunnelProvider.delete(record.tunnelId);
73
+ await tunnelStore.delete(id);
74
+
75
+ return c.json({ ok: true });
76
+ });
77
+
78
+ // GET /tunnels — list agent's tunnels
79
+ app.get("/", async (c) => {
80
+ const agent = c.get("agent");
81
+ const all = await tunnelStore.list();
82
+ const mine = all.filter((t) => t.agentId === agent.id);
83
+ return c.json({ tunnels: mine });
84
+ });
85
+
86
+ // GET /tunnels/discover — list all online gateways (public info only)
87
+ // Optional: ?domain=api.openai.com — filter by advertised domain
88
+ app.get("/discover", async (c) => {
89
+ const domain = c.req.query("domain");
90
+ const all = await tunnelStore.list();
91
+
92
+ let results = all.filter((t) => t.status === "active");
93
+ if (domain) {
94
+ results = results.filter((t) => t.advertisedDomains.includes(domain));
95
+ }
96
+
97
+ // Return public info only — no tunnelToken, no internal IDs
98
+ return c.json({
99
+ gateways: results.map((t) => ({
100
+ id: t.id,
101
+ name: t.gatewayName ?? `gateway-${t.id}`,
102
+ publicUrl: t.publicUrl,
103
+ advertisedDomains: t.advertisedDomains,
104
+ })),
105
+ });
106
+ });
107
+
108
+ // POST /tunnels/heartbeat — update advertised domains and confirm online
109
+ app.post("/heartbeat", async (c) => {
110
+ const agent = c.get("agent");
111
+ const body = await c.req.json<{ advertisedDomains?: string[] }>();
112
+
113
+ const record = await tunnelStore.getByAgent(agent.id);
114
+ if (!record) return c.json({ error: "No active tunnel" }, 404);
115
+
116
+ record.advertisedDomains = body.advertisedDomains ?? record.advertisedDomains;
117
+ record.lastSeen = Date.now();
118
+ await tunnelStore.put(record);
119
+
120
+ return c.json({ ok: true });
121
+ });
122
+
123
+ return app;
124
+ }
package/src/http.ts ADDED
@@ -0,0 +1,9 @@
1
+ export { createGateway, type GatewayOptions, type Env } from "./http/app.js";
2
+ export { adminAuth } from "./http/middleware/admin-auth.js";
3
+ export { publishOrAdminAuth, type PublishAuthContext } from "./http/middleware/publish-auth.js";
4
+ export { agentAuth } from "./http/middleware/agent-auth.js";
5
+ export { authRoutes, type AuthRouteOptions } from "./http/routes/auth.js";
6
+ export { registryRoutes, type RegistryRouteOptions } from "./http/routes/registry.js";
7
+ export { domainRoutes, type DomainRouteOptions } from "./http/routes/domains.js";
8
+ export { fsRoutes, type FsRouteOptions } from "./http/routes/fs.js";
9
+ export { tunnelRoutes, type TunnelRouteOptions } from "./http/routes/tunnels.js";
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ export const VERSION = "0.1.0";
2
+
3
+ export type {
4
+ EndpointSummary,
5
+ EndpointRecord,
6
+ EndpointPricing,
7
+ EndpointAnnotations,
8
+ ServiceRecord,
9
+ SearchResult,
10
+ ServiceSummary,
11
+ ServiceStatus,
12
+ VersionSummary,
13
+ SourceConfig,
14
+ RegistryStore,
15
+ } from "./registry/types.js";
16
+
17
+ export { MemoryRegistryStore } from "./registry/memory-store.js";
18
+ export { D1RegistryStore } from "./registry/d1-store.js";
19
+ export { parseSkillMd, parsePricingAnnotation, type ParseOptions } from "./registry/skill-parser.js";
20
+ export { skillToHttpConfig } from "./registry/skill-to-config.js";
21
+ export {
22
+ createRegistryResolver,
23
+ extractDomainPath,
24
+ type RegistryResolverHooks,
25
+ type RegistryResolverOptions,
26
+ } from "./registry/resolver.js";
27
+
28
+ export type { MeterRecord, MeterQuery, MeterStore } from "./metering/types.js";
29
+ export { MemoryMeterStore } from "./metering/memory-store.js";
30
+ export { D1MeterStore } from "./metering/d1-store.js";
31
+ export { lookupPricing, checkAccess, meter } from "./metering/pricing-guard.js";
32
+ export { VirtualFileBackend } from "./registry/virtual-files.js";
33
+
34
+ export type { StoredCredential, CredentialVault } from "./credential/types.js";
35
+ export { MemoryCredentialVault } from "./credential/memory-vault.js";
36
+ export { D1CredentialVault } from "./credential/d1-vault.js";
37
+ export { credentialRoutes } from "./http/routes/credentials.js";
38
+
39
+ export { queryDnsTxt } from "./http/lib/dns.js";
40
+ export { Context7Client, type Context7Options, type LibrarySearchResult } from "./registry/context7.js";
41
+ export { Context7Backend, type Context7BackendOptions } from "./registry/context7-backend.js";
42
+
43
+ export {
44
+ OnboardPipeline,
45
+ discoverFromApisGuru,
46
+ type PipelineOptions,
47
+ type ApisGuruOptions,
48
+ type ManifestEntry,
49
+ type ManifestAuth,
50
+ type OnboardResult,
51
+ type OnboardReport,
52
+ } from "./onboard/index.js";
53
+
54
+ export type { D1Database, D1PreparedStatement, D1Result, D1RunResult } from "./d1/types.js";
55
+ export { createSqliteD1 } from "./d1/sqlite-adapter.js";
56
+
57
+ export type { PeerGateway, LendingRule, PeerStore } from "./federation/types.js";
58
+ export { D1PeerStore } from "./federation/d1-peer-store.js";
59
+
60
+ export type { TunnelRecord, TunnelStore, TunnelProvider } from "./tunnel/types.js";
61
+ export { MemoryTunnelStore } from "./tunnel/memory-store.js";
62
+ export { CloudflareTunnelProvider } from "./tunnel/cloudflare-provider.js";
63
+ export { tunnelRoutes, type TunnelRouteOptions } from "./http/routes/tunnels.js";
@@ -0,0 +1,123 @@
1
+ import type { D1Database } from "../d1/types.js";
2
+ import type { MeterRecord, MeterQuery, MeterStore } from "./types.js";
3
+
4
+ const CREATE_METER_RECORDS = `
5
+ CREATE TABLE IF NOT EXISTS meter_records (
6
+ id TEXT PRIMARY KEY,
7
+ timestamp INTEGER NOT NULL,
8
+ domain TEXT NOT NULL,
9
+ version TEXT NOT NULL,
10
+ endpoint TEXT NOT NULL,
11
+ agent_id TEXT NOT NULL,
12
+ developer_id TEXT,
13
+ cost REAL NOT NULL,
14
+ currency TEXT NOT NULL DEFAULT 'USDC'
15
+ )`;
16
+
17
+ const CREATE_METER_INDEX_DOMAIN = `
18
+ CREATE INDEX IF NOT EXISTS idx_meter_domain ON meter_records(domain, timestamp)`;
19
+
20
+ const CREATE_METER_INDEX_AGENT = `
21
+ CREATE INDEX IF NOT EXISTS idx_meter_agent ON meter_records(agent_id, timestamp)`;
22
+
23
+ interface MeterRow {
24
+ id: string;
25
+ timestamp: number;
26
+ domain: string;
27
+ version: string;
28
+ endpoint: string;
29
+ agent_id: string;
30
+ developer_id: string | null;
31
+ cost: number;
32
+ currency: string;
33
+ }
34
+
35
+ export class D1MeterStore implements MeterStore {
36
+ constructor(private db: D1Database) {}
37
+
38
+ async initSchema(): Promise<void> {
39
+ await this.db.exec(CREATE_METER_RECORDS);
40
+ await this.db.exec(CREATE_METER_INDEX_DOMAIN);
41
+ await this.db.exec(CREATE_METER_INDEX_AGENT);
42
+ }
43
+
44
+ async record(entry: MeterRecord): Promise<void> {
45
+ await this.db
46
+ .prepare(
47
+ `INSERT INTO meter_records (id, timestamp, domain, version, endpoint, agent_id, developer_id, cost, currency)
48
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
49
+ )
50
+ .bind(
51
+ entry.id,
52
+ entry.timestamp,
53
+ entry.domain,
54
+ entry.version,
55
+ entry.endpoint,
56
+ entry.agentId,
57
+ entry.developerId ?? null,
58
+ entry.cost,
59
+ entry.currency,
60
+ )
61
+ .run();
62
+ }
63
+
64
+ async query(filter: MeterQuery): Promise<MeterRecord[]> {
65
+ const { sql, bindings } = this.buildQuery("SELECT *", filter);
66
+ const { results } = await this.db.prepare(sql).bind(...bindings).all<MeterRow>();
67
+ return results.map(rowToRecord);
68
+ }
69
+
70
+ async sum(filter: MeterQuery): Promise<{ total: number; currency: string }> {
71
+ const { sql, bindings } = this.buildQuery("SELECT COALESCE(SUM(cost), 0) as total, COALESCE(MIN(currency), 'USDC') as currency", filter);
72
+ const row = await this.db.prepare(sql).bind(...bindings).first<{ total: number; currency: string }>();
73
+ return { total: row?.total ?? 0, currency: row?.currency ?? "USDC" };
74
+ }
75
+
76
+ private buildQuery(select: string, filter: MeterQuery): { sql: string; bindings: unknown[] } {
77
+ const conditions: string[] = [];
78
+ const bindings: unknown[] = [];
79
+
80
+ if (filter.domain) {
81
+ conditions.push("domain = ?");
82
+ bindings.push(filter.domain);
83
+ }
84
+ if (filter.agentId) {
85
+ conditions.push("agent_id = ?");
86
+ bindings.push(filter.agentId);
87
+ }
88
+ if (filter.developerId) {
89
+ conditions.push("developer_id = ?");
90
+ bindings.push(filter.developerId);
91
+ }
92
+ if (filter.from) {
93
+ conditions.push("timestamp >= ?");
94
+ bindings.push(filter.from);
95
+ }
96
+ if (filter.to) {
97
+ conditions.push("timestamp <= ?");
98
+ bindings.push(filter.to);
99
+ }
100
+
101
+ let sql = `${select} FROM meter_records`;
102
+ if (conditions.length > 0) {
103
+ sql += ` WHERE ${conditions.join(" AND ")}`;
104
+ }
105
+ sql += " ORDER BY timestamp DESC";
106
+
107
+ return { sql, bindings };
108
+ }
109
+ }
110
+
111
+ function rowToRecord(row: MeterRow): MeterRecord {
112
+ return {
113
+ id: row.id,
114
+ timestamp: row.timestamp,
115
+ domain: row.domain,
116
+ version: row.version,
117
+ endpoint: row.endpoint,
118
+ agentId: row.agent_id,
119
+ ...(row.developer_id ? { developerId: row.developer_id } : {}),
120
+ cost: row.cost,
121
+ currency: row.currency,
122
+ };
123
+ }
@@ -0,0 +1,29 @@
1
+ import type { MeterRecord, MeterQuery, MeterStore } from "./types.js";
2
+
3
+ export class MemoryMeterStore implements MeterStore {
4
+ private records: MeterRecord[] = [];
5
+
6
+ async record(entry: MeterRecord): Promise<void> {
7
+ this.records.push(entry);
8
+ }
9
+
10
+ async query(filter: MeterQuery): Promise<MeterRecord[]> {
11
+ return this.records.filter((r) => this.matches(r, filter));
12
+ }
13
+
14
+ async sum(filter: MeterQuery): Promise<{ total: number; currency: string }> {
15
+ const matched = this.records.filter((r) => this.matches(r, filter));
16
+ const total = matched.reduce((acc, r) => acc + r.cost, 0);
17
+ const currency = matched[0]?.currency ?? "USDC";
18
+ return { total, currency };
19
+ }
20
+
21
+ private matches(record: MeterRecord, filter: MeterQuery): boolean {
22
+ if (filter.domain && record.domain !== filter.domain) return false;
23
+ if (filter.agentId && record.agentId !== filter.agentId) return false;
24
+ if (filter.developerId && record.developerId !== filter.developerId) return false;
25
+ if (filter.from && record.timestamp < filter.from) return false;
26
+ if (filter.to && record.timestamp > filter.to) return false;
27
+ return true;
28
+ }
29
+ }
@@ -0,0 +1,68 @@
1
+ import type { EndpointPricing, ServiceRecord } from "../registry/types.js";
2
+ import type { MeterStore, MeterRecord } from "./types.js";
3
+
4
+ export function lookupPricing(
5
+ record: ServiceRecord,
6
+ method: string,
7
+ path: string,
8
+ ): EndpointPricing | null {
9
+ // Find matching endpoint with pricing
10
+ for (const ep of record.endpoints) {
11
+ if (ep.method.toUpperCase() !== method.toUpperCase()) continue;
12
+ if (matchPath(ep.path, path)) {
13
+ return ep.pricing ?? null;
14
+ }
15
+ }
16
+ return null;
17
+ }
18
+
19
+ export function checkAccess(record: ServiceRecord): { allowed: boolean; reason?: string } {
20
+ if (record.status === "sunset") {
21
+ return { allowed: false, reason: "Service has been sunset" };
22
+ }
23
+ if (record.sunsetDate && record.sunsetDate < Date.now()) {
24
+ return { allowed: false, reason: "Service sunset date has passed" };
25
+ }
26
+ return { allowed: true };
27
+ }
28
+
29
+ export async function meter(
30
+ store: MeterStore,
31
+ opts: {
32
+ domain: string;
33
+ version: string;
34
+ endpoint: string;
35
+ agentId: string;
36
+ developerId?: string;
37
+ pricing: EndpointPricing;
38
+ },
39
+ ): Promise<MeterRecord> {
40
+ const entry: MeterRecord = {
41
+ id: `m_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
42
+ timestamp: Date.now(),
43
+ domain: opts.domain,
44
+ version: opts.version,
45
+ endpoint: opts.endpoint,
46
+ agentId: opts.agentId,
47
+ developerId: opts.developerId,
48
+ cost: opts.pricing.cost,
49
+ currency: opts.pricing.currency,
50
+ };
51
+ await store.record(entry);
52
+ return entry;
53
+ }
54
+
55
+ function matchPath(pattern: string, actual: string): boolean {
56
+ // Support :param pattern matching
57
+ const patternParts = pattern.split("/").filter(Boolean);
58
+ const actualParts = actual.split("/").filter(Boolean);
59
+
60
+ if (patternParts.length !== actualParts.length) return false;
61
+
62
+ for (let i = 0; i < patternParts.length; i++) {
63
+ if (patternParts[i].startsWith(":")) continue; // wildcard
64
+ if (patternParts[i] !== actualParts[i]) return false;
65
+ }
66
+
67
+ return true;
68
+ }
@@ -0,0 +1,25 @@
1
+ export interface MeterRecord {
2
+ id: string;
3
+ timestamp: number;
4
+ domain: string;
5
+ version: string;
6
+ endpoint: string;
7
+ agentId: string;
8
+ developerId?: string;
9
+ cost: number;
10
+ currency: string;
11
+ }
12
+
13
+ export interface MeterQuery {
14
+ domain?: string;
15
+ agentId?: string;
16
+ developerId?: string;
17
+ from?: number;
18
+ to?: number;
19
+ }
20
+
21
+ export interface MeterStore {
22
+ record(entry: MeterRecord): Promise<void>;
23
+ query(filter: MeterQuery): Promise<MeterRecord[]>;
24
+ sum(filter: MeterQuery): Promise<{ total: number; currency: string }>;
25
+ }