stellarium-ts 0.1.0 → 0.2.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/LICENSE +201 -0
  2. package/dist/client/operatorClient.d.ts +20 -0
  3. package/dist/client/operatorClient.d.ts.map +1 -0
  4. package/dist/client/operatorClient.js +18 -0
  5. package/dist/crypto/utils.d.ts +3 -0
  6. package/dist/crypto/utils.d.ts.map +1 -0
  7. package/dist/crypto/utils.js +19 -0
  8. package/dist/db/db.d.ts.map +1 -1
  9. package/dist/db/db.js +27 -7
  10. package/dist/peer/repository.d.ts +3 -0
  11. package/dist/peer/repository.d.ts.map +1 -1
  12. package/dist/peer/repository.js +22 -1
  13. package/dist/resource/invoker.d.ts +17 -0
  14. package/dist/resource/invoker.d.ts.map +1 -1
  15. package/dist/resource/invoker.js +43 -9
  16. package/dist/resource/repository.d.ts +35 -0
  17. package/dist/resource/repository.d.ts.map +1 -0
  18. package/dist/resource/repository.js +168 -0
  19. package/dist/resource/resolver.d.ts +1 -1
  20. package/dist/resource/resolver.d.ts.map +1 -1
  21. package/dist/routes.d.ts.map +1 -1
  22. package/dist/routes.js +53 -3
  23. package/dist/server.d.ts +2 -2
  24. package/dist/server.d.ts.map +1 -1
  25. package/dist/server.js +3 -3
  26. package/dist/service.d.ts +5 -3
  27. package/dist/service.d.ts.map +1 -1
  28. package/dist/service.js +6 -2
  29. package/dist/startup.d.ts +8 -2
  30. package/dist/startup.d.ts.map +1 -1
  31. package/dist/startup.js +77 -3
  32. package/dist/tests/crypto/utils.test.d.ts +2 -0
  33. package/dist/tests/crypto/utils.test.d.ts.map +1 -0
  34. package/dist/tests/crypto/utils.test.js +31 -0
  35. package/package.json +9 -3
  36. package/README.md +0 -3
  37. package/dist/resource/resource.d.ts +0 -16
  38. package/dist/resource/resource.d.ts.map +0 -1
  39. package/dist/resource/resource.js +0 -78
