@astrasyncai/verification-gateway 2.3.10 → 2.4.1
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/README.md +141 -9
- package/dist/bin/astrasync.js +498 -0
- package/dist/registration/index.d.mts +369 -0
- package/dist/registration/index.d.ts +369 -0
- package/dist/registration/index.js +368 -0
- package/dist/registration/index.js.map +1 -0
- package/dist/registration/index.mjs +325 -0
- package/dist/registration/index.mjs.map +1 -0
- package/package.json +14 -2
package/README.md
CHANGED
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
# @astrasyncai/verification-gateway
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The AstraSync KYA Platform SDK. One package, two roles: register agents with the KYA backend, and verify agents at any counterparty type (API / MCP / website / agent-to-agent).
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## What's in this package?
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
As of v2.4.0 this is the only npm package you need for AstraSync integration. Pick the subpath that matches what you're doing:
|
|
8
8
|
|
|
9
|
-
|
|
|
10
|
-
|
|
|
11
|
-
| **
|
|
12
|
-
| **
|
|
13
|
-
| **
|
|
9
|
+
| If you're… | Import from | Get |
|
|
10
|
+
| ----------------------------------------------------------------------------------------- | ------------------------------------------------ | --------------------------------------------------------------------------------------------- |
|
|
11
|
+
| **Registering an agent with the KYA backend** | `@astrasyncai/verification-gateway/registration` | `AstraSync` class — `register()`, `verify()`, `health()`. The `astrasync` CLI is bundled too. |
|
|
12
|
+
| **Building an agent that calls out to other services** (per-request credential injection) | `@astrasyncai/verification-gateway/agent` | `AgentClient`, `ChallengeHandler`, ownership validation, runtime PDLSS shape. |
|
|
13
|
+
| **Running an Express API that AI agents call into** | `@astrasyncai/verification-gateway/express` | `createMiddleware()` — protect routes, attach verification context. |
|
|
14
|
+
| **Running a Next.js app that AI agents call into** | `@astrasyncai/verification-gateway/nextjs` | `createMiddleware()`, Commerce Shield wiring. |
|
|
15
|
+
| **Running an MCP server that AI agents call into** | `@astrasyncai/verification-gateway/mcp` | MCP tool gates + inner-hop dedupe headers. |
|
|
16
|
+
| **Doing direct verification (no framework)** | `@astrasyncai/verification-gateway/sdk` | `VerificationGatewayClient` with retry / backoff / timeout. |
|
|
17
|
+
| **Parsing protocol-level credentials** (HTTP / A2A / MCP / x402 / AP2 / MPP) | `@astrasyncai/verification-gateway/transport` | Cross-protocol credential extraction + injection. |
|
|
18
|
+
| **Evaluating PDLSS offline** (local / hybrid mode) | `@astrasyncai/verification-gateway/gateway` | `AstraSyncGateway` (online / local / hybrid). |
|
|
19
|
+
| **Receiving webhook callbacks** | `@astrasyncai/verification-gateway/webhooks` | `verifyAstraSyncWebhook()`, `signAstraSyncWebhook()`. |
|
|
14
20
|
|
|
15
|
-
>
|
|
21
|
+
> All subpaths talk to the same backend — `POST /agents/verify-access` for verification, `POST /agents/register` for registration. The contract is shared; the role is just which side of the handshake you're sitting on.
|
|
22
|
+
|
|
23
|
+
Before v2.4.0 the agent-registration half shipped as a separate package (`@astrasyncai/agent-registration`, briefly; `@astrasyncai/sdk` before that). It was merged in to stop the two-package version drift and to give partners one install instead of two. The legacy `@astrasyncai/sdk` package on npm is deprecated with a pointer to this package; `@astrasyncai/agent-registration` was never published.
|
|
16
24
|
|
|
17
25
|
## Overview
|
|
18
26
|
|
|
@@ -95,6 +103,130 @@ if (result.verified && result.accessLevel !== 'none') {
|
|
|
95
103
|
}
|
|
96
104
|
```
|
|
97
105
|
|
|
106
|
+
### Agent Registration
|
|
107
|
+
|
|
108
|
+
**Use the SDK for all agent registration**, whether you're a developer firing off a one-shot CLI call or an autonomous agent self-registering on first run. The SDK handles auth-mode routing for you: signature-authenticated callers get synchronous registration; API-key-only callers get an owner-approval handshake (server-side rule, not negotiable).
|
|
109
|
+
|
|
110
|
+
#### `apiEndpoint` — runtime-challenge URL
|
|
111
|
+
|
|
112
|
+
`apiEndpoint` is the URL where your agent's verification-gateway SDK is mounted to receive runtime challenges from counterparties. Optional but recommended — if you omit it, your agent declares no runtime-challenge support, which is included in the verification payload and may cause some counterparties to decline access requests.
|
|
113
|
+
|
|
114
|
+
#### Two registration response modes
|
|
115
|
+
|
|
116
|
+
The same `register()` call returns one of two shapes depending on auth context:
|
|
117
|
+
|
|
118
|
+
- **201 active** — synchronous: returned when the request is signed with a crypto keypair (`privateKey` configured) or when authenticated via email+password. Result: `{ status: 'active', agent }`.
|
|
119
|
+
- **202 pending_approval** — API-key only: the SDK was authenticated with an API key but the request was not signed. The backend emails the account owner a "Sign In to Accept" link, fires a dashboard alert, and returns a tracking token. Result: `{ status: 'pending_approval', requestId, pollUrl, expiresAt }`. The agent becomes active only after the owner approves.
|
|
120
|
+
|
|
121
|
+
This split enforces the platform rule that **API-key registrations always require owner step-up** — registering an agent autonomously with just an API key is not a sufficient authority signal on its own.
|
|
122
|
+
|
|
123
|
+
#### Pattern A — developer / long-running agent (block until approved)
|
|
124
|
+
|
|
125
|
+
Best for CLI tools, dev-machine first-run, and long-running services:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import {
|
|
129
|
+
AstraSync,
|
|
130
|
+
RegistrationDeniedError,
|
|
131
|
+
RegistrationTimeoutError,
|
|
132
|
+
} from '@astrasyncai/verification-gateway/registration';
|
|
133
|
+
|
|
134
|
+
const sdk = new AstraSync({
|
|
135
|
+
apiKey: process.env.ASTRASYNC_API_KEY,
|
|
136
|
+
privateKey: process.env.ASTRASYNC_PRIVATE_KEY, // optional — when set, 201 sync path
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
const agent = await sdk.register({
|
|
141
|
+
name: 'invoice-bot',
|
|
142
|
+
description: 'Reconciles AP/AR across accounting systems.',
|
|
143
|
+
agentType: 'autonomous',
|
|
144
|
+
apiEndpoint: 'https://invoice-bot.example.com', // runtime-challenge URL
|
|
145
|
+
model: { modelProvider: 'anthropic', modelName: 'claude-sonnet-4-5' },
|
|
146
|
+
framework: { frameworkName: 'langchain', frameworkVersion: '0.3.0' },
|
|
147
|
+
protocols: ['a2a', 'mcp'],
|
|
148
|
+
pdlss: {
|
|
149
|
+
purpose: {
|
|
150
|
+
categories: ['accounting'],
|
|
151
|
+
allowedActions: ['accounting.read', 'accounting.write'],
|
|
152
|
+
},
|
|
153
|
+
limits: { stepUpThreshold: 1_000, approvalThreshold: 10_000, currency: 'USD' },
|
|
154
|
+
scope: { resources: ['xero.invoices', 'quickbooks.bills'] },
|
|
155
|
+
selfInstantiation: { allowed: false },
|
|
156
|
+
},
|
|
157
|
+
// Blocking mode: poll until the request resolves.
|
|
158
|
+
waitForApproval: true,
|
|
159
|
+
timeoutMs: 10 * 60 * 1000, // 10 minutes default
|
|
160
|
+
onPending: ({ ageMs }) =>
|
|
161
|
+
console.log(`Awaiting owner approval (${(ageMs / 1000).toFixed(0)}s)…`),
|
|
162
|
+
});
|
|
163
|
+
console.log(`Agent registered: ${agent.astrasyncIdLevel1}`);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (err instanceof RegistrationDeniedError) {
|
|
166
|
+
console.error('Owner denied:', err.reason);
|
|
167
|
+
} else if (err instanceof RegistrationTimeoutError) {
|
|
168
|
+
console.error(
|
|
169
|
+
'Timed out — request is still active server-side; poll later via sdk.waitForApproval(requestId)'
|
|
170
|
+
);
|
|
171
|
+
} else {
|
|
172
|
+
throw err;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Pattern B — serverless / scheduled agent (non-blocking, exit and resume)
|
|
178
|
+
|
|
179
|
+
Best for Lambda / Cloud Functions / cron-driven self-registration where you can't hold the runtime open for minutes:
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
const sdk = new AstraSync({ apiKey: process.env.ASTRASYNC_API_KEY });
|
|
183
|
+
|
|
184
|
+
const result = await sdk.register({
|
|
185
|
+
name: 'invoice-bot',
|
|
186
|
+
apiEndpoint: 'https://invoice-bot.example.com',
|
|
187
|
+
pdlss: { purpose: { categories: ['accounting'], allowedActions: ['accounting.read'] } },
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (result.status === 'pending_approval') {
|
|
191
|
+
// Store the requestId in your durable storage (DynamoDB, KV, etc.)
|
|
192
|
+
await store.set('astrasync.pendingRequestId', result.requestId);
|
|
193
|
+
console.log(`Awaiting owner approval at ${result.pollUrl}; will resume on next scheduled run.`);
|
|
194
|
+
return; // function exits — owner has time to approve
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
console.log(`Agent registered as ${result.agent.astrasyncIdLevel1}`);
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
On the next scheduled run, resume by polling:
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
const requestId = await store.get('astrasync.pendingRequestId');
|
|
204
|
+
const status = await sdk.pollRegistration(requestId);
|
|
205
|
+
if (status.state === 'approved') {
|
|
206
|
+
await store.set('astrasync.agentId', status.agent.kyaAgentId);
|
|
207
|
+
await store.delete('astrasync.pendingRequestId');
|
|
208
|
+
} else if (status.state === 'denied' || status.state === 'expired') {
|
|
209
|
+
console.error(`Registration terminated: ${status.state}`);
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
CLI equivalent — same surface from a shell, also via `npx`:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
npx astrasync register --name invoice-bot --agent-type autonomous \
|
|
217
|
+
--model-provider anthropic --model-name claude-sonnet-4-5 \
|
|
218
|
+
--framework-name langchain --framework-version 0.3.0 \
|
|
219
|
+
--protocols a2a,mcp \
|
|
220
|
+
--api-endpoint https://invoice-bot.example.com \
|
|
221
|
+
--pdlss '{"purpose":{"categories":["accounting"],"allowedActions":["accounting.read"]}}'
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
The CLI uses the same response logic — it will print a "Sign in to approve" message and a poll URL when the response is 202 pending, and exit non-zero on deny/expire.
|
|
225
|
+
|
|
226
|
+
> `/registration` is for **one-shot onboarding** — register an agent, look up its public profile, check API health. For **per-request credential injection** (attaching ASTRA-id, sessionId, PDLSS to outgoing HTTP / A2A / MCP calls from an already-registered agent), use `/agent` (`AgentClient`). The two roles are deliberately separate concerns: registration is identity, `/agent` is runtime.
|
|
227
|
+
|
|
228
|
+
> A `PDLSSConfig` type exists in both `/registration` and `/agent`, with **different shapes**. `/registration`'s `PDLSSConfig` is the boundary declaration submitted at register time; `/agent`'s `PDLSSConfig` is the per-request runtime request shape. Disambiguate via the import path — the root export deliberately does not re-export either.
|
|
229
|
+
|
|
98
230
|
### Inner-hop verification (MCP → REST dedupe with X-Astra-Verified-Hop)
|
|
99
231
|
|
|
100
232
|
When an MCP tool calls an inner REST endpoint that ALSO runs the
|
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/registration/errors.ts
|
|
27
|
+
var AstraSyncError = class extends Error {
|
|
28
|
+
constructor(message, statusCode, code) {
|
|
29
|
+
super(message);
|
|
30
|
+
this.name = "AstraSyncError";
|
|
31
|
+
this.statusCode = statusCode;
|
|
32
|
+
this.code = code;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var KYDRequiredError = class extends AstraSyncError {
|
|
36
|
+
constructor(response) {
|
|
37
|
+
const kydUrl = response.kydUrl || "https://astrasync.ai/developer-profile";
|
|
38
|
+
super(
|
|
39
|
+
`KYD verification required before registering agents.
|
|
40
|
+
Complete your KYD profile at: ${kydUrl}`,
|
|
41
|
+
403,
|
|
42
|
+
"KYD_REQUIRED"
|
|
43
|
+
);
|
|
44
|
+
this.name = "KYDRequiredError";
|
|
45
|
+
this.kydUrl = kydUrl;
|
|
46
|
+
this.ownerNotified = response.ownerNotified || false;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var AuthenticationError = class extends AstraSyncError {
|
|
50
|
+
constructor(message) {
|
|
51
|
+
super(message, 401, "AUTH_FAILED");
|
|
52
|
+
this.name = "AuthenticationError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var RegistrationDeniedError = class extends AstraSyncError {
|
|
56
|
+
constructor(requestId, reason) {
|
|
57
|
+
super(
|
|
58
|
+
`Registration request ${requestId} was denied by the account owner.${reason ? ` Reason: ${reason}` : ""}`,
|
|
59
|
+
403,
|
|
60
|
+
"REGISTRATION_DENIED"
|
|
61
|
+
);
|
|
62
|
+
this.name = "RegistrationDeniedError";
|
|
63
|
+
this.requestId = requestId;
|
|
64
|
+
this.reason = reason;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
var RegistrationExpiredError = class extends AstraSyncError {
|
|
68
|
+
constructor(requestId) {
|
|
69
|
+
super(
|
|
70
|
+
`Registration request ${requestId} expired before the owner approved it. Submit a new registration request.`,
|
|
71
|
+
410,
|
|
72
|
+
"REGISTRATION_EXPIRED"
|
|
73
|
+
);
|
|
74
|
+
this.name = "RegistrationExpiredError";
|
|
75
|
+
this.requestId = requestId;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
var RegistrationTimeoutError = class extends AstraSyncError {
|
|
79
|
+
constructor(requestId) {
|
|
80
|
+
super(
|
|
81
|
+
`Timed out waiting for owner approval of registration request ${requestId}. The request is still active server-side; poll the request to resume waiting.`,
|
|
82
|
+
408,
|
|
83
|
+
"REGISTRATION_TIMEOUT"
|
|
84
|
+
);
|
|
85
|
+
this.name = "RegistrationTimeoutError";
|
|
86
|
+
this.requestId = requestId;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// src/registration/api.ts
|
|
91
|
+
var DEFAULT_BASE_URL = "https://astrasync.ai";
|
|
92
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
93
|
+
var AstraSync = class {
|
|
94
|
+
constructor(config = {}) {
|
|
95
|
+
this.baseUrl = (config.baseUrl || process.env.ASTRASYNC_API_URL || DEFAULT_BASE_URL).replace(
|
|
96
|
+
/\/+$/,
|
|
97
|
+
""
|
|
98
|
+
);
|
|
99
|
+
this.apiKey = config.apiKey || process.env.ASTRASYNC_API_KEY;
|
|
100
|
+
this.email = config.email;
|
|
101
|
+
this.password = config.password;
|
|
102
|
+
this.privateKey = config.privateKey;
|
|
103
|
+
if (!this.apiKey && !this.email) {
|
|
104
|
+
throw new AuthenticationError(
|
|
105
|
+
"Authentication required. Provide apiKey, or email+password. Set ASTRASYNC_API_KEY env var or pass config to constructor."
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
if (this.email && !this.password) {
|
|
109
|
+
throw new AuthenticationError("Password is required when using email authentication.");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Register a new AI agent on the AstraSync KYA Platform.
|
|
114
|
+
*
|
|
115
|
+
* The backend response depends on auth context:
|
|
116
|
+
* - **Crypto-keypair signed** (`privateKey` configured): synchronous 201,
|
|
117
|
+
* returns `{ status: 'active', agent }`.
|
|
118
|
+
* - **API-key only** (no signature): 202 pending, returns
|
|
119
|
+
* `{ status: 'pending_approval', requestId, pollUrl, expiresAt }`. The
|
|
120
|
+
* owner is notified by email and a dashboard alert is emitted; the agent
|
|
121
|
+
* becomes active only after the owner approves.
|
|
122
|
+
*
|
|
123
|
+
* Blocking mode: pass `{ waitForApproval: true }` to have the SDK poll the
|
|
124
|
+
* request until it resolves, then return the live agent record. The promise
|
|
125
|
+
* rejects with `RegistrationDeniedError`, `RegistrationExpiredError`, or
|
|
126
|
+
* `RegistrationTimeoutError` on the corresponding terminal states.
|
|
127
|
+
*
|
|
128
|
+
* @example Non-blocking (default — best for serverless / scheduled agents):
|
|
129
|
+
* ```typescript
|
|
130
|
+
* const result = await sdk.register({ name, pdlss });
|
|
131
|
+
* if (result.status === 'pending_approval') {
|
|
132
|
+
* storeRequestId(result.requestId);
|
|
133
|
+
* return; // function exits; resume later via pollRegistration()
|
|
134
|
+
* }
|
|
135
|
+
* ```
|
|
136
|
+
*
|
|
137
|
+
* @example Blocking (best for long-running services + CLI):
|
|
138
|
+
* ```typescript
|
|
139
|
+
* const agent = await sdk.register({
|
|
140
|
+
* name, pdlss, waitForApproval: true, timeoutMs: 600_000,
|
|
141
|
+
* onPending: ({ ageMs }) => console.log(`waiting ${ageMs}ms`),
|
|
142
|
+
* });
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
async register(options) {
|
|
146
|
+
const body = {
|
|
147
|
+
name: options.name,
|
|
148
|
+
...options.description && { description: options.description },
|
|
149
|
+
...options.agentType && { agentType: options.agentType },
|
|
150
|
+
...options.apiEndpoint && { apiEndpoint: options.apiEndpoint },
|
|
151
|
+
...options.model && { model: options.model },
|
|
152
|
+
...options.framework && { framework: options.framework },
|
|
153
|
+
...options.protocols && { protocols: options.protocols },
|
|
154
|
+
...options.metadata && { metadata: options.metadata },
|
|
155
|
+
...options.pdlss && { pdlss: options.pdlss }
|
|
156
|
+
};
|
|
157
|
+
const { status, body: raw } = await this.requestWithStatus("POST", "/api/agents/register", body);
|
|
158
|
+
if (status === 201) {
|
|
159
|
+
const active = {
|
|
160
|
+
status: "active",
|
|
161
|
+
agent: raw.data.agent
|
|
162
|
+
};
|
|
163
|
+
return active;
|
|
164
|
+
}
|
|
165
|
+
const pendingBody = raw;
|
|
166
|
+
const pending = {
|
|
167
|
+
status: "pending_approval",
|
|
168
|
+
requestId: pendingBody.requestId,
|
|
169
|
+
expiresAt: pendingBody.expiresAt,
|
|
170
|
+
pollUrl: pendingBody.pollUrl,
|
|
171
|
+
message: pendingBody.message
|
|
172
|
+
};
|
|
173
|
+
if (!options.waitForApproval) return pending;
|
|
174
|
+
return this.waitForApproval(pendingBody.requestId, options);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Poll the current state of a pending-approval registration request.
|
|
178
|
+
*
|
|
179
|
+
* Useful for caller-driven polling when `waitForApproval: false` (the
|
|
180
|
+
* default). The endpoint is unauthenticated — pass the `requestId` that
|
|
181
|
+
* was returned from the 202 response.
|
|
182
|
+
*
|
|
183
|
+
* @returns `state: 'pending'` while awaiting; `'approved'` carries the
|
|
184
|
+
* minted agent in `agent`; `'denied'` may carry the owner's
|
|
185
|
+
* `reason`; `'expired'` is terminal after 14 days.
|
|
186
|
+
*/
|
|
187
|
+
async pollRegistration(requestId) {
|
|
188
|
+
const url = `${this.baseUrl}/api/agents/request-registration/${requestId}`;
|
|
189
|
+
const res = await fetch(url, { headers: { Accept: "application/json" } });
|
|
190
|
+
if (!res.ok) {
|
|
191
|
+
const errBody = await res.json().catch(() => ({}));
|
|
192
|
+
throw new AstraSyncError(
|
|
193
|
+
errBody.error || `pollRegistration failed: ${res.status}`,
|
|
194
|
+
res.status,
|
|
195
|
+
errBody.code
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
return await res.json();
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Block until a pending registration request resolves to a terminal state.
|
|
202
|
+
* Resolves to the live `AgentRecord` on approval; rejects with the matching
|
|
203
|
+
* Registration*Error on deny/expire/timeout. Usually called via
|
|
204
|
+
* `register({ waitForApproval: true })`, but exposed for callers that want
|
|
205
|
+
* to fire-and-forget the initial register call and resume waiting later
|
|
206
|
+
* (e.g. after restoring a stored `requestId` on cold start).
|
|
207
|
+
*/
|
|
208
|
+
async waitForApproval(requestId, options = {}) {
|
|
209
|
+
const timeoutMs = options.timeoutMs ?? 10 * 60 * 1e3;
|
|
210
|
+
const pollIntervalMs = options.pollIntervalMs ?? 5e3;
|
|
211
|
+
const start = Date.now();
|
|
212
|
+
const deadline = start + timeoutMs;
|
|
213
|
+
while (Date.now() < deadline) {
|
|
214
|
+
const result = await this.pollRegistration(requestId);
|
|
215
|
+
const ageMs = Date.now() - start;
|
|
216
|
+
options.onPending?.({ requestId, ageMs });
|
|
217
|
+
if (result.state === "approved") {
|
|
218
|
+
if (!result.agent) {
|
|
219
|
+
throw new AstraSyncError(
|
|
220
|
+
`Registration ${requestId} reported approved but no agent payload returned.`,
|
|
221
|
+
500
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return result.agent;
|
|
225
|
+
}
|
|
226
|
+
if (result.state === "denied") {
|
|
227
|
+
throw new RegistrationDeniedError(requestId, result.reason);
|
|
228
|
+
}
|
|
229
|
+
if (result.state === "expired") {
|
|
230
|
+
throw new RegistrationExpiredError(requestId);
|
|
231
|
+
}
|
|
232
|
+
await sleep(pollIntervalMs);
|
|
233
|
+
}
|
|
234
|
+
throw new RegistrationTimeoutError(requestId);
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Look up an agent's public profile by ASTRA ID or UUID.
|
|
238
|
+
*/
|
|
239
|
+
async verify(agentId) {
|
|
240
|
+
return this.request("GET", `/api/agents/verify/${agentId}`);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Check API health.
|
|
244
|
+
*/
|
|
245
|
+
async health() {
|
|
246
|
+
const res = await fetch(`${this.baseUrl}/api/health/`);
|
|
247
|
+
if (!res.ok) {
|
|
248
|
+
throw new AstraSyncError(`Health check failed: ${res.status}`, res.status);
|
|
249
|
+
}
|
|
250
|
+
return res.json();
|
|
251
|
+
}
|
|
252
|
+
// ── Private helpers ──────────────────────────────────────────────
|
|
253
|
+
async request(method, endpoint, body) {
|
|
254
|
+
const { body: parsed } = await this.requestWithStatus(method, endpoint, body);
|
|
255
|
+
return parsed;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Variant of {@link request} that also returns the HTTP status code, so
|
|
259
|
+
* callers can branch on 201 vs 202 (or other success codes) without losing
|
|
260
|
+
* type information about the response body.
|
|
261
|
+
*/
|
|
262
|
+
async requestWithStatus(method, endpoint, body) {
|
|
263
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
264
|
+
const headers = {
|
|
265
|
+
"Content-Type": "application/json"
|
|
266
|
+
};
|
|
267
|
+
const token = await this.getAuthToken();
|
|
268
|
+
headers["Authorization"] = `Bearer ${token}`;
|
|
269
|
+
if (this.privateKey) {
|
|
270
|
+
const signature = await this.signRequest(method, endpoint, body || {});
|
|
271
|
+
headers["X-AstraSync-Signature"] = signature;
|
|
272
|
+
}
|
|
273
|
+
const res = await fetch(url, {
|
|
274
|
+
method,
|
|
275
|
+
headers,
|
|
276
|
+
...body ? { body: JSON.stringify(body) } : {}
|
|
277
|
+
});
|
|
278
|
+
if (!res.ok) {
|
|
279
|
+
const errorBody = await res.json().catch(() => ({ error: res.statusText }));
|
|
280
|
+
if (res.status === 403 && errorBody.code === "KYD_REQUIRED") {
|
|
281
|
+
throw new KYDRequiredError(errorBody);
|
|
282
|
+
}
|
|
283
|
+
throw new AstraSyncError(
|
|
284
|
+
errorBody.error || `Request failed: ${res.status}`,
|
|
285
|
+
res.status,
|
|
286
|
+
errorBody.code
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
return { status: res.status, body: await res.json() };
|
|
290
|
+
}
|
|
291
|
+
async getAuthToken() {
|
|
292
|
+
if (this.apiKey) {
|
|
293
|
+
return this.apiKey;
|
|
294
|
+
}
|
|
295
|
+
if (this.cachedJwt && this.jwtExpiresAt && Date.now() < this.jwtExpiresAt) {
|
|
296
|
+
return this.cachedJwt;
|
|
297
|
+
}
|
|
298
|
+
const res = await fetch(`${this.baseUrl}/api/auth/login`, {
|
|
299
|
+
method: "POST",
|
|
300
|
+
headers: { "Content-Type": "application/json" },
|
|
301
|
+
body: JSON.stringify({ email: this.email, password: this.password })
|
|
302
|
+
});
|
|
303
|
+
if (!res.ok) {
|
|
304
|
+
const errorBody = await res.json().catch(() => ({}));
|
|
305
|
+
throw new AuthenticationError(
|
|
306
|
+
errorBody.message || errorBody.error || "Login failed"
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
const data = await res.json();
|
|
310
|
+
this.cachedJwt = data.data.token;
|
|
311
|
+
this.jwtExpiresAt = Date.now() + 6 * 24 * 60 * 60 * 1e3;
|
|
312
|
+
return this.cachedJwt;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Sign a request using secp256k1 (ethers.js).
|
|
316
|
+
* Canonical message format: METHOD:ENDPOINT:SORTED_JSON_BODY
|
|
317
|
+
* Must match apps/backend/src/services/signature-verify.service.ts exactly.
|
|
318
|
+
*/
|
|
319
|
+
async signRequest(method, endpoint, body) {
|
|
320
|
+
const { Wallet } = await import("ethers");
|
|
321
|
+
const sorted = this.sortObjectKeys(body);
|
|
322
|
+
const canonical = `${method}:${endpoint}:${JSON.stringify(sorted)}`;
|
|
323
|
+
const wallet = new Wallet(this.privateKey);
|
|
324
|
+
return wallet.signMessage(canonical);
|
|
325
|
+
}
|
|
326
|
+
/** Recursively sort object keys for canonical JSON representation. */
|
|
327
|
+
sortObjectKeys(obj) {
|
|
328
|
+
if (obj === null || typeof obj !== "object") {
|
|
329
|
+
return obj;
|
|
330
|
+
}
|
|
331
|
+
if (Array.isArray(obj)) {
|
|
332
|
+
return obj.map((item) => this.sortObjectKeys(item));
|
|
333
|
+
}
|
|
334
|
+
const sorted = {};
|
|
335
|
+
for (const key of Object.keys(obj).sort()) {
|
|
336
|
+
sorted[key] = this.sortObjectKeys(obj[key]);
|
|
337
|
+
}
|
|
338
|
+
return sorted;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
// src/bin/astrasync.ts
|
|
343
|
+
function usage() {
|
|
344
|
+
console.log(`
|
|
345
|
+
astrasync - AstraSync KYA Platform CLI
|
|
346
|
+
|
|
347
|
+
USAGE:
|
|
348
|
+
astrasync [options] <command> [args]
|
|
349
|
+
|
|
350
|
+
GLOBAL OPTIONS:
|
|
351
|
+
--api-key <key> API key (kya_ prefixed). Also reads ASTRASYNC_API_KEY env.
|
|
352
|
+
--email <email> Email for login auth
|
|
353
|
+
--password <pass> Password for login auth
|
|
354
|
+
--private-key <key> secp256k1 private key for crypto signing
|
|
355
|
+
--base-url <url> API base URL (default: https://astrasync.ai; use https://staging.astrasync.ai for staging)
|
|
356
|
+
--help Show this help message
|
|
357
|
+
|
|
358
|
+
COMMANDS:
|
|
359
|
+
register Register a new AI agent
|
|
360
|
+
verify <id> Look up an agent by ASTRA ID or UUID
|
|
361
|
+
health Check API health
|
|
362
|
+
|
|
363
|
+
REGISTER OPTIONS:
|
|
364
|
+
--name <name> Agent name (required)
|
|
365
|
+
--description <desc> Agent description
|
|
366
|
+
--agent-type <type> Agent type (default: general)
|
|
367
|
+
--api-endpoint <url> Agent API endpoint URL
|
|
368
|
+
--model-name <name> LLM model name (e.g. claude-opus-4.6)
|
|
369
|
+
--model-provider <provider> Model provider (e.g. anthropic, openai)
|
|
370
|
+
--model-type <type> Model type (default: llm)
|
|
371
|
+
--framework-name <name> Framework name (e.g. langchain, crewai)
|
|
372
|
+
--framework-version <ver> Framework version
|
|
373
|
+
|
|
374
|
+
EXAMPLES:
|
|
375
|
+
astrasync --api-key kya_xxx register --name "My Agent"
|
|
376
|
+
astrasync register --name "My Agent" --model-name gpt-4o --model-provider openai
|
|
377
|
+
astrasync verify ASTRA-abc123
|
|
378
|
+
astrasync health
|
|
379
|
+
`);
|
|
380
|
+
}
|
|
381
|
+
function parseArgs(args) {
|
|
382
|
+
const globals = {};
|
|
383
|
+
const commandArgs = {};
|
|
384
|
+
let command = "";
|
|
385
|
+
let inCommand = false;
|
|
386
|
+
for (let i = 0; i < args.length; i++) {
|
|
387
|
+
const arg = args[i];
|
|
388
|
+
if (!inCommand && !arg.startsWith("--")) {
|
|
389
|
+
command = arg;
|
|
390
|
+
inCommand = true;
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (arg === "--help" || arg === "-h") {
|
|
394
|
+
globals["help"] = "true";
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
if (arg.startsWith("--")) {
|
|
398
|
+
const key = arg.slice(2);
|
|
399
|
+
const value = args[++i] || "";
|
|
400
|
+
if (inCommand) {
|
|
401
|
+
commandArgs[key] = value;
|
|
402
|
+
} else {
|
|
403
|
+
globals[key] = value;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return { globals, command, commandArgs };
|
|
408
|
+
}
|
|
409
|
+
async function main() {
|
|
410
|
+
const { globals, command, commandArgs } = parseArgs(process.argv.slice(2));
|
|
411
|
+
if (globals["help"] || !command) {
|
|
412
|
+
usage();
|
|
413
|
+
process.exit(command ? 0 : 1);
|
|
414
|
+
}
|
|
415
|
+
const config = {
|
|
416
|
+
apiKey: globals["api-key"],
|
|
417
|
+
email: globals["email"],
|
|
418
|
+
password: globals["password"],
|
|
419
|
+
privateKey: globals["private-key"],
|
|
420
|
+
baseUrl: globals["base-url"]
|
|
421
|
+
};
|
|
422
|
+
try {
|
|
423
|
+
if (command === "health") {
|
|
424
|
+
const client2 = new AstraSync({
|
|
425
|
+
apiKey: config.apiKey || "health-check",
|
|
426
|
+
baseUrl: config.baseUrl
|
|
427
|
+
});
|
|
428
|
+
const result = await client2.health();
|
|
429
|
+
console.log(JSON.stringify(result, null, 2));
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const client = new AstraSync({
|
|
433
|
+
apiKey: config.apiKey,
|
|
434
|
+
email: config.email,
|
|
435
|
+
password: config.password,
|
|
436
|
+
privateKey: config.privateKey,
|
|
437
|
+
baseUrl: config.baseUrl
|
|
438
|
+
});
|
|
439
|
+
if (command === "register") {
|
|
440
|
+
const name = commandArgs["name"];
|
|
441
|
+
if (!name) {
|
|
442
|
+
console.error("Error: --name is required for register command");
|
|
443
|
+
process.exit(1);
|
|
444
|
+
}
|
|
445
|
+
const options = {
|
|
446
|
+
name,
|
|
447
|
+
...commandArgs["description"] && { description: commandArgs["description"] },
|
|
448
|
+
...commandArgs["agent-type"] && { agentType: commandArgs["agent-type"] },
|
|
449
|
+
...commandArgs["api-endpoint"] && { apiEndpoint: commandArgs["api-endpoint"] }
|
|
450
|
+
};
|
|
451
|
+
if (commandArgs["model-name"] && commandArgs["model-provider"]) {
|
|
452
|
+
options.model = {
|
|
453
|
+
modelName: commandArgs["model-name"],
|
|
454
|
+
modelProvider: commandArgs["model-provider"],
|
|
455
|
+
...commandArgs["model-type"] && {
|
|
456
|
+
modelType: commandArgs["model-type"]
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (commandArgs["framework-name"] && commandArgs["framework-version"]) {
|
|
461
|
+
options.framework = {
|
|
462
|
+
frameworkName: commandArgs["framework-name"],
|
|
463
|
+
frameworkVersion: commandArgs["framework-version"]
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
const result = await client.register(options);
|
|
467
|
+
console.log(JSON.stringify(result, null, 2));
|
|
468
|
+
} else if (command === "verify") {
|
|
469
|
+
const agentId = commandArgs["id"] || process.argv[process.argv.length - 1];
|
|
470
|
+
if (!agentId || agentId.startsWith("--")) {
|
|
471
|
+
console.error("Error: agent ID is required for verify command");
|
|
472
|
+
process.exit(1);
|
|
473
|
+
}
|
|
474
|
+
const result = await client.verify(agentId);
|
|
475
|
+
console.log(JSON.stringify(result, null, 2));
|
|
476
|
+
} else {
|
|
477
|
+
console.error(`Unknown command: ${command}`);
|
|
478
|
+
usage();
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (error instanceof KYDRequiredError) {
|
|
483
|
+
console.error(`
|
|
484
|
+
Error: ${error.message}`);
|
|
485
|
+
if (error.ownerNotified) {
|
|
486
|
+
console.error("A reminder email has been sent to your registered email address.");
|
|
487
|
+
}
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
if (error instanceof AstraSyncError) {
|
|
491
|
+
console.error(`
|
|
492
|
+
Error [${error.code || error.statusCode}]: ${error.message}`);
|
|
493
|
+
process.exit(1);
|
|
494
|
+
}
|
|
495
|
+
throw error;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
main();
|