stellarium-ts 0.19.0 → 0.20.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 (80) hide show
  1. package/dist/client/operatorClient.d.ts +34 -3
  2. package/dist/client/operatorClient.d.ts.map +1 -1
  3. package/dist/client/operatorClient.js +15 -0
  4. package/dist/contract/repository.d.ts +49 -0
  5. package/dist/contract/repository.d.ts.map +1 -0
  6. package/dist/contract/repository.js +80 -0
  7. package/dist/contract/service.d.ts +103 -0
  8. package/dist/contract/service.d.ts.map +1 -0
  9. package/dist/contract/service.js +297 -0
  10. package/dist/contract/state.d.ts +43 -0
  11. package/dist/contract/state.d.ts.map +1 -0
  12. package/dist/contract/state.js +79 -0
  13. package/dist/db/database.d.ts +25 -0
  14. package/dist/db/database.d.ts.map +1 -0
  15. package/dist/db/database.js +1 -0
  16. package/dist/db/db.d.ts +12 -4
  17. package/dist/db/db.d.ts.map +1 -1
  18. package/dist/db/db.js +97 -24
  19. package/dist/db/postgres.d.ts +39 -0
  20. package/dist/db/postgres.d.ts.map +1 -0
  21. package/dist/db/postgres.js +132 -0
  22. package/dist/db/sqlite.d.ts +16 -0
  23. package/dist/db/sqlite.d.ts.map +1 -0
  24. package/dist/db/sqlite.js +53 -0
  25. package/dist/grant/repository.d.ts +2 -2
  26. package/dist/grant/repository.d.ts.map +1 -1
  27. package/dist/grant/repository.js +2 -2
  28. package/dist/index.d.ts +3 -1
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +2 -1
  31. package/dist/peer/repository.d.ts +2 -2
  32. package/dist/peer/repository.d.ts.map +1 -1
  33. package/dist/peer/repository.js +1 -1
  34. package/dist/resource/invoker.d.ts +85 -0
  35. package/dist/resource/invoker.d.ts.map +1 -1
  36. package/dist/resource/invoker.js +69 -6
  37. package/dist/resource/repository.d.ts +51 -10
  38. package/dist/resource/repository.d.ts.map +1 -1
  39. package/dist/resource/repository.js +116 -61
  40. package/dist/resource/settlement.d.ts +5 -0
  41. package/dist/resource/settlement.d.ts.map +1 -1
  42. package/dist/resource/settlement.js +19 -0
  43. package/dist/routes.d.ts.map +1 -1
  44. package/dist/routes.js +161 -25
  45. package/dist/server.d.ts +30 -1
  46. package/dist/server.d.ts.map +1 -1
  47. package/dist/server.js +80 -5
  48. package/dist/service.d.ts +27 -2
  49. package/dist/service.d.ts.map +1 -1
  50. package/dist/service.js +16 -1
  51. package/dist/startup.d.ts +2 -2
  52. package/dist/startup.d.ts.map +1 -1
  53. package/dist/startup.js +43 -34
  54. package/dist/tests/contract/repository.test.d.ts +2 -0
  55. package/dist/tests/contract/repository.test.d.ts.map +1 -0
  56. package/dist/tests/contract/repository.test.js +68 -0
  57. package/dist/tests/contract/routes.test.d.ts +2 -0
  58. package/dist/tests/contract/routes.test.d.ts.map +1 -0
  59. package/dist/tests/contract/routes.test.js +252 -0
  60. package/dist/tests/contract/state-routes.test.d.ts +2 -0
  61. package/dist/tests/contract/state-routes.test.d.ts.map +1 -0
  62. package/dist/tests/contract/state-routes.test.js +158 -0
  63. package/dist/tests/contract/state.test.d.ts +2 -0
  64. package/dist/tests/contract/state.test.d.ts.map +1 -0
  65. package/dist/tests/contract/state.test.js +57 -0
  66. package/dist/tests/db/db.test.js +1 -1
  67. package/dist/tests/db/postgres.test.d.ts +2 -0
  68. package/dist/tests/db/postgres.test.d.ts.map +1 -0
  69. package/dist/tests/db/postgres.test.js +43 -0
  70. package/dist/tests/grant/service.test.js +3 -4
  71. package/dist/tests/helpers.d.ts +8 -0
  72. package/dist/tests/helpers.d.ts.map +1 -1
  73. package/dist/tests/helpers.js +37 -0
  74. package/dist/tests/integration/network.test.js +74 -4
  75. package/dist/tests/peer/repository.test.js +3 -4
  76. package/dist/tests/resource/repository.test.js +3 -4
  77. package/dist/tests/routes.test.js +10 -11
  78. package/dist/tests/server.test.js +30 -0
  79. package/dist/tests/startup.test.js +4 -5
  80. package/package.json +5 -2