@@ -0,0 +1,168 @@
1
+ import { verifyData } from "@/crypto/utils.js";
2
+ export class ResourceRepository {
3
+ db;
4
+ resources;
5
+ constructor(db, resources = []) {
6
+ this.db = db;
7
+ this.resources = resources;
8
+ }
9
+ async create(resource) {
10
+ const createdAt = new Date().toISOString();
11
+ const result = await this.db.run('INSERT INTO resource (path, name, base_url, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [resource.path, resource.name, resource.baseUrl, createdAt, createdAt]);
12
+ if (!result.changes) {
13
+ throw new Error('Failed to create resource');
14
+ }
15
+ const resourceId = result.lastID;
16
+ for (const fn of resource.functions) {
17
+ const functionResult = await this.db.run('INSERT INTO resource_function (resource_id, name, return_type, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [resourceId, fn.name, fn.returnType, createdAt, createdAt]);
18
+ const functionId = functionResult.lastID;
19
+ for (const parameter of fn.parameters) {
20
+ await this.db.run('INSERT INTO resource_function_parameter (resource_function_id, name, parameter_type, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [functionId, parameter.name, parameter.type, createdAt, createdAt]);
21
+ }
22
+ if (fn.paymentManifest) {
23
+ await this.db.run('INSERT INTO resource_function_payment_manifest (resource_function_id, cost_lamports, recipient_address, created_at, updated_at) VALUES (?, ?, ?, ?, ?)', [functionId, fn.paymentManifest.costLamports, fn.paymentManifest.recipientAddress, createdAt, createdAt]);
24
+ }
25
+ }
26
+ }
27
+ async saveAll(resources, existingPeer, signature, expiry) {
28
+ if (!verifyData({ resources, expiry }, existingPeer.publicKey, signature) || new Date(expiry) < new Date()) {
29
+ return false;
30
+ }
31
+ for (const resource of resources) {
32
+ const existingResource = await this.db.get('SELECT * FROM resource WHERE path = ? AND name = ?', [resource.path, resource.name]);
33
+ // TODO: A signature must accompany the resource to ensure the base url change is only made by the owner of the resource.
34
+ // This will avoid MITM attacks.
35
+ if (existingResource) {
36
+ await this.db.run('UPDATE resource SET base_url = ?, updated_at = ? WHERE path = ? AND name = ?', [resource.baseUrl, new Date().toISOString(), resource.path, resource.name]);
37
+ for (const fn of resource.functions) {
38
+ const updatedAt = new Date().toISOString();
39
+ await this.db.run('UPDATE resource_function SET return_type = ?, updated_at = ? WHERE resource_id = ? AND name = ?', [fn.returnType, updatedAt, existingResource.id, fn.name]);
40
+ const existingFn = await this.db.get('SELECT id FROM resource_function WHERE resource_id = ? AND name = ?', [existingResource.id, fn.name]);
41
+ if (!existingFn) {
42
+ continue;
43
+ }
44
+ for (const parameter of fn.parameters) {
45
+ await this.db.run('UPDATE resource_function_parameter SET parameter_type = ? WHERE resource_function_id = ? AND name = ?', [parameter.type, existingFn.id, parameter.name]);
46
+ }
47
+ if (fn.paymentManifest) {
48
+ await this.db.run('UPDATE resource_function_payment_manifest SET cost_lamports = ?, recipient_address = ?, updated_at = ? WHERE resource_function_id = ?', [fn.paymentManifest.costLamports, fn.paymentManifest.recipientAddress, updatedAt, existingFn.id]);
49
+ }
50
+ }
51
+ }
52
+ else {
53
+ await this.create(resource);
54
+ }
55
+ }
56
+ return true;
57
+ }
58
+ async getResourceByName(name) {
59
+ return this.resources.find(r => r.name === name);
60
+ }
61
+ async getInternalResource(path, name) {
62
+ return this.resources.find(r => r.path === path && r.name === name);
63
+ }
64
+ async getResource(key) {
65
+ const resource = this.resources.find(r => `${r.path}.${r.name}` === key);
66
+ if (resource) {
67
+ return resource;
68
+ }
69
+ // Split the key with format path.path.path.*.Name
70
+ const pathName = key.split('.');
71
+ const name = pathName.pop();
72
+ const path = pathName.join('.');
73
+ const result = await this.db.get('SELECT * FROM resource WHERE path = ? AND name = ?', [path, name]);
74
+ if (!result) {
75
+ return undefined;
76
+ }
77
+ const resourceFunctions = await this.db.all('SELECT * FROM resource_function WHERE resource_id = ?', [result.id]);
78
+ return {
79
+ path: result.path,
80
+ name: result.name,
81
+ functions: await Promise.all(resourceFunctions.map(async (f) => ({
82
+ name: f.name,
83
+ returnType: f.return_type,
84
+ parameters: await this.getResourceParameters(f.id),
85
+ // The ExternalResourceInvoker will handle the execution of the function
86
+ exec: async () => {
87
+ return void 0;
88
+ }
89
+ }))),
90
+ metadata: {
91
+ baseUrl: result.base_url,
92
+ paymentManifest: await this.getResourceFunctionPaymentManifest(result.id),
93
+ }
94
+ };
95
+ }
96
+ async getInternalResources() {
97
+ return this.resources.map(r => ({ ...r, baseUrl: process.env.BASE_URL }));
98
+ }
99
+ async getExternalResources() {
100
+ const results = await this.db.all('SELECT * FROM resource');
101
+ if (!results) {
102
+ return [];
103
+ }
104
+ return Promise.all(results.map(async (r) => ({
105
+ id: r.id,
106
+ path: r.path,
107
+ name: r.name,
108
+ baseUrl: r.base_url,
109
+ metadata: { baseUrl: r.base_url },
110
+ functions: await this.getResourceFunctions(r.id),
111
+ })));
112
+ }
113
+ async getResourceFunction(resourcePath, resourceName, functionName) {
114
+ const resource = await this.db.get('SELECT * FROM resource WHERE path = ? AND name = ?', [resourcePath, resourceName]);
115
+ if (!resource) {
116
+ return undefined;
117
+ }
118
+ const fn = await this.db.get('SELECT * FROM resource_function WHERE resource_id = ? AND name = ?', [resource.id, functionName]);
119
+ if (!fn) {
120
+ return undefined;
121
+ }
122
+ return {
123
+ id: fn.id,
124
+ name: fn.name,
125
+ returnType: fn.return_type,
126
+ parameters: await this.getResourceParameters(fn.id),
127
+ exec: async () => undefined,
128
+ paymentManifest: await this.getResourceFunctionPaymentManifest(fn.id),
129
+ };
130
+ }
131
+ async getResourceFunctions(resourceId) {
132
+ const rows = await this.db.all('SELECT * FROM resource_function WHERE resource_id = ?', [resourceId]);
133
+ return Promise.all(rows.map(async (f) => ({
134
+ id: f.id,
135
+ name: f.name,
136
+ returnType: f.return_type,
137
+ parameters: await this.getResourceParameters(f.id),
138
+ exec: async () => undefined,
139
+ paymentManifest: await this.getResourceFunctionPaymentManifest(f.id),
140
+ })));
141
+ }
142
+ async getResourceParameters(resourceFunctionId) {
143
+ return (await this.db.all('SELECT * FROM resource_function_parameter WHERE resource_function_id = ?', [resourceFunctionId])).map(p => ({
144
+ name: p.name,
145
+ type: p.parameter_type,
146
+ }));
147
+ }
148
+ async getResourceFunctionPaymentManifest(resourceFunctionId) {
149
+ const result = await this.db.get('SELECT * FROM resource_function_payment_manifest WHERE resource_function_id = ?', [resourceFunctionId]);
150
+ if (!result) {
151
+ return undefined;
152
+ }
153
+ return {
154
+ id: result.id,
155
+ costLamports: result.cost_lamports,
156
+ recipientAddress: result.recipient_address,
157
+ };
158
+ }
159
+ async getAllResources() {
160
+ const externalResources = await this.getExternalResources();
161
+ let resources = [];
162
+ for (const resource of Object.values(this.resources)) {
163
+ resources.push({ ...resource, baseUrl: process.env.BASE_URL });
164
+ }
165
+ resources.push(...externalResources);
166
+ return resources;
167
+ }
168
+ }
@@ -1,5 +1,5 @@
1
1
  import { Resource, ResourceResolver } from "requestscript";
2
- import { ResourceRepository } from "./resource.js";
2
+ import { ResourceRepository } from "./repository.js";
3
3
  /**
4
4
  * CombinationResourceResolver is a resource resolver that combines multiple resource resolvers.
5
5
  * It will first try to resolve the resource from the resource repository.
@@ -1 +1 @@
1
- {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/resource/resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnD;;;;;GAKG;AACH,qBAAa,2BAA4B,YAAW,gBAAgB;IACpD,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IAA/C,YAA6B,kBAAkB,EAAE,kBAAkB,EAAI;IAEjE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAExD;CACJ"}
1
+ {"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/resource/resolver.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAErD;;;;;GAKG;AACH,qBAAa,2BAA4B,YAAW,gBAAgB;IACpD,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IAA/C,YAA6B,kBAAkB,EAAE,kBAAkB,EAAI;IAEjE,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAExD;CACJ"}
@@ -1 +1 @@
1
- {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AACjD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAI5C,UAAU,YAAY;IAClB,OAAO,EAAE,OAAO,CAAC;CACpB;AAED;;;;GAIG;AACH,QAAA,MAAM,MAAM,EAAE,kBAAkB,CAAC,YAAY,CA2D5C,CAAA;eAGc,MAAM"}
1
+ {"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAKlD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAG5C,UAAU,YAAY;IAClB,OAAO,EAAE,OAAO,CAAC;CACpB;AA+BD;;;;GAIG;AACH,QAAA,MAAM,MAAM,EAAE,kBAAkB,CAAC,YAAY,CAyH5C,CAAC;eAGa,MAAM"}
package/dist/routes.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import { interpretParsed, parseScript } from 'requestscript';
2
+ import { verifyData } from './crypto/utils.js';
2
3
  /**
3
4
  * Encapsulates the routes
4
5
  * @param fastify Encapsulated Fastify Instance
5
6
  * @param options plugin options, refer to https://fastify.dev/docs/latest/Reference/Plugins/#plugin-options
6
7
  */
7
- const routes = async function (fastify, options) {
8
+ const routes = async (fastify, options) => {
8
9
  // Start Meta Routes
9
10
  fastify.get('/', async (request, reply) => {
10
11
  return { hello: 'world' };
@@ -13,6 +14,11 @@ const routes = async function (fastify, options) {
13
14
  const peers = await options.service.peerRepository.getPeerList();
14
15
  return { peers };
15
16
  });
17
+ fastify.put('/peers', async (request, reply) => {
18
+ const peer = request.body;
19
+ await options.service.peerRepository.createIfNotExists(peer, peer.signature, peer.expiry);
20
+ return { success: true };
21
+ });
16
22
  /**
17
23
  * Returns the list of resources that are available on the server.
18
24
  * Does not return resources that are external to the server.
@@ -27,17 +33,34 @@ const routes = async function (fastify, options) {
27
33
  name: fn.name,
28
34
  returnType: fn.returnType,
29
35
  parameters: fn.parameters,
36
+ paymentManifest: fn.paymentManifest ? ({
37
+ costLamports: fn.paymentManifest.costLamports,
38
+ recipientAddress: fn.paymentManifest.recipientAddress,
39
+ }) : undefined,
30
40
  })),
31
41
  baseUrl: resource.baseUrl,
32
42
  })),
33
43
  };
34
44
  });
45
+ fastify.put('/all-resources', async (request, reply) => {
46
+ const resourceRequest = request.body;
47
+ const resources = resourceRequest.resources;
48
+ const existingPeer = await options.service.peerRepository.getPeerByPublicKey(resourceRequest.publicKey);
49
+ if (!existingPeer) {
50
+ return reply.status(400).send({ error: 'Peer not found' });
51
+ }
52
+ const success = await options.service.resourceRepository.saveAll(resources, existingPeer, resourceRequest.signature, resourceRequest.expiry);
53
+ if (!success) {
54
+ return reply.status(400).send({ error: 'Invalid signature' });
55
+ }
56
+ return { success: true };
57
+ });
35
58
  // End Meta Routes
36
59
  // Start Run Routes
37
60
  fastify.post('/run', async (request, reply) => {
38
- const source = request.body;
61
+ const runRequest = request.body;
39
62
  try {
40
- const script = parseScript(source);
63
+ const script = parseScript(runRequest.source);
41
64
  if (script.declaration.kind !== 'request') {
42
65
  return reply.status(400).send({ error: 'Invalid script' });
43
66
  }
@@ -51,6 +74,33 @@ const routes = async function (fastify, options) {
51
74
  reply.status(500).send({ error: 'Internal server error' });
52
75
  }
53
76
  });
77
+ fastify.post('/run/resource', async (request, reply) => {
78
+ const resourceRequest = request.body;
79
+ const paymentVoucher = resourceRequest.paymentVoucher;
80
+ if (paymentVoucher) {
81
+ const isValid = verifyData(paymentVoucher, paymentVoucher.publicKey, paymentVoucher.signature);
82
+ if (!isValid) {
83
+ return reply.status(400).send({ error: 'Invalid payment voucher' });
84
+ }
85
+ }
86
+ const resource = await options.service.resourceRepository.getInternalResource(resourceRequest.resourcePath, resourceRequest.resourceName);
87
+ if (!resource) {
88
+ return reply.status(400).send({ error: 'Resource not found' });
89
+ }
90
+ const result = await options.service.resourceInvoker.invoke(resource, resourceRequest.functionName, resourceRequest.parameters);
91
+ // A voucher is optional. Settlement runs only after a successful invoke. Run this on a separate thread to avoid blocking the main thread.
92
+ if (paymentVoucher) {
93
+ setTimeout(async () => {
94
+ await options.service.operatorClient.settlePayment({
95
+ voucher: paymentVoucher,
96
+ resourcePath: resourceRequest.resourcePath,
97
+ resourceName: resourceRequest.resourceName,
98
+ parameters: resourceRequest.parameters,
99
+ });
100
+ }, 0);
101
+ }
102
+ return { returnValue: result };
103
+ });
54
104
  // End Run Routes
55
105
  };
56
106
  // ESM
package/dist/server.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import 'dotenv/config';
2
2
  import { FastifyInstance } from 'fastify';
3
- import { Resource } from 'requestscript';
3
+ import { StellariumResource } from './resource/repository.js';
4
4
  export interface StellariumNodeOptions {
5
5
  port?: number;
6
6
  startingPeer?: string;
7
7
  }
8
8
  export declare class StellariumNode {
9
9
  private resources;
10
- register(resource: Resource): Promise<void>;
10
+ register(resource: StellariumResource): Promise<void>;
11
11
  start(fastify: FastifyInstance, options: StellariumNodeOptions): Promise<void>;
12
12
  }
13
13
  //# sourceMappingURL=server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,eAAe,CAAC;AACvB,OAAgB,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAInD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAGzC,MAAM,WAAW,qBAAqB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,cAAc;IACvB,OAAO,CAAC,SAAS,CAAkB;IAE7B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhD;IAEK,KAAK,CAAC,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBnF;CACJ"}
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,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAK9D,MAAM,WAAW,qBAAqB;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,qBAAa,cAAc;IACvB,OAAO,CAAC,SAAS,CAA4B;IAEvC,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1D;IAEK,KAAK,CAAC,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAgCnF;CACJ"}
package/dist/server.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import 'dotenv/config';
2
+ import { createTables } from './db/db.js';
2
3
  import routes from './routes.js';
3
4
  import { createService } from './service.js';
4
5
  import { StartupService } from './startup.js';
5
- import { createTables } from './db/db.js';
6
6
  export class StellariumNode {
7
7
  resources = [];
8
8
  async register(resource) {
@@ -20,8 +20,8 @@ export class StellariumNode {
20
20
  process.exit(1);
21
21
  }
22
22
  if (options.startingPeer) {
23
- const startupService = new StartupService(service, options.startingPeer);
24
- Promise.resolve(startupService.loadFromPeer()).catch(err => {
23
+ const startupService = new StartupService(fastify.log, service, options.startingPeer, process.env.BASE_URL, process.env.SIGNING_PUBLIC_KEY, process.env.SIGNING_PRIVATE_KEY);
24
+ Promise.resolve(startupService.seedFromPeer()).catch(err => {
25
25
  fastify.log.error(err);
26
26
  process.exit(1);
27
27
  });
package/dist/service.d.ts CHANGED
@@ -1,11 +1,13 @@
1
- import { Resource, ResourceInvoker, ResourceResolver } from "requestscript";
1
+ import { ResourceInvoker, ResourceResolver } from "requestscript";
2
2
  import { PeerRepository } from "./peer/repository.js";
3
- import { ResourceRepository } from "./resource/resource.js";
3
+ import { ResourceRepository, type StellariumResource } from "./resource/repository.js";
4
+ import { OperatorClient } from "./client/operatorClient.js";
4
5
  export interface Service {
5
6
  peerRepository: PeerRepository;
6
7
  resourceRepository: ResourceRepository;
7
8
  resourceResolver: ResourceResolver;
8
9
  resourceInvoker: ResourceInvoker;
10
+ operatorClient: OperatorClient;
9
11
  }
10
- export declare function createService(resources: Resource[]): Service;
12
+ export declare function createService(resources: StellariumResource[]): Service;
11
13
  //# sourceMappingURL=service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAE5E,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAG5D,MAAM,WAAW,OAAO;IACpB,cAAc,EAAE,cAAc,CAAC;IAC/B,kBAAkB,EAAE,kBAAkB,CAAC;IACvC,gBAAgB,EAAE,gBAAgB,CAAC;IACnC,eAAe,EAAE,eAAe,CAAC;CACpC;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,OAAO,CAW5D"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,eAAe,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAE5E,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,MAAM,WAAW,OAAO;IACpB,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;CAClC;AAED,wBAAgB,aAAa,CAAC,SAAS,EAAE,kBAAkB,EAAE,GAAG,OAAO,CAiBtE"}
package/dist/service.js CHANGED
@@ -1,16 +1,20 @@
1
1
  import db from "./db/db.js";
2
2
  import { PeerRepository } from "./peer/repository.js";
3
3
  import { CombinationResourceResolver } from "./resource/resolver.js";
4
- import { ResourceRepository } from "./resource/resource.js";
4
+ import { ResourceRepository } from "./resource/repository.js";
5
5
  import { CombinationResourceInvoker } from "./resource/invoker.js";
6
+ import { OperatorClient } from "./client/operatorClient.js";
6
7
  export function createService(resources) {
7
8
  const resourceRepository = new ResourceRepository(db, resources);
8
9
  const resourceResolver = new CombinationResourceResolver(resourceRepository);
9
- const resourceInvoker = new CombinationResourceInvoker();
10
+ // TODO: Replace with actual values.
11
+ const resourceInvoker = new CombinationResourceInvoker(resourceRepository, 10000, "TODO", "TODO");
12
+ const operatorClient = new OperatorClient(process.env.OPERATOR_URL);
10
13
  return {
11
14
  peerRepository: new PeerRepository(db),
12
15
  resourceRepository,
13
16
  resourceResolver,
14
17
  resourceInvoker,
18
+ operatorClient,
15
19
  };
16
20
  }
package/dist/startup.d.ts CHANGED
@@ -1,10 +1,16 @@
1
+ import { FastifyBaseLogger } from "fastify";
1
2
  import { Service } from "./service.js";
2
3
  export declare class StartupService {
4
+ private readonly log;
3
5
  private readonly service;
4
6
  private readonly startingPeer;
5
- constructor(service: Service, startingPeer: string);
6
- loadFromPeer(): Promise<void>;
7
+ private readonly baseUrl;
8
+ private readonly publicKey;
9
+ private readonly privateKey;
10
+ constructor(log: FastifyBaseLogger, service: Service, startingPeer: string, baseUrl: string, publicKey: string, privateKey: string);
11
+ seedFromPeer(): Promise<void>;
7
12
  populatePeers(): Promise<void>;
8
13
  populateResources(): Promise<void>;
14
+ broadcastSelf(): Promise<void>;
9
15
  }
10
16
  //# sourceMappingURL=startup.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"startup.d.ts","sourceRoot":"","sources":["../src/startup.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,qBAAa,cAAc;IACX,OAAO,CAAC,QAAQ,CAAC,OAAO;IAAW,OAAO,CAAC,QAAQ,CAAC,YAAY;IAA5E,YAA6B,OAAO,EAAE,OAAO,EAAmB,YAAY,EAAE,MAAM,EAAI;IAGlF,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAGlC;IAEK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAiBnC;IAEK,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAYvC;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,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;IAEK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAqBnC;IAEK,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC,CAgBvC;IAEK,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC,CAqEnC;CACJ"}
package/dist/startup.js CHANGED
@@ -1,16 +1,30 @@
1
+ import { signData } from "./crypto/utils.js";
1
2
  export class StartupService {
3
+ log;
2
4
  service;
3
5
  startingPeer;
4
- constructor(service, startingPeer) {
6
+ baseUrl;
7
+ publicKey;
8
+ privateKey;
9
+ constructor(log, service, startingPeer, baseUrl, publicKey, privateKey) {
10
+ this.log = log;
5
11
  this.service = service;
6
12
  this.startingPeer = startingPeer;
13
+ this.baseUrl = baseUrl;
14
+ this.publicKey = publicKey;
15
+ this.privateKey = privateKey;
7
16
  }
8
17
  // If the peer list is empty, populate it using the peer start parameter.
9
- async loadFromPeer() {
18
+ async seedFromPeer() {
19
+ this.log.info(`Seeding from peer ${this.startingPeer}`);
10
20
  await this.populatePeers();
11
21
  await this.populateResources();
22
+ // This must be the last step that only runs if the previous steps were successful.
23
+ await this.broadcastSelf();
24
+ this.log.info(`Finished seeding from peer ${this.startingPeer}`);
12
25
  }
13
26
  async populatePeers() {
27
+ this.log.info(`Populating peers from ${this.startingPeer}`);
14
28
  const peerList = await this.service.peerRepository.getPeerList();
15
29
  if (peerList.length > 0) {
16
30
  return;
@@ -25,8 +39,10 @@ export class StartupService {
25
39
  await this.service.peerRepository.create(peer);
26
40
  }
27
41
  }
42
+ this.log.info(`Populated ${data.peers.length} peers`);
28
43
  }
29
44
  async populateResources() {
45
+ this.log.info(`Populating resources from ${this.startingPeer}`);
30
46
  const response = await fetch(`${this.startingPeer}/v1/resources`);
31
47
  const data = await response.json();
32
48
  if (!data.resources) {
@@ -34,8 +50,66 @@ export class StartupService {
34
50
  }
35
51
  if (data.resources) {
36
52
  for (const resource of data.resources) {
37
- await this.service.resourceRepository.create(resource, resource.baseUrl);
53
+ await this.service.resourceRepository.create(resource);
38
54
  }
39
55
  }
56
+ this.log.info(`Populated ${data.resources.length} resources`);
57
+ }
58
+ async broadcastSelf() {
59
+ this.log.info(`Broadcasting self to peers`);
60
+ // Let the peers know about this node.
61
+ const peers = await this.service.peerRepository.getPeerList();
62
+ // 10 minutes
63
+ const peerSignatureExpiry = new Date(Date.now() + 1000 * 60 * 10).toISOString();
64
+ const peerSignature = signData({
65
+ baseUrl: this.baseUrl,
66
+ name: this.baseUrl,
67
+ expiry: peerSignatureExpiry,
68
+ }, this.privateKey);
69
+ for (const peer of peers) {
70
+ const response = await fetch(`${peer.baseUrl}/v1/peers`, {
71
+ method: 'PUT',
72
+ headers: {
73
+ 'Content-Type': 'application/json',
74
+ },
75
+ body: JSON.stringify({
76
+ baseUrl: this.baseUrl,
77
+ name: this.baseUrl,
78
+ publicKey: this.publicKey,
79
+ signature: peerSignature,
80
+ expiry: peerSignatureExpiry,
81
+ }),
82
+ });
83
+ if (!response.ok) {
84
+ this.log.warn(`Failed to broadcast self to node ${peer.baseUrl}: ${response.statusText}`);
85
+ }
86
+ }
87
+ this.log.info(`Broadcasted self to ${peers.length} peers`);
88
+ this.log.info(`Broadcasting resources to peers`);
89
+ // Let the peers know about the resources available on this node.
90
+ const resources = await this.service.resourceRepository.getInternalResources();
91
+ // 10 minutes
92
+ const resourceSignatureExpiry = new Date(Date.now() + 1000 * 60 * 10).toISOString();
93
+ const resourceSignature = signData({
94
+ resources,
95
+ expiry: resourceSignatureExpiry,
96
+ }, this.privateKey);
97
+ for (const peer of peers) {
98
+ const response = await fetch(`${peer.baseUrl}/v1/all-resources`, {
99
+ method: 'PUT',
100
+ headers: {
101
+ 'Content-Type': 'application/json',
102
+ },
103
+ body: JSON.stringify({
104
+ resources,
105
+ expiry: resourceSignatureExpiry,
106
+ signature: resourceSignature,
107
+ }),
108
+ });
109
+ if (!response.ok) {
110
+ this.log.warn(`Failed to broadcast resources to node ${peer.baseUrl}: ${response.statusText}`);
111
+ }
112
+ }
113
+ this.log.info(`Broadcasted ${resources.length} resources to ${peers.length} peers`);
40
114
  }
41
115
  }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=utils.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.test.d.ts","sourceRoot":"","sources":["../../../src/tests/crypto/utils.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,31 @@
1
+ import { generateKeyPairSync } from "node:crypto";
2
+ import { signData, verifyData } from "../../crypto/utils.js";
3
+ import { describe, expect, test } from "vitest";
4
+ function generateBase64KeyPair() {
5
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
6
+ publicKeyEncoding: {
7
+ type: "spki",
8
+ format: "der",
9
+ },
10
+ privateKeyEncoding: {
11
+ type: "pkcs8",
12
+ format: "der",
13
+ },
14
+ });
15
+ return {
16
+ publicKey: publicKey.toString("base64url"),
17
+ privateKey: privateKey.toString("base64url"),
18
+ };
19
+ }
20
+ describe('crypto', () => {
21
+ describe('signData and verifyData', () => {
22
+ test('should sign and verify data', () => {
23
+ const data = {
24
+ message: 'Hello, world!',
25
+ };
26
+ const { privateKey, publicKey } = generateBase64KeyPair();
27
+ const signature = signData(data, privateKey);
28
+ expect(verifyData(data, publicKey, signature)).toBe(true);
29
+ });
30
+ });
31
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stellarium-ts",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A Stellarium Node in TypeScript.",
5
5
  "homepage": "https://github.com/qualletio/stellarium-ts#readme",
6
6
  "bugs": {
@@ -34,8 +34,12 @@
34
34
  "author": "Christopher Poulsen <christopher.poulsen@quallet.io>",
35
35
  "license": "ISC",
36
36
  "devDependencies": {
37
+ "@biomejs/biome": "^2.5.15",
37
38
  "@types/node": "^26.6.4",
38
- "typescript": "^7.0.2"
39
+ "turbo": "^2.11.7",
40
+ "typescript": "^7.0.2",
41
+ "vite": "^8.3.3",
42
+ "vitest": "^4.1.11"
39
43
  },
40
44
  "dependencies": {
41
45
  "dotenv": "^18.0.5",
@@ -48,6 +52,8 @@
48
52
  "build": "rm -rf dist && tsc",
49
53
  "start": "node dist/server.js",
50
54
  "dev": "node --watch src/server.ts",
51
- "test": "echo \"Error: no test specified\" && exit 1"
55
+ "test": "vitest --passWithNoTests --exclude 'dist/**'",
56
+ "lint": "biome lint src",
57
+ "check-types": "tsc --noEmit"
52
58
  }
53
59
  }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # stellarium-ts
2
-
3
- Typescript implementation of a Stellarium node
@@ -1,16 +0,0 @@
1
- import { Resource, ResourceFunctionParameter } from "requestscript";
2
- import { Database } from "sqlite";
3
- export interface ResourceWithBaseUrl extends Resource {
4
- baseUrl: string;
5
- }
6
- export declare class ResourceRepository {
7
- private readonly db;
8
- private resources;
9
- constructor(db: Database, resources?: Resource[]);
10
- create(resource: Resource, baseUrl: string): Promise<void>;
11
- getResource(key: string): Promise<Resource | undefined>;
12
- getExternalResources(): Promise<Resource[]>;
13
- getResourceParameters(resourceFunctionId: number): Promise<ResourceFunctionParameter[]>;
14
- getAllResources(): Promise<ResourceWithBaseUrl[]>;
15
- }
16
- //# sourceMappingURL=resource.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"resource.d.ts","sourceRoot":"","sources":["../../src/resource/resource.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAElC,MAAM,WAAW,mBAAoB,SAAQ,QAAQ;IACjD,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,kBAAkB;IACf,OAAO,CAAC,QAAQ,CAAC,EAAE;IAAY,OAAO,CAAC,SAAS;IAA5D,YAA6B,EAAE,EAAE,QAAQ,EAAU,SAAS,GAAE,QAAQ,EAAO,EAAI;IAE3E,MAAM,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM/D;IAEK,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,CAyC5D;IAEK,oBAAoB,IAAI,OAAO,CAAC,QAAQ,EAAE,CAAC,CAqBhD;IAEK,qBAAqB,CAAC,kBAAkB,EAAE,MAAM,GAAG,OAAO,CAAC,yBAAyB,EAAE,CAAC,CAK5F;IAEK,eAAe,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC,CAWtD;CACJ"}