stellarium-ts 0.15.0 → 0.17.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/client/operatorAuth.d.ts +19 -0
- package/dist/client/operatorAuth.d.ts.map +1 -0
- package/dist/client/operatorAuth.js +21 -0
- package/dist/client/operatorClient.d.ts +75 -16
- package/dist/client/operatorClient.d.ts.map +1 -1
- package/dist/client/operatorClient.js +82 -23
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +11 -3
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/resource/inference.d.ts +38 -0
- package/dist/resource/inference.d.ts.map +1 -0
- package/dist/resource/inference.js +74 -0
- package/dist/resource/invoker.d.ts +65 -8
- package/dist/resource/invoker.d.ts.map +1 -1
- package/dist/resource/invoker.js +120 -21
- package/dist/resource/repository.d.ts +29 -5
- package/dist/resource/repository.d.ts.map +1 -1
- package/dist/resource/repository.js +38 -7
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +206 -52
- package/dist/server.d.ts +13 -3
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +26 -4
- package/dist/service.d.ts +8 -1
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +3 -3
- package/dist/tests/client/operatorClient.test.js +94 -36
- package/dist/tests/db/db.test.js +18 -1
- package/dist/tests/helpers.d.ts +11 -1
- package/dist/tests/helpers.d.ts.map +1 -1
- package/dist/tests/helpers.js +38 -3
- package/dist/tests/integration/network.test.js +78 -7
- package/dist/tests/resource/invoker.test.js +95 -4
- package/dist/tests/resource/repository.test.js +20 -6
- package/dist/tests/routes.test.js +194 -23
- package/dist/tests/server.test.js +35 -8
- package/package.json +1 -1
package/dist/routes.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { createHash, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import rateLimit from '@fastify/rate-limit';
|
|
3
3
|
import { interpretParsed, parseScript } from 'requestscript';
|
|
4
|
-
import { invocationHash, paymentVoucherPayload } from './resource/invoker.js';
|
|
4
|
+
import { inferenceVoucherPayload, invocationHash, isInferenceVoucher, paymentVoucherPayload } from './resource/invoker.js';
|
|
5
|
+
import { inferenceCharge, isInferenceFunction, isInferenceManifest, parseMaxTokens } from './resource/inference.js';
|
|
5
6
|
import { safeVerifyData } from './crypto/utils.js';
|
|
6
7
|
import { InvalidPublicKeyError } from './grant/service.js';
|
|
7
8
|
// Body schemas check only the shape the handlers rely on. Signed payloads (resources) are
|
|
@@ -48,35 +49,63 @@ const runRequestSchema = {
|
|
|
48
49
|
required: ['source'],
|
|
49
50
|
properties: { source: { type: 'string' } },
|
|
50
51
|
};
|
|
52
|
+
const resourceCallProperties = {
|
|
53
|
+
resourcePath: { type: 'string' },
|
|
54
|
+
resourceName: { type: 'string' },
|
|
55
|
+
functionName: { type: 'string' },
|
|
56
|
+
parameters: {
|
|
57
|
+
type: 'array',
|
|
58
|
+
items: { type: 'object', required: ['name'], properties: { name: { type: 'string' } } },
|
|
59
|
+
},
|
|
60
|
+
// Shape only: the grants inside are signed, so coercing them would change the signed bytes.
|
|
61
|
+
authorization: { type: 'object' },
|
|
62
|
+
};
|
|
63
|
+
const fixedVoucherSchema = {
|
|
64
|
+
type: 'object',
|
|
65
|
+
required: ['requestId', 'invocationId', 'amount', 'recipientOrganizationId', 'signature', 'expiresAt', 'invocationHash', 'publicKey'],
|
|
66
|
+
properties: {
|
|
67
|
+
requestId: { type: 'string' },
|
|
68
|
+
invocationId: { type: 'string' },
|
|
69
|
+
amount: { type: 'integer' },
|
|
70
|
+
recipientOrganizationId: { type: 'string' },
|
|
71
|
+
signature: { type: 'string' },
|
|
72
|
+
expiresAt: { type: 'string' },
|
|
73
|
+
invocationHash: { type: 'string' },
|
|
74
|
+
publicKey: { type: 'string' },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const inferenceVoucherSchema = {
|
|
78
|
+
type: 'object',
|
|
79
|
+
required: ['kind', 'requestId', 'invocationId', 'maxAmount', 'inputCostMicroUsdPerMillionTokens', 'outputCostMicroUsdPerMillionTokens', 'maxTokens', 'recipientOrganizationId', 'signature', 'expiresAt', 'invocationHash', 'publicKey'],
|
|
80
|
+
properties: {
|
|
81
|
+
kind: { const: 'inference' },
|
|
82
|
+
requestId: { type: 'string' },
|
|
83
|
+
invocationId: { type: 'string' },
|
|
84
|
+
maxAmount: { type: 'integer', minimum: 0 },
|
|
85
|
+
inputCostMicroUsdPerMillionTokens: { type: 'integer', minimum: 0 },
|
|
86
|
+
outputCostMicroUsdPerMillionTokens: { type: 'integer', minimum: 0 },
|
|
87
|
+
maxTokens: { type: 'integer', minimum: 1 },
|
|
88
|
+
recipientOrganizationId: { type: 'string' },
|
|
89
|
+
signature: { type: 'string' },
|
|
90
|
+
expiresAt: { type: 'string' },
|
|
91
|
+
invocationHash: { type: 'string' },
|
|
92
|
+
publicKey: { type: 'string' },
|
|
93
|
+
},
|
|
94
|
+
};
|
|
51
95
|
const resourceRunRequestSchema = {
|
|
52
96
|
type: 'object',
|
|
53
97
|
required: ['resourcePath', 'resourceName', 'functionName', 'parameters'],
|
|
54
98
|
properties: {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
parameters: {
|
|
59
|
-
type: 'array',
|
|
60
|
-
items: { type: 'object', required: ['name'], properties: { name: { type: 'string' } } },
|
|
61
|
-
},
|
|
62
|
-
paymentVoucher: {
|
|
63
|
-
type: 'object',
|
|
64
|
-
required: ['requestId', 'invocationId', 'amount', 'recipientAddress', 'signature', 'expiresAt', 'invocationHash', 'publicKey'],
|
|
65
|
-
properties: {
|
|
66
|
-
requestId: { type: 'string' },
|
|
67
|
-
invocationId: { type: 'string' },
|
|
68
|
-
amount: { type: 'integer' },
|
|
69
|
-
recipientAddress: { type: 'string' },
|
|
70
|
-
signature: { type: 'string' },
|
|
71
|
-
expiresAt: { type: 'string' },
|
|
72
|
-
invocationHash: { type: 'string' },
|
|
73
|
-
publicKey: { type: 'string' },
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
// Shape only: the grants inside are signed, so coercing them would change the signed bytes.
|
|
77
|
-
authorization: { type: 'object' },
|
|
99
|
+
...resourceCallProperties,
|
|
100
|
+
// An inference voucher has `kind` and no `amount`, so at most one shape matches.
|
|
101
|
+
paymentVoucher: { oneOf: [fixedVoucherSchema, inferenceVoucherSchema] },
|
|
78
102
|
},
|
|
79
103
|
};
|
|
104
|
+
const resourceEstimateRequestSchema = {
|
|
105
|
+
type: 'object',
|
|
106
|
+
required: ['resourcePath', 'resourceName', 'functionName', 'parameters'],
|
|
107
|
+
properties: resourceCallProperties,
|
|
108
|
+
};
|
|
80
109
|
const grantRenewalRequestSchema = {
|
|
81
110
|
type: 'object',
|
|
82
111
|
required: ['grantId', 'publicKey', 'nonce', 'expiresAt', 'signature'],
|
|
@@ -102,25 +131,63 @@ function secretsEqual(a, b) {
|
|
|
102
131
|
}
|
|
103
132
|
/**
|
|
104
133
|
* A voucher is valid when it is bound to this exact call, signed by its public key, unexpired,
|
|
105
|
-
* and pays at least the function's cost
|
|
134
|
+
* and pays this node's organization. A fixed voucher must pay at least the function's cost. An inference
|
|
135
|
+
* voucher must be for an inference function, carry its rates, and sign the call's `maxTokens`.
|
|
136
|
+
* Whether its `maxAmount` covers the call is checked separately, against the function's estimator.
|
|
106
137
|
*/
|
|
107
|
-
function isValidPaymentVoucher(voucher, manifest, request) {
|
|
138
|
+
function isValidPaymentVoucher(voucher, resourceFunction, manifest, request, organizationId) {
|
|
108
139
|
const expectedHash = invocationHash(request.resourcePath, request.resourceName, request.functionName, request.parameters);
|
|
109
140
|
if (voucher.invocationHash !== expectedHash) {
|
|
110
141
|
return false;
|
|
111
142
|
}
|
|
112
|
-
if (voucher.
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
if (voucher.recipientAddress !== manifest.recipientAddress) {
|
|
143
|
+
if (!organizationId || voucher.recipientOrganizationId !== organizationId) {
|
|
116
144
|
return false;
|
|
117
145
|
}
|
|
118
146
|
const expiresAt = Date.parse(voucher.expiresAt);
|
|
119
147
|
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now()) {
|
|
120
148
|
return false;
|
|
121
149
|
}
|
|
150
|
+
if (isInferenceVoucher(voucher)) {
|
|
151
|
+
if (!isInferenceFunction(resourceFunction) || !isInferenceManifest(manifest)) {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
if (voucher.inputCostMicroUsdPerMillionTokens !== manifest.inputCostMicroUsdPerMillionTokens
|
|
155
|
+
|| voucher.outputCostMicroUsdPerMillionTokens !== manifest.outputCostMicroUsdPerMillionTokens) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
if (voucher.maxTokens !== parseMaxTokens(request.parameters)) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
return safeVerifyData(inferenceVoucherPayload(voucher), voucher.publicKey, voucher.signature);
|
|
162
|
+
}
|
|
163
|
+
if (isInferenceFunction(resourceFunction) || isInferenceManifest(manifest)) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
if (voucher.amount < manifest.costMicroUsd) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
122
169
|
return safeVerifyData(paymentVoucherPayload(voucher), voucher.publicKey, voucher.signature);
|
|
123
170
|
}
|
|
171
|
+
/** The function's own input-token estimate, or undefined if its estimator failed or returned something other than a count. */
|
|
172
|
+
async function estimateInputTokens(resourceFunction, request) {
|
|
173
|
+
try {
|
|
174
|
+
const inputTokens = await resourceFunction.estimateInputTokens?.(request.parameters);
|
|
175
|
+
return Number.isSafeInteger(inputTokens) && inputTokens >= 0 ? inputTokens : undefined;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** What the operator will capture for `usage`: its charge at the voucher's rates, capped at `maxAmount`. */
|
|
182
|
+
function cappedInferenceCharge(voucher, usage) {
|
|
183
|
+
try {
|
|
184
|
+
return Math.min(inferenceCharge(voucher, usage.inputTokens, usage.outputTokens), voucher.maxAmount);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
// The charge isn't a safe integer, so it is over any maxAmount.
|
|
188
|
+
return voucher.maxAmount;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
124
191
|
/**
|
|
125
192
|
* Encapsulates the routes
|
|
126
193
|
* @param fastify Encapsulated Fastify Instance
|
|
@@ -187,11 +254,16 @@ const routes = async (fastify, options) => {
|
|
|
187
254
|
type: parameter.type,
|
|
188
255
|
description: parameter.description,
|
|
189
256
|
})),
|
|
190
|
-
paymentManifest: fn.paymentManifest ? ({
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
257
|
+
paymentManifest: !fn.paymentManifest ? undefined : isInferenceManifest(fn.paymentManifest) ? {
|
|
258
|
+
inputCostMicroUsdPerMillionTokens: fn.paymentManifest.inputCostMicroUsdPerMillionTokens,
|
|
259
|
+
outputCostMicroUsdPerMillionTokens: fn.paymentManifest.outputCostMicroUsdPerMillionTokens,
|
|
260
|
+
recipientOrganizationId: fn.paymentManifest.recipientOrganizationId,
|
|
261
|
+
} : {
|
|
262
|
+
costMicroUsd: fn.paymentManifest.costMicroUsd,
|
|
263
|
+
recipientOrganizationId: fn.paymentManifest.recipientOrganizationId,
|
|
264
|
+
},
|
|
194
265
|
restricted: fn.restricted || undefined,
|
|
266
|
+
kind: isInferenceFunction(fn) ? 'inference' : undefined,
|
|
195
267
|
})),
|
|
196
268
|
baseUrl: resource.baseUrl,
|
|
197
269
|
})),
|
|
@@ -240,19 +312,20 @@ const routes = async (fastify, options) => {
|
|
|
240
312
|
reply.status(500).send({ error: 'Internal server error' });
|
|
241
313
|
}
|
|
242
314
|
});
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const paymentVoucher = resourceRequest.paymentVoucher;
|
|
315
|
+
/**
|
|
316
|
+
* Finds the local function a call names and checks its parameters and, for a restricted function,
|
|
317
|
+
* its authorization. Sends the refusal and returns undefined if a check fails.
|
|
318
|
+
*/
|
|
319
|
+
async function checkCall(resourceRequest, reply) {
|
|
249
320
|
const resource = await options.service.resourceRepository.getInternalResource(resourceRequest.resourcePath, resourceRequest.resourceName);
|
|
250
321
|
if (!resource) {
|
|
251
|
-
|
|
322
|
+
reply.status(400).send({ error: 'Resource not found' });
|
|
323
|
+
return undefined;
|
|
252
324
|
}
|
|
253
325
|
const resourceFunction = resource.functions.find(fn => fn.name === resourceRequest.functionName);
|
|
254
326
|
if (!resourceFunction) {
|
|
255
|
-
|
|
327
|
+
reply.status(400).send({ error: 'Function not found' });
|
|
328
|
+
return undefined;
|
|
256
329
|
}
|
|
257
330
|
// Grant constraints and the function both read parameters by name, so each name must be
|
|
258
331
|
// declared and appear once. Otherwise a duplicate or extra parameter could pass a
|
|
@@ -260,7 +333,8 @@ const routes = async (fastify, options) => {
|
|
|
260
333
|
const declared = new Set(resourceFunction.parameters.map(parameter => parameter.name));
|
|
261
334
|
const names = resourceRequest.parameters.map(parameter => parameter.name);
|
|
262
335
|
if (new Set(names).size !== names.length || !names.every(name => declared.has(name))) {
|
|
263
|
-
|
|
336
|
+
reply.status(400).send({ error: 'Parameters must be declared by the function and appear once each' });
|
|
337
|
+
return undefined;
|
|
264
338
|
}
|
|
265
339
|
// A restricted function is never run without a grant from this node. Checked before the
|
|
266
340
|
// voucher, so an unauthorized caller never gets near settlement.
|
|
@@ -268,36 +342,116 @@ const routes = async (fastify, options) => {
|
|
|
268
342
|
// Tells a caller without a usable grant where it can ask for one.
|
|
269
343
|
const grantEndpoint = options.service.grantService.acceptsRequests ? `${options.service.config.baseUrl}/v1/grants/request` : undefined;
|
|
270
344
|
if (!resourceRequest.authorization) {
|
|
271
|
-
|
|
345
|
+
reply.status(403).send({ error: 'Authorization is required', grantEndpoint });
|
|
346
|
+
return undefined;
|
|
272
347
|
}
|
|
273
348
|
if (!await options.service.grantService.verifyInvocation(resourceRequest.authorization, resourceRequest)) {
|
|
274
|
-
|
|
349
|
+
reply.status(403).send({ error: 'Invalid authorization', grantEndpoint });
|
|
350
|
+
return undefined;
|
|
275
351
|
}
|
|
276
352
|
}
|
|
353
|
+
return { resource, resourceFunction };
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Prices a call to a paid inference function before the caller signs its voucher: the function's
|
|
357
|
+
* estimate of the input tokens, and `maxAmount`, the charge for those plus `maxTokens` output tokens.
|
|
358
|
+
* Runs the provider's estimator, so a restricted function needs authorization here too.
|
|
359
|
+
*/
|
|
360
|
+
fastify.post('/run/resource/estimate', {
|
|
361
|
+
schema: { body: resourceEstimateRequestSchema },
|
|
362
|
+
config: { rateLimit: options.rateLimit ?? { max: 100, timeWindow: '1 minute' } },
|
|
363
|
+
}, async (request, reply) => {
|
|
364
|
+
const resourceRequest = request.body;
|
|
365
|
+
const call = await checkCall(resourceRequest, reply);
|
|
366
|
+
if (!call) {
|
|
367
|
+
return reply;
|
|
368
|
+
}
|
|
369
|
+
const { resourceFunction } = call;
|
|
370
|
+
const manifest = resourceFunction.paymentManifest;
|
|
371
|
+
if (!isInferenceFunction(resourceFunction) || !isInferenceManifest(manifest)) {
|
|
372
|
+
return reply.status(400).send({ error: 'Function is not a paid inference function' });
|
|
373
|
+
}
|
|
374
|
+
const maxTokens = parseMaxTokens(resourceRequest.parameters);
|
|
375
|
+
if (maxTokens === undefined) {
|
|
376
|
+
return reply.status(400).send({ error: 'maxTokens must be a positive int32' });
|
|
377
|
+
}
|
|
378
|
+
const inputTokens = await estimateInputTokens(resourceFunction, resourceRequest);
|
|
379
|
+
if (inputTokens === undefined) {
|
|
380
|
+
return reply.status(500).send({ error: 'Failed to estimate input tokens' });
|
|
381
|
+
}
|
|
382
|
+
return { inputTokens, maxAmount: inferenceCharge(manifest, inputTokens, maxTokens) };
|
|
383
|
+
});
|
|
384
|
+
fastify.post('/run/resource', {
|
|
385
|
+
schema: { body: resourceRunRequestSchema },
|
|
386
|
+
config: { rateLimit: options.rateLimit ?? { max: 100, timeWindow: '1 minute' } },
|
|
387
|
+
}, async (request, reply) => {
|
|
388
|
+
const resourceRequest = request.body;
|
|
389
|
+
const paymentVoucher = resourceRequest.paymentVoucher;
|
|
390
|
+
const call = await checkCall(resourceRequest, reply);
|
|
391
|
+
if (!call) {
|
|
392
|
+
return reply;
|
|
393
|
+
}
|
|
394
|
+
const { resource, resourceFunction } = call;
|
|
277
395
|
// Payment manifests are per function. A paid function is never run without a valid voucher.
|
|
278
396
|
const paymentManifest = resourceFunction.paymentManifest;
|
|
279
397
|
if (paymentManifest) {
|
|
280
398
|
if (!paymentVoucher) {
|
|
281
399
|
return reply.status(402).send({ error: 'Payment voucher is required' });
|
|
282
400
|
}
|
|
283
|
-
if (!isValidPaymentVoucher(paymentVoucher, paymentManifest, resourceRequest)) {
|
|
401
|
+
if (!isValidPaymentVoucher(paymentVoucher, resourceFunction, paymentManifest, resourceRequest, options.service.config.organizationId)) {
|
|
284
402
|
return reply.status(400).send({ error: 'Invalid payment voucher' });
|
|
285
403
|
}
|
|
286
404
|
if (await options.service.grantService.isBlocked(paymentVoucher.publicKey)) {
|
|
287
405
|
return reply.status(403).send({ error: 'Caller is blocked' });
|
|
288
406
|
}
|
|
407
|
+
// Re-estimated here, so inference never runs on a voucher that can't cover the expected cost.
|
|
408
|
+
if (isInferenceVoucher(paymentVoucher)) {
|
|
409
|
+
const inputTokens = await estimateInputTokens(resourceFunction, resourceRequest);
|
|
410
|
+
if (inputTokens === undefined) {
|
|
411
|
+
return reply.status(500).send({ error: 'Failed to estimate input tokens' });
|
|
412
|
+
}
|
|
413
|
+
const requiredMaxAmount = inferenceCharge(paymentVoucher, inputTokens, paymentVoucher.maxTokens);
|
|
414
|
+
if (paymentVoucher.maxAmount < requiredMaxAmount) {
|
|
415
|
+
return reply.status(402).send({ error: `Payment voucher maxAmount ${paymentVoucher.maxAmount} is below the estimated ${requiredMaxAmount} micro-USD`, requiredMaxAmount });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
// Hold the call's price on the payer's balance before running it, so a paid function never runs unpaid.
|
|
420
|
+
const paid = paymentManifest && paymentVoucher ? paymentVoucher : undefined;
|
|
421
|
+
if (paid) {
|
|
422
|
+
const authorization = await options.service.operatorClient.authorize(paid);
|
|
423
|
+
if (!authorization.ok) {
|
|
424
|
+
return reply.status(authorization.status).send({ error: authorization.error });
|
|
425
|
+
}
|
|
289
426
|
}
|
|
290
427
|
const result = await options.service.resourceInvoker.invoke(resource, resourceRequest.functionName, resourceRequest.parameters);
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
428
|
+
if (paid) {
|
|
429
|
+
let capture;
|
|
430
|
+
if (!result.error && !isInferenceVoucher(paid)) {
|
|
431
|
+
capture = { voucher: paid };
|
|
432
|
+
}
|
|
433
|
+
else if (!result.error && result.usage && isInferenceVoucher(paid)) {
|
|
434
|
+
// The function may report more output than it was allowed; the caller only signed for maxTokens.
|
|
435
|
+
const usage = { inputTokens: result.usage.inputTokens, outputTokens: Math.min(result.usage.outputTokens, paid.maxTokens) };
|
|
436
|
+
result.usage = usage;
|
|
437
|
+
// A free call has nothing to charge, so its hold is released instead.
|
|
438
|
+
if (cappedInferenceCharge(paid, usage) > 0) {
|
|
439
|
+
capture = { voucher: paid, usage };
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
// A failed or free call releases its hold. Nothing awaits this, so it doesn't delay the response,
|
|
443
|
+
// and an uncaught error here would be an unhandled rejection.
|
|
294
444
|
setTimeout(async () => {
|
|
295
|
-
// Nothing awaits this callback, so an uncaught error here would be an unhandled rejection.
|
|
296
445
|
try {
|
|
297
|
-
|
|
446
|
+
if (capture) {
|
|
447
|
+
await options.service.operatorClient.capture(capture);
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
await options.service.operatorClient.release(paid);
|
|
451
|
+
}
|
|
298
452
|
}
|
|
299
453
|
catch (error) {
|
|
300
|
-
fastify.log.error(error, 'Failed to
|
|
454
|
+
fastify.log.error(error, capture ? 'Failed to capture payment' : 'Failed to release payment');
|
|
301
455
|
}
|
|
302
456
|
}, 0);
|
|
303
457
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { ApproveGrant, GrantOptions } from './grant/service.js';
|
|
|
6
6
|
import { StellariumResource } from './resource/repository.js';
|
|
7
7
|
import { RateLimitConfig } from './routes.js';
|
|
8
8
|
import { type NodeConfig } from './service.js';
|
|
9
|
-
export declare const
|
|
9
|
+
export declare const DEFAULT_COST_LIMIT_MICRO_USD = 10000;
|
|
10
10
|
export declare const DEFAULT_GRANT_REFRESH_INTERVAL_MS: number;
|
|
11
11
|
export interface StellariumNodeOptions {
|
|
12
12
|
port?: number;
|
|
@@ -24,8 +24,18 @@ export interface StellariumNodeOptions {
|
|
|
24
24
|
apiKey?: string;
|
|
25
25
|
/** Defaults to `OPERATOR_URL`. */
|
|
26
26
|
operatorUrl?: string;
|
|
27
|
-
/**
|
|
28
|
-
|
|
27
|
+
/**
|
|
28
|
+
* One-time token from the dashboard that enrolls this node in an organization. Defaults to
|
|
29
|
+
* `ENROLLMENT_TOKEN`. Only needed on a node's first start; an enrolled node ignores it.
|
|
30
|
+
*/
|
|
31
|
+
enrollmentToken?: string;
|
|
32
|
+
/** Most this node pays for one remote call. Defaults to `COST_LIMIT_MICRO_USD`, then 10,000 ($0.01). */
|
|
33
|
+
costLimitMicroUsd?: number;
|
|
34
|
+
/**
|
|
35
|
+
* Most this node signs for in one remote inference call (the voucher's `maxAmount`).
|
|
36
|
+
* Defaults to `INFERENCE_COST_LIMIT_MICRO_USD`, then to the resolved `costLimitMicroUsd`.
|
|
37
|
+
*/
|
|
38
|
+
inferenceCostLimitMicroUsd?: number;
|
|
29
39
|
/** SQLite file. Defaults to `./requestscript.db`; use ':memory:' for tests. */
|
|
30
40
|
databasePath?: string;
|
|
31
41
|
/**
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAE1C,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
|
package/dist/server.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import 'dotenv/config';
|
|
2
2
|
import { DEFAULT_DATABASE_PATH, openDatabase } from './db/db.js';
|
|
3
|
+
import { validateInferenceFunction } from './resource/inference.js';
|
|
3
4
|
import routes from './routes.js';
|
|
4
5
|
import { createService } from './service.js';
|
|
5
6
|
import { StartupService } from './startup.js';
|
|
6
|
-
|
|
7
|
+
import { OperatorClient } from './client/operatorClient.js';
|
|
8
|
+
export const DEFAULT_COST_LIMIT_MICRO_USD = 10_000;
|
|
7
9
|
export const DEFAULT_GRANT_REFRESH_INTERVAL_MS = 1000 * 60 * 5;
|
|
8
10
|
function required(name, value) {
|
|
9
11
|
if (!value) {
|
|
@@ -15,9 +17,16 @@ function hostsPaidFunctions(resources) {
|
|
|
15
17
|
return resources.some(resource => resource.functions.some(fn => fn.paymentManifest));
|
|
16
18
|
}
|
|
17
19
|
export function resolveConfig(options, resources = []) {
|
|
18
|
-
const costLimit = options.
|
|
20
|
+
const costLimit = options.costLimitMicroUsd ?? (process.env.COST_LIMIT_MICRO_USD ? Number(process.env.COST_LIMIT_MICRO_USD) : DEFAULT_COST_LIMIT_MICRO_USD);
|
|
19
21
|
if (!Number.isSafeInteger(costLimit) || costLimit < 0) {
|
|
20
|
-
throw new Error('
|
|
22
|
+
throw new Error('costLimitMicroUsd must be a nonnegative integer');
|
|
23
|
+
}
|
|
24
|
+
const inferenceCostLimit = options.inferenceCostLimitMicroUsd ?? (process.env.INFERENCE_COST_LIMIT_MICRO_USD ? Number(process.env.INFERENCE_COST_LIMIT_MICRO_USD) : costLimit);
|
|
25
|
+
if (!Number.isSafeInteger(inferenceCostLimit) || inferenceCostLimit < 0) {
|
|
26
|
+
throw new Error('inferenceCostLimitMicroUsd must be a nonnegative integer');
|
|
27
|
+
}
|
|
28
|
+
for (const resource of resources) {
|
|
29
|
+
resource.functions.forEach(validateInferenceFunction);
|
|
21
30
|
}
|
|
22
31
|
return {
|
|
23
32
|
baseUrl: required('BASE_URL', options.baseUrl ?? process.env.BASE_URL),
|
|
@@ -28,7 +37,8 @@ export function resolveConfig(options, resources = []) {
|
|
|
28
37
|
operatorUrl: hostsPaidFunctions(resources)
|
|
29
38
|
? required('OPERATOR_URL', options.operatorUrl ?? process.env.OPERATOR_URL)
|
|
30
39
|
: options.operatorUrl ?? process.env.OPERATOR_URL,
|
|
31
|
-
|
|
40
|
+
costLimitMicroUsd: costLimit,
|
|
41
|
+
inferenceCostLimitMicroUsd: inferenceCostLimit,
|
|
32
42
|
};
|
|
33
43
|
}
|
|
34
44
|
export class StellariumNode {
|
|
@@ -43,6 +53,18 @@ export class StellariumNode {
|
|
|
43
53
|
*/
|
|
44
54
|
async start(fastify, options) {
|
|
45
55
|
const config = resolveConfig(options, this.resources);
|
|
56
|
+
const enrollmentToken = options.enrollmentToken ?? process.env.ENROLLMENT_TOKEN;
|
|
57
|
+
// Paid functions are paid to the node's organization, so a provider must be enrolled before it serves them.
|
|
58
|
+
// A node that only calls paid functions enrolls the same way, so its vouchers draw on its organization's balance.
|
|
59
|
+
if (hostsPaidFunctions(this.resources) || enrollmentToken) {
|
|
60
|
+
try {
|
|
61
|
+
({ organizationId: config.organizationId } = await new OperatorClient(config.operatorUrl, config.publicKey, config.privateKey).session(enrollmentToken));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const hint = enrollmentToken ? '' : '. Create an enrollment token for it at https://platform.stellarium.sh and set ENROLLMENT_TOKEN';
|
|
65
|
+
throw new Error(`${error instanceof Error ? error.message : String(error)}${hint}`, { cause: error });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
46
68
|
const db = await openDatabase(options.databasePath ?? DEFAULT_DATABASE_PATH);
|
|
47
69
|
fastify.addHook('onClose', async () => db.close());
|
|
48
70
|
const service = createService(db, this.resources, config, { approveGrant: options.approveGrant });
|
package/dist/service.d.ts
CHANGED
|
@@ -16,8 +16,15 @@ export interface NodeConfig {
|
|
|
16
16
|
apiKey?: string;
|
|
17
17
|
/** Operator base URL, including `/v1`. Required when the node hosts paid functions. */
|
|
18
18
|
operatorUrl?: string;
|
|
19
|
+
/**
|
|
20
|
+
* The operator organization this node is enrolled in, which is paid for its functions.
|
|
21
|
+
* `StellariumNode.start` asks the operator for it when the node hosts paid functions.
|
|
22
|
+
*/
|
|
23
|
+
organizationId?: string;
|
|
19
24
|
/** Most this node will pay for one remote call. */
|
|
20
|
-
|
|
25
|
+
costLimitMicroUsd: number;
|
|
26
|
+
/** Most this node will sign for in one remote inference call. */
|
|
27
|
+
inferenceCostLimitMicroUsd: number;
|
|
21
28
|
}
|
|
22
29
|
export interface Service {
|
|
23
30
|
config: NodeConfig;
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAClE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEvF,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAE5D,OAAO,EAAE,KAAK,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAErE,4GAA4G;AAC5G,MAAM,WAAW,UAAU;IACvB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mDAAmD;IACnD,
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAClE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEvF,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAE5D,OAAO,EAAE,KAAK,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAErE,4GAA4G;AAC5G,MAAM,WAAW,UAAU;IACvB,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAChB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,qCAAqC;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mDAAmD;IACnD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iEAAiE;IACjE,0BAA0B,EAAE,MAAM,CAAC;CACtC;AAED,MAAM,WAAW,OAAO;IACpB,MAAM,EAAE,UAAU,CAAC;IACnB,cAAc,EAAE,cAAc,CAAC;IAC/B,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,eAAe,EAAE,eAAe,CAAC;IACjC,cAAc,EAAE,cAAc,CAAC;IAC/B,YAAY,EAAE,YAAY,CAAC;CAC9B;AAED,MAAM,WAAW,cAAc;IAC3B,+EAA+E;IAC/E,YAAY,CAAC,EAAE,YAAY,CAAC;CAC/B;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,kBAAkB,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAoBtI"}
|
package/dist/service.js
CHANGED
|
@@ -6,11 +6,11 @@ import { OperatorClient } from "./client/operatorClient.js";
|
|
|
6
6
|
import { GrantRepository } from "./grant/repository.js";
|
|
7
7
|
import { GrantService } from "./grant/service.js";
|
|
8
8
|
export function createService(db, resources, config, options = {}) {
|
|
9
|
-
const resourceRepository = new ResourceRepository(db, resources, config.baseUrl);
|
|
9
|
+
const resourceRepository = new ResourceRepository(db, resources, config.baseUrl, config.organizationId);
|
|
10
10
|
const resourceResolver = new CombinationResourceResolver(resourceRepository);
|
|
11
11
|
const grantService = new GrantService(new GrantRepository(db), config, options.approveGrant);
|
|
12
|
-
const resourceInvoker = new CombinationResourceInvoker(resourceRepository, config.
|
|
13
|
-
const operatorClient = new OperatorClient(config.operatorUrl);
|
|
12
|
+
const resourceInvoker = new CombinationResourceInvoker(resourceRepository, config.costLimitMicroUsd, config.publicKey, config.privateKey, grantService, config.inferenceCostLimitMicroUsd);
|
|
13
|
+
const operatorClient = new OperatorClient(config.operatorUrl, config.publicKey, config.privateKey);
|
|
14
14
|
return {
|
|
15
15
|
config,
|
|
16
16
|
peerRepository: new PeerRepository(db),
|