@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.
- package/dist/chunk-56RA53VS.js +37 -0
- package/dist/chunk-CZJ75YTV.js +969 -0
- package/dist/chunk-QGM4M3NI.js +37 -0
- package/dist/http.cjs +1772 -0
- package/dist/http.d.cts +49 -0
- package/dist/http.d.ts +49 -0
- package/dist/http.js +748 -0
- package/dist/index.cjs +2436 -0
- package/dist/index.d.cts +436 -0
- package/dist/index.d.ts +436 -0
- package/dist/index.js +1434 -0
- package/dist/proxy-ClPcDgsO.d.cts +283 -0
- package/dist/proxy-qpda1ANS.d.ts +283 -0
- package/dist/proxy.cjs +148 -0
- package/dist/proxy.d.cts +6 -0
- package/dist/proxy.d.ts +6 -0
- package/dist/proxy.js +90 -0
- package/dist/testing.cjs +865 -0
- package/dist/testing.d.cts +12 -0
- package/dist/testing.d.ts +12 -0
- package/dist/testing.js +831 -0
- package/dist/tunnels-BviBEaih.d.cts +12 -0
- package/dist/tunnels-DFHNgmN7.d.ts +12 -0
- package/dist/types-C6JC9oTm.d.cts +21 -0
- package/dist/types-C6JC9oTm.d.ts +21 -0
- package/package.json +47 -0
- package/src/__tests__/sqlite-integration.test.ts +384 -0
- package/src/credential/d1-vault.ts +134 -0
- package/src/credential/memory-vault.ts +50 -0
- package/src/credential/types.ts +16 -0
- package/src/d1/__tests__/sqlite-adapter.test.ts +75 -0
- package/src/d1/sqlite-adapter.ts +59 -0
- package/src/d1/types.ts +22 -0
- package/src/federation/__tests__/d1-peer-store.test.ts +218 -0
- package/src/federation/__tests__/peer-client.test.ts +205 -0
- package/src/federation/__tests__/peer-store.test.ts +114 -0
- package/src/federation/d1-peer-store.ts +164 -0
- package/src/federation/peer-backend.ts +60 -0
- package/src/federation/peer-client.ts +122 -0
- package/src/federation/peer-store.ts +45 -0
- package/src/federation/types.ts +39 -0
- package/src/http/app.ts +152 -0
- package/src/http/lib/dns.ts +30 -0
- package/src/http/middleware/admin-auth.ts +18 -0
- package/src/http/middleware/agent-auth.ts +27 -0
- package/src/http/middleware/publish-auth.ts +39 -0
- package/src/http/routes/__tests__/federation.test.ts +364 -0
- package/src/http/routes/__tests__/peers.test.ts +290 -0
- package/src/http/routes/__tests__/proxy.test.ts +159 -0
- package/src/http/routes/auth.ts +39 -0
- package/src/http/routes/byok.ts +62 -0
- package/src/http/routes/credentials.ts +40 -0
- package/src/http/routes/domains.ts +174 -0
- package/src/http/routes/federation.ts +170 -0
- package/src/http/routes/fs.ts +89 -0
- package/src/http/routes/peers.ts +103 -0
- package/src/http/routes/proxy.ts +57 -0
- package/src/http/routes/registry.ts +222 -0
- package/src/http/routes/tunnels.ts +124 -0
- package/src/http.ts +9 -0
- package/src/index.ts +63 -0
- package/src/metering/d1-store.ts +123 -0
- package/src/metering/memory-store.ts +29 -0
- package/src/metering/pricing-guard.ts +68 -0
- package/src/metering/types.ts +25 -0
- package/src/onboard/apis-guru.ts +64 -0
- package/src/onboard/index.ts +4 -0
- package/src/onboard/manifest.ts +362 -0
- package/src/onboard/pipeline.ts +214 -0
- package/src/onboard/types.ts +72 -0
- package/src/proxy/__tests__/tool-registry.test.ts +93 -0
- package/src/proxy/tool-registry.ts +122 -0
- package/src/proxy.ts +12 -0
- package/src/registry/context7-backend.ts +93 -0
- package/src/registry/context7.ts +54 -0
- package/src/registry/d1-store.ts +242 -0
- package/src/registry/memory-store.ts +101 -0
- package/src/registry/openapi-compiler.ts +284 -0
- package/src/registry/resolver.ts +196 -0
- package/src/registry/rpc-compiler.ts +142 -0
- package/src/registry/skill-parser.ts +119 -0
- package/src/registry/skill-to-config.ts +239 -0
- package/src/registry/source-refresher.ts +83 -0
- package/src/registry/types.ts +129 -0
- package/src/registry/virtual-files.ts +76 -0
- package/src/testing/sqlite-d1.ts +64 -0
- package/src/testing.ts +2 -0
- package/src/tunnel/__tests__/cloudflare-provider.test.ts +255 -0
- package/src/tunnel/__tests__/tunnel.test.ts +542 -0
- package/src/tunnel/cloudflare-provider.ts +121 -0
- package/src/tunnel/memory-store.ts +30 -0
- package/src/tunnel/types.ts +28 -0
- package/test/credential/d1-vault.test.ts +127 -0
- package/test/credential/injection.test.ts +67 -0
- package/test/credential/memory-vault.test.ts +63 -0
- package/test/http/app.test.ts +300 -0
- package/test/http/byok-e2e.test.ts +240 -0
- package/test/http/byok.test.ts +115 -0
- package/test/http/credentials.test.ts +57 -0
- package/test/http/e2e.test.ts +260 -0
- package/test/integration/authenticated-apis.test.ts +185 -0
- package/test/integration/free-apis-e2e.test.ts +222 -0
- package/test/metering/d1-store.test.ts +82 -0
- package/test/metering/memory-store.test.ts +76 -0
- package/test/metering/pricing-guard.test.ts +108 -0
- package/test/onboard/apis-guru.test.ts +57 -0
- package/test/onboard/e2e.test.ts +70 -0
- package/test/onboard/pipeline.test.ts +318 -0
- package/test/onboard/real-apis.test.ts +483 -0
- package/test/registry/compilation-correctness.test.ts +132 -0
- package/test/registry/context7-backend.test.ts +88 -0
- package/test/registry/context7-e2e.test.ts +92 -0
- package/test/registry/context7.test.ts +73 -0
- package/test/registry/d1-store.test.ts +184 -0
- package/test/registry/integration.test.ts +129 -0
- package/test/registry/lazy-mount.test.ts +138 -0
- package/test/registry/memory-store.test.ts +171 -0
- package/test/registry/openapi-compiler.test.ts +267 -0
- package/test/registry/openapi-e2e.test.ts +154 -0
- package/test/registry/passthrough-e2e.test.ts +109 -0
- package/test/registry/resolver-peer.test.ts +299 -0
- package/test/registry/resolver.test.ts +228 -0
- package/test/registry/rpc-compiler.test.ts +112 -0
- package/test/registry/skill-parser.test.ts +151 -0
- package/test/registry/skill-to-config.test.ts +151 -0
- package/test/registry/skill-to-rpc-config.test.ts +142 -0
- package/test/registry/source-refresher.test.ts +90 -0
- package/test/registry/virtual-files.test.ts +96 -0
- package/tsconfig.json +4 -0
- 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
|
+
});
|