stellarium-ts 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/operatorClient.d.ts +36 -1
- package/dist/client/operatorClient.d.ts.map +1 -1
- package/dist/client/operatorClient.js +30 -5
- package/dist/client/runTokens.d.ts +23 -0
- package/dist/client/runTokens.d.ts.map +1 -0
- package/dist/client/runTokens.js +34 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/resource/inference.d.ts +2 -0
- package/dist/resource/inference.d.ts.map +1 -1
- package/dist/resource/inference.js +10 -0
- package/dist/resource/invoker.d.ts +37 -3
- package/dist/resource/invoker.d.ts.map +1 -1
- package/dist/resource/invoker.js +91 -24
- package/dist/resource/settlement.d.ts +25 -0
- package/dist/resource/settlement.d.ts.map +1 -0
- package/dist/resource/settlement.js +59 -0
- package/dist/routes.d.ts +16 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +84 -66
- package/dist/server.d.ts +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +13 -1
- package/dist/service.d.ts +11 -4
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +6 -1
- package/dist/tests/integration/network.test.js +18 -14
- package/dist/tests/resource/invoker.test.js +19 -0
- package/dist/tests/routes.test.js +94 -2
- package/package.json +3 -2
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
1
2
|
import Fastify from "fastify";
|
|
3
|
+
import { createLocalJWKSet, exportJWK, SignJWT } from "jose";
|
|
2
4
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
3
|
-
import { signData } from "../crypto/utils.js";
|
|
5
|
+
import { safeVerifyData, signData } from "../crypto/utils.js";
|
|
4
6
|
import { openDatabase } from "../db/db.js";
|
|
5
7
|
import { grantRenewalPayload, grantRequestPayload } from "../grant/grant.js";
|
|
6
8
|
import { GrantRepository } from "../grant/repository.js";
|
|
@@ -184,7 +186,7 @@ describe("routes", () => {
|
|
|
184
186
|
const response = await run("Bearer secret");
|
|
185
187
|
expect(response.statusCode).toBe(200);
|
|
186
188
|
// RequestScript resource calls evaluate to the invoker's { value, error } result.
|
|
187
|
-
expect(response.json()).toEqual({ returnValue: { value: 12, error: null } });
|
|
189
|
+
expect(response.json()).toEqual({ usage: { tokens: expect.any(Number) }, returnValue: { value: 12, error: null } });
|
|
188
190
|
});
|
|
189
191
|
test("requires an Authorization header", async () => {
|
|
190
192
|
const response = await run();
|
|
@@ -219,6 +221,96 @@ describe("routes", () => {
|
|
|
219
221
|
expect(response.statusCode).toBe(400);
|
|
220
222
|
expect(response.json()).toMatchObject({ error: "Invalid script" });
|
|
221
223
|
});
|
|
224
|
+
test("answers 422 for a script that fails while running", async () => {
|
|
225
|
+
const response = await run("Bearer secret", { source: "request R {\n return 1 / 0\n}" });
|
|
226
|
+
expect(response.statusCode).toBe(422);
|
|
227
|
+
expect(response.json()).toMatchObject({ error: "Script failed", details: expect.stringContaining("Division by zero"), usage: { tokens: 4 } });
|
|
228
|
+
});
|
|
229
|
+
describe("with run tokens", () => {
|
|
230
|
+
const operatorKeys = generateKeyPairSync("ed25519");
|
|
231
|
+
let tokenService;
|
|
232
|
+
let tokenApp;
|
|
233
|
+
let authorizeRun;
|
|
234
|
+
let captureRun;
|
|
235
|
+
let releaseRun;
|
|
236
|
+
beforeEach(async () => {
|
|
237
|
+
const jwk = await exportJWK(operatorKeys.publicKey);
|
|
238
|
+
tokenService = createService(db, [weather], config, { runTokenKeys: createLocalJWKSet({ keys: [{ ...jwk, alg: "EdDSA" }] }) });
|
|
239
|
+
authorizeRun = vi.spyOn(tokenService.operatorClient, "authorizeRun").mockResolvedValue({ ok: true, heldMicroUsd: 50_000, maxTokens: 9 });
|
|
240
|
+
captureRun = vi.spyOn(tokenService.operatorClient, "captureRun").mockResolvedValue(undefined);
|
|
241
|
+
releaseRun = vi.spyOn(tokenService.operatorClient, "releaseRun").mockResolvedValue(undefined);
|
|
242
|
+
vi.spyOn(tokenService.operatorClient, "authorize").mockResolvedValue({ ok: true });
|
|
243
|
+
vi.spyOn(tokenService.operatorClient, "capture").mockResolvedValue(undefined);
|
|
244
|
+
tokenApp = Fastify();
|
|
245
|
+
await tokenApp.register(routes, { prefix: "/v1", service: tokenService });
|
|
246
|
+
await tokenApp.ready();
|
|
247
|
+
});
|
|
248
|
+
afterEach(async () => {
|
|
249
|
+
await tokenApp.close();
|
|
250
|
+
});
|
|
251
|
+
/** A token the operator would issue, for `organizationId`'s client on this node unless `audience` says otherwise. */
|
|
252
|
+
const runToken = (organizationId, options = {}) => new SignJWT({ org: organizationId })
|
|
253
|
+
.setProtectedHeader({ alg: "EdDSA" })
|
|
254
|
+
.setIssuer("stellarium")
|
|
255
|
+
.setSubject("cli_test")
|
|
256
|
+
.setAudience(options.audience ?? config.publicKey)
|
|
257
|
+
.setIssuedAt()
|
|
258
|
+
.setExpirationTime(options.expiresIn ?? "15m")
|
|
259
|
+
.sign(options.key ?? operatorKeys.privateKey);
|
|
260
|
+
const runWith = async (token, source = temperatureSource) => tokenApp.inject({
|
|
261
|
+
method: "POST", url: "/v1/run", headers: { authorization: `Bearer ${token}` }, payload: { source },
|
|
262
|
+
});
|
|
263
|
+
test("bills another organization's run, and charges its call to this node's paid function to the run", async () => {
|
|
264
|
+
const token = await runToken("org_caller");
|
|
265
|
+
const response = await runWith(token);
|
|
266
|
+
expect(response.statusCode).toBe(200);
|
|
267
|
+
expect(response.json()).toEqual({ returnValue: { value: 12, error: null }, usage: { tokens: 17 } });
|
|
268
|
+
const runId = authorizeRun.mock.calls[0][0].runId;
|
|
269
|
+
expect(authorizeRun).toHaveBeenCalledWith({ token, runId: expect.any(String), source: temperatureSource });
|
|
270
|
+
// The local paid function signed a voucher for the run, paying this node's organization.
|
|
271
|
+
const [voucher] = tokenService.operatorClient.authorize.mock.calls[0];
|
|
272
|
+
expect(voucher).toMatchObject({ runId, recipientOrganizationId: ORGANIZATION, amount: COST, publicKey: config.publicKey });
|
|
273
|
+
expect(safeVerifyData(paymentVoucherPayload(voucher), voucher.publicKey, voucher.signature)).toBe(true);
|
|
274
|
+
await vi.waitFor(() => expect(captureRun).toHaveBeenCalledWith({ runId, tokens: 17 }));
|
|
275
|
+
});
|
|
276
|
+
test("runs this node's own organization's tokens free, without the operator", async () => {
|
|
277
|
+
const response = await runWith(await runToken(ORGANIZATION));
|
|
278
|
+
expect(response.json()).toEqual({ returnValue: { value: 12, error: null }, usage: { tokens: 17 } });
|
|
279
|
+
expect(authorizeRun).not.toHaveBeenCalled();
|
|
280
|
+
expect(tokenService.operatorClient.authorize).not.toHaveBeenCalled();
|
|
281
|
+
});
|
|
282
|
+
test("answers the operator's refusal without running the script", async () => {
|
|
283
|
+
authorizeRun.mockResolvedValue({ ok: false, status: 402, error: "Insufficient balance" });
|
|
284
|
+
const response = await runWith(await runToken("org_caller"));
|
|
285
|
+
expect(response.statusCode).toBe(402);
|
|
286
|
+
expect(response.json()).toEqual({ error: "Insufficient balance" });
|
|
287
|
+
expect(tokenService.operatorClient.authorize).not.toHaveBeenCalled();
|
|
288
|
+
});
|
|
289
|
+
test("charges a script that fails for what it executed, and releases a run the node failed", async () => {
|
|
290
|
+
const failing = await runWith(await runToken("org_caller"), "request R {\n return 1 / 0\n}");
|
|
291
|
+
expect(failing.statusCode).toBe(422);
|
|
292
|
+
await vi.waitFor(() => expect(captureRun).toHaveBeenCalledWith({ runId: expect.any(String), tokens: 4 }));
|
|
293
|
+
vi.spyOn(tokenService.resourceResolver, "resolve").mockRejectedValue(new Error("database is gone"));
|
|
294
|
+
const broken = await runWith(await runToken("org_caller"));
|
|
295
|
+
expect(broken.statusCode).toBe(500);
|
|
296
|
+
await vi.waitFor(() => expect(releaseRun).toHaveBeenCalledTimes(1));
|
|
297
|
+
});
|
|
298
|
+
test.each([
|
|
299
|
+
["addressed to another node", () => runToken("org_caller", { audience: generateBase64KeyPair().publicKey })],
|
|
300
|
+
["signed by another key", () => runToken("org_caller", { key: generateKeyPairSync("ed25519").privateKey })],
|
|
301
|
+
["expired", () => runToken("org_caller", { expiresIn: "-1m" })],
|
|
302
|
+
])("refuses a token %s", async (_, token) => {
|
|
303
|
+
const response = await runWith(await token());
|
|
304
|
+
expect(response.statusCode).toBe(401);
|
|
305
|
+
expect(response.json()).toEqual({ error: "Invalid run token" });
|
|
306
|
+
expect(authorizeRun).not.toHaveBeenCalled();
|
|
307
|
+
});
|
|
308
|
+
test("still accepts the API key", async () => {
|
|
309
|
+
const response = await tokenApp.inject({ method: "POST", url: "/v1/run", headers: { authorization: "Bearer secret" }, payload: { source: temperatureSource } });
|
|
310
|
+
expect(response.statusCode).toBe(200);
|
|
311
|
+
expect(authorizeRun).not.toHaveBeenCalled();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
222
314
|
});
|
|
223
315
|
describe("GET /v1/self", () => {
|
|
224
316
|
test("describes this node from its config", async () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stellarium-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "A Stellarium Node in TypeScript.",
|
|
5
5
|
"homepage": "https://github.com/qualletio/stellarium-ts#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -45,7 +45,8 @@
|
|
|
45
45
|
"@fastify/rate-limit": "^11.2.1",
|
|
46
46
|
"dotenv": "^18.0.5",
|
|
47
47
|
"fastify": "^5.12.5",
|
|
48
|
-
"
|
|
48
|
+
"jose": "6.1.0",
|
|
49
|
+
"requestscript": "1.2.0",
|
|
49
50
|
"sqlite": "^5.1.1",
|
|
50
51
|
"sqlite3": "^6.0.1"
|
|
51
52
|
},
|