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.
Files changed (39) hide show
  1. package/dist/client/operatorAuth.d.ts +19 -0
  2. package/dist/client/operatorAuth.d.ts.map +1 -0
  3. package/dist/client/operatorAuth.js +21 -0
  4. package/dist/client/operatorClient.d.ts +75 -16
  5. package/dist/client/operatorClient.d.ts.map +1 -1
  6. package/dist/client/operatorClient.js +82 -23
  7. package/dist/db/db.d.ts.map +1 -1
  8. package/dist/db/db.js +11 -3
  9. package/dist/index.d.ts +5 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +4 -1
  12. package/dist/resource/inference.d.ts +38 -0
  13. package/dist/resource/inference.d.ts.map +1 -0
  14. package/dist/resource/inference.js +74 -0
  15. package/dist/resource/invoker.d.ts +65 -8
  16. package/dist/resource/invoker.d.ts.map +1 -1
  17. package/dist/resource/invoker.js +120 -21
  18. package/dist/resource/repository.d.ts +29 -5
  19. package/dist/resource/repository.d.ts.map +1 -1
  20. package/dist/resource/repository.js +38 -7
  21. package/dist/routes.d.ts.map +1 -1
  22. package/dist/routes.js +206 -52
  23. package/dist/server.d.ts +13 -3
  24. package/dist/server.d.ts.map +1 -1
  25. package/dist/server.js +26 -4
  26. package/dist/service.d.ts +8 -1
  27. package/dist/service.d.ts.map +1 -1
  28. package/dist/service.js +3 -3
  29. package/dist/tests/client/operatorClient.test.js +94 -36
  30. package/dist/tests/db/db.test.js +18 -1
  31. package/dist/tests/helpers.d.ts +11 -1
  32. package/dist/tests/helpers.d.ts.map +1 -1
  33. package/dist/tests/helpers.js +38 -3
  34. package/dist/tests/integration/network.test.js +78 -7
  35. package/dist/tests/resource/invoker.test.js +95 -4
  36. package/dist/tests/resource/repository.test.js +20 -6
  37. package/dist/tests/routes.test.js +194 -23
  38. package/dist/tests/server.test.js +35 -8
  39. 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
- resourcePath: { type: 'string' },
56
- resourceName: { type: 'string' },
57
- functionName: { type: 'string' },
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 to the function's recipient.
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.amount < manifest.costMicroUsdc) {
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
- costMicroUsdc: fn.paymentManifest.costMicroUsdc,
192
- recipientAddress: fn.paymentManifest.recipientAddress,
193
- }) : undefined,
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
- fastify.post('/run/resource', {
244
- schema: { body: resourceRunRequestSchema },
245
- config: { rateLimit: options.rateLimit ?? { max: 100, timeWindow: '1 minute' } },
246
- }, async (request, reply) => {
247
- const resourceRequest = request.body;
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
- return reply.status(400).send({ error: 'Resource not found' });
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
- return reply.status(400).send({ error: 'Function not found' });
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
- return reply.status(400).send({ error: 'Parameters must be declared by the function and appear once each' });
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
- return reply.status(403).send({ error: 'Authorization is required', grantEndpoint });
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
- return reply.status(403).send({ error: 'Invalid authorization', grantEndpoint });
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
- // Settle only verified vouchers, and only after a successful invoke. A voucher sent to a free function is ignored.
292
- // Run this on a separate thread to avoid blocking the main thread.
293
- if (paymentManifest && paymentVoucher && !result.error) {
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
- await options.service.operatorClient.settlePayment({ voucher: paymentVoucher });
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 settle payment');
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 DEFAULT_COST_LIMIT_MICRO_USDC = 10000;
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
- /** Most this node pays for one remote call. Defaults to `COST_LIMIT_MICRO_USDC`, then 10,000 ($0.01). */
28
- costLimitMicroUsdc?: number;
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
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
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
- export const DEFAULT_COST_LIMIT_MICRO_USDC = 10_000;
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.costLimitMicroUsdc ?? (process.env.COST_LIMIT_MICRO_USDC ? Number(process.env.COST_LIMIT_MICRO_USDC) : DEFAULT_COST_LIMIT_MICRO_USDC);
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('costLimitMicroUsdc must be a nonnegative integer');
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
- costLimitMicroUsdc: costLimit,
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
- costLimitMicroUsdc: number;
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;
@@ -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,kBAAkB,EAAE,MAAM,CAAC;CAC9B;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"}
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.costLimitMicroUsdc, config.publicKey, config.privateKey, grantService);
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),