@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,28 @@
1
+ export interface TunnelRecord {
2
+ id: string;
3
+ agentId: string;
4
+ tunnelId: string; // Cloudflare tunnel ID
5
+ publicUrl: string; // https://{id}.tunnel.example.com
6
+ status: "active" | "deleted";
7
+ createdAt: number;
8
+ /** Domains this gateway has credentials for (for discovery) */
9
+ advertisedDomains: string[];
10
+ /** Display name for the gateway */
11
+ gatewayName?: string;
12
+ /** Last heartbeat timestamp (ms since epoch) */
13
+ lastSeen: number;
14
+ }
15
+
16
+ export interface TunnelStore {
17
+ get(id: string): Promise<TunnelRecord | null>;
18
+ getByAgent(agentId: string): Promise<TunnelRecord | null>;
19
+ put(record: TunnelRecord): Promise<void>;
20
+ delete(id: string): Promise<void>;
21
+ list(): Promise<TunnelRecord[]>;
22
+ }
23
+
24
+ /** Interface for Cloudflare Tunnel API operations */
25
+ export interface TunnelProvider {
26
+ create(name: string, hostname: string): Promise<{ tunnelId: string; tunnelToken: string }>;
27
+ delete(tunnelId: string): Promise<void>;
28
+ }
@@ -0,0 +1,127 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
2
+ import { D1CredentialVault } from "../../src/credential/d1-vault.js";
3
+ import { SqliteD1 } from "../../src/testing/sqlite-d1.js";
4
+
5
+ describe("D1CredentialVault", () => {
6
+ let db: SqliteD1;
7
+ let vault: D1CredentialVault;
8
+ let key: CryptoKey;
9
+
10
+ beforeEach(async () => {
11
+ db = new SqliteD1();
12
+ key = await crypto.subtle.generateKey(
13
+ { name: "AES-GCM", length: 256 },
14
+ false,
15
+ ["encrypt", "decrypt"],
16
+ );
17
+ vault = new D1CredentialVault(db, key);
18
+ await vault.initSchema();
19
+ });
20
+
21
+ afterEach(() => {
22
+ db.close();
23
+ });
24
+
25
+ it("should store and retrieve pool credential", async () => {
26
+ await vault.putPool("api.example.com", { type: "bearer", token: "tok_123" });
27
+ const cred = await vault.get("api.example.com");
28
+ expect(cred).not.toBeNull();
29
+ expect(cred!.auth).toEqual({ type: "bearer", token: "tok_123" });
30
+ expect(cred!.scope).toBe("pool");
31
+ });
32
+
33
+ it("should return null for unknown domain", async () => {
34
+ expect(await vault.get("unknown.com")).toBeNull();
35
+ });
36
+
37
+ it("should prefer BYOK over pool", async () => {
38
+ await vault.putPool("api.example.com", { type: "bearer", token: "pool_tok" });
39
+ await vault.putByok("api.example.com", "dev-1", { type: "bearer", token: "byok_tok" });
40
+ const cred = await vault.get("api.example.com", "dev-1");
41
+ expect(cred!.auth).toEqual({ type: "bearer", token: "byok_tok" });
42
+ });
43
+
44
+ it("should fall back to pool when no BYOK", async () => {
45
+ await vault.putPool("api.example.com", { type: "bearer", token: "pool_tok" });
46
+ const cred = await vault.get("api.example.com", "dev-2");
47
+ expect(cred!.auth).toEqual({ type: "bearer", token: "pool_tok" });
48
+ });
49
+
50
+ it("should delete pool credential", async () => {
51
+ await vault.putPool("api.example.com", { type: "bearer", token: "tok" });
52
+ await vault.delete("api.example.com");
53
+ expect(await vault.get("api.example.com")).toBeNull();
54
+ });
55
+
56
+ it("should delete BYOK credential", async () => {
57
+ await vault.putByok("api.example.com", "dev-1", { type: "bearer", token: "tok" });
58
+ await vault.delete("api.example.com", "dev-1");
59
+ expect(await vault.get("api.example.com", "dev-1")).toBeNull();
60
+ });
61
+
62
+ it("should list domains", async () => {
63
+ await vault.putPool("api.example.com", { type: "bearer", token: "t1" });
64
+ await vault.putPool("other.api.com", { type: "bearer", token: "t2" });
65
+ const domains = await vault.listDomains();
66
+ expect(domains).toContain("api.example.com");
67
+ expect(domains).toContain("other.api.com");
68
+ });
69
+
70
+ it("should call initSchema multiple times (idempotent)", async () => {
71
+ await vault.initSchema();
72
+ await vault.putPool("test.com", { type: "bearer", token: "x" });
73
+ expect(await vault.get("test.com")).not.toBeNull();
74
+ });
75
+
76
+ it("should decrypt legacy base64-encoded credentials", async () => {
77
+ // Simulate legacy data: plain base64(JSON) without AES-GCM encryption
78
+ const legacyAuth = { type: "bearer", token: "legacy_tok" };
79
+ const legacyEncoded = btoa(JSON.stringify(legacyAuth));
80
+ const now = Date.now();
81
+ await db
82
+ .prepare(
83
+ `INSERT INTO credentials (domain, scope, developer_id, auth_encrypted, created_at, updated_at)
84
+ VALUES (?, 'pool', '', ?, ?, ?)`,
85
+ )
86
+ .bind("legacy.example.com", legacyEncoded, now, now)
87
+ .run();
88
+
89
+ const cred = await vault.get("legacy.example.com");
90
+ expect(cred).not.toBeNull();
91
+ expect(cred!.auth).toEqual({ type: "bearer", token: "legacy_tok" });
92
+ });
93
+
94
+ it("should produce different ciphertext for same input (random IV)", async () => {
95
+ await vault.putPool("api.example.com", { type: "bearer", token: "tok" });
96
+ // Read raw encrypted value
97
+ const row1 = await db
98
+ .prepare("SELECT auth_encrypted FROM credentials WHERE domain = ?")
99
+ .bind("api.example.com")
100
+ .first<{ auth_encrypted: string }>();
101
+
102
+ // Re-encrypt with same data
103
+ await vault.putPool("api.example.com", { type: "bearer", token: "tok" });
104
+ const row2 = await db
105
+ .prepare("SELECT auth_encrypted FROM credentials WHERE domain = ?")
106
+ .bind("api.example.com")
107
+ .first<{ auth_encrypted: string }>();
108
+
109
+ // Random IV means ciphertext should differ
110
+ expect(row1!.auth_encrypted).not.toBe(row2!.auth_encrypted);
111
+ });
112
+
113
+ it("should not decrypt with a different key", async () => {
114
+ await vault.putPool("api.example.com", { type: "bearer", token: "secret" });
115
+
116
+ // Create vault with different key
117
+ const otherKey = await crypto.subtle.generateKey(
118
+ { name: "AES-GCM", length: 256 },
119
+ false,
120
+ ["encrypt", "decrypt"],
121
+ );
122
+ const otherVault = new D1CredentialVault(db, otherKey);
123
+
124
+ // AES-GCM decrypt fails → fallback to base64 decode → JSON.parse fails → throws
125
+ await expect(otherVault.get("api.example.com")).rejects.toThrow();
126
+ });
127
+ });
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { MemoryRegistryStore } from "../../src/registry/memory-store.js";
3
+ import { MemoryCredentialVault } from "../../src/credential/memory-vault.js";
4
+ import { parseSkillMd } from "../../src/registry/skill-parser.js";
5
+ import { createRegistryResolver } from "../../src/registry/resolver.js";
6
+
7
+ const SKILL_MD = `---
8
+ name: "Test API"
9
+ gateway: nkmc
10
+ version: "1.0"
11
+ roles: [agent]
12
+ ---
13
+
14
+ # Test API
15
+
16
+ A test service.
17
+
18
+ ## API
19
+
20
+ ### Get data
21
+
22
+ \`GET /api/data\` — public
23
+ `;
24
+
25
+ describe("Credential Injection", () => {
26
+ let store: MemoryRegistryStore;
27
+ let vault: MemoryCredentialVault;
28
+
29
+ beforeEach(async () => {
30
+ store = new MemoryRegistryStore();
31
+ vault = new MemoryCredentialVault();
32
+ const record = parseSkillMd("test-api.com", SKILL_MD);
33
+ await store.put("test-api.com", record);
34
+ });
35
+
36
+ it("should create resolver with vault", async () => {
37
+ const { onMiss, listDomains } = createRegistryResolver({ store, vault });
38
+ expect(typeof onMiss).toBe("function");
39
+ const domains = await listDomains();
40
+ expect(domains).toContain("test-api.com");
41
+ });
42
+
43
+ it("should mount backend when credentials exist", async () => {
44
+ await vault.putPool("test-api.com", { type: "bearer", token: "secret_token" });
45
+ const { onMiss } = createRegistryResolver({ store, vault });
46
+
47
+ let mountedBackend: any = null;
48
+ await onMiss("/test-api.com/data", (mount) => {
49
+ mountedBackend = mount.backend;
50
+ });
51
+
52
+ expect(mountedBackend).not.toBeNull();
53
+ // The backend is created — we can verify it was created with auth by checking
54
+ // that it exists (the auth is internal to HttpBackend, so we verify integration)
55
+ });
56
+
57
+ it("should mount backend even without credentials", async () => {
58
+ const { onMiss } = createRegistryResolver({ store, vault });
59
+
60
+ let mountPath = "";
61
+ await onMiss("/test-api.com/data", (mount) => {
62
+ mountPath = mount.path;
63
+ });
64
+
65
+ expect(mountPath).toBe("/test-api.com");
66
+ });
67
+ });
@@ -0,0 +1,63 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { MemoryCredentialVault } from "../../src/credential/memory-vault.js";
3
+
4
+ describe("MemoryCredentialVault", () => {
5
+ let vault: MemoryCredentialVault;
6
+
7
+ beforeEach(() => {
8
+ vault = new MemoryCredentialVault();
9
+ });
10
+
11
+ it("should store and retrieve pool credential", async () => {
12
+ await vault.putPool("api.example.com", { type: "bearer", token: "tok_123" });
13
+ const cred = await vault.get("api.example.com");
14
+ expect(cred).not.toBeNull();
15
+ expect(cred!.auth).toEqual({ type: "bearer", token: "tok_123" });
16
+ expect(cred!.scope).toBe("pool");
17
+ });
18
+
19
+ it("should return null for unknown domain", async () => {
20
+ const cred = await vault.get("unknown.com");
21
+ expect(cred).toBeNull();
22
+ });
23
+
24
+ it("should prefer BYOK over pool", async () => {
25
+ await vault.putPool("api.example.com", { type: "bearer", token: "pool_tok" });
26
+ await vault.putByok("api.example.com", "dev-1", { type: "bearer", token: "byok_tok" });
27
+ const cred = await vault.get("api.example.com", "dev-1");
28
+ expect(cred!.auth).toEqual({ type: "bearer", token: "byok_tok" });
29
+ expect(cred!.scope).toBe("byok");
30
+ });
31
+
32
+ it("should fall back to pool when no BYOK for developer", async () => {
33
+ await vault.putPool("api.example.com", { type: "bearer", token: "pool_tok" });
34
+ const cred = await vault.get("api.example.com", "dev-2");
35
+ expect(cred!.auth).toEqual({ type: "bearer", token: "pool_tok" });
36
+ });
37
+
38
+ it("should delete pool credential", async () => {
39
+ await vault.putPool("api.example.com", { type: "bearer", token: "tok" });
40
+ await vault.delete("api.example.com");
41
+ expect(await vault.get("api.example.com")).toBeNull();
42
+ });
43
+
44
+ it("should delete BYOK credential", async () => {
45
+ await vault.putByok("api.example.com", "dev-1", { type: "bearer", token: "tok" });
46
+ await vault.delete("api.example.com", "dev-1");
47
+ expect(await vault.get("api.example.com", "dev-1")).toBeNull();
48
+ });
49
+
50
+ it("should list domains", async () => {
51
+ await vault.putPool("api.example.com", { type: "bearer", token: "t1" });
52
+ await vault.putPool("other.api.com", { type: "bearer", token: "t2" });
53
+ const domains = await vault.listDomains();
54
+ expect(domains).toContain("api.example.com");
55
+ expect(domains).toContain("other.api.com");
56
+ });
57
+
58
+ it("should support api-key auth type", async () => {
59
+ await vault.putPool("cf.com", { type: "api-key", header: "X-Auth-Key", key: "abc" });
60
+ const cred = await vault.get("cf.com");
61
+ expect(cred!.auth).toEqual({ type: "api-key", header: "X-Auth-Key", key: "abc" });
62
+ });
63
+ });
@@ -0,0 +1,300 @@
1
+ import { describe, it, expect, beforeAll, beforeEach } from "vitest";
2
+ import { createGateway } from "../../src/http/app.js";
3
+ import { MemoryRegistryStore } from "../../src/registry/memory-store.js";
4
+ import { generateGatewayKeyPair, createTestToken } from "@nkmc/core/testing";
5
+ import type { GatewayKeyPair } from "@nkmc/core";
6
+
7
+ const SKILL_MD = `---
8
+ name: "Acme Store"
9
+ gateway: nkmc
10
+ version: "1.0"
11
+ roles: [agent]
12
+ ---
13
+
14
+ # Acme Store
15
+
16
+ E-commerce service for products.
17
+
18
+ ## Schema
19
+
20
+ ### products (read: public / write: agent)
21
+
22
+ Product catalog
23
+
24
+ | field | type | description |
25
+ |-------|------|-------------|
26
+ | id | string | Product ID |
27
+ | name | string | Product name |
28
+ | price | number | Price in USD |
29
+
30
+ ## API
31
+
32
+ ### List products
33
+
34
+ \`GET /api/products\` — public
35
+
36
+ Returns all products.
37
+
38
+ ### Create product
39
+
40
+ \`POST /api/products\` — 0.01 USDC / call, agent
41
+
42
+ Creates a new product.
43
+ `;
44
+
45
+ const ADMIN_TOKEN = "test-admin-secret";
46
+
47
+ describe("Gateway HTTP", () => {
48
+ let keys: GatewayKeyPair;
49
+
50
+ beforeAll(async () => {
51
+ keys = await generateGatewayKeyPair();
52
+ });
53
+
54
+ function createApp() {
55
+ const store = new MemoryRegistryStore();
56
+ const app = createGateway({
57
+ store,
58
+ gatewayPrivateKey: keys.privateKey,
59
+ gatewayPublicKey: keys.publicKey,
60
+ adminToken: ADMIN_TOKEN,
61
+ });
62
+ return { app, store };
63
+ }
64
+
65
+ describe("Admin Auth", () => {
66
+ it("should reject requests without auth", async () => {
67
+ const { app } = createApp();
68
+ const res = await app.request("/registry/services");
69
+ expect(res.status).toBe(401);
70
+ });
71
+
72
+ it("should reject requests with wrong token", async () => {
73
+ const { app } = createApp();
74
+ const res = await app.request("/registry/services", {
75
+ headers: { Authorization: "Bearer wrong-token" },
76
+ });
77
+ expect(res.status).toBe(403);
78
+ });
79
+
80
+ it("should accept requests with correct admin token", async () => {
81
+ const { app } = createApp();
82
+ const res = await app.request("/registry/services", {
83
+ headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
84
+ });
85
+ expect(res.status).toBe(200);
86
+ });
87
+ });
88
+
89
+ describe("Registry Routes", () => {
90
+ it("should register a service via skill.md", async () => {
91
+ const { app } = createApp();
92
+ const res = await app.request(
93
+ "/registry/services?domain=acme-store.com",
94
+ {
95
+ method: "POST",
96
+ headers: {
97
+ Authorization: `Bearer ${ADMIN_TOKEN}`,
98
+ "Content-Type": "text/markdown",
99
+ },
100
+ body: SKILL_MD,
101
+ },
102
+ );
103
+ expect(res.status).toBe(201);
104
+ const body = await res.json();
105
+ expect(body.ok).toBe(true);
106
+ expect(body.domain).toBe("acme-store.com");
107
+ expect(body.name).toBe("Acme Store");
108
+ });
109
+
110
+ it("should register a service via JSON body", async () => {
111
+ const { app } = createApp();
112
+ const res = await app.request(
113
+ "/registry/services?domain=acme-store.com",
114
+ {
115
+ method: "POST",
116
+ headers: {
117
+ Authorization: `Bearer ${ADMIN_TOKEN}`,
118
+ "Content-Type": "application/json",
119
+ },
120
+ body: JSON.stringify({ skillMd: SKILL_MD }),
121
+ },
122
+ );
123
+ expect(res.status).toBe(201);
124
+ });
125
+
126
+ it("should list registered services", async () => {
127
+ const { app } = createApp();
128
+ // Register first
129
+ await app.request("/registry/services?domain=acme-store.com", {
130
+ method: "POST",
131
+ headers: {
132
+ Authorization: `Bearer ${ADMIN_TOKEN}`,
133
+ "Content-Type": "text/markdown",
134
+ },
135
+ body: SKILL_MD,
136
+ });
137
+
138
+ const res = await app.request("/registry/services", {
139
+ headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
140
+ });
141
+ expect(res.status).toBe(200);
142
+ const list = await res.json();
143
+ expect(list).toHaveLength(1);
144
+ expect(list[0].domain).toBe("acme-store.com");
145
+ });
146
+
147
+ it("should get service details", async () => {
148
+ const { app } = createApp();
149
+ await app.request("/registry/services?domain=acme-store.com", {
150
+ method: "POST",
151
+ headers: {
152
+ Authorization: `Bearer ${ADMIN_TOKEN}`,
153
+ "Content-Type": "text/markdown",
154
+ },
155
+ body: SKILL_MD,
156
+ });
157
+
158
+ const res = await app.request("/registry/services/acme-store.com", {
159
+ headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
160
+ });
161
+ expect(res.status).toBe(200);
162
+ const detail = await res.json();
163
+ expect(detail.domain).toBe("acme-store.com");
164
+ expect(detail.skillMd).toBeTruthy();
165
+ });
166
+
167
+ it("should return 404 for unknown service", async () => {
168
+ const { app } = createApp();
169
+ const res = await app.request("/registry/services/unknown.com", {
170
+ headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
171
+ });
172
+ expect(res.status).toBe(404);
173
+ });
174
+
175
+ it("should delete a service", async () => {
176
+ const { app } = createApp();
177
+ await app.request("/registry/services?domain=acme-store.com", {
178
+ method: "POST",
179
+ headers: {
180
+ Authorization: `Bearer ${ADMIN_TOKEN}`,
181
+ "Content-Type": "text/markdown",
182
+ },
183
+ body: SKILL_MD,
184
+ });
185
+
186
+ const res = await app.request("/registry/services/acme-store.com", {
187
+ method: "DELETE",
188
+ headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
189
+ });
190
+ expect(res.status).toBe(200);
191
+ const body = await res.json();
192
+ expect(body.ok).toBe(true);
193
+
194
+ // Verify deleted
195
+ const getRes = await app.request("/registry/services/acme-store.com", {
196
+ headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
197
+ });
198
+ expect(getRes.status).toBe(404);
199
+ });
200
+
201
+ it("should search services", async () => {
202
+ const { app } = createApp();
203
+ await app.request("/registry/services?domain=acme-store.com", {
204
+ method: "POST",
205
+ headers: {
206
+ Authorization: `Bearer ${ADMIN_TOKEN}`,
207
+ "Content-Type": "text/markdown",
208
+ },
209
+ body: SKILL_MD,
210
+ });
211
+
212
+ const res = await app.request("/registry/services?q=products", {
213
+ headers: { Authorization: `Bearer ${ADMIN_TOKEN}` },
214
+ });
215
+ expect(res.status).toBe(200);
216
+ const results = await res.json();
217
+ expect(results.length).toBeGreaterThan(0);
218
+ });
219
+
220
+ it("should reject missing domain parameter", async () => {
221
+ const { app } = createApp();
222
+ const res = await app.request("/registry/services", {
223
+ method: "POST",
224
+ headers: {
225
+ Authorization: `Bearer ${ADMIN_TOKEN}`,
226
+ "Content-Type": "text/markdown",
227
+ },
228
+ body: SKILL_MD,
229
+ });
230
+ expect(res.status).toBe(400);
231
+ });
232
+ });
233
+
234
+ describe("Auth Token Route", () => {
235
+ it("should issue a JWT token", async () => {
236
+ const { app } = createApp();
237
+ const res = await app.request("/auth/token", {
238
+ method: "POST",
239
+ headers: { "Content-Type": "application/json" },
240
+ body: JSON.stringify({
241
+ sub: "agent-1",
242
+ svc: "acme-store.com",
243
+ roles: ["agent"],
244
+ }),
245
+ });
246
+ expect(res.status).toBe(200);
247
+ const body = await res.json();
248
+ expect(body.token).toBeTruthy();
249
+ expect(typeof body.token).toBe("string");
250
+ });
251
+
252
+ it("should reject missing fields", async () => {
253
+ const { app } = createApp();
254
+ const res = await app.request("/auth/token", {
255
+ method: "POST",
256
+ headers: { "Content-Type": "application/json" },
257
+ body: JSON.stringify({ sub: "agent-1" }),
258
+ });
259
+ expect(res.status).toBe(400);
260
+ });
261
+ });
262
+
263
+ describe("Agent Auth", () => {
264
+ it("should reject /execute without JWT", async () => {
265
+ const { app } = createApp();
266
+ const res = await app.request("/execute", {
267
+ method: "POST",
268
+ headers: { "Content-Type": "application/json" },
269
+ body: JSON.stringify({ command: "ls /" }),
270
+ });
271
+ expect(res.status).toBe(401);
272
+ });
273
+
274
+ it("should reject /fs/ without JWT", async () => {
275
+ const { app } = createApp();
276
+ const res = await app.request("/fs/");
277
+ expect(res.status).toBe(401);
278
+ });
279
+
280
+ it("should accept /execute with valid JWT", async () => {
281
+ const { app } = createApp();
282
+ const token = await createTestToken(keys.privateKey, {
283
+ sub: "agent-1",
284
+ roles: ["agent"],
285
+ svc: "gateway",
286
+ });
287
+
288
+ const res = await app.request("/execute", {
289
+ method: "POST",
290
+ headers: {
291
+ Authorization: `Bearer ${token}`,
292
+ "Content-Type": "application/json",
293
+ },
294
+ body: JSON.stringify({ command: "ls /" }),
295
+ });
296
+ // Should succeed (200) even if empty - the request was authenticated
297
+ expect(res.status).toBe(200);
298
+ });
299
+ });
300
+ });