stellarium-ts 0.19.1 → 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.
- package/dist/client/operatorClient.d.ts +34 -3
- package/dist/client/operatorClient.d.ts.map +1 -1
- package/dist/client/operatorClient.js +15 -0
- package/dist/contract/repository.d.ts +49 -0
- package/dist/contract/repository.d.ts.map +1 -0
- package/dist/contract/repository.js +80 -0
- package/dist/contract/service.d.ts +103 -0
- package/dist/contract/service.d.ts.map +1 -0
- package/dist/contract/service.js +297 -0
- package/dist/contract/state.d.ts +43 -0
- package/dist/contract/state.d.ts.map +1 -0
- package/dist/contract/state.js +79 -0
- package/dist/db/database.d.ts +25 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/database.js +1 -0
- package/dist/db/db.d.ts +12 -4
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/db.js +96 -28
- package/dist/db/postgres.d.ts +39 -0
- package/dist/db/postgres.d.ts.map +1 -0
- package/dist/db/postgres.js +132 -0
- package/dist/db/sqlite.d.ts +16 -0
- package/dist/db/sqlite.d.ts.map +1 -0
- package/dist/db/sqlite.js +53 -0
- package/dist/grant/repository.d.ts +2 -2
- package/dist/grant/repository.d.ts.map +1 -1
- package/dist/grant/repository.js +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/peer/repository.d.ts +2 -2
- package/dist/peer/repository.d.ts.map +1 -1
- package/dist/peer/repository.js +1 -1
- package/dist/resource/invoker.d.ts +85 -0
- package/dist/resource/invoker.d.ts.map +1 -1
- package/dist/resource/invoker.js +69 -6
- package/dist/resource/repository.d.ts +51 -10
- package/dist/resource/repository.d.ts.map +1 -1
- package/dist/resource/repository.js +116 -61
- package/dist/resource/settlement.d.ts +5 -0
- package/dist/resource/settlement.d.ts.map +1 -1
- package/dist/resource/settlement.js +19 -0
- package/dist/routes.d.ts.map +1 -1
- package/dist/routes.js +161 -25
- package/dist/server.d.ts +29 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +80 -6
- package/dist/service.d.ts +26 -3
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +16 -1
- package/dist/startup.d.ts +2 -2
- package/dist/startup.d.ts.map +1 -1
- package/dist/startup.js +43 -34
- package/dist/tests/contract/repository.test.d.ts +2 -0
- package/dist/tests/contract/repository.test.d.ts.map +1 -0
- package/dist/tests/contract/repository.test.js +68 -0
- package/dist/tests/contract/routes.test.d.ts +2 -0
- package/dist/tests/contract/routes.test.d.ts.map +1 -0
- package/dist/tests/contract/routes.test.js +252 -0
- package/dist/tests/contract/state-routes.test.d.ts +2 -0
- package/dist/tests/contract/state-routes.test.d.ts.map +1 -0
- package/dist/tests/contract/state-routes.test.js +158 -0
- package/dist/tests/contract/state.test.d.ts +2 -0
- package/dist/tests/contract/state.test.d.ts.map +1 -0
- package/dist/tests/contract/state.test.js +57 -0
- package/dist/tests/db/db.test.js +1 -1
- package/dist/tests/db/postgres.test.d.ts +2 -0
- package/dist/tests/db/postgres.test.d.ts.map +1 -0
- package/dist/tests/db/postgres.test.js +43 -0
- package/dist/tests/grant/service.test.js +3 -4
- package/dist/tests/helpers.d.ts +8 -0
- package/dist/tests/helpers.d.ts.map +1 -1
- package/dist/tests/helpers.js +37 -0
- package/dist/tests/integration/network.test.js +74 -4
- package/dist/tests/peer/repository.test.js +3 -4
- package/dist/tests/resource/repository.test.js +3 -4
- package/dist/tests/routes.test.js +10 -11
- package/dist/tests/server.test.js +22 -0
- package/dist/tests/startup.test.js +4 -5
- package/package.json +5 -2
package/dist/tests/helpers.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { generateKeyPairSync } from "node:crypto";
|
|
2
|
+
import { PGlite } from "@electric-sql/pglite";
|
|
3
|
+
import { createTables, openDatabase } from "../db/db.js";
|
|
4
|
+
import { PostgresDatabase } from "../db/postgres.js";
|
|
2
5
|
export function generateBase64KeyPair() {
|
|
3
6
|
const { publicKey, privateKey } = generateKeyPairSync("ed25519", {
|
|
4
7
|
publicKeyEncoding: { type: "spki", format: "der" },
|
|
@@ -82,3 +85,37 @@ export function modelResource(baseUrl = "http://127.0.0.1:3002") {
|
|
|
82
85
|
};
|
|
83
86
|
}
|
|
84
87
|
export const temperatureSource = 'request GetTemperature {\n const weather: com.example.Weather\n\n return weather.temperature(city: "Oslo")\n}';
|
|
88
|
+
/** Both databases a node can use. Suites that touch the database run against each. */
|
|
89
|
+
export const DIALECTS = ["sqlite", "postgres"];
|
|
90
|
+
export function pgliteDriver(pg) {
|
|
91
|
+
return {
|
|
92
|
+
query: async (text, params) => {
|
|
93
|
+
const result = await pg.query(text, params);
|
|
94
|
+
return { rows: result.rows, rowCount: result.affectedRows ?? 0 };
|
|
95
|
+
},
|
|
96
|
+
exec: async (text) => {
|
|
97
|
+
await pg.exec(text);
|
|
98
|
+
},
|
|
99
|
+
transaction: (work) => {
|
|
100
|
+
if (!("transaction" in pg)) {
|
|
101
|
+
throw new Error("Transactions cannot be nested");
|
|
102
|
+
}
|
|
103
|
+
return pg.transaction(tx => work(pgliteDriver(tx)));
|
|
104
|
+
},
|
|
105
|
+
close: async () => {
|
|
106
|
+
if ("close" in pg) {
|
|
107
|
+
await pg.close();
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** An empty node database: SQLite in memory, or Postgres in memory through PGlite. */
|
|
113
|
+
export async function openTestDatabase(dialect = "sqlite") {
|
|
114
|
+
if (dialect === "sqlite") {
|
|
115
|
+
return openDatabase(":memory:");
|
|
116
|
+
}
|
|
117
|
+
// 64-bit integers come back as numbers, as node-postgres is configured to return them.
|
|
118
|
+
const db = await PostgresDatabase.open(pgliteDriver(new PGlite({ parsers: { 20: Number } })));
|
|
119
|
+
await createTables(db);
|
|
120
|
+
return db;
|
|
121
|
+
}
|
|
@@ -5,7 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import Fastify from "fastify";
|
|
6
6
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
7
7
|
import { verifyData } from "../../crypto/utils.js";
|
|
8
|
-
import { paymentVoucherPayload } from "../../resource/invoker.js";
|
|
8
|
+
import { contractVoucherPayload, paymentVoucherPayload } from "../../resource/invoker.js";
|
|
9
9
|
import { StellariumNode } from "../../server.js";
|
|
10
10
|
import { COST, generateBase64KeyPair, INFERENCE_RATES, modelResource, ORGANIZATION, temperatureSource, weatherResource } from "../helpers.js";
|
|
11
11
|
/** A free function only callers with a grant from the provider may run. */
|
|
@@ -22,6 +22,8 @@ function vaultResource(baseUrl) {
|
|
|
22
22
|
const vaultSource = 'request OpenVault {\n const vault: com.example.Vault\n\n return vault.open(city: "Oslo")\n}';
|
|
23
23
|
const vault = { resourcePath: "com.example", resourceName: "Vault" };
|
|
24
24
|
const HOUR = 3_600_000;
|
|
25
|
+
const CONTRACT_PRICE = 1_000;
|
|
26
|
+
const forecastContract = 'contract com.example.Forecast(city: string): string {\n const weather: com.example.Weather\n const temperature = weather.temperature(city: city)\n return "${temperature.value} degrees"\n}';
|
|
25
27
|
const completeSource = 'request Complete {\n const model: com.example.Model\n\n return model.complete(prompt: "Hello world!", maxTokens: 10)\n}';
|
|
26
28
|
function freePort() {
|
|
27
29
|
return new Promise((resolve, reject) => {
|
|
@@ -45,8 +47,12 @@ describe("two-node network", { timeout: 15_000 }, () => {
|
|
|
45
47
|
const authorizations = [];
|
|
46
48
|
const releases = [];
|
|
47
49
|
const sessions = [];
|
|
50
|
+
const contractAuthorizations = [];
|
|
51
|
+
const runCaptures = [];
|
|
48
52
|
/** What the fake operator answers to the next authorizations; undefined places the hold. */
|
|
49
53
|
let refuseAuthorization;
|
|
54
|
+
/** The provider's price, as its organization set it on the dashboard. */
|
|
55
|
+
let price = CONTRACT_PRICE;
|
|
50
56
|
const callerKeys = generateBase64KeyPair();
|
|
51
57
|
let dir;
|
|
52
58
|
let operatorUrl;
|
|
@@ -84,7 +90,10 @@ describe("two-node network", { timeout: 15_000 }, () => {
|
|
|
84
90
|
authorizations.length = 0;
|
|
85
91
|
releases.length = 0;
|
|
86
92
|
sessions.length = 0;
|
|
93
|
+
contractAuthorizations.length = 0;
|
|
94
|
+
runCaptures.length = 0;
|
|
87
95
|
refuseAuthorization = undefined;
|
|
96
|
+
price = CONTRACT_PRICE;
|
|
88
97
|
const operator = Fastify();
|
|
89
98
|
servers.push(operator);
|
|
90
99
|
operator.post("/v1/nodes/session", async (request, reply) => {
|
|
@@ -92,7 +101,15 @@ describe("two-node network", { timeout: 15_000 }, () => {
|
|
|
92
101
|
return reply.status(401).send({ error: "Unsigned request" });
|
|
93
102
|
}
|
|
94
103
|
sessions.push(request.body);
|
|
95
|
-
return { organizationId: ORGANIZATION };
|
|
104
|
+
return { organizationId: ORGANIZATION, priceMicroUsdPerMillionTokens: price };
|
|
105
|
+
});
|
|
106
|
+
operator.post("/v1/runs/contract/authorize", async (request) => {
|
|
107
|
+
contractAuthorizations.push(request.body);
|
|
108
|
+
return { status: "held", heldMicroUsd: 50_000, maxTokens: 100, tokenBudgetMicroUsd: 1, payerOrganizationId: "org_caller" };
|
|
109
|
+
});
|
|
110
|
+
operator.post("/v1/runs/capture", async (request) => {
|
|
111
|
+
runCaptures.push(request.body);
|
|
112
|
+
return { status: "captured" };
|
|
96
113
|
});
|
|
97
114
|
operator.post("/v1/payments/authorize", async (request, reply) => {
|
|
98
115
|
authorizations.push(request.body);
|
|
@@ -123,6 +140,7 @@ describe("two-node network", { timeout: 15_000 }, () => {
|
|
|
123
140
|
operatorUrl,
|
|
124
141
|
databasePath: ":memory:",
|
|
125
142
|
apiKey: "provider-admin",
|
|
143
|
+
sessionRefreshIntervalMs: 50,
|
|
126
144
|
approveGrant: ({ details }) => details?.invite === "letmein" ? { lifetimeMs: HOUR } : undefined,
|
|
127
145
|
});
|
|
128
146
|
});
|
|
@@ -154,6 +172,56 @@ describe("two-node network", { timeout: 15_000 }, () => {
|
|
|
154
172
|
const second = await startCaller();
|
|
155
173
|
expect(await run(second.url)).toEqual({ usage: { tokens: expect.any(Number) }, returnValue: { value: 12, error: null } });
|
|
156
174
|
});
|
|
175
|
+
test("the provider's contract reaches the caller, which pays for it and for the paid call it makes", async () => {
|
|
176
|
+
const caller = await startCaller();
|
|
177
|
+
const deployed = await fetch(`${providerUrl}/v1/contracts`, {
|
|
178
|
+
method: "POST",
|
|
179
|
+
headers: { "Content-Type": "application/json", Authorization: "Bearer provider-admin" },
|
|
180
|
+
body: JSON.stringify({ source: forecastContract }),
|
|
181
|
+
});
|
|
182
|
+
expect(deployed.status).toBe(201);
|
|
183
|
+
// The provider announces its resources again when a contract is deployed.
|
|
184
|
+
await vi.waitFor(async () => expect((await getJson(`${caller.url}/v1/resources`)).resources).toContainEqual(expect.objectContaining({
|
|
185
|
+
name: "Forecast",
|
|
186
|
+
baseUrl: providerUrl,
|
|
187
|
+
functions: [expect.objectContaining({ name: "run", kind: "contract", paymentManifest: { priceMicroUsdPerMillionTokens: CONTRACT_PRICE, recipientOrganizationId: ORGANIZATION } })],
|
|
188
|
+
})));
|
|
189
|
+
const result = await run(caller.url, 'request R {\n const forecast: com.example.Forecast\n return forecast.run(city: "Oslo")\n}');
|
|
190
|
+
expect(result.returnValue).toEqual({ value: "12 degrees", error: null });
|
|
191
|
+
const [{ voucher, source }] = contractAuthorizations;
|
|
192
|
+
expect(source).toBe(forecastContract);
|
|
193
|
+
expect(voucher).toMatchObject({ kind: "contract", publicKey: callerKeys.publicKey, recipientOrganizationId: ORGANIZATION, priceMicroUsdPerMillionTokens: CONTRACT_PRICE });
|
|
194
|
+
expect(verifyData(contractVoucherPayload(voucher), voucher.publicKey, voucher.signature)).toBe(true);
|
|
195
|
+
// The contract's run pays for its tokens and for its call to the provider's paid function.
|
|
196
|
+
await vi.waitFor(() => expect(runCaptures).toEqual([{ runId: voucher.invocationId, tokens: expect.any(Number) }]));
|
|
197
|
+
await vi.waitFor(() => expect(settlements).toHaveLength(1));
|
|
198
|
+
expect(settlements[0]).toMatchObject({ amount: COST, recipientOrganizationId: ORGANIZATION, runId: voucher.invocationId });
|
|
199
|
+
});
|
|
200
|
+
test("a contract keeps state for the organization paying for a remote call", async () => {
|
|
201
|
+
const caller = await startCaller();
|
|
202
|
+
await fetch(`${providerUrl}/v1/contracts`, {
|
|
203
|
+
method: "POST",
|
|
204
|
+
headers: { "Content-Type": "application/json", Authorization: "Bearer provider-admin" },
|
|
205
|
+
body: JSON.stringify({ source: "contract com.example.Visits: object {\n var state(caller.org) visits: int32 = 0\n visits = visits + 1\n return { visits: visits, by: caller }\n}" }),
|
|
206
|
+
});
|
|
207
|
+
await vi.waitFor(async () => expect((await getJson(`${caller.url}/v1/resources`)).resources.map((resource) => resource.name)).toContain("Visits"));
|
|
208
|
+
const visit = 'request R {\n const visits: com.example.Visits\n return visits.run().value\n}';
|
|
209
|
+
expect((await run(caller.url, visit)).returnValue).toEqual({ visits: 1, by: { org: "org_caller", node: callerKeys.publicKey } });
|
|
210
|
+
expect((await run(caller.url, visit)).returnValue).toMatchObject({ visits: 2 });
|
|
211
|
+
});
|
|
212
|
+
test("the provider learns a new price from the operator, and tells peers its contracts' new price", async () => {
|
|
213
|
+
const caller = await startCaller();
|
|
214
|
+
await fetch(`${providerUrl}/v1/contracts`, {
|
|
215
|
+
method: "POST",
|
|
216
|
+
headers: { "Content-Type": "application/json", Authorization: "Bearer provider-admin" },
|
|
217
|
+
body: JSON.stringify({ source: forecastContract }),
|
|
218
|
+
});
|
|
219
|
+
price = 2 * CONTRACT_PRICE;
|
|
220
|
+
await vi.waitFor(async () => {
|
|
221
|
+
const forecast = (await getJson(`${caller.url}/v1/resources`)).resources.find((resource) => resource.name === "Forecast");
|
|
222
|
+
expect(forecast?.functions[0].paymentManifest.priceMicroUsdPerMillionTokens).toBe(2 * CONTRACT_PRICE);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
157
225
|
describe("restricted functions", () => {
|
|
158
226
|
test("the caller needs a grant from the provider, then calls with it", async () => {
|
|
159
227
|
const caller = await startCaller();
|
|
@@ -223,10 +291,12 @@ describe("two-node network", { timeout: 15_000 }, () => {
|
|
|
223
291
|
test("a caller with an enrollment token enrolls at startup; one without still reports its URL", async () => {
|
|
224
292
|
sessions.length = 0;
|
|
225
293
|
const enrolled = await startCaller({ operatorUrl, enrollmentToken: "enr_caller" });
|
|
226
|
-
|
|
294
|
+
// The provider refreshes its own session meanwhile.
|
|
295
|
+
const callerSessions = () => sessions.filter(session => session.baseUrl !== providerUrl);
|
|
296
|
+
expect(callerSessions()).toEqual([{ enrollmentToken: "enr_caller", baseUrl: enrolled.url }]);
|
|
227
297
|
sessions.length = 0;
|
|
228
298
|
const other = await startCaller({ operatorUrl, databasePath: path.join(dir, "caller-2.db") });
|
|
229
|
-
await vi.waitFor(() => expect(
|
|
299
|
+
await vi.waitFor(() => expect(callerSessions()).toEqual([{ baseUrl: other.url }]));
|
|
230
300
|
});
|
|
231
301
|
test("a node without paid functions or a token starts without waiting for an unreachable operator", async () => {
|
|
232
302
|
const caller = await startCaller({ operatorUrl: "http://127.0.0.1:1/v1", databasePath: path.join(dir, "caller-3.db") });
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
2
2
|
import { signData } from "../../crypto/utils.js";
|
|
3
|
-
import { openDatabase } from "../../db/db.js";
|
|
4
3
|
import { PeerRepository, peerAnnouncementPayload } from "../../peer/repository.js";
|
|
5
|
-
import { generateBase64KeyPair } from "../helpers.js";
|
|
6
|
-
describe("PeerRepository", () => {
|
|
4
|
+
import { DIALECTS, generateBase64KeyPair, openTestDatabase } from "../helpers.js";
|
|
5
|
+
describe.each(DIALECTS)("PeerRepository (%s)", (dialect) => {
|
|
7
6
|
const keys = generateBase64KeyPair();
|
|
8
7
|
const peer = { baseUrl: "http://peer.example", name: "peer", publicKey: keys.publicKey };
|
|
9
8
|
const expiry = () => new Date(Date.now() + 60_000).toISOString();
|
|
10
9
|
let db;
|
|
11
10
|
let repository;
|
|
12
11
|
beforeEach(async () => {
|
|
13
|
-
db = await
|
|
12
|
+
db = await openTestDatabase(dialect);
|
|
14
13
|
repository = new PeerRepository(db);
|
|
15
14
|
});
|
|
16
15
|
afterEach(async () => {
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
2
2
|
import { signData } from "../../crypto/utils.js";
|
|
3
|
-
import { openDatabase } from "../../db/db.js";
|
|
4
3
|
import { PeerRepository } from "../../peer/repository.js";
|
|
5
4
|
import { resourceAnnouncementPayload, ResourceRepository } from "../../resource/repository.js";
|
|
6
|
-
import { generateBase64KeyPair, ORGANIZATION, weatherResource } from "../helpers.js";
|
|
5
|
+
import { DIALECTS, generateBase64KeyPair, ORGANIZATION, weatherResource, openTestDatabase } from "../helpers.js";
|
|
7
6
|
const SELF = "http://self.example";
|
|
8
7
|
const PROVIDER = "http://provider.example";
|
|
9
8
|
function resource(name, functions, baseUrl = PROVIDER) {
|
|
@@ -16,11 +15,11 @@ function resource(name, functions, baseUrl = PROVIDER) {
|
|
|
16
15
|
functions: functions.map(fn => ({ name: "fn", parameters: [], returnType: "string", exec: async () => undefined, ...fn })),
|
|
17
16
|
};
|
|
18
17
|
}
|
|
19
|
-
describe("ResourceRepository", () => {
|
|
18
|
+
describe.each(DIALECTS)("ResourceRepository (%s)", (dialect) => {
|
|
20
19
|
let db;
|
|
21
20
|
let repository;
|
|
22
21
|
beforeEach(async () => {
|
|
23
|
-
db = await
|
|
22
|
+
db = await openTestDatabase(dialect);
|
|
24
23
|
repository = new ResourceRepository(db, [weatherResource(SELF)], SELF);
|
|
25
24
|
});
|
|
26
25
|
afterEach(async () => {
|
|
@@ -3,7 +3,6 @@ import Fastify from "fastify";
|
|
|
3
3
|
import { createLocalJWKSet, exportJWK, SignJWT } from "jose";
|
|
4
4
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
5
5
|
import { safeVerifyData, signData } from "../crypto/utils.js";
|
|
6
|
-
import { openDatabase } from "../db/db.js";
|
|
7
6
|
import { grantRenewalPayload, grantRequestPayload } from "../grant/grant.js";
|
|
8
7
|
import { GrantRepository } from "../grant/repository.js";
|
|
9
8
|
import { GrantService } from "../grant/service.js";
|
|
@@ -12,10 +11,10 @@ import { CombinationResourceInvoker, inferenceVoucherPayload, paymentVoucherPayl
|
|
|
12
11
|
import { resourceAnnouncementPayload } from "../resource/repository.js";
|
|
13
12
|
import routes from "../routes.js";
|
|
14
13
|
import { createService } from "../service.js";
|
|
15
|
-
import { COST, generateBase64KeyPair, INFERENCE_RATES, modelResource, ORGANIZATION, temperatureSource, testConfig, weatherResource } from "./helpers.js";
|
|
14
|
+
import { DIALECTS, COST, generateBase64KeyPair, INFERENCE_RATES, modelResource, ORGANIZATION, temperatureSource, testConfig, weatherResource, openTestDatabase } from "./helpers.js";
|
|
16
15
|
const parameters = [{ name: "city", value: "Oslo" }];
|
|
17
16
|
const inMinutes = (minutes) => new Date(Date.now() + minutes * 60_000).toISOString();
|
|
18
|
-
describe("routes", () => {
|
|
17
|
+
describe.each(DIALECTS)("routes (%s)", (dialect) => {
|
|
19
18
|
const weather = weatherResource();
|
|
20
19
|
const caller = generateBase64KeyPair();
|
|
21
20
|
// Signs vouchers as a remote caller would. Its local branch never touches the repository.
|
|
@@ -28,7 +27,7 @@ describe("routes", () => {
|
|
|
28
27
|
let capture;
|
|
29
28
|
let release;
|
|
30
29
|
beforeEach(async () => {
|
|
31
|
-
db = await
|
|
30
|
+
db = await openTestDatabase(dialect);
|
|
32
31
|
config = testConfig();
|
|
33
32
|
service = createService(db, [weather], config);
|
|
34
33
|
authorize = vi.spyOn(service.operatorClient, "authorize").mockResolvedValue({ ok: true });
|
|
@@ -490,7 +489,7 @@ describe("routes", () => {
|
|
|
490
489
|
});
|
|
491
490
|
});
|
|
492
491
|
});
|
|
493
|
-
describe("restricted functions", () => {
|
|
492
|
+
describe.each(DIALECTS)("restricted functions (%s)", (dialect) => {
|
|
494
493
|
const caller = generateBase64KeyPair();
|
|
495
494
|
const callerInvoker = new CombinationResourceInvoker(undefined, 100_000, caller.publicKey, caller.privateKey);
|
|
496
495
|
const restricted = {
|
|
@@ -512,12 +511,12 @@ describe("restricted functions", () => {
|
|
|
512
511
|
let fastify;
|
|
513
512
|
let grant;
|
|
514
513
|
beforeEach(async () => {
|
|
515
|
-
db = await
|
|
514
|
+
db = await openTestDatabase(dialect);
|
|
516
515
|
service = createService(db, [restricted], testConfig());
|
|
517
516
|
vi.spyOn(service.operatorClient, "authorize").mockResolvedValue({ ok: true });
|
|
518
517
|
vi.spyOn(service.operatorClient, "capture").mockResolvedValue(undefined);
|
|
519
518
|
vi.spyOn(service.operatorClient, "release").mockResolvedValue(undefined);
|
|
520
|
-
callerDb = await
|
|
519
|
+
callerDb = await openTestDatabase(dialect);
|
|
521
520
|
callerGrants = new GrantService(new GrantRepository(callerDb), { baseUrl: "http://caller", ...caller });
|
|
522
521
|
grant = await service.grantService.issue(caller.publicKey, [{ resourcePath: "com.example", resourceName: "Vault" }], { lifetimeMs: 3_600_000 });
|
|
523
522
|
fastify = Fastify();
|
|
@@ -553,7 +552,7 @@ describe("restricted functions", () => {
|
|
|
553
552
|
beforeEach(async () => {
|
|
554
553
|
await fastify.close();
|
|
555
554
|
await db.close();
|
|
556
|
-
db = await
|
|
555
|
+
db = await openTestDatabase(dialect);
|
|
557
556
|
service = createService(db, [restricted], testConfig(), {
|
|
558
557
|
approveGrant: ({ details }) => details?.invite === "letmein" ? { lifetimeMs: 3_600_000 } : undefined,
|
|
559
558
|
});
|
|
@@ -587,7 +586,7 @@ describe("restricted functions", () => {
|
|
|
587
586
|
test("answers 500 when the approval hook returns an invalid approval", async () => {
|
|
588
587
|
await fastify.close();
|
|
589
588
|
await db.close();
|
|
590
|
-
db = await
|
|
589
|
+
db = await openTestDatabase(dialect);
|
|
591
590
|
service = createService(db, [restricted], testConfig(), { approveGrant: () => ({ lifetimeMs: -1 }) });
|
|
592
591
|
fastify = Fastify();
|
|
593
592
|
await fastify.register(routes, { prefix: "/v1", service });
|
|
@@ -700,7 +699,7 @@ describe("restricted functions", () => {
|
|
|
700
699
|
});
|
|
701
700
|
});
|
|
702
701
|
});
|
|
703
|
-
describe("inference functions", () => {
|
|
702
|
+
describe.each(DIALECTS)("inference functions (%s)", (dialect) => {
|
|
704
703
|
const weather = weatherResource();
|
|
705
704
|
const model = modelResource();
|
|
706
705
|
const caller = generateBase64KeyPair();
|
|
@@ -716,7 +715,7 @@ describe("inference functions", () => {
|
|
|
716
715
|
let capture;
|
|
717
716
|
let release;
|
|
718
717
|
beforeEach(async () => {
|
|
719
|
-
db = await
|
|
718
|
+
db = await openTestDatabase(dialect);
|
|
720
719
|
service = createService(db, [weather, model], testConfig());
|
|
721
720
|
authorize = vi.spyOn(service.operatorClient, "authorize").mockResolvedValue({ ok: true });
|
|
722
721
|
capture = vi.spyOn(service.operatorClient, "capture").mockResolvedValue(undefined);
|
|
@@ -33,6 +33,28 @@ describe("resolveConfig", () => {
|
|
|
33
33
|
expect(resolveConfig(keys).databasePath).toBe("/data/requestscript.db");
|
|
34
34
|
expect(resolveConfig({ ...keys, databasePath: ":memory:" }).databasePath).toBe(":memory:");
|
|
35
35
|
});
|
|
36
|
+
test("uses Postgres when DATABASE_URL is set, in DATABASE_SCHEMA", () => {
|
|
37
|
+
vi.stubEnv("DATABASE_PATH", undefined);
|
|
38
|
+
vi.stubEnv("DATABASE_URL", "postgresql://node@db/stellarium");
|
|
39
|
+
vi.stubEnv("DATABASE_SCHEMA", "node_a");
|
|
40
|
+
const config = resolveConfig(keys);
|
|
41
|
+
expect(config).toMatchObject({ databaseUrl: "postgresql://node@db/stellarium", databaseSchema: "node_a" });
|
|
42
|
+
expect(config.databasePath).toBeUndefined();
|
|
43
|
+
vi.stubEnv("DATABASE_URL", "");
|
|
44
|
+
vi.stubEnv("DATABASE_SCHEMA", "");
|
|
45
|
+
expect(resolveConfig(keys).databasePath).toBe(DEFAULT_DATABASE_PATH);
|
|
46
|
+
expect(resolveConfig(keys).databaseUrl).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
test("refuses a database configuration it can't use", () => {
|
|
49
|
+
vi.stubEnv("DATABASE_PATH", undefined);
|
|
50
|
+
vi.stubEnv("DATABASE_URL", undefined);
|
|
51
|
+
vi.stubEnv("DATABASE_SCHEMA", undefined);
|
|
52
|
+
const url = "postgres://db/stellarium";
|
|
53
|
+
expect(() => resolveConfig({ ...keys, databaseUrl: "mysql://db/stellarium" })).toThrow("postgres://");
|
|
54
|
+
expect(() => resolveConfig({ ...keys, databaseUrl: url, databasePath: "./node.db" })).toThrow("not both");
|
|
55
|
+
expect(() => resolveConfig({ ...keys, databaseSchema: "node_a" })).toThrow("needs a Postgres");
|
|
56
|
+
expect(() => resolveConfig({ ...keys, databaseUrl: url, databaseSchema: "Node-A" })).toThrow("Invalid database schema");
|
|
57
|
+
});
|
|
36
58
|
test("requires the signing keys and base URL", () => {
|
|
37
59
|
vi.stubEnv("SIGNING_PRIVATE_KEY", undefined);
|
|
38
60
|
expect(() => resolveConfig({ baseUrl: keys.baseUrl, publicKey: keys.publicKey })).toThrow("SIGNING_PRIVATE_KEY is required");
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
import Fastify from "fastify";
|
|
2
2
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
3
|
-
import { openDatabase } from "../db/db.js";
|
|
4
3
|
import routes from "../routes.js";
|
|
5
4
|
import { createService } from "../service.js";
|
|
6
5
|
import { STARTUP_PAGE_SIZE, StartupService } from "../startup.js";
|
|
7
|
-
import { testConfig, weatherResource } from "./helpers.js";
|
|
6
|
+
import { DIALECTS, testConfig, weatherResource, openTestDatabase } from "./helpers.js";
|
|
8
7
|
const STARTING_PEER = "http://starting.example";
|
|
9
8
|
const SELF = "http://self.example";
|
|
10
|
-
describe("StartupService", () => {
|
|
9
|
+
describe.each(DIALECTS)("StartupService (%s)", (dialect) => {
|
|
11
10
|
const peerCount = STARTUP_PAGE_SIZE * 2 + 5;
|
|
12
11
|
const resourceCount = STARTUP_PAGE_SIZE + 1;
|
|
13
12
|
let databases;
|
|
@@ -16,7 +15,7 @@ describe("StartupService", () => {
|
|
|
16
15
|
let startup;
|
|
17
16
|
let requestedUrls;
|
|
18
17
|
beforeEach(async () => {
|
|
19
|
-
const startingDb = await
|
|
18
|
+
const startingDb = await openTestDatabase(dialect);
|
|
20
19
|
const startingService = createService(startingDb, [weatherResource(STARTING_PEER)], testConfig({ baseUrl: STARTING_PEER }));
|
|
21
20
|
for (let i = 0; i < peerCount; i++) {
|
|
22
21
|
const baseUrl = `http://peer${i}.example`;
|
|
@@ -36,7 +35,7 @@ describe("StartupService", () => {
|
|
|
36
35
|
const response = await startingNode.inject({ method: "GET", url: url.pathname + url.search });
|
|
37
36
|
return new Response(response.body, { status: response.statusCode, headers: { "Content-Type": "application/json" } });
|
|
38
37
|
});
|
|
39
|
-
const db = await
|
|
38
|
+
const db = await openTestDatabase(dialect);
|
|
40
39
|
databases = [startingDb, db];
|
|
41
40
|
const config = testConfig({ baseUrl: SELF });
|
|
42
41
|
service = createService(db, [], config);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stellarium-ts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "A Stellarium Node in TypeScript.",
|
|
5
5
|
"homepage": "https://github.com/qualletio/stellarium-ts#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -35,7 +35,9 @@
|
|
|
35
35
|
"license": "ISC",
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@biomejs/biome": "^2.5.15",
|
|
38
|
+
"@electric-sql/pglite": "^0.5.10",
|
|
38
39
|
"@types/node": "^26.6.4",
|
|
40
|
+
"@types/pg": "^8.23.1",
|
|
39
41
|
"turbo": "^2.11.7",
|
|
40
42
|
"typescript": "^7.0.2",
|
|
41
43
|
"vite": "^8.3.3",
|
|
@@ -46,7 +48,8 @@
|
|
|
46
48
|
"dotenv": "^18.0.5",
|
|
47
49
|
"fastify": "^5.12.5",
|
|
48
50
|
"jose": "6.1.0",
|
|
49
|
-
"
|
|
51
|
+
"pg": "^8.23.1",
|
|
52
|
+
"requestscript": "1.5.0",
|
|
50
53
|
"sqlite": "^5.1.1",
|
|
51
54
|
"sqlite3": "^6.0.1"
|
|
52
55
|
},
|