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.
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 +96 -28
  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 +29 -0
  46. package/dist/server.d.ts.map +1 -1
  47. package/dist/server.js +80 -6
  48. package/dist/service.d.ts +26 -3
  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 +22 -0
  79. package/dist/tests/startup.test.js +4 -5
  80. package/package.json +5 -2
@@ -0,0 +1,43 @@
1
+ import type { StateScope, StateWrite, StoredState } from "requestscript";
2
+ import type { NodeDatabase } from "../db/database.js";
3
+ export declare const DEFAULT_MAX_CONTRACT_STATE_BYTES: number;
4
+ /** How a scope is stored: '' for the shared copy, and a prefix keeping the scope's kind, so 3 and "3" differ. */
5
+ export declare function scopeKey(scope: StateScope): string;
6
+ export declare function scopeFromKey(key: string): StateScope;
7
+ export interface ContractStateRow {
8
+ variable: string;
9
+ scope: StateScope;
10
+ value: unknown;
11
+ updatedAt: string;
12
+ }
13
+ /** Why `commit` saved nothing. */
14
+ export type CommitRefusal = {
15
+ reason: 'conflict';
16
+ } | {
17
+ reason: 'too_large';
18
+ scope: StateScope;
19
+ bytes: number;
20
+ };
21
+ /**
22
+ * The state variables of contracts, one row per variable and scope. A row's version counts its writes, so a
23
+ * call that read an older copy can't save over a newer one.
24
+ */
25
+ export declare class ContractStateRepository {
26
+ private readonly db;
27
+ constructor(db: NodeDatabase);
28
+ get(path: string, name: string, variable: string, scope: StateScope): Promise<StoredState | undefined>;
29
+ /**
30
+ * Saves every write of one call, or none. A write carrying the version its call read saves only over that
31
+ * version, and one without a version only where nothing is stored, so a copy changed during the call is a
32
+ * conflict. Each scope written to must stay within `maxBytes`, its variables' encoded values together.
33
+ */
34
+ commit(path: string, name: string, writes: StateWrite[], maxBytes: number): Promise<{
35
+ ok: true;
36
+ } | ({
37
+ ok: false;
38
+ } & CommitRefusal)>;
39
+ /** Every variable and scope of a contract, with values as stored, ordered by variable then scope. */
40
+ list(path: string, name: string): Promise<ContractStateRow[]>;
41
+ deleteContract(path: string, name: string): Promise<void>;
42
+ }
43
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/contract/state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AACzE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAEtD,eAAO,MAAM,gCAAgC,QAAY,CAAC;AAE1D,iHAAiH;AACjH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAKlD;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAKpD;AAED,MAAM,WAAW,gBAAgB;IAC7B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,OAAO,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACrB;AAWD,kCAAkC;AAClC,MAAM,MAAM,aAAa,GAAG;IAAE,MAAM,EAAE,UAAU,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,KAAK,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAQ/G;;;GAGG;AACH,qBAAa,uBAAuB;IACpB,OAAO,CAAC,QAAQ,CAAC,EAAE;IAA/B,YAA6B,EAAE,EAAE,YAAY,EAAI;IAE3C,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAM3G;IAED;;;;OAIG;IACG,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAA;KAAE,GAAG,CAAC;QAAE,EAAE,EAAE,KAAK,CAAA;KAAE,GAAG,aAAa,CAAC,CAAC,CAmCxI;IAED,qGAAqG;IAC/F,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAGlE;IAEK,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE9D;CACJ"}
@@ -0,0 +1,79 @@
1
+ export const DEFAULT_MAX_CONTRACT_STATE_BYTES = 64 * 1024;
2
+ /** How a scope is stored: '' for the shared copy, and a prefix keeping the scope's kind, so 3 and "3" differ. */
3
+ export function scopeKey(scope) {
4
+ if (scope === null) {
5
+ return '';
6
+ }
7
+ return `${scope.kind === 'string' ? 's' : 'i'}:${scope.key}`;
8
+ }
9
+ export function scopeFromKey(key) {
10
+ if (key === '') {
11
+ return null;
12
+ }
13
+ return { kind: key.startsWith('s:') ? 'string' : 'integer', key: key.slice(2) };
14
+ }
15
+ class CommitRefused extends Error {
16
+ refusal;
17
+ constructor(refusal) {
18
+ super(refusal.reason);
19
+ this.refusal = refusal;
20
+ }
21
+ }
22
+ /**
23
+ * The state variables of contracts, one row per variable and scope. A row's version counts its writes, so a
24
+ * call that read an older copy can't save over a newer one.
25
+ */
26
+ export class ContractStateRepository {
27
+ db;
28
+ constructor(db) {
29
+ this.db = db;
30
+ }
31
+ async get(path, name, variable, scope) {
32
+ const row = await this.db.get('SELECT value, type, version FROM contract_state WHERE path = ? AND name = ? AND var_name = ? AND scope_key = ?', [path, name, variable, scopeKey(scope)]);
33
+ return row && { value: JSON.parse(row.value), type: row.type, version: row.version };
34
+ }
35
+ /**
36
+ * Saves every write of one call, or none. A write carrying the version its call read saves only over that
37
+ * version, and one without a version only where nothing is stored, so a copy changed during the call is a
38
+ * conflict. Each scope written to must stay within `maxBytes`, its variables' encoded values together.
39
+ */
40
+ async commit(path, name, writes, maxBytes) {
41
+ try {
42
+ await this.db.transaction(async (tx) => {
43
+ const now = new Date().toISOString();
44
+ for (const write of writes) {
45
+ const key = scopeKey(write.scope);
46
+ const value = JSON.stringify(write.value);
47
+ const result = write.version === undefined
48
+ ? await tx.run('INSERT INTO contract_state (path, name, var_name, scope_key, value, type, version, updated_at) VALUES (?, ?, ?, ?, ?, ?, 1, ?) ON CONFLICT DO NOTHING', [path, name, write.name, key, value, write.type, now])
49
+ : await tx.run('UPDATE contract_state SET value = ?, type = ?, version = version + 1, updated_at = ? WHERE path = ? AND name = ? AND var_name = ? AND scope_key = ? AND version = ?', [value, write.type, now, path, name, write.name, key, write.version]);
50
+ if (result.changes === 0) {
51
+ throw new CommitRefused({ reason: 'conflict' });
52
+ }
53
+ }
54
+ for (const key of new Set(writes.map(write => scopeKey(write.scope)))) {
55
+ const rows = await tx.all('SELECT value FROM contract_state WHERE path = ? AND name = ? AND scope_key = ?', [path, name, key]);
56
+ const bytes = rows.reduce((sum, row) => sum + Buffer.byteLength(row.value), 0);
57
+ if (bytes > maxBytes) {
58
+ throw new CommitRefused({ reason: 'too_large', scope: scopeFromKey(key), bytes });
59
+ }
60
+ }
61
+ });
62
+ }
63
+ catch (error) {
64
+ if (error instanceof CommitRefused) {
65
+ return { ok: false, ...error.refusal };
66
+ }
67
+ throw error;
68
+ }
69
+ return { ok: true };
70
+ }
71
+ /** Every variable and scope of a contract, with values as stored, ordered by variable then scope. */
72
+ async list(path, name) {
73
+ const rows = await this.db.all('SELECT var_name, scope_key, value, updated_at FROM contract_state WHERE path = ? AND name = ? ORDER BY var_name, scope_key', [path, name]);
74
+ return rows.map(row => ({ variable: row.var_name, scope: scopeFromKey(row.scope_key), value: JSON.parse(row.value), updatedAt: row.updated_at }));
75
+ }
76
+ async deleteContract(path, name) {
77
+ await this.db.run('DELETE FROM contract_state WHERE path = ? AND name = ?', [path, name]);
78
+ }
79
+ }
@@ -0,0 +1,25 @@
1
+ export type DatabaseDialect = 'sqlite' | 'postgres';
2
+ export interface RunResult {
3
+ /** Rows the statement inserted, updated or deleted. */
4
+ changes: number;
5
+ }
6
+ /**
7
+ * The node's database, SQLite or Postgres. SQL uses `?` placeholders on both, and sticks to what both
8
+ * accept: `ON CONFLICT` instead of `INSERT OR IGNORE`, and `RETURNING id` instead of the last insert id.
9
+ */
10
+ export interface NodeDatabase {
11
+ readonly dialect: DatabaseDialect;
12
+ get<T = any>(sql: string, params?: unknown[]): Promise<T | undefined>;
13
+ all<T = any[]>(sql: string, params?: unknown[]): Promise<T>;
14
+ run(sql: string, params?: unknown[]): Promise<RunResult>;
15
+ /** Runs one or more statements without parameters. */
16
+ exec(sql: string): Promise<void>;
17
+ /**
18
+ * Runs `work` in a transaction, committing if it resolves and rolling back if it throws. Transactions
19
+ * are serialized, so `work` must not start another one, and must query through `tx` only: on a database
20
+ * with one connection, such as PGlite, a query through the database waits for the transaction to end.
21
+ */
22
+ transaction<T>(work: (tx: NodeDatabase) => Promise<T>): Promise<T>;
23
+ close(): Promise<void>;
24
+ }
25
+ //# sourceMappingURL=database.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,UAAU,CAAC;AAEpD,MAAM,WAAW,SAAS;IACtB,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IACzB,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACtE,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5D,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;IACzD,sDAAsD;IACtD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC;;;;OAIG;IACH,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACnE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B"}
@@ -0,0 +1 @@
1
+ export {};
package/dist/db/db.d.ts CHANGED
@@ -1,6 +1,14 @@
1
- import { type Database } from 'sqlite';
1
+ import type { NodeDatabase } from './database.js';
2
2
  export declare const DEFAULT_DATABASE_PATH = "./requestscript.db";
