@absol-labs/agent 0.5.0 → 0.6.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/README.md +28 -5
- package/dist/discovery/registry.d.ts +260 -15
- package/dist/discovery/registry.d.ts.map +1 -1
- package/dist/discovery/registry.js +174 -31
- package/dist/discovery/registry.js.map +1 -1
- package/dist/frameworks/agentkit.js +1 -1
- package/dist/frameworks/agentkit.js.map +1 -1
- package/dist/frameworks/crewai.js +1 -1
- package/dist/frameworks/crewai.js.map +1 -1
- package/dist/gateway/caller-auth-gateway.d.ts +3 -1
- package/dist/gateway/caller-auth-gateway.d.ts.map +1 -1
- package/dist/gateway/caller-auth-gateway.js +9 -7
- package/dist/gateway/caller-auth-gateway.js.map +1 -1
- package/dist/gateway/http-server.d.ts +3 -1
- package/dist/gateway/http-server.d.ts.map +1 -1
- package/dist/gateway/http-server.js +15 -1
- package/dist/gateway/http-server.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mcp/server.js +2 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/sdk/invoke.d.ts +16 -2
- package/dist/sdk/invoke.d.ts.map +1 -1
- package/dist/sdk/invoke.js +117 -1
- package/dist/sdk/invoke.js.map +1 -1
- package/package.json +2 -2
- package/src/discovery/registry.ts +213 -35
- package/src/frameworks/agentkit.ts +1 -1
- package/src/frameworks/crewai.ts +1 -1
- package/src/gateway/caller-auth-gateway.ts +13 -9
- package/src/gateway/http-server.ts +18 -1
- package/src/index.ts +5 -0
- package/src/mcp/server.ts +2 -0
- package/src/sdk/invoke.ts +186 -4
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
deriveServiceRef,
|
|
3
3
|
recoverServiceBindingSigner,
|
|
4
|
+
recoverServiceDescriptorSigner,
|
|
4
5
|
serviceBindingSchema,
|
|
6
|
+
serviceDescriptorSchema,
|
|
7
|
+
type SignedServiceBinding,
|
|
8
|
+
type SignedServiceDescriptor,
|
|
5
9
|
} from "@absol-labs/shared";
|
|
6
10
|
import { isHex } from "viem";
|
|
7
11
|
import { z } from "zod";
|
|
@@ -35,9 +39,14 @@ export interface ServiceListing {
|
|
|
35
39
|
readonly serviceRef: `0x${string}`;
|
|
36
40
|
readonly operator: `0x${string}`;
|
|
37
41
|
readonly publicUrl: string;
|
|
42
|
+
readonly access: "public" | "gated";
|
|
43
|
+
/** Signed invocation origin; gated listings fail closed if this is absent. */
|
|
44
|
+
readonly accessUrl: string;
|
|
45
|
+
/** The schema-validated and cryptographically verified signed record. */
|
|
46
|
+
readonly signed: SignedServiceBinding | SignedServiceDescriptor;
|
|
38
47
|
/** Unsigned informational category from the listings row (may be absent). */
|
|
39
48
|
readonly category: string | null;
|
|
40
|
-
/** Optional
|
|
49
|
+
/** Optional unsigned operator-supplied resolver hints; never used for invocation. */
|
|
41
50
|
readonly targetMetadata?: Record<string, string>;
|
|
42
51
|
}
|
|
43
52
|
|
|
@@ -76,14 +85,16 @@ export const DEFAULT_METRIK_REGISTRY_URL =
|
|
|
76
85
|
const DEFAULT_TIMEOUT_MS = 5_000;
|
|
77
86
|
|
|
78
87
|
/**
|
|
79
|
-
* `serviceRef` of the built-in verified
|
|
88
|
+
* `serviceRef` of the built-in verified live-data listing. Kept as a named constant so
|
|
80
89
|
* callers can recognise (and dedupe against) the seeded row.
|
|
81
90
|
*/
|
|
82
|
-
export const
|
|
83
|
-
"
|
|
91
|
+
export const LIVE_DATA_SEED_SERVICE_REF =
|
|
92
|
+
"0x79e57358e98a35273c1cd9d5f1bf9ae1e0af8705db432eb706459dc4f88b6fe0" as const;
|
|
93
|
+
/** @deprecated Use {@link LIVE_DATA_SEED_SERVICE_REF}. */
|
|
94
|
+
export const DEMO_SEED_SERVICE_REF = LIVE_DATA_SEED_SERVICE_REF;
|
|
84
95
|
|
|
85
96
|
/**
|
|
86
|
-
* A
|
|
97
|
+
* A real, operator-signed live-data listing row embedded so `discoverServices()` returns
|
|
87
98
|
* something hireable with ZERO configuration (no Supabase anon key, no registry URL).
|
|
88
99
|
*
|
|
89
100
|
* This is PUBLIC data — a signed {@link https://github.com/Absol-Labs | Metrik}
|
|
@@ -94,23 +105,115 @@ export const DEMO_SEED_SERVICE_REF =
|
|
|
94
105
|
* with any field here and the seed silently drops itself, identically to a bad
|
|
95
106
|
* registry row. The `category` column is unsigned informational metadata only.
|
|
96
107
|
*/
|
|
97
|
-
export const
|
|
98
|
-
service_ref:
|
|
99
|
-
operator: "
|
|
100
|
-
public_url: "https://
|
|
101
|
-
category: "
|
|
108
|
+
export const LIVE_DATA_SEED_LISTING = {
|
|
109
|
+
service_ref: LIVE_DATA_SEED_SERVICE_REF,
|
|
110
|
+
operator: "0xB3e162711920dFD8933609019Dc35C73cC1a09F0",
|
|
111
|
+
public_url: "https://livedata.137.23.50.249.sslip.io",
|
|
112
|
+
category: "live-data",
|
|
102
113
|
signed_binding: {
|
|
103
|
-
serviceRef:
|
|
104
|
-
|
|
105
|
-
version:
|
|
106
|
-
operator: "
|
|
107
|
-
publicUrl: "https://
|
|
108
|
-
issuedAt:
|
|
114
|
+
serviceRef: LIVE_DATA_SEED_SERVICE_REF,
|
|
115
|
+
descriptor: {
|
|
116
|
+
version: 3,
|
|
117
|
+
operator: "0xB3e162711920dFD8933609019Dc35C73cC1a09F0",
|
|
118
|
+
publicUrl: "https://livedata.137.23.50.249.sslip.io",
|
|
119
|
+
issuedAt: 1_787_772_057,
|
|
120
|
+
interface: {
|
|
121
|
+
baseUrl: "https://livedata.137.23.50.249.sslip.io",
|
|
122
|
+
healthPath: "/health",
|
|
123
|
+
endpoints: [
|
|
124
|
+
{
|
|
125
|
+
name: "health",
|
|
126
|
+
path: "/health",
|
|
127
|
+
method: "GET",
|
|
128
|
+
description: "Liveness probe.",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "readiness",
|
|
132
|
+
path: "/readiness",
|
|
133
|
+
method: "GET",
|
|
134
|
+
description:
|
|
135
|
+
"Non-value dependency readiness backed by the same real BTC cache policy.",
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "price",
|
|
139
|
+
path: "/price",
|
|
140
|
+
method: "GET",
|
|
141
|
+
description:
|
|
142
|
+
"Real live crypto spot price (?symbol=BTC|ETH|SOL) from CoinGecko's public API.",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
authModel: "caller-auth",
|
|
146
|
+
example: {
|
|
147
|
+
endpoint: "price",
|
|
148
|
+
request: { symbol: "BTC" },
|
|
149
|
+
response: { symbol: "BTC", source: "coingecko-public-api" },
|
|
150
|
+
description:
|
|
151
|
+
"Real, nondeterministic live price — no re-execution guarantee.",
|
|
152
|
+
},
|
|
153
|
+
expectedStatuses: [200],
|
|
154
|
+
},
|
|
155
|
+
verification: {
|
|
156
|
+
canaries: [
|
|
157
|
+
{
|
|
158
|
+
path: "/readiness",
|
|
159
|
+
method: "GET",
|
|
160
|
+
matcher: {
|
|
161
|
+
type: "json-field-equals",
|
|
162
|
+
field: "source",
|
|
163
|
+
expected: "coingecko-public-api",
|
|
164
|
+
},
|
|
165
|
+
nonce: {
|
|
166
|
+
inject: { in: "query", name: "nonce" },
|
|
167
|
+
echo: { from: "json-path", path: "nonce" },
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
path: "/canary",
|
|
172
|
+
method: "GET",
|
|
173
|
+
matcher: {
|
|
174
|
+
type: "includes",
|
|
175
|
+
expected: "metrik-canary:live-data:",
|
|
176
|
+
},
|
|
177
|
+
nonce: {
|
|
178
|
+
inject: { in: "query", name: "nonce" },
|
|
179
|
+
echo: { from: "json-path", path: "nonce" },
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
schema: {
|
|
184
|
+
path: "/readiness?nonce=metrik-schema",
|
|
185
|
+
required: [
|
|
186
|
+
{ pointer: "ready", type: "boolean" },
|
|
187
|
+
{ pointer: "source", type: "string", nonEmpty: true },
|
|
188
|
+
{ pointer: "fetchedAt", type: "string", nonEmpty: true },
|
|
189
|
+
{ pointer: "priceAgeMs", type: "number" },
|
|
190
|
+
{ pointer: "freshness", type: "string", nonEmpty: true },
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
sla: { maxLatencyMs: 5000, freshnessSeconds: 60 },
|
|
194
|
+
},
|
|
195
|
+
commercial: {
|
|
196
|
+
minRatePerSecond: "100",
|
|
197
|
+
maxRatePerSecond: "10000",
|
|
198
|
+
currency: "USDC",
|
|
199
|
+
slaDescription:
|
|
200
|
+
"Failed or unproven intervals do not advance verified cumulative entitlement; the stream remains active until buyer close or expiry.",
|
|
201
|
+
refundPolicy:
|
|
202
|
+
"The buyer reclaims unspent escrow using the latest checkpoint after close/expiry, or the unverified escape path after its grace window.",
|
|
203
|
+
},
|
|
204
|
+
access: "gated",
|
|
205
|
+
callerAuth: {
|
|
206
|
+
mechanism: "on-chain",
|
|
207
|
+
role: "buyer",
|
|
208
|
+
accessUrl: "https://gateway.137.23.50.249.sslip.io",
|
|
209
|
+
},
|
|
109
210
|
},
|
|
110
211
|
signature:
|
|
111
|
-
"
|
|
212
|
+
"0x2f4d2784eb40b1542517bc19f12534fb2e97da4ade5a3ff56193fcfb97fbef921309f77af3445e336cb07a4379011b080bba700033a098f672b0b2f096197f161b",
|
|
112
213
|
},
|
|
113
214
|
} as const;
|
|
215
|
+
/** @deprecated Use {@link LIVE_DATA_SEED_LISTING}. */
|
|
216
|
+
export const DEMO_SEED_LISTING = LIVE_DATA_SEED_LISTING;
|
|
114
217
|
|
|
115
218
|
const signature65HexSchema = z
|
|
116
219
|
.string()
|
|
@@ -135,12 +238,25 @@ const signedBindingRecordSchema = z.object({
|
|
|
135
238
|
targetMetadata: z.record(z.string()).optional(),
|
|
136
239
|
});
|
|
137
240
|
|
|
241
|
+
const signedDescriptorRecordSchema = z.object({
|
|
242
|
+
serviceRef: bytes32HexSchema,
|
|
243
|
+
descriptor: serviceDescriptorSchema,
|
|
244
|
+
signature: signature65HexSchema,
|
|
245
|
+
targetMetadata: z.record(z.string()).optional(),
|
|
246
|
+
});
|
|
247
|
+
|
|
138
248
|
/** A Supabase `listings` row. Only `signed_binding` is trusted; `category` is
|
|
139
249
|
* carried through as unsigned informational metadata. Unknown columns are ignored. */
|
|
140
250
|
const listingRowSchema = z
|
|
141
251
|
.object({
|
|
252
|
+
service_ref: bytes32HexSchema.optional(),
|
|
253
|
+
operator: z.string().optional(),
|
|
254
|
+
public_url: z.string().optional(),
|
|
142
255
|
category: z.string().nullish(),
|
|
143
|
-
signed_binding:
|
|
256
|
+
signed_binding: z.union([
|
|
257
|
+
signedBindingRecordSchema,
|
|
258
|
+
signedDescriptorRecordSchema,
|
|
259
|
+
]),
|
|
144
260
|
})
|
|
145
261
|
.passthrough();
|
|
146
262
|
|
|
@@ -167,13 +283,13 @@ export async function discoverServices(
|
|
|
167
283
|
|
|
168
284
|
// `registryUnavailable` = the registry could not be queried at all (no fetch /
|
|
169
285
|
// network error / non-200). It never throws into the hire flow; instead it
|
|
170
|
-
// degrades to the built-in verified
|
|
286
|
+
// degrades to the built-in verified live-data seed (when enabled) so a cold user
|
|
171
287
|
// still discovers a hireable service.
|
|
172
288
|
let rows: unknown[] = [];
|
|
173
289
|
let registryUnavailable = false;
|
|
174
290
|
if (typeof fetchImpl !== "function") {
|
|
175
291
|
logger.warn(
|
|
176
|
-
"metrik discovery: no fetch implementation available; falling back to
|
|
292
|
+
"metrik discovery: no fetch implementation available; falling back to live-data seed only",
|
|
177
293
|
);
|
|
178
294
|
registryUnavailable = true;
|
|
179
295
|
} else {
|
|
@@ -182,9 +298,9 @@ export async function discoverServices(
|
|
|
182
298
|
} catch (error) {
|
|
183
299
|
// Fail-safe / buyer-favouring: a down, unreachable, or 401 registry (the
|
|
184
300
|
// Supabase default rejects credential-free reads) must never throw into the
|
|
185
|
-
// agent's hire flow — it degrades to the
|
|
301
|
+
// agent's hire flow — it degrades to the live-data seed.
|
|
186
302
|
logger.warn(
|
|
187
|
-
`metrik discovery: listings registry unavailable (${errorMessage(error)}); falling back to
|
|
303
|
+
`metrik discovery: listings registry unavailable (${errorMessage(error)}); falling back to live-data seed only`,
|
|
188
304
|
);
|
|
189
305
|
registryUnavailable = true;
|
|
190
306
|
}
|
|
@@ -210,21 +326,21 @@ export async function discoverServices(
|
|
|
210
326
|
}
|
|
211
327
|
}
|
|
212
328
|
|
|
213
|
-
// Append the built-in verified
|
|
329
|
+
// Append the built-in verified live-data listing when the registry did not already
|
|
214
330
|
// surface it. Runs through the SAME fail-closed verifyRow path, so it is
|
|
215
331
|
// verified exactly like a registry row and can never be a trust bypass.
|
|
216
332
|
if (includeDemoSeed && !seedAlreadyPresent) {
|
|
217
|
-
const seed = await verifyRow(DEMO_SEED_LISTING, "
|
|
333
|
+
const seed = await verifyRow(DEMO_SEED_LISTING, "live-data-seed", logger);
|
|
218
334
|
if (seed === null) {
|
|
219
335
|
// Defensive: only reachable if the embedded constant were corrupted.
|
|
220
336
|
logger.warn(
|
|
221
|
-
"metrik discovery: built-in
|
|
337
|
+
"metrik discovery: built-in live-data seed failed fail-closed verification; not seeding",
|
|
222
338
|
);
|
|
223
339
|
} else if (opts.category === undefined || seed.category === opts.category) {
|
|
224
340
|
logger.warn(
|
|
225
341
|
registryUnavailable
|
|
226
|
-
? "metrik discovery: registry unavailable — serving the built-in verified
|
|
227
|
-
: "metrik discovery: registry did not include
|
|
342
|
+
? "metrik discovery: registry unavailable — serving the built-in verified live-data listing"
|
|
343
|
+
: "metrik discovery: registry did not include live data — appending the built-in verified listing",
|
|
228
344
|
);
|
|
229
345
|
listings.push(seed);
|
|
230
346
|
}
|
|
@@ -284,26 +400,59 @@ async function verifyRow(
|
|
|
284
400
|
}
|
|
285
401
|
const { signed_binding: record, category } = parsed.data;
|
|
286
402
|
|
|
403
|
+
const signedIdentity =
|
|
404
|
+
"descriptor" in record
|
|
405
|
+
? {
|
|
406
|
+
operator: record.descriptor.operator,
|
|
407
|
+
publicUrl: record.descriptor.publicUrl,
|
|
408
|
+
}
|
|
409
|
+
: {
|
|
410
|
+
operator: record.binding.operator,
|
|
411
|
+
publicUrl: record.binding.publicUrl,
|
|
412
|
+
};
|
|
413
|
+
if (
|
|
414
|
+
(parsed.data.service_ref !== undefined &&
|
|
415
|
+
!eqHex(parsed.data.service_ref, record.serviceRef)) ||
|
|
416
|
+
(parsed.data.operator !== undefined &&
|
|
417
|
+
!eqHex(parsed.data.operator, signedIdentity.operator)) ||
|
|
418
|
+
(parsed.data.public_url !== undefined &&
|
|
419
|
+
parsed.data.public_url !== signedIdentity.publicUrl)
|
|
420
|
+
) {
|
|
421
|
+
logger.warn(
|
|
422
|
+
`metrik discovery: row ${index} dropped — flat identity columns disagree with signed record`,
|
|
423
|
+
);
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
|
|
287
427
|
let signer: `0x${string}` | null;
|
|
288
428
|
try {
|
|
289
|
-
signer = (
|
|
290
|
-
record
|
|
291
|
-
|
|
292
|
-
|
|
429
|
+
signer = (
|
|
430
|
+
"descriptor" in record
|
|
431
|
+
? await recoverServiceDescriptorSigner(
|
|
432
|
+
record.descriptor,
|
|
433
|
+
record.signature as `0x${string}`,
|
|
434
|
+
)
|
|
435
|
+
: await recoverServiceBindingSigner(
|
|
436
|
+
record.binding,
|
|
437
|
+
record.signature as `0x${string}`,
|
|
438
|
+
)
|
|
439
|
+
) as `0x${string}` | null;
|
|
293
440
|
} catch (error) {
|
|
294
441
|
logger.warn(
|
|
295
442
|
`metrik discovery: row ${index} dropped — signature recovery threw (${errorMessage(error)})`,
|
|
296
443
|
);
|
|
297
444
|
return null;
|
|
298
445
|
}
|
|
299
|
-
if (signer === null || !eqHex(signer,
|
|
446
|
+
if (signer === null || !eqHex(signer, signedIdentity.operator)) {
|
|
300
447
|
logger.warn(
|
|
301
448
|
`metrik discovery: row ${index} dropped — signature does not recover to operator (recovered ${signer ?? "null"})`,
|
|
302
449
|
);
|
|
303
450
|
return null;
|
|
304
451
|
}
|
|
305
452
|
|
|
306
|
-
const derivedRef = deriveServiceRef(
|
|
453
|
+
const derivedRef = deriveServiceRef(
|
|
454
|
+
"descriptor" in record ? record.descriptor : record.binding,
|
|
455
|
+
);
|
|
307
456
|
if (!eqHex(derivedRef, record.serviceRef)) {
|
|
308
457
|
logger.warn(
|
|
309
458
|
`metrik discovery: row ${index} dropped — serviceRef ${record.serviceRef} does not match derived ${derivedRef}`,
|
|
@@ -311,10 +460,31 @@ async function verifyRow(
|
|
|
311
460
|
return null;
|
|
312
461
|
}
|
|
313
462
|
|
|
463
|
+
const access =
|
|
464
|
+
"descriptor" in record ? (record.descriptor.access ?? "public") : "public";
|
|
465
|
+
const accessUrl =
|
|
466
|
+
"descriptor" in record
|
|
467
|
+
? access === "gated"
|
|
468
|
+
? record.descriptor.callerAuth?.accessUrl
|
|
469
|
+
: (record.descriptor.interface?.baseUrl ?? record.descriptor.publicUrl)
|
|
470
|
+
: record.binding.publicUrl;
|
|
471
|
+
if (
|
|
472
|
+
accessUrl === undefined ||
|
|
473
|
+
(access === "gated" && !isHttpsUrl(accessUrl))
|
|
474
|
+
) {
|
|
475
|
+
logger.warn(
|
|
476
|
+
`metrik discovery: row ${index} dropped — gated descriptor has no signed HTTPS callerAuth.accessUrl`,
|
|
477
|
+
);
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
|
|
314
481
|
return {
|
|
315
482
|
serviceRef: derivedRef,
|
|
316
|
-
operator:
|
|
317
|
-
publicUrl:
|
|
483
|
+
operator: signedIdentity.operator as `0x${string}`,
|
|
484
|
+
publicUrl: signedIdentity.publicUrl,
|
|
485
|
+
access,
|
|
486
|
+
accessUrl,
|
|
487
|
+
signed: record as SignedServiceBinding | SignedServiceDescriptor,
|
|
318
488
|
category: category ?? null,
|
|
319
489
|
...(record.targetMetadata === undefined
|
|
320
490
|
? {}
|
|
@@ -322,6 +492,14 @@ async function verifyRow(
|
|
|
322
492
|
};
|
|
323
493
|
}
|
|
324
494
|
|
|
495
|
+
function isHttpsUrl(value: string): boolean {
|
|
496
|
+
try {
|
|
497
|
+
return new URL(value).protocol === "https:";
|
|
498
|
+
} catch {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
325
503
|
function eqHex(a: string, b: string): boolean {
|
|
326
504
|
return a.toLowerCase() === b.toLowerCase();
|
|
327
505
|
}
|
|
@@ -290,7 +290,7 @@ export class MetrikActionProvider extends ActionProvider<WalletProvider> {
|
|
|
290
290
|
}
|
|
291
291
|
const lines = listings.map(
|
|
292
292
|
(listing) =>
|
|
293
|
-
`- ${listing.serviceRef} operator=${listing.operator}
|
|
293
|
+
`- ${listing.serviceRef} operator=${listing.operator} verificationUrl=${listing.publicUrl} access=${listing.access} accessUrl=${listing.accessUrl}${listing.category ? ` category=${listing.category}` : ""}`,
|
|
294
294
|
);
|
|
295
295
|
return (
|
|
296
296
|
`Discovered ${listings.length} verified Metrik service${listings.length === 1 ? "" : "s"} ` +
|
package/src/frameworks/crewai.ts
CHANGED
|
@@ -72,7 +72,7 @@ export function renderCrewAiVerifiedStreamPythonExample(
|
|
|
72
72
|
"",
|
|
73
73
|
"buyer_agent = Agent(",
|
|
74
74
|
' role="Verified Compute Buyer",',
|
|
75
|
-
' goal="Hire a verified service and
|
|
75
|
+
' goal="Hire a verified service and close it if delivery is no longer verified.",',
|
|
76
76
|
' backstory="An operator that only pays for oracle-verified delivery.",',
|
|
77
77
|
" mcps=[metrik_mcp],",
|
|
78
78
|
")",
|
|
@@ -145,6 +145,8 @@ export interface CallerAuthGatewayConfig {
|
|
|
145
145
|
readonly rpcUrl?: string;
|
|
146
146
|
/** The service this gateway fronts. Every request's stream AND capability must match it. */
|
|
147
147
|
readonly serviceRef: Hex;
|
|
148
|
+
/** Additional service refs accepted during signed-descriptor migrations. */
|
|
149
|
+
readonly serviceRefs?: readonly Hex[];
|
|
148
150
|
/** Chain id for EIP-712 domain separation. Default `84532` (Base Sepolia). */
|
|
149
151
|
readonly chainId?: number;
|
|
150
152
|
/** Capability header name. Default {@link CAPABILITY_HEADER_NAME}. */
|
|
@@ -176,18 +178,18 @@ export class CallerAuthGateway {
|
|
|
176
178
|
private readonly nonceCache: NonceReplayCache;
|
|
177
179
|
private readonly headerName: string;
|
|
178
180
|
private readonly domain: InvocationCapabilityDomainInput;
|
|
179
|
-
private readonly
|
|
181
|
+
private readonly serviceRefs: ReadonlySet<string>;
|
|
180
182
|
private readonly now: () => number;
|
|
181
183
|
|
|
182
184
|
constructor(config: CallerAuthGatewayConfig) {
|
|
183
185
|
if (!isAddress(config.escrowAddress)) {
|
|
184
186
|
throw new Error("escrowAddress must be an EVM address");
|
|
185
187
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
188
|
+
const serviceRefs = [config.serviceRef, ...(config.serviceRefs ?? [])];
|
|
189
|
+
for (const serviceRef of serviceRefs) {
|
|
190
|
+
if (!isHex(serviceRef, { strict: true }) || serviceRef.length !== 66) {
|
|
191
|
+
throw new Error("every serviceRef must be a 32-byte hex value");
|
|
192
|
+
}
|
|
191
193
|
}
|
|
192
194
|
if (config.streamReader === undefined && config.rpcUrl === undefined) {
|
|
193
195
|
throw new Error(
|
|
@@ -195,7 +197,7 @@ export class CallerAuthGateway {
|
|
|
195
197
|
);
|
|
196
198
|
}
|
|
197
199
|
|
|
198
|
-
this.
|
|
200
|
+
this.serviceRefs = new Set(serviceRefs.map((value) => value.toLowerCase()));
|
|
199
201
|
this.headerName = (
|
|
200
202
|
config.headerName ?? CAPABILITY_HEADER_NAME
|
|
201
203
|
).toLowerCase();
|
|
@@ -260,7 +262,7 @@ export class CallerAuthGateway {
|
|
|
260
262
|
return deny("path-mismatch", 403, capability);
|
|
261
263
|
}
|
|
262
264
|
|
|
263
|
-
if (
|
|
265
|
+
if (!this.serviceRefs.has(capability.serviceRef.toLowerCase())) {
|
|
264
266
|
return deny("service-ref-mismatch", 403, capability);
|
|
265
267
|
}
|
|
266
268
|
|
|
@@ -280,7 +282,9 @@ export class CallerAuthGateway {
|
|
|
280
282
|
if (stream.buyer.toLowerCase() !== capability.buyer.toLowerCase()) {
|
|
281
283
|
return deny("buyer-mismatch", 403, capability);
|
|
282
284
|
}
|
|
283
|
-
if (
|
|
285
|
+
if (
|
|
286
|
+
stream.serviceRef.toLowerCase() !== capability.serviceRef.toLowerCase()
|
|
287
|
+
) {
|
|
284
288
|
return deny("service-ref-mismatch", 403, capability);
|
|
285
289
|
}
|
|
286
290
|
if (stream.status !== "active") {
|
|
@@ -232,6 +232,7 @@ export interface CallerAuthGatewayEnvConfig {
|
|
|
232
232
|
readonly escrowAddress: Address;
|
|
233
233
|
readonly rpcUrl: string;
|
|
234
234
|
readonly serviceRef: Hex;
|
|
235
|
+
readonly serviceRefs: readonly Hex[];
|
|
235
236
|
readonly chainId: number;
|
|
236
237
|
readonly upstreamUrl: string;
|
|
237
238
|
readonly headerName?: string;
|
|
@@ -242,7 +243,8 @@ export interface CallerAuthGatewayEnvConfig {
|
|
|
242
243
|
* - `METRIK_GATEWAY_ESCROW_ADDRESS` (required) - the `StreamEscrowV2` this
|
|
243
244
|
* gateway checks streams against.
|
|
244
245
|
* - `METRIK_GATEWAY_RPC_URL` (required) - RPC used for the read-only stream check.
|
|
245
|
-
* - `METRIK_GATEWAY_SERVICE_REF` (required) - bytes32
|
|
246
|
+
* - `METRIK_GATEWAY_SERVICE_REF` (required) - primary bytes32 service ref.
|
|
247
|
+
* - `METRIK_GATEWAY_SERVICE_REFS` (optional) - comma-separated migration aliases.
|
|
246
248
|
* - `METRIK_GATEWAY_UPSTREAM_URL` (required) - the real service to proxy to on pass.
|
|
247
249
|
* - `METRIK_GATEWAY_CHAIN_ID` (optional) - default `84532` (Base Sepolia).
|
|
248
250
|
* - `METRIK_GATEWAY_HEADER_NAME` (optional) - default `x-metrik-capability`.
|
|
@@ -263,6 +265,17 @@ export function parseCallerAuthGatewayEnvConfig(
|
|
|
263
265
|
if (!isHex(serviceRef, { strict: true }) || serviceRef.length !== 66) {
|
|
264
266
|
throw new Error("METRIK_GATEWAY_SERVICE_REF must be a 32-byte hex value");
|
|
265
267
|
}
|
|
268
|
+
const serviceRefs = (env.METRIK_GATEWAY_SERVICE_REFS ?? "")
|
|
269
|
+
.split(",")
|
|
270
|
+
.map((value) => value.trim())
|
|
271
|
+
.filter((value) => value.length > 0);
|
|
272
|
+
for (const candidate of serviceRefs) {
|
|
273
|
+
if (!isHex(candidate, { strict: true }) || candidate.length !== 66) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
"METRIK_GATEWAY_SERVICE_REFS must contain only 32-byte hex values",
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
266
279
|
const upstreamUrl = requireEnv(env, "METRIK_GATEWAY_UPSTREAM_URL");
|
|
267
280
|
// eslint-disable-next-line no-new -- validates the URL is well-formed.
|
|
268
281
|
new URL(upstreamUrl);
|
|
@@ -288,6 +301,9 @@ export function parseCallerAuthGatewayEnvConfig(
|
|
|
288
301
|
escrowAddress,
|
|
289
302
|
rpcUrl,
|
|
290
303
|
serviceRef,
|
|
304
|
+
serviceRefs: [
|
|
305
|
+
...new Set(serviceRefs.map((value) => value.toLowerCase())),
|
|
306
|
+
] as Hex[],
|
|
291
307
|
chainId,
|
|
292
308
|
upstreamUrl,
|
|
293
309
|
...(headerName === undefined ? {} : { headerName }),
|
|
@@ -311,6 +327,7 @@ export async function startCallerAuthGatewayServerFromEnv(
|
|
|
311
327
|
escrowAddress: config.escrowAddress,
|
|
312
328
|
rpcUrl: config.rpcUrl,
|
|
313
329
|
serviceRef: config.serviceRef,
|
|
330
|
+
serviceRefs: config.serviceRefs,
|
|
314
331
|
chainId: config.chainId,
|
|
315
332
|
...(config.headerName === undefined
|
|
316
333
|
? {}
|
package/src/index.ts
CHANGED
|
@@ -49,6 +49,8 @@ export {
|
|
|
49
49
|
DEFAULT_METRIK_REGISTRY_URL,
|
|
50
50
|
DEMO_SEED_LISTING,
|
|
51
51
|
DEMO_SEED_SERVICE_REF,
|
|
52
|
+
LIVE_DATA_SEED_LISTING,
|
|
53
|
+
LIVE_DATA_SEED_SERVICE_REF,
|
|
52
54
|
discoverServices,
|
|
53
55
|
type DiscoverServicesOptions,
|
|
54
56
|
type ServiceListing,
|
|
@@ -219,6 +221,9 @@ export {
|
|
|
219
221
|
|
|
220
222
|
export {
|
|
221
223
|
InvokeBuyerMismatchError,
|
|
224
|
+
InvokeAccessUrlError,
|
|
225
|
+
InvokePathError,
|
|
226
|
+
InvokeServiceRefMismatchError,
|
|
222
227
|
InvokeStreamExpiredError,
|
|
223
228
|
InvokeStreamNotActiveError,
|
|
224
229
|
capabilityFor,
|
package/src/mcp/server.ts
CHANGED
|
@@ -923,6 +923,8 @@ function serializeServiceListing(
|
|
|
923
923
|
serviceRef: listing.serviceRef,
|
|
924
924
|
operator: listing.operator,
|
|
925
925
|
publicUrl: listing.publicUrl,
|
|
926
|
+
access: listing.access,
|
|
927
|
+
accessUrl: listing.accessUrl,
|
|
926
928
|
category: listing.category,
|
|
927
929
|
...(listing.targetMetadata === undefined
|
|
928
930
|
? {}
|