@ghostgate/sdk 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/index.js ADDED
@@ -0,0 +1,277 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { privateKeyToAccount } from "viem/accounts";
3
+ import { GhostFulfillmentMerchant } from "./fulfillment.js";
4
+ export * from "./fulfillment.js";
5
+ const DEFAULT_BASE_URL = "https://ghostprotocol.cc";
6
+ const DEFAULT_CHAIN_ID = 8453;
7
+ const DEFAULT_SERVICE_SLUG = "connect";
8
+ const DEFAULT_CREDIT_COST = 1;
9
+ const DEFAULT_HEARTBEAT_INTERVAL_MS = 60000;
10
+ const ACCESS_TYPES = {
11
+ Access: [
12
+ { name: "service", type: "string" },
13
+ { name: "timestamp", type: "uint256" },
14
+ { name: "nonce", type: "string" },
15
+ ],
16
+ };
17
+ const normalizeBaseUrl = (value) => value.replace(/\/+$/, "");
18
+ const normalizeOptionalString = (value) => {
19
+ const trimmed = value?.trim();
20
+ return trimmed ? trimmed : null;
21
+ };
22
+ const getApiKeyPrefix = (apiKey) => {
23
+ if (apiKey.length <= 8)
24
+ return apiKey;
25
+ return `${apiKey.slice(0, 8)}...`;
26
+ };
27
+ const parsePayload = async (response) => {
28
+ try {
29
+ return await response.json();
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ };
35
+ const deriveAgentId = (serviceSlug) => {
36
+ if (!serviceSlug)
37
+ return null;
38
+ const match = /^agent-(.+)$/i.exec(serviceSlug);
39
+ return match?.[1] ?? null;
40
+ };
41
+ const toOptionalMetadata = (value) => value && Object.keys(value).length > 0 ? value : undefined;
42
+ const assertTelemetryIdentity = (input) => {
43
+ if (input.apiKey || input.agentId || input.serviceSlug)
44
+ return;
45
+ throw new Error("Telemetry calls require at least one of apiKey, agentId, or serviceSlug.");
46
+ };
47
+ const normalizeTelemetryStatusCode = (value) => {
48
+ if (value == null)
49
+ return null;
50
+ if (!Number.isInteger(value) || value < 100 || value > 599) {
51
+ throw new Error("statusCode must be an integer in the HTTP status range.");
52
+ }
53
+ return value;
54
+ };
55
+ const buildCanaryHeaders = () => ({
56
+ "cache-control": "no-store",
57
+ "content-type": "application/json; charset=utf-8",
58
+ });
59
+ export const buildCanaryPayload = (serviceSlug) => {
60
+ const normalized = normalizeOptionalString(serviceSlug);
61
+ if (!normalized)
62
+ throw new Error("serviceSlug is required for canary payloads.");
63
+ return {
64
+ ghostgate: "ready",
65
+ service: normalized,
66
+ };
67
+ };
68
+ export const createCanaryHandler = (serviceSlug) => {
69
+ const payload = buildCanaryPayload(serviceSlug);
70
+ return (_req, res) => {
71
+ const headers = buildCanaryHeaders();
72
+ if (res && typeof res === "object") {
73
+ const response = res;
74
+ if (typeof response.status === "function" && typeof response.json === "function") {
75
+ for (const [key, value] of Object.entries(headers)) {
76
+ response.setHeader?.(key, value);
77
+ }
78
+ response.status(200);
79
+ return response.json(payload);
80
+ }
81
+ if (typeof response.writeHead === "function" && typeof response.end === "function") {
82
+ response.writeHead(200, headers);
83
+ return response.end(JSON.stringify(payload));
84
+ }
85
+ }
86
+ return {
87
+ status: 200,
88
+ headers,
89
+ body: payload,
90
+ };
91
+ };
92
+ };
93
+ export class GhostAgent {
94
+ constructor(config = {}) {
95
+ const normalizedServiceSlug = normalizeOptionalString(config.serviceSlug);
96
+ this.apiKey = normalizeOptionalString(config.apiKey) ?? null;
97
+ this.agentId = normalizeOptionalString(config.agentId);
98
+ this.baseUrl = normalizeBaseUrl(config.baseUrl ?? DEFAULT_BASE_URL);
99
+ this.privateKey = config.privateKey ?? null;
100
+ this.chainId = config.chainId ?? DEFAULT_CHAIN_ID;
101
+ this.telemetryServiceSlug = normalizedServiceSlug;
102
+ this.serviceSlug = normalizedServiceSlug ?? DEFAULT_SERVICE_SLUG;
103
+ this.creditCost = Number.isFinite(config.creditCost) && (config.creditCost ?? 0) > 0
104
+ ? Math.trunc(config.creditCost)
105
+ : DEFAULT_CREDIT_COST;
106
+ }
107
+ async connect(apiKey) {
108
+ const normalizedApiKey = normalizeOptionalString(apiKey) ?? this.apiKey;
109
+ if (!normalizedApiKey) {
110
+ throw new Error("connect(apiKey?) requires a non-empty API key via argument or constructor config.");
111
+ }
112
+ if (!this.privateKey) {
113
+ throw new Error("GhostAgent requires a signing privateKey in constructor config to call /api/gate/[...slug].");
114
+ }
115
+ const timestamp = BigInt(Math.floor(Date.now() / 1000));
116
+ const signedPayload = {
117
+ service: this.serviceSlug,
118
+ timestamp,
119
+ nonce: randomUUID().replace(/-/g, ""),
120
+ };
121
+ const headerPayload = {
122
+ service: this.serviceSlug,
123
+ timestamp: timestamp.toString(),
124
+ nonce: signedPayload.nonce,
125
+ };
126
+ const account = privateKeyToAccount(this.privateKey);
127
+ const signature = await account.signTypedData({
128
+ domain: {
129
+ name: "GhostGate",
130
+ version: "1",
131
+ chainId: this.chainId,
132
+ },
133
+ types: ACCESS_TYPES,
134
+ primaryType: "Access",
135
+ message: signedPayload,
136
+ });
137
+ const endpoint = `${this.baseUrl}/api/gate/${encodeURIComponent(this.serviceSlug)}`;
138
+ const response = await fetch(endpoint, {
139
+ method: "POST",
140
+ headers: {
141
+ "x-ghost-sig": signature,
142
+ "x-ghost-payload": JSON.stringify(headerPayload),
143
+ "x-ghost-credit-cost": String(this.creditCost),
144
+ accept: "application/json, text/plain;q=0.9, */*;q=0.8",
145
+ },
146
+ cache: "no-store",
147
+ });
148
+ const responsePayload = await parsePayload(response);
149
+ if (response.ok) {
150
+ this.apiKey = normalizedApiKey;
151
+ }
152
+ return {
153
+ connected: response.ok,
154
+ apiKeyPrefix: getApiKeyPrefix(normalizedApiKey),
155
+ endpoint,
156
+ status: response.status,
157
+ payload: responsePayload,
158
+ };
159
+ }
160
+ async pulse(input = {}) {
161
+ const apiKey = normalizeOptionalString(input.apiKey) ?? this.apiKey;
162
+ const serviceSlug = normalizeOptionalString(input.serviceSlug) ?? this.telemetryServiceSlug;
163
+ const agentId = normalizeOptionalString(input.agentId) ?? this.agentId ?? deriveAgentId(serviceSlug);
164
+ assertTelemetryIdentity({ apiKey, agentId, serviceSlug });
165
+ const endpoint = `${this.baseUrl}/api/telemetry/pulse`;
166
+ const response = await fetch(endpoint, {
167
+ method: "POST",
168
+ headers: {
169
+ "content-type": "application/json",
170
+ accept: "application/json, text/plain;q=0.9, */*;q=0.8",
171
+ },
172
+ body: JSON.stringify({
173
+ ...(apiKey ? { apiKey } : {}),
174
+ ...(agentId ? { agentId } : {}),
175
+ ...(serviceSlug ? { serviceSlug } : {}),
176
+ ...(toOptionalMetadata(input.metadata) ? { metadata: input.metadata } : {}),
177
+ }),
178
+ cache: "no-store",
179
+ });
180
+ return {
181
+ ok: response.ok,
182
+ endpoint,
183
+ status: response.status,
184
+ payload: await parsePayload(response),
185
+ };
186
+ }
187
+ async outcome(input) {
188
+ const apiKey = normalizeOptionalString(input.apiKey) ?? this.apiKey;
189
+ const serviceSlug = normalizeOptionalString(input.serviceSlug) ?? this.telemetryServiceSlug;
190
+ const agentId = normalizeOptionalString(input.agentId) ?? this.agentId ?? deriveAgentId(serviceSlug);
191
+ const statusCode = normalizeTelemetryStatusCode(input.statusCode);
192
+ assertTelemetryIdentity({ apiKey, agentId, serviceSlug });
193
+ const endpoint = `${this.baseUrl}/api/telemetry/outcome`;
194
+ const response = await fetch(endpoint, {
195
+ method: "POST",
196
+ headers: {
197
+ "content-type": "application/json",
198
+ accept: "application/json, text/plain;q=0.9, */*;q=0.8",
199
+ },
200
+ body: JSON.stringify({
201
+ ...(apiKey ? { apiKey } : {}),
202
+ ...(agentId ? { agentId } : {}),
203
+ ...(serviceSlug ? { serviceSlug } : {}),
204
+ success: Boolean(input.success),
205
+ ...(statusCode != null ? { statusCode } : {}),
206
+ ...(toOptionalMetadata(input.metadata) ? { metadata: input.metadata } : {}),
207
+ }),
208
+ cache: "no-store",
209
+ });
210
+ return {
211
+ ok: response.ok,
212
+ endpoint,
213
+ status: response.status,
214
+ payload: await parsePayload(response),
215
+ };
216
+ }
217
+ startHeartbeat(options = {}) {
218
+ const intervalMs = Number.isFinite(options.intervalMs) && (options.intervalMs ?? 0) > 0
219
+ ? Math.trunc(options.intervalMs)
220
+ : DEFAULT_HEARTBEAT_INTERVAL_MS;
221
+ const immediate = options.immediate ?? true;
222
+ let stopped = false;
223
+ let inFlight = false;
224
+ const tick = async () => {
225
+ if (stopped || inFlight)
226
+ return;
227
+ inFlight = true;
228
+ try {
229
+ const result = await this.pulse(options);
230
+ options.onResult?.(result);
231
+ }
232
+ catch (error) {
233
+ options.onError?.(error);
234
+ }
235
+ finally {
236
+ inFlight = false;
237
+ }
238
+ };
239
+ const timer = setInterval(() => {
240
+ void tick();
241
+ }, intervalMs);
242
+ if (immediate) {
243
+ void tick();
244
+ }
245
+ return {
246
+ stop: () => {
247
+ if (stopped)
248
+ return;
249
+ stopped = true;
250
+ clearInterval(timer);
251
+ },
252
+ };
253
+ }
254
+ get isConnected() {
255
+ return this.apiKey !== null;
256
+ }
257
+ get endpoint() {
258
+ return `${this.baseUrl}/api/gate`;
259
+ }
260
+ }
261
+ export class GhostMerchant extends GhostFulfillmentMerchant {
262
+ constructor(config) {
263
+ super(config);
264
+ const normalizedServiceSlug = normalizeOptionalString(config.serviceSlug);
265
+ if (!normalizedServiceSlug) {
266
+ throw new Error("GhostMerchant.serviceSlug is required.");
267
+ }
268
+ this.merchantServiceSlug = normalizedServiceSlug;
269
+ }
270
+ canaryPayload() {
271
+ return buildCanaryPayload(this.merchantServiceSlug);
272
+ }
273
+ canaryHandler() {
274
+ return createCanaryHandler(this.merchantServiceSlug);
275
+ }
276
+ }
277
+ export default GhostAgent;
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@ghostgate/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Ghost Protocol Node.js SDK for gate access, fulfillment, telemetry, and canary helpers.",
5
+ "author": "Ghost Protocol",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "homepage": "https://ghostprotocol.cc",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/Ghost-Protocol-Infrastructure/GHOST_PROTOCOL.git",
12
+ "directory": "packages/sdk"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/Ghost-Protocol-Infrastructure/GHOST_PROTOCOL/issues"
16
+ },
17
+ "keywords": [
18
+ "ghostprotocol",
19
+ "ghostgate",
20
+ "sdk",
21
+ "fulfillment",
22
+ "eip712",
23
+ "telemetry",
24
+ "base"
25
+ ],
26
+ "publishConfig": {
27
+ "access": "public"
28
+ },
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "sideEffects": false,
33
+ "files": [
34
+ "dist",
35
+ "README.md",
36
+ "LICENSE"
37
+ ],
38
+ "main": "./dist/index.js",
39
+ "module": "./dist/index.js",
40
+ "types": "./dist/index.d.ts",
41
+ "exports": {
42
+ ".": {
43
+ "types": "./dist/index.d.ts",
44
+ "import": "./dist/index.js"
45
+ },
46
+ "./fulfillment": {
47
+ "types": "./dist/fulfillment.d.ts",
48
+ "import": "./dist/fulfillment.js"
49
+ },
50
+ "./package.json": "./package.json"
51
+ },
52
+ "dependencies": {
53
+ "viem": "^2.21.0"
54
+ },
55
+ "scripts": {
56
+ "clean": "node -e \"import('node:fs').then(fs => fs.rmSync('dist', { recursive: true, force: true }))\"",
57
+ "build": "npm run clean && node ../../node_modules/typescript/bin/tsc -p tsconfig.build.json",
58
+ "prepack": "npm run build"
59
+ }
60
+ }