3
- /** Opens (or creates) the node database and brings its schema up to date. Pass ':memory:' for tests. */
4
- export declare function openDatabase(filename?: string): Promise<Database>;
5
- export declare function createTables(db: Database): Promise<void>;
3
+ export interface OpenDatabaseOptions {
4
+ /** Postgres only: the schema the node's tables live in, created if missing. Lets nodes share a database. */
5
+ schema?: string;
6
+ }
7
+ export declare function isPostgresUrl(location: string): boolean;
8
+ /**
9
+ * Opens the node database and brings its schema up to date. `location` is a `postgres://` or
10
+ * `postgresql://` URL, or a SQLite file. Pass ':memory:' for tests.
11
+ */
12
+ export declare function openDatabase(location?: string, options?: OpenDatabaseOptions): Promise<NodeDatabase>;
13
+ export declare function createTables(db: NodeDatabase): Promise<void>;
6
14
  //# sourceMappingURL=db.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/db/db.ts"],"names":[],"mappings":"AAGA,OAAO,EAAQ,KAAK,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAE7C,eAAO,MAAM,qBAAqB,uBAAuB,CAAC;AAc1D,wGAAwG;AACxG,wBAAsB,YAAY,CAAC,QAAQ,GAAE,MAA8B,GAAG,OAAO,CAAC,QAAQ,CAAC,CAO9F;AAED,wBAAsB,YAAY,CAAC,EAAE,EAAE,QAAQ,iBA+I9C"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/db/db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAmB,YAAY,EAAE,MAAM,eAAe,CAAC;AAInE,eAAO,MAAM,qBAAqB,uBAAuB,CAAC;AAgB1D,MAAM,WAAW,mBAAmB;IAChC,4GAA4G;IAC5G,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAEvD;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,QAAQ,GAAE,MAA8B,EAAE,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,YAAY,CAAC,CAWrI;AA0BD,wBAAsB,YAAY,CAAC,EAAE,EAAE,YAAY,iBAmLlD"}