package/dist/server.d.ts CHANGED
@@ -8,6 +8,8 @@ import { RateLimitConfig } from './routes.js';
8
8
  import { type NodeConfig } from './service.js';
9
9
  export declare const DEFAULT_COST_LIMIT_MICRO_USD = 10000;
10
10
  export declare const DEFAULT_GRANT_REFRESH_INTERVAL_MS: number;
11
+ /** How often a node with an operator asks it again for its organization and price, which may change on the dashboard. */
12
+ export declare const DEFAULT_SESSION_REFRESH_INTERVAL_MS: number;
11
13
  export interface StellariumNodeOptions {
12
14
  port?: number;
13
15
  host?: string;
@@ -36,8 +38,33 @@ export interface StellariumNodeOptions {
36
38
  * Defaults to `INFERENCE_COST_LIMIT_MICRO_USD`, then to the resolved `costLimitMicroUsd`.
37
39
  */
38
40
  inferenceCostLimitMicroUsd?: number;
39
- /** SQLite file. Defaults to `./requestscript.db`; use ':memory:' for tests. */
41
+ /**
42
+ * Most one call to a contract on another node may cost: what its voucher lets the provider hold for the
43
+ * contract's tokens and its paid calls. Defaults to `CONTRACT_COST_LIMIT_MICRO_USD`, then to the resolved
44
+ * `costLimitMicroUsd`.
45
+ */
46
+ contractCostLimitMicroUsd?: number;
47
+ /** What another organization pays to deploy a contract to this node. Defaults to `CONTRACT_DEPLOY_FEE_MICRO_USD`, then 0. */
48
+ contractDeployFeeMicroUsd?: number;
49
+ /** Largest contract source this node accepts, in bytes. Defaults to `MAX_CONTRACT_SOURCE_BYTES`, then 64 KiB. */
50
+ maxContractSourceBytes?: number;
51
+ /**
52
+ * Most one contract's state may hold in one scope, in bytes of its variables' encoded values. A call that
53
+ * would grow it past this fails and saves nothing. Defaults to `MAX_CONTRACT_STATE_BYTES`, then 64 KiB.
54
+ */
55
+ maxContractStateBytes?: number;
56
+ /** SQLite file. Defaults to `DATABASE_PATH`, then `./requestscript.db`; use ':memory:' for tests. */
40
57
  databasePath?: string;
58
+ /**
59
+ * A `postgres://` or `postgresql://` URL. Defaults to `DATABASE_URL`. When set, the node keeps its data in
60
+ * Postgres instead of a SQLite file, so `databasePath` must not be set too.
61
+ */
62
+ databaseUrl?: string;
63
+ /**
64
+ * Postgres schema for the node's tables, created if missing, so several nodes can share a database.
65
+ * Defaults to `DATABASE_SCHEMA`, then to the connection's search path.
66
+ */
67
+ databaseSchema?: string;
41
68
  /**
42
69
  * Grant chains addressed to this node, stored at start. Each is root first. A stored renewal
43
70
  * of the same grant is kept, and an expired chain is skipped with a warning.
@@ -45,6 +72,8 @@ export interface StellariumNodeOptions {
45
72
  grants?: Grant[][];
46
73
  /** How often held grants past half their lifetime are renewed. Defaults to 5 minutes. */
47
74
  grantRefreshIntervalMs?: number;
75
+ /** How often to ask the operator again for this node's organization and price. Defaults to five minutes. */
76
+ sessionRefreshIntervalMs?: number;
48
77
  /**
49
78
  * Decides callers' requests for grants on this node's resources (`POST /v1/grants/request`).
50
79
  * Without it, callers can only get grants from `issueGrant`.
@@ -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,12 +1,17 @@
1
1
  import 'dotenv/config';
2
- import { DEFAULT_DATABASE_PATH, openDatabase } from './db/db.js';
2
+ import { DEFAULT_DATABASE_PATH, isPostgresUrl, openDatabase } from './db/db.js';
3
+ import { assertSchemaName } from './db/postgres.js';
4
+ import { DEFAULT_MAX_CONTRACT_SOURCE_BYTES } from './contract/service.js';
5
+ import { DEFAULT_MAX_CONTRACT_STATE_BYTES } from './contract/state.js';
3
6
  import { validateInferenceFunction } from './resource/inference.js';
4
7
  import routes from './routes.js';
5
8
  import { createService } from './service.js';
6
- import { StartupService } from './startup.js';
9
+ import { announceResources, StartupService } from './startup.js';
7
10
  import { OperatorClient } from './client/operatorClient.js';
8
11
  export const DEFAULT_COST_LIMIT_MICRO_USD = 10_000;
9
12
  export const DEFAULT_GRANT_REFRESH_INTERVAL_MS = 1000 * 60 * 5;
13
+ /** How often a node with an operator asks it again for its organization and price, which may change on the dashboard. */
14
+ export const DEFAULT_SESSION_REFRESH_INTERVAL_MS = 1000 * 60 * 5;
10
15
  function required(name, value) {
11
16
  if (!value) {
12
17
  throw new Error(`${name} is required: pass it to StellariumNode.start or set it in the environment`);
@@ -28,6 +33,10 @@ export function resolveConfig(options, resources = []) {
28
33
  for (const resource of resources) {
29
34
  resource.functions.forEach(validateInferenceFunction);
30
35
  }
36
+ const contractCostLimit = nonnegativeInteger('contractCostLimitMicroUsd', options.contractCostLimitMicroUsd, process.env.CONTRACT_COST_LIMIT_MICRO_USD) ?? costLimit;
37
+ const contractDeployFee = nonnegativeInteger('contractDeployFeeMicroUsd', options.contractDeployFeeMicroUsd, process.env.CONTRACT_DEPLOY_FEE_MICRO_USD) ?? 0;
38
+ const maxContractSourceBytes = nonnegativeInteger('maxContractSourceBytes', options.maxContractSourceBytes, process.env.MAX_CONTRACT_SOURCE_BYTES) ?? DEFAULT_MAX_CONTRACT_SOURCE_BYTES;
39
+ const maxContractStateBytes = nonnegativeInteger('maxContractStateBytes', options.maxContractStateBytes, process.env.MAX_CONTRACT_STATE_BYTES) ?? DEFAULT_MAX_CONTRACT_STATE_BYTES;
31
40
  return {
32
41
  baseUrl: required('BASE_URL', options.baseUrl ?? process.env.BASE_URL),
33
42
  publicKey: required('SIGNING_PUBLIC_KEY', options.publicKey ?? process.env.SIGNING_PUBLIC_KEY),
@@ -39,8 +48,43 @@ export function resolveConfig(options, resources = []) {
39
48
  : options.operatorUrl ?? process.env.OPERATOR_URL,
40
49
  costLimitMicroUsd: costLimit,
41
50
  inferenceCostLimitMicroUsd: inferenceCostLimit,
51
+ contractCostLimitMicroUsd: contractCostLimit,
52
+ contractDeployFeeMicroUsd: contractDeployFee,
53
+ maxContractSourceBytes,
54
+ maxContractStateBytes,
55
+ ...resolveDatabase(options),
42
56
  };
43
57
  }
58
+ /** An option, else its environment variable, which must be a nonnegative integer. Undefined when neither is set. */
59
+ function nonnegativeInteger(name, option, env) {
60
+ const value = option ?? (env ? Number(env) : undefined);
61
+ if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
62
+ throw new Error(`${name} must be a nonnegative integer`);
63
+ }
64
+ return value;
65
+ }
66
+ function resolveDatabase(options) {
67
+ // Empty variables, as a compose file or .env may leave them, count as unset.
68
+ const databaseUrl = options.databaseUrl ?? (process.env.DATABASE_URL || undefined);
69
+ const databasePath = options.databasePath ?? (process.env.DATABASE_PATH || undefined);
70
+ const databaseSchema = options.databaseSchema ?? (process.env.DATABASE_SCHEMA || undefined);
71
+ if (databaseSchema !== undefined) {
72
+ if (databaseUrl === undefined) {
73
+ throw new Error('databaseSchema (DATABASE_SCHEMA) needs a Postgres databaseUrl (DATABASE_URL)');
74
+ }
75
+ assertSchemaName(databaseSchema);
76
+ }
77
+ if (databaseUrl === undefined) {
78
+ return { databasePath: databasePath ?? DEFAULT_DATABASE_PATH };
79
+ }
80
+ if (!isPostgresUrl(databaseUrl)) {
81
+ throw new Error('databaseUrl (DATABASE_URL) must be a postgres:// or postgresql:// URL');
82
+ }
83
+ if (databasePath !== undefined) {
84
+ throw new Error('Set either databaseUrl (DATABASE_URL) for Postgres or databasePath (DATABASE_PATH) for SQLite, not both');
85
+ }
86
+ return { databaseUrl, databaseSchema };
87
+ }
44
88
  export class StellariumNode {
45
89
  resources = [];
46
90
  service;
@@ -61,7 +105,7 @@ export class StellariumNode {
61
105
  const session = () => new OperatorClient(config.operatorUrl, config.publicKey, config.privateKey).session(enrollmentToken, config.baseUrl);
62
106
  if (hostsPaidFunctions(this.resources) || enrollmentToken) {
63
107
  try {
64
- ({ organizationId: config.organizationId } = await session());
108
+ ({ organizationId: config.organizationId, priceMicroUsdPerMillionTokens: config.priceMicroUsdPerMillionTokens } = await session());
65
109
  }
66
110
  catch (error) {
67
111
  const hint = enrollmentToken ? '' : '. Create an enrollment token for it at https://platform.stellarium.sh and set ENROLLMENT_TOKEN';
@@ -71,13 +115,14 @@ export class StellariumNode {
71
115
  else if (config.operatorUrl) {
72
116
  // Serving doesn't depend on it, so it doesn't hold up the start. Until it answers, run tokens from
73
117
  // this node's own organization are refused as if billed.
74
- session().then(({ organizationId }) => {
118
+ session().then(({ organizationId, priceMicroUsdPerMillionTokens }) => {
75
119
  config.organizationId = organizationId;
120
+ config.priceMicroUsdPerMillionTokens = priceMicroUsdPerMillionTokens;
76
121
  }, (error) => {
77
122
  fastify.log.warn(`No session with the operator, so this node can't be listed: ${error instanceof Error ? error.message : String(error)}`);
78
123
  });
79
124
  }
80
- const db = await openDatabase(options.databasePath ?? DEFAULT_DATABASE_PATH);
125
+ const db = await openDatabase(config.databaseUrl ?? config.databasePath, { schema: config.databaseSchema });
81
126
  fastify.addHook('onClose', async () => db.close());
82
127
  const service = createService(db, this.resources, config, { approveGrant: options.approveGrant });
83
128
  // A configured chain may be older than its stored renewal, or long expired; neither should stop the node.
@@ -112,11 +157,41 @@ export class StellariumNode {
112
157
  refresher.unref();
113
158
  };
114
159
  scheduleRefresh();
160
+ // Contracts are advertised at the node's price, which its organization may change at any time.
161
+ let sessionRefresher;
162
+ const scheduleSessionRefresh = () => {
163
+ sessionRefresher = setTimeout(async () => {
164
+ try {
165
+ const { organizationId, priceMicroUsdPerMillionTokens } = await session();
166
+ const changed = organizationId !== config.organizationId || priceMicroUsdPerMillionTokens !== config.priceMicroUsdPerMillionTokens;
167
+ config.organizationId = organizationId;
168
+ config.priceMicroUsdPerMillionTokens = priceMicroUsdPerMillionTokens;
169
+ if (changed && (await service.contractRepository.allLatest()).length > 0) {
170
+ service.contractService.announce?.();
171
+ }
172
+ }
173
+ catch (error) {
174
+ fastify.log.warn(`Failed to refresh this node's session with the operator: ${error instanceof Error ? error.message : String(error)}`);
175
+ }
176
+ if (!closed) {
177
+ scheduleSessionRefresh();
178
+ }
179
+ }, options.sessionRefreshIntervalMs ?? DEFAULT_SESSION_REFRESH_INTERVAL_MS);
180
+ sessionRefresher.unref();
181
+ };
182
+ if (config.operatorUrl) {
183
+ scheduleSessionRefresh();
184
+ }
115
185
  fastify.addHook('onClose', async () => {
116
186
  closed = true;
117
187
  clearTimeout(refresher);
188
+ clearTimeout(sessionRefresher);
118
189
  });
119
190
  await fastify.register(routes, { prefix: '/v1', service, rateLimit: options.rateLimit });
191
+ // Peers learn of a contract as soon as it is deployed, and forget it when it is deleted.
192
+ service.contractService.announce = () => {
193
+ announceResources(fastify.log, service).catch(error => fastify.log.error(error, 'Failed to announce resources'));
194
+ };
120
195
  const address = await fastify.listen({ port: options.port ?? 3000, host: options.host ?? '0.0.0.0' });
121
196
  fastify.log.info(`server listening on ${address}`);
122
197
  if (options.startingPeer) {
package/dist/service.d.ts CHANGED
@@ -1,12 +1,15 @@
1
1
  import type { JWTVerifyGetKey } from "jose";
2
2
  import { ResourceResolver } from "requestscript";
3
- import type { Database } from "sqlite";
3
+ import type { NodeDatabase } from "./db/database.js";
4
4
  import { PeerRepository } from "./peer/repository.js";
5
5
  import { ResourceRepository, type StellariumResource } from "./resource/repository.js";
6
6
  import { CombinationResourceInvoker } from "./resource/invoker.js";
7
7
  import { OperatorClient } from "./client/operatorClient.js";
8
8
  import { RunTokenVerifier } from "./client/runTokens.js";
9
9
  import { type ApproveGrant, GrantService } from "./grant/service.js";
10
+ import { ContractRepository } from "./contract/repository.js";
11
+ import { ContractService } from "./contract/service.js";
12
+ import { ContractStateRepository } from "./contract/state.js";
10
13
  /** Settings a node runs with. `StellariumNode.start` fills these from its options, then the environment. */
11
14
  export interface NodeConfig {
12
15
  /** URL peers use to reach this node. Also its peer name. */
@@ -28,6 +31,25 @@ export interface NodeConfig {
28
31
  costLimitMicroUsd: number;
29
32
  /** Most this node will sign for in one remote inference call. */
30
33
  inferenceCostLimitMicroUsd: number;
34
+ /** Most one call to a contract on another node may hold: its voucher's `maxAmount`. Defaults to `costLimitMicroUsd`. */
35
+ contractCostLimitMicroUsd?: number;
36
+ /** What another organization pays to deploy a contract to this node. 0 or unset is free. */
37
+ contractDeployFeeMicroUsd?: number;
38
+ /** Largest contract source this node accepts. Defaults to 64 KiB. */
39
+ maxContractSourceBytes?: number;
40
+ /** Most one contract's state may hold in one scope, its variables' encoded values together. Defaults to 64 KiB. */
41
+ maxContractStateBytes?: number;
42
+ /**
43
+ * This node's listed price per million script tokens, which its contracts are charged at. `StellariumNode.start`
44
+ * learns it from the operator.
45
+ */
46
+ priceMicroUsdPerMillionTokens?: number;
47
+ /** SQLite file the node opens, when it has no `databaseUrl`. */
48
+ databasePath?: string;
49
+ /** Postgres URL. When set, the node uses Postgres instead of SQLite. */
50
+ databaseUrl?: string;
51
+ /** Postgres schema the node's tables live in. */
52
+ databaseSchema?: string;
31
53
  }
32
54
  export interface Service {
33
55
  config: NodeConfig;
@@ -37,6 +59,9 @@ export interface Service {
37
59
  resourceInvoker: CombinationResourceInvoker;
38
60
  operatorClient: OperatorClient;
39
61
  grantService: GrantService;
62
+ contractRepository: ContractRepository;
63
+ contractService: ContractService;
64
+ contractStateRepository: ContractStateRepository;
40
65
  /** Verifies the run tokens `POST /v1/run` accepts. Set when the node has an operator URL. */
41
66
  runTokenVerifier?: RunTokenVerifier;
42
67
  }
@@ -46,5 +71,5 @@ export interface ServiceOptions {
46
71
  /** Replaces the operator's published keys for verifying run tokens, in tests. */
47
72
  runTokenKeys?: JWTVerifyGetKey;
48
73
  }
49
- export declare function createService(db: Database, resources: StellariumResource[], config: NodeConfig, options?: ServiceOptions): Service;
74
+ export declare function createService(db: NodeDatabase, resources: StellariumResource[], config: NodeConfig, options?: ServiceOptions): Service;
50
75
  //# sourceMappingURL=service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,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;AACvF,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,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,iJAAiJ;IACjJ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iHAAiH;IACjH,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,0BAA0B,CAAC;IAC5C,cAAc,EAAE,cAAc,CAAC;IAC/B,YAAY,EAAE,YAAY,CAAC;IAC3B,6FAA6F;IAC7F,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACvC;AAED,MAAM,WAAW,cAAc;IAC3B,+EAA+E;IAC/E,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,iFAAiF;IACjF,YAAY,CAAC,EAAE,eAAe,CAAC;CAClC;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,kBAAkB,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAwBtI"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,MAAM,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACvF,OAAO,EAAE,0BAA0B,EAAE,MAAM,uBAAuB,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAEzD,OAAO,EAAE,KAAK,YAAY,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAE9D,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,iJAAiJ;IACjJ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,iHAAiH;IACjH,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;IACnC,wHAAwH;IACxH,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,4FAA4F;IAC5F,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,qEAAqE;IACrE,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,mHAAmH;IACnH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;OAGG;IACH,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,gEAAgE;IAChE,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,cAAc,CAAC,EAAE,MAAM,CAAC;CAC3B;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,0BAA0B,CAAC;IAC5C,cAAc,EAAE,cAAc,CAAC;IAC/B,YAAY,EAAE,YAAY,CAAC;IAC3B,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,eAAe,EAAE,eAAe,CAAC;IACjC,uBAAuB,EAAE,uBAAuB,CAAC;IACjD,6FAA6F;IAC7F,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACvC;AAED,MAAM,WAAW,cAAc;IAC3B,+EAA+E;IAC/E,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,iFAAiF;IACjF,YAAY,CAAC,EAAE,eAAe,CAAC;CAClC;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,kBAAkB,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,OAAO,GAAE,cAAmB,GAAG,OAAO,CAqC1I"}
package/dist/service.js CHANGED
@@ -6,8 +6,17 @@ import { OperatorClient } from "./client/operatorClient.js";
6
6
  import { RunTokenVerifier } from "./client/runTokens.js";
7
7
  import { GrantRepository } from "./grant/repository.js";
8
8
  import { GrantService } from "./grant/service.js";
9
+ import { ContractRepository } from "./contract/repository.js";
10
+ import { ContractService } from "./contract/service.js";
11
+ import { ContractStateRepository } from "./contract/state.js";
9
12
  export function createService(db, resources, config, options = {}) {
10
- const resourceRepository = new ResourceRepository(db, resources, config.baseUrl, config.organizationId);
13
+ const contractRepository = new ContractRepository(db);
14
+ // Read from the config on each use, since the node may learn its organization and price after it starts.
15
+ const resourceRepository = new ResourceRepository(db, resources, config.baseUrl, config.organizationId, {
16
+ repository: contractRepository,
17
+ price: () => config.priceMicroUsdPerMillionTokens ?? 0,
18
+ organizationId: () => config.organizationId,
19
+ });
11
20
  const resourceResolver = new CombinationResourceResolver(resourceRepository);
12
21
  const grantService = new GrantService(new GrantRepository(db), config, options.approveGrant);
13
22
  const operatorClient = new OperatorClient(config.operatorUrl, config.publicKey, config.privateKey);
@@ -15,6 +24,9 @@ export function createService(db, resources, config, options = {}) {
15
24
  operatorClient,
16
25
  organizationId: config.organizationId,
17
26
  });
27
+ const contractStateRepository = new ContractStateRepository(db);
28
+ const contractService = new ContractService(contractRepository, contractStateRepository, resourceRepository, resourceResolver, resourceInvoker, operatorClient, config);
29
+ resourceInvoker.contracts = { costLimitMicroUsd: config.contractCostLimitMicroUsd ?? config.costLimitMicroUsd, host: contractService };
18
30
  return {
19
31
  config,
20
32
  peerRepository: new PeerRepository(db),
@@ -23,6 +35,9 @@ export function createService(db, resources, config, options = {}) {
23
35
  resourceInvoker,
24
36
  operatorClient,
25
37
  grantService,
38
+ contractRepository,
39
+ contractService,
40
+ contractStateRepository,
26
41
  runTokenVerifier: config.operatorUrl ? new RunTokenVerifier(config.operatorUrl, config.publicKey, options.runTokenKeys) : undefined,
27
42
  };
28
43
  }
package/dist/startup.d.ts CHANGED
@@ -4,6 +4,8 @@ import { Service } from "./service.js";
4
4
  export declare const STARTUP_REQUEST_TIMEOUT_MS = 10000;
5
5
  /** Peers and resources requested per page from the starting peer. It may return fewer. */
6
6
  export declare const STARTUP_PAGE_SIZE = 100;
7
+ /** Sends this node's resources, signed, to every peer it knows. Run at startup, and when a contract is deployed or deleted. */
8
+ export declare function announceResources(log: FastifyBaseLogger, service: Service): Promise<void>;
7
9
  export declare class StartupService {
8
10
  private readonly log;
9
11
  private readonly service;
@@ -23,8 +25,6 @@ export declare class StartupService {
23
25
  private isSelf;
24
26
  populatePeers(): Promise<void>;
25
27
  populateResources(): Promise<void>;
26
- /** PUT to every peer, logging failures instead of throwing so one bad peer doesn't stop the rest. */
27
- private putToPeers;
28
28
  broadcastSelf(): Promise<void>;
29
29
  }
30
30
  //# sourceMappingURL=startup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAI5C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,4CAA4C;AAC5C,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAEjD,0FAA0F;AAC1F,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAKrC,qBAAa,cAAc;IAEnB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAN/B,YACqB,GAAG,EAAE,iBAAiB,EACtB,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EACnC;IAGE,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAUlC;IAED,iFAAiF;YACnE,mBAAmB;IAQjC;;;OAGG;YACW,2BAA2B;IAsBzC,OAAO,CAAC,MAAM;IAIR,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAkBnC;IAEK,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAWvC;IAED,qGAAqG;YACvF,UAAU;IAqBlB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CA8BnC;CACJ"}
1
+ {"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAI5C,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC,4CAA4C;AAC5C,eAAO,MAAM,0BAA0B,QAAS,CAAC;AAEjD,0FAA0F;AAC1F,eAAO,MAAM,iBAAiB,MAAM,CAAC;AAiCrC,+HAA+H;AAC/H,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,iBAAiB,EAAE,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAiB/F;AAED,qBAAa,cAAc;IAEnB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,YAAY;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAN/B,YACqB,GAAG,EAAE,iBAAiB,EACtB,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,MAAM,EACpB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EACnC;IAGE,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAUlC;IAED,iFAAiF;YACnE,mBAAmB;IAQjC;;;OAGG;YACW,2BAA2B;IAsBzC,OAAO,CAAC,MAAM;IAIR,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAkBnC;IAEK,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAWvC;IAEK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAenC;CACJ"}
package/dist/startup.js CHANGED
@@ -7,6 +7,47 @@ export const STARTUP_REQUEST_TIMEOUT_MS = 10_000;
7
7
  export const STARTUP_PAGE_SIZE = 100;
8
8
  // 10 minutes
9
9
  const SIGNATURE_LIFETIME_MS = 1000 * 60 * 10;
10
+ /** PUT to every peer, logging failures instead of throwing so one bad peer doesn't stop the rest. */
11
+ async function putToPeers(log, peers, path, body, description) {
12
+ for (const peer of peers) {
13
+ try {
14
+ const response = await fetch(`${peer.baseUrl}/v1${path}`, {
15
+ method: 'PUT',
16
+ headers: {
17
+ 'Content-Type': 'application/json',
18
+ },
19
+ body: JSON.stringify(body),
20
+ signal: AbortSignal.timeout(STARTUP_REQUEST_TIMEOUT_MS),
21
+ });
22
+ if (!response.ok) {
23
+ log.warn(`Failed to broadcast ${description} to node ${peer.baseUrl}: ${response.status} ${await response.text()}`);
24
+ }
25
+ }
26
+ catch (error) {
27
+ log.warn(error, `Failed to broadcast ${description} to node ${peer.baseUrl}`);
28
+ }
29
+ }
30
+ }
31
+ /** The peers this node knows, other than itself. */
32
+ async function otherPeers(service) {
33
+ const { baseUrl, publicKey } = service.config;
34
+ return (await service.peerRepository.getPeerList()).filter(peer => peer.publicKey !== publicKey && peer.baseUrl !== baseUrl);
35
+ }
36
+ /** Sends this node's resources, signed, to every peer it knows. Run at startup, and when a contract is deployed or deleted. */
37
+ export async function announceResources(log, service) {
38
+ log.info(`Broadcasting resources to peers`);
39
+ const peers = await otherPeers(service);
40
+ const resources = await service.resourceRepository.getInternalResources();
41
+ const expiry = new Date(Date.now() + SIGNATURE_LIFETIME_MS).toISOString();
42
+ const signature = signData(resourceAnnouncementPayload(resources, expiry), service.config.privateKey);
43
+ await putToPeers(log, peers, '/all-resources', {
44
+ publicKey: service.config.publicKey,
45
+ resources,
46
+ expiry,
47
+ signature,
48
+ }, 'resources');
49
+ log.info(`Broadcasted ${resources.length} resources to ${peers.length} peers`);
50
+ }
10
51
  export class StartupService {
11
52
  log;
12
53
  service;
@@ -91,27 +132,6 @@ export class StartupService {
91
132
  });
92
133
  this.log.info(`Learned up to ${count} resources`);
93
134
  }
94
- /** PUT to every peer, logging failures instead of throwing so one bad peer doesn't stop the rest. */
95
- async putToPeers(peers, path, body, description) {
96
- for (const peer of peers) {
97
- try {
98
- const response = await fetch(`${peer.baseUrl}/v1${path}`, {
99
- method: 'PUT',
100
- headers: {
101
- 'Content-Type': 'application/json',
102
- },
103
- body: JSON.stringify(body),
104
- signal: AbortSignal.timeout(STARTUP_REQUEST_TIMEOUT_MS),
105
- });
106
- if (!response.ok) {
107
- this.log.warn(`Failed to broadcast ${description} to node ${peer.baseUrl}: ${response.status} ${await response.text()}`);
108
- }
109
- }
110
- catch (error) {
111
- this.log.warn(error, `Failed to broadcast ${description} to node ${peer.baseUrl}`);
112
- }
113
- }
114
- }
115
135
  async broadcastSelf() {
116
136
  this.log.info(`Broadcasting self to peers`);
117
137
  // Let the peers know about this node.
@@ -119,19 +139,8 @@ export class StartupService {
119
139
  const self = { baseUrl: this.baseUrl, name: this.baseUrl, publicKey: this.publicKey };
120
140
  const peerSignatureExpiry = new Date(Date.now() + SIGNATURE_LIFETIME_MS).toISOString();
121
141
  const peerSignature = signData(peerAnnouncementPayload(self, peerSignatureExpiry), this.privateKey);
122
- await this.putToPeers(peers, '/peers', { ...self, signature: peerSignature, expiry: peerSignatureExpiry }, 'self');
142
+ await putToPeers(this.log, peers, '/peers', { ...self, signature: peerSignature, expiry: peerSignatureExpiry }, 'self');
123
143
  this.log.info(`Broadcasted self to ${peers.length} peers`);
124
- this.log.info(`Broadcasting resources to peers`);
125
- // Let the peers know about the resources available on this node.
126
- const resources = await this.service.resourceRepository.getInternalResources();
127
- const resourceSignatureExpiry = new Date(Date.now() + SIGNATURE_LIFETIME_MS).toISOString();
128
- const resourceSignature = signData(resourceAnnouncementPayload(resources, resourceSignatureExpiry), this.privateKey);
129
- await this.putToPeers(peers, '/all-resources', {
130
- publicKey: this.publicKey,
131
- resources,
132
- expiry: resourceSignatureExpiry,
133
- signature: resourceSignature,
134
- }, 'resources');
135
- this.log.info(`Broadcasted ${resources.length} resources to ${peers.length} peers`);
144
+ await announceResources(this.log, this.service);
136
145
  }
137
146
  }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=repository.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repository.test.d.ts","sourceRoot":"","sources":["../../../src/tests/contract/repository.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,68 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
2
+ import { ContractRepository } from "../../contract/repository.js";
3
+ import { DIALECTS, openTestDatabase } from "../helpers.js";
4
+ function contract(version, overrides = {}) {
5
+ return {
6
+ path: "com.example",
7
+ name: "Sum",
8
+ version,
9
+ source: `contract com.example.Sum@${version}(a: int32): int32 { return a }`,
10
+ parameters: [{ name: "a", type: "int32" }],
11
+ returnType: "int32",
12
+ ownerOrganizationId: "org_a",
13
+ description: null,
14
+ ...overrides,
15
+ };
16
+ }
17
+ describe.each(DIALECTS)("ContractRepository (%s)", (dialect) => {
18
+ let db;
19
+ let repository;
20
+ beforeEach(async () => {
21
+ db = await openTestDatabase(dialect);
22
+ repository = new ContractRepository(db);
23
+ });
24
+ afterEach(async () => {
25
+ await db.close();
26
+ });
27
+ test("finds the latest version by comparing versions numerically", async () => {
28
+ for (const version of ["1", "1.10", "1.9"]) {
29
+ expect(await repository.save(contract(version))).toEqual({ ok: true });
30
+ }
31
+ expect(await repository.latest("com.example", "Sum")).toMatchObject({ version: "1.10", parameters: [{ name: "a", type: "int32" }], returnType: "int32" });
32
+ expect(await repository.latest("com.example", "Other")).toBeUndefined();
33
+ expect((await repository.allLatest()).map(c => c.version)).toEqual(["1.10"]);
34
+ });
35
+ test("keeps versions immutable", async () => {
36
+ await repository.save(contract("1"));
37
+ expect(await repository.save(contract("1", { source: "changed" }))).toEqual({ ok: false, refused: "version_exists" });
38
+ expect((await repository.latest("com.example", "Sum"))?.source).toContain("@1(");
39
+ });
40
+ test("gives every version to the first deployer's organization, and lets the admin add versions", async () => {
41
+ await repository.save(contract("1"));
42
+ expect(await repository.save(contract("2", { ownerOrganizationId: "org_b" }))).toEqual({ ok: false, refused: "owned_by_another" });
43
+ expect(await repository.save(contract("2", { ownerOrganizationId: null }))).toEqual({ ok: true });
44
+ expect(await repository.owner("com.example", "Sum")).toBe("org_a");
45
+ expect((await repository.latest("com.example", "Sum"))?.ownerOrganizationId).toBe("org_a");
46
+ });
47
+ test("an admin's contract belongs to no organization", async () => {
48
+ await repository.save(contract("1", { ownerOrganizationId: null }));
49
+ expect(await repository.owner("com.example", "Sum")).toBeNull();
50
+ expect(await repository.save(contract("2", { ownerOrganizationId: "org_b" }))).toEqual({ ok: false, refused: "owned_by_another" });
51
+ });
52
+ test("deletes one version or all of them", async () => {
53
+ await repository.save(contract("1"));
54
+ await repository.save(contract("2"));
55
+ expect(await repository.delete("com.example", "Sum", "2")).toBe(true);
56
+ expect((await repository.latest("com.example", "Sum"))?.version).toBe("1");
57
+ expect(await repository.delete("com.example", "Sum")).toBe(true);
58
+ expect(await repository.owner("com.example", "Sum")).toBeUndefined();
59
+ expect(await repository.delete("com.example", "Sum")).toBe(false);
60
+ });
61
+ test("lists every version a page at a time", async () => {
62
+ await repository.save(contract("1"));
63
+ await repository.save(contract("2"));
64
+ await repository.save(contract("1", { name: "Product" }));
65
+ expect((await repository.list(0, 2)).map(c => `${c.name}@${c.version}`)).toEqual(["Product@1", "Sum@1"]);
66
+ expect((await repository.list(2, 2)).map(c => `${c.name}@${c.version}`)).toEqual(["Sum@2"]);
67
+ });
68
+ });
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=routes.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"routes.test.d.ts","sourceRoot":"","sources":["../../../src/tests/contract/routes.test.ts"],"names":[],"mappings":""}