package/dist/db/db.js CHANGED
@@ -1,7 +1,5 @@
1
- import { mkdirSync } from 'node:fs';
2
- import { dirname } from 'node:path';
3
- import sqlite3 from 'sqlite3';
4
- import { open } from 'sqlite';
1
+ import { nodePostgresDriver, PostgresDatabase } from './postgres.js';
2
+ import { SqliteDatabase } from './sqlite.js';
5
3
  export const DEFAULT_DATABASE_PATH = './requestscript.db';
6
4
  /**
7
5
  * Bumped whenever the schema changes in a way `CREATE TABLE IF NOT EXISTS` can't apply.
@@ -12,19 +10,53 @@ export const DEFAULT_DATABASE_PATH = './requestscript.db';
12
10
  * Version 5 added `resource_function.kind` and per-token inference pricing, which made `cost_micro_usdc` nullable.
13
11
  * Version 6 moved payments to Stripe: `*_micro_usdc` columns became `*_micro_usd`, and `recipient_address`
14
12
  * became `recipient_organization_id`.
13
+ * Version 7 added contract functions: `resource_function.kind` 'contract', priced per million tokens in
14
+ * `resource_function_payment_manifest.price_micro_usd_per_million_tokens`.
15
15
  */
16
- const SCHEMA_VERSION = 6;
17
- /** Opens (or creates) the node database and brings its schema up to date. Pass ':memory:' for tests. */
18
- export async function openDatabase(filename = DEFAULT_DATABASE_PATH) {
19
- if (filename !== ':memory:') {
20
- mkdirSync(dirname(filename), { recursive: true });
16
+ const SCHEMA_VERSION = 7;
17
+ export function isPostgresUrl(location) {
18
+ return location.startsWith('postgres://') || location.startsWith('postgresql://');
19
+ }
20
+ /**
21
+ * Opens the node database and brings its schema up to date. `location` is a `postgres://` or
22
+ * `postgresql://` URL, or a SQLite file. Pass ':memory:' for tests.
23
+ */
24
+ export async function openDatabase(location = DEFAULT_DATABASE_PATH, options = {}) {
25
+ const db = isPostgresUrl(location)
26
+ ? await PostgresDatabase.open(nodePostgresDriver(location, options.schema), options.schema)
27
+ : await SqliteDatabase.open(location);
28
+ try {
29
+ await createTables(db);
30
+ }
31
+ catch (error) {
32
+ await db.close();
33
+ throw error;
21
34
  }
22
- const db = await open({ filename, driver: sqlite3.Database });
23
- await createTables(db);
24
35
  return db;
25
36
  }
37
+ /** Column types that differ between the dialects. */
38
+ function columnTypes(dialect) {
39
+ return dialect === 'sqlite'
40
+ ? { id: 'INTEGER PRIMARY KEY AUTOINCREMENT', bigint: 'INTEGER' }
41
+ : { id: 'BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY', bigint: 'BIGINT' };
42
+ }
43
+ /** SQLite keeps the version in `PRAGMA user_version`; Postgres, which has no equivalent, in a one-row table. */
44
+ async function getSchemaVersion(db) {
45
+ if (db.dialect === 'sqlite') {
46
+ return (await db.get('PRAGMA user_version'))?.user_version ?? 0;
47
+ }
48
+ await db.exec('CREATE TABLE IF NOT EXISTS stellarium_schema (version INTEGER NOT NULL)');
49
+ return (await db.get('SELECT version FROM stellarium_schema'))?.version ?? 0;
50
+ }
51
+ async function setSchemaVersion(db, version) {
52
+ if (db.dialect === 'sqlite') {
53
+ await db.exec(`PRAGMA user_version = ${version}`);
54
+ return;
55
+ }
56
+ await db.exec(`DELETE FROM stellarium_schema; INSERT INTO stellarium_schema (version) VALUES (${version})`);
57
+ }
26
58
  export async function createTables(db) {
27
- const { user_version: version } = await db.get('PRAGMA user_version') ?? { user_version: 0 };
59
+ const version = await getSchemaVersion(db);
28
60
  if (version < SCHEMA_VERSION) {
29
61
  // Resource rows are a cache of what peers advertise, and are relearned on the next broadcast.
30
62
  // SQLite can't drop a constraint in place, so rebuild them. Peers are kept.
@@ -35,9 +67,10 @@ export async function createTables(db) {
35
67
  DROP TABLE IF EXISTS resource;
36
68
  `);
37
69
  }
70
+ const t = columnTypes(db.dialect);
38
71
  await db.exec(`
39
72
  CREATE TABLE IF NOT EXISTS peer (
40
- id INTEGER PRIMARY KEY AUTOINCREMENT,
73
+ id ${t.id},
41
74
  base_url TEXT NOT NULL,
42
75
  public_key TEXT NOT NULL,
43
76
  name TEXT NOT NULL,
@@ -50,8 +83,8 @@ export async function createTables(db) {
50
83
  );
51
84
 
52
85
  CREATE TABLE IF NOT EXISTS resource (
53
- id INTEGER PRIMARY KEY AUTOINCREMENT,
54
- peer_id INTEGER,
86
+ id ${t.id},
87
+ peer_id ${t.bigint},
55
88
  path TEXT NOT NULL,
56
89
  name TEXT NOT NULL,
57
90
  base_url TEXT NOT NULL,
@@ -66,8 +99,8 @@ export async function createTables(db) {
66
99
  );
67
100
 
68
101
  CREATE TABLE IF NOT EXISTS resource_function (
69
- id INTEGER PRIMARY KEY AUTOINCREMENT,
70
- resource_id INTEGER NOT NULL,
102
+ id ${t.id},
103
+ resource_id ${t.bigint} NOT NULL,
71
104
  name TEXT NOT NULL,
72
105
  return_type TEXT NOT NULL,
73
106
  description TEXT,
@@ -82,8 +115,8 @@ export async function createTables(db) {
82
115
  );
83
116
 
84
117
  CREATE TABLE IF NOT EXISTS resource_function_parameter (
85
- id INTEGER PRIMARY KEY AUTOINCREMENT,
86
- resource_function_id INTEGER NOT NULL,
118
+ id ${t.id},
119
+ resource_function_id ${t.bigint} NOT NULL,
87
120
  name TEXT NOT NULL,
88
121
  parameter_type TEXT NOT NULL,
89
122
  description TEXT,
@@ -96,12 +129,14 @@ export async function createTables(db) {
96
129
  );
97
130
 
98
131
  CREATE TABLE IF NOT EXISTS resource_function_payment_manifest (
99
- id INTEGER PRIMARY KEY AUTOINCREMENT,
100
- resource_function_id INTEGER NOT NULL,
101
- -- Null for inference functions, which are priced per million tokens instead.
102
- cost_micro_usd INTEGER,
103
- input_cost_micro_usd_per_million_tokens INTEGER,
104
- output_cost_micro_usd_per_million_tokens INTEGER,
132
+ id ${t.id},
133
+ resource_function_id ${t.bigint} NOT NULL,
134
+ -- Null for inference and contract functions, which are priced per million tokens instead.
135
+ cost_micro_usd ${t.bigint},
136
+ input_cost_micro_usd_per_million_tokens ${t.bigint},
137
+ output_cost_micro_usd_per_million_tokens ${t.bigint},
138
+ -- Set for contract functions, which are charged for the tokens they execute.
139
+ price_micro_usd_per_million_tokens ${t.bigint},
105
140
  -- The hosting node's organization. Null only if a peer advertised the manifest without one.
106
141
  recipient_organization_id TEXT,
107
142
  created_at TEXT NOT NULL,
@@ -120,7 +155,7 @@ export async function createTables(db) {
120
155
  capabilities TEXT NOT NULL,
121
156
  parent_grant_issuer TEXT,
122
157
  parent_grant_id TEXT,
123
- lifetime_ms INTEGER NOT NULL,
158
+ lifetime_ms ${t.bigint} NOT NULL,
124
159
  renewable_until TEXT,
125
160
  created_at TEXT NOT NULL
126
161
  );
@@ -152,10 +187,43 @@ export async function createTables(db) {
152
187
  -- publicKey:nonce of accepted authorizations and grant requests, until they expire.
153
188
  CREATE TABLE IF NOT EXISTS seen_nonce (
154
189
  key TEXT PRIMARY KEY,
155
- expires_at INTEGER NOT NULL
190
+ expires_at ${t.bigint} NOT NULL
156
191
  );
157
192
  CREATE INDEX IF NOT EXISTS seen_nonce_expires_at ON seen_nonce (expires_at);
158
193
 
194
+ -- Contracts deployed to this node. Versions are immutable. The latest version of each is one of the
195
+ -- node's resources, with a single function, run. An owner of null is the node's admin.
196
+ CREATE TABLE IF NOT EXISTS contract (
197
+ path TEXT NOT NULL,
198
+ name TEXT NOT NULL,
199
+ version TEXT NOT NULL,
200
+ source TEXT NOT NULL,
201
+ -- JSON: [{ name, type }]
202
+ parameters TEXT NOT NULL,
203
+ return_type TEXT NOT NULL,
204
+ owner_organization_id TEXT,
205
+ description TEXT,
206
+ created_at TEXT NOT NULL,
207
+
208
+ PRIMARY KEY (path, name, version)
209
+ );
210
+
211
+ -- The state variables of the contracts deployed to this node, kept across versions. scope_key is ''
212
+ -- for a variable's shared copy, and 's:<string>' or 'i:<integer>' for a scoped one. value is the
213
+ -- variable's encoded JSON, and version counts its writes, so a call can't save over a newer copy.
214
+ CREATE TABLE IF NOT EXISTS contract_state (
215
+ path TEXT NOT NULL,
216
+ name TEXT NOT NULL,
217
+ var_name TEXT NOT NULL,
218
+ scope_key TEXT NOT NULL,
219
+ value TEXT NOT NULL,
220
+ type TEXT NOT NULL,
221
+ version ${t.bigint} NOT NULL,
222
+ updated_at TEXT NOT NULL,
223
+
224
+ PRIMARY KEY (path, name, var_name, scope_key)
225
+ );
226
+
159
227
  -- Keys an admin blocked from calling this node or getting grants from it.
160
228
  CREATE TABLE IF NOT EXISTS blocked_caller (
161
229
  public_key TEXT PRIMARY KEY,
@@ -163,5 +231,5 @@ export async function createTables(db) {
163
231
  blocked_at TEXT NOT NULL
164
232
  );
165
233
  `);
166
- await db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
234
+ await setSchemaVersion(db, SCHEMA_VERSION);
167
235
  }
@@ -0,0 +1,39 @@
1
+ import type { NodeDatabase, RunResult } from './database.js';
2
+ /** What `PostgresDatabase` needs from a Postgres client: node-postgres in production, PGlite in tests. */
3
+ export interface PostgresDriver {
4
+ query(text: string, params: unknown[]): Promise<{
5
+ rows: any[];
6
+ rowCount: number;
7
+ }>;
8
+ /** Runs one or more statements without parameters. */
9
+ exec(text: string): Promise<void>;
10
+ /** Runs `work` on one connection inside BEGIN and COMMIT, or ROLLBACK if it throws. */
11
+ transaction<T>(work: (driver: PostgresDriver) => Promise<T>): Promise<T>;
12
+ close(): Promise<void>;
13
+ }
14
+ /** Schema names are interpolated into SQL, so only plain identifiers are accepted. */
15
+ export declare function assertSchemaName(schema: string): void;
16
+ /** Rewrites `?` placeholders to `$1`, `$2`, ..., leaving quoted strings and identifiers alone. */
17
+ export declare function numberPlaceholders(sql: string): string;
18
+ /** A node-postgres pool. 64-bit integers come back as numbers, like SQLite's. */
19
+ export declare function nodePostgresDriver(url: string, schema?: string): PostgresDriver;
20
+ export declare class PostgresDatabase implements NodeDatabase {
21
+ private readonly driver;
22
+ private readonly inTransaction;
23
+ readonly dialect = "postgres";
24
+ /** `inTransaction` is set on the handle a transaction's work receives. */
25
+ constructor(driver: PostgresDriver, inTransaction?: boolean);
26
+ /** Creates `schema` if it doesn't exist. The driver must already resolve names in it. */
27
+ static open(driver: PostgresDriver, schema?: string): Promise<PostgresDatabase>;
28
+ get<T>(sql: string, params?: unknown[]): Promise<T | undefined>;
29
+ all<T>(sql: string, params?: unknown[]): Promise<T>;
30
+ run(sql: string, params?: unknown[]): Promise<RunResult>;
31
+ exec(sql: string): Promise<void>;
32
+ /**
33
+ * Transactions take one advisory lock for the node's schema, so they run one at a time as they do on
34
+ * SQLite. Their check-then-insert writes rely on that.
35
+ */
36
+ transaction<T>(work: (tx: NodeDatabase) => Promise<T>): Promise<T>;
37
+ close(): Promise<void>;
38
+ }
39
+ //# sourceMappingURL=postgres.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.d.ts","sourceRoot":"","sources":["../../src/db/postgres.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE7D,0GAA0G;AAC1G,MAAM,WAAW,cAAc;IAC3B,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,GAAG,EAAE,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACnF,sDAAsD;IACtD,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,uFAAuF;IACvF,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IACzE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAID,sFAAsF;AACtF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAIrD;AAED,kGAAkG;AAClG,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAoBtD;AAED,iFAAiF;AACjF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,cAAc,CA8C/E;AAED,qBAAa,gBAAiB,YAAW,YAAY;IAIrC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAAkB,OAAO,CAAC,QAAQ,CAAC,aAAa;IAHnF,QAAQ,CAAC,OAAO,cAAc;IAE9B,0EAA0E;IAC1E,YAA6B,MAAM,EAAE,cAAc,EAAmB,aAAa,UAAQ,EAAI;IAE/F,yFAAyF;IACzF,OAAa,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAMpF;IAEK,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAGxE;IAEK,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAG5D;IAEK,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,OAAO,CAAC,SAAS,CAAC,CAGjE;IAED,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/B;IAED;;;OAGG;IACH,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAQjE;IAED,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAErB;CACJ"}
@@ -0,0 +1,132 @@
1
+ import pg from 'pg';
2
+ const INT8_OID = 20;
3
+ /** Schema names are interpolated into SQL, so only plain identifiers are accepted. */
4
+ export function assertSchemaName(schema) {
5
+ if (!/^[a-z_][a-z0-9_]*$/.test(schema)) {
6
+ throw new Error(`Invalid database schema '${schema}': use lowercase letters, digits and underscores`);
7
+ }
8
+ }
9
+ /** Rewrites `?` placeholders to `$1`, `$2`, ..., leaving quoted strings and identifiers alone. */
10
+ export function numberPlaceholders(sql) {
11
+ let index = 0;
12
+ let quote;
13
+ let result = '';
14
+ for (const char of sql) {
15
+ if (quote) {
16
+ if (char === quote) {
17
+ quote = undefined;
18
+ }
19
+ result += char;
20
+ }
21
+ else if (char === "'" || char === '"') {
22
+ quote = char;
23
+ result += char;
24
+ }
25
+ else if (char === '?') {
26
+ result += `$${++index}`;
27
+ }
28
+ else {
29
+ result += char;
30
+ }
31
+ }
32
+ return result;
33
+ }
34
+ /** A node-postgres pool. 64-bit integers come back as numbers, like SQLite's. */
35
+ export function nodePostgresDriver(url, schema) {
36
+ const pool = new pg.Pool({
37
+ connectionString: url,
38
+ // Every connection resolves unqualified names in the node's schema.
39
+ options: schema ? `-c search_path=${schema}` : undefined,
40
+ types: { getTypeParser: ((oid, format) => oid === INT8_OID ? Number : pg.types.getTypeParser(oid, format)) },
41
+ });
42
+ const onClient = (client) => ({
43
+ query: async (text, params) => {
44
+ const result = await client.query(text, params);
45
+ return { rows: result.rows, rowCount: result.rowCount ?? 0 };
46
+ },
47
+ exec: async (text) => {
48
+ await client.query(text);
49
+ },
50
+ transaction: () => {
51
+ throw new Error('Transactions cannot be nested');
52
+ },
53
+ close: async () => { },
54
+ });
55
+ return {
56
+ query: async (text, params) => {
57
+ const result = await pool.query(text, params);
58
+ return { rows: result.rows, rowCount: result.rowCount ?? 0 };
59
+ },
60
+ exec: async (text) => {
61
+ await pool.query(text);
62
+ },
63
+ transaction: async (work) => {
64
+ const client = await pool.connect();
65
+ try {
66
+ await client.query('BEGIN');
67
+ try {
68
+ const result = await work(onClient(client));
69
+ await client.query('COMMIT');
70
+ return result;
71
+ }
72
+ catch (error) {
73
+ await client.query('ROLLBACK');
74
+ throw error;
75
+ }
76
+ }
77
+ finally {
78
+ client.release();
79
+ }
80
+ },
81
+ close: () => pool.end(),
82
+ };
83
+ }
84
+ export class PostgresDatabase {
85
+ driver;
86
+ inTransaction;
87
+ dialect = 'postgres';
88
+ /** `inTransaction` is set on the handle a transaction's work receives. */
89
+ constructor(driver, inTransaction = false) {
90
+ this.driver = driver;
91
+ this.inTransaction = inTransaction;
92
+ }
93
+ /** Creates `schema` if it doesn't exist. The driver must already resolve names in it. */
94
+ static async open(driver, schema) {
95
+ if (schema) {
96
+ assertSchemaName(schema);
97
+ await driver.exec(`CREATE SCHEMA IF NOT EXISTS ${schema}`);
98
+ }
99
+ return new PostgresDatabase(driver);
100
+ }
101
+ async get(sql, params = []) {
102
+ const { rows } = await this.driver.query(numberPlaceholders(sql), params);
103
+ return rows[0];
104
+ }
105
+ async all(sql, params = []) {
106
+ const { rows } = await this.driver.query(numberPlaceholders(sql), params);
107
+ return rows;
108
+ }
109
+ async run(sql, params = []) {
110
+ const { rowCount } = await this.driver.query(numberPlaceholders(sql), params);
111
+ return { changes: rowCount };
112
+ }
113
+ exec(sql) {
114
+ return this.driver.exec(sql);
115
+ }
116
+ /**
117
+ * Transactions take one advisory lock for the node's schema, so they run one at a time as they do on
118
+ * SQLite. Their check-then-insert writes rely on that.
119
+ */
120
+ transaction(work) {
121
+ if (this.inTransaction) {
122
+ throw new Error('Transactions cannot be nested');
123
+ }
124
+ return this.driver.transaction(async (driver) => {
125
+ await driver.query('SELECT pg_advisory_xact_lock(hashtext(current_schema()))', []);
126
+ return work(new PostgresDatabase(driver, true));
127
+ });
128
+ }
129
+ close() {
130
+ return this.driver.close();
131
+ }
132
+ }
@@ -0,0 +1,16 @@
1
+ import type { NodeDatabase, RunResult } from './database.js';
2
+ /** A SQLite file, or ':memory:'. */
3
+ export declare class SqliteDatabase implements NodeDatabase {
4
+ private readonly db;
5
+ readonly dialect = "sqlite";
6
+ private writes;
7
+ private constructor();
8
+ static open(filename: string): Promise<SqliteDatabase>;
9
+ get<T>(sql: string, params?: unknown[]): Promise<T | undefined>;
10
+ all<T>(sql: string, params?: unknown[]): Promise<T>;
11
+ run(sql: string, params?: unknown[]): Promise<RunResult>;
12
+ exec(sql: string): Promise<void>;
13
+ transaction<T>(work: (tx: NodeDatabase) => Promise<T>): Promise<T>;
14
+ close(): Promise<void>;
15
+ }
16
+ //# sourceMappingURL=sqlite.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sqlite.d.ts","sourceRoot":"","sources":["../../src/db/sqlite.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAE7D,oCAAoC;AACpC,qBAAa,cAAe,YAAW,YAAY;IAK3B,OAAO,CAAC,QAAQ,CAAC,EAAE;IAJvC,QAAQ,CAAC,OAAO,YAAY;IAE5B,OAAO,CAAC,MAAM,CAAuC;IAErD,OAAO,eAA8C;IAErD,OAAa,IAAI,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAK3D;IAED,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAElE;IAED,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAEtD;IAEK,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,OAAO,CAAC,SAAS,CAAC,CAGjE;IAED,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/B;IAED,WAAW,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,EAAE,YAAY,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAejE;IAED,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAErB;CACJ"}
@@ -0,0 +1,53 @@
1
+ import { mkdirSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import sqlite3 from 'sqlite3';
4
+ import { open } from 'sqlite';
5
+ /** A SQLite file, or ':memory:'. */
6
+ export class SqliteDatabase {
7
+ db;
8
+ dialect = 'sqlite';
9
+ // The connection is shared, so transactions are serialized to keep them from nesting.
10
+ writes = Promise.resolve();
11
+ constructor(db) {
12
+ this.db = db;
13
+ }
14
+ static async open(filename) {
15
+ if (filename !== ':memory:') {
16
+ mkdirSync(dirname(filename), { recursive: true });
17
+ }
18
+ return new SqliteDatabase(await open({ filename, driver: sqlite3.Database }));
19
+ }
20
+ get(sql, params = []) {
21
+ return this.db.get(sql, params);
22
+ }
23
+ all(sql, params = []) {
24
+ return this.db.all(sql, params);
25
+ }
26
+ async run(sql, params = []) {
27
+ const result = await this.db.run(sql, params);
28
+ return { changes: result.changes ?? 0 };
29
+ }
30
+ exec(sql) {
31
+ return this.db.exec(sql);
32
+ }
33
+ transaction(work) {
34
+ const run = async () => {
35
+ await this.db.exec('BEGIN IMMEDIATE');
36
+ try {
37
+ const result = await work(this);
38
+ await this.db.exec('COMMIT');
39
+ return result;
40
+ }
41
+ catch (error) {
42
+ await this.db.exec('ROLLBACK');
43
+ throw error;
44
+ }
45
+ };
46
+ const result = this.writes.then(run, run);
47
+ this.writes = result.catch(() => undefined);
48
+ return result;
49
+ }
50
+ close() {
51
+ return this.db.close();
52
+ }
53
+ }
@@ -1,4 +1,4 @@
1
- import type { Database } from "sqlite";
1
+ import type { NodeDatabase } from "../db/database.js";
2
2
  import type { Capability, Grant } from "./grant.js";
3
3
  export interface IssuedGrant {
4
4
  id: string;
@@ -25,7 +25,7 @@ export interface GrantKey {
25
25
  /** Grants this node issued, grants it holds, grants it revoked, nonces it has seen, and caller keys it blocked. */
26
26
  export declare class GrantRepository {
27
27
  private readonly db;
28
- constructor(db: Database);
28
+ constructor(db: NodeDatabase);
29
29
  saveIssued(grant: IssuedGrant): Promise<void>;
30
30
  getIssued(id: string): Promise<IssuedGrant | undefined>;
31
31
  /** Saves a chain this node holds, replacing an earlier copy of the same leaf grant. */